--- name: axiom-storage-diag description: Use when debugging 'files disappeared', 'data missing after restart', 'backup too large', 'can't save file', 'file not found', 'storage full error', 'file inaccessible when locked' - systematic local file storage diagnostics license: MIT metadata: version: "1.0.0" last-updated: "2025-12-12" --- # Local File Storage Diagnostics ## Overview **Core principle** 90% of file storage problems stem from choosing the wrong storage location, misunderstanding file protection levels, or missing backup exclusions—not iOS file system bugs. The iOS file system is battle-tested across millions of apps and devices. If your files are disappearing, becoming inaccessible, or causing backup issues, the problem is almost always in storage location choice or protection configuration. ## Red Flags — Suspect File Storage Issue If you see ANY of these: - Files mysteriously disappear after device restart - Files disappear randomly (weeks after creation) - App backup size unexpectedly large (>500 MB) - "File not found" after app background/foreground cycle - Files inaccessible when device is locked - Users report lost data after iOS update - Background tasks can't access files ❌ **FORBIDDEN** "iOS deleted my files, the file system is broken" - iOS file system handles billions of files daily across all apps - System behavior is documented and predictable - 99% of issues are location/protection mismatches ## Mandatory First Steps **ALWAYS check these FIRST** (before changing code): ```swift // 1. Check WHERE file is stored func diagnoseFileLocation(_ url: URL) { let path = url.path if path.contains("/tmp/") { print("⚠️ File in tmp/ - system purges aggressively") } else if path.contains("/Caches/") { print("⚠️ File in Caches/ - purged under storage pressure") } else if path.contains("/Documents/") { print("✅ File in Documents/ - never purged, backed up") } else if path.contains("/Library/Application Support/") { print("✅ File in Application Support/ - never purged, backed up") } } // 2. Check file protection level func diagnoseFileProtection(_ url: URL) throws { let attrs = try FileManager.default.attributesOfItem(atPath: url.path) if let protection = attrs[.protectionKey] as? FileProtectionType { print("Protection: \(protection)") if protection == .complete { print("⚠️ File inaccessible when device locked") } } } // 3. Check backup status func diagnoseBackupStatus(_ url: URL) throws { let values = try url.resourceValues(forKeys: [.isExcludedFromBackupKey]) if let excluded = values.isExcludedFromBackup { print("Excluded from backup: \(excluded)") } } // 4. Check file existence and size func diagnoseFileState(_ url: URL) { if FileManager.default.fileExists(atPath: url.path) { if let size = try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64 { print("File exists, size: \(size) bytes") } } else { print("❌ File does not exist") } } ``` --- ## Decision Tree ### Files Disappeared ``` Files missing? → Check where stored ├─ Disappeared after device restart │ ├─ Was in tmp/? → EXPECTED (tmp/ purged on reboot) │ │ → FIX: Move to Caches/ or Application Support/ │ │ │ ├─ Was in Caches/? → System purged (storage pressure) │ │ → FIX: Move to Application Support/ if can't be regenerated │ │ │ └─ Protection level .complete? → Inaccessible until unlock │ → FIX: Wait for unlock or use .completeUntilFirstUserAuthentication │ ├─ Disappeared randomly (weeks later) │ ├─ In Caches/? → System purged under storage pressure │ │ → EXPECTED if re-downloadable │ │ → FIX: Re-download when needed, or move to Application Support/ │ │ │ └─ In Documents or Application Support/? │ → Check if user deleted app (purges all data) │ → Check iOS update (rare, but check migration path) │ └─ Only some files missing → Check isExcludedFromBackup + iCloud sync → Check if file names have special characters → Check file permissions ``` ### Files Inaccessible ``` Can't access file? ├─ Error: "No permission" or NSFileReadNoPermissionError │ ├─ Device locked? → Check file protection │ │ └─ .complete protection? → Wait for unlock │ │ → FIX: Use .completeUntilFirstUserAuthentication │ │ │ └─ Background task accessing? → .complete blocks background │ → FIX: Change to .completeUntilFirstUserAuthentication │ ├─ File exists but read returns empty/nil │ └─ Check actual file size on disk │ → May be zero-byte file from failed write │ └─ File exists in debugger but not at runtime → Check if using wrong directory (Documents vs Caches) → Check URL construction ``` ### Backup Too Large ``` App backup > 500 MB? ├─ Check Documents directory size │ └─ Large files (>10 MB each)? │ ├─ Can they be re-downloaded? → Move to Caches + isExcludedFromBackup │ └─ User-created? → Keep in Documents (warn user if >1 GB) │ ├─ Check Application Support size │ └─ Downloaded media/podcasts? │ → Mark isExcludedFromBackup = true │ └─ Audit backup with code: ```swift func auditBackupSize() { let docsURL = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask )[0] let size = getDirectorySize(url: docsURL) print("Documents (backed up): \(size / 1_000_000) MB") } ``` ``` --- ## Common Patterns by Symptom ### Pattern 1: Files in tmp/ Disappear **Symptom**: Temp files missing after restart or even during app lifecycle **Cause**: tmp/ is purged aggressively by system **Fix**: ```swift // ❌ WRONG: Using tmp/ for anything that should persist let tmpURL = FileManager.default.temporaryDirectory let fileURL = tmpURL.appendingPathComponent("data.json") try data.write(to: fileURL) // WILL BE DELETED // ✅ CORRECT: Use Caches/ for re-generable data let cacheURL = FileManager.default.urls( for: .cachesDirectory, in: .userDomainMask )[0] let fileURL = cacheURL.appendingPathComponent("data.json") try data.write(to: fileURL) ``` ### Pattern 2: Caches Purged, Data Lost **Symptom**: Downloaded content disappears weeks later **Cause**: Caches/ is purged under storage pressure (expected behavior) **Fix**: Either re-download on demand OR move to Application Support if can't be regenerated ```swift // ✅ CORRECT: Handle missing cache gracefully func loadCachedImage(url: URL) async throws -> UIImage { let cacheURL = getCacheURL(for: url) // Try cache first if FileManager.default.fileExists(atPath: cacheURL.path), let data = try? Data(contentsOf: cacheURL), let image = UIImage(data: data) { return image } // Cache miss - re-download let (data, _) = try await URLSession.shared.data(from: url) try data.write(to: cacheURL) return UIImage(data: data)! } ``` ### Pattern 3: .complete Protection Blocks Background **Symptom**: Background tasks fail with "permission denied" **Cause**: Files with .complete protection inaccessible when locked **Fix**: ```swift // ❌ WRONG: .complete protection for background-accessed files try data.write(to: url, options: .completeFileProtection) // Background task fails when device locked // ✅ CORRECT: Use .completeUntilFirstUserAuthentication try data.write( to: url, options: .completeFileProtectionUntilFirstUserAuthentication ) // Accessible in background after first unlock ``` ### Pattern 4: Backup Bloat from Downloaded Content **Symptom**: App backup >1 GB, app rejected or users complain **Cause**: Downloaded content in Documents/ or not marked excluded **Fix**: ```swift // ✅ CORRECT: Exclude re-downloadable content func downloadPodcast(url: URL) async throws { let appSupportURL = FileManager.default.urls( for: .applicationSupportDirectory, in: .userDomainMask )[0] let podcastURL = appSupportURL .appendingPathComponent("Podcasts") .appendingPathComponent(url.lastPathComponent) // Download let (data, _) = try await URLSession.shared.data(from: url) try data.write(to: podcastURL) // Mark excluded from backup var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true try podcastURL.setResourceValues(resourceValues) } ``` --- ## Production Crisis Scenario **SYMPTOM**: Users report lost photos after iOS update **DIAGNOSIS STEPS**: 1. **Check storage location** (5 min): ```swift // Were photos in Caches/? let photosInCaches = path.contains("/Caches/") // If yes → system purged them (expected) ``` 2. **Check if backed up** (5 min): ```swift // Check if excluded from backup let excluded = try? url.resourceValues( forKeys: [.isExcludedFromBackupKey] ).isExcludedFromBackup // If excluded=true AND not synced → lost ``` 3. **Check migration path** (10 min): - Did app container path change? - Did we migrate data from old location? **ROOT CAUSES** (90% of cases): - Photos in Caches/ (purged under storage pressure) - Photos excluded from backup + no cloud sync - Migration code missing after major iOS update **FIX**: - User photos MUST be in Documents/ - Never exclude user-created content from backup - Always have cloud sync OR backup for user content --- ## Quick Diagnostic Checklist Run this on any storage problem: ```swift func diagnoseStorageIssue(fileURL: URL) { print("=== Storage Diagnosis ===") // 1. Location diagnoseFileLocation(fileURL) // 2. Protection try? diagnoseFileProtection(fileURL) // 3. Backup status try? diagnoseBackupStatus(fileURL) // 4. File state diagnoseFileState(fileURL) // 5. Directory size if let parentURL = fileURL.deletingLastPathComponent() as URL? { let size = getDirectorySize(url: parentURL) print("Parent directory size: \(size / 1_000_000) MB") } print("=== End Diagnosis ===") } ``` --- ## Related Skills - `axiom-storage` — Correct storage location decisions - `axiom-file-protection-ref` — Understanding protection levels - `axiom-storage-management-ref` — Purge behavior and capacity APIs --- **Last Updated**: 2025-12-12 **Skill Type**: Diagnostic