HarmonyOS 本地备份与系统备份:BackupExtensionAbility、快照导出和恢复

HarmonyOS 本地备份与系统备份:BackupExtensionAbility、快照导出和恢复
HarmonyOS 本地备份与系统备份BackupExtensionAbility、快照导出和恢复卡片工具类应用的数据量不大但用户对“不要丢卡片”非常敏感。这个项目里做了两层备份应用内的最近一次本地快照以及 HarmonyOS 系统备份能力。两者都复用AppDataService的状态导出和恢复逻辑。备份配置先打开系统备份能力需要在module.json5里注册 backup extension{ name: EntryBackupAbility, srcEntry: ./ets/entrybackupability/EntryBackupAbility.ets, type: backup, exported: false, metadata: [ { name: ohos.extension.backup, resource: $profile:backup_config } ] }同时backup_config.json里允许备份恢复{ allowToBackupRestore: true }这两个文件缺一不可。系统备份能力EntryBackupAbilityEntryBackupAbility.ets很薄只负责生命周期和日志export default class EntryBackupAbility extends BackupExtensionAbility { async onBackup() { try { appDataService.initialize(this.context); const backupPath: string appDataService.exportSystemBackup(this.context.backupDir); hilog.info(DOMAIN, testTag, onBackup ok %{public}s, backupPath); } catch (error) { hilog.error(DOMAIN, testTag, onBackup failed %{public}s, JSON.stringify(error)); } } async onRestore(bundleVersion: BundleVersion) { try { appDataService.initialize(this.context); const restored: boolean appDataService.restoreSystemBackup(this.context.backupDir); hilog.info(DOMAIN, testTag, onRestore ok %{public}s %{public}s, JSON.stringify(bundleVersion), ${restored}); } catch (error) { hilog.error(DOMAIN, testTag, onRestore failed %{public}s, JSON.stringify(error)); } } }重点是系统回调也要先初始化数据服务。Backup extension 不能假设主应用页面已经运行。应用内备份保存最近一次快照备份页的按钮很直接private manualBackup(): void { appDataService.backupNow(); this.refreshData(); } private restoreBackup(): void { appDataService.restoreLastBackup(); this.refreshData(); }服务层执行备份时导出 JSON 快照并记录备份大小backupNow(): BackupMetaModel { const now: Date new Date(); this.state.backupMeta.lastBackupAt formatDateTime(now); this.state.backupMeta.lastBackupSize ; const snapshot: string this.exportSnapshot(); this.state.backupMeta.lastBackupSize this.formatSize(snapshot.length); this.recordActivity(backup, 执行本地备份, 3, now); this.persistLastBackupSnapshot(snapshot); this.persistState(); return this.cloneBackupMeta(this.state.backupMeta); }注意先设置时间再导出快照最后写入状态和最近快照。系统备份写入 backupDir系统备份使用系统传入的backupDirexportSystemBackup(backupDirectory: string): string { const snapshot: string this.exportSnapshot(); const targetPath: string joinPath(backupDirectory, SYSTEM_BACKUP_FILE); const handle: fileIo.File fileIo.openSync( targetPath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC ); try { fileIo.writeSync(handle.fd, snapshot); fileIo.fsyncSync(handle.fd); } finally { fileIo.closeSync(handle); } return targetPath; }这里用了fsyncSync()目的是尽量保证文件落盘后再结束回调。对备份这种操作来说写入完整性比省一点时间更重要。恢复读快照后统一 import系统恢复读取同一个文件restoreSystemBackup(backupDirectory: string): boolean { const targetPath: string joinPath(backupDirectory, SYSTEM_BACKUP_FILE); try { const snapshot: string fileIo.readTextSync(targetPath); if (!snapshot.length) { return false; } this.importSnapshot(snapshot); this.state.backupMeta.lastBackupSize this.formatSize(snapshot.length); this.state.backupMeta.lastRestoreAt formatDateTime(new Date()); this.recordActivity(restoreBackup, 从系统备份恢复数据, 3, new Date()); this.persistState(); return true; } catch (_error) { return false; } }本地恢复和系统恢复都应该走同一套importSnapshot()这样版本兼容、normalize 和异常处理不会分叉。备份页展示的是当前本地状态BackupPage.ets页面读取摘要卡和元信息State summaryCard: ShowcaseCardModel appDataService.getBackupSummaryCard(); State backupMeta: BackupMetaModel appDataService.getBackupMeta(); private refreshData(): void { this.summaryCard appDataService.getBackupSummaryCard(); this.backupMeta appDataService.getBackupMeta(); }按钮操作后立即refreshData()页面就能看到最新备份时间、备份大小和自动备份状态。自动备份开关只改状态不做后台任务页面里有“自动备份”按钮appDataService.setAutoBackupEnabled(!this.backupMeta.autoBackupEnabled); this.refreshData();当前实现只是保存开关状态并未接后台定时任务。交付记录里需要明确这一点避免把 UI 状态误认为已经有系统级自动调度。常见坑Backup extension 里忘记appDataService.initialize(this.context)。module.json5配了 backup但backup_config.json缺失。系统备份和应用内备份使用两套 JSON导致恢复逻辑不一致。恢复后不调用persistState()页面刷新后状态又回退。备份大小在导出前计算导致显示不准确。验证建议按顺序验证D:\dev\command-line-tools\bin\hvigorw.bat assembleHap --no-daemon --stacktrace手工检查打开备份页点击“立即备份”最近备份时间更新。修改卡片后点击“恢复最近备份”确认数据回到备份状态。切换自动备份按钮状态文案同步变化。系统备份能力触发时project028-backup.json能写入 backupDir。恢复后统计页活动日志增加恢复记录。基础链路小结这个项目的备份设计很简单整份应用状态就是备份快照。应用内备份把快照存在 Preferences系统备份把快照写入backupDir文件。两者都复用服务层导出和恢复逻辑。对轻量 HarmonyOS 工具应用来说这比引入复杂备份模型更稳也更容易验证。备份页要同时满足用户体验和系统备份能力备份恢复不能只写“点击按钮保存快照”。Project028 的备份模块分两条线应用内手动备份/恢复以及 HarmonyOS 系统备份 Ability。前者解决用户主动操作后者服务系统迁移和平台能力两者都应该回到AppDataService不要在页面和 Ability 中各写一套序列化逻辑。应用内备份由BackupPage.ets调用appDataService.backupNow()和restoreLastBackup()。页面只负责触发、刷新和 Toast不应该直接拼装快照。这样能保证备份元数据、活动记录、持久化动作都由服务层统一维护。private manualBackup(): void { this.applyBackupMeta(appDataService.backupNow()); this.showBackupToast(已完成本地备份); this.reloadBackupPage(); } private restoreBackup(): void { const restored: boolean appDataService.restoreLastBackup(); this.refreshData(); this.showBackupToast(restored ? 已恢复最近备份 : 暂无可恢复的本地备份); }系统备份则在EntryBackupAbility.ets中完成。Ability 初始化服务层后调用exportSystemBackup(this.context.backupDir)或restoreSystemBackup(this.context.backupDir)。这说明备份目录由系统上下文提供业务侧只需要把当前状态导出为稳定快照。onBackup() { appDataService.initialize(this.context); const backupPath: string appDataService.exportSystemBackup(this.context.backupDir); } onRestore(bundleVersion: BundleVersion) { appDataService.initialize(this.context); const restored: boolean appDataService.restoreSystemBackup(this.context.backupDir); }还有一个审核视角需要单独说明备份页有说明性条目时点击必须有反馈。AGC 审核常会点页面上的每个看起来可交互的区域如果InfoRow有视觉点击感却没有响应容易被归类为“点击无反应”。Project028 后续修复中为InfoRow增加BackupInfoAction和 Toast就是这一点的体现。落地检查清单是否区分应用内备份和系统备份 Ability。是否说明备份快照只能由服务层生成页面不直接拼 JSON。是否覆盖空备份时的恢复结果。是否写到审核体验所有可点击行都要有反馈。是否覆盖真实路径BackupPage.ets、AppDataService.ets、EntryBackupAbility.ets。