import Dependencies import DependenciesMacros import Foundation public extension DependencyValues { /// Represents interactions with the file system. /// var fileClient: FileClient { get { self[FileClient.self] } set { self[FileClient.self] = newValue } } } /// Represents interactions with the file system. /// /// @DependencyClient public struct FileClient: Sendable { /// Copy an item from one location to another. public var copy: @Sendable (URL, URL) async throws -> Void /// Create a directory at the given location. public var createDirectory: @Sendable (URL) async throws -> Void /// Delete the item at the given location. public var delete: @Sendable (URL) async throws -> Void /// Check if a file exists at the given location. public var fileExists: @Sendable (URL) -> Bool = { _ in true } /// Find an ansible-vault file in the given location, checking up to 1 level deep /// in subfolders. public var findVaultFile: @Sendable (URL) async throws -> URL? /// Return the user's home directory. public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") } /// Check if an item is a directory or not. public var isDirectory: @Sendable (URL) async throws -> Bool /// Load a file from the given location. public var load: @Sendable (URL) async throws -> Data /// Write data to a file at the given location. public var write: @Sendable (Data, URL) async throws -> Void /// Find an ansible-vault file in the current directory, checking up to 1 level /// deep in subfolders. public func findVaultFileInCurrentDirectory() async throws -> URL? { try await findVaultFile(URL(filePath: "./")) } } extension FileClient: DependencyKey { public static let testValue: FileClient = Self() public static var liveValue: Self { let manager = LiveFileClient() return .init( copy: { try await manager.copy($0, to: $1) }, createDirectory: { try await manager.creatDirectory($0) }, delete: { try await manager.delete($0) }, fileExists: { manager.fileExists(at: $0) }, findVaultFile: { try await manager.findVaultFile(in: $0) }, homeDirectory: { manager.homeDirectory() }, isDirectory: { manager.isDirectory($0) }, load: { try await manager.load(from: $0) }, write: { try await manager.write($0, to: $1) } ) } } struct LiveFileClient: Sendable { private var manager: FileManager { FileManager.default } func copy(_ url: URL, to toUrl: URL) async throws { try manager.copyItem(at: url, to: toUrl) } func creatDirectory(_ url: URL) async throws { try manager.createDirectory(at: url, withIntermediateDirectories: true) } func delete(_ url: URL) async throws { try manager.removeItem(at: url) } func fileExists(at url: URL) -> Bool { manager.fileExists(atPath: url.cleanFilePath) } func findVaultFile(in url: URL) async throws -> URL? { guard isDirectory(url) else { return nil } let urls = try manager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) guard let vault = urls.firstVaultFile else { // check subfolders, 1 layer deep. let subfolders = urls.filter { isDirectory($0) } for folder in subfolders { let vault = try manager.contentsOfDirectory( at: folder, includingPropertiesForKeys: nil ) .firstVaultFile if let vault { return vault } } // Didn't find a file. return nil } return vault } func homeDirectory() -> URL { manager.homeDirectoryForCurrentUser } func isDirectory(_ url: URL) -> Bool { var isDirectory: ObjCBool = false #if os(Linux) _ = manager.fileExists(atPath: url.cleanFilePath, isDirectory: &isDirectory) #else manager.fileExists(atPath: url.cleanFilePath, isDirectory: &isDirectory) #endif return isDirectory.boolValue } func load(from url: URL) async throws -> Data { try Data(contentsOf: url) } func write(_ data: Data, to url: URL) async throws { try data.write(to: url) } } private extension Array where Element == URL { var firstVaultFile: URL? { first { url in let string = url.absoluteString return string.hasSuffix("vault.yml") || string.hasSuffix("vault.yaml") } } } public extension URL { var cleanFilePath: String { absoluteString .replacing("file://", with: "") .replacing("/private", with: "") } }