import Dependencies import DependenciesMacros import Foundation @_spi(Internal) public extension DependencyValues { var fileClient: FileClient { get { self[FileClient.self] } set { self[FileClient.self] = newValue } } } @_spi(Internal) @DependencyClient public struct FileClient: Sendable { /// Loads a file at the given path into the environment unless it can decode it as json, /// at which point it will return the decoded file contents as a ``Configuration`` item. public var loadFile: @Sendable (URL, inout [String: String], JSONDecoder) throws -> Configuration? /// Returns the user's home directory path. public var homeDir: @Sendable () -> URL = { URL(string: "~/")! } /// Check if a path is a directory. public var isDirectory: @Sendable (URL) -> Bool = { _ in false } /// Check if a path is a readable. public var isReadable: @Sendable (URL) -> Bool = { _ in false } /// Check if a file exists at the path. public var fileExists: @Sendable (String) -> Bool = { _ in false } public var findVaultFile: @Sendable (String) throws -> URL? /// Write data to a file. public var write: @Sendable (String, Data) throws -> Void public func findVaultFileInCurrentDirectory() throws -> URL? { try findVaultFile(".") } } @_spi(Internal) extension FileClient: DependencyKey { public static let testValue: FileClient = Self() public static func live(fileManager: FileManager = .default) -> Self { let client = LiveFileClient(fileManager: fileManager) return Self( loadFile: { try client.loadFile(at: $0, into: &$1, decoder: $2) }, homeDir: { client.homeDir }, isDirectory: { client.isDirectory(url: $0) }, isReadable: { client.isReadable(url: $0) }, fileExists: { client.fileExists(at: $0) }, findVaultFile: { try client.findVaultFile(in: $0) }, write: { path, data in try data.write(to: URL(filePath: path)) } ) } public static let liveValue = Self.live() } private struct LiveFileClient: @unchecked Sendable { private let fileManager: FileManager init(fileManager: FileManager) { self.fileManager = fileManager } var homeDir: URL { fileManager.homeDirectoryForCurrentUser } func isDirectory(url: URL) -> Bool { var isDirectory: ObjCBool = false fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory) return isDirectory.boolValue } func isReadable(url: URL) -> Bool { fileManager.isReadableFile(atPath: path(for: url)) } func fileExists(at path: String) -> Bool { fileManager.fileExists(atPath: path) } // func findVaultFileInCurrentDirectory() throws -> URL? { func findVaultFile(in filePath: String) throws -> URL? { let urls = try fileManager .contentsOfDirectory(at: URL(filePath: filePath), includingPropertiesForKeys: nil) if let vault = urls.firstVaultFile { return vault } // check for folders that end with "vars" and search those next. for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) { let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) if let vault = files.firstVaultFile { return vault } } // Fallback to check all sub-folders for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) { let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) if let vault = files.firstVaultFile { return vault } } return nil } func loadFile( at url: URL, into env: inout [String: String], decoder: JSONDecoder ) throws -> Configuration? { @Dependency(\.logger) var logger logger.trace("Begin load file for: \(path(for: url))") if url.absoluteString.hasSuffix(".json") { // Handle json file. let data = try Data(contentsOf: url) return try decoder.decode(Configuration.self, from: data) } let string = try String(contentsOfFile: path(for: url), encoding: .utf8) logger.trace("Loaded file contents: \(string)") let lines = string.split(separator: "\n") for line in lines { logger.trace("Line: \(line)") let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) let splitLine = strippedLine.split(separator: "=").map { $0.replacingOccurrences(of: "\"", with: "") } logger.trace("Split Line: \(splitLine)") guard splitLine.count >= 2 else { continue } if splitLine.count > 2 { let rest = splitLine.dropFirst() env[String(splitLine[0])] = String(rest.joined(separator: "=")) } else { env[String(splitLine[0])] = String(splitLine[1]) } } return nil } } private extension Array where Element == URL { var firstVaultFile: URL? { first { $0.absoluteString.hasSuffix("vault.yml") } } }