// @pdsjs/blobs-deno - Deno filesystem blob storage /// import { dirname, join } from 'node:path'; /** * Create filesystem blob storage for Deno * @param {string} baseDir - Base directory for blob storage * @returns {import('@pdsjs/core/ports.js').BlobPort} */ export function createDenoBlobs(baseDir) { /** * Get blob file path * @param {string} did * @param {string} cid */ function blobPath(did, cid) { // Use chars 1-3 of CID for sharding (matches @pdsjs/blobs-fs) const shard = cid.slice(1, 3); return join(baseDir, did, shard, cid); } return { async get(did, cid) { try { const path = blobPath(did, cid); const data = await Deno.readFile(path); const metaPath = `${path}.meta`; const meta = JSON.parse(await Deno.readTextFile(metaPath)); return { data, mimeType: meta.mimeType }; } catch (e) { if (e instanceof Deno.errors.NotFound) { return null; } throw e; } }, async put(did, cid, data, mimeType) { const path = blobPath(did, cid); await Deno.mkdir(dirname(path), { recursive: true }); await Deno.writeFile(path, data); await Deno.writeTextFile(`${path}.meta`, JSON.stringify({ mimeType })); }, async delete(did, cid) { try { const path = blobPath(did, cid); await Deno.remove(path); await Deno.remove(`${path}.meta`); } catch (e) { if (!(e instanceof Deno.errors.NotFound)) { throw e; } } }, /** * @param {string} did * @param {string} cid * @param {{start: number, end: number}} [range] - Inclusive byte positions */ async getStream(did, cid, range) { const path = blobPath(did, cid); /** @type {{mimeType: string}} */ let meta; try { meta = JSON.parse(await Deno.readTextFile(`${path}.meta`)); } catch (e) { if (e instanceof Deno.errors.NotFound) return null; throw e; } const file = await Deno.open(path, { read: true }); if (range) await file.seek(range.start, Deno.SeekMode.Start); let left = range ? range.end - range.start + 1 : Infinity; let closed = false; const close = () => { if (!closed) { closed = true; file.close(); } }; const body = new ReadableStream({ async pull(controller) { if (left === 0) { controller.close(); close(); return; } const buf = new Uint8Array(Math.min(64 * 1024, left)); const read = await file.read(buf); if (read === null) { controller.close(); close(); return; } controller.enqueue(buf.subarray(0, read)); if (left !== Infinity) left -= read; }, cancel() { close(); }, }); return { body, mimeType: meta.mimeType }; }, /** * Bytes stage in a temporary file and move under the CID at commit. The * `.tmp` directory sits beside the shard directories; a shard name is two * CID characters, so the names never collide. * @param {string} did */ async createWriter(did) { const dir = join(baseDir, did, '.tmp'); await Deno.mkdir(dir, { recursive: true }); const tempPath = join(dir, crypto.randomUUID()); const file = await Deno.open(tempPath, { write: true, create: true }); return { /** @param {Uint8Array} chunk */ async write(chunk) { let at = 0; while (at < chunk.length) { at += await file.write(chunk.subarray(at)); } }, /** * @param {string} cid * @param {string} mimeType */ async commit(cid, mimeType) { file.close(); const path = blobPath(did, cid); await Deno.mkdir(dirname(path), { recursive: true }); await Deno.rename(tempPath, path); await Deno.writeTextFile( `${path}.meta`, JSON.stringify({ mimeType }), ); }, async abort() { file.close(); try { await Deno.remove(tempPath); } catch (e) { if (!(e instanceof Deno.errors.NotFound)) throw e; } }, }; }, }; }