Add the on-device IndexedDB file registry and hash saves while downloading

This commit is contained in:
Claude
2026-09-30 07:31:18 +00:00
parent 8b7b2a62bd
commit 6af96622dd
8 changed files with 123 additions and 5 deletions

View File

@@ -13,12 +13,20 @@
* Out messages:
* { type: 'unsupported' } → caller falls back to main thread
* { type: 'progress', received } → bytes written so far
* { type: 'done', ext } → file stored as <videoId>.<ext>
* { type: 'done', ext, sha256, expectedSha, size }
* → file stored as <videoId>.<ext>;
* sha256 = hash of the stored bytes
* (P2P content id), expectedSha = the
* server's X-Content-SHA256 or null
* { type: 'error', error } → failed; .part cleaned up
* ========================================================================== */
'use strict';
// Incremental SHA-256 (frontend/sha256.js): the file is hashed while it is
// written, so the device knows its content id without re-reading the file.
try { importScripts('/sha256.js'); } catch { /* hashing unavailable — save still works */ }
async function getVideosDir() {
const root = await navigator.storage.getDirectory();
return root.getDirectoryHandle('videos', { create: true });
@@ -60,6 +68,7 @@ self.onmessage = async (e) => {
dir = await getVideosDir();
const partHandle = await dir.getFileHandle(partName, { create: true });
const access = await partHandle.createSyncAccessHandle();
const hasher = self.Sha256 ? self.Sha256.create() : null;
let offset = 0;
try {
const reader = res.body.getReader();
@@ -67,6 +76,7 @@ self.onmessage = async (e) => {
const { done, value } = await reader.read();
if (done) break;
access.write(value, { at: offset });
if (hasher) hasher.update(value);
offset += value.byteLength;
self.postMessage({ type: 'progress', received: offset });
}
@@ -81,6 +91,14 @@ self.onmessage = async (e) => {
if (expected > 0 && offset !== expected) {
throw new Error(`download cut short (${offset} of ${expected} bytes)`);
}
// The server names the hash of what it sent (media cache copies). A
// mismatch means the bytes were damaged on the way — never keep them.
const sha256 = hasher ? hasher.hex() : null;
const sent = (res.headers.get('x-content-sha256') || '').trim().toLowerCase();
const expectedSha = /^[0-9a-f]{64}$/.test(sent) ? sent : null;
if (sha256 && expectedSha && sha256 !== expectedSha) {
throw new Error('integrity check failed (content hash mismatch)');
}
// Finalize: .part → permanent name. Prefer the native rename, but treat
// ANY move() failure as "unavailable" and fall back to a chunked copy —
@@ -111,7 +129,7 @@ self.onmessage = async (e) => {
await dir.removeEntry(partName);
}
self.postMessage({ type: 'done', ext });
self.postMessage({ type: 'done', ext, sha256, expectedSha, size: offset });
} catch (err) {
// Never leave a corrupt partial behind
try { if (dir && partName) await dir.removeEntry(partName); } catch { /* gone */ }