import Foundation /// The only type in the app that knows the on-disk directory layout. public struct AppSupportPaths: Sendable { public let root: URL public let spool: URL public let archive: URL public let outbox: URL public let watchFolder: URL /// Production paths. public static func standard() throws -> AppSupportPaths { let fileManager = FileManager.default let desktop = try fileManager.url( for: .desktopDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) let downloads = try fileManager.url( for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) let root = try standardRoot(fileManager: fileManager) return try AppSupportPaths(root: root, outbox: desktop, watchFolder: downloads) } /// The standard `~/Library/Application Support/Shotdeck` root. Shared by /// `standard()`, `FolderSettings.resolvedAppSupportPaths()`, and /// `TransportSettings.resolvedAppSupportPaths()` so all three agree on where the /// root lives — the folder-resolution logic (AirDrop-only vs transport-aware) /// differs between those, the root computation never should. public static func standardRoot(fileManager: FileManager = .default) throws -> URL { let appSupportParent = try fileManager.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) return appSupportParent.appendingPathComponent("Shotdeck", isDirectory: true) } /// Test paths rooted anywhere. Every directory is created if missing. public init(root: URL, outbox: URL, watchFolder: URL) throws { self.root = root self.spool = root.appendingPathComponent("spool", isDirectory: true) self.archive = root.appendingPathComponent("archive", isDirectory: true) self.outbox = outbox self.watchFolder = watchFolder try Self.createDirectory(self.root) try Self.createDirectory(self.spool) try Self.createDirectory(self.archive) try Self.createDirectory(self.outbox) try Self.createDirectory(self.watchFolder) } public func sessionDirectory(_ id: UUID) -> URL { spool.appendingPathComponent(id.uuidString, isDirectory: true) } public func archiveDirectory(_ id: UUID) -> URL { archive.appendingPathComponent(id.uuidString, isDirectory: true) } private static func createDirectory(_ url: URL) throws { try FileManager.default.createDirectory( at: url, withIntermediateDirectories: true ) } }