856 lines
31 KiB
Swift
856 lines
31 KiB
Swift
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("ERROR: 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("WARNING: 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("ERROR: 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("ERROR: 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("ERROR: 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<String>()
|
|
|
|
// 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("ERROR: 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("WARNING: 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("ERROR: Failed to verify migration integrity: \(error)")
|
|
}
|
|
}
|
|
|
|
private func cleanupLegacyDirectory() {
|
|
do {
|
|
try fileManager.removeItem(at: legacyImagesDirectory)
|
|
print("Cleaned up legacy directory")
|
|
} catch {
|
|
print("WARNING: 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("WARNING: Migration state is stale, starting fresh")
|
|
removeMigrationState()
|
|
return nil
|
|
}
|
|
|
|
return state.isComplete ? nil : state
|
|
} catch {
|
|
print("ERROR: 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("ERROR: 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("ERROR: 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("ERROR: 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("ERROR: 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("ERROR: 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("ERROR: 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(
|
|
"WARNING 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("WARNING 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("ERROR: 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: 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("ERROR: Failed to migrate \(fileName): \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
print("Completed migration from previous Application Support directory")
|
|
|
|
} catch {
|
|
print("ERROR: Failed to migrate from previous Application Support: \(error)")
|
|
}
|
|
}
|
|
}
|