this repo has no description
at main 7.1 kB view raw
1import Foundation 2 3#if canImport(UIKit) 4import UIKit 5#endif 6 7/// Metadata about a completed backup, saved to track backup history and device information 8struct BackupMetadata: Codable { 9 /// When the backup completed 10 let completedAt: Date 11 12 /// Start time of the backup 13 let startedAt: Date 14 15 /// Duration of the backup in seconds 16 var duration: TimeInterval { 17 completedAt.timeIntervalSince(startedAt) 18 } 19 20 // MARK: - Account Information 21 22 /// The account's DID (Decentralized Identifier) 23 let did: String 24 25 /// The account's handle (e.g., user.bsky.social) 26 let handle: String 27 28 /// PDS (Personal Data Server) URL used for this backup 29 let pds: String 30 31 // MARK: - Backup Information 32 33 /// Name of the .car file containing the repository snapshot 34 let carFileName: String 35 36 /// Total number of blobs in this backup 37 let totalBlobs: Int 38 39 /// Number of blobs that were newly downloaded (vs already existed) 40 let newBlobsDownloaded: Int 41 42 /// Device information 43 let deviceInfo: DeviceInfo 44 45 /// Format the duration as a human-readable string 46 var formattedDuration: String { 47 let formatter = DateComponentsFormatter() 48 formatter.allowedUnits = [.hour, .minute, .second] 49 formatter.unitsStyle = .abbreviated 50 formatter.zeroFormattingBehavior = .dropAll 51 return formatter.string(from: duration) ?? "\(Int(duration))s" 52 } 53 54 struct DeviceInfo: Codable { 55 /// Device model (e.g., "iPhone 15 Pro", "Mac") 56 let model: String 57 58 /// OS version (e.g., "iOS 18.0", "macOS 15.0") 59 let osVersion: String 60 61 /// App version 62 let appVersion: String 63 64 /// User's device name (e.g., "Corey's iPhone") 65 let deviceName: String 66 67 #if os(iOS) 68 /// Battery level at backup completion (0.0-1.0), iOS only 69 let batteryLevel: Float? 70 71 /// Whether device was charging during backup, iOS only 72 let wasCharging: Bool? 73 #endif 74 75 /// Unique device identifier (for tracking which device performed backup) 76 let deviceIdentifier: String 77 78 static func current() -> DeviceInfo { 79 #if os(iOS) 80 let device = UIDevice.current 81 let batteryMonitoringWasEnabled = device.isBatteryMonitoringEnabled 82 device.isBatteryMonitoringEnabled = true 83 84 let batteryLevel: Float? = device.batteryState != .unknown ? device.batteryLevel : nil 85 let wasCharging: Bool? = device.batteryState == .charging || device.batteryState == .full 86 87 // Restore battery monitoring state 88 device.isBatteryMonitoringEnabled = batteryMonitoringWasEnabled 89 90 let model = Self.deviceModelName() 91 let osVersion = "iOS \(device.systemVersion)" 92 let deviceName = device.name 93 #else 94 let model = Self.macModelName() 95 let osVersion = Self.macOSVersion() 96 let deviceName = Host.current().localizedName ?? "Mac" 97 let batteryLevel: Float? = nil 98 let wasCharging: Bool? = nil 99 #endif 100 101 let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" 102 103 #if os(iOS) 104 let deviceIdentifier = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString 105 106 return DeviceInfo( 107 model: model, 108 osVersion: osVersion, 109 appVersion: appVersion, 110 deviceName: deviceName, 111 batteryLevel: batteryLevel, 112 wasCharging: wasCharging, 113 deviceIdentifier: deviceIdentifier 114 ) 115 #else 116 // For macOS, generate a stable identifier based on hardware UUID 117 let deviceIdentifier = Self.macDeviceIdentifier() 118 119 return DeviceInfo( 120 model: model, 121 osVersion: osVersion, 122 appVersion: appVersion, 123 deviceName: deviceName, 124 deviceIdentifier: deviceIdentifier 125 ) 126 #endif 127 } 128 129 #if os(iOS) 130 private static func deviceModelName() -> String { 131 var systemInfo = utsname() 132 uname(&systemInfo) 133 let machineMirror = Mirror(reflecting: systemInfo.machine) 134 let identifier = machineMirror.children.reduce("") { identifier, element in 135 guard let value = element.value as? Int8, value != 0 else { return identifier } 136 return identifier + String(UnicodeScalar(UInt8(value))) 137 } 138 139 // Map common identifiers to friendly names 140 let modelMap: [String: String] = [ 141 "iPhone14,2": "iPhone 13 Pro", 142 "iPhone14,3": "iPhone 13 Pro Max", 143 "iPhone14,4": "iPhone 13 mini", 144 "iPhone14,5": "iPhone 13", 145 "iPhone15,2": "iPhone 14 Pro", 146 "iPhone15,3": "iPhone 14 Pro Max", 147 "iPhone15,4": "iPhone 15", 148 "iPhone15,5": "iPhone 15 Plus", 149 "iPhone16,1": "iPhone 15 Pro", 150 "iPhone16,2": "iPhone 15 Pro Max", 151 ] 152 153 return modelMap[identifier] ?? identifier 154 } 155 #else 156 private static func macModelName() -> String { 157 var size = 0 158 sysctlbyname("hw.model", nil, &size, nil, 0) 159 var model = [CChar](repeating: 0, count: size) 160 sysctlbyname("hw.model", &model, &size, nil, 0) 161 return String(cString: model) 162 } 163 164 private static func macOSVersion() -> String { 165 let version = ProcessInfo.processInfo.operatingSystemVersion 166 return "macOS \(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" 167 } 168 169 private static func macDeviceIdentifier() -> String { 170 // Try to get hardware UUID via IOKit 171 var size = 0 172 sysctlbyname("kern.uuid", nil, &size, nil, 0) 173 var uuid = [CChar](repeating: 0, count: size) 174 sysctlbyname("kern.uuid", &uuid, &size, nil, 0) 175 return String(cString: uuid) 176 } 177 #endif 178 } 179 180 /// Save metadata to a JSON file 181 func save(to directory: URL) throws { 182 let fileURL = directory.appendingPathComponent("backup-metadata.json") 183 let encoder = JSONEncoder() 184 encoder.dateEncodingStrategy = .iso8601 185 encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 186 187 let data = try encoder.encode(self) 188 try data.write(to: fileURL) 189 } 190 191 /// Load metadata from a JSON file 192 static func load(from directory: URL) throws -> BackupMetadata? { 193 let fileURL = directory.appendingPathComponent("backup-metadata.json") 194 195 guard FileManager.default.fileExists(atPath: fileURL.path) else { 196 return nil 197 } 198 199 let data = try Data(contentsOf: fileURL) 200 let decoder = JSONDecoder() 201 decoder.dateDecodingStrategy = .iso8601 202 203 return try decoder.decode(BackupMetadata.self, from: data) 204 } 205}