Appearance
core(框架无关核心)
入口:import { ... } from '@pidaqing/uploader'
本文档与源码 v0.2.0(tag
sdk/v0.2.0)逐项核对。99% 场景直接使用适配层(Vue / React),core 文档面向深度集成与自定义控制面客户端的场景。
PartakeUploadManager(推荐入口)
同 hash 文件共享一次物理上传(方案 §5.7 共享执行体多任务模型);外部可见任务数 = 选择数(不丢失输入);任一共享任务 abort → 整组 aborted。
构造
ts
type ManagerOptions = Partial<PartakeConfig> & {
controlBase: string; // 必填;BFF 地址(同源路径),不以 / 结尾(内部 normalize 兜底)
client?: ControlClient; // 控制面客户端注入(测试点;默认 HttpControlClient)
onTaskAdd?: (task: Task) => void; // 任务生命周期钩子(vue adapter 经 reactive 包装注入响应式)
onTaskRemove?: (taskId: string) => void;
};
new PartakeUploadManager(opts: ManagerOptions)注意:显式
undefined的选项会被过滤后才与默认值合并(v0.2 批次 B 组件 props 透传逼出的缺陷修复)——不会覆盖DEFAULT_CONFIG。
方法
ts
readonly config: PartakeConfig // 合并后的生效配置(含默认值)
taskList(): Task[] // 全部任务快照
getTask(taskId: string): Task | undefined
stats(): { total: number; uploading: number; completed: number; failed: number; error: number }
// failed 口径含 quarantined
subscribe(listener: () => void): () => void // 订阅变更,返回退订函数
getVersion(): number // 版本号(任意突变/增删递增;配合 useSyncExternalStore)
addFiles(files: File[]): Task[] // 每文件一个 Task;校验失败标 error 不阻断批次
start(taskId?: string): void // 启动 idle/error 任务(缺省=全部;paused 跳过)
startAll(): void
abort(taskId: string): Promise<void> // 共享整组取消;uploading 态发 DELETE(服务端 abortMultipart)
abortAll(): Promise<void>
pause(taskId: string): Promise<void> // 软停止:取消活跃 PUT 不发 DELETE(保留分片供续传)
pauseAll(): Promise<void>
resume(taskId: string): void // 清 paused 重跑:缓存 hash → init 幂等 → resume 补传
resumeAll(): void
removeTask(taskId: string): Promise<void> // 本地移除;uploading/idle 态等效 abort
retryTask(taskId: string): void // error → 重跑(缓存 hash 免重算);completing 挂起 → 重发 complete约束语义:
- abort 优先于 pause:任何路径均清软停止标志;
- completing 阶段拒绝 abort / pause(抛
PartakeError('UPLOAD','CONFLICT', …)),需经轮询链/pending 兜底; - 进度加权(方案 §8):hashing 8% + initing 2% + uploading 80% + completing 10%(complete 挂起时封顶 99%)。
配置(PartakeConfig 与默认值)
DEFAULT_CONFIG 全量默认(显式 undefined 不会覆盖):
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
controlBase | string | 必填 | BFF 地址(不以 / 结尾) |
concurrency | number | 4 | 文件内分片并发(第一层信号量) |
globalConcurrency | number | 'auto' | 8 | 全局分片池(第二层;'auto'=8,最小 1) |
maxRetry | number | 5 | 分片级最大重试次数 |
maxUrlRenewalsPerChunk | number | 5 | 单分片预签名重签上限(超出抛 URL_RENEWAL_EXCEEDED) |
completeTimeout | number | 120_000 | complete 超时(ms;超时转轮询链) |
pollInterval | number | 3_000 | 状态轮询间隔(ms) |
pollTimeout | number | 600_000 | 轮询总限(ms;耗尽 → pending) |
autoStart | boolean | true | addFiles 后自动启动 |
hashWorkerEnabled | boolean | true | Worker 卸载 hash(不可用时主线程降级) |
maxHashSyncSize | number | 52_428_800 | 主线程 hash 上限 50MB(超过且 Worker 不可用 → HASH_FAILED) |
validate | ValidateConfig | false | { maxSize: 10GB, minSize: 1, accept: '*' } | false = 关闭校验 |
allowDuplicate | boolean | false | true:同 hash 排队独立 init(命中秒传,零字节),不挂载共享 |
speedLimit | number | 0 | 任务级限速(字节/秒;0=不限;按固定并发均分到活跃分片) |
urlWaterline | number | 'auto' | 'auto' | 预签名重签水位线(ms) |
safeFileName | boolean | true | 文件名净化(去 / \ ..、控制字符、NFC、≤200 字符) |
onMetrics | (e: MetricsEvent) => void | 无 | 指标事件回调(六类,见 MetricsEvent) |
工具函数
ts
validateFile(file: File, cfg: PartakeConfig['validate']): void // 抛 PartakeError('VALIDATE',…, detail:{reason})
sanitizeFileName(name: string): string // 兜底 'unnamed',≤200 字符
resolveWaterline(w: number | 'auto', conn?: { downlink?: number; rtt?: number }): numberresolveWaterline 三档启发式(无状态可测试):downlink ≥10 && rtt ≤50 → 20s;downlink <2 || rtt >150 → 90s;其余 → 45s;API/字段缺失 → 60s(v0.1 固定值);显式数字直连。
上传执行体 UploadEngine(低层)
多 Task 共享的执行体;单 fileId 物理生命周期。通常不经手——manager 内部创建。
ts
new UploadEngine(
client: ControlClient,
config: PartakeConfig,
globalSemaphore: Semaphore,
file: File,
fileName: string,
hooks: EngineHooks,
internal?: { backoffBaseMs?: number; batchParts?: number }, // 测试调参点,默认 {batchParts: 1000}
)
engine.run(cachedHash?: string): Promise<EngineResult> // 缓存 hash 免重算
engine.abort(opts?: { soft?: boolean }): Promise<void> // soft=true=软停止(pause 语义);completing 拒绝
engine.currentPhase: TaskPhase // 当前相位(挂载重放用)
engine.currentProgress: number // 当前加权进度ts
type EngineResult =
| { kind: 'completed'; fileId: string; scan?: ScanResult }
| { kind: 'quarantined'; fileId: string; scan?: ScanResult }
| { kind: 'failed'; fileId: string; scan?: ScanResult }
| { kind: 'aborted'; fileId?: string }
| { kind: 'paused'; fileId?: string } // 软停止:保持 uploading+paused
| { kind: 'error'; error: PartakeError }
| { kind: 'pending'; fileId: string }; // 轮询链耗尽/总限,保持 completing
type EngineHooks = {
onPhase: (phase: TaskPhase) => void;
onProgress: (fraction: number) => void; // 0..1 加权后总进度
onFileId: (fileId: string) => void;
onScan: (scan: ScanResult | undefined) => void;
metrics: (e: MetricsEvent) => void;
};主流程:hash → init(秒传命中直接完成)→ resume(拿已传清单)→ 分片上传 → complete;complete 幂等矩阵 + missingParts 补传(≤3 轮)+ 超时降级轮询链。
网络与协议
ts
class HttpControlClient {
constructor(controlBase: string); // 内部 normalize:去尾部斜杠
init(body): Promise<InitResponse>; // POST {base}/files/upload/init
partUrls(fileId, partNumbers): Promise<PartUrlsResponse>; // POST {base}/files/{id}/part-urls
complete(fileId, parts): Promise<CompleteResponse>; // POST {base}/files/{id}/complete
resume(fileId): Promise<ResumeResponse>; // GET {base}/files/{id}/resume
status(fileId): Promise<StatusResponse>; // GET {base}/files/{id}/status
remove(fileId): Promise<DeleteResponse>; // DELETE {base}/files/{id}
}实现 ControlClient 接口即可注入自定义客户端(契约见下节类型)。数据面(分片 PUT / 下载 GET)直连 S3 预签名 URL,不走控制面客户端。
错误体系
ts
class PartakeError extends Error {
readonly phase: ErrorPhase;
readonly code: ErrorCode;
readonly retryable: boolean; // NETWORK_ERROR/TIMEOUT/URL_EXPIRED → true;4xx → false
readonly cause?: unknown;
readonly detail?: { missingParts?: number[]; reason?: string };
toJSON(): PartakeErrorLike;
}
type ErrorPhase = 'VALIDATE' | 'HASH' | 'INIT' | 'PRESIGN' | 'UPLOAD' | 'COMPLETE'
| 'RESUME' | 'STATUS' | 'DELETE' | 'STORAGE';
type ErrorCode = 'NETWORK_ERROR' | 'TIMEOUT' | 'ABORTED' | 'UNAUTHORIZED' | 'FORBIDDEN'
| 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION_ERROR' | 'MISSING_PARTS'
| 'ETAG_MISMATCH' | 'URL_EXPIRED' | 'URL_RENEWAL_EXCEEDED'
| 'HASH_FAILED' | 'STORAGE_FAILED' | 'UNKNOWN';fromResponse(phase, status, body) 状态码映射:400(含 missingParts → MISSING_PARTS,否则 VALIDATION_ERROR)/ 401 UNAUTHORIZED / 403 FORBIDDEN / 404 NOT_FOUND / 409 CONFLICT / 429→NETWORK_ERROR(可重试)/ 5xx→NETWORK_ERROR(可重试)。
并发与 hash 原语
ts
class Semaphore { // 两层并发模型的基础设施
constructor(permits: number);
async acquire(): Promise<void>; // 等待者不计数,许可直接移交
release(): void;
get inFlight(): number;
}
backoffMs(attempt: number, baseMs = 1000, maxMs = 30_000): number; // 指数退避 + [50%,100%) jitter
sleep(ms: number, signal?: AbortSignal): Promise<void>; // abort → reject(new Error('aborted'))
computeFileHash(file: File, opts: HashOptions): Promise<string>; // hex SHA-256
type HashOptions = {
workerEnabled: boolean; // 内联 Worker(自包含 Blob URL)
maxSyncSize: number; // 主线程降级上限(超限 + Worker 不可用 → HASH_FAILED)
signal?: AbortSignal;
onProgress?: (fraction: number) => void; // 0 → 0.5(digesting) → 1
};任务与契约类型
Task
ts
type TaskPhase = 'idle' | 'hashing' | 'initing' | 'uploading' | 'completing';
type TaskStatus = 'idle' | 'uploading' | 'completed' | 'quarantined' | 'failed' | 'aborted' | 'error';
type Task = {
id: string;
file?: File;
fileName: string; // safeFileName 后(或原文,safeFileName:false 时)
fileSize: number;
phase: TaskPhase;
progress: number; // 0..1 加权(hashing 8% + init 2% + upload 80% + complete 10%)
status: TaskStatus;
paused?: boolean; // 软停止标志(pause 后 true;不新增 status 枚举)
error?: PartakeErrorLike;
fileId?: string; // init 后回填
scan?: ScanResult;
};8 路由契约响应(对齐 partake 网关)
ts
type InitResponse =
| { instant: true; fileId: string; status: 'completed' } // 秒传命中
| { instant: false; fileId: string; uploadId: string; partSize: number; totalParts: number };
type ScanResult = { result: 'ok' | 'infected' | 'error' | 'skipped';
virusName?: string; // result=infected
reason?: string }; // result=error/skipped(仅 complete 响应携带)
type CompleteResponse = { fileId: string;
status: 'uploading' | 'scanning' | 'completed' | 'quarantined' | 'failed' | 'aborted';
idempotent: boolean; scan?: ScanResult };
type ResumeResponse = { fileId: string; uploadId: string; partSize: number; totalParts: number;
uploaded: Array<{ partNumber: number; etag: string; size: number }> }; // 升序;非 uploading → 409
type StatusResponse = { fileId: string; status: string; isPublic: boolean;
scan: Omit<ScanResult, 'reason'> | null }; // status 口径无 reason
type PartUrlsResponse = { fileId: string;
urls: Array<{ partNumber: number; url: string; expiresAt: number }> };
type DownloadUrlResponse = { fileId: string; url: string; expiresAt: number }; // quarantined → 409
type DeleteResponse =
| { fileId: string; status: 'aborted' } // uploading 态
| { fileId: string; status: 'deleted'; remainingRefs: number }; // completed 态;其他态 409
type ControlClient = {
init(body: { fileName: string; fileSize: number; fileHash: string; contentType?: string }): Promise<InitResponse>;
partUrls(fileId: string, partNumbers: number[]): Promise<PartUrlsResponse>;
complete(fileId: string, parts: Array<{ partNumber: number; etag: string }>): Promise<CompleteResponse>;
resume(fileId: string): Promise<ResumeResponse>;
status(fileId: string): Promise<StatusResponse>;
remove(fileId: string): Promise<DeleteResponse>;
};MetricsEvent
ts
type MetricsEvent =
| { type: 'task_start'; taskId: string }
| { type: 'complete'; taskId: string; fileId: string }
| { type: 'fail'; taskId: string; code: string }
| { type: 'chunk_retry'; taskId: string; partNumber: number; attempt: number }
| { type: 'url_renew'; taskId: string; partNumber: number }
| { type: 'scan_wait'; taskId: string; pollCount: number };完整导出清单
ts
// class
PartakeUploadManager · UploadEngine · HttpControlClient · Semaphore · PartakeError
// function
sanitizeFileName · validateFile · resolveWaterline · computeFileHash · fromNetworkError
fromResponse · backoffMs · sleep
// 常量
DEFAULT_CONFIG
// type
ManagerOptions · PartakeConfig · ValidateConfig · EngineResult · EngineHooks · HashOptions
Task · TaskPhase · TaskStatus · PartakeErrorLike · ErrorPhase · ErrorCode
ControlClient · InitResponse · CompleteResponse · ResumeResponse · StatusResponse
PartUrlsResponse · DownloadUrlResponse · DeleteResponse · ScanResult · MetricsEvent
PartakeApiErrorBody