--- name: axiom-storage-management-ref description: Use when asking about 'purge files', 'storage pressure', 'disk space iOS', 'isExcludedFromBackup', 'URL resource values', 'volumeAvailableCapacity', 'low storage', 'file purging priority', 'cache management' - comprehensive reference for iOS storage management and URL resource value APIs license: MIT compatibility: iOS 5.0+, iPadOS 5.0+, macOS 10.7+ metadata: version: "1.0.0" last-updated: "2025-12-12" --- # iOS Storage Management Reference **Purpose**: Comprehensive reference for storage pressure, purging policies, disk space, and URL resource values **Availability**: iOS 5.0+ (basic), iOS 11.0+ (modern capacity APIs) **Context**: Answer to "Does iOS provide any way to mark files as 'purge as last resort'?" ## When to Use This Skill Use this skill when you need to: - Understand iOS file purging behavior - Check available disk space correctly - Set purge priorities for cached files - Exclude files from backup - Monitor storage pressure - Mark files as purgeable - Understand volume capacity APIs - Handle "low storage" scenarios ## The Core Question > **"Does iOS provide any way to mark files as 'purge as last resort'?"** **Answer**: Not directly, but iOS provides two approaches: 1. **Location-based purging** (implicit priority): - `tmp/` → Purged aggressively (anytime) - `Library/Caches/` → Purged under storage pressure - `Documents/`, `Application Support/` → Never purged 2. **Capacity checking** (explicit strategy): - `volumeAvailableCapacityForImportantUsage` — For must-save data - `volumeAvailableCapacityForOpportunisticUsage` — For nice-to-have data - Check before saving, choose location based on available space --- ## URL Resource Values for Storage ### Complete Reference Table | Resource Key | Type | Purpose | Availability | |--------------|------|---------|--------------| | `volumeAvailableCapacityKey` | Int64 | Total available space | iOS 5.0+ | | `volumeAvailableCapacityForImportantUsageKey` | Int64 | Space for essential files | iOS 11.0+ | | `volumeAvailableCapacityForOpportunisticUsageKey` | Int64 | Space for optional files | iOS 11.0+ | | `volumeTotalCapacityKey` | Int64 | Total volume capacity | iOS 5.0+ | | `isExcludedFromBackupKey` | Bool | Exclude from iCloud/iTunes backup | iOS 5.1+ | | `isPurgeableKey` | Bool | System can delete under pressure | iOS 9.0+ | | `fileAllocatedSizeKey` | Int64 | Actual disk space used | iOS 5.0+ | | `totalFileAllocatedSizeKey` | Int64 | Total allocated (including metadata) | iOS 5.0+ | ### Checking Available Space (Modern Approach) ```swift // ✅ CORRECT: Check appropriate capacity before saving func checkSpaceBeforeSaving(fileSize: Int64, isEssential: Bool) -> Bool { let homeURL = FileManager.default.homeDirectoryForCurrentUser do { let values = try homeURL.resourceValues(forKeys: [ .volumeAvailableCapacityForImportantUsageKey, .volumeAvailableCapacityForOpportunisticUsageKey ]) if isEssential { // For must-save data (user-created content, critical app data) let importantCapacity = values.volumeAvailableCapacityForImportantUsage ?? 0 return fileSize < importantCapacity } else { // For nice-to-have data (caches, thumbnails) let opportunisticCapacity = values.volumeAvailableCapacityForOpportunisticUsage ?? 0 return fileSize < opportunisticCapacity } } catch { print("Error checking capacity: \(error)") return false } } // Usage if checkSpaceBeforeSaving(fileSize: imageData.count, isEssential: true) { try imageData.write(to: documentsURL.appendingPathComponent("photo.jpg")) } else { showLowStorageAlert() } ``` ### Important vs Opportunistic Capacity **volumeAvailableCapacityForImportantUsage**: - Space reserved for **essential** operations - Use for: User-created content, must-save data - System reserves this space more aggressively - Higher threshold **volumeAvailableCapacityForOpportunisticUsage**: - Space available for **optional** operations - Use for: Caches, thumbnails, pre-fetching - Lower threshold (system may already be under pressure) - Indicates "go ahead if you want, but system is getting full" ```swift // ✅ CORRECT: Different thresholds for different data types func shouldDownloadThumbnail(size: Int64) -> Bool { let capacity = try? FileManager.default.homeDirectoryForCurrentUser .resourceValues(forKeys: [.volumeAvailableCapacityForOpportunisticUsageKey]) .volumeAvailableCapacityForOpportunisticUsage ?? 0 // Only download optional content if there's plenty of space return size < capacity } func canSaveUserDocument(size: Int64) -> Bool { let capacity = try? FileManager.default.homeDirectoryForCurrentUser .resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]) .volumeAvailableCapacityForImportantUsage ?? 0 // User documents are essential return size < capacity } ``` --- ## Backup Exclusion ### isExcludedFromBackup Files in `Caches/` are automatically excluded from backup, but you should **explicitly mark** re-downloadable files in other directories. ```swift // ✅ CORRECT: Exclude large re-downloadable files from backup func markExcludedFromBackup(url: URL) throws { var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true try url.setResourceValues(resourceValues) } // Example: Downloaded podcast episodes func downloadPodcast(url: URL) throws { let appSupportURL = FileManager.default.urls( for: .applicationSupportDirectory, in: .userDomainMask )[0] let podcastURL = appSupportURL .appendingPathComponent("Podcasts") .appendingPathComponent(url.lastPathComponent) // Download file let data = try Data(contentsOf: url) try data.write(to: podcastURL) // Mark as excluded from backup (can re-download) try markExcludedFromBackup(url: podcastURL) } ``` **When to exclude from backup**: - ✅ Downloaded content that can be re-fetched - ✅ Generated thumbnails - ✅ Cached API responses - ✅ Large media files from server - ❌ User-created content (always back up) - ❌ App data that can't be recreated ### Checking Backup Status ```swift // ✅ Check if file is excluded from backup func isExcludedFromBackup(url: URL) -> Bool { let values = try? url.resourceValues(forKeys: [.isExcludedFromBackupKey]) return values?.isExcludedFromBackup ?? false } ``` --- ## Purgeable Files ### isPurgeable Mark files as candidates for automatic purging by the system. ```swift // ✅ CORRECT: Mark cache files as purgeable func markAsPurgeable(url: URL) throws { var resourceValues = URLResourceValues() resourceValues.isPurgeable = true try url.setResourceValues(resourceValues) } // Example: Thumbnail cache func cacheThumbnail(image: UIImage, for url: URL) throws { let cacheURL = FileManager.default.urls( for: .cachesDirectory, in: .userDomainMask )[0] let thumbnailURL = cacheURL.appendingPathComponent(url.lastPathComponent) // Save thumbnail try image.pngData()?.write(to: thumbnailURL) // Mark as purgeable try markAsPurgeable(url: thumbnailURL) // Also exclude from backup var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true try thumbnailURL.setResourceValues(resourceValues) } ``` **Note**: Files in `Caches/` are already purgeable by location. Setting `isPurgeable` is advisory for files in other locations. --- ## Implicit Purge Priority (Location-Based) iOS purges files based on **location**, not explicit priority flags. ### Purge Priority Hierarchy ``` PURGED FIRST (Aggressive): └── tmp/ - Purged: Anytime (even while app running) - Lifetime: Hours to days - Use for: Truly temporary intermediates PURGED SECOND (Storage Pressure): └── Library/Caches/ - Purged: When system needs space - Lifetime: Weeks to months (if space available) - Use for: Re-downloadable, regenerable content NEVER PURGED (Permanent): ├── Documents/ │ - Backed up: ✅ Yes │ - Purged: ❌ Never (unless app deleted) │ - Use for: User-created content │ └── Library/Application Support/ - Backed up: ✅ Yes - Purged: ❌ Never (unless app deleted) - Use for: Essential app data ``` ### Implementation Strategy ```swift // ✅ CORRECT: Choose location based on purge priority needs func saveFile(data: Data, priority: FilePriority) throws { let url: URL switch priority { case .essential: // Never purged - for user-created or critical app data url = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask )[0].appendingPathComponent("important.dat") case .cacheable: // Purged under storage pressure - for re-downloadable content url = FileManager.default.urls( for: .cachesDirectory, in: .userDomainMask )[0].appendingPathComponent("cache.dat") case .temporary: // Purged aggressively - for temp files url = FileManager.default.temporaryDirectory .appendingPathComponent("temp.dat") } try data.write(to: url) // For cacheable files, mark excluded from backup if priority == .cacheable { var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true try url.setResourceValues(resourceValues) } } enum FilePriority { case essential // Never purge case cacheable // Purge under pressure case temporary // Purge aggressively } ``` --- ## Storage Pressure Detection ### Responding to Low Storage ```swift // ✅ CORRECT: Monitor for low storage and clean up proactively class StorageMonitor { func checkStorageAndCleanup() { let homeURL = FileManager.default.homeDirectoryForCurrentUser guard let values = try? homeURL.resourceValues(forKeys: [ .volumeAvailableCapacityForOpportunisticUsageKey, .volumeTotalCapacityKey ]) else { return } let availableSpace = values.volumeAvailableCapacityForOpportunisticUsage ?? 0 let totalSpace = values.volumeTotalCapacity ?? 1 // Calculate percentage let percentAvailable = Double(availableSpace) / Double(totalSpace) if percentAvailable < 0.10 { // Less than 10% free print("⚠️ Low storage detected, cleaning up...") cleanupCaches() } } func cleanupCaches() { let cacheURL = FileManager.default.urls( for: .cachesDirectory, in: .userDomainMask )[0] // Delete old cache files let fileManager = FileManager.default guard let files = try? fileManager.contentsOfDirectory( at: cacheURL, includingPropertiesForKeys: [.contentModificationDateKey] ) else { return } // Sort by modification date let sortedFiles = files.sorted { url1, url2 in let date1 = (try? url1.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate let date2 = (try? url2.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate return (date1 ?? .distantPast) < (date2 ?? .distantPast) } // Delete oldest files first for fileURL in sortedFiles.prefix(100) { try? fileManager.removeItem(at: fileURL) } } } ``` ### Background Cleanup Task ```swift // ✅ CORRECT: Register background task to clean up storage import BackgroundTasks func registerBackgroundCleanup() { BGTaskScheduler.shared.register( forTaskWithIdentifier: "com.example.app.cleanup", using: nil ) { task in self.handleStorageCleanup(task: task as! BGProcessingTask) } } func handleStorageCleanup(task: BGProcessingTask) { task.expirationHandler = { task.setTaskCompleted(success: false) } // Clean up old caches cleanupOldFiles() task.setTaskCompleted(success: true) } ``` --- ## File Size Calculation ### Getting Accurate File Sizes ```swift // ✅ CORRECT: Get actual disk usage (includes filesystem overhead) func getFileSize(url: URL) -> Int64? { let values = try? url.resourceValues(forKeys: [ .fileAllocatedSizeKey, .totalFileAllocatedSizeKey ]) // Use totalFileAllocatedSize for accurate disk usage return values?.totalFileAllocatedSize.map { Int64($0) } } // ✅ Calculate directory size func getDirectorySize(url: URL) -> Int64 { guard let enumerator = FileManager.default.enumerator( at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey] ) else { return 0 } var totalSize: Int64 = 0 for case let fileURL as URL in enumerator { if let size = getFileSize(url: fileURL) { totalSize += size } } return totalSize } // Usage let cacheSize = getDirectorySize(url: cachesDirectory) print("Cache using \(cacheSize / 1_000_000) MB") ``` --- ## Common Patterns ### Pattern 1: Smart Download Based on Available Space ```swift // ✅ CORRECT: Only download optional content if space available func downloadOptionalContent(url: URL, size: Int64) async throws { // Check opportunistic capacity let homeURL = FileManager.default.homeDirectoryForCurrentUser let values = try homeURL.resourceValues(forKeys: [ .volumeAvailableCapacityForOpportunisticUsageKey ]) guard let available = values.volumeAvailableCapacityForOpportunisticUsage, size < available else { print("Skipping download - low storage") return } // Proceed with download let data = try await URLSession.shared.data(from: url).0 try data.write(to: cachesDirectory.appendingPathComponent(url.lastPathComponent)) } ``` ### Pattern 2: Progressive Cache Cleanup ```swift // ✅ CORRECT: Clean up caches when approaching storage limits class CacheManager { func addToCache(data: Data, key: String) throws { let cacheURL = getCacheURL(for: key) // Check if we should clean up first if shouldCleanupCache(addingSize: Int64(data.count)) { cleanupOldestFiles(targetSize: 100 * 1_000_000) // 100 MB } try data.write(to: cacheURL) } func shouldCleanupCache(addingSize: Int64) -> Bool { let homeURL = FileManager.default.homeDirectoryForCurrentUser guard let values = try? homeURL.resourceValues(forKeys: [ .volumeAvailableCapacityForOpportunisticUsageKey ]) else { return false } let available = values.volumeAvailableCapacityForOpportunisticUsage ?? 0 // Clean up if less than 200 MB free return available < 200 * 1_000_000 } func cleanupOldestFiles(targetSize: Int64) { // Delete oldest cache files until under target // (implementation similar to earlier example) } } ``` ### Pattern 3: Exclude Downloaded Media from Backup ```swift // ✅ CORRECT: Downloaded podcast/video management class MediaDownloader { func downloadMedia(url: URL) async throws { let data = try await URLSession.shared.data(from: url).0 // Store in Application Support (not Caches, so it persists) let mediaURL = applicationSupportDirectory .appendingPathComponent("Downloads") .appendingPathComponent(url.lastPathComponent) try data.write(to: mediaURL) // But exclude from backup (can re-download) var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true try mediaURL.setResourceValues(resourceValues) } } ``` --- ## Debugging Storage Issues ### Audit Backup Size ```swift // ✅ Check what's being backed up func auditBackupSize() { let documentsURL = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask )[0] let size = getDirectorySize(url: documentsURL) print("Documents (backed up): \(size / 1_000_000) MB") // Check for large files that should be excluded if size > 100 * 1_000_000 { // > 100 MB print("⚠️ Large backup size - check for re-downloadable files") findLargeFiles(in: documentsURL) } } func findLargeFiles(in directory: URL) { guard let enumerator = FileManager.default.enumerator( at: directory, includingPropertiesForKeys: [.totalFileAllocatedSizeKey] ) else { return } for case let fileURL as URL in enumerator { if let size = getFileSize(url: fileURL), size > 10 * 1_000_000 { // > 10 MB print("Large file: \(fileURL.lastPathComponent) (\(size / 1_000_000) MB)") // Check if excluded from backup if !isExcludedFromBackup(url: fileURL) { print("⚠️ Should this be excluded from backup?") } } } } ``` --- ## Quick Reference | Task | API | Code | |------|-----|------| | Check space for essential file | `volumeAvailableCapacityForImportantUsageKey` | `values.volumeAvailableCapacityForImportantUsage` | | Check space for cache | `volumeAvailableCapacityForOpportunisticUsageKey` | `values.volumeAvailableCapacityForOpportunisticUsage` | | Exclude from backup | `isExcludedFromBackupKey` | `resourceValues.isExcludedFromBackup = true` | | Mark purgeable | `isPurgeableKey` | `resourceValues.isPurgeable = true` | | Get file size | `totalFileAllocatedSizeKey` | `values.totalFileAllocatedSize` | | Purge priority | Location-based | Use `tmp/` or `Caches/` directory | --- ## File Protection Quick Reference Set encryption level per file. See `axiom-file-protection-ref` for full guide. | Level | When Accessible | Use For | |-------|----------------|---------| | `.complete` | Only while unlocked | Passwords, tokens, health data | | `.completeUnlessOpen` | After first unlock if already open | Active downloads, media recording | | `.completeUntilFirstUserAuthentication` | After first unlock (default) | Most app data | | `.none` | Always, even before unlock | Background fetch data, push payloads | ```swift // Set protection on file try data.write(to: url, options: .completeFileProtection) // Set protection on directory try FileManager.default.createDirectory( at: url, withIntermediateDirectories: true, attributes: [.protectionKey: FileProtectionType.complete] ) // Check current protection let values = try url.resourceValues(forKeys: [.fileProtectionKey]) print("Protection: \(values.fileProtection ?? .none)") ``` --- ## Related Skills - `axiom-storage` — Decide where to store files - `axiom-file-protection-ref` — File encryption and security - `axiom-storage-diag` — Debug storage-related issues --- **Last Updated**: 2025-12-12 **Skill Type**: Reference **Minimum iOS**: 5.0 (basic), 11.0 (modern capacity APIs)