import type { Pool as MysqlPool } from "mysql2/promise"; import type { Pool as PgPool } from "pg"; /** (a)(clay 拍 a+c,2026-07-27)tidb 方言的默认 per-blob 帽 = **6 MiB,TiDB 自己的默认 * `txn-entry-size-limit`**。两堵墙真库实测(k3s8 tidb-test v7.5.1 默认配置,2026-07-27 探针): * 7MiB=`entry too large`(entry 墙,默认 6291456)、31MiB=同、32MiB=`max_allowed_packet`(包墙, * 64MiB ÷ ~2× 文本协议膨胀)——矮墙是 entry 的 6MiB,不是先前推测的包墙 ~31MiB。帽=诚实拒绝: * 超限 blob 在 query 之前 typed 拒(消费面 HTTP 413 / capture warn 拿到可行动信息);**帽拒绝的 * 在默认配置下本来也落不进去 ⇒ 零回归**。调大过 entry 限/packet 的部署用 SNAPSHOT_BLOB_SQL_MAX_BYTES * 放宽(两方言都生效)。**pg 方言默认不设帽**(扩展协议无这两堵墙,不人为降档)。D-1 附件战役同墙 * 教训:task-attachment-store.ts:19。 */ export declare const SQL_BLOB_DEFAULT_MAX_BYTES = 6291456; /** typed 判别错:blob 超过 SQL 店的承载帽。判别码供店侧结果映射/HTTP 面/日志使用——绝不能与 * transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。 */ export declare class BlobTooLargeError extends Error { readonly code: "blob_too_large_for_sql"; constructor(byteLen: number, capBytes: number); } /** * The content-addressed byte store. All four methods are keyed on the sha256 `hash` of the bytes: * - putBlob: store bytes (idempotent — a repeat for the same hash is a no-op-equivalent; the SQL twin refreshes * created_at so a just-written/re-used blob is GC-grace-protected). MinIO PUTs the object AND upserts the SQL INDEX. * - getBlob: the bytes for `hash`, or undefined if absent. A TRANSIENT backend fault THROWS (so a restore/import fails * LOUD, not a silent "missing blob"); only a genuine absence / corruption is undefined. * - hasBlobs: the subset of `hashes` that already exist — a single SQL `IN(...)` over the index for BOTH backends * (migration idempotency + reap probing + the §10 import presence pre-check; cheaper than a HEAD-per-hash on MinIO). * - deleteBlobs: byte-delete the given hashes (called ONLY by the async sweep reaper). Honors the SAME created_at * grace window as the SQL backend (MinIO reads the index's created_at) — delete-in-use is impossible. Returns the * count ACTUALLY deleted (the grace-passed subset), so the sweep reports real work, not the pre-grace orphan count. */ export interface BlobBackend { putBlob(hash: string, bytes: Uint8Array): Promise; getBlob(hash: string): Promise; hasBlobs(hashes: string[]): Promise>; deleteBlobs(hashes: string[]): Promise; } /** * SQL BlobBackend — the `snapshot_blob` table ops EXTRACTED VERBATIM from the tidb/pg file-snapshot stores (the * created_at-refreshing upsert, the bytes SELECT, the grace-window orphan DELETE→deleteBlobs, a has via SELECT). Two * dialect twins selected by `dialect`. Keeping the SQL byte-identical means the existing behaviour + the env-gated * real-DB integration suite are unchanged when MinIO is unset (the DEFAULT backend the file-snapshot stores construct). * * NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the reference (`not in the manifest`) filter: that * check stays in the owning store, which computes the orphan hash SET from its own domain's manifest and hands it here. The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob * that a concurrent in-flight snapshot still needs is never byte-deleted (delete-in-use is data loss; under-delete is * harmless — the next sweep collects it). */ export declare class SqlBlobBackend implements BlobBackend { private readonly dialect; private readonly pool; /** Effective per-blob cap: explicit ctor value wins; else tidb=SQL_BLOB_DEFAULT_MAX_BYTES, pg=uncapped. */ private readonly capBytes; /** Blob-domain table ([ref] blob 域隔离:file-history 域用自己的表,reap/GC 不跨域)。代码内字面量,非用户输入。 */ private readonly table; constructor(dialect: "tidb" | "pg", pool: MysqlPool | PgPool, maxBytes?: number, table?: string); /** The created_at-refreshing content-addressed upsert — VERBATIM the tidb/pg stores' `putBlobRow`. Refreshes * created_at even on a dedup hit so a re-used/just-written blob is "touched=now" and GC-grace-protected. * (a): an over-cap blob is REJECTED (typed {@link BlobTooLargeError}) BEFORE the driver ever sees the bytes — * the alternative is an inscrutable packet-limit error deep in the driver, and on the HTTP face a 502 that * invites the peer to retry a blob that can never land. */ putBlob(hash: string, bytes: Uint8Array): Promise; /** The bytes for `hash` (undefined if absent) — VERBATIM the tidb/pg stores' getBlob SELECT. A pool/query error * PROPAGATES (a transient DB fault is loud, not a silent "missing blob" — parity with the MinIO backend's throw). */ getBlob(hash: string): Promise; /** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */ hasBlobs(hashes: string[]): Promise>; /** Byte-delete the given hashes (the orphan SET is computed by the CALLER from its own domain's manifest). Retains * the grace-window guard so a just-written blob a concurrent in-flight write needs is never delete-in-use — VERBATIM * the tidb/pg stores' gcOrphanBlobs delete, minus the reference clause (the caller already excluded referenced * hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */ deleteBlobs(hashes: string[]): Promise; } /** SHARED index op(workspace 浏览面 #3,[ref]②)—— per-blob 字节数,读 SQL INDEX 的 `byte_len` * 列(sqlHasBlobs 同款单 IN 查询;MinIO 后端的 bytes-NULL 索引行同样带 byte_len,两后端同源)。 * tree/file 面的 size 投影与「超限先拒后拉」都靠它——绝不为拿大小去 getBlob 整块字节。 */ export declare function sqlBlobSizes(dialect: "tidb" | "pg", pool: MysqlPool | PgPool, hashes: string[]): Promise>; export interface MinioBlobBackendConfig { endpoint: string; bucket: string; accessKey: string; secretKey: string; region?: string; /** Key prefix for blob objects, e.g. "blobs/". Object key = keyPrefix + hash. Default "blobs/". */ keyPrefix?: string; /** Presigned-URL TTL (seconds). Default 3600 (1h) — must comfortably exceed a ~64 MiB single-PUT/GET on a slow link * (a SigV4 X-Amz-Expires bounds the WHOLE transfer from X-Amz-Date; a too-short TTL fails mid-stream). */ presignTtlSec?: number; /** 测试注入口(listObjects 分页钉用);生产缺省 global fetch。 */ fetchImpl?: typeof fetch; } /** * MinIO BlobBackend — content-addressed bytes in an S3-compatible object store, via the zero-dependency * {@link presignS3Url} SigV4 generator + `fetch` (NO S3 SDK; the repo's no-SDK posture). The object key is * `keyPrefix + hash` (content-addressed → idempotent PUT, dedup across snapshots is automatic). * * THE SQL INDEX: the bytes go to MinIO, but a `snapshot_blob` INDEX row `(blob_hash, byte_len, created_at)` with bytes * NULL is kept in SQL (same pool/dialect). This is what makes the orphan-reference computation + the created_at grace * window + the §10 presence pre-check work identically to the SQL backend (see the file header). hasBlobs reads the * index; putBlob PUTs then upserts the index (created_at-refresh on conflict); deleteBlobs deletes the grace-passed * index rows AND the matching MinIO objects (called ONLY from the async sweep reaper). * * INTEGRITY (security-critical): getBlob VERIFIES sha256(bytes) === hash before returning. A content-addressed store's * whole safety rests on bytes-match-hash; a mismatch means corruption (network) or POISONING (a hostile object written * under a hash it doesn't match) — treated as ABSENT (undefined), NEVER returning wrong bytes. This mirrors the * importManifest content-address check the SQL stores already do. */ /** 鲁棒性批2 C2/C3/C6(2026-08-03):S3 兼容端点 IO 的统一 abort 上界(fleet-client C1 定常量形 * 先例)。半开端点此前各挂 undici 默认 ~300s headersTimeout——blob GET 卡住 restore/session-import * 同步路径,SendUserFile 工具调用永不返回。120s 对慢链路 ~64MiB 单 PUT/GET 仍宽裕(presignTtlSec * 注释同口径),远小于病形。send-user-file 与 leader/diffup 同 import 此常量。 */ export declare const OBJECT_STORE_IO_TIMEOUT_MS = 120000; export declare class MinioBlobBackend implements BlobBackend { private readonly cfg; private readonly keyPrefix; private readonly ttl; /** The SQL index pool+dialect, or undefined for an index-LESS backend (the migration script + the unit suite, which * exercise ONLY the object I/O — put/get/has-by-HEAD/delete — with no DB). When undefined, hasBlobs falls back to a * HEAD-per-hash probe and deleteBlobs ignores the grace window (the script is a stop-the-world copy, not the live GC * path). The file-snapshot stores ALWAYS pass the pool, so the live reference-tracking always has the index. */ private readonly indexPool?; private readonly indexDialect?; /** Blob-domain index table ([ref] 域隔离,SqlBlobBackend.table 同款;默认 = E19 纪元的 snapshot_blob)。 */ private readonly indexTable; constructor(cfg: MinioBlobBackendConfig, index?: { dialect: "tidb" | "pg"; pool: MysqlPool | PgPool; table?: string; }); private objectKey; private presign; /** PUT the bytes under the content-addressed key, THEN upsert the SQL INDEX row (bytes NULL, created_at refresh on * conflict — the SAME grace discipline as SqlBlobBackend, so a just-PUT blob an in-flight snapshot needs is touched= * now and protected). Idempotent (same hash ⇒ overwrite-with-identical-bytes + created_at bump). A non-2xx PUT throws * (an upload failure must surface — a silent drop would later read_failed on restore). The PUT happens BEFORE the * index upsert so an index row never claims a blob the object store doesn't hold (an index row with no object would * let hasBlobs report a phantom present; the reverse — an object with no index — is harmless and pruned by a bucket * lifecycle if it ever happens). NOTE no post-PUT read-back verify on the LIVE path (perf): content-addressing * catches a corrupted PUT at the next getBlob sha-check (fail-closed); the one-time migration script DOES read-back. */ putBlob(hash: string, bytes: Uint8Array): Promise; /** GET the bytes for `hash`, then VERIFY content-address (sha256(bytes)===hash). THREE branches (finding 11): * - 404 → undefined (genuinely ABSENT — the file-snapshot store treats undefined as a missing blob). * - sha256 MISMATCH → undefined (corruption/poisoning; treat corrupt-as-absent, fail-CLOSED, NEVER wrong bytes). * - a NON-2xx-NON-404 (5xx/throttle) OR a network/fetch REJECT → THROW (a TRANSIENT fault must fail LOUD so the * restore/import surfaces an error, NOT silently drop a file as "missing"; the SQL backend likewise propagates a * pool error). The snapshot store's restore() wraps applyManifest so this throw becomes a {ok:false} restore_failed. */ getBlob(hash: string): Promise; /** The subset of `hashes` present — reads the SQL INDEX (cheaper than a HEAD-per-hash AND consistent with the SQL * backend). Falls back to a presigned HEAD per hash ONLY in object-only mode (no index pool: the migration / unit * path). The caller passes DISTINCT hashes (migration idempotency batch / reap orphan set / §10 presence pre-check). */ hasBlobs(hashes: string[]): Promise>; /** D-1 孤儿对象 GC(clay 拍 2026-07-28):列举本后端 keyPrefix 下的全部对象(ListObjectsV2 分页, * 零依赖 XML 取 //)。**server 端 sweep 专用**——presign 的 * bucket-GET 列举能力绝不外发。响应非 2xx 即 throw(sweep 调用方自 catch:列举失败=本轮跳过, * 绝不把「列不出来」当「没有孤儿」)。 */ listObjects(): AsyncGenerator<{ hash: string; lastModifiedMs: number; }>; /** 单对象 HEAD 探针(复审 B-F1 的删前复查面):返回当前 LastModified(缺席=对象已不在)。 * put 会 PUT 同键 ⇒ 刷新 LastModified,所以「删前再 HEAD 一次」能看见「刚刚被重新引用」的对象。 */ headObject(hash: string): Promise<{ lastModifiedMs: number; } | undefined>; /** Byte-delete the given hashes — called ONLY from the async sweepOrphanBlobs reaper (NEVER the synchronous purge * path). Honors the SAME created_at grace window as the SQL backend: with an index, DELETE the grace-passed index * rows first (the index's created_at is the authoritative age — an S3 object carries no service-controlled timestamp * the backend reads), then delete the MinIO objects ONLY for those grace-passed hashes (so a just-PUT blob an * in-flight snapshot needs is NOT delete-in-use). ERROR-TOLERANT + BOUNDED: each object delete is independent (a * failure is skipped → retried next sweep), at most MINIO_OP_CONCURRENCY in flight (no FD/throttle storm). In * object-only mode (no index pool: the migration/unit cleanup path) it deletes every given hash with no grace. */ deleteBlobs(hashes: string[]): Promise; } //# sourceMappingURL=blob-backend.d.ts.map