159 lines
4.7 KiB
Swift
159 lines
4.7 KiB
Swift
import Foundation
|
|
|
|
public enum LocalArtworkStoreError: LocalizedError, Equatable, Sendable {
|
|
case emptyArtworkData
|
|
case missingLocalFile(path: String)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .emptyArtworkData:
|
|
return "The local artwork data was empty."
|
|
case let .missingLocalFile(path):
|
|
return "The local artwork file is missing: \(path)"
|
|
}
|
|
}
|
|
}
|
|
|
|
public protocol LocalArtworkStore: Actor {
|
|
func saveArtwork(
|
|
_ data: Data,
|
|
sha256: String,
|
|
mimeType: String
|
|
) async throws -> String
|
|
func readArtwork(at localFilePath: String) async throws -> Data
|
|
func fileExists(at localFilePath: String) async -> Bool
|
|
}
|
|
|
|
public actor FileLocalArtworkStore: LocalArtworkStore {
|
|
private let baseDirectoryURL: URL
|
|
private let fileManager: FileManager
|
|
|
|
public init(
|
|
baseDirectoryURL: URL? = nil,
|
|
fileManager: FileManager = .default
|
|
) throws {
|
|
self.fileManager = fileManager
|
|
if let baseDirectoryURL {
|
|
self.baseDirectoryURL = baseDirectoryURL
|
|
} else {
|
|
self.baseDirectoryURL = try Self.defaultBaseDirectoryURL(fileManager: fileManager)
|
|
}
|
|
}
|
|
|
|
public func saveArtwork(
|
|
_ data: Data,
|
|
sha256: String,
|
|
mimeType: String
|
|
) async throws -> String {
|
|
guard !data.isEmpty else {
|
|
throw LocalArtworkStoreError.emptyArtworkData
|
|
}
|
|
|
|
try fileManager.createDirectory(
|
|
at: baseDirectoryURL,
|
|
withIntermediateDirectories: true
|
|
)
|
|
|
|
let fileURL = localFileURL(sha256: sha256, mimeType: mimeType)
|
|
if fileManager.fileExists(atPath: fileURL.path) {
|
|
return fileURL.standardizedFileURL.path
|
|
}
|
|
|
|
try data.write(to: fileURL, options: .atomic)
|
|
return fileURL.standardizedFileURL.path
|
|
}
|
|
|
|
public func readArtwork(at localFilePath: String) async throws -> Data {
|
|
let fileURL = URL(fileURLWithPath: localFilePath).standardizedFileURL
|
|
guard fileManager.fileExists(atPath: fileURL.path) else {
|
|
throw LocalArtworkStoreError.missingLocalFile(path: localFilePath)
|
|
}
|
|
|
|
return try Data(contentsOf: fileURL)
|
|
}
|
|
|
|
public func fileExists(at localFilePath: String) async -> Bool {
|
|
let fileURL = URL(fileURLWithPath: localFilePath).standardizedFileURL
|
|
return fileManager.fileExists(atPath: fileURL.path)
|
|
}
|
|
|
|
private static func defaultBaseDirectoryURL(fileManager: FileManager) throws -> URL {
|
|
guard let applicationSupportURL = fileManager.urls(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask
|
|
).first else {
|
|
throw CocoaError(.fileNoSuchFile)
|
|
}
|
|
|
|
return applicationSupportURL
|
|
.appendingPathComponent("Velody", isDirectory: true)
|
|
.appendingPathComponent("local-artwork", isDirectory: true)
|
|
}
|
|
|
|
private func localFileURL(sha256: String, mimeType: String) -> URL {
|
|
baseDirectoryURL.appendingPathComponent(
|
|
"\(sha256).\(Self.fileExtension(for: mimeType))"
|
|
)
|
|
}
|
|
|
|
private static func fileExtension(for mimeType: String) -> String {
|
|
switch mimeType.lowercased() {
|
|
case "image/jpeg", "image/jpg":
|
|
return "jpg"
|
|
case "image/png":
|
|
return "png"
|
|
default:
|
|
return "img"
|
|
}
|
|
}
|
|
}
|
|
|
|
public actor InMemoryLocalArtworkStore: LocalArtworkStore {
|
|
private var files: [String: Data]
|
|
|
|
public init(files: [String: Data] = [:]) {
|
|
self.files = files
|
|
}
|
|
|
|
public func saveArtwork(
|
|
_ data: Data,
|
|
sha256: String,
|
|
mimeType: String
|
|
) async throws -> String {
|
|
guard !data.isEmpty else {
|
|
throw LocalArtworkStoreError.emptyArtworkData
|
|
}
|
|
|
|
let localFilePath = Self.localFilePath(sha256: sha256, mimeType: mimeType)
|
|
files[localFilePath] = data
|
|
return localFilePath
|
|
}
|
|
|
|
public func readArtwork(at localFilePath: String) async throws -> Data {
|
|
guard let data = files[localFilePath] else {
|
|
throw LocalArtworkStoreError.missingLocalFile(path: localFilePath)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
public func fileExists(at localFilePath: String) async -> Bool {
|
|
files[localFilePath] != nil
|
|
}
|
|
|
|
private static func localFilePath(sha256: String, mimeType: String) -> String {
|
|
let fileExtension: String
|
|
|
|
switch mimeType.lowercased() {
|
|
case "image/jpeg", "image/jpg":
|
|
fileExtension = "jpg"
|
|
case "image/png":
|
|
fileExtension = "png"
|
|
default:
|
|
fileExtension = "img"
|
|
}
|
|
|
|
return "/in-memory/local-artwork/\(sha256).\(fileExtension)"
|
|
}
|
|
}
|