告别万元订阅与格式锁定:深度拆解 Keelhaven —— 专为 Mac 打造的开源隐私备份神器(Swift 6 原生菜单栏 + Restic 工业级加密引擎 + S3/NAS 全链路实操)

告别万元订阅与格式锁定:深度拆解 Keelhaven —— 专为 Mac 打造的开源隐私备份神器(Swift 6 原生菜单栏 + Restic 工业级加密引擎 + S3/NAS 全链路实操)

在数字游民与开发者的日常工作中,数据丢失的恐惧始终如影随形:

  • 一次误执行 rm -rf
  • 一次 Git Rebase 事故覆盖了未提交的代码草稿;
  • 移动硬盘遭遇文件系统损坏,或者固态硬盘遭遇毫无征兆的主控暴毙;
  • 外出办公时 MacBook 意外遗失或进水损坏……

很多人以为开通了 iCloud Drive 就能高枕无忧,但这其实是一个致命的认知误区:同步(Sync)绝对不等于备份(Backup)。当一个文件在本地被勒索病毒加密、被误删或被脏数据污染时,iCloud 会在几毫秒内将这个破坏性的改动忠实地同步到你所有的 Apple 设备上。

那么,macOS 自带的 Time Machine(时间机器) 呢?

  • 时间机器是本地整机灾难恢复的标杆,但它高度依赖插在工位桌上的那块专用外置硬盘;
  • 当你尝试通过局域网 SMB / NAS 跑时间机器时,庞大脆弱的 sparsebundle 镜像经常因为网络波动损坏崩溃;
  • 面对低成本的对象存储(如 AWS S3、Cloudflare R2、Backblaze B2、Wasabi),时间机器完全无法原生支持端到端客户端加密上传。

市面上的商业级工具(如 Arq Backup),虽然支持云端存储,但不仅闭源且售价高昂(单机买断或每年 $60 订阅),更重要的是其采用了专有格式,一旦停止维护,数据提取极其被动。

在开源界,restic 早已是公认的工业级备份王者:基于 Go 语言、支持 AES-256-CTR + Poly1305 强加密、基于 Rabin 指纹的变长内容分块去重(CDC)、支持几乎所有存储后端。然而,restic 是一个纯 CLI 命令行工具 —— 编写 launchd plist 配置繁琐、笔记本合盖休眠后任务经常漏跑、密码管理与运行状态完全缺乏可视化。

开源社区迎来了一款真正补齐 Mac 备份最后一块拼图的作品 —— Keelhaven(由开发者 shenxianpeng 开源)。

它用现代 Swift 6 + 原生 SwiftUI,为久经考验的 restic 引擎穿上了一件极其克制优雅的“菜单栏外衣”:住进 macOS 菜单栏、无 Dock 栏干扰、密钥全进系统钥匙串配合 Touch ID、支持睡眠唤醒补跑与断网锁自愈、全面兼容外置硬盘/S3/SFTP,并且 100% 保持标准 restic 开放格式,零锁定、零遥测、零云端账号!

本文将以架构设计与源码实现的第一视角,深度剖析这款专为 Mac 打造的隐私优先备份神器。


🧭 一眼看穿:macOS 备份方案全景横向对比

在深入技术架构之前,我们先通过客观的维度对比,理清 Keelhaven 在 Mac 备份生态中的精准生态位:

评估维度 Keelhaven (本项目) Apple Time Machine (时间机器) 商业软件 (Arq Backup 7) 开源跨平台 (Kopia Desktop)
开源与许可 100% 开源 (GPLv3) 闭源 (macOS 系统级) 闭源商业软件 开源 (Apache-2.0)
费用模式 完全免费,无订阅、无套路 免费 (系统自带) $49.99 授权费 或 $60/年订阅 免费
底层备份引擎 restic 0.19+ (全球工业级基石) APFS 快照 / FSEvents 专有备份引擎 Kopia 原生引擎
供应商锁定风险 零锁定 (任何电脑用 restic 命令行可秒级恢复) 强绑定 macOS 生态 存在专有格式与版本兼容包袱 需安装 Kopia CLI
端到端加密 AES-256 + Poly1305 (离开 Mac 前完成加密) 仅支持整盘 FileVault 加密 支持端到端加密 支持端到端加密
远端存储兼容性 全兼容 (本地/S3/B2/R2/Wasabi/SFTP/NAS) 局域网 SMB 脆弱,不支持对象存储 支持多种云端对象存储 支持多种云端对象存储
macOS 原生度 极高 (SwiftUI 菜单栏常驻,仅 20MB 内存) 深度系统集成 独立大型主窗口应用 臃肿的 Electron 套壳 (150MB+ 内存)
钥匙串与生物识别 系统 Keychain + Touch ID 密码提取 系统钥匙串 专有密码管理 独立应用主密码
遥测与隐私 零统计、零遥测、零外部服务器 遵循 Apple 隐私协议 包含许可校验与商业网络请求 社区标准构建

[!NOTE]
黄金 3-2-1 备份法则的最佳搭档
Keelhaven 并非用于取代时间机器,而是天作之合:

  • Time Machine 负责工位桌面上那块移动硬盘的整机全盘快照,专治系统崩溃与新机迁移;
  • Keelhaven 负责你最核心的个人资产(代码库、Obsidian 笔记、财务票据、精修照片、密钥配置),通过客户端强加密实时异步推送到异地的 S3 云存储或家庭 NAS,抵御火灾、盗窃与物理硬件损毁!

一、 系统架构:严格分层的双模块哲学

在软件工程中,许多工具往往因为把业务逻辑、CLI 进程调用和 UI 渲染混在一起,导致后期难以维护且极易崩溃。

Keelhaven 在设计之初就在 docs/ARCHITECTURE.md 中确立了坚不可摧的双层硬隔离架构

flowchart TD
    subgraph AppTarget["Keelhaven (SwiftUI 应用主工程)"]
        UI["MenuBar 菜单栏视图 + 引导向导 (Wizard)"]
        AppState["@MainActor @Observable AppState (全局状态中枢)"]
        Services["系统服务薄包装: SchedulerService / NotificationService / BiometricAuth"]
        UI --> AppState
        AppState --> Services
    end

    subgraph CoreTarget["KeelhavenCore (纯 SwiftPM 核心引擎包 - 零 UI 依赖)"]
        Models["领域模型: BackupPlan / Destination / Schedule / RetentionPolicy"]
        Keychain["凭据隔离: KeychainStoring / KeychainStore"]
        SchedMath["纯函数调度数学: SchedulePolicy / CheckPolicy / PrunePolicy"]
        ResticRunner["异步进程执行器: ResticRunner (Actor)"]
        Parsers["NDJSON 流式解析器: ResticJSON (基于 restic 真实 Fixture)"]
        
        ResticRunner --> Parsers
    end

    subgraph ExternalEngine["底层随包内置二进制"]
        ResticBin["restic Universal Binary (arm64 + x86_64, ≥ 0.19)"]
    end

    AppState ==>|依赖与调用| CoreTarget
    ResticRunner ==>|fork minimal clean process| ResticBin

1. 架构核心优势

  1. KeelhavenCore 纯逻辑自治:完全剥离了 AppKit / SwiftUI 视图层,涵盖了所有数据模型、进程管道、Keychain 存储协议与调度算法。其所有代码在 GitHub Actions CI 中强制执行 llvm-cov 100% 行级测试覆盖率门禁(Line Coverage Gate)
  2. 免除一切环境依赖(No-Homebrew Needed):传统开源 GUI 经常要求用户先跑 brew install xxx。Keelhaven 在构建期通过 Scripts/fetch-restic.sh 校验 SHA256 签名,直接将通用的 arm64 + x86_64 restic 原生二进制封装到 .app 包内的 Contents/MacOS/restic 中,下载打开即用;
  3. 单例排队执行(Serialized Backups):全应用生命周期内严格保证同一时间仅有一个 restic 进程在运行。多计划备份、定时体检与空间回收严格串行排队,从根源上杜绝了对同一个存储仓库的并发加锁冲突。

二、 核心技术与硬核工程实现深度剖析

1. 绝不泄露秘密:零命令行落盘的 Keychain 凭据体系

在 Linux 或 macOS 终端中,许多开发者习惯通过 restic backup -p password.txt 或命令行参数传递凭据。这是一个巨大的安全隐患 —— 系统上的任何未授权进程都可以通过 ps aux | grep restic 直接截获明文密码!

Keelhaven 在 RepoCredentials.swiftKeychainStore.swift 中构建了坚不可摧的凭据管道:

sequenceDiagram
    autonumber
    participant App as AppState (主线程)
    participant KC as macOS 系统 Keychain (钥匙串)
    participant Runner as ResticRunner (Swift Actor)
    participant Child as restic 子进程

    App->>KC: 检索该计划 UUID 对应的加密凭据
    KC-->>App: 返回内存中的私有密码与 S3 密钥
    App->>Runner: 构造 RepoCredentials 注入 Actor
    Note over Runner: 构建极简白名单环境字典 (Minimal Clean Environment)<br/>只保留 PATH、TMPDIR 等基础系统变量
    Runner->>Runner: 注入 RESTIC_PASSWORD, AWS_ACCESS_KEY_ID...
    Runner->>Child: Process.run() 仅通过进程私有内存环境注入
    Note over Child: 运行期间 argv 只有 ["backup", "--json"]<br/>ps aux 绝无明文泄露,磁盘零明文残留!
  • Touch ID 生物识别提取:在 BiometricAuthService 中,用户如果想查看或拷贝自己当初设置的仓库密码,必须通过 Mac 原生 Touch ID 指纹鉴权,防止他人借用电脑时窃取密码;
  • 凭据生命周期与计划解绑销毁:当用户删除某个备份计划时,系统会原子化地擦除 Keychain 中的对应条目。

2. 毫秒级异步流式解析:NDJSON + Swift AsyncThrowingStream

restic 在执行备份时,提供了一个强大的 --json 标志,能够实时以换行分隔的 JSON 流(Newline Delimited JSON, NDJSON)输出备份进度:

{"message_type":"status","percent_done":0.42,"total_files":1520,"bytes_done":104857600}

ResticRunner.swift 中,Keelhaven 没有采用低效的定时轮询或全量缓冲,而是充分利用 Swift 现代并发特性,将 POSIX Pipe 的输入流接入 bytes.lines 异步迭代器:

// ResticRunner.swift 核心异步流式管道实现节选
public nonisolated func backupStream(
    _ command: ResticCommand,
    destination: Destination,
    credentials: RepoCredentials
) -> AsyncThrowingStream<BackupProgressEvent, Error> {
    AsyncThrowingStream { continuation in
        let process = Process()
        process.executableURL = self.binaryURL
        process.arguments = command.arguments
        process.environment = credentials.environment(for: destination)
        process.standardInput = FileHandle.nullDevice

        let stdoutPipe = Pipe()
        let stderrPipe = Pipe()
        process.standardOutput = stdoutPipe
        process.standardError = stderrPipe

        let exitCodes = AsyncStream<Int32> { exitContinuation in
            process.terminationHandler = { finished in
                exitContinuation.yield(finished.terminationStatus)
                exitContinuation.finish()
            }
        }

        let worker = Task {
            do {
                try process.run()
            } catch {
                continuation.finish(throwing: ResticError.binaryNotFound)
                return
            }

            // 监听 stdout 的行切片异步流
            var lines = stdoutPipe.fileHandleForReading.bytes.lines.makeAsyncIterator()
            while let line = (try? await lines.next()) ?? nil {
                if let event = ResticJSON.decodeProgressEvent(fromLine: line) {
                    continuation.yield(event) // 实时通知 UI 驱动菜单栏进度条
                }
            }

            // 获取退出码并根据错误模型分类
            var exitCode: Int32 = -1
            for await code in exitCodes { exitCode = code }

            if exitCode == 0 {
                continuation.finish()
            } else {
                let stderrData = try? stderrPipe.fileHandleForReading.readToEnd()
                let stderrText = String(decoding: stderrData ?? Data(), as: UTF8.self)
                continuation.finish(throwing: ResticError.classify(exitCode: exitCode, stderr: stderrText))
            }
        }

        // 优雅取消:用户点击停止时发送 SIGINT,触发 restic 干净解锁并退出
        continuation.onTermination = { termination in
            guard case .cancelled = termination else { return }
            if process.isRunning {
                process.interrupt() // 发送 SIGINT 触发 restic 自身释放锁
            }
            worker.cancel()
        }
    }
}

[!TIP]
优雅取消的黑科技:普通软件在强制终止进程时往往发送 SIGKILL,这会导致 restic 的仓库锁(Lock)残留在远程存储桶中。Keelhaven 在监听到任务取消时调用 process.interrupt()(即发送 SIGINT),restic 捕获该信号后会先清理远程仓库锁,再干净自毁退出


3. 休眠唤醒与纯函数调度数学(SchedulePolicy

许多运行在后台的备份工具,最让人头疼的是 MacBook 合盖休眠后,原本定在下午 3 点的计划就彻底“漏跑”了,直到第二天同一时间才会再次触发。

Keelhaven 将调度逻辑剥离为纯函数(Pure Functions)SchedulePolicy

public enum SchedulePolicy {
    /// 判定某个计划在给定的时间点是否已经“逾期未跑”
    public static func isDue(
        _ plan: BackupPlan,
        now: Date,
        calendar: Calendar = .current
    ) -> Bool {
        // 从未运行过的计划立即执行
        guard let lastRunDate = plan.lastRun?.date else {
            return true
        }
        // 计算上一次运行之后理应触发的下一次时间节点
        return nextRun(for: plan.schedule, after: lastRunDate, calendar: calendar) <= now
    }
}

AppState.swift 中,调度器不仅每 60 秒轮询一次,还会监听 macOS 原生工作区通知:

NSWorkspace.shared.notificationCenter.addObserver(
    forName: NSWorkspace.didWakeNotification,
    object: nil,
    queue: .main
) { _ in
    AppState.shared.runDuePlans()
}

当你合上电脑外出开会、两小时后重新翻开屏幕唤醒 Mac 时,Keelhaven 在唤醒的一瞬间便会检测到错过的窗口,立即静默自动补齐备份!


4. 尾随式生命周期:体检(Check)与空间回收(Prune)的工程智慧

restic 的两个高级命令:

  1. restic check:全量遍历索引树,校验仓库完整性;
  2. restic forget --prune:按策略丢弃老快照,并重新打包(Repack)数据块,回收存储空间。

很多初级开发者会简单地把这两个操作也做成独立的定时器,但这在真实网络环境下是灾难性的:--prune 重打包会消耗大量网络上行下行流量与 API 调用次数;且如果单独触发,很容易与常规备份争抢仓库排他锁。

Keelhaven 采用了极其聪明的 “尾随机制(Tail-Riding Lifecycle)”

flowchart LR
    Start([定时器触发]) --> RunBackup["① 执行增量备份 (restic backup)"]
    RunBackup --> BackupSuccess{"备份成功?"}
    
    BackupSuccess -- 否 --> AlertFail["弹窗报警并在行内显示失败原因"]
    
    BackupSuccess -- 是 --> CheckDue{"CheckPolicy.isDue?<br/>(默认每周最多1次)"}
    CheckDue -- 是 --> RunCheck["② 尾随运行完整性检查 (restic check)"]
    CheckDue -- 否 --> PruneDue
    
    RunCheck --> PruneDue{"PrunePolicy.isDue?<br/>(保留策略开启且满7天)"}
    PruneDue -- 是 --> RunPrune["③ 尾随回收空间 (restic forget --prune)"]
    PruneDue -- 否 --> Finish([任务安静结束])
    RunPrune --> Finish
  • 目标确定可达:只有在主备份 100% 成功后,才判定当前远端存储可连通;
  • 消除排他锁争抢:同一个串行队列下紧接着执行 checkprune,完全避免锁竞争;
  • 智能频次限流:空间回收极为消耗带宽,因此即便你设置的是“每小时备份”,PrunePolicy 也强制将其拦截在每周最多执行一次,在空间回收与云端 API 账单之间达成了完美平衡。

5. 孤立锁自愈机制(Orphan Lock Recovery)

当网络突然断开、或者电脑电量耗尽意外关机时,restic 在远端仓库中留下的独占锁文件可能无法被清除,导致后续所有备份持续报错 repository is already locked (code 11)

传统方式下,用户必须打开终端自己敲 restic unlock。而在 Keelhaven 中:

  • AppState 精准识别 restic 的退出码 11,在 UI 状态行中呈现独特的 .failedLocked 状态;
  • 不展示冷冰冰的报错红字,而是动态呈现一个 “解除锁定并重试 (Unlock)” 按钮;
  • 内部调用安全模式的 restic unlock(不带 --remove-all),只清除属主已失效的僵尸锁,绝不误伤其他 Mac 正在进行的合法备份!

三、 实战上手:从零搭建双重容灾备份体系

现在,我们以实操视角,带你一步步配置一套符合专业标准的备份方案。

1. 极速安装

方式 A:Homebrew Cask 一键安装(推荐)

brew install --cask shenxianpeng/tap/keelhaven

方式 B:终端极简脚本安装

curl -fsSL https://keelhaven.app/install.sh | bash

[!TIP]
关于初次打开的 macOS 门禁提示
由于 Keelhaven 为纯免费开源软件,尚未向 Apple 支付昂贵的商业开发者年费进行公证,首次双击打开若提示“无法验证开发者”:

  • macOS 15 (Sequoia):打开 系统设置 › 隐私与安全性,拉到底部点 仍要打开
  • 开发者极客一条命令:终端执行 xattr -d com.apple.quarantine /Applications/Keelhaven.app 即可永久移除系统隔离标记。

2. 场景实操一:备份至随身外置移动固态硬盘(SSD)

  1. 点击 macOS 顶部菜单栏的 Keelhaven 图标,点击 “新建计划 (New Plan)”
  2. 选择源目录:通过原生文件选择器挑选你的重要文件夹(如 ~/Documents~/Projects~/Pictures);
  3. 选择存储目的地:点击 “本地或外部驱动器 (Local Directory)”,选择移动硬盘挂载路径(如 /Volumes/ExtremeSSD/KeelhavenBackup);
  4. 设置独立加密密码:输入并牢记密码,Keelhaven 会自动将其加密注入 macOS Keychain;
  5. 设定频率:选择每天或每周定时触发。

3. 场景实操二:备份至任意兼容 S3 的对象存储(Cloudflare R2 / Backblaze B2 / AWS)

Cloudflare R2(每月提供 10GB 免费存储且零出网流量费)为例:

  1. 在 Cloudflare 控制台创建 R2 Bucket(例如 my-mac-backup),生成 Access Key IDSecret Access Key,复制 S3 Endpoint 地址:
    https://<account_id>.r2.cloudflarestorage.com
  2. 在 Keelhaven 新建计划中选择 “S3 兼容存储 (S3 Bucket)”
    • Endpointhttps://<account_id>.r2.cloudflarestorage.com
    • Bucketmy-mac-backup
    • Regionauto
    • Access Key / Secret Key:填入凭据(保存在系统钥匙串中)
  3. 设置仓库加密主密码,点击创建。Keelhaven 将自动调用 restic init 初始化远端存储桶,并开启毫秒级增量流式备份!

4. 极致无锁定证明:脱离 Keelhaven 恢复数据的黑客演练

假设你的电脑丢失了,手头只有一台普通的 Linux 服务器或另一台新 Mac,没有任何 Keelhaven 客户端,你如何取回你的数据?

这正是 Keelhaven 最令人安心的地方 —— 没有任何私有协议封装

# 1. 任意机器安装原生 restic 命令行
brew install restic   # 或 apt install restic

# 2. 导出你在 Keychain 里保存的密码与 S3 凭据
export RESTIC_REPOSITORY="s3:https://<account_id>.r2.cloudflarestorage.com/my-mac-backup"
export RESTIC_PASSWORD="YourSecretPassword"
export AWS_ACCESS_KEY_ID="your-r2-key"
export AWS_SECRET_ACCESS_KEY="your-r2-secret"

# 3. 查看所有历史快照
restic snapshots

# 输出示例:
# ID        Time                 Host        Tags        Paths
# ----------------------------------------------------------------------
# 4f8a12b0  2026-09-05 20:30:00  MacBook     keelhaven   /Users/me/Projects
# 9d3c5e7a  2026-09-06 18:00:00  MacBook     keelhaven   /Users/me/Projects

# 4. 一键将最新快照恢复至本地当前目录
restic restore latest --target ./RestoredData

数据完整如初!你的资产掌控权永远紧握在自己手中,不受任何商业公司兴衰的裹挟。


四、 总结与极客思考

在软件行业充斥着“为了上云而上云”、“为了收费而发明私有格式”的今天,Keelhaven 的出现如同一缕清流:

  1. 小而美的 Unix 哲学:它没有重造一套脆弱的备份算法轮子,而是站在巨人肩膀上,将世界上最顶级的加密引擎(restic)与 Apple 生态中最优雅的人机交互(SwiftUI 菜单栏常驻、Keychain、Touch ID)进行了严丝合缝的拼接;
  2. 绝对的数字自主权:拒绝账号体系,拒绝中转云端,备份目的地完全由用户自主掌控;
  3. 教科书级的现代 Swift 工程典范:100% 测试覆盖率的纯逻辑核心包、基于 AsyncSequence 的进程流式迭代、纯函数无副作用的调度判定。

如果你正在寻找一款能够守护你多年心血资产、静默无感、在关键时刻绝不掉链子的 macOS 异地备份方案,Keelhaven 绝对是你 Mac 菜单栏中那个最值得信赖的“避风港(Haven)”。


项目仓库GitHub - shenxianpeng/keelhaven
官方网站keelhaven.app
开源协议:GPL-3.0 License (restic 引擎为 BSD-2-Clause)
运行要求:macOS 14.0+ (Apple Silicon / Intel Universal)