Skip to content

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 不会覆盖):

字段类型默认说明
controlBasestring必填BFF 地址(不以 / 结尾)
concurrencynumber4文件内分片并发(第一层信号量)
globalConcurrencynumber | 'auto'8全局分片池(第二层;'auto'=8,最小 1)
maxRetrynumber5分片级最大重试次数
maxUrlRenewalsPerChunknumber5单分片预签名重签上限(超出抛 URL_RENEWAL_EXCEEDED)
completeTimeoutnumber120_000complete 超时(ms;超时转轮询链)
pollIntervalnumber3_000状态轮询间隔(ms)
pollTimeoutnumber600_000轮询总限(ms;耗尽 → pending)
autoStartbooleantrueaddFiles 后自动启动
hashWorkerEnabledbooleantrueWorker 卸载 hash(不可用时主线程降级)
maxHashSyncSizenumber52_428_800主线程 hash 上限 50MB(超过且 Worker 不可用 → HASH_FAILED)
validateValidateConfig | false{ maxSize: 10GB, minSize: 1, accept: '*' }false = 关闭校验
allowDuplicatebooleanfalsetrue:同 hash 排队独立 init(命中秒传,零字节),不挂载共享
speedLimitnumber0任务级限速(字节/秒;0=不限;按固定并发均分到活跃分片)
urlWaterlinenumber | 'auto''auto'预签名重签水位线(ms)
safeFileNamebooleantrue文件名净化(去 / \ ..、控制字符、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 }): number

resolveWaterline 三档启发式(无状态可测试):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

MIT Licensed · partake 生态