implemented user backups

This commit is contained in:
Fabian Freund
2025-12-18 18:38:06 +01:00
parent 2f1ecd50e1
commit 018915298a
19 changed files with 1172 additions and 78 deletions
@@ -0,0 +1,154 @@
import 'dart:io';
import 'package:convert/convert.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:secure_archive/secure_archive.dart';
import 'package:weblibre/core/filesystem.dart';
import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/features/user/domain/repositories/profile.dart';
part 'user_backup.g.dart';
@Riverpod(keepAlive: true)
class UserBackupService extends _$UserBackupService {
static final dateFormatter = FixedDateTimeFormatter('YYYY-MM-DD_hhmmss');
Future<Directory> getBackupDirectory() async {
return Directory(
p.join(
await getExternalStorageDirectory().then(
(dir) => Directory(
dir!.path.replaceFirst('/data/', '/media/'),
).parent.path,
),
'Backup',
),
);
}
Stream<File> getBackupListStream() async* {
final backupDirectory = await getBackupDirectory();
await for (final entity in backupDirectory.list(recursive: true)) {
if (entity is File) {
yield entity;
}
}
}
Future<bool> createUserBackup(
Profile profile, {
required String password,
required bool integrityCheck,
}) async {
final timestamp = dateFormatter.encode(DateTime.now());
final outputFile = File(
p.join(
await getBackupDirectory().then((dir) => dir.path),
'backup_${profile.name}_$timestamp.weblibre',
),
);
await outputFile.parent.create(recursive: true);
final backup = SecureArchivePack(
outputFile: outputFile,
sourceDirectory: filesystem.getProfileDir(profile.uuidValue),
argon2Params: Argon2Params.memoryConstrained(),
);
await backup.pack(password, integrityCheck: integrityCheck);
return true;
}
Future<bool> restoreAndCreateNew(
File backupFile, {
required String profileName,
required String password,
}) async {
final outputDirectory = Directory(
p.join(filesystem.profilesDir.path, p.basename(backupFile.path)),
);
try {
final backup = SecureArchiveUnpack(
inputFile: backupFile,
outputDirectory: outputDirectory,
argon2Params: Argon2Params.memoryConstrained(),
);
await backup.unpack(password).then((_) async {
final newProfile = Profile.create(name: profileName);
final newPath = filesystem.getProfileDir(newProfile.uuidValue);
await outputDirectory.rename(newPath.path);
await filesystem.updateProfileMetadata(newProfile);
});
ref.invalidate(profileRepositoryProvider);
return true;
} finally {
try {
if (await outputDirectory.exists()) {
await outputDirectory.delete(recursive: true);
}
} catch (_) {
// Ignore cleanup errors
}
}
}
Future<bool> restoreAndCreateOrOverride(
File backupFile, {
required String password,
required FutureOr<bool?> Function() confirmOverrideCallback,
}) async {
final outputDirectory = Directory(
p.join(filesystem.profilesDir.path, p.basename(backupFile.path)),
);
try {
final backup = SecureArchiveUnpack(
inputFile: backupFile,
outputDirectory: outputDirectory,
argon2Params: Argon2Params.memoryConstrained(),
);
await backup.unpack(password).then((_) async {
final existingProfile = await filesystem.readProfileMetadata(
outputDirectory,
);
if (existingProfile != null) {
if (existingProfile.uuidValue == filesystem.selectedProfile) {
throw Exception(
'Unable to override active User, please switch to another User and try again',
);
}
final profileDir = filesystem.getProfileDir(
existingProfile.uuidValue,
);
if (await profileDir.exists()) {
final result = await confirmOverrideCallback();
if (result == true) {
await profileDir.delete(recursive: true);
await outputDirectory.rename(profileDir.path);
}
}
}
});
ref.invalidate(profileRepositoryProvider);
return true;
} finally {
await outputDirectory.delete(recursive: true);
}
}
@override
void build() {}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_backup.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(UserBackupService)
const userBackupServiceProvider = UserBackupServiceProvider._();
final class UserBackupServiceProvider
extends $NotifierProvider<UserBackupService, void> {
const UserBackupServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'userBackupServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$userBackupServiceHash();
@$internal
@override
UserBackupService create() => UserBackupService();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$userBackupServiceHash() => r'358e4c58703b14cff767184a4f658f7eac0454f6';
abstract class _$UserBackupService extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleValue(ref, null);
}
}