跳到主要内容

存储与文件上传

@vef-framework-react/core 提供了一个基于框架 sys/storage 后端资源构建的分片、可断点续传的文件上传客户端。它负责分片切分、有限并发的分片上传、带退避的重试,以及(可选启用的)跨页面刷新恢复中断上传。

关于 React Hook 适配层,参见 @vef-framework-react/hooks 中的 useUpload——它为最常见的单任务上传场景封装了本页的全部能力。若需并行/批量上传,请直接使用 Uploader

Uploader

驱动单个文件完成后端 sys/storage 资源暴露的四步协议(init_upload → upload_part* → complete_upload)。一次性使用:已完成或已失败的运行无法重新启动——每个文件都应创建一个全新的 Uploader

import { HTTP_CLIENT, Uploader, useApiClient } from "@vef-framework-react/core";

const apiClient = useApiClient();
const http = apiClient[HTTP_CLIENT];

const uploader = new Uploader(http, file, {
onProgress: progress => console.log(progress.percent),
onStatusChange: status => console.log(status)
});

const result = await uploader.start();
// result: { bucket, key, eTag, size, contentType, lastModified, originalFilename, metadata? }

构造函数

new Uploader(http: Readonly<HttpClient>, file: Blob, options?: UploaderOptions)

http 是从 apiClient[HTTP_CLIENT] 获取的 HttpClient(参见 HTTP 与 API 客户端)。当 file 是普通 Blob 而非 File 时,必须通过 options.init.filename 提供文件名。

UploaderOptions

选项类型默认值说明
apiPathstring"/api"RPC 入口 URL
resourcestring"sys/storage"RPC 资源名
versionstring"v1"RPC 版本
initUploadInit后端会话的初始化时覆盖项
partConcurrencynumber3并行上传的最大分片数
maxPartRetriesnumber3每个分片的重试预算(首次尝试计为 1 次)
retryBaseDelaynumber500分片重试指数退避的基础延迟(毫秒)
retryMaxDelaynumber8000单次重试的最大等待延迟(毫秒)
signalAbortSignal外部中止信号,与上传器内部的控制器组合生效
onProgress(progress: UploadProgress) => void每次分片进度更新时调用
onStatusChange(status: UploadStatus) => void每次 UploadStatus 变化时调用
onSessionOpened(session: UploadSessionSnapshot) => void后端确认上传会话(无论是新建还是续传)后调用一次

UploadInit

字段类型说明
filenamestring覆盖原始文件名;默认为 file.name
contentTypestring覆盖声明的 MIME 类型;默认为 file.type
publicbooleantrue 时对象落入 pub/(匿名可读),否则落入 priv/(需鉴权读取)

UploadProgress

字段类型说明
loadednumber已确认的字节数(不会超过 total
totalnumber文件总字节数
partsCompletednumber后端已完全接受的分片数
partsTotalnumber上传计划中的分片总数
percentnumberloaded / total,四舍五入为整数百分比

UploadStatus

type UploadStatus =
| "idle"
| "initializing"
| "uploading"
| "completing"
| "aborting"
| "succeeded"
| "failed"
| "aborted";

状态转换是线性的,直至到达终态(succeededfailedaborted)。

UploadResult

字段类型说明
bucketstring后端存储桶名称
keystring对象键(带 pub/priv/ 前缀)
eTagstring后端分配的 ETag
sizenumber最终对象大小(字节)
contentTypestring存储的内容类型
lastModifiedstringISO-8601 时间戳
originalFilenamestring上传过程中跟踪的文件名
metadataRecord<string, string> | undefined后端附加的元数据

实例 API

成员类型说明
statusUploadStatus(getter)当前生命周期状态
progressUploadProgress(getter)最新的聚合进度快照
start(plan?)(plan?: ResumePlan) => Promise<UploadResult>启动上传;重复调用返回同一个 Promise。传入 { kind: "resume", ... } 形态的计划(见下文)可跳过 init_upload,续传此前的会话
abort()() => Promise<void>取消进行中的上传,并尽力中止后端会话;在任何状态下调用都是安全的

uploadFile

构造 Uploader 并立即启动它的便捷包装函数:

import { uploadFile } from "@vef-framework-react/core";

const result = await uploadFile(http, file, { onProgress: p => console.log(p.percent) });

当 UI 需要调用 abort() 或独立于返回的 Promise 观察 status 变化时,请直接使用 Uploader 类。

续传中断的上传

续传能力是可选启用的,涉及两个关注点:在本地持久化 ResumeRecord,以及在决定是否续传前向后端确认哪些分片已经上传成功。

resolveResumePlan

根据给定的文件指纹计算 ResumePlan:查找持久化的记录,对照系统时钟和后端实时的 list_parts 结果进行校验,并将结果交给调用方提供的决策处理函数。

import { LocalStoragePersistence, PrefixFingerprinter, resolveResumePlan } from "@vef-framework-react/core";

const persistence = new LocalStoragePersistence();
const fingerprinter = new PrefixFingerprinter();
const fingerprint = await fingerprinter.fingerprint(file);

const plan = await resolveResumePlan({
fingerprint,
persistence,
ctx: { http, apiPath: "/api", resource: "sys/storage", version: "v1" },
onResumeDetected: async candidate => {
const resume = window.confirm(`Resume uploading ${candidate.record.key}?`);
return resume ? { kind: "resume" } : { kind: "discard" };
}
});

const result = await uploader.start(plan);

如果未提供 onResumeDetected 处理函数,默认决策为 { kind: "discard" }——续传错文件远比一次多余的全新上传更糟糕。

ResolveResumeInputs

字段类型说明
fingerprintstring文件的稳定标识,由 FileFingerprinter 计算得出
persistenceResumablePersistence续传记录的读写来源
ctxProtocolContext{ http, apiPath, resource, version }——与 Uploader 内部构建的形状相同
onResumeDetectedResumeDecisionHandler可选;默认始终丢弃
signalGenericAbortSignal用于正在进行的 list_parts / abort 调用的可选取消信号

ResumePlan

type ResumePlan =
| { kind: "fresh" }
| {
kind: "resume";
claimId: string;
key: string;
partSize: number;
partCount: number;
expiresAt: string;
completedParts: readonly ListedPart[];
};

ResumeCandidateResumeDecision

类型形状说明
ResumeCandidate{ record: ResumeRecord; completedParts: ListedPart[] }传递给 onResumeDetectedcompletedParts 是实时的 list_parts 结果,而非持久化记录本身
ResumeDecision{ kind: "resume" } | { kind: "discard" }调用方的选择
ResumeDecisionHandler(candidate: ResumeCandidate) => Promise<ResumeDecision>处理函数类型

持久化

ResumablePersistence

ResumeRecord 值的可插拔存储接口。

interface ResumablePersistence {
load: (fingerprint: string) => Promise<ResumeRecord | null>;
save: (record: ResumeRecord) => Promise<void>;
remove: (fingerprint: string) => Promise<void>;
}

LocalStoragePersistence

默认实现,基于 window.localStorage

import { LocalStoragePersistence } from "@vef-framework-react/core";

const persistence = new LocalStoragePersistence(); // keys namespaced under "__VEF_UPLOAD_RESUME__"
成员类型说明
constructor(keyPrefix?: string)存储键的命名空间前缀(默认 "__VEF_UPLOAD_RESUME__"
clearAll()() => Promise<void>丢弃该实例拥有的全部记录(按其键前缀匹配);在退出登录/切换账号时调用

ResumeRecord

字段类型说明
fingerprintstring查找键,由 FileFingerprinter 计算得出
claimIdstring服务端分配的 claim ID
keystring后端分配的对象键
partSizenumber后端权威的分片大小
partCountnumber计划的分片总数
expiresAtstringISO-8601 过期时间
savedAtnumber记录最后写入时的 Date.now()

指纹计算

FileFingerprinter

interface FileFingerprinter {
fingerprint: (file: File) => Promise<string>;
}
实现依据说明
WeakFingerprintername:size:lastModified同步、始终可用;若内容变化但未改变文件名/大小/修改时间,会发生碰撞
PrefixFingerprinterWeakFingerprinter 的字段加上文件前 4 MiB 的 SHA-256(窗口大小可通过构造函数的 prefixBytes 参数覆盖)需要 crypto.subtle;不可用时会同步抛出异常——此时应回退到 WeakFingerprinter

常量与错误

导出说明
STORAGE_API_PATH"/api"默认 apiPath
STORAGE_RESOURCE"sys/storage"默认 resource
STORAGE_VERSION"v1"默认 version
PUBLIC_PREFIX"pub/"匿名可读对象的键前缀
PRIVATE_PREFIX"priv/"需鉴权读取对象的键前缀
错误继承自触发时机
UploadErrorError所有存储错误的基类;instanceof UploadError 可用于区分上传器故障
UploadAbortedErrorUploadError调用方(或外部信号)取消了进行中的上传
UploadProtocolErrorUploadError协议 RPC(init_upload / complete_upload / abort_upload / list_parts)发生不可恢复的失败;携带 action
UploadPartErrorUploadError某个分片在超过 maxPartRetries 后仍然失败;携带 partNumberattempts

类型导出

类型说明
UploaderOptionsUploader 的构造选项
UploadInit初始化时的 filename/contentType/public 覆盖项
UploadProgress聚合进度快照
UploadStatus生命周期状态联合类型
UploadResult上传成功的结果
UploadSessionSnapshot传递给 onSessionOpened{ claimId, key, partSize, partCount, expiresAt }
ResumablePersistence持久化接口
ResumeRecord单条持久化的续传记录
FileFingerprinter指纹计算接口
ResolveResumeInputsresolveResumePlan 的输入
ResumeCandidate传递给 onResumeDetected{ record, completedParts }
ResumeDecision{ kind: "resume" } | { kind: "discard" }
ResumeDecisionHandler(candidate: ResumeCandidate) => Promise<ResumeDecision>
ResumePlan{ kind: "fresh" } 或完整的续传会话形状

此外还导出了更底层的协议函数(initUploaduploadPartlistPartscompleteUploadabortUpload)及其请求/响应类型(InitUploadParamsInitUploadResponseUploadPartResponseListedPartListPartsResponseCompleteUploadResponseObjectInfoProtocolContext),供需要基于同一后端协议构建自定义上传流程的调用方使用。