// // ImageCacheMigrations.swift // damus // // Created by Daniel D’Aquino on 2025-04-26. // import Foundation import Kingfisher struct ImageCacheMigrations { static func migrateKingfisherCacheIfNeeded() { let fileManager = FileManager.default let defaults = UserDefaults.standard let migration1Key = "KingfisherCacheMigrated" // Never ever changes let migration2Key = "KingfisherCacheMigratedV2" // Never ever changes let migration1Done = defaults.bool(forKey: migration1Key) let migration2Done = defaults.bool(forKey: migration2Key) guard !migration1Done || !migration2Done else { // All migrations are already done. Skip. return } let oldCachePath = migration1Done ? migration1KingfisherCachePath() : migration0KingfisherCachePath() // New shared cache location let newCachePath = kingfisherCachePath().path if fileManager.fileExists(atPath: oldCachePath) { do { // Move the old cache to the new location try fileManager.moveItem(atPath: oldCachePath, toPath: newCachePath) Log.info("Successfully migrated Kingfisher cache to %s", for: .storage, newCachePath) } catch { do { // Cache data is not essential, fallback to deleting the cache and starting all over // It's better than leaving significant garbage data stuck indefinitely on the user's phone try fileManager.removeItem(atPath: newCachePath) try fileManager.removeItem(atPath: oldCachePath) } catch { Log.error("Failed to migrate cache: %s", for: .storage, error.localizedDescription) return // Do not mark them as complete, we can try again next time the user reloads the app } } } // Mark migrations as complete defaults.set(true, forKey: migration1Key) defaults.set(true, forKey: migration2Key) } static private func migration0KingfisherCachePath() -> String { // Implementation note: These are old, so they should not be changed let defaultCache = ImageCache.default return defaultCache.diskStorage.directoryURL.path } static private func migration1KingfisherCachePath() -> String { // Implementation note: These are old, so they are hard-coded on purpose, because we can't change these values from the past. let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.damus")! return groupURL.appendingPathComponent("ImageCache").path } /// The latest path for kingfisher to store cached images on. /// /// Documentation references: /// - https://developer.apple.com/documentation/foundation/filemanager/containerurl(forsecurityapplicationgroupidentifier:)#:~:text=The%20system%20creates%20only%20the%20Library/Caches%20subdirectory%20automatically /// - https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#:~:text=Put%20data%20cache,files%20as%20needed. static func kingfisherCachePath() -> URL { let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER)! return groupURL .appendingPathComponent("Library") .appendingPathComponent("Caches") .appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME) } }