---
name: backup-auditor
description: Use when 用户想审计 NAS 上的备份 — "我的备份还新鲜吗"、"备份作业是不是挂了"、"哪些旧备份可以轮转删掉"、"关键目录都有备份吗"、"有没有孤儿备份/空备份"。只读审计:scan 分析备份集(版本轮转/陈旧检测/空备份),coverage 核对关键源目录有无对应备份;LLM 出轮转/补备份计划,用户确认后由 Agent 执行。
  触发词:备份审计、备份检查、备份新鲜度、备份轮转、旧备份清理、备份保留策略、关键目录备份、有没有备份、孤儿备份、空备份、备份作业、Time Machine、快照审计、backup audit、check backups、backup rotation。
  不适用:内容级去重(走 dedup-finder);普通文件整理(走 photo/work/portfolio-organizer);本 skill 只审计「备份的健康度」,不做文件命名规范。
---

# Backup Auditor — 备份健康审计

## 概述

**跨 NAS 通用**:审计脚本跑在**本地挂载路径**上(SMB/NFS),纯 stdlib 零依赖。

备份最怕两件事:**该备的没备**(覆盖缺口)、**备了但早就停了/备坏了**(陈旧、空备份)。
本 skill 做**只读审计**,不碰备份数据:
- `scan` — 把备份目录里的项按「备份集」聚合(同一目标的多版本),查陈旧、单版本、空备份、可轮转旧版本
- `coverage` — 拿源数据目录对照备份目录,查哪些关键目录没备份、哪些是孤儿备份

**写操作不在脚本里** — 审计出问题后,LLM 生成轮转/补备份计划,
经用户确认后由 Agent 执行(旧版本优先 `mv` 归档而非直接 `rm`)。

## Prerequisites

```bash
open smb://<NAS_IP>/备份        # 挂载备份目录
open smb://<NAS_IP>/data        # (coverage 模式)挂载源数据目录
python3 --version               # ≥3.9,无第三方依赖
```

## 命令

| 命令 | 用途 |
|------|------|
| `scan --root BACKUP` | 备份集版本/陈旧/轮转分析(只读) |
| `scan --root BACKUP --stale-days 35 --keep 3` | 自定义陈旧阈值与保留份数 |
| `coverage --source DATA --backup BACKUP` | 关键目录有无备份核对(只读) |
| `--json --output F` | JSON 输出 |

```bash
# 备份集审计:最新超过 35 天算陈旧,每组保留最近 3 份
python3 backup_auditor.py scan --root /Volumes/nas/备份

# 覆盖核对:源 data 下的关键目录是否都有备份
python3 backup_auditor.py coverage --source /Volumes/nas/data --backup /Volumes/nas/备份
```

## 备份集识别

脚本把备份目录下的项按 `base_key` 聚成「备份集」——剥离名字里的
**日期**(`2024-01-01`/`20240101`/`2024年01月01日`)、**版本号**(`v2`/`备份3`/`(1)`)、
**归档扩展名**(`.tar.gz`/`.zip`/`.dmg`/`.sparsebundle`…)后的公共名。

例:`照片备份_2024-01-01.tar.gz`、`照片备份_2024-02-01.tar.gz` → 同一集 `照片备份`,2 个版本。

## scan 检出项

| 项 | 判定 | action |
|----|------|--------|
| 备份陈旧 | 集内最新版本 mtime 超 `--stale-days` | `review-set`(查备份作业) |
| 单版本备份 | 集内只有 1 份(无历史冗余) | `review-set` |
| 空备份 | 0 字节 / 0 文件 | `investigate`(疑似失败) |
| 可轮转旧版本 | 集内版本数 > `--keep`,最旧的若干 | `rotate-out`(归档/删) |

`stats.reclaimable_bytes` = 可轮转旧版本体积合计。

## coverage 检出项

| 项 | 判定 | action |
|----|------|--------|
| 关键目录无备份 | 源目录名在备份集里找不到匹配 | `add-backup` |
| 有备份但陈旧 | 匹配到但最新版本超 `--stale-days` | `refresh-backup` |
| 孤儿备份 | 备份集在源里找不到对应目录 | `review-orphan`(源已删/改名) |

匹配按归一名(base_key)双向包含,容忍 `data照片` ↔ `照片备份` 这类差异。

## 工作流

### 场景 1:用户说"我的备份还正常吗"

**步骤**:
1. **exec** `python3 backup_auditor.py scan --root <备份目录> --output /tmp/backup.json`
2. 读 JSON `stats`:备份集数、陈旧集、单版本集、空备份、可轮转数、可回收空间
3. 汇报:**重点先说陈旧集**(备份作业可能已停)和**空备份**(疑似失败)
4. **不做写操作** — 问用户要不要出轮转/修复计划

### 场景 2:用户说"旧备份太多,清理一下"

**步骤**:
1. 复用 JSON,过滤 `action == rotate-out`
2. 出轮转清单**预览**:每组保留最近 `--keep` 份,其余列出
3. 用户确认后**优先归档而非删除**:
   ```bash
   mkdir -p "/Volumes/nas/备份/_rotated"
   mv -n "/Volumes/nas/备份/照片备份_2023-01-01.tar.gz" "/Volumes/nas/备份/_rotated/"
   ```
   归档一段时间确认无需回溯,再统一删;极空间也可走 `zs mv` / `zs rm`

   **Windows(PowerShell)**:没有 `mkdir -p` / `mv -n`,等价写法——
   ```powershell
   New-Item -ItemType Directory -Force -Path "Z:\备份\_rotated" | Out-Null
   Move-Item "Z:\备份\照片备份_2023-01-01.tar.gz" "Z:\备份\_rotated\"   # 不加 -Force = 不覆盖
   ```
   中文路径先 `chcp 65001`;完整对照见 [skills/README.md](../README.md) 的
   「Windows 用户:执行阶段要换命令」。极空间用户直接用 `zs mv` 可绕开 shell 差异。
4. **删旧备份前必须确认最新版本完好** — 别把唯一可用备份轮转掉

### 场景 3:用户说"关键目录都备份了吗"

**步骤**:
1. **exec** `python3 backup_auditor.py coverage --source <数据目录> --backup <备份目录>`
2. 读 `stats`:已覆盖 / 缺失 / 陈旧 / 孤儿
3. **缺失(add-backup)优先报告** — 这是最大风险
4. 补备份计划交用户确认后由 Agent / 备份工具执行(本 skill 不做实际备份)

## 审计顺序(严格)

1. 先 `scan` 看整体健康:陈旧集 → 空备份 → 单版本 → 可轮转
2. **陈旧/空备份是「备份可能已失效」的信号,优先级最高**(先修复再谈清理)
3. 轮转旧版本前,确认该集最新版本完好且可恢复
4. `coverage` 补缺口:关键目录无备份的,先建备份再优化保留策略
5. 孤儿备份确认源确实已删/改名后再清理
6. 重新 `scan` / `coverage` 验证

## 保留策略速查

| 备份类型 | 建议保留 | 说明 |
|----------|----------|------|
| 每日增量 | 最近 7 天 | `--keep 7` |
| 每周全量 | 最近 4 周 | `--keep 4` |
| 每月归档 | 最近 12 月 | `--keep 12` |
| 关键数据 | ≥2 份 + 异地 | 单版本集要补冗余(3-2-1 原则) |

陈旧阈值按备份频率定:每日备份 `--stale-days 2`,每周 `--stale-days 8`,每月 `--stale-days 35`。

## 关键约束

1. **脚本只读**:无 apply / rm 子命令;轮转/删除由 Agent 在用户确认后执行
2. **轮转优先归档**:旧备份先 `mv` 到 `_rotated/`,确认无需回溯再删
3. **删旧前验新**:轮转/删除任何旧版本前,确认该集最新版本完好可恢复
4. **陈旧 = 修复优先**:陈旧/空备份说明备份作业可能已坏,先修作业再清理
5. **不做实际备份**:本 skill 只审计;补备份交给 NAS 备份工具/Time Machine/rsync
6. **覆盖匹配是启发式**:按归一名双向包含,可能漏配/误配,缺失项人工复核

## 踩坑

| 坑 | 表现 | 解决 |
|----|------|------|
| 备份集拆散 | 命名不统一导致同一目标被分成多集 | base_key 已剥离日期/版本;命名差异大时需人工合并 |
| 稀疏镜像误判空 | `.sparsebundle` 是目录,size 统计可能偏差 | dir_size 递归求和;稀疏bundle 看 file_count 更可靠 |
| mtime 不等于备份时间 | SMB 拷贝可能刷新 mtime | 陈旧判定用 mtime,跨机整理时注意;必要时看名字里的日期 |
| coverage 误报缺失 | 源目录名与备份名差异大 | 双向包含匹配已容忍部分差异;仍误报则人工确认 |
| 超大备份目录卡住 | dir_size 递归慢 | 内置 20000 文件上限;巨型备份集单独审计 |

## 已知 gap

- 不校验备份**可恢复性**(不试解压/挂载),空备份只看 size/file_count
- 不做增量备份的链式完整性检查(需解析备份格式)
- 备份集聚合靠命名启发式,命名极不规范时可能误聚/漏聚
- coverage 的源↔备份匹配是名字包含,不理解备份工具的实际映射关系
- 不识别 NAS 厂商专有备份格式(群晖 HBB、威联通 HBS 等)的内部版本

## 故障排查

| 现象 | 处理 |
|------|------|
| `--root/--source/--backup 不是有效目录` | 确认已挂载:`ls /Volumes/` |
| 备份集全为单版本 | 用户可能用「覆盖式」备份(无历史);确认策略后调 `--keep 1` 免报 |
| coverage 全报缺失 | 源与备份命名体系完全不同;人工建立映射或按子目录逐个核对 |
| dir_size 很慢 | 备份集内文件极多;脚本有文件数上限,超大集单独处理 |
