CW-875 BackupServiceV3 (#2064)

* implement v3 backup system

- Add new BackupServiceV3 with chunked backup support and checksums
- Implement json-based metadata in backup system
- Instead of binary file export a .zip file that user can open and
see that it is a cake backup
(should also prevent 3rd party software from corrupting binary
data inside of the file, in case it doesn't checksum will fail,
and user will know for sure that backup got corrupted)
- Update flutter to 3.27.4 to use archive ^4.x.x (it offers in memory
archive operations)

* fix wallets not getting restored properly

* prevent out of memory errors on files that are way too big during restore

* Update lib/view_model/backup_view_model.dart [skip ci]

* Update lib/core/backup_service.dart [skip ci]

* Update lib/core/backup_service.dart

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan 2025-03-22 02:16:54 +01:00 committed by GitHub
parent a085eff984
commit 102ab8dbe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1193 additions and 807 deletions

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cake_wallet/core/backup_service.dart';
import 'package:cake_wallet/core/backup_service_v3.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
@ -15,10 +16,10 @@ import 'package:path_provider/path_provider.dart';
part 'backup_view_model.g.dart';
class BackupExportFile {
BackupExportFile(this.content, {required this.name});
BackupExportFile(this.file, {required this.name});
final String name;
final List<int> content;
final File file;
}
class BackupViewModel = BackupViewModelBase with _$BackupViewModel;
@ -38,7 +39,7 @@ abstract class BackupViewModelBase with Store {
final SecureStorage secureStorage;
final SecretStore secretStore;
final BackupService backupService;
final BackupServiceV3 backupService;
@observable
ExecutionState state;
@ -59,14 +60,14 @@ abstract class BackupViewModelBase with Store {
Future<BackupExportFile?> exportBackup() async {
try {
state = IsExecutingState();
final backupContent = await backupService.exportBackup(backupPassword);
final backupFile = await backupService.exportBackupFile(backupPassword);
state = ExecutedSuccessfullyState();
final now = DateTime.now();
final formatter = DateFormat('yyyy-MM-dd_Hm');
final snakeAppName = approximatedAppName.replaceAll(' ', '_').toLowerCase();
final fileName = '${snakeAppName}_backup_${formatter.format(now)}';
final fileName = '${snakeAppName}_backup_${formatter.format(now)}.zip';
return BackupExportFile(backupContent.toList(), name: fileName);
return BackupExportFile(backupFile, name: fileName);
} catch (e) {
printV(e.toString());
state = FailureState(e.toString());
@ -77,26 +78,35 @@ abstract class BackupViewModelBase with Store {
Future<String> saveBackupFileLocally(BackupExportFile backup) async {
final appDir = await getAppDir();
final path = '${appDir.path}/${backup.name}';
final backupFile = File(path);
await backupFile.writeAsBytes(backup.content);
if (File(path).existsSync()) {
File(path).deleteSync();
}
await backup.file.copy(path);
return path;
}
Future<void> removeBackupFileLocally(BackupExportFile backup) async {
final appDir = await getAppDir();
final path = '${appDir.path}/${backup.name}';
final backupFile = File(path);
await backupFile.delete();
if (File(path).existsSync()) {
File(path).deleteSync();
}
}
@action
void showMasterPassword() => isBackupPasswordVisible = true;
@action
Future<void> saveToDownload(String name, List<int> content) async {
Future<void> saveToDownload(String name, File file) async {
if (!Platform.isAndroid) {
return;
}
const downloadDirPath = '/storage/emulated/0/Download'; // For Android
final filePath = '$downloadDirPath/${name}';
final file = File(filePath);
await file.writeAsBytes(content);
final downloadFile = File(filePath);
if (downloadFile.existsSync()) {
downloadFile.deleteSync();
}
await file.copy(filePath);
}
}