Files
flutter_server_box/lib/data/model/app/bak/backup_service.dart
lollipopkit🏳️‍⚧️ a97b3cf43e opt.: bak pwd is optional (#872)
2025-08-31 11:11:47 +08:00

165 lines
5.4 KiB
Dart

import 'package:computer/computer.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
import 'package:server_box/data/model/app/bak/backup_source.dart';
import 'package:server_box/data/model/app/bak/utils.dart';
/// Service class for handling backup operations
class BackupService {
/// Perform backup operation with the given source
static Future<void> backup(BuildContext context, BackupSource source) async {
try {
final saved = await SecureStoreProps.bakPwd.read();
final password = saved?.isEmpty == true ? null : saved;
final path = await BackupV2.backup(null, password?.isEmpty == true ? null : password);
await source.saveContent(path);
if (source is ClipboardBackupSource) {
context.showSnackBar(libL10n.success);
}
} catch (e, s) {
context.showErrDialog(e, s, libL10n.backup);
}
}
/// Perform restore operation with the given source
static Future<void> restore(BuildContext context, BackupSource source) async {
final text = await source.getContent();
if (text == null) {
// Show empty message for clipboard source
if (source is ClipboardBackupSource) {
context.showSnackBar(libL10n.empty);
}
return;
}
await restoreFromText(context, text);
}
/// Handle restore from text with decryption support
static Future<void> restoreFromText(BuildContext context, String text) async {
// Check if backup is encrypted
final isEncrypted = Cryptor.isEncrypted(text);
String? password;
if (!isEncrypted) {
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text),
);
if (err != null || backup == null) return;
await _confirmAndRestore(context, backup);
} catch (e, s) {
Loggers.app.warning('Import backup failed', e, s);
context.showErrDialog(e, s, libL10n.restore);
}
return;
}
// Try with saved password first
final savedPassword = await SecureStoreProps.bakPwd.read();
if (savedPassword != null && savedPassword.isNotEmpty) {
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (
text,
savedPassword,
)),
);
if (err == null && backup != null) {
await _confirmAndRestore(context, backup);
return;
}
} catch (e) {
// Saved password failed, will prompt for manual input
}
}
// Prompt for password with retry logic
while (true) {
password = await _showPasswordDialog(context, title: libL10n.pwd, hint: l10n.backupEncrypted);
if (password == null) return; // User cancelled
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (
text,
password,
)),
);
if (err != null || backup == null) continue;
await _confirmAndRestore(context, backup);
return;
} catch (e) {
if (e.toString().contains('incorrect password') || e.toString().contains('Failed to decrypt')) {
final retry = await context.showRoundDialog<bool>(
title: l10n.backupPasswordWrong,
child: Text(l10n.backupPasswordWrong),
actions: [
TextButton(onPressed: () => context.pop(false), child: Text(libL10n.cancel)),
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.retry)),
],
);
if (retry != true) return;
continue; // Try again
} else {
// Other error, show and exit
context.showErrDialog(e, null, libL10n.restore);
return;
}
}
}
}
/// Confirm and execute restore operation
static Future<void> _confirmAndRestore(BuildContext context, (dynamic, String) backup) async {
await context.showRoundDialog(
title: libL10n.restore,
child: Text(libL10n.askContinue('${libL10n.restore} ${libL10n.backup}(${backup.$2})')),
actions: Btn.ok(
onTap: () async {
await backup.$1.merge(force: true);
context.pop();
},
).toList,
);
}
/// Show password input dialog
static Future<String?> _showPasswordDialog(
BuildContext context, {
String? initial,
String? title,
String? hint,
}) async {
final controller = TextEditingController(text: initial ?? '');
final result = await context.showRoundDialog<String>(
title: title ?? libL10n.pwd,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(hint ?? l10n.backupPasswordTip, style: UIs.textGrey),
UIs.height13,
Input(
label: l10n.backupPassword,
controller: controller,
obscureText: true,
onSubmitted: (_) => context.pop(controller.text),
),
],
),
actions: [
Btn.cancel(),
TextButton(onPressed: () => context.pop(controller.text), child: Text(libL10n.ok)),
],
);
controller.dispose();
return result;
}
}