// // ImageCacheStore.swift // World Manager for Minecraft // // Created by OpenAI on 2026-05-26. // import CryptoKit import Foundation actor ImageCacheStore { static let shared = ImageCacheStore() private let fileManager: FileManager private let cacheDirectoryURL: URL private let cacheDirectoryPath: String init(fileManager: FileManager = .default) { self.fileManager = fileManager let cachesURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first ?? fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Caches", isDirectory: true) let cacheDirectoryURL = cachesURL .appendingPathComponent("World Manager for Minecraft", isDirectory: true) .appendingPathComponent("ImageCache", isDirectory: true) self.cacheDirectoryURL = cacheDirectoryURL self.cacheDirectoryPath = cacheDirectoryURL.standardizedFileURL.path } func cachedImageURL(for sourceImageURL: URL?) -> URL? { guard let sourceImageURL else { return nil } let normalizedSourceURL = sourceImageURL.standardizedFileURL if isCachedImageURL(normalizedSourceURL) { return fileManager.fileExists(atPath: normalizedSourceURL.path) ? normalizedSourceURL : nil } guard fileManager.fileExists(atPath: normalizedSourceURL.path) else { return nil } guard let resourceValues = try? normalizedSourceURL.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey]), let fileSize = resourceValues.fileSize else { return nil } let modifiedStamp = resourceValues.contentModificationDate?.timeIntervalSince1970 ?? 0 let sourceKey = digest(for: normalizedSourceURL.path) let versionKey = digest(for: "\(normalizedSourceURL.path)|\(modifiedStamp)|\(fileSize)") let pathExtension = normalizedSourceURL.pathExtension.isEmpty ? "img" : normalizedSourceURL.pathExtension let cachedURL = cacheDirectoryURL .appendingPathComponent("\(sourceKey)-\(versionKey)", isDirectory: false) .appendingPathExtension(pathExtension) do { try fileManager.createDirectory(at: cacheDirectoryURL, withIntermediateDirectories: true) if fileManager.fileExists(atPath: cachedURL.path) { return cachedURL } purgeStaleVariants(forSourceKey: sourceKey, keeping: cachedURL) try fileManager.copyItem(at: normalizedSourceURL, to: cachedURL) return cachedURL } catch { return nil } } func isCachedImageURL(_ url: URL) -> Bool { url.standardizedFileURL.path.hasPrefix(cacheDirectoryPath + "/") } func cachedImageURL( forRemoteData data: Data, cacheKey: String, pathExtension: String ) -> URL? { let normalizedExtension = pathExtension.isEmpty ? "img" : pathExtension let dataDigest = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() let sourceKey = digest(for: cacheKey) let cachedURL = cacheDirectoryURL .appendingPathComponent("\(sourceKey)-\(dataDigest)", isDirectory: false) .appendingPathExtension(normalizedExtension) do { try fileManager.createDirectory(at: cacheDirectoryURL, withIntermediateDirectories: true) if fileManager.fileExists(atPath: cachedURL.path) { return cachedURL } purgeStaleVariants(forSourceKey: sourceKey, keeping: cachedURL) try data.write(to: cachedURL, options: .atomic) return cachedURL } catch { return nil } } private func purgeStaleVariants(forSourceKey sourceKey: String, keeping cachedURL: URL) { guard let cachedFiles = try? fileManager.contentsOfDirectory( at: cacheDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles] ) else { return } for url in cachedFiles where url.lastPathComponent.hasPrefix(sourceKey + "-") && url != cachedURL { try? fileManager.removeItem(at: url) } } private func digest(for value: String) -> String { let bytes = SHA256.hash(data: Data(value.utf8)) return bytes.map { String(format: "%02x", $0) }.joined() } }