zerobucket 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -29
- package/dist/bin.js +284 -6
- package/dist/index.cjs +282 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +282 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/adapters/shared.ts","../src/adapters/postgres.ts","../src/adapters/sqlite.ts","../src/adapters/mysql.ts","../src/heic.ts","../src/validation.ts","../src/optimize.ts","../src/object-storage.ts","../src/validators/pdf.ts"],"sourcesContent":["/**\r\n * The public ZeroBucket SDK entry point.\r\n *\r\n * const images = new ZeroBucket({ connectionString: process.env.DATABASE_URL });\r\n * const id = await images.put(fileBuffer);\r\n * const image = await images.get(id);\r\n *\r\n * The developer never thinks about BYTEA, checksums or connection pooling.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport { readFile, stat } from \"node:fs/promises\";\r\nimport { basename } from \"node:path\";\r\nimport type { Pool } from \"pg\";\r\nimport type {\r\n CallOptions,\r\n MigrateToDedupOptions,\r\n MigrateToDedupResult,\r\n StorageBackend,\r\n} from \"./adapters/base.js\";\r\nimport type { ObjectStorageLike } from \"./object-storage.js\";\r\nimport { PostgresBackend } from \"./adapters/postgres.js\";\r\nimport { SQLiteBackend } from \"./adapters/sqlite.js\";\r\nimport { MySQLBackend } from \"./adapters/mysql.js\";\r\nimport { DEFAULT_STREAM_CHUNK_SIZE, isValidId } from \"./adapters/shared.js\";\r\nimport {\r\n AccessDeniedError,\r\n ImageNotFoundError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n} from \"./errors.js\";\r\nimport { type OptimizeOptions, optimizeImage } from \"./optimize.js\";\r\nimport type {\r\n BatchDeleteResult,\r\n BatchGetResult,\r\n BatchPutResult,\r\n Image,\r\n ImageMetadata,\r\n OperationEvent,\r\n PreparedRow,\r\n Connection,\r\n MysqlPoolLike,\r\n} from \"./types.js\";\r\nimport {\r\n DEFAULT_MAX_PIXELS,\r\n SUPPORTED_FORMATS,\r\n validateImage,\r\n} from \"./validation.js\";\r\nimport type { ContentValidator } from \"./validators/types.js\";\r\n\r\n/** 8 MiB: a practical ceiling for \"small app\" images. Override with `maxBytes`. */\r\nexport const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;\r\n\r\n/**\r\n * What put() accepts:\r\n * - Buffer / Uint8Array / ArrayBuffer: the bytes themselves\r\n * - string: a filesystem PATH (never raw content)\r\n * - Blob / File (e.g. from `request.formData()`)\r\n * - Node Readable or web ReadableStream (read in bounded chunks)\r\n */\r\nexport type ImageInput =\r\n | Buffer\r\n | Uint8Array\r\n | ArrayBuffer\r\n | string\r\n | Blob\r\n | AsyncIterable<Uint8Array | string>;\r\n\r\nexport type BeforeGetHook<C = unknown> = (\r\n imageId: string,\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\nexport type BeforePutHook<C = unknown> = (\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\n\r\nexport interface ZeroBucketOptions<C = unknown> {\r\n /** PostgreSQL connection string. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool. It is NOT closed by close(). */\r\n pool?: Pool;\r\n /**\r\n * Use SQLite instead of PostgreSQL: a database file path (or \":memory:\"). Needs `npm install better-sqlite3`.\r\n * `dedup`, `objectStorage`, `autoMigrate` and `onOperation` apply to it too. Same file format as the Python\r\n * package's SQLiteBackend, so both can share one `.db` file.\r\n */\r\n sqlite?: string;\r\n /**\r\n * Use MySQL or MariaDB instead of PostgreSQL: `mysql://user:password@host:3306/database`\r\n * (`mariadb://` works too). Needs `npm install mysql2`. Same schema as the Python package's\r\n * MySQLBackend, so both can share one database. Classic mode only for now (`dedup` is not\r\n * available for MySQL yet). `maxRetries`, `retryBaseDelayMs`, `poolMaxSize`, `poolTimeoutMs`,\r\n * `autoMigrate`, `objectStorage` and `onOperation` apply to it too.\r\n */\r\n mysql?: string;\r\n /** Bring your own `mysql2/promise` Pool for MySQL/MariaDB. It is NOT closed by close(). */\r\n mysqlPool?: MysqlPoolLike;\r\n /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite` / `mysql`. */\r\n backend?: StorageBackend;\r\n\r\n /** Max accepted size in bytes. Default 8 MiB. */\r\n maxBytes?: number;\r\n /** Decoded-pixel ceiling that rejects decompression bombs. Default ~89 MP. */\r\n maxPixels?: number;\r\n /** Accepted image formats. Default [\"jpeg\", \"png\", \"webp\"]. */\r\n allowedFormats?: readonly string[];\r\n\r\n autoMigrate?: boolean;\r\n maxRetries?: number;\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n poolTimeoutMs?: number;\r\n /** Fire-and-forget observability. Exceptions thrown inside it are swallowed. */\r\n onOperation?: (event: OperationEvent) => void;\r\n\r\n /**\r\n * S3-compatible object storage, enabling `tierToObjectStorage()` and transparent\r\n * reads of images the Python or Node package tiered. Configure it with the same\r\n * bucket and credentials used to tier. Without it, reading a tiered image throws\r\n * a clear StorageError.\r\n */\r\n objectStorage?: ObjectStorageLike;\r\n\r\n /**\r\n * Content-addressed mode: identical bytes are stored once and shared by reference, each image\r\n * keeping its own id and filename. Cannot be combined with `objectStorage`. Uses separate tables\r\n * (zerobucket_blobs / zerobucket_image_refs); see `migrateClassicToDedup()` for existing data.\r\n */\r\n dedup?: boolean;\r\n\r\n /**\r\n * Authorization hook for get / getMany (per id) / getStream / streamTo / metadata.\r\n * Return false to deny. A hook that THROWS fails closed: the exception\r\n * propagates (or is captured per item in getMany) and is never treated as allow.\r\n * NOT called for exists(). Only ever called with a well-formed UUID: a malformed id is\r\n * \"not found\" (ImageNotFoundError, or `error: \"not found\"` per item in getMany) and never\r\n * reaches the hook.\r\n */\r\n beforeGet?: BeforeGetHook<C>;\r\n /** Authorization hook for put / putMany (evaluated once per call). Same fail-closed rule. */\r\n beforePut?: BeforePutHook<C>;\r\n}\r\n\r\ninterface PutBase<C> {\r\n /** Display filename stored in metadata (never used for validation). */\r\n filename?: string;\r\n /** `true` = strip metadata only; or pass { maxWidth, format, quality } to resize/re-encode. */\r\n optimize?: boolean | OptimizeOptions;\r\n /** Store non-image content through the same machinery. Incompatible with `optimize`. */\r\n validator?: ContentValidator;\r\n /** Join your own transaction. ZeroBucket then does not commit, roll back or retry. */\r\n connection?: Connection;\r\n /** Passed to beforePut. */\r\n context?: C;\r\n}\r\nexport type PutOptions<C = unknown> = PutBase<C>;\r\nexport interface PutManyOptions<C = unknown> extends Omit<\r\n PutBase<C>,\r\n \"filename\"\r\n> {\r\n /** Same length as `images`; use null/undefined entries to fall back to each input's own name. */\r\n filenames?: (string | null | undefined)[];\r\n}\r\nexport interface GetOptions<C = unknown> {\r\n connection?: Connection;\r\n context?: C;\r\n}\r\nexport interface StreamOptions<C = unknown> extends GetOptions<C> {\r\n /** Bytes per chunk. Default 1 MiB. */\r\n chunkSize?: number;\r\n /** Inclusive byte range, e.g. { start: 0, end: 1023 }. `end` is clamped to the object size. */\r\n range?: { start: number; end?: number };\r\n}\r\n\r\n/** Anything with write(); e.g. fs.WriteStream, http.ServerResponse, or a custom sink. Backpressure is honoured. */\r\nexport interface WritableLike {\r\n write(chunk: Buffer): boolean | void | Promise<void>;\r\n once?(event: \"drain\", listener: () => void): unknown;\r\n}\r\n\r\n/** Bounded concurrency for per-item validation work. */\r\nconst PREPARE_CONCURRENCY = 4;\r\n\r\nexport class ZeroBucket<C = unknown> {\r\n private readonly backend: StorageBackend;\r\n private readonly maxBytes: number;\r\n private readonly maxPixels: number;\r\n private readonly allowedFormats: readonly string[];\r\n private readonly beforeGet: BeforeGetHook<C> | undefined;\r\n private readonly beforePut: BeforePutHook<C> | undefined;\r\n\r\n constructor(options: ZeroBucketOptions<C> = {}) {\r\n const chosen = [\r\n options.sqlite !== undefined && \"`sqlite`\",\r\n (options.mysql !== undefined || options.mysqlPool) &&\r\n \"`mysql`/`mysqlPool`\",\r\n (options.connectionString || options.pool) && \"`connectionString`/`pool`\",\r\n ].filter(Boolean);\r\n if (chosen.length > 1) {\r\n throw new TypeError(\r\n `Choose one database: ${chosen.join(\" OR \")}, not several.`,\r\n );\r\n }\r\n this.backend =\r\n options.backend ??\r\n (options.sqlite !== undefined\r\n ? new SQLiteBackend({\r\n path: options.sqlite,\r\n autoMigrate: options.autoMigrate,\r\n dedup: options.dedup,\r\n objectStorage: options.objectStorage,\r\n onOperation: options.onOperation,\r\n })\r\n : options.mysql !== undefined || options.mysqlPool\r\n ? new MySQLBackend({\r\n connectionString: options.mysql,\r\n pool: options.mysqlPool,\r\n autoMigrate: options.autoMigrate,\r\n maxRetries: options.maxRetries,\r\n retryBaseDelayMs: options.retryBaseDelayMs,\r\n poolMinSize: options.poolMinSize,\r\n poolMaxSize: options.poolMaxSize,\r\n poolTimeoutMs: options.poolTimeoutMs,\r\n onOperation: options.onOperation,\r\n objectStorage: options.objectStorage,\r\n dedup: options.dedup,\r\n })\r\n : new PostgresBackend({\r\n connectionString: options.connectionString,\r\n pool: options.pool,\r\n autoMigrate: options.autoMigrate,\r\n maxRetries: options.maxRetries,\r\n retryBaseDelayMs: options.retryBaseDelayMs,\r\n poolMinSize: options.poolMinSize,\r\n poolMaxSize: options.poolMaxSize,\r\n poolTimeoutMs: options.poolTimeoutMs,\r\n onOperation: options.onOperation,\r\n objectStorage: options.objectStorage,\r\n dedup: options.dedup,\r\n }));\r\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\r\n this.maxPixels = options.maxPixels ?? DEFAULT_MAX_PIXELS;\r\n this.allowedFormats = options.allowedFormats ?? SUPPORTED_FORMATS;\r\n this.beforeGet = options.beforeGet;\r\n this.beforePut = options.beforePut;\r\n }\r\n\r\n /** Construct AND connect/migrate immediately, so a bad DATABASE_URL fails at startup, not on first request. */\r\n static async connect<C = unknown>(\r\n options: ZeroBucketOptions<C> = {},\r\n ): Promise<ZeroBucket<C>> {\r\n const zb = new ZeroBucket<C>(options);\r\n await zb.ready();\r\n return zb;\r\n }\r\n\r\n /** Resolves once connected and migrated. Called automatically on first use. */\r\n ready(): Promise<void> {\r\n return this.backend.ready();\r\n }\r\n\r\n // ---- hooks -----------------------------------------------------------\r\n\r\n private async checkBeforeGet(\r\n imageId: string,\r\n context: C | undefined,\r\n ): Promise<void> {\r\n // A malformed id can never exist, so it is \"not found\" and your hook is never\r\n // called with it. The hook therefore only ever sees well-formed UUIDs, and can\r\n // safely use the id in SQL (`WHERE id = $1` on a uuid column) without first\r\n // validating a string that may have come straight from a URL.\r\n if (!isValidId(imageId)) throw new ImageNotFoundError(imageId);\r\n if (!this.beforeGet) return;\r\n if (!(await this.beforeGet(imageId, context)))\r\n throw new AccessDeniedError(\"get\", imageId);\r\n }\r\n\r\n private async checkBeforePut(context: C | undefined): Promise<void> {\r\n if (!this.beforePut) return;\r\n if (!(await this.beforePut(context))) throw new AccessDeniedError(\"put\");\r\n }\r\n\r\n // ---- write path ------------------------------------------------------\r\n\r\n private async prepareRow(\r\n image: ImageInput,\r\n o: PutBase<C> & { filename?: string | null },\r\n ): Promise<PreparedRow> {\r\n const { data, filename } = await readInput(\r\n image,\r\n o.filename ?? undefined,\r\n this.maxBytes,\r\n );\r\n\r\n let finalData: Buffer;\r\n let mimeType: string;\r\n let width: number | null;\r\n let height: number | null;\r\n let sizeBytes: number;\r\n\r\n if (o.validator) {\r\n if (o.optimize) {\r\n throw new ImageValidationError(\r\n \"optimize is not supported together with a custom validator: the optimize pipeline \" +\r\n \"(resize/re-encode) is image-specific. Validate/transform custom content types yourself before put().\",\r\n );\r\n }\r\n const v = await o.validator.validate(data, { maxBytes: this.maxBytes });\r\n finalData = data;\r\n mimeType = v.mimeType;\r\n width = v.width ?? null;\r\n height = v.height ?? null;\r\n sizeBytes = v.sizeBytes;\r\n } else {\r\n const validated = await validateImage(data, {\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n allowedFormats: this.allowedFormats,\r\n });\r\n if (o.optimize) {\r\n const opts = o.optimize === true ? {} : o.optimize;\r\n const r = await optimizeImage(data, {\r\n ...opts,\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n });\r\n finalData = r.data;\r\n mimeType = r.mimeType;\r\n width = r.width;\r\n height = r.height;\r\n sizeBytes = r.sizeBytes;\r\n } else {\r\n finalData = data;\r\n mimeType = validated.mimeType;\r\n width = validated.width;\r\n height = validated.height;\r\n sizeBytes = validated.sizeBytes;\r\n }\r\n }\r\n\r\n return {\r\n data: finalData,\r\n mimeType,\r\n originalFilename: filename ?? null,\r\n sizeBytes,\r\n width,\r\n height,\r\n checksumSha256: createHash(\"sha256\").update(finalData).digest(\"hex\"),\r\n };\r\n }\r\n\r\n /** Validate, optionally optimize, checksum and store one image. Returns its id (a UUID). */\r\n async put(image: ImageInput, options: PutOptions<C> = {}): Promise<string> {\r\n await this.checkBeforePut(options.context);\r\n const row = await this.prepareRow(image, options);\r\n return this.backend.put(row, { connection: options.connection });\r\n }\r\n\r\n /**\r\n * Store many images. Best-effort, not all-or-nothing: one bad image does not\r\n * abort the rest. Check each result's `success` / `error`. Settings apply to\r\n * every item. Valid items are inserted in ONE transaction.\r\n */\r\n async putMany(\r\n images: ImageInput[],\r\n options: PutManyOptions<C> = {},\r\n ): Promise<BatchPutResult[]> {\r\n const { filenames } = options;\r\n if (filenames && filenames.length !== images.length) {\r\n throw new RangeError(\r\n \"filenames must be the same length as images if provided\",\r\n );\r\n }\r\n\r\n const fail = (index: number, error: string): BatchPutResult => ({\r\n index,\r\n imageId: null,\r\n error,\r\n success: false,\r\n });\r\n\r\n if (this.beforePut) {\r\n try {\r\n if (!(await this.beforePut(options.context)))\r\n return images.map((_, i) => fail(i, \"access denied\"));\r\n } catch (exc) {\r\n return images.map((_, i) => fail(i, errText(exc))); // fail closed, per item\r\n }\r\n }\r\n\r\n const results: (BatchPutResult | undefined)[] = new Array(images.length);\r\n const prepared: { index: number; row: PreparedRow }[] = [];\r\n\r\n await mapLimit(images, PREPARE_CONCURRENCY, async (image, i) => {\r\n try {\r\n const row = await this.prepareRow(image, {\r\n ...options,\r\n filename: filenames?.[i] ?? undefined,\r\n });\r\n prepared.push({ index: i, row });\r\n } catch (exc) {\r\n results[i] = fail(i, errText(exc));\r\n }\r\n });\r\n prepared.sort((a, b) => a.index - b.index);\r\n\r\n if (prepared.length > 0) {\r\n try {\r\n const ids = await this.backend.putMany(\r\n prepared.map((p) => p.row),\r\n { connection: options.connection },\r\n );\r\n prepared.forEach((p, n) => {\r\n results[p.index] = {\r\n index: p.index,\r\n imageId: ids[n]!,\r\n error: null,\r\n success: true,\r\n };\r\n });\r\n } catch (exc) {\r\n // The whole DB batch failed; every validated item shared that transaction.\r\n for (const p of prepared)\r\n results[p.index] = fail(p.index, errText(exc));\r\n }\r\n }\r\n return results as BatchPutResult[];\r\n }\r\n\r\n // ---- read path -------------------------------------------------------\r\n\r\n /** Retrieve a full image including bytes. Throws ImageNotFoundError if missing. */\r\n async get(imageId: string, options: GetOptions<C> = {}): Promise<Image> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const record = await this.backend.get(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!record) throw new ImageNotFoundError(imageId);\r\n return {\r\n data: record.data,\r\n mimeType: record.mimeType,\r\n filename: record.originalFilename,\r\n sizeBytes: record.sizeBytes,\r\n width: record.width,\r\n height: record.height,\r\n checksumSha256: record.checksumSha256,\r\n };\r\n }\r\n\r\n /**\r\n * Retrieve many images in one query. Results come back in input order; a\r\n * missing id is `error: \"not found\"`, not an exception. A hook denial or hook\r\n * exception affects only that id.\r\n */\r\n async getMany(\r\n imageIds: string[],\r\n options: GetOptions<C> = {},\r\n ): Promise<BatchGetResult[]> {\r\n const results: (BatchGetResult | undefined)[] = new Array(imageIds.length);\r\n const allowedIdx: number[] = [];\r\n\r\n for (let i = 0; i < imageIds.length; i++) {\r\n const imageId = imageIds[i]!;\r\n if (!isValidId(imageId)) {\r\n // Same as a missing id, and the hook never sees it (see checkBeforeGet).\r\n results[i] = { imageId, image: null, error: \"not found\", success: false };\r\n continue;\r\n }\r\n if (this.beforeGet) {\r\n try {\r\n if (!(await this.beforeGet(imageId, options.context))) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: \"access denied\",\r\n success: false,\r\n };\r\n continue;\r\n }\r\n } catch (exc) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: errText(exc),\r\n success: false,\r\n };\r\n continue;\r\n }\r\n }\r\n allowedIdx.push(i);\r\n }\r\n\r\n if (allowedIdx.length > 0) {\r\n const records = await this.backend.getMany(\r\n allowedIdx.map((i) => imageIds[i]!),\r\n { connection: options.connection },\r\n );\r\n const byId = new Map(records.map((r) => [r.id.toLowerCase(), r]));\r\n for (const i of allowedIdx) {\r\n const imageId = imageIds[i]!;\r\n const r = byId.get(imageId.toLowerCase());\r\n results[i] = r\r\n ? {\r\n imageId,\r\n image: {\r\n data: r.data,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n },\r\n error: null,\r\n success: true,\r\n }\r\n : { imageId, image: null, error: \"not found\", success: false };\r\n }\r\n }\r\n return results as BatchGetResult[];\r\n }\r\n\r\n /**\r\n * Stream an image's bytes in chunks without holding the whole image in memory.\r\n * Not-found is checked eagerly: the returned promise rejects before any chunk\r\n * is read. A concurrent delete mid-stream makes iteration throw StorageError\r\n * rather than silently truncating. Pass `range` for partial reads.\r\n *\r\n * Honest limitation: reduces Node-side memory, not Postgres-side (the server\r\n * still handles the full BYTEA value per chunk request).\r\n */\r\n async getStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<AsyncIterable<Buffer>> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const { range } = options;\r\n if (range) {\r\n if (\r\n !Number.isInteger(range.start) ||\r\n range.start < 0 ||\r\n (range.end !== undefined &&\r\n (!Number.isInteger(range.end) || range.end < range.start))\r\n ) {\r\n throw new RangeError(\r\n \"range must be { start >= 0, end >= start } with integer values\",\r\n );\r\n }\r\n }\r\n const stream = await this.backend.getStream(imageId, {\r\n chunkSize: options.chunkSize ?? DEFAULT_STREAM_CHUNK_SIZE,\r\n connection: options.connection,\r\n range: range\r\n ? { start: range.start, end: range.end ?? Number.MAX_SAFE_INTEGER }\r\n : undefined,\r\n });\r\n if (!stream) throw new ImageNotFoundError(imageId);\r\n return stream;\r\n }\r\n\r\n /** Same as getStream(), as a Web `ReadableStream` you can hand straight to `new Response(...)`. */\r\n async toWebStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<ReadableStream<Uint8Array>> {\r\n const iterable = await this.getStream(imageId, options);\r\n const iterator = iterable[Symbol.asyncIterator]();\r\n return new ReadableStream<Uint8Array>({\r\n async pull(controller) {\r\n try {\r\n const { value, done } = await iterator.next();\r\n if (done) controller.close();\r\n else\r\n controller.enqueue(\r\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength),\r\n );\r\n } catch (exc) {\r\n controller.error(exc);\r\n }\r\n },\r\n async cancel() {\r\n await iterator.return?.();\r\n },\r\n });\r\n }\r\n\r\n /** Write an image to `destination` chunk by chunk (honouring backpressure). Returns bytes written. Does not end the destination. */\r\n async streamTo(\r\n imageId: string,\r\n destination: WritableLike,\r\n options: StreamOptions<C> = {},\r\n ): Promise<number> {\r\n let total = 0;\r\n for await (const chunk of await this.getStream(imageId, options)) {\r\n const result = destination.write(chunk);\r\n if (result === false && typeof destination.once === \"function\") {\r\n await new Promise<void>((resolve) =>\r\n destination.once!(\"drain\", resolve),\r\n );\r\n } else if (\r\n result &&\r\n typeof (result as Promise<void>).then === \"function\"\r\n ) {\r\n await result;\r\n }\r\n total += chunk.length;\r\n }\r\n return total;\r\n }\r\n\r\n /** Metadata without pulling the bytes. Gated by the same beforeGet hook as get(). */\r\n async metadata(\r\n imageId: string,\r\n options: GetOptions<C> = {},\r\n ): Promise<ImageMetadata> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const r = await this.backend.getMetadata(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!r) throw new ImageNotFoundError(imageId);\r\n return {\r\n imageId: r.id,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n };\r\n }\r\n\r\n /** Whether an image exists. Deliberately NOT gated by beforeGet (returns no data or metadata). */\r\n exists(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.exists(imageId, options);\r\n }\r\n\r\n /** Delete an image. Returns true if it existed. */\r\n delete(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.delete(imageId, options);\r\n }\r\n\r\n /** Delete many in one query. Results are in input order; a missing id is `deleted: false`, not an error. */\r\n async deleteMany(\r\n imageIds: string[],\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<BatchDeleteResult[]> {\r\n if (imageIds.length === 0) return [];\r\n const deleted = new Set(\r\n (await this.backend.deleteMany(imageIds, options)).map((id) =>\r\n id.toLowerCase(),\r\n ),\r\n );\r\n return imageIds.map((imageId) => ({\r\n imageId,\r\n deleted: deleted.has(imageId.toLowerCase()),\r\n error: null,\r\n success: true,\r\n }));\r\n }\r\n\r\n /**\r\n * Move an image's bytes from Postgres into object storage; reads, streams and\r\n * deletes keep working transparently. Returns true if tiered now, false if it was\r\n * already tiered (safe to re-run in a backfill). Throws ImageNotFoundError if missing.\r\n *\r\n * The upload runs inside the database transaction: if it fails, the row is left\r\n * completely untouched. Explicit and opt-in: put() never tiers automatically.\r\n * Not gated by beforeGet/beforePut (it is a maintenance operation, as in the Python package).\r\n */\r\n async tierToObjectStorage(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n const result = await this.backend.tierToObjectStorage(imageId, options);\r\n if (result === null) throw new ImageNotFoundError(imageId);\r\n return result;\r\n }\r\n\r\n /**\r\n * One-time, non-destructive copy of classic-mode images into a dedup instance's tables, keeping every\r\n * id. Call it on an instance created with `dedup: true`. Safe to re-run. See PostgresBackend for details.\r\n */\r\n async migrateClassicToDedup(\r\n opts?: MigrateToDedupOptions,\r\n ): Promise<MigrateToDedupResult> {\r\n if (!this.backend.migrateClassicToDedup) {\r\n throw new StorageError(\r\n \"This backend does not support migrateClassicToDedup().\",\r\n );\r\n }\r\n return this.backend.migrateClassicToDedup(opts);\r\n }\r\n\r\n /** Release the pool (if ZeroBucket created it). */\r\n close(): Promise<void> {\r\n return this.backend.close();\r\n }\r\n}\r\n\r\n// ---- input handling ----------------------------------------------------\r\n\r\n/**\r\n * Read any supported input into a Buffer. Streams are consumed in bounded\r\n * chunks and rejected as soon as they pass maxBytes (reported size is then a\r\n * lower bound), rather than buffering an arbitrarily large upload first.\r\n * Files and Blobs are size-checked BEFORE their bytes are read.\r\n */\r\nexport async function readInput(\r\n image: ImageInput,\r\n filename: string | undefined,\r\n maxBytes: number,\r\n): Promise<{ data: Buffer; filename: string | undefined }> {\r\n if (typeof image === \"string\") {\r\n const size = (await stat(image)).size;\r\n if (size > maxBytes) throw new ImageTooLargeError(size, maxBytes);\r\n return {\r\n data: await readFile(image),\r\n filename: filename ?? basename(image),\r\n };\r\n }\r\n if (Buffer.isBuffer(image)) return { data: image, filename };\r\n if (image instanceof Uint8Array)\r\n return {\r\n data: Buffer.from(image.buffer, image.byteOffset, image.byteLength),\r\n filename,\r\n };\r\n if (image instanceof ArrayBuffer)\r\n return { data: Buffer.from(image), filename };\r\n if (typeof Blob !== \"undefined\" && image instanceof Blob) {\r\n if (image.size > maxBytes)\r\n throw new ImageTooLargeError(image.size, maxBytes);\r\n const name = (image as { name?: unknown }).name;\r\n return {\r\n data: Buffer.from(await image.arrayBuffer()),\r\n filename: filename ?? (typeof name === \"string\" ? name : undefined),\r\n };\r\n }\r\n if (\r\n image &&\r\n typeof (image as AsyncIterable<unknown>)[Symbol.asyncIterator] ===\r\n \"function\"\r\n ) {\r\n const chunks: Buffer[] = [];\r\n let total = 0;\r\n for await (const chunk of image as AsyncIterable<Uint8Array | string>) {\r\n const buf =\r\n typeof chunk === \"string\"\r\n ? Buffer.from(chunk)\r\n : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\r\n total += buf.length;\r\n if (total > maxBytes) {\r\n (image as { destroy?: () => void }).destroy?.();\r\n throw new ImageTooLargeError(total, maxBytes);\r\n }\r\n chunks.push(buf);\r\n }\r\n const p = (image as { path?: unknown }).path;\r\n return {\r\n data: Buffer.concat(chunks, total),\r\n filename: filename ?? (typeof p === \"string\" ? basename(p) : undefined),\r\n };\r\n }\r\n throw new TypeError(\r\n \"Unsupported image input. Pass a Buffer, Uint8Array, file path string, Blob/File, or a readable stream.\",\r\n );\r\n}\r\n\r\nasync function mapLimit<T>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T, index: number) => Promise<void>,\r\n): Promise<void> {\r\n let next = 0;\r\n const workers = Array.from(\r\n { length: Math.min(limit, items.length) },\r\n async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n await fn(items[i]!, i);\r\n }\r\n },\r\n );\r\n await Promise.all(workers);\r\n}\r\n\r\nfunction errText(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}","/** Helpers shared by every storage adapter (kept here so Postgres and SQLite cannot drift apart). */\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type { StoredRecord } from \"./base.js\";\r\n\r\n/** 1 MiB. Default chunk size for getStream(). */\r\nexport const DEFAULT_STREAM_CHUNK_SIZE = 1024 * 1024;\r\n\r\nconst UUID_RE =\r\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\r\n\r\n/** True if `id` is a syntactically valid UUID. Malformed ids can never exist, so they short-circuit to \"not found\". */\r\nexport function isValidId(id: string): boolean {\r\n return typeof id === \"string\" && UUID_RE.test(id);\r\n}\r\n\r\nexport function msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport function wrap(prefix: string, exc: unknown): StorageError {\r\n return exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`${prefix}: ${msg(exc)}`, { cause: exc });\r\n}\r\n\r\nexport function sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\nexport async function mapLimit<T, R>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T) => Promise<R>,\r\n): Promise<R[]> {\r\n const out: R[] = new Array(items.length);\r\n let next = 0;\r\n await Promise.all(\r\n Array.from({ length: Math.min(limit, items.length) }, async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n out[i] = await fn(items[i]!);\r\n }\r\n }),\r\n );\r\n return out;\r\n}\r\n\r\nexport function tieredWithoutStorage(\r\n id: string,\r\n key: string | null,\r\n): StorageError {\r\n return new StorageError(\r\n `Image ${JSON.stringify(id)} is stored in object storage (key=${JSON.stringify(key)}) ` +\r\n \"but this ZeroBucket was constructed without `objectStorage`. Configure it with the \" +\r\n \"same bucket and credentials that were used to tier this image.\",\r\n );\r\n}\r\n\r\n/** Turn a DB row into a record, fetching the bytes from object storage if the row is tiered. */\r\nexport async function hydrateRow(\r\n row: any,\r\n id: string,\r\n os: ObjectStorageLike | undefined,\r\n): Promise<StoredRecord> {\r\n let data: Buffer;\r\n if (row.storage_backend === \"object_storage\") {\r\n if (!os) throw tieredWithoutStorage(id, row.object_storage_key);\r\n data = await os.download(row.object_storage_key);\r\n if (data.length !== row.size_bytes) {\r\n throw new StorageError(\r\n `Object ${JSON.stringify(row.object_storage_key)} for image ${JSON.stringify(id)} has ${data.length} bytes ` +\r\n `but the database records ${row.size_bytes}. The stored object is corrupted or was replaced.`,\r\n );\r\n }\r\n } else {\r\n data = row.data as Buffer;\r\n }\r\n return {\r\n id: String(row.id),\r\n data,\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n}\r\n\r\n/** Ranged reads straight from the object store; fails loudly rather than ever truncating. */\r\nexport async function* streamFromObjectStorage(\r\n os: ObjectStorageLike,\r\n id: string,\r\n key: string,\r\n start: number,\r\n end: number,\r\n chunkSize: number,\r\n): AsyncGenerator<Buffer> {\r\n let offset = start;\r\n while (offset <= end) {\r\n const last = Math.min(offset + chunkSize - 1, end);\r\n const expected = last - offset + 1;\r\n const chunk = await os.downloadRange(key, offset, last);\r\n if (chunk.length !== expected) {\r\n throw new StorageError(\r\n `Object ${JSON.stringify(key)} for image ${JSON.stringify(id)} returned ${chunk.length} bytes for a ${expected}-byte range; ` +\r\n \"the stored object is shorter than the database records.\",\r\n );\r\n }\r\n yield chunk;\r\n offset = last + 1;\r\n }\r\n}\r\n\r\n/** 1024-based, like Postgres' pg_size_pretty: \"812 bytes\", \"13.2 kB\", \"4.0 MB\". */\r\nexport function prettySize(bytes: number): string {\r\n if (bytes < 1024) return `${bytes} bytes`;\r\n const units = [\"kB\", \"MB\", \"GB\", \"TB\"];\r\n let v = bytes / 1024;\r\n let i = 0;\r\n while (v >= 1024 && i < units.length - 1) {\r\n v /= 1024;\r\n i += 1;\r\n }\r\n return `${v.toFixed(1)} ${units[i]}`;\r\n}\r\n","/**\r\n * PostgreSQL storage adapter (classic mode).\r\n *\r\n * Uses the EXACT schema the Python package creates (zerobucket_images, including\r\n * the object-storage tiering columns and CHECK constraint), so a Node app and a\r\n * Python app can share one database and read each other's rows.\r\n *\r\n * Lessons from the Python build, applied here from day one:\r\n * - Retry re-runs the WHOLE unit of work on a fresh connection after a full\r\n * ROLLBACK. It never replays a single statement of a multi-statement\r\n * transaction (the \"half-applied put\" hazard).\r\n * - Retry never applies when the caller passes `connection` (the caller owns\r\n * the transaction; restarting it is their decision).\r\n * - Pool acquire has an explicit timeout.\r\n * - Migration runs under an advisory lock so N concurrent first-callers do not\r\n * race on CREATE TABLE.\r\n */\r\nimport type { Pool, PoolClient, PoolConfig } from \"pg\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n OperationEvent,\r\n PreparedRow,\r\n Queryable,\r\n} from \"../types.js\";\r\nimport {\r\n DEFAULT_STREAM_CHUNK_SIZE,\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\nimport type {\r\n CallOptions,\r\n ImageListEntry,\r\n ImageListOptions,\r\n MigrateToDedupOptions,\r\n MigrateToDedupResult,\r\n StorageBackend,\r\n StorageInfo,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n\r\n-- Object-storage tiering columns (additive; identical to the Python package).\r\nALTER TABLE zerobucket_images ALTER COLUMN data DROP NOT NULL;\r\nALTER TABLE zerobucket_images\r\n ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'postgres';\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_bucket TEXT;\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_key TEXT;\r\nDO $$\r\nBEGIN\r\n IF NOT EXISTS (\r\n SELECT 1 FROM pg_constraint WHERE conname = 'zerobucket_storage_location_check'\r\n ) THEN\r\n ALTER TABLE zerobucket_images ADD CONSTRAINT zerobucket_storage_location_check\r\n CHECK (\r\n (storage_backend = 'postgres'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n );\r\n END IF;\r\nEND $$;\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_storage_backend\r\n ON zerobucket_images (storage_backend) WHERE storage_backend != 'postgres';\r\n`;\r\n\r\nconst INSERT = `\r\nINSERT INTO zerobucket_images\r\n (data, mime_type, original_filename, size_bytes, width, height, checksum_sha256)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nRETURNING id;`;\r\n\r\n// How image bytes are read (measured: benchmarks/node, 0.10.1 notes in the CHANGELOG).\r\n//\r\n// A bytea column is sent as HEX TEXT by default: two characters per byte, so an 11.5 MB image crosses the wire as ~24 MB,\r\n// becomes one 24 MB string, and is decoded back (about 110 MB of transient memory). Reading it as base64 instead sends\r\n// ~16 MB and decodes it with Buffer.from(..., \"base64\"): about a third faster and about 40% less memory, byte-for-byte\r\n// identical. node-postgres's \"binary: true\" is NOT an option: its protocol parser turns every field into a UTF-8 string\r\n// first, so bytea comes back corrupted (and slower). The server-side encode costs the same as hex output.\r\n//\r\n// Postgres wraps base64 output every 76 characters; Buffer.from(..., \"base64\") ignores the line breaks.\r\n// The column is aliased data_b64 (full rows) / chunk (ranged reads) and decoded in hydrate() / getStream().\r\nconst SELECT_FULL = `\r\nSELECT id, encode(data, 'base64') AS data_b64, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\nconst SELECT_MANY = `\r\nSELECT id, encode(data, 'base64') AS data_b64, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = ANY($1::uuid[]);`;\r\n\r\nconst SELECT_METADATA = `\r\nSELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\n// substring() is 1-indexed and clamps `length` at the value's end.\r\nconst SELECT_CHUNK = `SELECT encode(substring(data FROM $1::int FOR $2::int), 'base64') AS chunk FROM zerobucket_images WHERE id = $3;`;\r\nconst SELECT_STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = $1;`;\r\nconst DELETE_RETURNING = `DELETE FROM zerobucket_images WHERE id = $1 RETURNING storage_backend, object_storage_key;`;\r\nconst DELETE_MANY = `DELETE FROM zerobucket_images WHERE id = ANY($1::uuid[]) RETURNING id, storage_backend, object_storage_key;`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = $1;`;\r\n\r\n// FOR UPDATE: a per-row lock held until the transaction ends (see tierToObjectStorage).\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = $1 FOR UPDATE;`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = $1, object_storage_key = $2, updated_at = now() WHERE id = $3;`;\r\n\r\n// ---- dedup mode: content-addressed blobs + per-image refs (same schema as the Python package) ----\r\nconst DEDUP_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 CHAR(64) PRIMARY KEY,\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n ref_count INTEGER NOT NULL DEFAULT 0,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n checksum_sha256 CHAR(64) NOT NULL REFERENCES zerobucket_blobs(checksum_sha256),\r\n original_filename TEXT,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_checksum ON zerobucket_image_refs (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_created_at ON zerobucket_image_refs (created_at);\r\n`;\r\n\r\n// $7 is how many references this call adds (1 for put; N when a batch holds N identical images).\r\n// ON CONFLICT makes the increment atomic at the row level: 20 concurrent puts of the same bytes give ref_count == 20.\r\nconst D_UPSERT_BLOB = `\r\nINSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nON CONFLICT (checksum_sha256) DO UPDATE SET ref_count = zerobucket_blobs.ref_count + EXCLUDED.ref_count;`;\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (checksum_sha256, original_filename) VALUES ($1, $2) RETURNING id;`;\r\n// Aliased to the classic column names so one row mapper serves both modes.\r\nconst D_COLS = `r.id, encode(b.data, 'base64') AS data_b64, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256,\r\n 'postgres'::text AS storage_backend, NULL::text AS object_storage_bucket, NULL::text AS object_storage_key`;\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = $1 RETURNING checksum_sha256;`;\r\nconst D_DELETE_REFS = `DELETE FROM zerobucket_image_refs WHERE id = ANY($1::uuid[]) RETURNING id, checksum_sha256;`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - $1 WHERE checksum_sha256 = $2 RETURNING ref_count;`;\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ANY($1::text[]) AND ref_count <= 0;`;\r\n\r\ninterface SqlSet {\r\n schema: string;\r\n selectFull: string;\r\n selectMany: string;\r\n selectMetadata: string;\r\n selectStreamInfo: string;\r\n selectChunk: string;\r\n exists: string;\r\n}\r\nconst CLASSIC_SQL: SqlSet = {\r\n schema: SCHEMA,\r\n selectFull: SELECT_FULL,\r\n selectMany: SELECT_MANY,\r\n selectMetadata: SELECT_METADATA,\r\n selectStreamInfo: SELECT_STREAM_INFO,\r\n selectChunk: SELECT_CHUNK,\r\n exists: EXISTS,\r\n};\r\nconst DEDUP_SQL: SqlSet = {\r\n schema: DEDUP_SCHEMA,\r\n selectFull: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = $1;`,\r\n selectMany: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ANY($1::uuid[]);`,\r\n selectMetadata: `SELECT r.id, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256 ${D_FROM} WHERE r.id = $1;`,\r\n selectStreamInfo: `SELECT b.size_bytes, 'postgres'::text AS storage_backend, NULL::text AS object_storage_key ${D_FROM} WHERE r.id = $1;`,\r\n selectChunk: `SELECT encode(substring(b.data FROM $1::int FOR $2::int), 'base64') AS chunk ${D_FROM} WHERE r.id = $3;`,\r\n exists: `SELECT 1 FROM zerobucket_image_refs WHERE id = $1;`,\r\n};\r\n\r\nexport { isValidId };\r\n\r\n// SQLSTATEs worth retrying: transient conditions only, never constraint/syntax errors.\r\nconst RETRYABLE_SQLSTATES = new Set([\r\n \"40001\", // serialization_failure\r\n \"40P01\", // deadlock_detected\r\n \"08000\", // connection_exception\r\n \"08003\", // connection_does_not_exist\r\n \"08006\", // connection_failure\r\n \"08001\", // sqlclient_unable_to_establish_sqlconnection\r\n \"08004\", // sqlserver_rejected_establishment_of_sqlconnection\r\n \"57P01\", // admin_shutdown\r\n \"57P02\", // crash_shutdown\r\n \"57P03\", // cannot_connect_now\r\n \"53000\", // insufficient_resources\r\n \"53300\", // too_many_connections\r\n]);\r\nconst RETRYABLE_NODE_CODES = new Set([\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"ETIMEDOUT\",\r\n \"EPIPE\",\r\n \"ECONNABORTED\",\r\n]);\r\nconst CONNECTION_MESSAGE_RE =\r\n /connection terminated|connection timeout|timeout exceeded when trying to connect|client has encountered a connection error/i;\r\n\r\n/** Transient (worth retrying) vs. permanent (fails identically every time). */\r\nexport function isRetryable(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (typeof code === \"string\") {\r\n if (RETRYABLE_SQLSTATES.has(code) || RETRYABLE_NODE_CODES.has(code))\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\n/** Errors after which the pooled connection must be destroyed, not returned to the pool. */\r\nfunction isConnectionLevel(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (\r\n typeof code === \"string\" &&\r\n (RETRYABLE_NODE_CODES.has(code) ||\r\n code.startsWith(\"08\") ||\r\n code.startsWith(\"57\"))\r\n ) {\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\nconst MAX_BACKOFF_MS = 2000;\r\n\r\nexport function backoffDelayMs(attempt: number, baseDelayMs: number): number {\r\n const exponential = baseDelayMs * 2 ** (attempt - 1);\r\n const jitter = Math.random() * baseDelayMs;\r\n return Math.min(exponential + jitter, MAX_BACKOFF_MS);\r\n}\r\n\r\nexport interface PostgresBackendOptions {\r\n /** postgresql:// connection string. Required unless `pool` is given. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool (not closed by `close()`). Pool sizing options are then ignored. */\r\n pool?: Pool;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Retries for transient errors on pooled calls. Default 3; 0 disables. */\r\n maxRetries?: number;\r\n /** Base for exponential backoff with jitter (capped at 2s). Default 100. */\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n /** Max wait for a pooled connection. Default 10000. */\r\n poolTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n /**\r\n * Enables transparent reads of tiered rows and `tierToObjectStorage()`.\r\n * Configure it with the same bucket/credentials used to tier the images.\r\n */\r\n objectStorage?: ObjectStorageLike;\r\n /**\r\n * Content-addressed mode: identical bytes are stored once (zerobucket_blobs) and each\r\n * put() adds a lightweight reference (zerobucket_image_refs) with its own id and filename.\r\n * Separate tables from classic mode, so the two can never be confused. Cannot be combined\r\n * with `objectStorage`. Same schema as the Python package's dedup mode.\r\n */\r\n dedup?: boolean;\r\n}\r\n\r\ntype Work<T> = (q: Queryable) => Promise<T>;\r\n\r\nexport class PostgresBackend implements StorageBackend {\r\n private pool: Pool | undefined;\r\n private readonly ownsPool: boolean;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly maxRetries: number;\r\n private readonly retryBaseDelayMs: number;\r\n private readonly autoMigrate: boolean;\r\n private readonly dedup: boolean;\r\n private readonly sql: SqlSet;\r\n\r\n constructor(private readonly opts: PostgresBackendOptions) {\r\n if (!opts.connectionString && !opts.pool) {\r\n throw new TypeError(\r\n \"PostgresBackend requires either `connectionString` or `pool`\",\r\n );\r\n }\r\n this.ownsPool = !opts.pool;\r\n this.pool = opts.pool;\r\n this.maxRetries = opts.maxRetries ?? 3;\r\n this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 100;\r\n this.autoMigrate = opts.autoMigrate ?? true;\r\n this.dedup = opts.dedup ?? false;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n this.sql = this.dedup ? DEDUP_SQL : CLASSIC_SQL;\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined; // allow a later attempt (e.g. the DB was briefly down)\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n if (!this.pool) {\r\n let pg: { Pool: typeof Pool };\r\n try {\r\n const mod = (await import(\"pg\")) as unknown as {\r\n default?: { Pool: typeof Pool };\r\n Pool: typeof Pool;\r\n };\r\n pg = mod.default ?? mod; // CJS package: ESM gives { default }, some interop gives it directly\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"PostgresBackend requires the 'pg' package. Install it with: npm install pg\",\r\n { cause },\r\n );\r\n }\r\n const config: PoolConfig = {\r\n connectionString: this.opts.connectionString,\r\n min: this.opts.poolMinSize ?? 1,\r\n max: this.opts.poolMaxSize ?? 5,\r\n connectionTimeoutMillis: this.opts.poolTimeoutMs ?? 10_000,\r\n };\r\n this.pool = new pg.Pool(config);\r\n // An idle client erroring (server restart, network drop) must not crash the process.\r\n this.pool.on(\"error\", () => {});\r\n }\r\n try {\r\n if (this.autoMigrate) await this.migrate();\r\n else await this.pool.query(\"SELECT 1\");\r\n } catch (exc) {\r\n if (this.ownsPool) {\r\n await this.pool.end().catch(() => {});\r\n this.pool = undefined;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n }\r\n\r\n /** Create the schema if missing. Serialized across processes with an advisory lock. */\r\n async migrate(): Promise<void> {\r\n const pool = this.pool!;\r\n let client: PoolClient;\r\n try {\r\n client = await pool.connect();\r\n } catch (exc) {\r\n throw new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate'))\",\r\n );\r\n await client.query(this.sql.schema);\r\n await client.query(\"COMMIT\");\r\n client.release();\r\n } catch (exc) {\r\n await client.query(\"ROLLBACK\").catch(() => {});\r\n client.release(true);\r\n throw new StorageError(`Migration failed: ${msg(exc)}`, { cause: exc });\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const pool = this.pool;\r\n this.pool = undefined;\r\n this.readyPromise = undefined;\r\n if (pool && this.ownsPool) await pool.end();\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /**\r\n * Run `work` and return its result, emitting exactly one OperationEvent.\r\n *\r\n * connection given -> run once on it, no BEGIN/COMMIT, no retry.\r\n * connection absent -> pooled; `tx` wraps the work in BEGIN/COMMIT; transient\r\n * failures retry the WHOLE closure on a fresh connection.\r\n */\r\n private async run<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n work: Work<T>,\r\n { tx = false }: { tx?: boolean } = {},\r\n ): Promise<T> {\r\n if (connection) {\r\n if (!isQueryable(connection)) {\r\n throw new TypeError(\r\n \"PostgresBackend expects a pg client/pool client as `connection` (an object with query()), not a SQLite Database.\",\r\n );\r\n }\r\n const start = performance.now();\r\n try {\r\n const result = tx\r\n ? await this.atomic(connection, work)\r\n : await work(connection);\r\n this.emit(operation, start, true, null, 0);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), 0);\r\n throw exc;\r\n }\r\n }\r\n\r\n await this.ready();\r\n const start = performance.now();\r\n let attempt = 0;\r\n for (;;) {\r\n let client: PoolClient | undefined;\r\n let destroy = false;\r\n try {\r\n client = await this.pool!.connect();\r\n if (tx) await client.query(\"BEGIN\");\r\n const result = await work(client);\r\n if (tx) await client.query(\"COMMIT\");\r\n this.emit(operation, start, true, null, attempt);\r\n return result;\r\n } catch (exc) {\r\n if (client && tx) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true; // cannot trust a connection that cannot roll back\r\n }\r\n }\r\n if (isConnectionLevel(exc)) destroy = true;\r\n attempt += 1;\r\n if (attempt > this.maxRetries || !isRetryable(exc)) {\r\n this.emit(operation, start, false, msg(exc), attempt - 1);\r\n throw exc;\r\n }\r\n } finally {\r\n client?.release(destroy);\r\n }\r\n await sleep(backoffDelayMs(attempt, this.retryBaseDelayMs));\r\n }\r\n }\r\n\r\n /**\r\n * Make multi-statement work all-or-nothing on a CALLER-supplied connection.\r\n * - Caller has an open transaction -> SAVEPOINT: a failure rolls back only our work and the\r\n * caller's transaction stays usable; success joins their transaction.\r\n * - No open transaction (an autocommit pg.Client) -> open and commit our own, so a failure\r\n * can never leave half-applied state (e.g. an inflated dedup ref_count).\r\n */\r\n private async atomic<T>(conn: Queryable, work: Work<T>): Promise<T> {\r\n let ownTx = false;\r\n try {\r\n await conn.query(\"SAVEPOINT zerobucket_atomic\");\r\n } catch (exc) {\r\n if ((exc as { code?: string }).code !== \"25P01\") throw exc; // 25P01 = no active transaction\r\n ownTx = true;\r\n await conn.query(\"BEGIN\");\r\n }\r\n try {\r\n const result = await work(conn);\r\n await conn.query(\r\n ownTx ? \"COMMIT\" : \"RELEASE SAVEPOINT zerobucket_atomic\",\r\n );\r\n return result;\r\n } catch (exc) {\r\n try {\r\n await conn.query(\r\n ownTx ? \"ROLLBACK\" : \"ROLLBACK TO SAVEPOINT zerobucket_atomic\",\r\n );\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n if (this.dedup) {\r\n return await this.run(\r\n \"put\",\r\n connection,\r\n async (q) => {\r\n await q.query(D_UPSERT_BLOB, blobParams(row, 1));\r\n return String(\r\n (\r\n await q.query(D_INSERT_REF, [\r\n row.checksumSha256,\r\n row.originalFilename,\r\n ])\r\n ).rows[0].id,\r\n );\r\n },\r\n { tx: true },\r\n );\r\n }\r\n return await this.run(\"put\", connection, async (q) => {\r\n const res = await q.query(INSERT, rowParams(row));\r\n return String(res.rows[0].id);\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n if (this.dedup) {\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n // Identical images within one batch share a blob: count them, upsert each distinct\r\n // checksum ONCE in sorted order (a global lock order, so two overlapping batches\r\n // can never deadlock on each other's blob rows), then create the refs in input order.\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const g = groups.get(checksum)!;\r\n await q.query(D_UPSERT_BLOB, blobParams(g.row, g.n));\r\n }\r\n const ids: string[] = [];\r\n for (const r of rows) {\r\n ids.push(\r\n String(\r\n (\r\n await q.query(D_INSERT_REF, [\r\n r.checksumSha256,\r\n r.originalFilename,\r\n ])\r\n ).rows[0].id,\r\n ),\r\n );\r\n }\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n }\r\n // One transaction. A transient failure rolls back EVERYTHING, so a retry\r\n // from scratch cannot create duplicates. Statements are awaited one at a\r\n // time on the single connection (queuing concurrent queries on one pg\r\n // client is deprecated and breaks in pg@9), so result order matches input\r\n // order by construction.\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n const ids: string[] = [];\r\n for (const r of rows)\r\n ids.push(String((await q.query(INSERT, rowParams(r))).rows[0].id));\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectFull, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? this.hydrate(row, id) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.run(\r\n \"get_many\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMany, [valid])).rows,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) => this.hydrate(r, String(r.id)));\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMetadata, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectStreamInfo, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(id, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n id,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n const self = this;\r\n\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start; // 0-based\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.run(\r\n \"get_stream\",\r\n connection,\r\n async (q) =>\r\n (await q.query(self.sql.selectChunk, [offset + 1, length, id]))\r\n .rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n // base64 text from encode(substring(...)); null when the row has no bytes, \"\" when the range is past the end\r\n const chunk: Buffer | null =\r\n row.chunk == null ? null : Buffer.from(row.chunk, \"base64\");\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n if (this.dedup) return this.deleteDedup(id, connection);\r\n try {\r\n const row = await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => (await q.query(DELETE_RETURNING, [id])).rows[0],\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObject([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n if (this.dedup) return this.deleteManyDedup(valid, connection);\r\n try {\r\n const rows = await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => (await q.query(DELETE_MANY, [valid])).rows,\r\n );\r\n await this.cleanupObject(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.run(\r\n \"exists\",\r\n connection,\r\n async (q) => ((await q.query(this.sql.exists, [id])).rowCount ?? 0) > 0,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- maintenance (CLI) -----------------------------------------------\r\n\r\n async getInfo(): Promise<StorageInfo | null> {\r\n try {\r\n return await this.run(\"info\", undefined, async (q) => {\r\n const imgTable = this.dedup\r\n ? \"zerobucket_image_refs\"\r\n : \"zerobucket_images\";\r\n const present = await q.query(\r\n \"SELECT to_regclass($1) AS a, to_regclass('zerobucket_blobs') AS b\",\r\n [imgTable],\r\n );\r\n if (!present.rows[0].a || (this.dedup && !present.rows[0].b))\r\n return null;\r\n const j = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const size = this.dedup ? \"b.size_bytes\" : \"size_bytes\";\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n const mime = this.dedup ? \"b.mime_type\" : \"mime_type\";\r\n const tot = (\r\n await q.query(\r\n `SELECT count(*)::int AS n, coalesce(sum(${size}), 0)::text AS bytes, min(${created})::text AS oldest, max(${created})::text AS newest ${j}`,\r\n )\r\n ).rows[0];\r\n const disk = (\r\n await q.query(\r\n this.dedup\r\n ? \"SELECT pg_size_pretty(pg_total_relation_size('zerobucket_blobs') + pg_total_relation_size('zerobucket_image_refs')) AS s\"\r\n : \"SELECT pg_size_pretty(pg_total_relation_size('zerobucket_images')) AS s\",\r\n )\r\n ).rows[0].s as string;\r\n const fmt = (\r\n await q.query(\r\n `SELECT ${mime} AS m, count(*)::int AS n, coalesce(sum(${size}), 0)::text AS bytes ${j} GROUP BY ${mime} ORDER BY count(*) DESC, ${mime}`,\r\n )\r\n ).rows;\r\n const info: StorageInfo = {\r\n mode: this.dedup ? \"dedup\" : \"classic\",\r\n count: tot.n,\r\n totalBytes: Number(tot.bytes),\r\n // Postgres prints the offset as \"+00\"; Python's CLI (and ISO-8601) print \"+00:00\". Normalise so output is identical.\r\n oldest: tot.oldest && tot.oldest.replace(/([+-]\\d\\d)$/, \"$1:00\"),\r\n newest: tot.newest && tot.newest.replace(/([+-]\\d\\d)$/, \"$1:00\"),\r\n onDisk: disk,\r\n byFormat: fmt.map((r) => ({\r\n mimeType: r.m,\r\n count: r.n,\r\n bytes: Number(r.bytes),\r\n })),\r\n };\r\n if (this.dedup) {\r\n const b = (\r\n await q.query(\r\n \"SELECT count(*)::int AS n, coalesce(sum(size_bytes), 0)::text AS bytes FROM zerobucket_blobs\",\r\n )\r\n ).rows[0];\r\n info.dedup = { blobs: b.n, storedBytes: Number(b.bytes) };\r\n } else {\r\n const t = (\r\n await q.query(\r\n \"SELECT count(*)::int AS n, coalesce(sum(size_bytes), 0)::text AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'\",\r\n )\r\n ).rows[0];\r\n info.tiered = { count: t.n, bytes: Number(t.bytes) };\r\n }\r\n return info;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to read storage info\", exc);\r\n }\r\n }\r\n\r\n async listImages(o: ImageListOptions = {}): Promise<ImageListEntry[]> {\r\n const where: string[] = [];\r\n const params: unknown[] = [];\r\n const add = (sql: string, v: unknown) => (\r\n params.push(v),\r\n where.push(sql.replace(\"$?\", `$${params.length}`))\r\n );\r\n if (o.minSize !== undefined)\r\n add(`${this.dedup ? \"b.size_bytes\" : \"size_bytes\"} >= $?`, o.minSize);\r\n if (o.olderThanDays !== undefined)\r\n add(\r\n `${this.dedup ? \"r.created_at\" : \"created_at\"} <= now() - make_interval(days => $?::int)`,\r\n o.olderThanDays,\r\n );\r\n if (o.onlyUntiered && !this.dedup)\r\n where.push(\"storage_backend = 'postgres'\");\r\n const from = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const idCol = this.dedup ? \"r.id\" : \"id\";\r\n const tierCol = this.dedup\r\n ? \"false AS tiered\"\r\n : \"(storage_backend = 'object_storage') AS tiered\";\r\n const order =\r\n o.sample !== undefined\r\n ? \"ORDER BY random()\"\r\n : `ORDER BY ${this.dedup ? \"r.created_at\" : \"created_at\"}, ${idCol}`;\r\n const lim = o.sample ?? o.limit;\r\n let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? \" WHERE \" + where.join(\" AND \") : \"\"} ${order}`;\r\n if (lim !== undefined) {\r\n params.push(lim);\r\n sql += ` LIMIT $${params.length}`;\r\n }\r\n try {\r\n const rows = await this.run(\r\n \"list_images\",\r\n undefined,\r\n async (q) => (await q.query(sql, params)).rows,\r\n );\r\n return rows.map((r) => ({ id: String(r.id), tiered: Boolean(r.tiered) }));\r\n } catch (exc) {\r\n throw wrap(\"Failed to list images\", exc);\r\n }\r\n }\r\n\r\n // ---- dedup deletes / migration ---------------------------------------\r\n\r\n private async deleteDedup(\r\n id: string,\r\n connection: Connection | undefined,\r\n ): Promise<boolean> {\r\n try {\r\n return await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const ref = (await q.query(D_DELETE_REF, [id])).rows[0];\r\n if (!ref) return false; // id did not exist: nothing to decrement either\r\n const checksum = ref.checksum_sha256 as string;\r\n const dec = (await q.query(D_DECREMENT, [1, checksum])).rows[0];\r\n if (dec && dec.ref_count <= 0)\r\n await q.query(D_DELETE_EMPTY, [[checksum]]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n private async deleteManyDedup(\r\n ids: string[],\r\n connection: Connection | undefined,\r\n ): Promise<string[]> {\r\n try {\r\n return await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const deleted = (await q.query(D_DELETE_REFS, [ids])).rows;\r\n if (deleted.length === 0) return [];\r\n // Several deleted refs may share one blob: decrement each blob by its exact count,\r\n // in sorted checksum order (global lock order, so concurrent batches cannot deadlock).\r\n const counts = new Map<string, number>();\r\n for (const r of deleted)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n const emptied: string[] = [];\r\n for (const checksum of [...counts.keys()].sort()) {\r\n const dec = (\r\n await q.query(D_DECREMENT, [counts.get(checksum), checksum])\r\n ).rows[0];\r\n if (dec && dec.ref_count <= 0) emptied.push(checksum);\r\n }\r\n if (emptied.length > 0) await q.query(D_DELETE_EMPTY, [emptied]);\r\n return deleted.map((r) => String(r.id));\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n /**\r\n * One-time, NON-DESTRUCTIVE copy of every row in the classic `zerobucket_images` table into this\r\n * dedup instance's tables, preserving every id (external references keep working) and\r\n * deduplicating identical content on the way. The classic table is never modified.\r\n *\r\n * Rows are copied in small batches (bounded memory instead of loading the whole table);\r\n * re-running is safe (already-migrated ids are skipped and reported, not an error). Classic rows\r\n * already tiered to object storage are refused up front with a clear message (their bytes are\r\n * not in the database to copy) unless `skipTiered: true`, which skips and counts them instead.\r\n * Still all-or-nothing: one transaction.\r\n */\r\n async migrateClassicToDedup(\r\n opts: MigrateToDedupOptions = {},\r\n ): Promise<MigrateToDedupResult> {\r\n if (!this.dedup) {\r\n throw new StorageError(\r\n \"migrateClassicToDedup() must be called on an instance created with dedup: true.\",\r\n );\r\n }\r\n await this.ready();\r\n const client = await this.pool!.connect();\r\n let destroy = false;\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate_dedup'))\",\r\n );\r\n if (\r\n !(await client.query(\"SELECT to_regclass('zerobucket_images') AS t\"))\r\n .rows[0].t\r\n ) {\r\n throw new StorageError(\r\n \"No classic zerobucket_images table found in this database -- nothing to migrate.\",\r\n );\r\n }\r\n let tieredSkipped = 0;\r\n const hasTierCol = (\r\n await client.query(\r\n \"SELECT 1 FROM information_schema.columns WHERE table_name = 'zerobucket_images' AND column_name = 'storage_backend'\",\r\n )\r\n ).rowCount;\r\n if (hasTierCol) {\r\n const tiered = (\r\n await client.query(\r\n \"SELECT count(*)::int AS n FROM zerobucket_images WHERE storage_backend <> 'postgres'\",\r\n )\r\n ).rows[0].n;\r\n if (tiered > 0 && !opts.skipTiered) {\r\n throw new StorageError(\r\n `${tiered} classic image(s) are tiered to object storage, so their bytes are not in the database to migrate. ` +\r\n \"Dedup mode does not support tiering. Migrate only an un-tiered table, bring those images back first, \" +\r\n \"or pass skipTiered: true (CLI: --skip-tiered) to skip them and migrate the rest.\",\r\n );\r\n }\r\n tieredSkipped = tiered;\r\n }\r\n // Only reached with tiered rows present when skipTiered is set: leave them out of the copy.\r\n const onlyInDb = hasTierCol && tieredSkipped > 0;\r\n\r\n const seen = new Set<string>();\r\n let migrated = 0;\r\n let already = 0;\r\n // created_at travels as TEXT, never a JS Date: Postgres keeps microseconds, a Date keeps only\r\n // milliseconds, and a truncated keyset cursor sorts before its own row (infinite loop).\r\n let cursor: { createdAt: string; id: string } | null = null;\r\n const BATCH = 20;\r\n for (;;) {\r\n const page: { rows: any[] } = cursor\r\n ? await client.query(\r\n `SELECT id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at::text AS created_at_txt\r\n FROM zerobucket_images WHERE (created_at, id) > ($1::timestamptz, $2::uuid)${onlyInDb ? \" AND storage_backend = 'postgres'\" : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n [cursor.createdAt, cursor.id],\r\n )\r\n : await client.query(\r\n `SELECT id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at::text AS created_at_txt\r\n FROM zerobucket_images${onlyInDb ? \" WHERE storage_backend = 'postgres'\" : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n );\r\n if (page.rows.length === 0) break;\r\n const last = page.rows[page.rows.length - 1];\r\n if (\r\n cursor &&\r\n cursor.createdAt === last.created_at_txt &&\r\n cursor.id === last.id\r\n ) {\r\n throw new StorageError(\r\n \"Migration made no progress (keyset cursor did not advance); aborting instead of looping.\",\r\n );\r\n }\r\n cursor = { createdAt: last.created_at_txt, id: last.id };\r\n\r\n const have = new Set(\r\n (\r\n await client.query(\r\n \"SELECT id FROM zerobucket_image_refs WHERE id = ANY($1::uuid[])\",\r\n [page.rows.map((r) => r.id)],\r\n )\r\n ).rows.map((r) => String(r.id)),\r\n );\r\n const fresh = page.rows.filter((r) => !have.has(String(r.id)));\r\n already += page.rows.length - fresh.length;\r\n\r\n const groups = new Map<string, { row: any; n: number }>();\r\n for (const r of fresh) {\r\n const g = groups.get(r.checksum_sha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksum_sha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const { row, n } = groups.get(checksum)!;\r\n await client.query(D_UPSERT_BLOB, [\r\n checksum,\r\n row.data,\r\n row.mime_type,\r\n row.size_bytes,\r\n row.width,\r\n row.height,\r\n n,\r\n ]);\r\n }\r\n for (const r of fresh) {\r\n await client.query(\r\n \"INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at) VALUES ($1, $2, $3, $4::timestamptz)\",\r\n [r.id, r.checksum_sha256, r.original_filename, r.created_at_txt],\r\n );\r\n seen.add(r.checksum_sha256);\r\n migrated += 1;\r\n }\r\n }\r\n await client.query(\"COMMIT\");\r\n return {\r\n imagesMigrated: migrated,\r\n distinctBlobsCreated: seen.size,\r\n duplicateReferencesFound: migrated - seen.size,\r\n alreadyMigrated: already,\r\n tieredSkipped,\r\n };\r\n } catch (exc) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Migration to dedup failed: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n } finally {\r\n client.release(destroy);\r\n }\r\n }\r\n\r\n // ---- helpers ---------------------------------------------------------\r\n\r\n private hydrate(row: any, id: string): Promise<StoredRecord> {\r\n // The shared row mapper expects a Buffer in `data`; the query returns it base64-encoded (see SELECT_FULL). A tiered\r\n // row has no bytes in the database, so encode(NULL) is NULL and `data` stays null.\r\n row.data =\r\n row.data_b64 == null ? null : Buffer.from(row.data_b64, \"base64\");\r\n delete row.data_b64;\r\n return hydrateRow(row, id, this.opts.objectStorage);\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone. Ordering is deliberate:\r\n * once the row is deleted the image is correctly \"not found\" to everyone, so\r\n * a failed object delete only leaves a harmless orphan, whereas deleting the\r\n * object first and then failing the row delete would leave a row pointing at nothing.\r\n *\r\n * Never runs inside a caller-owned transaction (`connection`): we cannot know\r\n * whether it will commit, and destroying data before the commit outcome is known\r\n * could leave a rolled-back row pointing at a deleted object. In that case the\r\n * object stays until you remove it yourself after committing (objectStorage.delete(id)).\r\n * Failures are reported through onOperation and never fail the delete itself.\r\n */\r\n private async cleanupObject(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes out of Postgres into object storage, replacing the\r\n * row's `data` with a pointer. Returns:\r\n * null - no such image\r\n * false - already tiered (a safe no-op, so backfills can be re-run)\r\n * true - tiered just now\r\n *\r\n * SAFETY: the upload happens INSIDE the same transaction as the row lock\r\n * (SELECT ... FOR UPDATE) and the UPDATE that flips the row. If the upload\r\n * fails, the transaction rolls back and the row is untouched: there is no\r\n * moment when the bytes exist nowhere, or when a row claims to be tiered\r\n * without a completed upload.\r\n *\r\n * TRADEOFF: the row lock is held for the whole upload. Concurrent writes\r\n * to THIS image (delete, tier) wait; every other row is unaffected, and plain\r\n * reads are never blocked (Postgres MVCC). Meant for explicit maintenance, not hot paths.\r\n *\r\n * Retry is safe: a transient error replays the whole closure, and the\r\n * deterministic key (the image id) makes the re-upload an idempotent overwrite.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n }\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const key = id.toLowerCase();\r\n try {\r\n return await this.run(\r\n \"tier_to_object_storage\",\r\n connection,\r\n async (q) => {\r\n const row = (await q.query(SELECT_FOR_TIERING, [id])).rows[0];\r\n if (!row) return null;\r\n if (row.storage_backend !== \"postgres\") return false;\r\n await os.upload(key, row.data as Buffer, { mimeType: row.mime_type });\r\n await q.query(UPDATE_AFTER_TIERING, [os.bucket, key, id]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n\r\nfunction blobParams(r: PreparedRow, refs: number): unknown[] {\r\n return [\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n refs,\r\n ];\r\n}\r\n\r\nfunction rowParams(r: PreparedRow): unknown[] {\r\n return [\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n ];\r\n}\r\n\r\nfunction isQueryable(c: Connection): c is Queryable {\r\n return typeof (c as Queryable).query === \"function\";\r\n}\r\n","/**\r\n * SQLite storage adapter (classic mode, dedup mode, streaming, object-storage tiering).\r\n *\r\n * Uses the EXACT schema the Python package's SQLiteBackend creates, so a Node app and a\r\n * Python app can share one `.db` file: ids are TEXT UUIDs generated by the application, timestamps are\r\n * ISO-8601 TEXT, classic rows say storage_backend='sqlite', and tiered rows point at object storage.\r\n *\r\n * HOW THIS DIFFERS FROM THE POSTGRES ADAPTER (and why)\r\n *\r\n * - The driver (`better-sqlite3`) is SYNCHRONOUS. Every operation here is async only in signature: the\r\n * SQLite call itself runs on the event loop and returns almost immediately (it is a local file).\r\n * Because a blocked synchronous call would freeze the whole process, lock contention is never waited\r\n * out inside the driver. Each attempt uses a ~25 ms busy timeout and BUSY errors are retried here with\r\n * ASYNC sleeps, up to `busyTimeoutMs` in total, so the event loop stays free.\r\n * - One long-lived connection serves all normal operations. Each unit of work runs to completion\r\n * synchronously inside BEGIN IMMEDIATE ... COMMIT, so concurrent requests can never interleave inside\r\n * a transaction.\r\n * - Tiering awaits a network upload while holding a write transaction, so it uses its OWN dedicated\r\n * connection (otherwise other requests would run inside that open transaction and a rollback would\r\n * erase their writes). SQLite has no per-row locks: BEGIN IMMEDIATE locks the WHOLE DATABASE for the\r\n * duration of the upload. Writes from other PROCESSES wait (and time out after busyTimeoutMs); writes\r\n * from this process queue behind an in-process async lock. Reads are never blocked (WAL).\r\n * - WAL mode is a durable file-level setting. It is set at most once per backend instance, and only if\r\n * the file is not already in WAL mode (re-issuing it on every connection caused real lock contention\r\n * on Windows in the Python package).\r\n * - Streaming reads use ranged `substr(data, offset, length)` queries (better-sqlite3 has no incremental\r\n * blob handle). A row deleted mid-stream therefore raises StorageError, exactly like Postgres. (The\r\n * Python SYNC SQLite adapter instead keeps streaming from a WAL snapshot; pass your own `connection`\r\n * inside a transaction here if you need that.) SQLite still reads the whole blob per chunk, in native\r\n * memory: this bounds Node-side memory only.\r\n */\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { existsSync, statSync } from \"node:fs\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n OperationEvent,\r\n PreparedRow,\r\n SqliteDatabaseLike,\r\n} from \"../types.js\";\r\nimport type {\r\n CallOptions,\r\n ImageListEntry,\r\n ImageListOptions,\r\n StorageBackend,\r\n StorageInfo,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\nimport {\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n prettySize,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\n\r\n// ---- schema (identical to the Python package) -------------------------------------------------\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id TEXT PRIMARY KEY,\r\n data BLOB,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 TEXT NOT NULL,\r\n created_at TEXT NOT NULL,\r\n updated_at TEXT NOT NULL,\r\n storage_backend TEXT NOT NULL DEFAULT 'sqlite',\r\n object_storage_bucket TEXT,\r\n object_storage_key TEXT,\r\n CHECK (\r\n (storage_backend = 'sqlite'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n )\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n`;\r\n\r\nconst DEDUP_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 TEXT PRIMARY KEY,\r\n data BLOB NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n ref_count INTEGER NOT NULL DEFAULT 0,\r\n created_at TEXT NOT NULL\r\n);\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id TEXT PRIMARY KEY,\r\n checksum_sha256 TEXT NOT NULL REFERENCES zerobucket_blobs(checksum_sha256),\r\n original_filename TEXT,\r\n created_at TEXT NOT NULL,\r\n updated_at TEXT NOT NULL\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_checksum ON zerobucket_image_refs (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_created_at ON zerobucket_image_refs (created_at);\r\n`;\r\n\r\n// ---- SQL --------------------------------------------------------------------------------------\r\n\r\nconst COLS = `id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, storage_backend, object_storage_bucket, object_storage_key`;\r\nconst INSERT = `INSERT INTO zerobucket_images (id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\r\nconst SELECT_FULL = `SELECT ${COLS} FROM zerobucket_images WHERE id = ?`;\r\nconst selectMany = (n: number) =>\r\n `SELECT ${COLS} FROM zerobucket_images WHERE id IN (${placeholders(n)})`;\r\nconst SELECT_METADATA = `SELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256 FROM zerobucket_images WHERE id = ?`;\r\nconst STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = ?`;\r\nconst CHUNK = `SELECT substr(data, ?, ?) AS chunk FROM zerobucket_images WHERE id = ?`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = ?`;\r\nconst DELETE_ONE = `DELETE FROM zerobucket_images WHERE id = ? RETURNING storage_backend, object_storage_key`;\r\nconst deleteMany = (n: number) =>\r\n `DELETE FROM zerobucket_images WHERE id IN (${placeholders(n)}) RETURNING id, storage_backend, object_storage_key`;\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = ?`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = ?, object_storage_key = ?, updated_at = ? WHERE id = ?`;\r\n\r\n// dedup. Reads are aliased to the classic column names so one row mapper serves both modes.\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_COLS = `r.id AS id, b.data AS data, b.mime_type AS mime_type, r.original_filename AS original_filename, b.size_bytes AS size_bytes, b.width AS width, b.height AS height, r.checksum_sha256 AS checksum_sha256, 'sqlite' AS storage_backend, NULL AS object_storage_bucket, NULL AS object_storage_key`;\r\nconst D_UPSERT = `INSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (checksum_sha256) DO UPDATE SET ref_count = ref_count + excluded.ref_count`;\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`;\r\nconst D_SELECT_FULL = `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ?`;\r\nconst dSelectMany = (n: number) =>\r\n `SELECT ${D_COLS} ${D_FROM} WHERE r.id IN (${placeholders(n)})`;\r\nconst D_SELECT_METADATA = `SELECT r.id AS id, b.mime_type AS mime_type, r.original_filename AS original_filename, b.size_bytes AS size_bytes, b.width AS width, b.height AS height, r.checksum_sha256 AS checksum_sha256 ${D_FROM} WHERE r.id = ?`;\r\nconst D_STREAM_INFO = `SELECT b.size_bytes AS size_bytes, 'sqlite' AS storage_backend, NULL AS object_storage_key ${D_FROM} WHERE r.id = ?`;\r\nconst D_CHUNK = `SELECT substr(b.data, ?, ?) AS chunk ${D_FROM} WHERE r.id = ?`;\r\nconst D_EXISTS = `SELECT 1 FROM zerobucket_image_refs WHERE id = ?`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = ? RETURNING checksum_sha256`;\r\nconst dDeleteRefs = (n: number) =>\r\n `DELETE FROM zerobucket_image_refs WHERE id IN (${placeholders(n)}) RETURNING id, checksum_sha256`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - ? WHERE checksum_sha256 = ? RETURNING ref_count`;\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ? AND ref_count <= 0`;\r\n\r\n/** SQLite's default max bound variables is 999 on old builds; stay well under it. */\r\nconst IN_CHUNK = 500;\r\n/** Per-attempt busy wait inside the (synchronous) driver. Longer waits are done by async retry, never by blocking the loop. */\r\nconst ATTEMPT_BUSY_MS = 25;\r\nexport const DEFAULT_BUSY_TIMEOUT_MS = 10_000;\r\n\r\nfunction placeholders(n: number): string {\r\n return Array.from({ length: n }, () => \"?\").join(\", \");\r\n}\r\n\r\n/** Python-compatible ISO-8601 UTC with microseconds: 2026-10-03T12:00:00.123000+00:00 */\r\nfunction isoNow(): string {\r\n return new Date().toISOString().replace(\"Z\", \"000+00:00\");\r\n}\r\n\r\nfunction isBusy(exc: unknown): boolean {\r\n const code = (exc as { code?: unknown })?.code;\r\n return (\r\n typeof code === \"string\" &&\r\n (code.startsWith(\"SQLITE_BUSY\") || code.startsWith(\"SQLITE_LOCKED\"))\r\n );\r\n}\r\n\r\n/** The members of better-sqlite3's Database that this adapter uses beyond SqliteDatabaseLike. */\r\ninterface Db extends SqliteDatabaseLike {\r\n pragma(source: string, options?: { simple?: boolean }): unknown;\r\n close(): void;\r\n}\r\ntype DbCtor = new (path: string, options?: { timeout?: number }) => Db;\r\n\r\n/** Tiny async mutex: serializes in-process writers (and tiering) without blocking the event loop. */\r\nclass Mutex {\r\n private tail: Promise<void> = Promise.resolve();\r\n run<T>(fn: () => Promise<T>): Promise<T> {\r\n const prev = this.tail;\r\n let release!: () => void;\r\n this.tail = new Promise<void>((r) => (release = r));\r\n return prev.then(fn).finally(release);\r\n }\r\n}\r\n\r\nexport interface SQLiteBackendOptions {\r\n /** Database file path, or \":memory:\" for a throwaway in-process database (tiering is unavailable for \":memory:\"). */\r\n path: string;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Content-addressed mode (separate tables; same schema as the Python package). Cannot be combined with objectStorage. */\r\n dedup?: boolean;\r\n /** Enables tiering and transparent reads of tiered rows. */\r\n objectStorage?: ObjectStorageLike;\r\n /** Total time to keep retrying when another connection/process holds the write lock. Default 10000. */\r\n busyTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n}\r\n\r\nexport class SQLiteBackend implements StorageBackend {\r\n private db: Db | undefined;\r\n private Database: DbCtor | undefined;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly dedup: boolean;\r\n private readonly busyTimeoutMs: number;\r\n private readonly writeLock = new Mutex();\r\n\r\n constructor(private readonly opts: SQLiteBackendOptions) {\r\n if (!opts.path)\r\n throw new TypeError(\r\n 'SQLiteBackend requires a `path` (a file path or \":memory:\")',\r\n );\r\n this.dedup = opts.dedup ?? false;\r\n this.busyTimeoutMs = opts.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined;\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n try {\r\n const mod = (await import(\"better-sqlite3\")) as unknown as {\r\n default?: DbCtor;\r\n } & DbCtor;\r\n this.Database = mod.default ?? mod;\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"SQLiteBackend requires the 'better-sqlite3' package. Install it with: npm install better-sqlite3\",\r\n { cause },\r\n );\r\n }\r\n const db = new this.Database(this.opts.path, { timeout: ATTEMPT_BUSY_MS });\r\n try {\r\n await this.retrying(() => {\r\n // WAL is durable and file-level: only convert if the file is not already WAL (avoids needless\r\n // lock contention when many processes open the same fresh file at once).\r\n if (\r\n this.opts.path !== \":memory:\" &&\r\n db.pragma(\"journal_mode\", { simple: true }) !== \"wal\"\r\n ) {\r\n db.pragma(\"journal_mode = WAL\");\r\n }\r\n db.pragma(\"foreign_keys = ON\");\r\n if (this.opts.autoMigrate ?? true)\r\n db.exec(this.dedup ? DEDUP_SCHEMA : SCHEMA);\r\n });\r\n } catch (exc) {\r\n try {\r\n db.close();\r\n } catch {\r\n // ignore\r\n }\r\n throw new StorageError(\r\n `Could not open SQLite database ${JSON.stringify(this.opts.path)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n this.db = db;\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const db = this.db;\r\n this.db = undefined;\r\n this.readyPromise = undefined;\r\n // let any in-flight write finish before the handle goes away\r\n await this.writeLock.run(async () => {\r\n db?.close();\r\n });\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /** Re-run `fn` while another connection holds a lock, sleeping asynchronously between attempts. `fn` must be atomic. */\r\n private async retrying<T>(\r\n fn: () => T,\r\n onRetry?: (n: number) => void,\r\n ): Promise<T> {\r\n const deadline = performance.now() + this.busyTimeoutMs;\r\n let attempt = 0;\r\n for (;;) {\r\n try {\r\n return fn();\r\n } catch (exc) {\r\n if (!isBusy(exc) || performance.now() >= deadline) throw exc;\r\n attempt += 1;\r\n onRetry?.(attempt);\r\n await sleep(\r\n Math.min(5 * 2 ** Math.min(attempt, 5), 100) + Math.random() * 5,\r\n );\r\n }\r\n }\r\n }\r\n\r\n /** BEGIN IMMEDIATE takes the write lock up front, so a BUSY comes before any work and the whole unit is safely re-runnable. */\r\n private tx<T>(db: Db, work: (db: Db) => T): T {\r\n db.exec(\"BEGIN IMMEDIATE\");\r\n try {\r\n const result = work(db);\r\n db.exec(\"COMMIT\");\r\n return result;\r\n } catch (exc) {\r\n try {\r\n db.exec(\"ROLLBACK\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n /**\r\n * All-or-nothing work on a CALLER-supplied handle. SAVEPOINT works whether or not the caller has an open\r\n * transaction: inside one it nests (a failure rolls back only our work); outside one it begins and, on\r\n * RELEASE, commits.\r\n */\r\n private atomic<T>(db: Db, work: (db: Db) => T): T {\r\n db.exec(\"SAVEPOINT zerobucket_atomic\");\r\n try {\r\n const result = work(db);\r\n db.exec(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n return result;\r\n } catch (exc) {\r\n try {\r\n db.exec(\"ROLLBACK TO SAVEPOINT zerobucket_atomic\");\r\n db.exec(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n private async execute<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n mode: \"read\" | \"write\",\r\n work: (db: Db) => T,\r\n ): Promise<T> {\r\n const start = performance.now();\r\n let retries = 0;\r\n try {\r\n let result: T;\r\n if (connection) {\r\n if (typeof (connection as SqliteDatabaseLike).prepare !== \"function\") {\r\n throw new TypeError(\r\n \"SQLiteBackend expects a better-sqlite3 Database as `connection` (an object with prepare()), not a pg client.\",\r\n );\r\n }\r\n const db = connection as Db;\r\n result = mode === \"write\" ? this.atomic(db, work) : work(db);\r\n } else {\r\n await this.ready();\r\n const db = this.db!;\r\n const onRetry = (n: number) => (retries = n);\r\n result =\r\n mode === \"write\"\r\n ? await this.writeLock.run(() =>\r\n this.retrying(() => this.tx(db, work), onRetry),\r\n )\r\n : await this.retrying(() => work(db), onRetry);\r\n }\r\n this.emit(operation, start, true, null, retries);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), retries);\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n return await this.execute(\"put\", connection, \"write\", (db) =>\r\n this.insertOne(db, row),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n private insertOne(db: Db, r: PreparedRow): string {\r\n const id = randomUUID();\r\n const now = isoNow();\r\n if (this.dedup) {\r\n db.prepare(D_UPSERT).run(\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n 1,\r\n now,\r\n );\r\n db.prepare(D_INSERT_REF).run(\r\n id,\r\n r.checksumSha256,\r\n r.originalFilename,\r\n now,\r\n now,\r\n );\r\n } else {\r\n db.prepare(INSERT).run(\r\n id,\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n now,\r\n now,\r\n );\r\n }\r\n return id;\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n return await this.execute(\"put_many\", connection, \"write\", (db) => {\r\n if (!this.dedup) return rows.map((r) => this.insertOne(db, r));\r\n // Identical images in one batch share a blob: count them and upsert each distinct checksum once.\r\n const now = isoNow();\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const [checksum, { row, n }] of groups) {\r\n db.prepare(D_UPSERT).run(\r\n checksum,\r\n row.data,\r\n row.mimeType,\r\n row.sizeBytes,\r\n row.width,\r\n row.height,\r\n n,\r\n now,\r\n );\r\n }\r\n return rows.map((r) => {\r\n const id = randomUUID();\r\n db.prepare(D_INSERT_REF).run(\r\n id,\r\n r.checksumSha256,\r\n r.originalFilename,\r\n now,\r\n now,\r\n );\r\n return id;\r\n });\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n let row: any;\r\n try {\r\n row = await this.execute(\"get\", connection, \"read\", (db) =>\r\n db.prepare(this.dedup ? D_SELECT_FULL : SELECT_FULL).get(norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? hydrateRow(row, norm, this.opts.objectStorage) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = [\r\n ...new Set(ids.filter(isValidId).map((i) => i.toLowerCase())),\r\n ];\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.execute(\"get_many\", connection, \"read\", (db) => {\r\n const out: any[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n out.push(\r\n ...db\r\n .prepare(\r\n this.dedup ? dSelectMany(part.length) : selectMany(part.length),\r\n )\r\n .all(...part),\r\n );\r\n }\r\n return out;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) =>\r\n hydrateRow(r, String(r.id), this.opts.objectStorage),\r\n );\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.execute(\"get_metadata\", connection, \"read\", (db) =>\r\n db\r\n .prepare(this.dedup ? D_SELECT_METADATA : SELECT_METADATA)\r\n .get(id.toLowerCase()),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.execute(\"get_metadata\", connection, \"read\", (db) =>\r\n db.prepare(this.dedup ? D_STREAM_INFO : STREAM_INFO).get(norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(norm, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n norm,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n\r\n const self = this;\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start;\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.execute(\"get_stream\", connection, \"read\", (db) =>\r\n db\r\n .prepare(self.dedup ? D_CHUNK : CHUNK)\r\n .get(offset + 1, length, norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(norm)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer | null = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(norm)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n const norm = id.toLowerCase();\r\n try {\r\n if (this.dedup) {\r\n return await this.execute(\"delete\", connection, \"write\", (db) => {\r\n const ref = db.prepare(D_DELETE_REF).get(norm);\r\n if (!ref) return false;\r\n const dec = db.prepare(D_DECREMENT).get(1, ref.checksum_sha256);\r\n if (dec && dec.ref_count <= 0)\r\n db.prepare(D_DELETE_EMPTY).run(ref.checksum_sha256);\r\n return true;\r\n });\r\n }\r\n const row = await this.execute(\"delete\", connection, \"write\", (db) =>\r\n db.prepare(DELETE_ONE).get(norm),\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObjects([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = [\r\n ...new Set(ids.filter(isValidId).map((i) => i.toLowerCase())),\r\n ];\r\n if (valid.length === 0) return [];\r\n try {\r\n if (this.dedup) {\r\n return await this.execute(\"delete_many\", connection, \"write\", (db) => {\r\n const deleted: { id: string; checksum_sha256: string }[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n deleted.push(...db.prepare(dDeleteRefs(part.length)).all(...part));\r\n }\r\n const counts = new Map<string, number>();\r\n for (const r of deleted)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n for (const [checksum, n] of counts) {\r\n const dec = db.prepare(D_DECREMENT).get(n, checksum);\r\n if (dec && dec.ref_count <= 0)\r\n db.prepare(D_DELETE_EMPTY).run(checksum);\r\n }\r\n return deleted.map((r) => String(r.id));\r\n });\r\n }\r\n const rows = await this.execute(\r\n \"delete_many\",\r\n connection,\r\n \"write\",\r\n (db) => {\r\n const out: any[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n out.push(...db.prepare(deleteMany(part.length)).all(...part));\r\n }\r\n return out;\r\n },\r\n );\r\n await this.cleanupObjects(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.execute(\r\n \"exists\",\r\n connection,\r\n \"read\",\r\n (db) =>\r\n db.prepare(this.dedup ? D_EXISTS : EXISTS).get(id.toLowerCase()) !==\r\n undefined,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- maintenance (CLI) -----------------------------------------------\r\n\r\n async getInfo(): Promise<StorageInfo | null> {\r\n try {\r\n return await this.execute(\"info\", undefined, \"read\", (db) => {\r\n const has = (t: string) =>\r\n db\r\n .prepare(\r\n \"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?\",\r\n )\r\n .get(t) !== undefined;\r\n if (\r\n !has(this.dedup ? \"zerobucket_image_refs\" : \"zerobucket_images\") ||\r\n (this.dedup && !has(\"zerobucket_blobs\"))\r\n )\r\n return null;\r\n const j = this.dedup ? D_FROM : \"FROM zerobucket_images\";\r\n const size = this.dedup ? \"b.size_bytes\" : \"size_bytes\";\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n const mime = this.dedup ? \"b.mime_type\" : \"mime_type\";\r\n const tot = db\r\n .prepare(\r\n `SELECT count(*) AS n, coalesce(sum(${size}), 0) AS bytes, min(${created}) AS oldest, max(${created}) AS newest ${j}`,\r\n )\r\n .get();\r\n const fmt = db\r\n .prepare(\r\n `SELECT ${mime} AS m, count(*) AS n, coalesce(sum(${size}), 0) AS bytes ${j} GROUP BY ${mime} ORDER BY count(*) DESC, ${mime}`,\r\n )\r\n .all();\r\n let onDisk: string | null = null;\r\n if (this.opts.path !== \":memory:\") {\r\n let total = statSync(this.opts.path).size;\r\n for (const suffix of [\"-wal\", \"-shm\"])\r\n if (existsSync(this.opts.path + suffix))\r\n total += statSync(this.opts.path + suffix).size;\r\n onDisk = prettySize(total);\r\n }\r\n const info: StorageInfo = {\r\n mode: this.dedup ? \"dedup\" : \"classic\",\r\n count: tot.n,\r\n totalBytes: Number(tot.bytes),\r\n oldest: tot.oldest,\r\n newest: tot.newest,\r\n onDisk,\r\n byFormat: fmt.map((r: any) => ({\r\n mimeType: r.m,\r\n count: r.n,\r\n bytes: Number(r.bytes),\r\n })),\r\n };\r\n if (this.dedup) {\r\n const b = db\r\n .prepare(\r\n \"SELECT count(*) AS n, coalesce(sum(size_bytes), 0) AS bytes FROM zerobucket_blobs\",\r\n )\r\n .get();\r\n info.dedup = { blobs: b.n, storedBytes: Number(b.bytes) };\r\n } else {\r\n const t = db\r\n .prepare(\r\n \"SELECT count(*) AS n, coalesce(sum(size_bytes), 0) AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'\",\r\n )\r\n .get();\r\n info.tiered = { count: t.n, bytes: Number(t.bytes) };\r\n }\r\n return info;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to read storage info\", exc);\r\n }\r\n }\r\n\r\n async listImages(o: ImageListOptions = {}): Promise<ImageListEntry[]> {\r\n const where: string[] = [];\r\n const params: unknown[] = [];\r\n if (o.minSize !== undefined)\r\n (where.push(`${this.dedup ? \"b.size_bytes\" : \"size_bytes\"} >= ?`),\r\n params.push(o.minSize));\r\n if (o.olderThanDays !== undefined) {\r\n // timestamps are ISO-8601 text in one fixed format, so string comparison is chronological\r\n const cutoff = new Date(Date.now() - o.olderThanDays * 86_400_000)\r\n .toISOString()\r\n .replace(\"Z\", \"000+00:00\");\r\n where.push(`${this.dedup ? \"r.created_at\" : \"created_at\"} <= ?`);\r\n params.push(cutoff);\r\n }\r\n if (o.onlyUntiered && !this.dedup) where.push(\"storage_backend = 'sqlite'\");\r\n const from = this.dedup ? D_FROM : \"FROM zerobucket_images\";\r\n const idCol = this.dedup ? \"r.id\" : \"id\";\r\n const tierCol = this.dedup\r\n ? \"0 AS tiered\"\r\n : \"(storage_backend = 'object_storage') AS tiered\";\r\n const order =\r\n o.sample !== undefined\r\n ? \"ORDER BY random()\"\r\n : `ORDER BY ${this.dedup ? \"r.created_at\" : \"created_at\"}, ${idCol}`;\r\n const lim = o.sample ?? o.limit;\r\n let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? \" WHERE \" + where.join(\" AND \") : \"\"} ${order}`;\r\n if (lim !== undefined) ((sql += \" LIMIT ?\"), params.push(lim));\r\n try {\r\n const rows = await this.execute(\"list_images\", undefined, \"read\", (db) =>\r\n db.prepare(sql).all(...params),\r\n );\r\n return rows.map((r: any) => ({\r\n id: String(r.id),\r\n tiered: Boolean(r.tiered),\r\n }));\r\n } catch (exc) {\r\n throw wrap(\"Failed to list images\", exc);\r\n }\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone (a failed object delete leaves a harmless orphan; the\r\n * reverse order could leave a row pointing at nothing). Skipped inside a caller-owned transaction, since the\r\n * commit outcome is unknown. Failures never fail the delete; they are reported through onOperation.\r\n */\r\n private async cleanupObjects(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes into object storage. null = no such image, false = already tiered, true = tiered now.\r\n *\r\n * SAFETY (same guarantee as Postgres): the upload happens INSIDE a BEGIN IMMEDIATE transaction; if it fails the\r\n * transaction rolls back and the row is untouched.\r\n *\r\n * TRADEOFF (coarser than Postgres): SQLite has no row locks, so BEGIN IMMEDIATE locks the WHOLE DATABASE for the\r\n * upload. Other processes' writes wait; this process's writes queue behind an async lock; reads are not blocked.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup)\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ sqlite, objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (connection) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() does not accept `connection` on SQLite: it needs its own dedicated connection for the BEGIN IMMEDIATE transaction.\",\r\n );\r\n }\r\n if (this.opts.path === \":memory:\") {\r\n throw new StorageError(\r\n \"tierToObjectStorage() needs a database file: an in-memory SQLite database cannot be shared with its dedicated tiering connection.\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n const start = performance.now();\r\n let retries = 0;\r\n try {\r\n await this.ready();\r\n const result = await this.writeLock.run(async () => {\r\n const tdb = new this.Database!(this.opts.path, {\r\n timeout: ATTEMPT_BUSY_MS,\r\n });\r\n try {\r\n await this.retrying(\r\n () => tdb.exec(\"BEGIN IMMEDIATE\"),\r\n (n) => (retries = n),\r\n );\r\n try {\r\n const row = tdb.prepare(SELECT_FOR_TIERING).get(norm);\r\n if (!row) {\r\n tdb.exec(\"ROLLBACK\");\r\n return null;\r\n }\r\n if (row.storage_backend !== \"sqlite\") {\r\n tdb.exec(\"ROLLBACK\");\r\n return false;\r\n }\r\n await os.upload(norm, row.data as Buffer, {\r\n mimeType: row.mime_type,\r\n });\r\n tdb\r\n .prepare(UPDATE_AFTER_TIERING)\r\n .run(os.bucket, norm, isoNow(), norm);\r\n tdb.exec(\"COMMIT\");\r\n return true;\r\n } catch (exc) {\r\n try {\r\n tdb.exec(\"ROLLBACK\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n } finally {\r\n tdb.close();\r\n }\r\n });\r\n this.emit(\"tier_to_object_storage\", start, true, null, retries);\r\n return result;\r\n } catch (exc) {\r\n this.emit(\"tier_to_object_storage\", start, false, msg(exc), retries);\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n","/**\r\n * MySQL / MariaDB storage adapter (classic mode, streaming, object-storage tiering).\r\n *\r\n * Uses the EXACT schema the Python package's MySQLBackend creates (CHAR(36) ids, LONGBLOB data,\r\n * naive-UTC DATETIME(6) timestamps, storage_backend = 'mysql', the same CHECK constraint), so a Node\r\n * app and a Python app can share one database and read each other's rows.\r\n *\r\n * Decisions that differ from the Postgres adapter, each verified against a real MariaDB 10.11:\r\n *\r\n * - NO `RETURNING`. MySQL 8.0 has none (MariaDB 10.5+ does, but one code path must serve both).\r\n * delete()/deleteMany() therefore run `SELECT ... FOR UPDATE` and then `DELETE` inside ONE\r\n * transaction, which also tells us which rows were tiered (so their objects can be cleaned up).\r\n * - ids are generated by the application (`crypto.randomUUID()`), exactly like SQLite and Python.\r\n * - Timestamps are written as pre-formatted UTC strings, never as JS Dates, so the result does not\r\n * depend on the server's or the process's timezone setting.\r\n * - Blobs travel through `execute()` (binary protocol, raw bytes). `query()` would inline a blob\r\n * as a hex literal (2x the size, and the whole statement counts against max_allowed_packet).\r\n * Everything else uses `query()`: with `execute()`, JS numbers are sent as DOUBLE, which MySQL\r\n * 8.0.22+ rejects in places like LIMIT and offsets, and SUBSTRING() offsets are such a place.\r\n * - mysql2's pool has no acquire timeout, so `poolTimeoutMs` is implemented here. A waiter that\r\n * times out is removed in effect: if its connection arrives late it is released straight back.\r\n * - Retry re-runs the WHOLE unit of work on a fresh connection after a full ROLLBACK, never a single\r\n * statement (the Python package hit exactly that \"half-applied put\" hazard with DBUtils).\r\n * Connection loss, lock-wait timeout (1205) and deadlock (1213) are retried; the pool-timeout\r\n * error is NOT (retrying would multiply the wait).\r\n * - A caller-supplied `connection` (your own mysql2 connection) is never committed, rolled back or\r\n * retried. For multi-statement work on it we first probe whether a transaction is open\r\n * (SAVEPOINT + ROLLBACK TO fails with 1305 when none is): inside one we use a SAVEPOINT so a\r\n * failure undoes only our work; outside one (autocommit) we open and commit our own.\r\n * - Migration runs under `GET_LOCK()` so N processes starting at once do not race on the DDL, and it\r\n * issues NO DDL when the schema is already current: even a no-op ALTER TABLE needs an exclusive\r\n * metadata lock, which queues behind every open transaction and blocks all later queries on the table.\r\n * (The Python package always runs `ALTER TABLE ... MODIFY data` at startup; both additions are\r\n * purely additive and work on MySQL and MariaDB.)\r\n * - Dedup mode (`dedup: true`) uses the Python package's two tables (zerobucket_blobs: one row per distinct\r\n * content with a ref_count; zerobucket_image_refs: one row per image). A batch groups identical images,\r\n * upserts each distinct blob ONCE in sorted checksum order and decrements in sorted order too: a global\r\n * lock order, so two overlapping batches can never deadlock on each other's blob rows. Without `RETURNING`\r\n * the decrement is `UPDATE ... ref_count = ref_count - n` (which takes the row lock) followed by a point\r\n * `DELETE ... WHERE ref_count <= 0`; the foreign key makes deleting a still-referenced blob impossible.\r\n * - Batch deletes lock with one `SELECT ... FOR UPDATE` and then use POINT statements, never `DELETE ... IN (...)`.\r\n * On MariaDB 10.11 that statement is planned as a full scan on a small table (EXPLAIN type=ALL) while the ordered\r\n * SELECT gets a range plan; a scan locks every row it passes, so overlapping batches deadlocked (measured: 126 per\r\n * 300 concurrent transactions with IN-list deletes, 0 with point deletes). MySQL 8.0.46 plans the same delete as a\r\n * range on just the listed keys and did NOT deadlock in the same test; point deletes do not depend on the plan, so\r\n * they are used on both.\r\n * Like the other adapters, dedup cannot be combined with object storage (a blob may be shared).\r\n */\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n MysqlPoolConnectionLike,\r\n MysqlPoolLike,\r\n MysqlQueryable,\r\n OperationEvent,\r\n PreparedRow,\r\n} from \"../types.js\";\r\nimport type {\r\n CallOptions,\r\n ImageListEntry,\r\n ImageListOptions,\r\n StorageBackend,\r\n StorageInfo,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\nimport {\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n prettySize,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\n\r\n// ---- schema (identical to the Python package: classic table, then the additive tiering migration) ----\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id CHAR(36) PRIMARY KEY,\r\n data LONGBLOB NOT NULL,\r\n mime_type VARCHAR(255) NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INT NOT NULL,\r\n width INT,\r\n height INT,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n created_at DATETIME(6) NOT NULL,\r\n updated_at DATETIME(6) NOT NULL,\r\n INDEX idx_zerobucket_checksum (checksum_sha256),\r\n INDEX idx_zerobucket_created_at (created_at)\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;\r\n\r\nconst TIERING_CHECK = \"chk_zerobucket_storage\";\r\nconst MIGRATE_LOCK = \"zerobucket_migrate\";\r\n\r\n// ---- SQL ---------------------------------------------------------------------------------------\r\n\r\nconst COLS = `id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, storage_backend, object_storage_bucket, object_storage_key`;\r\nconst INSERT = `INSERT INTO zerobucket_images (id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\r\nconst SELECT_FULL = `SELECT ${COLS} FROM zerobucket_images WHERE id = ?`;\r\nconst SELECT_MANY = `SELECT ${COLS} FROM zerobucket_images WHERE id IN (?)`;\r\nconst SELECT_METADATA = `SELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256 FROM zerobucket_images WHERE id = ?`;\r\nconst STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = ?`;\r\n// SUBSTRING() is 1-indexed and clamps `length` at the end of the value.\r\nconst CHUNK = `SELECT SUBSTRING(data FROM ? FOR ?) AS chunk FROM zerobucket_images WHERE id = ?`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = ?`;\r\nconst LOCK_ONE = `SELECT storage_backend, object_storage_key FROM zerobucket_images WHERE id = ? FOR UPDATE`;\r\nconst DELETE_ONE = `DELETE FROM zerobucket_images WHERE id = ?`;\r\n// LOCKING RULE for every batch write here: lock the rows with a set-based SELECT ... FOR UPDATE, then change\r\n// them with POINT statements (`WHERE id = ?`), always in sorted order. Never `DELETE ... WHERE id IN (...)`:\r\n// MariaDB 10.11 plans that as a full scan on a small table (EXPLAIN type=ALL), and a scan under REPEATABLE READ\r\n// locks EVERY row it passes, including rows another batch already holds, so two overlapping batches deadlock.\r\n// Measured on MariaDB 10.11: 126 deadlocks in 300 concurrent transactions with the IN-list delete, 0 with point\r\n// deletes (also 0 with a concurrent writer). MySQL 8.0.46 plans the same delete as a range on the listed keys and\r\n// did not deadlock in the same test; point deletes do not depend on the plan, so they are used on both.\r\n// FORCE INDEX(PRIMARY) on the SELECT makes it probe exactly the listed keys instead of scanning (record locks on\r\n// existing rows only, no gap locks), and ORDER BY id gives every writer the same lock order.\r\nconst LOCK_MANY = `SELECT id, storage_backend, object_storage_key FROM zerobucket_images FORCE INDEX (PRIMARY) WHERE id IN (?) ORDER BY id FOR UPDATE`;\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = ? FOR UPDATE`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = ?, object_storage_key = ?, updated_at = ? WHERE id = ?`;\r\n\r\n/** Value of `storage_backend` for rows whose bytes live in the database (same as the Python package). */\r\nconst LOCAL = \"mysql\";\r\n\r\n// ---- dedup mode: content-addressed blobs + per-image refs (same schema as the Python package) ----\r\n\r\nconst DEDUP_BLOBS_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 CHAR(64) PRIMARY KEY,\r\n data LONGBLOB NOT NULL,\r\n mime_type VARCHAR(255) NOT NULL,\r\n size_bytes INT NOT NULL,\r\n width INT,\r\n height INT,\r\n ref_count INT NOT NULL DEFAULT 0,\r\n created_at DATETIME(6) NOT NULL\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;\r\n\r\nconst DEDUP_REFS_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id CHAR(36) PRIMARY KEY,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n original_filename TEXT,\r\n created_at DATETIME(6) NOT NULL,\r\n updated_at DATETIME(6) NOT NULL,\r\n INDEX idx_zerobucket_image_refs_checksum (checksum_sha256),\r\n INDEX idx_zerobucket_image_refs_created_at (created_at),\r\n FOREIGN KEY (checksum_sha256) REFERENCES zerobucket_blobs(checksum_sha256)\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;\r\n\r\n/**\r\n * Insert a blob with `n` references, or add `n` to an existing blob's count. InnoDB makes the increment\r\n * atomic at the row level, so 20 concurrent puts of the same bytes give ref_count == 20. `n` is a\r\n * count this module computed (never user input); it is inlined because prepared statements send JS\r\n * numbers as DOUBLE, which makes `ref_count + ?` a floating-point expression.\r\n */\r\nfunction upsertBlobSql(n: number): string {\r\n if (!Number.isInteger(n) || n < 1) throw new RangeError(\"invalid ref count\");\r\n return `INSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ${n}, ?) ON DUPLICATE KEY UPDATE ref_count = ref_count + ${n}`;\r\n}\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`;\r\n// Aliased to the classic column names so one row mapper serves both modes.\r\nconst D_COLS = `r.id, b.data, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256, '${LOCAL}' AS storage_backend, NULL AS object_storage_bucket, NULL AS object_storage_key`;\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_LOCK_REF = `SELECT checksum_sha256 FROM zerobucket_image_refs WHERE id = ? FOR UPDATE`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = ?`;\r\nconst D_LOCK_REFS = `SELECT id, checksum_sha256 FROM zerobucket_image_refs FORCE INDEX (PRIMARY) WHERE id IN (?) ORDER BY id FOR UPDATE`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - ? WHERE checksum_sha256 = ?`;\r\n// Point delete, never an IN list (see the LOCKING RULE above); a no-op unless the count reached zero.\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ? AND ref_count <= 0`;\r\n\r\ninterface SqlSet {\r\n selectFull: string;\r\n selectMany: string;\r\n selectMetadata: string;\r\n streamInfo: string;\r\n chunk: string;\r\n exists: string;\r\n}\r\nconst CLASSIC_SQL: SqlSet = {\r\n selectFull: SELECT_FULL,\r\n selectMany: SELECT_MANY,\r\n selectMetadata: SELECT_METADATA,\r\n streamInfo: STREAM_INFO,\r\n chunk: CHUNK,\r\n exists: EXISTS,\r\n};\r\nconst DEDUP_SQL: SqlSet = {\r\n selectFull: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ?`,\r\n selectMany: `SELECT ${D_COLS} ${D_FROM} WHERE r.id IN (?)`,\r\n selectMetadata: `SELECT r.id, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256 ${D_FROM} WHERE r.id = ?`,\r\n streamInfo: `SELECT b.size_bytes, '${LOCAL}' AS storage_backend, NULL AS object_storage_key ${D_FROM} WHERE r.id = ?`,\r\n chunk: `SELECT SUBSTRING(b.data FROM ? FOR ?) AS chunk ${D_FROM} WHERE r.id = ?`,\r\n exists: `SELECT 1 FROM zerobucket_image_refs WHERE id = ?`,\r\n};\r\n\r\n// ---- error classification ----------------------------------------------------------------------\r\n\r\nconst RETRYABLE_ERRNOS = new Set([\r\n 1205, // ER_LOCK_WAIT_TIMEOUT\r\n 1213, // ER_LOCK_DEADLOCK\r\n 1040, // ER_CON_COUNT_ERROR (too many connections)\r\n 1053, // ER_SERVER_SHUTDOWN\r\n 1927, // ER_CONNECTION_KILLED (MariaDB)\r\n 2002, // CR_CONNECTION_ERROR\r\n 2003, // CR_CONN_HOST_ERROR\r\n 2006, // CR_SERVER_GONE_ERROR\r\n 2013, // CR_SERVER_LOST\r\n]);\r\nconst CONNECTION_CODES = new Set([\r\n \"PROTOCOL_CONNECTION_LOST\",\r\n \"PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR\",\r\n \"PROTOCOL_ENQUEUE_AFTER_QUIT\",\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"ETIMEDOUT\",\r\n \"EPIPE\",\r\n \"ECONNABORTED\",\r\n]);\r\n\r\ninterface MysqlError {\r\n code?: unknown;\r\n errno?: unknown;\r\n fatal?: unknown;\r\n}\r\n\r\nfunction errnoOf(exc: unknown): number | undefined {\r\n const n = (exc as MysqlError | null)?.errno;\r\n return typeof n === \"number\" ? n : undefined;\r\n}\r\n\r\n/** Thrown when waiting for a pool slot times out. Not retryable: retrying would just wait again. */\r\nclass PoolTimeoutError extends StorageError {}\r\n\r\n/** Transient (worth retrying) vs. permanent (fails identically every time). */\r\nexport function isRetryable(exc: unknown): boolean {\r\n if (!(exc instanceof Error) || exc instanceof PoolTimeoutError) return false;\r\n const e = exc as MysqlError;\r\n const n = errnoOf(exc);\r\n if (n !== undefined && RETRYABLE_ERRNOS.has(n)) return true;\r\n if (typeof e.code === \"string\") {\r\n if (CONNECTION_CODES.has(e.code)) return true;\r\n if (\r\n e.code === \"ER_LOCK_WAIT_TIMEOUT\" ||\r\n e.code === \"ER_LOCK_DEADLOCK\" ||\r\n e.code === \"ER_CON_COUNT_ERROR\" ||\r\n e.code === \"ER_SERVER_SHUTDOWN\" ||\r\n e.code === \"ER_CONNECTION_KILLED\"\r\n )\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\n/** Errors after which the pooled connection must be destroyed, not returned to the pool. */\r\nfunction isConnectionLevel(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const e = exc as MysqlError;\r\n if (e.fatal === true) return true;\r\n if (typeof e.code === \"string\" && CONNECTION_CODES.has(e.code)) return true;\r\n const n = errnoOf(exc);\r\n return n === 2006 || n === 2013 || n === 1927 || n === 1053;\r\n}\r\n\r\nconst MAX_BACKOFF_MS = 2000;\r\n\r\nexport function backoffDelayMs(attempt: number, baseDelayMs: number): number {\r\n const exponential = baseDelayMs * 2 ** (attempt - 1);\r\n const jitter = Math.random() * baseDelayMs;\r\n return Math.min(exponential + jitter, MAX_BACKOFF_MS);\r\n}\r\n\r\n/** Turn the one error users hit in practice into an actionable message; everything else passes through wrap(). */\r\nfunction friendly(prefix: string, exc: unknown): StorageError {\r\n if (errnoOf(exc) === 1153) {\r\n return new StorageError(\r\n `${prefix}: the image is larger than the server's max_allowed_packet. Raise it (for example \\`SET GLOBAL max_allowed_packet = 67108864\\`, ` +\r\n `and set it in my.cnf to make it permanent), or lower \\`maxBytes\\`.`,\r\n { cause: exc },\r\n );\r\n }\r\n return wrap(prefix, exc);\r\n}\r\n\r\n/** Naive UTC `YYYY-MM-DD HH:MM:SS.ffffff`, the format of the Python package's DATETIME(6) values. */\r\nfunction mysqlNow(): string {\r\n return `${new Date().toISOString().replace(\"T\", \" \").replace(\"Z\", \"\")}000`;\r\n}\r\n\r\nfunction normalizeUrl(url: string): string {\r\n return url.replace(/^mariadb:\\/\\//i, \"mysql://\");\r\n}\r\n\r\nfunction isMysqlConnection(c: Connection): c is MysqlQueryable {\r\n const m = c as Partial<MysqlQueryable>;\r\n return typeof m.query === \"function\" && typeof m.execute === \"function\";\r\n}\r\n\r\nexport interface MySQLBackendOptions {\r\n /** `mysql://user:password@host:3306/database` (`mariadb://` is accepted too). Required unless `pool` is given. */\r\n connectionString?: string;\r\n /** Bring your own `mysql2/promise` Pool (not closed by `close()`). Pool sizing options are then ignored. */\r\n pool?: MysqlPoolLike;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Retries for transient errors on pooled calls. Default 3; 0 disables. */\r\n maxRetries?: number;\r\n /** Base for exponential backoff with jitter (capped at 2s). Default 100. */\r\n retryBaseDelayMs?: number;\r\n /** Accepted for parity with the other backends. mysql2 opens connections on demand, so there is no minimum to hold. */\r\n poolMinSize?: number;\r\n /** Maximum simultaneous connections. Default 5. */\r\n poolMaxSize?: number;\r\n /** Max wait for a pooled connection (also the TCP connect timeout). Default 10000. */\r\n poolTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n /** Enables transparent reads of tiered rows and `tierToObjectStorage()`. */\r\n objectStorage?: ObjectStorageLike;\r\n /**\r\n * Content-addressed storage: identical bytes are stored once and shared by every image that has them\r\n * (reference counted). Same tables as the Python package's dedup mode. Cannot be combined with\r\n * `objectStorage` (a blob may be shared by many images).\r\n */\r\n dedup?: boolean;\r\n}\r\n\r\ntype Work<T> = (q: MysqlQueryable) => Promise<T>;\r\n\r\nexport class MySQLBackend implements StorageBackend {\r\n private pool: MysqlPoolLike | undefined;\r\n private readonly ownsPool: boolean;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly maxRetries: number;\r\n private readonly retryBaseDelayMs: number;\r\n private readonly poolTimeoutMs: number;\r\n private readonly autoMigrate: boolean;\r\n private readonly dedup: boolean;\r\n private readonly sql: SqlSet;\r\n\r\n constructor(private readonly opts: MySQLBackendOptions) {\r\n if (!opts.connectionString && !opts.pool) {\r\n throw new TypeError(\r\n \"MySQLBackend requires either `connectionString` or `pool`\",\r\n );\r\n }\r\n this.dedup = opts.dedup ?? false;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n this.sql = this.dedup ? DEDUP_SQL : CLASSIC_SQL;\r\n this.ownsPool = !opts.pool;\r\n this.pool = opts.pool;\r\n this.maxRetries = opts.maxRetries ?? 3;\r\n this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 100;\r\n this.poolTimeoutMs = opts.poolTimeoutMs ?? 10_000;\r\n this.autoMigrate = opts.autoMigrate ?? true;\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined; // allow a later attempt (e.g. the database was briefly down)\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n if (!this.pool) {\r\n let mod: {\r\n createPool(config: Record<string, unknown>): MysqlPoolLike;\r\n default?: {\r\n createPool(config: Record<string, unknown>): MysqlPoolLike;\r\n };\r\n };\r\n try {\r\n mod = (await import(\"mysql2/promise\")) as unknown as typeof mod;\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"MySQLBackend requires the 'mysql2' package. Install it with: npm install mysql2\",\r\n { cause },\r\n );\r\n }\r\n const createPool = (mod.default ?? mod).createPool;\r\n this.pool = createPool({\r\n uri: normalizeUrl(this.opts.connectionString!),\r\n connectionLimit: this.opts.poolMaxSize ?? 5,\r\n waitForConnections: true,\r\n queueLimit: 0,\r\n connectTimeout: this.poolTimeoutMs,\r\n charset: \"utf8mb4\",\r\n timezone: \"Z\",\r\n enableKeepAlive: true,\r\n });\r\n }\r\n try {\r\n if (this.autoMigrate) {\r\n await this.runPooled(\"migrate\", (q) => this.migrate(q), false);\r\n } else {\r\n await this.runPooled(\"ping\", (q) => q.query(\"SELECT 1\"), false);\r\n }\r\n } catch (exc) {\r\n if (this.ownsPool) {\r\n await this.pool.end().catch(() => {});\r\n this.pool = undefined;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Could not connect to MySQL/MariaDB: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Create the schema if missing, under a server-side named lock so concurrent first callers (in this\r\n * or any other process) run the DDL one at a time. Every step checks before it changes anything, so\r\n * re-running is a no-op.\r\n */\r\n private async migrate(q: MysqlQueryable): Promise<void> {\r\n const locked = (\r\n (await q.query(\"SELECT GET_LOCK(?, 30) AS got\", [MIGRATE_LOCK]))[0] as {\r\n got: number | null;\r\n }[]\r\n )[0]?.got;\r\n if (locked !== 1) {\r\n throw new StorageError(\r\n \"Timed out waiting for another process to finish migrating the ZeroBucket schema.\",\r\n );\r\n }\r\n try {\r\n if (this.dedup) {\r\n await this.migrateDedup(q);\r\n return;\r\n }\r\n // DDL only when something is actually missing. Even a no-op ALTER TABLE needs an EXCLUSIVE metadata\r\n // lock, which waits behind every open transaction on the table, and while it waits, every other\r\n // query on that table queues behind IT. Starting an app instance during a long transaction must\r\n // not be able to stall all traffic, so a current schema issues no DDL at all.\r\n const tableExists =\r\n (\r\n (\r\n await q.query(\r\n \"SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images'\",\r\n )\r\n )[0] as unknown[]\r\n ).length > 0;\r\n if (!tableExists) await q.query(SCHEMA);\r\n const cols = new Map(\r\n (\r\n (\r\n await q.query(\r\n \"SELECT COLUMN_NAME AS c, IS_NULLABLE AS nullable FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images'\",\r\n )\r\n )[0] as { c: string; nullable: string }[]\r\n ).map((r) => [r.c, r.nullable]),\r\n );\r\n if (!cols.has(\"storage_backend\"))\r\n await q.query(\r\n `ALTER TABLE zerobucket_images ADD COLUMN storage_backend VARCHAR(20) NOT NULL DEFAULT '${LOCAL}'`,\r\n );\r\n if (!cols.has(\"object_storage_bucket\"))\r\n await q.query(\r\n \"ALTER TABLE zerobucket_images ADD COLUMN object_storage_bucket VARCHAR(255)\",\r\n );\r\n if (!cols.has(\"object_storage_key\"))\r\n await q.query(\r\n \"ALTER TABLE zerobucket_images ADD COLUMN object_storage_key VARCHAR(512)\",\r\n );\r\n if (cols.get(\"data\") === \"NO\")\r\n await q.query(\r\n \"ALTER TABLE zerobucket_images MODIFY data LONGBLOB NULL\",\r\n );\r\n const hasCheck =\r\n (\r\n (\r\n await q.query(\r\n \"SELECT 1 FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images' AND CONSTRAINT_NAME = ?\",\r\n [TIERING_CHECK],\r\n )\r\n )[0] as unknown[]\r\n ).length > 0;\r\n if (!hasCheck) {\r\n await q.query(\r\n `ALTER TABLE zerobucket_images ADD CONSTRAINT ${TIERING_CHECK} CHECK (` +\r\n `(storage_backend = '${LOCAL}' AND data IS NOT NULL AND object_storage_bucket IS NULL AND object_storage_key IS NULL) OR ` +\r\n `(storage_backend = 'object_storage' AND data IS NULL AND object_storage_bucket IS NOT NULL AND object_storage_key IS NOT NULL))`,\r\n );\r\n }\r\n } catch (exc) {\r\n throw new StorageError(`Migration failed: ${msg(exc)}`, { cause: exc });\r\n } finally {\r\n await q.query(\"SELECT RELEASE_LOCK(?)\", [MIGRATE_LOCK]).catch(() => {});\r\n }\r\n }\r\n\r\n /** Dedup tables, created only if missing (same no-needless-DDL rule as the classic table). */\r\n private async migrateDedup(q: MysqlQueryable): Promise<void> {\r\n const have = new Set(\r\n (\r\n (\r\n await q.query(\r\n \"SELECT TABLE_NAME AS t FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('zerobucket_blobs', 'zerobucket_image_refs')\",\r\n )\r\n )[0] as { t: string }[]\r\n ).map((r) => r.t),\r\n );\r\n if (!have.has(\"zerobucket_blobs\")) await q.query(DEDUP_BLOBS_SCHEMA); // the refs table references it\r\n if (!have.has(\"zerobucket_image_refs\")) await q.query(DEDUP_REFS_SCHEMA);\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const pool = this.pool;\r\n this.pool = undefined;\r\n this.readyPromise = undefined;\r\n if (pool && this.ownsPool) await pool.end();\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /** Wait for a pool slot, but never longer than poolTimeoutMs. */\r\n private acquire(): Promise<MysqlPoolConnectionLike> {\r\n const pool = this.pool;\r\n if (this.closed || !pool)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n return new Promise((resolve, reject) => {\r\n let settled = false;\r\n const timer = setTimeout(() => {\r\n settled = true;\r\n reject(\r\n new PoolTimeoutError(\r\n `Timed out after ${this.poolTimeoutMs}ms waiting for a MySQL/MariaDB connection ` +\r\n `(poolMaxSize=${this.opts.poolMaxSize ?? 5}; all connections are in use).`,\r\n ),\r\n );\r\n }, this.poolTimeoutMs);\r\n pool.getConnection().then(\r\n (conn) => {\r\n if (settled) {\r\n conn.release(); // arrived after we gave up: hand it straight back\r\n return;\r\n }\r\n settled = true;\r\n clearTimeout(timer);\r\n resolve(conn);\r\n },\r\n (exc) => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(timer);\r\n reject(exc);\r\n },\r\n );\r\n });\r\n }\r\n\r\n /**\r\n * Run `work` on a pooled connection. `tx` wraps it in a transaction. Transient failures retry the\r\n * WHOLE closure on a fresh connection after a full rollback.\r\n */\r\n private async runPooled<T>(\r\n operation: string,\r\n work: Work<T>,\r\n tx: boolean,\r\n ): Promise<T> {\r\n const start = performance.now();\r\n let attempt = 0;\r\n for (;;) {\r\n let conn: MysqlPoolConnectionLike | undefined;\r\n let destroy = false;\r\n try {\r\n conn = await this.acquire();\r\n if (tx) await conn.beginTransaction();\r\n const result = await work(conn);\r\n if (tx) await conn.commit();\r\n this.emit(operation, start, true, null, attempt);\r\n return result;\r\n } catch (exc) {\r\n if (conn && tx) {\r\n try {\r\n await conn.rollback();\r\n } catch {\r\n destroy = true; // cannot trust a connection that cannot roll back\r\n }\r\n }\r\n if (isConnectionLevel(exc)) destroy = true;\r\n attempt += 1;\r\n if (attempt > this.maxRetries || !isRetryable(exc)) {\r\n this.emit(operation, start, false, msg(exc), attempt - 1);\r\n throw exc;\r\n }\r\n } finally {\r\n if (conn) {\r\n if (destroy) conn.destroy();\r\n else conn.release();\r\n }\r\n }\r\n await sleep(backoffDelayMs(attempt, this.retryBaseDelayMs));\r\n }\r\n }\r\n\r\n /**\r\n * Run `work` and return its result, emitting exactly one OperationEvent.\r\n *\r\n * connection given -> run once on it; no retry, and no commit/rollback of the caller's transaction.\r\n * connection absent -> pooled, retried (see runPooled).\r\n */\r\n private async run<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n work: Work<T>,\r\n { tx = false }: { tx?: boolean } = {},\r\n ): Promise<T> {\r\n if (connection) {\r\n if (!isMysqlConnection(connection)) {\r\n throw new TypeError(\r\n \"MySQLBackend expects a mysql2/promise connection as `connection` (an object with query() and execute()).\",\r\n );\r\n }\r\n const start = performance.now();\r\n try {\r\n const result = tx\r\n ? await this.atomic(connection, work)\r\n : await work(connection);\r\n this.emit(operation, start, true, null, 0);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), 0);\r\n throw exc;\r\n }\r\n }\r\n await this.ready();\r\n return this.runPooled(operation, work, tx);\r\n }\r\n\r\n /**\r\n * Make multi-statement work all-or-nothing on a CALLER-supplied connection.\r\n *\r\n * Is a transaction open? SAVEPOINT followed by ROLLBACK TO SAVEPOINT succeeds only inside one (in\r\n * autocommit mode the savepoint is gone before the next statement and the rollback fails with 1305).\r\n * - Open -> reuse that SAVEPOINT: a failure undoes only our work; success joins the caller's transaction.\r\n * - None -> START TRANSACTION and COMMIT our own, so a failure can never leave half-applied state.\r\n */\r\n private async atomic<T>(conn: MysqlQueryable, work: Work<T>): Promise<T> {\r\n let inTransaction: boolean;\r\n await conn.query(\"SAVEPOINT zerobucket_atomic\");\r\n try {\r\n await conn.query(\"ROLLBACK TO SAVEPOINT zerobucket_atomic\");\r\n inTransaction = true;\r\n } catch (exc) {\r\n if (errnoOf(exc) !== 1305) throw exc; // ER_SP_DOES_NOT_EXIST: no open transaction\r\n inTransaction = false;\r\n await conn.query(\"START TRANSACTION\");\r\n }\r\n try {\r\n const result = await work(conn);\r\n await conn.query(\r\n inTransaction ? \"RELEASE SAVEPOINT zerobucket_atomic\" : \"COMMIT\",\r\n );\r\n return result;\r\n } catch (exc) {\r\n try {\r\n await conn.query(\r\n inTransaction\r\n ? \"ROLLBACK TO SAVEPOINT zerobucket_atomic\"\r\n : \"ROLLBACK\",\r\n );\r\n if (inTransaction)\r\n await conn.query(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n const id = randomUUID();\r\n if (this.dedup) {\r\n await this.run(\r\n \"put\",\r\n connection,\r\n async (q) => {\r\n const now = mysqlNow();\r\n await q.execute(upsertBlobSql(1), blobParams(row, now));\r\n await q.execute(D_INSERT_REF, refParams(id, row, now));\r\n },\r\n { tx: true },\r\n );\r\n return id;\r\n }\r\n await this.run(\"put\", connection, async (q) => {\r\n await q.execute(INSERT, rowParams(id, row, mysqlNow()));\r\n });\r\n return id;\r\n } catch (exc) {\r\n throw friendly(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n // Ids are minted before the transaction, so a retry after a full rollback re-inserts the same\r\n // ids and can never create duplicates. One INSERT at a time keeps every packet one image big.\r\n const ids = rows.map(() => randomUUID());\r\n await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n const now = mysqlNow();\r\n if (this.dedup) {\r\n // Identical images in one batch share a blob: count them, upsert each distinct checksum ONCE\r\n // in sorted order (a global lock order, so overlapping batches cannot deadlock), then create\r\n // the refs in input order.\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const g = groups.get(checksum)!;\r\n await q.execute(upsertBlobSql(g.n), blobParams(g.row, now));\r\n }\r\n for (let i = 0; i < rows.length; i++)\r\n await q.execute(D_INSERT_REF, refParams(ids[i]!, rows[i]!, now));\r\n return;\r\n }\r\n for (let i = 0; i < rows.length; i++)\r\n await q.execute(INSERT, rowParams(ids[i]!, rows[i]!, now));\r\n },\r\n { tx: true },\r\n );\r\n return ids;\r\n } catch (exc) {\r\n throw friendly(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectFull, [id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? this.hydrate(row, id) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.run(\r\n \"get_many\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMany, [valid]))[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) => this.hydrate(r, String(r.id)));\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMetadata, [id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.streamInfo, [id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(id, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n id,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n const self = this;\r\n\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start; // 0-based\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.run(\r\n \"get_stream\",\r\n connection,\r\n async (q) =>\r\n (await q.query(self.sql.chunk, [offset + 1, length, id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer | null = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n if (this.dedup) return this.deleteDedup(id, connection);\r\n try {\r\n // No RETURNING: lock the row, learn where its bytes live, delete it, all in one transaction.\r\n const row = await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const found = (await q.query(LOCK_ONE, [id]))[0][0];\r\n if (!found) return undefined;\r\n await q.query(DELETE_ONE, [id]);\r\n return found;\r\n },\r\n { tx: true },\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObject([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = [...new Set(ids.filter(isValidId))];\r\n if (valid.length === 0) return [];\r\n if (this.dedup) return this.deleteManyDedup(valid, connection);\r\n try {\r\n const rows: any[] = await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const found = (await q.query(LOCK_MANY, [valid]))[0] as any[];\r\n for (const r of found) await q.query(DELETE_ONE, [r.id]); // point deletes, in sorted order\r\n return found;\r\n },\r\n { tx: true },\r\n );\r\n await this.cleanupObject(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.run(\r\n \"exists\",\r\n connection,\r\n async (q) => (await q.query(this.sql.exists, [id]))[0].length > 0,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- maintenance (CLI) -----------------------------------------------\r\n\r\n /** Storage statistics, or null if the tables do not exist yet (nothing is created). */\r\n async getInfo(): Promise<StorageInfo | null> {\r\n try {\r\n return await this.run(\"info\", undefined, async (q) => {\r\n const need = this.dedup\r\n ? [\"zerobucket_image_refs\", \"zerobucket_blobs\"]\r\n : [\"zerobucket_images\"];\r\n const present = new Set(\r\n (\r\n (\r\n await q.query(\r\n \"SELECT TABLE_NAME AS t FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?)\",\r\n [need],\r\n )\r\n )[0] as { t: string }[]\r\n ).map((r) => r.t),\r\n );\r\n if (!need.every((t) => present.has(t))) return null;\r\n const j = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const size = this.dedup ? \"b.size_bytes\" : \"size_bytes\";\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n const mime = this.dedup ? \"b.mime_type\" : \"mime_type\";\r\n const tot = (\r\n await q.query(\r\n `SELECT COUNT(*) AS n, COALESCE(SUM(${size}), 0) AS bytes, CAST(MIN(${created}) AS CHAR) AS oldest, CAST(MAX(${created}) AS CHAR) AS newest ${j}`,\r\n )\r\n )[0][0];\r\n // information_schema statistics are an estimate (MySQL 8 caches them for up to 24h), hence \"approximate\".\r\n const disk = (\r\n await q.query(\r\n \"SELECT COALESCE(SUM(DATA_LENGTH + INDEX_LENGTH), 0) AS s FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?)\",\r\n [need],\r\n )\r\n )[0][0];\r\n const fmt = (\r\n await q.query(\r\n `SELECT ${mime} AS m, COUNT(*) AS n, COALESCE(SUM(${size}), 0) AS bytes ${j} GROUP BY ${mime} ORDER BY COUNT(*) DESC, ${mime}`,\r\n )\r\n )[0] as { m: string; n: number; bytes: string }[];\r\n // created_at is naive UTC; print it the way the Postgres output does (ISO with an explicit +00:00).\r\n const utc = (v: string | null) => (v ? `${v}+00:00` : null);\r\n const info: StorageInfo = {\r\n mode: this.dedup ? \"dedup\" : \"classic\",\r\n count: Number(tot.n),\r\n totalBytes: Number(tot.bytes),\r\n oldest: utc(tot.oldest),\r\n newest: utc(tot.newest),\r\n onDisk: prettySize(Number(disk.s)),\r\n byFormat: fmt.map((r) => ({\r\n mimeType: r.m,\r\n count: Number(r.n),\r\n bytes: Number(r.bytes),\r\n })),\r\n };\r\n if (this.dedup) {\r\n const b = (\r\n await q.query(\r\n \"SELECT COUNT(*) AS n, COALESCE(SUM(size_bytes), 0) AS bytes FROM zerobucket_blobs\",\r\n )\r\n )[0][0];\r\n info.dedup = { blobs: Number(b.n), storedBytes: Number(b.bytes) };\r\n } else {\r\n const t = (\r\n await q.query(\r\n \"SELECT COUNT(*) AS n, COALESCE(SUM(size_bytes), 0) AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'\",\r\n )\r\n )[0][0];\r\n info.tiered = { count: Number(t.n), bytes: Number(t.bytes) };\r\n }\r\n return info;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to read storage info\", exc);\r\n }\r\n }\r\n\r\n /** Ids matching the filters: oldest first, or a random sample. Used by the CLI's verify and tier. */\r\n async listImages(o: ImageListOptions = {}): Promise<ImageListEntry[]> {\r\n const where: string[] = [];\r\n const params: unknown[] = [];\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n if (o.minSize !== undefined) {\r\n where.push(`${this.dedup ? \"b.size_bytes\" : \"size_bytes\"} >= ?`);\r\n params.push(o.minSize);\r\n }\r\n if (o.olderThanDays !== undefined) {\r\n // created_at is naive UTC, so compare against UTC_TIMESTAMP, never NOW() (which is session-timezone local).\r\n where.push(`${created} <= UTC_TIMESTAMP(6) - INTERVAL ? DAY`);\r\n params.push(o.olderThanDays);\r\n }\r\n if (o.onlyUntiered && !this.dedup)\r\n where.push(`storage_backend = '${LOCAL}'`);\r\n const from = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const idCol = this.dedup ? \"r.id\" : \"id\";\r\n const tierCol = this.dedup\r\n ? \"0 AS tiered\"\r\n : \"(storage_backend = 'object_storage') AS tiered\";\r\n const order =\r\n o.sample !== undefined\r\n ? \"ORDER BY RAND()\"\r\n : `ORDER BY ${created}, ${idCol}`;\r\n const lim = o.sample ?? o.limit;\r\n let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? \" WHERE \" + where.join(\" AND \") : \"\"} ${order}`;\r\n if (lim !== undefined) {\r\n if (!Number.isInteger(lim) || lim < 0)\r\n throw new RangeError(\"limit/sample must be a non-negative integer\");\r\n sql += ` LIMIT ${lim}`;\r\n }\r\n try {\r\n const rows = await this.run(\r\n \"list_images\",\r\n undefined,\r\n async (q) => (await q.query(sql, params))[0] as any[],\r\n );\r\n return rows.map((r) => ({\r\n id: String(r.id),\r\n tiered: Boolean(Number(r.tiered)),\r\n }));\r\n } catch (exc) {\r\n throw wrap(\"Failed to list images\", exc);\r\n }\r\n }\r\n\r\n // ---- dedup deletes ---------------------------------------------------\r\n\r\n /**\r\n * Lock the ref row, delete it, then decrement its blob (the UPDATE takes the blob row lock, so two\r\n * deletes of different refs to one blob serialize instead of racing) and remove the blob once nothing\r\n * references it. All in one transaction: a failure can never leave a ref without its count or the reverse.\r\n */\r\n private async deleteDedup(\r\n id: string,\r\n connection: Connection | undefined,\r\n ): Promise<boolean> {\r\n try {\r\n return await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const ref = (await q.query(D_LOCK_REF, [id]))[0][0];\r\n if (!ref) return false; // id did not exist: nothing to decrement either\r\n const checksum = ref.checksum_sha256 as string;\r\n await q.query(D_DELETE_REF, [id]);\r\n await q.query(D_DECREMENT, [1, checksum]);\r\n await q.query(D_DELETE_EMPTY, [checksum]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n private async deleteManyDedup(\r\n ids: string[],\r\n connection: Connection | undefined,\r\n ): Promise<string[]> {\r\n try {\r\n return await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const found = (await q.query(D_LOCK_REFS, [ids]))[0] as any[];\r\n if (found.length === 0) return [];\r\n for (const r of found) await q.query(D_DELETE_REF, [r.id]); // point deletes, in sorted order\r\n // Several deleted refs may share one blob: decrement each blob by its exact count, in sorted\r\n // checksum order (a global lock order, so concurrent batches cannot deadlock).\r\n const counts = new Map<string, number>();\r\n for (const r of found)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n for (const checksum of [...counts.keys()].sort()) {\r\n await q.query(D_DECREMENT, [counts.get(checksum), checksum]);\r\n await q.query(D_DELETE_EMPTY, [checksum]);\r\n }\r\n return found.map((r) => String(r.id));\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n // ---- helpers ---------------------------------------------------------\r\n\r\n private hydrate(row: any, id: string): Promise<StoredRecord> {\r\n return hydrateRow(row, id, this.opts.objectStorage);\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone: once the row is deleted the image is correctly\r\n * \"not found\" to everyone, so a failed object delete only leaves a harmless orphan, whereas deleting\r\n * the object first and then failing the row delete would leave a row pointing at nothing.\r\n *\r\n * Never runs inside a caller-owned transaction (`connection`): we cannot know whether it will\r\n * commit. In that case the object stays until you remove it yourself after committing. Failures are\r\n * reported through onOperation and never fail the delete itself.\r\n */\r\n private async cleanupObject(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes out of the database into object storage, replacing the row's `data` with a\r\n * pointer. Returns null (no such image), false (already tiered: a safe no-op, so backfills can be\r\n * re-run) or true (tiered just now).\r\n *\r\n * SAFETY: the upload happens INSIDE the same transaction as the row lock (SELECT ... FOR UPDATE)\r\n * and the UPDATE that flips the row. If the upload fails the transaction rolls back and the row is\r\n * untouched: there is no moment when the bytes exist nowhere, or when a row claims to be tiered\r\n * without a completed upload.\r\n *\r\n * TRADEOFF: InnoDB row-locks only THIS row for the duration of the upload. Concurrent writes to this\r\n * image wait (up to innodb_lock_wait_timeout, 50s by default); every other row is unaffected and\r\n * plain reads are never blocked. Meant for explicit maintenance, not hot paths.\r\n *\r\n * Retry is safe: a transient error replays the whole closure, and the deterministic key (the image\r\n * id) makes the re-upload an idempotent overwrite.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n }\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const key = id.toLowerCase();\r\n try {\r\n return await this.run(\r\n \"tier_to_object_storage\",\r\n connection,\r\n async (q) => {\r\n const row = (await q.query(SELECT_FOR_TIERING, [id]))[0][0];\r\n if (!row) return null;\r\n if (row.storage_backend !== LOCAL) return false;\r\n await os.upload(key, row.data as Buffer, { mimeType: row.mime_type });\r\n await q.query(UPDATE_AFTER_TIERING, [os.bucket, key, mysqlNow(), id]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n\r\nfunction blobParams(r: PreparedRow, now: string): unknown[] {\r\n return [\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n now,\r\n ];\r\n}\r\n\r\nfunction refParams(id: string, r: PreparedRow, now: string): unknown[] {\r\n return [id, r.checksumSha256, r.originalFilename, now, now];\r\n}\r\n\r\nfunction rowParams(id: string, r: PreparedRow, now: string): unknown[] {\r\n return [\r\n id,\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n now,\r\n now,\r\n ];\r\n}\r\n","/**\r\n * Optional HEIC/HEIF support (iPhone photos).\r\n *\r\n * sharp's prebuilt libvips cannot decode HEIC (HEVC is patent-encumbered), so HEIC is handled by the optional\r\n * peer dependency `libheif-js` (libheif + libde265 compiled to WebAssembly).\r\n *\r\n * WHY A WORKER THREAD: the WASM decoder is synchronous CPU work. Measured: a 12-megapixel photo takes ~2 seconds.\r\n * Run on the main thread, every iPhone upload would freeze the whole server for that long. The decoder therefore\r\n * lives in one long-lived worker thread (unref'd, so it never keeps your process alive) and is called through\r\n * promises. sharp itself uses libuv's thread pool the same way.\r\n *\r\n * LICENSE NOTE: libheif-js is LGPL-3.0. It is not bundled with or redistributed by this package: you install it\r\n * yourself (`npm install libheif-js`), which is what keeps this package MIT.\r\n *\r\n * LIMITS (honest): decoding only. There is no HEVC encoder in the WASM build, so `optimize` can convert FROM HEIC\r\n * to JPEG/WebP/PNG but not TO HEIC (Python's pillow-heif can). If a file holds several images, the first\r\n * top-level image is used.\r\n */\r\nimport { createRequire } from \"node:module\";\r\nimport { join } from \"node:path\";\r\nimport { Worker } from \"node:worker_threads\";\r\nimport { StorageError } from \"./errors.js\";\r\n\r\n/** ISO-BMFF `ftyp` major brands that identify HEIC/HEIF. Identical to the Python package's list. */\r\nconst HEIC_BRANDS = new Set([\r\n \"heic\",\r\n \"heix\",\r\n \"hevc\",\r\n \"heim\",\r\n \"heis\",\r\n \"mif1\",\r\n \"msf1\",\r\n]);\r\n\r\n/** True if the bytes start with an `ftyp` box carrying a HEIC/HEIF brand (content sniffing only; says nothing about validity). */\r\nexport function looksLikeHeic(data: Uint8Array): boolean {\r\n if (data.length < 12) return false;\r\n if (String.fromCharCode(data[4]!, data[5]!, data[6]!, data[7]!) !== \"ftyp\")\r\n return false;\r\n return HEIC_BRANDS.has(\r\n String.fromCharCode(data[8]!, data[9]!, data[10]!, data[11]!),\r\n );\r\n}\r\n\r\nexport const HEIC_INSTALL_HINT = \"npm install libheif-js\";\r\n\r\n// Runs inside the worker (CommonJS, evaluated from a string so it needs no separate file in either build format).\r\nconst WORKER_SOURCE = `\r\nconst { parentPort, workerData } = require(\"node:worker_threads\");\r\nconst libheif = require(workerData.libPath);\r\n// libheif-js reports decode problems by dumping a raw error object to the console. Capture that text instead:\r\n// it becomes part of the error we return (the useful diagnosis), and nothing is printed to the host's stderr.\r\nlet diag = \"\";\r\nfor (const level of [\"log\", \"info\", \"warn\", \"error\"]) {\r\n console[level] = (...args) => {\r\n diag = args.map((a) => (a && a.message) || (typeof a === \"string\" ? a : \"\")).filter(Boolean).join(\" \").trim();\r\n };\r\n}\r\nconst describe = (e) => {\r\n const base = String((e && e.message) || e);\r\n return diag && !base.includes(diag) ? base + \": \" + diag : base;\r\n};\r\nfunction open(bytes) {\r\n const images = new libheif.HeifDecoder().decode(bytes);\r\n if (!images || images.length === 0) throw new Error(\"no images found in HEIF container\");\r\n return images[0];\r\n}\r\nparentPort.on(\"message\", async ({ id, op, data }) => {\r\n diag = \"\";\r\n try {\r\n const img = open(new Uint8Array(data));\r\n const width = img.get_width();\r\n const height = img.get_height();\r\n if (op === \"info\") return parentPort.postMessage({ id, ok: true, width, height });\r\n const out = await new Promise((resolve, reject) =>\r\n img.display({ data: new Uint8ClampedArray(width * height * 4), width, height }, (d) =>\r\n d ? resolve(d) : reject(new Error(\"HEIF processing error\")),\r\n ),\r\n );\r\n if (op === \"validate\") return parentPort.postMessage({ id, ok: true, width, height });\r\n const rgba = out.data.buffer;\r\n parentPort.postMessage({ id, ok: true, width, height, rgba }, [rgba]);\r\n } catch (e) {\r\n parentPort.postMessage({ id, ok: false, error: describe(e) });\r\n }\r\n});\r\n`;\r\n\r\n/** The decoder worker itself failed (crashed or was killed). That is an infrastructure fault, not a bad image. */\r\nexport class HeicWorkerError extends StorageError {}\r\n\r\n/** `owner` ties a job to the worker that is running it, so one worker's death can never fail another worker's jobs. */\r\ntype Pending = { owner: Worker; resolve(v: any): void; reject(e: Error): void };\r\n\r\nlet worker: Worker | undefined;\r\nlet nextId = 1;\r\nconst pending = new Map<number, Pending>();\r\nconst stats = { info: 0, validate: 0, decode: 0, workersStarted: 0 };\r\n\r\n/** How the library path is found. Overridable for tests only. */\r\nlet resolveLib: () => string = () => {\r\n const attempts: (() => string)[] = [\r\n () => createRequire(import.meta.url).resolve(\"libheif-js/wasm-bundle\"), // next to / above this package\r\n () =>\r\n createRequire(join(process.cwd(), \"noop.js\")).resolve(\r\n \"libheif-js/wasm-bundle\",\r\n ), // the app's own node_modules\r\n ];\r\n let last: unknown;\r\n for (const attempt of attempts) {\r\n try {\r\n return attempt();\r\n } catch (e) {\r\n last = e;\r\n }\r\n }\r\n throw last;\r\n};\r\n\r\n/** Whether `libheif-js` can be found. */\r\nexport function heicAvailable(): boolean {\r\n try {\r\n resolveLib();\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Reference lifecycle: the worker is ref'd ONLY while it has jobs in flight and unref'd when idle.\r\n * - always ref'd: an idle decoder would keep your process (a CLI, a script, a test run) from ever exiting;\r\n * - always unref'd: the process could exit in the middle of a decode, since an unfinished promise does not\r\n * keep Node alive on its own.\r\n */\r\nfunction releaseIfIdle(w: Worker): void {\r\n for (const p of pending.values()) if (p.owner === w) return;\r\n w.unref();\r\n}\r\n\r\nfunction startWorker(): Worker {\r\n let libPath: string;\r\n try {\r\n libPath = resolveLib();\r\n } catch (cause) {\r\n throw new StorageError(\r\n `HEIC support requires the optional dependency 'libheif-js'. Install it with: ${HEIC_INSTALL_HINT}`,\r\n { cause },\r\n );\r\n }\r\n const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { libPath } });\r\n stats.workersStarted += 1;\r\n w.on(\r\n \"message\",\r\n (\r\n m: { id: number; ok: boolean; error?: string } & Record<string, unknown>,\r\n ) => {\r\n const p = pending.get(m.id);\r\n if (!p) return;\r\n pending.delete(m.id);\r\n releaseIfIdle(w);\r\n if (m.ok) p.resolve(m);\r\n else p.reject(new Error(m.error));\r\n },\r\n );\r\n const fail = (err: Error) => {\r\n if (worker === w) worker = undefined; // next call starts a fresh worker\r\n // A dying worker emits 'error' and 'exit' separately, and a replacement may already be running by the time the\r\n // second one arrives. Only ever fail the jobs THIS worker owned.\r\n for (const [id, p] of pending) {\r\n if (p.owner !== w) continue;\r\n pending.delete(id);\r\n p.reject(err);\r\n }\r\n };\r\n w.on(\"error\", (e) =>\r\n fail(\r\n new HeicWorkerError(\r\n `HEIC decoder worker crashed: ${e instanceof Error ? e.message : String(e)}`,\r\n { cause: e },\r\n ),\r\n ),\r\n );\r\n w.on(\"exit\", (code) =>\r\n fail(\r\n new HeicWorkerError(\r\n `HEIC decoder worker exited unexpectedly (code ${code})`,\r\n ),\r\n ),\r\n );\r\n w.unref(); // an idle decoder must never keep the host process alive (after listeners: adding them can re-ref the port)\r\n return w;\r\n}\r\n\r\nfunction call(\r\n op: \"info\" | \"validate\" | \"decode\",\r\n data: Uint8Array,\r\n): Promise<any> {\r\n stats[op] += 1;\r\n const w = (worker ??= startWorker());\r\n const id = nextId++;\r\n // Copy: the caller's Buffer may be a view into a shared pool, and the copy is transferred without a second copy.\r\n const copy = new Uint8Array(data).buffer;\r\n return new Promise((resolve, reject) => {\r\n pending.set(id, { owner: w, resolve, reject });\r\n w.ref(); // keep the process alive until this job settles\r\n w.postMessage({ id, op, data: copy }, [copy]);\r\n });\r\n}\r\n\r\n/** Read the dimensions from the container header. Cheap: no pixels are decoded. */\r\nexport async function heicInfo(\r\n data: Uint8Array,\r\n): Promise<{ width: number; height: number }> {\r\n const r = await call(\"info\", data);\r\n return { width: r.width, height: r.height };\r\n}\r\n\r\n/** Fully decode every pixel (to catch truncated/corrupted bodies) but return only the dimensions. */\r\nexport async function heicValidate(\r\n data: Uint8Array,\r\n): Promise<{ width: number; height: number }> {\r\n const r = await call(\"validate\", data);\r\n return { width: r.width, height: r.height };\r\n}\r\n\r\n/** Decode to raw RGBA pixels (4 bytes per pixel, already rotated/mirrored per the file's transform boxes). */\r\nexport async function decodeHeic(\r\n data: Uint8Array,\r\n): Promise<{ width: number; height: number; rgba: Buffer }> {\r\n const r = await call(\"decode\", data);\r\n return {\r\n width: r.width,\r\n height: r.height,\r\n rgba: Buffer.from(r.rgba as ArrayBuffer),\r\n };\r\n}\r\n\r\n/** Test hooks. Not exported from the package entry point. */\r\nexport const _testing = {\r\n stats,\r\n setResolver(fn: (() => string) | undefined): void {\r\n resolveLib = fn ?? _defaultResolver;\r\n void worker?.terminate();\r\n worker = undefined;\r\n },\r\n terminateWorker(): Promise<number> | undefined {\r\n return worker?.terminate();\r\n },\r\n hasWorker: () => worker !== undefined,\r\n};\r\nconst _defaultResolver = resolveLib;\r\n","/**\r\n * Image validation. Never trusts file extensions or client-supplied MIME types:\r\n * the bytes are decoded with sharp (libvips) and the *detected* format is what\r\n * gets validated and stored.\r\n *\r\n * sharp is an optional peer dependency, imported lazily, so `import \"zerobucket\"`\r\n * works without it (e.g. when you only store PDFs through a custom validator).\r\n */\r\nimport {\r\n HEIC_INSTALL_HINT,\r\n heicAvailable,\r\n heicInfo,\r\n heicValidate,\r\n looksLikeHeic,\r\n} from \"./heic.js\";\r\nimport {\r\n CorruptedImageError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n UnsupportedFormatError,\r\n} from \"./errors.js\";\r\n\r\n/** sharp format name -> canonical MIME type. Deliberately small allowlist. */\r\nconst FORMAT_TO_MIME: Record<string, string> = {\r\n jpeg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n heic: \"image/heic\", // optional: needs `npm install libheif-js` (see heic.ts)\r\n};\r\n\r\nexport const SUPPORTED_FORMATS: readonly string[] = Object.keys(FORMAT_TO_MIME);\r\n\r\n/** Reject images that would decode to more pixels than this, regardless of compressed size (~89 MP). */\r\nexport const DEFAULT_MAX_PIXELS = 89_000_000;\r\n\r\nexport interface ValidatedImage {\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n}\r\n\r\n// sharp <= 0.34 types the module as `export =` (the namespace itself is callable); sharp >= 0.35 ships separate\r\n// ESM typings where the callable is the namespace's `default`. Resolve to the callable either way.\r\ntype SharpModule = typeof import(\"sharp\");\r\ntype Sharp = SharpModule extends (...args: never[]) => unknown\r\n ? SharpModule\r\n : SharpModule extends { default: infer D }\r\n ? D\r\n : never;\r\nlet sharpPromise: Promise<Sharp> | undefined;\r\n\r\n/** Lazily load sharp, with an actionable error if it is not installed. */\r\nexport function loadSharp(): Promise<Sharp> {\r\n sharpPromise ??= import(\"sharp\").then(\r\n // ESM gives { default: sharp }; some CJS interop gives sharp directly.\r\n (m) =>\r\n (m as unknown as { default?: Sharp }).default ?? (m as unknown as Sharp),\r\n (cause) => {\r\n sharpPromise = undefined;\r\n throw new StorageError(\r\n \"Image validation requires the optional dependency 'sharp'. Install it with: npm install sharp\",\r\n { cause },\r\n );\r\n },\r\n );\r\n return sharpPromise;\r\n}\r\n\r\nexport async function validateImage(\r\n data: Buffer,\r\n opts: {\r\n maxBytes: number;\r\n maxPixels?: number;\r\n allowedFormats?: readonly string[];\r\n },\r\n): Promise<ValidatedImage> {\r\n const {\r\n maxBytes,\r\n maxPixels = DEFAULT_MAX_PIXELS,\r\n allowedFormats = SUPPORTED_FORMATS,\r\n } = opts;\r\n const sizeBytes = data.length;\r\n if (sizeBytes > maxBytes) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n if (sizeBytes === 0) throw new CorruptedImageError(\"Image data is empty\");\r\n\r\n // sharp's prebuilt libvips cannot decode HEIC, so it is handled (in a worker thread) by the optional libheif-js.\r\n if (looksLikeHeic(data))\r\n return validateHeic(data, { maxBytes, maxPixels, allowedFormats });\r\n\r\n const sharp = await loadSharp();\r\n // failOn \"error\": truncated / corrupt bodies throw instead of silently decoding partially.\r\n const make = () =>\r\n sharp(data, { limitInputPixels: maxPixels, failOn: \"error\" });\r\n\r\n let format: string | undefined;\r\n let width: number | undefined;\r\n let height: number | undefined;\r\n try {\r\n const meta = await make().metadata();\r\n format = meta.format;\r\n width = meta.width;\r\n height = meta.height;\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Could not decode image: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n\r\n if (\r\n !format ||\r\n !allowedFormats.includes(format) ||\r\n !(format in FORMAT_TO_MIME)\r\n ) {\r\n throw new UnsupportedFormatError(format ?? null, allowedFormats);\r\n }\r\n if (!width || !height)\r\n throw new CorruptedImageError(\"Image has no readable dimensions\");\r\n\r\n // Header parsing is not enough: force a full pixel decode to catch truncated bodies.\r\n //\r\n // How the decode is forced matters a lot, and was measured (benchmarks/node, `npm run validation-cost`):\r\n // - `.stats()` (used before 0.10.0) also computes entropy, sharpness and the dominant colour, and holds the whole\r\n // decoded image in memory: ~1.1 s and ~150 MB for a 45-megapixel JPEG. It also silently accepts a truncated\r\n // interlaced PNG.\r\n // - `.resize(1, 1)` is fast, but JPEG/WebP shrink-on-load decodes at reduced scale and then misses a file that is\r\n // only missing its last few bytes (an interrupted upload), so it is NOT used.\r\n // - Extracting a one-pixel-wide column of the full height is read sequentially, so every scanline is decoded at\r\n // full resolution and then discarded: the same decode work as a bare decode, with nothing kept and a ~1 KB output.\r\n // tests/validation.test.ts pins both properties: damaged files are still rejected, and memory stays flat.\r\n try {\r\n await sharp(data, {\r\n limitInputPixels: maxPixels,\r\n failOn: \"error\",\r\n sequentialRead: true,\r\n })\r\n .extract({ left: 0, top: 0, width: 1, height })\r\n .raw()\r\n .toBuffer();\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Image data is truncated or corrupted: ${decodeDetail(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n\r\n return { mimeType: FORMAT_TO_MIME[format]!, width, height, sizeBytes };\r\n}\r\n\r\nasync function validateHeic(\r\n data: Buffer,\r\n {\r\n maxBytes,\r\n maxPixels,\r\n allowedFormats,\r\n }: { maxBytes: number; maxPixels: number; allowedFormats: readonly string[] },\r\n): Promise<ValidatedImage> {\r\n const sizeBytes = data.length;\r\n // Same order as the Python package: a missing optional dependency is reported as such, never as \"corrupt\".\r\n if (!heicAvailable()) {\r\n throw new ImageValidationError(\r\n `This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`,\r\n );\r\n }\r\n if (!allowedFormats.includes(\"heic\"))\r\n throw new UnsupportedFormatError(\"heic\", allowedFormats);\r\n\r\n let width: number;\r\n let height: number;\r\n try {\r\n ({ width, height } = await heicInfo(data)); // header only: cheap, and lets us refuse a pixel bomb BEFORE decoding\r\n } catch (exc) {\r\n if (exc instanceof StorageError) throw exc; // the decoder worker failed: not the image's fault\r\n throw new CorruptedImageError(\r\n `Could not decode image: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n if (!width || !height)\r\n throw new CorruptedImageError(\"Image has no readable dimensions\");\r\n if (width * height > maxPixels)\r\n throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n\r\n try {\r\n await heicValidate(data); // full pixel decode, like Pillow's img.load(): catches truncated bodies\r\n } catch (exc) {\r\n if (exc instanceof StorageError) throw exc;\r\n throw new CorruptedImageError(\r\n `Image data is truncated or corrupted: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n return { mimeType: FORMAT_TO_MIME.heic!, width, height, sizeBytes };\r\n}\r\n\r\n/** libvips reports some decoder failures (for example a damaged interlaced PNG) only as \"Unknown error\". */\r\nfunction decodeDetail(exc: unknown): string {\r\n const msg = errMessage(exc).trim();\r\n return /^unknown error$/i.test(msg)\r\n ? \"the decoder stopped before the end of the image\"\r\n : msg;\r\n}\r\n\r\nfunction isPixelLimit(exc: unknown): boolean {\r\n return /pixel limit/i.test(errMessage(exc));\r\n}\r\n\r\nexport function errMessage(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport { ImageValidationError };\r\n","/**\r\n * Optimize pipeline: strip metadata, optionally downscale and re-encode.\r\n * Mirrors the Python package's optimize_image(), implemented on sharp.\r\n *\r\n * Differences from the Python/Pillow version, both deliberate:\r\n * - EXIF orientation is applied (auto-rotate) BEFORE metadata is stripped.\r\n * Stripping EXIF without applying it would leave phone photos sideways.\r\n * - Transparent PNG/WebP converted to JPEG is flattened onto white, not black.\r\n */\r\nimport { ImageValidationError } from \"./errors.js\";\r\nimport {\r\n decodeHeic,\r\n heicAvailable,\r\n HEIC_INSTALL_HINT,\r\n looksLikeHeic,\r\n} from \"./heic.js\";\r\nimport { DEFAULT_MAX_PIXELS, loadSharp, validateImage } from \"./validation.js\";\r\n\r\n/** Formats the optimizer can WRITE. (HEIC can be read but not written: there is no HEVC encoder for Node.) */\r\nconst ENCODABLE_FORMATS = [\"jpeg\", \"png\", \"webp\"] as const;\r\n\r\nexport const DEFAULT_JPEG_QUALITY = 90;\r\nexport const DEFAULT_WEBP_QUALITY = 88;\r\n\r\nexport interface OptimizeOptions {\r\n /** Downscale (aspect ratio preserved, Lanczos) if wider than this. Never upscales. */\r\n maxWidth?: number;\r\n /** \"jpeg\" | \"png\" | \"webp\". Defaults to the source format. */\r\n format?: string;\r\n /** 1-100, JPEG/WebP only (ignored for PNG). */\r\n quality?: number;\r\n}\r\n\r\nexport interface OptimizationResult {\r\n data: Buffer;\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n originalSizeBytes: number;\r\n}\r\n\r\nexport async function optimizeImage(\r\n data: Buffer,\r\n opts: OptimizeOptions & { maxBytes: number; maxPixels?: number },\r\n): Promise<OptimizationResult> {\r\n const { maxWidth, quality, maxBytes, maxPixels = DEFAULT_MAX_PIXELS } = opts;\r\n const sharp = await loadSharp();\r\n const originalSizeBytes = data.length;\r\n\r\n const requestedRaw = opts.format?.toLowerCase();\r\n const requested = requestedRaw === \"jpg\" ? \"jpeg\" : requestedRaw;\r\n if (requested === \"heic\" || requested === \"heif\") {\r\n throw new ImageValidationError(\r\n \"Encoding to HEIC is not available in the Node package (there is no HEVC encoder for Node). \" +\r\n \"Optimize to jpeg, webp or png instead, or store the original HEIC without `optimize`.\",\r\n );\r\n }\r\n\r\n // Where the pixels come from: HEIC is decoded by libheif-js (in a worker thread) to raw RGBA that sharp then\r\n // re-encodes; everything else is read by sharp directly. EXIF is irrelevant for raw input (HEIC orientation\r\n // is already applied by the decoder), and sharp strips metadata on output either way.\r\n let input: Buffer = data;\r\n let inputOptions: Parameters<typeof sharp>[1] = {\r\n limitInputPixels: maxPixels,\r\n failOn: \"error\",\r\n };\r\n let sourceFormat: string | undefined;\r\n if (looksLikeHeic(data)) {\r\n if (!heicAvailable()) {\r\n throw new ImageValidationError(\r\n `This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`,\r\n );\r\n }\r\n if (!requested) {\r\n throw new ImageValidationError(\r\n \"Optimizing a HEIC image needs an explicit output format, because re-encoding to HEIC is not available in the \" +\r\n \"Node package. Pass optimize: { format: 'jpeg' | 'webp' | 'png' } (for example { format: 'webp', maxWidth: 1600 }).\",\r\n );\r\n }\r\n const decoded = await decodeHeic(data);\r\n input = decoded.rgba;\r\n inputOptions = {\r\n raw: { width: decoded.width, height: decoded.height, channels: 4 },\r\n };\r\n sourceFormat = \"heic\";\r\n } else {\r\n sourceFormat = (await sharp(data, inputOptions).metadata()).format;\r\n }\r\n const target = requested ?? sourceFormat ?? \"jpeg\";\r\n if (!(ENCODABLE_FORMATS as readonly string[]).includes(target)) {\r\n throw new ImageValidationError(\r\n `Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify([...ENCODABLE_FORMATS].sort())}`,\r\n );\r\n }\r\n\r\n // .rotate() with no args = auto-orient from EXIF. sharp drops all metadata on output by default.\r\n let pipeline = sharp(input, inputOptions).rotate();\r\n if (maxWidth !== undefined) {\r\n pipeline = pipeline.resize({\r\n width: maxWidth,\r\n fit: \"inside\",\r\n withoutEnlargement: true,\r\n kernel: \"lanczos3\",\r\n });\r\n }\r\n\r\n if (target === \"jpeg\") {\r\n pipeline = pipeline\r\n .flatten({ background: \"#ffffff\" })\r\n .jpeg({\r\n quality: quality ?? DEFAULT_JPEG_QUALITY,\r\n mozjpeg: true,\r\n progressive: true,\r\n });\r\n } else if (target === \"webp\") {\r\n pipeline = pipeline.webp({\r\n quality: quality ?? DEFAULT_WEBP_QUALITY,\r\n effort: 6,\r\n });\r\n } else {\r\n pipeline = pipeline.png({ compressionLevel: 9 }); // no lossy knob; quality ignored by design\r\n }\r\n\r\n const out = await pipeline.toBuffer();\r\n\r\n // Defense in depth: re-validate our own output before it is trusted.\r\n const revalidated = await validateImage(out, { maxBytes, maxPixels });\r\n return {\r\n data: out,\r\n mimeType: revalidated.mimeType,\r\n width: revalidated.width,\r\n height: revalidated.height,\r\n sizeBytes: revalidated.sizeBytes,\r\n originalSizeBytes,\r\n };\r\n}\r\n","/**\r\n * S3-compatible object storage for tiering (AWS S3, MinIO, Cloudflare R2,\r\n * Backblaze B2, DigitalOcean Spaces, ...).\r\n *\r\n * Scoped to exactly what tiering needs: upload, download, ranged download,\r\n * idempotent delete, exists. The bucket must already exist: bucket creation\r\n * involves choices (region, versioning, lifecycle) that belong to you, not to\r\n * a storage library.\r\n *\r\n * `@aws-sdk/client-s3` is an optional peer dependency, imported lazily on first\r\n * use, so `import \"zerobucket\"` works without it.\r\n *\r\n * Objects are keyed by the image's UUID, with no prefix. That is the same key\r\n * scheme the Python package uses, so either package can read, tier and clean\r\n * up objects the other one created in the same bucket.\r\n */\r\nimport { StorageError } from \"./errors.js\";\r\n\r\n/** The surface the Postgres adapter needs. Implement this to plug in a non-S3 store (or a test double). */\r\nexport interface ObjectStorageLike {\r\n readonly bucket: string;\r\n upload(key: string, data: Buffer, opts: { mimeType: string }): Promise<void>;\r\n download(key: string): Promise<Buffer>;\r\n /** Inclusive byte range, fetched with a real HTTP Range request. */\r\n downloadRange(\r\n key: string,\r\n start: number,\r\n endInclusive: number,\r\n ): Promise<Buffer>;\r\n /** Idempotent: deleting a missing key is not an error. */\r\n delete(key: string): Promise<void>;\r\n exists(key: string): Promise<boolean>;\r\n}\r\n\r\nexport interface ObjectStorageOptions {\r\n /** Must already exist. */\r\n bucket: string;\r\n /** Set for anything that is not AWS S3 itself (MinIO, R2, B2, Spaces, ...). */\r\n endpoint?: string;\r\n /** Default: AWS_REGION / AWS_DEFAULT_REGION, else \"us-east-1\". Many S3-compatible services ignore it but the SDK requires one. */\r\n region?: string;\r\n /** Default: the SDK's standard chain (env vars, shared config, IAM role, ...). */\r\n credentials?: {\r\n accessKeyId: string;\r\n secretAccessKey: string;\r\n sessionToken?: string;\r\n };\r\n /** Path-style addressing (`endpoint/bucket/key`). Default: true when `endpoint` is set, else false. */\r\n forcePathStyle?: boolean;\r\n /** Bring your own configured S3Client (advanced). */\r\n client?: unknown;\r\n}\r\n\r\ntype Sdk = typeof import(\"@aws-sdk/client-s3\");\r\ntype S3 = import(\"@aws-sdk/client-s3\").S3Client;\r\n\r\nexport class ObjectStorage implements ObjectStorageLike {\r\n readonly bucket: string;\r\n private clientPromise: Promise<{ sdk: Sdk; client: S3 }> | undefined;\r\n\r\n constructor(private readonly opts: ObjectStorageOptions) {\r\n if (!opts.bucket)\r\n throw new TypeError(\"ObjectStorage requires a bucket name\");\r\n this.bucket = opts.bucket;\r\n }\r\n\r\n private connection(): Promise<{ sdk: Sdk; client: S3 }> {\r\n this.clientPromise ??= (async () => {\r\n let sdk: Sdk;\r\n try {\r\n sdk = await import(\"@aws-sdk/client-s3\");\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"Object-storage tiering requires the optional dependency '@aws-sdk/client-s3'. \" +\r\n \"Install it with: npm install @aws-sdk/client-s3\",\r\n { cause },\r\n );\r\n }\r\n const o = this.opts;\r\n const client =\r\n (o.client as S3 | undefined) ??\r\n new sdk.S3Client({\r\n endpoint: o.endpoint,\r\n region:\r\n o.region ??\r\n process.env.AWS_REGION ??\r\n process.env.AWS_DEFAULT_REGION ??\r\n \"us-east-1\",\r\n credentials: o.credentials,\r\n forcePathStyle: o.forcePathStyle ?? o.endpoint !== undefined,\r\n });\r\n return { sdk, client };\r\n })().catch((exc) => {\r\n this.clientPromise = undefined;\r\n throw exc;\r\n });\r\n return this.clientPromise;\r\n }\r\n\r\n async upload(\r\n key: string,\r\n data: Buffer,\r\n { mimeType }: { mimeType: string },\r\n ): Promise<void> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n await client.send(\r\n new sdk.PutObjectCommand({\r\n Bucket: this.bucket,\r\n Key: key,\r\n Body: data,\r\n ContentType: mimeType,\r\n }),\r\n );\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to upload ${JSON.stringify(key)} to bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async download(key: string): Promise<Buffer> {\r\n return this.get(key, undefined);\r\n }\r\n\r\n async downloadRange(\r\n key: string,\r\n start: number,\r\n endInclusive: number,\r\n ): Promise<Buffer> {\r\n return this.get(key, `bytes=${start}-${endInclusive}`);\r\n }\r\n\r\n private async get(key: string, range: string | undefined): Promise<Buffer> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n const res = await client.send(\r\n new sdk.GetObjectCommand({\r\n Bucket: this.bucket,\r\n Key: key,\r\n Range: range,\r\n }),\r\n );\r\n return Buffer.from(await res.Body!.transformToByteArray());\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to download ${JSON.stringify(key)} from bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n // S3 DeleteObject succeeds for keys that do not exist, so this is idempotent by nature.\r\n await client.send(\r\n new sdk.DeleteObjectCommand({ Bucket: this.bucket, Key: key }),\r\n );\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to delete ${JSON.stringify(key)} from bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n await client.send(\r\n new sdk.HeadObjectCommand({ Bucket: this.bucket, Key: key }),\r\n );\r\n return true;\r\n } catch (exc) {\r\n const e = exc as {\r\n name?: string;\r\n $metadata?: { httpStatusCode?: number };\r\n };\r\n if (\r\n e.name === \"NotFound\" ||\r\n e.name === \"NoSuchKey\" ||\r\n e.$metadata?.httpStatusCode === 404\r\n )\r\n return false;\r\n throw new StorageError(\r\n `Failed to check ${JSON.stringify(key)} in bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n}\r\n\r\nfunction msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n","import { ContentValidationError } from \"../errors.js\";\r\nimport type { ContentValidator, ValidatedContent } from \"./types.js\";\r\n\r\nexport const DEFAULT_MAX_PDF_BYTES = 20 * 1024 * 1024;\r\n\r\n/**\r\n * Validates that bytes look like a PDF: `%PDF-` magic bytes, non-empty, under a\r\n * size ceiling. Deliberately minimal.\r\n *\r\n * Security note: this does NOT parse the PDF object structure and does not\r\n * detect embedded JavaScript, forms or launch actions. Treat it as a sanity\r\n * gate, not a security boundary, if you serve user-uploaded PDFs to other users.\r\n */\r\nexport class PDFValidator implements ContentValidator {\r\n constructor(private readonly opts: { maxBytes?: number } = {}) {}\r\n\r\n validate(data: Buffer, { maxBytes }: { maxBytes: number }): ValidatedContent {\r\n const effectiveMax = Math.min(\r\n maxBytes,\r\n this.opts.maxBytes ?? DEFAULT_MAX_PDF_BYTES,\r\n );\r\n if (data.length === 0)\r\n throw new ContentValidationError(\"PDF data is empty\");\r\n if (data.length > effectiveMax) {\r\n throw new ContentValidationError(\r\n `PDF is ${data.length} bytes, exceeds the maximum of ${effectiveMax} bytes`,\r\n );\r\n }\r\n // The spec allows junk before the header; real readers scan the first 1024 bytes.\r\n if (!data.subarray(0, 1024).includes(\"%PDF-\")) {\r\n throw new ContentValidationError(\r\n \"This does not look like a valid PDF (missing '%PDF-' header). Content is \" +\r\n \"checked by inspecting the actual bytes, not the filename or any client-supplied content type.\",\r\n );\r\n }\r\n return {\r\n mimeType: \"application/pdf\",\r\n sizeBytes: data.length,\r\n width: null,\r\n height: null,\r\n };\r\n }\r\n}\r\n"],"mappings":"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","names":["start","sql","SCHEMA","DEDUP_SCHEMA","INSERT","SELECT_FULL","SELECT_METADATA","EXISTS","SELECT_FOR_TIERING","UPDATE_AFTER_TIERING","D_FROM","D_COLS","D_INSERT_REF","D_DELETE_REF","D_DECREMENT","D_DELETE_EMPTY","randomUUID","SCHEMA","COLS","INSERT","SELECT_FULL","SELECT_MANY","SELECT_METADATA","STREAM_INFO","CHUNK","EXISTS","DELETE_ONE","SELECT_FOR_TIERING","UPDATE_AFTER_TIERING","D_INSERT_REF","D_COLS","D_FROM","D_DELETE_REF","D_DECREMENT","D_DELETE_EMPTY","CLASSIC_SQL","DEDUP_SQL","isRetryable","isConnectionLevel","MAX_BACKOFF_MS","backoffDelayMs","randomUUID","blobParams","rowParams","msg","mapLimit","msg"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/adapters/shared.ts","../src/adapters/postgres.ts","../src/adapters/sqlite.ts","../src/adapters/mysql.ts","../src/heic.ts","../src/validation.ts","../src/optimize.ts","../src/object-storage.ts","../src/validators/pdf.ts"],"sourcesContent":["/**\r\n * The public ZeroBucket SDK entry point.\r\n *\r\n * const images = new ZeroBucket({ connectionString: process.env.DATABASE_URL });\r\n * const id = await images.put(fileBuffer);\r\n * const image = await images.get(id);\r\n *\r\n * The developer never thinks about BYTEA, checksums or connection pooling.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport { readFile, stat } from \"node:fs/promises\";\r\nimport { basename } from \"node:path\";\r\nimport type { Pool } from \"pg\";\r\nimport type {\r\n CallOptions,\r\n MigrateToDedupOptions,\r\n MigrateToDedupResult,\r\n StorageBackend,\r\n} from \"./adapters/base.js\";\r\nimport type { ObjectStorageLike } from \"./object-storage.js\";\r\nimport { PostgresBackend } from \"./adapters/postgres.js\";\r\nimport { SQLiteBackend } from \"./adapters/sqlite.js\";\r\nimport { MySQLBackend } from \"./adapters/mysql.js\";\r\nimport { DEFAULT_STREAM_CHUNK_SIZE, isValidId } from \"./adapters/shared.js\";\r\nimport {\r\n AccessDeniedError,\r\n ImageNotFoundError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n} from \"./errors.js\";\r\nimport { type OptimizeOptions, optimizeImage } from \"./optimize.js\";\r\nimport type {\r\n BatchDeleteResult,\r\n BatchGetResult,\r\n BatchPutResult,\r\n Image,\r\n ImageMetadata,\r\n OperationEvent,\r\n PreparedRow,\r\n Connection,\r\n MysqlPoolLike,\r\n} from \"./types.js\";\r\nimport {\r\n DEFAULT_MAX_PIXELS,\r\n SUPPORTED_FORMATS,\r\n validateImage,\r\n} from \"./validation.js\";\r\nimport type { ContentValidator } from \"./validators/types.js\";\r\n\r\n/** 8 MiB: a practical ceiling for \"small app\" images. Override with `maxBytes`. */\r\nexport const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;\r\n\r\n/**\r\n * What put() accepts:\r\n * - Buffer / Uint8Array / ArrayBuffer: the bytes themselves\r\n * - string: a filesystem PATH (never raw content)\r\n * - Blob / File (e.g. from `request.formData()`)\r\n * - Node Readable or web ReadableStream (read in bounded chunks)\r\n */\r\nexport type ImageInput =\r\n | Buffer\r\n | Uint8Array\r\n | ArrayBuffer\r\n | string\r\n | Blob\r\n | AsyncIterable<Uint8Array | string>;\r\n\r\nexport type BeforeGetHook<C = unknown> = (\r\n imageId: string,\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\nexport type BeforePutHook<C = unknown> = (\r\n context: C | undefined,\r\n) => boolean | Promise<boolean>;\r\n\r\nexport interface ZeroBucketOptions<C = unknown> {\r\n /** PostgreSQL connection string. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool. It is NOT closed by close(). */\r\n pool?: Pool;\r\n /**\r\n * Use SQLite instead of PostgreSQL: a database file path (or \":memory:\"). Needs `npm install better-sqlite3`.\r\n * `dedup`, `objectStorage`, `autoMigrate` and `onOperation` apply to it too. Same file format as the Python\r\n * package's SQLiteBackend, so both can share one `.db` file.\r\n */\r\n sqlite?: string;\r\n /**\r\n * Use MySQL or MariaDB instead of PostgreSQL: `mysql://user:password@host:3306/database`\r\n * (`mariadb://` works too). Needs `npm install mysql2`. Same schema as the Python package's\r\n * MySQLBackend, so both can share one database. Classic mode only for now (`dedup` is not\r\n * available for MySQL yet). `maxRetries`, `retryBaseDelayMs`, `poolMaxSize`, `poolTimeoutMs`,\r\n * `autoMigrate`, `objectStorage` and `onOperation` apply to it too.\r\n */\r\n mysql?: string;\r\n /** Bring your own `mysql2/promise` Pool for MySQL/MariaDB. It is NOT closed by close(). */\r\n mysqlPool?: MysqlPoolLike;\r\n /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite` / `mysql`. */\r\n backend?: StorageBackend;\r\n\r\n /** Max accepted size in bytes. Default 8 MiB. */\r\n maxBytes?: number;\r\n /** Decoded-pixel ceiling that rejects decompression bombs. Default ~89 MP. */\r\n maxPixels?: number;\r\n /** Accepted image formats. Default [\"jpeg\", \"png\", \"webp\"]. */\r\n allowedFormats?: readonly string[];\r\n\r\n autoMigrate?: boolean;\r\n maxRetries?: number;\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n poolTimeoutMs?: number;\r\n /** Fire-and-forget observability. Exceptions thrown inside it are swallowed. */\r\n onOperation?: (event: OperationEvent) => void;\r\n\r\n /**\r\n * S3-compatible object storage, enabling `tierToObjectStorage()` and transparent\r\n * reads of images the Python or Node package tiered. Configure it with the same\r\n * bucket and credentials used to tier. Without it, reading a tiered image throws\r\n * a clear StorageError.\r\n */\r\n objectStorage?: ObjectStorageLike;\r\n\r\n /**\r\n * Content-addressed mode: identical bytes are stored once and shared by reference, each image\r\n * keeping its own id and filename. Cannot be combined with `objectStorage`. Uses separate tables\r\n * (zerobucket_blobs / zerobucket_image_refs); see `migrateClassicToDedup()` for existing data.\r\n */\r\n dedup?: boolean;\r\n\r\n /**\r\n * Authorization hook for get / getMany (per id) / getStream / streamTo / metadata.\r\n * Return false to deny. A hook that THROWS fails closed: the exception\r\n * propagates (or is captured per item in getMany) and is never treated as allow.\r\n * NOT called for exists(). Only ever called with a well-formed UUID: a malformed id is\r\n * \"not found\" (ImageNotFoundError, or `error: \"not found\"` per item in getMany) and never\r\n * reaches the hook.\r\n */\r\n beforeGet?: BeforeGetHook<C>;\r\n /** Authorization hook for put / putMany (evaluated once per call). Same fail-closed rule. */\r\n beforePut?: BeforePutHook<C>;\r\n}\r\n\r\ninterface PutBase<C> {\r\n /** Display filename stored in metadata (never used for validation). */\r\n filename?: string;\r\n /** `true` = strip metadata only; or pass { maxWidth, format, quality } to resize/re-encode. */\r\n optimize?: boolean | OptimizeOptions;\r\n /** Store non-image content through the same machinery. Incompatible with `optimize`. */\r\n validator?: ContentValidator;\r\n /** Join your own transaction. ZeroBucket then does not commit, roll back or retry. */\r\n connection?: Connection;\r\n /** Passed to beforePut. */\r\n context?: C;\r\n}\r\nexport type PutOptions<C = unknown> = PutBase<C>;\r\nexport interface PutManyOptions<C = unknown> extends Omit<\r\n PutBase<C>,\r\n \"filename\"\r\n> {\r\n /** Same length as `images`; use null/undefined entries to fall back to each input's own name. */\r\n filenames?: (string | null | undefined)[];\r\n}\r\nexport interface GetOptions<C = unknown> {\r\n connection?: Connection;\r\n context?: C;\r\n}\r\nexport interface StreamOptions<C = unknown> extends GetOptions<C> {\r\n /** Bytes per chunk. Default 1 MiB. */\r\n chunkSize?: number;\r\n /** Inclusive byte range, e.g. { start: 0, end: 1023 }. `end` is clamped to the object size. */\r\n range?: { start: number; end?: number };\r\n}\r\n\r\n/** Anything with write(); e.g. fs.WriteStream, http.ServerResponse, or a custom sink. Backpressure is honoured. */\r\nexport interface WritableLike {\r\n write(chunk: Buffer): boolean | void | Promise<void>;\r\n once?(event: \"drain\", listener: () => void): unknown;\r\n}\r\n\r\n/** Bounded concurrency for per-item validation work. */\r\nconst PREPARE_CONCURRENCY = 4;\r\n\r\nexport class ZeroBucket<C = unknown> {\r\n private readonly backend: StorageBackend;\r\n private readonly maxBytes: number;\r\n private readonly maxPixels: number;\r\n private readonly allowedFormats: readonly string[];\r\n private readonly beforeGet: BeforeGetHook<C> | undefined;\r\n private readonly beforePut: BeforePutHook<C> | undefined;\r\n\r\n constructor(options: ZeroBucketOptions<C> = {}) {\r\n const chosen = [\r\n options.sqlite !== undefined && \"`sqlite`\",\r\n (options.mysql !== undefined || options.mysqlPool) &&\r\n \"`mysql`/`mysqlPool`\",\r\n (options.connectionString || options.pool) && \"`connectionString`/`pool`\",\r\n ].filter(Boolean);\r\n if (chosen.length > 1) {\r\n throw new TypeError(\r\n `Choose one database: ${chosen.join(\" OR \")}, not several.`,\r\n );\r\n }\r\n this.backend =\r\n options.backend ??\r\n (options.sqlite !== undefined\r\n ? new SQLiteBackend({\r\n path: options.sqlite,\r\n autoMigrate: options.autoMigrate,\r\n dedup: options.dedup,\r\n objectStorage: options.objectStorage,\r\n onOperation: options.onOperation,\r\n })\r\n : options.mysql !== undefined || options.mysqlPool\r\n ? new MySQLBackend({\r\n connectionString: options.mysql,\r\n pool: options.mysqlPool,\r\n autoMigrate: options.autoMigrate,\r\n maxRetries: options.maxRetries,\r\n retryBaseDelayMs: options.retryBaseDelayMs,\r\n poolMinSize: options.poolMinSize,\r\n poolMaxSize: options.poolMaxSize,\r\n poolTimeoutMs: options.poolTimeoutMs,\r\n onOperation: options.onOperation,\r\n objectStorage: options.objectStorage,\r\n dedup: options.dedup,\r\n })\r\n : new PostgresBackend({\r\n connectionString: options.connectionString,\r\n pool: options.pool,\r\n autoMigrate: options.autoMigrate,\r\n maxRetries: options.maxRetries,\r\n retryBaseDelayMs: options.retryBaseDelayMs,\r\n poolMinSize: options.poolMinSize,\r\n poolMaxSize: options.poolMaxSize,\r\n poolTimeoutMs: options.poolTimeoutMs,\r\n onOperation: options.onOperation,\r\n objectStorage: options.objectStorage,\r\n dedup: options.dedup,\r\n }));\r\n this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\r\n this.maxPixels = options.maxPixels ?? DEFAULT_MAX_PIXELS;\r\n this.allowedFormats = options.allowedFormats ?? SUPPORTED_FORMATS;\r\n this.beforeGet = options.beforeGet;\r\n this.beforePut = options.beforePut;\r\n }\r\n\r\n /** Construct AND connect/migrate immediately, so a bad DATABASE_URL fails at startup, not on first request. */\r\n static async connect<C = unknown>(\r\n options: ZeroBucketOptions<C> = {},\r\n ): Promise<ZeroBucket<C>> {\r\n const zb = new ZeroBucket<C>(options);\r\n await zb.ready();\r\n return zb;\r\n }\r\n\r\n /** Resolves once connected and migrated. Called automatically on first use. */\r\n ready(): Promise<void> {\r\n return this.backend.ready();\r\n }\r\n\r\n // ---- hooks -----------------------------------------------------------\r\n\r\n private async checkBeforeGet(\r\n imageId: string,\r\n context: C | undefined,\r\n ): Promise<void> {\r\n // A malformed id can never exist, so it is \"not found\" and your hook is never\r\n // called with it. The hook therefore only ever sees well-formed UUIDs, and can\r\n // safely use the id in SQL (`WHERE id = $1` on a uuid column) without first\r\n // validating a string that may have come straight from a URL.\r\n if (!isValidId(imageId)) throw new ImageNotFoundError(imageId);\r\n if (!this.beforeGet) return;\r\n if (!(await this.beforeGet(imageId, context)))\r\n throw new AccessDeniedError(\"get\", imageId);\r\n }\r\n\r\n private async checkBeforePut(context: C | undefined): Promise<void> {\r\n if (!this.beforePut) return;\r\n if (!(await this.beforePut(context))) throw new AccessDeniedError(\"put\");\r\n }\r\n\r\n // ---- write path ------------------------------------------------------\r\n\r\n private async prepareRow(\r\n image: ImageInput,\r\n o: PutBase<C> & { filename?: string | null },\r\n ): Promise<PreparedRow> {\r\n const { data, filename } = await readInput(\r\n image,\r\n o.filename ?? undefined,\r\n this.maxBytes,\r\n );\r\n\r\n let finalData: Buffer;\r\n let mimeType: string;\r\n let width: number | null;\r\n let height: number | null;\r\n let sizeBytes: number;\r\n\r\n if (o.validator) {\r\n if (o.optimize) {\r\n throw new ImageValidationError(\r\n \"optimize is not supported together with a custom validator: the optimize pipeline \" +\r\n \"(resize/re-encode) is image-specific. Validate/transform custom content types yourself before put().\",\r\n );\r\n }\r\n const v = await o.validator.validate(data, { maxBytes: this.maxBytes });\r\n finalData = data;\r\n mimeType = v.mimeType;\r\n width = v.width ?? null;\r\n height = v.height ?? null;\r\n sizeBytes = v.sizeBytes;\r\n } else {\r\n const validated = await validateImage(data, {\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n allowedFormats: this.allowedFormats,\r\n });\r\n if (o.optimize) {\r\n const opts = o.optimize === true ? {} : o.optimize;\r\n const r = await optimizeImage(data, {\r\n ...opts,\r\n maxBytes: this.maxBytes,\r\n maxPixels: this.maxPixels,\r\n });\r\n finalData = r.data;\r\n mimeType = r.mimeType;\r\n width = r.width;\r\n height = r.height;\r\n sizeBytes = r.sizeBytes;\r\n } else {\r\n finalData = data;\r\n mimeType = validated.mimeType;\r\n width = validated.width;\r\n height = validated.height;\r\n sizeBytes = validated.sizeBytes;\r\n }\r\n }\r\n\r\n return {\r\n data: finalData,\r\n mimeType,\r\n originalFilename: filename ?? null,\r\n sizeBytes,\r\n width,\r\n height,\r\n checksumSha256: createHash(\"sha256\").update(finalData).digest(\"hex\"),\r\n };\r\n }\r\n\r\n /** Validate, optionally optimize, checksum and store one image. Returns its id (a UUID). */\r\n async put(image: ImageInput, options: PutOptions<C> = {}): Promise<string> {\r\n await this.checkBeforePut(options.context);\r\n const row = await this.prepareRow(image, options);\r\n return this.backend.put(row, { connection: options.connection });\r\n }\r\n\r\n /**\r\n * Store many images. Best-effort, not all-or-nothing: one bad image does not\r\n * abort the rest. Check each result's `success` / `error`. Settings apply to\r\n * every item. Valid items are inserted in ONE transaction.\r\n */\r\n async putMany(\r\n images: ImageInput[],\r\n options: PutManyOptions<C> = {},\r\n ): Promise<BatchPutResult[]> {\r\n const { filenames } = options;\r\n if (filenames && filenames.length !== images.length) {\r\n throw new RangeError(\r\n \"filenames must be the same length as images if provided\",\r\n );\r\n }\r\n\r\n const fail = (index: number, error: string): BatchPutResult => ({\r\n index,\r\n imageId: null,\r\n error,\r\n success: false,\r\n });\r\n\r\n if (this.beforePut) {\r\n try {\r\n if (!(await this.beforePut(options.context)))\r\n return images.map((_, i) => fail(i, \"access denied\"));\r\n } catch (exc) {\r\n return images.map((_, i) => fail(i, errText(exc))); // fail closed, per item\r\n }\r\n }\r\n\r\n const results: (BatchPutResult | undefined)[] = new Array(images.length);\r\n const prepared: { index: number; row: PreparedRow }[] = [];\r\n\r\n await mapLimit(images, PREPARE_CONCURRENCY, async (image, i) => {\r\n try {\r\n const row = await this.prepareRow(image, {\r\n ...options,\r\n filename: filenames?.[i] ?? undefined,\r\n });\r\n prepared.push({ index: i, row });\r\n } catch (exc) {\r\n results[i] = fail(i, errText(exc));\r\n }\r\n });\r\n prepared.sort((a, b) => a.index - b.index);\r\n\r\n if (prepared.length > 0) {\r\n try {\r\n const ids = await this.backend.putMany(\r\n prepared.map((p) => p.row),\r\n { connection: options.connection },\r\n );\r\n prepared.forEach((p, n) => {\r\n results[p.index] = {\r\n index: p.index,\r\n imageId: ids[n]!,\r\n error: null,\r\n success: true,\r\n };\r\n });\r\n } catch (exc) {\r\n // The whole DB batch failed; every validated item shared that transaction.\r\n for (const p of prepared)\r\n results[p.index] = fail(p.index, errText(exc));\r\n }\r\n }\r\n return results as BatchPutResult[];\r\n }\r\n\r\n // ---- read path -------------------------------------------------------\r\n\r\n /** Retrieve a full image including bytes. Throws ImageNotFoundError if missing. */\r\n async get(imageId: string, options: GetOptions<C> = {}): Promise<Image> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const record = await this.backend.get(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!record) throw new ImageNotFoundError(imageId);\r\n return {\r\n data: record.data,\r\n mimeType: record.mimeType,\r\n filename: record.originalFilename,\r\n sizeBytes: record.sizeBytes,\r\n width: record.width,\r\n height: record.height,\r\n checksumSha256: record.checksumSha256,\r\n };\r\n }\r\n\r\n /**\r\n * Retrieve many images in one query. Results come back in input order; a\r\n * missing id is `error: \"not found\"`, not an exception. A hook denial or hook\r\n * exception affects only that id.\r\n */\r\n async getMany(\r\n imageIds: string[],\r\n options: GetOptions<C> = {},\r\n ): Promise<BatchGetResult[]> {\r\n const results: (BatchGetResult | undefined)[] = new Array(imageIds.length);\r\n const allowedIdx: number[] = [];\r\n\r\n for (let i = 0; i < imageIds.length; i++) {\r\n const imageId = imageIds[i]!;\r\n if (!isValidId(imageId)) {\r\n // Same as a missing id, and the hook never sees it (see checkBeforeGet).\r\n results[i] = { imageId, image: null, error: \"not found\", success: false };\r\n continue;\r\n }\r\n if (this.beforeGet) {\r\n try {\r\n if (!(await this.beforeGet(imageId, options.context))) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: \"access denied\",\r\n success: false,\r\n };\r\n continue;\r\n }\r\n } catch (exc) {\r\n results[i] = {\r\n imageId,\r\n image: null,\r\n error: errText(exc),\r\n success: false,\r\n };\r\n continue;\r\n }\r\n }\r\n allowedIdx.push(i);\r\n }\r\n\r\n if (allowedIdx.length > 0) {\r\n const records = await this.backend.getMany(\r\n allowedIdx.map((i) => imageIds[i]!),\r\n { connection: options.connection },\r\n );\r\n const byId = new Map(records.map((r) => [r.id.toLowerCase(), r]));\r\n for (const i of allowedIdx) {\r\n const imageId = imageIds[i]!;\r\n const r = byId.get(imageId.toLowerCase());\r\n results[i] = r\r\n ? {\r\n imageId,\r\n image: {\r\n data: r.data,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n },\r\n error: null,\r\n success: true,\r\n }\r\n : { imageId, image: null, error: \"not found\", success: false };\r\n }\r\n }\r\n return results as BatchGetResult[];\r\n }\r\n\r\n /**\r\n * Stream an image's bytes in chunks without holding the whole image in memory.\r\n * Not-found is checked eagerly: the returned promise rejects before any chunk\r\n * is read. A concurrent delete mid-stream makes iteration throw StorageError\r\n * rather than silently truncating. Pass `range` for partial reads.\r\n *\r\n * Honest limitation: reduces Node-side memory, not Postgres-side (the server\r\n * still handles the full BYTEA value per chunk request).\r\n */\r\n async getStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<AsyncIterable<Buffer>> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const { range } = options;\r\n if (range) {\r\n if (\r\n !Number.isInteger(range.start) ||\r\n range.start < 0 ||\r\n (range.end !== undefined &&\r\n (!Number.isInteger(range.end) || range.end < range.start))\r\n ) {\r\n throw new RangeError(\r\n \"range must be { start >= 0, end >= start } with integer values\",\r\n );\r\n }\r\n }\r\n const stream = await this.backend.getStream(imageId, {\r\n chunkSize: options.chunkSize ?? DEFAULT_STREAM_CHUNK_SIZE,\r\n connection: options.connection,\r\n range: range\r\n ? { start: range.start, end: range.end ?? Number.MAX_SAFE_INTEGER }\r\n : undefined,\r\n });\r\n if (!stream) throw new ImageNotFoundError(imageId);\r\n return stream;\r\n }\r\n\r\n /** Same as getStream(), as a Web `ReadableStream` you can hand straight to `new Response(...)`. */\r\n async toWebStream(\r\n imageId: string,\r\n options: StreamOptions<C> = {},\r\n ): Promise<ReadableStream<Uint8Array>> {\r\n const iterable = await this.getStream(imageId, options);\r\n const iterator = iterable[Symbol.asyncIterator]();\r\n return new ReadableStream<Uint8Array>({\r\n async pull(controller) {\r\n try {\r\n const { value, done } = await iterator.next();\r\n if (done) controller.close();\r\n else\r\n controller.enqueue(\r\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength),\r\n );\r\n } catch (exc) {\r\n controller.error(exc);\r\n }\r\n },\r\n async cancel() {\r\n await iterator.return?.();\r\n },\r\n });\r\n }\r\n\r\n /** Write an image to `destination` chunk by chunk (honouring backpressure). Returns bytes written. Does not end the destination. */\r\n async streamTo(\r\n imageId: string,\r\n destination: WritableLike,\r\n options: StreamOptions<C> = {},\r\n ): Promise<number> {\r\n let total = 0;\r\n for await (const chunk of await this.getStream(imageId, options)) {\r\n const result = destination.write(chunk);\r\n if (result === false && typeof destination.once === \"function\") {\r\n await new Promise<void>((resolve) =>\r\n destination.once!(\"drain\", resolve),\r\n );\r\n } else if (\r\n result &&\r\n typeof (result as Promise<void>).then === \"function\"\r\n ) {\r\n await result;\r\n }\r\n total += chunk.length;\r\n }\r\n return total;\r\n }\r\n\r\n /** Metadata without pulling the bytes. Gated by the same beforeGet hook as get(). */\r\n async metadata(\r\n imageId: string,\r\n options: GetOptions<C> = {},\r\n ): Promise<ImageMetadata> {\r\n await this.checkBeforeGet(imageId, options.context);\r\n const r = await this.backend.getMetadata(imageId, {\r\n connection: options.connection,\r\n });\r\n if (!r) throw new ImageNotFoundError(imageId);\r\n return {\r\n imageId: r.id,\r\n mimeType: r.mimeType,\r\n filename: r.originalFilename,\r\n sizeBytes: r.sizeBytes,\r\n width: r.width,\r\n height: r.height,\r\n checksumSha256: r.checksumSha256,\r\n };\r\n }\r\n\r\n /** Whether an image exists. Deliberately NOT gated by beforeGet (returns no data or metadata). */\r\n exists(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.exists(imageId, options);\r\n }\r\n\r\n /** Delete an image. Returns true if it existed. */\r\n delete(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n return this.backend.delete(imageId, options);\r\n }\r\n\r\n /** Delete many in one query. Results are in input order; a missing id is `deleted: false`, not an error. */\r\n async deleteMany(\r\n imageIds: string[],\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<BatchDeleteResult[]> {\r\n if (imageIds.length === 0) return [];\r\n const deleted = new Set(\r\n (await this.backend.deleteMany(imageIds, options)).map((id) =>\r\n id.toLowerCase(),\r\n ),\r\n );\r\n return imageIds.map((imageId) => ({\r\n imageId,\r\n deleted: deleted.has(imageId.toLowerCase()),\r\n error: null,\r\n success: true,\r\n }));\r\n }\r\n\r\n /**\r\n * Move an image's bytes from Postgres into object storage; reads, streams and\r\n * deletes keep working transparently. Returns true if tiered now, false if it was\r\n * already tiered (safe to re-run in a backfill). Throws ImageNotFoundError if missing.\r\n *\r\n * The upload runs inside the database transaction: if it fails, the row is left\r\n * completely untouched. Explicit and opt-in: put() never tiers automatically.\r\n * Not gated by beforeGet/beforePut (it is a maintenance operation, as in the Python package).\r\n */\r\n async tierToObjectStorage(\r\n imageId: string,\r\n options: Pick<CallOptions, \"connection\"> = {},\r\n ): Promise<boolean> {\r\n const result = await this.backend.tierToObjectStorage(imageId, options);\r\n if (result === null) throw new ImageNotFoundError(imageId);\r\n return result;\r\n }\r\n\r\n /**\r\n * One-time, non-destructive copy of classic-mode images into a dedup instance's tables, keeping every\r\n * id. Call it on an instance created with `dedup: true`. Safe to re-run. See PostgresBackend for details.\r\n */\r\n async migrateClassicToDedup(\r\n opts?: MigrateToDedupOptions,\r\n ): Promise<MigrateToDedupResult> {\r\n if (!this.backend.migrateClassicToDedup) {\r\n throw new StorageError(\r\n \"This backend does not support migrateClassicToDedup().\",\r\n );\r\n }\r\n return this.backend.migrateClassicToDedup(opts);\r\n }\r\n\r\n /** Release the pool (if ZeroBucket created it). */\r\n close(): Promise<void> {\r\n return this.backend.close();\r\n }\r\n}\r\n\r\n// ---- input handling ----------------------------------------------------\r\n\r\n/**\r\n * Read any supported input into a Buffer. Streams are consumed in bounded\r\n * chunks and rejected as soon as they pass maxBytes (reported size is then a\r\n * lower bound), rather than buffering an arbitrarily large upload first.\r\n * Files and Blobs are size-checked BEFORE their bytes are read.\r\n */\r\nexport async function readInput(\r\n image: ImageInput,\r\n filename: string | undefined,\r\n maxBytes: number,\r\n): Promise<{ data: Buffer; filename: string | undefined }> {\r\n if (typeof image === \"string\") {\r\n const size = (await stat(image)).size;\r\n if (size > maxBytes) throw new ImageTooLargeError(size, maxBytes);\r\n return {\r\n data: await readFile(image),\r\n filename: filename ?? basename(image),\r\n };\r\n }\r\n if (Buffer.isBuffer(image)) return { data: image, filename };\r\n if (image instanceof Uint8Array)\r\n return {\r\n data: Buffer.from(image.buffer, image.byteOffset, image.byteLength),\r\n filename,\r\n };\r\n if (image instanceof ArrayBuffer)\r\n return { data: Buffer.from(image), filename };\r\n if (typeof Blob !== \"undefined\" && image instanceof Blob) {\r\n if (image.size > maxBytes)\r\n throw new ImageTooLargeError(image.size, maxBytes);\r\n const name = (image as { name?: unknown }).name;\r\n return {\r\n data: Buffer.from(await image.arrayBuffer()),\r\n filename: filename ?? (typeof name === \"string\" ? name : undefined),\r\n };\r\n }\r\n if (\r\n image &&\r\n typeof (image as AsyncIterable<unknown>)[Symbol.asyncIterator] ===\r\n \"function\"\r\n ) {\r\n const chunks: Buffer[] = [];\r\n let total = 0;\r\n for await (const chunk of image as AsyncIterable<Uint8Array | string>) {\r\n const buf =\r\n typeof chunk === \"string\"\r\n ? Buffer.from(chunk)\r\n : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);\r\n total += buf.length;\r\n if (total > maxBytes) {\r\n (image as { destroy?: () => void }).destroy?.();\r\n throw new ImageTooLargeError(total, maxBytes);\r\n }\r\n chunks.push(buf);\r\n }\r\n const p = (image as { path?: unknown }).path;\r\n return {\r\n data: Buffer.concat(chunks, total),\r\n filename: filename ?? (typeof p === \"string\" ? basename(p) : undefined),\r\n };\r\n }\r\n throw new TypeError(\r\n \"Unsupported image input. Pass a Buffer, Uint8Array, file path string, Blob/File, or a readable stream.\",\r\n );\r\n}\r\n\r\nasync function mapLimit<T>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T, index: number) => Promise<void>,\r\n): Promise<void> {\r\n let next = 0;\r\n const workers = Array.from(\r\n { length: Math.min(limit, items.length) },\r\n async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n await fn(items[i]!, i);\r\n }\r\n },\r\n );\r\n await Promise.all(workers);\r\n}\r\n\r\nfunction errText(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}","/** Helpers shared by every storage adapter (kept here so Postgres and SQLite cannot drift apart). */\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type { StoredRecord } from \"./base.js\";\r\n\r\n/** 1 MiB. Default chunk size for getStream(). */\r\nexport const DEFAULT_STREAM_CHUNK_SIZE = 1024 * 1024;\r\n\r\nconst UUID_RE =\r\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\r\n\r\n/** True if `id` is a syntactically valid UUID. Malformed ids can never exist, so they short-circuit to \"not found\". */\r\nexport function isValidId(id: string): boolean {\r\n return typeof id === \"string\" && UUID_RE.test(id);\r\n}\r\n\r\nexport function msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport function wrap(prefix: string, exc: unknown): StorageError {\r\n return exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`${prefix}: ${msg(exc)}`, { cause: exc });\r\n}\r\n\r\nexport function sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\nexport async function mapLimit<T, R>(\r\n items: T[],\r\n limit: number,\r\n fn: (item: T) => Promise<R>,\r\n): Promise<R[]> {\r\n const out: R[] = new Array(items.length);\r\n let next = 0;\r\n await Promise.all(\r\n Array.from({ length: Math.min(limit, items.length) }, async () => {\r\n while (next < items.length) {\r\n const i = next++;\r\n out[i] = await fn(items[i]!);\r\n }\r\n }),\r\n );\r\n return out;\r\n}\r\n\r\nexport function tieredWithoutStorage(\r\n id: string,\r\n key: string | null,\r\n): StorageError {\r\n return new StorageError(\r\n `Image ${JSON.stringify(id)} is stored in object storage (key=${JSON.stringify(key)}) ` +\r\n \"but this ZeroBucket was constructed without `objectStorage`. Configure it with the \" +\r\n \"same bucket and credentials that were used to tier this image.\",\r\n );\r\n}\r\n\r\n/** Turn a DB row into a record, fetching the bytes from object storage if the row is tiered. */\r\nexport async function hydrateRow(\r\n row: any,\r\n id: string,\r\n os: ObjectStorageLike | undefined,\r\n): Promise<StoredRecord> {\r\n let data: Buffer;\r\n if (row.storage_backend === \"object_storage\") {\r\n if (!os) throw tieredWithoutStorage(id, row.object_storage_key);\r\n data = await os.download(row.object_storage_key);\r\n if (data.length !== row.size_bytes) {\r\n throw new StorageError(\r\n `Object ${JSON.stringify(row.object_storage_key)} for image ${JSON.stringify(id)} has ${data.length} bytes ` +\r\n `but the database records ${row.size_bytes}. The stored object is corrupted or was replaced.`,\r\n );\r\n }\r\n } else {\r\n data = row.data as Buffer;\r\n }\r\n return {\r\n id: String(row.id),\r\n data,\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n}\r\n\r\n/** Ranged reads straight from the object store; fails loudly rather than ever truncating. */\r\nexport async function* streamFromObjectStorage(\r\n os: ObjectStorageLike,\r\n id: string,\r\n key: string,\r\n start: number,\r\n end: number,\r\n chunkSize: number,\r\n): AsyncGenerator<Buffer> {\r\n let offset = start;\r\n while (offset <= end) {\r\n const last = Math.min(offset + chunkSize - 1, end);\r\n const expected = last - offset + 1;\r\n const chunk = await os.downloadRange(key, offset, last);\r\n if (chunk.length !== expected) {\r\n throw new StorageError(\r\n `Object ${JSON.stringify(key)} for image ${JSON.stringify(id)} returned ${chunk.length} bytes for a ${expected}-byte range; ` +\r\n \"the stored object is shorter than the database records.\",\r\n );\r\n }\r\n yield chunk;\r\n offset = last + 1;\r\n }\r\n}\r\n\r\n/** 1024-based, like Postgres' pg_size_pretty: \"812 bytes\", \"13.2 kB\", \"4.0 MB\". */\r\nexport function prettySize(bytes: number): string {\r\n if (bytes < 1024) return `${bytes} bytes`;\r\n const units = [\"kB\", \"MB\", \"GB\", \"TB\"];\r\n let v = bytes / 1024;\r\n let i = 0;\r\n while (v >= 1024 && i < units.length - 1) {\r\n v /= 1024;\r\n i += 1;\r\n }\r\n return `${v.toFixed(1)} ${units[i]}`;\r\n}\r\n","/**\r\n * PostgreSQL storage adapter (classic mode).\r\n *\r\n * Uses the EXACT schema the Python package creates (zerobucket_images, including\r\n * the object-storage tiering columns and CHECK constraint), so a Node app and a\r\n * Python app can share one database and read each other's rows.\r\n *\r\n * Lessons from the Python build, applied here from day one:\r\n * - Retry re-runs the WHOLE unit of work on a fresh connection after a full\r\n * ROLLBACK. It never replays a single statement of a multi-statement\r\n * transaction (the \"half-applied put\" hazard).\r\n * - Retry never applies when the caller passes `connection` (the caller owns\r\n * the transaction; restarting it is their decision).\r\n * - Pool acquire has an explicit timeout.\r\n * - Migration runs under an advisory lock so N concurrent first-callers do not\r\n * race on CREATE TABLE.\r\n */\r\nimport type { Pool, PoolClient, PoolConfig } from \"pg\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n OperationEvent,\r\n PreparedRow,\r\n Queryable,\r\n} from \"../types.js\";\r\nimport {\r\n DEFAULT_STREAM_CHUNK_SIZE,\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\nimport type {\r\n CallOptions,\r\n ImageListEntry,\r\n ImageListOptions,\r\n MigrateToDedupOptions,\r\n MigrateToDedupResult,\r\n StorageBackend,\r\n StorageInfo,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n\r\n-- Object-storage tiering columns (additive; identical to the Python package).\r\nALTER TABLE zerobucket_images ALTER COLUMN data DROP NOT NULL;\r\nALTER TABLE zerobucket_images\r\n ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'postgres';\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_bucket TEXT;\r\nALTER TABLE zerobucket_images ADD COLUMN IF NOT EXISTS object_storage_key TEXT;\r\nDO $$\r\nBEGIN\r\n IF NOT EXISTS (\r\n SELECT 1 FROM pg_constraint WHERE conname = 'zerobucket_storage_location_check'\r\n ) THEN\r\n ALTER TABLE zerobucket_images ADD CONSTRAINT zerobucket_storage_location_check\r\n CHECK (\r\n (storage_backend = 'postgres'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n );\r\n END IF;\r\nEND $$;\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_storage_backend\r\n ON zerobucket_images (storage_backend) WHERE storage_backend != 'postgres';\r\n`;\r\n\r\nconst INSERT = `\r\nINSERT INTO zerobucket_images\r\n (data, mime_type, original_filename, size_bytes, width, height, checksum_sha256)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nRETURNING id;`;\r\n\r\n// How image bytes are read (measured: benchmarks/node, 0.10.1 notes in the CHANGELOG).\r\n//\r\n// A bytea column is sent as HEX TEXT by default: two characters per byte, so an 11.5 MB image crosses the wire as ~24 MB,\r\n// becomes one 24 MB string, and is decoded back (about 110 MB of transient memory). Reading it as base64 instead sends\r\n// ~16 MB and decodes it with Buffer.from(..., \"base64\"): about a third faster and about 40% less memory, byte-for-byte\r\n// identical. node-postgres's \"binary: true\" is NOT an option: its protocol parser turns every field into a UTF-8 string\r\n// first, so bytea comes back corrupted (and slower). The server-side encode costs the same as hex output.\r\n//\r\n// Postgres wraps base64 output every 76 characters; Buffer.from(..., \"base64\") ignores the line breaks.\r\n// The column is aliased data_b64 (full rows) / chunk (ranged reads) and decoded in hydrate() / getStream().\r\nconst SELECT_FULL = `\r\nSELECT id, encode(data, 'base64') AS data_b64, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\nconst SELECT_MANY = `\r\nSELECT id, encode(data, 'base64') AS data_b64, mime_type, original_filename, size_bytes, width, height,\r\n checksum_sha256, storage_backend, object_storage_bucket, object_storage_key\r\nFROM zerobucket_images WHERE id = ANY($1::uuid[]);`;\r\n\r\nconst SELECT_METADATA = `\r\nSELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256\r\nFROM zerobucket_images WHERE id = $1;`;\r\n\r\n// substring() is 1-indexed and clamps `length` at the value's end.\r\nconst SELECT_CHUNK = `SELECT encode(substring(data FROM $1::int FOR $2::int), 'base64') AS chunk FROM zerobucket_images WHERE id = $3;`;\r\nconst SELECT_STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = $1;`;\r\nconst DELETE_RETURNING = `DELETE FROM zerobucket_images WHERE id = $1 RETURNING storage_backend, object_storage_key;`;\r\nconst DELETE_MANY = `DELETE FROM zerobucket_images WHERE id = ANY($1::uuid[]) RETURNING id, storage_backend, object_storage_key;`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = $1;`;\r\n\r\n// FOR UPDATE: a per-row lock held until the transaction ends (see tierToObjectStorage).\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = $1 FOR UPDATE;`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = $1, object_storage_key = $2, updated_at = now() WHERE id = $3;`;\r\n\r\n// ---- dedup mode: content-addressed blobs + per-image refs (same schema as the Python package) ----\r\nconst DEDUP_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 CHAR(64) PRIMARY KEY,\r\n data BYTEA NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n ref_count INTEGER NOT NULL DEFAULT 0,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\r\n checksum_sha256 CHAR(64) NOT NULL REFERENCES zerobucket_blobs(checksum_sha256),\r\n original_filename TEXT,\r\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\r\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_checksum ON zerobucket_image_refs (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_created_at ON zerobucket_image_refs (created_at);\r\n`;\r\n\r\n// $7 is how many references this call adds (1 for put; N when a batch holds N identical images).\r\n// ON CONFLICT makes the increment atomic at the row level: 20 concurrent puts of the same bytes give ref_count == 20.\r\nconst D_UPSERT_BLOB = `\r\nINSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count)\r\nVALUES ($1, $2, $3, $4, $5, $6, $7)\r\nON CONFLICT (checksum_sha256) DO UPDATE SET ref_count = zerobucket_blobs.ref_count + EXCLUDED.ref_count;`;\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (checksum_sha256, original_filename) VALUES ($1, $2) RETURNING id;`;\r\n// Aliased to the classic column names so one row mapper serves both modes.\r\nconst D_COLS = `r.id, encode(b.data, 'base64') AS data_b64, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256,\r\n 'postgres'::text AS storage_backend, NULL::text AS object_storage_bucket, NULL::text AS object_storage_key`;\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = $1 RETURNING checksum_sha256;`;\r\nconst D_DELETE_REFS = `DELETE FROM zerobucket_image_refs WHERE id = ANY($1::uuid[]) RETURNING id, checksum_sha256;`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - $1 WHERE checksum_sha256 = $2 RETURNING ref_count;`;\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ANY($1::text[]) AND ref_count <= 0;`;\r\n\r\ninterface SqlSet {\r\n schema: string;\r\n selectFull: string;\r\n selectMany: string;\r\n selectMetadata: string;\r\n selectStreamInfo: string;\r\n selectChunk: string;\r\n exists: string;\r\n}\r\nconst CLASSIC_SQL: SqlSet = {\r\n schema: SCHEMA,\r\n selectFull: SELECT_FULL,\r\n selectMany: SELECT_MANY,\r\n selectMetadata: SELECT_METADATA,\r\n selectStreamInfo: SELECT_STREAM_INFO,\r\n selectChunk: SELECT_CHUNK,\r\n exists: EXISTS,\r\n};\r\nconst DEDUP_SQL: SqlSet = {\r\n schema: DEDUP_SCHEMA,\r\n selectFull: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = $1;`,\r\n selectMany: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ANY($1::uuid[]);`,\r\n selectMetadata: `SELECT r.id, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256 ${D_FROM} WHERE r.id = $1;`,\r\n selectStreamInfo: `SELECT b.size_bytes, 'postgres'::text AS storage_backend, NULL::text AS object_storage_key ${D_FROM} WHERE r.id = $1;`,\r\n selectChunk: `SELECT encode(substring(b.data FROM $1::int FOR $2::int), 'base64') AS chunk ${D_FROM} WHERE r.id = $3;`,\r\n exists: `SELECT 1 FROM zerobucket_image_refs WHERE id = $1;`,\r\n};\r\n\r\nexport { isValidId };\r\n\r\n// SQLSTATEs worth retrying: transient conditions only, never constraint/syntax errors.\r\nconst RETRYABLE_SQLSTATES = new Set([\r\n \"40001\", // serialization_failure\r\n \"40P01\", // deadlock_detected\r\n \"08000\", // connection_exception\r\n \"08003\", // connection_does_not_exist\r\n \"08006\", // connection_failure\r\n \"08001\", // sqlclient_unable_to_establish_sqlconnection\r\n \"08004\", // sqlserver_rejected_establishment_of_sqlconnection\r\n \"57P01\", // admin_shutdown\r\n \"57P02\", // crash_shutdown\r\n \"57P03\", // cannot_connect_now\r\n \"53000\", // insufficient_resources\r\n \"53300\", // too_many_connections\r\n]);\r\nconst RETRYABLE_NODE_CODES = new Set([\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"ETIMEDOUT\",\r\n \"EPIPE\",\r\n \"ECONNABORTED\",\r\n]);\r\nconst CONNECTION_MESSAGE_RE =\r\n /connection terminated|connection timeout|timeout exceeded when trying to connect|client has encountered a connection error/i;\r\n\r\n/** Transient (worth retrying) vs. permanent (fails identically every time). */\r\nexport function isRetryable(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (typeof code === \"string\") {\r\n if (RETRYABLE_SQLSTATES.has(code) || RETRYABLE_NODE_CODES.has(code))\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\n/** Errors after which the pooled connection must be destroyed, not returned to the pool. */\r\nfunction isConnectionLevel(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const code = (exc as { code?: unknown }).code;\r\n if (\r\n typeof code === \"string\" &&\r\n (RETRYABLE_NODE_CODES.has(code) ||\r\n code.startsWith(\"08\") ||\r\n code.startsWith(\"57\"))\r\n ) {\r\n return true;\r\n }\r\n return CONNECTION_MESSAGE_RE.test(exc.message);\r\n}\r\n\r\nconst MAX_BACKOFF_MS = 2000;\r\n\r\nexport function backoffDelayMs(attempt: number, baseDelayMs: number): number {\r\n const exponential = baseDelayMs * 2 ** (attempt - 1);\r\n const jitter = Math.random() * baseDelayMs;\r\n return Math.min(exponential + jitter, MAX_BACKOFF_MS);\r\n}\r\n\r\nexport interface PostgresBackendOptions {\r\n /** postgresql:// connection string. Required unless `pool` is given. */\r\n connectionString?: string;\r\n /** Bring your own pg.Pool (not closed by `close()`). Pool sizing options are then ignored. */\r\n pool?: Pool;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Retries for transient errors on pooled calls. Default 3; 0 disables. */\r\n maxRetries?: number;\r\n /** Base for exponential backoff with jitter (capped at 2s). Default 100. */\r\n retryBaseDelayMs?: number;\r\n poolMinSize?: number;\r\n poolMaxSize?: number;\r\n /** Max wait for a pooled connection. Default 10000. */\r\n poolTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n /**\r\n * Enables transparent reads of tiered rows and `tierToObjectStorage()`.\r\n * Configure it with the same bucket/credentials used to tier the images.\r\n */\r\n objectStorage?: ObjectStorageLike;\r\n /**\r\n * Content-addressed mode: identical bytes are stored once (zerobucket_blobs) and each\r\n * put() adds a lightweight reference (zerobucket_image_refs) with its own id and filename.\r\n * Separate tables from classic mode, so the two can never be confused. Cannot be combined\r\n * with `objectStorage`. Same schema as the Python package's dedup mode.\r\n */\r\n dedup?: boolean;\r\n}\r\n\r\ntype Work<T> = (q: Queryable) => Promise<T>;\r\n\r\nexport class PostgresBackend implements StorageBackend {\r\n private pool: Pool | undefined;\r\n private readonly ownsPool: boolean;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly maxRetries: number;\r\n private readonly retryBaseDelayMs: number;\r\n private readonly autoMigrate: boolean;\r\n private readonly dedup: boolean;\r\n private readonly sql: SqlSet;\r\n\r\n constructor(private readonly opts: PostgresBackendOptions) {\r\n if (!opts.connectionString && !opts.pool) {\r\n throw new TypeError(\r\n \"PostgresBackend requires either `connectionString` or `pool`\",\r\n );\r\n }\r\n this.ownsPool = !opts.pool;\r\n this.pool = opts.pool;\r\n this.maxRetries = opts.maxRetries ?? 3;\r\n this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 100;\r\n this.autoMigrate = opts.autoMigrate ?? true;\r\n this.dedup = opts.dedup ?? false;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n this.sql = this.dedup ? DEDUP_SQL : CLASSIC_SQL;\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined; // allow a later attempt (e.g. the DB was briefly down)\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n if (!this.pool) {\r\n let pg: { Pool: typeof Pool };\r\n try {\r\n const mod = (await import(\"pg\")) as unknown as {\r\n default?: { Pool: typeof Pool };\r\n Pool: typeof Pool;\r\n };\r\n pg = mod.default ?? mod; // CJS package: ESM gives { default }, some interop gives it directly\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"PostgresBackend requires the 'pg' package. Install it with: npm install pg\",\r\n { cause },\r\n );\r\n }\r\n const config: PoolConfig = {\r\n connectionString: this.opts.connectionString,\r\n min: this.opts.poolMinSize ?? 1,\r\n max: this.opts.poolMaxSize ?? 5,\r\n connectionTimeoutMillis: this.opts.poolTimeoutMs ?? 10_000,\r\n };\r\n this.pool = new pg.Pool(config);\r\n // An idle client erroring (server restart, network drop) must not crash the process.\r\n this.pool.on(\"error\", () => {});\r\n }\r\n try {\r\n if (this.autoMigrate) await this.migrate();\r\n else await this.pool.query(\"SELECT 1\");\r\n } catch (exc) {\r\n if (this.ownsPool) {\r\n await this.pool.end().catch(() => {});\r\n this.pool = undefined;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n }\r\n\r\n /** Create the schema if missing. Serialized across processes with an advisory lock. */\r\n async migrate(): Promise<void> {\r\n const pool = this.pool!;\r\n let client: PoolClient;\r\n try {\r\n client = await pool.connect();\r\n } catch (exc) {\r\n throw new StorageError(`Could not connect to PostgreSQL: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate'))\",\r\n );\r\n await client.query(this.sql.schema);\r\n await client.query(\"COMMIT\");\r\n client.release();\r\n } catch (exc) {\r\n await client.query(\"ROLLBACK\").catch(() => {});\r\n client.release(true);\r\n throw new StorageError(`Migration failed: ${msg(exc)}`, { cause: exc });\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const pool = this.pool;\r\n this.pool = undefined;\r\n this.readyPromise = undefined;\r\n if (pool && this.ownsPool) await pool.end();\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /**\r\n * Run `work` and return its result, emitting exactly one OperationEvent.\r\n *\r\n * connection given -> run once on it, no BEGIN/COMMIT, no retry.\r\n * connection absent -> pooled; `tx` wraps the work in BEGIN/COMMIT; transient\r\n * failures retry the WHOLE closure on a fresh connection.\r\n */\r\n private async run<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n work: Work<T>,\r\n { tx = false }: { tx?: boolean } = {},\r\n ): Promise<T> {\r\n if (connection) {\r\n if (!isQueryable(connection)) {\r\n throw new TypeError(\r\n \"PostgresBackend expects a pg client/pool client as `connection` (an object with query()), not a SQLite Database.\",\r\n );\r\n }\r\n const start = performance.now();\r\n try {\r\n const result = tx\r\n ? await this.atomic(connection, work)\r\n : await work(connection);\r\n this.emit(operation, start, true, null, 0);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), 0);\r\n throw exc;\r\n }\r\n }\r\n\r\n await this.ready();\r\n const start = performance.now();\r\n let attempt = 0;\r\n for (;;) {\r\n let client: PoolClient | undefined;\r\n let destroy = false;\r\n try {\r\n client = await this.pool!.connect();\r\n if (tx) await client.query(\"BEGIN\");\r\n const result = await work(client);\r\n if (tx) await client.query(\"COMMIT\");\r\n this.emit(operation, start, true, null, attempt);\r\n return result;\r\n } catch (exc) {\r\n if (client && tx) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true; // cannot trust a connection that cannot roll back\r\n }\r\n }\r\n if (isConnectionLevel(exc)) destroy = true;\r\n attempt += 1;\r\n if (attempt > this.maxRetries || !isRetryable(exc)) {\r\n this.emit(operation, start, false, msg(exc), attempt - 1);\r\n throw exc;\r\n }\r\n } finally {\r\n client?.release(destroy);\r\n }\r\n await sleep(backoffDelayMs(attempt, this.retryBaseDelayMs));\r\n }\r\n }\r\n\r\n /**\r\n * Make multi-statement work all-or-nothing on a CALLER-supplied connection.\r\n * - Caller has an open transaction -> SAVEPOINT: a failure rolls back only our work and the\r\n * caller's transaction stays usable; success joins their transaction.\r\n * - No open transaction (an autocommit pg.Client) -> open and commit our own, so a failure\r\n * can never leave half-applied state (e.g. an inflated dedup ref_count).\r\n */\r\n private async atomic<T>(conn: Queryable, work: Work<T>): Promise<T> {\r\n let ownTx = false;\r\n try {\r\n await conn.query(\"SAVEPOINT zerobucket_atomic\");\r\n } catch (exc) {\r\n if ((exc as { code?: string }).code !== \"25P01\") throw exc; // 25P01 = no active transaction\r\n ownTx = true;\r\n await conn.query(\"BEGIN\");\r\n }\r\n try {\r\n const result = await work(conn);\r\n await conn.query(\r\n ownTx ? \"COMMIT\" : \"RELEASE SAVEPOINT zerobucket_atomic\",\r\n );\r\n return result;\r\n } catch (exc) {\r\n try {\r\n await conn.query(\r\n ownTx ? \"ROLLBACK\" : \"ROLLBACK TO SAVEPOINT zerobucket_atomic\",\r\n );\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n if (this.dedup) {\r\n return await this.run(\r\n \"put\",\r\n connection,\r\n async (q) => {\r\n await q.query(D_UPSERT_BLOB, blobParams(row, 1));\r\n return String(\r\n (\r\n await q.query(D_INSERT_REF, [\r\n row.checksumSha256,\r\n row.originalFilename,\r\n ])\r\n ).rows[0].id,\r\n );\r\n },\r\n { tx: true },\r\n );\r\n }\r\n return await this.run(\"put\", connection, async (q) => {\r\n const res = await q.query(INSERT, rowParams(row));\r\n return String(res.rows[0].id);\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n if (this.dedup) {\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n // Identical images within one batch share a blob: count them, upsert each distinct\r\n // checksum ONCE in sorted order (a global lock order, so two overlapping batches\r\n // can never deadlock on each other's blob rows), then create the refs in input order.\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const g = groups.get(checksum)!;\r\n await q.query(D_UPSERT_BLOB, blobParams(g.row, g.n));\r\n }\r\n const ids: string[] = [];\r\n for (const r of rows) {\r\n ids.push(\r\n String(\r\n (\r\n await q.query(D_INSERT_REF, [\r\n r.checksumSha256,\r\n r.originalFilename,\r\n ])\r\n ).rows[0].id,\r\n ),\r\n );\r\n }\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n }\r\n // One transaction. A transient failure rolls back EVERYTHING, so a retry\r\n // from scratch cannot create duplicates. Statements are awaited one at a\r\n // time on the single connection (queuing concurrent queries on one pg\r\n // client is deprecated and breaks in pg@9), so result order matches input\r\n // order by construction.\r\n return await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n const ids: string[] = [];\r\n for (const r of rows)\r\n ids.push(String((await q.query(INSERT, rowParams(r))).rows[0].id));\r\n return ids;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectFull, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? this.hydrate(row, id) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.run(\r\n \"get_many\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMany, [valid])).rows,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) => this.hydrate(r, String(r.id)));\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMetadata, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectStreamInfo, [id])).rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(id, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n id,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n const self = this;\r\n\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start; // 0-based\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.run(\r\n \"get_stream\",\r\n connection,\r\n async (q) =>\r\n (await q.query(self.sql.selectChunk, [offset + 1, length, id]))\r\n .rows[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n // base64 text from encode(substring(...)); null when the row has no bytes, \"\" when the range is past the end\r\n const chunk: Buffer | null =\r\n row.chunk == null ? null : Buffer.from(row.chunk, \"base64\");\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n if (this.dedup) return this.deleteDedup(id, connection);\r\n try {\r\n const row = await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => (await q.query(DELETE_RETURNING, [id])).rows[0],\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObject([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n if (this.dedup) return this.deleteManyDedup(valid, connection);\r\n try {\r\n const rows = await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => (await q.query(DELETE_MANY, [valid])).rows,\r\n );\r\n await this.cleanupObject(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.run(\r\n \"exists\",\r\n connection,\r\n async (q) => ((await q.query(this.sql.exists, [id])).rowCount ?? 0) > 0,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- maintenance (CLI) -----------------------------------------------\r\n\r\n async getInfo(): Promise<StorageInfo | null> {\r\n try {\r\n return await this.run(\"info\", undefined, async (q) => {\r\n const imgTable = this.dedup\r\n ? \"zerobucket_image_refs\"\r\n : \"zerobucket_images\";\r\n const present = await q.query(\r\n \"SELECT to_regclass($1) AS a, to_regclass('zerobucket_blobs') AS b\",\r\n [imgTable],\r\n );\r\n if (!present.rows[0].a || (this.dedup && !present.rows[0].b))\r\n return null;\r\n const j = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const size = this.dedup ? \"b.size_bytes\" : \"size_bytes\";\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n const mime = this.dedup ? \"b.mime_type\" : \"mime_type\";\r\n const tot = (\r\n await q.query(\r\n `SELECT count(*)::int AS n, coalesce(sum(${size}), 0)::text AS bytes, min(${created})::text AS oldest, max(${created})::text AS newest ${j}`,\r\n )\r\n ).rows[0];\r\n const disk = (\r\n await q.query(\r\n this.dedup\r\n ? \"SELECT pg_size_pretty(pg_total_relation_size('zerobucket_blobs') + pg_total_relation_size('zerobucket_image_refs')) AS s\"\r\n : \"SELECT pg_size_pretty(pg_total_relation_size('zerobucket_images')) AS s\",\r\n )\r\n ).rows[0].s as string;\r\n const fmt = (\r\n await q.query(\r\n `SELECT ${mime} AS m, count(*)::int AS n, coalesce(sum(${size}), 0)::text AS bytes ${j} GROUP BY ${mime} ORDER BY count(*) DESC, ${mime}`,\r\n )\r\n ).rows;\r\n const info: StorageInfo = {\r\n mode: this.dedup ? \"dedup\" : \"classic\",\r\n count: tot.n,\r\n totalBytes: Number(tot.bytes),\r\n // Postgres prints the offset as \"+00\"; Python's CLI (and ISO-8601) print \"+00:00\". Normalise so output is identical.\r\n oldest: tot.oldest && tot.oldest.replace(/([+-]\\d\\d)$/, \"$1:00\"),\r\n newest: tot.newest && tot.newest.replace(/([+-]\\d\\d)$/, \"$1:00\"),\r\n onDisk: disk,\r\n byFormat: fmt.map((r) => ({\r\n mimeType: r.m,\r\n count: r.n,\r\n bytes: Number(r.bytes),\r\n })),\r\n };\r\n if (this.dedup) {\r\n const b = (\r\n await q.query(\r\n \"SELECT count(*)::int AS n, coalesce(sum(size_bytes), 0)::text AS bytes FROM zerobucket_blobs\",\r\n )\r\n ).rows[0];\r\n info.dedup = { blobs: b.n, storedBytes: Number(b.bytes) };\r\n } else {\r\n const t = (\r\n await q.query(\r\n \"SELECT count(*)::int AS n, coalesce(sum(size_bytes), 0)::text AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'\",\r\n )\r\n ).rows[0];\r\n info.tiered = { count: t.n, bytes: Number(t.bytes) };\r\n }\r\n return info;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to read storage info\", exc);\r\n }\r\n }\r\n\r\n async listImages(o: ImageListOptions = {}): Promise<ImageListEntry[]> {\r\n const where: string[] = [];\r\n const params: unknown[] = [];\r\n const add = (sql: string, v: unknown) => (\r\n params.push(v),\r\n where.push(sql.replace(\"$?\", `$${params.length}`))\r\n );\r\n if (o.minSize !== undefined)\r\n add(`${this.dedup ? \"b.size_bytes\" : \"size_bytes\"} >= $?`, o.minSize);\r\n if (o.olderThanDays !== undefined)\r\n add(\r\n `${this.dedup ? \"r.created_at\" : \"created_at\"} <= now() - make_interval(days => $?::int)`,\r\n o.olderThanDays,\r\n );\r\n if (o.onlyUntiered && !this.dedup)\r\n where.push(\"storage_backend = 'postgres'\");\r\n const from = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const idCol = this.dedup ? \"r.id\" : \"id\";\r\n const tierCol = this.dedup\r\n ? \"false AS tiered\"\r\n : \"(storage_backend = 'object_storage') AS tiered\";\r\n const order =\r\n o.sample !== undefined\r\n ? \"ORDER BY random()\"\r\n : `ORDER BY ${this.dedup ? \"r.created_at\" : \"created_at\"}, ${idCol}`;\r\n const lim = o.sample ?? o.limit;\r\n let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? \" WHERE \" + where.join(\" AND \") : \"\"} ${order}`;\r\n if (lim !== undefined) {\r\n params.push(lim);\r\n sql += ` LIMIT $${params.length}`;\r\n }\r\n try {\r\n const rows = await this.run(\r\n \"list_images\",\r\n undefined,\r\n async (q) => (await q.query(sql, params)).rows,\r\n );\r\n return rows.map((r) => ({ id: String(r.id), tiered: Boolean(r.tiered) }));\r\n } catch (exc) {\r\n throw wrap(\"Failed to list images\", exc);\r\n }\r\n }\r\n\r\n // ---- dedup deletes / migration ---------------------------------------\r\n\r\n private async deleteDedup(\r\n id: string,\r\n connection: Connection | undefined,\r\n ): Promise<boolean> {\r\n try {\r\n return await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const ref = (await q.query(D_DELETE_REF, [id])).rows[0];\r\n if (!ref) return false; // id did not exist: nothing to decrement either\r\n const checksum = ref.checksum_sha256 as string;\r\n const dec = (await q.query(D_DECREMENT, [1, checksum])).rows[0];\r\n if (dec && dec.ref_count <= 0)\r\n await q.query(D_DELETE_EMPTY, [[checksum]]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n private async deleteManyDedup(\r\n ids: string[],\r\n connection: Connection | undefined,\r\n ): Promise<string[]> {\r\n try {\r\n return await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const deleted = (await q.query(D_DELETE_REFS, [ids])).rows;\r\n if (deleted.length === 0) return [];\r\n // Several deleted refs may share one blob: decrement each blob by its exact count,\r\n // in sorted checksum order (global lock order, so concurrent batches cannot deadlock).\r\n const counts = new Map<string, number>();\r\n for (const r of deleted)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n const emptied: string[] = [];\r\n for (const checksum of [...counts.keys()].sort()) {\r\n const dec = (\r\n await q.query(D_DECREMENT, [counts.get(checksum), checksum])\r\n ).rows[0];\r\n if (dec && dec.ref_count <= 0) emptied.push(checksum);\r\n }\r\n if (emptied.length > 0) await q.query(D_DELETE_EMPTY, [emptied]);\r\n return deleted.map((r) => String(r.id));\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n /**\r\n * One-time, NON-DESTRUCTIVE copy of every row in the classic `zerobucket_images` table into this\r\n * dedup instance's tables, preserving every id (external references keep working) and\r\n * deduplicating identical content on the way. The classic table is never modified.\r\n *\r\n * Rows are copied in small batches (bounded memory instead of loading the whole table);\r\n * re-running is safe (already-migrated ids are skipped and reported, not an error). Classic rows\r\n * already tiered to object storage are refused up front with a clear message (their bytes are\r\n * not in the database to copy) unless `skipTiered: true`, which skips and counts them instead.\r\n * Still all-or-nothing: one transaction.\r\n */\r\n async migrateClassicToDedup(\r\n opts: MigrateToDedupOptions = {},\r\n ): Promise<MigrateToDedupResult> {\r\n if (!this.dedup) {\r\n throw new StorageError(\r\n \"migrateClassicToDedup() must be called on an instance created with dedup: true.\",\r\n );\r\n }\r\n await this.ready();\r\n const client = await this.pool!.connect();\r\n let destroy = false;\r\n try {\r\n await client.query(\"BEGIN\");\r\n await client.query(\r\n \"SELECT pg_advisory_xact_lock(hashtext('zerobucket_migrate_dedup'))\",\r\n );\r\n if (\r\n !(await client.query(\"SELECT to_regclass('zerobucket_images') AS t\"))\r\n .rows[0].t\r\n ) {\r\n throw new StorageError(\r\n \"No classic zerobucket_images table found in this database -- nothing to migrate.\",\r\n );\r\n }\r\n let tieredSkipped = 0;\r\n const hasTierCol = (\r\n await client.query(\r\n \"SELECT 1 FROM information_schema.columns WHERE table_name = 'zerobucket_images' AND column_name = 'storage_backend'\",\r\n )\r\n ).rowCount;\r\n if (hasTierCol) {\r\n const tiered = (\r\n await client.query(\r\n \"SELECT count(*)::int AS n FROM zerobucket_images WHERE storage_backend <> 'postgres'\",\r\n )\r\n ).rows[0].n;\r\n if (tiered > 0 && !opts.skipTiered) {\r\n throw new StorageError(\r\n `${tiered} classic image(s) are tiered to object storage, so their bytes are not in the database to migrate. ` +\r\n \"Dedup mode does not support tiering. Migrate only an un-tiered table, bring those images back first, \" +\r\n \"or pass skipTiered: true (CLI: --skip-tiered) to skip them and migrate the rest.\",\r\n );\r\n }\r\n tieredSkipped = tiered;\r\n }\r\n // Only reached with tiered rows present when skipTiered is set: leave them out of the copy.\r\n const onlyInDb = hasTierCol && tieredSkipped > 0;\r\n\r\n const seen = new Set<string>();\r\n let migrated = 0;\r\n let already = 0;\r\n // created_at travels as TEXT, never a JS Date: Postgres keeps microseconds, a Date keeps only\r\n // milliseconds, and a truncated keyset cursor sorts before its own row (infinite loop).\r\n let cursor: { createdAt: string; id: string } | null = null;\r\n const BATCH = 20;\r\n for (;;) {\r\n const page: { rows: any[] } = cursor\r\n ? await client.query(\r\n `SELECT id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at::text AS created_at_txt\r\n FROM zerobucket_images WHERE (created_at, id) > ($1::timestamptz, $2::uuid)${onlyInDb ? \" AND storage_backend = 'postgres'\" : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n [cursor.createdAt, cursor.id],\r\n )\r\n : await client.query(\r\n `SELECT id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at::text AS created_at_txt\r\n FROM zerobucket_images${onlyInDb ? \" WHERE storage_backend = 'postgres'\" : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n );\r\n if (page.rows.length === 0) break;\r\n const last = page.rows[page.rows.length - 1];\r\n if (\r\n cursor &&\r\n cursor.createdAt === last.created_at_txt &&\r\n cursor.id === last.id\r\n ) {\r\n throw new StorageError(\r\n \"Migration made no progress (keyset cursor did not advance); aborting instead of looping.\",\r\n );\r\n }\r\n cursor = { createdAt: last.created_at_txt, id: last.id };\r\n\r\n const have = new Set(\r\n (\r\n await client.query(\r\n \"SELECT id FROM zerobucket_image_refs WHERE id = ANY($1::uuid[])\",\r\n [page.rows.map((r) => r.id)],\r\n )\r\n ).rows.map((r) => String(r.id)),\r\n );\r\n const fresh = page.rows.filter((r) => !have.has(String(r.id)));\r\n already += page.rows.length - fresh.length;\r\n\r\n const groups = new Map<string, { row: any; n: number }>();\r\n for (const r of fresh) {\r\n const g = groups.get(r.checksum_sha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksum_sha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const { row, n } = groups.get(checksum)!;\r\n await client.query(D_UPSERT_BLOB, [\r\n checksum,\r\n row.data,\r\n row.mime_type,\r\n row.size_bytes,\r\n row.width,\r\n row.height,\r\n n,\r\n ]);\r\n }\r\n for (const r of fresh) {\r\n await client.query(\r\n \"INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at) VALUES ($1, $2, $3, $4::timestamptz)\",\r\n [r.id, r.checksum_sha256, r.original_filename, r.created_at_txt],\r\n );\r\n seen.add(r.checksum_sha256);\r\n migrated += 1;\r\n }\r\n }\r\n await client.query(\"COMMIT\");\r\n return {\r\n imagesMigrated: migrated,\r\n distinctBlobsCreated: seen.size,\r\n duplicateReferencesFound: migrated - seen.size,\r\n alreadyMigrated: already,\r\n tieredSkipped,\r\n };\r\n } catch (exc) {\r\n try {\r\n await client.query(\"ROLLBACK\");\r\n } catch {\r\n destroy = true;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Migration to dedup failed: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n } finally {\r\n client.release(destroy);\r\n }\r\n }\r\n\r\n // ---- helpers ---------------------------------------------------------\r\n\r\n private hydrate(row: any, id: string): Promise<StoredRecord> {\r\n // The shared row mapper expects a Buffer in `data`; the query returns it base64-encoded (see SELECT_FULL). A tiered\r\n // row has no bytes in the database, so encode(NULL) is NULL and `data` stays null.\r\n row.data =\r\n row.data_b64 == null ? null : Buffer.from(row.data_b64, \"base64\");\r\n delete row.data_b64;\r\n return hydrateRow(row, id, this.opts.objectStorage);\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone. Ordering is deliberate:\r\n * once the row is deleted the image is correctly \"not found\" to everyone, so\r\n * a failed object delete only leaves a harmless orphan, whereas deleting the\r\n * object first and then failing the row delete would leave a row pointing at nothing.\r\n *\r\n * Never runs inside a caller-owned transaction (`connection`): we cannot know\r\n * whether it will commit, and destroying data before the commit outcome is known\r\n * could leave a rolled-back row pointing at a deleted object. In that case the\r\n * object stays until you remove it yourself after committing (objectStorage.delete(id)).\r\n * Failures are reported through onOperation and never fail the delete itself.\r\n */\r\n private async cleanupObject(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes out of Postgres into object storage, replacing the\r\n * row's `data` with a pointer. Returns:\r\n * null - no such image\r\n * false - already tiered (a safe no-op, so backfills can be re-run)\r\n * true - tiered just now\r\n *\r\n * SAFETY: the upload happens INSIDE the same transaction as the row lock\r\n * (SELECT ... FOR UPDATE) and the UPDATE that flips the row. If the upload\r\n * fails, the transaction rolls back and the row is untouched: there is no\r\n * moment when the bytes exist nowhere, or when a row claims to be tiered\r\n * without a completed upload.\r\n *\r\n * TRADEOFF: the row lock is held for the whole upload. Concurrent writes\r\n * to THIS image (delete, tier) wait; every other row is unaffected, and plain\r\n * reads are never blocked (Postgres MVCC). Meant for explicit maintenance, not hot paths.\r\n *\r\n * Retry is safe: a transient error replays the whole closure, and the\r\n * deterministic key (the image id) makes the re-upload an idempotent overwrite.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n }\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const key = id.toLowerCase();\r\n try {\r\n return await this.run(\r\n \"tier_to_object_storage\",\r\n connection,\r\n async (q) => {\r\n const row = (await q.query(SELECT_FOR_TIERING, [id])).rows[0];\r\n if (!row) return null;\r\n if (row.storage_backend !== \"postgres\") return false;\r\n await os.upload(key, row.data as Buffer, { mimeType: row.mime_type });\r\n await q.query(UPDATE_AFTER_TIERING, [os.bucket, key, id]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n\r\nfunction blobParams(r: PreparedRow, refs: number): unknown[] {\r\n return [\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n refs,\r\n ];\r\n}\r\n\r\nfunction rowParams(r: PreparedRow): unknown[] {\r\n return [\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n ];\r\n}\r\n\r\nfunction isQueryable(c: Connection): c is Queryable {\r\n return typeof (c as Queryable).query === \"function\";\r\n}\r\n","/**\r\n * SQLite storage adapter (classic mode, dedup mode, streaming, object-storage tiering).\r\n *\r\n * Uses the EXACT schema the Python package's SQLiteBackend creates, so a Node app and a\r\n * Python app can share one `.db` file: ids are TEXT UUIDs generated by the application, timestamps are\r\n * ISO-8601 TEXT, classic rows say storage_backend='sqlite', and tiered rows point at object storage.\r\n *\r\n * HOW THIS DIFFERS FROM THE POSTGRES ADAPTER (and why)\r\n *\r\n * - The driver (`better-sqlite3`) is SYNCHRONOUS. Every operation here is async only in signature: the\r\n * SQLite call itself runs on the event loop and returns almost immediately (it is a local file).\r\n * Because a blocked synchronous call would freeze the whole process, lock contention is never waited\r\n * out inside the driver. Each attempt uses a ~25 ms busy timeout and BUSY errors are retried here with\r\n * ASYNC sleeps, up to `busyTimeoutMs` in total, so the event loop stays free.\r\n * - One long-lived connection serves all normal operations. Each unit of work runs to completion\r\n * synchronously inside BEGIN IMMEDIATE ... COMMIT, so concurrent requests can never interleave inside\r\n * a transaction.\r\n * - Tiering awaits a network upload while holding a write transaction, so it uses its OWN dedicated\r\n * connection (otherwise other requests would run inside that open transaction and a rollback would\r\n * erase their writes). SQLite has no per-row locks: BEGIN IMMEDIATE locks the WHOLE DATABASE for the\r\n * duration of the upload. Writes from other PROCESSES wait (and time out after busyTimeoutMs); writes\r\n * from this process queue behind an in-process async lock. Reads are never blocked (WAL).\r\n * - WAL mode is a durable file-level setting. It is set at most once per backend instance, and only if\r\n * the file is not already in WAL mode (re-issuing it on every connection caused real lock contention\r\n * on Windows in the Python package).\r\n * - Streaming reads use ranged `substr(data, offset, length)` queries (better-sqlite3 has no incremental\r\n * blob handle). A row deleted mid-stream therefore raises StorageError, exactly like Postgres. (The\r\n * Python SYNC SQLite adapter instead keeps streaming from a WAL snapshot; pass your own `connection`\r\n * inside a transaction here if you need that.) SQLite still reads the whole blob per chunk, in native\r\n * memory: this bounds Node-side memory only.\r\n */\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { existsSync, statSync } from \"node:fs\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n OperationEvent,\r\n PreparedRow,\r\n SqliteDatabaseLike,\r\n} from \"../types.js\";\r\nimport type {\r\n CallOptions,\r\n ImageListEntry,\r\n ImageListOptions,\r\n MigrateToDedupOptions,\r\n MigrateToDedupResult,\r\n StorageBackend,\r\n StorageInfo,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\nimport {\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n prettySize,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\n\r\n// ---- schema (identical to the Python package) -------------------------------------------------\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id TEXT PRIMARY KEY,\r\n data BLOB,\r\n mime_type TEXT NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n checksum_sha256 TEXT NOT NULL,\r\n created_at TEXT NOT NULL,\r\n updated_at TEXT NOT NULL,\r\n storage_backend TEXT NOT NULL DEFAULT 'sqlite',\r\n object_storage_bucket TEXT,\r\n object_storage_key TEXT,\r\n CHECK (\r\n (storage_backend = 'sqlite'\r\n AND data IS NOT NULL\r\n AND object_storage_key IS NULL\r\n AND object_storage_bucket IS NULL)\r\n OR\r\n (storage_backend = 'object_storage'\r\n AND data IS NULL\r\n AND object_storage_key IS NOT NULL\r\n AND object_storage_bucket IS NOT NULL)\r\n )\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_checksum ON zerobucket_images (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_created_at ON zerobucket_images (created_at);\r\n`;\r\n\r\nconst DEDUP_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 TEXT PRIMARY KEY,\r\n data BLOB NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n size_bytes INTEGER NOT NULL,\r\n width INTEGER,\r\n height INTEGER,\r\n ref_count INTEGER NOT NULL DEFAULT 0,\r\n created_at TEXT NOT NULL\r\n);\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id TEXT PRIMARY KEY,\r\n checksum_sha256 TEXT NOT NULL REFERENCES zerobucket_blobs(checksum_sha256),\r\n original_filename TEXT,\r\n created_at TEXT NOT NULL,\r\n updated_at TEXT NOT NULL\r\n);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_checksum ON zerobucket_image_refs (checksum_sha256);\r\nCREATE INDEX IF NOT EXISTS idx_zerobucket_image_refs_created_at ON zerobucket_image_refs (created_at);\r\n`;\r\n\r\n// ---- SQL --------------------------------------------------------------------------------------\r\n\r\nconst COLS = `id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, storage_backend, object_storage_bucket, object_storage_key`;\r\nconst INSERT = `INSERT INTO zerobucket_images (id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\r\nconst SELECT_FULL = `SELECT ${COLS} FROM zerobucket_images WHERE id = ?`;\r\nconst selectMany = (n: number) =>\r\n `SELECT ${COLS} FROM zerobucket_images WHERE id IN (${placeholders(n)})`;\r\nconst SELECT_METADATA = `SELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256 FROM zerobucket_images WHERE id = ?`;\r\nconst STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = ?`;\r\nconst CHUNK = `SELECT substr(data, ?, ?) AS chunk FROM zerobucket_images WHERE id = ?`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = ?`;\r\nconst DELETE_ONE = `DELETE FROM zerobucket_images WHERE id = ? RETURNING storage_backend, object_storage_key`;\r\nconst deleteMany = (n: number) =>\r\n `DELETE FROM zerobucket_images WHERE id IN (${placeholders(n)}) RETURNING id, storage_backend, object_storage_key`;\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = ?`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = ?, object_storage_key = ?, updated_at = ? WHERE id = ?`;\r\n\r\n// dedup. Reads are aliased to the classic column names so one row mapper serves both modes.\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_COLS = `r.id AS id, b.data AS data, b.mime_type AS mime_type, r.original_filename AS original_filename, b.size_bytes AS size_bytes, b.width AS width, b.height AS height, r.checksum_sha256 AS checksum_sha256, 'sqlite' AS storage_backend, NULL AS object_storage_bucket, NULL AS object_storage_key`;\r\nconst D_UPSERT = `INSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (checksum_sha256) DO UPDATE SET ref_count = ref_count + excluded.ref_count`;\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`;\r\nconst D_SELECT_FULL = `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ?`;\r\nconst dSelectMany = (n: number) =>\r\n `SELECT ${D_COLS} ${D_FROM} WHERE r.id IN (${placeholders(n)})`;\r\nconst D_SELECT_METADATA = `SELECT r.id AS id, b.mime_type AS mime_type, r.original_filename AS original_filename, b.size_bytes AS size_bytes, b.width AS width, b.height AS height, r.checksum_sha256 AS checksum_sha256 ${D_FROM} WHERE r.id = ?`;\r\nconst D_STREAM_INFO = `SELECT b.size_bytes AS size_bytes, 'sqlite' AS storage_backend, NULL AS object_storage_key ${D_FROM} WHERE r.id = ?`;\r\nconst D_CHUNK = `SELECT substr(b.data, ?, ?) AS chunk ${D_FROM} WHERE r.id = ?`;\r\nconst D_EXISTS = `SELECT 1 FROM zerobucket_image_refs WHERE id = ?`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = ? RETURNING checksum_sha256`;\r\nconst dDeleteRefs = (n: number) =>\r\n `DELETE FROM zerobucket_image_refs WHERE id IN (${placeholders(n)}) RETURNING id, checksum_sha256`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - ? WHERE checksum_sha256 = ? RETURNING ref_count`;\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ? AND ref_count <= 0`;\r\n\r\n/** SQLite's default max bound variables is 999 on old builds; stay well under it. */\r\nconst IN_CHUNK = 500;\r\n/** Per-attempt busy wait inside the (synchronous) driver. Longer waits are done by async retry, never by blocking the loop. */\r\nconst ATTEMPT_BUSY_MS = 25;\r\nexport const DEFAULT_BUSY_TIMEOUT_MS = 10_000;\r\n\r\nfunction placeholders(n: number): string {\r\n return Array.from({ length: n }, () => \"?\").join(\", \");\r\n}\r\n\r\n/** Python-compatible ISO-8601 UTC with microseconds: 2026-10-03T12:00:00.123000+00:00 */\r\nfunction isoNow(): string {\r\n return new Date().toISOString().replace(\"Z\", \"000+00:00\");\r\n}\r\n\r\nfunction isBusy(exc: unknown): boolean {\r\n const code = (exc as { code?: unknown })?.code;\r\n return (\r\n typeof code === \"string\" &&\r\n (code.startsWith(\"SQLITE_BUSY\") || code.startsWith(\"SQLITE_LOCKED\"))\r\n );\r\n}\r\n\r\n/** The members of better-sqlite3's Database that this adapter uses beyond SqliteDatabaseLike. */\r\ninterface Db extends SqliteDatabaseLike {\r\n pragma(source: string, options?: { simple?: boolean }): unknown;\r\n close(): void;\r\n}\r\ntype DbCtor = new (path: string, options?: { timeout?: number }) => Db;\r\n\r\n/** Tiny async mutex: serializes in-process writers (and tiering) without blocking the event loop. */\r\nclass Mutex {\r\n private tail: Promise<void> = Promise.resolve();\r\n run<T>(fn: () => Promise<T>): Promise<T> {\r\n const prev = this.tail;\r\n let release!: () => void;\r\n this.tail = new Promise<void>((r) => (release = r));\r\n return prev.then(fn).finally(release);\r\n }\r\n}\r\n\r\nexport interface SQLiteBackendOptions {\r\n /** Database file path, or \":memory:\" for a throwaway in-process database (tiering is unavailable for \":memory:\"). */\r\n path: string;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Content-addressed mode (separate tables; same schema as the Python package). Cannot be combined with objectStorage. */\r\n dedup?: boolean;\r\n /** Enables tiering and transparent reads of tiered rows. */\r\n objectStorage?: ObjectStorageLike;\r\n /** Total time to keep retrying when another connection/process holds the write lock. Default 10000. */\r\n busyTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n}\r\n\r\nexport class SQLiteBackend implements StorageBackend {\r\n private db: Db | undefined;\r\n private Database: DbCtor | undefined;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly dedup: boolean;\r\n private readonly busyTimeoutMs: number;\r\n private readonly writeLock = new Mutex();\r\n\r\n constructor(private readonly opts: SQLiteBackendOptions) {\r\n if (!opts.path)\r\n throw new TypeError(\r\n 'SQLiteBackend requires a `path` (a file path or \":memory:\")',\r\n );\r\n this.dedup = opts.dedup ?? false;\r\n this.busyTimeoutMs = opts.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined;\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n try {\r\n const mod = (await import(\"better-sqlite3\")) as unknown as {\r\n default?: DbCtor;\r\n } & DbCtor;\r\n this.Database = mod.default ?? mod;\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"SQLiteBackend requires the 'better-sqlite3' package. Install it with: npm install better-sqlite3\",\r\n { cause },\r\n );\r\n }\r\n const db = new this.Database(this.opts.path, { timeout: ATTEMPT_BUSY_MS });\r\n try {\r\n await this.retrying(() => {\r\n // WAL is durable and file-level: only convert if the file is not already WAL (avoids needless\r\n // lock contention when many processes open the same fresh file at once).\r\n if (\r\n this.opts.path !== \":memory:\" &&\r\n db.pragma(\"journal_mode\", { simple: true }) !== \"wal\"\r\n ) {\r\n db.pragma(\"journal_mode = WAL\");\r\n }\r\n db.pragma(\"foreign_keys = ON\");\r\n if (this.opts.autoMigrate ?? true)\r\n db.exec(this.dedup ? DEDUP_SCHEMA : SCHEMA);\r\n });\r\n } catch (exc) {\r\n try {\r\n db.close();\r\n } catch {\r\n // ignore\r\n }\r\n throw new StorageError(\r\n `Could not open SQLite database ${JSON.stringify(this.opts.path)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n this.db = db;\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const db = this.db;\r\n this.db = undefined;\r\n this.readyPromise = undefined;\r\n // let any in-flight write finish before the handle goes away\r\n await this.writeLock.run(async () => {\r\n db?.close();\r\n });\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /** Re-run `fn` while another connection holds a lock, sleeping asynchronously between attempts. `fn` must be atomic. */\r\n private async retrying<T>(\r\n fn: () => T,\r\n onRetry?: (n: number) => void,\r\n ): Promise<T> {\r\n const deadline = performance.now() + this.busyTimeoutMs;\r\n let attempt = 0;\r\n for (;;) {\r\n try {\r\n return fn();\r\n } catch (exc) {\r\n if (!isBusy(exc) || performance.now() >= deadline) throw exc;\r\n attempt += 1;\r\n onRetry?.(attempt);\r\n await sleep(\r\n Math.min(5 * 2 ** Math.min(attempt, 5), 100) + Math.random() * 5,\r\n );\r\n }\r\n }\r\n }\r\n\r\n /** BEGIN IMMEDIATE takes the write lock up front, so a BUSY comes before any work and the whole unit is safely re-runnable. */\r\n private tx<T>(db: Db, work: (db: Db) => T): T {\r\n db.exec(\"BEGIN IMMEDIATE\");\r\n try {\r\n const result = work(db);\r\n db.exec(\"COMMIT\");\r\n return result;\r\n } catch (exc) {\r\n try {\r\n db.exec(\"ROLLBACK\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n /**\r\n * All-or-nothing work on a CALLER-supplied handle. SAVEPOINT works whether or not the caller has an open\r\n * transaction: inside one it nests (a failure rolls back only our work); outside one it begins and, on\r\n * RELEASE, commits.\r\n */\r\n private atomic<T>(db: Db, work: (db: Db) => T): T {\r\n db.exec(\"SAVEPOINT zerobucket_atomic\");\r\n try {\r\n const result = work(db);\r\n db.exec(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n return result;\r\n } catch (exc) {\r\n try {\r\n db.exec(\"ROLLBACK TO SAVEPOINT zerobucket_atomic\");\r\n db.exec(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n private async execute<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n mode: \"read\" | \"write\",\r\n work: (db: Db) => T,\r\n ): Promise<T> {\r\n const start = performance.now();\r\n let retries = 0;\r\n try {\r\n let result: T;\r\n if (connection) {\r\n if (typeof (connection as SqliteDatabaseLike).prepare !== \"function\") {\r\n throw new TypeError(\r\n \"SQLiteBackend expects a better-sqlite3 Database as `connection` (an object with prepare()), not a pg client.\",\r\n );\r\n }\r\n const db = connection as Db;\r\n result = mode === \"write\" ? this.atomic(db, work) : work(db);\r\n } else {\r\n await this.ready();\r\n const db = this.db!;\r\n const onRetry = (n: number) => (retries = n);\r\n result =\r\n mode === \"write\"\r\n ? await this.writeLock.run(() =>\r\n this.retrying(() => this.tx(db, work), onRetry),\r\n )\r\n : await this.retrying(() => work(db), onRetry);\r\n }\r\n this.emit(operation, start, true, null, retries);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), retries);\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n return await this.execute(\"put\", connection, \"write\", (db) =>\r\n this.insertOne(db, row),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n private insertOne(db: Db, r: PreparedRow): string {\r\n const id = randomUUID();\r\n const now = isoNow();\r\n if (this.dedup) {\r\n db.prepare(D_UPSERT).run(\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n 1,\r\n now,\r\n );\r\n db.prepare(D_INSERT_REF).run(\r\n id,\r\n r.checksumSha256,\r\n r.originalFilename,\r\n now,\r\n now,\r\n );\r\n } else {\r\n db.prepare(INSERT).run(\r\n id,\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n now,\r\n now,\r\n );\r\n }\r\n return id;\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n return await this.execute(\"put_many\", connection, \"write\", (db) => {\r\n if (!this.dedup) return rows.map((r) => this.insertOne(db, r));\r\n // Identical images in one batch share a blob: count them and upsert each distinct checksum once.\r\n const now = isoNow();\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const [checksum, { row, n }] of groups) {\r\n db.prepare(D_UPSERT).run(\r\n checksum,\r\n row.data,\r\n row.mimeType,\r\n row.sizeBytes,\r\n row.width,\r\n row.height,\r\n n,\r\n now,\r\n );\r\n }\r\n return rows.map((r) => {\r\n const id = randomUUID();\r\n db.prepare(D_INSERT_REF).run(\r\n id,\r\n r.checksumSha256,\r\n r.originalFilename,\r\n now,\r\n now,\r\n );\r\n return id;\r\n });\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n let row: any;\r\n try {\r\n row = await this.execute(\"get\", connection, \"read\", (db) =>\r\n db.prepare(this.dedup ? D_SELECT_FULL : SELECT_FULL).get(norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? hydrateRow(row, norm, this.opts.objectStorage) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = [\r\n ...new Set(ids.filter(isValidId).map((i) => i.toLowerCase())),\r\n ];\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.execute(\"get_many\", connection, \"read\", (db) => {\r\n const out: any[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n out.push(\r\n ...db\r\n .prepare(\r\n this.dedup ? dSelectMany(part.length) : selectMany(part.length),\r\n )\r\n .all(...part),\r\n );\r\n }\r\n return out;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) =>\r\n hydrateRow(r, String(r.id), this.opts.objectStorage),\r\n );\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.execute(\"get_metadata\", connection, \"read\", (db) =>\r\n db\r\n .prepare(this.dedup ? D_SELECT_METADATA : SELECT_METADATA)\r\n .get(id.toLowerCase()),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.execute(\"get_metadata\", connection, \"read\", (db) =>\r\n db.prepare(this.dedup ? D_STREAM_INFO : STREAM_INFO).get(norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(norm, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n norm,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n\r\n const self = this;\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start;\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.execute(\"get_stream\", connection, \"read\", (db) =>\r\n db\r\n .prepare(self.dedup ? D_CHUNK : CHUNK)\r\n .get(offset + 1, length, norm),\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(norm)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer | null = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(norm)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n const norm = id.toLowerCase();\r\n try {\r\n if (this.dedup) {\r\n return await this.execute(\"delete\", connection, \"write\", (db) => {\r\n const ref = db.prepare(D_DELETE_REF).get(norm);\r\n if (!ref) return false;\r\n const dec = db.prepare(D_DECREMENT).get(1, ref.checksum_sha256);\r\n if (dec && dec.ref_count <= 0)\r\n db.prepare(D_DELETE_EMPTY).run(ref.checksum_sha256);\r\n return true;\r\n });\r\n }\r\n const row = await this.execute(\"delete\", connection, \"write\", (db) =>\r\n db.prepare(DELETE_ONE).get(norm),\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObjects([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = [\r\n ...new Set(ids.filter(isValidId).map((i) => i.toLowerCase())),\r\n ];\r\n if (valid.length === 0) return [];\r\n try {\r\n if (this.dedup) {\r\n return await this.execute(\"delete_many\", connection, \"write\", (db) => {\r\n const deleted: { id: string; checksum_sha256: string }[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n deleted.push(...db.prepare(dDeleteRefs(part.length)).all(...part));\r\n }\r\n const counts = new Map<string, number>();\r\n for (const r of deleted)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n for (const [checksum, n] of counts) {\r\n const dec = db.prepare(D_DECREMENT).get(n, checksum);\r\n if (dec && dec.ref_count <= 0)\r\n db.prepare(D_DELETE_EMPTY).run(checksum);\r\n }\r\n return deleted.map((r) => String(r.id));\r\n });\r\n }\r\n const rows = await this.execute(\r\n \"delete_many\",\r\n connection,\r\n \"write\",\r\n (db) => {\r\n const out: any[] = [];\r\n for (let i = 0; i < valid.length; i += IN_CHUNK) {\r\n const part = valid.slice(i, i + IN_CHUNK);\r\n out.push(...db.prepare(deleteMany(part.length)).all(...part));\r\n }\r\n return out;\r\n },\r\n );\r\n await this.cleanupObjects(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.execute(\r\n \"exists\",\r\n connection,\r\n \"read\",\r\n (db) =>\r\n db.prepare(this.dedup ? D_EXISTS : EXISTS).get(id.toLowerCase()) !==\r\n undefined,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- migration: classic -> dedup --------------------------------------\r\n\r\n /**\r\n * One-time, NON-DESTRUCTIVE copy of every row in the classic `zerobucket_images` table into this\r\n * dedup instance's tables, preserving every id (external references keep working) and\r\n * deduplicating identical content on the way. The classic table is never modified. Same contract\r\n * as `PostgresBackend.migrateClassicToDedup()`.\r\n *\r\n * Rows are copied in small batches (bounded memory instead of loading the whole table);\r\n * re-running is safe (already-migrated ids are skipped and reported, not an error). Classic rows\r\n * already tiered to object storage are refused up front with a clear message (their bytes are\r\n * not in the database to copy) unless `skipTiered: true`, which skips and counts them instead.\r\n * All-or-nothing: ONE `BEGIN IMMEDIATE` transaction, so a failure part-way leaves the dedup\r\n * tables exactly as they were. That transaction holds SQLite's single write lock for the whole\r\n * copy, so other writers to this database file wait until it finishes (readers are not blocked).\r\n */\r\n async migrateClassicToDedup(\r\n opts: MigrateToDedupOptions = {},\r\n ): Promise<MigrateToDedupResult> {\r\n if (!this.dedup) {\r\n throw new StorageError(\r\n \"migrateClassicToDedup() must be called on an instance created with dedup: true.\",\r\n );\r\n }\r\n try {\r\n return await this.execute(\r\n \"migrate_to_dedup\",\r\n undefined,\r\n \"write\",\r\n (db) => {\r\n if (\r\n !db\r\n .prepare(\r\n \"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'zerobucket_images'\",\r\n )\r\n .get()\r\n ) {\r\n throw new StorageError(\r\n \"No classic zerobucket_images table found in this database -- nothing to migrate.\",\r\n );\r\n }\r\n let tieredSkipped = 0;\r\n const hasTierCol = (\r\n db.prepare(\"PRAGMA table_info(zerobucket_images)\").all() as {\r\n name: string;\r\n }[]\r\n ).some((c) => c.name === \"storage_backend\");\r\n if (hasTierCol) {\r\n const tiered = (\r\n db\r\n .prepare(\r\n \"SELECT count(*) AS n FROM zerobucket_images WHERE storage_backend <> 'sqlite'\",\r\n )\r\n .get() as { n: number }\r\n ).n;\r\n if (tiered > 0 && !opts.skipTiered) {\r\n throw new StorageError(\r\n `${tiered} classic image(s) are tiered to object storage, so their bytes are not in the database to migrate. ` +\r\n \"Dedup mode does not support tiering. Migrate only an un-tiered table, bring those images back first, \" +\r\n \"or pass skipTiered: true (CLI: --skip-tiered) to skip them and migrate the rest.\",\r\n );\r\n }\r\n tieredSkipped = tiered;\r\n }\r\n // Only reached with tiered rows present when skipTiered is set: leave them out of the copy.\r\n const onlyInDb = hasTierCol && tieredSkipped > 0;\r\n\r\n const seen = new Set<string>();\r\n let migrated = 0;\r\n let already = 0;\r\n // Keyset paging on (created_at, id): created_at is TEXT here (a fixed-width ISO string), so it\r\n // round-trips exactly. Rows sharing one created_at (a putMany batch) are ordered by id.\r\n let cursor: { createdAt: string; id: string } | null = null;\r\n const BATCH = 20;\r\n const columns =\r\n \"id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at, updated_at\";\r\n for (;;) {\r\n const page: any[] = cursor\r\n ? db\r\n .prepare(\r\n `SELECT ${columns} FROM zerobucket_images WHERE (created_at, id) > (?, ?)${onlyInDb ? \" AND storage_backend = 'sqlite'\" : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n )\r\n .all(cursor.createdAt, cursor.id)\r\n : db\r\n .prepare(\r\n `SELECT ${columns} FROM zerobucket_images${onlyInDb ? \" WHERE storage_backend = 'sqlite'\" : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n )\r\n .all();\r\n if (page.length === 0) break;\r\n const last = page[page.length - 1];\r\n if (\r\n cursor &&\r\n cursor.createdAt === last.created_at &&\r\n cursor.id === last.id\r\n ) {\r\n throw new StorageError(\r\n \"Migration made no progress (keyset cursor did not advance); aborting instead of looping.\",\r\n );\r\n }\r\n cursor = { createdAt: last.created_at, id: last.id };\r\n\r\n const have = new Set(\r\n (\r\n db\r\n .prepare(\r\n `SELECT id FROM zerobucket_image_refs WHERE id IN (${placeholders(page.length)})`,\r\n )\r\n .all(...page.map((r) => r.id)) as { id: string }[]\r\n ).map((r) => r.id),\r\n );\r\n const fresh = page.filter((r) => !have.has(r.id));\r\n already += page.length - fresh.length;\r\n\r\n // Identical content within this page shares one blob, counted once per reference.\r\n const groups = new Map<string, { row: any; n: number }>();\r\n for (const r of fresh) {\r\n const g = groups.get(r.checksum_sha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksum_sha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const { row, n } = groups.get(checksum)!;\r\n db.prepare(D_UPSERT).run(\r\n checksum,\r\n row.data,\r\n row.mime_type,\r\n row.size_bytes,\r\n row.width,\r\n row.height,\r\n n,\r\n row.created_at,\r\n );\r\n }\r\n for (const r of fresh) {\r\n db.prepare(D_INSERT_REF).run(\r\n r.id,\r\n r.checksum_sha256,\r\n r.original_filename,\r\n r.created_at,\r\n r.updated_at,\r\n );\r\n seen.add(r.checksum_sha256);\r\n migrated += 1;\r\n }\r\n }\r\n return {\r\n imagesMigrated: migrated,\r\n distinctBlobsCreated: seen.size,\r\n duplicateReferencesFound: migrated - seen.size,\r\n alreadyMigrated: already,\r\n tieredSkipped,\r\n };\r\n },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Migration to dedup failed\", exc);\r\n }\r\n }\r\n\r\n // ---- maintenance (CLI) -----------------------------------------------\r\n\r\n async getInfo(): Promise<StorageInfo | null> {\r\n try {\r\n return await this.execute(\"info\", undefined, \"read\", (db) => {\r\n const has = (t: string) =>\r\n db\r\n .prepare(\r\n \"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?\",\r\n )\r\n .get(t) !== undefined;\r\n if (\r\n !has(this.dedup ? \"zerobucket_image_refs\" : \"zerobucket_images\") ||\r\n (this.dedup && !has(\"zerobucket_blobs\"))\r\n )\r\n return null;\r\n const j = this.dedup ? D_FROM : \"FROM zerobucket_images\";\r\n const size = this.dedup ? \"b.size_bytes\" : \"size_bytes\";\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n const mime = this.dedup ? \"b.mime_type\" : \"mime_type\";\r\n const tot = db\r\n .prepare(\r\n `SELECT count(*) AS n, coalesce(sum(${size}), 0) AS bytes, min(${created}) AS oldest, max(${created}) AS newest ${j}`,\r\n )\r\n .get();\r\n const fmt = db\r\n .prepare(\r\n `SELECT ${mime} AS m, count(*) AS n, coalesce(sum(${size}), 0) AS bytes ${j} GROUP BY ${mime} ORDER BY count(*) DESC, ${mime}`,\r\n )\r\n .all();\r\n let onDisk: string | null = null;\r\n if (this.opts.path !== \":memory:\") {\r\n let total = statSync(this.opts.path).size;\r\n for (const suffix of [\"-wal\", \"-shm\"])\r\n if (existsSync(this.opts.path + suffix))\r\n total += statSync(this.opts.path + suffix).size;\r\n onDisk = prettySize(total);\r\n }\r\n const info: StorageInfo = {\r\n mode: this.dedup ? \"dedup\" : \"classic\",\r\n count: tot.n,\r\n totalBytes: Number(tot.bytes),\r\n oldest: tot.oldest,\r\n newest: tot.newest,\r\n onDisk,\r\n byFormat: fmt.map((r: any) => ({\r\n mimeType: r.m,\r\n count: r.n,\r\n bytes: Number(r.bytes),\r\n })),\r\n };\r\n if (this.dedup) {\r\n const b = db\r\n .prepare(\r\n \"SELECT count(*) AS n, coalesce(sum(size_bytes), 0) AS bytes FROM zerobucket_blobs\",\r\n )\r\n .get();\r\n info.dedup = { blobs: b.n, storedBytes: Number(b.bytes) };\r\n } else {\r\n const t = db\r\n .prepare(\r\n \"SELECT count(*) AS n, coalesce(sum(size_bytes), 0) AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'\",\r\n )\r\n .get();\r\n info.tiered = { count: t.n, bytes: Number(t.bytes) };\r\n }\r\n return info;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to read storage info\", exc);\r\n }\r\n }\r\n\r\n async listImages(o: ImageListOptions = {}): Promise<ImageListEntry[]> {\r\n const where: string[] = [];\r\n const params: unknown[] = [];\r\n if (o.minSize !== undefined)\r\n (where.push(`${this.dedup ? \"b.size_bytes\" : \"size_bytes\"} >= ?`),\r\n params.push(o.minSize));\r\n if (o.olderThanDays !== undefined) {\r\n // timestamps are ISO-8601 text in one fixed format, so string comparison is chronological\r\n const cutoff = new Date(Date.now() - o.olderThanDays * 86_400_000)\r\n .toISOString()\r\n .replace(\"Z\", \"000+00:00\");\r\n where.push(`${this.dedup ? \"r.created_at\" : \"created_at\"} <= ?`);\r\n params.push(cutoff);\r\n }\r\n if (o.onlyUntiered && !this.dedup) where.push(\"storage_backend = 'sqlite'\");\r\n const from = this.dedup ? D_FROM : \"FROM zerobucket_images\";\r\n const idCol = this.dedup ? \"r.id\" : \"id\";\r\n const tierCol = this.dedup\r\n ? \"0 AS tiered\"\r\n : \"(storage_backend = 'object_storage') AS tiered\";\r\n const order =\r\n o.sample !== undefined\r\n ? \"ORDER BY random()\"\r\n : `ORDER BY ${this.dedup ? \"r.created_at\" : \"created_at\"}, ${idCol}`;\r\n const lim = o.sample ?? o.limit;\r\n let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? \" WHERE \" + where.join(\" AND \") : \"\"} ${order}`;\r\n if (lim !== undefined) ((sql += \" LIMIT ?\"), params.push(lim));\r\n try {\r\n const rows = await this.execute(\"list_images\", undefined, \"read\", (db) =>\r\n db.prepare(sql).all(...params),\r\n );\r\n return rows.map((r: any) => ({\r\n id: String(r.id),\r\n tiered: Boolean(r.tiered),\r\n }));\r\n } catch (exc) {\r\n throw wrap(\"Failed to list images\", exc);\r\n }\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone (a failed object delete leaves a harmless orphan; the\r\n * reverse order could leave a row pointing at nothing). Skipped inside a caller-owned transaction, since the\r\n * commit outcome is unknown. Failures never fail the delete; they are reported through onOperation.\r\n */\r\n private async cleanupObjects(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes into object storage. null = no such image, false = already tiered, true = tiered now.\r\n *\r\n * SAFETY (same guarantee as Postgres): the upload happens INSIDE a BEGIN IMMEDIATE transaction; if it fails the\r\n * transaction rolls back and the row is untouched.\r\n *\r\n * TRADEOFF (coarser than Postgres): SQLite has no row locks, so BEGIN IMMEDIATE locks the WHOLE DATABASE for the\r\n * upload. Other processes' writes wait; this process's writes queue behind an async lock; reads are not blocked.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup)\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ sqlite, objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (connection) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() does not accept `connection` on SQLite: it needs its own dedicated connection for the BEGIN IMMEDIATE transaction.\",\r\n );\r\n }\r\n if (this.opts.path === \":memory:\") {\r\n throw new StorageError(\r\n \"tierToObjectStorage() needs a database file: an in-memory SQLite database cannot be shared with its dedicated tiering connection.\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const norm = id.toLowerCase();\r\n const start = performance.now();\r\n let retries = 0;\r\n try {\r\n await this.ready();\r\n const result = await this.writeLock.run(async () => {\r\n const tdb = new this.Database!(this.opts.path, {\r\n timeout: ATTEMPT_BUSY_MS,\r\n });\r\n try {\r\n await this.retrying(\r\n () => tdb.exec(\"BEGIN IMMEDIATE\"),\r\n (n) => (retries = n),\r\n );\r\n try {\r\n const row = tdb.prepare(SELECT_FOR_TIERING).get(norm);\r\n if (!row) {\r\n tdb.exec(\"ROLLBACK\");\r\n return null;\r\n }\r\n if (row.storage_backend !== \"sqlite\") {\r\n tdb.exec(\"ROLLBACK\");\r\n return false;\r\n }\r\n await os.upload(norm, row.data as Buffer, {\r\n mimeType: row.mime_type,\r\n });\r\n tdb\r\n .prepare(UPDATE_AFTER_TIERING)\r\n .run(os.bucket, norm, isoNow(), norm);\r\n tdb.exec(\"COMMIT\");\r\n return true;\r\n } catch (exc) {\r\n try {\r\n tdb.exec(\"ROLLBACK\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n } finally {\r\n tdb.close();\r\n }\r\n });\r\n this.emit(\"tier_to_object_storage\", start, true, null, retries);\r\n return result;\r\n } catch (exc) {\r\n this.emit(\"tier_to_object_storage\", start, false, msg(exc), retries);\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n","/**\r\n * MySQL / MariaDB storage adapter (classic mode, streaming, object-storage tiering).\r\n *\r\n * Uses the EXACT schema the Python package's MySQLBackend creates (CHAR(36) ids, LONGBLOB data,\r\n * naive-UTC DATETIME(6) timestamps, storage_backend = 'mysql', the same CHECK constraint), so a Node\r\n * app and a Python app can share one database and read each other's rows.\r\n *\r\n * Decisions that differ from the Postgres adapter, each verified against a real MariaDB 10.11:\r\n *\r\n * - NO `RETURNING`. MySQL 8.0 has none (MariaDB 10.5+ does, but one code path must serve both).\r\n * delete()/deleteMany() therefore run `SELECT ... FOR UPDATE` and then `DELETE` inside ONE\r\n * transaction, which also tells us which rows were tiered (so their objects can be cleaned up).\r\n * - ids are generated by the application (`crypto.randomUUID()`), exactly like SQLite and Python.\r\n * - Timestamps are written as pre-formatted UTC strings, never as JS Dates, so the result does not\r\n * depend on the server's or the process's timezone setting.\r\n * - Blobs travel through `execute()` (binary protocol, raw bytes). `query()` would inline a blob\r\n * as a hex literal (2x the size, and the whole statement counts against max_allowed_packet).\r\n * Everything else uses `query()`: with `execute()`, JS numbers are sent as DOUBLE, which MySQL\r\n * 8.0.22+ rejects in places like LIMIT and offsets, and SUBSTRING() offsets are such a place.\r\n * - mysql2's pool has no acquire timeout, so `poolTimeoutMs` is implemented here. A waiter that\r\n * times out is removed in effect: if its connection arrives late it is released straight back.\r\n * - Retry re-runs the WHOLE unit of work on a fresh connection after a full ROLLBACK, never a single\r\n * statement (the Python package hit exactly that \"half-applied put\" hazard with DBUtils).\r\n * Connection loss, lock-wait timeout (1205) and deadlock (1213) are retried; the pool-timeout\r\n * error is NOT (retrying would multiply the wait).\r\n * - A caller-supplied `connection` (your own mysql2 connection) is never committed, rolled back or\r\n * retried. For multi-statement work on it we first probe whether a transaction is open\r\n * (SAVEPOINT + ROLLBACK TO fails with 1305 when none is): inside one we use a SAVEPOINT so a\r\n * failure undoes only our work; outside one (autocommit) we open and commit our own.\r\n * - Migration runs under `GET_LOCK()` so N processes starting at once do not race on the DDL, and it\r\n * issues NO DDL when the schema is already current: even a no-op ALTER TABLE needs an exclusive\r\n * metadata lock, which queues behind every open transaction and blocks all later queries on the table.\r\n * (The Python package always runs `ALTER TABLE ... MODIFY data` at startup; both additions are\r\n * purely additive and work on MySQL and MariaDB.)\r\n * - Dedup mode (`dedup: true`) uses the Python package's two tables (zerobucket_blobs: one row per distinct\r\n * content with a ref_count; zerobucket_image_refs: one row per image). A batch groups identical images,\r\n * upserts each distinct blob ONCE in sorted checksum order and decrements in sorted order too: a global\r\n * lock order, so two overlapping batches can never deadlock on each other's blob rows. Without `RETURNING`\r\n * the decrement is `UPDATE ... ref_count = ref_count - n` (which takes the row lock) followed by a point\r\n * `DELETE ... WHERE ref_count <= 0`; the foreign key makes deleting a still-referenced blob impossible.\r\n * - Batch deletes lock with one `SELECT ... FOR UPDATE` and then use POINT statements, never `DELETE ... IN (...)`.\r\n * On MariaDB 10.11 that statement is planned as a full scan on a small table (EXPLAIN type=ALL) while the ordered\r\n * SELECT gets a range plan; a scan locks every row it passes, so overlapping batches deadlocked (measured: 126 per\r\n * 300 concurrent transactions with IN-list deletes, 0 with point deletes). MySQL 8.0.46 plans the same delete as a\r\n * range on just the listed keys and did NOT deadlock in the same test; point deletes do not depend on the plan, so\r\n * they are used on both.\r\n * Like the other adapters, dedup cannot be combined with object storage (a blob may be shared).\r\n */\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { StorageError } from \"../errors.js\";\r\nimport type { ObjectStorageLike } from \"../object-storage.js\";\r\nimport type {\r\n Connection,\r\n MysqlPoolConnectionLike,\r\n MysqlPoolLike,\r\n MysqlQueryable,\r\n OperationEvent,\r\n PreparedRow,\r\n} from \"../types.js\";\r\nimport type {\r\n CallOptions,\r\n ImageListEntry,\r\n ImageListOptions,\r\n MigrateToDedupOptions,\r\n MigrateToDedupResult,\r\n StorageBackend,\r\n StorageInfo,\r\n StoredRecord,\r\n StoredRecordMetadata,\r\n StreamOptions,\r\n} from \"./base.js\";\r\nimport {\r\n hydrateRow,\r\n isValidId,\r\n mapLimit,\r\n msg,\r\n prettySize,\r\n sleep,\r\n streamFromObjectStorage,\r\n tieredWithoutStorage,\r\n wrap,\r\n} from \"./shared.js\";\r\n\r\n// ---- schema (identical to the Python package: classic table, then the additive tiering migration) ----\r\n\r\nconst SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_images (\r\n id CHAR(36) PRIMARY KEY,\r\n data LONGBLOB NOT NULL,\r\n mime_type VARCHAR(255) NOT NULL,\r\n original_filename TEXT,\r\n size_bytes INT NOT NULL,\r\n width INT,\r\n height INT,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n created_at DATETIME(6) NOT NULL,\r\n updated_at DATETIME(6) NOT NULL,\r\n INDEX idx_zerobucket_checksum (checksum_sha256),\r\n INDEX idx_zerobucket_created_at (created_at)\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;\r\n\r\nconst TIERING_CHECK = \"chk_zerobucket_storage\";\r\nconst MIGRATE_LOCK = \"zerobucket_migrate\";\r\nconst MIGRATE_DEDUP_LOCK = \"zerobucket_migrate_dedup\";\r\n\r\n// ---- SQL ---------------------------------------------------------------------------------------\r\n\r\nconst COLS = `id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, storage_backend, object_storage_bucket, object_storage_key`;\r\nconst INSERT = `INSERT INTO zerobucket_images (id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\r\nconst SELECT_FULL = `SELECT ${COLS} FROM zerobucket_images WHERE id = ?`;\r\nconst SELECT_MANY = `SELECT ${COLS} FROM zerobucket_images WHERE id IN (?)`;\r\nconst SELECT_METADATA = `SELECT id, mime_type, original_filename, size_bytes, width, height, checksum_sha256 FROM zerobucket_images WHERE id = ?`;\r\nconst STREAM_INFO = `SELECT size_bytes, storage_backend, object_storage_key FROM zerobucket_images WHERE id = ?`;\r\n// SUBSTRING() is 1-indexed and clamps `length` at the end of the value.\r\nconst CHUNK = `SELECT SUBSTRING(data FROM ? FOR ?) AS chunk FROM zerobucket_images WHERE id = ?`;\r\nconst EXISTS = `SELECT 1 FROM zerobucket_images WHERE id = ?`;\r\nconst LOCK_ONE = `SELECT storage_backend, object_storage_key FROM zerobucket_images WHERE id = ? FOR UPDATE`;\r\nconst DELETE_ONE = `DELETE FROM zerobucket_images WHERE id = ?`;\r\n// LOCKING RULE for every batch write here: lock the rows with a set-based SELECT ... FOR UPDATE, then change\r\n// them with POINT statements (`WHERE id = ?`), always in sorted order. Never `DELETE ... WHERE id IN (...)`:\r\n// MariaDB 10.11 plans that as a full scan on a small table (EXPLAIN type=ALL), and a scan under REPEATABLE READ\r\n// locks EVERY row it passes, including rows another batch already holds, so two overlapping batches deadlock.\r\n// Measured on MariaDB 10.11: 126 deadlocks in 300 concurrent transactions with the IN-list delete, 0 with point\r\n// deletes (also 0 with a concurrent writer). MySQL 8.0.46 plans the same delete as a range on the listed keys and\r\n// did not deadlock in the same test; point deletes do not depend on the plan, so they are used on both.\r\n// FORCE INDEX(PRIMARY) on the SELECT makes it probe exactly the listed keys instead of scanning (record locks on\r\n// existing rows only, no gap locks), and ORDER BY id gives every writer the same lock order.\r\nconst LOCK_MANY = `SELECT id, storage_backend, object_storage_key FROM zerobucket_images FORCE INDEX (PRIMARY) WHERE id IN (?) ORDER BY id FOR UPDATE`;\r\nconst SELECT_FOR_TIERING = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = ? FOR UPDATE`;\r\nconst UPDATE_AFTER_TIERING = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = ?, object_storage_key = ?, updated_at = ? WHERE id = ?`;\r\n\r\n/** Value of `storage_backend` for rows whose bytes live in the database (same as the Python package). */\r\nconst LOCAL = \"mysql\";\r\n\r\n// ---- dedup mode: content-addressed blobs + per-image refs (same schema as the Python package) ----\r\n\r\nconst DEDUP_BLOBS_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_blobs (\r\n checksum_sha256 CHAR(64) PRIMARY KEY,\r\n data LONGBLOB NOT NULL,\r\n mime_type VARCHAR(255) NOT NULL,\r\n size_bytes INT NOT NULL,\r\n width INT,\r\n height INT,\r\n ref_count INT NOT NULL DEFAULT 0,\r\n created_at DATETIME(6) NOT NULL\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;\r\n\r\nconst DEDUP_REFS_SCHEMA = `\r\nCREATE TABLE IF NOT EXISTS zerobucket_image_refs (\r\n id CHAR(36) PRIMARY KEY,\r\n checksum_sha256 CHAR(64) NOT NULL,\r\n original_filename TEXT,\r\n created_at DATETIME(6) NOT NULL,\r\n updated_at DATETIME(6) NOT NULL,\r\n INDEX idx_zerobucket_image_refs_checksum (checksum_sha256),\r\n INDEX idx_zerobucket_image_refs_created_at (created_at),\r\n FOREIGN KEY (checksum_sha256) REFERENCES zerobucket_blobs(checksum_sha256)\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;\r\n\r\n/**\r\n * Insert a blob with `n` references, or add `n` to an existing blob's count. InnoDB makes the increment\r\n * atomic at the row level, so 20 concurrent puts of the same bytes give ref_count == 20. `n` is a\r\n * count this module computed (never user input); it is inlined because prepared statements send JS\r\n * numbers as DOUBLE, which makes `ref_count + ?` a floating-point expression.\r\n */\r\nfunction upsertBlobSql(n: number): string {\r\n if (!Number.isInteger(n) || n < 1) throw new RangeError(\"invalid ref count\");\r\n return `INSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ${n}, ?) ON DUPLICATE KEY UPDATE ref_count = ref_count + ${n}`;\r\n}\r\nconst D_INSERT_REF = `INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`;\r\n// Aliased to the classic column names so one row mapper serves both modes.\r\nconst D_COLS = `r.id, b.data, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256, '${LOCAL}' AS storage_backend, NULL AS object_storage_bucket, NULL AS object_storage_key`;\r\nconst D_FROM = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;\r\nconst D_LOCK_REF = `SELECT checksum_sha256 FROM zerobucket_image_refs WHERE id = ? FOR UPDATE`;\r\nconst D_DELETE_REF = `DELETE FROM zerobucket_image_refs WHERE id = ?`;\r\nconst D_LOCK_REFS = `SELECT id, checksum_sha256 FROM zerobucket_image_refs FORCE INDEX (PRIMARY) WHERE id IN (?) ORDER BY id FOR UPDATE`;\r\nconst D_DECREMENT = `UPDATE zerobucket_blobs SET ref_count = ref_count - ? WHERE checksum_sha256 = ?`;\r\n// Point delete, never an IN list (see the LOCKING RULE above); a no-op unless the count reached zero.\r\nconst D_DELETE_EMPTY = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ? AND ref_count <= 0`;\r\n\r\ninterface SqlSet {\r\n selectFull: string;\r\n selectMany: string;\r\n selectMetadata: string;\r\n streamInfo: string;\r\n chunk: string;\r\n exists: string;\r\n}\r\nconst CLASSIC_SQL: SqlSet = {\r\n selectFull: SELECT_FULL,\r\n selectMany: SELECT_MANY,\r\n selectMetadata: SELECT_METADATA,\r\n streamInfo: STREAM_INFO,\r\n chunk: CHUNK,\r\n exists: EXISTS,\r\n};\r\nconst DEDUP_SQL: SqlSet = {\r\n selectFull: `SELECT ${D_COLS} ${D_FROM} WHERE r.id = ?`,\r\n selectMany: `SELECT ${D_COLS} ${D_FROM} WHERE r.id IN (?)`,\r\n selectMetadata: `SELECT r.id, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256 ${D_FROM} WHERE r.id = ?`,\r\n streamInfo: `SELECT b.size_bytes, '${LOCAL}' AS storage_backend, NULL AS object_storage_key ${D_FROM} WHERE r.id = ?`,\r\n chunk: `SELECT SUBSTRING(b.data FROM ? FOR ?) AS chunk ${D_FROM} WHERE r.id = ?`,\r\n exists: `SELECT 1 FROM zerobucket_image_refs WHERE id = ?`,\r\n};\r\n\r\n// ---- error classification ----------------------------------------------------------------------\r\n\r\nconst RETRYABLE_ERRNOS = new Set([\r\n 1205, // ER_LOCK_WAIT_TIMEOUT\r\n 1213, // ER_LOCK_DEADLOCK\r\n 1040, // ER_CON_COUNT_ERROR (too many connections)\r\n 1053, // ER_SERVER_SHUTDOWN\r\n 1927, // ER_CONNECTION_KILLED (MariaDB)\r\n 2002, // CR_CONNECTION_ERROR\r\n 2003, // CR_CONN_HOST_ERROR\r\n 2006, // CR_SERVER_GONE_ERROR\r\n 2013, // CR_SERVER_LOST\r\n]);\r\nconst CONNECTION_CODES = new Set([\r\n \"PROTOCOL_CONNECTION_LOST\",\r\n \"PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR\",\r\n \"PROTOCOL_ENQUEUE_AFTER_QUIT\",\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"ETIMEDOUT\",\r\n \"EPIPE\",\r\n \"ECONNABORTED\",\r\n]);\r\n\r\ninterface MysqlError {\r\n code?: unknown;\r\n errno?: unknown;\r\n fatal?: unknown;\r\n}\r\n\r\nfunction errnoOf(exc: unknown): number | undefined {\r\n const n = (exc as MysqlError | null)?.errno;\r\n return typeof n === \"number\" ? n : undefined;\r\n}\r\n\r\n/** Thrown when waiting for a pool slot times out. Not retryable: retrying would just wait again. */\r\nclass PoolTimeoutError extends StorageError {}\r\n\r\n/** Transient (worth retrying) vs. permanent (fails identically every time). */\r\nexport function isRetryable(exc: unknown): boolean {\r\n if (!(exc instanceof Error) || exc instanceof PoolTimeoutError) return false;\r\n const e = exc as MysqlError;\r\n const n = errnoOf(exc);\r\n if (n !== undefined && RETRYABLE_ERRNOS.has(n)) return true;\r\n if (typeof e.code === \"string\") {\r\n if (CONNECTION_CODES.has(e.code)) return true;\r\n if (\r\n e.code === \"ER_LOCK_WAIT_TIMEOUT\" ||\r\n e.code === \"ER_LOCK_DEADLOCK\" ||\r\n e.code === \"ER_CON_COUNT_ERROR\" ||\r\n e.code === \"ER_SERVER_SHUTDOWN\" ||\r\n e.code === \"ER_CONNECTION_KILLED\"\r\n )\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\n/** Errors after which the pooled connection must be destroyed, not returned to the pool. */\r\nfunction isConnectionLevel(exc: unknown): boolean {\r\n if (!(exc instanceof Error)) return false;\r\n const e = exc as MysqlError;\r\n if (e.fatal === true) return true;\r\n if (typeof e.code === \"string\" && CONNECTION_CODES.has(e.code)) return true;\r\n const n = errnoOf(exc);\r\n return n === 2006 || n === 2013 || n === 1927 || n === 1053;\r\n}\r\n\r\nconst MAX_BACKOFF_MS = 2000;\r\n\r\nexport function backoffDelayMs(attempt: number, baseDelayMs: number): number {\r\n const exponential = baseDelayMs * 2 ** (attempt - 1);\r\n const jitter = Math.random() * baseDelayMs;\r\n return Math.min(exponential + jitter, MAX_BACKOFF_MS);\r\n}\r\n\r\n/** Turn the one error users hit in practice into an actionable message; everything else passes through wrap(). */\r\nfunction friendly(prefix: string, exc: unknown): StorageError {\r\n if (errnoOf(exc) === 1153) {\r\n return new StorageError(\r\n `${prefix}: the image is larger than the server's max_allowed_packet. Raise it (for example \\`SET GLOBAL max_allowed_packet = 67108864\\`, ` +\r\n `and set it in my.cnf to make it permanent), or lower \\`maxBytes\\`.`,\r\n { cause: exc },\r\n );\r\n }\r\n return wrap(prefix, exc);\r\n}\r\n\r\n/** Naive UTC `YYYY-MM-DD HH:MM:SS.ffffff`, the format of the Python package's DATETIME(6) values. */\r\nfunction mysqlNow(): string {\r\n return `${new Date().toISOString().replace(\"T\", \" \").replace(\"Z\", \"\")}000`;\r\n}\r\n\r\nfunction normalizeUrl(url: string): string {\r\n return url.replace(/^mariadb:\\/\\//i, \"mysql://\");\r\n}\r\n\r\nfunction isMysqlConnection(c: Connection): c is MysqlQueryable {\r\n const m = c as Partial<MysqlQueryable>;\r\n return typeof m.query === \"function\" && typeof m.execute === \"function\";\r\n}\r\n\r\nexport interface MySQLBackendOptions {\r\n /** `mysql://user:password@host:3306/database` (`mariadb://` is accepted too). Required unless `pool` is given. */\r\n connectionString?: string;\r\n /** Bring your own `mysql2/promise` Pool (not closed by `close()`). Pool sizing options are then ignored. */\r\n pool?: MysqlPoolLike;\r\n /** Create the schema on first use. Default true. */\r\n autoMigrate?: boolean;\r\n /** Retries for transient errors on pooled calls. Default 3; 0 disables. */\r\n maxRetries?: number;\r\n /** Base for exponential backoff with jitter (capped at 2s). Default 100. */\r\n retryBaseDelayMs?: number;\r\n /** Accepted for parity with the other backends. mysql2 opens connections on demand, so there is no minimum to hold. */\r\n poolMinSize?: number;\r\n /** Maximum simultaneous connections. Default 5. */\r\n poolMaxSize?: number;\r\n /** Max wait for a pooled connection (also the TCP connect timeout). Default 10000. */\r\n poolTimeoutMs?: number;\r\n onOperation?: (event: OperationEvent) => void;\r\n /** Enables transparent reads of tiered rows and `tierToObjectStorage()`. */\r\n objectStorage?: ObjectStorageLike;\r\n /**\r\n * Content-addressed storage: identical bytes are stored once and shared by every image that has them\r\n * (reference counted). Same tables as the Python package's dedup mode. Cannot be combined with\r\n * `objectStorage` (a blob may be shared by many images).\r\n */\r\n dedup?: boolean;\r\n}\r\n\r\ntype Work<T> = (q: MysqlQueryable) => Promise<T>;\r\n\r\nexport class MySQLBackend implements StorageBackend {\r\n private pool: MysqlPoolLike | undefined;\r\n private readonly ownsPool: boolean;\r\n private readyPromise: Promise<void> | undefined;\r\n private closed = false;\r\n private readonly maxRetries: number;\r\n private readonly retryBaseDelayMs: number;\r\n private readonly poolTimeoutMs: number;\r\n private readonly autoMigrate: boolean;\r\n private readonly dedup: boolean;\r\n private readonly sql: SqlSet;\r\n\r\n constructor(private readonly opts: MySQLBackendOptions) {\r\n if (!opts.connectionString && !opts.pool) {\r\n throw new TypeError(\r\n \"MySQLBackend requires either `connectionString` or `pool`\",\r\n );\r\n }\r\n this.dedup = opts.dedup ?? false;\r\n if (this.dedup && opts.objectStorage) {\r\n throw new TypeError(\r\n \"objectStorage is not supported together with dedup: true (same restriction as the Python package). \" +\r\n \"Tiering moves one image's bytes, but a deduplicated blob can be shared by many images.\",\r\n );\r\n }\r\n this.sql = this.dedup ? DEDUP_SQL : CLASSIC_SQL;\r\n this.ownsPool = !opts.pool;\r\n this.pool = opts.pool;\r\n this.maxRetries = opts.maxRetries ?? 3;\r\n this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 100;\r\n this.poolTimeoutMs = opts.poolTimeoutMs ?? 10_000;\r\n this.autoMigrate = opts.autoMigrate ?? true;\r\n }\r\n\r\n // ---- lifecycle -------------------------------------------------------\r\n\r\n ready(): Promise<void> {\r\n if (this.closed)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n this.readyPromise ??= this.init().catch((exc) => {\r\n this.readyPromise = undefined; // allow a later attempt (e.g. the database was briefly down)\r\n throw exc;\r\n });\r\n return this.readyPromise;\r\n }\r\n\r\n private async init(): Promise<void> {\r\n if (!this.pool) {\r\n let mod: {\r\n createPool(config: Record<string, unknown>): MysqlPoolLike;\r\n default?: {\r\n createPool(config: Record<string, unknown>): MysqlPoolLike;\r\n };\r\n };\r\n try {\r\n mod = (await import(\"mysql2/promise\")) as unknown as typeof mod;\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"MySQLBackend requires the 'mysql2' package. Install it with: npm install mysql2\",\r\n { cause },\r\n );\r\n }\r\n const createPool = (mod.default ?? mod).createPool;\r\n this.pool = createPool({\r\n uri: normalizeUrl(this.opts.connectionString!),\r\n connectionLimit: this.opts.poolMaxSize ?? 5,\r\n waitForConnections: true,\r\n queueLimit: 0,\r\n connectTimeout: this.poolTimeoutMs,\r\n charset: \"utf8mb4\",\r\n timezone: \"Z\",\r\n enableKeepAlive: true,\r\n });\r\n }\r\n try {\r\n if (this.autoMigrate) {\r\n await this.runPooled(\"migrate\", (q) => this.migrate(q), false);\r\n } else {\r\n await this.runPooled(\"ping\", (q) => q.query(\"SELECT 1\"), false);\r\n }\r\n } catch (exc) {\r\n if (this.ownsPool) {\r\n await this.pool.end().catch(() => {});\r\n this.pool = undefined;\r\n }\r\n throw exc instanceof StorageError\r\n ? exc\r\n : new StorageError(`Could not connect to MySQL/MariaDB: ${msg(exc)}`, {\r\n cause: exc,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Create the schema if missing, under a server-side named lock so concurrent first callers (in this\r\n * or any other process) run the DDL one at a time. Every step checks before it changes anything, so\r\n * re-running is a no-op.\r\n */\r\n private async migrate(q: MysqlQueryable): Promise<void> {\r\n const locked = (\r\n (await q.query(\"SELECT GET_LOCK(?, 30) AS got\", [MIGRATE_LOCK]))[0] as {\r\n got: number | null;\r\n }[]\r\n )[0]?.got;\r\n if (locked !== 1) {\r\n throw new StorageError(\r\n \"Timed out waiting for another process to finish migrating the ZeroBucket schema.\",\r\n );\r\n }\r\n try {\r\n if (this.dedup) {\r\n await this.migrateDedup(q);\r\n return;\r\n }\r\n // DDL only when something is actually missing. Even a no-op ALTER TABLE needs an EXCLUSIVE metadata\r\n // lock, which waits behind every open transaction on the table, and while it waits, every other\r\n // query on that table queues behind IT. Starting an app instance during a long transaction must\r\n // not be able to stall all traffic, so a current schema issues no DDL at all.\r\n const tableExists =\r\n (\r\n (\r\n await q.query(\r\n \"SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images'\",\r\n )\r\n )[0] as unknown[]\r\n ).length > 0;\r\n if (!tableExists) await q.query(SCHEMA);\r\n const cols = new Map(\r\n (\r\n (\r\n await q.query(\r\n \"SELECT COLUMN_NAME AS c, IS_NULLABLE AS nullable FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images'\",\r\n )\r\n )[0] as { c: string; nullable: string }[]\r\n ).map((r) => [r.c, r.nullable]),\r\n );\r\n if (!cols.has(\"storage_backend\"))\r\n await q.query(\r\n `ALTER TABLE zerobucket_images ADD COLUMN storage_backend VARCHAR(20) NOT NULL DEFAULT '${LOCAL}'`,\r\n );\r\n if (!cols.has(\"object_storage_bucket\"))\r\n await q.query(\r\n \"ALTER TABLE zerobucket_images ADD COLUMN object_storage_bucket VARCHAR(255)\",\r\n );\r\n if (!cols.has(\"object_storage_key\"))\r\n await q.query(\r\n \"ALTER TABLE zerobucket_images ADD COLUMN object_storage_key VARCHAR(512)\",\r\n );\r\n if (cols.get(\"data\") === \"NO\")\r\n await q.query(\r\n \"ALTER TABLE zerobucket_images MODIFY data LONGBLOB NULL\",\r\n );\r\n const hasCheck =\r\n (\r\n (\r\n await q.query(\r\n \"SELECT 1 FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images' AND CONSTRAINT_NAME = ?\",\r\n [TIERING_CHECK],\r\n )\r\n )[0] as unknown[]\r\n ).length > 0;\r\n if (!hasCheck) {\r\n await q.query(\r\n `ALTER TABLE zerobucket_images ADD CONSTRAINT ${TIERING_CHECK} CHECK (` +\r\n `(storage_backend = '${LOCAL}' AND data IS NOT NULL AND object_storage_bucket IS NULL AND object_storage_key IS NULL) OR ` +\r\n `(storage_backend = 'object_storage' AND data IS NULL AND object_storage_bucket IS NOT NULL AND object_storage_key IS NOT NULL))`,\r\n );\r\n }\r\n } catch (exc) {\r\n throw new StorageError(`Migration failed: ${msg(exc)}`, { cause: exc });\r\n } finally {\r\n await q.query(\"SELECT RELEASE_LOCK(?)\", [MIGRATE_LOCK]).catch(() => {});\r\n }\r\n }\r\n\r\n /** Dedup tables, created only if missing (same no-needless-DDL rule as the classic table). */\r\n private async migrateDedup(q: MysqlQueryable): Promise<void> {\r\n const have = new Set(\r\n (\r\n (\r\n await q.query(\r\n \"SELECT TABLE_NAME AS t FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('zerobucket_blobs', 'zerobucket_image_refs')\",\r\n )\r\n )[0] as { t: string }[]\r\n ).map((r) => r.t),\r\n );\r\n if (!have.has(\"zerobucket_blobs\")) await q.query(DEDUP_BLOBS_SCHEMA); // the refs table references it\r\n if (!have.has(\"zerobucket_image_refs\")) await q.query(DEDUP_REFS_SCHEMA);\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.closed = true;\r\n const pool = this.pool;\r\n this.pool = undefined;\r\n this.readyPromise = undefined;\r\n if (pool && this.ownsPool) await pool.end();\r\n }\r\n\r\n // ---- execution core --------------------------------------------------\r\n\r\n private emit(\r\n operation: string,\r\n start: number,\r\n success: boolean,\r\n error: string | null,\r\n retryCount: number,\r\n ): void {\r\n const cb = this.opts.onOperation;\r\n if (!cb) return;\r\n try {\r\n cb({\r\n operation,\r\n durationMs: performance.now() - start,\r\n success,\r\n error,\r\n retryCount,\r\n });\r\n } catch {\r\n // A bug in a metrics callback must never break a real operation.\r\n }\r\n }\r\n\r\n /** Wait for a pool slot, but never longer than poolTimeoutMs. */\r\n private acquire(): Promise<MysqlPoolConnectionLike> {\r\n const pool = this.pool;\r\n if (this.closed || !pool)\r\n return Promise.reject(\r\n new StorageError(\"This ZeroBucket instance has been closed\"),\r\n );\r\n return new Promise((resolve, reject) => {\r\n let settled = false;\r\n const timer = setTimeout(() => {\r\n settled = true;\r\n reject(\r\n new PoolTimeoutError(\r\n `Timed out after ${this.poolTimeoutMs}ms waiting for a MySQL/MariaDB connection ` +\r\n `(poolMaxSize=${this.opts.poolMaxSize ?? 5}; all connections are in use).`,\r\n ),\r\n );\r\n }, this.poolTimeoutMs);\r\n pool.getConnection().then(\r\n (conn) => {\r\n if (settled) {\r\n conn.release(); // arrived after we gave up: hand it straight back\r\n return;\r\n }\r\n settled = true;\r\n clearTimeout(timer);\r\n resolve(conn);\r\n },\r\n (exc) => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(timer);\r\n reject(exc);\r\n },\r\n );\r\n });\r\n }\r\n\r\n /**\r\n * Run `work` on a pooled connection. `tx` wraps it in a transaction. Transient failures retry the\r\n * WHOLE closure on a fresh connection after a full rollback.\r\n *\r\n * `lock` serializes this work against other callers holding the same named lock (`GET_LOCK`). The\r\n * ORDER matters and is the whole point: the lock is taken BEFORE the transaction begins (so the\r\n * transaction's snapshot is taken after any earlier holder has finished) and released AFTER the\r\n * transaction commits or rolls back (so a later holder can never start while this one's rows are\r\n * still uncommitted and invisible to it). Releasing inside `work`, before the commit, lets a second\r\n * holder read a stale snapshot and collide on primary keys.\r\n */\r\n private async runPooled<T>(\r\n operation: string,\r\n work: Work<T>,\r\n tx: boolean,\r\n lock?: { name: string; timeoutSec: number; timeoutMessage: string },\r\n ): Promise<T> {\r\n const start = performance.now();\r\n let attempt = 0;\r\n for (;;) {\r\n let conn: MysqlPoolConnectionLike | undefined;\r\n let destroy = false;\r\n let held = false;\r\n try {\r\n conn = await this.acquire();\r\n if (lock) {\r\n const got = (\r\n (\r\n await conn.query(\"SELECT GET_LOCK(?, ?) AS got\", [\r\n lock.name,\r\n lock.timeoutSec,\r\n ])\r\n )[0] as { got: number | null }[]\r\n )[0]?.got;\r\n if (got !== 1) throw new StorageError(lock.timeoutMessage);\r\n held = true;\r\n }\r\n if (tx) await conn.beginTransaction();\r\n const result = await work(conn);\r\n if (tx) await conn.commit();\r\n this.emit(operation, start, true, null, attempt);\r\n return result;\r\n } catch (exc) {\r\n if (conn && tx) {\r\n try {\r\n await conn.rollback();\r\n } catch {\r\n destroy = true; // cannot trust a connection that cannot roll back\r\n }\r\n }\r\n if (isConnectionLevel(exc)) destroy = true;\r\n attempt += 1;\r\n if (attempt > this.maxRetries || !isRetryable(exc)) {\r\n this.emit(operation, start, false, msg(exc), attempt - 1);\r\n throw exc;\r\n }\r\n } finally {\r\n if (conn) {\r\n // After the commit/rollback above. A destroyed connection takes its lock with it.\r\n if (held && lock && !destroy) {\r\n try {\r\n await conn.query(\"SELECT RELEASE_LOCK(?)\", [lock.name]);\r\n } catch {\r\n destroy = true; // cannot confirm the lock is gone: drop the session, which frees it\r\n }\r\n }\r\n if (destroy) conn.destroy();\r\n else conn.release();\r\n }\r\n }\r\n await sleep(backoffDelayMs(attempt, this.retryBaseDelayMs));\r\n }\r\n }\r\n\r\n /**\r\n * Run `work` and return its result, emitting exactly one OperationEvent.\r\n *\r\n * connection given -> run once on it; no retry, and no commit/rollback of the caller's transaction.\r\n * connection absent -> pooled, retried (see runPooled).\r\n */\r\n private async run<T>(\r\n operation: string,\r\n connection: Connection | undefined,\r\n work: Work<T>,\r\n {\r\n tx = false,\r\n lock,\r\n }: {\r\n tx?: boolean;\r\n lock?: { name: string; timeoutSec: number; timeoutMessage: string };\r\n } = {},\r\n ): Promise<T> {\r\n if (connection) {\r\n if (!isMysqlConnection(connection)) {\r\n throw new TypeError(\r\n \"MySQLBackend expects a mysql2/promise connection as `connection` (an object with query() and execute()).\",\r\n );\r\n }\r\n const start = performance.now();\r\n try {\r\n const result = tx\r\n ? await this.atomic(connection, work)\r\n : await work(connection);\r\n this.emit(operation, start, true, null, 0);\r\n return result;\r\n } catch (exc) {\r\n this.emit(operation, start, false, msg(exc), 0);\r\n throw exc;\r\n }\r\n }\r\n await this.ready();\r\n return this.runPooled(operation, work, tx, lock);\r\n }\r\n\r\n /**\r\n * Make multi-statement work all-or-nothing on a CALLER-supplied connection.\r\n *\r\n * Is a transaction open? SAVEPOINT followed by ROLLBACK TO SAVEPOINT succeeds only inside one (in\r\n * autocommit mode the savepoint is gone before the next statement and the rollback fails with 1305).\r\n * - Open -> reuse that SAVEPOINT: a failure undoes only our work; success joins the caller's transaction.\r\n * - None -> START TRANSACTION and COMMIT our own, so a failure can never leave half-applied state.\r\n */\r\n private async atomic<T>(conn: MysqlQueryable, work: Work<T>): Promise<T> {\r\n let inTransaction: boolean;\r\n await conn.query(\"SAVEPOINT zerobucket_atomic\");\r\n try {\r\n await conn.query(\"ROLLBACK TO SAVEPOINT zerobucket_atomic\");\r\n inTransaction = true;\r\n } catch (exc) {\r\n if (errnoOf(exc) !== 1305) throw exc; // ER_SP_DOES_NOT_EXIST: no open transaction\r\n inTransaction = false;\r\n await conn.query(\"START TRANSACTION\");\r\n }\r\n try {\r\n const result = await work(conn);\r\n await conn.query(\r\n inTransaction ? \"RELEASE SAVEPOINT zerobucket_atomic\" : \"COMMIT\",\r\n );\r\n return result;\r\n } catch (exc) {\r\n try {\r\n await conn.query(\r\n inTransaction\r\n ? \"ROLLBACK TO SAVEPOINT zerobucket_atomic\"\r\n : \"ROLLBACK\",\r\n );\r\n if (inTransaction)\r\n await conn.query(\"RELEASE SAVEPOINT zerobucket_atomic\");\r\n } catch {\r\n // keep the original error\r\n }\r\n throw exc;\r\n }\r\n }\r\n\r\n // ---- StorageBackend --------------------------------------------------\r\n\r\n async put(\r\n row: PreparedRow,\r\n { connection }: CallOptions = {},\r\n ): Promise<string> {\r\n try {\r\n const id = randomUUID();\r\n if (this.dedup) {\r\n await this.run(\r\n \"put\",\r\n connection,\r\n async (q) => {\r\n const now = mysqlNow();\r\n await q.execute(upsertBlobSql(1), blobParams(row, now));\r\n await q.execute(D_INSERT_REF, refParams(id, row, now));\r\n },\r\n { tx: true },\r\n );\r\n return id;\r\n }\r\n await this.run(\"put\", connection, async (q) => {\r\n await q.execute(INSERT, rowParams(id, row, mysqlNow()));\r\n });\r\n return id;\r\n } catch (exc) {\r\n throw friendly(\"Failed to store image\", exc);\r\n }\r\n }\r\n\r\n async putMany(\r\n rows: PreparedRow[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n if (rows.length === 0) return [];\r\n try {\r\n // Ids are minted before the transaction, so a retry after a full rollback re-inserts the same\r\n // ids and can never create duplicates. One INSERT at a time keeps every packet one image big.\r\n const ids = rows.map(() => randomUUID());\r\n await this.run(\r\n \"put_many\",\r\n connection,\r\n async (q) => {\r\n const now = mysqlNow();\r\n if (this.dedup) {\r\n // Identical images in one batch share a blob: count them, upsert each distinct checksum ONCE\r\n // in sorted order (a global lock order, so overlapping batches cannot deadlock), then create\r\n // the refs in input order.\r\n const groups = new Map<string, { row: PreparedRow; n: number }>();\r\n for (const r of rows) {\r\n const g = groups.get(r.checksumSha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksumSha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const g = groups.get(checksum)!;\r\n await q.execute(upsertBlobSql(g.n), blobParams(g.row, now));\r\n }\r\n for (let i = 0; i < rows.length; i++)\r\n await q.execute(D_INSERT_REF, refParams(ids[i]!, rows[i]!, now));\r\n return;\r\n }\r\n for (let i = 0; i < rows.length; i++)\r\n await q.execute(INSERT, rowParams(ids[i]!, rows[i]!, now));\r\n },\r\n { tx: true },\r\n );\r\n return ids;\r\n } catch (exc) {\r\n throw friendly(\"Failed to store image batch\", exc);\r\n }\r\n }\r\n\r\n async get(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectFull, [id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image\", exc);\r\n }\r\n return row ? this.hydrate(row, id) : null;\r\n }\r\n\r\n async getMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecord[]> {\r\n const valid = ids.filter(isValidId);\r\n if (valid.length === 0) return [];\r\n let rows: any[];\r\n try {\r\n rows = await this.run(\r\n \"get_many\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMany, [valid]))[0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image batch\", exc);\r\n }\r\n return mapLimit(rows, 4, (r) => this.hydrate(r, String(r.id)));\r\n }\r\n\r\n async getMetadata(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<StoredRecordMetadata | null> {\r\n if (!isValidId(id)) return null;\r\n let row: any;\r\n try {\r\n row = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.selectMetadata, [id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!row) return null;\r\n return {\r\n id: String(row.id),\r\n mimeType: row.mime_type,\r\n originalFilename: row.original_filename,\r\n sizeBytes: row.size_bytes,\r\n width: row.width,\r\n height: row.height,\r\n checksumSha256: row.checksum_sha256,\r\n };\r\n }\r\n\r\n async getStream(\r\n id: string,\r\n opts: StreamOptions,\r\n ): Promise<AsyncIterable<Buffer> | null> {\r\n if (!isValidId(id)) return null;\r\n const { connection, chunkSize } = opts;\r\n if (!Number.isInteger(chunkSize) || chunkSize <= 0)\r\n throw new RangeError(\"chunkSize must be a positive integer\");\r\n\r\n let info: any;\r\n try {\r\n info = await this.run(\r\n \"get_metadata\",\r\n connection,\r\n async (q) => (await q.query(this.sql.streamInfo, [id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to retrieve image metadata\", exc);\r\n }\r\n if (!info) return null;\r\n const totalSize: number = info.size_bytes;\r\n const start = opts.range?.start ?? 0;\r\n const end = Math.min(opts.range?.end ?? totalSize - 1, totalSize - 1); // inclusive, clamped\r\n\r\n if (info.storage_backend === \"object_storage\") {\r\n const os = this.opts.objectStorage;\r\n if (!os) throw tieredWithoutStorage(id, info.object_storage_key);\r\n return streamFromObjectStorage(\r\n os,\r\n id,\r\n info.object_storage_key,\r\n start,\r\n end,\r\n chunkSize,\r\n );\r\n }\r\n const self = this;\r\n\r\n async function* generate(): AsyncGenerator<Buffer> {\r\n let offset = start; // 0-based\r\n let delivered = 0;\r\n const wanted = Math.max(0, end - start + 1);\r\n while (delivered < wanted) {\r\n const length = Math.min(chunkSize, wanted - delivered);\r\n let row: any;\r\n try {\r\n row = await self.run(\r\n \"get_stream\",\r\n connection,\r\n async (q) =>\r\n (await q.query(self.sql.chunk, [offset + 1, length, id]))[0][0],\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to stream image\", exc);\r\n }\r\n if (!row) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} was deleted while streaming (delivered ${delivered} of ${wanted} bytes). ` +\r\n \"Pass connection with your own open transaction if you need a consistent read across concurrent writers.\",\r\n );\r\n }\r\n const chunk: Buffer | null = row.chunk;\r\n if (!chunk || chunk.length === 0) {\r\n throw new StorageError(\r\n `Image ${JSON.stringify(id)} ended early while streaming (delivered ${delivered} of ${wanted} bytes).`,\r\n );\r\n }\r\n yield chunk;\r\n offset += chunk.length;\r\n delivered += chunk.length;\r\n }\r\n }\r\n return generate();\r\n }\r\n\r\n async delete(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n if (this.dedup) return this.deleteDedup(id, connection);\r\n try {\r\n // No RETURNING: lock the row, learn where its bytes live, delete it, all in one transaction.\r\n const row = await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const found = (await q.query(LOCK_ONE, [id]))[0][0];\r\n if (!found) return undefined;\r\n await q.query(DELETE_ONE, [id]);\r\n return found;\r\n },\r\n { tx: true },\r\n );\r\n if (row?.storage_backend === \"object_storage\")\r\n await this.cleanupObject([row.object_storage_key], connection);\r\n return row !== undefined;\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n async deleteMany(\r\n ids: string[],\r\n { connection }: CallOptions = {},\r\n ): Promise<string[]> {\r\n const valid = [...new Set(ids.filter(isValidId))];\r\n if (valid.length === 0) return [];\r\n if (this.dedup) return this.deleteManyDedup(valid, connection);\r\n try {\r\n const rows: any[] = await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const found = (await q.query(LOCK_MANY, [valid]))[0] as any[];\r\n for (const r of found) await q.query(DELETE_ONE, [r.id]); // point deletes, in sorted order\r\n return found;\r\n },\r\n { tx: true },\r\n );\r\n await this.cleanupObject(\r\n rows\r\n .filter((r) => r.storage_backend === \"object_storage\")\r\n .map((r) => r.object_storage_key as string),\r\n connection,\r\n );\r\n return rows.map((r) => String(r.id));\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n async exists(id: string, { connection }: CallOptions = {}): Promise<boolean> {\r\n if (!isValidId(id)) return false;\r\n try {\r\n return await this.run(\r\n \"exists\",\r\n connection,\r\n async (q) => (await q.query(this.sql.exists, [id]))[0].length > 0,\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to check image existence\", exc);\r\n }\r\n }\r\n\r\n // ---- migration: classic -> dedup --------------------------------------\r\n\r\n /**\r\n * One-time, NON-DESTRUCTIVE copy of every row in the classic `zerobucket_images` table into this\r\n * dedup instance's tables, preserving every id (external references keep working) and\r\n * deduplicating identical content on the way. The classic table is never modified. Same contract\r\n * as `PostgresBackend.migrateClassicToDedup()`.\r\n *\r\n * Rows are copied in small batches (bounded memory instead of loading the whole table);\r\n * re-running is safe (already-migrated ids are skipped and reported, not an error). Classic rows\r\n * already tiered to object storage are refused up front with a clear message (their bytes are\r\n * not in the database to copy) unless `skipTiered: true`, which skips and counts them instead.\r\n * All-or-nothing: ONE transaction, so a failure part-way leaves the dedup tables exactly as they\r\n * were. A named lock (`GET_LOCK`) serializes two migrations started at once; it is taken before the\r\n * transaction and released only after it commits (see `runPooled`), so a second migration always sees\r\n * the first one's rows.\r\n */\r\n async migrateClassicToDedup(\r\n opts: MigrateToDedupOptions = {},\r\n ): Promise<MigrateToDedupResult> {\r\n if (!this.dedup) {\r\n throw new StorageError(\r\n \"migrateClassicToDedup() must be called on an instance created with dedup: true.\",\r\n );\r\n }\r\n try {\r\n return await this.run(\r\n \"migrate_to_dedup\",\r\n undefined,\r\n (q) => this.copyClassicToDedup(q, opts),\r\n {\r\n tx: true,\r\n lock: {\r\n name: MIGRATE_DEDUP_LOCK,\r\n timeoutSec: 30,\r\n timeoutMessage:\r\n \"Timed out waiting for another migration to dedup to finish.\",\r\n },\r\n },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Migration to dedup failed\", exc);\r\n }\r\n }\r\n\r\n private async copyClassicToDedup(\r\n q: MysqlQueryable,\r\n opts: MigrateToDedupOptions,\r\n ): Promise<MigrateToDedupResult> {\r\n const rowsOf = async (sql: string, params?: unknown[]): Promise<any[]> =>\r\n (await q.query(sql, params))[0] as any[];\r\n\r\n if (\r\n (\r\n await rowsOf(\r\n \"SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images'\",\r\n )\r\n ).length === 0\r\n ) {\r\n throw new StorageError(\r\n \"No classic zerobucket_images table found in this database -- nothing to migrate.\",\r\n );\r\n }\r\n let tieredSkipped = 0;\r\n const hasTierCol =\r\n (\r\n await rowsOf(\r\n \"SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images' AND COLUMN_NAME = 'storage_backend'\",\r\n )\r\n ).length > 0;\r\n if (hasTierCol) {\r\n const tiered = Number(\r\n (\r\n await rowsOf(\r\n `SELECT COUNT(*) AS n FROM zerobucket_images WHERE storage_backend <> '${LOCAL}'`,\r\n )\r\n )[0].n,\r\n );\r\n if (tiered > 0 && !opts.skipTiered) {\r\n throw new StorageError(\r\n `${tiered} classic image(s) are tiered to object storage, so their bytes are not in the database to migrate. ` +\r\n \"Dedup mode does not support tiering. Migrate only an un-tiered table, bring those images back first, \" +\r\n \"or pass skipTiered: true (CLI: --skip-tiered) to skip them and migrate the rest.\",\r\n );\r\n }\r\n tieredSkipped = tiered;\r\n }\r\n // Only reached with tiered rows present when skipTiered is set: leave them out of the copy.\r\n const onlyInDb = hasTierCol && tieredSkipped > 0;\r\n\r\n const seen = new Set<string>();\r\n let migrated = 0;\r\n let already = 0;\r\n // created_at/updated_at travel as TEXT, never a JS Date: DATETIME(6) keeps microseconds, a Date\r\n // keeps only milliseconds, and a truncated keyset cursor sorts before its own row (infinite loop).\r\n let cursor: { createdAt: string; id: string } | null = null;\r\n const BATCH = 20;\r\n const columns =\r\n \"id, data, mime_type, original_filename, size_bytes, width, height, checksum_sha256, \" +\r\n \"CAST(created_at AS CHAR) AS created_at_txt, CAST(updated_at AS CHAR) AS updated_at_txt\";\r\n for (;;) {\r\n const page: any[] = cursor\r\n ? await rowsOf(\r\n `SELECT ${columns} FROM zerobucket_images WHERE (created_at, id) > (?, ?)${onlyInDb ? ` AND storage_backend = '${LOCAL}'` : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n [cursor.createdAt, cursor.id],\r\n )\r\n : await rowsOf(\r\n `SELECT ${columns} FROM zerobucket_images${onlyInDb ? ` WHERE storage_backend = '${LOCAL}'` : \"\"} ORDER BY created_at, id LIMIT ${BATCH}`,\r\n );\r\n if (page.length === 0) break;\r\n const last = page[page.length - 1];\r\n if (\r\n cursor &&\r\n cursor.createdAt === last.created_at_txt &&\r\n cursor.id === last.id\r\n ) {\r\n throw new StorageError(\r\n \"Migration made no progress (keyset cursor did not advance); aborting instead of looping.\",\r\n );\r\n }\r\n cursor = { createdAt: last.created_at_txt, id: last.id };\r\n\r\n const have = new Set(\r\n (\r\n await rowsOf(\"SELECT id FROM zerobucket_image_refs WHERE id IN (?)\", [\r\n page.map((r) => r.id),\r\n ])\r\n ).map((r) => String(r.id)),\r\n );\r\n const fresh = page.filter((r) => !have.has(String(r.id)));\r\n already += page.length - fresh.length;\r\n\r\n // Identical content within this page shares one blob, counted once per reference. Blobs are\r\n // upserted in sorted checksum order (one global lock order, as everywhere else in this adapter).\r\n const groups = new Map<string, { row: any; n: number }>();\r\n for (const r of fresh) {\r\n const g = groups.get(r.checksum_sha256);\r\n if (g) g.n += 1;\r\n else groups.set(r.checksum_sha256, { row: r, n: 1 });\r\n }\r\n for (const checksum of [...groups.keys()].sort()) {\r\n const { row, n } = groups.get(checksum)!;\r\n await q.query(upsertBlobSql(n), [\r\n checksum,\r\n row.data,\r\n row.mime_type,\r\n row.size_bytes,\r\n row.width,\r\n row.height,\r\n row.created_at_txt,\r\n ]);\r\n }\r\n for (const r of fresh) {\r\n await q.query(D_INSERT_REF, [\r\n r.id,\r\n r.checksum_sha256,\r\n r.original_filename,\r\n r.created_at_txt,\r\n r.updated_at_txt,\r\n ]);\r\n seen.add(r.checksum_sha256);\r\n migrated += 1;\r\n }\r\n }\r\n return {\r\n imagesMigrated: migrated,\r\n distinctBlobsCreated: seen.size,\r\n duplicateReferencesFound: migrated - seen.size,\r\n alreadyMigrated: already,\r\n tieredSkipped,\r\n };\r\n }\r\n\r\n // ---- maintenance (CLI) -----------------------------------------------\r\n\r\n /** Storage statistics, or null if the tables do not exist yet (nothing is created). */\r\n async getInfo(): Promise<StorageInfo | null> {\r\n try {\r\n return await this.run(\"info\", undefined, async (q) => {\r\n const need = this.dedup\r\n ? [\"zerobucket_image_refs\", \"zerobucket_blobs\"]\r\n : [\"zerobucket_images\"];\r\n const present = new Set(\r\n (\r\n (\r\n await q.query(\r\n \"SELECT TABLE_NAME AS t FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?)\",\r\n [need],\r\n )\r\n )[0] as { t: string }[]\r\n ).map((r) => r.t),\r\n );\r\n if (!need.every((t) => present.has(t))) return null;\r\n const j = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const size = this.dedup ? \"b.size_bytes\" : \"size_bytes\";\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n const mime = this.dedup ? \"b.mime_type\" : \"mime_type\";\r\n const tot = (\r\n await q.query(\r\n `SELECT COUNT(*) AS n, COALESCE(SUM(${size}), 0) AS bytes, CAST(MIN(${created}) AS CHAR) AS oldest, CAST(MAX(${created}) AS CHAR) AS newest ${j}`,\r\n )\r\n )[0][0];\r\n // information_schema statistics are an estimate (MySQL 8 caches them for up to 24h), hence \"approximate\".\r\n const disk = (\r\n await q.query(\r\n \"SELECT COALESCE(SUM(DATA_LENGTH + INDEX_LENGTH), 0) AS s FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?)\",\r\n [need],\r\n )\r\n )[0][0];\r\n const fmt = (\r\n await q.query(\r\n `SELECT ${mime} AS m, COUNT(*) AS n, COALESCE(SUM(${size}), 0) AS bytes ${j} GROUP BY ${mime} ORDER BY COUNT(*) DESC, ${mime}`,\r\n )\r\n )[0] as { m: string; n: number; bytes: string }[];\r\n // created_at is naive UTC; print it the way the Postgres output does (ISO with an explicit +00:00).\r\n const utc = (v: string | null) => (v ? `${v}+00:00` : null);\r\n const info: StorageInfo = {\r\n mode: this.dedup ? \"dedup\" : \"classic\",\r\n count: Number(tot.n),\r\n totalBytes: Number(tot.bytes),\r\n oldest: utc(tot.oldest),\r\n newest: utc(tot.newest),\r\n onDisk: prettySize(Number(disk.s)),\r\n byFormat: fmt.map((r) => ({\r\n mimeType: r.m,\r\n count: Number(r.n),\r\n bytes: Number(r.bytes),\r\n })),\r\n };\r\n if (this.dedup) {\r\n const b = (\r\n await q.query(\r\n \"SELECT COUNT(*) AS n, COALESCE(SUM(size_bytes), 0) AS bytes FROM zerobucket_blobs\",\r\n )\r\n )[0][0];\r\n info.dedup = { blobs: Number(b.n), storedBytes: Number(b.bytes) };\r\n } else {\r\n const t = (\r\n await q.query(\r\n \"SELECT COUNT(*) AS n, COALESCE(SUM(size_bytes), 0) AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'\",\r\n )\r\n )[0][0];\r\n info.tiered = { count: Number(t.n), bytes: Number(t.bytes) };\r\n }\r\n return info;\r\n });\r\n } catch (exc) {\r\n throw wrap(\"Failed to read storage info\", exc);\r\n }\r\n }\r\n\r\n /** Ids matching the filters: oldest first, or a random sample. Used by the CLI's verify and tier. */\r\n async listImages(o: ImageListOptions = {}): Promise<ImageListEntry[]> {\r\n const where: string[] = [];\r\n const params: unknown[] = [];\r\n const created = this.dedup ? \"r.created_at\" : \"created_at\";\r\n if (o.minSize !== undefined) {\r\n where.push(`${this.dedup ? \"b.size_bytes\" : \"size_bytes\"} >= ?`);\r\n params.push(o.minSize);\r\n }\r\n if (o.olderThanDays !== undefined) {\r\n // created_at is naive UTC, so compare against UTC_TIMESTAMP, never NOW() (which is session-timezone local).\r\n where.push(`${created} <= UTC_TIMESTAMP(6) - INTERVAL ? DAY`);\r\n params.push(o.olderThanDays);\r\n }\r\n if (o.onlyUntiered && !this.dedup)\r\n where.push(`storage_backend = '${LOCAL}'`);\r\n const from = this.dedup\r\n ? \"FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256\"\r\n : \"FROM zerobucket_images\";\r\n const idCol = this.dedup ? \"r.id\" : \"id\";\r\n const tierCol = this.dedup\r\n ? \"0 AS tiered\"\r\n : \"(storage_backend = 'object_storage') AS tiered\";\r\n const order =\r\n o.sample !== undefined\r\n ? \"ORDER BY RAND()\"\r\n : `ORDER BY ${created}, ${idCol}`;\r\n const lim = o.sample ?? o.limit;\r\n let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? \" WHERE \" + where.join(\" AND \") : \"\"} ${order}`;\r\n if (lim !== undefined) {\r\n if (!Number.isInteger(lim) || lim < 0)\r\n throw new RangeError(\"limit/sample must be a non-negative integer\");\r\n sql += ` LIMIT ${lim}`;\r\n }\r\n try {\r\n const rows = await this.run(\r\n \"list_images\",\r\n undefined,\r\n async (q) => (await q.query(sql, params))[0] as any[],\r\n );\r\n return rows.map((r) => ({\r\n id: String(r.id),\r\n tiered: Boolean(Number(r.tiered)),\r\n }));\r\n } catch (exc) {\r\n throw wrap(\"Failed to list images\", exc);\r\n }\r\n }\r\n\r\n // ---- dedup deletes ---------------------------------------------------\r\n\r\n /**\r\n * Lock the ref row, delete it, then decrement its blob (the UPDATE takes the blob row lock, so two\r\n * deletes of different refs to one blob serialize instead of racing) and remove the blob once nothing\r\n * references it. All in one transaction: a failure can never leave a ref without its count or the reverse.\r\n */\r\n private async deleteDedup(\r\n id: string,\r\n connection: Connection | undefined,\r\n ): Promise<boolean> {\r\n try {\r\n return await this.run(\r\n \"delete\",\r\n connection,\r\n async (q) => {\r\n const ref = (await q.query(D_LOCK_REF, [id]))[0][0];\r\n if (!ref) return false; // id did not exist: nothing to decrement either\r\n const checksum = ref.checksum_sha256 as string;\r\n await q.query(D_DELETE_REF, [id]);\r\n await q.query(D_DECREMENT, [1, checksum]);\r\n await q.query(D_DELETE_EMPTY, [checksum]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image\", exc);\r\n }\r\n }\r\n\r\n private async deleteManyDedup(\r\n ids: string[],\r\n connection: Connection | undefined,\r\n ): Promise<string[]> {\r\n try {\r\n return await this.run(\r\n \"delete_many\",\r\n connection,\r\n async (q) => {\r\n const found = (await q.query(D_LOCK_REFS, [ids]))[0] as any[];\r\n if (found.length === 0) return [];\r\n for (const r of found) await q.query(D_DELETE_REF, [r.id]); // point deletes, in sorted order\r\n // Several deleted refs may share one blob: decrement each blob by its exact count, in sorted\r\n // checksum order (a global lock order, so concurrent batches cannot deadlock).\r\n const counts = new Map<string, number>();\r\n for (const r of found)\r\n counts.set(\r\n r.checksum_sha256,\r\n (counts.get(r.checksum_sha256) ?? 0) + 1,\r\n );\r\n for (const checksum of [...counts.keys()].sort()) {\r\n await q.query(D_DECREMENT, [counts.get(checksum), checksum]);\r\n await q.query(D_DELETE_EMPTY, [checksum]);\r\n }\r\n return found.map((r) => String(r.id));\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to delete image batch\", exc);\r\n }\r\n }\r\n\r\n // ---- helpers ---------------------------------------------------------\r\n\r\n private hydrate(row: any, id: string): Promise<StoredRecord> {\r\n return hydrateRow(row, id, this.opts.objectStorage);\r\n }\r\n\r\n /**\r\n * Remove tiered objects AFTER their rows are gone: once the row is deleted the image is correctly\r\n * \"not found\" to everyone, so a failed object delete only leaves a harmless orphan, whereas deleting\r\n * the object first and then failing the row delete would leave a row pointing at nothing.\r\n *\r\n * Never runs inside a caller-owned transaction (`connection`): we cannot know whether it will\r\n * commit. In that case the object stays until you remove it yourself after committing. Failures are\r\n * reported through onOperation and never fail the delete itself.\r\n */\r\n private async cleanupObject(\r\n keys: string[],\r\n connection: Connection | undefined,\r\n ): Promise<void> {\r\n const os = this.opts.objectStorage;\r\n if (!os || connection || keys.length === 0) return;\r\n await Promise.all(\r\n keys.map(async (key) => {\r\n const start = performance.now();\r\n try {\r\n await os.delete(key);\r\n this.emit(\"object_storage_delete\", start, true, null, 0);\r\n } catch (exc) {\r\n this.emit(\"object_storage_delete\", start, false, msg(exc), 0);\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Move an image's bytes out of the database into object storage, replacing the row's `data` with a\r\n * pointer. Returns null (no such image), false (already tiered: a safe no-op, so backfills can be\r\n * re-run) or true (tiered just now).\r\n *\r\n * SAFETY: the upload happens INSIDE the same transaction as the row lock (SELECT ... FOR UPDATE)\r\n * and the UPDATE that flips the row. If the upload fails the transaction rolls back and the row is\r\n * untouched: there is no moment when the bytes exist nowhere, or when a row claims to be tiered\r\n * without a completed upload.\r\n *\r\n * TRADEOFF: InnoDB row-locks only THIS row for the duration of the upload. Concurrent writes to this\r\n * image wait (up to innodb_lock_wait_timeout, 50s by default); every other row is unaffected and\r\n * plain reads are never blocked. Meant for explicit maintenance, not hot paths.\r\n *\r\n * Retry is safe: a transient error replays the whole closure, and the deterministic key (the image\r\n * id) makes the re-upload an idempotent overwrite.\r\n */\r\n async tierToObjectStorage(\r\n id: string,\r\n { connection }: CallOptions = {},\r\n ): Promise<boolean | null> {\r\n if (this.dedup) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images).\",\r\n );\r\n }\r\n const os = this.opts.objectStorage;\r\n if (!os) {\r\n throw new StorageError(\r\n \"tierToObjectStorage() requires objectStorage to be configured (new ZeroBucket({ objectStorage: new ObjectStorage({ bucket }) })).\",\r\n );\r\n }\r\n if (!isValidId(id)) return null;\r\n const key = id.toLowerCase();\r\n try {\r\n return await this.run(\r\n \"tier_to_object_storage\",\r\n connection,\r\n async (q) => {\r\n const row = (await q.query(SELECT_FOR_TIERING, [id]))[0][0];\r\n if (!row) return null;\r\n if (row.storage_backend !== LOCAL) return false;\r\n await os.upload(key, row.data as Buffer, { mimeType: row.mime_type });\r\n await q.query(UPDATE_AFTER_TIERING, [os.bucket, key, mysqlNow(), id]);\r\n return true;\r\n },\r\n { tx: true },\r\n );\r\n } catch (exc) {\r\n throw wrap(\"Failed to tier image to object storage\", exc);\r\n }\r\n }\r\n}\r\n\r\nfunction blobParams(r: PreparedRow, now: string): unknown[] {\r\n return [\r\n r.checksumSha256,\r\n r.data,\r\n r.mimeType,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n now,\r\n ];\r\n}\r\n\r\nfunction refParams(id: string, r: PreparedRow, now: string): unknown[] {\r\n return [id, r.checksumSha256, r.originalFilename, now, now];\r\n}\r\n\r\nfunction rowParams(id: string, r: PreparedRow, now: string): unknown[] {\r\n return [\r\n id,\r\n r.data,\r\n r.mimeType,\r\n r.originalFilename,\r\n r.sizeBytes,\r\n r.width,\r\n r.height,\r\n r.checksumSha256,\r\n now,\r\n now,\r\n ];\r\n}\r\n","/**\r\n * Optional HEIC/HEIF support (iPhone photos).\r\n *\r\n * sharp's prebuilt libvips cannot decode HEIC (HEVC is patent-encumbered), so HEIC is handled by the optional\r\n * peer dependency `libheif-js` (libheif + libde265 compiled to WebAssembly).\r\n *\r\n * WHY A WORKER THREAD: the WASM decoder is synchronous CPU work. Measured: a 12-megapixel photo takes ~2 seconds.\r\n * Run on the main thread, every iPhone upload would freeze the whole server for that long. The decoder therefore\r\n * lives in one long-lived worker thread (unref'd, so it never keeps your process alive) and is called through\r\n * promises. sharp itself uses libuv's thread pool the same way.\r\n *\r\n * LICENSE NOTE: libheif-js is LGPL-3.0. It is not bundled with or redistributed by this package: you install it\r\n * yourself (`npm install libheif-js`), which is what keeps this package MIT.\r\n *\r\n * LIMITS (honest): decoding only. There is no HEVC encoder in the WASM build, so `optimize` can convert FROM HEIC\r\n * to JPEG/WebP/PNG but not TO HEIC (Python's pillow-heif can). If a file holds several images, the first\r\n * top-level image is used.\r\n */\r\nimport { createRequire } from \"node:module\";\r\nimport { join } from \"node:path\";\r\nimport { Worker } from \"node:worker_threads\";\r\nimport { StorageError } from \"./errors.js\";\r\n\r\n/** ISO-BMFF `ftyp` major brands that identify HEIC/HEIF. Identical to the Python package's list. */\r\nconst HEIC_BRANDS = new Set([\r\n \"heic\",\r\n \"heix\",\r\n \"hevc\",\r\n \"heim\",\r\n \"heis\",\r\n \"mif1\",\r\n \"msf1\",\r\n]);\r\n\r\n/** True if the bytes start with an `ftyp` box carrying a HEIC/HEIF brand (content sniffing only; says nothing about validity). */\r\nexport function looksLikeHeic(data: Uint8Array): boolean {\r\n if (data.length < 12) return false;\r\n if (String.fromCharCode(data[4]!, data[5]!, data[6]!, data[7]!) !== \"ftyp\")\r\n return false;\r\n return HEIC_BRANDS.has(\r\n String.fromCharCode(data[8]!, data[9]!, data[10]!, data[11]!),\r\n );\r\n}\r\n\r\nexport const HEIC_INSTALL_HINT = \"npm install libheif-js\";\r\n\r\n// Runs inside the worker (CommonJS, evaluated from a string so it needs no separate file in either build format).\r\nconst WORKER_SOURCE = `\r\nconst { parentPort, workerData } = require(\"node:worker_threads\");\r\nconst libheif = require(workerData.libPath);\r\n// libheif-js reports decode problems by dumping a raw error object to the console. Capture that text instead:\r\n// it becomes part of the error we return (the useful diagnosis), and nothing is printed to the host's stderr.\r\nlet diag = \"\";\r\nfor (const level of [\"log\", \"info\", \"warn\", \"error\"]) {\r\n console[level] = (...args) => {\r\n diag = args.map((a) => (a && a.message) || (typeof a === \"string\" ? a : \"\")).filter(Boolean).join(\" \").trim();\r\n };\r\n}\r\nconst describe = (e) => {\r\n const base = String((e && e.message) || e);\r\n return diag && !base.includes(diag) ? base + \": \" + diag : base;\r\n};\r\nfunction open(bytes) {\r\n const images = new libheif.HeifDecoder().decode(bytes);\r\n if (!images || images.length === 0) throw new Error(\"no images found in HEIF container\");\r\n return images[0];\r\n}\r\nparentPort.on(\"message\", async ({ id, op, data }) => {\r\n diag = \"\";\r\n try {\r\n const img = open(new Uint8Array(data));\r\n const width = img.get_width();\r\n const height = img.get_height();\r\n if (op === \"info\") return parentPort.postMessage({ id, ok: true, width, height });\r\n const out = await new Promise((resolve, reject) =>\r\n img.display({ data: new Uint8ClampedArray(width * height * 4), width, height }, (d) =>\r\n d ? resolve(d) : reject(new Error(\"HEIF processing error\")),\r\n ),\r\n );\r\n if (op === \"validate\") return parentPort.postMessage({ id, ok: true, width, height });\r\n const rgba = out.data.buffer;\r\n parentPort.postMessage({ id, ok: true, width, height, rgba }, [rgba]);\r\n } catch (e) {\r\n parentPort.postMessage({ id, ok: false, error: describe(e) });\r\n }\r\n});\r\n`;\r\n\r\n/** The decoder worker itself failed (crashed or was killed). That is an infrastructure fault, not a bad image. */\r\nexport class HeicWorkerError extends StorageError {}\r\n\r\n/** `owner` ties a job to the worker that is running it, so one worker's death can never fail another worker's jobs. */\r\ntype Pending = { owner: Worker; resolve(v: any): void; reject(e: Error): void };\r\n\r\nlet worker: Worker | undefined;\r\nlet nextId = 1;\r\nconst pending = new Map<number, Pending>();\r\nconst stats = { info: 0, validate: 0, decode: 0, workersStarted: 0 };\r\n\r\n/** How the library path is found. Overridable for tests only. */\r\nlet resolveLib: () => string = () => {\r\n const attempts: (() => string)[] = [\r\n () => createRequire(import.meta.url).resolve(\"libheif-js/wasm-bundle\"), // next to / above this package\r\n () =>\r\n createRequire(join(process.cwd(), \"noop.js\")).resolve(\r\n \"libheif-js/wasm-bundle\",\r\n ), // the app's own node_modules\r\n ];\r\n let last: unknown;\r\n for (const attempt of attempts) {\r\n try {\r\n return attempt();\r\n } catch (e) {\r\n last = e;\r\n }\r\n }\r\n throw last;\r\n};\r\n\r\n/** Whether `libheif-js` can be found. */\r\nexport function heicAvailable(): boolean {\r\n try {\r\n resolveLib();\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Reference lifecycle: the worker is ref'd ONLY while it has jobs in flight and unref'd when idle.\r\n * - always ref'd: an idle decoder would keep your process (a CLI, a script, a test run) from ever exiting;\r\n * - always unref'd: the process could exit in the middle of a decode, since an unfinished promise does not\r\n * keep Node alive on its own.\r\n */\r\nfunction releaseIfIdle(w: Worker): void {\r\n for (const p of pending.values()) if (p.owner === w) return;\r\n w.unref();\r\n}\r\n\r\nfunction startWorker(): Worker {\r\n let libPath: string;\r\n try {\r\n libPath = resolveLib();\r\n } catch (cause) {\r\n throw new StorageError(\r\n `HEIC support requires the optional dependency 'libheif-js'. Install it with: ${HEIC_INSTALL_HINT}`,\r\n { cause },\r\n );\r\n }\r\n const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { libPath } });\r\n stats.workersStarted += 1;\r\n w.on(\r\n \"message\",\r\n (\r\n m: { id: number; ok: boolean; error?: string } & Record<string, unknown>,\r\n ) => {\r\n const p = pending.get(m.id);\r\n if (!p) return;\r\n pending.delete(m.id);\r\n releaseIfIdle(w);\r\n if (m.ok) p.resolve(m);\r\n else p.reject(new Error(m.error));\r\n },\r\n );\r\n const fail = (err: Error) => {\r\n if (worker === w) worker = undefined; // next call starts a fresh worker\r\n // A dying worker emits 'error' and 'exit' separately, and a replacement may already be running by the time the\r\n // second one arrives. Only ever fail the jobs THIS worker owned.\r\n for (const [id, p] of pending) {\r\n if (p.owner !== w) continue;\r\n pending.delete(id);\r\n p.reject(err);\r\n }\r\n };\r\n w.on(\"error\", (e) =>\r\n fail(\r\n new HeicWorkerError(\r\n `HEIC decoder worker crashed: ${e instanceof Error ? e.message : String(e)}`,\r\n { cause: e },\r\n ),\r\n ),\r\n );\r\n w.on(\"exit\", (code) =>\r\n fail(\r\n new HeicWorkerError(\r\n `HEIC decoder worker exited unexpectedly (code ${code})`,\r\n ),\r\n ),\r\n );\r\n w.unref(); // an idle decoder must never keep the host process alive (after listeners: adding them can re-ref the port)\r\n return w;\r\n}\r\n\r\nfunction call(\r\n op: \"info\" | \"validate\" | \"decode\",\r\n data: Uint8Array,\r\n): Promise<any> {\r\n stats[op] += 1;\r\n const w = (worker ??= startWorker());\r\n const id = nextId++;\r\n // Copy: the caller's Buffer may be a view into a shared pool, and the copy is transferred without a second copy.\r\n const copy = new Uint8Array(data).buffer;\r\n return new Promise((resolve, reject) => {\r\n pending.set(id, { owner: w, resolve, reject });\r\n w.ref(); // keep the process alive until this job settles\r\n w.postMessage({ id, op, data: copy }, [copy]);\r\n });\r\n}\r\n\r\n/** Read the dimensions from the container header. Cheap: no pixels are decoded. */\r\nexport async function heicInfo(\r\n data: Uint8Array,\r\n): Promise<{ width: number; height: number }> {\r\n const r = await call(\"info\", data);\r\n return { width: r.width, height: r.height };\r\n}\r\n\r\n/** Fully decode every pixel (to catch truncated/corrupted bodies) but return only the dimensions. */\r\nexport async function heicValidate(\r\n data: Uint8Array,\r\n): Promise<{ width: number; height: number }> {\r\n const r = await call(\"validate\", data);\r\n return { width: r.width, height: r.height };\r\n}\r\n\r\n/** Decode to raw RGBA pixels (4 bytes per pixel, already rotated/mirrored per the file's transform boxes). */\r\nexport async function decodeHeic(\r\n data: Uint8Array,\r\n): Promise<{ width: number; height: number; rgba: Buffer }> {\r\n const r = await call(\"decode\", data);\r\n return {\r\n width: r.width,\r\n height: r.height,\r\n rgba: Buffer.from(r.rgba as ArrayBuffer),\r\n };\r\n}\r\n\r\n/** Test hooks. Not exported from the package entry point. */\r\nexport const _testing = {\r\n stats,\r\n setResolver(fn: (() => string) | undefined): void {\r\n resolveLib = fn ?? _defaultResolver;\r\n void worker?.terminate();\r\n worker = undefined;\r\n },\r\n terminateWorker(): Promise<number> | undefined {\r\n return worker?.terminate();\r\n },\r\n hasWorker: () => worker !== undefined,\r\n};\r\nconst _defaultResolver = resolveLib;\r\n","/**\r\n * Image validation. Never trusts file extensions or client-supplied MIME types:\r\n * the bytes are decoded with sharp (libvips) and the *detected* format is what\r\n * gets validated and stored.\r\n *\r\n * sharp is an optional peer dependency, imported lazily, so `import \"zerobucket\"`\r\n * works without it (e.g. when you only store PDFs through a custom validator).\r\n */\r\nimport {\r\n HEIC_INSTALL_HINT,\r\n heicAvailable,\r\n heicInfo,\r\n heicValidate,\r\n looksLikeHeic,\r\n} from \"./heic.js\";\r\nimport {\r\n CorruptedImageError,\r\n ImageTooLargeError,\r\n ImageValidationError,\r\n StorageError,\r\n UnsupportedFormatError,\r\n} from \"./errors.js\";\r\n\r\n/** sharp format name -> canonical MIME type. Deliberately small allowlist. */\r\nconst FORMAT_TO_MIME: Record<string, string> = {\r\n jpeg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n heic: \"image/heic\", // optional: needs `npm install libheif-js` (see heic.ts)\r\n};\r\n\r\nexport const SUPPORTED_FORMATS: readonly string[] = Object.keys(FORMAT_TO_MIME);\r\n\r\n/** Reject images that would decode to more pixels than this, regardless of compressed size (~89 MP). */\r\nexport const DEFAULT_MAX_PIXELS = 89_000_000;\r\n\r\nexport interface ValidatedImage {\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n}\r\n\r\n// sharp <= 0.34 types the module as `export =` (the namespace itself is callable); sharp >= 0.35 ships separate\r\n// ESM typings where the callable is the namespace's `default`. Resolve to the callable either way.\r\ntype SharpModule = typeof import(\"sharp\");\r\ntype Sharp = SharpModule extends (...args: never[]) => unknown\r\n ? SharpModule\r\n : SharpModule extends { default: infer D }\r\n ? D\r\n : never;\r\nlet sharpPromise: Promise<Sharp> | undefined;\r\n\r\n/** Lazily load sharp, with an actionable error if it is not installed. */\r\nexport function loadSharp(): Promise<Sharp> {\r\n sharpPromise ??= import(\"sharp\").then(\r\n // ESM gives { default: sharp }; some CJS interop gives sharp directly.\r\n (m) =>\r\n (m as unknown as { default?: Sharp }).default ?? (m as unknown as Sharp),\r\n (cause) => {\r\n sharpPromise = undefined;\r\n throw new StorageError(\r\n \"Image validation requires the optional dependency 'sharp'. Install it with: npm install sharp\",\r\n { cause },\r\n );\r\n },\r\n );\r\n return sharpPromise;\r\n}\r\n\r\nexport async function validateImage(\r\n data: Buffer,\r\n opts: {\r\n maxBytes: number;\r\n maxPixels?: number;\r\n allowedFormats?: readonly string[];\r\n },\r\n): Promise<ValidatedImage> {\r\n const {\r\n maxBytes,\r\n maxPixels = DEFAULT_MAX_PIXELS,\r\n allowedFormats = SUPPORTED_FORMATS,\r\n } = opts;\r\n const sizeBytes = data.length;\r\n if (sizeBytes > maxBytes) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n if (sizeBytes === 0) throw new CorruptedImageError(\"Image data is empty\");\r\n\r\n // sharp's prebuilt libvips cannot decode HEIC, so it is handled (in a worker thread) by the optional libheif-js.\r\n if (looksLikeHeic(data))\r\n return validateHeic(data, { maxBytes, maxPixels, allowedFormats });\r\n\r\n const sharp = await loadSharp();\r\n // failOn \"error\": truncated / corrupt bodies throw instead of silently decoding partially.\r\n const make = () =>\r\n sharp(data, { limitInputPixels: maxPixels, failOn: \"error\" });\r\n\r\n let format: string | undefined;\r\n let width: number | undefined;\r\n let height: number | undefined;\r\n try {\r\n const meta = await make().metadata();\r\n format = meta.format;\r\n width = meta.width;\r\n height = meta.height;\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Could not decode image: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n\r\n if (\r\n !format ||\r\n !allowedFormats.includes(format) ||\r\n !(format in FORMAT_TO_MIME)\r\n ) {\r\n throw new UnsupportedFormatError(format ?? null, allowedFormats);\r\n }\r\n if (!width || !height)\r\n throw new CorruptedImageError(\"Image has no readable dimensions\");\r\n\r\n // Header parsing is not enough: force a full pixel decode to catch truncated bodies.\r\n //\r\n // How the decode is forced matters a lot, and was measured (benchmarks/node, `npm run validation-cost`):\r\n // - `.stats()` (used before 0.10.0) also computes entropy, sharpness and the dominant colour, and holds the whole\r\n // decoded image in memory: ~1.1 s and ~150 MB for a 45-megapixel JPEG. It also silently accepts a truncated\r\n // interlaced PNG.\r\n // - `.resize(1, 1)` is fast, but JPEG/WebP shrink-on-load decodes at reduced scale and then misses a file that is\r\n // only missing its last few bytes (an interrupted upload), so it is NOT used.\r\n // - Extracting a one-pixel-wide column of the full height is read sequentially, so every scanline is decoded at\r\n // full resolution and then discarded: the same decode work as a bare decode, with nothing kept and a ~1 KB output.\r\n // tests/validation.test.ts pins both properties: damaged files are still rejected, and memory stays flat.\r\n try {\r\n await sharp(data, {\r\n limitInputPixels: maxPixels,\r\n failOn: \"error\",\r\n sequentialRead: true,\r\n })\r\n .extract({ left: 0, top: 0, width: 1, height })\r\n .raw()\r\n .toBuffer();\r\n } catch (exc) {\r\n if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n throw new CorruptedImageError(\r\n `Image data is truncated or corrupted: ${decodeDetail(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n\r\n return { mimeType: FORMAT_TO_MIME[format]!, width, height, sizeBytes };\r\n}\r\n\r\nasync function validateHeic(\r\n data: Buffer,\r\n {\r\n maxBytes,\r\n maxPixels,\r\n allowedFormats,\r\n }: { maxBytes: number; maxPixels: number; allowedFormats: readonly string[] },\r\n): Promise<ValidatedImage> {\r\n const sizeBytes = data.length;\r\n // Same order as the Python package: a missing optional dependency is reported as such, never as \"corrupt\".\r\n if (!heicAvailable()) {\r\n throw new ImageValidationError(\r\n `This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`,\r\n );\r\n }\r\n if (!allowedFormats.includes(\"heic\"))\r\n throw new UnsupportedFormatError(\"heic\", allowedFormats);\r\n\r\n let width: number;\r\n let height: number;\r\n try {\r\n ({ width, height } = await heicInfo(data)); // header only: cheap, and lets us refuse a pixel bomb BEFORE decoding\r\n } catch (exc) {\r\n if (exc instanceof StorageError) throw exc; // the decoder worker failed: not the image's fault\r\n throw new CorruptedImageError(\r\n `Could not decode image: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n if (!width || !height)\r\n throw new CorruptedImageError(\"Image has no readable dimensions\");\r\n if (width * height > maxPixels)\r\n throw new ImageTooLargeError(sizeBytes, maxBytes);\r\n\r\n try {\r\n await heicValidate(data); // full pixel decode, like Pillow's img.load(): catches truncated bodies\r\n } catch (exc) {\r\n if (exc instanceof StorageError) throw exc;\r\n throw new CorruptedImageError(\r\n `Image data is truncated or corrupted: ${errMessage(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n return { mimeType: FORMAT_TO_MIME.heic!, width, height, sizeBytes };\r\n}\r\n\r\n/** libvips reports some decoder failures (for example a damaged interlaced PNG) only as \"Unknown error\". */\r\nfunction decodeDetail(exc: unknown): string {\r\n const msg = errMessage(exc).trim();\r\n return /^unknown error$/i.test(msg)\r\n ? \"the decoder stopped before the end of the image\"\r\n : msg;\r\n}\r\n\r\nfunction isPixelLimit(exc: unknown): boolean {\r\n return /pixel limit/i.test(errMessage(exc));\r\n}\r\n\r\nexport function errMessage(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n\r\nexport { ImageValidationError };\r\n","/**\r\n * Optimize pipeline: strip metadata, optionally downscale and re-encode.\r\n * Mirrors the Python package's optimize_image(), implemented on sharp.\r\n *\r\n * Differences from the Python/Pillow version, both deliberate:\r\n * - EXIF orientation is applied (auto-rotate) BEFORE metadata is stripped.\r\n * Stripping EXIF without applying it would leave phone photos sideways.\r\n * - Transparent PNG/WebP converted to JPEG is flattened onto white, not black.\r\n */\r\nimport { ImageValidationError } from \"./errors.js\";\r\nimport {\r\n decodeHeic,\r\n heicAvailable,\r\n HEIC_INSTALL_HINT,\r\n looksLikeHeic,\r\n} from \"./heic.js\";\r\nimport { DEFAULT_MAX_PIXELS, loadSharp, validateImage } from \"./validation.js\";\r\n\r\n/** Formats the optimizer can WRITE. (HEIC can be read but not written: there is no HEVC encoder for Node.) */\r\nconst ENCODABLE_FORMATS = [\"jpeg\", \"png\", \"webp\"] as const;\r\n\r\nexport const DEFAULT_JPEG_QUALITY = 90;\r\nexport const DEFAULT_WEBP_QUALITY = 88;\r\n\r\nexport interface OptimizeOptions {\r\n /** Downscale (aspect ratio preserved, Lanczos) if wider than this. Never upscales. */\r\n maxWidth?: number;\r\n /** \"jpeg\" | \"png\" | \"webp\". Defaults to the source format. */\r\n format?: string;\r\n /** 1-100, JPEG/WebP only (ignored for PNG). */\r\n quality?: number;\r\n}\r\n\r\nexport interface OptimizationResult {\r\n data: Buffer;\r\n mimeType: string;\r\n width: number;\r\n height: number;\r\n sizeBytes: number;\r\n originalSizeBytes: number;\r\n}\r\n\r\nexport async function optimizeImage(\r\n data: Buffer,\r\n opts: OptimizeOptions & { maxBytes: number; maxPixels?: number },\r\n): Promise<OptimizationResult> {\r\n const { maxWidth, quality, maxBytes, maxPixels = DEFAULT_MAX_PIXELS } = opts;\r\n const sharp = await loadSharp();\r\n const originalSizeBytes = data.length;\r\n\r\n const requestedRaw = opts.format?.toLowerCase();\r\n const requested = requestedRaw === \"jpg\" ? \"jpeg\" : requestedRaw;\r\n if (requested === \"heic\" || requested === \"heif\") {\r\n throw new ImageValidationError(\r\n \"Encoding to HEIC is not available in the Node package (there is no HEVC encoder for Node). \" +\r\n \"Optimize to jpeg, webp or png instead, or store the original HEIC without `optimize`.\",\r\n );\r\n }\r\n\r\n // Where the pixels come from: HEIC is decoded by libheif-js (in a worker thread) to raw RGBA that sharp then\r\n // re-encodes; everything else is read by sharp directly. EXIF is irrelevant for raw input (HEIC orientation\r\n // is already applied by the decoder), and sharp strips metadata on output either way.\r\n let input: Buffer = data;\r\n let inputOptions: Parameters<typeof sharp>[1] = {\r\n limitInputPixels: maxPixels,\r\n failOn: \"error\",\r\n };\r\n let sourceFormat: string | undefined;\r\n if (looksLikeHeic(data)) {\r\n if (!heicAvailable()) {\r\n throw new ImageValidationError(\r\n `This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`,\r\n );\r\n }\r\n if (!requested) {\r\n throw new ImageValidationError(\r\n \"Optimizing a HEIC image needs an explicit output format, because re-encoding to HEIC is not available in the \" +\r\n \"Node package. Pass optimize: { format: 'jpeg' | 'webp' | 'png' } (for example { format: 'webp', maxWidth: 1600 }).\",\r\n );\r\n }\r\n const decoded = await decodeHeic(data);\r\n input = decoded.rgba;\r\n inputOptions = {\r\n raw: { width: decoded.width, height: decoded.height, channels: 4 },\r\n };\r\n sourceFormat = \"heic\";\r\n } else {\r\n sourceFormat = (await sharp(data, inputOptions).metadata()).format;\r\n }\r\n const target = requested ?? sourceFormat ?? \"jpeg\";\r\n if (!(ENCODABLE_FORMATS as readonly string[]).includes(target)) {\r\n throw new ImageValidationError(\r\n `Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify([...ENCODABLE_FORMATS].sort())}`,\r\n );\r\n }\r\n\r\n // .rotate() with no args = auto-orient from EXIF. sharp drops all metadata on output by default.\r\n let pipeline = sharp(input, inputOptions).rotate();\r\n if (maxWidth !== undefined) {\r\n pipeline = pipeline.resize({\r\n width: maxWidth,\r\n fit: \"inside\",\r\n withoutEnlargement: true,\r\n kernel: \"lanczos3\",\r\n });\r\n }\r\n\r\n if (target === \"jpeg\") {\r\n pipeline = pipeline\r\n .flatten({ background: \"#ffffff\" })\r\n .jpeg({\r\n quality: quality ?? DEFAULT_JPEG_QUALITY,\r\n mozjpeg: true,\r\n progressive: true,\r\n });\r\n } else if (target === \"webp\") {\r\n pipeline = pipeline.webp({\r\n quality: quality ?? DEFAULT_WEBP_QUALITY,\r\n effort: 6,\r\n });\r\n } else {\r\n pipeline = pipeline.png({ compressionLevel: 9 }); // no lossy knob; quality ignored by design\r\n }\r\n\r\n const out = await pipeline.toBuffer();\r\n\r\n // Defense in depth: re-validate our own output before it is trusted.\r\n const revalidated = await validateImage(out, { maxBytes, maxPixels });\r\n return {\r\n data: out,\r\n mimeType: revalidated.mimeType,\r\n width: revalidated.width,\r\n height: revalidated.height,\r\n sizeBytes: revalidated.sizeBytes,\r\n originalSizeBytes,\r\n };\r\n}\r\n","/**\r\n * S3-compatible object storage for tiering (AWS S3, MinIO, Cloudflare R2,\r\n * Backblaze B2, DigitalOcean Spaces, ...).\r\n *\r\n * Scoped to exactly what tiering needs: upload, download, ranged download,\r\n * idempotent delete, exists. The bucket must already exist: bucket creation\r\n * involves choices (region, versioning, lifecycle) that belong to you, not to\r\n * a storage library.\r\n *\r\n * `@aws-sdk/client-s3` is an optional peer dependency, imported lazily on first\r\n * use, so `import \"zerobucket\"` works without it.\r\n *\r\n * Objects are keyed by the image's UUID, with no prefix. That is the same key\r\n * scheme the Python package uses, so either package can read, tier and clean\r\n * up objects the other one created in the same bucket.\r\n */\r\nimport { StorageError } from \"./errors.js\";\r\n\r\n/** The surface the Postgres adapter needs. Implement this to plug in a non-S3 store (or a test double). */\r\nexport interface ObjectStorageLike {\r\n readonly bucket: string;\r\n upload(key: string, data: Buffer, opts: { mimeType: string }): Promise<void>;\r\n download(key: string): Promise<Buffer>;\r\n /** Inclusive byte range, fetched with a real HTTP Range request. */\r\n downloadRange(\r\n key: string,\r\n start: number,\r\n endInclusive: number,\r\n ): Promise<Buffer>;\r\n /** Idempotent: deleting a missing key is not an error. */\r\n delete(key: string): Promise<void>;\r\n exists(key: string): Promise<boolean>;\r\n}\r\n\r\nexport interface ObjectStorageOptions {\r\n /** Must already exist. */\r\n bucket: string;\r\n /** Set for anything that is not AWS S3 itself (MinIO, R2, B2, Spaces, ...). */\r\n endpoint?: string;\r\n /** Default: AWS_REGION / AWS_DEFAULT_REGION, else \"us-east-1\". Many S3-compatible services ignore it but the SDK requires one. */\r\n region?: string;\r\n /** Default: the SDK's standard chain (env vars, shared config, IAM role, ...). */\r\n credentials?: {\r\n accessKeyId: string;\r\n secretAccessKey: string;\r\n sessionToken?: string;\r\n };\r\n /** Path-style addressing (`endpoint/bucket/key`). Default: true when `endpoint` is set, else false. */\r\n forcePathStyle?: boolean;\r\n /** Bring your own configured S3Client (advanced). */\r\n client?: unknown;\r\n}\r\n\r\ntype Sdk = typeof import(\"@aws-sdk/client-s3\");\r\ntype S3 = import(\"@aws-sdk/client-s3\").S3Client;\r\n\r\nexport class ObjectStorage implements ObjectStorageLike {\r\n readonly bucket: string;\r\n private clientPromise: Promise<{ sdk: Sdk; client: S3 }> | undefined;\r\n\r\n constructor(private readonly opts: ObjectStorageOptions) {\r\n if (!opts.bucket)\r\n throw new TypeError(\"ObjectStorage requires a bucket name\");\r\n this.bucket = opts.bucket;\r\n }\r\n\r\n private connection(): Promise<{ sdk: Sdk; client: S3 }> {\r\n this.clientPromise ??= (async () => {\r\n let sdk: Sdk;\r\n try {\r\n sdk = await import(\"@aws-sdk/client-s3\");\r\n } catch (cause) {\r\n throw new StorageError(\r\n \"Object-storage tiering requires the optional dependency '@aws-sdk/client-s3'. \" +\r\n \"Install it with: npm install @aws-sdk/client-s3\",\r\n { cause },\r\n );\r\n }\r\n const o = this.opts;\r\n const client =\r\n (o.client as S3 | undefined) ??\r\n new sdk.S3Client({\r\n endpoint: o.endpoint,\r\n region:\r\n o.region ??\r\n process.env.AWS_REGION ??\r\n process.env.AWS_DEFAULT_REGION ??\r\n \"us-east-1\",\r\n credentials: o.credentials,\r\n forcePathStyle: o.forcePathStyle ?? o.endpoint !== undefined,\r\n });\r\n return { sdk, client };\r\n })().catch((exc) => {\r\n this.clientPromise = undefined;\r\n throw exc;\r\n });\r\n return this.clientPromise;\r\n }\r\n\r\n async upload(\r\n key: string,\r\n data: Buffer,\r\n { mimeType }: { mimeType: string },\r\n ): Promise<void> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n await client.send(\r\n new sdk.PutObjectCommand({\r\n Bucket: this.bucket,\r\n Key: key,\r\n Body: data,\r\n ContentType: mimeType,\r\n }),\r\n );\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to upload ${JSON.stringify(key)} to bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async download(key: string): Promise<Buffer> {\r\n return this.get(key, undefined);\r\n }\r\n\r\n async downloadRange(\r\n key: string,\r\n start: number,\r\n endInclusive: number,\r\n ): Promise<Buffer> {\r\n return this.get(key, `bytes=${start}-${endInclusive}`);\r\n }\r\n\r\n private async get(key: string, range: string | undefined): Promise<Buffer> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n const res = await client.send(\r\n new sdk.GetObjectCommand({\r\n Bucket: this.bucket,\r\n Key: key,\r\n Range: range,\r\n }),\r\n );\r\n return Buffer.from(await res.Body!.transformToByteArray());\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to download ${JSON.stringify(key)} from bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n // S3 DeleteObject succeeds for keys that do not exist, so this is idempotent by nature.\r\n await client.send(\r\n new sdk.DeleteObjectCommand({ Bucket: this.bucket, Key: key }),\r\n );\r\n } catch (exc) {\r\n throw new StorageError(\r\n `Failed to delete ${JSON.stringify(key)} from bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n const { sdk, client } = await this.connection();\r\n try {\r\n await client.send(\r\n new sdk.HeadObjectCommand({ Bucket: this.bucket, Key: key }),\r\n );\r\n return true;\r\n } catch (exc) {\r\n const e = exc as {\r\n name?: string;\r\n $metadata?: { httpStatusCode?: number };\r\n };\r\n if (\r\n e.name === \"NotFound\" ||\r\n e.name === \"NoSuchKey\" ||\r\n e.$metadata?.httpStatusCode === 404\r\n )\r\n return false;\r\n throw new StorageError(\r\n `Failed to check ${JSON.stringify(key)} in bucket ${JSON.stringify(this.bucket)}: ${msg(exc)}`,\r\n { cause: exc },\r\n );\r\n }\r\n }\r\n}\r\n\r\nfunction msg(exc: unknown): string {\r\n return exc instanceof Error ? exc.message : String(exc);\r\n}\r\n","import { ContentValidationError } from \"../errors.js\";\r\nimport type { ContentValidator, ValidatedContent } from \"./types.js\";\r\n\r\nexport const DEFAULT_MAX_PDF_BYTES = 20 * 1024 * 1024;\r\n\r\n/**\r\n * Validates that bytes look like a PDF: `%PDF-` magic bytes, non-empty, under a\r\n * size ceiling. Deliberately minimal.\r\n *\r\n * Security note: this does NOT parse the PDF object structure and does not\r\n * detect embedded JavaScript, forms or launch actions. Treat it as a sanity\r\n * gate, not a security boundary, if you serve user-uploaded PDFs to other users.\r\n */\r\nexport class PDFValidator implements ContentValidator {\r\n constructor(private readonly opts: { maxBytes?: number } = {}) {}\r\n\r\n validate(data: Buffer, { maxBytes }: { maxBytes: number }): ValidatedContent {\r\n const effectiveMax = Math.min(\r\n maxBytes,\r\n this.opts.maxBytes ?? DEFAULT_MAX_PDF_BYTES,\r\n );\r\n if (data.length === 0)\r\n throw new ContentValidationError(\"PDF data is empty\");\r\n if (data.length > effectiveMax) {\r\n throw new ContentValidationError(\r\n `PDF is ${data.length} bytes, exceeds the maximum of ${effectiveMax} bytes`,\r\n );\r\n }\r\n // The spec allows junk before the header; real readers scan the first 1024 bytes.\r\n if (!data.subarray(0, 1024).includes(\"%PDF-\")) {\r\n throw new ContentValidationError(\r\n \"This does not look like a valid PDF (missing '%PDF-' header). Content is \" +\r\n \"checked by inspecting the actual bytes, not the filename or any client-supplied content type.\",\r\n );\r\n }\r\n return {\r\n mimeType: \"application/pdf\",\r\n sizeBytes: data.length,\r\n width: null,\r\n height: null,\r\n };\r\n }\r\n}\r\n"],"mappings":"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","names":["start","sql","SCHEMA","DEDUP_SCHEMA","INSERT","SELECT_FULL","SELECT_METADATA","EXISTS","SELECT_FOR_TIERING","UPDATE_AFTER_TIERING","D_FROM","D_COLS","D_INSERT_REF","D_DELETE_REF","D_DECREMENT","D_DELETE_EMPTY","randomUUID","SCHEMA","COLS","INSERT","SELECT_FULL","SELECT_MANY","SELECT_METADATA","STREAM_INFO","CHUNK","EXISTS","DELETE_ONE","SELECT_FOR_TIERING","UPDATE_AFTER_TIERING","D_INSERT_REF","D_COLS","D_FROM","D_DELETE_REF","D_DECREMENT","D_DELETE_EMPTY","CLASSIC_SQL","DEDUP_SQL","isRetryable","isConnectionLevel","MAX_BACKOFF_MS","backoffDelayMs","randomUUID","blobParams","rowParams","msg","mapLimit","msg"]}
|