Disk+InternalHelpers.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import Foundation
  2. extension Disk {
  3. /// Create and returns a URL constructed from specified directory/path
  4. static func createURL(for path: String?, in directory: Directory) throws -> URL {
  5. let filePrefix = "file://"
  6. var validPath: String?
  7. if let path = path {
  8. do {
  9. validPath = try getValidFilePath(from: path)
  10. } catch {
  11. throw error
  12. }
  13. }
  14. var searchPathDirectory: FileManager.SearchPathDirectory
  15. switch directory {
  16. case .documents:
  17. searchPathDirectory = .documentDirectory
  18. case .caches:
  19. searchPathDirectory = .cachesDirectory
  20. case .applicationSupport:
  21. searchPathDirectory = .applicationSupportDirectory
  22. case .temporary:
  23. if var url = URL(string: NSTemporaryDirectory()) {
  24. if let validPath = validPath {
  25. url = url.appendingPathComponent(validPath, isDirectory: false)
  26. }
  27. if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix {
  28. let fixedUrlString = filePrefix + url.absoluteString
  29. url = URL(string: fixedUrlString)!
  30. }
  31. return url
  32. } else {
  33. throw createError(
  34. .couldNotAccessTemporaryDirectory,
  35. description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")",
  36. failureReason: "Could not get access to the application's temporary directory.",
  37. recoverySuggestion: "Use a different directory."
  38. )
  39. }
  40. case let .sharedContainer(appGroupName):
  41. if var url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) {
  42. if let validPath = validPath {
  43. url = url.appendingPathComponent(validPath, isDirectory: false)
  44. }
  45. if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix {
  46. let fixedUrl = filePrefix + url.absoluteString
  47. url = URL(string: fixedUrl)!
  48. }
  49. return url
  50. } else {
  51. throw createError(
  52. .couldNotAccessSharedContainer,
  53. description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")",
  54. failureReason: "Could not get access to shared container with app group named \(appGroupName).",
  55. recoverySuggestion: "Check that the app-group name in the entitlement matches the string provided."
  56. )
  57. }
  58. }
  59. if var url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first {
  60. if let validPath = validPath {
  61. url = url.appendingPathComponent(validPath, isDirectory: false)
  62. }
  63. if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix {
  64. let fixedUrlString = filePrefix + url.absoluteString
  65. url = URL(string: fixedUrlString)!
  66. }
  67. return url
  68. } else {
  69. throw createError(
  70. .couldNotAccessUserDomainMask,
  71. description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")",
  72. failureReason: "Could not get access to the file system's user domain mask.",
  73. recoverySuggestion: "Use a different directory."
  74. )
  75. }
  76. }
  77. /// Find an existing file's URL or throw an error if it doesn't exist
  78. static func getExistingFileURL(for path: String?, in directory: Directory) throws -> URL {
  79. do {
  80. let url = try createURL(for: path, in: directory)
  81. if FileManager.default.fileExists(atPath: url.path) {
  82. return url
  83. }
  84. throw createError(
  85. .noFileFound,
  86. description: "Could not find an existing file or folder at \(url.path).",
  87. failureReason: "There is no existing file or folder at \(url.path)",
  88. recoverySuggestion: "Check if a file or folder exists before trying to commit an operation on it."
  89. )
  90. } catch {
  91. throw error
  92. }
  93. }
  94. /// Convert a user generated name to a valid file name
  95. static func getValidFilePath(from originalString: String) throws -> String {
  96. var invalidCharacters = CharacterSet(charactersIn: ":")
  97. invalidCharacters.formUnion(.newlines)
  98. invalidCharacters.formUnion(.illegalCharacters)
  99. invalidCharacters.formUnion(.controlCharacters)
  100. let pathWithoutIllegalCharacters = originalString
  101. .components(separatedBy: invalidCharacters)
  102. .joined(separator: "")
  103. let validFileName = removeSlashesAtBeginning(of: pathWithoutIllegalCharacters)
  104. guard !validFileName.isEmpty, validFileName != "." else {
  105. throw createError(
  106. .invalidFileName,
  107. description: "\(originalString) is an invalid file name.",
  108. failureReason: "Cannot write/read a file with the name \(originalString) on disk.",
  109. recoverySuggestion: "Use another file name with alphanumeric characters."
  110. )
  111. }
  112. return validFileName
  113. }
  114. /// Helper method for getValidFilePath(from:) to remove all "/" at the beginning of a String
  115. static func removeSlashesAtBeginning(of string: String) -> String {
  116. var string = string
  117. if string.prefix(1) == "/" {
  118. string.remove(at: string.startIndex)
  119. }
  120. if string.prefix(1) == "/" {
  121. string = removeSlashesAtBeginning(of: string)
  122. }
  123. return string
  124. }
  125. /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system
  126. static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for path: String?, in directory: Directory) throws {
  127. do {
  128. let url = try getExistingFileURL(for: path, in: directory)
  129. var resourceUrl = url
  130. var resourceValues = URLResourceValues()
  131. resourceValues.isExcludedFromBackup = isExcludedFromBackup
  132. try resourceUrl.setResourceValues(resourceValues)
  133. } catch {
  134. throw error
  135. }
  136. }
  137. /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system
  138. static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for url: URL) throws {
  139. do {
  140. var resourceUrl = url
  141. var resourceValues = URLResourceValues()
  142. resourceValues.isExcludedFromBackup = isExcludedFromBackup
  143. try resourceUrl.setResourceValues(resourceValues)
  144. } catch {
  145. throw error
  146. }
  147. }
  148. /// Create necessary sub folders before creating a file
  149. static func createSubfoldersBeforeCreatingFile(at url: URL) throws {
  150. do {
  151. let subfolderUrl = url.deletingLastPathComponent()
  152. var subfolderExists = false
  153. var isDirectory: ObjCBool = false
  154. if FileManager.default.fileExists(atPath: subfolderUrl.path, isDirectory: &isDirectory) {
  155. if isDirectory.boolValue {
  156. subfolderExists = true
  157. }
  158. }
  159. if !subfolderExists {
  160. try FileManager.default.createDirectory(at: subfolderUrl, withIntermediateDirectories: true, attributes: nil)
  161. }
  162. } catch {
  163. throw error
  164. }
  165. }
  166. /// Get Int from a file name
  167. static func fileNameInt(_ url: URL) -> Int? {
  168. let fileExtension = url.pathExtension
  169. let filePath = url.lastPathComponent
  170. let fileName = filePath.replacingOccurrences(of: fileExtension, with: "")
  171. return Int(String(fileName.filter { "0123456789".contains($0) }))
  172. }
  173. }