atproto user agency toolkit for individuals and groups
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 107 lines 2.7 kB view raw
1import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; 2import { join } from "node:path"; 3import { create as createCid, CODEC_RAW, toString as cidToString } from "@atcute/cid"; 4 5export interface BlobRef { 6 $type: "blob"; 7 ref: { $link: string }; 8 mimeType: string; 9 size: number; 10} 11 12export interface BlobResult { 13 bytes: Uint8Array; 14 mimeType: string; 15 size: number; 16} 17 18/** 19 * BlobStore manages blob storage on the local filesystem. 20 * Blobs are stored with CID-based filenames under DATA_DIR/blobs/{did}/. 21 */ 22export class BlobStore { 23 private blobDir: string; 24 25 constructor(dataDir: string, private did: string) { 26 this.blobDir = join(dataDir, "blobs", did); 27 mkdirSync(this.blobDir, { recursive: true }); 28 } 29 30 /** 31 * Upload a blob and return a BlobRef. 32 */ 33 async putBlob(bytes: Uint8Array, mimeType: string): Promise<BlobRef> { 34 const cidObj = await createCid(CODEC_RAW, bytes); 35 const cidStr = cidToString(cidObj); 36 37 const blobPath = join(this.blobDir, cidStr); 38 const metaPath = join(this.blobDir, `${cidStr}.meta`); 39 40 writeFileSync(blobPath, bytes); 41 writeFileSync( 42 metaPath, 43 JSON.stringify({ mimeType, size: bytes.length }), 44 ); 45 46 return { 47 $type: "blob", 48 ref: { $link: cidStr }, 49 mimeType, 50 size: bytes.length, 51 }; 52 } 53 54 /** 55 * Retrieve a blob by CID string. 56 */ 57 getBlob(cid: string): BlobResult | null { 58 const blobPath = join(this.blobDir, cid); 59 const metaPath = join(this.blobDir, `${cid}.meta`); 60 61 if (!existsSync(blobPath)) return null; 62 63 const bytes = new Uint8Array(readFileSync(blobPath)); 64 let mimeType = "application/octet-stream"; 65 66 if (existsSync(metaPath)) { 67 try { 68 const meta = JSON.parse(readFileSync(metaPath, "utf-8")); 69 mimeType = meta.mimeType || mimeType; 70 } catch { 71 // Ignore corrupt metadata 72 } 73 } 74 75 return { bytes, mimeType, size: bytes.length }; 76 } 77 78 /** 79 * Check if a blob exists. 80 */ 81 hasBlob(cid: string): boolean { 82 return existsSync(join(this.blobDir, cid)); 83 } 84 85 /** 86 * List all blob CIDs (for listBlobs endpoint). 87 */ 88 listBlobs(limit: number = 500, cursor?: string): { cids: string[]; cursor?: string } { 89 const { readdirSync } = require("node:fs") as typeof import("node:fs"); 90 const entries = readdirSync(this.blobDir) 91 .filter((name: string) => !name.endsWith(".meta")) 92 .sort(); 93 94 let startIdx = 0; 95 if (cursor) { 96 const idx = entries.indexOf(cursor); 97 startIdx = idx >= 0 ? idx + 1 : 0; 98 } 99 100 const slice = entries.slice(startIdx, startIdx + limit + 1); 101 const hasMore = slice.length > limit; 102 const cids = hasMore ? slice.slice(0, limit) : slice; 103 const nextCursor = hasMore ? cids[cids.length - 1] : undefined; 104 105 return { cids, cursor: nextCursor }; 106 } 107}