chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @pdsjs/blobs-deno - Deno filesystem blob storage
/// <reference types="@types/deno" />
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;
}
},
};
},
};
}