SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
2.5 KB · 48 lines typescript
Raw Blame History
1/** Read-only access to the raw document archive (MinIO/S3). Keys are `raw/<connector>/<doc>/<hash>.<ext>.zst|gz`. */2import { S3Client, GetObjectCommand, HeadBucketCommand } from "@aws-sdk/client-s3";3import * as zlib from "node:zlib";4import { getEnv } from "./env.js";56let _client: S3Client | null = null;7export function getS3(): S3Client {8  if (_client) return _client;9  const env = getEnv();10  _client = new S3Client({ endpoint: env.s3Endpoint, region: env.s3Region, forcePathStyle: true, credentials: { accessKeyId: env.s3AccessKey, secretAccessKey: env.s3SecretKey }, requestHandler: { requestTimeout: 20_000, connectionTimeout: 3_000 } });11  return _client;12}1314const zstd = zlib as unknown as { zstdDecompressSync?: (b: Buffer) => Buffer };1516/** Decompress by key suffix; when the suffix lies, try zstd then gzip then raw. */17export function decompress(key: string, body: Buffer): Buffer {18  const tryZstd = () => (zstd.zstdDecompressSync ? zstd.zstdDecompressSync(body) : null);19  const tryGz = () => zlib.gunzipSync(body);20  if (key.endsWith(".zst")) { try { const r = tryZstd(); if (r) return r; } catch { /* fall through */ } try { return tryGz(); } catch { return body; } }21  if (key.endsWith(".gz")) { try { return tryGz(); } catch { try { const r = tryZstd(); if (r) return r; } catch { /* */ } return body; } }22  try { const r = tryZstd(); if (r) return r; } catch { /* */ }23  try { return tryGz(); } catch { return body; }24}2526export async function getRawObject(key: string, maxBytes = 5 * 1024 * 1024): Promise<{ body: Buffer; contentType: string | null; truncated: boolean } | null> {27  try {28    const r = await getS3().send(new GetObjectCommand({ Bucket: getEnv().s3Bucket, Key: key }));29    const bytes = r.Body ? Buffer.from(await r.Body.transformToByteArray()) : Buffer.alloc(0);30    const full = decompress(key, bytes);31    const truncated = full.length > maxBytes;32    return { body: truncated ? full.subarray(0, maxBytes) : full, contentType: r.Metadata?.["content-type"] ?? null, truncated };33  } catch (e) {34    const err = e as { name?: string; $metadata?: { httpStatusCode?: number } };35    if (err.name === "NoSuchKey" || err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return null;36    throw e;37  }38}3940export async function storageOk(): Promise<boolean> {41  try {42    await Promise.race([getS3().send(new HeadBucketCommand({ Bucket: getEnv().s3Bucket })), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 2_500))]);43    return true;44  } catch {45    return false;46  }47}48