Files
bare-operating-system/packages/bare-os-coreutils/src/meshdrop.js
T
2026-04-26 08:40:53 -04:00

429 lines
14 KiB
JavaScript

const BARE_MESHDROP_CHUNK_RAW = 12 * 1024
function bareMeshdropName(path) {
const parts = String(path || '').split('/')
return parts[parts.length - 1] || 'file.bin'
}
function bareMeshdropChunkB64Limit() {
const raw = BARE_MESHDROP_CHUNK_RAW
return Math.ceil((raw * 4) / 3) + 16
}
function bareMeshdropChunkSliceB64(b64, index) {
const max = bareMeshdropChunkB64Limit()
const from = index * max
const to = Math.min(b64.length, from + max)
return b64.slice(from, to)
}
async function bareMeshdropDbRead(ctx) {
const p = bareP2pDataFile(ctx, 'meshdrop')
const cur = await bareP2pReadJson(ctx, p)
if (cur && typeof cur === 'object') return cur
return {
schema: 2,
outgoing: {},
incoming: {},
transfers: {}
}
}
async function bareMeshdropDbWrite(ctx, db) {
return bareP2pWriteJson(ctx, bareP2pDataFile(ctx, 'meshdrop'), db)
}
function bareMeshdropEnsureTransfer(db, transferId) {
if (!db.transfers || typeof db.transfers !== 'object') db.transfers = {}
if (!db.transfers[transferId]) {
db.transfers[transferId] = {
transferId,
status: 'new',
chunksTotal: 0,
chunksReceived: 0,
updatedAtMs: Date.now()
}
}
return db.transfers[transferId]
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'meshdrop'
const parsed = bareP2pParseCommonFlags(argv.slice(1))
const args = parsed.rest
const opt = parsed.opt
if (opt.help || args.length === 0) {
ctx.console.log(
bareP2pHelpText(
argv0,
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.',
argv0 +
' offer FILE [--to PEER_HINT] | send-next OFFER_ID [MAX_CHUNKS] | inbox [N] | accept OFFER_ID | fetch OFFER_ID [DEST] | status [TRANSFER_ID] | cancel TRANSFER_ID',
['offer ./file.bin --to peerHint', 'send-next offer-123 8', 'status --summary'],
['swarmtop', 'peerctl']
)
)
if (args.length === 0) ctx.exitCode = 1
return
}
const sub = args[0]
if (sub === 'offer') {
const file = args[1]
if (!file) {
return bareP2pError(ctx, argv0, 'offer requires FILE', argv0 + ' --help', opt)
}
const b = await ctx.vfs.readFile(file)
if (!b) {
return bareP2pError(ctx, argv0, 'unable to read ' + file, 'check file path', opt)
if (opt.dryRun) {
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'offer', file, to }, opt)
return
}
}
const b64 = ctx.b4a.toString(b, 'base64')
const toIdx = args.indexOf('--to')
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
const offerId = bareP2pId('offer')
const transferId = bareP2pId('xfer')
const chunkChars = bareMeshdropChunkB64Limit()
const chunksTotal = Math.max(1, Math.ceil(b64.length / chunkChars))
const firstChunk = bareMeshdropChunkSliceB64(b64, 0)
const db = await bareMeshdropDbRead(ctx)
if (!db.outgoing || typeof db.outgoing !== 'object') db.outgoing = {}
db.outgoing[offerId] = {
offerId,
transferId,
file,
fileName: bareMeshdropName(file),
byteLength: b.byteLength,
base64: b64,
chunkChars,
chunksTotal,
sentChunks: firstChunk ? 1 : 0,
to,
status: 'offered',
updatedAtMs: Date.now()
}
const tr = bareMeshdropEnsureTransfer(db, transferId)
tr.role = 'sender'
tr.offerId = offerId
tr.status = 'offered'
tr.chunksTotal = chunksTotal
tr.chunksReceived = 0
tr.updatedAtMs = Date.now()
await bareMeshdropDbWrite(ctx, db)
if (typeof ctx.bareOsEmitMirrorDriveHint === 'function') {
ctx.bareOsEmitMirrorDriveHint({
app: 'meshdrop',
phase: 'offer',
offerId,
transferId,
fileName: bareMeshdropName(file),
byteLength: b.byteLength,
chunksTotal
})
}
const r = bareP2pSend(ctx, 'meshdrop', 'offer', {
offerId,
transferId,
fromPath: file,
fileName: bareMeshdropName(file),
byteLength: b.byteLength,
chunkChars,
chunksTotal,
firstChunkB64: firstChunk,
to
})
if (r && r.ok === false) {
return bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
}
bareP2pPrint(ctx, 'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)', opt)
bareP2pMaybeNext(ctx, opt, argv0 + ' send-next ' + offerId)
return
}
if (sub === 'send-next') {
const offerId = String(args[1] || '').trim()
if (!offerId) {
return bareP2pError(ctx, argv0, 'send-next requires OFFER_ID', argv0 + ' status', opt)
}
const maxChunks = Math.max(
1,
Math.min(64, parseInt(args[2] || '8', 10) || 8)
)
const db = await bareMeshdropDbRead(ctx)
const rec = db?.outgoing?.[offerId]
if (!rec) {
return bareP2pError(ctx, argv0, 'unknown offer: ' + offerId, argv0 + ' status', opt)
}
let sent = 0
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
const idx = rec.sentChunks
const chunk = bareMeshdropChunkSliceB64(rec.base64, idx)
const r = bareP2pSend(ctx, 'meshdrop', 'chunk', {
offerId: rec.offerId,
transferId: rec.transferId,
index: idx,
chunksTotal: rec.chunksTotal,
chunkB64: chunk
})
if (r && r.ok === false) break
rec.sentChunks++
rec.updatedAtMs = Date.now()
sent++
}
rec.status = rec.sentChunks >= rec.chunksTotal ? 'all-chunks-sent' : 'sending'
const tr = bareMeshdropEnsureTransfer(db, rec.transferId)
tr.status = rec.status
tr.updatedAtMs = rec.updatedAtMs
await bareMeshdropDbWrite(ctx, db)
bareP2pSend(ctx, 'meshdrop', 'progress', {
offerId: rec.offerId,
transferId: rec.transferId,
sentChunks: rec.sentChunks,
chunksTotal: rec.chunksTotal
})
bareP2pPrint(ctx, 'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal, opt)
return
}
if (sub === 'inbox') {
const n = Math.max(1, Math.min(200, parseInt(args[1] || '20', 10) || 20))
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
const offers = rows.filter((r) => r.packet?.kind === 'offer').slice(-n)
for (const row of offers) {
const p = row.packet?.payload || {}
const offerId = typeof p.offerId === 'string' ? p.offerId : '?'
const fileName = typeof p.fileName === 'string' ? p.fileName : '?'
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
bareP2pPrint(ctx,
'[' +
bareP2pFmtClock(row.receivedAtMs) +
'] ' +
offerId +
' ' +
fileName +
' ' +
bytes +
'B chunks=' +
String(chunks) +
' from=' +
from
, opt)
}
return
}
if (sub === 'accept') {
const offerId = String(args[1] || '').trim()
if (!offerId) {
return bareP2pError(ctx, argv0, 'accept requires OFFER_ID', argv0 + ' inbox', opt)
}
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
const offer = rows.findLast(
(r) =>
r.packet?.kind === 'offer' &&
r.packet.payload &&
typeof r.packet.payload === 'object' &&
r.packet.payload.offerId === offerId
)
if (!offer) {
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
}
const p = offer.packet.payload
const transferId =
typeof p.transferId === 'string' ? p.transferId : bareP2pId('xfer')
const fileName =
typeof p.fileName === 'string' ? p.fileName : offerId + '.received'
const chunksTotal = typeof p.chunksTotal === 'number' ? p.chunksTotal : 1
const db = await bareMeshdropDbRead(ctx)
if (!db.incoming || typeof db.incoming !== 'object') db.incoming = {}
db.incoming[offerId] = {
offerId,
transferId,
fileName,
chunksTotal,
chunks: {},
acceptedAtMs: Date.now(),
status: 'accepted'
}
if (typeof p.firstChunkB64 === 'string' && p.firstChunkB64) {
db.incoming[offerId].chunks[0] = p.firstChunkB64
}
const tr = bareMeshdropEnsureTransfer(db, transferId)
tr.role = 'receiver'
tr.offerId = offerId
tr.status = 'accepted'
tr.chunksTotal = chunksTotal
tr.chunksReceived = Object.keys(db.incoming[offerId].chunks).length
tr.updatedAtMs = Date.now()
await bareMeshdropDbWrite(ctx, db)
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
bareP2pPrint(ctx, 'accepted ' + offerId + ' (transfer ' + transferId + ')', opt)
return
}
if (sub === 'fetch') {
const offerId = String(args[1] || '').trim()
if (!offerId) {
return bareP2pError(ctx, argv0, 'fetch requires OFFER_ID', argv0 + ' inbox', opt)
}
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
const offer = rows.findLast(
(r) =>
r.packet?.kind === 'offer' &&
r.packet.payload &&
typeof r.packet.payload === 'object' &&
r.packet.payload.offerId === offerId
)
if (!offer) {
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
}
const p = offer.packet.payload
const fileName =
typeof p.fileName === 'string' ? p.fileName : offerId + '.bin'
const dest = args[2] || fileName
const db = await bareMeshdropDbRead(ctx)
const incoming = db?.incoming?.[offerId]
if (!incoming) {
return bareP2pError(ctx, argv0, 'offer not accepted locally', argv0 + ' accept ' + offerId, opt)
}
const chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
const pieces = []
for (let i = 0; i < total; i++) {
const part = chunks[i]
if (typeof part !== 'string' || !part) {
return bareP2pError(ctx, argv0, 'missing chunk ' + i + '/' + total, argv0 + ' status ' + incoming.transferId, opt)
}
pieces.push(part)
}
const buf = ctx.b4a.from(pieces.join(''), 'base64')
await ctx.vfs.writeFile(dest, buf)
incoming.status = 'saved'
incoming.savedTo = dest
incoming.savedAtMs = Date.now()
const tr = bareMeshdropEnsureTransfer(db, incoming.transferId)
tr.status = 'saved'
tr.chunksReceived = total
tr.updatedAtMs = Date.now()
await bareMeshdropDbWrite(ctx, db)
bareP2pSend(ctx, 'meshdrop', 'complete', {
offerId,
transferId: incoming.transferId,
savedTo: dest
})
bareP2pPrint(ctx, 'saved ' + offerId + ' -> ' + dest, opt)
return
}
if (sub === 'status') {
const transferId = String(args[1] || '').trim()
const db = await bareMeshdropDbRead(ctx)
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
for (const row of rows) {
const kind = row.packet?.kind
const p = row.packet?.payload
if (!p || typeof p !== 'object') continue
const id = typeof p.transferId === 'string' ? p.transferId : ''
if (transferId && id !== transferId) continue
if (kind === 'chunk' && typeof p.offerId === 'string' && typeof p.index === 'number') {
const inRec = db?.incoming?.[p.offerId]
if (inRec && inRec.status !== 'cancelled' && inRec.status !== 'saved') {
if (!inRec.chunks || typeof inRec.chunks !== 'object') inRec.chunks = {}
if (typeof p.chunkB64 === 'string' && !inRec.chunks[p.index]) {
inRec.chunks[p.index] = p.chunkB64
}
const got = Object.keys(inRec.chunks).length
inRec.status = got >= inRec.chunksTotal ? 'received-all' : 'receiving'
const tr = bareMeshdropEnsureTransfer(db, inRec.transferId)
tr.status = inRec.status
tr.chunksTotal = inRec.chunksTotal
tr.chunksReceived = got
tr.updatedAtMs = Date.now()
}
}
if (kind === 'cancel' && id) {
const tr = bareMeshdropEnsureTransfer(db, id)
tr.status = 'cancelled'
tr.updatedAtMs = Date.now()
}
if (kind === 'progress' && id) {
const tr = bareMeshdropEnsureTransfer(db, id)
if (typeof p.sentChunks === 'number') tr.sentChunks = p.sentChunks
if (typeof p.chunksTotal === 'number') tr.chunksTotal = p.chunksTotal
tr.status = 'sending'
tr.updatedAtMs = Date.now()
}
if (kind === 'complete' && id) {
const tr = bareMeshdropEnsureTransfer(db, id)
tr.status = 'complete'
tr.updatedAtMs = Date.now()
}
}
await bareMeshdropDbWrite(ctx, db)
const items = Object.values(db.transfers || {})
.filter((x) => !transferId || x.transferId === transferId)
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
if (!items.length) {
bareP2pPrint(ctx, opt.summary ? { transfers: 0 } : 'no transfers', opt)
return
}
if (opt.summary) {
bareP2pPrint(
ctx,
{
transfers: items.length,
active: items.filter((x) => String(x.status || '').includes('send') || String(x.status || '').includes('receiv')).length
},
opt
)
return
}
for (const it of items) {
bareP2pPrint(
ctx,
(it.transferId || '?') +
' role=' +
String(it.role || '?') +
' status=' +
String(it.status || '?') +
' chunks=' +
String(it.chunksReceived || 0) +
'/' +
String(it.chunksTotal || 0)
,
opt
)
}
return
}
if (sub === 'cancel') {
const transferId = String(args[1] || '').trim()
if (!transferId) {
return bareP2pError(ctx, argv0, 'cancel requires TRANSFER_ID', argv0 + ' status', opt)
}
if (!opt.yes) {
bareP2pPrint(ctx, 'confirmation required: pass --yes to cancel transfer', opt)
ctx.exitCode = 1
return
}
const db = await bareMeshdropDbRead(ctx)
const tr = bareMeshdropEnsureTransfer(db, transferId)
tr.status = 'cancelled'
tr.updatedAtMs = Date.now()
await bareMeshdropDbWrite(ctx, db)
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
bareP2pPrint(ctx, 'cancelled ' + transferId, opt)
return
}
const sug = bareP2pSuggestSubcommand(sub, ['offer', 'send-next', 'inbox', 'accept', 'fetch', 'status', 'cancel'])
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
}