mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 20:22:12 +00:00
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
233 lines
7.1 KiB
Dart
233 lines
7.1 KiB
Dart
// ignore_for_file: parameter_assignments
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:background_downloader/background_downloader.dart';
|
|
import 'package:clock/clock.dart';
|
|
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift_flutter/drift_flutter.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
|
import 'package:twonly/src/database/twonly.db.dart';
|
|
import 'package:twonly/src/model/json/userdata.dart';
|
|
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
|
import 'package:twonly/src/services/backup/common.backup.dart';
|
|
import 'package:twonly/src/utils/log.dart';
|
|
import 'package:twonly/src/utils/misc.dart';
|
|
import 'package:twonly/src/utils/storage.dart';
|
|
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
|
|
|
Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|
if (gUser.twonlySafeBackup == null) {
|
|
return;
|
|
}
|
|
|
|
if (gUser.twonlySafeBackup!.backupUploadState ==
|
|
LastBackupUploadState.pending) {
|
|
Log.warn('Backup upload is already pending.');
|
|
return;
|
|
}
|
|
|
|
final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone;
|
|
if (!force && lastUpdateTime != null) {
|
|
if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
Log.info('Starting new twonly Backup!');
|
|
|
|
final baseDir = globalApplicationSupportDirectory;
|
|
|
|
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
|
|
await backupDir.create(recursive: true);
|
|
|
|
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
|
|
|
|
final backupDatabaseFileCleaned = File(
|
|
join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
|
|
);
|
|
|
|
// copy database
|
|
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
|
await originalDatabase.copy(backupDatabaseFile.path);
|
|
|
|
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
|
final backupDB = TwonlyDB(
|
|
driftDatabase(
|
|
name: 'twonly.backup',
|
|
native: DriftNativeOptions(
|
|
databaseDirectory: () async {
|
|
return backupDir;
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
await backupDB.deleteDataForTwonlySafe();
|
|
|
|
await backupDB.customStatement('VACUUM INTO ?', [
|
|
backupDatabaseFileCleaned.path,
|
|
]);
|
|
|
|
await backupDB.printTableSizes();
|
|
|
|
await backupDB.close();
|
|
|
|
// ignore: inference_failure_on_collection_literal
|
|
final secureStorageBackup = {};
|
|
const storage = FlutterSecureStorage();
|
|
secureStorageBackup[SecureStorageKeys.signalIdentity] = await storage.read(
|
|
key: SecureStorageKeys.signalIdentity,
|
|
);
|
|
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = await storage
|
|
.read(key: SecureStorageKeys.signalSignedPreKey);
|
|
|
|
final userBackup = await getUser();
|
|
if (userBackup == null) return;
|
|
// FILTER settings which should not be in the backup
|
|
userBackup
|
|
..twonlySafeBackup = null
|
|
..lastImageSend = null
|
|
..todaysImageCounter = null
|
|
..lastPlanBallance = ''
|
|
..additionalUserInvites = ''
|
|
..signalLastSignedPreKeyUpdated = null;
|
|
|
|
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
|
|
|
|
// Compress and convert backup data
|
|
|
|
final twonlyDatabaseBytes = await backupDatabaseFileCleaned.readAsBytes();
|
|
await backupDatabaseFile.delete();
|
|
await backupDatabaseFileCleaned.delete();
|
|
|
|
Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}');
|
|
Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}');
|
|
|
|
final backupProto = TwonlySafeBackupContent(
|
|
secureStorageJson: jsonEncode(secureStorageBackup),
|
|
twonlyDatabase: twonlyDatabaseBytes,
|
|
);
|
|
|
|
final backupBytes = gzip.encode(backupProto.writeToBuffer());
|
|
|
|
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
|
|
|
|
if (gUser.twonlySafeBackup!.lastBackupDone == null ||
|
|
gUser.twonlySafeBackup!.lastBackupDone!.isAfter(
|
|
clock.now().subtract(const Duration(days: 90)),
|
|
)) {
|
|
force = true;
|
|
}
|
|
|
|
final lastHash = await storage.read(
|
|
key: SecureStorageKeys.twonlySafeLastBackupHash,
|
|
);
|
|
|
|
if (lastHash != null && !force) {
|
|
if (backupHash == lastHash) {
|
|
Log.info('Since last backup nothing has changed.');
|
|
return;
|
|
}
|
|
}
|
|
await storage.write(
|
|
key: SecureStorageKeys.twonlySafeLastBackupHash,
|
|
value: backupHash,
|
|
);
|
|
|
|
// Encrypt backup data
|
|
|
|
final chacha20 = FlutterChacha20.poly1305Aead();
|
|
final nonce = chacha20.newNonce();
|
|
|
|
final secretBox = await chacha20.encrypt(
|
|
backupBytes,
|
|
secretKey: SecretKey(gUser.twonlySafeBackup!.encryptionKey),
|
|
nonce: nonce,
|
|
);
|
|
|
|
final encryptedBackupBytes = TwonlySafeBackupEncrypted(
|
|
mac: secretBox.mac.bytes,
|
|
nonce: nonce,
|
|
cipherText: secretBox.cipherText,
|
|
).writeToBuffer();
|
|
|
|
Log.info('Backup files created.');
|
|
|
|
final encryptedBackupBytesFile = File(
|
|
join(backupDir.path, 'twonly_safe.backup'),
|
|
);
|
|
|
|
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
|
|
|
Log.info(
|
|
'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.',
|
|
);
|
|
|
|
if (gUser.backupServer != null) {
|
|
if (encryptedBackupBytes.length > gUser.backupServer!.maxBackupBytes) {
|
|
Log.error('Backup is to big for the alternative backup server.');
|
|
await updateUserdata((user) {
|
|
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
|
return user;
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
final task = UploadTask.fromFile(
|
|
taskId: 'backup',
|
|
file: encryptedBackupBytesFile,
|
|
httpRequestMethod: 'PUT',
|
|
url: (await getTwonlySafeBackupUrl())!,
|
|
post: 'binary',
|
|
retries: 2,
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
);
|
|
if (await FileDownloader().enqueue(task)) {
|
|
Log.info('Starting upload from twonly Backup.');
|
|
await updateUserdata((user) {
|
|
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
|
|
user.twonlySafeBackup!.lastBackupDone = clock.now();
|
|
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
|
|
return user;
|
|
});
|
|
gUpdateBackupView();
|
|
} else {
|
|
Log.error('Error starting UploadTask for twonly Backup.');
|
|
}
|
|
}
|
|
|
|
Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
|
if (update.status == TaskStatus.failed ||
|
|
update.status == TaskStatus.canceled) {
|
|
await updateUserdata((user) {
|
|
if (user.twonlySafeBackup != null) {
|
|
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
|
}
|
|
return user;
|
|
});
|
|
} else if (update.status == TaskStatus.complete) {
|
|
Log.info(
|
|
'twonly Backup uploaded with status code ${update.responseStatusCode}',
|
|
);
|
|
await updateUserdata((user) {
|
|
if (user.twonlySafeBackup != null) {
|
|
user.twonlySafeBackup!.backupUploadState =
|
|
LastBackupUploadState.success;
|
|
}
|
|
return user;
|
|
});
|
|
} else {
|
|
Log.info('Backup is in state: ${update.status}');
|
|
return;
|
|
}
|
|
gUpdateBackupView();
|
|
}
|