Strapi Data Transfer 之 Local Strapi Source Provider 全解:从初始化实例读取实体、关系、配置与资产的传输源
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
本文围绕 Strapi monorepo 中@strapi/data-transfer包的Local Strapi Source Provider(本地 Strapi 数据源提供者)展开:它直接连接一个已初始化的Strapi实例,借助数据库查询引擎(Query Engine / queryBuilder)以流式方式读取实体、关系链接、应用配置、Schema 与媒体资产,是strapi export命令与远程 Pull 流程的数据出口。读完本文,你将掌握该 Provider 的两个选项getStrapi/autoDestroy的确切语义、bootstrap 与 close 的生命周期行为,以及各数据流(entities / links / configuration / schemas / assets)的底层实现路径与边界条件。
文档定位:Source Provider 是什么
官方文档条目 Local Strapi Source 对该 Provider 的一句话定义是:
This provider will retrieve data from an initialized
strapiinstance using its Entity Service and Query Engine.
即:该 Provider不从文件、也不是从远端 API取数,而是直接驱动一个已在当前进程中初始化的 Strapi 实例,通过其数据库查询能力把数据"泵"进 Transfer Engine 的读取流。源码入口位于 LocalStrapiSourceProvider 实现,工厂函数为createLocalStrapiSourceProvider,Provider 的注册名为source::local-strapi。
它属于>export interface ILocalStrapiSourceProviderOptions { getStrapi(): Core.Strapi | Promise<Core.Strapi>; // return an initialized instance of Strapi autoDestroy?: boolean; // shut down the instance returned by getStrapi() at the end of the transfer }
逐项说明:
getStrapi(必填):一个返回已初始化Strapi 实例的函数(允许异步)。Provider 本身不负责启动 Strapi——实例的创建、配置加载、数据库连接都由调用方完成,Provider 只消费它。这与"谁拥有实例,谁负责实例化"的所有权模型一致。autoDestroy(可选,默认 true):控制传输结束时是否调用strapi.destroy()。源码在 close() 方法 中的判定是:
async close(): Promise<void> { const { autoDestroy } = this.options; assertValidStrapi(this.strapi); this.strapi.db.lifecycles.enable(); // Basically `!== false` but more deterministic if (autoDestroy === undefined || autoDestroy === true) { await this.strapi?.destroy(); } }也就是说,只有显式传autoDestroy: false才不销毁实例。单元用例 index.test.ts 的 Close 组 验证了三种取值:undefined与true时destroy恰好被调用一次,false时从未调用。
注意所有权陷阱:如果实例是由你的进程(而非 Provider)创建的,通常应保持同一实例的生命周期由自己管理,此时考虑
autoDestroy: false;而strapi exportCLI 场景下实例由命令本身创建、用完即弃,交由 Provider 自动销毁即可。
生命周期:bootstrap 阶段的关键动作
bootstrap()是 Transfer Engine 启动传输前调用 Provider 的入口,源码见 bootstrap:
async bootstrap(diagnostics?: IDiagnosticReporter): Promise<void> { this.#diagnostics = diagnostics; this.strapi = await this.options.getStrapi(); this.strapi.db.lifecycles.disable(); }两件事值得关注:
- 延迟获取实例:只有调用
bootstrap后this.strapi才被赋值(测试用例 Bootstrap 组 确认了"bootstrap 之前provider.strapi未定义")。 - 禁用数据库生命周期钩子:读取过程中调用
this.strapi.db.lifecycles.disable(),避免读取触发的查询意外激活业务方定义的 lifecycle 回调;close()时对称地enable()恢复。
Provider 还内置了诊断上报:#reportInfo/#reportWarning/#reportError会把事件写入IDiagnosticReporter(origin 标记为local-source-provider),CLI 侧通过engine.diagnostics.onDiagnostic(...)订阅并打印。流读取过程中的错误会经由#handleStreamError同时写入strapi.log.error和诊断通道,错误消息统一带[Data transfer]前缀,便于在混合日志中定位。
五类读取流:数据到底怎么被读出来
Provider 实现了ISourceProvider接口,对外暴露五类产出能力,全部基于strapi.db.queryBuilder(...).stream()的流式查询(而非一次性findAll),这是它能在不撑爆内存的前提下处理大体量数据的关键:
1. 实体流 createEntitiesReadStream
实现位于 entities.ts。逻辑分两层:
createEntitiesStream遍历strapi.contentTypes的每一个 UID,逐个构建查询:queryBuilder(uid).select('*').populate(...)并取.stream()。其中的 populate 参数由 Entity 查询工具 生成的query.deepPopulateComponentLikeQuery提供,用于把"组件形态"(component-like)关联也展开;- 任一内容类型的流读取失败时,不会静默丢弃:源码注释明确说明"每一个被跳过的实体都会留下悬空链接",因此通过
options.onWarning上报Failed to read all entities of type "<uid>" from the source, the remaining entities of this type were skipped: ...后继续处理其余类型; createEntitiesTransformStream再把原始行{ id, ...attributes }归一为传输格式{ type: <uid>, id, data: attributes }。
最终createEntitiesReadStream用stream-chain把"多内容类型原始流"与"格式转换流"串联(见 index.ts 中 createEntitiesReadStream)。
2. 关系流 createLinksReadStream
位于 links.ts。它遍历所有内容类型与组件的 UID(strapi.contentTypes+strapi.components),对每个 UID 调用 createLinkQuery 的generateAll生成器,产出ILink(左右两端引用)。一个值得注意的健壮性设计:悬空链接(指向已不存在实体的关系)会被跳过并计数,警告使用createCappedWarningReporter限量输出,结束后汇总上报Links export omitted N relation(s) pointing at missing entities...,提示用户导入后核对关系完整性。
3. 配置流 createConfigurationReadStream
位于 configuration.ts。它把两类"应用级配置"打包为{ type, value }项:
- Core Store:
queryBuilder('strapi::core-store').stream(),并将 JSON 字符串列value解析为对象; - Webhook:
queryBuilder('strapi::webhook').stream()。
其中 Core Store 项在导出前还会经过enrichProjectSettingsForExport处理(见 project-settings-logos.ts),把项目设置中的 logo 等资产信息一并补全,保证导入端可以还原。
4. Schema 读取 getSchemas / createSchemasReadStream
getSchemas()把strapi.contentTypes与strapi.components合并后,经schemasToValidJSON与mapSchemasValues处理成合法的 JSON Schema 结构;createSchemasReadStream()则直接把各 Schema 作为可迭代流输出(见 index.ts)。
5. 资产流 createAssetsReadStream
实现位于 assets.ts,是五类流中边界条件最多的一个:
- 数据源为
queryBuilder('plugin::upload.file').select('*').stream(),逐条处理上传文件记录; - Provider 分支:若
file.provider === 'local',文件路径拼接为join(strapi.dirs.static.public, file.url)并用fs-extra的createReadStream直读;否则(如 S3、Cloudinary 等)走signUploadFileForTransfer——当对应上传 Provider 返回私有资源(provider.isPrivate()为真)时,调用provider.getSignedUrl生成签名 URL,再通过strapi.fetch流式下载; - 文件缺失:统计大小时若捕获
ENOENT,不抛错而是warnMissingAsset并continue,警告消息形如[Data transfer] Media item <id> (hash: <hash>) exists in database but no corresponding file was found to transfer. Path: ...——数据库有记录但磁盘文件丢失的情况被降级为可观察的告警; - 格式图(formats):主文件之后逐个遍历
file.formats,每项以{ ...fileFormat, type: format, id: file.id, mainHash: file.hash }作为元数据单独 yield,从而保留"同一媒体多个变换产物"的从属关系; - 产出统一封装为
{ metadata, filepath, filename: hash + ext, stream, stats: { size } }的IAsset流(Duplex)。
6. 阶段总量 getStageTotals
getStageTotals(stage)仅对assets阶段返回估算值(委托 estimateAssetTotals),其他阶段返回null,供引擎计算进度百分比。
谁在使用它:strapi export 与远程 Pull
在 CLI 侧,strapi export命令是该 Provider 最典型的消费方,见 export 命令 action:
const createSourceProvider = (strapi: Core.Strapi) => { return createLocalStrapiSourceProvider({ async getStrapi() { return strapi; // 命令先 createStrapiInstance() 启动实例,再闭包返回 }, }); };完整链路为:createStrapiInstance()启动实例 → 构造 source(本文主角)与 destination(tar/dir 文件 Provider)→createTransferEngine(source, destination, { versionStrategy: 'ignore', schemaStrategy: 'ignore', exclude/only/throttle/transforms, ... })→engine.transfer()→ 校验产物并打印结果表。由于导出的目标端没有可比对版本,两个 strategy 均固定为ignore。
此外,远程 Pull 流程的本地端同样复用它:pull.ts 中this.provider = createLocalStrapiSourceProvider({...}),即"从远端拉取到本地"时,本地实例既是被读的数据源,也参与流程协商。
手动组合的最小可运行示例
以下示例演示脱离 CLI、在脚本中直接使用该 Provider 并搭配文件目标端,展示选项的正确用法:
import fs from 'fs'; import { engine, file, strapi } from '@strapi/data-transfer'; import { createStrapiInstance } from './bootstrap-strapi'; // 自行封装:加载 config/ 并 await strapi() const { createTransferEngine } = engine; const { providers: { createLocalFileDestinationProvider } } = file; const { providers: { createLocalStrapiSourceProvider } } = strapi; const source = createLocalStrapiSourceProvider({ async getStrapi() { // 返回一个已初始化(数据库已连接)的 Strapi 实例 return createStrapiInstance(); }, // autoDestroy 省略即默认 true:传输结束后自动销毁上面创建的实例 }); const destination = createLocalFileDestinationProvider({ file: { path: 'backup.tar', maxSizeJsonl: 100 * 1024 * 1024 }, // 单个 jsonl 文件上限 100MB compression: { enabled: false }, encryption: { enabled: false }, }); const transferEngine = createTransferEngine(source, destination); const results = await transferEngine.transfer(); console.log(results);要点提示:
getStrapi的返回值必须是完成初始化的实例(含数据库连接),否则 bootstrap 后首次查询即失败;- 若实例由外部常驻进程管理(例如在长驻服务内做增量导出),应显式传
autoDestroy: false,避免 Provider 提前destroy(); - 警告(缺失文件、悬空链接等)会进入诊断通道,生产脚本建议订阅
transferEngine.diagnostics落盘,与 export 命令中engine.diagnostics.onDiagnostic(formatDiagnostic(...))的用法一致。
小结
Local Strapi Source Provider 是 Strapi 数据迁移体系的"本地数据出口":
- 所有权清晰——
getStrapi只要求"给我一个已初始化的实例",实例生命周期默认随传输结束销毁(autoDestroy显式为false除外),并有单测钉死三种取值的行为; - 全流式读取——实体、关系、配置、Schema、资产全部通过
queryBuilder(...).stream()增量产出,配合deepPopulateComponentLikeQuery与 formats 从属关系处理,覆盖 Strapi 数据模型的主要面; - 失败可观察——读流错误、悬空链接、磁盘文件缺失都有带
[Data transfer]前缀的告警路径,而非静默失败; - 多入口复用——同一 Provider 同时服务于
strapi export命令行与远程 Pull 流程的本地侧。
相关源码入口:Provider 主体、实体流、关系流、配置流、资产流、Provider 测试 与 export 命令。
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考