Transfer saved songs directly between paired devices with resumable OPFS writes
This commit is contained in:
38
frontend/direct-recv-worker.js
Normal file
38
frontend/direct-recv-worker.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
importScripts('/sha256.js');
|
||||
let dir, handle, access, hash, pos = 0, claim, name;
|
||||
let chain = Promise.resolve();
|
||||
self.onmessage = e => { chain = chain.then(() => run(e.data)); };
|
||||
async function close() { if (access) { access.flush(); access.close(); access = null; } }
|
||||
async function run(m) {
|
||||
try {
|
||||
if (m.op === 'open') {
|
||||
claim = m.file;
|
||||
if (!/^[A-Za-z0-9_-]{1,128}$/.test(claim.id) || !/^[a-f0-9]{64}$/.test(claim.cid)) throw Error('Invalid file identity');
|
||||
dir = await (await navigator.storage.getDirectory()).getDirectoryHandle('videos', { create: true });
|
||||
name = `${claim.id}.${claim.cid}.direct.part`;
|
||||
handle = await dir.getFileHandle(name, { create: true });
|
||||
access = await handle.createSyncAccessHandle();
|
||||
pos = access.getSize(); if (pos > claim.size) { access.truncate(0); pos = 0; }
|
||||
hash = self.Sha256.create();
|
||||
for (let at = 0; at < pos; at += 1024 * 1024) { const b = new Uint8Array(Math.min(1024 * 1024, pos - at)); access.read(b, { at }); hash.update(b); }
|
||||
self.postMessage({ op: 'opened', offset: pos });
|
||||
} else if (m.op === 'chunk') {
|
||||
const b = new Uint8Array(m.buf);
|
||||
if (!access || b.length > 65536 || pos + b.length > claim.size) throw Error('Invalid file chunk');
|
||||
if (access.write(b, { at: pos }) !== b.length) throw Error('Incomplete OPFS write');
|
||||
hash.update(b); pos += b.length; access.flush();
|
||||
self.postMessage({ op: 'progress', received: pos });
|
||||
} else if (m.op === 'abort') { await close(); self.postMessage({ op: 'closed' }); }
|
||||
else if (m.op === 'finish') {
|
||||
await close();
|
||||
if (pos !== claim.size || hash.hex() !== claim.cid) { await dir.removeEntry(name); throw Error('File verification failed; retry starts from zero'); }
|
||||
const final = `${claim.id}.mp4`;
|
||||
// Copy only after verification; existing saved playback remains intact until then.
|
||||
const out = await (await dir.getFileHandle(final, { create: true })).createSyncAccessHandle();
|
||||
try { const f = await handle.getFile(); for (let at = 0; at < f.size; at += 1024 * 1024) out.write(new Uint8Array(await f.slice(at, at + 1024 * 1024).arrayBuffer()), { at }); out.truncate(f.size); out.flush(); } finally { out.close(); }
|
||||
await dir.removeEntry(name);
|
||||
self.postMessage({ op: 'done', file: claim });
|
||||
}
|
||||
} catch (e) { await close(); self.postMessage({ op: 'error', error: e.message }); }
|
||||
}
|
||||
Reference in New Issue
Block a user