Something went wrong. Try again.
Periodic backups of ATProto repos and blobs safekeeper.dummy.cafe
atproto backup
Something went wrong. Try again.
3.5 kB · 121 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122import { mkdir, readdir, rm, stat, statfs } from 'node:fs/promises';import { createReadStream, createWriteStream } from 'node:fs';import { dirname, join, posix, sep } from 'node:path';import { Readable } from 'node:stream';import { pipeline } from 'node:stream/promises';import type { Storage, StorageEntry, StorageUsage } from './index';
export class LocalStorage implements Storage { constructor(private readonly root: string) {}
private resolve(logicalPath: string): string { // Normalise to OS-native separators. We reject leading "/" and any ".." // segment to prevent writes outside the root. const parts = logicalPath.split('/').filter((p) => p.length > 0); for (const p of parts) { if (p === '..' || p === '.') throw new Error(`Illegal path segment in ${logicalPath}`); } return join(this.root, ...parts); }
private toLogical(absPath: string): string { const rel = absPath.startsWith(this.root) ? absPath.slice(this.root.length) : absPath; return rel.split(sep).filter((p) => p.length > 0).join('/'); }
async put( path: string, data: Uint8Array | ReadableStream<Uint8Array> ): Promise<void> { const abs = this.resolve(path); await mkdir(dirname(abs), { recursive: true }); if (data instanceof Uint8Array) { await Bun.write(abs, data); return; } // Stream: pipe through Node's fs writer to keep memory bounded. const writer = createWriteStream(abs); await pipeline(Readable.fromWeb(data as never), writer); }
async get(path: string): Promise<ReadableStream<Uint8Array> | null> { const abs = this.resolve(path); try { await stat(abs); } catch { return null; } const nodeStream = createReadStream(abs); return Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>; }
async exists(path: string): Promise<boolean> { try { await stat(this.resolve(path)); return true; } catch { return false; } }
async delete(path: string): Promise<void> { await rm(this.resolve(path), { force: true, recursive: true }); }
async stat(path: string): Promise<{ size: number; mtime: Date } | null> { try { const s = await stat(this.resolve(path)); return { size: s.size, mtime: s.mtime }; } catch { return null; } }
async *list(prefix: string): AsyncIterable<StorageEntry> { const absPrefix = this.resolve(prefix); let entries; try { entries = await readdir(absPrefix, { withFileTypes: true }); } catch { return; } for (const e of entries) { const childAbs = join(absPrefix, e.name); if (e.isDirectory()) { yield* this.list(posix.join(prefix, e.name)); } else if (e.isFile()) { const s = await stat(childAbs); yield { path: this.toLogical(childAbs), size: s.size, mtime: s.mtime }; } } }
async sizeOf(prefix: string): Promise<number> { let total = 0; for await (const e of this.list(prefix)) total += e.size; return total; }
async usage(): Promise<StorageUsage> { // Ensure root exists so statfs works on a fresh deployment. await mkdir(this.root, { recursive: true }); const used = await this.sizeOf(''); let freeBytes: number | null = null; let totalBytes: number | null = null; try { const fs = await statfs(this.root); freeBytes = Number(fs.bavail) * Number(fs.bsize); totalBytes = Number(fs.blocks) * Number(fs.bsize); } catch { // statfs may not be supported on all platforms; leave null. } return { usedBytes: used, freeBytes, totalBytes }; }}