Puter.js 文件上传完整指南:puter.fs.upload() 的 API 用法、生命周期回调与双通道上传原理
【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter
puter.fs.upload()是 Puter.js(Puter SDK)面向用户文件系统(FS)的上传入口,用于把网页或本地环境中的文件、拖拽条目、Blob等数据写入当前用户自己的 Puter 文件空间。本文以官方 API 文档 upload.md 为骨架,结合仓库内 SDK 源码(src/puter-js/src/modules/FileSystem/operations/upload/)逐一讲解参数语义、init/start/progress/abort回调、失败语义(含failedItems与各错误码)、目录上传与缩略图生成的平台差异,以及"签名批量直传 vs 旧版/batch转发"两种底层上传通道,帮助你写出健壮、可打断、带进度与缩略图的上传代码。
适用平台与文档定位
该 API 在前置元信息中声明的可用平台为websites、apps、nodejs、workers四类(见 upload.md 头部 front-matter)。从 SDK 实现看,不同环境走的上传通道并不相同:
web(对应 websites)、gui、app环境走签名批量直传通道(signed batch write);nodejs、workers环境走旧版/batch转发通道(legacy batch)。
这一差异直接决定了目录上传能力、错误码形态与进度语义,详见后文"上传目录的平台差异"与"双通道原理"两节。文档是公开 API 参考,正文可直接作为puter.fs.upload()的权威用法说明;代码佐证来自仓库源码,读者可沿路径自行核对。
API 语法与一次上传的执行流程
upload是挂载在puter.fs命名空间上的异步操作(在 FileSystem/index.js 中注册),支持三种调用形态:
puter.fs.upload(items) puter.fs.upload(items, dirPath) puter.fs.upload(items, dirPath, options)items:要上传的内容(必填);dirPath:目标目录(可选,省略时上传到应用自身根目录);options:上传行为配置与回调集合(可选)。
从 operations/upload/index.js 的编排逻辑看,一次调用内部大致依次经历:
- 鉴权:若在 web 环境且尚无
authToken,先调用puter.ui.authenticateWithPuter()完成登录,失败则直接 reject; - 目录校验与路径解析:目标目录不能是根目录
/;相对路径会通过getAbsolutePathForApp解析到应用根目录下; - 条目归一化(normalizeUploadEntries):把多种输入形态扁平化为统一条目列表;
- 缩略图准备:按选项在浏览器端生成缩略图(SDK 会等待这一步完成后再发送文件);
- 空间预检:非 web 环境且待传总量达到阈值时,先查剩余空间,不足则提前失败,避免"上传到一半被服务器拒绝";
- 执行上传:优先尝试签名批量直传,后端不支持(返回 404/405/501 等)时自动回退到旧版
/batch通道。
源码注释还特意说明了根目录限制的合理性:dirPath === '/'时会以Can not upload to root directory.拒绝;省略dirPath与应用相对路径都会归一化到应用的专属目录(getAbsolutePathForApp.js)。
参数一:items——被上传的对象
官方文档给出的合法类型是InputFileList、FileList、File对象数组或Blob对象数组。对照 types.js 中的UploadItems类型与 entries.js 的归一化实现,实际可接受的输入范围更宽:
| 输入形态 | 处理方式(依据 entries.js 源码) |
|---|---|
DataTransferItemList/DataTransferItem(含数组包裹) | 标记为拖拽场景,先经puter.ui.getEntriesFromDataTransferItems解析出文件与目录,再按"目录优先、文件按字节大小升序"排序;拖拽对象可能携带子目录结构(isDirectory、fullPath) |
FileList/ 单个File/File数组 | 摊平为数组并按size升序排序,为其补上filepath/fullPath(等于文件名) |
单个Blob | 包装成File(类型application/octet-stream,文件名来自options.name) |
| 字符串 | 包装成名为default.txt、类型text/plain的File |
| 其他任何类型 | 抛出{ code: 'field_invalid', message: 'upload() items parameter is an invalid type' } |
归一化后的条目会在separateFilesAndDirs中被进一步区分:
- 目录条目:解析
finalPath/fullPath,去掉前导/,拼到dirPath下形成待创建目录路径; - 文件条目:依据
finalPath/filepath/fullPath/name决定目标位置与文件名;大小写不敏感的.ds_store会被自动跳过;名字为空则视为"仅建空目录"; options.createFileParent打开时,会为嵌套路径逐级生成父目录(用uniqueDirs去重)。
了解这些内部规则有助于正确预估上传结果。例如,把File单个对象直接传入时返回单个FSItem,而传入多个对象时返回数组——这一"单数/复数"判定贯穿两种上传通道的收尾逻辑。
参数二:dirPath——上传目标目录
dirPath是一个字符串,表示内容要落盘的目录:
- 省略时,上传到应用自身的根目录;
- 传入以
/开头的绝对路径则直接作为目标; - 传入相对路径(如
'./uploads')会被解析为"应用根目录 + 该相对路径"。
需要特别注意的是根目录本身(/)不能作为上传目标(SDK 会直接拒绝),因为上传语义是"往目录里写入条目",根目录归用户所有。若dirPath指定的目录尚不存在,默认并不会自动创建,需要通过createMissingParents或先用puter.fs.mkdir()建好,详见下文选项说明。
参数三:options 与上传生命周期回调
options是一组键值对,控制命名冲突策略、父目录创建与缩略图行为,同时承载整个上传过程的回调。全部可选字段以 JSDoc 形式定义在 types.js 的UploadOptionsOwn中,上传编排则在 index.js 中消费。先看官方文档明确列出的五个行为开关:
| 选项 | 类型 | 默认值 | 语义 |
|---|---|---|---|
overwrite | Boolean | false | 目标已存在时是否覆盖。为true时dedupeName被忽略(签名通道中会强制把重名去重关闭) |
dedupeName | Boolean | true | 目标已存在且不覆盖时,是否自动对文件名去重(通常表现为追加序号);overwrite: true时该字段无效 |
createMissingParents | Boolean | false | 目标父目录不存在时是否自动逐级创建 |
generateThumbnails | Boolean | false | 是否在上传前于浏览器端为图片生成缩略图;无法解码的文件与生成失败都会被静默跳过 |
thumbnailGenerator | Function | — | 自定义缩略图生成器(file, context) => string \| undefined(可返回 Promise),覆盖内置图片生成器 |
thumbnail | String | — | 备用缩略图(data URL 或 URL),用于文件没有生成出缩略图的情况;超过 2 MiB 的 data URL 会被丢弃 |
补充几个从源码能确认、但文档正文未展开的内部细节:
createMissingParents的宽松判定:在签名通道中,只要createMissingAncestors、createMissingParents、createFileParent任一为真,或者本次上传本身就包含目录条目(dirs.length > 0),都会自动补建缺失父目录(见 signedBatchUpload.js)。dedupeName与overwrite的联动:签名通道里文件元数据中的dedupeName在覆盖开启时被强制置为false(signedBatchUpload.js);旧版通道的 mkdir/write 操作里dedupe_name仍取options.dedupeName ?? true(legacyBatchUpload.js)。两条通道的措辞略有差异,但对公开 API 而言行为一致。thumbnail只是兜底:当文件生成了缩略图时以生成为准;仅当无生成结果时才退而使用thumbnail字段。
生命周期回调:init / start / progress / abort
以下回调用于追踪一次上传的完整生命周期,其中operationId是 SDK 为本次上传生成的 UUID,因此同一页面并行发起多次上传时,可用它区分不同操作、避免把进度串台:
| 回调 | 签名 | 触发时机 |
|---|---|---|
init | (operationId, xhr) | 请求对象已创建、尚未发送时。回调会拿到原始XMLHttpRequest,因此可以自行调用xhr.abort()主动取消上传 |
start | () | 上传开始发送字节时;无参数 |
progress | (operationId, progress) | 字节发送过程中持续触发;progress是0到100之间的百分比数值 |
abort | (operationId) | 上传被中止时 |
一个带进度回调的最小示例(官方文档原例):
puter.fs.upload(items, './uploads', { progress: (operationId, progress) => { console.log(`${Math.round(progress)}%`); }, });取消的两种途径与差异。通过init拿到的xhr.abort()是"主动取消句柄":在缩略图等上传准备阶段调用时,SDK 会中止准备流程,上传不会启动,Promise 以{ code: 'upload_aborted', message: 'Upload aborted.' }拒绝(见 index.js 对xhr.abort的覆写:它会触发内部AbortController,调用options.abort,再以upload_aborted拒绝)。自定义缩略图生成器收到context.signal中止信号后应当尽快停止工作,因为 SDK 在真正发送文件前会等待缩略图准备完成。
除文档列出的四个回调外,UploadOptions还透传RequestCallbacks层的success与error回调(Promise 风格的补充):success(items)在成功解析后触发,error(e)在失败时触发(types.js)。使用options.error时注意,它只是通知钩子,Promise 仍然会以同一个错误拒绝,两者并不互斥。
返回值与失败语义:成功或全量失败,绝无"半好半坏"
调用返回一个Promise,其成功形态遵循"条数即形态":
items中只有一个条目时,resolve 为单个FSItem对象;items含多个条目时,resolve 为FSItem对象数组;- 任一环节失败时 Promise 一律reject——永远不会 resolve 出一个"部分成功 + 部分错误"的混合值(index.js 顶层承诺注释明确写死了这一约定)。
部分失败的细节:failedItems
当失败发生在"单个条目层面"而非"整个请求层面"时,拒绝值会额外携带failedItems数组,其中每个元素描述一个失败条目:
path:该条目的目标路径;message:失败原因描述;code、status:服务端给出时的错误码与 HTTP 状态码。
并且,部分失败不会回滚:已经写入成功的条目会保留,只有失败条目需要调用方决定如何补偿重试。
全部条目以同因失败时,错误码会被"上提"
如果failedItems里的每个条目都以完全相同的code和status失败,说明失败原因属于"整个请求"而非某个具体文件,SDK 会把这些字段同时挂在拒绝值本身上(见 signedBatchUpload.js 的sharedFailureFields:仅当所有失败条目共享同一非空code/status时才上提,混合失败则不猜测)。最典型的场景是账号存储配额耗尽:无论一次传入多少文件,上传都会以code: 'storage_limit_reached'、status: 413整体拒绝。调用方只需判断一次error.code即可命中此类情况(SDK 内部的promptIfStorageLimitError也会据此弹出空间不足提示,见 storageLimitPrompt.js)。
nodejs / workers 环境下的批次错误码
在nodejs与workers上,上传走后端转发式的旧/batch端点,此时拒绝值携带一组稳定的批次级code(实现于 legacyBatchUpload.js):
| 错误码 | 含义 |
|---|---|
batch_upload_failed | 批次内所有操作都失败,未写入任何内容 |
batch_upload_partially_failed | 部分操作成功、部分失败;failedCount与totalCount分别说明失败数与总数,results按发送顺序保存每个操作的原始结果 |
batch_upload_no_results | 请求本身成功(2xx),但服务端未报告任何写入结果 |
旧版通道以 HTTP218作为"批次内至少一个操作失败"的标志:SDK 会把响应体中的操作结果逐个按error: true或非 200 状态判为失败(isFailedBatchResult),再汇总为上述buildBatchFailureError结构;若options.strict为真(write()等内部操作使用),则直接抛出那个失败操作本身而不是批次汇总。
上传目录:平台差异与规避方案
目录上传(含拖入的目录条目,以及createFileParent隐式建目录)只在websites与apps上受支持。这与前文所述的通道差异直接相关:
- 签名批量直传通道会把目录条目作为
type: 'directory'的注册项发送(其contentType为application/x-puter-directory,size为 0,见 signedBatchUpload.js),后端据此建立目录树; - 旧版
/batch通道本质是"逐条 mkdir/write 请求 + 文件字节"的 multipart 转发,无法创建目录树。因此目录上传一旦落入该通道(典型即nodejs/workers),会直接以batch_upload_failed拒绝。
推荐规避方案:在nodejs/workers中不要试图上传目录对象,而是先用puter.fs.mkdir()建好目录,再逐个把目录内的文件用upload(file, dirPath)传进去。这也是官方文档明确建议的做法。
缩略图:内置生成器、自定义生成器与 PDF
generateThumbnails打开后,SDK 会在浏览器端为浏览器可解码的图片生成缩略图。内置生成器实现位于 thumbnails.js,其行为可以从常量与算法还原:
- 只在存在
document且输入是File时工作,并通过 MIME 前缀或扩展名(.png/.jpg/.jpeg/.gif/.bmp/.webp/.tiff/.avif/.jfif)判断是否为图片; - 用
<canvas>等比缩放,优先尝试image/webp(质量 0.85)→image/jpeg(质量 0.8)→image/png三种廉价编码,逐步把边长从默认128px折半(最小32px),直到编码结果小于 2 MiB(MAX_THUMBNAIL_BYTES = 2 * 1024 * 1024); - 整个过程是"尽力而为"的:任何一步失败都返回
undefined,绝不阻断原文件上传。
PDF 与内置生成器的边界
内置生成器不含 PDF 渲染能力(SDK 未内嵌 PDF.js);Puter 桌面端对 PDF 的预览图支持是桌面产品单独提供的。需要 PDF 缩略图的应用应当自带渲染器,通过thumbnailGenerator挂入,并把其余文件委托给内置生成器——官方示例正是这么写的:
const file = new File(['Hello!'], 'hello.txt', { type: 'text/plain' }); await puter.fs.upload(file, './', { thumbnailGenerator: async (file, { defaultGenerator, signal }) => { if (signal.aborted) return undefined; return defaultGenerator(file); }, });thumbnailGenerator回调约定(见 types.js 的ThumbnailGeneratorContext):
- 每个文件至多调用一次,可返回 Promise;
- 返回缩略图 data URL 或普通 URL;返回
undefined表示跳过该文件的缩略图; - 回调抛出的异常会被忽略(包装进 try/catch),不会阻断上传;
context.defaultGenerator(file)即上述内置图片生成器;context.signal是上传准备期的AbortSignal,取消时会置为 aborted,自定义生成器应据此尽早停手,并自行设定时间与资源预算;- 存在自定义生成器时,即使不写
generateThumbnails也会触发缩略图准备(generateThumbnails()的触发条件是二者其一为真)。
缩略图传输的容错规则
在签名直传通道中,data URL 形态的缩略图会先被转成 Blob、用独立的签名 URL 上传到对象存储,再在完成阶段引用其thumbnailUrl(见 signedBatchUpload.js 的uploadSignedFileTask)。这里的容错边界非常清晰:
- 单独的缩略图传输若失败或超过 5 秒(
THUMBNAIL_UPLOAD_TIMEOUT_MS = 5000,见 constants.js)会被跳过,原文件继续照常上传; - 显式的上传取消仍会中止整个流程;
- 原文件字节传输本身的错误则照常以 reject 暴露。
也就是说:缩略图只是"锦上添花",任何缩略图问题都不应让用户的核心上传失败。
底层原理:签名批量直传与旧版 /batch 双通道
puter.fs.upload的实现体现了 Puter 上传体系的两次架构演进。理解这两条通道,有助于排查进度异常与错误码差异。
通道一:签名批量直传(signed batch write)
针对web/gui/app环境,SDK 首选 signedBatchUpload.js 描述的流程:
- 把全部条目按每批 500 个分块(
SIGNED_BATCH_REQUEST_CHUNK_SIZE),以块组流水线方式(并发4个 chunk,SIGNED_BATCH_CHUNK_PIPELINE_CONCURRENCY)POST 到/fs/startBatchWrite,为每个文件换取临时签名存储 URL; - 文件字节直接由客户端 PUT 到签名 URL(不经过 Puter 服务器中转),文件级并发8(
SIGNED_BATCH_FILE_UPLOAD_CONCURRENCY);大文件按服务端声明的分片方案走 multipart,分片签名不足时调/fs/signMultipartParts补充,分片上传并发同为8(SIGNED_MULTIPART_PART_UPLOAD_CONCURRENCY),并收集每个分片的 ETag; - 全部字节就绪后,以
uploadId(sessionId)+ 分片 ETag 调/fs/completeBatchWrite完成落盘;若整批完成请求失败,则退化为对每个条目单独调/fs/completeWrite重试; - 任一失败文件会调
/fs/abortWrite清理其服务端会话,最终汇总为带failedItems的partial错误并上提共享code/status。
SDK 会把"后端是否支持签名直传"缓存在模块实例上(signedBatchWriteSupported):一旦某次/fs/startBatchWrite返回 404/405/501 等"能力缺失"信号(isSignedBatchWriteUnavailableError),本次立即回退到旧通道,且后续上传永久走旧通道,避免重复探测。反过来,一次成功会把能力标记为可用。
通道二:旧版 /batch 转发
legacyBatchUpload.js 是能力缺失或环境不支持(nodejs/workers)时的退路,也是nodejs/workers的唯一通道:
- 把
mkdir与write两类操作序列化进同一份FormData,连同全部文件字节一次性 multipart POST 到/batch,由服务器代为转发字节到云端存储; - 目录会按路径长度逆序(深的在前)整理,并用
$dir_i占位符把嵌套文件的路径改写为相对于父目录的形式; - 进度被拆成两段:客户端 → 服务器的字节(
xhr.upload事件)与服务器 → 云端的字节(通过 socket 的upload.progress事件 + 100ms 轮询),两者相加得到整体百分比。因此在nodejs/workers环境中若 socket 不可用,云端段的进度自然不前进——注释明确说明这是预期行为; - 也因此总字节量被按 2 倍估算(
totalSize * 2,一份给客户端上传、一份给服务器转发),operation_id会写进 FormData 与每个操作,用于服务端与进度事件的对账。
空间预检与其他守护逻辑
无论走哪条通道,上传前都有几道"前置闸门"(index.js):
- 空间预检:仅非 web 环境执行。当本次上传总量 ≥ 1 MiB(
SPACE_CHECK_MIN_BYTES,低于该阈值时预检往返开销大于省下的成本)时,先调用puter.fs.space()对比capacity - used与totalSize,不足则提前以NOT_ENOUGH_SPACE拒绝; - 空上传拦截:归一化后若既无文件也无目录,以
EMPTY_UPLOAD(No files or directories to upload.)拒绝; - 一次性
start:两条通道共用一个startCallbackFired标志,确保签名通道失败回退到旧通道后start回调不会重复触发。
服务端对应的落地实现可继续追读 FSController.ts 与 FSService.ts(含/fs/startBatchWrite、/fs/completeBatchWrite、/fs/abortWrite等路由与业务逻辑)。
完整可运行示例
示例一:从文件选择框上传(官方原例)
<html> <body> <!-- 在真实页面中请以你的部署方式加载 Puter.js SDK(构建产物见 src/puter-js/) --> <input type="file" id="file-input" /> <script> // File input let fileInput = document.getElementById('file-input'); // Upload the file when the user selects it fileInput.onchange = () => { puter.fs.upload(fileInput.files).then((file) => { puter.print(`File uploaded successfully to: ${file.path}`); }) }; </script> </body> </html>fileInput.files是一个FileList,被归一化后上传到应用根目录;因为只选了一个文件,成功回调里的file是单个FSItem,可直接读取其path等属性。
示例二:上传到指定目录 + 进度展示 + 显式取消
const input = document.getElementById('file-input'); input.onchange = async () => { // 通过 init 拿到句柄,稍后用于取消 let uploadXhr; try { const uploaded = await puter.fs.upload(input.files, './uploads', { createMissingParents: true, // 目标目录不存在时自动创建 generateThumbnails: true, // 浏览器端为图片生成缩略图 overwrite: false, // 不覆盖已存在文件 init: (operationId, xhr) => { uploadXhr = xhr; // 保留取消句柄 console.log('upload started with operationId:', operationId); }, start: () => console.log('sending...'), progress: (operationId, progress) => { console.log(`upload ${operationId}: ${Math.round(progress)}%`); }, abort: (operationId) => console.log('aborted:', operationId), }); // 多文件时 uploaded 是 FSItem[],单文件时是 FSItem const items = Array.isArray(uploaded) ? uploaded : [uploaded]; console.log('ok:', items.map((item) => item.path)); } catch (e) { if (e.code === 'upload_aborted') { console.log('用户取消了上传'); } else if (e.code === 'storage_limit_reached' && e.status === 413) { console.log('存储配额不足'); } else { console.error('部分/全部失败', e.failedItems ?? e); } } }; // 例如点击按钮时取消: function cancelUpload() { if (uploadXhr) uploadXhr.abort(); }示例三:Blob 与字符串内容的上传
单文件上传时 resolve 为单个FSItem,这也让"用代码构造内容再上传"变得非常方便(Blob 以options.name命名,字符串默认落盘为default.txt):
// Blob:需要显式命名 const blob = new Blob(['hello from blob'], { type: 'text/plain' }); await puter.fs.upload(blob, '/Documents', { name: 'note.txt' }); // 字符串:直接作为文本内容 await puter.fs.upload('hello from puter', '/Documents');示例四:nodejs / workers 中上传目录内文件
由于旧版/batch无法建目录树,nodejs里遇到目录应拆解为"先 mkdir、再逐个上传文件":
// nodejs 环境(示意):对目录下每个文件 await puter.fs.mkdir({ path: '/projects/my-app' }); for (const localFilePath of filesToUpload) { await puter.fs.upload(localFilePath, '/projects/my-app'); }常见错误码速查
| 错误码 / 形态 | 出现场景 | 处理建议 |
|---|---|---|
field_invalid | items不是任何受支持的类型 | 校验输入,传File/Blob/FileList/DataTransferItemList或字符串 |
EMPTY_UPLOAD | 归一化后没有任何文件或目录 | 检查拖拽解析结果是否为空 |
NOT_ENOUGH_SPACE | 非 web 环境空间预检不足(≥1 MiB 时触发) | 提示用户清理空间或升级配额 |
upload_aborted(Upload aborted.) | 准备期通过init句柄取消 | 属预期取消,按用户主动放弃处理 |
storage_limit_reached/status: 413 | 配额耗尽导致整批被拒(共享code/status被上提) | 一次判断e.code即可,不必遍历failedItems |
batch_upload_failed | nodejs/workers 旧通道整批失败(含目录上传) | 目录场景改用mkdir+ 逐文件上传 |
batch_upload_partially_failed | 旧通道部分成功部分失败 | 读取failedItems/failedCount/totalCount,重试失败项(成功项不会回滚) |
batch_upload_no_results | 旧通道 2xx 但服务端未报告写入结果 | 用stat/readdir核对目标是否实际写入 |
延伸阅读
- API 文档原文:FS/upload.md;文件系统总览:FS
- 返回对象形态:FSItem;配套操作:puter.fs.mkdir()、puter.fs.write()
- SDK 上传源码:operations/upload/(编排 index.js、归一化 entries.js、缩略图 thumbnails.js、签名通道 signedBatchUpload.js、旧通道 legacyBatchUpload.js、常量 constants.js)
- 相关测试:upload/index.test.js、upload/thumbnailUpload.test.js、upload/signedBatchUpload.test.js
- 服务端实现:FSController.ts、FSService.ts
【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考