Files
receipity/lib/services/backup_service.dart
T

162 lines
4.9 KiB
Dart

/// Build, share, and restore a Receipity JSON backup.
library;
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'backup_format.dart';
import 'product_repository.dart';
class BackupService {
BackupService(this.repository);
final ProductRepository repository;
Future<BackupDocument> buildBackup() async {
final products = await repository.dumpTable('products');
final shops = await repository.dumpTable('shops');
final images = <String, String>{};
Future<void> addImage(String? path) async {
final key = backupImageKey(path);
if (key == null || images.containsKey(key)) return;
final file = File(path!);
if (!file.existsSync()) return;
images[key] = base64Encode(await file.readAsBytes());
}
final rewrittenProducts = <Map<String, Object?>>[];
for (final row in products) {
final copy = Map<String, Object?>.from(row);
final path = copy['image_path'] as String?;
await addImage(path);
copy['image_path'] = backupImageKey(path);
rewrittenProducts.add(copy);
}
final rewrittenShops = <Map<String, Object?>>[];
for (final row in shops) {
final copy = Map<String, Object?>.from(row);
final path = copy['receipt_image_path'] as String?;
await addImage(path);
copy['receipt_image_path'] = backupImageKey(path);
rewrittenShops.add(copy);
}
return BackupDocument(
version: kBackupVersion,
exportedAt: DateTime.now().toIso8601String(),
darkMode: await repository.getDarkMode(),
supermarkets: await repository.dumpTable('supermarkets'),
products: rewrittenProducts,
shops: rewrittenShops,
shopItems: await repository.dumpTable('shop_items'),
images: images,
);
}
Future<bool> exportBackup() async {
final document = await buildBackup();
final bytes = Uint8List.fromList(utf8.encode(document.encode()));
final stamp = DateTime.now().toIso8601String().split('T').first;
final fileName = 'receipity-backup-$stamp.json';
try {
final saved = await FilePicker.saveFile(
dialogTitle: 'Save Receipity backup',
fileName: fileName,
bytes: bytes,
mimeType: 'application/json',
type: FileType.custom,
allowedExtensions: const ['json'],
);
return saved != null;
} catch (_) {
// Fall through to the share sheet if save-as is unavailable.
}
final dir = await getTemporaryDirectory();
final file = File(p.join(dir.path, fileName));
await file.writeAsBytes(bytes, flush: true);
final result = await SharePlus.instance.share(
ShareParams(
files: [XFile(file.path, mimeType: 'application/json')],
subject: 'Receipity backup',
text: 'Receipity backup $stamp',
),
);
return result.status != ShareResultStatus.dismissed;
}
Future<bool> importBackup() async {
final picked = await FilePicker.pickFile(
dialogTitle: 'Import Receipity backup',
type: FileType.custom,
allowedExtensions: const ['json'],
);
if (picked == null) return false;
final bytes = await picked.readAsBytes();
await restoreBackup(utf8.decode(bytes));
return true;
}
Future<void> restoreBackup(String json) async {
final document = BackupDocument.parse(json);
await repository.clearImageFolders();
final products = <Map<String, Object?>>[];
final now = DateTime.now().toIso8601String();
for (final row in document.products) {
final copy = Map<String, Object?>.from(row);
copy['image_path'] = await _restoreImage(copy['image_path'], document.images);
copy['created_at'] ??= now;
copy['updated_at'] ??= copy['created_at'];
if (copy['normalized_name'] == null && copy['name'] is String) {
copy['normalized_name'] = ProductRepository.normalizeName(
copy['name']! as String,
);
}
products.add(copy);
}
final shops = <Map<String, Object?>>[];
for (final row in document.shops) {
final copy = Map<String, Object?>.from(row);
copy['receipt_image_path'] = await _restoreImage(
copy['receipt_image_path'],
document.images,
);
copy['created_at'] ??= now;
shops.add(copy);
}
await repository.replaceAllData(
supermarkets: document.supermarkets,
products: products,
shops: shops,
shopItems: document.shopItems,
darkMode: document.darkMode,
);
}
Future<String?> _restoreImage(
Object? key,
Map<String, String> images,
) async {
if (key is! String || key.isEmpty) return null;
final encoded = images[key];
if (encoded == null) return null;
try {
return await repository.writeImageBytes(key, base64Decode(encoded));
} catch (_) {
return null;
}
}
}