Add local backup/restore: encrypted .enc export to Download/loginvault, restore from list (no file_picker to avoid SDK conflict)
This commit is contained in:
94
lib/backup_service.dart
Normal file
94
lib/backup_service.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:login_vault_app/database_helper.dart';
|
||||
import 'package:login_vault_app/encryptor.dart';
|
||||
import 'package:login_vault_app/auth_service.dart';
|
||||
import 'package:login_vault_app/constants.dart';
|
||||
|
||||
/// Backup & restore the local vault to a file on device storage.
|
||||
/// The file is encrypted with the app password (not the constant key), so it
|
||||
/// stays safe if copied to Drive / shared. User can later move the .enc file
|
||||
/// to Google Drive manually.
|
||||
class BackupService {
|
||||
/// Export all vault entries into an encrypted JSON file.
|
||||
/// Returns the saved file path, or throws on error.
|
||||
static Future<String> exportEncrypted(AuthService auth) async {
|
||||
final pw = await auth.getAppPassword();
|
||||
if (pw == null) throw Exception('App password belum diset');
|
||||
final all = await DatabaseHelper().getAll();
|
||||
final plain = jsonEncode({
|
||||
'app': 'login_vault',
|
||||
'version': 1,
|
||||
'entries': all.map((e) => {'id': e.id, 'content': e.content}).toList(),
|
||||
});
|
||||
// encrypt with app password-derived key
|
||||
final enc = Encryptor.encryptWithPassword(plain, pw);
|
||||
final dir = await _targetDir();
|
||||
final ts = DateTime.now().toIso8601String().replaceAll(':', '-').split('.')[0];
|
||||
final file = File('${dir.path}/loginvault_backup_$ts.enc');
|
||||
await file.writeAsString(enc);
|
||||
return file.path;
|
||||
}
|
||||
|
||||
/// Import an encrypted backup file into the DB (merge by id, overwrite).
|
||||
/// [path] is the file the user picked (or default backup file).
|
||||
static Future<int> importEncrypted(AuthService auth, String path) async {
|
||||
final pw = await auth.getAppPassword();
|
||||
if (pw == null) throw Exception('App password belum diset');
|
||||
final file = File(path);
|
||||
if (!await file.exists()) throw Exception('File backup tidak ditemukan');
|
||||
final enc = await file.readAsString();
|
||||
final plain = Encryptor.decryptWithPassword(enc, pw);
|
||||
final data = jsonDecode(plain) as Map<String, dynamic>;
|
||||
final entries = (data['entries'] as List).cast<Map<String, dynamic>>();
|
||||
final db = DatabaseHelper();
|
||||
int count = 0;
|
||||
for (final e in entries) {
|
||||
final id = e['id'] as String;
|
||||
final content = e['content'] as String;
|
||||
// insert or update
|
||||
try {
|
||||
await db.insert(VaultEntry(id: id, content: content));
|
||||
} catch (_) {
|
||||
await db.update(id, content);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Default backup directory:
|
||||
/// - Android: Download/loginvault (so user can move to Drive)
|
||||
/// - others: app documents dir
|
||||
static Future<Directory> _targetDir() async {
|
||||
if (Platform.isAndroid) {
|
||||
// request storage permission for Android < 33 (scoped)
|
||||
if (await Permission.storage.request().isGranted ||
|
||||
await Permission.manageExternalStorage.request().isGranted) {
|
||||
final download = Directory('/storage/emulated/0/Download/loginvault');
|
||||
if (!await download.exists()) await download.create(recursive: true);
|
||||
return download;
|
||||
}
|
||||
}
|
||||
final docs = await getExternalStorageDirectory();
|
||||
final dir = Directory('${docs?.path ?? (await getApplicationDocumentsDirectory()).path}/loginvault');
|
||||
if (!await dir.exists()) await dir.create(recursive: true);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// List existing backup files in the default folder.
|
||||
static Future<List<FileSystemEntity>> listBackups() async {
|
||||
try {
|
||||
final dir = await _targetDir();
|
||||
return dir
|
||||
.listSync()
|
||||
.where((f) => f.path.endsWith('.enc'))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user