feat(backup): add automatic weekly encrypted backups

A silent safety net against in-app data loss or DB corruption, with zero
friction. Driven off the app lifecycle (startup + when hidden), it writes a
sealed .tanemaki copy into the app's private storage once a week, keeping the
newest 3 (rotating). Reuses the manual backup's encryption via the extracted
ExportImportService.buildSealedBackup(); never throws, so a failed copy can't
crash startup or backgrounding. Not registered on web (no file storage).

Settings shows a quiet reassurance line with the date of the last automatic
copy (localised via intl), hidden where no automatic backup exists.
This commit is contained in:
vjrj 2026-07-10 10:17:36 +02:00
parent cf6975b748
commit 9c449964fd
19 changed files with 559 additions and 29 deletions

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:commons_core/commons_core.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
@ -15,6 +16,8 @@ import '../db/encrypted_executor.dart';
import '../i18n/strings.g.dart';
import '../security/secret_store.dart';
import '../security/secure_key_store.dart';
import '../services/auto_backup_service.dart';
import '../services/auto_backup_store.dart';
import '../services/export_import_service.dart';
import '../services/file_picker_file_service.dart';
import '../services/file_service.dart';
@ -98,6 +101,23 @@ Future<void> configureDependencies() async {
..registerSingleton<RecoverySheetService>(
RecoverySheetService(files: fileService),
);
// Automatic silent backups need real file storage; the web build has none, so
// it simply goes without (the manual "save a copy" still works there).
if (!kIsWeb) {
getIt.registerSingleton<AutoBackupService>(
AutoBackupService(
exporter: getIt<ExportImportService>(),
store: AutoBackupStore(secretStore),
directory: _backupsDir,
),
);
}
}
Future<Directory> _backupsDir() async {
final dir = await getApplicationSupportDirectory();
return Directory(p.join(dir.path, 'backups'));
}
Future<File> _databaseFile() async {