secure-upload-fastify-sdk 1.0.5 → 1.0.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../../src/server/routes/multipart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAW7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AA2EpD,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,EAAE,EAAE,mBAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CAiff"}
1
+ {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../../src/server/routes/multipart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAmD,MAAM,SAAS,CAAC;AAWhG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAiJpD,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,EAAE,EAAE,mBAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CA8ff"}
@@ -129,7 +129,76 @@ export interface SecureUploadOptions {
129
129
  lookbackDays?: number;
130
130
  batchSize?: number;
131
131
  };
132
+ /**
133
+ * 分片合并完成后的业务钩子(v1.5.1+)
134
+ * - 在 fileRepo.create 之后、multipartRepo.markCompleted 之前调用
135
+ * - 钩子可以读取已合并的原文件、判断、做后处理(水印 / 缩略图 / 转码 / ...)
136
+ * - 钩子返回 OnCompletePatch 时,SDK 会调 fileRepo.update(fileId, patch) 把 file record 升级
137
+ * - 钩子返回 undefined / void → 不 update,保留原 file record
138
+ * - 钩子抛错 → SDK 捕获并 log.warn,**不影响主流程**(上传仍然返回 200)
139
+ * - 钩子超时由钩子内部自行控制(SDK 不加全局超时,留给业务方)
140
+ *
141
+ * 设计动机:
142
+ * - 不污染 SDK 主链路(分片/秒传/限流等),又允许业务做"上传完做点什么"
143
+ * - 业务可自由决定要不要 update file record / 是否生成新文件 / 是否写其他表
144
+ *
145
+ * 使用示例(上传后做水印):
146
+ * await app.register(secureUploadPlugin, {
147
+ * ...,
148
+ * onComplete: async (ctx) => {
149
+ * const buf = await ctx.getOriginalBuffer();
150
+ * if (!ctx.file.contentType.startsWith('image/')) return;
151
+ * const result = await processWatermark(buf, ...);
152
+ * const put = await ctx.storage.put(result.output, {...});
153
+ * return { storageKey: put.key, size: result.output.length,
154
+ * contentType: result.outputMime, watermarked: true };
155
+ * },
156
+ * });
157
+ */
158
+ onComplete?: OnCompleteHook;
159
+ }
160
+ /**
161
+ * 钩子返回的 file record 升级 patch
162
+ * - 字段都是 optional,只更新传入的字段
163
+ * - storageKey 改变后,SDK 内部不会主动删除原 storageKey 上的文件
164
+ * (钩子自己决定要不要删 / 留 / 转存)
165
+ */
166
+ export interface OnCompletePatch {
167
+ /** 替换后的存储 key(钩子 put 新文件后传过来) */
168
+ storageKey?: string;
169
+ /** 替换后的字节数 */
170
+ size?: number;
171
+ /** 替换后的 mime */
172
+ contentType?: string;
173
+ /** 替换后的业务元信息(整段覆盖) */
174
+ meta?: Record<string, unknown>;
175
+ /** 是否已嵌入数字水印 */
176
+ watermarked?: boolean;
177
+ /** 是否是原图(水印链路里 isOriginal=false,表示这是从原图派生的版本) */
178
+ isOriginal?: boolean;
179
+ /** 父文件 id(派生关系) */
180
+ parentId?: string | null;
181
+ /** 过期时间(留痕 / 清理用) */
182
+ retentionExpiresAt?: Date | null;
132
183
  }
184
+ /**
185
+ * 钩子上下文(v1.5.1+)
186
+ * - file: 刚 fileRepo.create 出来的 file record
187
+ * - storage: 钩子可以 put 新文件 / get 原文件
188
+ * - getOriginalBuffer(): 把 file.storageKey 对应的文件读成 Buffer(给水印 / 转码用)
189
+ * - log: fastify logger,钩子记日志用它
190
+ */
191
+ export interface OnCompleteContext {
192
+ file: FileRecord;
193
+ storage: StorageAdapter;
194
+ log: any;
195
+ getOriginalBuffer(): Promise<Buffer>;
196
+ }
197
+ /**
198
+ * 业务钩子签名
199
+ * - 异步函数;返回 patch / undefined;抛错会被 SDK 捕获不影响主流程
200
+ */
201
+ export type OnCompleteHook = (ctx: OnCompleteContext) => Promise<OnCompletePatch | void>;
133
202
  /**
134
203
  * 运行时上下文(挂到 fastify.secureUpload)
135
204
  * - 路由处理函数从这里拿 storage / repo / validator
@@ -159,6 +228,8 @@ export interface SecureUploadRuntime {
159
228
  };
160
229
  /** session → userId,默认 base64 JWT,用户可注入 */
161
230
  validateSession: SessionValidator;
231
+ /** 分片合并完成后的业务钩子(可选) */
232
+ onComplete: OnCompleteHook | null;
162
233
  }
163
234
  declare module 'fastify' {
164
235
  interface FastifyInstance {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
@@ -85,7 +85,12 @@ function toFileRecord(row) {
85
85
  meta: row.meta && typeof row.meta === "object" ? row.meta : null,
86
86
  createdAt: row.createdAt,
87
87
  updatedAt: row.updatedAt,
88
- deletedAt: row.deletedAt
88
+ deletedAt: row.deletedAt,
89
+ // v1.5.1+:水印溯源字段(可选;旧 schema/老库里 row 可能没有,需要兜底)
90
+ watermarked: row.watermarked ?? false,
91
+ isOriginal: row.isOriginal ?? true,
92
+ parentId: row.parentId ?? null,
93
+ retentionExpiresAt: row.retentionExpiresAt ?? null
89
94
  };
90
95
  }
91
96
  var PrismaMultipartRepository = class {
@@ -251,6 +256,27 @@ var PrismaFileRepository = class {
251
256
  });
252
257
  return toFileRecord(row);
253
258
  }
259
+ /**
260
+ * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法
261
+ * - 只更新传入的字段;其他字段保留原值
262
+ * - 文件不存在 → 抛错(由调用方决定是否捕获)
263
+ * - 业务字段 watermarked/isOriginal/parentId/retentionExpiresAt 需要 schema v1.4.0+
264
+ */
265
+ async update(id, patch) {
266
+ const data = {};
267
+ if (patch.storageKey !== void 0) data.storageKey = patch.storageKey;
268
+ if (patch.size !== void 0) data.size = BigInt(patch.size);
269
+ if (patch.contentType !== void 0) data.contentType = patch.contentType;
270
+ if (patch.meta !== void 0) data.meta = patch.meta;
271
+ if (patch.watermarked !== void 0) data.watermarked = patch.watermarked;
272
+ if (patch.isOriginal !== void 0) data.isOriginal = patch.isOriginal;
273
+ if (patch.parentId !== void 0) data.parentId = patch.parentId;
274
+ if (patch.retentionExpiresAt !== void 0) {
275
+ data.retentionExpiresAt = patch.retentionExpiresAt;
276
+ }
277
+ const row = await this.prisma.file.update({ where: { id }, data });
278
+ return toFileRecord(row);
279
+ }
254
280
  async softDelete(id, metaPatch) {
255
281
  if (metaPatch) {
256
282
  const cur = await this.prisma.file.findUnique({ where: { id } });
@@ -477,6 +503,30 @@ var InMemoryFileRepository = class {
477
503
  };
478
504
  this.store.set(id, next);
479
505
  }
506
+ /**
507
+ * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法
508
+ * - 只更新传入的字段;其他字段保留原值
509
+ * - 文件不存在 → 抛错(由调用方决定是否捕获)
510
+ */
511
+ async update(id, patch) {
512
+ const rec = this.store.get(id);
513
+ if (!rec) throw new Error("NOT_FOUND");
514
+ const now = /* @__PURE__ */ new Date();
515
+ const next = {
516
+ ...rec,
517
+ storageKey: patch.storageKey ?? rec.storageKey,
518
+ size: patch.size !== void 0 ? BigInt(patch.size) : rec.size,
519
+ contentType: patch.contentType ?? rec.contentType,
520
+ meta: patch.meta === void 0 ? rec.meta : patch.meta,
521
+ watermarked: patch.watermarked ?? rec.watermarked,
522
+ isOriginal: patch.isOriginal ?? rec.isOriginal,
523
+ parentId: patch.parentId === void 0 ? rec.parentId : patch.parentId,
524
+ retentionExpiresAt: patch.retentionExpiresAt === void 0 ? rec.retentionExpiresAt : patch.retentionExpiresAt,
525
+ updatedAt: now
526
+ };
527
+ this.store.set(id, next);
528
+ return next;
529
+ }
480
530
  async listOrphans(opts) {
481
531
  const sinceMs = opts.sinceDays * 24 * 3600 * 1e3;
482
532
  const list = [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/server/utils/repository/index.ts","../../../../src/server/utils/repository/types.ts","../../../../src/server/utils/repository/prisma.ts","../../../../src/server/utils/repository/memory.ts"],"sourcesContent":["/**\r\n * Repository 统一导出\r\n *\r\n * 用法 1:用 Prisma(SDK 内置实现)\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from '.../repository';\r\n * const multipart = new PrismaMultipartRepository(prisma);\r\n * const file = new PrismaFileRepository(prisma);\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'custom', multipart, file },\r\n * ...\r\n * });\r\n *\r\n * 用法 2:用内存实现(测试)\r\n * import { InMemoryMultipartRepository, InMemoryFileRepository } from '.../repository';\r\n *\r\n * 用法 3:完全自实现(Drizzle / TypeORM / Mongo / ...)\r\n * class MyMultipartRepo implements MultipartRepository { ... }\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'custom', multipart: new MyMultipartRepo(), file: new MyFileRepo() },\r\n * ...\r\n * });\r\n */\r\n\r\nexport {\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n} from './prisma';\r\nexport {\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n} from './memory';\r\nexport {\r\n MultipartStatus,\r\n FileStatus,\r\n type MultipartRecord,\r\n type FileRecord,\r\n type CreateInitInput,\r\n type CreateFileInput,\r\n type AddPartOutcome,\r\n type MultipartRepository,\r\n type FileRepository,\r\n} from './types';\r\nexport type { MultipartStatusT, FileStatusT } from './types-extra';\r\n","/**\r\n * 数据层仓库接口\r\n *\r\n * SDK 不强制绑定 ORM,只依赖这 2 个接口:\r\n * - MultipartRepository: 分片上传上下文(init/part/complete/abort/expire)\r\n * - FileRepository: 文件元信息(秒传查询、orphan 扫描)\r\n *\r\n * 接入方式:\r\n * - kind: 'prisma' → SDK 用内置 PrismaMultipartRepository + PrismaFileRepository\r\n * - kind: 'custom' → 用户注入自己的实现(Drizzle / TypeORM / 内存 / Mongo ...)\r\n */\r\n\r\nimport type { MultipartStatusT, FileStatusT } from './types-extra';\r\n\r\n// ============================================================\r\n// MultipartRecord / FileRecord: 业务层最小字段集\r\n// ============================================================\r\n\r\nexport interface MultipartRecord {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n totalSize: bigint;\r\n partSize: number;\r\n totalParts: number;\r\n isPublic: boolean;\r\n status: MultipartStatusT;\r\n receivedParts: number[];\r\n partHashes: Record<string, string>;\r\n meta: Record<string, unknown> | null;\r\n expectedHash: string | null;\r\n fileId: string | null;\r\n createdAt: Date;\r\n updatedAt: Date;\r\n expiresAt: Date;\r\n completedAt: Date | null;\r\n abortedAt: Date | null;\r\n}\r\n\r\nexport interface FileRecord {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n size: bigint;\r\n storageKey: string;\r\n isPublic: boolean;\r\n status: FileStatusT;\r\n uploadId: string | null;\r\n totalParts: number | null;\r\n contentHash: string | null;\r\n meta: Record<string, unknown> | null;\r\n createdAt: Date;\r\n updatedAt: Date;\r\n deletedAt: Date | null;\r\n}\r\n\r\n// ============================================================\r\n// 输入\r\n// ============================================================\r\n\r\nexport interface CreateInitInput {\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n totalSize: number;\r\n partSize: number;\r\n totalParts: number;\r\n isPublic: boolean;\r\n expectedHash?: string | null;\r\n meta?: Record<string, unknown> | null;\r\n ttlSeconds: number;\r\n}\r\n\r\nexport interface CreateFileInput {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n size: number;\r\n storageKey: string;\r\n isPublic: boolean;\r\n uploadId: string;\r\n totalParts: number;\r\n contentHash: string;\r\n meta?: Record<string, unknown> | null;\r\n}\r\n\r\nexport interface AddPartResult {\r\n updated: true;\r\n record: MultipartRecord;\r\n}\r\n\r\nexport interface AddPartDuplicate {\r\n updated: false;\r\n duplicate: true;\r\n existingHash: string | null;\r\n record: MultipartRecord;\r\n}\r\n\r\nexport type AddPartOutcome = AddPartResult | AddPartDuplicate;\r\n\r\n// ============================================================\r\n// 接口\r\n// ============================================================\r\n\r\nexport interface MultipartRepository {\r\n createInit(input: CreateInitInput): Promise<MultipartRecord>;\r\n findById(id: string): Promise<MultipartRecord | null>;\r\n addPart(id: string, partNumber: number, partHash: string | null): Promise<AddPartOutcome>;\r\n markCompleted(id: string, fileId: string): Promise<MultipartRecord>;\r\n markAborted(id: string): Promise<MultipartRecord>;\r\n markExpired(id: string): Promise<void>;\r\n listExpired(limit: number): Promise<MultipartRecord[]>;\r\n}\r\n\r\nexport interface FileRepository {\r\n findByHash(hash: string): Promise<FileRecord | null>;\r\n /**\r\n * v1.4.2: 严格秒传查询。\r\n * 必须同时满足: contentHash 相同、未删除、磁盘副本可见性与上传请求一致。\r\n * - isPublic=true → 命中 isPublic=true 的记录\r\n * - isPublic=false → 命中 isPublic=false 且 userId 与上传者一致的记录\r\n * 否则返回 null,调用方应走正常分片上传(新建独立 fileId)。\r\n */\r\n findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null>;\r\n findById(id: string): Promise<FileRecord | null>;\r\n create(input: CreateFileInput): Promise<FileRecord>;\r\n softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void>;\r\n listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]>;\r\n}\r\n\r\n// ============================================================\r\n// 状态枚举(用户实现 repo 时可复用)\r\n// ============================================================\r\n\r\nexport const MultipartStatus = {\r\n INIT: 'INIT',\r\n UPLOADING: 'UPLOADING',\r\n COMPLETED: 'COMPLETED',\r\n ABORTED: 'ABORTED',\r\n EXPIRED: 'EXPIRED',\r\n FAILED: 'FAILED',\r\n} as const;\r\n\r\nexport const FileStatus = {\r\n PENDING: 'PENDING',\r\n UPLOADING: 'UPLOADING',\r\n COMPLETED: 'COMPLETED',\r\n FAILED: 'FAILED',\r\n} as const;\r\n","/**\r\n * Prisma 实现: MultipartRepository + FileRepository\r\n *\r\n * 假定用户项目里有以下两张表(见 src/prisma/schema.prisma):\r\n * - MultipartUpload(id, userId, filename, contentType, totalSize, partSize,\r\n * totalParts, isPublic, status, receivedParts(JSON),\r\n * partHashes(JSON), meta(JSON), expectedHash, fileId,\r\n * createdAt, updatedAt, expiresAt, completedAt, abortedAt)\r\n * - File(id, userId, filename, contentType, size, storageKey, isPublic,\r\n * status, uploadId, totalParts, contentHash, meta, createdAt,\r\n * updatedAt, deletedAt)\r\n *\r\n * 用户调用:\r\n * import { PrismaClient } from '@prisma/client';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from '...';\r\n * const prisma = new PrismaClient();\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'prisma', prisma },\r\n * ...\r\n * });\r\n */\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype AnyClient = any;\r\n\r\nimport type {\r\n MultipartRepository,\r\n FileRepository,\r\n MultipartRecord,\r\n FileRecord,\r\n CreateInitInput,\r\n CreateFileInput,\r\n AddPartOutcome,\r\n} from './types';\r\nimport { MultipartStatus, FileStatus } from './types';\r\n\r\nfunction toMultipartRecord(row: any): MultipartRecord {\r\n return {\r\n id: row.id,\r\n userId: row.userId,\r\n filename: row.filename,\r\n contentType: row.contentType,\r\n totalSize: row.totalSize,\r\n partSize: row.partSize,\r\n totalParts: row.totalParts,\r\n isPublic: row.isPublic,\r\n status: row.status,\r\n receivedParts: Array.isArray(row.receivedParts) ? row.receivedParts : [],\r\n partHashes: row.partHashes && typeof row.partHashes === 'object' ? row.partHashes : {},\r\n meta: row.meta && typeof row.meta === 'object' ? row.meta : null,\r\n expectedHash: row.expectedHash,\r\n fileId: row.fileId,\r\n createdAt: row.createdAt,\r\n updatedAt: row.updatedAt,\r\n expiresAt: row.expiresAt,\r\n completedAt: row.completedAt,\r\n abortedAt: row.abortedAt,\r\n };\r\n}\r\n\r\nfunction toFileRecord(row: any): FileRecord {\r\n return {\r\n id: row.id,\r\n userId: row.userId,\r\n filename: row.filename,\r\n contentType: row.contentType,\r\n size: row.size,\r\n storageKey: row.storageKey,\r\n isPublic: row.isPublic,\r\n status: row.status,\r\n uploadId: row.uploadId,\r\n totalParts: row.totalParts,\r\n contentHash: row.contentHash,\r\n meta: row.meta && typeof row.meta === 'object' ? row.meta : null,\r\n createdAt: row.createdAt,\r\n updatedAt: row.updatedAt,\r\n deletedAt: row.deletedAt,\r\n };\r\n}\r\n\r\n// ============================================================\r\n// PrismaMultipartRepository\r\n// ============================================================\r\n\r\nexport class PrismaMultipartRepository implements MultipartRepository {\r\n constructor(private prisma: AnyClient) {}\r\n\r\n async createInit(input: CreateInitInput): Promise<MultipartRecord> {\r\n const expiresAt = new Date(Date.now() + input.ttlSeconds * 1000);\r\n const row = await this.prisma.multipartUpload.create({\r\n data: {\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n totalSize: BigInt(input.totalSize),\r\n partSize: input.partSize,\r\n totalParts: input.totalParts,\r\n isPublic: input.isPublic,\r\n status: MultipartStatus.INIT,\r\n receivedParts: [],\r\n partHashes: {},\r\n meta: input.meta ?? undefined,\r\n expectedHash: input.expectedHash ?? null,\r\n expiresAt,\r\n },\r\n });\r\n return toMultipartRecord(row);\r\n }\r\n\r\n async findById(id: string): Promise<MultipartRecord | null> {\r\n const row = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return row ? toMultipartRecord(row) : null;\r\n }\r\n\r\n async addPart(\r\n id: string,\r\n partNumber: number,\r\n partHash: string | null\r\n ): Promise<AddPartOutcome> {\r\n return await this.prisma.$transaction(async (tx: AnyClient) => {\r\n const row: any = await tx.multipartUpload.findUnique({ where: { id } });\r\n if (!row) throw new Error('NOT_FOUND');\r\n\r\n const received: number[] = Array.isArray(row.receivedParts) ? row.receivedParts : [];\r\n const hashes: Record<string, string> =\r\n row.partHashes && typeof row.partHashes === 'object' ? row.partHashes : {};\r\n\r\n if (received.includes(partNumber)) {\r\n return {\r\n updated: false as const,\r\n duplicate: true as const,\r\n existingHash: hashes[String(partNumber)] ?? null,\r\n record: toMultipartRecord(row),\r\n };\r\n }\r\n\r\n const next = [...received, partNumber].sort((a, b) => a - b);\r\n const nextHashes = { ...hashes };\r\n if (partHash) nextHashes[String(partNumber)] = partHash;\r\n\r\n const newStatus =\r\n row.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : row.status;\r\n\r\n if (\r\n row.status === MultipartStatus.COMPLETED ||\r\n row.status === MultipartStatus.ABORTED ||\r\n row.status === MultipartStatus.EXPIRED ||\r\n row.status === MultipartStatus.FAILED\r\n ) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n\r\n const updated = await tx.multipartUpload.update({\r\n where: { id },\r\n data: {\r\n receivedParts: next,\r\n partHashes: nextHashes,\r\n status: newStatus,\r\n },\r\n });\r\n return { updated: true as const, record: toMultipartRecord(updated) };\r\n });\r\n }\r\n\r\n async markCompleted(id: string, fileId: string): Promise<MultipartRecord> {\r\n const row = await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.COMPLETED,\r\n fileId,\r\n completedAt: new Date(),\r\n },\r\n });\r\n if (row.count === 0) {\r\n const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n if (!cur) throw new Error('NOT_FOUND');\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return toMultipartRecord(fresh);\r\n }\r\n\r\n async markAborted(id: string): Promise<MultipartRecord> {\r\n const row = await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.ABORTED,\r\n abortedAt: new Date(),\r\n },\r\n });\r\n if (row.count === 0) {\r\n const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n if (!cur) throw new Error('NOT_FOUND');\r\n if (cur.status === MultipartStatus.COMPLETED) throw new Error('ALREADY_COMPLETED');\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return toMultipartRecord(fresh);\r\n }\r\n\r\n async markExpired(id: string): Promise<void> {\r\n await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.EXPIRED,\r\n abortedAt: new Date(),\r\n },\r\n });\r\n }\r\n\r\n async listExpired(limit: number): Promise<MultipartRecord[]> {\r\n const rows = await this.prisma.multipartUpload.findMany({\r\n where: {\r\n status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] },\r\n expiresAt: { lt: new Date() },\r\n },\r\n take: limit,\r\n orderBy: { expiresAt: 'asc' },\r\n });\r\n return rows.map(toMultipartRecord);\r\n }\r\n}\r\n\r\n// ============================================================\r\n// PrismaFileRepository\r\n// ============================================================\r\n\r\nexport class PrismaFileRepository implements FileRepository {\r\n constructor(private prisma: AnyClient) {}\r\n\r\n async findByHash(hash: string): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findFirst({\r\n where: { contentHash: hash, deletedAt: null },\r\n });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findFirst({\r\n where: {\r\n contentHash: hash,\r\n deletedAt: null,\r\n isPublic,\r\n ...(isPublic ? {} : { userId }),\r\n },\r\n orderBy: { createdAt: 'asc' },\r\n });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async findById(id: string): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findUnique({ where: { id } });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async create(input: CreateFileInput): Promise<FileRecord> {\r\n const row = await this.prisma.file.create({\r\n data: {\r\n id: input.id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n size: BigInt(input.size),\r\n storageKey: input.storageKey,\r\n isPublic: input.isPublic,\r\n status: FileStatus.COMPLETED,\r\n uploadId: input.uploadId,\r\n totalParts: input.totalParts,\r\n contentHash: input.contentHash,\r\n meta: input.meta ?? undefined,\r\n },\r\n });\r\n return toFileRecord(row);\r\n }\r\n\r\n async softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void> {\r\n if (metaPatch) {\r\n const cur = await this.prisma.file.findUnique({ where: { id } });\r\n const next = { ...((cur?.meta as any) ?? {}), ...metaPatch };\r\n await this.prisma.file.update({\r\n where: { id },\r\n data: { deletedAt: new Date(), meta: next },\r\n });\r\n } else {\r\n await this.prisma.file.update({\r\n where: { id },\r\n data: { deletedAt: new Date() },\r\n });\r\n }\r\n }\r\n\r\n async listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]> {\r\n const lookbackMs = opts.sinceDays * 24 * 3600 * 1000;\r\n const rows = await this.prisma.file.findMany({\r\n where: {\r\n deletedAt: null,\r\n storageKey: { not: '' },\r\n createdAt: { gt: new Date(Date.now() - lookbackMs) },\r\n },\r\n select: {\r\n id: true,\r\n userId: true,\r\n filename: true,\r\n contentType: true,\r\n size: true,\r\n storageKey: true,\r\n isPublic: true,\r\n status: true,\r\n uploadId: true,\r\n totalParts: true,\r\n contentHash: true,\r\n meta: true,\r\n createdAt: true,\r\n updatedAt: true,\r\n deletedAt: true,\r\n },\r\n take: opts.limit,\r\n orderBy: { createdAt: 'desc' },\r\n });\r\n return rows.map(toFileRecord);\r\n }\r\n}\r\n","/**\r\n * 内存仓库(测试 / POC 场景)\r\n *\r\n * 用 Map 模拟两张表,无并发安全保证,仅用于单进程测试。\r\n */\r\n\r\nimport type {\r\n MultipartRepository,\r\n FileRepository,\r\n MultipartRecord,\r\n FileRecord,\r\n CreateInitInput,\r\n CreateFileInput,\r\n AddPartOutcome,\r\n} from './types';\r\nimport { MultipartStatus, FileStatus } from './types';\r\n\r\n// ============================================================\r\n// InMemoryMultipartRepository\r\n// ============================================================\r\n\r\nexport class InMemoryMultipartRepository implements MultipartRepository {\r\n private store = new Map<string, MultipartRecord>();\r\n private idCounter = 0;\r\n\r\n private nextId(): string {\r\n return `mp_${Date.now()}_${++this.idCounter}`;\r\n }\r\n\r\n async createInit(input: CreateInitInput): Promise<MultipartRecord> {\r\n const id = this.nextId();\r\n const now = new Date();\r\n const rec: MultipartRecord = {\r\n id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n totalSize: BigInt(input.totalSize),\r\n partSize: input.partSize,\r\n totalParts: input.totalParts,\r\n isPublic: input.isPublic,\r\n status: MultipartStatus.INIT,\r\n receivedParts: [],\r\n partHashes: {},\r\n meta: input.meta ?? null,\r\n expectedHash: input.expectedHash ?? null,\r\n fileId: null,\r\n createdAt: now,\r\n updatedAt: now,\r\n expiresAt: new Date(now.getTime() + input.ttlSeconds * 1000),\r\n completedAt: null,\r\n abortedAt: null,\r\n };\r\n this.store.set(id, rec);\r\n return rec;\r\n }\r\n\r\n async findById(id: string): Promise<MultipartRecord | null> {\r\n return this.store.get(id) ?? null;\r\n }\r\n\r\n async addPart(\r\n id: string,\r\n partNumber: number,\r\n partHash: string | null\r\n ): Promise<AddPartOutcome> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n\r\n if (\r\n rec.status === MultipartStatus.COMPLETED ||\r\n rec.status === MultipartStatus.ABORTED ||\r\n rec.status === MultipartStatus.EXPIRED ||\r\n rec.status === MultipartStatus.FAILED\r\n ) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n\r\n if (rec.receivedParts.includes(partNumber)) {\r\n return {\r\n updated: false,\r\n duplicate: true,\r\n existingHash: rec.partHashes[String(partNumber)] ?? null,\r\n record: rec,\r\n };\r\n }\r\n\r\n const next = [...rec.receivedParts, partNumber].sort((a, b) => a - b);\r\n const nextHashes = { ...rec.partHashes };\r\n if (partHash) nextHashes[String(partNumber)] = partHash;\r\n\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n receivedParts: next,\r\n partHashes: nextHashes,\r\n status: rec.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : rec.status,\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return { updated: true, record: updated };\r\n }\r\n\r\n async markCompleted(id: string, fileId: string): Promise<MultipartRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n status: MultipartStatus.COMPLETED,\r\n fileId,\r\n completedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return updated;\r\n }\r\n\r\n async markAborted(id: string): Promise<MultipartRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n if (rec.status === MultipartStatus.COMPLETED) throw new Error('ALREADY_COMPLETED');\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n status: MultipartStatus.ABORTED,\r\n abortedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return updated;\r\n }\r\n\r\n async markExpired(id: string): Promise<void> {\r\n const rec = this.store.get(id);\r\n if (!rec) return;\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) return;\r\n this.store.set(id, {\r\n ...rec,\r\n status: MultipartStatus.EXPIRED,\r\n abortedAt: new Date(),\r\n updatedAt: new Date(),\r\n });\r\n }\r\n\r\n async listExpired(limit: number): Promise<MultipartRecord[]> {\r\n const now = Date.now();\r\n const list: MultipartRecord[] = [];\r\n for (const rec of this.store.values()) {\r\n if (\r\n (rec.status === MultipartStatus.INIT || rec.status === MultipartStatus.UPLOADING) &&\r\n rec.expiresAt.getTime() < now\r\n ) {\r\n list.push(rec);\r\n }\r\n }\r\n list.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());\r\n return list.slice(0, limit);\r\n }\r\n}\r\n\r\n// ============================================================\r\n// InMemoryFileRepository\r\n// ============================================================\r\n\r\nexport class InMemoryFileRepository implements FileRepository {\r\n private store = new Map<string, FileRecord>();\r\n private idCounter = 0;\r\n\r\n private nextId(): string {\r\n return `f_${Date.now()}_${++this.idCounter}`;\r\n }\r\n\r\n async findByHash(hash: string): Promise<FileRecord | null> {\r\n for (const f of this.store.values()) {\r\n if (f.contentHash === hash && !f.deletedAt) return f;\r\n }\r\n return null;\r\n }\r\n\r\n async findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null> {\r\n for (const f of this.store.values()) {\r\n if (f.deletedAt) continue;\r\n if (f.contentHash !== hash) continue;\r\n if (f.isPublic !== isPublic) continue;\r\n if (!isPublic && f.userId !== userId) continue;\r\n return f;\r\n }\r\n return null;\r\n }\r\n\r\n async findById(id: string): Promise<FileRecord | null> {\r\n return this.store.get(id) ?? null;\r\n }\r\n\r\n async create(input: CreateFileInput): Promise<FileRecord> {\r\n let id = input.id;\r\n if (!id) id = this.nextId();\r\n const now = new Date();\r\n const rec: FileRecord = {\r\n id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n size: BigInt(input.size),\r\n storageKey: input.storageKey,\r\n isPublic: input.isPublic,\r\n status: FileStatus.COMPLETED,\r\n uploadId: input.uploadId,\r\n totalParts: input.totalParts,\r\n contentHash: input.contentHash,\r\n meta: input.meta ?? null,\r\n createdAt: now,\r\n updatedAt: now,\r\n deletedAt: null,\r\n };\r\n this.store.set(id, rec);\r\n return rec;\r\n }\r\n\r\n async softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void> {\r\n const rec = this.store.get(id);\r\n if (!rec) return;\r\n const next: FileRecord = {\r\n ...rec,\r\n meta: metaPatch ? { ...(rec.meta ?? {}), ...metaPatch } : rec.meta,\r\n deletedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, next);\r\n }\r\n\r\n async listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]> {\r\n const sinceMs = opts.sinceDays * 24 * 3600 * 1000;\r\n const list: FileRecord[] = [];\r\n for (const f of this.store.values()) {\r\n if (f.deletedAt) continue;\r\n if (f.createdAt.getTime() < Date.now() - sinceMs) continue;\r\n list.push(f);\r\n }\r\n list.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\r\n return list.slice(0, opts.limit);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/server/utils/repository/index.ts","../../../../src/server/utils/repository/types.ts","../../../../src/server/utils/repository/prisma.ts","../../../../src/server/utils/repository/memory.ts"],"sourcesContent":["/**\r\n * Repository 统一导出\r\n *\r\n * 用法 1:用 Prisma(SDK 内置实现)\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from '.../repository';\r\n * const multipart = new PrismaMultipartRepository(prisma);\r\n * const file = new PrismaFileRepository(prisma);\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'custom', multipart, file },\r\n * ...\r\n * });\r\n *\r\n * 用法 2:用内存实现(测试)\r\n * import { InMemoryMultipartRepository, InMemoryFileRepository } from '.../repository';\r\n *\r\n * 用法 3:完全自实现(Drizzle / TypeORM / Mongo / ...)\r\n * class MyMultipartRepo implements MultipartRepository { ... }\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'custom', multipart: new MyMultipartRepo(), file: new MyFileRepo() },\r\n * ...\r\n * });\r\n */\r\n\r\nexport {\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n} from './prisma';\r\nexport {\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n} from './memory';\r\nexport {\r\n MultipartStatus,\r\n FileStatus,\r\n type MultipartRecord,\r\n type FileRecord,\r\n type CreateInitInput,\r\n type CreateFileInput,\r\n type AddPartOutcome,\r\n type MultipartRepository,\r\n type FileRepository,\r\n} from './types';\r\nexport type { MultipartStatusT, FileStatusT } from './types-extra';\r\n","/**\r\n * 数据层仓库接口\r\n *\r\n * SDK 不强制绑定 ORM,只依赖这 2 个接口:\r\n * - MultipartRepository: 分片上传上下文(init/part/complete/abort/expire)\r\n * - FileRepository: 文件元信息(秒传查询、orphan 扫描)\r\n *\r\n * 接入方式:\r\n * - kind: 'prisma' → SDK 用内置 PrismaMultipartRepository + PrismaFileRepository\r\n * - kind: 'custom' → 用户注入自己的实现(Drizzle / TypeORM / 内存 / Mongo ...)\r\n */\r\n\r\nimport type { MultipartStatusT, FileStatusT } from './types-extra';\r\n\r\n// ============================================================\r\n// MultipartRecord / FileRecord: 业务层最小字段集\r\n// ============================================================\r\n\r\nexport interface MultipartRecord {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n totalSize: bigint;\r\n partSize: number;\r\n totalParts: number;\r\n isPublic: boolean;\r\n status: MultipartStatusT;\r\n receivedParts: number[];\r\n partHashes: Record<string, string>;\r\n meta: Record<string, unknown> | null;\r\n expectedHash: string | null;\r\n fileId: string | null;\r\n createdAt: Date;\r\n updatedAt: Date;\r\n expiresAt: Date;\r\n completedAt: Date | null;\r\n abortedAt: Date | null;\r\n}\r\n\r\n/**\r\n * v1.5.1 业务可选字段(钩子可填)\r\n * - watermarked / isOriginal / parentId / retentionExpiresAt\r\n * - 都是 optional,SDK 内部不强制存在(早期 SDK / 自实现 repo 可以不实现)\r\n */\r\nexport interface FileRecordExt {\r\n /** 是否已嵌入数字水印 */\r\n watermarked?: boolean;\r\n /** 是否是原图(派生版本时为 false) */\r\n isOriginal?: boolean;\r\n /** 父文件 id(派生关系) */\r\n parentId?: string | null;\r\n /** 过期时间(留痕 / 清理用) */\r\n retentionExpiresAt?: Date | null;\r\n}\r\n\r\n/**\r\n * 完整 file record(基础字段 + 业务可选字段)\r\n */\r\nexport type FileRecord = {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n size: bigint;\r\n storageKey: string;\r\n isPublic: boolean;\r\n status: FileStatusT;\r\n uploadId: string | null;\r\n totalParts: number | null;\r\n contentHash: string | null;\r\n meta: Record<string, unknown> | null;\r\n createdAt: Date;\r\n updatedAt: Date;\r\n deletedAt: Date | null;\r\n} & FileRecordExt;\r\n\r\n// ============================================================\r\n// 输入\r\n// ============================================================\r\n\r\nexport interface CreateInitInput {\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n totalSize: number;\r\n partSize: number;\r\n totalParts: number;\r\n isPublic: boolean;\r\n expectedHash?: string | null;\r\n meta?: Record<string, unknown> | null;\r\n ttlSeconds: number;\r\n}\r\n\r\nexport interface CreateFileInput {\r\n id: string;\r\n userId: number | null;\r\n filename: string;\r\n contentType: string;\r\n size: number;\r\n storageKey: string;\r\n isPublic: boolean;\r\n uploadId: string;\r\n totalParts: number;\r\n contentHash: string;\r\n meta?: Record<string, unknown> | null;\r\n}\r\n\r\n/**\r\n * 文件记录 update patch(v1.5.1+)\r\n * - 钩子 onComplete 返回 SDK 后,SDK 调 fileRepo.update(fileId, patch)\r\n * - 字段都是 optional,只更新传入的字段\r\n * - 字段语义同 FileRecordExt\r\n */\r\nexport type UpdateFileInput = FileRecordExt & {\r\n storageKey?: string;\r\n size?: number;\r\n contentType?: string;\r\n meta?: Record<string, unknown> | null;\r\n};\r\n\r\nexport interface AddPartResult {\r\n updated: true;\r\n record: MultipartRecord;\r\n}\r\n\r\nexport interface AddPartDuplicate {\r\n updated: false;\r\n duplicate: true;\r\n existingHash: string | null;\r\n record: MultipartRecord;\r\n}\r\n\r\nexport type AddPartOutcome = AddPartResult | AddPartDuplicate;\r\n\r\n// ============================================================\r\n// 接口\r\n// ============================================================\r\n\r\nexport interface MultipartRepository {\r\n createInit(input: CreateInitInput): Promise<MultipartRecord>;\r\n findById(id: string): Promise<MultipartRecord | null>;\r\n addPart(id: string, partNumber: number, partHash: string | null): Promise<AddPartOutcome>;\r\n markCompleted(id: string, fileId: string): Promise<MultipartRecord>;\r\n markAborted(id: string): Promise<MultipartRecord>;\r\n markExpired(id: string): Promise<void>;\r\n listExpired(limit: number): Promise<MultipartRecord[]>;\r\n}\r\n\r\nexport interface FileRepository {\r\n findByHash(hash: string): Promise<FileRecord | null>;\r\n /**\r\n * v1.4.2: 严格秒传查询。\r\n * 必须同时满足: contentHash 相同、未删除、磁盘副本可见性与上传请求一致。\r\n * - isPublic=true → 命中 isPublic=true 的记录\r\n * - isPublic=false → 命中 isPublic=false 且 userId 与上传者一致的记录\r\n * 否则返回 null,调用方应走正常分片上传(新建独立 fileId)。\r\n */\r\n findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null>;\r\n findById(id: string): Promise<FileRecord | null>;\r\n create(input: CreateFileInput): Promise<FileRecord>;\r\n /**\r\n * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法\r\n * - 只更新传入的字段(其他字段保留原值)\r\n * - 文件不存在 / 已删除 → 抛错(钩子会捕获并 log.warn,不影响主流程)\r\n */\r\n update(id: string, patch: UpdateFileInput): Promise<FileRecord>;\r\n softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void>;\r\n listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]>;\r\n}\r\n\r\n// ============================================================\r\n// 状态枚举(用户实现 repo 时可复用)\r\n// ============================================================\r\n\r\nexport const MultipartStatus = {\r\n INIT: 'INIT',\r\n UPLOADING: 'UPLOADING',\r\n COMPLETED: 'COMPLETED',\r\n ABORTED: 'ABORTED',\r\n EXPIRED: 'EXPIRED',\r\n FAILED: 'FAILED',\r\n} as const;\r\n\r\nexport const FileStatus = {\r\n PENDING: 'PENDING',\r\n UPLOADING: 'UPLOADING',\r\n COMPLETED: 'COMPLETED',\r\n FAILED: 'FAILED',\r\n} as const;\r\n","/**\r\n * Prisma 实现: MultipartRepository + FileRepository\r\n *\r\n * 假定用户项目里有以下两张表(见 src/prisma/schema.prisma):\r\n * - MultipartUpload(id, userId, filename, contentType, totalSize, partSize,\r\n * totalParts, isPublic, status, receivedParts(JSON),\r\n * partHashes(JSON), meta(JSON), expectedHash, fileId,\r\n * createdAt, updatedAt, expiresAt, completedAt, abortedAt)\r\n * - File(id, userId, filename, contentType, size, storageKey, isPublic,\r\n * status, uploadId, totalParts, contentHash, meta, createdAt,\r\n * updatedAt, deletedAt)\r\n *\r\n * 用户调用:\r\n * import { PrismaClient } from '@prisma/client';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from '...';\r\n * const prisma = new PrismaClient();\r\n * await app.register(secureUploadPlugin, {\r\n * repository: { kind: 'prisma', prisma },\r\n * ...\r\n * });\r\n */\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype AnyClient = any;\r\n\r\nimport type {\r\n MultipartRepository,\r\n FileRepository,\r\n MultipartRecord,\r\n FileRecord,\r\n CreateInitInput,\r\n CreateFileInput,\r\n AddPartOutcome,\r\n UpdateFileInput,\r\n} from './types';\r\nimport { MultipartStatus, FileStatus } from './types';\r\n\r\nfunction toMultipartRecord(row: any): MultipartRecord {\r\n return {\r\n id: row.id,\r\n userId: row.userId,\r\n filename: row.filename,\r\n contentType: row.contentType,\r\n totalSize: row.totalSize,\r\n partSize: row.partSize,\r\n totalParts: row.totalParts,\r\n isPublic: row.isPublic,\r\n status: row.status,\r\n receivedParts: Array.isArray(row.receivedParts) ? row.receivedParts : [],\r\n partHashes: row.partHashes && typeof row.partHashes === 'object' ? row.partHashes : {},\r\n meta: row.meta && typeof row.meta === 'object' ? row.meta : null,\r\n expectedHash: row.expectedHash,\r\n fileId: row.fileId,\r\n createdAt: row.createdAt,\r\n updatedAt: row.updatedAt,\r\n expiresAt: row.expiresAt,\r\n completedAt: row.completedAt,\r\n abortedAt: row.abortedAt,\r\n };\r\n}\r\n\r\nfunction toFileRecord(row: any): FileRecord {\r\n return {\r\n id: row.id,\r\n userId: row.userId,\r\n filename: row.filename,\r\n contentType: row.contentType,\r\n size: row.size,\r\n storageKey: row.storageKey,\r\n isPublic: row.isPublic,\r\n status: row.status,\r\n uploadId: row.uploadId,\r\n totalParts: row.totalParts,\r\n contentHash: row.contentHash,\r\n meta: row.meta && typeof row.meta === 'object' ? row.meta : null,\r\n createdAt: row.createdAt,\r\n updatedAt: row.updatedAt,\r\n deletedAt: row.deletedAt,\r\n // v1.5.1+:水印溯源字段(可选;旧 schema/老库里 row 可能没有,需要兜底)\r\n watermarked: row.watermarked ?? false,\r\n isOriginal: row.isOriginal ?? true,\r\n parentId: row.parentId ?? null,\r\n retentionExpiresAt: row.retentionExpiresAt ?? null,\r\n };\r\n}\r\n\r\n// ============================================================\r\n// PrismaMultipartRepository\r\n// ============================================================\r\n\r\nexport class PrismaMultipartRepository implements MultipartRepository {\r\n constructor(private prisma: AnyClient) {}\r\n\r\n async createInit(input: CreateInitInput): Promise<MultipartRecord> {\r\n const expiresAt = new Date(Date.now() + input.ttlSeconds * 1000);\r\n const row = await this.prisma.multipartUpload.create({\r\n data: {\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n totalSize: BigInt(input.totalSize),\r\n partSize: input.partSize,\r\n totalParts: input.totalParts,\r\n isPublic: input.isPublic,\r\n status: MultipartStatus.INIT,\r\n receivedParts: [],\r\n partHashes: {},\r\n meta: input.meta ?? undefined,\r\n expectedHash: input.expectedHash ?? null,\r\n expiresAt,\r\n },\r\n });\r\n return toMultipartRecord(row);\r\n }\r\n\r\n async findById(id: string): Promise<MultipartRecord | null> {\r\n const row = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return row ? toMultipartRecord(row) : null;\r\n }\r\n\r\n async addPart(\r\n id: string,\r\n partNumber: number,\r\n partHash: string | null\r\n ): Promise<AddPartOutcome> {\r\n return await this.prisma.$transaction(async (tx: AnyClient) => {\r\n const row: any = await tx.multipartUpload.findUnique({ where: { id } });\r\n if (!row) throw new Error('NOT_FOUND');\r\n\r\n const received: number[] = Array.isArray(row.receivedParts) ? row.receivedParts : [];\r\n const hashes: Record<string, string> =\r\n row.partHashes && typeof row.partHashes === 'object' ? row.partHashes : {};\r\n\r\n if (received.includes(partNumber)) {\r\n return {\r\n updated: false as const,\r\n duplicate: true as const,\r\n existingHash: hashes[String(partNumber)] ?? null,\r\n record: toMultipartRecord(row),\r\n };\r\n }\r\n\r\n const next = [...received, partNumber].sort((a, b) => a - b);\r\n const nextHashes = { ...hashes };\r\n if (partHash) nextHashes[String(partNumber)] = partHash;\r\n\r\n const newStatus =\r\n row.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : row.status;\r\n\r\n if (\r\n row.status === MultipartStatus.COMPLETED ||\r\n row.status === MultipartStatus.ABORTED ||\r\n row.status === MultipartStatus.EXPIRED ||\r\n row.status === MultipartStatus.FAILED\r\n ) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n\r\n const updated = await tx.multipartUpload.update({\r\n where: { id },\r\n data: {\r\n receivedParts: next,\r\n partHashes: nextHashes,\r\n status: newStatus,\r\n },\r\n });\r\n return { updated: true as const, record: toMultipartRecord(updated) };\r\n });\r\n }\r\n\r\n async markCompleted(id: string, fileId: string): Promise<MultipartRecord> {\r\n const row = await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.COMPLETED,\r\n fileId,\r\n completedAt: new Date(),\r\n },\r\n });\r\n if (row.count === 0) {\r\n const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n if (!cur) throw new Error('NOT_FOUND');\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return toMultipartRecord(fresh);\r\n }\r\n\r\n async markAborted(id: string): Promise<MultipartRecord> {\r\n const row = await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.ABORTED,\r\n abortedAt: new Date(),\r\n },\r\n });\r\n if (row.count === 0) {\r\n const cur = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n if (!cur) throw new Error('NOT_FOUND');\r\n if (cur.status === MultipartStatus.COMPLETED) throw new Error('ALREADY_COMPLETED');\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const fresh = await this.prisma.multipartUpload.findUnique({ where: { id } });\r\n return toMultipartRecord(fresh);\r\n }\r\n\r\n async markExpired(id: string): Promise<void> {\r\n await this.prisma.multipartUpload.updateMany({\r\n where: { id, status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] } },\r\n data: {\r\n status: MultipartStatus.EXPIRED,\r\n abortedAt: new Date(),\r\n },\r\n });\r\n }\r\n\r\n async listExpired(limit: number): Promise<MultipartRecord[]> {\r\n const rows = await this.prisma.multipartUpload.findMany({\r\n where: {\r\n status: { in: [MultipartStatus.INIT, MultipartStatus.UPLOADING] },\r\n expiresAt: { lt: new Date() },\r\n },\r\n take: limit,\r\n orderBy: { expiresAt: 'asc' },\r\n });\r\n return rows.map(toMultipartRecord);\r\n }\r\n}\r\n\r\n// ============================================================\r\n// PrismaFileRepository\r\n// ============================================================\r\n\r\nexport class PrismaFileRepository implements FileRepository {\r\n constructor(private prisma: AnyClient) {}\r\n\r\n async findByHash(hash: string): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findFirst({\r\n where: { contentHash: hash, deletedAt: null },\r\n });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findFirst({\r\n where: {\r\n contentHash: hash,\r\n deletedAt: null,\r\n isPublic,\r\n ...(isPublic ? {} : { userId }),\r\n },\r\n orderBy: { createdAt: 'asc' },\r\n });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async findById(id: string): Promise<FileRecord | null> {\r\n const row = await this.prisma.file.findUnique({ where: { id } });\r\n return row ? toFileRecord(row) : null;\r\n }\r\n\r\n async create(input: CreateFileInput): Promise<FileRecord> {\r\n const row = await this.prisma.file.create({\r\n data: {\r\n id: input.id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n size: BigInt(input.size),\r\n storageKey: input.storageKey,\r\n isPublic: input.isPublic,\r\n status: FileStatus.COMPLETED,\r\n uploadId: input.uploadId,\r\n totalParts: input.totalParts,\r\n contentHash: input.contentHash,\r\n meta: input.meta ?? undefined,\r\n },\r\n });\r\n return toFileRecord(row);\r\n }\r\n\r\n /**\r\n * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法\r\n * - 只更新传入的字段;其他字段保留原值\r\n * - 文件不存在 → 抛错(由调用方决定是否捕获)\r\n * - 业务字段 watermarked/isOriginal/parentId/retentionExpiresAt 需要 schema v1.4.0+\r\n */\r\n async update(id: string, patch: UpdateFileInput): Promise<FileRecord> {\r\n const data: Record<string, unknown> = {};\r\n if (patch.storageKey !== undefined) data.storageKey = patch.storageKey;\r\n if (patch.size !== undefined) data.size = BigInt(patch.size);\r\n if (patch.contentType !== undefined) data.contentType = patch.contentType;\r\n if (patch.meta !== undefined) data.meta = patch.meta;\r\n if (patch.watermarked !== undefined) data.watermarked = patch.watermarked;\r\n if (patch.isOriginal !== undefined) data.isOriginal = patch.isOriginal;\r\n if (patch.parentId !== undefined) data.parentId = patch.parentId;\r\n if (patch.retentionExpiresAt !== undefined) {\r\n data.retentionExpiresAt = patch.retentionExpiresAt;\r\n }\r\n const row = await this.prisma.file.update({ where: { id }, data });\r\n return toFileRecord(row);\r\n }\r\n\r\n async softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void> {\r\n if (metaPatch) {\r\n const cur = await this.prisma.file.findUnique({ where: { id } });\r\n const next = { ...((cur?.meta as any) ?? {}), ...metaPatch };\r\n await this.prisma.file.update({\r\n where: { id },\r\n data: { deletedAt: new Date(), meta: next },\r\n });\r\n } else {\r\n await this.prisma.file.update({\r\n where: { id },\r\n data: { deletedAt: new Date() },\r\n });\r\n }\r\n }\r\n\r\n async listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]> {\r\n const lookbackMs = opts.sinceDays * 24 * 3600 * 1000;\r\n const rows = await this.prisma.file.findMany({\r\n where: {\r\n deletedAt: null,\r\n storageKey: { not: '' },\r\n createdAt: { gt: new Date(Date.now() - lookbackMs) },\r\n },\r\n select: {\r\n id: true,\r\n userId: true,\r\n filename: true,\r\n contentType: true,\r\n size: true,\r\n storageKey: true,\r\n isPublic: true,\r\n status: true,\r\n uploadId: true,\r\n totalParts: true,\r\n contentHash: true,\r\n meta: true,\r\n createdAt: true,\r\n updatedAt: true,\r\n deletedAt: true,\r\n },\r\n take: opts.limit,\r\n orderBy: { createdAt: 'desc' },\r\n });\r\n return rows.map(toFileRecord);\r\n }\r\n}\r\n","/**\r\n * 内存仓库(测试 / POC 场景)\r\n *\r\n * 用 Map 模拟两张表,无并发安全保证,仅用于单进程测试。\r\n */\r\n\r\nimport type {\r\n MultipartRepository,\r\n FileRepository,\r\n MultipartRecord,\r\n FileRecord,\r\n CreateInitInput,\r\n CreateFileInput,\r\n AddPartOutcome,\r\n UpdateFileInput,\r\n} from './types';\r\nimport { MultipartStatus, FileStatus } from './types';\r\n\r\n// ============================================================\r\n// InMemoryMultipartRepository\r\n// ============================================================\r\n\r\nexport class InMemoryMultipartRepository implements MultipartRepository {\r\n private store = new Map<string, MultipartRecord>();\r\n private idCounter = 0;\r\n\r\n private nextId(): string {\r\n return `mp_${Date.now()}_${++this.idCounter}`;\r\n }\r\n\r\n async createInit(input: CreateInitInput): Promise<MultipartRecord> {\r\n const id = this.nextId();\r\n const now = new Date();\r\n const rec: MultipartRecord = {\r\n id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n totalSize: BigInt(input.totalSize),\r\n partSize: input.partSize,\r\n totalParts: input.totalParts,\r\n isPublic: input.isPublic,\r\n status: MultipartStatus.INIT,\r\n receivedParts: [],\r\n partHashes: {},\r\n meta: input.meta ?? null,\r\n expectedHash: input.expectedHash ?? null,\r\n fileId: null,\r\n createdAt: now,\r\n updatedAt: now,\r\n expiresAt: new Date(now.getTime() + input.ttlSeconds * 1000),\r\n completedAt: null,\r\n abortedAt: null,\r\n };\r\n this.store.set(id, rec);\r\n return rec;\r\n }\r\n\r\n async findById(id: string): Promise<MultipartRecord | null> {\r\n return this.store.get(id) ?? null;\r\n }\r\n\r\n async addPart(\r\n id: string,\r\n partNumber: number,\r\n partHash: string | null\r\n ): Promise<AddPartOutcome> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n\r\n if (\r\n rec.status === MultipartStatus.COMPLETED ||\r\n rec.status === MultipartStatus.ABORTED ||\r\n rec.status === MultipartStatus.EXPIRED ||\r\n rec.status === MultipartStatus.FAILED\r\n ) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n\r\n if (rec.receivedParts.includes(partNumber)) {\r\n return {\r\n updated: false,\r\n duplicate: true,\r\n existingHash: rec.partHashes[String(partNumber)] ?? null,\r\n record: rec,\r\n };\r\n }\r\n\r\n const next = [...rec.receivedParts, partNumber].sort((a, b) => a - b);\r\n const nextHashes = { ...rec.partHashes };\r\n if (partHash) nextHashes[String(partNumber)] = partHash;\r\n\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n receivedParts: next,\r\n partHashes: nextHashes,\r\n status: rec.status === MultipartStatus.INIT ? MultipartStatus.UPLOADING : rec.status,\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return { updated: true, record: updated };\r\n }\r\n\r\n async markCompleted(id: string, fileId: string): Promise<MultipartRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n status: MultipartStatus.COMPLETED,\r\n fileId,\r\n completedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return updated;\r\n }\r\n\r\n async markAborted(id: string): Promise<MultipartRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n if (rec.status === MultipartStatus.COMPLETED) throw new Error('ALREADY_COMPLETED');\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) {\r\n throw new Error('STATUS_INVALID');\r\n }\r\n const updated: MultipartRecord = {\r\n ...rec,\r\n status: MultipartStatus.ABORTED,\r\n abortedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, updated);\r\n return updated;\r\n }\r\n\r\n async markExpired(id: string): Promise<void> {\r\n const rec = this.store.get(id);\r\n if (!rec) return;\r\n if (rec.status !== MultipartStatus.INIT && rec.status !== MultipartStatus.UPLOADING) return;\r\n this.store.set(id, {\r\n ...rec,\r\n status: MultipartStatus.EXPIRED,\r\n abortedAt: new Date(),\r\n updatedAt: new Date(),\r\n });\r\n }\r\n\r\n async listExpired(limit: number): Promise<MultipartRecord[]> {\r\n const now = Date.now();\r\n const list: MultipartRecord[] = [];\r\n for (const rec of this.store.values()) {\r\n if (\r\n (rec.status === MultipartStatus.INIT || rec.status === MultipartStatus.UPLOADING) &&\r\n rec.expiresAt.getTime() < now\r\n ) {\r\n list.push(rec);\r\n }\r\n }\r\n list.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());\r\n return list.slice(0, limit);\r\n }\r\n}\r\n\r\n// ============================================================\r\n// InMemoryFileRepository\r\n// ============================================================\r\n\r\nexport class InMemoryFileRepository implements FileRepository {\r\n private store = new Map<string, FileRecord>();\r\n private idCounter = 0;\r\n\r\n private nextId(): string {\r\n return `f_${Date.now()}_${++this.idCounter}`;\r\n }\r\n\r\n async findByHash(hash: string): Promise<FileRecord | null> {\r\n for (const f of this.store.values()) {\r\n if (f.contentHash === hash && !f.deletedAt) return f;\r\n }\r\n return null;\r\n }\r\n\r\n async findDedupCandidate(\r\n hash: string,\r\n isPublic: boolean,\r\n userId: number | null\r\n ): Promise<FileRecord | null> {\r\n for (const f of this.store.values()) {\r\n if (f.deletedAt) continue;\r\n if (f.contentHash !== hash) continue;\r\n if (f.isPublic !== isPublic) continue;\r\n if (!isPublic && f.userId !== userId) continue;\r\n return f;\r\n }\r\n return null;\r\n }\r\n\r\n async findById(id: string): Promise<FileRecord | null> {\r\n return this.store.get(id) ?? null;\r\n }\r\n\r\n async create(input: CreateFileInput): Promise<FileRecord> {\r\n let id = input.id;\r\n if (!id) id = this.nextId();\r\n const now = new Date();\r\n const rec: FileRecord = {\r\n id,\r\n userId: input.userId,\r\n filename: input.filename,\r\n contentType: input.contentType,\r\n size: BigInt(input.size),\r\n storageKey: input.storageKey,\r\n isPublic: input.isPublic,\r\n status: FileStatus.COMPLETED,\r\n uploadId: input.uploadId,\r\n totalParts: input.totalParts,\r\n contentHash: input.contentHash,\r\n meta: input.meta ?? null,\r\n createdAt: now,\r\n updatedAt: now,\r\n deletedAt: null,\r\n };\r\n this.store.set(id, rec);\r\n return rec;\r\n }\r\n\r\n async softDelete(id: string, metaPatch?: Record<string, unknown>): Promise<void> {\r\n const rec = this.store.get(id);\r\n if (!rec) return;\r\n const next: FileRecord = {\r\n ...rec,\r\n meta: metaPatch ? { ...(rec.meta ?? {}), ...metaPatch } : rec.meta,\r\n deletedAt: new Date(),\r\n updatedAt: new Date(),\r\n };\r\n this.store.set(id, next);\r\n }\r\n\r\n /**\r\n * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法\r\n * - 只更新传入的字段;其他字段保留原值\r\n * - 文件不存在 → 抛错(由调用方决定是否捕获)\r\n */\r\n async update(id: string, patch: UpdateFileInput): Promise<FileRecord> {\r\n const rec = this.store.get(id);\r\n if (!rec) throw new Error('NOT_FOUND');\r\n const now = new Date();\r\n const next: FileRecord = {\r\n ...rec,\r\n storageKey: patch.storageKey ?? rec.storageKey,\r\n size: patch.size !== undefined ? BigInt(patch.size) : rec.size,\r\n contentType: patch.contentType ?? rec.contentType,\r\n meta: patch.meta === undefined ? rec.meta : patch.meta,\r\n watermarked: patch.watermarked ?? rec.watermarked,\r\n isOriginal: patch.isOriginal ?? rec.isOriginal,\r\n parentId: patch.parentId === undefined ? rec.parentId : patch.parentId,\r\n retentionExpiresAt:\r\n patch.retentionExpiresAt === undefined\r\n ? rec.retentionExpiresAt\r\n : patch.retentionExpiresAt,\r\n updatedAt: now,\r\n };\r\n this.store.set(id, next);\r\n return next;\r\n }\r\n\r\n async listOrphans(opts: { sinceDays: number; limit: number }): Promise<FileRecord[]> {\r\n const sinceMs = opts.sinceDays * 24 * 3600 * 1000;\r\n const list: FileRecord[] = [];\r\n for (const f of this.store.values()) {\r\n if (f.deletedAt) continue;\r\n if (f.createdAt.getTime() < Date.now() - sinceMs) continue;\r\n list.push(f);\r\n }\r\n list.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\r\n return list.slice(0, opts.limit);\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
@@ -54,7 +54,12 @@ function toFileRecord(row) {
54
54
  meta: row.meta && typeof row.meta === "object" ? row.meta : null,
55
55
  createdAt: row.createdAt,
56
56
  updatedAt: row.updatedAt,
57
- deletedAt: row.deletedAt
57
+ deletedAt: row.deletedAt,
58
+ // v1.5.1+:水印溯源字段(可选;旧 schema/老库里 row 可能没有,需要兜底)
59
+ watermarked: row.watermarked ?? false,
60
+ isOriginal: row.isOriginal ?? true,
61
+ parentId: row.parentId ?? null,
62
+ retentionExpiresAt: row.retentionExpiresAt ?? null
58
63
  };
59
64
  }
60
65
  var PrismaMultipartRepository = class {
@@ -220,6 +225,27 @@ var PrismaFileRepository = class {
220
225
  });
221
226
  return toFileRecord(row);
222
227
  }
228
+ /**
229
+ * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法
230
+ * - 只更新传入的字段;其他字段保留原值
231
+ * - 文件不存在 → 抛错(由调用方决定是否捕获)
232
+ * - 业务字段 watermarked/isOriginal/parentId/retentionExpiresAt 需要 schema v1.4.0+
233
+ */
234
+ async update(id, patch) {
235
+ const data = {};
236
+ if (patch.storageKey !== void 0) data.storageKey = patch.storageKey;
237
+ if (patch.size !== void 0) data.size = BigInt(patch.size);
238
+ if (patch.contentType !== void 0) data.contentType = patch.contentType;
239
+ if (patch.meta !== void 0) data.meta = patch.meta;
240
+ if (patch.watermarked !== void 0) data.watermarked = patch.watermarked;
241
+ if (patch.isOriginal !== void 0) data.isOriginal = patch.isOriginal;
242
+ if (patch.parentId !== void 0) data.parentId = patch.parentId;
243
+ if (patch.retentionExpiresAt !== void 0) {
244
+ data.retentionExpiresAt = patch.retentionExpiresAt;
245
+ }
246
+ const row = await this.prisma.file.update({ where: { id }, data });
247
+ return toFileRecord(row);
248
+ }
223
249
  async softDelete(id, metaPatch) {
224
250
  if (metaPatch) {
225
251
  const cur = await this.prisma.file.findUnique({ where: { id } });
@@ -446,6 +472,30 @@ var InMemoryFileRepository = class {
446
472
  };
447
473
  this.store.set(id, next);
448
474
  }
475
+ /**
476
+ * v1.5.1: 钩子 onComplete 返回 patch 后 SDK 调这个方法
477
+ * - 只更新传入的字段;其他字段保留原值
478
+ * - 文件不存在 → 抛错(由调用方决定是否捕获)
479
+ */
480
+ async update(id, patch) {
481
+ const rec = this.store.get(id);
482
+ if (!rec) throw new Error("NOT_FOUND");
483
+ const now = /* @__PURE__ */ new Date();
484
+ const next = {
485
+ ...rec,
486
+ storageKey: patch.storageKey ?? rec.storageKey,
487
+ size: patch.size !== void 0 ? BigInt(patch.size) : rec.size,
488
+ contentType: patch.contentType ?? rec.contentType,
489
+ meta: patch.meta === void 0 ? rec.meta : patch.meta,
490
+ watermarked: patch.watermarked ?? rec.watermarked,
491
+ isOriginal: patch.isOriginal ?? rec.isOriginal,
492
+ parentId: patch.parentId === void 0 ? rec.parentId : patch.parentId,
493
+ retentionExpiresAt: patch.retentionExpiresAt === void 0 ? rec.retentionExpiresAt : patch.retentionExpiresAt,
494
+ updatedAt: now
495
+ };
496
+ this.store.set(id, next);
497
+ return next;
498
+ }
449
499
  async listOrphans(opts) {
450
500
  const sinceMs = opts.sinceDays * 24 * 3600 * 1e3;
451
501
  const list = [];