field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraBackupFiles.swift (3892B)


      1 import Foundation
      2 import RadrootsKit
      3 import TeraKitBindings
      4 
      5 /// Uses the existing durable file owner. It never reads or copies live DB files.
      6 struct TeraBackupFiles {
      7   let roots: RadrootsAppleFileRoots
      8   let publicKey: String
      9 
     10   func prepare() throws {
     11     // The caller has validated the binding and observed protected data. Marker
     12     // installation creates directories through the no-follow durable owner.
     13     for directory in ["sqlite", "manifests", "staged_blob_leases"] {
     14       try prepare(directory: directory)
     15     }
     16   }
     17 
     18   /// Metadata belongs on the admitted directory before the owner installs
     19   /// read-only leases. Original media remains in the canonical staging root.
     20   func mediaAccess() throws -> RadrootsAppleFileAccess {
     21     try prepare(directory: "staged_blob_leases")
     22     return try RadrootsAppleFileAccess(roots: RadrootsAppleFileRoots(
     23       appIdentifier: roots.appIdentifier, dataRoot: accountDirectory,
     24       cacheRoot: roots.cacheRoot, temporaryRoot: roots.temporaryRoot,
     25       stagedBlobsRoot: roots.stagedBlobsRoot
     26     ))
     27   }
     28 
     29   func read(id: String, completed: Bool = false) throws -> Data? {
     30     let relative = "backups/\(publicKey)/manifests/\(name(id: id, completed: completed))"
     31     do {
     32       guard case let .inline(data) = try RadrootsAppleFileAccess(roots: roots).read(
     33         RadrootsFileReference(scope: .data, relativePath: relative),
     34         mode: .inline(maxBytes: Int(applicationBackupLimits().manifestBytes))
     35       ) else { throw TeraLocalBackupHost.failure("backup_verification_failed") }
     36       return data
     37     } catch RadrootsAppleFileError.notFound {
     38       return nil
     39     }
     40   }
     41 
     42   func install(_ bytes: Data, id: String, completed: Bool) throws {
     43     guard !bytes.isEmpty, bytes.count <= applicationBackupLimits().manifestBytes else {
     44       throw TeraLocalBackupHost.failure("backup_capacity_exceeded")
     45     }
     46     // Opaque IDs are create-only: different bytes cannot replace prior evidence.
     47     try access(directory: "manifests").installStagedBlob(bytes, reference: RadrootsStagedBlobReference(
     48       blobID: name(id: id, completed: completed), sizeBytes: bytes.count
     49     ))
     50   }
     51 
     52   /// Applies local custody policy only to exact objects just admitted by the
     53   /// file owner. This never walks or recursively repairs unrelated paths.
     54   static func protect(_ admittedURL: URL) throws {
     55     let attributes = try admittedURL.resourceValues(forKeys: [.isSymbolicLinkKey])
     56     guard attributes.isSymbolicLink == false else { throw RadrootsAppleFileError.invalidRequest }
     57     var url = admittedURL
     58     var values = URLResourceValues()
     59     values.isExcludedFromBackup = true
     60     try url.setResourceValues(values)
     61     #if os(iOS)
     62     try FileManager.default.setAttributes(
     63       [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: url.path
     64     )
     65     #endif
     66   }
     67 
     68   private var accountDirectory: URL {
     69     roots.dataRoot.appendingPathComponent("backups", isDirectory: true)
     70       .appendingPathComponent(publicKey, isDirectory: true)
     71   }
     72 
     73   private func prepare(directory: String) throws {
     74     let access = try access(directory: directory)
     75     let marker = Data("tera.local.backup.v1\n".utf8)
     76     try access.installStagedBlob(marker, reference: RadrootsStagedBlobReference(blobID: "owner_v1", sizeBytes: marker.count))
     77     try Self.protect(accountDirectory)
     78     try Self.protect(access.roots.stagedBlobsRoot)
     79   }
     80 
     81   private func access(directory: String) throws -> RadrootsAppleFileAccess {
     82     try RadrootsAppleFileAccess(roots: RadrootsAppleFileRoots(
     83       appIdentifier: roots.appIdentifier, dataRoot: roots.dataRoot,
     84       cacheRoot: roots.cacheRoot, temporaryRoot: roots.temporaryRoot,
     85       stagedBlobsRoot: accountDirectory.appendingPathComponent(directory, isDirectory: true)
     86     ))
     87   }
     88 
     89   private func name(id: String, completed: Bool) -> String {
     90     "\(id)_\(completed ? "complete" : "candidate")"
     91   }
     92 }