Strapi 数据迁移:Local Strapi Destination Provider 的配置选项、Restore 策略与回滚机制详解
本文基于 Strapi 官方文档 Local Strapi Destination Provider 及其底层源码,详解 data-transfer 引擎中“本地 Strapi 目标提供者”的完整用法:它如何通过 Entity Service 与 Query Engine 向一个已初始化的 Strapi 实例写入数据、restore 冲突策略的每个选项如何工作,以及出错时数据库事务回滚与媒体文件备份恢复是如何协同保障数据安全的。读完后你能够独立编写迁移脚本、正确配置 restore 选项,并理解迁移失败时备份目录 uploads_backup_{timestamp} 的处置方式。
定位:把本地 Strapi 实例作为迁移目标
Local Strapi Destination Provider 是 Strapi data-transfer(数据迁移)引擎中的一个目标端(destination)提供者,其核心职责正如文档所述:
This provider will insert data into an initialized
strapiinstance using its Entity Service and Query Engine.
即通过 Entity Service(实体服务)与 Query Engine(查询引擎)把源端数据逐条插入到一个已经初始化完成的 Strapi 实例中。结合本地提供者的总览文档 00-overview.md 可知:创建本地 Strapi 数据提供者必须传入一个可用的 strapi 服务端对象,交互所依赖的就是该实例的 Entity Service 与 Query Engine。因此有一个硬性前提——如果本地 Strapi 项目无法启动(例如启动报错),该提供者就无法使用。
从源码结构看,该提供者的完整实现位于 local-destination/index.ts,注册名为 destination::local-strapi(index.ts#L32-L33)。
Provider Options:四个核心选项
文档指出接受选项的类型定义为 ILocalFileSourceProviderOptions,对应的源码实现是 ILocalStrapiDestinationProviderOptions(index.ts#L22-L30):
getStrapi(): Strapi.Strapi | Promise<Strapi.Strapi>; // return an initialized instance of Strapi
autoDestroy?: boolean; // shut down the instance returned by getStrapi() at the end of the transfer
restore?: restore.IRestoreOptions; // the options to use when strategy is 'restore'
strategy: 'restore'; // conflict management strategy; only the restore strategy is available at this time
逐项说明如下:
| 选项 | 必填 | 说明 |
|---|---|---|
getStrapi |
是 | 返回一个已初始化的 Strapi 实例(可同步、可异步)。提供者 bootstrap 时会调用它获取实例(index.ts#L60-L69),取不到则抛出 ProviderInitializationError('Could not access local strapi') |
autoDestroy |
否 | 迁移结束后是否自动关闭(destroy)getStrapi() 返回的实例。默认行为是关闭:源码中判断为 `autoDestroy === undefined |
restore |
策略为 restore 时必填 | 冲突为 restore 时在迁移前清空目标数据所使用的选项,详见下文 |
strategy |
是 | 冲突管理策略。当前只支持 "restore",源码中 VALID_CONFLICT_STRATEGIES = ['restore']、DEFAULT_CONFLICT_STRATEGY = 'restore'(index.ts#L19-L20) |
两点使用注意事项,来自文档与源码的共同印证:
strategy与restore的强校验。bootstrap阶段的#validateOptions()会做两件事:策略不在VALID_CONFLICT_STRATEGIES中时抛出ProviderValidationError;策略为restore但未提供restore选项时抛出Missing restore options(index.ts#L109-L123)。autoDestroy的场景化取值。总览文档给出的建议是:通过外部脚本跑迁移时,建议autoDestroy: true(或不设置)以保证实例被正确关闭;但如果你在正在运行的 Strapi 实例内部执行迁移,必须设置autoDestroy: false,否则迁移结束时你的 Strapi 实例会被一并销毁。
此外源码中还暴露了一个文档未提及的可选回调 onTransferPhase?: (message: string) => void(index.ts#L28-L29),用于在 beforeTransfer(restore 准备阶段)向 CLI/UI 上报人类可读的进度,例如 Local: backing up existing upload folder…、Local: deleting existing media files from disk… 等。
Restore 策略:先清空、再写入
strategy: 'restore' 的含义在文档中非常明确:
A conflict management strategy of "restore" deletes all existing Strapi data before a transfer to avoid any conflicts.
即在迁移开始前删除目标端已有的 Strapi 数据,从根本上避免主键/关联冲突。restore 选项的完整接口(源码定义见 strategies/restore/index.ts#L6-L18):
export interface IRestoreOptions {
assets?: boolean; // delete media library files before transfer
configuration?: {
webhook?: boolean; // delete webhooks before transfer
coreStore?: boolean; // delete core store before transfer
};
entities?: {
include?: string[]; // only delete these stage entities before transfer
exclude?: string[]; // exclude these stage entities from deletion
filters?: ((contentType: ContentTypeSchema) => boolean)[]; // custom filters to exclude a content type from deletion
params?: { [uid: string]: unknown }; // params object passed to deleteMany before transfer for custom deletions
};
}
assets:是否清理媒体库文件
assets 同时控制三件事,源码中统一由 #areAssetsIncluded() 读取 this.options.restore?.assets 来判断(index.ts#L72-L74):
- 是否把现有
public/uploads目录备份到uploads_backup_{timestamp}(见下文回滚一节); - 是否通过查询
plugin::upload.file记录、并调用strapi.plugin('upload').provider.delete(file)逐个删除磁盘上的媒体文件(包括formats衍生图,见#deleteAllAssets,index.ts#L134-L164); - 是否允许资产写入流工作。若未开启
assets却尝试传输资产,createAssetsWriteStream()会直接抛出ProviderTransferError: Attempting to transfer assets when 'assets' is not set in restore options(index.ts#L316-L323)。
从源码结构看,只有当 upload 插件的 provider 配置为 local 时,备份/清理逻辑才会真正执行(index.ts#L261、index.ts#L303)。
configuration.webhook / configuration.coreStore:清理全局配置数据
这两个布尔值控制是否删除 Webhook 与核心存储(core store)中的既有记录。对应实现 deleteConfigurationRecords 有一个值得注意的细节:两者默认值均为 true(strategies/restore/index.ts#L114-L143):
const { coreStore = true, webhook = true } = options?.configuration ?? {};
也就是说,如果你完全不传 configuration,Webhook 与 core-store 仍会被清空;若想保留,必须显式传 configuration: { webhook: false, coreStore: false }。
entities.include / exclude / filters / params:精细化控制实体删除范围
实体清理逻辑在 deleteEntitiesRecords 中实现(strategies/restore/index.ts#L36-L112),源码注释与逻辑共同界定了各字段语义:
include(白名单):注释写明 "include means 'only include these types'"——一旦设置了include,只有列出的内容类型(及模型)会被删除,其余全部保留。exclude(黑名单):被列出的 uid 免于删除;"not being excluded doesn't mean it's kept",即未设置 exclude 时默认删除所有。filters(自定义过滤器):一组(contentType: ContentTypeSchema) => boolean函数,所有过滤器都返回true时该内容类型才会被删除(源码用entities.filters.every((filter) => filter(contentType))实现),适合按 schema 特征批量排除某类内容类型。params(按 uid 的自定义删除参数):{ [uid: string]: unknown }对象,会在deleteMany调用时透传,用于对特定内容类型施加自定义删除条件。
执行细节上,内容类型走 Entity Service 的 contentTypeQuery(uid).deleteMany(entities?.params),而模型(models)直接走 strapi.db.query(uid).deleteMany({}),所有类型并行删除(Promise.all)并汇总每个 uid 的删除计数。
数据写入流:实体、链接、配置与资产
清空之后,引擎会创建四类写入流(Writable stream)把源端数据逐条灌入目标实例。以实体写入流为例,restore 策略的实现位于 strategies/restore/entities.ts:
const created = await create({
data,
populate: getDeepPopulateComponentLikeQuery(contentType, { select: 'id' }),
select: 'id',
});
updateMappingTable(type, id, created.id);
每条实体在创建后会调用 updateMappingTable(type, oldID, newID),把源端 ID 映射到目标端新生成的 ID,并递归登记组件实例的 ID 映射(collectComponentIdMappings)。这张映射表随后被链接写入流(createLinksWriteStream 中的 mapID)用于重写实体间引用,保证关联指向新库中的真实记录。这是迁移后外键与组件引用不出现悬空 ID 的关键机制(index.ts#L352-L370)。
配置写入流同理,restoreConfigs 会把 core-store 与 webhook 记录重新创建,且 core-store 的恢复会经过 restoreProjectSettingsRow 特殊处理项目设置行(logo 等)(configuration.ts#L31-L39)。
整个 beforeTransfer 阶段的执行顺序在 index.ts#L172-L197 中一目了然:
- 事务挂载(
transaction.attach); - 若
assets开启:备份public/uploads到备份目录; - 若
assets开启:流式读取plugin::upload.file记录并删除磁盘上的媒体文件; - 按
restore选项删除实体与配置数据。
回滚机制:数据库事务 + 媒体文件备份
这是文档中技术含量最高的一节。Local Strapi Destination Provider 在出错时自动提供回滚机制,文档将其分为两条路径,源码均可一一对应验证:
数据库回滚:包裹 restore 与插入的事务
"对 Strapi 数据,是通过一个包裹 restore 和数据插入的数据库事务实现的,成功则提交,失败则回滚。" 源码实现分两层:
- 事务抽象层:
bootstrap中通过utils.transaction.createTransaction(this.strapi)创建一个事务对象(index.ts#L68)。该实现基于strapi.db.transaction开启一个长事务,内部用 EventEmitter 维护一个回调队列——任何阶段(restore、实体写入、链接写入、配置写入)都通过transaction.attach(cb)把自己的操作挂入同一事务;end()关闭队列后事务正常提交,rollback()则触发底层 rollback(utils/transaction.ts#L7-L98)。 - 提供者层:
rollback()方法直接执行this.transaction?.rollback()(index.ts#L166-L170)。
此外,bootstrap 时还会先 strapi.db.lifecycles.disable()、close 时 enable()(index.ts#L67、index.ts#L102),确保批量迁移期间不被生命周期钩子干扰。
媒体文件回滚:uploads_backup_{timestamp} 备份目录
媒体文件不在数据库事务的保护范围内,文档给出的方案在源码中完整可见:
- 备份:
#handleAssetsBackup()仅在assets开启且 upload provider 为local时执行。它先检查public/uploads及其父目录的读/写/存在权限,然后把整个uploads目录移动到public/uploads_backup_{timestamp}(目录名在构造函数中用uploads_backup_${Date.now()}生成,index.ts#L57),并重新创建空uploads目录、写入.gitkeep占位(index.ts#L253-L294)。权限不足时会抛出ASSETS_DIRECTORY_ERR错误。 - 成功:迁移成功后
#removeAssetsBackup()用fse.rm(backupDirectory, { recursive: true, force: true })删除备份目录(index.ts#L296-L313)。 - 失败:删除本次导入的文件并把备份目录还原回
uploads。文档同时明确警告:某些失败情况下备份可能无法自动还原,需要你手动把备份中的资产文件恢复回去——这是运维时需要记住的兜底手段。
只读 uploads 环境的限制
文档末尾的注意事项值得所有部署者留意:
Because of the need for write access, environments without filesystem permissions to move the assets folder (common for virtual environments where /uploads is mounted as a read-only drive) will be unable to include assets in a transfer and the asset stage must be excluded in order to run the transfer.
翻译过来即:由于备份机制需要移动整个 uploads 目录的写权限,在 /uploads 以只读方式挂载的环境(常见的虚拟化部署)中,无法将资产纳入迁移,此时必须在迁移配置中排除 asset 阶段(也就是不启用 restore.assets)才能跑通迁移。
实践要点小结
- 使用本地目标提供者前,确保目标 Strapi 项目能正常启动并提供
getStrapi回调;这是所有操作的硬依赖。 strategy只能是'restore',且必须同时提供restore选项,否则bootstrap校验阶段即会失败。- 用
entities.include / exclude / filters / params精确控制清理范围;记住include一旦设置就是“只删这些”的白名单语义。 configuration不传时 webhook 与 core-store 默认都会被清空,需要保留时显式设为false。- 是否在 Strapi 实例内部执行迁移,决定
autoDestroy的取值:外部脚本用true,实例内部必须false。 - 失败后先查数据库是否已整体回滚;若
assets开启且备份还原失败,检查public/uploads_backup_{timestamp}目录并手动恢复。 - 只读 uploads 挂载的环境必须排除资产阶段。
相关测试用例可进一步印证上述行为,例如 restore.test.ts、restore-entities.test.ts、assets.test.ts 与 assets-destination-writable.test.ts,覆盖了实体清理范围、资产备份与写入流等关键路径。
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