Strapi Data Transfer 数据文件(.tar)内部结构详解:目录布局、JSONL 分片与加密压缩机制
Strapi 的数据迁移(Data Transfer)模块定义了标准的 Strapi Data File:一个以 .tar 为基础、可选 gzip 压缩和 AES 加密的归档文件,内部用 POSIX 风格路径组织 metadata.json 与各数据阶段的 JSONL 分片。读完本文,你将理解该数据文件的完整目录结构、metadata.json 的兼容字段、{stage}_{序号}.jsonl 的连续编号读取约定,以及 JSON Lines 流式解析为何能让大规模传输保持低内存占用,并能对照 Strapi File Structure 官方文档 在仓库源码中验证每一处约定。
数据文件的载体:tar + 可选 gzip + 可选加密
根据文档定义,Strapi 的文件 Provider(File Provider)期望的输入是一个 .tar 文件,它可以:
- 可选地被 gzip 压缩;
- 可选地用 aes-128-ecb 算法加密。
归档内部的路径一律使用 POSIX 风格(正斜杠分隔)。这一点在目标端 Provider 的落盘逻辑中得到了印证:最终产物的文件名就是 路径.tar,开启压缩后追加 .gz 后缀,再开启加密后追加 .enc 后缀,三段式扩展名顺序固定为 .tar.gz.enc,见 归档路径拼接逻辑:
get #archivePath() {
let filePath = `${file.path}.tar`;
if (compression.enabled) {
filePath += '.gz';
}
if (encryption.enabled) {
filePath += '.enc';
}
return filePath;
}
在流管线中,加密与压缩的顺序是:tar.pack() 产出原始归档字节流 → 先经过 gzip → 再经过加密密码器,写入磁盘。该管线组装见 bootstrap 中的 archiveTransforms。源端读取时的反序管线(fs.createReadStream → 解密 → gunzip)则在 源端 Provider 的 #getBackupStream 中实现。
加密算法的默认值正是文档所说的 aes-128-ecb:createEncryptionCipher 的第二个参数默认 'aes-128-ecb'。值得注意的实现细节是,密钥并非直接用作密码,而是先经 scryptSync(key, '', 16) 派生出 16 字节安全密钥再交给 createCipheriv,同一文件里还定义了 aes128/aes192/aes256 等其他策略(见 encrypt.ts)。
顶层目录结构:metadata 与四个数据阶段
文档给出的标准布局如下(此处完整继承原文档的结构树):
./
configuration
entities
links
metadata.json
schemas
./configuration:
configuration_00001.jsonl
./entities:
entities_00001.jsonl
./links:
links_00001.jsonl
./schemas:
schemas_00001.jsonl
即根目录下有一个 metadata.json 文件,加上 configuration、entities、links、schemas 四个目录,每个目录内放按序号编号的 .jsonl 文件。
从源码结构看,这四个阶段与源端 Provider 的一对读流方法一一对应:createEntitiesReadStream / createSchemasReadStream / createLinksReadStream / createConfigurationReadStream 分别调用 #streamJsonlDirectory('entities' | 'schemas' | 'links' | 'configuration'),以目录名为过滤条件从 tar 归档中筛选出对应 JSONL 文件逐个流式解析。也就是说,目录名即数据阶段(stage)的标识,Provider 不依赖任何清单文件,只靠路径前缀来归类。
此外,归档中还会携带二进制资产:媒体文件放在 assets/uploads/,其对应的 JSON 元数据放在 assets/metadata/{filename}.json。源端 createAssetsReadStream 用 tar 的 Parser 过滤 assets/uploads 下的文件条目,并逐一读取同名元数据组装成 IAsset 对象;目标端 createAssetsWriteStream 则反向写入这两条路径。资产是 tar 的原生文件条目而非 JSONL,这一点与四个文本数据阶段有本质区别。
metadata.json:来源信息与版本兼容检查
文档要求 metadata.json 至少包含:
createdAt:文件创建时间戳;strapi.version:创建该文件的 Strapi 版本(用于兼容性检查)。
示例(继承自原文档):
{
"createdAt": "2023-06-26T07:31:20.062Z",
"strapi": {
"version": "4.11.2"
}
}
该结构在类型层有精确对应——IMetadata 将 createdAt 与 strapi.version 都定义为可选字段:
export interface IMetadata {
strapi?: {
version?: string;
};
createdAt?: string;
}
源端 Provider 把 metadata.json 作为文件有效性的前置探针:bootstrap 阶段会先尝试解析该文件,一旦失败就抛出 ProviderInitializationError,且当文件处于加密状态时会提示“Key is incorrect or the file ... is not a valid Strapi data file.”——这实际上把“密钥错误”和“文件损坏”合并为同一诊断入口。getMetadata() 返回的元数据按 IProvider 接口的注释 用途是 version validation(版本校验),即迁移引擎据此做跨版本兼容判断。
目标端在收尾时会把源端元数据原样写回归档:LocalFileDestinationProvider.close 调用 #writeMetadata,通过 createTarEntryStream 写入名为 metadata.json 的 tar 条目。因此一份再导出的备份会保留源文件的版本与时间戳信息。
JSONL 分片:命名格式与“连续编号读到缺失即结束”的约定
文档对每个数据阶段目录的约定是:
- 文件名格式为
{stage}/{stage}_{5位序号}.jsonl,例如entities/entities_00001.jsonl; - 每个阶段可以有任意数量的文件,只要序号连续;
- 读取规则:读完
00001后尝试读取00002,若不存在,则认为该阶段数据读取完毕。
写入端的命名规则由 createFilePathFactory 固化,padStart(5, '0') 正好对应“5 位序号”:
export const createFilePathFactory =
(type: string) =>
(fileIndex = 0): string => {
// always write tar files with posix paths so we have a standard format for paths regardless of system
return posix.join(
// "{type}" directory
type,
// "${type}_XXXXX.jsonl" file
`${type}_${String(fileIndex).padStart(5, '0')}.jsonl`
);
};
注意注释明确说明:tar 内路径总是写成 POSIX 格式,与操作系统无关。
至于“什么时候切一个新分片”,createTarEntryStream 给出了答案:写入缓冲累计超过 maxSize 时 flush() 一个 tar 条目并使 fileIndex 自增。maxSize 的默认值是 2.56e8(约 256 MB),而 Provider 选项中的 file.maxSizeJsonl 允许调用方覆盖它——这对应 ILocalFileDestinationProviderOptions 中定义的 maxSize / maxSizeJsonl 参数(见 Destination 选项文档)。单个 chunk 本身超过上限时会直接抛出 payload too large 错误,防止单个 JSON 对象撑爆一个分片。
读取端并不用“先列目录再找最后一个文件”的方式,而是由 tar 的 Parser 按归档内实际出现的所有匹配条目依次触发 onReadEntry——由于 tar 条目顺序即写入顺序(00001、00002、…),文档描述的“读到缺失即完成”在流式读取下天然成立,无需随机访问。
为什么是 JSON Lines:逐行解析与恒定内存占用
原文档给出的理由是:JSON Lines(.jsonl)文件以换行符分隔 JSON 对象,使 Provider 可以一次只读一行,而不是把整个文件载入内存,从而最小化传输过程中的 RAM 占用,并允许单个文件包含任意多的数据。
源码中这一设计体现在 源端 #streamJsonlDirectory:每个 tar 条目(即一个 JSONL 分片)被接上 stream-json 的 jsonl/Parser(开启 checkErrors: true),逐行产出对象后再转成统一的下行流;写入端则对称地使用 stream-json/jsonl/Stringer 将对象逐行序列化(见 createEntitiesWriteStream 中的 chain([stringer(), entryStream]))。整条链路是纯流式的:tar 解包 → 逐行 JSON 解析 → 逐条写入,任何时刻内存中只有“一个 tar 条目的一行 + 少量缓冲”。
这个流式行为有专门的测试佐证,例如 源端 Provider 测试 中构造真实 tar 归档并验证 entities 读流在下游消费慢时能正确暂停(backpressure),说明大文件传输不会因缓冲失控。
另外有一个容易被忽略的兼容细节:分片解析失败时不会静默跳过,而是销毁输出流并抛出带上下文的 ProviderTransferError(指明是哪个备份文件解析出错,见 异常处理),便于用户定位损坏的分片。
POSIX 路径约定与旧版 Windows 路径的兼容
文档强调归档内部必须使用 POSIX 风格路径。源端的 路径工具函数 解释了为什么还需要额外的容错代码:在迁移引擎 4.9.0 及更早版本中,Windows 系统上生成的导出文件里混有 Windows 风格反斜杠路径;现在所有路径统一存为 POSIX,但为了兼容遗留文件和用户在 Windows 上用第三方 tar 工具手工制作的备份,读取时需要做分隔符归一化:
unknownPathToPosix:若路径不含正斜杠则按 win32 反斜杠转换为 POSIX;isPathEquivalent:判断两条未知格式路径归一化后是否指向同一位置(用于精确匹配metadata.json这类单文件);isFilePathInDirname:判断某文件是否落在指定阶段目录内(用于 JSONL 分片过滤)。
该文件头部注释还说明了一个约束:由于分隔符转换的存在,导出文件中的文件名永远不能包含正斜杠(即使转义也不行),这是手工制作 tar 备份时需要遵守的格式限制。
小结:从文档约定到源码实现的一一映射
| 文档约定 | 源码落点 |
|---|---|
.tar + 可选 gzip + 可选 aes-128-ecb |
归档路径与管线、加密策略 |
| 内部使用 POSIX 路径 | 路径归一化工具、createFilePathFactory 的 posix.join |
metadata.json 含 createdAt 与 strapi.version |
IMetadata 类型、bootstrap 前置校验 |
{stage}_{5位序号}.jsonl 命名 |
createFilePathFactory |
| 每阶段任意数量分片、读到缺失即结束 | 源端流式读取 |
| JSONL 逐行解析、低内存传输 | stream-json 的 jsonl/Parser / Stringer(源端与目标端读写流) |
配合 Source Provider 文档 与 Destination Provider 文档 可以进一步看到:CLI 导入文件时会根据扩展名(.gz、.enc)自动推断压缩/加密选项,而 source::local-file 与 destination::local-file 两个 Provider 正是本文所讲文件结构的生产者与消费者。理解这套布局后,你就能手工检查一份 Strapi Data File 的内部构成,或在自定义工具中按同一约定生成可被 Strapi 迁移引擎识别的备份文件。
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 StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00