import Foundation extension Disk { /// Create and returns a URL constructed from specified directory/path static func createURL(for path: String?, in directory: Directory) throws -> URL { let filePrefix = "file://" var validPath: String? if let path = path { do { validPath = try getValidFilePath(from: path) } catch { throw error } } var searchPathDirectory: FileManager.SearchPathDirectory switch directory { case .documents: searchPathDirectory = .documentDirectory case .caches: searchPathDirectory = .cachesDirectory case .applicationSupport: searchPathDirectory = .applicationSupportDirectory case .temporary: if var url = URL(string: NSTemporaryDirectory()) { if let validPath = validPath { url = url.appendingPathComponent(validPath, isDirectory: false) } if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix { let fixedUrlString = filePrefix + url.absoluteString url = URL(string: fixedUrlString)! } return url } else { throw createError( .couldNotAccessTemporaryDirectory, description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")", failureReason: "Could not get access to the application's temporary directory.", recoverySuggestion: "Use a different directory." ) } case let .sharedContainer(appGroupName): if var url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) { if let validPath = validPath { url = url.appendingPathComponent(validPath, isDirectory: false) } if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix { let fixedUrl = filePrefix + url.absoluteString url = URL(string: fixedUrl)! } return url } else { throw createError( .couldNotAccessSharedContainer, description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")", failureReason: "Could not get access to shared container with app group named \(appGroupName).", recoverySuggestion: "Check that the app-group name in the entitlement matches the string provided." ) } } if var url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first { if let validPath = validPath { url = url.appendingPathComponent(validPath, isDirectory: false) } if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix { let fixedUrlString = filePrefix + url.absoluteString url = URL(string: fixedUrlString)! } return url } else { throw createError( .couldNotAccessUserDomainMask, description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")", failureReason: "Could not get access to the file system's user domain mask.", recoverySuggestion: "Use a different directory." ) } } /// Find an existing file's URL or throw an error if it doesn't exist static func getExistingFileURL(for path: String?, in directory: Directory) throws -> URL { do { let url = try createURL(for: path, in: directory) if FileManager.default.fileExists(atPath: url.path) { return url } throw createError( .noFileFound, description: "Could not find an existing file or folder at \(url.path).", failureReason: "There is no existing file or folder at \(url.path)", recoverySuggestion: "Check if a file or folder exists before trying to commit an operation on it." ) } catch { throw error } } /// Convert a user generated name to a valid file name static func getValidFilePath(from originalString: String) throws -> String { var invalidCharacters = CharacterSet(charactersIn: ":") invalidCharacters.formUnion(.newlines) invalidCharacters.formUnion(.illegalCharacters) invalidCharacters.formUnion(.controlCharacters) let pathWithoutIllegalCharacters = originalString .components(separatedBy: invalidCharacters) .joined(separator: "") let validFileName = removeSlashesAtBeginning(of: pathWithoutIllegalCharacters) guard !validFileName.isEmpty, validFileName != "." else { throw createError( .invalidFileName, description: "\(originalString) is an invalid file name.", failureReason: "Cannot write/read a file with the name \(originalString) on disk.", recoverySuggestion: "Use another file name with alphanumeric characters." ) } return validFileName } /// Helper method for getValidFilePath(from:) to remove all "/" at the beginning of a String static func removeSlashesAtBeginning(of string: String) -> String { var string = string if string.prefix(1) == "/" { string.remove(at: string.startIndex) } if string.prefix(1) == "/" { string = removeSlashesAtBeginning(of: string) } return string } /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for path: String?, in directory: Directory) throws { do { let url = try getExistingFileURL(for: path, in: directory) var resourceUrl = url var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = isExcludedFromBackup try resourceUrl.setResourceValues(resourceValues) } catch { throw error } } /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for url: URL) throws { do { var resourceUrl = url var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = isExcludedFromBackup try resourceUrl.setResourceValues(resourceValues) } catch { throw error } } /// Create necessary sub folders before creating a file static func createSubfoldersBeforeCreatingFile(at url: URL) throws { do { let subfolderUrl = url.deletingLastPathComponent() var subfolderExists = false var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: subfolderUrl.path, isDirectory: &isDirectory) { if isDirectory.boolValue { subfolderExists = true } } if !subfolderExists { try FileManager.default.createDirectory(at: subfolderUrl, withIntermediateDirectories: true, attributes: nil) } } catch { throw error } } /// Get Int from a file name static func fileNameInt(_ url: URL) -> Int? { let fileExtension = url.pathExtension let filePath = url.lastPathComponent let fileName = filePath.replacingOccurrences(of: fileExtension, with: "") return Int(String(fileName.filter { "0123456789".contains($0) })) } }