// Storage abstraction // // All backup artifacts (repo CARs, blobs) go through this interface, so // swapping the backend (local FS today, S3/WebDAV tomorrow) is a drop-in. // // Paths are forward-slash logical paths (e.g. "repos/did:plc:abc/2026-04-18.car"). // Backends are responsible for any on-disk encoding they need. import { env } from '../env'; import { LocalStorage } from './local'; export interface StorageUsage { // Bytes currently occupied by our stored artifacts. usedBytes: number; // Bytes free on the backing device, or null for backends where this is // not well-defined (e.g. S3). freeBytes: number | null; // Total bytes on the backing device, or null where not well-defined. totalBytes: number | null; } export interface StorageEntry { path: string; size: number; mtime: Date; } export interface Storage { put( path: string, data: Uint8Array | ReadableStream ): Promise; get(path: string): Promise | null>; exists(path: string): Promise; delete(path: string): Promise; stat(path: string): Promise<{ size: number; mtime: Date } | null>; // Recursive listing. Yields every object under the prefix. list(prefix: string): AsyncIterable; // Aggregate size under a prefix. Implementations may cache / optimise. sizeOf(prefix: string): Promise; // Overall backing-device usage stats. usage(): Promise; } let _storage: Storage | null = null; export function storage(): Storage { if (_storage) return _storage; switch (env.STORAGE_BACKEND) { case 'local': _storage = new LocalStorage(env.STORAGE_LOCAL_ROOT); return _storage; default: { const _exhaustive: never = env.STORAGE_BACKEND; throw new Error(`Unknown storage backend: ${_exhaustive}`); } } }