secure-upload-fastify-sdk 1.0.3 → 1.0.4

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,"sources":["../../src/server/index.ts","../../src/server/fastify-plugin.ts","../../src/server/utils/storage/local.ts","../../src/server/utils/storage/memory.ts","../../src/server/utils/repository/types.ts","../../src/server/utils/repository/prisma.ts","../../src/server/utils/repository/memory.ts","../../src/server/utils/strategy.ts","../../src/server/errors.ts","../../src/server/session.ts","../../src/server/routes/multipart.ts","../../src/server/routes/_internal.ts","../../src/server/utils/redis-helpers.ts","../../src/server/utils/memory-rate-limit.ts","../../src/server/routes/smart.ts","../../src/server/utils/cleaner.ts","../../src/server/utils/orphan-scanner.ts"],"sourcesContent":["/**\r\n * Server 公开 API\r\n *\r\n * 主入口:\r\n * import secureUploadPlugin, { type SecureUploadOptions } from 'secure-upload-fastify-sdk/server';\r\n * await app.register(secureUploadPlugin, { ... });\r\n *\r\n * 工具类:\r\n * import { LocalStorage, InMemoryStorage } from 'secure-upload-fastify-sdk/storage';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from 'secure-upload-fastify-sdk/repository';\r\n * import { ErrorCode, SecureError } from 'secure-upload-fastify-sdk/errors';\r\n * import { chooseStrategy, suggestConcurrency, DEFAULT_STRATEGY_CONFIG } from 'secure-upload-fastify-sdk/strategy';\r\n */\r\n\r\nexport {\r\n default,\r\n default as secureUploadPlugin,\r\n LocalStorage,\r\n InMemoryStorage,\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n} from './fastify-plugin';\r\n\r\nexport type {\r\n SecureUploadOptions,\r\n SecureUploadRuntime,\r\n SecureMode,\r\n RepositoryConfig,\r\n StorageConfig,\r\n PathPrefixConfig,\r\n SessionValidator,\r\n InitResponse,\r\n PartResponse,\r\n CompleteResponse,\r\n AbortResponse,\r\n StatusResponse,\r\n MultipartRecord,\r\n FileRecord,\r\n} from './types';\r\n\r\nexport {\r\n ErrorCode,\r\n SecureError,\r\n getHttpStatus,\r\n} from './errors';\r\n","/**\r\n * Fastify 主插件\r\n *\r\n * 注册 5 + 1 端点:\r\n * - {pathPrefix.multipart}/init\r\n * - {pathPrefix.multipart}/part\r\n * - {pathPrefix.multipart}/complete\r\n * - {pathPrefix.multipart}/abort\r\n * - {pathPrefix.multipart}/status\r\n * - {pathPrefix.smart}/init\r\n *\r\n * 用法(详见 README):\r\n *\r\n * import Fastify from 'fastify';\r\n * import uploadPlugin from 'secure-upload-fastify-sdk/server';\r\n * import { LocalStorage } from 'secure-upload-fastify-sdk/storage';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from 'secure-upload-fastify-sdk/repository';\r\n *\r\n * const app = Fastify({ logger: true });\r\n * await app.register(uploadPlugin, {\r\n * storage: { adapter: 'local', options: { baseDir: '/var/upload' } },\r\n * repository: { kind: 'prisma', prisma },\r\n * redis,\r\n * pathPrefix: { multipart: '/api/v1/upload/multipart', smart: '/api/v1/upload/smart' },\r\n * fileUrlPrefix: '/api/v1/upload/files',\r\n * });\r\n */\r\n\r\nimport fp from 'fastify-plugin';\r\nimport type { FastifyInstance } from 'fastify';\r\nimport { LocalStorage } from './utils/storage/local';\r\nimport { InMemoryStorage } from './utils/storage/memory';\r\nimport { PrismaMultipartRepository, PrismaFileRepository } from './utils/repository/prisma';\r\nimport { InMemoryMultipartRepository, InMemoryFileRepository } from './utils/repository/memory';\r\nimport { DEFAULT_STRATEGY_CONFIG } from './utils/strategy';\r\nimport { defaultSessionValidator } from './session';\r\nimport { registerMultipartRoutes } from './routes/multipart';\r\nimport { registerSmartRoutes } from './routes/smart';\r\nimport { startMultipartCleanupTask } from './utils/cleaner';\r\nimport { startOrphanFileScanTask } from './utils/orphan-scanner';\r\nimport { SecureError } from './errors';\r\nimport type {\r\n SecureUploadOptions,\r\n SecureUploadRuntime,\r\n RepositoryConfig,\r\n StorageConfig,\r\n} from './types';\r\nimport type { StorageAdapter } from './utils/storage/types';\r\nimport type { MultipartRepository, FileRepository } from './utils/repository/types';\r\n\r\nconst KB = 1024;\r\nconst MB = 1024 * KB;\r\nconst GB = 1024 * MB;\r\n\r\nconst DEFAULTS = {\r\n enabled: true,\r\n fileMaxSize: 5 * GB,\r\n partMin: 5 * MB,\r\n partMax: 50 * MB,\r\n partDefault: 20 * MB,\r\n ttlSeconds: 24 * 60 * 60,\r\n suggestedConcurrency: 3,\r\n initRateLimit: 30,\r\n partRateLimit: 600,\r\n smartMaxFiles: 50,\r\n smartInitRateLimit: 30,\r\n multipartPath: '/multipart',\r\n smartPath: '/smart',\r\n fileUrlPrefix: '/files',\r\n secure: 'raw' as const,\r\n};\r\n\r\nasync function resolveStorage(cfg: StorageConfig): Promise<StorageAdapter> {\r\n if (typeof cfg === 'object' && 'put' in cfg && typeof (cfg as any).put === 'function') {\r\n // 直接传 StorageAdapter 实例\r\n return cfg as StorageAdapter;\r\n }\r\n const preset = cfg as { adapter: 'local'; options: { baseDir: string } };\r\n if (preset.adapter === 'local') {\r\n return new LocalStorage(preset.options.baseDir);\r\n }\r\n throw new Error(`[secure-upload-sdk] 未知的 storage.adapter: ${(preset as any).adapter}`);\r\n}\r\n\r\nfunction resolveRepository(cfg: RepositoryConfig): {\r\n multipart: MultipartRepository;\r\n file: FileRepository;\r\n} {\r\n if (cfg.kind === 'prisma') {\r\n return {\r\n multipart: new PrismaMultipartRepository(cfg.prisma),\r\n file: new PrismaFileRepository(cfg.prisma),\r\n };\r\n }\r\n return { multipart: cfg.multipart, file: cfg.file };\r\n}\r\n\r\nasync function secureUploadPluginImpl(\r\n app: FastifyInstance,\r\n options: SecureUploadOptions\r\n): Promise<void> {\r\n // 1. 校验必填\r\n if (!options.storage) {\r\n throw new Error('[secure-upload-sdk] options.storage 必填');\r\n }\r\n if (!options.repository) {\r\n throw new Error('[secure-upload-sdk] options.repository 必填');\r\n }\r\n\r\n // 2. 解析 storage / repository\r\n const storage = await resolveStorage(options.storage);\r\n const { multipart: multipartRepo, file: fileRepo } = resolveRepository(options.repository);\r\n\r\n // 3. 构造运行时上下文\r\n const runtime: SecureUploadRuntime = {\r\n storage,\r\n multipartRepo,\r\n fileRepo,\r\n redis: options.redis ?? null,\r\n config: {\r\n enabled: options.enabled ?? DEFAULTS.enabled,\r\n secure: options.secure?.mode ?? DEFAULTS.secure,\r\n fileMaxSize: options.fileMaxSize ?? DEFAULTS.fileMaxSize,\r\n partMin: options.partSize?.min ?? DEFAULTS.partMin,\r\n partMax: options.partSize?.max ?? DEFAULTS.partMax,\r\n partDefault: options.partSize?.default ?? DEFAULTS.partDefault,\r\n ttlSeconds: options.ttlSeconds ?? DEFAULTS.ttlSeconds,\r\n suggestedConcurrency: options.suggestedClientConcurrency ?? DEFAULTS.suggestedConcurrency,\r\n initRateLimit: options.rateLimit?.initPerMin ?? DEFAULTS.initRateLimit,\r\n partRateLimit: options.rateLimit?.partPerMin ?? DEFAULTS.partRateLimit,\r\n smartMaxFiles: options.rateLimit?.smartMaxFilesPerInit ?? DEFAULTS.smartMaxFiles,\r\n smartInitRateLimit: options.rateLimit?.smartInitRateLimit ?? DEFAULTS.smartInitRateLimit,\r\n multipartPath: options.pathPrefix?.multipart ?? DEFAULTS.multipartPath,\r\n smartPath: options.pathPrefix?.smart ?? DEFAULTS.smartPath,\r\n fileUrlPrefix: options.fileUrlPrefix ?? DEFAULTS.fileUrlPrefix,\r\n strategy: {\r\n ...DEFAULT_STRATEGY_CONFIG,\r\n ...(options.strategy ?? {}),\r\n },\r\n },\r\n validateSession: options.sessionValidator ?? defaultSessionValidator,\r\n };\r\n\r\n app.decorate('secureUpload', runtime);\r\n\r\n // 4. 注册路由\r\n await registerMultipartRoutes(app, runtime);\r\n await registerSmartRoutes(app, runtime);\r\n\r\n // 4.5 全局错误处理:把 SecureError 转成统一格式,code 保持数字\r\n // 注意:fastify 注入的 err 类型是 FastifyError(有 code/statusCode),与我们的 SecureError code 冲突,\r\n // 所以这里把 err 显式声明为 FastifyError | SecureError(联合而非交叉)\r\n app.setErrorHandler(function (this: any, err: unknown, req: any, reply: any) {\r\n if (err instanceof SecureError) {\r\n return reply.status(err.statusCode).send({\r\n success: false,\r\n code: err.code,\r\n error: err.name,\r\n message: err.message,\r\n });\r\n }\r\n // Zod 校验错误\r\n const anyErr = err as any;\r\n if (anyErr && typeof anyErr === 'object' && 'issues' in anyErr && Array.isArray(anyErr.issues)) {\r\n return reply.status(400).send({\r\n success: false,\r\n code: 1001, // INVALID_REQUEST\r\n error: 'ZodValidationError',\r\n message: anyErr.issues?.[0]?.message ?? 'invalid request',\r\n });\r\n }\r\n // 其他错误 → 500\r\n app.log.error({ err }, '❌ secure-upload-sdk 错误');\r\n return reply.status(anyErr?.statusCode ?? 500).send({\r\n success: false,\r\n code: 1500, // INTERNAL_ERROR\r\n error: anyErr?.name ?? 'INTERNAL_ERROR',\r\n message: anyErr?.message ?? 'internal error',\r\n });\r\n });\r\n\r\n // 5. 后台任务\r\n if (options.cleanupTask?.enabled !== false) {\r\n startMultipartCleanupTask({\r\n enabled: true,\r\n intervalSeconds: options.cleanupTask?.intervalSeconds ?? 300,\r\n batchLimit: options.cleanupTask?.batchLimit ?? 100,\r\n storage,\r\n multipartRepo,\r\n log: app.log,\r\n onClose: (cb) => app.addHook('onClose', async () => cb()),\r\n });\r\n }\r\n if (options.orphanScan?.enabled !== false) {\r\n startOrphanFileScanTask({\r\n enabled: true,\r\n intervalSeconds: options.orphanScan?.intervalSeconds ?? 300,\r\n lookbackDays: options.orphanScan?.lookbackDays ?? 7,\r\n batchSize: options.orphanScan?.batchSize ?? 2000,\r\n storage,\r\n fileRepo,\r\n log: app.log,\r\n onClose: (cb) => app.addHook('onClose', async () => cb()),\r\n });\r\n }\r\n\r\n app.log.info(\r\n {\r\n secure: runtime.config.secure,\r\n storage: storage.constructor.name,\r\n repository: options.repository.kind,\r\n redis: options.redis ? 'yes' : 'memory-fallback',\r\n multipartPath: runtime.config.multipartPath,\r\n smartPath: runtime.config.smartPath,\r\n },\r\n '🚀 secure-upload-fastify-sdk 插件已注册'\r\n );\r\n}\r\n\r\nexport default fp(secureUploadPluginImpl, {\r\n name: 'secure-upload-fastify-sdk',\r\n fastify: '4.x',\r\n});\r\n\r\n// ============================================================\r\n// 内部导出(供测试用)\r\n// ============================================================\r\nexport {\r\n LocalStorage,\r\n InMemoryStorage,\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n};\r\n","/**\r\n * 本地磁盘存储实现(默认)\r\n *\r\n * 目录布局:\r\n * <baseDir>/yyyy/mm/dd/<id>.<ext> <- 合并后的正式文件\r\n * <baseDir>/_multipart/<uploadId>/part_0001.bin\r\n * part_0002.bin\r\n * ...\r\n *\r\n * 兼容性: 实现 StorageAdapter 接口,可被 SDK 接受。\r\n * 后续可写 S3Storage(实现同一接口)直接替换,业务代码零改动。\r\n */\r\n\r\nimport { promises as fs, createReadStream, createWriteStream } from 'node:fs';\r\nimport path from 'node:path';\r\nimport { createHash } from 'node:crypto';\r\nimport type { ReadStream } from 'node:fs';\r\nimport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\nexport interface LocalStorageOptions {\r\n /** 存储根目录(绝对路径或相对 cwd) */\r\n baseDir: string;\r\n}\r\n\r\nexport class LocalStorage implements StorageAdapter {\r\n private baseDir: string;\r\n\r\n constructor(opts: LocalStorageOptions | string) {\r\n this.baseDir = typeof opts === 'string' ? opts : opts.baseDir;\r\n }\r\n\r\n // ============================================================\r\n // 内部工具\r\n // ============================================================\r\n\r\n private inferExt(filename: string, contentType: string): string {\r\n const fromName = path.extname(filename).toLowerCase();\r\n if (fromName) return fromName;\r\n const map: Record<string, string> = {\r\n 'image/png': '.png',\r\n 'image/jpeg': '.jpg',\r\n 'image/gif': '.gif',\r\n 'image/webp': '.webp',\r\n 'image/svg+xml': '.svg',\r\n 'application/pdf': '.pdf',\r\n 'text/plain': '.txt',\r\n 'application/json': '.json',\r\n 'application/zip': '.zip',\r\n 'video/mp4': '.mp4',\r\n };\r\n return map[contentType] ?? '.bin';\r\n }\r\n\r\n private generateKey(filename: string, contentType: string, id: string): string {\r\n const now = new Date();\r\n const yyyy = now.getFullYear();\r\n const mm = String(now.getMonth() + 1).padStart(2, '0');\r\n const dd = String(now.getDate()).padStart(2, '0');\r\n const ext = this.inferExt(filename, contentType);\r\n return `${yyyy}/${mm}/${dd}/${id}${ext}`;\r\n }\r\n\r\n private multipartDir(uploadId: string): string {\r\n return path.resolve(this.baseDir, '_multipart', uploadId);\r\n }\r\n\r\n private partPath(uploadId: string, partNumber: number): string {\r\n if (!Number.isInteger(partNumber) || partNumber < 1) {\r\n throw new Error(`[storage] partNumber 非法: ${partNumber}`);\r\n }\r\n return path.join(this.multipartDir(uploadId), `part_${String(partNumber).padStart(4, '0')}.bin`);\r\n }\r\n\r\n // ============================================================\r\n // StorageAdapter 实现\r\n // ============================================================\r\n\r\n async put(data: Buffer, meta: FileMeta & { id: string }): Promise<PutResult> {\r\n const key = this.generateKey(meta.filename, meta.contentType, meta.id);\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n await fs.mkdir(path.dirname(absolutePath), { recursive: true });\r\n await fs.writeFile(absolutePath, data);\r\n return { key, absolutePath };\r\n }\r\n\r\n get(key: string): ReadStream {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n return createReadStream(absolutePath);\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n try {\r\n await fs.unlink(absolutePath);\r\n } catch (e: any) {\r\n if (e.code !== 'ENOENT') throw e;\r\n }\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n try {\r\n await fs.access(absolutePath);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n async putPart(\r\n uploadId: string,\r\n partNumber: number,\r\n data: Buffer\r\n ): Promise<{ size: number; path: string }> {\r\n const dir = this.multipartDir(uploadId);\r\n await fs.mkdir(dir, { recursive: true });\r\n const finalPath = this.partPath(uploadId, partNumber);\r\n const tmpPath = `${finalPath}.tmp`;\r\n try {\r\n await fs.writeFile(tmpPath, data);\r\n await fs.rename(tmpPath, finalPath);\r\n } catch (e) {\r\n try { await fs.unlink(tmpPath); } catch { /* ignore */ }\r\n throw e;\r\n }\r\n return { size: data.length, path: finalPath };\r\n }\r\n\r\n async listParts(uploadId: string): Promise<number[]> {\r\n const dir = this.multipartDir(uploadId);\r\n try {\r\n const names = await fs.readdir(dir);\r\n const nums: number[] = [];\r\n for (const n of names) {\r\n const m = n.match(/^part_(\\d{4,})\\.bin$/);\r\n if (m) nums.push(parseInt(m[1], 10));\r\n }\r\n nums.sort((a, b) => a - b);\r\n return nums;\r\n } catch (e: any) {\r\n if (e.code === 'ENOENT') return [];\r\n throw e;\r\n }\r\n }\r\n\r\n async partSize(uploadId: string, partNumber: number): Promise<number> {\r\n const p = this.partPath(uploadId, partNumber);\r\n const stat = await fs.stat(p);\r\n return stat.size;\r\n }\r\n\r\n async mergeParts(\r\n uploadId: string,\r\n totalParts: number,\r\n target: { id: string; filename: string; contentType: string }\r\n ): Promise<MergeResult> {\r\n const key = this.generateKey(target.filename, target.contentType, target.id);\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n await fs.mkdir(path.dirname(absolutePath), { recursive: true });\r\n const tmpPath = `${absolutePath}.merge.tmp`;\r\n\r\n const out = createWriteStream(tmpPath);\r\n try {\r\n for (let i = 1; i <= totalParts; i++) {\r\n const partPath = this.partPath(uploadId, i);\r\n try {\r\n await fs.access(partPath);\r\n } catch {\r\n throw new Error(`[storage] 缺分片 part_${i} (uploadId=${uploadId})`);\r\n }\r\n await new Promise<void>((resolve, reject) => {\r\n const rs = createReadStream(partPath);\r\n rs.on('error', reject);\r\n rs.on('end', resolve);\r\n rs.pipe(out, { end: false });\r\n });\r\n }\r\n out.end();\r\n await new Promise<void>((resolve, reject) => {\r\n out.on('finish', () => resolve());\r\n out.on('error', reject);\r\n });\r\n const stat = await fs.stat(tmpPath);\r\n await fs.rename(tmpPath, absolutePath);\r\n return { key, absolutePath, size: stat.size };\r\n } catch (e) {\r\n try { await fs.unlink(tmpPath); } catch { /* ignore */ }\r\n throw e;\r\n }\r\n }\r\n\r\n async cleanupMultipart(uploadId: string): Promise<void> {\r\n const dir = this.multipartDir(uploadId);\r\n try {\r\n await fs.rm(dir, { recursive: true, force: true });\r\n } catch (e: any) {\r\n if (e.code !== 'ENOENT') {\r\n console.warn(`[storage] cleanup multipart failed: uploadId=${uploadId} err=${e.message}`);\r\n }\r\n }\r\n }\r\n\r\n async computeMergedHash(uploadId: string, totalParts: number): Promise<string> {\r\n const hash = createHash('sha256');\r\n for (let i = 1; i <= totalParts; i++) {\r\n const partPath = this.partPath(uploadId, i);\r\n const stream = createReadStream(partPath);\r\n await new Promise<void>((resolve, reject) => {\r\n stream.on('data', (chunk) => hash.update(chunk as Buffer));\r\n stream.on('end', resolve);\r\n stream.on('error', reject);\r\n });\r\n }\r\n return hash.digest('hex');\r\n }\r\n}\r\n","/**\r\n * 内存存储实现(测试 / POC 场景)\r\n *\r\n * 用 Map 模拟:\r\n * - files: key -> Buffer (正式文件)\r\n * - parts: uploadId -> Map<partNumber, Buffer> (分片)\r\n * - partSizes: uploadId -> Map<partNumber, number>\r\n *\r\n * 注意: 内存存储在重启时全部丢失,只用于测试。\r\n * 生产请使用 LocalStorage / S3Storage / OSSStorage。\r\n */\r\n\r\nimport { Readable } from 'node:stream';\r\nimport { createHash } from 'node:crypto';\r\nimport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\nexport class InMemoryStorage implements StorageAdapter {\r\n private files = new Map<string, Buffer>();\r\n private parts = new Map<string, Map<number, Buffer>>();\r\n\r\n async put(data: Buffer, meta: FileMeta & { id: string }): Promise<PutResult> {\r\n const key = `mem/${meta.id}`;\r\n this.files.set(key, Buffer.from(data));\r\n return { key };\r\n }\r\n\r\n get(key: string): NodeJS.ReadableStream {\r\n const buf = this.files.get(key);\r\n if (!buf) throw new Error(`[memory-storage] not found: ${key}`);\r\n return Readable.from(buf);\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n this.files.delete(key);\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n return this.files.has(key);\r\n }\r\n\r\n async putPart(\r\n uploadId: string,\r\n partNumber: number,\r\n data: Buffer\r\n ): Promise<{ size: number }> {\r\n let m = this.parts.get(uploadId);\r\n if (!m) {\r\n m = new Map();\r\n this.parts.set(uploadId, m);\r\n }\r\n m.set(partNumber, Buffer.from(data));\r\n return { size: data.length };\r\n }\r\n\r\n async listParts(uploadId: string): Promise<number[]> {\r\n const m = this.parts.get(uploadId);\r\n if (!m) return [];\r\n return [...m.keys()].sort((a, b) => a - b);\r\n }\r\n\r\n async mergeParts(\r\n uploadId: string,\r\n totalParts: number,\r\n target: { id: string; filename: string; contentType: string }\r\n ): Promise<MergeResult> {\r\n const m = this.parts.get(uploadId);\r\n if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);\r\n const parts: Buffer[] = [];\r\n for (let i = 1; i <= totalParts; i++) {\r\n const p = m.get(i);\r\n if (!p) throw new Error(`[memory-storage] missing part ${i}`);\r\n parts.push(p);\r\n }\r\n const merged = Buffer.concat(parts);\r\n const key = `mem/${target.id}`;\r\n this.files.set(key, merged);\r\n return { key, size: merged.length };\r\n }\r\n\r\n async computeMergedHash(uploadId: string, totalParts: number): Promise<string> {\r\n const hash = createHash('sha256');\r\n const m = this.parts.get(uploadId);\r\n if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);\r\n for (let i = 1; i <= totalParts; i++) {\r\n const p = m.get(i);\r\n if (!p) throw new Error(`[memory-storage] missing part ${i}`);\r\n hash.update(p);\r\n }\r\n return hash.digest('hex');\r\n }\r\n\r\n async cleanupMultipart(uploadId: string): Promise<void> {\r\n this.parts.delete(uploadId);\r\n }\r\n}\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","/**\r\n * Smart 上传策略选择器\r\n *\r\n * 服务端根据 totalSize 自动选择 ONE_SHOT 或 MULTIPART:\r\n * - ONE_SHOT: 1 个 part(适合 ≤ ONE_SHOT_MAX 的小文件)\r\n * 客户端只调 1 次 part + 1 次 complete = 2 RTT\r\n * - MULTIPART: N 个 part,根据文件大小动态调整 partSize:\r\n * ≤ 1GB → 1MB / part\r\n * 1 ~ 10GB → 5MB / part\r\n * > 10GB → 10MB / part\r\n *\r\n * 设计约束\r\n * ========\r\n * - 纯函数:无副作用、不读 IO、不访问全局状态,易于单测\r\n * - 0 字节文件合法(partSize=0, totalParts=1)\r\n * - 决策结果携带 reason 字段,便于日志/排障\r\n * - 不与 config 直接耦合:调用方传入 cfg\r\n */\r\n\r\nexport type StrategyKind = 'ONE_SHOT' | 'MULTIPART';\r\n\r\nexport interface StrategyConfig {\r\n /** ≤ 此字节数走 ONE_SHOT(默认 1MB) */\r\n oneShotMax: number;\r\n /** 默认分片大小(默认 1MB) */\r\n defaultPartSize: number;\r\n /** 超过此字节数切到大分片(默认 1GB) */\r\n bigFileThreshold: number;\r\n /** 大文件分片大小(默认 5MB) */\r\n bigFilePartSize: number;\r\n /** 超过此字节数切到超大分片(默认 10GB) */\r\n hugeFileThreshold: number;\r\n /** 超大文件分片大小(默认 10MB) */\r\n hugeFilePartSize: number;\r\n}\r\n\r\nexport interface StrategyResult {\r\n kind: StrategyKind;\r\n /** 实际分片大小(字节) */\r\n partSize: number;\r\n /** 总分片数(ONE_SHOT 固定为 1) */\r\n totalParts: number;\r\n /** 决策原因(便于日志/调试) */\r\n reason: string;\r\n}\r\n\r\n/** 默认策略配置 */\r\nexport const DEFAULT_STRATEGY_CONFIG: StrategyConfig = {\r\n oneShotMax: 1024 * 1024, // 1MB\r\n defaultPartSize: 1024 * 1024, // 1MB\r\n bigFileThreshold: 1024 ** 3, // 1GB\r\n bigFilePartSize: 5 * 1024 * 1024, // 5MB\r\n hugeFileThreshold: 10 * 1024 ** 3, // 10GB\r\n hugeFilePartSize: 10 * 1024 * 1024, // 10MB\r\n};\r\n\r\n/**\r\n * 根据 totalSize 选出上传策略\r\n *\r\n * @throws totalSize 非法(负数 / NaN / 非有限数)\r\n *\r\n * @example\r\n * chooseStrategy(1024) // → ONE_SHOT 1 part, partSize=1024\r\n * chooseStrategy(5 * 1024 * 1024) // → MULTIPART 5 part, partSize=1MB\r\n * chooseStrategy(5 * 1024**3) // → MULTIPART 1024 part, partSize=5MB\r\n * chooseStrategy(50 * 1024**3) // → MULTIPART 5120 part, partSize=10MB\r\n */\r\nexport function chooseStrategy(\r\n totalSize: number,\r\n cfg: StrategyConfig = DEFAULT_STRATEGY_CONFIG\r\n): StrategyResult {\r\n if (!Number.isFinite(totalSize) || totalSize < 0) {\r\n throw new Error(`[strategy] totalSize 非法: ${totalSize}(必须 ≥ 0 的有限数)`);\r\n }\r\n validateConfig(cfg);\r\n\r\n if (totalSize === 0) {\r\n return { kind: 'ONE_SHOT', partSize: 0, totalParts: 1, reason: '0字节' };\r\n }\r\n\r\n if (totalSize <= cfg.oneShotMax) {\r\n return {\r\n kind: 'ONE_SHOT',\r\n partSize: totalSize,\r\n totalParts: 1,\r\n reason: `≤ ${cfg.oneShotMax}B 走 ONE_SHOT(1 part 直传)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.hugeFileThreshold) {\r\n const partSize = cfg.hugeFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.hugeFileThreshold}B 走 HUGE(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.bigFileThreshold) {\r\n const partSize = cfg.bigFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.bigFileThreshold}B 走 BIG(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n const partSize = cfg.defaultPartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `≤ ${cfg.bigFileThreshold}B 走 DEFAULT(${partSize}B/part)`,\r\n };\r\n}\r\n\r\n/**\r\n * 建议的客户端并发上传 part 数\r\n * - ONE_SHOT:固定 1(只传 1 个 part,无需并发)\r\n * - totalParts ≤ 4:用 totalParts(part 太少,无并发必要)\r\n * - totalParts > 4:用 4(服务端限流/带宽考虑)\r\n */\r\nexport function suggestConcurrency(s: StrategyResult): number {\r\n if (s.kind === 'ONE_SHOT') return 1;\r\n if (s.totalParts <= 4) return Math.max(1, s.totalParts);\r\n return 4;\r\n}\r\n\r\nfunction validateConfig(cfg: StrategyConfig): void {\r\n const fields: Array<[keyof StrategyConfig, string]> = [\r\n ['oneShotMax', 'oneShotMax'],\r\n ['defaultPartSize', 'defaultPartSize'],\r\n ['bigFileThreshold', 'bigFileThreshold'],\r\n ['bigFilePartSize', 'bigFilePartSize'],\r\n ['hugeFileThreshold', 'hugeFileThreshold'],\r\n ['hugeFilePartSize', 'hugeFilePartSize'],\r\n ];\r\n for (const [k, name] of fields) {\r\n if (!Number.isFinite(cfg[k]) || cfg[k] <= 0) {\r\n throw new Error(`[strategy] cfg.${name} 非法: ${cfg[k]}(必须 > 0)`);\r\n }\r\n }\r\n if (cfg.oneShotMax > cfg.defaultPartSize) {\r\n throw new Error(\r\n `[strategy] oneShotMax(${cfg.oneShotMax}) 不能 > defaultPartSize(${cfg.defaultPartSize})`\r\n );\r\n }\r\n if (cfg.bigFileThreshold >= cfg.hugeFileThreshold) {\r\n throw new Error(\r\n `[strategy] bigFileThreshold(${cfg.bigFileThreshold}) 必须 < hugeFileThreshold(${cfg.hugeFileThreshold})`\r\n );\r\n }\r\n if (cfg.defaultPartSize > cfg.bigFilePartSize) {\r\n throw new Error(\r\n `[strategy] defaultPartSize(${cfg.defaultPartSize}) 不能 > bigFilePartSize(${cfg.bigFilePartSize})`\r\n );\r\n }\r\n if (cfg.bigFilePartSize > cfg.hugeFilePartSize) {\r\n throw new Error(\r\n `[strategy] bigFilePartSize(${cfg.bigFilePartSize}) 不能 > hugeFilePartSize(${cfg.hugeFilePartSize})`\r\n );\r\n }\r\n}\r\n","/**\r\n * 错误码 + SecureError\r\n *\r\n * 设计:\r\n * - 1000-1099: 通用请求错误(INVALID_REQUEST 等)\r\n * - 1400-1499: HTTP 语义错误(NOT_FOUND / FORBIDDEN / RATE_LIMITED 等)\r\n * - 1500-1599: 上传相关(BINARY / BATCH / MULTIPART)\r\n *\r\n * 独立于 secure-crypto-top-zod-sdk 的错误码空间,\r\n * 即使用户不引入加密 SDK,这套码也能独立用。\r\n */\r\n\r\nexport enum ErrorCode {\r\n // 通用\r\n INVALID_REQUEST = 1001,\r\n INTERNAL_ERROR = 1500,\r\n NOT_FOUND = 1400,\r\n FORBIDDEN = 1403,\r\n RATE_LIMITED = 1402,\r\n\r\n // binary 协议(网关透传场景)\r\n BINARY_PAYLOAD_EMPTY = 1501,\r\n BINARY_PAYLOAD_TOO_LARGE = 1502,\r\n BINARY_AUTH_REQUIRED = 1503,\r\n BINARY_AUTH_FAILED = 1504,\r\n\r\n // 批量上传\r\n BATCH_EMPTY = 1510,\r\n BATCH_TOO_MANY_FILES = 1511,\r\n BATCH_TOTAL_TOO_LARGE = 1512,\r\n BATCH_FILE_INVALID = 1513,\r\n\r\n // 分片上传(1520-1539)\r\n MULTIPART_NOT_FOUND = 1520,\r\n MULTIPART_ALREADY_COMPLETED = 1521,\r\n MULTIPART_ALREADY_ABORTED = 1522,\r\n MULTIPART_EXPIRED = 1523,\r\n MULTIPART_STATUS_INVALID = 1524,\r\n MULTIPART_TOTAL_TOO_LARGE = 1530,\r\n MULTIPART_PART_TOO_LARGE = 1531,\r\n MULTIPART_PART_TOO_SMALL = 1532,\r\n MULTIPART_INVALID_PART_NUMBER = 1533,\r\n MULTIPART_DUPLICATE_PART = 1534,\r\n MULTIPART_PART_HASH_MISMATCH = 1535,\r\n MULTIPART_FINAL_HASH_MISMATCH = 1536,\r\n MULTIPART_INCOMPLETE_PARTS = 1537,\r\n MULTIPART_AUTH_REQUIRED = 1538,\r\n MULTIPART_INVALID_ARG = 1539,\r\n}\r\n\r\nconst HTTP: Record<ErrorCode, number> = {\r\n [ErrorCode.INVALID_REQUEST]: 400,\r\n [ErrorCode.INTERNAL_ERROR]: 500,\r\n [ErrorCode.NOT_FOUND]: 404,\r\n [ErrorCode.FORBIDDEN]: 403,\r\n [ErrorCode.RATE_LIMITED]: 429,\r\n // binary\r\n [ErrorCode.BINARY_PAYLOAD_EMPTY]: 400,\r\n [ErrorCode.BINARY_PAYLOAD_TOO_LARGE]: 413,\r\n [ErrorCode.BINARY_AUTH_REQUIRED]: 401,\r\n [ErrorCode.BINARY_AUTH_FAILED]: 401,\r\n // batch\r\n [ErrorCode.BATCH_EMPTY]: 400,\r\n [ErrorCode.BATCH_TOO_MANY_FILES]: 413,\r\n [ErrorCode.BATCH_TOTAL_TOO_LARGE]: 413,\r\n [ErrorCode.BATCH_FILE_INVALID]: 400,\r\n // multipart\r\n [ErrorCode.MULTIPART_NOT_FOUND]: 404,\r\n [ErrorCode.MULTIPART_ALREADY_COMPLETED]: 409,\r\n [ErrorCode.MULTIPART_ALREADY_ABORTED]: 409,\r\n [ErrorCode.MULTIPART_EXPIRED]: 410,\r\n [ErrorCode.MULTIPART_STATUS_INVALID]: 409,\r\n [ErrorCode.MULTIPART_TOTAL_TOO_LARGE]: 413,\r\n [ErrorCode.MULTIPART_PART_TOO_LARGE]: 413,\r\n [ErrorCode.MULTIPART_PART_TOO_SMALL]: 400,\r\n [ErrorCode.MULTIPART_INVALID_PART_NUMBER]: 400,\r\n [ErrorCode.MULTIPART_DUPLICATE_PART]: 409,\r\n [ErrorCode.MULTIPART_PART_HASH_MISMATCH]: 400,\r\n [ErrorCode.MULTIPART_FINAL_HASH_MISMATCH]: 400,\r\n [ErrorCode.MULTIPART_INCOMPLETE_PARTS]: 409,\r\n [ErrorCode.MULTIPART_AUTH_REQUIRED]: 401,\r\n [ErrorCode.MULTIPART_INVALID_ARG]: 400,\r\n};\r\n\r\nexport class SecureError extends Error {\r\n constructor(public code: ErrorCode, message?: string) {\r\n super(message ?? ErrorCode[code]);\r\n this.name = 'SecureError';\r\n }\r\n get statusCode(): number {\r\n return HTTP[this.code];\r\n }\r\n}\r\n\r\nexport function getHttpStatus(code: ErrorCode): number {\r\n return HTTP[code];\r\n}\r\n","/**\r\n * Session 验证(JWT payload 解析)\r\n *\r\n * 默认实现: base64url 解码 payload,读 sub / exp 字段\r\n * - 不做 HMAC 签名验证(SDK 不持有 appSecret,无法验签)\r\n * - 只读 payload 拿 userId + 校验 exp\r\n * - 高级场景: 用户通过 options.sessionValidator 注入 jwt.verify()\r\n *\r\n * 安全性:\r\n * - 完整安全链路要求:gateway 已做 JWT 验签,业务服务信任透传的 session\r\n * - 如果用户在业务服务自己做验签,通过 options.sessionValidator 注入即可\r\n */\r\n\r\nimport { SecureError, ErrorCode } from './errors';\r\nimport type { SessionValidator } from './types';\r\n\r\nexport function parseJwtPayload(token: string | undefined): { sub?: number; exp?: number } | null {\r\n if (!token) return null;\r\n try {\r\n const parts = token.split('.');\r\n if (parts.length !== 3) return null;\r\n return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport const defaultSessionValidator: SessionValidator = (token, isPublic) => {\r\n if (isPublic) return null;\r\n if (!token) {\r\n throw new SecureError(ErrorCode.BINARY_AUTH_REQUIRED, 'private 上传必须提供 session (JWT)');\r\n }\r\n const p = parseJwtPayload(token);\r\n if (!p?.sub) throw new SecureError(ErrorCode.BINARY_AUTH_FAILED, 'session 格式无效');\r\n if (p.exp && p.exp * 1000 < Date.now()) {\r\n throw new SecureError(ErrorCode.BINARY_AUTH_FAILED, 'session 已过期');\r\n }\r\n return Number(p.sub);\r\n};\r\n","/**\r\n * 分片上传路由\r\n *\r\n * 端点(相对 pathPrefix.multipart,默认 '/multipart'):\r\n * - POST /init\r\n * - POST /part (binary 协议,从 x-secure-raw-base64 或 secureRequest.raw 读)\r\n * - POST /complete\r\n * - POST /abort\r\n * - POST /status\r\n *\r\n * 鉴权:\r\n * - private:需要 JWT(从 sr.session 或 x-secure-session 头)\r\n * - public :无需登录,userId=null\r\n *\r\n * 协议:\r\n * - secure-crypto 模式: 走 req.secureRequest.biz + req.secureRequest.raw\r\n * - raw 模式: 走 req.body + x-secure-raw-base64 header\r\n */\r\n\r\nimport crypto from 'node:crypto';\r\nimport { z } from 'zod';\r\nimport type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';\r\nimport { SecureError, ErrorCode } from '../errors';\r\nimport {\r\n resolveSession,\r\n resolveSessionKey,\r\n resolvePartData,\r\n resolvePartBiz,\r\n} from './_internal';\r\nimport { MultipartStatus } from '../utils/repository/types';\r\nimport { slidingWindowRateLimit, withLock } from '../utils/redis-helpers';\r\nimport { memorySlidingWindowRateLimit, memoryWithLock } from '../utils/memory-rate-limit';\r\nimport type { SecureUploadRuntime } from '../types';\r\nimport type {\r\n InitResponse,\r\n PartResponse,\r\n CompleteResponse,\r\n AbortResponse,\r\n StatusResponse,\r\n} from '../types';\r\n\r\n// ============================================================\r\n// Zod 校验 schema\r\n// ============================================================\r\n\r\nconst InitBodySchema = z.object({\r\n filename: z.string().min(1).max(255),\r\n contentType: z.string().min(1).max(128).default('application/octet-stream'),\r\n totalSize: z.coerce.number().int().nonnegative(),\r\n partSize: z.coerce.number().int().positive().optional(),\r\n isPublic: z.coerce.boolean().default(false),\r\n expectedHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n meta: z.record(z.unknown()).optional(),\r\n}).strict();\r\n\r\nconst CompleteBodySchema = z.object({\r\n uploadId: z.string().min(1).max(128),\r\n parts: z.array(z.object({\r\n partNumber: z.coerce.number().int().positive(),\r\n hash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n })).optional(),\r\n finalHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n}).strict();\r\n\r\nconst AbortBodySchema = z.object({\r\n uploadId: z.string().min(1).max(128),\r\n}).strict();\r\n\r\nconst StatusBodySchema = z.object({\r\n uploadId: z.string().min(1).max(128),\r\n});\r\n\r\n// ============================================================\r\n// 限流 + 锁:统一抽象(redis 或内存)\r\n// ============================================================\r\n\r\nasync function rateLimit(\r\n rt: SecureUploadRuntime,\r\n key: string,\r\n windowMs: number,\r\n limit: number\r\n): Promise<{ ok: boolean; retryAfterMs: number }> {\r\n if (rt.redis) {\r\n const r = await slidingWindowRateLimit(rt.redis, key, windowMs, limit);\r\n return { ok: r.ok, retryAfterMs: r.retryAfterMs };\r\n }\r\n const r = memorySlidingWindowRateLimit(key, windowMs, limit);\r\n return { ok: r.ok, retryAfterMs: r.retryAfterMs };\r\n}\r\n\r\nasync function lock<T>(\r\n rt: SecureUploadRuntime,\r\n key: string,\r\n ttlMs: number,\r\n fn: () => Promise<T>,\r\n retries = 0\r\n): Promise<T> {\r\n if (rt.redis) {\r\n return await withLock(rt.redis, key, ttlMs, fn, retries);\r\n }\r\n return await memoryWithLock(key, fn, retries);\r\n}\r\n\r\n// ============================================================\r\n// 路由注册\r\n// ============================================================\r\n\r\nexport async function registerMultipartRoutes(\r\n app: FastifyInstance,\r\n rt: SecureUploadRuntime\r\n): Promise<void> {\r\n if (!rt.config.enabled) {\r\n app.log.warn('⚠️ multipart 上传已禁用 (enabled=false)');\r\n return;\r\n }\r\n\r\n const base = rt.config.multipartPath;\r\n app.log.info(\r\n {\r\n routes: [`${base}/init`, `${base}/part`, `${base}/complete`, `${base}/abort`, `${base}/status`],\r\n partMin: rt.config.partMin,\r\n partMax: rt.config.partMax,\r\n partDefault: rt.config.partDefault,\r\n fileMax: rt.config.fileMaxSize,\r\n ttl: rt.config.ttlSeconds,\r\n },\r\n '🔧 multipart-routes: 注册分片路由...'\r\n );\r\n\r\n // ============================================================\r\n // 1. POST /init\r\n // ============================================================\r\n app.post(`${base}/init`, async (req: FastifyRequest, reply: FastifyReply): Promise<InitResponse> => {\r\n const body = InitBodySchema.parse(req.body ?? {});\r\n\r\n // 1.1 鉴权\r\n const { userId, sessionKey } = await resolveSession(req, body.isPublic, rt.validateSession);\r\n\r\n // 1.2 限流\r\n const rlKey = `rl:mp:init:${userId ?? sessionKey.slice(0, 32)}`;\r\n const rl = await rateLimit(rt, rlKey, 60_000, rt.config.initRateLimit);\r\n if (!rl.ok) {\r\n throw new SecureError(\r\n ErrorCode.RATE_LIMITED,\r\n `init 过于频繁,请 ${Math.ceil(rl.retryAfterMs / 1000)}s 后重试`\r\n );\r\n }\r\n\r\n // 1.3 校验 totalSize\r\n if (body.totalSize > rt.config.fileMaxSize) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_TOTAL_TOO_LARGE,\r\n `文件总大小 ${body.totalSize} 超过单文件限制 ${rt.config.fileMaxSize}`\r\n );\r\n }\r\n\r\n // 1.4 计算 partSize / totalParts\r\n let partSize = body.partSize ?? rt.config.partDefault;\r\n if (partSize < rt.config.partMin) partSize = rt.config.partMin;\r\n if (partSize > rt.config.partMax) partSize = rt.config.partMax;\r\n const totalParts = body.totalSize === 0 ? 1 : Math.ceil(body.totalSize / partSize);\r\n\r\n // 1.5 ★ 秒传:如果客户端传了 expectedHash,先去 File 表查\r\n // 注意:必须严格按 (hash, isPublic, userId) 匹配,避免把\"另一种可见性/其他人\"的文件塞给当前上传者\r\n if (body.expectedHash) {\r\n const existing = await rt.fileRepo.findDedupCandidate(\r\n body.expectedHash,\r\n body.isPublic,\r\n userId\r\n );\r\n if (existing) {\r\n const diskOk = await rt.storage.exists(existing.storageKey).catch(() => false);\r\n if (diskOk) {\r\n req.log.info(\r\n { hash: body.expectedHash, fileId: existing.id, isPublic: body.isPublic, userId },\r\n '⚡ 秒传命中(严格匹配可见性+身份)'\r\n );\r\n return {\r\n success: true,\r\n data: {\r\n uploadId: '',\r\n partSize: 0,\r\n totalParts: 0,\r\n totalSize: Number(existing.size),\r\n expiresAt: '',\r\n ttl: 0,\r\n suggestedConcurrency: 0,\r\n dedup: {\r\n fileId: existing.id,\r\n filename: existing.filename,\r\n size: Number(existing.size),\r\n contentType: existing.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${existing.id}`,\r\n },\r\n },\r\n };\r\n }\r\n // 磁盘丢失 → 软删孤儿\r\n req.log.warn(\r\n { hash: body.expectedHash, fileId: existing.id, storageKey: existing.storageKey, userId },\r\n '⚠️ DB 命中但磁盘已丢失,放弃秒传走正常分片'\r\n );\r\n await rt.fileRepo\r\n .softDelete(existing.id, { orphanedAt: new Date().toISOString() })\r\n .catch((e: unknown) => {\r\n req.log.warn({ err: e, fileId: existing.id }, '⚠️ 软删除孤儿 record 失败');\r\n });\r\n }\r\n }\r\n\r\n // 1.6 创建 multipart record\r\n const rec = await rt.multipartRepo.createInit({\r\n userId,\r\n filename: body.filename,\r\n contentType: body.contentType,\r\n totalSize: body.totalSize,\r\n partSize,\r\n totalParts,\r\n isPublic: body.isPublic,\r\n expectedHash: body.expectedHash ?? null,\r\n meta: body.meta ?? null,\r\n ttlSeconds: rt.config.ttlSeconds,\r\n });\r\n\r\n req.log.info(\r\n { uploadId: rec.id, userId, totalSize: body.totalSize, partSize, totalParts, isPublic: body.isPublic },\r\n '✅ multipart: init'\r\n );\r\n\r\n return {\r\n success: true,\r\n data: {\r\n uploadId: rec.id,\r\n partSize,\r\n totalParts,\r\n totalSize: body.totalSize,\r\n expiresAt: rec.expiresAt.toISOString(),\r\n ttl: rt.config.ttlSeconds,\r\n suggestedConcurrency: rt.config.suggestedConcurrency,\r\n },\r\n };\r\n });\r\n\r\n // ============================================================\r\n // 2. POST /part (binary 协议)\r\n // ============================================================\r\n app.post(`${base}/part`, async (req: FastifyRequest, reply: FastifyReply): Promise<PartResponse> => {\r\n // 2.1 读 part 数据(从 secureRequest.raw 或 header)\r\n const { data } = resolvePartData(req);\r\n\r\n if (data.length === 0) {\r\n throw new SecureError(ErrorCode.BINARY_PAYLOAD_EMPTY, 'part 数据为空');\r\n }\r\n if (data.length > rt.config.partMax) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_PART_TOO_LARGE,\r\n `part 大小 ${data.length} 超过 ${rt.config.partMax}`\r\n );\r\n }\r\n\r\n // 2.2 业务字段\r\n const { uploadId, partNumber, partHash } = resolvePartBiz(req);\r\n\r\n // 2.3 鉴权(私有 multipart 也要登录)\r\n const sessionKey = resolveSessionKey(req);\r\n\r\n // 2.4 限流\r\n const rlKey = `rl:mp:part:${sessionKey.slice(0, 64)}`;\r\n const rl = await rateLimit(rt, rlKey, 60_000, rt.config.partRateLimit);\r\n if (!rl.ok) {\r\n throw new SecureError(\r\n ErrorCode.RATE_LIMITED,\r\n `part 上传过于频繁,请 ${Math.ceil(rl.retryAfterMs / 1000)}s 后重试`\r\n );\r\n }\r\n\r\n // 2.5 查询 record\r\n const rec = await rt.multipartRepo.findById(uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在: ${uploadId}`);\r\n }\r\n\r\n // 2.6 归属校验\r\n if (!rec.isPublic) {\r\n const userId = await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n if (userId === null) {\r\n throw new SecureError(ErrorCode.NOT_FOUND, 'uploadId 不存在');\r\n }\r\n }\r\n\r\n // 2.7 状态校验\r\n if (rec.status === MultipartStatus.COMPLETED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, 'uploadId 已完成,不能继续 part');\r\n }\r\n if (rec.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止,不能继续 part');\r\n }\r\n if (rec.status === MultipartStatus.EXPIRED || rec.expiresAt.getTime() < Date.now()) {\r\n throw new SecureError(ErrorCode.MULTIPART_EXPIRED, 'uploadId 已过期');\r\n }\r\n\r\n // 2.8 partNumber 越界\r\n if (partNumber < 1 || partNumber > rec.totalParts) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_INVALID_PART_NUMBER,\r\n `partNumber=${partNumber} 越界 [1, ${rec.totalParts}]`\r\n );\r\n }\r\n\r\n // 2.9 算 hash\r\n const actualHash = crypto.createHash('sha256').update(data).digest('hex');\r\n if (partHash && partHash.toLowerCase() !== actualHash) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_PART_HASH_MISMATCH,\r\n `part ${partNumber} 声明 hash ${partHash} 与实际 ${actualHash} 不一致`\r\n );\r\n }\r\n\r\n // 写盘(并发,fast)\r\n await rt.storage.putPart(uploadId, partNumber, data);\r\n\r\n // 元数据更新串行化\r\n const writeRes = await lock(\r\n rt,\r\n `lock:mp:${uploadId}:meta`,\r\n 10_000,\r\n async () => {\r\n const cur = await rt.multipartRepo.findById(uploadId);\r\n if (!cur) throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, 'uploadId 不存在');\r\n if (cur.status === MultipartStatus.COMPLETED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, 'uploadId 已完成');\r\n }\r\n if (cur.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止');\r\n }\r\n const r = await rt.multipartRepo.addPart(uploadId, partNumber, actualHash);\r\n if (r.updated) return { ok: true, record: r.record };\r\n if (r.existingHash && r.existingHash === actualHash) {\r\n return { ok: true, record: r.record, idempotent: true };\r\n }\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_DUPLICATE_PART,\r\n `part ${partNumber} 已存在(原 hash=${r.existingHash ?? 'null'},新 hash=${actualHash})`\r\n );\r\n },\r\n 5\r\n );\r\n\r\n req.log.info(\r\n {\r\n uploadId, partNumber, size: data.length,\r\n receivedCount: writeRes.record.receivedParts.length,\r\n idempotent: (writeRes as any).idempotent === true,\r\n },\r\n '✅ multipart: part 写入'\r\n );\r\n\r\n return {\r\n success: true,\r\n data: {\r\n uploadId,\r\n partNumber,\r\n size: data.length,\r\n receivedCount: writeRes.record.receivedParts.length,\r\n totalParts: rec.totalParts,\r\n },\r\n };\r\n });\r\n\r\n // ============================================================\r\n // 3. POST /complete\r\n // ============================================================\r\n app.post(`${base}/complete`, async (req: FastifyRequest, reply: FastifyReply): Promise<CompleteResponse> => {\r\n const body = CompleteBodySchema.parse(req.body ?? {});\r\n\r\n // 3.1 查 record\r\n const rec = await rt.multipartRepo.findById(body.uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在`);\r\n }\r\n\r\n // 3.2 归属校验\r\n if (!rec.isPublic) {\r\n await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n }\r\n\r\n // 3.3 状态校验\r\n if (rec.status === MultipartStatus.COMPLETED) {\r\n if (rec.fileId) {\r\n const file = await rt.fileRepo.findById(rec.fileId);\r\n if (file) {\r\n return {\r\n success: true,\r\n data: {\r\n fileId: file.id,\r\n filename: file.filename,\r\n size: Number(file.size),\r\n contentType: file.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${file.id}`,\r\n finalHash: file.contentHash ?? '',\r\n idempotent: true,\r\n },\r\n };\r\n }\r\n }\r\n }\r\n if (rec.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止');\r\n }\r\n if (rec.status === MultipartStatus.EXPIRED || rec.expiresAt.getTime() < Date.now()) {\r\n throw new SecureError(ErrorCode.MULTIPART_EXPIRED, 'uploadId 已过期');\r\n }\r\n\r\n // 3.4 校验 receivedParts 是否齐全\r\n const expected = Array.from({ length: rec.totalParts }, (_, i) => i + 1);\r\n const missing = expected.filter((n) => !rec.receivedParts.includes(n));\r\n if (missing.length > 0) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_INCOMPLETE_PARTS,\r\n `缺分片: [${missing.slice(0, 10).join(',')}${missing.length > 10 ? `...(${missing.length}片)` : ''}]`\r\n );\r\n }\r\n\r\n // 3.5 客户端声明的 parts hash\r\n if (body.parts && body.parts.length > 0) {\r\n const expectedHashes: Record<number, string> = {};\r\n for (const p of body.parts) {\r\n if (p.hash) expectedHashes[p.partNumber] = p.hash.toLowerCase();\r\n }\r\n for (const [pnStr, h] of Object.entries(rec.partHashes)) {\r\n const pn = Number(pnStr);\r\n if (expectedHashes[pn] && expectedHashes[pn] !== h.toLowerCase()) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_PART_HASH_MISMATCH,\r\n `part ${pn} 服务端 hash ${h} 与客户端声明 ${expectedHashes[pn]} 不一致`\r\n );\r\n }\r\n }\r\n }\r\n\r\n // 3.6 合并(分布式锁)\r\n const fileId = `f_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;\r\n const result = await lock(\r\n rt,\r\n `lock:mp:${body.uploadId}`,\r\n 60_000,\r\n async () => {\r\n const cur = await rt.multipartRepo.findById(body.uploadId);\r\n if (!cur) throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, 'uploadId 不存在');\r\n if (cur.status === MultipartStatus.COMPLETED && cur.fileId) {\r\n return { idempotent: true, fileId: cur.fileId };\r\n }\r\n if (cur.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止');\r\n }\r\n\r\n const merged = await rt.storage.mergeParts(\r\n body.uploadId,\r\n rec.totalParts,\r\n { id: fileId, filename: rec.filename, contentType: rec.contentType }\r\n );\r\n\r\n const finalHash = await rt.storage.computeMergedHash(body.uploadId, rec.totalParts);\r\n\r\n if (body.finalHash && body.finalHash.toLowerCase() !== finalHash) {\r\n await rt.storage.delete(merged.key);\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_FINAL_HASH_MISMATCH,\r\n `最终 hash 不一致(声明=${body.finalHash},实际=${finalHash})`\r\n );\r\n }\r\n\r\n const file = await rt.fileRepo.create({\r\n id: fileId,\r\n userId: rec.userId,\r\n filename: rec.filename,\r\n contentType: rec.contentType,\r\n size: merged.size,\r\n storageKey: merged.key,\r\n isPublic: rec.isPublic,\r\n uploadId: rec.id,\r\n totalParts: rec.totalParts,\r\n contentHash: finalHash,\r\n meta: rec.meta ?? undefined,\r\n });\r\n\r\n await rt.multipartRepo.markCompleted(rec.id, fileId);\r\n\r\n return { idempotent: false, fileId: file.id, size: merged.size, finalHash };\r\n }\r\n );\r\n\r\n // 3.7 清理临时分片\r\n if (!(result as any).idempotent) {\r\n rt.storage.cleanupMultipart(body.uploadId).catch((e) => {\r\n req.log.warn({ err: e, uploadId: body.uploadId }, '⚠️ 清理分片目录失败');\r\n });\r\n }\r\n\r\n req.log.info(\r\n {\r\n uploadId: body.uploadId, fileId: (result as any).fileId,\r\n size: (result as any).size, userId: rec.userId,\r\n },\r\n '✅ multipart: complete'\r\n );\r\n\r\n const file = await rt.fileRepo.findById((result as any).fileId);\r\n if (!file) {\r\n throw new SecureError(ErrorCode.INTERNAL_ERROR, 'file record lost');\r\n }\r\n\r\n return {\r\n success: true,\r\n data: {\r\n fileId: file.id,\r\n filename: file.filename,\r\n size: Number(file.size),\r\n contentType: file.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${file.id}`,\r\n finalHash: (result as any).finalHash,\r\n idempotent: (result as any).idempotent === true,\r\n },\r\n };\r\n });\r\n\r\n // ============================================================\r\n // 4. POST /abort\r\n // ============================================================\r\n app.post(`${base}/abort`, async (req: FastifyRequest, reply: FastifyReply): Promise<AbortResponse> => {\r\n const body = AbortBodySchema.parse(req.body ?? {});\r\n\r\n const rec = await rt.multipartRepo.findById(body.uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在`);\r\n }\r\n\r\n if (!rec.isPublic) {\r\n await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n }\r\n\r\n if (rec.status === MultipartStatus.COMPLETED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, '已完成的 uploadId 不能 abort');\r\n }\r\n if (rec.status === MultipartStatus.ABORTED) {\r\n return { success: true, data: { uploadId: rec.id, status: 'ABORTED', idempotent: true } };\r\n }\r\n\r\n try {\r\n await rt.multipartRepo.markAborted(body.uploadId);\r\n } catch (e: any) {\r\n if (e.message === 'ALREADY_COMPLETED') {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, '已完成的 uploadId 不能 abort');\r\n }\r\n if (e.message === 'STATUS_INVALID') {\r\n throw new SecureError(ErrorCode.MULTIPART_STATUS_INVALID, '状态机非法转移');\r\n }\r\n throw e;\r\n }\r\n\r\n rt.storage.cleanupMultipart(body.uploadId).catch((e) => {\r\n req.log.warn({ err: e, uploadId: body.uploadId }, '⚠️ abort 后清理失败');\r\n });\r\n\r\n req.log.info({ uploadId: body.uploadId, userId: rec.userId }, '🛑 multipart: abort');\r\n\r\n return { success: true, data: { uploadId: rec.id, status: 'ABORTED' } };\r\n });\r\n\r\n // ============================================================\r\n // 5. POST /status\r\n // ============================================================\r\n app.post(`${base}/status`, async (req: FastifyRequest, reply: FastifyReply): Promise<StatusResponse> => {\r\n const body = StatusBodySchema.parse(req.body ?? {});\r\n\r\n const rec = await rt.multipartRepo.findById(body.uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在`);\r\n }\r\n\r\n if (!rec.isPublic) {\r\n await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n }\r\n\r\n // 列出磁盘上的 part\r\n const diskParts = await rt.storage.listParts(rec.id);\r\n const diskSet = new Set(diskParts);\r\n const dbSet = new Set(rec.receivedParts);\r\n const mergedReceived = Array.from(new Set([...diskSet, ...dbSet])).sort((a, b) => a - b);\r\n\r\n return {\r\n success: true,\r\n data: {\r\n uploadId: rec.id,\r\n status: rec.status,\r\n filename: rec.filename,\r\n contentType: rec.contentType,\r\n totalSize: Number(rec.totalSize),\r\n partSize: rec.partSize,\r\n totalParts: rec.totalParts,\r\n receivedParts: mergedReceived,\r\n receivedCount: mergedReceived.length,\r\n isPublic: rec.isPublic,\r\n expiresAt: rec.expiresAt.toISOString(),\r\n createdAt: rec.createdAt.toISOString(),\r\n completedAt: rec.completedAt?.toISOString() ?? null,\r\n fileId: rec.fileId,\r\n },\r\n };\r\n });\r\n}\r\n\r\n/** 内部:对 multipart 记录做归属校验 */\r\nasync function resolveUserIdForRecord(\r\n req: FastifyRequest,\r\n expectedUserId: number | null,\r\n isPublic: boolean,\r\n rt: SecureUploadRuntime\r\n): Promise<number | null> {\r\n const sr: any = (req as any).secureRequest;\r\n const session = sr?.session ?? (req.headers['x-secure-session'] as string | undefined);\r\n const userId = await rt.validateSession(session, isPublic);\r\n if (!isPublic && userId !== expectedUserId) {\r\n throw new SecureError(ErrorCode.NOT_FOUND, 'uploadId 不存在');\r\n }\r\n return userId;\r\n}\r\n","/**\r\n * 共享路由辅助\r\n * - resolveUserId / resolveSessionKey\r\n * - raw 模式 vs secure-crypto 模式差异\r\n */\r\n\r\nimport type { FastifyRequest } from 'fastify';\r\nimport { SecureError, ErrorCode } from '../errors';\r\n\r\ntype ValidateFn = (\r\n token: string | undefined,\r\n isPublic: boolean\r\n) => number | null | Promise<number | null>;\r\n\r\nexport interface SessionResolution {\r\n userId: number | null;\r\n sessionKey: string;\r\n}\r\n\r\n/**\r\n * 从 req 上拿 session / userId\r\n * - secure-crypto 模式: 优先 req.secureRequest.session(加密 SDK 解析后的字段)\r\n * - raw 模式: 从 x-secure-session header 读\r\n */\r\nexport async function resolveSession(\r\n req: FastifyRequest,\r\n isPublic: boolean,\r\n validate: ValidateFn\r\n): Promise<SessionResolution> {\r\n const sr: any = (req as any).secureRequest;\r\n const session = sr?.session ?? (req.headers['x-secure-session'] as string | undefined);\r\n const userId = await validate(session, isPublic);\r\n const sessionKey = session ?? 'anonymous';\r\n return { userId: userId ?? null, sessionKey };\r\n}\r\n\r\n/** 单独拿 session 字符串(给限流 key 用) */\r\nexport function resolveSessionKey(req: FastifyRequest): string {\r\n const sr: any = (req as any).secureRequest;\r\n return sr?.session ?? (req.headers['x-secure-session'] as string | undefined) ?? 'anonymous';\r\n}\r\n\r\n/** 单独从 session 拿 userId(用于多文件批量场景,需要 userId 拼限流 key) */\r\nexport async function resolveUserId(\r\n req: FastifyRequest,\r\n isPublic: boolean,\r\n validate: ValidateFn\r\n): Promise<number | null> {\r\n const sr: any = (req as any).secureRequest;\r\n const session = sr?.session ?? (req.headers['x-secure-session'] as string | undefined);\r\n return await validate(session, isPublic);\r\n}\r\n\r\n/**\r\n * 从 req 上读 part 数据(secure-crypto 模式从 secureRequest.raw 拿,raw 模式从 header 拿)\r\n * - secure-crypto: x-secure-raw-base64(网关/gateway 解析 binary envelope 后透传)\r\n * - raw: 直接从 body 读\r\n */\r\nexport function resolvePartData(req: FastifyRequest): { data: Buffer; declaredSize: number } {\r\n const sr: any = (req as any).secureRequest;\r\n if (sr?.raw?.data) {\r\n return { data: sr.raw.data as Buffer, declaredSize: Number(sr.raw.size ?? (sr.raw.data as Buffer).length) };\r\n }\r\n // raw 模式:从 x-secure-raw-base64 header 读\r\n const rawBase64 = req.headers['x-secure-raw-base64'] as string | undefined;\r\n const declaredSize = Number(req.headers['x-secure-content-length'] || 0);\r\n if (!rawBase64) {\r\n throw new SecureError(\r\n ErrorCode.INVALID_REQUEST,\r\n '缺少 part 数据(secure-crypto 模式读 secureRequest.raw,raw 模式读 x-secure-raw-base64 header)'\r\n );\r\n }\r\n return { data: Buffer.from(rawBase64, 'base64'), declaredSize };\r\n}\r\n\r\n/**\r\n * 从 req 上读 part 业务字段(uploadId / partNumber / partHash)\r\n * - secure-crypto 模式: 优先 sr.biz\r\n * - raw 模式: req.body\r\n */\r\nexport function resolvePartBiz(req: FastifyRequest): {\r\n uploadId: string;\r\n partNumber: number;\r\n partHash: string | null;\r\n} {\r\n const sr: any = (req as any).secureRequest;\r\n const biz = sr?.biz ?? (req.body as any) ?? {};\r\n const uploadId = String(biz.uploadId ?? '').trim();\r\n const partNumberRaw = biz.partNumber;\r\n const partHash: string | null = biz.partHash ?? null;\r\n if (!uploadId) {\r\n throw new SecureError(ErrorCode.INVALID_REQUEST, '缺少 uploadId');\r\n }\r\n const partNumber = Number(partNumberRaw);\r\n if (!Number.isInteger(partNumber) || partNumber < 1) {\r\n throw new SecureError(ErrorCode.MULTIPART_INVALID_PART_NUMBER, `partNumber 非法: ${partNumberRaw}`);\r\n }\r\n return { uploadId, partNumber, partHash };\r\n}\r\n","/**\r\n * Redis 辅助工具(可选注入,分布式场景使用)\r\n *\r\n * 提供:\r\n * - slidingWindowRateLimit: 滑动窗口限流(Lua + ZSET,精确无突刺)\r\n * - acquireLock / releaseLock / withLock: 分布式锁\r\n *\r\n * ioredis 客户端作为 SDK peerDependency(可选),不传时 SDK 用内存降级\r\n */\r\n\r\nimport type Redis from 'ioredis';\r\n\r\nconst LUA_SLIDING_WINDOW_RATE_LIMIT = `\r\n-- KEYS[1] = rate limit key\r\n-- ARGV[1] = window (ms)\r\n-- ARGV[2] = limit (max requests in window)\r\n-- ARGV[3] = now (ms)\r\n-- ARGV[4] = member (unique per request)\r\nlocal key = KEYS[1]\r\nlocal window = tonumber(ARGV[1])\r\nlocal limit = tonumber(ARGV[2])\r\nlocal now = tonumber(ARGV[3])\r\nlocal member = ARGV[4]\r\nredis.call('ZREMRANGEBYSCORE', key, 0, now - window)\r\nlocal count = redis.call('ZCARD', key)\r\nif count >= limit then\r\n local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\r\n local retryAfter = window\r\n if #oldest >= 2 then\r\n retryAfter = (tonumber(oldest[2]) + window) - now\r\n if retryAfter < 0 then retryAfter = 0 end\r\n end\r\n return { 0, retryAfter }\r\nend\r\nredis.call('ZADD', key, now, member)\r\nredis.call('PEXPIRE', key, window)\r\nreturn { 1, 0 }\r\n`;\r\n\r\nconst LUA_RELEASE_LOCK = `\r\nif redis.call('GET', KEYS[1]) == ARGV[1] then\r\n return redis.call('DEL', KEYS[1])\r\nelse\r\n return 0\r\nend\r\n`;\r\n\r\nconst LUA_ACQUIRE_LOCK = `\r\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', tonumber(ARGV[2])) then\r\n return 1\r\nelse\r\n return 0\r\nend\r\n`;\r\n\r\nexport async function slidingWindowRateLimit(\r\n redis: Redis,\r\n key: string,\r\n windowMs: number,\r\n limit: number\r\n): Promise<{ ok: boolean; retryAfterMs: number; remaining: number }> {\r\n const now = Date.now();\r\n const member = `${now}-${Math.random().toString(36).slice(2, 10)}`;\r\n const result = (await redis.eval(\r\n LUA_SLIDING_WINDOW_RATE_LIMIT,\r\n 1,\r\n key,\r\n String(windowMs),\r\n String(limit),\r\n String(now),\r\n member\r\n )) as [number, number];\r\n\r\n const ok = result[0] === 1;\r\n return {\r\n ok,\r\n retryAfterMs: ok ? 0 : Number(result[1] ?? 0),\r\n remaining: ok ? limit - 1 : 0,\r\n };\r\n}\r\n\r\nexport async function acquireLock(\r\n redis: Redis,\r\n key: string,\r\n ttlMs = 5_000\r\n): Promise<{ ok: true; token: string } | { ok: false; token: null }> {\r\n const token = `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\r\n const ok = await redis.eval(LUA_ACQUIRE_LOCK, 1, key, token, String(ttlMs));\r\n if (ok === 1) return { ok: true, token };\r\n return { ok: false, token: null };\r\n}\r\n\r\nexport async function releaseLock(redis: Redis, key: string, token: string): Promise<boolean> {\r\n const r = await redis.eval(LUA_RELEASE_LOCK, 1, key, token);\r\n return r === 1;\r\n}\r\n\r\nexport async function withLock<T>(\r\n redis: Redis,\r\n key: string,\r\n ttlMs: number,\r\n fn: () => Promise<T>,\r\n retries = 0\r\n): Promise<T> {\r\n let lastErr: unknown;\r\n for (let i = 0; i <= retries; i++) {\r\n const lock = await acquireLock(redis, key, ttlMs);\r\n if (lock.ok) {\r\n try {\r\n return await fn();\r\n } finally {\r\n await releaseLock(redis, key, lock.token).catch(() => {\r\n // 释放失败不影响主流程\r\n });\r\n }\r\n }\r\n lastErr = new Error(`[redis-helpers] acquireLock failed: ${key}`);\r\n if (i < retries) {\r\n const wait = 50 * Math.pow(2, i) + Math.floor(Math.random() * 20);\r\n await new Promise((r) => setTimeout(r, wait));\r\n }\r\n }\r\n const e: any = lastErr instanceof Error ? lastErr : new Error('[redis-helpers] withLock failed');\r\n e.code = 'LOCK_BUSY';\r\n throw e;\r\n}\r\n","/**\r\n * 内存版限流 + 锁(单进程降级)\r\n *\r\n * 当 SDK 未注入 redis 时,使用本实现:\r\n * - 限流: 进程内 Map<key, timestamps[]>(滑动窗口)\r\n * - 锁: 进程内 Map<key, boolean>(互斥)\r\n *\r\n * 注意: 内存版只在单进程内有效,多实例部署请传 redis。\r\n */\r\n\r\ninterface MemLimitEntry {\r\n timestamps: number[];\r\n}\r\n\r\nconst _limitStore = new Map<string, MemLimitEntry>();\r\nconst _lockStore = new Map<string, boolean>();\r\n\r\n/** 清理过期条目,避免 Map 无限增长 */\r\nfunction gcLimit(now: number, windowMs: number) {\r\n if (_limitStore.size > 5000) {\r\n for (const [k, v] of _limitStore.entries()) {\r\n const filtered = v.timestamps.filter((t) => t > now - windowMs);\r\n if (filtered.length === 0) _limitStore.delete(k);\r\n else _limitStore.set(k, { timestamps: filtered });\r\n }\r\n }\r\n}\r\n\r\nexport function memorySlidingWindowRateLimit(\r\n key: string,\r\n windowMs: number,\r\n limit: number\r\n): { ok: boolean; retryAfterMs: number; remaining: number } {\r\n const now = Date.now();\r\n gcLimit(now, windowMs);\r\n\r\n const entry = _limitStore.get(key) ?? { timestamps: [] };\r\n // 移除过期\r\n entry.timestamps = entry.timestamps.filter((t) => t > now - windowMs);\r\n\r\n if (entry.timestamps.length >= limit) {\r\n const oldest = entry.timestamps[0];\r\n const retryAfter = Math.max(0, oldest + windowMs - now);\r\n return { ok: false, retryAfterMs: retryAfter, remaining: 0 };\r\n }\r\n\r\n entry.timestamps.push(now);\r\n _limitStore.set(key, entry);\r\n return { ok: true, retryAfterMs: 0, remaining: limit - entry.timestamps.length };\r\n}\r\n\r\nexport function memoryAcquireLock(key: string): boolean {\r\n if (_lockStore.get(key)) return false;\r\n _lockStore.set(key, true);\r\n return true;\r\n}\r\n\r\nexport function memoryReleaseLock(key: string): void {\r\n _lockStore.delete(key);\r\n}\r\n\r\nexport async function memoryWithLock<T>(\r\n key: string,\r\n fn: () => Promise<T>,\r\n retries = 0\r\n): Promise<T> {\r\n for (let i = 0; i <= retries; i++) {\r\n if (memoryAcquireLock(key)) {\r\n try {\r\n return await fn();\r\n } finally {\r\n memoryReleaseLock(key);\r\n }\r\n }\r\n if (i < retries) {\r\n const wait = 50 * Math.pow(2, i) + Math.floor(Math.random() * 20);\r\n await new Promise((r) => setTimeout(r, wait));\r\n }\r\n }\r\n const e: any = new Error(`[memory-lock] LOCK_BUSY: ${key}`);\r\n e.code = 'LOCK_BUSY';\r\n throw e;\r\n}\r\n\r\n/** 内部:统一抽象(让路由层不感知 redis / 内存) */\r\nexport interface LockLike {\r\n withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>, retries?: number): Promise<T>;\r\n}\r\n\r\nexport interface RateLimitLike {\r\n rateLimit(key: string, windowMs: number, limit: number): Promise<{ ok: boolean; retryAfterMs: number; remaining: number }>;\r\n}\r\n","/**\r\n * Smart 统一上传路由\r\n *\r\n * 端点(相对 pathPrefix.smart,默认 '/smart'):\r\n * - POST /init\r\n * 批量:一次接收 N 个文件 → 智能分流(ONE_SHOT / MULTIPART)\r\n * 单个文件失败不阻塞其他\r\n *\r\n * 后续 part/complete/abort/status 复用 multipart/*(SDK SmartClient 内部自动调)\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport type { FastifyInstance, FastifyRequest } from 'fastify';\r\nimport { SecureError, ErrorCode } from '../errors';\r\nimport { resolveSession, resolveUserId } from './_internal';\r\nimport { chooseStrategy, suggestConcurrency } from '../utils/strategy';\r\nimport { slidingWindowRateLimit } from '../utils/redis-helpers';\r\nimport { memorySlidingWindowRateLimit } from '../utils/memory-rate-limit';\r\nimport type { SecureUploadRuntime } from '../types';\r\nimport { MultipartStatus, FileStatus } from '../utils/repository/types';\r\n\r\n// ============================================================\r\n// Schemas\r\n// ============================================================\r\n\r\nconst FileInitItemSchema = z.object({\r\n filename: z.string().min(1).max(255),\r\n contentType: z.string().min(1).max(128).default('application/octet-stream'),\r\n size: z.coerce.number().int().nonnegative(),\r\n isPublic: z.coerce.boolean().default(false),\r\n expectedHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n meta: z.record(z.unknown()).optional(),\r\n}).strict();\r\n\r\n// ✅ 改后:工厂函数,接收 maxFiles 参数\r\nfunction makeSmartInitBodySchema(maxFiles: number) {\r\n return z.object({\r\n files: z.array(FileInitItemSchema).min(1).max(maxFiles),\r\n }).strict();\r\n}\r\n\r\n// ============================================================\r\n// 内部类型\r\n// ============================================================\r\n\r\ntype SmartInitItemResult =\r\n | { ok: false; index: number; filename: string; code: number; error: string; message: string }\r\n | {\r\n ok: true; index: number; filename: string; size: number;\r\n dedup: true;\r\n fileId: string; contentType: string; url: string;\r\n }\r\n | {\r\n ok: true; index: number; filename: string; size: number;\r\n dedup: false;\r\n uploadId: string; contentType: string; isPublic: boolean;\r\n partSize: number; totalParts: number; totalSize: number;\r\n strategy: 'ONE_SHOT' | 'MULTIPART';\r\n suggestedConcurrency: number;\r\n expiresAt: string; ttl: number;\r\n };\r\n\r\n// ============================================================\r\n// 单文件处理\r\n// ============================================================\r\n\r\nasync function processOneFile(\r\n req: FastifyRequest,\r\n rt: SecureUploadRuntime,\r\n file: z.infer<typeof FileInitItemSchema>,\r\n index: number\r\n): Promise<SmartInitItemResult> {\r\n try {\r\n // 1. 鉴权\r\n const { userId } = await resolveSession(req, file.isPublic, rt.validateSession);\r\n\r\n // 2. 校验 size\r\n if (file.size > rt.config.fileMaxSize) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_TOTAL_TOO_LARGE,\r\n `文件 ${file.filename} 大小 ${file.size} 超过单文件限制 ${rt.config.fileMaxSize}`\r\n );\r\n }\r\n\r\n // 3. 秒传\r\n // 注意:必须严格按 (hash, isPublic, userId) 匹配,避免把\"另一种可见性/其他人\"的文件塞给当前上传者\r\n if (file.expectedHash) {\r\n const existing = await rt.fileRepo.findDedupCandidate(\r\n file.expectedHash,\r\n file.isPublic,\r\n userId\r\n );\r\n if (existing) {\r\n const diskOk = await rt.storage.exists(existing.storageKey).catch(() => false);\r\n if (diskOk) {\r\n req.log.info(\r\n { hash: file.expectedHash, fileId: existing.id, isPublic: file.isPublic, userId, index },\r\n '⚡ smart: 秒传命中(严格匹配可见性+身份)'\r\n );\r\n return {\r\n ok: true, index, filename: file.filename, size: Number(existing.size),\r\n dedup: true,\r\n fileId: existing.id,\r\n contentType: existing.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${existing.id}`,\r\n };\r\n }\r\n // 磁盘丢失\r\n req.log.warn(\r\n { hash: file.expectedHash, fileId: existing.id, userId, index },\r\n '⚠️ smart: DB 命中但磁盘丢失,放弃秒传'\r\n );\r\n await rt.fileRepo\r\n .softDelete(existing.id, { orphanedAt: new Date().toISOString() })\r\n .catch((e: unknown) => {\r\n req.log.warn({ err: e, fileId: existing.id }, '⚠️ 软删除孤儿 record 失败');\r\n });\r\n }\r\n }\r\n\r\n // 4. 智能策略\r\n const strategy = chooseStrategy(file.size, rt.config.strategy);\r\n req.log.info(\r\n {\r\n index, filename: file.filename, size: file.size,\r\n strategy: strategy.kind, partSize: strategy.partSize, totalParts: strategy.totalParts,\r\n reason: strategy.reason,\r\n },\r\n `🧠 smart: 选策略 ${strategy.kind}(${strategy.reason})`\r\n );\r\n\r\n // 5. 创建 multipart 记录\r\n const rec = await rt.multipartRepo.createInit({\r\n userId,\r\n filename: file.filename,\r\n contentType: file.contentType,\r\n totalSize: file.size,\r\n partSize: strategy.partSize,\r\n totalParts: strategy.totalParts,\r\n isPublic: file.isPublic,\r\n expectedHash: file.expectedHash ?? null,\r\n meta: file.meta ?? null,\r\n ttlSeconds: rt.config.ttlSeconds,\r\n });\r\n\r\n return {\r\n ok: true, index, filename: file.filename, size: file.size,\r\n dedup: false,\r\n uploadId: rec.id,\r\n contentType: file.contentType,\r\n isPublic: file.isPublic,\r\n partSize: strategy.partSize,\r\n totalParts: strategy.totalParts,\r\n totalSize: file.size,\r\n strategy: strategy.kind,\r\n suggestedConcurrency: suggestConcurrency(strategy),\r\n expiresAt: rec.expiresAt.toISOString(),\r\n ttl: rt.config.ttlSeconds,\r\n };\r\n } catch (e: any) {\r\n const code = e instanceof SecureError ? e.code : (e?.code as number) ?? ErrorCode.INVALID_REQUEST;\r\n const message = e?.message ?? String(e);\r\n const errName = e instanceof SecureError ? e.name : 'INTERNAL_ERROR';\r\n return {\r\n ok: false, index, filename: file.filename,\r\n code, error: errName, message,\r\n };\r\n }\r\n}\r\n\r\n// ============================================================\r\n// 路由注册\r\n// ============================================================\r\n\r\nexport async function registerSmartRoutes(\r\n app: FastifyInstance,\r\n rt: SecureUploadRuntime\r\n): Promise<void> {\r\n if (!rt.config.enabled) {\r\n app.log.warn('⚠️ smart 上传已禁用 (enabled=false)');\r\n return;\r\n }\r\n\r\n const base = rt.config.smartPath;\r\n app.log.info(\r\n {\r\n routes: [`${base}/init`],\r\n maxFiles: rt.config.smartMaxFiles,\r\n rateLimit: rt.config.smartInitRateLimit,\r\n },\r\n '🔧 smart-routes: 注册 smart 上传路由(part/complete/abort/status 复用 multipart/*)...'\r\n );\r\n\r\n app.post(`${base}/init`, async (req: FastifyRequest) => {\r\n // ✅ 改后:从 rt.config 读,跟业务校验(line 194)同源\r\n const schema = makeSmartInitBodySchema(rt.config.smartMaxFiles);\r\n const body = schema.parse(req.body ?? {});\r\n\r\n if (body.files.length > rt.config.smartMaxFiles) {\r\n throw new SecureError(\r\n ErrorCode.BATCH_TOO_MANY_FILES,\r\n `单次 /smart/init 最多 ${rt.config.smartMaxFiles} 个文件,实际 ${body.files.length}`\r\n );\r\n }\r\n\r\n // 1.1 限流\r\n const sessionKeyForRl = (() => {\r\n const sr: any = (req as any).secureRequest;\r\n return sr?.session ?? (req.headers['x-secure-session'] as string | undefined) ?? 'anonymous';\r\n })();\r\n const allPublic = body.files.every((f) => f.isPublic);\r\n const userIdForRl = await resolveUserId(req, allPublic, rt.validateSession);\r\n const rlKey = `rl:mp:init:${userIdForRl ?? sessionKeyForRl.slice(0, 32)}`;\r\n\r\n for (let i = 0; i < body.files.length; i++) {\r\n const rl = rt.redis\r\n ? await slidingWindowRateLimit(rt.redis, rlKey, 60_000, rt.config.smartInitRateLimit)\r\n : memorySlidingWindowRateLimit(rlKey, 60_000, rt.config.smartInitRateLimit);\r\n\r\n if (!rl.ok) {\r\n req.log.warn(\r\n { remaining: 0, consumed: i, total: body.files.length, retryAfterMs: rl.retryAfterMs },\r\n '⚠️ smart: 批量配额不足,后续文件标记失败'\r\n );\r\n const results: SmartInitItemResult[] = [];\r\n for (let j = 0; j < i; j++) {\r\n const r = await processOneFile(req, rt, body.files[j], j);\r\n results.push(r);\r\n }\r\n for (let j = i; j < body.files.length; j++) {\r\n results.push({\r\n ok: false, index: j, filename: body.files[j].filename,\r\n code: ErrorCode.RATE_LIMITED,\r\n error: 'RATE_LIMITED',\r\n message: `批量配额不足,需 ${Math.ceil(rl.retryAfterMs / 1000)}s 后重试`,\r\n });\r\n }\r\n return {\r\n success: true,\r\n data: {\r\n files: results,\r\n summary: {\r\n total: body.files.length,\r\n ok: results.filter((r) => r.ok).length,\r\n failed: results.filter((r) => !r.ok).length,\r\n dedup: results.filter((r) => r.ok && (r as any).dedup === true).length,\r\n oneShot: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'ONE_SHOT').length,\r\n multipart: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'MULTIPART').length,\r\n rateLimited: body.files.length - i,\r\n },\r\n },\r\n };\r\n }\r\n }\r\n\r\n // 1.2 逐个文件处理\r\n const t0 = Date.now();\r\n const results: SmartInitItemResult[] = [];\r\n for (let i = 0; i < body.files.length; i++) {\r\n const f = body.files[i];\r\n const result = await processOneFile(req, rt, f, i);\r\n results.push(result);\r\n }\r\n\r\n // 1.3 汇总\r\n const summary = {\r\n total: results.length,\r\n ok: results.filter((r) => r.ok).length,\r\n failed: results.filter((r) => !r.ok).length,\r\n dedup: results.filter((r) => r.ok && (r as any).dedup === true).length,\r\n oneShot: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'ONE_SHOT').length,\r\n multipart: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'MULTIPART').length,\r\n };\r\n\r\n req.log.info(\r\n { total: summary.total, ok: summary.ok, dedup: summary.dedup, oneShot: summary.oneShot, multipart: summary.multipart, ms: Date.now() - t0 },\r\n '✅ smart: init 汇总'\r\n );\r\n\r\n return { success: true, data: { files: results, summary } };\r\n });\r\n}\r\n\r\n// 重新导出 MultipartStatus / FileStatus(供 SDK 内部使用)\r\nexport { MultipartStatus, FileStatus };\r\n","/**\r\n * 后台清理过期 uploadId 任务\r\n *\r\n * - 每 intervalSeconds 扫描一次\r\n * - 把超时的 INIT/UPLOADING 标记为 EXPIRED + 清理磁盘\r\n */\r\n\r\nimport type { FastifyBaseLogger } from 'fastify';\r\nimport type { StorageAdapter } from './storage/types';\r\nimport type { MultipartRepository } from './repository/types';\r\n\r\nexport interface MultipartCleanerOptions {\r\n enabled: boolean;\r\n intervalSeconds: number;\r\n batchLimit: number;\r\n storage: StorageAdapter;\r\n multipartRepo: MultipartRepository;\r\n log: FastifyBaseLogger;\r\n onClose: (cb: () => void) => void;\r\n}\r\n\r\nexport function startMultipartCleanupTask(opts: MultipartCleanerOptions): void {\r\n if (!opts.enabled) {\r\n opts.log.info('⏸️ multipart-cleanup: 已禁用');\r\n return;\r\n }\r\n const intervalMs = opts.intervalSeconds * 1000;\r\n const tick = async () => {\r\n try {\r\n const expired = await opts.multipartRepo.listExpired(opts.batchLimit);\r\n if (expired.length === 0) return;\r\n opts.log.info({ count: expired.length }, '🧹 multipart cleanup: 发现过期 uploadId');\r\n for (const rec of expired) {\r\n try {\r\n await opts.multipartRepo.markExpired(rec.id);\r\n await opts.storage.cleanupMultipart(rec.id);\r\n opts.log.info({ uploadId: rec.id }, '🧹 multipart cleanup: 已清理');\r\n } catch (e) {\r\n opts.log.warn({ err: e, uploadId: rec.id }, '⚠️ multipart cleanup 单条失败');\r\n }\r\n }\r\n } catch (e) {\r\n opts.log.error({ err: e }, '❌ multipart cleanup tick 失败');\r\n }\r\n };\r\n setImmediate(tick);\r\n const timer = setInterval(tick, intervalMs);\r\n opts.onClose(() => clearInterval(timer));\r\n opts.log.info(\r\n { intervalSec: opts.intervalSeconds },\r\n '🧹 multipart cleanup task 已启动'\r\n );\r\n}\r\n","/**\r\n * 孤儿文件扫描任务\r\n *\r\n * 背景:\r\n * 秒传路径只查 DB,可能存在\"DB 有 / 磁盘无\"的不一致:\r\n * - 用户/运维手动 rm 磁盘文件\r\n * - 容器重启后丢失 volume mount\r\n * - 分布式存储同步失败\r\n *\r\n * 已有被动防御:秒传命中 DB 后 storage.exists() 二次校验\r\n * 本任务作为\"主动巡检\"兜底:定期扫一遍,自动 soft-delete,\r\n * 防止其他业务路径(如 /files/:id GET)又返回 404 \"文件已丢失\"。\r\n *\r\n * 触发:服务冷启动(setImmediate) + setInterval\r\n * 容错:\r\n * - 单条 storage.exists() 抛错 → 跳过该条,记 warn\r\n * - 单条 fileRepo.softDelete 失败 → 跳过该条,记 warn\r\n * - 整个 tick 失败 → 记 error,不影响下次\r\n */\r\n\r\nimport type { FastifyBaseLogger } from 'fastify';\r\nimport type { StorageAdapter } from './storage/types';\r\nimport type { FileRepository } from './repository/types';\r\n\r\nexport interface OrphanScannerOptions {\r\n enabled: boolean;\r\n intervalSeconds: number;\r\n lookbackDays: number;\r\n batchSize: number;\r\n storage: StorageAdapter;\r\n fileRepo: FileRepository;\r\n log: FastifyBaseLogger;\r\n onClose: (cb: () => void) => void;\r\n}\r\n\r\nexport function startOrphanFileScanTask(opts: OrphanScannerOptions): void {\r\n if (!opts.enabled) {\r\n opts.log.info('⏸️ orphan-file-scan: 已禁用');\r\n return;\r\n }\r\n const intervalMs = opts.intervalSeconds * 1000;\r\n const batchSize = opts.batchSize;\r\n\r\n const tick = async () => {\r\n try {\r\n const recent = await opts.fileRepo.listOrphans({\r\n sinceDays: opts.lookbackDays,\r\n limit: batchSize,\r\n });\r\n if (recent.length === 0) {\r\n opts.log.debug({ lookbackDays: opts.lookbackDays }, '🧹 orphan-file-scan: 无可扫记录');\r\n return;\r\n }\r\n let orphanCount = 0;\r\n const samples: Array<{ fileId: string; storageKey: string; size: number }> = [];\r\n for (const f of recent) {\r\n try {\r\n const ok = await opts.storage.exists(f.storageKey);\r\n if (!ok) {\r\n await opts.fileRepo.softDelete(f.id, { orphanedAt: new Date().toISOString() });\r\n orphanCount++;\r\n if (samples.length < 5) {\r\n samples.push({ fileId: f.id, storageKey: f.storageKey, size: Number(f.size) });\r\n }\r\n }\r\n } catch (e) {\r\n opts.log.warn(\r\n { err: e, fileId: f.id, storageKey: f.storageKey },\r\n '⚠️ orphan-file-scan: 单条检测失败'\r\n );\r\n }\r\n }\r\n if (orphanCount > 0) {\r\n opts.log.warn(\r\n { scanned: recent.length, orphans: orphanCount, sample: samples },\r\n '🧹 orphan-file-scan: 完成本轮'\r\n );\r\n } else {\r\n opts.log.info(\r\n { scanned: recent.length, orphans: 0 },\r\n '🧹 orphan-file-scan: 完成本轮(全部一致)'\r\n );\r\n }\r\n } catch (e) {\r\n opts.log.error({ err: e }, '❌ orphan-file-scan tick 失败');\r\n }\r\n };\r\n\r\n setImmediate(tick);\r\n const timer = setInterval(tick, intervalMs);\r\n opts.onClose(() => clearInterval(timer));\r\n opts.log.info(\r\n {\r\n intervalSec: opts.intervalSeconds,\r\n lookbackDays: opts.lookbackDays,\r\n batchSize,\r\n },\r\n '🧹 orphan-file-scan task 已启动'\r\n );\r\n}\r\n"],"mappings":"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","names":["path","fs","import_node_crypto","partSize","ErrorCode","import_node_crypto","lock","r","crypto","file","import_zod","results","fp"]}
1
+ {"version":3,"sources":["../../src/server/index.ts","../../src/server/fastify-plugin.ts","../../src/server/utils/storage/local.ts","../../src/server/utils/storage/memory.ts","../../src/server/utils/repository/types.ts","../../src/server/utils/repository/prisma.ts","../../src/server/utils/repository/memory.ts","../../src/server/utils/strategy.ts","../../src/server/errors.ts","../../src/server/session.ts","../../src/server/routes/multipart.ts","../../src/server/routes/_internal.ts","../../src/server/utils/redis-helpers.ts","../../src/server/utils/memory-rate-limit.ts","../../src/server/routes/smart.ts","../../src/server/utils/cleaner.ts","../../src/server/utils/orphan-scanner.ts"],"sourcesContent":["/**\r\n * Server 公开 API\r\n *\r\n * 主入口:\r\n * import secureUploadPlugin, { type SecureUploadOptions } from 'secure-upload-fastify-sdk/server';\r\n * await app.register(secureUploadPlugin, { ... });\r\n *\r\n * 工具类:\r\n * import { LocalStorage, InMemoryStorage } from 'secure-upload-fastify-sdk/storage';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from 'secure-upload-fastify-sdk/repository';\r\n * import { ErrorCode, SecureError } from 'secure-upload-fastify-sdk/errors';\r\n * import { chooseStrategy, suggestConcurrency, DEFAULT_STRATEGY_CONFIG } from 'secure-upload-fastify-sdk/strategy';\r\n */\r\n\r\nexport {\r\n default,\r\n default as secureUploadPlugin,\r\n LocalStorage,\r\n InMemoryStorage,\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n} from './fastify-plugin';\r\n\r\nexport type {\r\n SecureUploadOptions,\r\n SecureUploadRuntime,\r\n SecureMode,\r\n RepositoryConfig,\r\n StorageConfig,\r\n PathPrefixConfig,\r\n SessionValidator,\r\n InitResponse,\r\n PartResponse,\r\n CompleteResponse,\r\n AbortResponse,\r\n StatusResponse,\r\n MultipartRecord,\r\n FileRecord,\r\n} from './types';\r\n\r\nexport {\r\n ErrorCode,\r\n SecureError,\r\n getHttpStatus,\r\n} from './errors';\r\n","/**\r\n * Fastify 主插件\r\n *\r\n * 注册 5 + 1 端点:\r\n * - {pathPrefix.multipart}/init\r\n * - {pathPrefix.multipart}/part\r\n * - {pathPrefix.multipart}/complete\r\n * - {pathPrefix.multipart}/abort\r\n * - {pathPrefix.multipart}/status\r\n * - {pathPrefix.smart}/init\r\n *\r\n * 用法(详见 README):\r\n *\r\n * import Fastify from 'fastify';\r\n * import uploadPlugin from 'secure-upload-fastify-sdk/server';\r\n * import { LocalStorage } from 'secure-upload-fastify-sdk/storage';\r\n * import { PrismaMultipartRepository, PrismaFileRepository } from 'secure-upload-fastify-sdk/repository';\r\n *\r\n * const app = Fastify({ logger: true });\r\n * await app.register(uploadPlugin, {\r\n * storage: { adapter: 'local', options: { baseDir: '/var/upload' } },\r\n * repository: { kind: 'prisma', prisma },\r\n * redis,\r\n * pathPrefix: { multipart: '/api/v1/upload/multipart', smart: '/api/v1/upload/smart' },\r\n * fileUrlPrefix: '/api/v1/upload/files',\r\n * });\r\n */\r\n\r\nimport fp from 'fastify-plugin';\r\nimport type { FastifyInstance } from 'fastify';\r\nimport { LocalStorage } from './utils/storage/local';\r\nimport { InMemoryStorage } from './utils/storage/memory';\r\nimport { PrismaMultipartRepository, PrismaFileRepository } from './utils/repository/prisma';\r\nimport { InMemoryMultipartRepository, InMemoryFileRepository } from './utils/repository/memory';\r\nimport { DEFAULT_STRATEGY_CONFIG } from './utils/strategy';\r\nimport { defaultSessionValidator } from './session';\r\nimport { registerMultipartRoutes } from './routes/multipart';\r\nimport { registerSmartRoutes } from './routes/smart';\r\nimport { startMultipartCleanupTask } from './utils/cleaner';\r\nimport { startOrphanFileScanTask } from './utils/orphan-scanner';\r\nimport { SecureError } from './errors';\r\nimport type {\r\n SecureUploadOptions,\r\n SecureUploadRuntime,\r\n RepositoryConfig,\r\n StorageConfig,\r\n} from './types';\r\nimport type { StorageAdapter } from './utils/storage/types';\r\nimport type { MultipartRepository, FileRepository } from './utils/repository/types';\r\n\r\nconst KB = 1024;\r\nconst MB = 1024 * KB;\r\nconst GB = 1024 * MB;\r\n\r\nconst DEFAULTS = {\r\n enabled: true,\r\n fileMaxSize: 5 * GB,\r\n partMin: 5 * MB,\r\n partMax: 50 * MB,\r\n partDefault: 20 * MB,\r\n ttlSeconds: 24 * 60 * 60,\r\n suggestedConcurrency: 3,\r\n initRateLimit: 30,\r\n partRateLimit: 600,\r\n smartMaxFiles: 50,\r\n smartInitRateLimit: 30,\r\n multipartPath: '/multipart',\r\n smartPath: '/smart',\r\n fileUrlPrefix: '/files',\r\n secure: 'raw' as const,\r\n};\r\n\r\nasync function resolveStorage(cfg: StorageConfig): Promise<StorageAdapter> {\r\n if (typeof cfg === 'object' && 'put' in cfg && typeof (cfg as any).put === 'function') {\r\n // 直接传 StorageAdapter 实例\r\n return cfg as StorageAdapter;\r\n }\r\n const preset = cfg as { adapter: 'local'; options: { baseDir: string } };\r\n if (preset.adapter === 'local') {\r\n return new LocalStorage(preset.options.baseDir);\r\n }\r\n throw new Error(`[secure-upload-sdk] 未知的 storage.adapter: ${(preset as any).adapter}`);\r\n}\r\n\r\nfunction resolveRepository(cfg: RepositoryConfig): {\r\n multipart: MultipartRepository;\r\n file: FileRepository;\r\n} {\r\n if (cfg.kind === 'prisma') {\r\n return {\r\n multipart: new PrismaMultipartRepository(cfg.prisma),\r\n file: new PrismaFileRepository(cfg.prisma),\r\n };\r\n }\r\n return { multipart: cfg.multipart, file: cfg.file };\r\n}\r\n\r\nasync function secureUploadPluginImpl(\r\n app: FastifyInstance,\r\n options: SecureUploadOptions\r\n): Promise<void> {\r\n // 1. 校验必填\r\n if (!options.storage) {\r\n throw new Error('[secure-upload-sdk] options.storage 必填');\r\n }\r\n if (!options.repository) {\r\n throw new Error('[secure-upload-sdk] options.repository 必填');\r\n }\r\n\r\n // 2. 解析 storage / repository\r\n const storage = await resolveStorage(options.storage);\r\n const { multipart: multipartRepo, file: fileRepo } = resolveRepository(options.repository);\r\n\r\n // 3. 构造运行时上下文\r\n const runtime: SecureUploadRuntime = {\r\n storage,\r\n multipartRepo,\r\n fileRepo,\r\n redis: options.redis ?? null,\r\n config: {\r\n enabled: options.enabled ?? DEFAULTS.enabled,\r\n secure: options.secure?.mode ?? DEFAULTS.secure,\r\n fileMaxSize: options.fileMaxSize ?? DEFAULTS.fileMaxSize,\r\n partMin: options.partSize?.min ?? DEFAULTS.partMin,\r\n partMax: options.partSize?.max ?? DEFAULTS.partMax,\r\n partDefault: options.partSize?.default ?? DEFAULTS.partDefault,\r\n ttlSeconds: options.ttlSeconds ?? DEFAULTS.ttlSeconds,\r\n suggestedConcurrency: options.suggestedClientConcurrency ?? DEFAULTS.suggestedConcurrency,\r\n initRateLimit: options.rateLimit?.initPerMin ?? DEFAULTS.initRateLimit,\r\n partRateLimit: options.rateLimit?.partPerMin ?? DEFAULTS.partRateLimit,\r\n smartMaxFiles: options.rateLimit?.smartMaxFilesPerInit ?? DEFAULTS.smartMaxFiles,\r\n smartInitRateLimit: options.rateLimit?.smartInitRateLimit ?? DEFAULTS.smartInitRateLimit,\r\n multipartPath: options.pathPrefix?.multipart ?? DEFAULTS.multipartPath,\r\n smartPath: options.pathPrefix?.smart ?? DEFAULTS.smartPath,\r\n fileUrlPrefix: options.fileUrlPrefix ?? DEFAULTS.fileUrlPrefix,\r\n strategy: {\r\n ...DEFAULT_STRATEGY_CONFIG,\r\n ...(options.strategy ?? {}),\r\n },\r\n },\r\n validateSession: options.sessionValidator ?? defaultSessionValidator,\r\n };\r\n\r\n app.decorate('secureUpload', runtime);\r\n\r\n // 4. 注册路由\r\n await registerMultipartRoutes(app, runtime);\r\n await registerSmartRoutes(app, runtime);\r\n\r\n // 4.5 全局错误处理:把 SecureError 转成统一格式,code 保持数字\r\n // 注意:fastify 注入的 err 类型是 FastifyError(有 code/statusCode),与我们的 SecureError code 冲突,\r\n // 所以这里把 err 显式声明为 FastifyError | SecureError(联合而非交叉)\r\n app.setErrorHandler(function (this: any, err: unknown, req: any, reply: any) {\r\n if (err instanceof SecureError) {\r\n return reply.status(err.statusCode).send({\r\n success: false,\r\n code: err.code,\r\n error: err.name,\r\n message: err.message,\r\n });\r\n }\r\n // Zod 校验错误\r\n const anyErr = err as any;\r\n if (anyErr && typeof anyErr === 'object' && 'issues' in anyErr && Array.isArray(anyErr.issues)) {\r\n return reply.status(400).send({\r\n success: false,\r\n code: 1001, // INVALID_REQUEST\r\n error: 'ZodValidationError',\r\n message: anyErr.issues?.[0]?.message ?? 'invalid request',\r\n });\r\n }\r\n // 其他错误 → 500\r\n app.log.error({ err }, '❌ secure-upload-sdk 错误');\r\n return reply.status(anyErr?.statusCode ?? 500).send({\r\n success: false,\r\n code: 1500, // INTERNAL_ERROR\r\n error: anyErr?.name ?? 'INTERNAL_ERROR',\r\n message: anyErr?.message ?? 'internal error',\r\n });\r\n });\r\n\r\n // 5. 后台任务\r\n if (options.cleanupTask?.enabled !== false) {\r\n startMultipartCleanupTask({\r\n enabled: true,\r\n intervalSeconds: options.cleanupTask?.intervalSeconds ?? 300,\r\n batchLimit: options.cleanupTask?.batchLimit ?? 100,\r\n storage,\r\n multipartRepo,\r\n log: app.log,\r\n onClose: (cb) => app.addHook('onClose', async () => cb()),\r\n });\r\n }\r\n if (options.orphanScan?.enabled !== false) {\r\n startOrphanFileScanTask({\r\n enabled: true,\r\n intervalSeconds: options.orphanScan?.intervalSeconds ?? 300,\r\n lookbackDays: options.orphanScan?.lookbackDays ?? 7,\r\n batchSize: options.orphanScan?.batchSize ?? 2000,\r\n storage,\r\n fileRepo,\r\n log: app.log,\r\n onClose: (cb) => app.addHook('onClose', async () => cb()),\r\n });\r\n }\r\n\r\n app.log.info(\r\n {\r\n secure: runtime.config.secure,\r\n storage: storage.constructor.name,\r\n repository: options.repository.kind,\r\n redis: options.redis ? 'yes' : 'memory-fallback',\r\n multipartPath: runtime.config.multipartPath,\r\n smartPath: runtime.config.smartPath,\r\n },\r\n '🚀 secure-upload-fastify-sdk 插件已注册'\r\n );\r\n}\r\n\r\nexport default fp(secureUploadPluginImpl, {\r\n name: 'secure-upload-fastify-sdk',\r\n fastify: '4.x',\r\n});\r\n\r\n// ============================================================\r\n// 内部导出(供测试用)\r\n// ============================================================\r\nexport {\r\n LocalStorage,\r\n InMemoryStorage,\r\n PrismaMultipartRepository,\r\n PrismaFileRepository,\r\n InMemoryMultipartRepository,\r\n InMemoryFileRepository,\r\n};\r\n","/**\r\n * 本地磁盘存储实现(默认)\r\n *\r\n * 目录布局:\r\n * <baseDir>/yyyy/mm/dd/<id>.<ext> <- 合并后的正式文件\r\n * <baseDir>/_multipart/<uploadId>/part_0001.bin\r\n * part_0002.bin\r\n * ...\r\n *\r\n * 兼容性: 实现 StorageAdapter 接口,可被 SDK 接受。\r\n * 后续可写 S3Storage(实现同一接口)直接替换,业务代码零改动。\r\n */\r\n\r\nimport { promises as fs, createReadStream, createWriteStream } from 'node:fs';\r\nimport path from 'node:path';\r\nimport { createHash } from 'node:crypto';\r\nimport type { ReadStream } from 'node:fs';\r\nimport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\nexport interface LocalStorageOptions {\r\n /** 存储根目录(绝对路径或相对 cwd) */\r\n baseDir: string;\r\n}\r\n\r\nexport class LocalStorage implements StorageAdapter {\r\n private baseDir: string;\r\n\r\n constructor(opts: LocalStorageOptions | string) {\r\n this.baseDir = typeof opts === 'string' ? opts : opts.baseDir;\r\n }\r\n\r\n // ============================================================\r\n // 内部工具\r\n // ============================================================\r\n\r\n private inferExt(filename: string, contentType: string): string {\r\n const fromName = path.extname(filename).toLowerCase();\r\n if (fromName) return fromName;\r\n const map: Record<string, string> = {\r\n 'image/png': '.png',\r\n 'image/jpeg': '.jpg',\r\n 'image/gif': '.gif',\r\n 'image/webp': '.webp',\r\n 'image/svg+xml': '.svg',\r\n 'application/pdf': '.pdf',\r\n 'text/plain': '.txt',\r\n 'application/json': '.json',\r\n 'application/zip': '.zip',\r\n 'video/mp4': '.mp4',\r\n };\r\n return map[contentType] ?? '.bin';\r\n }\r\n\r\n private generateKey(filename: string, contentType: string, id: string): string {\r\n const now = new Date();\r\n const yyyy = now.getFullYear();\r\n const mm = String(now.getMonth() + 1).padStart(2, '0');\r\n const dd = String(now.getDate()).padStart(2, '0');\r\n const ext = this.inferExt(filename, contentType);\r\n return `${yyyy}/${mm}/${dd}/${id}${ext}`;\r\n }\r\n\r\n private multipartDir(uploadId: string): string {\r\n return path.resolve(this.baseDir, '_multipart', uploadId);\r\n }\r\n\r\n private partPath(uploadId: string, partNumber: number): string {\r\n if (!Number.isInteger(partNumber) || partNumber < 1) {\r\n throw new Error(`[storage] partNumber 非法: ${partNumber}`);\r\n }\r\n return path.join(this.multipartDir(uploadId), `part_${String(partNumber).padStart(4, '0')}.bin`);\r\n }\r\n\r\n // ============================================================\r\n // StorageAdapter 实现\r\n // ============================================================\r\n\r\n async put(data: Buffer, meta: FileMeta & { id: string }): Promise<PutResult> {\r\n const key = this.generateKey(meta.filename, meta.contentType, meta.id);\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n await fs.mkdir(path.dirname(absolutePath), { recursive: true });\r\n await fs.writeFile(absolutePath, data);\r\n return { key, absolutePath };\r\n }\r\n\r\n get(key: string): ReadStream {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n return createReadStream(absolutePath);\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n try {\r\n await fs.unlink(absolutePath);\r\n } catch (e: any) {\r\n if (e.code !== 'ENOENT') throw e;\r\n }\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n try {\r\n await fs.access(absolutePath);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n async putPart(\r\n uploadId: string,\r\n partNumber: number,\r\n data: Buffer\r\n ): Promise<{ size: number; path: string }> {\r\n const dir = this.multipartDir(uploadId);\r\n await fs.mkdir(dir, { recursive: true });\r\n const finalPath = this.partPath(uploadId, partNumber);\r\n const tmpPath = `${finalPath}.tmp`;\r\n try {\r\n await fs.writeFile(tmpPath, data);\r\n await fs.rename(tmpPath, finalPath);\r\n } catch (e) {\r\n try { await fs.unlink(tmpPath); } catch { /* ignore */ }\r\n throw e;\r\n }\r\n return { size: data.length, path: finalPath };\r\n }\r\n\r\n async listParts(uploadId: string): Promise<number[]> {\r\n const dir = this.multipartDir(uploadId);\r\n try {\r\n const names = await fs.readdir(dir);\r\n const nums: number[] = [];\r\n for (const n of names) {\r\n const m = n.match(/^part_(\\d{4,})\\.bin$/);\r\n if (m) nums.push(parseInt(m[1], 10));\r\n }\r\n nums.sort((a, b) => a - b);\r\n return nums;\r\n } catch (e: any) {\r\n if (e.code === 'ENOENT') return [];\r\n throw e;\r\n }\r\n }\r\n\r\n async partSize(uploadId: string, partNumber: number): Promise<number> {\r\n const p = this.partPath(uploadId, partNumber);\r\n const stat = await fs.stat(p);\r\n return stat.size;\r\n }\r\n\r\n async mergeParts(\r\n uploadId: string,\r\n totalParts: number,\r\n target: { id: string; filename: string; contentType: string }\r\n ): Promise<MergeResult> {\r\n const key = this.generateKey(target.filename, target.contentType, target.id);\r\n const absolutePath = path.resolve(this.baseDir, key);\r\n await fs.mkdir(path.dirname(absolutePath), { recursive: true });\r\n const tmpPath = `${absolutePath}.merge.tmp`;\r\n\r\n const out = createWriteStream(tmpPath);\r\n try {\r\n for (let i = 1; i <= totalParts; i++) {\r\n const partPath = this.partPath(uploadId, i);\r\n try {\r\n await fs.access(partPath);\r\n } catch {\r\n throw new Error(`[storage] 缺分片 part_${i} (uploadId=${uploadId})`);\r\n }\r\n await new Promise<void>((resolve, reject) => {\r\n const rs = createReadStream(partPath);\r\n rs.on('error', reject);\r\n rs.on('end', resolve);\r\n rs.pipe(out, { end: false });\r\n });\r\n }\r\n out.end();\r\n await new Promise<void>((resolve, reject) => {\r\n out.on('finish', () => resolve());\r\n out.on('error', reject);\r\n });\r\n const stat = await fs.stat(tmpPath);\r\n await fs.rename(tmpPath, absolutePath);\r\n return { key, absolutePath, size: stat.size };\r\n } catch (e) {\r\n try { await fs.unlink(tmpPath); } catch { /* ignore */ }\r\n throw e;\r\n }\r\n }\r\n\r\n async cleanupMultipart(uploadId: string): Promise<void> {\r\n const dir = this.multipartDir(uploadId);\r\n try {\r\n await fs.rm(dir, { recursive: true, force: true });\r\n } catch (e: any) {\r\n if (e.code !== 'ENOENT') {\r\n console.warn(`[storage] cleanup multipart failed: uploadId=${uploadId} err=${e.message}`);\r\n }\r\n }\r\n }\r\n\r\n async computeMergedHash(uploadId: string, totalParts: number): Promise<string> {\r\n const hash = createHash('sha256');\r\n for (let i = 1; i <= totalParts; i++) {\r\n const partPath = this.partPath(uploadId, i);\r\n const stream = createReadStream(partPath);\r\n await new Promise<void>((resolve, reject) => {\r\n stream.on('data', (chunk) => hash.update(chunk as Buffer));\r\n stream.on('end', resolve);\r\n stream.on('error', reject);\r\n });\r\n }\r\n return hash.digest('hex');\r\n }\r\n}\r\n","/**\r\n * 内存存储实现(测试 / POC 场景)\r\n *\r\n * 用 Map 模拟:\r\n * - files: key -> Buffer (正式文件)\r\n * - parts: uploadId -> Map<partNumber, Buffer> (分片)\r\n * - partSizes: uploadId -> Map<partNumber, number>\r\n *\r\n * 注意: 内存存储在重启时全部丢失,只用于测试。\r\n * 生产请使用 LocalStorage / S3Storage / OSSStorage。\r\n */\r\n\r\nimport { Readable } from 'node:stream';\r\nimport { createHash } from 'node:crypto';\r\nimport type {\r\n StorageAdapter,\r\n FileMeta,\r\n PutResult,\r\n MergeResult,\r\n} from './types';\r\n\r\nexport class InMemoryStorage implements StorageAdapter {\r\n private files = new Map<string, Buffer>();\r\n private parts = new Map<string, Map<number, Buffer>>();\r\n\r\n async put(data: Buffer, meta: FileMeta & { id: string }): Promise<PutResult> {\r\n const key = `mem/${meta.id}`;\r\n this.files.set(key, Buffer.from(data));\r\n return { key };\r\n }\r\n\r\n get(key: string): NodeJS.ReadableStream {\r\n const buf = this.files.get(key);\r\n if (!buf) throw new Error(`[memory-storage] not found: ${key}`);\r\n return Readable.from(buf);\r\n }\r\n\r\n async delete(key: string): Promise<void> {\r\n this.files.delete(key);\r\n }\r\n\r\n async exists(key: string): Promise<boolean> {\r\n return this.files.has(key);\r\n }\r\n\r\n async putPart(\r\n uploadId: string,\r\n partNumber: number,\r\n data: Buffer\r\n ): Promise<{ size: number }> {\r\n let m = this.parts.get(uploadId);\r\n if (!m) {\r\n m = new Map();\r\n this.parts.set(uploadId, m);\r\n }\r\n m.set(partNumber, Buffer.from(data));\r\n return { size: data.length };\r\n }\r\n\r\n async listParts(uploadId: string): Promise<number[]> {\r\n const m = this.parts.get(uploadId);\r\n if (!m) return [];\r\n return [...m.keys()].sort((a, b) => a - b);\r\n }\r\n\r\n async mergeParts(\r\n uploadId: string,\r\n totalParts: number,\r\n target: { id: string; filename: string; contentType: string }\r\n ): Promise<MergeResult> {\r\n const m = this.parts.get(uploadId);\r\n if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);\r\n const parts: Buffer[] = [];\r\n for (let i = 1; i <= totalParts; i++) {\r\n const p = m.get(i);\r\n if (!p) throw new Error(`[memory-storage] missing part ${i}`);\r\n parts.push(p);\r\n }\r\n const merged = Buffer.concat(parts);\r\n const key = `mem/${target.id}`;\r\n this.files.set(key, merged);\r\n return { key, size: merged.length };\r\n }\r\n\r\n async computeMergedHash(uploadId: string, totalParts: number): Promise<string> {\r\n const hash = createHash('sha256');\r\n const m = this.parts.get(uploadId);\r\n if (!m) throw new Error(`[memory-storage] no parts for ${uploadId}`);\r\n for (let i = 1; i <= totalParts; i++) {\r\n const p = m.get(i);\r\n if (!p) throw new Error(`[memory-storage] missing part ${i}`);\r\n hash.update(p);\r\n }\r\n return hash.digest('hex');\r\n }\r\n\r\n async cleanupMultipart(uploadId: string): Promise<void> {\r\n this.parts.delete(uploadId);\r\n }\r\n}\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","/**\r\n * Smart 上传策略选择器\r\n *\r\n * 服务端根据 totalSize 自动选择 ONE_SHOT 或 MULTIPART:\r\n * - ONE_SHOT: 1 个 part(适合 ≤ ONE_SHOT_MAX 的小文件)\r\n * 客户端只调 1 次 part + 1 次 complete = 2 RTT\r\n * - MULTIPART: N 个 part,根据文件大小动态调整 partSize:\r\n * ≤ 1GB → 1MB / part\r\n * 1 ~ 10GB → 5MB / part\r\n * > 10GB → 10MB / part\r\n *\r\n * 设计约束\r\n * ========\r\n * - 纯函数:无副作用、不读 IO、不访问全局状态,易于单测\r\n * - 0 字节文件合法(partSize=0, totalParts=1)\r\n * - 决策结果携带 reason 字段,便于日志/排障\r\n * - 不与 config 直接耦合:调用方传入 cfg\r\n */\r\n\r\nexport type StrategyKind = 'ONE_SHOT' | 'MULTIPART';\r\n\r\nexport interface StrategyConfig {\r\n /** ≤ 此字节数走 ONE_SHOT(默认 1MB) */\r\n oneShotMax: number;\r\n /** 默认分片大小(默认 1MB) */\r\n defaultPartSize: number;\r\n /** 超过此字节数切到大分片(默认 1GB) */\r\n bigFileThreshold: number;\r\n /** 大文件分片大小(默认 5MB) */\r\n bigFilePartSize: number;\r\n /** 超过此字节数切到超大分片(默认 10GB) */\r\n hugeFileThreshold: number;\r\n /** 超大文件分片大小(默认 10MB) */\r\n hugeFilePartSize: number;\r\n}\r\n\r\nexport interface StrategyResult {\r\n kind: StrategyKind;\r\n /** 实际分片大小(字节) */\r\n partSize: number;\r\n /** 总分片数(ONE_SHOT 固定为 1) */\r\n totalParts: number;\r\n /** 决策原因(便于日志/调试) */\r\n reason: string;\r\n}\r\n\r\n/** 默认策略配置 */\r\nexport const DEFAULT_STRATEGY_CONFIG: StrategyConfig = {\r\n oneShotMax: 1024 * 1024, // 1MB\r\n defaultPartSize: 1024 * 1024, // 1MB\r\n bigFileThreshold: 1024 ** 3, // 1GB\r\n bigFilePartSize: 5 * 1024 * 1024, // 5MB\r\n hugeFileThreshold: 10 * 1024 ** 3, // 10GB\r\n hugeFilePartSize: 10 * 1024 * 1024, // 10MB\r\n};\r\n\r\n/**\r\n * 根据 totalSize 选出上传策略\r\n *\r\n * @throws totalSize 非法(负数 / NaN / 非有限数)\r\n *\r\n * @example\r\n * chooseStrategy(1024) // → ONE_SHOT 1 part, partSize=1024\r\n * chooseStrategy(5 * 1024 * 1024) // → MULTIPART 5 part, partSize=1MB\r\n * chooseStrategy(5 * 1024**3) // → MULTIPART 1024 part, partSize=5MB\r\n * chooseStrategy(50 * 1024**3) // → MULTIPART 5120 part, partSize=10MB\r\n */\r\nexport function chooseStrategy(\r\n totalSize: number,\r\n cfg: StrategyConfig = DEFAULT_STRATEGY_CONFIG\r\n): StrategyResult {\r\n if (!Number.isFinite(totalSize) || totalSize < 0) {\r\n throw new Error(`[strategy] totalSize 非法: ${totalSize}(必须 ≥ 0 的有限数)`);\r\n }\r\n validateConfig(cfg);\r\n\r\n if (totalSize === 0) {\r\n return { kind: 'ONE_SHOT', partSize: 0, totalParts: 1, reason: '0字节' };\r\n }\r\n\r\n if (totalSize <= cfg.oneShotMax) {\r\n return {\r\n kind: 'ONE_SHOT',\r\n partSize: totalSize,\r\n totalParts: 1,\r\n reason: `≤ ${cfg.oneShotMax}B 走 ONE_SHOT(1 part 直传)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.hugeFileThreshold) {\r\n const partSize = cfg.hugeFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.hugeFileThreshold}B 走 HUGE(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n if (totalSize > cfg.bigFileThreshold) {\r\n const partSize = cfg.bigFilePartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `> ${cfg.bigFileThreshold}B 走 BIG(${partSize}B/part)`,\r\n };\r\n }\r\n\r\n const partSize = cfg.defaultPartSize;\r\n return {\r\n kind: 'MULTIPART',\r\n partSize,\r\n totalParts: Math.ceil(totalSize / partSize),\r\n reason: `≤ ${cfg.bigFileThreshold}B 走 DEFAULT(${partSize}B/part)`,\r\n };\r\n}\r\n\r\n/**\r\n * 建议的客户端并发上传 part 数\r\n * - ONE_SHOT:固定 1(只传 1 个 part,无需并发)\r\n * - totalParts ≤ 4:用 totalParts(part 太少,无并发必要)\r\n * - totalParts > 4:用 4(服务端限流/带宽考虑)\r\n */\r\nexport function suggestConcurrency(s: StrategyResult): number {\r\n if (s.kind === 'ONE_SHOT') return 1;\r\n if (s.totalParts <= 4) return Math.max(1, s.totalParts);\r\n return 4;\r\n}\r\n\r\nfunction validateConfig(cfg: StrategyConfig): void {\r\n const fields: Array<[keyof StrategyConfig, string]> = [\r\n ['oneShotMax', 'oneShotMax'],\r\n ['defaultPartSize', 'defaultPartSize'],\r\n ['bigFileThreshold', 'bigFileThreshold'],\r\n ['bigFilePartSize', 'bigFilePartSize'],\r\n ['hugeFileThreshold', 'hugeFileThreshold'],\r\n ['hugeFilePartSize', 'hugeFilePartSize'],\r\n ];\r\n for (const [k, name] of fields) {\r\n if (!Number.isFinite(cfg[k]) || cfg[k] <= 0) {\r\n throw new Error(`[strategy] cfg.${name} 非法: ${cfg[k]}(必须 > 0)`);\r\n }\r\n }\r\n if (cfg.oneShotMax > cfg.defaultPartSize) {\r\n throw new Error(\r\n `[strategy] oneShotMax(${cfg.oneShotMax}) 不能 > defaultPartSize(${cfg.defaultPartSize})`\r\n );\r\n }\r\n if (cfg.bigFileThreshold >= cfg.hugeFileThreshold) {\r\n throw new Error(\r\n `[strategy] bigFileThreshold(${cfg.bigFileThreshold}) 必须 < hugeFileThreshold(${cfg.hugeFileThreshold})`\r\n );\r\n }\r\n if (cfg.defaultPartSize > cfg.bigFilePartSize) {\r\n throw new Error(\r\n `[strategy] defaultPartSize(${cfg.defaultPartSize}) 不能 > bigFilePartSize(${cfg.bigFilePartSize})`\r\n );\r\n }\r\n if (cfg.bigFilePartSize > cfg.hugeFilePartSize) {\r\n throw new Error(\r\n `[strategy] bigFilePartSize(${cfg.bigFilePartSize}) 不能 > hugeFilePartSize(${cfg.hugeFilePartSize})`\r\n );\r\n }\r\n}\r\n","/**\r\n * 错误码 + SecureError\r\n *\r\n * 设计:\r\n * - 1000-1099: 通用请求错误(INVALID_REQUEST 等)\r\n * - 1400-1499: HTTP 语义错误(NOT_FOUND / FORBIDDEN / RATE_LIMITED 等)\r\n * - 1500-1599: 上传相关(BINARY / BATCH / MULTIPART)\r\n *\r\n * 独立于 secure-crypto-top-zod-sdk 的错误码空间,\r\n * 即使用户不引入加密 SDK,这套码也能独立用。\r\n */\r\n\r\nexport enum ErrorCode {\r\n // 通用\r\n INVALID_REQUEST = 1001,\r\n INTERNAL_ERROR = 1500,\r\n NOT_FOUND = 1400,\r\n FORBIDDEN = 1403,\r\n RATE_LIMITED = 1402,\r\n\r\n // binary 协议(网关透传场景)\r\n BINARY_PAYLOAD_EMPTY = 1501,\r\n BINARY_PAYLOAD_TOO_LARGE = 1502,\r\n BINARY_AUTH_REQUIRED = 1503,\r\n BINARY_AUTH_FAILED = 1504,\r\n\r\n // 批量上传\r\n BATCH_EMPTY = 1510,\r\n BATCH_TOO_MANY_FILES = 1511,\r\n BATCH_TOTAL_TOO_LARGE = 1512,\r\n BATCH_FILE_INVALID = 1513,\r\n\r\n // 分片上传(1520-1539)\r\n MULTIPART_NOT_FOUND = 1520,\r\n MULTIPART_ALREADY_COMPLETED = 1521,\r\n MULTIPART_ALREADY_ABORTED = 1522,\r\n MULTIPART_EXPIRED = 1523,\r\n MULTIPART_STATUS_INVALID = 1524,\r\n MULTIPART_TOTAL_TOO_LARGE = 1530,\r\n MULTIPART_PART_TOO_LARGE = 1531,\r\n MULTIPART_PART_TOO_SMALL = 1532,\r\n MULTIPART_INVALID_PART_NUMBER = 1533,\r\n MULTIPART_DUPLICATE_PART = 1534,\r\n MULTIPART_PART_HASH_MISMATCH = 1535,\r\n MULTIPART_FINAL_HASH_MISMATCH = 1536,\r\n MULTIPART_INCOMPLETE_PARTS = 1537,\r\n MULTIPART_AUTH_REQUIRED = 1538,\r\n MULTIPART_INVALID_ARG = 1539,\r\n}\r\n\r\nconst HTTP: Record<ErrorCode, number> = {\r\n [ErrorCode.INVALID_REQUEST]: 400,\r\n [ErrorCode.INTERNAL_ERROR]: 500,\r\n [ErrorCode.NOT_FOUND]: 404,\r\n [ErrorCode.FORBIDDEN]: 403,\r\n [ErrorCode.RATE_LIMITED]: 429,\r\n // binary\r\n [ErrorCode.BINARY_PAYLOAD_EMPTY]: 400,\r\n [ErrorCode.BINARY_PAYLOAD_TOO_LARGE]: 413,\r\n [ErrorCode.BINARY_AUTH_REQUIRED]: 401,\r\n [ErrorCode.BINARY_AUTH_FAILED]: 401,\r\n // batch\r\n [ErrorCode.BATCH_EMPTY]: 400,\r\n [ErrorCode.BATCH_TOO_MANY_FILES]: 413,\r\n [ErrorCode.BATCH_TOTAL_TOO_LARGE]: 413,\r\n [ErrorCode.BATCH_FILE_INVALID]: 400,\r\n // multipart\r\n [ErrorCode.MULTIPART_NOT_FOUND]: 404,\r\n [ErrorCode.MULTIPART_ALREADY_COMPLETED]: 409,\r\n [ErrorCode.MULTIPART_ALREADY_ABORTED]: 409,\r\n [ErrorCode.MULTIPART_EXPIRED]: 410,\r\n [ErrorCode.MULTIPART_STATUS_INVALID]: 409,\r\n [ErrorCode.MULTIPART_TOTAL_TOO_LARGE]: 413,\r\n [ErrorCode.MULTIPART_PART_TOO_LARGE]: 413,\r\n [ErrorCode.MULTIPART_PART_TOO_SMALL]: 400,\r\n [ErrorCode.MULTIPART_INVALID_PART_NUMBER]: 400,\r\n [ErrorCode.MULTIPART_DUPLICATE_PART]: 409,\r\n [ErrorCode.MULTIPART_PART_HASH_MISMATCH]: 400,\r\n [ErrorCode.MULTIPART_FINAL_HASH_MISMATCH]: 400,\r\n [ErrorCode.MULTIPART_INCOMPLETE_PARTS]: 409,\r\n [ErrorCode.MULTIPART_AUTH_REQUIRED]: 401,\r\n [ErrorCode.MULTIPART_INVALID_ARG]: 400,\r\n};\r\n\r\nexport class SecureError extends Error {\r\n constructor(public code: ErrorCode, message?: string) {\r\n super(message ?? ErrorCode[code]);\r\n this.name = 'SecureError';\r\n }\r\n get statusCode(): number {\r\n return HTTP[this.code];\r\n }\r\n}\r\n\r\nexport function getHttpStatus(code: ErrorCode): number {\r\n return HTTP[code];\r\n}\r\n","/**\r\n * Session 验证(JWT payload 解析)\r\n *\r\n * 默认实现: base64url 解码 payload,读 sub / exp 字段\r\n * - 不做 HMAC 签名验证(SDK 不持有 appSecret,无法验签)\r\n * - 只读 payload 拿 userId + 校验 exp\r\n * - 高级场景: 用户通过 options.sessionValidator 注入 jwt.verify()\r\n *\r\n * 安全性:\r\n * - 完整安全链路要求:gateway 已做 JWT 验签,业务服务信任透传的 session\r\n * - 如果用户在业务服务自己做验签,通过 options.sessionValidator 注入即可\r\n */\r\n\r\nimport { SecureError, ErrorCode } from './errors';\r\nimport type { SessionValidator } from './types';\r\n\r\nexport function parseJwtPayload(token: string | undefined): { sub?: number; exp?: number } | null {\r\n if (!token) return null;\r\n try {\r\n const parts = token.split('.');\r\n if (parts.length !== 3) return null;\r\n return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport const defaultSessionValidator: SessionValidator = (token, isPublic) => {\r\n if (isPublic) return null;\r\n if (!token) {\r\n throw new SecureError(ErrorCode.BINARY_AUTH_REQUIRED, 'private 上传必须提供 session (JWT)');\r\n }\r\n const p = parseJwtPayload(token);\r\n if (!p?.sub) throw new SecureError(ErrorCode.BINARY_AUTH_FAILED, 'session 格式无效');\r\n if (p.exp && p.exp * 1000 < Date.now()) {\r\n throw new SecureError(ErrorCode.BINARY_AUTH_FAILED, 'session 已过期');\r\n }\r\n return Number(p.sub);\r\n};\r\n","/**\r\n * 分片上传路由\r\n *\r\n * 端点(相对 pathPrefix.multipart,默认 '/multipart'):\r\n * - POST /init\r\n * - POST /part (binary 协议,从 x-secure-raw-base64 或 secureRequest.raw 读)\r\n * - POST /complete\r\n * - POST /abort\r\n * - POST /status\r\n *\r\n * 鉴权:\r\n * - private:需要 JWT(从 sr.session 或 x-secure-session 头)\r\n * - public :无需登录,userId=null\r\n *\r\n * 协议:\r\n * - secure-crypto 模式: 走 req.secureRequest.biz + req.secureRequest.raw\r\n * - raw 模式: 走 req.body + x-secure-raw-base64 header\r\n */\r\n\r\nimport crypto from 'node:crypto';\r\nimport { z } from 'zod';\r\nimport type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';\r\nimport { SecureError, ErrorCode } from '../errors';\r\nimport {\r\n resolveSession,\r\n resolveSessionKey,\r\n resolvePartData,\r\n resolvePartBiz,\r\n} from './_internal';\r\nimport { MultipartStatus } from '../utils/repository/types';\r\nimport { slidingWindowRateLimit, withLock } from '../utils/redis-helpers';\r\nimport { memorySlidingWindowRateLimit, memoryWithLock } from '../utils/memory-rate-limit';\r\nimport type { SecureUploadRuntime } from '../types';\r\nimport type {\r\n InitResponse,\r\n PartResponse,\r\n CompleteResponse,\r\n AbortResponse,\r\n StatusResponse,\r\n} from '../types';\r\n\r\n// ============================================================\r\n// Zod 校验 schema\r\n// ============================================================\r\n\r\nconst InitBodySchema = z.object({\r\n filename: z.string().min(1).max(255),\r\n contentType: z.string().min(1).max(128).default('application/octet-stream'),\r\n totalSize: z.coerce.number().int().nonnegative(),\r\n partSize: z.coerce.number().int().positive().optional(),\r\n isPublic: z.coerce.boolean().default(false),\r\n expectedHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n meta: z.record(z.unknown()).optional(),\r\n}).strict();\r\n\r\nconst CompleteBodySchema = z.object({\r\n uploadId: z.string().min(1).max(128),\r\n parts: z.array(z.object({\r\n partNumber: z.coerce.number().int().positive(),\r\n hash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n })).optional(),\r\n finalHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n}).strict();\r\n\r\nconst AbortBodySchema = z.object({\r\n uploadId: z.string().min(1).max(128),\r\n}).strict();\r\n\r\nconst StatusBodySchema = z.object({\r\n uploadId: z.string().min(1).max(128),\r\n});\r\n\r\n// ============================================================\r\n// 限流 + 锁:统一抽象(redis 或内存)\r\n// ============================================================\r\n\r\nasync function rateLimit(\r\n rt: SecureUploadRuntime,\r\n key: string,\r\n windowMs: number,\r\n limit: number\r\n): Promise<{ ok: boolean; retryAfterMs: number }> {\r\n if (rt.redis) {\r\n const r = await slidingWindowRateLimit(rt.redis, key, windowMs, limit);\r\n return { ok: r.ok, retryAfterMs: r.retryAfterMs };\r\n }\r\n const r = memorySlidingWindowRateLimit(key, windowMs, limit);\r\n return { ok: r.ok, retryAfterMs: r.retryAfterMs };\r\n}\r\n\r\nasync function lock<T>(\r\n rt: SecureUploadRuntime,\r\n key: string,\r\n ttlMs: number,\r\n fn: () => Promise<T>,\r\n retries = 0\r\n): Promise<T> {\r\n if (rt.redis) {\r\n return await withLock(rt.redis, key, ttlMs, fn, retries);\r\n }\r\n return await memoryWithLock(key, fn, retries);\r\n}\r\n\r\n// ============================================================\r\n// 路由注册\r\n// ============================================================\r\n\r\nexport async function registerMultipartRoutes(\r\n app: FastifyInstance,\r\n rt: SecureUploadRuntime\r\n): Promise<void> {\r\n if (!rt.config.enabled) {\r\n app.log.warn('⚠️ multipart 上传已禁用 (enabled=false)');\r\n return;\r\n }\r\n\r\n const base = rt.config.multipartPath;\r\n app.log.info(\r\n {\r\n routes: [`${base}/init`, `${base}/part`, `${base}/complete`, `${base}/abort`, `${base}/status`],\r\n partMin: rt.config.partMin,\r\n partMax: rt.config.partMax,\r\n partDefault: rt.config.partDefault,\r\n fileMax: rt.config.fileMaxSize,\r\n ttl: rt.config.ttlSeconds,\r\n },\r\n '🔧 multipart-routes: 注册分片路由...'\r\n );\r\n\r\n // ============================================================\r\n // 1. POST /init\r\n // ============================================================\r\n app.post(`${base}/init`, async (req: FastifyRequest, reply: FastifyReply): Promise<InitResponse> => {\r\n const body = InitBodySchema.parse(req.body ?? {});\r\n\r\n // 1.1 鉴权\r\n const { userId, sessionKey } = await resolveSession(req, body.isPublic, rt.validateSession);\r\n\r\n // 1.2 限流\r\n const rlKey = `rl:mp:init:${userId ?? sessionKey.slice(0, 32)}`;\r\n const rl = await rateLimit(rt, rlKey, 60_000, rt.config.initRateLimit);\r\n if (!rl.ok) {\r\n throw new SecureError(\r\n ErrorCode.RATE_LIMITED,\r\n `init 过于频繁,请 ${Math.ceil(rl.retryAfterMs / 1000)}s 后重试`\r\n );\r\n }\r\n\r\n // 1.3 校验 totalSize\r\n if (body.totalSize > rt.config.fileMaxSize) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_TOTAL_TOO_LARGE,\r\n `文件总大小 ${body.totalSize} 超过单文件限制 ${rt.config.fileMaxSize}`\r\n );\r\n }\r\n\r\n // 1.4 计算 partSize / totalParts\r\n let partSize = body.partSize ?? rt.config.partDefault;\r\n if (partSize < rt.config.partMin) partSize = rt.config.partMin;\r\n if (partSize > rt.config.partMax) partSize = rt.config.partMax;\r\n const totalParts = body.totalSize === 0 ? 1 : Math.ceil(body.totalSize / partSize);\r\n\r\n // 1.5 ★ 秒传:如果客户端传了 expectedHash,先去 File 表查\r\n // 注意:必须严格按 (hash, isPublic, userId) 匹配,避免把\"另一种可见性/其他人\"的文件塞给当前上传者\r\n if (body.expectedHash) {\r\n const existing = await rt.fileRepo.findDedupCandidate(\r\n body.expectedHash,\r\n body.isPublic,\r\n userId\r\n );\r\n if (existing) {\r\n const diskOk = await rt.storage.exists(existing.storageKey).catch(() => false);\r\n if (diskOk) {\r\n req.log.info(\r\n { hash: body.expectedHash, fileId: existing.id, isPublic: body.isPublic, userId },\r\n '⚡ 秒传命中(严格匹配可见性+身份)'\r\n );\r\n return {\r\n success: true,\r\n data: {\r\n uploadId: '',\r\n partSize: 0,\r\n totalParts: 0,\r\n totalSize: Number(existing.size),\r\n expiresAt: '',\r\n ttl: 0,\r\n suggestedConcurrency: 0,\r\n dedup: {\r\n fileId: existing.id,\r\n filename: existing.filename,\r\n size: Number(existing.size),\r\n contentType: existing.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${existing.id}`,\r\n },\r\n },\r\n };\r\n }\r\n // 磁盘丢失 → 软删孤儿\r\n req.log.warn(\r\n { hash: body.expectedHash, fileId: existing.id, storageKey: existing.storageKey, userId },\r\n '⚠️ DB 命中但磁盘已丢失,放弃秒传走正常分片'\r\n );\r\n await rt.fileRepo\r\n .softDelete(existing.id, { orphanedAt: new Date().toISOString() })\r\n .catch((e: unknown) => {\r\n req.log.warn({ err: e, fileId: existing.id }, '⚠️ 软删除孤儿 record 失败');\r\n });\r\n }\r\n }\r\n\r\n // 1.6 创建 multipart record\r\n const rec = await rt.multipartRepo.createInit({\r\n userId,\r\n filename: body.filename,\r\n contentType: body.contentType,\r\n totalSize: body.totalSize,\r\n partSize,\r\n totalParts,\r\n isPublic: body.isPublic,\r\n expectedHash: body.expectedHash ?? null,\r\n meta: body.meta ?? null,\r\n ttlSeconds: rt.config.ttlSeconds,\r\n });\r\n\r\n req.log.info(\r\n { uploadId: rec.id, userId, totalSize: body.totalSize, partSize, totalParts, isPublic: body.isPublic },\r\n '✅ multipart: init'\r\n );\r\n\r\n return {\r\n success: true,\r\n data: {\r\n uploadId: rec.id,\r\n partSize,\r\n totalParts,\r\n totalSize: body.totalSize,\r\n expiresAt: rec.expiresAt.toISOString(),\r\n ttl: rt.config.ttlSeconds,\r\n suggestedConcurrency: rt.config.suggestedConcurrency,\r\n },\r\n };\r\n });\r\n\r\n // ============================================================\r\n // 2. POST /part (binary 协议)\r\n // ============================================================\r\n app.post(`${base}/part`, async (req: FastifyRequest, reply: FastifyReply): Promise<PartResponse> => {\r\n // 2.1 读 part 数据(从 secureRequest.raw 或 header)\r\n const { data } = resolvePartData(req);\r\n\r\n if (data.length === 0) {\r\n throw new SecureError(ErrorCode.BINARY_PAYLOAD_EMPTY, 'part 数据为空');\r\n }\r\n if (data.length > rt.config.partMax) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_PART_TOO_LARGE,\r\n `part 大小 ${data.length} 超过 ${rt.config.partMax}`\r\n );\r\n }\r\n\r\n // 2.2 业务字段\r\n const { uploadId, partNumber, partHash } = resolvePartBiz(req);\r\n\r\n // 2.3 鉴权(私有 multipart 也要登录)\r\n // const sessionKey = resolveSessionKey(req);\r\n\r\n // 2.4 限流\r\n // const rlKey = `rl:mp:part:${sessionKey.slice(0, 64)}`;\r\n // ★ 按 uploadId 限流,避免单用户并发多文件时互相挤兑 600 part/min 配额\r\n const rlKey = `rl:mp:part:${uploadId}`;\r\n const rl = await rateLimit(rt, rlKey, 60_000, rt.config.partRateLimit);\r\n if (!rl.ok) {\r\n throw new SecureError(\r\n ErrorCode.RATE_LIMITED,\r\n `part 上传过于频繁,请 ${Math.ceil(rl.retryAfterMs / 1000)}s 后重试`\r\n );\r\n }\r\n\r\n // 2.5 查询 record\r\n const rec = await rt.multipartRepo.findById(uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在: ${uploadId}`);\r\n }\r\n\r\n // 2.6 归属校验\r\n if (!rec.isPublic) {\r\n const userId = await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n if (userId === null) {\r\n throw new SecureError(ErrorCode.NOT_FOUND, 'uploadId 不存在');\r\n }\r\n }\r\n\r\n // 2.7 状态校验\r\n if (rec.status === MultipartStatus.COMPLETED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, 'uploadId 已完成,不能继续 part');\r\n }\r\n if (rec.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止,不能继续 part');\r\n }\r\n if (rec.status === MultipartStatus.EXPIRED || rec.expiresAt.getTime() < Date.now()) {\r\n throw new SecureError(ErrorCode.MULTIPART_EXPIRED, 'uploadId 已过期');\r\n }\r\n\r\n // 2.8 partNumber 越界\r\n if (partNumber < 1 || partNumber > rec.totalParts) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_INVALID_PART_NUMBER,\r\n `partNumber=${partNumber} 越界 [1, ${rec.totalParts}]`\r\n );\r\n }\r\n\r\n // 2.9 算 hash\r\n const actualHash = crypto.createHash('sha256').update(data).digest('hex');\r\n if (partHash && partHash.toLowerCase() !== actualHash) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_PART_HASH_MISMATCH,\r\n `part ${partNumber} 声明 hash ${partHash} 与实际 ${actualHash} 不一致`\r\n );\r\n }\r\n\r\n // 写盘(并发,fast)\r\n await rt.storage.putPart(uploadId, partNumber, data);\r\n\r\n // 元数据更新串行化\r\n const writeRes = await lock(\r\n rt,\r\n `lock:mp:${uploadId}:meta`,\r\n 10_000,\r\n async () => {\r\n const cur = await rt.multipartRepo.findById(uploadId);\r\n if (!cur) throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, 'uploadId 不存在');\r\n if (cur.status === MultipartStatus.COMPLETED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, 'uploadId 已完成');\r\n }\r\n if (cur.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止');\r\n }\r\n const r = await rt.multipartRepo.addPart(uploadId, partNumber, actualHash);\r\n if (r.updated) return { ok: true, record: r.record };\r\n if (r.existingHash && r.existingHash === actualHash) {\r\n return { ok: true, record: r.record, idempotent: true };\r\n }\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_DUPLICATE_PART,\r\n `part ${partNumber} 已存在(原 hash=${r.existingHash ?? 'null'},新 hash=${actualHash})`\r\n );\r\n },\r\n 5\r\n );\r\n\r\n req.log.info(\r\n {\r\n uploadId, partNumber, size: data.length,\r\n receivedCount: writeRes.record.receivedParts.length,\r\n idempotent: (writeRes as any).idempotent === true,\r\n },\r\n '✅ multipart: part 写入'\r\n );\r\n\r\n return {\r\n success: true,\r\n data: {\r\n uploadId,\r\n partNumber,\r\n size: data.length,\r\n receivedCount: writeRes.record.receivedParts.length,\r\n totalParts: rec.totalParts,\r\n },\r\n };\r\n });\r\n\r\n // ============================================================\r\n // 3. POST /complete\r\n // ============================================================\r\n app.post(`${base}/complete`, async (req: FastifyRequest, reply: FastifyReply): Promise<CompleteResponse> => {\r\n const body = CompleteBodySchema.parse(req.body ?? {});\r\n\r\n // 3.1 查 record\r\n const rec = await rt.multipartRepo.findById(body.uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在`);\r\n }\r\n\r\n // 3.2 归属校验\r\n if (!rec.isPublic) {\r\n await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n }\r\n\r\n // 3.3 状态校验\r\n if (rec.status === MultipartStatus.COMPLETED) {\r\n if (rec.fileId) {\r\n const file = await rt.fileRepo.findById(rec.fileId);\r\n if (file) {\r\n return {\r\n success: true,\r\n data: {\r\n fileId: file.id,\r\n filename: file.filename,\r\n size: Number(file.size),\r\n contentType: file.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${file.id}`,\r\n finalHash: file.contentHash ?? '',\r\n idempotent: true,\r\n },\r\n };\r\n }\r\n }\r\n }\r\n if (rec.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止');\r\n }\r\n if (rec.status === MultipartStatus.EXPIRED || rec.expiresAt.getTime() < Date.now()) {\r\n throw new SecureError(ErrorCode.MULTIPART_EXPIRED, 'uploadId 已过期');\r\n }\r\n\r\n // 3.4 校验 receivedParts 是否齐全\r\n const expected = Array.from({ length: rec.totalParts }, (_, i) => i + 1);\r\n const missing = expected.filter((n) => !rec.receivedParts.includes(n));\r\n if (missing.length > 0) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_INCOMPLETE_PARTS,\r\n `缺分片: [${missing.slice(0, 10).join(',')}${missing.length > 10 ? `...(${missing.length}片)` : ''}]`\r\n );\r\n }\r\n\r\n // 3.5 客户端声明的 parts hash\r\n if (body.parts && body.parts.length > 0) {\r\n const expectedHashes: Record<number, string> = {};\r\n for (const p of body.parts) {\r\n if (p.hash) expectedHashes[p.partNumber] = p.hash.toLowerCase();\r\n }\r\n for (const [pnStr, h] of Object.entries(rec.partHashes)) {\r\n const pn = Number(pnStr);\r\n if (expectedHashes[pn] && expectedHashes[pn] !== h.toLowerCase()) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_PART_HASH_MISMATCH,\r\n `part ${pn} 服务端 hash ${h} 与客户端声明 ${expectedHashes[pn]} 不一致`\r\n );\r\n }\r\n }\r\n }\r\n\r\n // 3.6 合并(分布式锁)\r\n const fileId = `f_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;\r\n const result = await lock(\r\n rt,\r\n `lock:mp:${body.uploadId}`,\r\n 60_000,\r\n async () => {\r\n const cur = await rt.multipartRepo.findById(body.uploadId);\r\n if (!cur) throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, 'uploadId 不存在');\r\n if (cur.status === MultipartStatus.COMPLETED && cur.fileId) {\r\n return { idempotent: true, fileId: cur.fileId };\r\n }\r\n if (cur.status === MultipartStatus.ABORTED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_ABORTED, 'uploadId 已终止');\r\n }\r\n\r\n const merged = await rt.storage.mergeParts(\r\n body.uploadId,\r\n rec.totalParts,\r\n { id: fileId, filename: rec.filename, contentType: rec.contentType }\r\n );\r\n\r\n const finalHash = await rt.storage.computeMergedHash(body.uploadId, rec.totalParts);\r\n\r\n if (body.finalHash && body.finalHash.toLowerCase() !== finalHash) {\r\n await rt.storage.delete(merged.key);\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_FINAL_HASH_MISMATCH,\r\n `最终 hash 不一致(声明=${body.finalHash},实际=${finalHash})`\r\n );\r\n }\r\n\r\n const file = await rt.fileRepo.create({\r\n id: fileId,\r\n userId: rec.userId,\r\n filename: rec.filename,\r\n contentType: rec.contentType,\r\n size: merged.size,\r\n storageKey: merged.key,\r\n isPublic: rec.isPublic,\r\n uploadId: rec.id,\r\n totalParts: rec.totalParts,\r\n contentHash: finalHash,\r\n meta: rec.meta ?? undefined,\r\n });\r\n\r\n await rt.multipartRepo.markCompleted(rec.id, fileId);\r\n\r\n return { idempotent: false, fileId: file.id, size: merged.size, finalHash };\r\n }\r\n );\r\n\r\n // 3.7 清理临时分片\r\n if (!(result as any).idempotent) {\r\n rt.storage.cleanupMultipart(body.uploadId).catch((e) => {\r\n req.log.warn({ err: e, uploadId: body.uploadId }, '⚠️ 清理分片目录失败');\r\n });\r\n }\r\n\r\n req.log.info(\r\n {\r\n uploadId: body.uploadId, fileId: (result as any).fileId,\r\n size: (result as any).size, userId: rec.userId,\r\n },\r\n '✅ multipart: complete'\r\n );\r\n\r\n const file = await rt.fileRepo.findById((result as any).fileId);\r\n if (!file) {\r\n throw new SecureError(ErrorCode.INTERNAL_ERROR, 'file record lost');\r\n }\r\n\r\n return {\r\n success: true,\r\n data: {\r\n fileId: file.id,\r\n filename: file.filename,\r\n size: Number(file.size),\r\n contentType: file.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${file.id}`,\r\n finalHash: (result as any).finalHash,\r\n idempotent: (result as any).idempotent === true,\r\n },\r\n };\r\n });\r\n\r\n // ============================================================\r\n // 4. POST /abort\r\n // ============================================================\r\n app.post(`${base}/abort`, async (req: FastifyRequest, reply: FastifyReply): Promise<AbortResponse> => {\r\n const body = AbortBodySchema.parse(req.body ?? {});\r\n\r\n const rec = await rt.multipartRepo.findById(body.uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在`);\r\n }\r\n\r\n if (!rec.isPublic) {\r\n await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n }\r\n\r\n if (rec.status === MultipartStatus.COMPLETED) {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, '已完成的 uploadId 不能 abort');\r\n }\r\n if (rec.status === MultipartStatus.ABORTED) {\r\n return { success: true, data: { uploadId: rec.id, status: 'ABORTED', idempotent: true } };\r\n }\r\n\r\n try {\r\n await rt.multipartRepo.markAborted(body.uploadId);\r\n } catch (e: any) {\r\n if (e.message === 'ALREADY_COMPLETED') {\r\n throw new SecureError(ErrorCode.MULTIPART_ALREADY_COMPLETED, '已完成的 uploadId 不能 abort');\r\n }\r\n if (e.message === 'STATUS_INVALID') {\r\n throw new SecureError(ErrorCode.MULTIPART_STATUS_INVALID, '状态机非法转移');\r\n }\r\n throw e;\r\n }\r\n\r\n rt.storage.cleanupMultipart(body.uploadId).catch((e) => {\r\n req.log.warn({ err: e, uploadId: body.uploadId }, '⚠️ abort 后清理失败');\r\n });\r\n\r\n req.log.info({ uploadId: body.uploadId, userId: rec.userId }, '🛑 multipart: abort');\r\n\r\n return { success: true, data: { uploadId: rec.id, status: 'ABORTED' } };\r\n });\r\n\r\n // ============================================================\r\n // 5. POST /status\r\n // ============================================================\r\n app.post(`${base}/status`, async (req: FastifyRequest, reply: FastifyReply): Promise<StatusResponse> => {\r\n const body = StatusBodySchema.parse(req.body ?? {});\r\n\r\n const rec = await rt.multipartRepo.findById(body.uploadId);\r\n if (!rec) {\r\n throw new SecureError(ErrorCode.MULTIPART_NOT_FOUND, `uploadId 不存在`);\r\n }\r\n\r\n if (!rec.isPublic) {\r\n await resolveUserIdForRecord(req, rec.userId, rec.isPublic, rt);\r\n }\r\n\r\n // 列出磁盘上的 part\r\n const diskParts = await rt.storage.listParts(rec.id);\r\n const diskSet = new Set(diskParts);\r\n const dbSet = new Set(rec.receivedParts);\r\n const mergedReceived = Array.from(new Set([...diskSet, ...dbSet])).sort((a, b) => a - b);\r\n\r\n return {\r\n success: true,\r\n data: {\r\n uploadId: rec.id,\r\n status: rec.status,\r\n filename: rec.filename,\r\n contentType: rec.contentType,\r\n totalSize: Number(rec.totalSize),\r\n partSize: rec.partSize,\r\n totalParts: rec.totalParts,\r\n receivedParts: mergedReceived,\r\n receivedCount: mergedReceived.length,\r\n isPublic: rec.isPublic,\r\n expiresAt: rec.expiresAt.toISOString(),\r\n createdAt: rec.createdAt.toISOString(),\r\n completedAt: rec.completedAt?.toISOString() ?? null,\r\n fileId: rec.fileId,\r\n },\r\n };\r\n });\r\n}\r\n\r\n/** 内部:对 multipart 记录做归属校验 */\r\nasync function resolveUserIdForRecord(\r\n req: FastifyRequest,\r\n expectedUserId: number | null,\r\n isPublic: boolean,\r\n rt: SecureUploadRuntime\r\n): Promise<number | null> {\r\n const sr: any = (req as any).secureRequest;\r\n const session = sr?.session ?? (req.headers['x-secure-session'] as string | undefined);\r\n const userId = await rt.validateSession(session, isPublic);\r\n if (!isPublic && userId !== expectedUserId) {\r\n throw new SecureError(ErrorCode.NOT_FOUND, 'uploadId 不存在');\r\n }\r\n return userId;\r\n}\r\n","/**\r\n * 共享路由辅助\r\n * - resolveUserId / resolveSessionKey\r\n * - raw 模式 vs secure-crypto 模式差异\r\n */\r\n\r\nimport type { FastifyRequest } from 'fastify';\r\nimport { SecureError, ErrorCode } from '../errors';\r\n\r\ntype ValidateFn = (\r\n token: string | undefined,\r\n isPublic: boolean\r\n) => number | null | Promise<number | null>;\r\n\r\nexport interface SessionResolution {\r\n userId: number | null;\r\n sessionKey: string;\r\n}\r\n\r\n/**\r\n * 从 req 上拿 session / userId\r\n * - secure-crypto 模式: 优先 req.secureRequest.session(加密 SDK 解析后的字段)\r\n * - raw 模式: 从 x-secure-session header 读\r\n */\r\nexport async function resolveSession(\r\n req: FastifyRequest,\r\n isPublic: boolean,\r\n validate: ValidateFn\r\n): Promise<SessionResolution> {\r\n const sr: any = (req as any).secureRequest;\r\n const session = sr?.session ?? (req.headers['x-secure-session'] as string | undefined);\r\n const userId = await validate(session, isPublic);\r\n const sessionKey = session ?? 'anonymous';\r\n return { userId: userId ?? null, sessionKey };\r\n}\r\n\r\n/** 单独拿 session 字符串(给限流 key 用) */\r\nexport function resolveSessionKey(req: FastifyRequest): string {\r\n const sr: any = (req as any).secureRequest;\r\n return sr?.session ?? (req.headers['x-secure-session'] as string | undefined) ?? 'anonymous';\r\n}\r\n\r\n/** 单独从 session 拿 userId(用于多文件批量场景,需要 userId 拼限流 key) */\r\nexport async function resolveUserId(\r\n req: FastifyRequest,\r\n isPublic: boolean,\r\n validate: ValidateFn\r\n): Promise<number | null> {\r\n const sr: any = (req as any).secureRequest;\r\n const session = sr?.session ?? (req.headers['x-secure-session'] as string | undefined);\r\n return await validate(session, isPublic);\r\n}\r\n\r\n/**\r\n * 从 req 上读 part 数据(secure-crypto 模式从 secureRequest.raw 拿,raw 模式从 header 拿)\r\n * - secure-crypto: x-secure-raw-base64(网关/gateway 解析 binary envelope 后透传)\r\n * - raw: 直接从 body 读\r\n */\r\nexport function resolvePartData(req: FastifyRequest): { data: Buffer; declaredSize: number } {\r\n const sr: any = (req as any).secureRequest;\r\n if (sr?.raw?.data) {\r\n return { data: sr.raw.data as Buffer, declaredSize: Number(sr.raw.size ?? (sr.raw.data as Buffer).length) };\r\n }\r\n // raw 模式:从 x-secure-raw-base64 header 读\r\n const rawBase64 = req.headers['x-secure-raw-base64'] as string | undefined;\r\n const declaredSize = Number(req.headers['x-secure-content-length'] || 0);\r\n if (!rawBase64) {\r\n throw new SecureError(\r\n ErrorCode.INVALID_REQUEST,\r\n '缺少 part 数据(secure-crypto 模式读 secureRequest.raw,raw 模式读 x-secure-raw-base64 header)'\r\n );\r\n }\r\n return { data: Buffer.from(rawBase64, 'base64'), declaredSize };\r\n}\r\n\r\n/**\r\n * 从 req 上读 part 业务字段(uploadId / partNumber / partHash)\r\n * - secure-crypto 模式: 优先 sr.biz\r\n * - raw 模式: req.body\r\n */\r\nexport function resolvePartBiz(req: FastifyRequest): {\r\n uploadId: string;\r\n partNumber: number;\r\n partHash: string | null;\r\n} {\r\n const sr: any = (req as any).secureRequest;\r\n const biz = sr?.biz ?? (req.body as any) ?? {};\r\n const uploadId = String(biz.uploadId ?? '').trim();\r\n const partNumberRaw = biz.partNumber;\r\n const partHash: string | null = biz.partHash ?? null;\r\n if (!uploadId) {\r\n throw new SecureError(ErrorCode.INVALID_REQUEST, '缺少 uploadId');\r\n }\r\n const partNumber = Number(partNumberRaw);\r\n if (!Number.isInteger(partNumber) || partNumber < 1) {\r\n throw new SecureError(ErrorCode.MULTIPART_INVALID_PART_NUMBER, `partNumber 非法: ${partNumberRaw}`);\r\n }\r\n return { uploadId, partNumber, partHash };\r\n}\r\n","/**\r\n * Redis 辅助工具(可选注入,分布式场景使用)\r\n *\r\n * 提供:\r\n * - slidingWindowRateLimit: 滑动窗口限流(Lua + ZSET,精确无突刺)\r\n * - acquireLock / releaseLock / withLock: 分布式锁\r\n *\r\n * ioredis 客户端作为 SDK peerDependency(可选),不传时 SDK 用内存降级\r\n */\r\n\r\nimport type Redis from 'ioredis';\r\n\r\nconst LUA_SLIDING_WINDOW_RATE_LIMIT = `\r\n-- KEYS[1] = rate limit key\r\n-- ARGV[1] = window (ms)\r\n-- ARGV[2] = limit (max requests in window)\r\n-- ARGV[3] = now (ms)\r\n-- ARGV[4] = member (unique per request)\r\nlocal key = KEYS[1]\r\nlocal window = tonumber(ARGV[1])\r\nlocal limit = tonumber(ARGV[2])\r\nlocal now = tonumber(ARGV[3])\r\nlocal member = ARGV[4]\r\nredis.call('ZREMRANGEBYSCORE', key, 0, now - window)\r\nlocal count = redis.call('ZCARD', key)\r\nif count >= limit then\r\n local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\r\n local retryAfter = window\r\n if #oldest >= 2 then\r\n retryAfter = (tonumber(oldest[2]) + window) - now\r\n if retryAfter < 0 then retryAfter = 0 end\r\n end\r\n return { 0, retryAfter }\r\nend\r\nredis.call('ZADD', key, now, member)\r\nredis.call('PEXPIRE', key, window)\r\nreturn { 1, 0 }\r\n`;\r\n\r\nconst LUA_RELEASE_LOCK = `\r\nif redis.call('GET', KEYS[1]) == ARGV[1] then\r\n return redis.call('DEL', KEYS[1])\r\nelse\r\n return 0\r\nend\r\n`;\r\n\r\nconst LUA_ACQUIRE_LOCK = `\r\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', tonumber(ARGV[2])) then\r\n return 1\r\nelse\r\n return 0\r\nend\r\n`;\r\n\r\nexport async function slidingWindowRateLimit(\r\n redis: Redis,\r\n key: string,\r\n windowMs: number,\r\n limit: number\r\n): Promise<{ ok: boolean; retryAfterMs: number; remaining: number }> {\r\n const now = Date.now();\r\n const member = `${now}-${Math.random().toString(36).slice(2, 10)}`;\r\n const result = (await redis.eval(\r\n LUA_SLIDING_WINDOW_RATE_LIMIT,\r\n 1,\r\n key,\r\n String(windowMs),\r\n String(limit),\r\n String(now),\r\n member\r\n )) as [number, number];\r\n\r\n const ok = result[0] === 1;\r\n return {\r\n ok,\r\n retryAfterMs: ok ? 0 : Number(result[1] ?? 0),\r\n remaining: ok ? limit - 1 : 0,\r\n };\r\n}\r\n\r\nexport async function acquireLock(\r\n redis: Redis,\r\n key: string,\r\n ttlMs = 5_000\r\n): Promise<{ ok: true; token: string } | { ok: false; token: null }> {\r\n const token = `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\r\n const ok = await redis.eval(LUA_ACQUIRE_LOCK, 1, key, token, String(ttlMs));\r\n if (ok === 1) return { ok: true, token };\r\n return { ok: false, token: null };\r\n}\r\n\r\nexport async function releaseLock(redis: Redis, key: string, token: string): Promise<boolean> {\r\n const r = await redis.eval(LUA_RELEASE_LOCK, 1, key, token);\r\n return r === 1;\r\n}\r\n\r\nexport async function withLock<T>(\r\n redis: Redis,\r\n key: string,\r\n ttlMs: number,\r\n fn: () => Promise<T>,\r\n retries = 0\r\n): Promise<T> {\r\n let lastErr: unknown;\r\n for (let i = 0; i <= retries; i++) {\r\n const lock = await acquireLock(redis, key, ttlMs);\r\n if (lock.ok) {\r\n try {\r\n return await fn();\r\n } finally {\r\n await releaseLock(redis, key, lock.token).catch(() => {\r\n // 释放失败不影响主流程\r\n });\r\n }\r\n }\r\n lastErr = new Error(`[redis-helpers] acquireLock failed: ${key}`);\r\n if (i < retries) {\r\n const wait = 50 * Math.pow(2, i) + Math.floor(Math.random() * 20);\r\n await new Promise((r) => setTimeout(r, wait));\r\n }\r\n }\r\n const e: any = lastErr instanceof Error ? lastErr : new Error('[redis-helpers] withLock failed');\r\n e.code = 'LOCK_BUSY';\r\n throw e;\r\n}\r\n","/**\r\n * 内存版限流 + 锁(单进程降级)\r\n *\r\n * 当 SDK 未注入 redis 时,使用本实现:\r\n * - 限流: 进程内 Map<key, timestamps[]>(滑动窗口)\r\n * - 锁: 进程内 Map<key, boolean>(互斥)\r\n *\r\n * 注意: 内存版只在单进程内有效,多实例部署请传 redis。\r\n */\r\n\r\ninterface MemLimitEntry {\r\n timestamps: number[];\r\n}\r\n\r\nconst _limitStore = new Map<string, MemLimitEntry>();\r\nconst _lockStore = new Map<string, boolean>();\r\n\r\n/** 清理过期条目,避免 Map 无限增长 */\r\nfunction gcLimit(now: number, windowMs: number) {\r\n if (_limitStore.size > 5000) {\r\n for (const [k, v] of _limitStore.entries()) {\r\n const filtered = v.timestamps.filter((t) => t > now - windowMs);\r\n if (filtered.length === 0) _limitStore.delete(k);\r\n else _limitStore.set(k, { timestamps: filtered });\r\n }\r\n }\r\n}\r\n\r\nexport function memorySlidingWindowRateLimit(\r\n key: string,\r\n windowMs: number,\r\n limit: number\r\n): { ok: boolean; retryAfterMs: number; remaining: number } {\r\n const now = Date.now();\r\n gcLimit(now, windowMs);\r\n\r\n const entry = _limitStore.get(key) ?? { timestamps: [] };\r\n // 移除过期\r\n entry.timestamps = entry.timestamps.filter((t) => t > now - windowMs);\r\n\r\n if (entry.timestamps.length >= limit) {\r\n const oldest = entry.timestamps[0];\r\n const retryAfter = Math.max(0, oldest + windowMs - now);\r\n return { ok: false, retryAfterMs: retryAfter, remaining: 0 };\r\n }\r\n\r\n entry.timestamps.push(now);\r\n _limitStore.set(key, entry);\r\n return { ok: true, retryAfterMs: 0, remaining: limit - entry.timestamps.length };\r\n}\r\n\r\nexport function memoryAcquireLock(key: string): boolean {\r\n if (_lockStore.get(key)) return false;\r\n _lockStore.set(key, true);\r\n return true;\r\n}\r\n\r\nexport function memoryReleaseLock(key: string): void {\r\n _lockStore.delete(key);\r\n}\r\n\r\nexport async function memoryWithLock<T>(\r\n key: string,\r\n fn: () => Promise<T>,\r\n retries = 0\r\n): Promise<T> {\r\n for (let i = 0; i <= retries; i++) {\r\n if (memoryAcquireLock(key)) {\r\n try {\r\n return await fn();\r\n } finally {\r\n memoryReleaseLock(key);\r\n }\r\n }\r\n if (i < retries) {\r\n const wait = 50 * Math.pow(2, i) + Math.floor(Math.random() * 20);\r\n await new Promise((r) => setTimeout(r, wait));\r\n }\r\n }\r\n const e: any = new Error(`[memory-lock] LOCK_BUSY: ${key}`);\r\n e.code = 'LOCK_BUSY';\r\n throw e;\r\n}\r\n\r\n/** 内部:统一抽象(让路由层不感知 redis / 内存) */\r\nexport interface LockLike {\r\n withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>, retries?: number): Promise<T>;\r\n}\r\n\r\nexport interface RateLimitLike {\r\n rateLimit(key: string, windowMs: number, limit: number): Promise<{ ok: boolean; retryAfterMs: number; remaining: number }>;\r\n}\r\n","/**\r\n * Smart 统一上传路由\r\n *\r\n * 端点(相对 pathPrefix.smart,默认 '/smart'):\r\n * - POST /init\r\n * 批量:一次接收 N 个文件 → 智能分流(ONE_SHOT / MULTIPART)\r\n * 单个文件失败不阻塞其他\r\n *\r\n * 后续 part/complete/abort/status 复用 multipart/*(SDK SmartClient 内部自动调)\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport type { FastifyInstance, FastifyRequest } from 'fastify';\r\nimport { SecureError, ErrorCode } from '../errors';\r\nimport { resolveSession, resolveUserId } from './_internal';\r\nimport { chooseStrategy, suggestConcurrency } from '../utils/strategy';\r\nimport { slidingWindowRateLimit } from '../utils/redis-helpers';\r\nimport { memorySlidingWindowRateLimit } from '../utils/memory-rate-limit';\r\nimport type { SecureUploadRuntime } from '../types';\r\nimport { MultipartStatus, FileStatus } from '../utils/repository/types';\r\n\r\n// ============================================================\r\n// Schemas\r\n// ============================================================\r\n\r\nconst FileInitItemSchema = z.object({\r\n filename: z.string().min(1).max(255),\r\n contentType: z.string().min(1).max(128).default('application/octet-stream'),\r\n size: z.coerce.number().int().nonnegative(),\r\n isPublic: z.coerce.boolean().default(false),\r\n expectedHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),\r\n meta: z.record(z.unknown()).optional(),\r\n}).strict();\r\n\r\n// ✅ 改后:工厂函数,接收 maxFiles 参数\r\nfunction makeSmartInitBodySchema(maxFiles: number) {\r\n return z.object({\r\n files: z.array(FileInitItemSchema).min(1).max(maxFiles),\r\n }).strict();\r\n}\r\n\r\n// ============================================================\r\n// 内部类型\r\n// ============================================================\r\n\r\ntype SmartInitItemResult =\r\n | { ok: false; index: number; filename: string; code: number; error: string; message: string }\r\n | {\r\n ok: true; index: number; filename: string; size: number;\r\n dedup: true;\r\n fileId: string; contentType: string; url: string;\r\n }\r\n | {\r\n ok: true; index: number; filename: string; size: number;\r\n dedup: false;\r\n uploadId: string; contentType: string; isPublic: boolean;\r\n partSize: number; totalParts: number; totalSize: number;\r\n strategy: 'ONE_SHOT' | 'MULTIPART';\r\n suggestedConcurrency: number;\r\n expiresAt: string; ttl: number;\r\n };\r\n\r\n// ============================================================\r\n// 单文件处理\r\n// ============================================================\r\n\r\nasync function processOneFile(\r\n req: FastifyRequest,\r\n rt: SecureUploadRuntime,\r\n file: z.infer<typeof FileInitItemSchema>,\r\n index: number\r\n): Promise<SmartInitItemResult> {\r\n try {\r\n // 1. 鉴权\r\n const { userId } = await resolveSession(req, file.isPublic, rt.validateSession);\r\n\r\n // 2. 校验 size\r\n if (file.size > rt.config.fileMaxSize) {\r\n throw new SecureError(\r\n ErrorCode.MULTIPART_TOTAL_TOO_LARGE,\r\n `文件 ${file.filename} 大小 ${file.size} 超过单文件限制 ${rt.config.fileMaxSize}`\r\n );\r\n }\r\n\r\n // 3. 秒传\r\n // 注意:必须严格按 (hash, isPublic, userId) 匹配,避免把\"另一种可见性/其他人\"的文件塞给当前上传者\r\n if (file.expectedHash) {\r\n const existing = await rt.fileRepo.findDedupCandidate(\r\n file.expectedHash,\r\n file.isPublic,\r\n userId\r\n );\r\n if (existing) {\r\n const diskOk = await rt.storage.exists(existing.storageKey).catch(() => false);\r\n if (diskOk) {\r\n req.log.info(\r\n { hash: file.expectedHash, fileId: existing.id, isPublic: file.isPublic, userId, index },\r\n '⚡ smart: 秒传命中(严格匹配可见性+身份)'\r\n );\r\n return {\r\n ok: true, index, filename: file.filename, size: Number(existing.size),\r\n dedup: true,\r\n fileId: existing.id,\r\n contentType: existing.contentType,\r\n url: `${rt.config.fileUrlPrefix}/${existing.id}`,\r\n };\r\n }\r\n // 磁盘丢失\r\n req.log.warn(\r\n { hash: file.expectedHash, fileId: existing.id, userId, index },\r\n '⚠️ smart: DB 命中但磁盘丢失,放弃秒传'\r\n );\r\n await rt.fileRepo\r\n .softDelete(existing.id, { orphanedAt: new Date().toISOString() })\r\n .catch((e: unknown) => {\r\n req.log.warn({ err: e, fileId: existing.id }, '⚠️ 软删除孤儿 record 失败');\r\n });\r\n }\r\n }\r\n\r\n // 4. 智能策略\r\n const strategy = chooseStrategy(file.size, rt.config.strategy);\r\n req.log.info(\r\n {\r\n index, filename: file.filename, size: file.size,\r\n strategy: strategy.kind, partSize: strategy.partSize, totalParts: strategy.totalParts,\r\n reason: strategy.reason,\r\n },\r\n `🧠 smart: 选策略 ${strategy.kind}(${strategy.reason})`\r\n );\r\n\r\n // 5. 创建 multipart 记录\r\n const rec = await rt.multipartRepo.createInit({\r\n userId,\r\n filename: file.filename,\r\n contentType: file.contentType,\r\n totalSize: file.size,\r\n partSize: strategy.partSize,\r\n totalParts: strategy.totalParts,\r\n isPublic: file.isPublic,\r\n expectedHash: file.expectedHash ?? null,\r\n meta: file.meta ?? null,\r\n ttlSeconds: rt.config.ttlSeconds,\r\n });\r\n\r\n return {\r\n ok: true, index, filename: file.filename, size: file.size,\r\n dedup: false,\r\n uploadId: rec.id,\r\n contentType: file.contentType,\r\n isPublic: file.isPublic,\r\n partSize: strategy.partSize,\r\n totalParts: strategy.totalParts,\r\n totalSize: file.size,\r\n strategy: strategy.kind,\r\n suggestedConcurrency: suggestConcurrency(strategy),\r\n expiresAt: rec.expiresAt.toISOString(),\r\n ttl: rt.config.ttlSeconds,\r\n };\r\n } catch (e: any) {\r\n const code = e instanceof SecureError ? e.code : (e?.code as number) ?? ErrorCode.INVALID_REQUEST;\r\n const message = e?.message ?? String(e);\r\n const errName = e instanceof SecureError ? e.name : 'INTERNAL_ERROR';\r\n return {\r\n ok: false, index, filename: file.filename,\r\n code, error: errName, message,\r\n };\r\n }\r\n}\r\n\r\n// ============================================================\r\n// 路由注册\r\n// ============================================================\r\n\r\nexport async function registerSmartRoutes(\r\n app: FastifyInstance,\r\n rt: SecureUploadRuntime\r\n): Promise<void> {\r\n if (!rt.config.enabled) {\r\n app.log.warn('⚠️ smart 上传已禁用 (enabled=false)');\r\n return;\r\n }\r\n\r\n const base = rt.config.smartPath;\r\n app.log.info(\r\n {\r\n routes: [`${base}/init`],\r\n maxFiles: rt.config.smartMaxFiles,\r\n rateLimit: rt.config.smartInitRateLimit,\r\n },\r\n '🔧 smart-routes: 注册 smart 上传路由(part/complete/abort/status 复用 multipart/*)...'\r\n );\r\n\r\n app.post(`${base}/init`, async (req: FastifyRequest) => {\r\n // ✅ 改后:从 rt.config 读,跟业务校验(line 194)同源\r\n const schema = makeSmartInitBodySchema(rt.config.smartMaxFiles);\r\n const body = schema.parse(req.body ?? {});\r\n\r\n if (body.files.length > rt.config.smartMaxFiles) {\r\n throw new SecureError(\r\n ErrorCode.BATCH_TOO_MANY_FILES,\r\n `单次 /smart/init 最多 ${rt.config.smartMaxFiles} 个文件,实际 ${body.files.length}`\r\n );\r\n }\r\n\r\n // 1.1 限流\r\n const sessionKeyForRl = (() => {\r\n const sr: any = (req as any).secureRequest;\r\n return sr?.session ?? (req.headers['x-secure-session'] as string | undefined) ?? 'anonymous';\r\n })();\r\n const allPublic = body.files.every((f) => f.isPublic);\r\n const userIdForRl = await resolveUserId(req, allPublic, rt.validateSession);\r\n const rlKey = `rl:mp:init:${userIdForRl ?? sessionKeyForRl.slice(0, 32)}`;\r\n\r\n for (let i = 0; i < body.files.length; i++) {\r\n const rl = rt.redis\r\n ? await slidingWindowRateLimit(rt.redis, rlKey, 60_000, rt.config.smartInitRateLimit)\r\n : memorySlidingWindowRateLimit(rlKey, 60_000, rt.config.smartInitRateLimit);\r\n\r\n if (!rl.ok) {\r\n req.log.warn(\r\n { remaining: 0, consumed: i, total: body.files.length, retryAfterMs: rl.retryAfterMs },\r\n '⚠️ smart: 批量配额不足,后续文件标记失败'\r\n );\r\n const results: SmartInitItemResult[] = [];\r\n for (let j = 0; j < i; j++) {\r\n const r = await processOneFile(req, rt, body.files[j], j);\r\n results.push(r);\r\n }\r\n for (let j = i; j < body.files.length; j++) {\r\n results.push({\r\n ok: false, index: j, filename: body.files[j].filename,\r\n code: ErrorCode.RATE_LIMITED,\r\n error: 'RATE_LIMITED',\r\n message: `批量配额不足,需 ${Math.ceil(rl.retryAfterMs / 1000)}s 后重试`,\r\n });\r\n }\r\n return {\r\n success: true,\r\n data: {\r\n files: results,\r\n summary: {\r\n total: body.files.length,\r\n ok: results.filter((r) => r.ok).length,\r\n failed: results.filter((r) => !r.ok).length,\r\n dedup: results.filter((r) => r.ok && (r as any).dedup === true).length,\r\n oneShot: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'ONE_SHOT').length,\r\n multipart: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'MULTIPART').length,\r\n rateLimited: body.files.length - i,\r\n },\r\n },\r\n };\r\n }\r\n }\r\n\r\n // 1.2 逐个文件处理\r\n const t0 = Date.now();\r\n const results: SmartInitItemResult[] = [];\r\n for (let i = 0; i < body.files.length; i++) {\r\n const f = body.files[i];\r\n const result = await processOneFile(req, rt, f, i);\r\n results.push(result);\r\n }\r\n\r\n // 1.3 汇总\r\n const summary = {\r\n total: results.length,\r\n ok: results.filter((r) => r.ok).length,\r\n failed: results.filter((r) => !r.ok).length,\r\n dedup: results.filter((r) => r.ok && (r as any).dedup === true).length,\r\n oneShot: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'ONE_SHOT').length,\r\n multipart: results.filter((r) => r.ok && (r as any).dedup === false && (r as any).strategy === 'MULTIPART').length,\r\n };\r\n\r\n req.log.info(\r\n { total: summary.total, ok: summary.ok, dedup: summary.dedup, oneShot: summary.oneShot, multipart: summary.multipart, ms: Date.now() - t0 },\r\n '✅ smart: init 汇总'\r\n );\r\n\r\n return { success: true, data: { files: results, summary } };\r\n });\r\n}\r\n\r\n// 重新导出 MultipartStatus / FileStatus(供 SDK 内部使用)\r\nexport { MultipartStatus, FileStatus };\r\n","/**\r\n * 后台清理过期 uploadId 任务\r\n *\r\n * - 每 intervalSeconds 扫描一次\r\n * - 把超时的 INIT/UPLOADING 标记为 EXPIRED + 清理磁盘\r\n */\r\n\r\nimport type { FastifyBaseLogger } from 'fastify';\r\nimport type { StorageAdapter } from './storage/types';\r\nimport type { MultipartRepository } from './repository/types';\r\n\r\nexport interface MultipartCleanerOptions {\r\n enabled: boolean;\r\n intervalSeconds: number;\r\n batchLimit: number;\r\n storage: StorageAdapter;\r\n multipartRepo: MultipartRepository;\r\n log: FastifyBaseLogger;\r\n onClose: (cb: () => void) => void;\r\n}\r\n\r\nexport function startMultipartCleanupTask(opts: MultipartCleanerOptions): void {\r\n if (!opts.enabled) {\r\n opts.log.info('⏸️ multipart-cleanup: 已禁用');\r\n return;\r\n }\r\n const intervalMs = opts.intervalSeconds * 1000;\r\n const tick = async () => {\r\n try {\r\n const expired = await opts.multipartRepo.listExpired(opts.batchLimit);\r\n if (expired.length === 0) return;\r\n opts.log.info({ count: expired.length }, '🧹 multipart cleanup: 发现过期 uploadId');\r\n for (const rec of expired) {\r\n try {\r\n await opts.multipartRepo.markExpired(rec.id);\r\n await opts.storage.cleanupMultipart(rec.id);\r\n opts.log.info({ uploadId: rec.id }, '🧹 multipart cleanup: 已清理');\r\n } catch (e) {\r\n opts.log.warn({ err: e, uploadId: rec.id }, '⚠️ multipart cleanup 单条失败');\r\n }\r\n }\r\n } catch (e) {\r\n opts.log.error({ err: e }, '❌ multipart cleanup tick 失败');\r\n }\r\n };\r\n setImmediate(tick);\r\n const timer = setInterval(tick, intervalMs);\r\n opts.onClose(() => clearInterval(timer));\r\n opts.log.info(\r\n { intervalSec: opts.intervalSeconds },\r\n '🧹 multipart cleanup task 已启动'\r\n );\r\n}\r\n","/**\r\n * 孤儿文件扫描任务\r\n *\r\n * 背景:\r\n * 秒传路径只查 DB,可能存在\"DB 有 / 磁盘无\"的不一致:\r\n * - 用户/运维手动 rm 磁盘文件\r\n * - 容器重启后丢失 volume mount\r\n * - 分布式存储同步失败\r\n *\r\n * 已有被动防御:秒传命中 DB 后 storage.exists() 二次校验\r\n * 本任务作为\"主动巡检\"兜底:定期扫一遍,自动 soft-delete,\r\n * 防止其他业务路径(如 /files/:id GET)又返回 404 \"文件已丢失\"。\r\n *\r\n * 触发:服务冷启动(setImmediate) + setInterval\r\n * 容错:\r\n * - 单条 storage.exists() 抛错 → 跳过该条,记 warn\r\n * - 单条 fileRepo.softDelete 失败 → 跳过该条,记 warn\r\n * - 整个 tick 失败 → 记 error,不影响下次\r\n */\r\n\r\nimport type { FastifyBaseLogger } from 'fastify';\r\nimport type { StorageAdapter } from './storage/types';\r\nimport type { FileRepository } from './repository/types';\r\n\r\nexport interface OrphanScannerOptions {\r\n enabled: boolean;\r\n intervalSeconds: number;\r\n lookbackDays: number;\r\n batchSize: number;\r\n storage: StorageAdapter;\r\n fileRepo: FileRepository;\r\n log: FastifyBaseLogger;\r\n onClose: (cb: () => void) => void;\r\n}\r\n\r\nexport function startOrphanFileScanTask(opts: OrphanScannerOptions): void {\r\n if (!opts.enabled) {\r\n opts.log.info('⏸️ orphan-file-scan: 已禁用');\r\n return;\r\n }\r\n const intervalMs = opts.intervalSeconds * 1000;\r\n const batchSize = opts.batchSize;\r\n\r\n const tick = async () => {\r\n try {\r\n const recent = await opts.fileRepo.listOrphans({\r\n sinceDays: opts.lookbackDays,\r\n limit: batchSize,\r\n });\r\n if (recent.length === 0) {\r\n opts.log.debug({ lookbackDays: opts.lookbackDays }, '🧹 orphan-file-scan: 无可扫记录');\r\n return;\r\n }\r\n let orphanCount = 0;\r\n const samples: Array<{ fileId: string; storageKey: string; size: number }> = [];\r\n for (const f of recent) {\r\n try {\r\n const ok = await opts.storage.exists(f.storageKey);\r\n if (!ok) {\r\n await opts.fileRepo.softDelete(f.id, { orphanedAt: new Date().toISOString() });\r\n orphanCount++;\r\n if (samples.length < 5) {\r\n samples.push({ fileId: f.id, storageKey: f.storageKey, size: Number(f.size) });\r\n }\r\n }\r\n } catch (e) {\r\n opts.log.warn(\r\n { err: e, fileId: f.id, storageKey: f.storageKey },\r\n '⚠️ orphan-file-scan: 单条检测失败'\r\n );\r\n }\r\n }\r\n if (orphanCount > 0) {\r\n opts.log.warn(\r\n { scanned: recent.length, orphans: orphanCount, sample: samples },\r\n '🧹 orphan-file-scan: 完成本轮'\r\n );\r\n } else {\r\n opts.log.info(\r\n { scanned: recent.length, orphans: 0 },\r\n '🧹 orphan-file-scan: 完成本轮(全部一致)'\r\n );\r\n }\r\n } catch (e) {\r\n opts.log.error({ err: e }, '❌ orphan-file-scan tick 失败');\r\n }\r\n };\r\n\r\n setImmediate(tick);\r\n const timer = setInterval(tick, intervalMs);\r\n opts.onClose(() => clearInterval(timer));\r\n opts.log.info(\r\n {\r\n intervalSec: opts.intervalSeconds,\r\n lookbackDays: opts.lookbackDays,\r\n batchSize,\r\n },\r\n '🧹 orphan-file-scan task 已启动'\r\n );\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC4BA,4BAAe;;;ACff,qBAAoE;AACpE,uBAAiB;AACjB,yBAA2B;AAcpB,IAAM,eAAN,MAA6C;AAAA,EAC1C;AAAA,EAER,YAAY,MAAoC;AAC9C,SAAK,UAAU,OAAO,SAAS,WAAW,OAAO,KAAK;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA,EAMQ,SAAS,UAAkB,aAA6B;AAC9D,UAAM,WAAW,iBAAAA,QAAK,QAAQ,QAAQ,EAAE,YAAY;AACpD,QAAI,SAAU,QAAO;AACrB,UAAM,MAA8B;AAAA,MAClC,aAAa;AAAA,MACb,cAAc;AAAA,MACd,aAAa;AAAA,MACb,cAAc;AAAA,MACd,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,cAAc;AAAA,MACd,oBAAoB;AAAA,MACpB,mBAAmB;AAAA,MACnB,aAAa;AAAA,IACf;AACA,WAAO,IAAI,WAAW,KAAK;AAAA,EAC7B;AAAA,EAEQ,YAAY,UAAkB,aAAqB,IAAoB;AAC7E,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,OAAO,IAAI,YAAY;AAC7B,UAAM,KAAK,OAAO,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,UAAM,KAAK,OAAO,IAAI,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AAChD,UAAM,MAAM,KAAK,SAAS,UAAU,WAAW;AAC/C,WAAO,GAAG,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,GAAG;AAAA,EACxC;AAAA,EAEQ,aAAa,UAA0B;AAC7C,WAAO,iBAAAA,QAAK,QAAQ,KAAK,SAAS,cAAc,QAAQ;AAAA,EAC1D;AAAA,EAEQ,SAAS,UAAkB,YAA4B;AAC7D,QAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GAAG;AACnD,YAAM,IAAI,MAAM,sCAA4B,UAAU,EAAE;AAAA,IAC1D;AACA,WAAO,iBAAAA,QAAK,KAAK,KAAK,aAAa,QAAQ,GAAG,QAAQ,OAAO,UAAU,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM;AAAA,EACjG;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,MAAc,MAAqD;AAC3E,UAAM,MAAM,KAAK,YAAY,KAAK,UAAU,KAAK,aAAa,KAAK,EAAE;AACrE,UAAM,eAAe,iBAAAA,QAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,UAAM,eAAAC,SAAG,MAAM,iBAAAD,QAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,eAAAC,SAAG,UAAU,cAAc,IAAI;AACrC,WAAO,EAAE,KAAK,aAAa;AAAA,EAC7B;AAAA,EAEA,IAAI,KAAyB;AAC3B,UAAM,eAAe,iBAAAD,QAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,eAAO,iCAAiB,YAAY;AAAA,EACtC;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,UAAM,eAAe,iBAAAA,QAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,QAAI;AACF,YAAM,eAAAC,SAAG,OAAO,YAAY;AAAA,IAC9B,SAAS,GAAQ;AACf,UAAI,EAAE,SAAS,SAAU,OAAM;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,KAA+B;AAC1C,UAAM,eAAe,iBAAAD,QAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,QAAI;AACF,YAAM,eAAAC,SAAG,OAAO,YAAY;AAC5B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QACJ,UACA,YACA,MACyC;AACzC,UAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,UAAM,eAAAA,SAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACvC,UAAM,YAAY,KAAK,SAAS,UAAU,UAAU;AACpD,UAAM,UAAU,GAAG,SAAS;AAC5B,QAAI;AACF,YAAM,eAAAA,SAAG,UAAU,SAAS,IAAI;AAChC,YAAM,eAAAA,SAAG,OAAO,SAAS,SAAS;AAAA,IACpC,SAAS,GAAG;AACV,UAAI;AAAE,cAAM,eAAAA,SAAG,OAAO,OAAO;AAAA,MAAG,QAAQ;AAAA,MAAe;AACvD,YAAM;AAAA,IACR;AACA,WAAO,EAAE,MAAM,KAAK,QAAQ,MAAM,UAAU;AAAA,EAC9C;AAAA,EAEA,MAAM,UAAU,UAAqC;AACnD,UAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,QAAI;AACF,YAAM,QAAQ,MAAM,eAAAA,SAAG,QAAQ,GAAG;AAClC,YAAM,OAAiB,CAAC;AACxB,iBAAW,KAAK,OAAO;AACrB,cAAM,IAAI,EAAE,MAAM,sBAAsB;AACxC,YAAI,EAAG,MAAK,KAAK,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC;AAAA,MACrC;AACA,WAAK,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AACzB,aAAO;AAAA,IACT,SAAS,GAAQ;AACf,UAAI,EAAE,SAAS,SAAU,QAAO,CAAC;AACjC,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,UAAkB,YAAqC;AACpE,UAAM,IAAI,KAAK,SAAS,UAAU,UAAU;AAC5C,UAAM,OAAO,MAAM,eAAAA,SAAG,KAAK,CAAC;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,WACJ,UACA,YACA,QACsB;AACtB,UAAM,MAAM,KAAK,YAAY,OAAO,UAAU,OAAO,aAAa,OAAO,EAAE;AAC3E,UAAM,eAAe,iBAAAD,QAAK,QAAQ,KAAK,SAAS,GAAG;AACnD,UAAM,eAAAC,SAAG,MAAM,iBAAAD,QAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,UAAU,GAAG,YAAY;AAE/B,UAAM,UAAM,kCAAkB,OAAO;AACrC,QAAI;AACF,eAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,cAAM,WAAW,KAAK,SAAS,UAAU,CAAC;AAC1C,YAAI;AACF,gBAAM,eAAAC,SAAG,OAAO,QAAQ;AAAA,QAC1B,QAAQ;AACN,gBAAM,IAAI,MAAM,qCAAsB,CAAC,cAAc,QAAQ,GAAG;AAAA,QAClE;AACA,cAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,gBAAM,SAAK,iCAAiB,QAAQ;AACpC,aAAG,GAAG,SAAS,MAAM;AACrB,aAAG,GAAG,OAAO,OAAO;AACpB,aAAG,KAAK,KAAK,EAAE,KAAK,MAAM,CAAC;AAAA,QAC7B,CAAC;AAAA,MACH;AACA,UAAI,IAAI;AACR,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,YAAI,GAAG,UAAU,MAAM,QAAQ,CAAC;AAChC,YAAI,GAAG,SAAS,MAAM;AAAA,MACxB,CAAC;AACD,YAAM,OAAO,MAAM,eAAAA,SAAG,KAAK,OAAO;AAClC,YAAM,eAAAA,SAAG,OAAO,SAAS,YAAY;AACrC,aAAO,EAAE,KAAK,cAAc,MAAM,KAAK,KAAK;AAAA,IAC9C,SAAS,GAAG;AACV,UAAI;AAAE,cAAM,eAAAA,SAAG,OAAO,OAAO;AAAA,MAAG,QAAQ;AAAA,MAAe;AACvD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,UAAiC;AACtD,UAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,QAAI;AACF,YAAM,eAAAA,SAAG,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,IACnD,SAAS,GAAQ;AACf,UAAI,EAAE,SAAS,UAAU;AACvB,gBAAQ,KAAK,gDAAgD,QAAQ,QAAQ,EAAE,OAAO,EAAE;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,kBAAkB,UAAkB,YAAqC;AAC7E,UAAM,WAAO,+BAAW,QAAQ;AAChC,aAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,YAAM,WAAW,KAAK,SAAS,UAAU,CAAC;AAC1C,YAAM,aAAS,iCAAiB,QAAQ;AACxC,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,eAAO,GAAG,QAAQ,CAAC,UAAU,KAAK,OAAO,KAAe,CAAC;AACzD,eAAO,GAAG,OAAO,OAAO;AACxB,eAAO,GAAG,SAAS,MAAM;AAAA,MAC3B,CAAC;AAAA,IACH;AACA,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B;AACF;;;AChNA,yBAAyB;AACzB,IAAAC,sBAA2B;AAQpB,IAAM,kBAAN,MAAgD;AAAA,EAC7C,QAAQ,oBAAI,IAAoB;AAAA,EAChC,QAAQ,oBAAI,IAAiC;AAAA,EAErD,MAAM,IAAI,MAAc,MAAqD;AAC3E,UAAM,MAAM,OAAO,KAAK,EAAE;AAC1B,SAAK,MAAM,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC;AACrC,WAAO,EAAE,IAAI;AAAA,EACf;AAAA,EAEA,IAAI,KAAoC;AACtC,UAAM,MAAM,KAAK,MAAM,IAAI,GAAG;AAC9B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+BAA+B,GAAG,EAAE;AAC9D,WAAO,4BAAS,KAAK,GAAG;AAAA,EAC1B;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AAAA,EAEA,MAAM,OAAO,KAA+B;AAC1C,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,MAAM,QACJ,UACA,YACA,MAC2B;AAC3B,QAAI,IAAI,KAAK,MAAM,IAAI,QAAQ;AAC/B,QAAI,CAAC,GAAG;AACN,UAAI,oBAAI,IAAI;AACZ,WAAK,MAAM,IAAI,UAAU,CAAC;AAAA,IAC5B;AACA,MAAE,IAAI,YAAY,OAAO,KAAK,IAAI,CAAC;AACnC,WAAO,EAAE,MAAM,KAAK,OAAO;AAAA,EAC7B;AAAA,EAEA,MAAM,UAAU,UAAqC;AACnD,UAAM,IAAI,KAAK,MAAM,IAAI,QAAQ;AACjC,QAAI,CAAC,EAAG,QAAO,CAAC;AAChB,WAAO,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAAA,EAC3C;AAAA,EAEA,MAAM,WACJ,UACA,YACA,QACsB;AACtB,UAAM,IAAI,KAAK,MAAM,IAAI,QAAQ;AACjC,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,QAAQ,EAAE;AACnE,UAAM,QAAkB,CAAC;AACzB,aAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,YAAM,IAAI,EAAE,IAAI,CAAC;AACjB,UAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,CAAC,EAAE;AAC5D,YAAM,KAAK,CAAC;AAAA,IACd;AACA,UAAM,SAAS,OAAO,OAAO,KAAK;AAClC,UAAM,MAAM,OAAO,OAAO,EAAE;AAC5B,SAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,WAAO,EAAE,KAAK,MAAM,OAAO,OAAO;AAAA,EACpC;AAAA,EAEA,MAAM,kBAAkB,UAAkB,YAAqC;AAC7E,UAAM,WAAO,gCAAW,QAAQ;AAChC,UAAM,IAAI,KAAK,MAAM,IAAI,QAAQ;AACjC,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,QAAQ,EAAE;AACnE,aAAS,IAAI,GAAG,KAAK,YAAY,KAAK;AACpC,YAAM,IAAI,EAAE,IAAI,CAAC;AACjB,UAAI,CAAC,EAAG,OAAM,IAAI,MAAM,iCAAiC,CAAC,EAAE;AAC5D,WAAK,OAAO,CAAC;AAAA,IACf;AACA,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B;AAAA,EAEA,MAAM,iBAAiB,UAAiC;AACtD,SAAK,MAAM,OAAO,QAAQ;AAAA,EAC5B;AACF;;;AC0CO,IAAM,kBAAkB;AAAA,EAC7B,MAAM;AAAA,EACN,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,IAAM,aAAa;AAAA,EACxB,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AAAA,EACX,QAAQ;AACV;;;ACvHA,SAAS,kBAAkB,KAA2B;AACpD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,aAAa,IAAI;AAAA,IACjB,WAAW,IAAI;AAAA,IACf,UAAU,IAAI;AAAA,IACd,YAAY,IAAI;AAAA,IAChB,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,IACZ,eAAe,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AAAA,IACvE,YAAY,IAAI,cAAc,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa,CAAC;AAAA,IACrF,MAAM,IAAI,QAAQ,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,IAC5D,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,aAAa,IAAI;AAAA,IACjB,WAAW,IAAI;AAAA,EACjB;AACF;AAEA,SAAS,aAAa,KAAsB;AAC1C,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,aAAa,IAAI;AAAA,IACjB,MAAM,IAAI;AAAA,IACV,YAAY,IAAI;AAAA,IAChB,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,YAAY,IAAI;AAAA,IAChB,aAAa,IAAI;AAAA,IACjB,MAAM,IAAI,QAAQ,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,IAC5D,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,EACjB;AACF;AAMO,IAAM,4BAAN,MAA+D;AAAA,EACpE,YAAoB,QAAmB;AAAnB;AAAA,EAAoB;AAAA,EAApB;AAAA,EAEpB,MAAM,WAAW,OAAkD;AACjE,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,aAAa,GAAI;AAC/D,UAAM,MAAM,MAAM,KAAK,OAAO,gBAAgB,OAAO;AAAA,MACnD,MAAM;AAAA,QACJ,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM;AAAA,QAChB,aAAa,MAAM;AAAA,QACnB,WAAW,OAAO,MAAM,SAAS;AAAA,QACjC,UAAU,MAAM;AAAA,QAChB,YAAY,MAAM;AAAA,QAClB,UAAU,MAAM;AAAA,QAChB,QAAQ,gBAAgB;AAAA,QACxB,eAAe,CAAC;AAAA,QAChB,YAAY,CAAC;AAAA,QACb,MAAM,MAAM,QAAQ;AAAA,QACpB,cAAc,MAAM,gBAAgB;AAAA,QACpC;AAAA,MACF;AAAA,IACF,CAAC;AACD,WAAO,kBAAkB,GAAG;AAAA,EAC9B;AAAA,EAEA,MAAM,SAAS,IAA6C;AAC1D,UAAM,MAAM,MAAM,KAAK,OAAO,gBAAgB,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC1E,WAAO,MAAM,kBAAkB,GAAG,IAAI;AAAA,EACxC;AAAA,EAEA,MAAM,QACJ,IACA,YACA,UACyB;AACzB,WAAO,MAAM,KAAK,OAAO,aAAa,OAAO,OAAkB;AAC7D,YAAM,MAAW,MAAM,GAAG,gBAAgB,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AACtE,UAAI,CAAC,IAAK,OAAM,IAAI,MAAM,WAAW;AAErC,YAAM,WAAqB,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AACnF,YAAM,SACJ,IAAI,cAAc,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa,CAAC;AAE3E,UAAI,SAAS,SAAS,UAAU,GAAG;AACjC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,WAAW;AAAA,UACX,cAAc,OAAO,OAAO,UAAU,CAAC,KAAK;AAAA,UAC5C,QAAQ,kBAAkB,GAAG;AAAA,QAC/B;AAAA,MACF;AAEA,YAAM,OAAO,CAAC,GAAG,UAAU,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC3D,YAAM,aAAa,EAAE,GAAG,OAAO;AAC/B,UAAI,SAAU,YAAW,OAAO,UAAU,CAAC,IAAI;AAE/C,YAAM,YACJ,IAAI,WAAW,gBAAgB,OAAO,gBAAgB,YAAY,IAAI;AAExE,UACE,IAAI,WAAW,gBAAgB,aAC/B,IAAI,WAAW,gBAAgB,WAC/B,IAAI,WAAW,gBAAgB,WAC/B,IAAI,WAAW,gBAAgB,QAC/B;AACA,cAAM,IAAI,MAAM,gBAAgB;AAAA,MAClC;AAEA,YAAM,UAAU,MAAM,GAAG,gBAAgB,OAAO;AAAA,QAC9C,OAAO,EAAE,GAAG;AAAA,QACZ,MAAM;AAAA,UACJ,eAAe;AAAA,UACf,YAAY;AAAA,UACZ,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AACD,aAAO,EAAE,SAAS,MAAe,QAAQ,kBAAkB,OAAO,EAAE;AAAA,IACtE,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,cAAc,IAAY,QAA0C;AACxE,UAAM,MAAM,MAAM,KAAK,OAAO,gBAAgB,WAAW;AAAA,MACvD,OAAO,EAAE,IAAI,QAAQ,EAAE,IAAI,CAAC,gBAAgB,MAAM,gBAAgB,SAAS,EAAE,EAAE;AAAA,MAC/E,MAAM;AAAA,QACJ,QAAQ,gBAAgB;AAAA,QACxB;AAAA,QACA,aAAa,oBAAI,KAAK;AAAA,MACxB;AAAA,IACF,CAAC;AACD,QAAI,IAAI,UAAU,GAAG;AACnB,YAAM,MAAM,MAAM,KAAK,OAAO,gBAAgB,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC1E,UAAI,CAAC,IAAK,OAAM,IAAI,MAAM,WAAW;AACrC,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AACA,UAAM,QAAQ,MAAM,KAAK,OAAO,gBAAgB,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC5E,WAAO,kBAAkB,KAAK;AAAA,EAChC;AAAA,EAEA,MAAM,YAAY,IAAsC;AACtD,UAAM,MAAM,MAAM,KAAK,OAAO,gBAAgB,WAAW;AAAA,MACvD,OAAO,EAAE,IAAI,QAAQ,EAAE,IAAI,CAAC,gBAAgB,MAAM,gBAAgB,SAAS,EAAE,EAAE;AAAA,MAC/E,MAAM;AAAA,QACJ,QAAQ,gBAAgB;AAAA,QACxB,WAAW,oBAAI,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AACD,QAAI,IAAI,UAAU,GAAG;AACnB,YAAM,MAAM,MAAM,KAAK,OAAO,gBAAgB,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC1E,UAAI,CAAC,IAAK,OAAM,IAAI,MAAM,WAAW;AACrC,UAAI,IAAI,WAAW,gBAAgB,UAAW,OAAM,IAAI,MAAM,mBAAmB;AACjF,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AACA,UAAM,QAAQ,MAAM,KAAK,OAAO,gBAAgB,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC5E,WAAO,kBAAkB,KAAK;AAAA,EAChC;AAAA,EAEA,MAAM,YAAY,IAA2B;AAC3C,UAAM,KAAK,OAAO,gBAAgB,WAAW;AAAA,MAC3C,OAAO,EAAE,IAAI,QAAQ,EAAE,IAAI,CAAC,gBAAgB,MAAM,gBAAgB,SAAS,EAAE,EAAE;AAAA,MAC/E,MAAM;AAAA,QACJ,QAAQ,gBAAgB;AAAA,QACxB,WAAW,oBAAI,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,YAAY,OAA2C;AAC3D,UAAM,OAAO,MAAM,KAAK,OAAO,gBAAgB,SAAS;AAAA,MACtD,OAAO;AAAA,QACL,QAAQ,EAAE,IAAI,CAAC,gBAAgB,MAAM,gBAAgB,SAAS,EAAE;AAAA,QAChE,WAAW,EAAE,IAAI,oBAAI,KAAK,EAAE;AAAA,MAC9B;AAAA,MACA,MAAM;AAAA,MACN,SAAS,EAAE,WAAW,MAAM;AAAA,IAC9B,CAAC;AACD,WAAO,KAAK,IAAI,iBAAiB;AAAA,EACnC;AACF;AAMO,IAAM,uBAAN,MAAqD;AAAA,EAC1D,YAAoB,QAAmB;AAAnB;AAAA,EAAoB;AAAA,EAApB;AAAA,EAEpB,MAAM,WAAW,MAA0C;AACzD,UAAM,MAAM,MAAM,KAAK,OAAO,KAAK,UAAU;AAAA,MAC3C,OAAO,EAAE,aAAa,MAAM,WAAW,KAAK;AAAA,IAC9C,CAAC;AACD,WAAO,MAAM,aAAa,GAAG,IAAI;AAAA,EACnC;AAAA,EAEA,MAAM,mBACJ,MACA,UACA,QAC4B;AAC5B,UAAM,MAAM,MAAM,KAAK,OAAO,KAAK,UAAU;AAAA,MAC3C,OAAO;AAAA,QACL,aAAa;AAAA,QACb,WAAW;AAAA,QACX;AAAA,QACA,GAAI,WAAW,CAAC,IAAI,EAAE,OAAO;AAAA,MAC/B;AAAA,MACA,SAAS,EAAE,WAAW,MAAM;AAAA,IAC9B,CAAC;AACD,WAAO,MAAM,aAAa,GAAG,IAAI;AAAA,EACnC;AAAA,EAEA,MAAM,SAAS,IAAwC;AACrD,UAAM,MAAM,MAAM,KAAK,OAAO,KAAK,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC/D,WAAO,MAAM,aAAa,GAAG,IAAI;AAAA,EACnC;AAAA,EAEA,MAAM,OAAO,OAA6C;AACxD,UAAM,MAAM,MAAM,KAAK,OAAO,KAAK,OAAO;AAAA,MACxC,MAAM;AAAA,QACJ,IAAI,MAAM;AAAA,QACV,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM;AAAA,QAChB,aAAa,MAAM;AAAA,QACnB,MAAM,OAAO,MAAM,IAAI;AAAA,QACvB,YAAY,MAAM;AAAA,QAClB,UAAU,MAAM;AAAA,QAChB,QAAQ,WAAW;AAAA,QACnB,UAAU,MAAM;AAAA,QAChB,YAAY,MAAM;AAAA,QAClB,aAAa,MAAM;AAAA,QACnB,MAAM,MAAM,QAAQ;AAAA,MACtB;AAAA,IACF,CAAC;AACD,WAAO,aAAa,GAAG;AAAA,EACzB;AAAA,EAEA,MAAM,WAAW,IAAY,WAAoD;AAC/E,QAAI,WAAW;AACb,YAAM,MAAM,MAAM,KAAK,OAAO,KAAK,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC/D,YAAM,OAAO,EAAE,GAAK,KAAK,QAAgB,CAAC,GAAI,GAAG,UAAU;AAC3D,YAAM,KAAK,OAAO,KAAK,OAAO;AAAA,QAC5B,OAAO,EAAE,GAAG;AAAA,QACZ,MAAM,EAAE,WAAW,oBAAI,KAAK,GAAG,MAAM,KAAK;AAAA,MAC5C,CAAC;AAAA,IACH,OAAO;AACL,YAAM,KAAK,OAAO,KAAK,OAAO;AAAA,QAC5B,OAAO,EAAE,GAAG;AAAA,QACZ,MAAM,EAAE,WAAW,oBAAI,KAAK,EAAE;AAAA,MAChC,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,MAAmE;AACnF,UAAM,aAAa,KAAK,YAAY,KAAK,OAAO;AAChD,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,SAAS;AAAA,MAC3C,OAAO;AAAA,QACL,WAAW;AAAA,QACX,YAAY,EAAE,KAAK,GAAG;AAAA,QACtB,WAAW,EAAE,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,EAAE;AAAA,MACrD;AAAA,MACA,QAAQ;AAAA,QACN,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,aAAa;AAAA,QACb,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,aAAa;AAAA,QACb,MAAM;AAAA,QACN,WAAW;AAAA,QACX,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,MACA,MAAM,KAAK;AAAA,MACX,SAAS,EAAE,WAAW,OAAO;AAAA,IAC/B,CAAC;AACD,WAAO,KAAK,IAAI,YAAY;AAAA,EAC9B;AACF;;;AChTO,IAAM,8BAAN,MAAiE;AAAA,EAC9D,QAAQ,oBAAI,IAA6B;AAAA,EACzC,YAAY;AAAA,EAEZ,SAAiB;AACvB,WAAO,MAAM,KAAK,IAAI,CAAC,IAAI,EAAE,KAAK,SAAS;AAAA,EAC7C;AAAA,EAEA,MAAM,WAAW,OAAkD;AACjE,UAAM,KAAK,KAAK,OAAO;AACvB,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,MAAuB;AAAA,MAC3B;AAAA,MACA,QAAQ,MAAM;AAAA,MACd,UAAU,MAAM;AAAA,MAChB,aAAa,MAAM;AAAA,MACnB,WAAW,OAAO,MAAM,SAAS;AAAA,MACjC,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,QAAQ,gBAAgB;AAAA,MACxB,eAAe,CAAC;AAAA,MAChB,YAAY,CAAC;AAAA,MACb,MAAM,MAAM,QAAQ;AAAA,MACpB,cAAc,MAAM,gBAAgB;AAAA,MACpC,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,WAAW;AAAA,MACX,WAAW,IAAI,KAAK,IAAI,QAAQ,IAAI,MAAM,aAAa,GAAI;AAAA,MAC3D,aAAa;AAAA,MACb,WAAW;AAAA,IACb;AACA,SAAK,MAAM,IAAI,IAAI,GAAG;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,IAA6C;AAC1D,WAAO,KAAK,MAAM,IAAI,EAAE,KAAK;AAAA,EAC/B;AAAA,EAEA,MAAM,QACJ,IACA,YACA,UACyB;AACzB,UAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AAC7B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,WAAW;AAErC,QACE,IAAI,WAAW,gBAAgB,aAC/B,IAAI,WAAW,gBAAgB,WAC/B,IAAI,WAAW,gBAAgB,WAC/B,IAAI,WAAW,gBAAgB,QAC/B;AACA,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AAEA,QAAI,IAAI,cAAc,SAAS,UAAU,GAAG;AAC1C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,WAAW;AAAA,QACX,cAAc,IAAI,WAAW,OAAO,UAAU,CAAC,KAAK;AAAA,QACpD,QAAQ;AAAA,MACV;AAAA,IACF;AAEA,UAAM,OAAO,CAAC,GAAG,IAAI,eAAe,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AACpE,UAAM,aAAa,EAAE,GAAG,IAAI,WAAW;AACvC,QAAI,SAAU,YAAW,OAAO,UAAU,CAAC,IAAI;AAE/C,UAAM,UAA2B;AAAA,MAC/B,GAAG;AAAA,MACH,eAAe;AAAA,MACf,YAAY;AAAA,MACZ,QAAQ,IAAI,WAAW,gBAAgB,OAAO,gBAAgB,YAAY,IAAI;AAAA,MAC9E,WAAW,oBAAI,KAAK;AAAA,IACtB;AACA,SAAK,MAAM,IAAI,IAAI,OAAO;AAC1B,WAAO,EAAE,SAAS,MAAM,QAAQ,QAAQ;AAAA,EAC1C;AAAA,EAEA,MAAM,cAAc,IAAY,QAA0C;AACxE,UAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AAC7B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,WAAW;AACrC,QAAI,IAAI,WAAW,gBAAgB,QAAQ,IAAI,WAAW,gBAAgB,WAAW;AACnF,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AACA,UAAM,UAA2B;AAAA,MAC/B,GAAG;AAAA,MACH,QAAQ,gBAAgB;AAAA,MACxB;AAAA,MACA,aAAa,oBAAI,KAAK;AAAA,MACtB,WAAW,oBAAI,KAAK;AAAA,IACtB;AACA,SAAK,MAAM,IAAI,IAAI,OAAO;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,IAAsC;AACtD,UAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AAC7B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,WAAW;AACrC,QAAI,IAAI,WAAW,gBAAgB,UAAW,OAAM,IAAI,MAAM,mBAAmB;AACjF,QAAI,IAAI,WAAW,gBAAgB,QAAQ,IAAI,WAAW,gBAAgB,WAAW;AACnF,YAAM,IAAI,MAAM,gBAAgB;AAAA,IAClC;AACA,UAAM,UAA2B;AAAA,MAC/B,GAAG;AAAA,MACH,QAAQ,gBAAgB;AAAA,MACxB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB;AACA,SAAK,MAAM,IAAI,IAAI,OAAO;AAC1B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,IAA2B;AAC3C,UAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AAC7B,QAAI,CAAC,IAAK;AACV,QAAI,IAAI,WAAW,gBAAgB,QAAQ,IAAI,WAAW,gBAAgB,UAAW;AACrF,SAAK,MAAM,IAAI,IAAI;AAAA,MACjB,GAAG;AAAA,MACH,QAAQ,gBAAgB;AAAA,MACxB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,YAAY,OAA2C;AAC3D,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,OAA0B,CAAC;AACjC,eAAW,OAAO,KAAK,MAAM,OAAO,GAAG;AACrC,WACG,IAAI,WAAW,gBAAgB,QAAQ,IAAI,WAAW,gBAAgB,cACvE,IAAI,UAAU,QAAQ,IAAI,KAC1B;AACA,aAAK,KAAK,GAAG;AAAA,MACf;AAAA,IACF;AACA,SAAK,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,CAAC;AACjE,WAAO,KAAK,MAAM,GAAG,KAAK;AAAA,EAC5B;AACF;AAMO,IAAM,yBAAN,MAAuD;AAAA,EACpD,QAAQ,oBAAI,IAAwB;AAAA,EACpC,YAAY;AAAA,EAEZ,SAAiB;AACvB,WAAO,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,KAAK,SAAS;AAAA,EAC5C;AAAA,EAEA,MAAM,WAAW,MAA0C;AACzD,eAAW,KAAK,KAAK,MAAM,OAAO,GAAG;AACnC,UAAI,EAAE,gBAAgB,QAAQ,CAAC,EAAE,UAAW,QAAO;AAAA,IACrD;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,mBACJ,MACA,UACA,QAC4B;AAC5B,eAAW,KAAK,KAAK,MAAM,OAAO,GAAG;AACnC,UAAI,EAAE,UAAW;AACjB,UAAI,EAAE,gBAAgB,KAAM;AAC5B,UAAI,EAAE,aAAa,SAAU;AAC7B,UAAI,CAAC,YAAY,EAAE,WAAW,OAAQ;AACtC,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,IAAwC;AACrD,WAAO,KAAK,MAAM,IAAI,EAAE,KAAK;AAAA,EAC/B;AAAA,EAEA,MAAM,OAAO,OAA6C;AACxD,QAAI,KAAK,MAAM;AACf,QAAI,CAAC,GAAI,MAAK,KAAK,OAAO;AAC1B,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,MAAkB;AAAA,MACtB;AAAA,MACA,QAAQ,MAAM;AAAA,MACd,UAAU,MAAM;AAAA,MAChB,aAAa,MAAM;AAAA,MACnB,MAAM,OAAO,MAAM,IAAI;AAAA,MACvB,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,QAAQ,WAAW;AAAA,MACnB,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM;AAAA,MAClB,aAAa,MAAM;AAAA,MACnB,MAAM,MAAM,QAAQ;AAAA,MACpB,WAAW;AAAA,MACX,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AACA,SAAK,MAAM,IAAI,IAAI,GAAG;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,IAAY,WAAoD;AAC/E,UAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AAC7B,QAAI,CAAC,IAAK;AACV,UAAM,OAAmB;AAAA,MACvB,GAAG;AAAA,MACH,MAAM,YAAY,EAAE,GAAI,IAAI,QAAQ,CAAC,GAAI,GAAG,UAAU,IAAI,IAAI;AAAA,MAC9D,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB;AACA,SAAK,MAAM,IAAI,IAAI,IAAI;AAAA,EACzB;AAAA,EAEA,MAAM,YAAY,MAAmE;AACnF,UAAM,UAAU,KAAK,YAAY,KAAK,OAAO;AAC7C,UAAM,OAAqB,CAAC;AAC5B,eAAW,KAAK,KAAK,MAAM,OAAO,GAAG;AACnC,UAAI,EAAE,UAAW;AACjB,UAAI,EAAE,UAAU,QAAQ,IAAI,KAAK,IAAI,IAAI,QAAS;AAClD,WAAK,KAAK,CAAC;AAAA,IACb;AACA,SAAK,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,CAAC;AACjE,WAAO,KAAK,MAAM,GAAG,KAAK,KAAK;AAAA,EACjC;AACF;;;AC3MO,IAAM,0BAA0C;AAAA,EACrD,YAAY,OAAO;AAAA;AAAA,EACnB,iBAAiB,OAAO;AAAA;AAAA,EACxB,kBAAkB,QAAQ;AAAA;AAAA,EAC1B,iBAAiB,IAAI,OAAO;AAAA;AAAA,EAC5B,mBAAmB,KAAK,QAAQ;AAAA;AAAA,EAChC,kBAAkB,KAAK,OAAO;AAAA;AAChC;AAaO,SAAS,eACd,WACA,MAAsB,yBACN;AAChB,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,YAAY,GAAG;AAChD,UAAM,IAAI,MAAM,sCAA4B,SAAS,kDAAe;AAAA,EACtE;AACA,iBAAe,GAAG;AAElB,MAAI,cAAc,GAAG;AACnB,WAAO,EAAE,MAAM,YAAY,UAAU,GAAG,YAAY,GAAG,QAAQ,gBAAM;AAAA,EACvE;AAEA,MAAI,aAAa,IAAI,YAAY;AAC/B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,QAAQ,UAAK,IAAI,UAAU;AAAA,IAC7B;AAAA,EACF;AAEA,MAAI,YAAY,IAAI,mBAAmB;AACrC,UAAMC,YAAW,IAAI;AACrB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAAA;AAAA,MACA,YAAY,KAAK,KAAK,YAAYA,SAAQ;AAAA,MAC1C,QAAQ,KAAK,IAAI,iBAAiB,iBAAYA,SAAQ;AAAA,IACxD;AAAA,EACF;AAEA,MAAI,YAAY,IAAI,kBAAkB;AACpC,UAAMA,YAAW,IAAI;AACrB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAAA;AAAA,MACA,YAAY,KAAK,KAAK,YAAYA,SAAQ;AAAA,MAC1C,QAAQ,KAAK,IAAI,gBAAgB,gBAAWA,SAAQ;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,WAAW,IAAI;AACrB,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,YAAY,KAAK,KAAK,YAAY,QAAQ;AAAA,IAC1C,QAAQ,UAAK,IAAI,gBAAgB,oBAAe,QAAQ;AAAA,EAC1D;AACF;AAQO,SAAS,mBAAmB,GAA2B;AAC5D,MAAI,EAAE,SAAS,WAAY,QAAO;AAClC,MAAI,EAAE,cAAc,EAAG,QAAO,KAAK,IAAI,GAAG,EAAE,UAAU;AACtD,SAAO;AACT;AAEA,SAAS,eAAe,KAA2B;AACjD,QAAM,SAAgD;AAAA,IACpD,CAAC,cAAc,YAAY;AAAA,IAC3B,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,oBAAoB,kBAAkB;AAAA,IACvC,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,qBAAqB,mBAAmB;AAAA,IACzC,CAAC,oBAAoB,kBAAkB;AAAA,EACzC;AACA,aAAW,CAAC,GAAG,IAAI,KAAK,QAAQ;AAC9B,QAAI,CAAC,OAAO,SAAS,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,GAAG;AAC3C,YAAM,IAAI,MAAM,kBAAkB,IAAI,kBAAQ,IAAI,CAAC,CAAC,oBAAU;AAAA,IAChE;AAAA,EACF;AACA,MAAI,IAAI,aAAa,IAAI,iBAAiB;AACxC,UAAM,IAAI;AAAA,MACR,yBAAyB,IAAI,UAAU,oCAA0B,IAAI,eAAe;AAAA,IACtF;AAAA,EACF;AACA,MAAI,IAAI,oBAAoB,IAAI,mBAAmB;AACjD,UAAM,IAAI;AAAA,MACR,+BAA+B,IAAI,gBAAgB,sCAA4B,IAAI,iBAAiB;AAAA,IACtG;AAAA,EACF;AACA,MAAI,IAAI,kBAAkB,IAAI,iBAAiB;AAC7C,UAAM,IAAI;AAAA,MACR,8BAA8B,IAAI,eAAe,oCAA0B,IAAI,eAAe;AAAA,IAChG;AAAA,EACF;AACA,MAAI,IAAI,kBAAkB,IAAI,kBAAkB;AAC9C,UAAM,IAAI;AAAA,MACR,8BAA8B,IAAI,eAAe,qCAA2B,IAAI,gBAAgB;AAAA,IAClG;AAAA,EACF;AACF;;;ACxJO,IAAK,YAAL,kBAAKC,eAAL;AAEL,EAAAA,sBAAA,qBAAqB,QAArB;AACA,EAAAA,sBAAA,oBAAqB,QAArB;AACA,EAAAA,sBAAA,eAAqB,QAArB;AACA,EAAAA,sBAAA,eAAqB,QAArB;AACA,EAAAA,sBAAA,kBAAqB,QAArB;AAGA,EAAAA,sBAAA,0BAA2B,QAA3B;AACA,EAAAA,sBAAA,8BAA2B,QAA3B;AACA,EAAAA,sBAAA,0BAA2B,QAA3B;AACA,EAAAA,sBAAA,wBAA2B,QAA3B;AAGA,EAAAA,sBAAA,iBAA2B,QAA3B;AACA,EAAAA,sBAAA,0BAA2B,QAA3B;AACA,EAAAA,sBAAA,2BAA2B,QAA3B;AACA,EAAAA,sBAAA,wBAA2B,QAA3B;AAGA,EAAAA,sBAAA,yBAAmC,QAAnC;AACA,EAAAA,sBAAA,iCAAmC,QAAnC;AACA,EAAAA,sBAAA,+BAAmC,QAAnC;AACA,EAAAA,sBAAA,uBAAmC,QAAnC;AACA,EAAAA,sBAAA,8BAAmC,QAAnC;AACA,EAAAA,sBAAA,+BAAmC,QAAnC;AACA,EAAAA,sBAAA,8BAAmC,QAAnC;AACA,EAAAA,sBAAA,8BAAmC,QAAnC;AACA,EAAAA,sBAAA,mCAAmC,QAAnC;AACA,EAAAA,sBAAA,8BAAmC,QAAnC;AACA,EAAAA,sBAAA,kCAAmC,QAAnC;AACA,EAAAA,sBAAA,mCAAmC,QAAnC;AACA,EAAAA,sBAAA,gCAAmC,QAAnC;AACA,EAAAA,sBAAA,6BAAmC,QAAnC;AACA,EAAAA,sBAAA,2BAAmC,QAAnC;AAnCU,SAAAA;AAAA,GAAA;AAsCZ,IAAM,OAAkC;AAAA,EACtC,CAAC,0BAAyB,GAAG;AAAA,EAC7B,CAAC,yBAAwB,GAAG;AAAA,EAC5B,CAAC,oBAAmB,GAAG;AAAA,EACvB,CAAC,oBAAmB,GAAG;AAAA,EACvB,CAAC,uBAAsB,GAAG;AAAA;AAAA,EAE1B,CAAC,+BAA8B,GAAG;AAAA,EAClC,CAAC,mCAAkC,GAAG;AAAA,EACtC,CAAC,+BAA8B,GAAG;AAAA,EAClC,CAAC,6BAA4B,GAAG;AAAA;AAAA,EAEhC,CAAC,sBAAqB,GAAG;AAAA,EACzB,CAAC,+BAA8B,GAAG;AAAA,EAClC,CAAC,gCAA+B,GAAG;AAAA,EACnC,CAAC,6BAA4B,GAAG;AAAA;AAAA,EAEhC,CAAC,8BAA6B,GAAG;AAAA,EACjC,CAAC,sCAAqC,GAAG;AAAA,EACzC,CAAC,oCAAmC,GAAG;AAAA,EACvC,CAAC,4BAA2B,GAAG;AAAA,EAC/B,CAAC,mCAAkC,GAAG;AAAA,EACtC,CAAC,oCAAmC,GAAG;AAAA,EACvC,CAAC,mCAAkC,GAAG;AAAA,EACtC,CAAC,mCAAkC,GAAG;AAAA,EACtC,CAAC,wCAAuC,GAAG;AAAA,EAC3C,CAAC,mCAAkC,GAAG;AAAA,EACtC,CAAC,uCAAsC,GAAG;AAAA,EAC1C,CAAC,wCAAuC,GAAG;AAAA,EAC3C,CAAC,qCAAoC,GAAG;AAAA,EACxC,CAAC,kCAAiC,GAAG;AAAA,EACrC,CAAC,gCAA+B,GAAG;AACrC;AAEO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YAAmB,MAAiB,SAAkB;AACpD,UAAM,WAAW,UAAU,IAAI,CAAC;AADf;AAEjB,SAAK,OAAO;AAAA,EACd;AAAA,EAHmB;AAAA,EAInB,IAAI,aAAqB;AACvB,WAAO,KAAK,KAAK,IAAI;AAAA,EACvB;AACF;AAEO,SAAS,cAAc,MAAyB;AACrD,SAAO,KAAK,IAAI;AAClB;;;AChFO,SAAS,gBAAgB,OAAkE;AAChG,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,WAAO,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,GAAG,WAAW,EAAE,SAAS,OAAO,CAAC;AAAA,EACxE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,IAAM,0BAA4C,CAAC,OAAO,aAAa;AAC5E,MAAI,SAAU,QAAO;AACrB,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,6CAA4C,4DAA8B;AAAA,EACtF;AACA,QAAM,IAAI,gBAAgB,KAAK;AAC/B,MAAI,CAAC,GAAG,IAAK,OAAM,IAAI,2CAA0C,kCAAc;AAC/E,MAAI,EAAE,OAAO,EAAE,MAAM,MAAO,KAAK,IAAI,GAAG;AACtC,UAAM,IAAI,2CAA0C,4BAAa;AAAA,EACnE;AACA,SAAO,OAAO,EAAE,GAAG;AACrB;;;ACnBA,IAAAC,sBAAmB;AACnB,iBAAkB;;;ACIlB,eAAsB,eACpB,KACA,UACA,UAC4B;AAC5B,QAAM,KAAW,IAAY;AAC7B,QAAM,UAAU,IAAI,WAAY,IAAI,QAAQ,kBAAkB;AAC9D,QAAM,SAAS,MAAM,SAAS,SAAS,QAAQ;AAC/C,QAAM,aAAa,WAAW;AAC9B,SAAO,EAAE,QAAQ,UAAU,MAAM,WAAW;AAC9C;AASA,eAAsB,cACpB,KACA,UACA,UACwB;AACxB,QAAM,KAAW,IAAY;AAC7B,QAAM,UAAU,IAAI,WAAY,IAAI,QAAQ,kBAAkB;AAC9D,SAAO,MAAM,SAAS,SAAS,QAAQ;AACzC;AAOO,SAAS,gBAAgB,KAA6D;AAC3F,QAAM,KAAW,IAAY;AAC7B,MAAI,IAAI,KAAK,MAAM;AACjB,WAAO,EAAE,MAAM,GAAG,IAAI,MAAgB,cAAc,OAAO,GAAG,IAAI,QAAS,GAAG,IAAI,KAAgB,MAAM,EAAE;AAAA,EAC5G;AAEA,QAAM,YAAY,IAAI,QAAQ,qBAAqB;AACnD,QAAM,eAAe,OAAO,IAAI,QAAQ,yBAAyB,KAAK,CAAC;AACvE,MAAI,CAAC,WAAW;AACd,UAAM,IAAI;AAAA;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,MAAM,OAAO,KAAK,WAAW,QAAQ,GAAG,aAAa;AAChE;AAOO,SAAS,eAAe,KAI7B;AACA,QAAM,KAAW,IAAY;AAC7B,QAAM,MAAM,IAAI,OAAQ,IAAI,QAAgB,CAAC;AAC7C,QAAM,WAAW,OAAO,IAAI,YAAY,EAAE,EAAE,KAAK;AACjD,QAAM,gBAAgB,IAAI;AAC1B,QAAM,WAA0B,IAAI,YAAY;AAChD,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,wCAAuC,uBAAa;AAAA,EAChE;AACA,QAAM,aAAa,OAAO,aAAa;AACvC,MAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GAAG;AACnD,UAAM,IAAI,sDAAqD,4BAAkB,aAAa,EAAE;AAAA,EAClG;AACA,SAAO,EAAE,UAAU,YAAY,SAAS;AAC1C;;;ACtFA,IAAM,gCAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BtC,IAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQzB,IAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQzB,eAAsB,uBACpB,OACA,KACA,UACA,OACmE;AACnE,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,SAAS,GAAG,GAAG,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAChE,QAAM,SAAU,MAAM,MAAM;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,QAAQ;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,OAAO,GAAG;AAAA,IACV;AAAA,EACF;AAEA,QAAM,KAAK,OAAO,CAAC,MAAM;AACzB,SAAO;AAAA,IACL;AAAA,IACA,cAAc,KAAK,IAAI,OAAO,OAAO,CAAC,KAAK,CAAC;AAAA,IAC5C,WAAW,KAAK,QAAQ,IAAI;AAAA,EAC9B;AACF;AAEA,eAAsB,YACpB,OACA,KACA,QAAQ,KAC2D;AACnE,QAAM,QAAQ,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACtE,QAAM,KAAK,MAAM,MAAM,KAAK,kBAAkB,GAAG,KAAK,OAAO,OAAO,KAAK,CAAC;AAC1E,MAAI,OAAO,EAAG,QAAO,EAAE,IAAI,MAAM,MAAM;AACvC,SAAO,EAAE,IAAI,OAAO,OAAO,KAAK;AAClC;AAEA,eAAsB,YAAY,OAAc,KAAa,OAAiC;AAC5F,QAAM,IAAI,MAAM,MAAM,KAAK,kBAAkB,GAAG,KAAK,KAAK;AAC1D,SAAO,MAAM;AACf;AAEA,eAAsB,SACpB,OACA,KACA,OACA,IACA,UAAU,GACE;AACZ,MAAI;AACJ,WAAS,IAAI,GAAG,KAAK,SAAS,KAAK;AACjC,UAAMC,QAAO,MAAM,YAAY,OAAO,KAAK,KAAK;AAChD,QAAIA,MAAK,IAAI;AACX,UAAI;AACF,eAAO,MAAM,GAAG;AAAA,MAClB,UAAE;AACA,cAAM,YAAY,OAAO,KAAKA,MAAK,KAAK,EAAE,MAAM,MAAM;AAAA,QAEtD,CAAC;AAAA,MACH;AAAA,IACF;AACA,cAAU,IAAI,MAAM,uCAAuC,GAAG,EAAE;AAChE,QAAI,IAAI,SAAS;AACf,YAAM,OAAO,KAAK,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,EAAE;AAChE,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC;AAAA,IAC9C;AAAA,EACF;AACA,QAAM,IAAS,mBAAmB,QAAQ,UAAU,IAAI,MAAM,iCAAiC;AAC/F,IAAE,OAAO;AACT,QAAM;AACR;;;AC/GA,IAAM,cAAc,oBAAI,IAA2B;AACnD,IAAM,aAAa,oBAAI,IAAqB;AAG5C,SAAS,QAAQ,KAAa,UAAkB;AAC9C,MAAI,YAAY,OAAO,KAAM;AAC3B,eAAW,CAAC,GAAG,CAAC,KAAK,YAAY,QAAQ,GAAG;AAC1C,YAAM,WAAW,EAAE,WAAW,OAAO,CAAC,MAAM,IAAI,MAAM,QAAQ;AAC9D,UAAI,SAAS,WAAW,EAAG,aAAY,OAAO,CAAC;AAAA,UAC1C,aAAY,IAAI,GAAG,EAAE,YAAY,SAAS,CAAC;AAAA,IAClD;AAAA,EACF;AACF;AAEO,SAAS,6BACd,KACA,UACA,OAC0D;AAC1D,QAAM,MAAM,KAAK,IAAI;AACrB,UAAQ,KAAK,QAAQ;AAErB,QAAM,QAAQ,YAAY,IAAI,GAAG,KAAK,EAAE,YAAY,CAAC,EAAE;AAEvD,QAAM,aAAa,MAAM,WAAW,OAAO,CAAC,MAAM,IAAI,MAAM,QAAQ;AAEpE,MAAI,MAAM,WAAW,UAAU,OAAO;AACpC,UAAM,SAAS,MAAM,WAAW,CAAC;AACjC,UAAM,aAAa,KAAK,IAAI,GAAG,SAAS,WAAW,GAAG;AACtD,WAAO,EAAE,IAAI,OAAO,cAAc,YAAY,WAAW,EAAE;AAAA,EAC7D;AAEA,QAAM,WAAW,KAAK,GAAG;AACzB,cAAY,IAAI,KAAK,KAAK;AAC1B,SAAO,EAAE,IAAI,MAAM,cAAc,GAAG,WAAW,QAAQ,MAAM,WAAW,OAAO;AACjF;AAEO,SAAS,kBAAkB,KAAsB;AACtD,MAAI,WAAW,IAAI,GAAG,EAAG,QAAO;AAChC,aAAW,IAAI,KAAK,IAAI;AACxB,SAAO;AACT;AAEO,SAAS,kBAAkB,KAAmB;AACnD,aAAW,OAAO,GAAG;AACvB;AAEA,eAAsB,eACpB,KACA,IACA,UAAU,GACE;AACZ,WAAS,IAAI,GAAG,KAAK,SAAS,KAAK;AACjC,QAAI,kBAAkB,GAAG,GAAG;AAC1B,UAAI;AACF,eAAO,MAAM,GAAG;AAAA,MAClB,UAAE;AACA,0BAAkB,GAAG;AAAA,MACvB;AAAA,IACF;AACA,QAAI,IAAI,SAAS;AACf,YAAM,OAAO,KAAK,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,EAAE;AAChE,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC;AAAA,IAC9C;AAAA,EACF;AACA,QAAM,IAAS,IAAI,MAAM,4BAA4B,GAAG,EAAE;AAC1D,IAAE,OAAO;AACT,QAAM;AACR;;;AHrCA,IAAM,iBAAiB,aAAE,OAAO;AAAA,EAC9B,UAAU,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACnC,aAAa,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,0BAA0B;AAAA,EAC1E,WAAW,aAAE,OAAO,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC/C,UAAU,aAAE,OAAO,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,UAAU,aAAE,OAAO,QAAQ,EAAE,QAAQ,KAAK;AAAA,EAC1C,cAAc,aAAE,OAAO,EAAE,MAAM,iBAAiB,EAAE,SAAS;AAAA,EAC3D,MAAM,aAAE,OAAO,aAAE,QAAQ,CAAC,EAAE,SAAS;AACvC,CAAC,EAAE,OAAO;AAEV,IAAM,qBAAqB,aAAE,OAAO;AAAA,EAClC,UAAU,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACnC,OAAO,aAAE,MAAM,aAAE,OAAO;AAAA,IACtB,YAAY,aAAE,OAAO,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IAC7C,MAAM,aAAE,OAAO,EAAE,MAAM,iBAAiB,EAAE,SAAS;AAAA,EACrD,CAAC,CAAC,EAAE,SAAS;AAAA,EACb,WAAW,aAAE,OAAO,EAAE,MAAM,iBAAiB,EAAE,SAAS;AAC1D,CAAC,EAAE,OAAO;AAEV,IAAM,kBAAkB,aAAE,OAAO;AAAA,EAC/B,UAAU,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AACrC,CAAC,EAAE,OAAO;AAEV,IAAM,mBAAmB,aAAE,OAAO;AAAA,EAChC,UAAU,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AACrC,CAAC;AAMD,eAAe,UACb,IACA,KACA,UACA,OACgD;AAChD,MAAI,GAAG,OAAO;AACZ,UAAMC,KAAI,MAAM,uBAAuB,GAAG,OAAO,KAAK,UAAU,KAAK;AACrE,WAAO,EAAE,IAAIA,GAAE,IAAI,cAAcA,GAAE,aAAa;AAAA,EAClD;AACA,QAAM,IAAI,6BAA6B,KAAK,UAAU,KAAK;AAC3D,SAAO,EAAE,IAAI,EAAE,IAAI,cAAc,EAAE,aAAa;AAClD;AAEA,eAAe,KACb,IACA,KACA,OACA,IACA,UAAU,GACE;AACZ,MAAI,GAAG,OAAO;AACZ,WAAO,MAAM,SAAS,GAAG,OAAO,KAAK,OAAO,IAAI,OAAO;AAAA,EACzD;AACA,SAAO,MAAM,eAAe,KAAK,IAAI,OAAO;AAC9C;AAMA,eAAsB,wBACpB,KACA,IACe;AACf,MAAI,CAAC,GAAG,OAAO,SAAS;AACtB,QAAI,IAAI,KAAK,wEAAqC;AAClD;AAAA,EACF;AAEA,QAAM,OAAO,GAAG,OAAO;AACvB,MAAI,IAAI;AAAA,IACN;AAAA,MACE,QAAQ,CAAC,GAAG,IAAI,SAAS,GAAG,IAAI,SAAS,GAAG,IAAI,aAAa,GAAG,IAAI,UAAU,GAAG,IAAI,SAAS;AAAA,MAC9F,SAAS,GAAG,OAAO;AAAA,MACnB,SAAS,GAAG,OAAO;AAAA,MACnB,aAAa,GAAG,OAAO;AAAA,MACvB,SAAS,GAAG,OAAO;AAAA,MACnB,KAAK,GAAG,OAAO;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AAKA,MAAI,KAAK,GAAG,IAAI,SAAS,OAAO,KAAqB,UAA+C;AAClG,UAAM,OAAO,eAAe,MAAM,IAAI,QAAQ,CAAC,CAAC;AAGhD,UAAM,EAAE,QAAQ,WAAW,IAAI,MAAM,eAAe,KAAK,KAAK,UAAU,GAAG,eAAe;AAG1F,UAAM,QAAQ,cAAc,UAAU,WAAW,MAAM,GAAG,EAAE,CAAC;AAC7D,UAAM,KAAK,MAAM,UAAU,IAAI,OAAO,KAAQ,GAAG,OAAO,aAAa;AACrE,QAAI,CAAC,GAAG,IAAI;AACV,YAAM,IAAI;AAAA;AAAA,QAER,wCAAe,KAAK,KAAK,GAAG,eAAe,GAAI,CAAC;AAAA,MAClD;AAAA,IACF;AAGA,QAAI,KAAK,YAAY,GAAG,OAAO,aAAa;AAC1C,YAAM,IAAI;AAAA;AAAA,QAER,kCAAS,KAAK,SAAS,+CAAY,GAAG,OAAO,WAAW;AAAA,MAC1D;AAAA,IACF;AAGA,QAAI,WAAW,KAAK,YAAY,GAAG,OAAO;AAC1C,QAAI,WAAW,GAAG,OAAO,QAAS,YAAW,GAAG,OAAO;AACvD,QAAI,WAAW,GAAG,OAAO,QAAS,YAAW,GAAG,OAAO;AACvD,UAAM,aAAa,KAAK,cAAc,IAAI,IAAI,KAAK,KAAK,KAAK,YAAY,QAAQ;AAIjF,QAAI,KAAK,cAAc;AACrB,YAAM,WAAW,MAAM,GAAG,SAAS;AAAA,QACjC,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,MACF;AACA,UAAI,UAAU;AACZ,cAAM,SAAS,MAAM,GAAG,QAAQ,OAAO,SAAS,UAAU,EAAE,MAAM,MAAM,KAAK;AAC7E,YAAI,QAAQ;AACV,cAAI,IAAI;AAAA,YACN,EAAE,MAAM,KAAK,cAAc,QAAQ,SAAS,IAAI,UAAU,KAAK,UAAU,OAAO;AAAA,YAChF;AAAA,UACF;AACA,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,MAAM;AAAA,cACJ,UAAU;AAAA,cACV,UAAU;AAAA,cACV,YAAY;AAAA,cACZ,WAAW,OAAO,SAAS,IAAI;AAAA,cAC/B,WAAW;AAAA,cACX,KAAK;AAAA,cACL,sBAAsB;AAAA,cACtB,OAAO;AAAA,gBACL,QAAQ,SAAS;AAAA,gBACjB,UAAU,SAAS;AAAA,gBACnB,MAAM,OAAO,SAAS,IAAI;AAAA,gBAC1B,aAAa,SAAS;AAAA,gBACtB,KAAK,GAAG,GAAG,OAAO,aAAa,IAAI,SAAS,EAAE;AAAA,cAChD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,YAAI,IAAI;AAAA,UACN,EAAE,MAAM,KAAK,cAAc,QAAQ,SAAS,IAAI,YAAY,SAAS,YAAY,OAAO;AAAA,UACxF;AAAA,QACF;AACA,cAAM,GAAG,SACN,WAAW,SAAS,IAAI,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC,EAChE,MAAM,CAAC,MAAe;AACrB,cAAI,IAAI,KAAK,EAAE,KAAK,GAAG,QAAQ,SAAS,GAAG,GAAG,iEAAoB;AAAA,QACpE,CAAC;AAAA,MACL;AAAA,IACF;AAGA,UAAM,MAAM,MAAM,GAAG,cAAc,WAAW;AAAA,MAC5C;AAAA,MACA,UAAU,KAAK;AAAA,MACf,aAAa,KAAK;AAAA,MAClB,WAAW,KAAK;AAAA,MAChB;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf,cAAc,KAAK,gBAAgB;AAAA,MACnC,MAAM,KAAK,QAAQ;AAAA,MACnB,YAAY,GAAG,OAAO;AAAA,IACxB,CAAC;AAED,QAAI,IAAI;AAAA,MACN,EAAE,UAAU,IAAI,IAAI,QAAQ,WAAW,KAAK,WAAW,UAAU,YAAY,UAAU,KAAK,SAAS;AAAA,MACrG;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,QACJ,UAAU,IAAI;AAAA,QACd;AAAA,QACA;AAAA,QACA,WAAW,KAAK;AAAA,QAChB,WAAW,IAAI,UAAU,YAAY;AAAA,QACrC,KAAK,GAAG,OAAO;AAAA,QACf,sBAAsB,GAAG,OAAO;AAAA,MAClC;AAAA,IACF;AAAA,EACF,CAAC;AAKD,MAAI,KAAK,GAAG,IAAI,SAAS,OAAO,KAAqB,UAA+C;AAElG,UAAM,EAAE,KAAK,IAAI,gBAAgB,GAAG;AAEpC,QAAI,KAAK,WAAW,GAAG;AACrB,YAAM,IAAI,6CAA4C,+BAAW;AAAA,IACnE;AACA,QAAI,KAAK,SAAS,GAAG,OAAO,SAAS;AACnC,YAAM,IAAI;AAAA;AAAA,QAER,qBAAW,KAAK,MAAM,iBAAO,GAAG,OAAO,OAAO;AAAA,MAChD;AAAA,IACF;AAGA,UAAM,EAAE,UAAU,YAAY,SAAS,IAAI,eAAe,GAAG;AAQ7D,UAAM,QAAQ,cAAc,QAAQ;AACpC,UAAM,KAAK,MAAM,UAAU,IAAI,OAAO,KAAQ,GAAG,OAAO,aAAa;AACrE,QAAI,CAAC,GAAG,IAAI;AACV,YAAM,IAAI;AAAA;AAAA,QAER,oDAAiB,KAAK,KAAK,GAAG,eAAe,GAAI,CAAC;AAAA,MACpD;AAAA,IACF;AAGA,UAAM,MAAM,MAAM,GAAG,cAAc,SAAS,QAAQ;AACpD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,4CAA2C,gCAAiB,QAAQ,EAAE;AAAA,IAClF;AAGA,QAAI,CAAC,IAAI,UAAU;AACjB,YAAM,SAAS,MAAM,uBAAuB,KAAK,IAAI,QAAQ,IAAI,UAAU,EAAE;AAC7E,UAAI,WAAW,MAAM;AACnB,cAAM,IAAI,kCAAiC,6BAAc;AAAA,MAC3D;AAAA,IACF;AAGA,QAAI,IAAI,WAAW,gBAAgB,WAAW;AAC5C,YAAM,IAAI,oDAAmD,2DAAwB;AAAA,IACvF;AACA,QAAI,IAAI,WAAW,gBAAgB,SAAS;AAC1C,YAAM,IAAI,kDAAiD,2DAAwB;AAAA,IACrF;AACA,QAAI,IAAI,WAAW,gBAAgB,WAAW,IAAI,UAAU,QAAQ,IAAI,KAAK,IAAI,GAAG;AAClF,YAAM,IAAI,0CAAyC,6BAAc;AAAA,IACnE;AAGA,QAAI,aAAa,KAAK,aAAa,IAAI,YAAY;AACjD,YAAM,IAAI;AAAA;AAAA,QAER,cAAc,UAAU,qBAAW,IAAI,UAAU;AAAA,MACnD;AAAA,IACF;AAGA,UAAM,aAAa,oBAAAC,QAAO,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACxE,QAAI,YAAY,SAAS,YAAY,MAAM,YAAY;AACrD,YAAM,IAAI;AAAA;AAAA,QAER,QAAQ,UAAU,sBAAY,QAAQ,uBAAQ,UAAU;AAAA,MAC1D;AAAA,IACF;AAGA,UAAM,GAAG,QAAQ,QAAQ,UAAU,YAAY,IAAI;AAGnD,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA,YAAY;AACV,cAAM,MAAM,MAAM,GAAG,cAAc,SAAS,QAAQ;AACpD,YAAI,CAAC,IAAK,OAAM,IAAI,4CAA2C,6BAAc;AAC7E,YAAI,IAAI,WAAW,gBAAgB,WAAW;AAC5C,gBAAM,IAAI,oDAAmD,6BAAc;AAAA,QAC7E;AACA,YAAI,IAAI,WAAW,gBAAgB,SAAS;AAC1C,gBAAM,IAAI,kDAAiD,6BAAc;AAAA,QAC3E;AACA,cAAM,IAAI,MAAM,GAAG,cAAc,QAAQ,UAAU,YAAY,UAAU;AACzE,YAAI,EAAE,QAAS,QAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,OAAO;AACnD,YAAI,EAAE,gBAAgB,EAAE,iBAAiB,YAAY;AACnD,iBAAO,EAAE,IAAI,MAAM,QAAQ,EAAE,QAAQ,YAAY,KAAK;AAAA,QACxD;AACA,cAAM,IAAI;AAAA;AAAA,UAER,QAAQ,UAAU,mCAAe,EAAE,gBAAgB,MAAM,gBAAW,UAAU;AAAA,QAChF;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAEA,QAAI,IAAI;AAAA,MACN;AAAA,QACE;AAAA,QAAU;AAAA,QAAY,MAAM,KAAK;AAAA,QACjC,eAAe,SAAS,OAAO,cAAc;AAAA,QAC7C,YAAa,SAAiB,eAAe;AAAA,MAC/C;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA,MAAM,KAAK;AAAA,QACX,eAAe,SAAS,OAAO,cAAc;AAAA,QAC7C,YAAY,IAAI;AAAA,MAClB;AAAA,IACF;AAAA,EACF,CAAC;AAKD,MAAI,KAAK,GAAG,IAAI,aAAa,OAAO,KAAqB,UAAmD;AAC1G,UAAM,OAAO,mBAAmB,MAAM,IAAI,QAAQ,CAAC,CAAC;AAGpD,UAAM,MAAM,MAAM,GAAG,cAAc,SAAS,KAAK,QAAQ;AACzD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,4CAA2C,6BAAc;AAAA,IACrE;AAGA,QAAI,CAAC,IAAI,UAAU;AACjB,YAAM,uBAAuB,KAAK,IAAI,QAAQ,IAAI,UAAU,EAAE;AAAA,IAChE;AAGA,QAAI,IAAI,WAAW,gBAAgB,WAAW;AAC5C,UAAI,IAAI,QAAQ;AACd,cAAMC,QAAO,MAAM,GAAG,SAAS,SAAS,IAAI,MAAM;AAClD,YAAIA,OAAM;AACR,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,MAAM;AAAA,cACJ,QAAQA,MAAK;AAAA,cACb,UAAUA,MAAK;AAAA,cACf,MAAM,OAAOA,MAAK,IAAI;AAAA,cACtB,aAAaA,MAAK;AAAA,cAClB,KAAK,GAAG,GAAG,OAAO,aAAa,IAAIA,MAAK,EAAE;AAAA,cAC1C,WAAWA,MAAK,eAAe;AAAA,cAC/B,YAAY;AAAA,YACd;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,QAAI,IAAI,WAAW,gBAAgB,SAAS;AAC1C,YAAM,IAAI,kDAAiD,6BAAc;AAAA,IAC3E;AACA,QAAI,IAAI,WAAW,gBAAgB,WAAW,IAAI,UAAU,QAAQ,IAAI,KAAK,IAAI,GAAG;AAClF,YAAM,IAAI,0CAAyC,6BAAc;AAAA,IACnE;AAGA,UAAM,WAAW,MAAM,KAAK,EAAE,QAAQ,IAAI,WAAW,GAAG,CAAC,GAAG,MAAM,IAAI,CAAC;AACvE,UAAM,UAAU,SAAS,OAAO,CAAC,MAAM,CAAC,IAAI,cAAc,SAAS,CAAC,CAAC;AACrE,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,IAAI;AAAA;AAAA,QAER,wBAAS,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC,GAAG,QAAQ,SAAS,KAAK,OAAO,QAAQ,MAAM,YAAO,EAAE;AAAA,MAChG;AAAA,IACF;AAGA,QAAI,KAAK,SAAS,KAAK,MAAM,SAAS,GAAG;AACvC,YAAM,iBAAyC,CAAC;AAChD,iBAAW,KAAK,KAAK,OAAO;AAC1B,YAAI,EAAE,KAAM,gBAAe,EAAE,UAAU,IAAI,EAAE,KAAK,YAAY;AAAA,MAChE;AACA,iBAAW,CAAC,OAAO,CAAC,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AACvD,cAAM,KAAK,OAAO,KAAK;AACvB,YAAI,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,EAAE,YAAY,GAAG;AAChE,gBAAM,IAAI;AAAA;AAAA,YAER,QAAQ,EAAE,4BAAa,CAAC,yCAAW,eAAe,EAAE,CAAC;AAAA,UACvD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,SAAS,KAAK,KAAK,IAAI,CAAC,IAAI,oBAAAD,QAAO,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;AACvE,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA,WAAW,KAAK,QAAQ;AAAA,MACxB;AAAA,MACA,YAAY;AACV,cAAM,MAAM,MAAM,GAAG,cAAc,SAAS,KAAK,QAAQ;AACzD,YAAI,CAAC,IAAK,OAAM,IAAI,4CAA2C,6BAAc;AAC7E,YAAI,IAAI,WAAW,gBAAgB,aAAa,IAAI,QAAQ;AAC1D,iBAAO,EAAE,YAAY,MAAM,QAAQ,IAAI,OAAO;AAAA,QAChD;AACA,YAAI,IAAI,WAAW,gBAAgB,SAAS;AAC1C,gBAAM,IAAI,kDAAiD,6BAAc;AAAA,QAC3E;AAEA,cAAM,SAAS,MAAM,GAAG,QAAQ;AAAA,UAC9B,KAAK;AAAA,UACL,IAAI;AAAA,UACJ,EAAE,IAAI,QAAQ,UAAU,IAAI,UAAU,aAAa,IAAI,YAAY;AAAA,QACrE;AAEA,cAAM,YAAY,MAAM,GAAG,QAAQ,kBAAkB,KAAK,UAAU,IAAI,UAAU;AAElF,YAAI,KAAK,aAAa,KAAK,UAAU,YAAY,MAAM,WAAW;AAChE,gBAAM,GAAG,QAAQ,OAAO,OAAO,GAAG;AAClC,gBAAM,IAAI;AAAA;AAAA,YAER,qDAAkB,KAAK,SAAS,iBAAO,SAAS;AAAA,UAClD;AAAA,QACF;AAEA,cAAMC,QAAO,MAAM,GAAG,SAAS,OAAO;AAAA,UACpC,IAAI;AAAA,UACJ,QAAQ,IAAI;AAAA,UACZ,UAAU,IAAI;AAAA,UACd,aAAa,IAAI;AAAA,UACjB,MAAM,OAAO;AAAA,UACb,YAAY,OAAO;AAAA,UACnB,UAAU,IAAI;AAAA,UACd,UAAU,IAAI;AAAA,UACd,YAAY,IAAI;AAAA,UAChB,aAAa;AAAA,UACb,MAAM,IAAI,QAAQ;AAAA,QACpB,CAAC;AAED,cAAM,GAAG,cAAc,cAAc,IAAI,IAAI,MAAM;AAEnD,eAAO,EAAE,YAAY,OAAO,QAAQA,MAAK,IAAI,MAAM,OAAO,MAAM,UAAU;AAAA,MAC5E;AAAA,IACF;AAGA,QAAI,CAAE,OAAe,YAAY;AAC/B,SAAG,QAAQ,iBAAiB,KAAK,QAAQ,EAAE,MAAM,CAAC,MAAM;AACtD,YAAI,IAAI,KAAK,EAAE,KAAK,GAAG,UAAU,KAAK,SAAS,GAAG,+DAAa;AAAA,MACjE,CAAC;AAAA,IACH;AAEA,QAAI,IAAI;AAAA,MACN;AAAA,QACE,UAAU,KAAK;AAAA,QAAU,QAAS,OAAe;AAAA,QACjD,MAAO,OAAe;AAAA,QAAM,QAAQ,IAAI;AAAA,MAC1C;AAAA,MACA;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,GAAG,SAAS,SAAU,OAAe,MAAM;AAC9D,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,uCAAsC,kBAAkB;AAAA,IACpE;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,QACJ,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,MAAM,OAAO,KAAK,IAAI;AAAA,QACtB,aAAa,KAAK;AAAA,QAClB,KAAK,GAAG,GAAG,OAAO,aAAa,IAAI,KAAK,EAAE;AAAA,QAC1C,WAAY,OAAe;AAAA,QAC3B,YAAa,OAAe,eAAe;AAAA,MAC7C;AAAA,IACF;AAAA,EACF,CAAC;AAKD,MAAI,KAAK,GAAG,IAAI,UAAU,OAAO,KAAqB,UAAgD;AACpG,UAAM,OAAO,gBAAgB,MAAM,IAAI,QAAQ,CAAC,CAAC;AAEjD,UAAM,MAAM,MAAM,GAAG,cAAc,SAAS,KAAK,QAAQ;AACzD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,4CAA2C,6BAAc;AAAA,IACrE;AAEA,QAAI,CAAC,IAAI,UAAU;AACjB,YAAM,uBAAuB,KAAK,IAAI,QAAQ,IAAI,UAAU,EAAE;AAAA,IAChE;AAEA,QAAI,IAAI,WAAW,gBAAgB,WAAW;AAC5C,YAAM,IAAI,oDAAmD,sDAAwB;AAAA,IACvF;AACA,QAAI,IAAI,WAAW,gBAAgB,SAAS;AAC1C,aAAO,EAAE,SAAS,MAAM,MAAM,EAAE,UAAU,IAAI,IAAI,QAAQ,WAAW,YAAY,KAAK,EAAE;AAAA,IAC1F;AAEA,QAAI;AACF,YAAM,GAAG,cAAc,YAAY,KAAK,QAAQ;AAAA,IAClD,SAAS,GAAQ;AACf,UAAI,EAAE,YAAY,qBAAqB;AACrC,cAAM,IAAI,oDAAmD,sDAAwB;AAAA,MACvF;AACA,UAAI,EAAE,YAAY,kBAAkB;AAClC,cAAM,IAAI,iDAAgD,4CAAS;AAAA,MACrE;AACA,YAAM;AAAA,IACR;AAEA,OAAG,QAAQ,iBAAiB,KAAK,QAAQ,EAAE,MAAM,CAAC,MAAM;AACtD,UAAI,IAAI,KAAK,EAAE,KAAK,GAAG,UAAU,KAAK,SAAS,GAAG,mDAAgB;AAAA,IACpE,CAAC;AAED,QAAI,IAAI,KAAK,EAAE,UAAU,KAAK,UAAU,QAAQ,IAAI,OAAO,GAAG,4BAAqB;AAEnF,WAAO,EAAE,SAAS,MAAM,MAAM,EAAE,UAAU,IAAI,IAAI,QAAQ,UAAU,EAAE;AAAA,EACxE,CAAC;AAKD,MAAI,KAAK,GAAG,IAAI,WAAW,OAAO,KAAqB,UAAiD;AACtG,UAAM,OAAO,iBAAiB,MAAM,IAAI,QAAQ,CAAC,CAAC;AAElD,UAAM,MAAM,MAAM,GAAG,cAAc,SAAS,KAAK,QAAQ;AACzD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,4CAA2C,6BAAc;AAAA,IACrE;AAEA,QAAI,CAAC,IAAI,UAAU;AACjB,YAAM,uBAAuB,KAAK,IAAI,QAAQ,IAAI,UAAU,EAAE;AAAA,IAChE;AAGA,UAAM,YAAY,MAAM,GAAG,QAAQ,UAAU,IAAI,EAAE;AACnD,UAAM,UAAU,IAAI,IAAI,SAAS;AACjC,UAAM,QAAQ,IAAI,IAAI,IAAI,aAAa;AACvC,UAAM,iBAAiB,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAEvF,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,QACJ,UAAU,IAAI;AAAA,QACd,QAAQ,IAAI;AAAA,QACZ,UAAU,IAAI;AAAA,QACd,aAAa,IAAI;AAAA,QACjB,WAAW,OAAO,IAAI,SAAS;AAAA,QAC/B,UAAU,IAAI;AAAA,QACd,YAAY,IAAI;AAAA,QAChB,eAAe;AAAA,QACf,eAAe,eAAe;AAAA,QAC9B,UAAU,IAAI;AAAA,QACd,WAAW,IAAI,UAAU,YAAY;AAAA,QACrC,WAAW,IAAI,UAAU,YAAY;AAAA,QACrC,aAAa,IAAI,aAAa,YAAY,KAAK;AAAA,QAC/C,QAAQ,IAAI;AAAA,MACd;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAGA,eAAe,uBACb,KACA,gBACA,UACA,IACwB;AACxB,QAAM,KAAW,IAAY;AAC7B,QAAM,UAAU,IAAI,WAAY,IAAI,QAAQ,kBAAkB;AAC9D,QAAM,SAAS,MAAM,GAAG,gBAAgB,SAAS,QAAQ;AACzD,MAAI,CAAC,YAAY,WAAW,gBAAgB;AAC1C,UAAM,IAAI,kCAAiC,6BAAc;AAAA,EAC3D;AACA,SAAO;AACT;;;AIzmBA,IAAAC,cAAkB;AAclB,IAAM,qBAAqB,cAAE,OAAO;AAAA,EAClC,UAAU,cAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACnC,aAAa,cAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,0BAA0B;AAAA,EAC1E,MAAM,cAAE,OAAO,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC1C,UAAU,cAAE,OAAO,QAAQ,EAAE,QAAQ,KAAK;AAAA,EAC1C,cAAc,cAAE,OAAO,EAAE,MAAM,iBAAiB,EAAE,SAAS;AAAA,EAC3D,MAAM,cAAE,OAAO,cAAE,QAAQ,CAAC,EAAE,SAAS;AACvC,CAAC,EAAE,OAAO;AAGV,SAAS,wBAAwB,UAAkB;AACjD,SAAO,cAAE,OAAO;AAAA,IACd,OAAO,cAAE,MAAM,kBAAkB,EAAE,IAAI,CAAC,EAAE,IAAI,QAAQ;AAAA,EACxD,CAAC,EAAE,OAAO;AACZ;AA2BA,eAAe,eACb,KACA,IACA,MACA,OAC8B;AAC9B,MAAI;AAEF,UAAM,EAAE,OAAO,IAAI,MAAM,eAAe,KAAK,KAAK,UAAU,GAAG,eAAe;AAG9E,QAAI,KAAK,OAAO,GAAG,OAAO,aAAa;AACrC,YAAM,IAAI;AAAA;AAAA,QAER,gBAAM,KAAK,QAAQ,iBAAO,KAAK,IAAI,+CAAY,GAAG,OAAO,WAAW;AAAA,MACtE;AAAA,IACF;AAIA,QAAI,KAAK,cAAc;AACrB,YAAM,WAAW,MAAM,GAAG,SAAS;AAAA,QACjC,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,MACF;AACA,UAAI,UAAU;AACZ,cAAM,SAAS,MAAM,GAAG,QAAQ,OAAO,SAAS,UAAU,EAAE,MAAM,MAAM,KAAK;AAC7E,YAAI,QAAQ;AACV,cAAI,IAAI;AAAA,YACN,EAAE,MAAM,KAAK,cAAc,QAAQ,SAAS,IAAI,UAAU,KAAK,UAAU,QAAQ,MAAM;AAAA,YACvF;AAAA,UACF;AACA,iBAAO;AAAA,YACL,IAAI;AAAA,YAAM;AAAA,YAAO,UAAU,KAAK;AAAA,YAAU,MAAM,OAAO,SAAS,IAAI;AAAA,YACpE,OAAO;AAAA,YACP,QAAQ,SAAS;AAAA,YACjB,aAAa,SAAS;AAAA,YACtB,KAAK,GAAG,GAAG,OAAO,aAAa,IAAI,SAAS,EAAE;AAAA,UAChD;AAAA,QACF;AAEA,YAAI,IAAI;AAAA,UACN,EAAE,MAAM,KAAK,cAAc,QAAQ,SAAS,IAAI,QAAQ,MAAM;AAAA,UAC9D;AAAA,QACF;AACA,cAAM,GAAG,SACN,WAAW,SAAS,IAAI,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC,EAChE,MAAM,CAAC,MAAe;AACrB,cAAI,IAAI,KAAK,EAAE,KAAK,GAAG,QAAQ,SAAS,GAAG,GAAG,iEAAoB;AAAA,QACpE,CAAC;AAAA,MACL;AAAA,IACF;AAGA,UAAM,WAAW,eAAe,KAAK,MAAM,GAAG,OAAO,QAAQ;AAC7D,QAAI,IAAI;AAAA,MACN;AAAA,QACE;AAAA,QAAO,UAAU,KAAK;AAAA,QAAU,MAAM,KAAK;AAAA,QAC3C,UAAU,SAAS;AAAA,QAAM,UAAU,SAAS;AAAA,QAAU,YAAY,SAAS;AAAA,QAC3E,QAAQ,SAAS;AAAA,MACnB;AAAA,MACA,uCAAiB,SAAS,IAAI,IAAI,SAAS,MAAM;AAAA,IACnD;AAGA,UAAM,MAAM,MAAM,GAAG,cAAc,WAAW;AAAA,MAC5C;AAAA,MACA,UAAU,KAAK;AAAA,MACf,aAAa,KAAK;AAAA,MAClB,WAAW,KAAK;AAAA,MAChB,UAAU,SAAS;AAAA,MACnB,YAAY,SAAS;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,cAAc,KAAK,gBAAgB;AAAA,MACnC,MAAM,KAAK,QAAQ;AAAA,MACnB,YAAY,GAAG,OAAO;AAAA,IACxB,CAAC;AAED,WAAO;AAAA,MACL,IAAI;AAAA,MAAM;AAAA,MAAO,UAAU,KAAK;AAAA,MAAU,MAAM,KAAK;AAAA,MACrD,OAAO;AAAA,MACP,UAAU,IAAI;AAAA,MACd,aAAa,KAAK;AAAA,MAClB,UAAU,KAAK;AAAA,MACf,UAAU,SAAS;AAAA,MACnB,YAAY,SAAS;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,UAAU,SAAS;AAAA,MACnB,sBAAsB,mBAAmB,QAAQ;AAAA,MACjD,WAAW,IAAI,UAAU,YAAY;AAAA,MACrC,KAAK,GAAG,OAAO;AAAA,IACjB;AAAA,EACF,SAAS,GAAQ;AACf,UAAM,OAAO,aAAa,cAAc,EAAE,OAAQ,GAAG;AACrD,UAAM,UAAU,GAAG,WAAW,OAAO,CAAC;AACtC,UAAM,UAAU,aAAa,cAAc,EAAE,OAAO;AACpD,WAAO;AAAA,MACL,IAAI;AAAA,MAAO;AAAA,MAAO,UAAU,KAAK;AAAA,MACjC;AAAA,MAAM,OAAO;AAAA,MAAS;AAAA,IACxB;AAAA,EACF;AACF;AAMA,eAAsB,oBACpB,KACA,IACe;AACf,MAAI,CAAC,GAAG,OAAO,SAAS;AACtB,QAAI,IAAI,KAAK,oEAAiC;AAC9C;AAAA,EACF;AAEA,QAAM,OAAO,GAAG,OAAO;AACvB,MAAI,IAAI;AAAA,IACN;AAAA,MACE,QAAQ,CAAC,GAAG,IAAI,OAAO;AAAA,MACvB,UAAU,GAAG,OAAO;AAAA,MACpB,WAAW,GAAG,OAAO;AAAA,IACvB;AAAA,IACA;AAAA,EACF;AAEA,MAAI,KAAK,GAAG,IAAI,SAAS,OAAO,QAAwB;AAEtD,UAAM,SAAS,wBAAwB,GAAG,OAAO,aAAa;AAC9D,UAAM,OAAO,OAAO,MAAM,IAAI,QAAQ,CAAC,CAAC;AAExC,QAAI,KAAK,MAAM,SAAS,GAAG,OAAO,eAAe;AAC/C,YAAM,IAAI;AAAA;AAAA,QAER,yCAAqB,GAAG,OAAO,aAAa,oCAAW,KAAK,MAAM,MAAM;AAAA,MAC1E;AAAA,IACF;AAGA,UAAM,mBAAmB,MAAM;AAC7B,YAAM,KAAW,IAAY;AAC7B,aAAO,IAAI,WAAY,IAAI,QAAQ,kBAAkB,KAA4B;AAAA,IACnF,GAAG;AACH,UAAM,YAAY,KAAK,MAAM,MAAM,CAAC,MAAM,EAAE,QAAQ;AACpD,UAAM,cAAc,MAAM,cAAc,KAAK,WAAW,GAAG,eAAe;AAC1E,UAAM,QAAQ,cAAc,eAAe,gBAAgB,MAAM,GAAG,EAAE,CAAC;AAEvE,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,YAAM,KAAK,GAAG,QACV,MAAM,uBAAuB,GAAG,OAAO,OAAO,KAAQ,GAAG,OAAO,kBAAkB,IAClF,6BAA6B,OAAO,KAAQ,GAAG,OAAO,kBAAkB;AAE5E,UAAI,CAAC,GAAG,IAAI;AACV,YAAI,IAAI;AAAA,UACN,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,KAAK,MAAM,QAAQ,cAAc,GAAG,aAAa;AAAA,UACrF;AAAA,QACF;AACA,cAAMC,WAAiC,CAAC;AACxC,iBAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,gBAAM,IAAI,MAAM,eAAe,KAAK,IAAI,KAAK,MAAM,CAAC,GAAG,CAAC;AACxD,UAAAA,SAAQ,KAAK,CAAC;AAAA,QAChB;AACA,iBAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAAA,SAAQ,KAAK;AAAA,YACX,IAAI;AAAA,YAAO,OAAO;AAAA,YAAG,UAAU,KAAK,MAAM,CAAC,EAAE;AAAA,YAC7C;AAAA,YACA,OAAO;AAAA,YACP,SAAS,+CAAY,KAAK,KAAK,GAAG,eAAe,GAAI,CAAC;AAAA,UACxD,CAAC;AAAA,QACH;AACA,eAAO;AAAA,UACL,SAAS;AAAA,UACT,MAAM;AAAA,YACJ,OAAOA;AAAA,YACP,SAAS;AAAA,cACP,OAAO,KAAK,MAAM;AAAA,cAClB,IAAIA,SAAQ,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE;AAAA,cAChC,QAAQA,SAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE;AAAA,cACrC,OAAOA,SAAQ,OAAO,CAAC,MAAM,EAAE,MAAO,EAAU,UAAU,IAAI,EAAE;AAAA,cAChE,SAASA,SAAQ,OAAO,CAAC,MAAM,EAAE,MAAO,EAAU,UAAU,SAAU,EAAU,aAAa,UAAU,EAAE;AAAA,cACzG,WAAWA,SAAQ,OAAO,CAAC,MAAM,EAAE,MAAO,EAAU,UAAU,SAAU,EAAU,aAAa,WAAW,EAAE;AAAA,cAC5G,aAAa,KAAK,MAAM,SAAS;AAAA,YACnC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,KAAK,KAAK,IAAI;AACpB,UAAM,UAAiC,CAAC;AACxC,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,YAAM,IAAI,KAAK,MAAM,CAAC;AACtB,YAAM,SAAS,MAAM,eAAe,KAAK,IAAI,GAAG,CAAC;AACjD,cAAQ,KAAK,MAAM;AAAA,IACrB;AAGA,UAAM,UAAU;AAAA,MACd,OAAO,QAAQ;AAAA,MACf,IAAI,QAAQ,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE;AAAA,MAChC,QAAQ,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE;AAAA,MACrC,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,MAAO,EAAU,UAAU,IAAI,EAAE;AAAA,MAChE,SAAS,QAAQ,OAAO,CAAC,MAAM,EAAE,MAAO,EAAU,UAAU,SAAU,EAAU,aAAa,UAAU,EAAE;AAAA,MACzG,WAAW,QAAQ,OAAO,CAAC,MAAM,EAAE,MAAO,EAAU,UAAU,SAAU,EAAU,aAAa,WAAW,EAAE;AAAA,IAC9G;AAEA,QAAI,IAAI;AAAA,MACN,EAAE,OAAO,QAAQ,OAAO,IAAI,QAAQ,IAAI,OAAO,QAAQ,OAAO,SAAS,QAAQ,SAAS,WAAW,QAAQ,WAAW,IAAI,KAAK,IAAI,IAAI,GAAG;AAAA,MAC1I;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,MAAM,EAAE,OAAO,SAAS,QAAQ,EAAE;AAAA,EAC5D,CAAC;AACH;;;ACpQO,SAAS,0BAA0B,MAAqC;AAC7E,MAAI,CAAC,KAAK,SAAS;AACjB,SAAK,IAAI,KAAK,qDAA4B;AAC1C;AAAA,EACF;AACA,QAAM,aAAa,KAAK,kBAAkB;AAC1C,QAAM,OAAO,YAAY;AACvB,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,cAAc,YAAY,KAAK,UAAU;AACpE,UAAI,QAAQ,WAAW,EAAG;AAC1B,WAAK,IAAI,KAAK,EAAE,OAAO,QAAQ,OAAO,GAAG,gEAAqC;AAC9E,iBAAW,OAAO,SAAS;AACzB,YAAI;AACF,gBAAM,KAAK,cAAc,YAAY,IAAI,EAAE;AAC3C,gBAAM,KAAK,QAAQ,iBAAiB,IAAI,EAAE;AAC1C,eAAK,IAAI,KAAK,EAAE,UAAU,IAAI,GAAG,GAAG,iDAA2B;AAAA,QACjE,SAAS,GAAG;AACV,eAAK,IAAI,KAAK,EAAE,KAAK,GAAG,UAAU,IAAI,GAAG,GAAG,yDAA2B;AAAA,QACzE;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AACV,WAAK,IAAI,MAAM,EAAE,KAAK,EAAE,GAAG,4CAA6B;AAAA,IAC1D;AAAA,EACF;AACA,eAAa,IAAI;AACjB,QAAM,QAAQ,YAAY,MAAM,UAAU;AAC1C,OAAK,QAAQ,MAAM,cAAc,KAAK,CAAC;AACvC,OAAK,IAAI;AAAA,IACP,EAAE,aAAa,KAAK,gBAAgB;AAAA,IACpC;AAAA,EACF;AACF;;;ACjBO,SAAS,wBAAwB,MAAkC;AACxE,MAAI,CAAC,KAAK,SAAS;AACjB,SAAK,IAAI,KAAK,oDAA2B;AACzC;AAAA,EACF;AACA,QAAM,aAAa,KAAK,kBAAkB;AAC1C,QAAM,YAAY,KAAK;AAEvB,QAAM,OAAO,YAAY;AACvB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,SAAS,YAAY;AAAA,QAC7C,WAAW,KAAK;AAAA,QAChB,OAAO;AAAA,MACT,CAAC;AACD,UAAI,OAAO,WAAW,GAAG;AACvB,aAAK,IAAI,MAAM,EAAE,cAAc,KAAK,aAAa,GAAG,4DAA4B;AAChF;AAAA,MACF;AACA,UAAI,cAAc;AAClB,YAAM,UAAuE,CAAC;AAC9E,iBAAW,KAAK,QAAQ;AACtB,YAAI;AACF,gBAAM,KAAK,MAAM,KAAK,QAAQ,OAAO,EAAE,UAAU;AACjD,cAAI,CAAC,IAAI;AACP,kBAAM,KAAK,SAAS,WAAW,EAAE,IAAI,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC;AAC7E;AACA,gBAAI,QAAQ,SAAS,GAAG;AACtB,sBAAQ,KAAK,EAAE,QAAQ,EAAE,IAAI,YAAY,EAAE,YAAY,MAAM,OAAO,EAAE,IAAI,EAAE,CAAC;AAAA,YAC/E;AAAA,UACF;AAAA,QACF,SAAS,GAAG;AACV,eAAK,IAAI;AAAA,YACP,EAAE,KAAK,GAAG,QAAQ,EAAE,IAAI,YAAY,EAAE,WAAW;AAAA,YACjD;AAAA,UACF;AAAA,QACF;AAAA,MACF;AACA,UAAI,cAAc,GAAG;AACnB,aAAK,IAAI;AAAA,UACP,EAAE,SAAS,OAAO,QAAQ,SAAS,aAAa,QAAQ,QAAQ;AAAA,UAChE;AAAA,QACF;AAAA,MACF,OAAO;AACL,aAAK,IAAI;AAAA,UACP,EAAE,SAAS,OAAO,QAAQ,SAAS,EAAE;AAAA,UACrC;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AACV,WAAK,IAAI,MAAM,EAAE,KAAK,EAAE,GAAG,2CAA4B;AAAA,IACzD;AAAA,EACF;AAEA,eAAa,IAAI;AACjB,QAAM,QAAQ,YAAY,MAAM,UAAU;AAC1C,OAAK,QAAQ,MAAM,cAAc,KAAK,CAAC;AACvC,OAAK,IAAI;AAAA,IACP;AAAA,MACE,aAAa,KAAK;AAAA,MAClB,cAAc,KAAK;AAAA,MACnB;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACF;;;AfjDA,IAAM,KAAK;AACX,IAAM,KAAK,OAAO;AAClB,IAAM,KAAK,OAAO;AAElB,IAAM,WAAW;AAAA,EACf,SAAS;AAAA,EACT,aAAa,IAAI;AAAA,EACjB,SAAS,IAAI;AAAA,EACb,SAAS,KAAK;AAAA,EACd,aAAa,KAAK;AAAA,EAClB,YAAY,KAAK,KAAK;AAAA,EACtB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,eAAe;AAAA,EACf,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,eAAe;AAAA,EACf,QAAQ;AACV;AAEA,eAAe,eAAe,KAA6C;AACzE,MAAI,OAAO,QAAQ,YAAY,SAAS,OAAO,OAAQ,IAAY,QAAQ,YAAY;AAErF,WAAO;AAAA,EACT;AACA,QAAM,SAAS;AACf,MAAI,OAAO,YAAY,SAAS;AAC9B,WAAO,IAAI,aAAa,OAAO,QAAQ,OAAO;AAAA,EAChD;AACA,QAAM,IAAI,MAAM,2DAA6C,OAAe,OAAO,EAAE;AACvF;AAEA,SAAS,kBAAkB,KAGzB;AACA,MAAI,IAAI,SAAS,UAAU;AACzB,WAAO;AAAA,MACL,WAAW,IAAI,0BAA0B,IAAI,MAAM;AAAA,MACnD,MAAM,IAAI,qBAAqB,IAAI,MAAM;AAAA,IAC3C;AAAA,EACF;AACA,SAAO,EAAE,WAAW,IAAI,WAAW,MAAM,IAAI,KAAK;AACpD;AAEA,eAAe,uBACb,KACA,SACe;AAEf,MAAI,CAAC,QAAQ,SAAS;AACpB,UAAM,IAAI,MAAM,kDAAwC;AAAA,EAC1D;AACA,MAAI,CAAC,QAAQ,YAAY;AACvB,UAAM,IAAI,MAAM,qDAA2C;AAAA,EAC7D;AAGA,QAAM,UAAU,MAAM,eAAe,QAAQ,OAAO;AACpD,QAAM,EAAE,WAAW,eAAe,MAAM,SAAS,IAAI,kBAAkB,QAAQ,UAAU;AAGzF,QAAM,UAA+B;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,QAAQ,SAAS;AAAA,IACxB,QAAQ;AAAA,MACN,SAAS,QAAQ,WAAW,SAAS;AAAA,MACrC,QAAQ,QAAQ,QAAQ,QAAQ,SAAS;AAAA,MACzC,aAAa,QAAQ,eAAe,SAAS;AAAA,MAC7C,SAAS,QAAQ,UAAU,OAAO,SAAS;AAAA,MAC3C,SAAS,QAAQ,UAAU,OAAO,SAAS;AAAA,MAC3C,aAAa,QAAQ,UAAU,WAAW,SAAS;AAAA,MACnD,YAAY,QAAQ,cAAc,SAAS;AAAA,MAC3C,sBAAsB,QAAQ,8BAA8B,SAAS;AAAA,MACrE,eAAe,QAAQ,WAAW,cAAc,SAAS;AAAA,MACzD,eAAe,QAAQ,WAAW,cAAc,SAAS;AAAA,MACzD,eAAe,QAAQ,WAAW,wBAAwB,SAAS;AAAA,MACnE,oBAAoB,QAAQ,WAAW,sBAAsB,SAAS;AAAA,MACtE,eAAe,QAAQ,YAAY,aAAa,SAAS;AAAA,MACzD,WAAW,QAAQ,YAAY,SAAS,SAAS;AAAA,MACjD,eAAe,QAAQ,iBAAiB,SAAS;AAAA,MACjD,UAAU;AAAA,QACR,GAAG;AAAA,QACH,GAAI,QAAQ,YAAY,CAAC;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,iBAAiB,QAAQ,oBAAoB;AAAA,EAC/C;AAEA,MAAI,SAAS,gBAAgB,OAAO;AAGpC,QAAM,wBAAwB,KAAK,OAAO;AAC1C,QAAM,oBAAoB,KAAK,OAAO;AAKtC,MAAI,gBAAgB,SAAqB,KAAc,KAAU,OAAY;AAC3E,QAAI,eAAe,aAAa;AAC9B,aAAO,MAAM,OAAO,IAAI,UAAU,EAAE,KAAK;AAAA,QACvC,SAAS;AAAA,QACT,MAAM,IAAI;AAAA,QACV,OAAO,IAAI;AAAA,QACX,SAAS,IAAI;AAAA,MACf,CAAC;AAAA,IACH;AAEA,UAAM,SAAS;AACf,QAAI,UAAU,OAAO,WAAW,YAAY,YAAY,UAAU,MAAM,QAAQ,OAAO,MAAM,GAAG;AAC9F,aAAO,MAAM,OAAO,GAAG,EAAE,KAAK;AAAA,QAC5B,SAAS;AAAA,QACT,MAAM;AAAA;AAAA,QACN,OAAO;AAAA,QACP,SAAS,OAAO,SAAS,CAAC,GAAG,WAAW;AAAA,MAC1C,CAAC;AAAA,IACH;AAEA,QAAI,IAAI,MAAM,EAAE,IAAI,GAAG,uCAAwB;AAC/C,WAAO,MAAM,OAAO,QAAQ,cAAc,GAAG,EAAE,KAAK;AAAA,MAClD,SAAS;AAAA,MACT,MAAM;AAAA;AAAA,MACN,OAAO,QAAQ,QAAQ;AAAA,MACvB,SAAS,QAAQ,WAAW;AAAA,IAC9B,CAAC;AAAA,EACH,CAAC;AAGD,MAAI,QAAQ,aAAa,YAAY,OAAO;AAC1C,8BAA0B;AAAA,MACxB,SAAS;AAAA,MACT,iBAAiB,QAAQ,aAAa,mBAAmB;AAAA,MACzD,YAAY,QAAQ,aAAa,cAAc;AAAA,MAC/C;AAAA,MACA;AAAA,MACA,KAAK,IAAI;AAAA,MACT,SAAS,CAAC,OAAO,IAAI,QAAQ,WAAW,YAAY,GAAG,CAAC;AAAA,IAC1D,CAAC;AAAA,EACH;AACA,MAAI,QAAQ,YAAY,YAAY,OAAO;AACzC,4BAAwB;AAAA,MACtB,SAAS;AAAA,MACT,iBAAiB,QAAQ,YAAY,mBAAmB;AAAA,MACxD,cAAc,QAAQ,YAAY,gBAAgB;AAAA,MAClD,WAAW,QAAQ,YAAY,aAAa;AAAA,MAC5C;AAAA,MACA;AAAA,MACA,KAAK,IAAI;AAAA,MACT,SAAS,CAAC,OAAO,IAAI,QAAQ,WAAW,YAAY,GAAG,CAAC;AAAA,IAC1D,CAAC;AAAA,EACH;AAEA,MAAI,IAAI;AAAA,IACN;AAAA,MACE,QAAQ,QAAQ,OAAO;AAAA,MACvB,SAAS,QAAQ,YAAY;AAAA,MAC7B,YAAY,QAAQ,WAAW;AAAA,MAC/B,OAAO,QAAQ,QAAQ,QAAQ;AAAA,MAC/B,eAAe,QAAQ,OAAO;AAAA,MAC9B,WAAW,QAAQ,OAAO;AAAA,IAC5B;AAAA,IACA;AAAA,EACF;AACF;AAEA,IAAO,6BAAQ,sBAAAC,SAAG,wBAAwB;AAAA,EACxC,MAAM;AAAA,EACN,SAAS;AACX,CAAC;","names":["path","fs","import_node_crypto","partSize","ErrorCode","import_node_crypto","lock","r","crypto","file","import_zod","results","fp"]}