// // AppBackupConfiguration.swift // Workouts // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import Foundation import IndieBackup /// Tells IndieBackup what to snapshot and how to bring the app back to a /// consistent state after a restore. /// /// `backupRoot` is the ubiquity container's `Documents/` directory — the exact /// tree `SyncEngine`'s `DocumentFileStore` writes to: `Splits/`, `Workouts/`, /// and the `Stubs/` soft-delete tombstones. Backing up that whole tree, /// tombstones included, is deliberate — a restore that dropped the stubs could /// resurrect records the user had already deleted. The SwiftData store is never /// in the backup: it's a rebuildable cache, regenerated from the restored files /// by `rebuildCacheAfterRestore`. /// /// This type is nonisolated on purpose — the package reads `backupRoot` /// synchronously off the main actor during its detached directory mirror. It /// holds the `@MainActor` `SyncEngine`, which is safe because a global-actor /// class is implicitly `Sendable`, and the three restore hooks only ever reach /// it via `await`. final class AppBackupConfiguration: BackupConfiguration { private let syncEngine: SyncEngine init(syncEngine: SyncEngine) { self.syncEngine = syncEngine } var backupFileExtension: String { "workoutsbackup" } var backupDisplayName: String { "Workouts Backup" } /// The container's `Documents/` folder. Resolved directly from `FileManager` /// with the app's explicit ubiquity container id so this stays synchronous and /// nonisolated; it yields the identical URL `SyncEngine` exposes as /// `containerDocumentsURL` once connected. Backup and restore are only /// reachable past the iCloud gate, so the container is always resolved by the /// time this is read — the package's local `documentsURL` fallback is only a /// belt-and-suspenders never-nil guarantee. var backupRoot: URL { if let container = FileManager.default.url( forUbiquityContainerIdentifier: SyncEngine.containerIdentifier ) { let docs = container.appendingPathComponent("Documents", isDirectory: true) try? FileManager.default.createDirectory(at: docs, withIntermediateDirectories: true) return docs } return iCloudDocumentManager.shared.documentsURL } /// After the restore has replaced the document tree on disk, rebuild the /// SwiftData cache from those files. func rebuildCacheAfterRestore(progress: @escaping (Double, String) -> Void) async throws { progress(0.0, "Rebuilding your library…") await syncEngine.rebuildCache() progress(1.0, "Library rebuilt") } /// Stop the metadata observer before the restore touches disk, so the bulk /// file mirror isn't observed as a storm of live edits. func prepareForRestore() async { await syncEngine.beginRestore() } /// Resume the metadata observer (fresh, re-baselined) after the restore. func finishRestore() async { await syncEngine.endRestore() } }