import 'dart:io'; import 'auto_backup_store.dart'; import 'export_import_service.dart'; /// Makes a silent, encrypted safety copy of the inventory on a schedule — a net /// against in-app data loss or DB corruption, with zero friction. Triggered /// from the app lifecycle (see [AutoBackupGate]), it writes into the app's /// private storage; the manual "save a copy" stays the way to move one off the /// device. /// /// Copies are **sealed** (same [ExportImportService.buildSealedBackup] bytes as /// a manual backup): they reveal nothing at rest and open with the recovery /// code. The newest few are kept and older ones rotated out. class AutoBackupService { AutoBackupService({ required ExportImportService exporter, required AutoBackupStore store, required Future Function() directory, DateTime Function() now = DateTime.now, }) : _exporter = exporter, _store = store, _directory = directory, _now = now; final ExportImportService _exporter; final AutoBackupStore _store; final Future Function() _directory; final DateTime Function() _now; /// Filename prefix so rotation only ever touches automatic copies, never a /// backup the user saved by hand. static const _prefix = 'tanemaki-auto-'; /// How many automatic copies to keep. A rotating window (not a single file) /// so a corrupt copy can't overwrite the only backup. static const _keep = 3; /// When the last automatic copy was made, or null if none has run yet. For a /// reassurance line in Settings. Future lastBackupAt() => _store.lastBackupAt(); /// Makes a fresh copy when at least [interval] has passed since the last one. /// Returns true when it wrote a copy, false when not due (or on any failure — /// a backup must never crash startup or the move to the background). Future runIfDue({Duration interval = const Duration(days: 7)}) async { try { final last = await _store.lastBackupAt(); if (last != null && _now().difference(last) < interval) return false; } on Object { return false; } return backupNow(); } /// Makes a copy right now, ignoring the schedule — for an explicit "back up /// now" tap. Returns true on success, false on any failure (never throws). Future backupNow() async { try { final now = _now(); final dir = await _directory(); await dir.create(recursive: true); final sealed = await _exporter.buildSealedBackup(); final date = now.toIso8601String().substring(0, 10); final file = File('${dir.path}/$_prefix$date.tanemaki'); await file.writeAsBytes(sealed, flush: true); await _rotate(dir); await _store.markBackupAt(now); return true; } on Object { // Silent net: a failed copy is logged nowhere sensitive and never thrown. return false; } } /// Keeps the newest [_keep] automatic copies; deletes the rest. Filenames end /// in an ISO date, so a lexical sort is chronological. Future _rotate(Directory dir) async { final copies = (await dir.list().toList()) .whereType() .where((f) => f.uri.pathSegments.last.startsWith(_prefix)) .toList() ..sort((a, b) => a.path.compareTo(b.path)); final excess = copies.length - _keep; if (excess <= 0) return; for (final stale in copies.take(excess)) { await stale.delete(); } } }