备份与恢复数据
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 入口提及):见设置页面
概述
备份与恢复解决的核心问题是:如何在不停止应用、不依赖第三方压缩库、且失败时绝不留下半成品数据的前提下,安全地迁移用户数据。
该能力的设计要点:
- 一致性优先:数据库运行在 WAL 模式下,直接复制
database.db文件可能得到不含最新写入的不完整文件。导出时通过 SQLiteCpp 的在线备份接口生成合并了 WAL 当前状态的独立快照。 - 外部工具压缩:压缩与解压委托给 Windows 自带的 PowerShell(
Compress-Archive/Expand-Archive),避免在 C++ 侧引入 ZIP 库依赖。 - 两阶段发布:ZIP 先写入
.partial.zip临时名,压缩成功后再rename为最终文件名,保证用户目录中不会出现可被误用的半成品归档。 - 先校验后破坏:恢复前先用只读脚本校验 ZIP 根目录必须包含
database.db、settings.json、app_version.txt三个必需文件,防止用户误选普通 ZIP 后清空当前数据。 - 退出后替换:正在使用的数据库与设置文件无法在进程存活时安全替换,因此恢复动作被编排为一个独立 PowerShell 脚本:等待(必要时强杀)当前进程 → 删除旧数据(含
-wal/-shm)→ 解压归档 → 重启应用。
典型使用场景:用户迁移设备、升级前留存快照、故障后回滚到已知良好状态。入口位于设置界面中的备份区域,远端浏览器打开的页面会被禁止触发宿主机文件选择对话框。
架构
分层说明:
- 前端层:
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 注册中心统一挂载:
// 注册数据备份与恢复端点
endpoints::backup::register_all(state);Source: registry.cpp
register_all 内部完成两个方法的注册与文档字符串声明:
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.export | ExportParams(UTF-8 目标目录) | ExportResult(备份路径、版本、时间戳、大小) | 同步完成导出,返回归档元数据 |
backup.restore | RestoreParams(UTF-8 备份 ZIP 路径) | RestoreResult(scheduled = true) | 仅"排定"恢复;实际替换发生在应用退出之后 |
错误映射策略:功能层的字符串错误统一转换为 JSON-RPC 服务端错误:
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:
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)
逐步解读(对应源码中的真实执行顺序):
- 前置校验:目标目录必须已存在(不做隐式创建,因为目录本身由前端文件对话框选择);
app_state.runtime_info->version必须可用,因为它既是归档内容也是文件名的一部分。 - 隔离工作目录:
create_operation_directory()在AppData/temp下创建backup-{毫秒时间戳}-{进程PID}目录,时间戳 + PID 双重保证并发导出不互相覆盖。 - 数据库快照:调用
core::database::backup_to,见下文"数据库一致性快照"一节。 - 附带数据复制:
copy_settings复制settings.json;copy_backgrounds以recursive | overwrite_existing递归复制托管背景目录;app_version.txt直接由版本字符串写入。 - 安全文件名:版本号中除字母、数字、
.、-之外的字符全部替换为_,最终归档名为SpinningMomo-Backup-v{版本}-{毫秒时间戳}.zip。 - 压缩与两阶段发布:PowerShell 脚本把
payload/内容压缩到.partial.zip;退出码为 0 才执行rename发布为最终名;任何一步失败都删除临时 ZIP 并清理工作目录。 - 收尾:
file_size读取最终归档大小填入ExportResult;若读取大小失败,归档虽已发布但调用方会收到错误。
关键代码——两阶段发布与失败清理:
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)
恢复与导出最大的差异在于同步性:导出在 RPC 调用内同步完成;恢复只是"排定",真正的数据替换发生在调用方进程退出之后。恢复脚本的核心逻辑(真实 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 SilentlyContinueSource: 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 的在线备份封装解决该问题:
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.db | core::database::backup_to | SQLite 一致性快照(已合并 WAL) |
settings.json | AppData 根目录复制 | 应用设置 |
app_version.txt | runtime_info->version 原样写入 | 生成备份时的应用版本 |
backgrounds/ | AppData 递归复制 | 托管背景目录,恢复时整目录替换 |
归档命名:SpinningMomo-Backup-v{安全化版本号}-{Unix毫秒时间戳}.zip,中间产物为 SpinningMomo-Backup-{时间戳}.partial.zip。校验脚本只检查三个根级必需文件是否存在,backgrounds/ 可选(老版本备份或无背景时不阻断恢复)。
使用示例
前端调用封装
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 根级直接是四个备份条目而非嵌套一层目录:
$items = Get-ChildItem -LiteralPath $SourceDirectory -Force
Compress-Archive -Path $items.FullName -DestinationPath $DestinationPath -CompressionLevel Optimal -ForceSource: backup.cpp
校验脚本只读打开 ZIP,把条目名中的反斜杠统一为 / 后比对三个必需根文件:
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 命名的目录中,清理函数带路径越界检查:
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_directory | UTF-8 字符串(前端传入) | ExportParams | 无默认,必须已存在的目录 | 归档输出位置,由宿主文件对话框选择 |
backup_path | UTF-8 字符串(前端传入) | RestoreParams | 无默认,必须为 .zip 常规文件 | 待恢复归档 |
| 恢复响应延迟 | std::chrono::milliseconds | 代码常量 | 750ms | ExitEvent 投递前的等待,保证 RPC 成功响应先送达 |
| 进程等待上限 | 脚本参数 | PowerShell 脚本 | 15000ms | 恢复脚本等待应用退出的最长时间,超时强杀 |
| 工作目录位置 | 路径 | GetAppDataSubdirectory("temp") | AppData/temp | 备份中间产物与恢复脚本落点 |
| 压缩级别 | 脚本参数 | Compress-Archive | Optimal | 导出 ZIP 压缩级别 |
| 目录名模式 | 格式串 | std::format | backup-{毫秒}-{PID} | 保证并发导出不冲突 |
| 归档名模式 | 格式串 | std::format | SpinningMomo-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->versionparams(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