TeraLocalBackupHost.swift (4915B)
1 import Foundation 2 import RadrootsKit 3 import TeraKitBindings 4 import UIKit 5 6 /// Explicit local backup only. Credential export and restore are separate ports. 7 actor TeraLocalBackupHost: TeraBackupHost { 8 let roots: RadrootsAppleFileRoots 9 let publicKey: String 10 let generation: String 11 let protectedData: @Sendable () async -> Bool 12 13 init(roots: RadrootsAppleFileRoots, publicKey: String, generation: String, 14 protectedData: @escaping @Sendable () async -> Bool = currentProtectedData) throws 15 { 16 self.roots = try TeraDurableMediaRoots.selectingStaging(in: roots) 17 self.publicKey = publicKey 18 self.generation = generation 19 self.protectedData = protectedData 20 } 21 22 /// The complete FFI call retains admission, including every callback and any 23 /// canceled callback still finishing. Persistent markers also guard restart. 24 func capture(runtime: TeraRuntime, request: FfiBackupRequest) async throws -> FfiBackupManifest { 25 try await admit(request) 26 let use = try reserve() 27 defer { withExtendedLifetime(use) {} } 28 return try await runtime.captureApplicationBackup(request: request, host: self) 29 } 30 31 func prepare(request: FfiBackupRequest) async throws { 32 try await admit(request) 33 let use = try reserve() 34 defer { withExtendedLifetime(use) {} } 35 do { try files.prepare() } catch { throw Self.failure("backup_unavailable") } 36 } 37 38 func loadCandidate(request: FfiBackupRequest) async throws -> Data? { 39 try await admit(request) 40 let use = try reserve() 41 defer { withExtendedLifetime(use) {} } 42 do { 43 return try files.read(id: request.backupId) 44 } catch { throw Self.failure("backup_verification_failed") } 45 } 46 47 func retainMedia(request: FfiBackupRequest, media: [FfiBackupMedia]) async throws -> [FfiBackupMedia] { 48 try await admit(request) 49 let use = try reserve() 50 defer { withExtendedLifetime(use) {} } 51 try retain(request: request, media: media) 52 return media 53 } 54 55 func persistCandidate(manifest: FfiBackupManifest) async throws { 56 try await admit(manifest.request) 57 let use = try reserve() 58 defer { withExtendedLifetime(use) {} } 59 do { 60 try retain(request: manifest.request, media: manifest.media) 61 try files.install(manifest.manifest, id: manifest.request.backupId, completed: false) 62 } catch { throw Self.failure("backup_publication_incomplete") } 63 } 64 65 func publishComplete(manifest: FfiBackupManifest) async throws { 66 try await admit(manifest.request) 67 let use = try reserve() 68 defer { withExtendedLifetime(use) {} } 69 do { 70 try retain(request: manifest.request, media: manifest.media) 71 guard try files.read(id: manifest.request.backupId) == manifest.manifest else { 72 throw Self.failure("backup_conflict") 73 } 74 try files.install(manifest.manifest, id: manifest.request.backupId, completed: true) 75 } catch { throw Self.failure("backup_publication_incomplete") } 76 } 77 78 private var files: TeraBackupFiles { 79 TeraBackupFiles(roots: roots, publicKey: publicKey) 80 } 81 82 private func reserve() throws -> RadrootsFileUseReservation { 83 do { 84 return try TeraMediaProcessUse.admit(root: roots.dataRoot) 85 } catch { throw Self.failure("backup_unavailable") } 86 } 87 88 private func admit(_ request: FfiBackupRequest) async throws { 89 try validateApplicationBackupRequest(request: request) 90 guard request.publicKey == publicKey else { throw Self.failure("backup_identity_mismatch") } 91 guard request.sourceGeneration == generation else { throw Self.failure("backup_generation_mismatch") } 92 guard await protectedData() else { throw Self.failure("backup_unavailable") } 93 guard !Task.isCancelled else { throw Self.failure("backup_publication_incomplete") } 94 } 95 96 private func retain(request: FfiBackupRequest, media: [FfiBackupMedia]) throws { 97 try validateApplicationBackupMedia(request: request, media: media) 98 do { 99 let access = try files.mediaAccess() 100 for item in media { 101 try Task.checkCancellation() 102 guard let size = Int(exactly: item.byteLength) else { throw Self.failure("backup_media_unavailable") } 103 let blob = try RadrootsStagedBlobReference(blobID: item.sha256, sizeBytes: size) 104 _ = try access.leaseStagedBlob(blob, expectedSHA256: item.sha256, identifier: item.leaseIdentifier) 105 } 106 } catch { throw Self.failure("backup_media_unavailable") } 107 } 108 109 nonisolated static func failure(_ code: String) -> TeraAppError { 110 .Failure(report: TeraErrorRecord(schemaVersion: 1, code: code, category: "backup", retryable: false, 111 recoveryActions: ["inspect_local_stores"], operationId: nil, capabilityId: nil, 112 safeMessage: "The local backup could not be completed.")) 113 } 114 115 private static func currentProtectedData() async -> Bool { 116 await MainActor.run { UIApplication.shared.isProtectedDataAvailable } 117 } 118 }