Repository Wiki
ChanIok/SpinningMomo

备份与恢复数据

SpinningMomo 提供基于 JSON-RPC 的用户数据导出(backup.export)与完全替换式恢复(backup.restore)能力:导出时生成包含 SQLite 一致性快照、设置文件、托管背景与应用版本的单一 ZIP 归档;恢复时通过外部 PowerShell 脚本在应用退出后原子地替换 AppData 中的数据并重启应用。

目的与范围

本页完整覆盖"备份与恢复数据"这一运维能力的端到端实现:

  • RPC 端点层:core::rpc::endpoints::backup 中 backup.export / backup.restore 的注册、参数与错误映射
  • 功能实现层:features::backup 中的导出流水线、归档校验与恢复脚本生成
  • 数据库快照:core::database::backup_to 基于 SQLiteCpp 在线备份的 WAL 一致性快照
  • 前端接入:web/src/features/settings/backupApi.ts 的调用封装与本地访问限制
  • 备份归档(ZIP)的内容结构与命名规则
  • 失败模式、边界条件、并发与运维注意事项

以下相关主题属于兄弟页面,本页仅做交叉引用,不展开:

  • SQLite 数据库连接池、WAL 模式与任务调度的整体设计:见 core.database 相关页面
  • RPC 注册中心与 JSON-RPC 协议通用机制(register_method、RpcError、ErrorCode):见 RPC 框架页面
  • utils::powershell 脚本执行器的进程创建细节:见工具库页面
  • 设置界面的整体布局(BackupSettingsContent.vue 仅作为 UI 入口提及):见设置页面

概述

备份与恢复解决的核心问题是:如何在不停止应用、不依赖第三方压缩库、且失败时绝不留下半成品数据的前提下,安全地迁移用户数据。

该能力的设计要点:

  1. 一致性优先:数据库运行在 WAL 模式下,直接复制 database.db 文件可能得到不含最新写入的不完整文件。导出时通过 SQLiteCpp 的在线备份接口生成合并了 WAL 当前状态的独立快照。
  2. 外部工具压缩:压缩与解压委托给 Windows 自带的 PowerShell(Compress-Archive / Expand-Archive),避免在 C++ 侧引入 ZIP 库依赖。
  3. 两阶段发布:ZIP 先写入 .partial.zip 临时名,压缩成功后再 rename 为最终文件名,保证用户目录中不会出现可被误用的半成品归档。
  4. 先校验后破坏:恢复前先用只读脚本校验 ZIP 根目录必须包含 database.db、settings.json、app_version.txt 三个必需文件,防止用户误选普通 ZIP 后清空当前数据。
  5. 退出后替换:正在使用的数据库与设置文件无法在进程存活时安全替换,因此恢复动作被编排为一个独立 PowerShell 脚本:等待(必要时强杀)当前进程 → 删除旧数据(含 -wal/-shm)→ 解压归档 → 重启应用。

典型使用场景:用户迁移设备、升级前留存快照、故障后回滚到已知良好状态。入口位于设置界面中的备份区域,远端浏览器打开的页面会被禁止触发宿主机文件选择对话框。

架构

Loading diagram...

分层说明:

  • 前端层:BackupSettingsContent.vue 承载交互;backupApi.ts 通过 call 发起 JSON-RPC 调用,并在 selectBackupArchive 中强制本地访问检查,弹出的文件对话框过滤条件为 ZIP backup (*.zip)|*.zip。
  • RPC 端点层:register_all 将两个处理器挂到 RPC 注册表上,端点目录中的 handle_export / handle_restore 是薄封装——只做协程适配、错误到 RpcError 的映射,以及恢复成功后延迟请求应用退出。
  • 功能层:真正的编排逻辑位于 features::backup,全部返回 std::expected<T, std::string>,顶层再包一层 try/catch 兜底。
  • 基础设施层:数据库快照、PowerShell 执行、AppData 路径解析与越界检查、字符串编码转换各司其职,功能层不直接触碰进程创建或 SQLite C API。
  • 文件系统:所有中间产物都落在 AppData/temp 下的本次操作目录,导出结束后整目录清理。

端点注册与 RPC 契约

端点由 RPC 注册中心统一挂载:

cpp
// 注册数据备份与恢复端点 endpoints::backup::register_all(state);

Source: registry.cpp

register_all 内部完成两个方法的注册与文档字符串声明:

cpp
1// 注册数据导出和完全替换恢复端点。 2auto register_all(core::AppState& app_state) -> void { 3 core::rpc::register_method<features::backup::ExportParams, features::backup::ExportResult>( 4 app_state, app_state.rpc->registry, "backup.export", handle_export, 5 "Export database, settings, managed backgrounds and app version to ZIP"); 6 7 core::rpc::register_method<features::backup::RestoreParams, features::backup::RestoreResult>( 8 app_state, app_state.rpc->registry, "backup.restore", handle_restore, 9 "Replace application data from ZIP after exit and restart the application"); 10}

Source: backup.cpp

方法请求参数类型响应类型语义
backup.exportExportParams(UTF-8 目标目录)ExportResult(备份路径、版本、时间戳、大小)同步完成导出,返回归档元数据
backup.restoreRestoreParams(UTF-8 备份 ZIP 路径)RestoreResult(scheduled = true)仅"排定"恢复;实际替换发生在应用退出之后

错误映射策略:功能层的字符串错误统一转换为 JSON-RPC 服务端错误:

cpp
1// 将业务错误统一映射为 JSON-RPC 服务错误。 2auto make_service_error(std::string error) -> core::rpc::RpcError { 3 return core::rpc::RpcError{ 4 .code = static_cast<int>(core::rpc::ErrorCode::ServerError), 5 .message = "Service error: " + std::move(error), 6 }; 7}

Source: backup.cpp

handle_restore 中还包含一个关键的时序处理:恢复脚本已排定后,不能立刻退出应用,否则成功响应来不及送达前端。因此用一个 750ms 定时器延迟投递 ExitEvent:

cpp
1asio::co_spawn( 2 *io_context, 3 [&app_state]() -> asio::awaitable<void> { 4 // 给 RPC 桥留出发送成功响应的时间,再由 UI 线程执行完整退出流程。 5 asio::steady_timer timer(co_await asio::this_coro::executor, 6 std::chrono::milliseconds(750)); 7 co_await timer.async_wait(asio::use_awaitable); 8 core::events::post(app_state, ui::floating_window::events::ExitEvent{}); 9 }, 10 core::async::log_completion("Backup restore exit request"));

Source: backup.cpp

设计意图:ExitEvent 投递到事件总线后由 UI 线程执行"完整退出流程"(保存状态、关闭数据库、销毁窗口),而不是在 RPC 协程里粗暴终止进程——这保证数据库连接正常关闭,随后外部脚本才能安全删除 database.db。

核心流程

导出流水线(export_backup_impl)

Loading diagram...

逐步解读(对应源码中的真实执行顺序):

  1. 前置校验:目标目录必须已存在(不做隐式创建,因为目录本身由前端文件对话框选择);app_state.runtime_info->version 必须可用,因为它既是归档内容也是文件名的一部分。
  2. 隔离工作目录:create_operation_directory() 在 AppData/temp 下创建 backup-{毫秒时间戳}-{进程PID} 目录,时间戳 + PID 双重保证并发导出不互相覆盖。
  3. 数据库快照:调用 core::database::backup_to,见下文"数据库一致性快照"一节。
  4. 附带数据复制:copy_settings 复制 settings.json;copy_backgrounds 以 recursive | overwrite_existing 递归复制托管背景目录;app_version.txt 直接由版本字符串写入。
  5. 安全文件名:版本号中除字母、数字、.、- 之外的字符全部替换为 _,最终归档名为 SpinningMomo-Backup-v{版本}-{毫秒时间戳}.zip。
  6. 压缩与两阶段发布:PowerShell 脚本把 payload/ 内容压缩到 .partial.zip;退出码为 0 才执行 rename 发布为最终名;任何一步失败都删除临时 ZIP 并清理工作目录。
  7. 收尾:file_size 读取最终归档大小填入 ExportResult;若读取大小失败,归档虽已发布但调用方会收到错误。

关键代码——两阶段发布与失败清理:

cpp
1auto compress_result = utils::powershell::run_script_and_wait( 2 script_path, {L"-SourceDirectory", payload_directory.wstring(), L"-DestinationPath", 3 temporary_path.wstring()}); 4if (!compress_result || *compress_result != 0) { 5 std::error_code remove_error; 6 std::filesystem::remove(temporary_path, remove_error); 7 remove_operation_directory(operation_directory); 8 return std::unexpected(compress_result ? "Windows PowerShell failed to create backup" 9 : compress_result.error()); 10} 11 12std::error_code rename_error; 13std::filesystem::rename(temporary_path, final_path, rename_error); 14if (rename_error) { 15 std::error_code remove_error; 16 std::filesystem::remove(temporary_path, remove_error); 17 remove_operation_directory(operation_directory); 18 return std::unexpected("Failed to publish backup archive: " + rename_error.message()); 19}

Source: backup.cpp

恢复编排(restore_backup_impl)

Loading diagram...

恢复与导出最大的差异在于同步性:导出在 RPC 调用内同步完成;恢复只是"排定",真正的数据替换发生在调用方进程退出之后。恢复脚本的核心逻辑(真实 PowerShell 内容):

powershell
1$process = Get-Process -Id $PidToWait -ErrorAction SilentlyContinue 2if ($process -and -not $process.WaitForExit(15000)) { 3 Stop-Process -Id $PidToWait -Force 4 Start-Sleep -Seconds 1 5} 6 7$targets = @( 8 'database.db', 9 'database.db-wal', 10 'database.db-shm', 11 'settings.json', 12 'app_version.txt', 13 'backgrounds' 14) 15foreach ($target in $targets) { 16 $targetPath = Join-Path -Path $AppDataDirectory -ChildPath $target 17 if (Test-Path -LiteralPath $targetPath) { 18 Remove-Item -LiteralPath $targetPath -Recurse -Force 19 } 20} 21 22Expand-Archive -LiteralPath $ArchivePath -DestinationPath $AppDataDirectory -Force 23$workingDirectory = Split-Path -Parent $ExecutablePath 24Start-Process -FilePath $ExecutablePath -WorkingDirectory $workingDirectory | Out-Null 25Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue

Source: backup.cpp

细节解读:

  • WaitForExit(15000) + 强杀兜底:正常路径由 ExitEvent 触发优雅退出;若应用卡住超过 15 秒,脚本强制结束进程。强杀后 Start-Sleep -Seconds 1 留出文件句柄释放时间。
  • 删除清单显式列举 -wal / -shm:恢复时绝不能把新 database.db 与旧 WAL 残留混在一起——那会导致 SQLite 尝试回放不匹配的 WAL 而损坏数据。这是恢复删除列表包含 WAL/SHM 文件的根本原因。
  • -Force 解压 + 全量替换:backgrounds 采用整目录替换语义,删除归档中不存在的背景文件,与导出侧"托管背景"的定义一致。
  • 脚本自我清理:Remove-Item -LiteralPath $PSCommandPath 在重启应用后删除脚本自身,避免 AppData/temp 堆积残留。

数据库一致性快照

数据库运行于 WAL 模式时,database.db 主文件中的数据可能落后于 -wal 日志。core::database::backup_to 用 SQLiteCpp 的在线备份封装解决该问题:

cpp
1// 从运行中的数据库生成一致快照,避免直接复制 WAL 数据库得到不完整文件。 2auto backup_to(core::AppState& app_state, const std::filesystem::path& destination_path) 3 -> std::expected<void, std::string> { 4 if (destination_path.empty()) { 5 return std::unexpected("Database backup destination is empty"); 6 } 7 8 std::expected<void, std::string> backup_result{}; 9 auto job_result = run_database_job(app_state, [&](SQLite::Database& source) { 10 // 先清理目标位置的旧快照,避免 SQLite 在已有文件之上叠加。 11 std::error_code remove_error; 12 if (std::filesystem::exists(destination_path, remove_error) && !remove_error) { 13 std::filesystem::remove(destination_path, remove_error); 14 if (remove_error) { 15 backup_result = 16 std::unexpected("Failed to remove old database snapshot: " + remove_error.message()); 17 return; 18 } 19 } 20 21 // 交给 SQLiteCpp 的在线备份封装生成包含当前 WAL 状态的一致快照。 22 source.backup(destination_path.string().c_str(), SQLite::Database::Save); 23 backup_result = {}; 24 }); 25 ... 26}

Sources:

设计意图:

  • 通过 run_database_job 在数据库专用任务上下文中执行,而非直接在 RPC 协程里触碰连接——这保证备份与其他数据库操作串行化,不会读到事务中间状态。
  • SQLite::Database::Save 模式的在线备份逐页复制数据库,并把 WAL 中的已提交内容一并合并进快照,产物是一个可独立打开的完整数据库文件。
  • 目标已存在时先 remove,防止在旧文件上叠加导致损坏。
  • 失败路径细分为三类错误消息:SQLite backup failed: ...(SQLite 异常)、Database backup failed: ...(标准异常)、以及 Failed to remove old database snapshot: ...(文件系统层)。

备份归档结构

条目来源说明
database.dbcore::database::backup_toSQLite 一致性快照(已合并 WAL)
settings.jsonAppData 根目录复制应用设置
app_version.txtruntime_info->version 原样写入生成备份时的应用版本
backgrounds/AppData 递归复制托管背景目录,恢复时整目录替换

归档命名:SpinningMomo-Backup-v{安全化版本号}-{Unix毫秒时间戳}.zip,中间产物为 SpinningMomo-Backup-{时间戳}.partial.zip。校验脚本只检查三个根级必需文件是否存在,backgrounds/ 可选(老版本备份或无背景时不阻断恢复)。

使用示例

前端调用封装

typescript
1// 仅允许本机页面选择备份文件,避免远端打开宿主机文件对话框。 2export async function selectBackupArchive(title: string): Promise<string | null> { 3 if (!isLocalAccess()) { 4 throw new Error('Backup selection is only available in the local application window.') 5 } 6 7 const parentWindowMode = isWebView() ? 1 : 2 8 const result = await call<{ paths: string[] }>( 9 'dialog.openFile', 10 { 11 title, 12 filter: 'ZIP backup (*.zip)|*.zip', 13 allowMultiple: false, 14 parentWindowMode, 15 }, 16 0 17 ) 18 return result.paths[0] || null 19} 20 21export async function exportBackup(destinationDirectory: string): Promise<BackupExportResult> { 22 return call<BackupExportResult>('backup.export', { destinationDirectory }, 0) 23} 24 25export async function restoreBackup(backupPath: string): Promise<void> { 26 await call('backup.restore', { backupPath }, 0) 27}

Source: backupApi.ts

说明:selectBackupArchive 中的 isLocalAccess() 检查是安全边界——远端浏览器页面不能触发宿主机文件对话框;parentWindowMode 依据是否运行在 WebView 中选择不同的父窗口模式,保证对话框模态归属正确。

压缩与校验脚本

压缩脚本使用 -LiteralPath 枚举后逐项压缩(而非压缩目录本身),保证 ZIP 根级直接是四个备份条目而非嵌套一层目录:

powershell
$items = Get-ChildItem -LiteralPath $SourceDirectory -Force Compress-Archive -Path $items.FullName -DestinationPath $DestinationPath -CompressionLevel Optimal -Force

Source: backup.cpp

校验脚本只读打开 ZIP,把条目名中的反斜杠统一为 / 后比对三个必需根文件:

powershell
1Add-Type -AssemblyName System.IO.Compression.FileSystem 2$archive = [IO.Compression.ZipFile]::OpenRead($ArchivePath) 3try { 4 $entryNames = @($archive.Entries | ForEach-Object { $_.FullName -replace '\\', '/' }) 5 foreach ($requiredName in @('database.db', 'settings.json', 'app_version.txt')) { 6 if ($entryNames -notcontains $requiredName) { 7 throw "Required backup file is missing: $requiredName" 8 } 9 } 10} finally { 11 $archive.Dispose() 12}

Source: backup.cpp

工作目录生命周期管理

导出的所有中间产物都隔离在按时间戳 + PID 命名的目录中,清理函数带路径越界检查:

cpp
1// 删除经过 AppData/temp 边界检查的工作目录。 2auto remove_operation_directory(const std::filesystem::path& operation_directory) -> void { 3 auto temp_directory_result = utils::path::GetAppDataSubdirectory("temp"); 4 if (!temp_directory_result || 5 !utils::path::IsPathWithinBase(operation_directory, *temp_directory_result)) { 6 return; 7 } 8 9 std::error_code remove_error; 10 std::filesystem::remove_all(operation_directory, remove_error); 11}

Source: backup.cpp

设计意图:IsPathWithinBase 防御性地确保 remove_all 永远只会删除 AppData/temp 之内的目录。即使将来目录名生成逻辑出错,也不会误删用户任意路径。

配置项

备份与恢复能力没有独立配置文件。其行为完全由运行时参数与固定约定决定:

参数 / 约定类型来源默认 / 固定值说明
destination_directoryUTF-8 字符串(前端传入)ExportParams无默认,必须已存在的目录归档输出位置,由宿主文件对话框选择
backup_pathUTF-8 字符串(前端传入)RestoreParams无默认,必须为 .zip 常规文件待恢复归档
恢复响应延迟std::chrono::milliseconds代码常量750msExitEvent 投递前的等待,保证 RPC 成功响应先送达
进程等待上限脚本参数PowerShell 脚本15000ms恢复脚本等待应用退出的最长时间,超时强杀
工作目录位置路径GetAppDataSubdirectory("temp")AppData/temp备份中间产物与恢复脚本落点
压缩级别脚本参数Compress-ArchiveOptimal导出 ZIP 压缩级别
目录名模式格式串std::formatbackup-{毫秒}-{PID}保证并发导出不冲突
归档名模式格式串std::formatSpinningMomo-Backup-v{版本}-{毫秒}.zip版本号中非安全字符替换为 _
文件对话框过滤字符串前端`ZIP backup (*.zip)*.zip`

API 参考

features::backup::export_backup(app_state, params) -> std::expected<ExportResult, std::string>

导出数据库快照、设置、版本与托管背景到单个 ZIP。顶层包装 detail::export_backup_impl,捕获 std::exception 并转换为字符串错误(前缀 Backup export failed: )。

参数:

  • app_state (core::AppState&):应用全局状态,用于获取数据库句柄与 runtime_info->version
  • params (const ExportParams&):含 UTF-8 编码的 destination_directory

返回: 成功时 ExportResult{backup_path, app_version, created_at, size};失败时字符串描述错误原因。

失败条件(来自源码的显式分支):

  • 目标目录不存在:Backup destination directory does not exist
  • 版本不可用:Current application version is unavailable
  • 临时/payload 目录创建失败、数据库快照失败、settings/背景复制失败、版本文件写入失败
  • PowerShell 压缩失败(退出码非 0 或进程启动失败)
  • 发布重命名失败:Failed to publish backup archive: ...
  • 读取归档大小失败(此时归档已生成但调用方收到错误)

features::backup::restore_backup(params) -> std::expected<RestoreResult, std::string>

校验归档并排定完全替换式恢复。顶层包装 detail::restore_backup_impl,异常前缀 Backup restore failed: 。

参数:

  • params (const RestoreParams&):含 UTF-8 编码的 backup_path

返回: 成功时 RestoreResult{.scheduled = true}——只表示恢复脚本已成功启动,不代表数据已替换。

失败条件:

  • 归档不存在或非常规文件:Backup archive does not exist
  • 扩展名(大小写不敏感)非 .zip:Backup archive must be a .zip file
  • 路径准备失败:Failed to prepare application restore paths
  • 校验脚本写入/执行失败
  • 校验未通过:Selected ZIP is not a SpinningMomo backup: ...
  • 恢复脚本启动失败

core::database::backup_to(app_state, destination_path) -> std::expected<void, std::string>

通过 SQLiteCpp 在线备份生成一致性数据库快照。见"数据库一致性快照"一节。

参数:

  • app_state (core::AppState&):用于通过 run_database_job 获取数据库连接
  • destination_path (const std::filesystem::path&):快照输出路径,不得为空

返回: 成功时 void;失败时含 SQLite backup failed: / Database backup failed: / Failed to remove old database snapshot: 前缀的错误。

前端 API(backupApi.ts)

函数签名对应 RPC
selectBackupArchive(title: string) => Promise<string | null>dialog.openFile
exportBackup(destinationDirectory: string) => Promise<BackupExportResult>backup.export
restoreBackup(backupPath: string) => Promise<void>backup.restore

失败模式、边界情况与并发

先校验后破坏(不可绕过的安全闸门):恢复流程在删除任何现有数据之前,先以只读方式打开 ZIP 校验三个必需根文件。校验失败的 ZIP 不会触发删除,当前数据保持原样。这防御了"用户误选任意 ZIP 导致数据被清空"的最严重事故。

两阶段发布防半成品:导出产物先写 .partial.zip,压缩成功后才原子 rename 为最终名。即使压缩中途失败或应用崩溃,目标目录中也不会出现看似完整实则截断的归档(残留的 .partial.zip 会在失败分支被显式删除;若进程崩溃遗留,其扩展名也不匹配 *.zip 恢复过滤条件)。

WAL 残留风险:恢复脚本显式删除 database.db-wal 与 database.db-shm。若只替换主库文件而保留旧 WAL,SQLite 会尝试回放与主库不匹配的日志导致数据损坏——这是删除清单包含 WAL/SHM 的根本原因。

优雅退出的双保险:ExitEvent 走正常退出流程确保数据库连接关闭;外部脚本 WaitForExit(15000) 超时后 Stop-Process -Force 兜底,随后 Start-Sleep -Seconds 1 等待句柄释放,避免 Remove-Item 因文件占用而失败。

并发语义:

  • 导出通过 run_database_job 与其他数据库操作串行化,快照不会读到事务中间状态。
  • 工作目录名含毫秒时间戳与 PID,并发导出互不覆盖。
  • 导出与恢复同时发起时:恢复脚本排定后会删除 database.db 等文件,期间任何新的导出/数据库操作都可能失败——源码未对此做互斥锁,属于已知边界,实际由"恢复排定后 750ms 内必然退出"缓解。
  • 恢复脚本排定后不应再发起其他写操作,因为进程即将退出,写入会被丢弃或与替换语义冲突。

编码边界:RPC 参数是 UTF-8,Windows 文件系统 API 需要宽字符。FromUtf8 / ToUtf8 在边界处完成转换,ExportResult.backup_path 返回给前端的也是 UTF-8。

清理路径的防御性设计:remove_operation_directory 只删除位于 AppData/temp 之内的目录(IsPathWithinBase 检查),且使用 std::error_code 版本避免异常逃逸;失败时静默忽略(残留目录可被后续清理,不影响正确性)。

跨版本兼容:恢复脚本不校验 app_version.txt 内容,只确认其存在——老版本备份可恢复到新版本应用中,由应用自身的迁移逻辑处理数据结构升级。

性能与运维注意事项

  • 压缩成本:Compress-Archive -CompressionLevel Optimal 在背景目录较大时耗时明显,导出在 RPC 协程内同步执行,前端应有进行中状态提示。
  • 外部进程依赖:依赖 Windows PowerShell 与 System.IO.Compression.FileSystem 程序集。Compress-Archive / Expand-Archive 对超长路径(>260 字符)或含特殊字符的文件名可能失败,错误会以退出码非 0 形式返回并映射为 Windows PowerShell failed to create backup。
  • 磁盘空间:导出峰值占用 = 工作目录(payload 副本)+ 临时 ZIP + 最终 ZIP,约为数据的 2 倍多;恢复峰值 = 旧数据 + 解压新数据。
  • 重启语义:恢复后由脚本以原可执行文件路径和工作目录重启应用;若用户手动更改了 AppData 位置或可执行文件被移动,脚本启动参数在排定时已固化,可能出现重启失败——数据本身已完成替换。
  • 残留清理:恢复脚本自删除;导出工作目录在成功/失败路径均调用清理,但进程崩溃时可能遗留 AppData/temp/backup-* 目录,可安全手工删除。
  • 运维建议:定期导出到非 AppData 所在磁盘;恢复前可先做一次导出作为"回滚点"(恢复操作本身不自动生成安全备份,源码中没有该机制)。

扩展点

  • 新增备份内容:在 export_backup_impl 的 payload 组装段(copy_settings / copy_backgrounds / write_text_file 调用处之后)追加新的复制或写入步骤,同时更新 write_validate_script 的必需文件列表与 write_restore_script 的删除清单($targets 数组)。三者必须同步修改,否则校验或替换语义会不一致。
  • 更换压缩实现:压缩/解压全部经由 utils::powershell 边界,替换为内置 ZIP 库时只需改写 write_compress_script 调用点与恢复脚本中的 Expand-Archive 段,RPC 契约不变。
  • 版本迁移钩子:恢复流程当前不读取 app_version.txt 做版本比对;若需要"拒绝降级恢复"或恢复前迁移,可在 restore_backup_impl 校验通过后、启动脚本前插入解析逻辑。
  • 前端集成:backupApi.ts 的三个函数是唯一前端入口,任何新的备份相关 UI(如定时备份、云同步)都应复用这两个 RPC 方法或平行新增端点,而不是绕过 RPC 直接触碰文件。

相关链接

  • RPC 框架与端点注册机制:src/core/rpc/registry.cpp
  • 数据库层与任务调度:src/core/database/database.cpp
  • PowerShell 执行工具:src/utils/powershell/
  • AppData 路径工具:src/utils/path/
  • 设置界面备份入口(UI 呈现):web/src/features/settings/components/BackupSettingsContent.vue
  • 前端备份 API:web/src/features/settings/backupApi.ts

Sources

(3 files)
src/core/rpc/endpoints/backup
src/features/backup
web/src/features/settings