Puter FSItem 对象开发指南:文件系统条目(文件/目录)的完整属性与操作方法
FSItem 是 Puter 文件系统中一切“条目”的统一对象模型:无论你通过 puter.fs.stat() 查询一个文件,还是通过 puter.fs.readdir() 列出一个目录,返回的都是 FSItem 实例。它把文件与目录共有的元数据(标识、路径、时间戳、大小、共享状态)和常用的内容操作(读、写、改名、移动、复制、删除、建目录)封装成了可直接调用、可链式串联的对象方法。读完本文你将能独立实现“读取/覆写文件内容、在目录内移动/复制/重命名条目、递归创建子目录并遍历目录树、处理重名冲突与目录专属操作限制”等典型开发场景,并理解 FSItem 在 puter.js 源码层是如何把方法转发给底层 FS 操作的真实机制。
FSItem 在 Puter 文件系统 API 中的定位
在 Puter 的文件系统抽象中,文件与目录被统一建模为“条目(entry / item)”。官方对象文档 fsitem.md 开宗明义地定义:
一个
FSItem对象代表 Puter 文件系统中的一个文件或一个目录。
它在 puter.js(浏览器侧 JavaScript SDK)中的落地实现在 FSItem.js,其类注释进一步说明了构造来源:
A file or a directory in the Puter file system. Accepts an entry in any of the shapes the API returns it in (
is_dir,fsentry_is_dir, …)。
也就是说,无论后端 API 返回的是旧版命名(is_dir、fsentry_*)还是 camelCase 命名,FSItem 的构造函数都会统一规整为文档化的属性。通常你在拿到 FSItem 之后,就能直接对条目本身发起后续操作,而无须再拼路径去调用全局的 puter.fs.*。
需要特别澄清一个常见误解:文档中 FSItem 对象带有一系列属性名(如 is_shared)由 stat() 与 readdir() 设置,这一点已在原文档标注——并非所有途径拿到的条目都带有全部字段,详见后文“属性”章节。
FSItem 的属性(Attributes)
原文档为 FSItem 定义了 9 个公开属性。下表汇总了名称、类型与含义,随后逐条展开并补充源码层面的细节。
| 属性 | 类型 | 含义 |
|---|---|---|
id |
String | 条目唯一标识,由 Puter 在创建条目时生成 |
name |
String | 条目的名称 |
path |
String | 条目相对文件系统根目录的路径 |
isDir |
Boolean | 是否为目录:true 为目录,false 为文件 |
created |
Integer | 创建时间的 Unix 时间戳 |
modified |
Integer | 最后修改时间的 Unix 时间戳 |
accessed |
Integer | 最后访问时间的 Unix 时间戳 |
size |
Integer | 条目大小(字节);目录该项为 null |
is_shared |
Boolean | null | 该条目是否被共享给了他人 |
id(String)
包含条目唯一标识符的字符串,由 Puter 在条目创建时生成。在源码层,FSItem 构造函数同时维护了 uid、id、uuid 三个同值字段(见 FSItem.js),取值优先级依次为 options.uid ?? options.uuid ?? options.fsentry_uid ?? options.fsentry_id ?? options.fsentry_uuid ?? options.id。id 之所以重要,在于条目的 uid 永不改变——即使条目被移动或重命名,用 uid 仍能稳定寻址(这是后续 rename() 优先按 uid 调用的依据)。
name(String)
条目的名称字符串,来自 API 的 name 或 fsentry_name。
path(String)
条目相对文件系统根目录的路径。在 puter.js 的操作实现中,传给后端的路径会经过 getAbsolutePathForApp() 解析,相对路径会相对于应用自己的根目录解析(参见 stat.js 与 readdir.js)。
isDir(Boolean)
标识条目是否为目录的布尔值。若为 true,表示该条目是目录;若为 false,则表示该条目是文件。这是 FSItem 最常用的判别字段,因为它决定了能否安全调用 read()/write()(仅文件)或 mkdir()/readdir()(仅目录)。
从源码看,这一字段还带着向后兼容的历史包袱:构造函数中保留了更早的别名 isDirectory,并且读取时兼容 options.isDirectory || options.isDir || options.is_dir || options.fsentry_is_dir 四种拼写(FSItem.js)。对应测试 FSItem.test.js 明确断言 isDir 与 isDirectory 在文档化拼写和旧拼写下均保持一致。也就是说,老代码里的 item.isDirectory 依然可用,但新代码应以文档为准使用 isDir。
created / modified / accessed(Integer)
三个整数类型的 Unix 时间戳(毫秒级,从后端直接透传),分别表示条目的创建时间、最后修改时间与最后访问时间。源码中它们同样兼容 fsentry_created / fsentry_modified / fsentry_accessed 拼写。
size(Integer)
以字节为单位的条目大小。当条目是目录时,size 为 null。这条规则在原文档与源码(FSItem.js 的 JSDoc:null if the item is a directory)中都有明确表述。若需要目录的累计体积,可使用 stat() 的 returnSize 选项做服务器端汇总(参见 stat.js),而不是读 size 字段。
is_shared(Boolean | null)
该条目是否已与他人共享:
true:已被共享;false:未被共享;null:条目不属于你(无法获知共享状态)。
原文档同时给出了三条重要的语义边界,理解它们能避免误用:
- 统计口径:它统计的是持有该条目
manage权限的任何人授予的共享; - 只看条目本身:只反映条目自身上的共享,不包含从被共享的父目录继承来的访问权限;
- 来源受限:该字段仅由
stat()与readdir()设置;通过其他任何方式(例如write()、move()返回的条目)获取的 FSItem 不携带此字段。
这一点与 puter.js 中 stat() 的 returnShares 选项存在关联:当传入 returnShares: true 时,/stat 响应会附带 shares 数组并转换为共享对象(见 stat.js)。而 is_shared 本身是由后端在普通(非 returnShares)场景下随条目元数据返回的汇总标志。
源码中的非文档化属性(内部表面)
除文档列出的公开属性外,FSItem 实例还带有若干辅助字段,开发时值得了解但不应过度依赖(源码将其设计为“可能变化或消失”的非公开表面):
readURL/writeURL/metadataURL:可读/可写/元数据的签名 URL;_internalProperties:不可枚举的内部属性包(enumerable: false),其中signature与expires会从签名 URL 的查询参数中推断(FSItem.js),还包含一个file_signaturegetter,产出与/sign端点一致的形状,可传给launch_app让其他应用打开该文件;watch/open/setAsWallpaper/versions/trash/metadata:源码中已预留但标注// todo - implement的占位方法(见 FSItem.js 与 #L294-L337),当前调用为空操作,不应在正式代码中依赖。
FSItem 的方法(Methods)
FSItem 的公开方法本质上是把“对条目自身的操作”转换为对 puter.fs.* 对应能力的调用。单元测试 FSItem.test.js 的注释点明了设计意图:这些方法是 puter.fs.* 的薄包装,测试锁定了它们转发参数的精确形状。
read()
读取文件的内容。
语法
fsitem.read()
参数
无。
返回值
一个 Promise,解析为包含文件内容的 Blob(浏览器标准 Web API 对象)。拿到 Blob 后,既可以直接作为资源使用,也可以用 await blob.text() 取字符串。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.write('hello.txt', 'Hello, world!');
const item = await puter.fs.stat('hello.txt');
const blob = await item.read();
puter.print(await blob.text());
})();
</script>
</body>
</html>
源码层面,read() 直接转发为 puter.fs.read(this.path)(FSItem.js),测试断言其按 path 发起读取(FSItem.test.js)。更完整的参数能力(如范围读取)参见 read 操作文档。
write()
向文件写入数据,覆盖其原有内容。
语法
fsitem.write(data)
参数
data(String | File | Blob,必填):要写入文件的数据。
返回值
一个 Promise,解析为写入后文件的 FSItem 对象。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.write('hello.txt', 'Hello, world!');
const item = await puter.fs.stat('hello.txt');
await item.write('Updated contents!');
puter.print('File updated');
})();
</script>
</body>
</html>
注意这里的语义与全局 puter.fs.write() 不同:FSItem 的 write() 固定以 overwrite: true、dedupeName: false 转发(FSItem.js),即“就地覆写、不做自动去重改名”。测试用例对参数形状做了精确锁定(FSItem.test.js),并验证二进制 Blob 数据会被原样透传。若需要“写入时若重名则自动改名”,应改走全局 write 操作文档 的参数化形式。
rename()
重命名条目。
语法
fsitem.rename(newName)
参数
newName(String,必填):条目的新名称。
返回值
一个 Promise,解析为重命名后条目的 FSItem 对象。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.write('hello.txt', 'Hello, world!');
const item = await puter.fs.stat('hello.txt');
await item.rename('renamed.txt');
puter.print('File renamed');
})();
</script>
</body>
</html>
rename() 的转发逻辑在源码中最为讲究:优先以 uid 寻址,仅当对象没有 uid 时才退回按 path 寻址(FSItem.js)。代码注释解释了原因——this object was handed out 之后条目可能已被移动,但 uid 永不改变。对应测试覆盖了两种分支(FSItem.test.js)。这保证了即使 FSItem 是通过较早的 stat 结果获得的,改名的目标依然精确。
move()
将条目移动到另一个位置。
语法
fsitem.move(destination)
fsitem.move(destination, overwrite)
fsitem.move(destination, overwrite, newName)
参数
destination(String,必填):要移入的目录,或条目的新路径。overwrite(Boolean,可选):目标处已存在条目时是否覆盖。默认false。newName(String,可选):新位置处条目的名称。默认沿用当前名称。
返回值
一个 Promise,解析为移动后条目的 FSItem 对象。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const dirname = puter.randName();
await puter.fs.mkdir(dirname);
await puter.fs.write('hello.txt', 'Hello, world!');
const item = await puter.fs.stat('hello.txt');
await item.move(dirname);
puter.print(`Moved hello.txt into ${dirname}`);
})();
</script>
</body>
</html>
源码实现将 overwrite 与 newName 作为选项对象转发(FSItem.js),overwrite 默认 false、newName 缺省不传。测试断言了两种签名组合下的精确转发(FSItem.test.js)。更复杂的移动能力(如跨目录与重名处理策略的完整选项)参见 move 操作文档。
copy()
将条目复制到另一个目录。
语法
fsitem.copy(destinationDirectory)
fsitem.copy(destinationDirectory, autoRename)
fsitem.copy(destinationDirectory, autoRename, overwrite)
参数
destinationDirectory(String,必填):要把条目复制进去的目录。autoRename(Boolean,可选):目标处已存在同名条目时,是否自动挑选一个空闲名称。默认false,即重名冲突时复制失败。overwrite(Boolean,可选):是否覆盖目标处已存在的条目。默认false。
返回值
一个 Promise,解析为复制后条目的 FSItem 对象。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const dirname = puter.randName();
await puter.fs.mkdir(dirname);
await puter.fs.write('hello.txt', 'Hello, world!');
const item = await puter.fs.stat('hello.txt');
await item.copy(dirname);
puter.print(`Copied hello.txt into ${dirname}`);
})();
</script>
</body>
</html>
源码层面 copy() 把 autoRename 映射为底层选项 dedupeName,并把 overwrite 一并转发(FSItem.js)。这里有一个值得注意的细节:当调用者没有传 autoRename 时,dedupeName 保持 undefined 而非 false——测试注释解释了原因:后端 copy 的默认行为就是“复制且重名时自动改名”,如果强制发送 dedupeName: false,反而会把后端的默认宽松行为变成冲突报错(FSItem.test.js)。也就是说,在 FSItem.copy 的语境下,三参签名里的第二个参数 autoRename 一旦显式传 true 才会去重,传 false 或省略则交给后端默认逻辑;这与 move() 中 overwrite 的默认语义并不相同,使用时务必区分。完整选项参见 copy 操作文档。
delete()
删除条目。
语法
fsitem.delete()
参数
无。
返回值
一个 Promise,在条目被删除后解析。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.write('hello.txt', 'Hello, world!');
const item = await puter.fs.stat('hello.txt');
await item.delete();
puter.print('File deleted');
})();
</script>
</body>
</html>
源码直接转发 puter.fs.delete(this.path)(FSItem.js)。注意目前删除是物理删除而非进回收站——源码中 trash() 方法仍是 // todo implement trashing 的占位状态(FSItem.js),正式代码请勿假设存在可恢复的“回收站”语义。
mkdir()
在该条目内部创建新的子目录。该条目必须是目录,否则抛出错误。
语法
fsitem.mkdir(name)
fsitem.mkdir(name, autoRename)
参数
name(String,必填):要创建的子目录名称。autoRename(Boolean,可选):当同名目录已存在时,是否自动挑选空闲名称。默认false。
返回值
一个 Promise,解析为所创建目录的 FSItem 对象。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const dirname = puter.randName();
await puter.fs.mkdir(dirname);
const dir = await puter.fs.stat(dirname);
await dir.mkdir('subdir');
puter.print(`Created subdir inside ${dirname}`);
})();
</script>
</body>
</html>
源码在调用前先做了目录校验:若 this.isDirectory 为假,直接抛出 Error('mkdir() can only be called on a directory'),并且不会发起任何后端请求(FSItem.js)。测试对此分支做了断言——非目录对象调用 mkdir 抛错且 fs.mkdir 从未被调用(FSItem.test.js)。同时注意子目录路径是使用 path.join(this.path, name) 拼出来的。全局能力参见 mkdir 操作文档。
readdir()
列出该条目的内容。该条目必须是目录,否则抛出错误。
语法
fsitem.readdir()
参数
无。
返回值
一个 Promise,解析为 FSItem 对象数组,目录中的每一项对应一个 FSItem。
示例
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const dirname = puter.randName();
await puter.fs.mkdir(dirname);
await puter.fs.write(dirname + '/hello.txt', 'Hello, world!');
const dir = await puter.fs.stat(dirname);
const children = await dir.readdir();
children.forEach((child) => puter.print(child.name + '<br>'));
})();
</script>
</body>
</html>
与 mkdir() 一致,readdir() 在非目录对象上调用会抛出 Error('readdir() can only be called on a directory') 且不发起请求(FSItem.js),测试覆盖了该约束(FSItem.test.js)。实际的后端列出逻辑则由 readdir.js 承载:默认返回完整数组(内部逐页抓取并合并),传入 cursor/includeTotal 则返回 {items, cursor?, total?} 分页封套,stream: true 时可获得异步迭代器。每次成功读取后,目录内的各条目还会被写入 puter.js 客户端缓存(键为 item:<path>),从而让随后的按 path 查询可以命中 consistency: 'eventual' 的缓存。带参数分页的完整用法参见 readdir 操作文档。
把 FSItem 与 Puter FS API 结合使用
FSItem 的方法覆盖了绝大多数“拿到条目之后做什么”的需求,但理解它与全局 FS API 的分工,能让代码更清晰:
- 获得 FSItem 的入口:主要是
puter.fs.stat()(单条目元数据)与puter.fs.readdir()(目录条目数组);此外puter.fs.write()、mkdir()等操作也会在成功时返回对应条目的 FSItem。 - 获得
is_shared等共享信息:必须经由stat()/readdir()取得条目,其相关能力可结合 共享相关文档 与 share 文档 交叉使用。 - FSItem 方法无法覆盖的参数化能力(如
write的自动去重、copy的精细冲突策略、readdir的分页与流式遍历、stat的returnSize/returnPermissions/returnVersions等)应回退到全局 FS 模块的操作函数。FSItem 的薄包装设计(见 FSItem.js 各方法实现)也印证了这一点:它保证的是常用路径的简洁,而不是对后端能力的穷尽封装。
典型组合用法:遍历目录树并清理
综合本文各方法,可以写出一个“递归遍历 + 条件删除”的典型流程:对根目录 readdir() 得到子项数组,依据 isDir 决定进入子树还是处理文件;需要递归时对目录 FSItem 再次调用 readdir(),清理时则调用 delete()。配合 mkdir() 的目录前置校验,整个流程无需关心“目标到底是不是目录”——FSItem 自己会在语义不匹配时及时抛出可读的错误信息。这样一个 20 行左右的函数,就完整覆盖了 Puter 文件系统中最常用的“对象式”操作方式。
小结
FSItem 把 Puter 文件系统面向用户的编程模型统一成了“条目即对象”:id/name/path/isDir 描述身份与形态,created/modified/accessed/size 描述元数据,is_shared 描述共享边界(仅来自 stat()/readdir()),而 read/write/rename/move/copy/delete/mkdir/readdir 八个方法则覆盖了文件与目录上的核心操作。源码实现(FSItem.js 及其测试 FSItem.test.js)表明,这些方法是对 puter.fs.* 的精确转发:rename 优先按 uid 寻址以抵御移动,write 固定就地覆写,copy 的 autoRename 与后端 dedupeName 存在微妙的映射规则,mkdir/readdir 对非目录调用会抛错拒发请求。把握这些边界,你的 Puter 应用代码就能在“简洁”与“可控”之间取得平衡。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00