import Foundation enum FileSystem { fileprivate static var fm: FileManager { .default } static func folders(in folder: URL) throws -> [URL] { try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey]) .filter { $0.isDirectory } } /** Copy a file to the destination, creating the containing folder if needed */ static func copy(_ source: URL, to destination: URL) throws { try destination.ensureParentFolderExistence() try source.copy(to: destination) } } extension URL { func ensureParentFolderExistence() throws { try deletingLastPathComponent().ensureFolderExistence() } func ensureFolderExistence() throws { if !exists { print("Creating directory \(path)") try wrap(.failedToWriteFile(path)) { try FileManager.default.createDirectory(at: self, withIntermediateDirectories: true) } } } var isDirectory: Bool { (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true } var exists: Bool { FileSystem.fm.fileExists(atPath: path) } func delete() throws { try FileSystem.fm.removeItem(at: self) } func copy(to url: URL) throws { try wrap(.failedToWriteFile(url.path)) { if url.exists { try url.delete() } try url.ensureParentFolderExistence() try FileSystem.fm.copyItem(at: self, to: url) } } } extension Data { func createFolderAndWrite(to url: URL) throws { try url.ensureParentFolderExistence() // if url.exists { // print("Overwriting \(url.path)") // } try wrap(.failedToWriteFile(url.path)) { try write(to: url) } } } extension String { func createFolderAndWrite(to url: URL) throws { try data(using: .utf8)!.createFolderAndWrite(to: url) } } extension Encodable { func writeJSON(to url: URL) throws { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let content = try wrap(.failedToEncodeJSON(url.path)) { try encoder.encode(self) } try content.createFolderAndWrite(to: url) } } extension Decodable { init(decodeFrom url: URL) throws { let data = try wrap(.failedToOpenFile(url.path)) { try Data(contentsOf: url) } self = try wrap({ .failedToDecodeJSON(file: url.path, error: $0.localizedDescription)}) { try JSONDecoder().decode(Self.self, from: data) } } }