SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
11 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
2.2 KB · 70 lines typescript
Raw Blame History
1import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";2import { dirname, join, resolve } from "node:path";3import { zstdCompressSync, zstdDecompressSync } from "node:zlib";4import { sha256 } from "@websensor/core";56/**7 * Content-addressed blob store for snapshots and diffs. Identical content is stored once8 * (`sha256/ab/cd/<hash>.zst`). The interface is storage-agnostic so an S3/MinIO driver can9 * be added without touching callers.10 */11export interface BlobStore {12  put(data: Buffer | string, meta?: { contentType?: string }): Promise<{ key: string; bytes: number; deduplicated: boolean }>;13  get(key: string): Promise<Buffer>;14  getText(key: string): Promise<string>;15  exists(key: string): Promise<boolean>;16}1718export class FsBlobStore implements BlobStore {19  constructor(private readonly root: string) {}2021  private pathFor(key: string): string {22    return join(this.root, key + ".zst");23  }2425  static keyFor(hash: string): string {26    return `sha256/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}`;27  }2829  async put(data: Buffer | string): Promise<{ key: string; bytes: number; deduplicated: boolean }> {30    const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;31    const key = FsBlobStore.keyFor(sha256(buf));32    const p = this.pathFor(key);33    try {34      const s = await stat(p);35      if (s.size > 0) return { key, bytes: buf.length, deduplicated: true };36    } catch {37      // not present38    }39    await mkdir(dirname(p), { recursive: true });40    const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;41    await writeFile(tmp, zstdCompressSync(buf));42    await rename(tmp, p);43    return { key, bytes: buf.length, deduplicated: false };44  }4546  async get(key: string): Promise<Buffer> {47    const raw = await readFile(this.pathFor(key));48    return zstdDecompressSync(raw);49  }5051  async getText(key: string): Promise<string> {52    return (await this.get(key)).toString("utf8");53  }5455  async exists(key: string): Promise<boolean> {56    try {57      await stat(this.pathFor(key));58      return true;59    } catch {60      return false;61    }62  }63}6465let store: BlobStore | null = null;66export function getBlobStore(): BlobStore {67  if (!store) store = new FsBlobStore(resolve(process.env.BLOB_STORE_DIR ?? "./data/blobs"));68  return store;69}70