ContactImageStore.swift 67 lignes · 2609 octets
import UIKit

struct ImageStoreError: Error, CustomStringConvertible {
    let message: String
    var description: String { "ImageStoreError: \(message)" }
}

/// Port of android `data/ContactImageStore.kt`.
/// Layout: <Application Support>/contacts/<contactId>/<name> (Android used
/// filesDir with the same relative scheme).
final class ContactImageStore: ContactImageStoring {
    private let root: URL

    init(rootDirectory: URL? = nil) {
        if let rootDirectory {
            root = rootDirectory
        } else {
            let base = FileManager.default.urls(
                for: .applicationSupportDirectory,
                in: .userDomainMask
            )[0]
            root = base.appendingPathComponent("contacts", isDirectory: true)
        }
    }

    private func dir(_ id: Int64) -> URL {
        let url = root.appendingPathComponent(String(id), isDirectory: true)
        try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }

    @discardableResult
    func saveJpeg(contactId: Int64, name: String, image: UIImage, quality: CGFloat = 0.9) throws -> String {
        guard let data = image.jpegData(compressionQuality: quality) else {
            throw ImageStoreError(message: "JPEG encoding failed for \(name)")
        }
        return try saveBytes(contactId: contactId, name: name, bytes: data)
    }

    /// Writes raw bytes (e.g. an image downloaded from the server).
    @discardableResult
    func saveBytes(contactId: Int64, name: String, bytes: Data) throws -> String {
        let file = dir(contactId).appendingPathComponent(name)
        try bytes.write(to: file, options: .atomic)
        return file.path
    }

    func deleteAll(contactId: Int64) {
        try? FileManager.default.removeItem(
            at: root.appendingPathComponent(String(contactId), isDirectory: true)
        )
    }

    /// Copies re-encoding after EXIF correction, so the stored file is already
    /// upright (Kotlin `copyImage`).
    func copyImage(fromPath: String?, toContactId: Int64, name: String) -> String? {
        guard let fromPath, fromPath.isNotBlank else { return nil }
        let src = URL(fileURLWithPath: fromPath)
        var isDirectory: ObjCBool = false
        guard FileManager.default.fileExists(atPath: src.path, isDirectory: &isDirectory),
              !isDirectory.boolValue else {
            return nil
        }
        guard let oriented = ImageOrientation.fromFile(src)?.bitmap else { return nil }
        return try? saveJpeg(contactId: toContactId, name: name, image: oriented)
    }
}