
Remix file-storage 包完整演进与实现解析从 LocalFileStorage 到 createFsFileStorage 的文件存储 API 迁移指南【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remixremix-run/file-storage是 Remix 生态中专用于服务端File对象的 key/value 存储库为本地磁盘与内存提供统一的存储后端。本文以 packages/file-storage/CHANGELOG.md 的版本演进为骨架结合当前仓库源码逐一拆解其 API 设计、分页列举、哈希分片与 LazyFile 流式读取等底层实现并给出从旧版LocalFileStorage/MemoryFileStorage类到新版工厂函数createFsFileStorage()/createMemoryFileStorage()的完整迁移路径。读完本文你将掌握该库全部核心操作get/set/put/remove/has/list的用法、参数语义、默认值以及版本升级时的破坏性变更清单。一、包定位面向服务端 File 对象的 key/value 存储packages/file-storage/README.md 对该包的定位做了清晰描述它提供面向服务端File对象的 key/value 存储接口让 Remix 应用能够在本地磁盘与内存两种后端之间使用同一套 API。其核心特性包括简单 API直观的 key/value 接口类似 Web Storage但存储的是File而非字符串多后端内置文件系统后端与内存后端另有独立的 file-storage-s3 提供 S3 后端流式支持可从存储中流式读取与写入文件内容元数据保留完整保留file.name、file.type、file.size、file.lastModified等File元数据。从 package.json 的exports字段可以看到当前包的三个公开入口remix-run/file-storage类型定义、remix-run/file-storage/fs文件系统后端与remix-run/file-storage/memory内存后端对应的源码文件分别为 src/index.ts、src/fs.ts 与 src/memory.ts。二、当前 API 面貌FileStorage 接口与两种后端CHANGELOG.md 中 v0.13.0 是一次里程碑式的破坏性变更LocalFileStorage类被createFsFileStorage(directory)工厂函数取代MemoryFileStorage类被createMemoryFileStorage()工厂函数取代随后 v0.13.5 又引入FileLike别名并让FileStorage接口对具体后端返回的File值类型泛型化。当前版本的完整接口定义位于 src/lib/file-storage.ts包含六个方法方法签名语义getget(key): file \| null按 key 读取文件不存在时返回nullhashas(key): boolean判断某 key 是否存在setset(key, file): void将文件写入指定 keyputput(key, file): file写入并立即返回由该存储支撑的新文件setget的便捷组合removeremove(key): void删除指定 key 的文件listlist(options?): ListResult按条件列举存储中的文件支持分页2.1 文件系统后端createFsFileStoragesrc/lib/backends/fs.ts 实现了createFsFileStorage(directory)。创建时会校验传入路径若路径已存在但不是目录抛出Path ... is not a directory错误若路径不存在则递归创建目录fs.mkdirSync(rootDir, { recursive: true })。源码注释还强调了两个重要约定不做覆盖防护实现“不会尝试避免覆盖已有文件”因此传入的目录应是一个专门为本次存储对象新建、独占使用的目录key 与磁盘文件名无关key 可以是任意字符串包括文件系统不允许的字符多个同名File也可以存进同一个存储对象——实际落盘路径由 key 的哈希决定而非文件名。一个完整的读写示例来自 README.mdimport { createFsFileStorage } from remix/file-storage/fs let storage createFsFileStorage(./user/files) let file new File([hello world], hello.txt, { type: text/plain }) let key hello-key // 将文件写入存储 await storage.set(key, file) // 稍后读取 let fileFromStorage await storage.get(key) if (fileFromStorage ! null) { // 原文件元数据完整保留 fileFromStorage.name // hello.txt fileFromStorage.type // text/plain // 文件系统后端返回 LazyFile可直接流式读取 let response new Response(fileFromStorage.stream()) } // 从存储中删除 await storage.remove(key)2.2 内存后端createMemoryFileStoragesrc/lib/backends/memory.ts 用Mapstring, File实现同名接口。值得注意的实现细节putFile在写入时会通过file.arrayBuffer()将内容缓冲为独立副本再以new File([buffer], name, { lastModified, type })重建一个新File存入 Map——这既是 v0.6.0 中缓冲 MemoryFileStorage 中文件内容的延续也保证了存入的文件不会因外部File的后续变更而受影响。三、list()前缀过滤、元数据列举与游标分页storage.list(options)是 v0.6.0 加入的核心能力其options在 src/lib/file-storage.ts 中有精确定义CHANGELOG.md v0.6.0 一节给出了完整的参数语义选项类型说明cursorstring不透明的分页游标用于在存储的 key 间翻页includeMetadataboolean为true时在结果中包含文件元数据limitnumber返回文件的最大数量prefixstring只返回 key 以该字符串开头的文件3.1 基础列举与元数据不带任何选项时result.files是{ key: string }对象的数组let result await storage.list({ prefix: user123/ }) console.log(result.files) // [ // { key: user123/... }, // { key: user123/... }, // ... // ]传入includeMetadata: true后每个条目扩展为完整的 FileMetadatalastModified、name、size、type均为毫秒时间戳 / 文件名 / 字节数 / MIME 类型let result await storage.list({ prefix: user123/, includeMetadata: true }) console.log(result.files) // [ // { // key: user123/..., // lastModified: 1737955705270, // name: hello.txt, // size: 16, // type: text/plain // }, // ... // ]3.2 游标分页分页通过结果对象中的不透明cursor属性完成若cursor不为undefined说明还有更多文件将其原样传回下次调用的options即可取得下一页。完整遍历整个存储的惯用写法let result await storage.list() console.log(result.files) while (result.cursor ! undefined) { result await storage.list({ cursor: result.cursor }) console.log(result.files) }limit用于控制每次返回的条数。两个后端的默认值有所不同来自源码文件系统后端 fs.tslimit默认32内存后端 memory.tslimit默认Infinity。fs.test.ts 的lists files with pagination用例验证了完整分页闭环limit: 2返回 2 条并给出非空游标用该游标继续list取回剩余 3 条两次结果合并后与全部 5 个 key 完全一致limit: 0则返回空数组且无游标。lists files by key prefixfs.test.ts验证了prefix: b只返回b与b/c两个 key。四、底层实现原理哈希分片、元数据文件与 LazyFile4.1 SHA-256 哈希与两级目录分片文件系统后端在落盘前会对 key 计算哈希fs.ts 的computeHash默认SHA-256并以哈希的前 2 个十六进制字符作为子目录名、完整哈希作为文件名基础文件本体rootDir/hash前2位/hash.dat元数据rootDir/hash前2位/hash.meta.jsonasync function getPaths(key: string) { let hash await computeHash(key) let directory path.join(rootDir, hash.slice(0, 2)) return { directory, filePath: path.join(directory, ${hash}.dat), metaPath: path.join(directory, ${hash}.meta.json), } }这正是 CHANGELOG 中两条演进的核心内容v0.4.0引入分片存储目录shards storage directories将文件分散到多个子目录以提升文件系统扩展性并修复了并发set的竞态问题v0.6.0分片目录名从8 个字符缩减为 2 个字符BREAKING CHANGE在可扩展性与目录数量之间取得平衡——2 位十六进制最多 256 个分片目录v0.9.0remove删除文件后若所在分片目录已空则一并移除fs.ts 中readdir判空后rmdirfs.test.ts 的removes empty hash directories after removing files用例专门覆盖此行为。4.2 元数据持久化与文件大小入元数据写入时putFile后端会将key、lastModified、name、size、type序列化为 JSON 写入.meta.jsonlet meta: FileMetadata { key, lastModified: file.lastModified, name: file.name, size: file.size, type: file.type, } await fsp.writeFile(metaPath, JSON.stringify(meta))v0.13.5的变更点是文件系统存储改为把文件大小持久化进元数据而不是在带元数据列举时另行推导 size——fs.test.ts 的stores file size in metadata用例直接读取磁盘上的.meta.json断言size字段与源文件一致验证了这一点。4.3 返回 LazyFile支持流式读取get与put返回的并不是内存中的普通File而是通过openLazyFile(filePath, { lastModified, name, type })来自remix-run/fs打开的LazyFile见 fs.ts。LazyFile 是 lazy-file 提供的流式File实现内容按需从磁盘流式读取因此可以直接let response new Response(fileFromStorage.stream())而不会先把整个文件读入内存。这正是 README Streaming Support 特性的底层支撑也是 v0.13.1 起改用remix-run/fs的openLazyFile()新 API、v0.12.0 将remix-run/fs引入依赖关系的原因。4.4 旧文件自动清理v0.2.1起LocalFileStorage在向同一 key 写入新文件时会自动清理旧文件。当前实现中remove采用Promise.all([unlink(filePath), unlink(metaPath)])同时删除.dat与.meta.json并以isNoEntityErrorENOENT容错保证对不存在文件的删除不会抛错。五、put()写入后立即取得可读文件v0.5.0新增storage.put(key, file)作为set(key, file)get(key)这一高频组合的便捷封装。使用前后对照// 之前 await storage.set(key, file) let newFile await storage.get(key)! // 之后 let newFile await storage.put(key, file)对文件系统后端而言put内部走同一套putFile流程写文件 → 写元数据 →openLazyFile返回可直接流式读取的LazyFilefs.ts内存后端则返回缓冲后的新Filememory.ts。fs.test.ts 的puts files用例验证了put返回文件在name、type、lastModified、size上与源文件一致且has(key)立即为真。六、版本演进时间线从 v0.1.0 到 v0.13.7综合 CHANGELOG.md 全部条目可梳理出该包从 2024-08 到 2025-11 的关键演进版本日期关键变更类型v0.1.02024-08-24初始发布—v0.2.02024-08-26LocalFileStorage/MemoryFileStorage分别移到file-storage/local、file-storage/memory导出破坏性v0.2.12024-09-04同 key 写入新文件时自动清理旧文件修复v0.3.02024-11-14新增 CommonJS 构建升级 lazy-file3.1.0增强v0.4.02025-01-08修复并发set竞态存储目录分片增强/修复v0.4.12025-01-10修复 npm 包中file-storage/local类型缺失修复v0.5.02025-01-25新增storage.put(key, file)增强v0.6.02025-02-04分片目录名 8 字符改 2 字符内存后端缓冲文件内容新增storage.list(options)破坏性v0.6.12025-02-06修复与form-data-parser配合使用的回归修复v0.7.02025-06-10将/src打入 npm 包go to definition 直达源码统一类型esbuild 直接构建增强v0.8.02025-07-21包名从mjackson/file-storage改名为remix-run/file-storage破坏性v0.9.02025-07-25LocalFileStorage删除文件后移除空的分片目录增强v0.10.02025-10-22移除 CommonJS 构建仅保留 ESM破坏性v0.11.02025-11-05remix-run/lazy-file移入peerDependencies改用tsc构建dist目录镜像src布局构建v0.12.02025-11-20新增remix-run/fspeer dependency改从remix-run/fs导入依赖v0.13.02025-11-25类改工厂函数见下节破坏性v0.13.1—升级remix-run/fspeer 依赖使用新openLazyFile()API依赖v0.13.2—remix-run/*peer 依赖改为普通依赖依赖v0.13.3 ~ v0.13.4—滚动升级fs与lazy-file依赖依赖v0.13.5—新增FileLike别名FileStorage泛型化createFsFileStorage()暴露LazyFile返回类型文件大小持久化进元数据增强v0.13.6 ~ v0.13.7—滚动升级fs与lazy-file依赖依赖七、v0.13.0 迁移指南类改为工厂函数v0.13.0 是迁移成本最高的一次破坏性变更CHANGELOG.md 给出了标准的前后对照。变更前import { LocalFileStorage } from remix-run/file-storage/local import { MemoryFileStorage } from remix-run/file-storage/memory let fsStorage new LocalFileStorage(./files) let memoryStorage new MemoryFileStorage()变更后import { createFsFileStorage } from remix-run/file-storage/fs import { createMemoryFileStorage } from remix-run/file-storage/memory let fsStorage createFsFileStorage(./files) let memoryStorage createMemoryFileStorage()注意两点变化导入路径变化file-storage/local变为file-storage/fs同时入口也从旧版导出路径迁移为 package.json 中声明的./fs、./memory子路径实例化方式变化new关键字不再使用改由工厂函数直接返回存储对象调用点不再需要new其余方法签名不变因此业务代码中set/get/list/remove的调用无需改动。在 v0.13.5 之后工厂函数还有类型层面的收益createFsFileStorage()返回FileStorageLazyFilecreateMemoryFileStorage()返回FileStorageFile编译器能在调用get()/put()时就明确你拿到的具体文件类型。fs.test.ts 顶部就有一段编译期 API 契约检查断言原生File与LazyFile均满足FileLike且createFsFileStorage的返回类型满足FileStorageLazyFile——一旦 API 契约漂移TypeScript 会直接让测试文件编译失败。八、工程化与依赖演进ESM-only、tsc 构建与依赖策略CHANGELOG 中还有一组不改变 API 但影响使用方式的工程化变更升级时同样需要留意v0.10.0 起仅支持 ESMCommonJS 构建被移除。若项目仍处于 CommonJS 环境需要使用动态import()引入该包v0.7.0 起 npm 包包含/src类型定义与源码布局一致IDE 的 go to definition 可以直接跳到真实源码便于阅读与调试v0.11.0 起用tsc构建dist目录镜像src目录布局替代此前 esbuild/tsup 的扁平化输出模块间相对路径在构建后保持一致依赖策略的两次转向v0.11.0 将remix-run/lazy-file移入 peerDependenciesv0.12.0 新增remix-run/fspeer dependency而v0.13.2 又将remix-run/*peer 依赖改回普通 dependencies见 package.jsonremix-run/fs与remix-run/lazy-file均为直接依赖——这意味着安装remix-run/file-storage时会自动带上这两个依赖无需手动安装 peer 依赖。九、生态协作与 form-data-parser、lazy-file、S3 的关系README.md 列出了三个关联包构成完整的文件上传-存储链路form-data-parser解析multipart/form-data请求中的FileUpload与 file-storage 配合即可边解析边入库。v0.6.1 修复的正是二者配合时的回归问题fs.test.ts 的集成用例演示了完整流程parseFormData(request, async (file) { await storage.set(hello, file) })之后storage.list({ includeMetadata: true })能取回 key、文件名、大小、MIME 类型与时间戳全部元数据lazy-filefile-storage 内部使用的流式File实现文件系统后端的get/put返回的就是LazyFilefile-storage-s3同一FileStorage接口的 S3 后端业务代码可在本地磁盘、内存与 S3 之间无痛切换。十、总结与升级建议remix-run/file-storage的演进史本质上是一个API 从类到工厂、后端从单一到可插拔、底层从整读整写到流式分片的收敛过程。对使用方而言当前版本最重要的三点结论只用工厂函数创建存储文件系统用createFsFileStorage(./dir)内存用createMemoryFileStorage()二者共用同一FileStorage接口src/lib/file-storage.ts利用 list() 的能力prefix做目录式筛选、includeMetadata拿完整元数据、cursorlimit做分页文件系统后端默认每页 32 条升级时对照破坏性清单v0.13.0类→工厂、v0.10.0ESM-only、v0.8.0包改名、v0.6.0分片目录 2 字符 list API、v0.2.0local/memory 子路径导出——若从 0.x 早期版本直升需要一次性处理这些迁移点而 CHANGELOG.md 本身即是最完整的迁移手册。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考