import Foundation import SwiftUI class ImageManager { static let shared = ImageManager() private let fileManager = FileManager.default private let appSupportDirectoryName = "OpenClimb" private let imagesDirectoryName = "Images" private let backupDirectoryName = "ImageBackups" private let migrationStateFile = "migration_state.json" private let migrationLockFile = "migration.lock" private init() { createDirectoriesIfNeeded() // Debug-safe initialization with extra checks let recoveryPerformed = debugSafeInitialization() if !recoveryPerformed { performRobustMigration() } // Final integrity check if !validateStorageIntegrity() { print("๐Ÿšจ CRITICAL: Storage integrity compromised - attempting emergency recovery") emergencyImageRestore() } logDirectoryInfo() } var appSupportDirectory: URL { let urls = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask) return urls.first!.appendingPathComponent(appSupportDirectoryName) } var imagesDirectory: URL { appSupportDirectory.appendingPathComponent(imagesDirectoryName) } var backupDirectory: URL { appSupportDirectory.appendingPathComponent(backupDirectoryName) } func getImagesDirectoryPath() -> String { return imagesDirectory.path } private var legacyDocumentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } var legacyImagesDirectory: URL { legacyDocumentsDirectory.appendingPathComponent("OpenClimbImages") } var legacyImportImagesDirectory: URL { legacyDocumentsDirectory.appendingPathComponent("images") } private func createDirectoriesIfNeeded() { // Create Application Support structure [appSupportDirectory, imagesDirectory, backupDirectory].forEach { directory in if !fileManager.fileExists(atPath: directory.path) { do { try fileManager.createDirectory( at: directory, withIntermediateDirectories: true, attributes: [ .protectionKey: FileProtectionType.completeUntilFirstUserAuthentication ]) print("โœ… Created directory: \(directory.path)") } catch { print("โŒ Failed to create directory \(directory.path): \(error)") } } } // Exclude from iCloud backup to prevent storage issues excludeFromiCloudBackup() } private func excludeFromiCloudBackup() { do { var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true var imagesURL = imagesDirectory var backupURL = backupDirectory try imagesURL.setResourceValues(resourceValues) try backupURL.setResourceValues(resourceValues) print("โœ… Excluded image directories from iCloud backup") } catch { print("โš ๏ธ Failed to exclude from iCloud backup: \(error)") } } private struct MigrationState: Codable { let version: Int let startTime: Date let completedFiles: [String] let totalFiles: Int let isComplete: Bool let lastCheckpoint: Date static let currentVersion = 2 } private var migrationStateURL: URL { appSupportDirectory.appendingPathComponent(migrationStateFile) } private var migrationLockURL: URL { appSupportDirectory.appendingPathComponent(migrationLockFile) } private func performRobustMigration() { print("๐Ÿ”„ Starting robust image migration system...") // Check for interrupted migration if let incompleteState = loadMigrationState() { print("๐Ÿ”ง Detected interrupted migration, resuming...") resumeMigration(from: incompleteState) } else { // Start fresh migration startNewMigration() } // Always verify migration integrity verifyMigrationIntegrity() // Clean up migration state files cleanupMigrationState() } private func startNewMigration() { // First check for images in previous Application Support directories if let previousAppSupportImages = findPreviousAppSupportImages() { print("๐Ÿ“ Found images in previous Application Support directory") migratePreviousAppSupportImages(from: previousAppSupportImages) return } // Check if legacy directories exist let hasLegacyImages = fileManager.fileExists(atPath: legacyImagesDirectory.path) let hasLegacyImportImages = fileManager.fileExists(atPath: legacyImportImagesDirectory.path) guard hasLegacyImages || hasLegacyImportImages else { print("โœ… No legacy images to migrate") return } // Create migration lock createMigrationLock() do { var allLegacyFiles: [String] = [] // Collect files from OpenClimbImages directory if fileManager.fileExists(atPath: legacyImagesDirectory.path) { let legacyFiles = try fileManager.contentsOfDirectory( atPath: legacyImagesDirectory.path) allLegacyFiles.append(contentsOf: legacyFiles) print("๐Ÿ“ฆ Found \(legacyFiles.count) images in OpenClimbImages") } // Collect files from Documents/images directory if fileManager.fileExists(atPath: legacyImportImagesDirectory.path) { let importFiles = try fileManager.contentsOfDirectory( atPath: legacyImportImagesDirectory.path) allLegacyFiles.append(contentsOf: importFiles) print("๐Ÿ“ฆ Found \(importFiles.count) images in Documents/images") } print("๐Ÿ“ฆ Total legacy images to migrate: \(allLegacyFiles.count)") let initialState = MigrationState( version: MigrationState.currentVersion, startTime: Date(), completedFiles: [], totalFiles: allLegacyFiles.count, isComplete: false, lastCheckpoint: Date() ) saveMigrationState(initialState) performMigrationWithCheckpoints(files: allLegacyFiles, currentState: initialState) } catch { print("โŒ Failed to start migration: \(error)") } } private func resumeMigration(from state: MigrationState) { print("๐Ÿ”„ Resuming migration from checkpoint...") print("๐Ÿ“Š Progress: \(state.completedFiles.count)/\(state.totalFiles)") do { let legacyFiles = try fileManager.contentsOfDirectory( atPath: legacyImagesDirectory.path) let remainingFiles = legacyFiles.filter { !state.completedFiles.contains($0) } print("๐Ÿ“ฆ Resuming with \(remainingFiles.count) remaining files") performMigrationWithCheckpoints(files: remainingFiles, currentState: state) } catch { print("โŒ Failed to resume migration: \(error)") // Fallback: start fresh removeMigrationState() startNewMigration() } } private func performMigrationWithCheckpoints(files: [String], currentState: MigrationState) { var migratedCount = currentState.completedFiles.count var failedCount = 0 var completedFiles = currentState.completedFiles for (index, fileName) in files.enumerated() { autoreleasepool { // Check both legacy directories for the file var legacyFilePath: URL? if fileManager.fileExists( atPath: legacyImagesDirectory.appendingPathComponent(fileName).path) { legacyFilePath = legacyImagesDirectory.appendingPathComponent(fileName) } else if fileManager.fileExists( atPath: legacyImportImagesDirectory.appendingPathComponent(fileName).path) { legacyFilePath = legacyImportImagesDirectory.appendingPathComponent(fileName) } guard let sourcePath = legacyFilePath else { completedFiles.append(fileName) return } let newFilePath = imagesDirectory.appendingPathComponent(fileName) let backupPath = backupDirectory.appendingPathComponent(fileName) // Skip if already exists in new location if fileManager.fileExists(atPath: newFilePath.path) { completedFiles.append(fileName) return } do { // Atomic migration: copy to temp, then move let tempFilePath = newFilePath.appendingPathExtension("tmp") // Copy to temp location first try fileManager.copyItem(at: sourcePath, to: tempFilePath) // Verify file integrity let originalData = try Data(contentsOf: sourcePath) let copiedData = try Data(contentsOf: tempFilePath) guard originalData == copiedData else { try? fileManager.removeItem(at: tempFilePath) throw NSError( domain: "MigrationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "File integrity check failed"]) } // Move from temp to final location try fileManager.moveItem(at: tempFilePath, to: newFilePath) // Create backup copy try? fileManager.copyItem(at: newFilePath, to: backupPath) completedFiles.append(fileName) migratedCount += 1 print("โœ… Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))") } catch { failedCount += 1 print("โŒ Failed to migrate \(fileName): \(error)") } // Save checkpoint every 5 files or if interrupted if (index + 1) % 5 == 0 { let checkpointState = MigrationState( version: MigrationState.currentVersion, startTime: currentState.startTime, completedFiles: completedFiles, totalFiles: currentState.totalFiles, isComplete: false, lastCheckpoint: Date() ) saveMigrationState(checkpointState) print("๐Ÿ’พ Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)") } } } // Mark migration as complete let finalState = MigrationState( version: MigrationState.currentVersion, startTime: currentState.startTime, completedFiles: completedFiles, totalFiles: currentState.totalFiles, isComplete: true, lastCheckpoint: Date() ) saveMigrationState(finalState) print("๐Ÿ Migration complete: \(migratedCount) migrated, \(failedCount) failed") // Clean up legacy directory if no failures if failedCount == 0 { cleanupLegacyDirectory() } } private func verifyMigrationIntegrity() { print("๐Ÿ” Verifying migration integrity...") var allLegacyFiles = Set() // Collect files from both legacy directories do { if fileManager.fileExists(atPath: legacyImagesDirectory.path) { let legacyFiles = Set( try fileManager.contentsOfDirectory(atPath: legacyImagesDirectory.path)) allLegacyFiles.formUnion(legacyFiles) } if fileManager.fileExists(atPath: legacyImportImagesDirectory.path) { let importFiles = Set( try fileManager.contentsOfDirectory(atPath: legacyImportImagesDirectory.path)) allLegacyFiles.formUnion(importFiles) } } catch { print("โŒ Failed to read legacy directories: \(error)") return } guard !allLegacyFiles.isEmpty else { print("โœ… No legacy directories to verify against") return } do { let migratedFiles = Set( try fileManager.contentsOfDirectory(atPath: imagesDirectory.path)) let missingFiles = allLegacyFiles.subtracting(migratedFiles) if missingFiles.isEmpty { print("โœ… Migration integrity verified - all files present") cleanupLegacyDirectory() } else { print("โš ๏ธ Missing \(missingFiles.count) files, re-triggering migration") // Re-trigger migration for missing files performMigrationWithCheckpoints( files: Array(missingFiles), currentState: MigrationState( version: MigrationState.currentVersion, startTime: Date(), completedFiles: [], totalFiles: missingFiles.count, isComplete: false, lastCheckpoint: Date() )) } } catch { print("โŒ Failed to verify migration integrity: \(error)") } } private func cleanupLegacyDirectory() { do { try fileManager.removeItem(at: legacyImagesDirectory) print("๐Ÿ—‘๏ธ Cleaned up legacy directory") } catch { print("โš ๏ธ Failed to clean up legacy directory: \(error)") } } private func loadMigrationState() -> MigrationState? { guard fileManager.fileExists(atPath: migrationStateURL.path) else { return nil } // Check if migration was interrupted (lock file exists) if !fileManager.fileExists(atPath: migrationLockURL.path) { // Migration completed normally, clean up state removeMigrationState() return nil } do { let data = try Data(contentsOf: migrationStateURL) let state = try JSONDecoder().decode(MigrationState.self, from: data) // Check if state is too old (more than 1 hour) if Date().timeIntervalSince(state.lastCheckpoint) > 3600 { print("โš ๏ธ Migration state is stale, starting fresh") removeMigrationState() return nil } return state.isComplete ? nil : state } catch { print("โŒ Failed to load migration state: \(error)") removeMigrationState() return nil } } private func saveMigrationState(_ state: MigrationState) { do { let data = try JSONEncoder().encode(state) try data.write(to: migrationStateURL) } catch { print("โŒ Failed to save migration state: \(error)") } } private func removeMigrationState() { try? fileManager.removeItem(at: migrationStateURL) } private func createMigrationLock() { let lockData = "Migration in progress - \(Date())".data(using: .utf8) ?? Data() try? lockData.write(to: migrationLockURL) } private func cleanupMigrationState() { try? fileManager.removeItem(at: migrationStateURL) try? fileManager.removeItem(at: migrationLockURL) print("๐Ÿงน Cleaned up migration state files") } func saveImageData(_ data: Data, withName name: String? = nil) -> String? { let fileName = name ?? "\(UUID().uuidString).jpg" let primaryPath = imagesDirectory.appendingPathComponent(fileName) let backupPath = backupDirectory.appendingPathComponent(fileName) do { // Save to primary location try data.write(to: primaryPath) // Create backup copy try data.write(to: backupPath) print("โœ… Saved image with backup: \(fileName)") return fileName } catch { print("โŒ Failed to save image \(fileName): \(error)") return nil } } func loadImageData(fromPath path: String) -> Data? { let primaryPath = getFullPath(from: path) let backupPath = backupDirectory.appendingPathComponent(getRelativePath(from: path)) // Try primary location first if fileManager.fileExists(atPath: primaryPath), let data = try? Data(contentsOf: URL(fileURLWithPath: primaryPath)) { return data } // Fallback to backup location if fileManager.fileExists(atPath: backupPath.path), let data = try? Data(contentsOf: backupPath) { print("๐Ÿ“ฆ Restored image from backup: \(path)") // Restore to primary location try? data.write(to: URL(fileURLWithPath: primaryPath)) return data } return nil } func imageExists(atPath path: String) -> Bool { let primaryPath = getFullPath(from: path) let backupPath = backupDirectory.appendingPathComponent(getRelativePath(from: path)) return fileManager.fileExists(atPath: primaryPath) || fileManager.fileExists(atPath: backupPath.path) } func deleteImage(atPath path: String) -> Bool { let primaryPath = getFullPath(from: path) let backupPath = backupDirectory.appendingPathComponent(getRelativePath(from: path)) var success = true // Delete from primary location if fileManager.fileExists(atPath: primaryPath) { do { try fileManager.removeItem(atPath: primaryPath) } catch { print("โŒ Failed to delete primary image at \(primaryPath): \(error)") success = false } } // Delete from backup location if fileManager.fileExists(atPath: backupPath.path) { do { try fileManager.removeItem(at: backupPath) } catch { print("โŒ Failed to delete backup image at \(backupPath.path): \(error)") success = false } } return success } func deleteImages(atPaths paths: [String]) { for path in paths { _ = deleteImage(atPath: path) } } func getFullPath(from relativePath: String) -> String { // If it's already a full path, check if it's legacy and needs migration if relativePath.hasPrefix("/") { // If it's pointing to legacy Documents directory, redirect to new location if relativePath.contains("Documents/OpenClimbImages") { let fileName = URL(fileURLWithPath: relativePath).lastPathComponent return imagesDirectory.appendingPathComponent(fileName).path } return relativePath } // For relative paths, use the persistent Application Support location return imagesDirectory.appendingPathComponent(relativePath).path } func getRelativePath(from fullPath: String) -> String { if !fullPath.hasPrefix("/") { return fullPath } return URL(fileURLWithPath: fullPath).lastPathComponent } func performMaintenance() { print("๐Ÿ”ง Starting image maintenance...") syncBackups() validateImageIntegrity() cleanupOrphanedFiles() } private func syncBackups() { do { let primaryFiles = try fileManager.contentsOfDirectory(atPath: imagesDirectory.path) let backupFiles = Set(try fileManager.contentsOfDirectory(atPath: backupDirectory.path)) for fileName in primaryFiles { if !backupFiles.contains(fileName) { let primaryPath = imagesDirectory.appendingPathComponent(fileName) let backupPath = backupDirectory.appendingPathComponent(fileName) try? fileManager.copyItem(at: primaryPath, to: backupPath) print("๐Ÿ”„ Created missing backup for: \(fileName)") } } } catch { print("โŒ Failed to sync backups: \(error)") } } private func validateImageIntegrity() { do { let files = try fileManager.contentsOfDirectory(atPath: imagesDirectory.path) var validFiles = 0 for fileName in files { let filePath = imagesDirectory.appendingPathComponent(fileName) if let data = try? Data(contentsOf: filePath), data.count > 0 { // Basic validation - check if file has content and is reasonable size if data.count > 100 { // Minimum viable image size validFiles += 1 } } } print("โœ… Validated \(validFiles) of \(files.count) image files") } catch { print("โŒ Failed to validate images: \(error)") } } private func cleanupOrphanedFiles() { // This would need access to the data manager to check which files are actually referenced print("๐Ÿงน Cleanup would require coordination with data manager") } func getStorageInfo() -> (primaryCount: Int, backupCount: Int, totalSize: Int64) { let primaryCount = ((try? fileManager.contentsOfDirectory(atPath: imagesDirectory.path)) ?? []).count let backupCount = ((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).count var totalSize: Int64 = 0 [imagesDirectory, backupDirectory].forEach { directory in if let enumerator = fileManager.enumerator( at: directory, includingPropertiesForKeys: [.fileSizeKey]) { for case let url as URL in enumerator { if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize { totalSize += Int64(size) } } } } return (primaryCount, backupCount, totalSize) } private func logDirectoryInfo() { let info = getStorageInfo() let previousDir = findPreviousAppSupportImages() print( """ ๐Ÿ“ OpenClimb Image Storage: - App Support: \(appSupportDirectory.path) - Images: \(imagesDirectory.path) (\(info.primaryCount) files) - Backups: \(backupDirectory.path) (\(info.backupCount) files) - Previous Dir: \(previousDir?.path ?? "None found") - Legacy Dir: \(legacyImagesDirectory.path) (exists: \(fileManager.fileExists(atPath: legacyImagesDirectory.path))) - Legacy Import Dir: \(legacyImportImagesDirectory.path) (exists: \(fileManager.fileExists(atPath: legacyImportImagesDirectory.path))) - Total Size: \(info.totalSize / 1024)KB """) } func forceRecoveryMigration() { print("๐Ÿšจ FORCE RECOVERY: Starting manual migration recovery...") // Remove any stale state removeMigrationState() try? fileManager.removeItem(at: migrationLockURL) // Force fresh migration startNewMigration() print("๐Ÿšจ FORCE RECOVERY: Migration recovery completed") } func saveImportedImage(_ imageData: Data, filename: String) throws -> String { let imagePath = imagesDirectory.appendingPathComponent(filename) let backupPath = backupDirectory.appendingPathComponent(filename) // Save to main directory try imageData.write(to: imagePath) // Create backup try? imageData.write(to: backupPath) print("๐Ÿ“ฅ Imported image: \(filename)") return filename } func emergencyImageRestore() { print("๐Ÿ†˜ EMERGENCY: Attempting image restoration...") // Try to restore from backup directory do { let backupFiles = try fileManager.contentsOfDirectory(atPath: backupDirectory.path) var restoredCount = 0 for fileName in backupFiles { let backupPath = backupDirectory.appendingPathComponent(fileName) let primaryPath = imagesDirectory.appendingPathComponent(fileName) // Only restore if primary doesn't exist if !fileManager.fileExists(atPath: primaryPath.path) { try? fileManager.copyItem(at: backupPath, to: primaryPath) restoredCount += 1 } } print("๐Ÿ†˜ EMERGENCY: Restored \(restoredCount) images from backup") } catch { print("๐Ÿ†˜ EMERGENCY: Failed to restore from backup: \(error)") } // Try previous Application Support directories first if let previousAppSupportImages = findPreviousAppSupportImages() { print("๐Ÿ†˜ EMERGENCY: Found previous Application Support images, migrating...") migratePreviousAppSupportImages(from: previousAppSupportImages) return } // Try legacy migration as last resort if fileManager.fileExists(atPath: legacyImagesDirectory.path) || fileManager.fileExists(atPath: legacyImportImagesDirectory.path) { print("๐Ÿ†˜ EMERGENCY: Attempting legacy migration as fallback...") forceRecoveryMigration() } } func debugSafeInitialization() -> Bool { print("๐Ÿ› DEBUG SAFE: Performing debug-safe initialization check...") // Check if we're in a debug environment #if DEBUG print("๐Ÿ› DEBUG SAFE: Debug environment detected") // Check for interrupted migration more aggressively if fileManager.fileExists(atPath: migrationLockURL.path) { print("๐Ÿ› DEBUG SAFE: Found migration lock - likely debug interruption") // Give extra time for file system to stabilize Thread.sleep(forTimeInterval: 1.0) // Try emergency recovery emergencyImageRestore() // Clean up lock try? fileManager.removeItem(at: migrationLockURL) return true } #endif // Check if primary storage is empty but backup exists let primaryEmpty = (try? fileManager.contentsOfDirectory(atPath: imagesDirectory.path).isEmpty) ?? true let backupHasFiles = ((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).count > 0 if primaryEmpty && backupHasFiles { print("๐Ÿ› DEBUG SAFE: Primary empty but backup exists - restoring") emergencyImageRestore() return true } // Check if primary storage is empty but previous Application Support images exist if primaryEmpty, let previousAppSupportImages = findPreviousAppSupportImages() { print("๐Ÿ› DEBUG SAFE: Primary empty but found previous Application Support images") migratePreviousAppSupportImages(from: previousAppSupportImages) return true } return false } func validateStorageIntegrity() -> Bool { let primaryFiles = Set( (try? fileManager.contentsOfDirectory(atPath: imagesDirectory.path)) ?? []) let backupFiles = Set( (try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []) // Check if we have more backups than primary files (sign of corruption) if backupFiles.count > primaryFiles.count + 5 { print("โš ๏ธ INTEGRITY: Backup count significantly exceeds primary - potential corruption") return false } // Check if primary is completely empty but we have data elsewhere if primaryFiles.isEmpty && !backupFiles.isEmpty { print("โš ๏ธ INTEGRITY: Primary storage empty but backups exist") return false } return true } func findPreviousAppSupportImages() -> URL? { // Get the Application Support base directory guard let appSupportBase = fileManager.urls( for: .applicationSupportDirectory, in: .userDomainMask ).first else { print("โŒ Could not access Application Support directory") return nil } // Look for OpenClimb directories in Application Support do { let contents = try fileManager.contentsOfDirectory( at: appSupportBase, includingPropertiesForKeys: nil) for url in contents { var isDirectory: ObjCBool = false guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory), isDirectory.boolValue else { continue } // Check if it's an OpenClimb directory but not the current one if url.lastPathComponent.contains("OpenClimb") && url.path != appSupportDirectory.path { let imagesDir = url.appendingPathComponent(imagesDirectoryName) if fileManager.fileExists(atPath: imagesDir.path) { let imageFiles = (try? fileManager.contentsOfDirectory(atPath: imagesDir.path)) ?? [] if !imageFiles.isEmpty { return imagesDir } } } } } catch { print("โŒ Error scanning for previous Application Support directories: \(error)") } return nil } private func migratePreviousAppSupportImages(from sourceDirectory: URL) { print("๐Ÿ”„ Migrating images from previous Application Support directory") do { let imageFiles = try fileManager.contentsOfDirectory(atPath: sourceDirectory.path) for fileName in imageFiles { autoreleasepool { let sourcePath = sourceDirectory.appendingPathComponent(fileName) let destinationPath = imagesDirectory.appendingPathComponent(fileName) let backupPath = backupDirectory.appendingPathComponent(fileName) // Skip if already exists in destination if fileManager.fileExists(atPath: destinationPath.path) { return } do { // Copy to main directory try fileManager.copyItem(at: sourcePath, to: destinationPath) // Create backup try? fileManager.copyItem(at: sourcePath, to: backupPath) print("โœ… Migrated: \(fileName)") } catch { print("โŒ Failed to migrate \(fileName): \(error)") } } } print("โœ… Completed migration from previous Application Support directory") } catch { print("โŒ Failed to migrate from previous Application Support: \(error)") } } }