1.2.2 - "Bug fixes and improvements"
This commit is contained in:
@@ -3,8 +3,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Manages the overall data state timestamp for sync purposes. This tracks when any data in the
|
||||
/// local database was last modified, independent of individual entity timestamps.
|
||||
/// Manages the overall data state timestamp for sync purposes
|
||||
class DataStateManager {
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
@@ -14,7 +13,6 @@ class DataStateManager {
|
||||
static let initialized = "openclimb_data_state_initialized"
|
||||
}
|
||||
|
||||
/// Shared instance for app-wide use
|
||||
static let shared = DataStateManager()
|
||||
|
||||
private init() {
|
||||
@@ -36,21 +34,21 @@ class DataStateManager {
|
||||
func updateDataState() {
|
||||
let now = ISO8601DateFormatter().string(from: Date())
|
||||
userDefaults.set(now, forKey: Keys.lastModified)
|
||||
print("📝 iOS Data state updated to: \(now)")
|
||||
print("iOS Data state updated to: \(now)")
|
||||
}
|
||||
|
||||
/// Gets the current data state timestamp. This represents when any data was last modified
|
||||
/// locally.
|
||||
func getLastModified() -> String {
|
||||
if let storedTimestamp = userDefaults.string(forKey: Keys.lastModified) {
|
||||
print("📅 iOS DataStateManager returning stored timestamp: \(storedTimestamp)")
|
||||
print("iOS DataStateManager returning stored timestamp: \(storedTimestamp)")
|
||||
return storedTimestamp
|
||||
}
|
||||
|
||||
// If no timestamp is stored, return epoch time to indicate very old data
|
||||
// This ensures server data will be considered newer than uninitialized local data
|
||||
let epochTime = "1970-01-01T00:00:00.000Z"
|
||||
print("⚠️ No data state timestamp found - returning epoch time: \(epochTime)")
|
||||
print("WARNING: No data state timestamp found - returning epoch time: \(epochTime)")
|
||||
return epochTime
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@@ -11,7 +10,7 @@ import SwiftUI
|
||||
@State private var testResults: [String] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
List {
|
||||
StatusSection()
|
||||
|
||||
@@ -263,10 +262,10 @@ import SwiftUI
|
||||
ForEach(testResults.indices, id: \.self) { index in
|
||||
HStack {
|
||||
Image(
|
||||
systemName: testResults[index].contains("✅")
|
||||
systemName: testResults[index].contains("PASS")
|
||||
? "checkmark.circle.fill" : "exclamationmark.triangle.fill"
|
||||
)
|
||||
.foregroundColor(testResults[index].contains("✅") ? .green : .orange)
|
||||
.foregroundColor(testResults[index].contains("PASS") ? .green : .orange)
|
||||
|
||||
Text(testResults[index])
|
||||
.font(.caption)
|
||||
@@ -286,24 +285,24 @@ import SwiftUI
|
||||
|
||||
// Test 1: Check iOS version compatibility
|
||||
if iconHelper.supportsModernIconFeatures {
|
||||
testResults.append("✅ iOS 17+ features supported")
|
||||
testResults.append("PASS: iOS 17+ features supported")
|
||||
} else {
|
||||
testResults.append(
|
||||
"⚠️ Running on iOS version that doesn't support modern icon features")
|
||||
"WARNING: Running on iOS version that doesn't support modern icon features")
|
||||
}
|
||||
|
||||
// Test 2: Check dark mode detection
|
||||
let detectedDarkMode = iconHelper.isInDarkMode(for: colorScheme)
|
||||
let systemDarkMode = colorScheme == .dark
|
||||
if detectedDarkMode == systemDarkMode {
|
||||
testResults.append("✅ Dark mode detection matches system setting")
|
||||
testResults.append("PASS: Dark mode detection matches system setting")
|
||||
} else {
|
||||
testResults.append("⚠️ Dark mode detection mismatch")
|
||||
testResults.append("WARNING: Dark mode detection mismatch")
|
||||
}
|
||||
|
||||
// Test 3: Check recommended variant
|
||||
let variant = iconHelper.getRecommendedIconVariant(for: colorScheme)
|
||||
testResults.append("✅ Recommended icon variant: \(variant.description)")
|
||||
testResults.append("PASS: Recommended icon variant: \(variant.description)")
|
||||
|
||||
// Test 4: Test asset availability
|
||||
validateAssetConfiguration()
|
||||
@@ -316,7 +315,7 @@ import SwiftUI
|
||||
iconHelper.updateDarkModeStatus(for: colorScheme)
|
||||
let variant = iconHelper.getRecommendedIconVariant(for: colorScheme)
|
||||
testResults.append(
|
||||
"✅ Icon appearance test completed - Current variant: \(variant.description)")
|
||||
"PASS: Icon appearance test completed - Current variant: \(variant.description)")
|
||||
}
|
||||
|
||||
private func validateAssetConfiguration() {
|
||||
@@ -327,20 +326,20 @@ import SwiftUI
|
||||
]
|
||||
|
||||
for asset in expectedAssets {
|
||||
testResults.append("✅ Asset '\(asset)' configuration found")
|
||||
testResults.append("PASS: Asset '\(asset)' configuration found")
|
||||
}
|
||||
}
|
||||
|
||||
private func checkBundleResources() {
|
||||
// Check bundle identifier
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? "Unknown"
|
||||
testResults.append("✅ Bundle ID: \(bundleId)")
|
||||
testResults.append("PASS: Bundle ID: \(bundleId)")
|
||||
|
||||
// Check app version
|
||||
let version =
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
|
||||
testResults.append("✅ App version: \(version) (\(build))")
|
||||
testResults.append("PASS: App version: \(version) (\(build))")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +363,7 @@ import SwiftUI
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationStack {
|
||||
VStack(spacing: 30) {
|
||||
Text("Icon Appearance Comparison")
|
||||
.font(.title2)
|
||||
|
||||
@@ -23,7 +23,7 @@ class ImageManager {
|
||||
|
||||
// Final integrity check
|
||||
if !validateStorageIntegrity() {
|
||||
print("🚨 CRITICAL: Storage integrity compromised - attempting emergency recovery")
|
||||
print("CRITICAL: Storage integrity compromised - attempting emergency recovery")
|
||||
emergencyImageRestore()
|
||||
}
|
||||
|
||||
@@ -69,9 +69,9 @@ class ImageManager {
|
||||
attributes: [
|
||||
.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication
|
||||
])
|
||||
print("✅ Created directory: \(directory.path)")
|
||||
print("Created directory: \(directory.path)")
|
||||
} catch {
|
||||
print("❌ Failed to create directory \(directory.path): \(error)")
|
||||
print("ERROR: Failed to create directory \(directory.path): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,9 @@ class ImageManager {
|
||||
var backupURL = backupDirectory
|
||||
try imagesURL.setResourceValues(resourceValues)
|
||||
try backupURL.setResourceValues(resourceValues)
|
||||
print("✅ Excluded image directories from iCloud backup")
|
||||
print("Excluded image directories from iCloud backup")
|
||||
} catch {
|
||||
print("⚠️ Failed to exclude from iCloud backup: \(error)")
|
||||
print("WARNING: Failed to exclude from iCloud backup: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,11 +114,11 @@ class ImageManager {
|
||||
}
|
||||
|
||||
private func performRobustMigration() {
|
||||
print("🔄 Starting robust image migration system...")
|
||||
print("Starting robust image migration system...")
|
||||
|
||||
// Check for interrupted migration
|
||||
if let incompleteState = loadMigrationState() {
|
||||
print("🔧 Detected interrupted migration, resuming...")
|
||||
print("Detected interrupted migration, resuming...")
|
||||
resumeMigration(from: incompleteState)
|
||||
} else {
|
||||
// Start fresh migration
|
||||
@@ -135,7 +135,7 @@ class ImageManager {
|
||||
private func startNewMigration() {
|
||||
// First check for images in previous Application Support directories
|
||||
if let previousAppSupportImages = findPreviousAppSupportImages() {
|
||||
print("📁 Found images in previous Application Support directory")
|
||||
print("Found images in previous Application Support directory")
|
||||
migratePreviousAppSupportImages(from: previousAppSupportImages)
|
||||
return
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class ImageManager {
|
||||
let hasLegacyImportImages = fileManager.fileExists(atPath: legacyImportImagesDirectory.path)
|
||||
|
||||
guard hasLegacyImages || hasLegacyImportImages else {
|
||||
print("✅ No legacy images to migrate")
|
||||
print("No legacy images to migrate")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class ImageManager {
|
||||
let legacyFiles = try fileManager.contentsOfDirectory(
|
||||
atPath: legacyImagesDirectory.path)
|
||||
allLegacyFiles.append(contentsOf: legacyFiles)
|
||||
print("📦 Found \(legacyFiles.count) images in OpenClimbImages")
|
||||
print("Found \(legacyFiles.count) images in OpenClimbImages")
|
||||
}
|
||||
|
||||
// Collect files from Documents/images directory
|
||||
@@ -168,10 +168,10 @@ class ImageManager {
|
||||
let importFiles = try fileManager.contentsOfDirectory(
|
||||
atPath: legacyImportImagesDirectory.path)
|
||||
allLegacyFiles.append(contentsOf: importFiles)
|
||||
print("📦 Found \(importFiles.count) images in Documents/images")
|
||||
print("Found \(importFiles.count) images in Documents/images")
|
||||
}
|
||||
|
||||
print("📦 Total legacy images to migrate: \(allLegacyFiles.count)")
|
||||
print("Total legacy images to migrate: \(allLegacyFiles.count)")
|
||||
|
||||
let initialState = MigrationState(
|
||||
version: MigrationState.currentVersion,
|
||||
@@ -186,24 +186,24 @@ class ImageManager {
|
||||
performMigrationWithCheckpoints(files: allLegacyFiles, currentState: initialState)
|
||||
|
||||
} catch {
|
||||
print("❌ Failed to start migration: \(error)")
|
||||
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)")
|
||||
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")
|
||||
print("Resuming with \(remainingFiles.count) remaining files")
|
||||
performMigrationWithCheckpoints(files: remainingFiles, currentState: state)
|
||||
|
||||
} catch {
|
||||
print("❌ Failed to resume migration: \(error)")
|
||||
print("ERROR: Failed to resume migration: \(error)")
|
||||
// Fallback: start fresh
|
||||
removeMigrationState()
|
||||
startNewMigration()
|
||||
@@ -270,11 +270,11 @@ class ImageManager {
|
||||
completedFiles.append(fileName)
|
||||
migratedCount += 1
|
||||
|
||||
print("✅ Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))")
|
||||
print("Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))")
|
||||
|
||||
} catch {
|
||||
failedCount += 1
|
||||
print("❌ Failed to migrate \(fileName): \(error)")
|
||||
print("ERROR: Failed to migrate \(fileName): \(error)")
|
||||
}
|
||||
|
||||
// Save checkpoint every 5 files or if interrupted
|
||||
@@ -288,7 +288,7 @@ class ImageManager {
|
||||
lastCheckpoint: Date()
|
||||
)
|
||||
saveMigrationState(checkpointState)
|
||||
print("💾 Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)")
|
||||
print("Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ class ImageManager {
|
||||
)
|
||||
saveMigrationState(finalState)
|
||||
|
||||
print("🏁 Migration complete: \(migratedCount) migrated, \(failedCount) failed")
|
||||
print("Migration complete: \(migratedCount) migrated, \(failedCount) failed")
|
||||
|
||||
// Clean up legacy directory if no failures
|
||||
if failedCount == 0 {
|
||||
@@ -313,7 +313,7 @@ class ImageManager {
|
||||
}
|
||||
|
||||
private func verifyMigrationIntegrity() {
|
||||
print("🔍 Verifying migration integrity...")
|
||||
print("Verifying migration integrity...")
|
||||
|
||||
var allLegacyFiles = Set<String>()
|
||||
|
||||
@@ -331,12 +331,12 @@ class ImageManager {
|
||||
allLegacyFiles.formUnion(importFiles)
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to read legacy directories: \(error)")
|
||||
print("ERROR: Failed to read legacy directories: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
guard !allLegacyFiles.isEmpty else {
|
||||
print("✅ No legacy directories to verify against")
|
||||
print("No legacy directories to verify against")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -347,10 +347,10 @@ class ImageManager {
|
||||
let missingFiles = allLegacyFiles.subtracting(migratedFiles)
|
||||
|
||||
if missingFiles.isEmpty {
|
||||
print("✅ Migration integrity verified - all files present")
|
||||
print("Migration integrity verified - all files present")
|
||||
cleanupLegacyDirectory()
|
||||
} else {
|
||||
print("⚠️ Missing \(missingFiles.count) files, re-triggering migration")
|
||||
print("WARNING: Missing \(missingFiles.count) files, re-triggering migration")
|
||||
// Re-trigger migration for missing files
|
||||
performMigrationWithCheckpoints(
|
||||
files: Array(missingFiles),
|
||||
@@ -364,16 +364,16 @@ class ImageManager {
|
||||
))
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to verify migration integrity: \(error)")
|
||||
print("ERROR: Failed to verify migration integrity: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupLegacyDirectory() {
|
||||
do {
|
||||
try fileManager.removeItem(at: legacyImagesDirectory)
|
||||
print("🗑️ Cleaned up legacy directory")
|
||||
print("Cleaned up legacy directory")
|
||||
} catch {
|
||||
print("⚠️ Failed to clean up legacy directory: \(error)")
|
||||
print("WARNING: Failed to clean up legacy directory: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,14 +395,14 @@ class ImageManager {
|
||||
|
||||
// Check if state is too old (more than 1 hour)
|
||||
if Date().timeIntervalSince(state.lastCheckpoint) > 3600 {
|
||||
print("⚠️ Migration state is stale, starting fresh")
|
||||
print("WARNING: Migration state is stale, starting fresh")
|
||||
removeMigrationState()
|
||||
return nil
|
||||
}
|
||||
|
||||
return state.isComplete ? nil : state
|
||||
} catch {
|
||||
print("❌ Failed to load migration state: \(error)")
|
||||
print("ERROR: Failed to load migration state: \(error)")
|
||||
removeMigrationState()
|
||||
return nil
|
||||
}
|
||||
@@ -413,7 +413,7 @@ class ImageManager {
|
||||
let data = try JSONEncoder().encode(state)
|
||||
try data.write(to: migrationStateURL)
|
||||
} catch {
|
||||
print("❌ Failed to save migration state: \(error)")
|
||||
print("ERROR: Failed to save migration state: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ class ImageManager {
|
||||
private func cleanupMigrationState() {
|
||||
try? fileManager.removeItem(at: migrationStateURL)
|
||||
try? fileManager.removeItem(at: migrationLockURL)
|
||||
print("🧹 Cleaned up migration state files")
|
||||
print("Cleaned up migration state files")
|
||||
}
|
||||
|
||||
func saveImageData(_ data: Data, withName name: String? = nil) -> String? {
|
||||
@@ -444,10 +444,10 @@ class ImageManager {
|
||||
// Create backup copy
|
||||
try data.write(to: backupPath)
|
||||
|
||||
print("✅ Saved image with backup: \(fileName)")
|
||||
print("Saved image with backup: \(fileName)")
|
||||
return fileName
|
||||
} catch {
|
||||
print("❌ Failed to save image \(fileName): \(error)")
|
||||
print("ERROR: Failed to save image \(fileName): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -467,7 +467,7 @@ class ImageManager {
|
||||
if fileManager.fileExists(atPath: backupPath.path),
|
||||
let data = try? Data(contentsOf: backupPath)
|
||||
{
|
||||
print("📦 Restored image from backup: \(path)")
|
||||
print("Restored image from backup: \(path)")
|
||||
|
||||
// Restore to primary location
|
||||
try? data.write(to: URL(fileURLWithPath: primaryPath))
|
||||
@@ -497,7 +497,7 @@ class ImageManager {
|
||||
do {
|
||||
try fileManager.removeItem(atPath: primaryPath)
|
||||
} catch {
|
||||
print("❌ Failed to delete primary image at \(primaryPath): \(error)")
|
||||
print("ERROR: Failed to delete primary image at \(primaryPath): \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
@@ -507,7 +507,7 @@ class ImageManager {
|
||||
do {
|
||||
try fileManager.removeItem(at: backupPath)
|
||||
} catch {
|
||||
print("❌ Failed to delete backup image at \(backupPath.path): \(error)")
|
||||
print("ERROR: Failed to delete backup image at \(backupPath.path): \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
@@ -544,7 +544,7 @@ class ImageManager {
|
||||
}
|
||||
|
||||
func performMaintenance() {
|
||||
print("🔧 Starting image maintenance...")
|
||||
print("Starting image maintenance...")
|
||||
|
||||
syncBackups()
|
||||
validateImageIntegrity()
|
||||
@@ -562,11 +562,11 @@ class ImageManager {
|
||||
let backupPath = backupDirectory.appendingPathComponent(fileName)
|
||||
|
||||
try? fileManager.copyItem(at: primaryPath, to: backupPath)
|
||||
print("🔄 Created missing backup for: \(fileName)")
|
||||
print("Created missing backup for: \(fileName)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to sync backups: \(error)")
|
||||
print("ERROR: Failed to sync backups: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,15 +585,15 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Validated \(validFiles) of \(files.count) image files")
|
||||
print("Validated \(validFiles) of \(files.count) image files")
|
||||
} catch {
|
||||
print("❌ Failed to validate images: \(error)")
|
||||
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")
|
||||
print("Cleanup would require coordination with data manager")
|
||||
}
|
||||
|
||||
func getStorageInfo() -> (primaryCount: Int, backupCount: Int, totalSize: Int64) {
|
||||
@@ -623,7 +623,7 @@ class ImageManager {
|
||||
let previousDir = findPreviousAppSupportImages()
|
||||
print(
|
||||
"""
|
||||
📁 OpenClimb Image Storage:
|
||||
OpenClimb Image Storage:
|
||||
- App Support: \(appSupportDirectory.path)
|
||||
- Images: \(imagesDirectory.path) (\(info.primaryCount) files)
|
||||
- Backups: \(backupDirectory.path) (\(info.backupCount) files)
|
||||
@@ -635,7 +635,7 @@ class ImageManager {
|
||||
}
|
||||
|
||||
func forceRecoveryMigration() {
|
||||
print("🚨 FORCE RECOVERY: Starting manual migration recovery...")
|
||||
print("FORCE RECOVERY: Starting manual migration recovery...")
|
||||
|
||||
// Remove any stale state
|
||||
removeMigrationState()
|
||||
@@ -644,7 +644,7 @@ class ImageManager {
|
||||
// Force fresh migration
|
||||
startNewMigration()
|
||||
|
||||
print("🚨 FORCE RECOVERY: Migration recovery completed")
|
||||
print("FORCE RECOVERY: Migration recovery completed")
|
||||
}
|
||||
|
||||
func saveImportedImage(_ imageData: Data, filename: String) throws -> String {
|
||||
@@ -657,12 +657,12 @@ class ImageManager {
|
||||
// Create backup
|
||||
try? imageData.write(to: backupPath)
|
||||
|
||||
print("📥 Imported image: \(filename)")
|
||||
print("Imported image: \(filename)")
|
||||
return filename
|
||||
}
|
||||
|
||||
func emergencyImageRestore() {
|
||||
print("🆘 EMERGENCY: Attempting image restoration...")
|
||||
print("EMERGENCY: Attempting image restoration...")
|
||||
|
||||
// Try to restore from backup directory
|
||||
do {
|
||||
@@ -680,14 +680,14 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
print("🆘 EMERGENCY: Restored \(restoredCount) images from backup")
|
||||
print("EMERGENCY: Restored \(restoredCount) images from backup")
|
||||
} catch {
|
||||
print("🆘 EMERGENCY: Failed to restore from backup: \(error)")
|
||||
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...")
|
||||
print("EMERGENCY: Found previous Application Support images, migrating...")
|
||||
migratePreviousAppSupportImages(from: previousAppSupportImages)
|
||||
return
|
||||
}
|
||||
@@ -696,21 +696,21 @@ class ImageManager {
|
||||
if fileManager.fileExists(atPath: legacyImagesDirectory.path)
|
||||
|| fileManager.fileExists(atPath: legacyImportImagesDirectory.path)
|
||||
{
|
||||
print("🆘 EMERGENCY: Attempting legacy migration as fallback...")
|
||||
print("EMERGENCY: Attempting legacy migration as fallback...")
|
||||
forceRecoveryMigration()
|
||||
}
|
||||
}
|
||||
|
||||
func debugSafeInitialization() -> Bool {
|
||||
print("🐛 DEBUG SAFE: Performing debug-safe initialization check...")
|
||||
print("DEBUG SAFE: Performing debug-safe initialization check...")
|
||||
|
||||
// Check if we're in a debug environment
|
||||
#if DEBUG
|
||||
print("🐛 DEBUG SAFE: Debug environment detected")
|
||||
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")
|
||||
print("DEBUG SAFE: Found migration lock - likely debug interruption")
|
||||
|
||||
// Give extra time for file system to stabilize
|
||||
Thread.sleep(forTimeInterval: 1.0)
|
||||
@@ -732,14 +732,14 @@ class ImageManager {
|
||||
((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).count > 0
|
||||
|
||||
if primaryEmpty && backupHasFiles {
|
||||
print("🐛 DEBUG SAFE: Primary empty but backup exists - restoring")
|
||||
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")
|
||||
print("DEBUG SAFE: Primary empty but found previous Application Support images")
|
||||
migratePreviousAppSupportImages(from: previousAppSupportImages)
|
||||
return true
|
||||
}
|
||||
@@ -755,13 +755,15 @@ class ImageManager {
|
||||
|
||||
// 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")
|
||||
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("⚠️ INTEGRITY: Primary storage empty but backups exist")
|
||||
print("WARNING INTEGRITY: Primary storage empty but backups exist")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -775,7 +777,7 @@ class ImageManager {
|
||||
for: .applicationSupportDirectory, in: .userDomainMask
|
||||
).first
|
||||
else {
|
||||
print("❌ Could not access Application Support directory")
|
||||
print("ERROR: Could not access Application Support directory")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -808,13 +810,13 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("❌ Error scanning for previous Application Support directories: \(error)")
|
||||
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")
|
||||
print("Migrating images from previous Application Support directory")
|
||||
|
||||
do {
|
||||
let imageFiles = try fileManager.contentsOfDirectory(atPath: sourceDirectory.path)
|
||||
@@ -837,17 +839,17 @@ class ImageManager {
|
||||
// Create backup
|
||||
try? fileManager.copyItem(at: sourcePath, to: backupPath)
|
||||
|
||||
print("✅ Migrated: \(fileName)")
|
||||
print("Migrated: \(fileName)")
|
||||
} catch {
|
||||
print("❌ Failed to migrate \(fileName): \(error)")
|
||||
print("ERROR: Failed to migrate \(fileName): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Completed migration from previous Application Support directory")
|
||||
print("Completed migration from previous Application Support directory")
|
||||
|
||||
} catch {
|
||||
print("❌ Failed to migrate from previous Application Support: \(error)")
|
||||
print("ERROR: Failed to migrate from previous Application Support: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,54 +4,36 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
/// Utility for creating consistent image filenames across iOS and Android platforms.
|
||||
/// Uses deterministic naming based on problem ID and timestamp to ensure sync compatibility.
|
||||
/// Utility for creating consistent image filenames across platforms
|
||||
class ImageNamingUtils {
|
||||
|
||||
private static let imageExtension = ".jpg"
|
||||
private static let hashLength = 12 // First 12 chars of SHA-256
|
||||
private static let hashLength = 12
|
||||
|
||||
/// Generates a deterministic filename for a problem image.
|
||||
/// Format: "problem_{hash}_{index}.jpg"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - problemId: The ID of the problem this image belongs to
|
||||
/// - timestamp: ISO8601 timestamp when the image was created
|
||||
/// - imageIndex: The index of this image for the problem (0, 1, 2, etc.)
|
||||
/// - Returns: A consistent filename that will be the same across platforms
|
||||
/// Generates a deterministic filename for a problem image
|
||||
static func generateImageFilename(problemId: String, timestamp: String, imageIndex: Int)
|
||||
-> String
|
||||
{
|
||||
// Create a deterministic hash from problemId + timestamp + index
|
||||
|
||||
let input = "\(problemId)_\(timestamp)_\(imageIndex)"
|
||||
let hash = createHash(from: input)
|
||||
|
||||
return "problem_\(hash)_\(imageIndex)\(imageExtension)"
|
||||
}
|
||||
|
||||
/// Generates a deterministic filename for a problem image using current timestamp.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - problemId: The ID of the problem this image belongs to
|
||||
/// - imageIndex: The index of this image for the problem (0, 1, 2, etc.)
|
||||
/// - Returns: A consistent filename
|
||||
/// Generates a deterministic filename using current timestamp
|
||||
static func generateImageFilename(problemId: String, imageIndex: Int) -> String {
|
||||
let timestamp = ISO8601DateFormatter().string(from: Date())
|
||||
return generateImageFilename(
|
||||
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
|
||||
}
|
||||
|
||||
/// Extracts problem ID from an image filename created by this utility.
|
||||
/// Returns nil if the filename doesn't match our naming convention.
|
||||
///
|
||||
/// - Parameter filename: The image filename
|
||||
/// - Returns: The hash identifier or nil if not a valid filename
|
||||
/// Extracts problem ID from an image filename
|
||||
static func extractProblemIdFromFilename(_ filename: String) -> String? {
|
||||
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format: problem_{hash}_{index}.jpg
|
||||
let nameWithoutExtension = String(filename.dropLast(imageExtension.count))
|
||||
let parts = nameWithoutExtension.components(separatedBy: "_")
|
||||
|
||||
@@ -59,14 +41,10 @@ class ImageNamingUtils {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the hash as identifier
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
/// Validates if a filename follows our naming convention.
|
||||
///
|
||||
/// - Parameter filename: The filename to validate
|
||||
/// - Returns: true if it matches our convention, false otherwise
|
||||
/// Validates if a filename follows our naming convention
|
||||
static func isValidImageFilename(_ filename: String) -> Bool {
|
||||
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
|
||||
return false
|
||||
@@ -79,32 +57,19 @@ class ImageNamingUtils {
|
||||
&& Int(parts[2]) != nil
|
||||
}
|
||||
|
||||
/// Migrates an existing UUID-based filename to our naming convention.
|
||||
/// This is used during sync to rename downloaded images.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - oldFilename: The existing filename (UUID-based)
|
||||
/// - problemId: The problem ID this image belongs to
|
||||
/// - imageIndex: The index of this image
|
||||
/// - Returns: The new filename following our convention
|
||||
/// Migrates an existing filename to our naming convention
|
||||
static func migrateFilename(oldFilename: String, problemId: String, imageIndex: Int) -> String {
|
||||
// If it's already using our convention, keep it
|
||||
|
||||
if isValidImageFilename(oldFilename) {
|
||||
return oldFilename
|
||||
}
|
||||
|
||||
// Generate new deterministic name
|
||||
// Use current timestamp to maintain some consistency
|
||||
let timestamp = ISO8601DateFormatter().string(from: Date())
|
||||
return generateImageFilename(
|
||||
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
|
||||
}
|
||||
|
||||
/// Creates a deterministic hash from input string.
|
||||
/// Uses SHA-256 and takes first 12 characters for filename safety.
|
||||
///
|
||||
/// - Parameter input: The input string to hash
|
||||
/// - Returns: First 12 characters of SHA-256 hash in lowercase
|
||||
/// Creates a deterministic hash from input string
|
||||
private static func createHash(from input: String) -> String {
|
||||
let inputData = Data(input.utf8)
|
||||
let hashed = SHA256.hash(data: inputData)
|
||||
@@ -112,13 +77,7 @@ class ImageNamingUtils {
|
||||
return String(hashString.prefix(hashLength))
|
||||
}
|
||||
|
||||
/// Batch renames images for a problem to use our naming convention.
|
||||
/// Returns a mapping of old filename -> new filename.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - problemId: The problem ID
|
||||
/// - existingFilenames: List of current image filenames for this problem
|
||||
/// - Returns: Dictionary mapping old filename to new filename
|
||||
/// Batch renames images for a problem to use our naming convention
|
||||
static func batchRenameForProblem(problemId: String, existingFilenames: [String]) -> [String:
|
||||
String]
|
||||
{
|
||||
@@ -135,10 +94,7 @@ class ImageNamingUtils {
|
||||
return renameMap
|
||||
}
|
||||
|
||||
/// Validates that a collection of filenames follow our naming convention.
|
||||
///
|
||||
/// - Parameter filenames: Array of filenames to validate
|
||||
/// - Returns: Dictionary with validation results
|
||||
/// Validates that a collection of filenames follow our naming convention
|
||||
static func validateFilenames(_ filenames: [String]) -> ImageValidationResult {
|
||||
var validImages: [String] = []
|
||||
var invalidImages: [String] = []
|
||||
@@ -159,7 +115,7 @@ class ImageNamingUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of image filename validation
|
||||
// Result of image filename validation
|
||||
struct ImageValidationResult {
|
||||
let totalImages: Int
|
||||
let validImages: [String]
|
||||
|
||||
Reference in New Issue
Block a user