ImageFileStorage.swift 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import Combine
  2. import CoreImage
  3. import Swinject
  4. protocol ImageFileStorage: AnyObject {
  5. var name: String { get }
  6. func saveImage(_: CIImage, imageClass: String) -> AnyPublisher<URL, FileStorageError>
  7. func imageClasses() -> [String]
  8. func fileURLs(imageClass: String) -> [URL]
  9. func moveImage(url: URL, toImageClass: String) -> AnyPublisher<Void, FileStorageError>
  10. func remove(url: URL)
  11. }
  12. final class BaseImageFileStorage: ImageFileStorage, Injectable {
  13. let name: String
  14. @Injected() private var fileManager: FileManager!
  15. private let processQueue = DispatchQueue(label: "BaseImageFileStorage.processQueue")
  16. private var lifetime = Set<AnyCancellable>()
  17. private lazy var directoryURL: URL = {
  18. let url = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
  19. return url.appendingPathComponent(name, isDirectory: true)
  20. }()
  21. init(
  22. resolver: Resolver,
  23. name: String
  24. ) {
  25. self.name = name
  26. injectServices(resolver)
  27. }
  28. func saveImage(_ image: CIImage, imageClass: String) -> AnyPublisher<URL, FileStorageError> {
  29. Future<URL, FileStorageError> { promise in
  30. self.createDirectoryIfNeeded(imageClass: imageClass)
  31. .receive(on: self.processQueue)
  32. .sink(receiveCompletion: { res in
  33. switch res {
  34. case .finished: break
  35. case let .failure(error):
  36. promise(.failure(error))
  37. }
  38. }, receiveValue: { url in
  39. let id = UUID().uuidString
  40. let fileURL = url.appendingPathComponent(id).appendingPathExtension("jpeg")
  41. let imageData = CIContext().jpegRepresentation(
  42. of: image,
  43. colorSpace: CGColorSpaceCreateDeviceRGB()
  44. )!
  45. if self.fileManager.createFile(atPath: fileURL.path, contents: imageData, attributes: nil) {
  46. promise(.success(fileURL))
  47. } else {
  48. promise(.failure(.cannotCreateFile(url: fileURL)))
  49. }
  50. })
  51. .store(in: &self.lifetime)
  52. }.eraseToAnyPublisher()
  53. }
  54. func imageClasses() -> [String] {
  55. var urls = fileManager.enumerator(
  56. at: directoryURL,
  57. includingPropertiesForKeys: nil,
  58. options: [.skipsSubdirectoryDescendants, .skipsPackageDescendants, .skipsHiddenFiles],
  59. errorHandler: nil
  60. )?.allObjects as? [URL] ?? []
  61. urls = urls.filter { $0.hasDirectoryPath }
  62. let classes = urls.map(\.lastPathComponent)
  63. return classes
  64. }
  65. func fileURLs(imageClass: String) -> [URL] {
  66. var urls = fileManager.enumerator(
  67. at: directory(imageClass: imageClass),
  68. includingPropertiesForKeys: nil,
  69. options: [.skipsSubdirectoryDescendants, .skipsPackageDescendants, .skipsHiddenFiles],
  70. errorHandler: nil
  71. )?.allObjects as? [URL] ?? []
  72. urls = urls.filter { $0.pathExtension == "jpeg" }
  73. return urls
  74. }
  75. func moveImage(url: URL, toImageClass imageClass: String) -> AnyPublisher<Void, FileStorageError> {
  76. createDirectoryIfNeeded(imageClass: imageClass)
  77. .map { (folderURL: URL) -> Void in
  78. let toURL = folderURL.appendingPathComponent(url.lastPathComponent)
  79. try? self.fileManager.moveItem(at: url, to: toURL)
  80. }.eraseToAnyPublisher()
  81. }
  82. func remove(url: URL) {
  83. processQueue.async {
  84. try? self.fileManager.removeItem(at: url)
  85. }
  86. }
  87. private func directory(imageClass: String) -> URL {
  88. directoryURL.appendingPathComponent(imageClass)
  89. }
  90. private func createDirectoryIfNeeded(imageClass: String) -> AnyPublisher<URL, FileStorageError> {
  91. Future<URL, FileStorageError> { promise in
  92. self.processQueue.async {
  93. let dirURL = self.directory(imageClass: imageClass)
  94. guard !self.fileManager.fileExists(atPath: dirURL.path) else {
  95. promise(.success(dirURL))
  96. return
  97. }
  98. do {
  99. try self.fileManager.createDirectory(
  100. at: dirURL,
  101. withIntermediateDirectories: true,
  102. attributes: nil
  103. )
  104. promise(.success(dirURL))
  105. } catch {
  106. promise(.failure(.cannotCreateDirectory(url: dirURL, error: error)))
  107. }
  108. }
  109. }.eraseToAnyPublisher()
  110. }
  111. }