import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { create as createCid, CODEC_RAW, toString as cidToString } from "@atcute/cid"; export interface BlobRef { $type: "blob"; ref: { $link: string }; mimeType: string; size: number; } export interface BlobResult { bytes: Uint8Array; mimeType: string; size: number; } /** * BlobStore manages blob storage on the local filesystem. * Blobs are stored with CID-based filenames under DATA_DIR/blobs/{did}/. */ export class BlobStore { private blobDir: string; constructor(dataDir: string, private did: string) { this.blobDir = join(dataDir, "blobs", did); mkdirSync(this.blobDir, { recursive: true }); } /** * Upload a blob and return a BlobRef. */ async putBlob(bytes: Uint8Array, mimeType: string): Promise { const cidObj = await createCid(CODEC_RAW, bytes); const cidStr = cidToString(cidObj); const blobPath = join(this.blobDir, cidStr); const metaPath = join(this.blobDir, `${cidStr}.meta`); writeFileSync(blobPath, bytes); writeFileSync( metaPath, JSON.stringify({ mimeType, size: bytes.length }), ); return { $type: "blob", ref: { $link: cidStr }, mimeType, size: bytes.length, }; } /** * Retrieve a blob by CID string. */ getBlob(cid: string): BlobResult | null { const blobPath = join(this.blobDir, cid); const metaPath = join(this.blobDir, `${cid}.meta`); if (!existsSync(blobPath)) return null; const bytes = new Uint8Array(readFileSync(blobPath)); let mimeType = "application/octet-stream"; if (existsSync(metaPath)) { try { const meta = JSON.parse(readFileSync(metaPath, "utf-8")); mimeType = meta.mimeType || mimeType; } catch { // Ignore corrupt metadata } } return { bytes, mimeType, size: bytes.length }; } /** * Check if a blob exists. */ hasBlob(cid: string): boolean { return existsSync(join(this.blobDir, cid)); } /** * List all blob CIDs (for listBlobs endpoint). */ listBlobs(limit: number = 500, cursor?: string): { cids: string[]; cursor?: string } { const { readdirSync } = require("node:fs") as typeof import("node:fs"); const entries = readdirSync(this.blobDir) .filter((name: string) => !name.endsWith(".meta")) .sort(); let startIdx = 0; if (cursor) { const idx = entries.indexOf(cursor); startIdx = idx >= 0 ? idx + 1 : 0; } const slice = entries.slice(startIdx, startIdx + limit + 1); const hasMore = slice.length > limit; const cids = hasMore ? slice.slice(0, limit) : slice; const nextCursor = hasMore ? cids[cids.length - 1] : undefined; return { cids, cursor: nextCursor }; } }