mcpField
A
axiom-storage-diag

axiom-storage-diag

github.com/charleswiltgen开发1.2k170 次安装今天更新
代码仓库

关于 axiom-storage-diag

面向开发者的 iOS 文件存储诊断技能,用于排查文件丢失、无法访问或备份异常等问题。它从存储位置、文件保护设置和备份排除项入手,提供检查步骤与修正模式。

主要功能

  1. 01诊断存储位置问题
  2. 02检查文件保护设置
  3. 03排查备份体积异常
  4. 04临时文件与缓存处理指南
  5. 05检查备份排除设置

适用场景

  1. 01追查设备重启后文件消失的问题
  2. 02排查设备锁定时后台无法访问文件
  3. 03减少可重新下载内容占用的备份空间

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-storage-diag

安装后在 Claude Code 中输入/axiom-storage-diag确认已加载。

详细介绍

本地文件存储诊断

概述

核心原则 90% 的文件存储问题源于选择了错误的存储位置、误解了文件保护级别,或遗漏了备份排除设置,而非 iOS 文件系统漏洞。

iOS 文件系统经过了数百万个应用和设备的实战验证。如果文件消失、无法访问或导致备份问题,几乎总是存储位置选择或保护配置有误。

危险信号 — 怀疑文件存储问题

如果出现以下任一情况:

  • 设备重启后文件莫名消失
  • 文件随机消失(创建数周后)
  • 应用备份大小异常庞大(>500 MB)
  • 应用进入后台再回到前台后出现“找不到文件”
  • 设备锁定时无法访问文件
  • 用户报告 iOS 更新后数据丢失
  • 后台任务无法访问文件

❌ 禁止“iOS 删除了我的文件,文件系统坏了”

  • iOS 文件系统每天为所有应用处理数十亿个文件
  • 系统行为有文档说明且可预测
  • 99% 的问题都是存储位置或保护设置不匹配

必须先执行的步骤

务必先检查这些项目(再更改代码):

// 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")
    }
}

决策树

文件消失
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
文件无法访问
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
备份过大
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")
    }
    ```

常见症状模式

模式 1:tmp/ 中的文件消失

症状:重启后,甚至在应用运行期间,临时文件也会丢失

原因:系统会积极清理 tmp/

修复:

// ❌ 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)
模式 2:缓存被清理,数据丢失

症状:下载的内容在数周后消失

原因:存储空间不足时会清理 Caches/(预期行为)

修复:按需重新下载,或者如果内容无法重新生成,则移至 Application Support

// ✅ 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)!
}
模式 3:.complete 保护阻止后台访问

症状:后台任务失败并显示“权限被拒绝”

原因:设备锁定时,无法访问采用 .complete 保护的文件

修复:

// ❌ 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
模式 4:下载内容导致备份膨胀

症状:应用备份超过 1 GB,应用被拒绝上架或用户投诉

原因:下载的内容存放在 Documents/ 中,或未标记为排除项

修复:

// ✅ 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)
}

生产环境危机情景

症状:用户报告 iOS 更新后照片丢失

诊断步骤:

  1. 检查存储位置(5 分钟):

    // Were photos in Caches/?
    let photosInCaches = path.contains("/Caches/")
    // If yes → system purged them (expected)
    
  2. 检查是否已备份(5 分钟):

    // Check if excluded from backup
    let excluded = try? url.resourceValues(
        forKeys: [.isExcludedFromBackupKey]
    ).isExcludedFromBackup
    // If excluded=true AND not synced → lost
    
  3. 检查迁移路径(10 分钟):

    • 应用容器路径是否发生变化?
    • 是否已从旧位置迁移数据?

根本原因(90% 的情况):

  • 照片存放在 Caches/ 中(存储空间不足时会被清理)
  • 照片被排除在备份之外,且没有云同步
  • 主要 iOS 更新后缺少迁移代码

修复:

  • 用户照片必须存放在 Documents/ 中
  • 切勿将用户创建的内容排除在备份之外
  • 用户内容务必进行云同步或备份

快速诊断清单

遇到任何存储问题时运行此清单:

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 ===")
}

相关技能

  • axiom-storage — 正确选择存储位置
  • axiom-file-protection-ref — 了解保护级别
  • axiom-storage-management-ref — 清理行为和容量 API

最后更新:2025-12-12 技能类型:诊断

评论

支持 Markdown