import Foundation import os /// Makes sure every file inside a board package actually has bytes on this device before the loader /// is allowed to walk it. /// /// **Why this exists at all.** `BoardLoader` is fail-fast by design: an `index.md` it cannot read is /// a `BoardLoadError`, not a warning. On the Mac that is exactly right — an unreadable file is a real /// defect. On the phone it is routinely a file iCloud has simply not brought down yet, or has evicted /// to reclaim storage. Handing the loader a half-materialized package would turn ordinary sync /// latency into the decision surface's "this board is broken", which is the wrong sentence and the /// wrong recovery. So the sweep runs first, and a board that is not yet whole waits in a downloading /// state instead of failing. /// /// **A package's own metadata item is not enough to decide this.** `NSMetadataQuery` reports a /// download status for the `.kanban` item as a whole, but that aggregate has been unreliable for /// packages across releases and says nothing about *which* item is missing. The sweep asks each file /// directly, which is also what lets it request the downloads. enum PackageMaterialization { /// One sweep's answer. struct Progress: Sendable, Equatable { /// Items that are ubiquitous and not yet current. A download has been requested for each. var pending: Int /// Every item the walk saw, including directories and the package root. var total: Int /// The first download request that was refused, if any — best-effort observability. A refusal /// is not a failure of the sweep: the next sweep asks again, and the daemon usually answers /// the second time. var refusal: String? var isComplete: Bool { pending == 0 } /// 0…1 across the package, for a determinate progress view. `nil` where there is nothing to /// report on. var fractionMaterialized: Double? { guard total > 0 else { return nil } return Double(total - pending) / Double(total) } } private static let logger = Logger(subsystem: "dev.rzen.indie.KanbanMobile", category: "cloud") /// Walks the package, requests a download for every item that is not current, and answers what is /// still outstanding. /// /// Blocking (a full directory enumeration plus a resource-value read per item) — callers run it /// off the main actor. /// /// **Hidden entries are included, deliberately**: `/.trash/` is materialized trash the /// loader reads, so a package whose trash has not come down is not yet loadable. This is the one /// walk in the mobile layer that does *not* use the loader's `.skipsHiddenFiles` posture. /// /// Answers `Progress(pending: 0, total: 0)` for a package under `LANEWORK_LOCAL_ROOT`, where /// nothing is a ubiquitous item — "complete", which is the correct reading of a folder that is /// simply already there. nonisolated static func sweep(packageAt root: URL) -> Progress { var progress = Progress(pending: 0, total: 0, refusal: nil) func consider(_ url: URL) { progress.total += 1 guard !isCurrent(url) else { return } progress.pending += 1 do { try FileManager.default.startDownloadingUbiquitousItem(at: url) } catch { if progress.refusal == nil { progress.refusal = error.localizedDescription logger.warning("download request refused for \(url.lastPathComponent, privacy: .public): \(error.localizedDescription, privacy: .public)") } } } consider(root) guard let walk = FileManager.default.enumerator( at: root, includingPropertiesForKeys: [.isUbiquitousItemKey, .ubiquitousItemDownloadingStatusKey], options: [] ) else { return progress } for case let url as URL in walk { consider(url) } return progress } /// Whether one item has bytes here now. /// /// Two shapes are read as "not here". The modern one is a dataless file at its real path whose /// `ubiquitousItemDownloadingStatus` is `.notDownloaded`. The legacy one is a hidden `.icloud` /// placeholder standing where the file will land — still produced in some states, and invisible /// to a resource-value read on the *real* name because that name does not exist yet. Both are /// counted, and `startDownloadingUbiquitousItem` accepts either URL. /// /// A non-ubiquitous item (anything under the DEBUG local root, and any stray the daemon does not /// manage) is current by definition. private nonisolated static func isCurrent(_ url: URL) -> Bool { if url.pathExtension == "icloud", url.lastPathComponent.hasPrefix(".") { return false } guard let values = try? url.resourceValues(forKeys: [.isUbiquitousItemKey, .ubiquitousItemDownloadingStatusKey]), values.isUbiquitousItem == true else { return true } // `.downloaded` means "a local copy exists but a newer one may be in the cloud" — bytes are // here, which is the only question this walk asks. Only `.notDownloaded` blocks a load. return values.ubiquitousItemDownloadingStatus != .notDownloaded } }