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.
- package/README.md +152 -0
- package/dist/index.js +103 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +103 -4
- package/dist/index.mjs.map +1 -1
- package/dist/server/fastify-plugin.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +103 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +103 -4
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/routes/multipart.d.ts.map +1 -1
- package/dist/server/types.d.ts +71 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/repository/index.js +51 -1
- package/dist/server/utils/repository/index.js.map +1 -1
- package/dist/server/utils/repository/index.mjs +51 -1
- package/dist/server/utils/repository/index.mjs.map +1 -1
- package/dist/server/utils/repository/memory.d.ts +7 -1
- package/dist/server/utils/repository/memory.d.ts.map +1 -1
- package/dist/server/utils/repository/prisma.d.ts +8 -1
- package/dist/server/utils/repository/prisma.d.ts.map +1 -1
- package/dist/server/utils/repository/types.d.ts +38 -2
- package/dist/server/utils/repository/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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"}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -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 = [];
|