~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
151 lines · 4.3 KB Raw History
// @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;
          }
        },
      };
    },
  };
}