Keep using Photos.app like you always do. Attic quietly backs up your originals and edits to an S3 bucket you control. One-way, append-only.
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Address code review findings

- Remove PowerAssertion.end() to eliminate double-endActivity risk
- Fix hardcoded "15 minutes" in timeout error message to use actual value
- Use guard let self in NWPathNetworkMonitor closure
- Cap stabilization delay to remaining timeout budget
- Document intentional divergence between isTransientUploadError and isTransient

+17 -10
+7 -1
Sources/AtticCore/BackupPipeline.swift
··· 233 233 } 234 234 235 235 /// Check if an upload error looks like a transient network issue. 236 + /// 237 + /// Intentionally a superset of `RetryPolicy.isTransient` — includes 238 + /// "nsurlerrordomain" and "cfnetwork" so the pipeline's network-pause 239 + /// logic catches errors that `withRetry` deliberately does not retry 240 + /// (avoiding 7s of backoff before the network monitor can take over). 236 241 private func isTransientUploadError(_ error: Error) -> Bool { 237 242 let message = String(describing: error).lowercased() 238 243 let patterns = [ ··· 329 334 } 330 335 } else { 331 336 // Network timeout — save manifest and stop 337 + let timeoutMinutes = Int(networkTimeout.components.seconds) / 60 332 338 report.appendError( 333 339 uuid: exported.uuid, 334 - message: "Network unavailable for 15 minutes, backup paused" 340 + message: "Network unavailable for \(timeoutMinutes) minutes, backup paused" 335 341 ) 336 342 report.failed += 1 337 343 if sinceLastSave > 0 {
+9 -4
Sources/AtticCore/NWPathNetworkMonitor.swift
··· 18 18 public init() { 19 19 monitor = NWPathMonitor() 20 20 monitor.pathUpdateHandler = { [weak self] path in 21 - self?.lock.withLock { 22 - self?.currentStatus = path.status 21 + guard let self else { return } 22 + self.lock.withLock { 23 + self.currentStatus = path.status 23 24 } 24 25 } 25 26 monitor.start(queue: queue) ··· 41 42 try Task.checkCancellation() 42 43 43 44 if isNetworkAvailable { 44 - // Wait for stabilization to avoid flicker 45 - try await Task.sleep(for: stabilizationDelay) 45 + // Wait for stabilization to avoid flicker, capped to remaining time 46 + let remaining = deadline - ContinuousClock.now 47 + let stabilize = min(stabilizationDelay, remaining) 48 + if stabilize > .zero { 49 + try await Task.sleep(for: stabilize) 50 + } 46 51 try Task.checkCancellation() 47 52 48 53 // Confirm network is still up after stabilization
+1 -5
Sources/AtticCore/PowerAssertion.swift
··· 18 18 ) 19 19 } 20 20 21 - public func end() { 22 - ProcessInfo.processInfo.endActivity(activity) 23 - } 24 - 25 21 deinit { 26 - end() 22 + ProcessInfo.processInfo.endActivity(activity) 27 23 } 28 24 }