Strapi 数据迁移之目标端 Provider(IDestinationProvider)完整解析:从 WriteStream 接口到本地/远程落盘实现
Strapi 数据迁移之目标端 ProviderIDestinationProvider完整解析从 WriteStream 接口到本地/远程落盘实现【免费下载链接】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/strapiStrapi 的数据传输引擎Data Transfer Engine实验性功能以“源 Provider 提供 Readable 流、目标 Provider 提供 Writable 流”的对称模型完成备份、还原与跨实例迁移。本文以官方文档 Destination Providers 为核心结合packages/core/data-transfer中的接口定义、传输引擎调用链与三套内置目标 Provider 实现讲解如何理解并自行实现一个目标端 Provider读完后你能掌握IDestinationProvider接口的全部成员含义、五个传输阶段的数据契约、引擎与目标端的完整生命周期以及 Local Strapi、Strapi File、Remote Strapi 三种内置实现的关键差异与回滚rollback机制。1. Destination Provider 在传输架构中的位置数据迁移由五个顺序阶段组成每个阶段遵循相同的流生命周期模式只是处理的数据类型不同见 Stream Lifecycle 文档阶段数据内容1. Schemas数据库结构、内容类型配置、组件定义2. Entities实际内容不含关系组件数据、Dynamic Zone、媒体元数据不含文件3. Assets/uploads目录下的文件、文件元数据、格式/变体4. Links实体间关系内容类型关系、组件关系、媒体关系5. ConfigurationStrapi 配置、项目设置、API 配置在每个阶段中传输引擎会依次向源端请求create{stage}ReadStream()、创建用于数据改写/过滤的 Transform 流、创建进度追踪流最后向目标端请求create{stage}WriteStream()把整条管道串起来Source Provider (Readable) └─ Transform Stream过滤/映射 └─ Progress Tracker统计 count/bytes └─ Destination Provider (Writable)因此目标 Provider 的职责可以一句话概括为每个阶段提供一个 Writable 流接收从源端 Readable 流中管道过来的每一条 entity、link关系、asset文件、configuration 实体或内容类型 schema。这正是官方文档 Destination Providers 的核心定义。与 Source Providers 的约定互为镜像源端负责stream.write(entity)逐条发出数据并在发完后关闭流引擎才会进入下一阶段目标端则负责逐条接收并落盘/落库。引擎侧的对应调用点在 engine/index.ts 中可以逐一看到engine/index.ts:903 const destination await this.destinationProvider.createSchemasWriteStream?.(); engine/index.ts:920 const destination await this.destinationProvider.createEntitiesWriteStream?.(); engine/index.ts:965 const destination await this.destinationProvider.createLinksWriteStream?.(); engine/index.ts:1003 const destination await this.destinationProvider.createAssetsWriteStream?.(); engine/index.ts:1048 const destination await this.destinationProvider.createConfigurationWriteStream?.();注意引擎对每个方法都使用?.可选调用——这与接口定义中五个 WriteStream 方法全部为可选成员是一致的意味着一个目标 Provider 不必支持全部阶段。2. IDestinationProvider 接口的完整定义目标 Provider 必须实现IDestinationProvider接口。文档中给出的路径是发布产物里的packages/core/data-transfer/types/providers.d.ts其在当前仓库中的源码定义位于 types/providers.tsexport interface IProvider { type: ProviderType; // source | destination name: string; // 该 Provider 的唯一名称 results?: IProviderTransferResults; // 供引擎外部追踪结果的可选对象 // bootstrap() 在传输引擎引导阶段被调用 // 用于初始化建立数据库连接、打开文件、校验授权等 bootstrap?(diagnostics?: IDiagnosticReporter): MaybePromisevoid; close?(): MaybePromisevoid; // 在传输引擎关闭时被调用 getMetadata(): MaybePromiseIMetadata | null; // 返回用于版本校验的元数据 getSchemas?(): MaybePromiseRecordstring, Struct.Schema | null; // 返回用于 schema 校验的 schema beforeTransfer?(): MaybePromisevoid; // 在传输阶段开始执行前立即被调用 } export interface IDestinationProvider extends IProvider { results?: IDestinationProviderTransferResults; /** * 可选的 rollback 实现。 * 当传输过程中抛出错误时被调用允许执行回滚操作 */ rollback?T extends Error Error(e: T): MaybePromisevoid; setMetadata?(target: ProviderType, metadata: IMetadata): IDestinationProvider; onWarning?: (message: string) void; createEntitiesWriteStream?(): MaybePromiseWritable; createLinksWriteStream?(): MaybePromiseWritable; createAssetsWriteStream?(): MaybePromiseWritable; createConfigurationWriteStream?(): MaybePromiseWritable; createSchemasWriteStream?(): MaybePromiseWritable; }各成员的职责如下成员调用时机说明type/name构造时必须标记为destination并给出唯一名称如destination::local-strapiresults传输结束后Provider 专属的结果对象会被引擎合并进ITransferResults见 types/utils.ts 中的ITransferResultsbootstrap(diagnostics)引擎初始化阶段做重量级初始化连接 Strapi 实例、打开目标文件等引擎在 engine/index.ts 第 708 行 传入诊断报告器close()引擎关闭阶段释放连接、提交/结束事务等引擎对源/目标两端的close()使用Promise.allSettled并发执行并分别处理清理错误getMetadata()版本校验返回{ createdAt, strapi: { version } }之类的元数据用于源/目标版本一致性验证getSchemas()schema 校验提供内容类型与组件的 schema 集合供引擎做 schema 匹配beforeTransfer()所有阶段开始前目标端做前置准备的标准位置如执行 restore 清理、备份rollback(e)阶段内抛出错误时引擎在捕获错误后调用engine/index.ts 第 858 行await this.destinationProvider.rollback?.(e as Error)由 Provider 自行决定回滚策略setMetadata(target, metadata)引擎引导时引擎将源端元数据注入目标端engine/index.ts 第 698 行 传入source典型用途是文件 Provider 把双方元数据写进归档onWarning传输过程中非致命警告回调如链接映射时找不到目标 IDcreate*WriteStream()×5各阶段开始时返回该阶段的 Writable 流五个方法对应五个阶段全部可选每个阶段写入流的数据类型由 types/utils.ts 中的TransferStageTypeMap约束export type TransferStageTypeMap { schemas: Struct.Schema; entities: IEntity; links: ILink; assets: IAsset; configuration: unknown; }; export type TransferStage keyof TransferStageTypeMap;也就是说createEntitiesWriteStream()返回的 Writable 每次write(chunk)收到的都是一个IEntitycreateLinksWriteStream()收到ILink依此类推。3. 引擎与目标 Provider 的生命周期协作把 engine/index.ts 中的调用点按时间顺序排列目标 Provider 经历的完整生命周期为bootstrap(diagnostics) // L708与 source.bootstrap 并发执行 setMetadata(source, m) // L698接收源端元数据 beforeTransfer() // 阶段开始前的准备如 restore 清理 createSchemasWriteStream() // L903 createEntitiesWriteStream() // L920 createLinksWriteStream() // L965 createAssetsWriteStream() // L1003 createConfigurationWriteStream() // L1048 rollback(e)? // L858仅当出错时 close() // L724与 source.close 用 allSettled 并发执行几个对实现者重要的细节阶段内串行、两端对称。引擎先拿到源端 Readable再拿到目标端 Writable然后source.pipe(transform).pipe(tracker).pipe(destination)并等待整条管道close事件阶段才算结束流程示意见 Stream Lifecycle 文档。进度与结果分离。逐阶段的 count/bytes/耗时由引擎的 Progress Tracker 统计进TransferProgress结构与文档中的TransferProgress接口一致定义见 types/utils.ts 第 93-95 行而业务性结果如生成的文件路径则由 Provider 写入自己的results。可选成员的宽容性。bootstrap、close、rollback、五个 WriteStream 方法全部为可选引擎一律用?.调用因此一个只支持部分阶段的 Provider例如只导出文件、不写库是完全合法的。4. 自写目标 Provider 的实现要点以“把每个阶段的数据追加写入一个目录下的 JSONL 文件”为例一个最小可用的目标 Provider 大致如下示例代码为说明性实现非仓库内置文件import { Writable } from stream; import { createWriteStream, appendFile } from fs; import type { IDestinationProvider, IMetadata, ProviderType, } from strapi/data-transfer/dist/types; // 或仓库内 src/types 对应导出 class JsonlFileDestinationProvider implements IDestinationProvider { type: ProviderType destination; name destination::my-jsonl; results: { dir?: string } {}; constructor(private opts: { dir: string }) {} async bootstrap() { /* 创建目录等初始化 */ } getMetadata(): IMetadata | null { // 返回 null 表示不参与版本校验 return null; } getSchemas() { return null; } beforeTransfer() { /* 清理旧文件等前置操作 */ } private jsonl(stage: string): Writable { const file appendFile(${this.opts.dir}/${stage}.jsonl); return Writable.wrap(file, { objectMode: true, write(chunk, _enc, cb) { file.write(${JSON.stringify(chunk)}\n, cb); }, }); } createEntitiesWriteStream() { return Promise.resolve(this.jsonl(entities)); } createLinksWriteStream() { return Promise.resolve(this.jsonl(links)); } createAssetsWriteStream() { return Promise.resolve(this.jsonl(assets)); } createConfigurationWriteStream() { return Promise.resolve(this.jsonl(configuration)); } createSchemasWriteStream() { return Promise.resolve(this.jsonl(schemas)); } async close() { /* 关闭已打开的文件句柄 */ } // 未实现 rollback出错时目标端不做补偿需在文档中说明该语义 }实现时的关键契约Writable 必须处于 objectMode引擎按对象entity/link/asset…逐条写入而不是按字节错误即中止Writable 上抛出的任何错误都会沿管道传播引擎记录stage::error并触发rollback?.()警告用onWarning无法致命但需要用户知晓的情况例如某条关系的目标 ID 映射不到通过onWarning?.(message)上报资源清理放在close()无论成功失败引擎关闭时都会调用回滚语义自己定义接口只负责调用rollback(e)是否可回滚、回滚到什么程度完全由实现决定。仓库中已有大量可直接对照的测试验证了这套契约在各阶段下的行为例如 local-destination 的 restore 测试、assets 写入流测试 以及 file destination 测试。5. 内置目标 Provider 逐个剖析Strapi 为三种介质都提供了目标 Provider总览见 Providers 概览Strapi 文件、本地 Strapi、远程 Strapi。它们对外呈现同一套create*WriteStream()接口但各自带有一组专属初始化选项。5.1 Local FileStrapi 文件Destination Provider该 Provider 输出一份标准 Strapi 数据文件Strapi Data File实现位于 file/providers/destination/index.ts。文档Strapi File Destination特别指出该 Provider 不提供 schema 与 metadata因此永远不会触发 schema 匹配错误或版本校验错误——这与接口定义吻合getMetadata()对目标侧并非强校验依据。其选项ILocalFileDestinationProviderOptions定义如下文档与源码一致export interface ILocalFileDestinationProviderOptions { encryption: { enabled: boolean; // 是否对文件加密 key?: string; // encryption.enabled 为 true 时使用的密钥 }; compression: { enabled: boolean; // 是否用 gzip 压缩 }; file: { path: string; // 要创建的文件名 maxSize?: number; // 单个备份文件的最大字节数 maxSizeJsonl?: number; // 每个 jsonl 文件达到多少行后滚动到下一个文件 }; }从源码可以看出几个实现细节产物命名规则#archivePath 计算逻辑基础名{file.path}.tar开启压缩追加.gz开启加密再追加.enc即最终文件形如backup.tar.gz.enc管道构成bootstrap()中创建tar.pack()归档流并接到fs.createWriteStream配合zlib.createGzip()与createEncryptionCipher加密实现位于 utils/encryption各阶段的 JSONL 数据则经由stream-json的stringer序列化为 JSONL 后作为 tar entry 写入依赖tar-stream、stream-chain元数据注入setMetadata()把源/目标双方元数据缓存进#providersMetadata随归档一起落盘供日后导入时核对结果上报成功后在results.file.path中记录生成文件路径供 CLI 读取。5.2 Local Strapi Destination Provider这是最复杂的目标 Provider把数据插入一个已初始化的 Strapi 实例走其 Entity Service 与 Query Engine。文档见 Local Strapi Destination实现见 strapi/providers/local-destination/index.ts。Provider 选项ILocalStrapiDestinationProviderOptionsexport interface ILocalStrapiDestinationProviderOptions { getStrapi(): Core.Strapi | PromiseCore.Strapi; // 返回已初始化的 Strapi 实例 autoDestroy?: boolean; // 传输结束后是否销毁 getStrapi() 返回的实例 restore?: restore.IRestoreOptions; // strategy 为 restore 时必传的清理选项 strategy: restore; // 冲突处理策略当前仅支持 restore onTransferPhase?: (message: string) void; // CLI/UI 前置阶段的人类可读进度回调 }源码中VALID_CONFLICT_STRATEGIES [restore]bootstrap()阶段会先做选项校验策略非法或 restore 缺少选项时抛出ProviderValidationError然后this.strapi await this.options.getStrapi(); this.strapi.db.lifecycles.disable(); // 迁移期间禁用数据库生命周期钩子 this.transaction utils.transaction.createTransaction(this.strapi);restore 策略与 IRestoreOptions。restore 的含义是传输前先删除目标库中的既有数据以避免冲突。可选项如下引自官方文档export interface IRestoreOptions { assets?: boolean; // 传输前删除媒体库文件 configuration?: { webhook?: boolean; // 传输前删除 webhooks coreStore?: boolean; // 传输前删除 core store }; entities?: { include?: string[]; // 仅删除这些内容类型的实体 exclude?: string[]; // 排除这些内容类型 filters?: ((contentType: ContentTypeSchema) boolean)[]; // 自定义过滤器 params?: { [uid: string]: unknown }; // 传给 deleteMany 的自定义删除参数 }; }beforeTransfer()中的实际执行顺序#L172-L197开启事务并attach若restore.assets为真先把public/uploads整体移动到public/uploads_backup_{timestamp}备份目录仅当 upload provider 为local时执行再清空 uploads通过restore.deleteRecords实现位于 strategies/restore/ 下的configuration.ts、entities.ts、links.ts按IRestoreOptions清库。各阶段 WriteStream 的实现方式createEntitiesWriteStream()按策略委托给restore.createEntitiesWriteStream并在每写入一条记录时通过updateMappingTable(type, oldID, newID)维护一张私有映射表#entitiesMapper第 50-56 行——这是后续 links 阶段能把源端旧 ID 换到目标端新 ID 的基础createLinksWriteStream()把mapID (uid, id) this.#entitiesMapper[uid]?.[id]传给 restore 策略的 links 写入流完成关系重映射映射不到时经onWarning上报createAssetsWriteStream()返回 assets-destination-writable.ts 创建的 Writable写入文件字节、调用 upload provider 落盘并用resolveUploadFileId把媒体元数据 ID 映射为新 ID若restore.assets未开启而管道里出现 asset 流会直接抛出ProviderTransferErrorcreateConfigurationWriteStream()委托restore.createConfigurationWriteStream(strapi, transaction)。回滚Rollback机制文档与源码一致地分两层Strapi 数据restore 清理与全部数据插入被包在同一个数据库事务里成功则提交、失败则回滚。Provider 的rollback()就是执行this.transaction?.rollback()第 166-170 行上传文件由于文件操作不在数据库事务内采用“先备份、后决定去留”的策略——开始前把uploads移入uploads_backup_{timestamp}成功时删除备份失败时删除导入失败的文件并把备份移回。文档同时明确提示某些失败场景下备份可能无法自动还原需要手动恢复且若环境对 uploads 目录无写权限如只读挂载则必须把 assets 阶段排除在传输之外。close()的收尾顺序同样值得注意transaction.end()→strapi.db.lifecycles.enable()恢复生命周期钩子 → 按autoDestroy默认 true决定是否strapi.destroy()第 98-107 行。5.3 Remote Strapi Destination Provider远程目标 Providerstrapi/providers/remote-destination/index.ts把本地目标 Provider 包装了一层 WebSocket 通道连接远程 Strapi 管理端通过消息协议逐阶段推进并把数据推给对端由对端走与 5.2 相同的 restore/写入逻辑。文档见 Remote Strapi Destination。选项在本地 Provider 的restore与strategy之外增加三项interface ITransferTokenAuth { type: token; // 认证策略名 token: string; // 传输令牌 } export interface IRemoteStrapiDestinationProviderOptions extends PickILocalStrapiDestinationProviderOptions, restore | strategy { url: URL; // 远程 Strapi admin 的地址 auth?: ITransferTokenAuth; retryMessageOptions?: { retryMessageTimeout: number; // 等待单条消息响应的毫秒数 retryMessageMaxRetries: number; // 单条消息放弃前最大重试次数 }; }使用注意官方文档原文要点url必须带http/https协议连接时会被转换为ws/wss鉴于传输令牌具备很高的访问权限强烈建议使用安全的https→wss连接。消息重试行为的对应测试可见 remote-destination 测试目录。6. 约束、限制与最佳实践综合文档标签三个 provider 文档均标注experimental与源码实现或选用目标 Provider 时应注意接口全部可选、按需实现。五个create*WriteStream()与bootstrap/close/rollback均为可选成员引擎对缺失方法采取跳过语义但如果不实现某阶段的 WriteStream该阶段数据会被直接丢弃属于显式设计而非缺陷。metadata 与 schema 是校验开关。getMetadata()返回null即不参与版本校验getSchemas()缺失即跳过 schema 匹配。Local File 目标 Provider 正是利用这一点使“备份文件”本身不承担校验职责。rollback 是尽力语义。接口注释明确 rollback 只“允许执行回滚操作”具体保证由实现给出Local Strapi 用事务备份双保险Local File 目标则不存在回滚文件写到一半时只能手动清理。assets 阶段处于不稳定状态。Providers 概览 明确说明目前所有数据传输 Provider 只处理本地媒体资产/upload目录Provider 媒体如 S3/Cloudinary支持仍在开发中与资产相关的一切——包括 Strapi 文件结构、restore 策略与资产回滚——均按unstable对待近期可能变化。写自己的 Provider 时的对照路径接口定义见 types/providers.ts阶段数据类型见 types/utils.ts错误类型ProviderValidationError、ProviderTransferError等见 errors/providers.ts引擎行为与调用点见 engine/index.ts最完整的参考实现是 Local Strapi 目标 Provider含策略目录 strategies/restore/。7. 小结Destination Provider 是 Strapi 数据传输引擎的“落盘端”抽象它用create{stage}WriteStream()一族可选方法把 schemas、entities、links、assets、configuration 五个阶段的数据从源端管道中逐条接收并写入目标介质。理解了 IDestinationProvider 接口、引擎生命周期调用点 以及三套内置实现Local File、Local Strapi、Remote Strapi的差异与回滚语义后你就可以按本文第 4 节的契约为目标介质对象存储、数据仓库、其他 CMS 等写出一个行为可预测、可测试的目标 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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考