☰
bup join 详解:从 bup 仓库拼接并还原数据的核心工具
2026/9/28 8:24:03 网站建设 项目流程
  • 灾备
  • CLI
  • 存储

【免费下载链接】bup

Very efficient backup system based on the git packfile format, providing fast incremental saves and global deduplication (among and within files, including virtual machine images). Please post problems or patches to the mailing list for discussion (see the end of the README below).

项目地址:https://gitcode.com/gh_mirrors/bu/bup
点击查看免费下载

bup(Backup Utility Program)是一套基于 git packfile 格式的高效备份系统,其核心优势在于滚动校验和的块级去重:无论文件内容如何变动,稳定的分块边界都能让绝大多数数据在后续备份中避免重复存储。bup join正是这套流水线的“还原端”命令——它大致是bup split的逆操作,用于从本地或远程 bup 仓库中取回某个文件(或整棵目录树、整条提交历史)的原始字节流。阅读完本文,你将掌握bup join的完整语法、-r/--remote远程仓库配置、基于 git ref/hash 的寻址方式,以及它配合tar完成“备份-还原”闭环的实战用法,并深入理解其底层的对象遍历与网络协议实现。

命令概述:bup join是什么

根据 bup-join 手册页,bup join被定义为"concatenate files from a bup repository"(从 bup 仓库拼接文件),它是 bup-split 的逆操作:bup split把文件切成约 8KB 的块存入仓库,bup join则把仓库中保存的块重新拼接成完整的原始字节流并输出到标准输出。

关键设计点在于,bup join的参数不是普通的文件路径,而是任意 git 对象引用(ref)或哈希,包括:

  • 分支名(branch names),例如mybackup
  • 提交 ID(commit ids)
  • 树 ID(tree ids)
  • 块 ID(blob ids)

也就是说,只要能在仓库中找到对象,join就能还原它。对 commit 或 tree 进行 join 时,命令会递归地遍历其下所有可到达的 blob,把它们的内容按序拼接为一个整体;对 blob 进行 join 时则直接输出该 blob 的内容。

注意:bup join处理的是"原始数据字节流"。它不理解 tar 归档或普通目录的层次结构——那是 bup restore、bup fuse、bup ftp等工具的工作。join的典型用法是把 bup 当作一个"带内容寻址的、全局去重的对象流仓库",配合tar等外部管道消费拼接结果。

语法与选项

完整语法

bup join [-r host:path] [refs or hashes...]
  • refs or hashes...:要拼接的一个或多个 git 引用或对象哈希,格式可以是git(1)接受的一切形式(分支名、commit/tree/blob 的完整或缩写哈希等)。
  • 若命令行上未提供任何 ref/hash,bup join会从标准输入逐行读取它们(每行一个)。

-r, --remote=[user@]host:[path],--remote=URL

从指定远程仓库取回数据,默认通过 SSH 传输。远程路径的完整语义参见 bup 主手册的 REMOTE OPTIONS 章节:

  • 支持两种格式:URL(如ssh://...、bup://...)或[user@]host:[path]的 SCP 风格写法。判断规则是:只要参数以包含 "authority" 的合法 URL scheme 前缀(即SCHEME://)开头,且 scheme 为ssh或bup,就按 URL 处理;否则按user@host:path解析。
  • 两种格式下,若未给出 path,则使用远程服务器上的默认仓库路径(远程环境变量BUP_DIR若已设置则用之,否则为~/.bup);SSH 连接参数可通过~/.ssh/config中的自定义 host 条目配置(见ssh_config(5))。
  • 在user@host:path语法中,若有@符号,则最右侧@之前的所有内容都属于用户名,例如-r x@y@z表示用户x@y、主机z;host 后面必须跟冒号,冒号之后的部分为路径。
  • 官方手册建议:在完全通用的场景下优先使用 URL 形式,避免歧义。例如ssh://x/y这种本意为"主机 ssh、路径 //x/y"的写法会被解析为"主机 x、路径 /y"的 URL。

从 stdin 读取引用

当命令行参数为空时,join从 stdin 逐行读取引用,每一行对应一个 ref。这一点在源码中有清晰实现:lib/bup/cmd/join.py 中,当extra(位置参数)为空时,调用linereader(stdin)逐行产出引用;linereader定义于 lib/bup/helpers.py,作用是从文件对象中逐行产出并去掉行尾换行符。

输出目标文件:-o(源码补充)

join.py 的实现 还暴露出手册页未单独列出的隐藏选项-o, --output:当指定-o时,拼接结果写入指定文件而非标准输出。这正是 test/ext/test-split-join 中bup join <tags2t.tmp -o out2t.tmp一行所验证的行为。对于不熟悉手册的读者,这同样是一个可用的实操能力。

行为细节

  • 多个 ref 会按命令行顺序依次拼接,输出到同一个输出流(for ref in ...: for blob in src.join(ref): outfile.write(blob),见 lib/bup/cmd/join.py)。
  • 若某个 ref 在仓库中不存在,命令会打印形如error: <ref> does not exist的错误并返回失败退出码(源码中对KeyError的处理,见 lib/bup/cmd/join.py)。
  • 成功时退出码为 0,失败时为 1(EXIT_FAILURE/EXIT_SUCCESS来自 lib/bup/helpers.py)。

工作原理:从引用到字节流的递归遍历

bup join的核心逻辑位于虚拟文件系统层。仓库接口RepoProtocol定义了join(ref)契约(见 lib/bup/repo/base.py),本地仓库将其实现为vfs.join(见 lib/bup/repo/local.py)。

lib/bup/vfs.py 中的 join 函数 是理解整条链路的关键,它的递归策略如下:

  1. 先用get_ref(repo, ref)解析引用,得到(oidx, type, size, data_iter)四元组;若解析不到,抛出ref <ref> does not exist的GitError(lib/bup/vfs.py)。
  2. 对解析结果递归_join:
    • blob:直接逐块产出内容迭代器it中的数据;
    • tree:用tree_iter遍历树条目(模式、名字、子对象哈希),对每个子条目递归_join;
    • commit:从提交对象内容中解析出tree <hash>首行,取其后的树哈希递归遍历(lib/bup/vfs.py);
    • 其他类型则抛出GitError,提示不是 blob/tree/commit。

换言之,join产出的字节流 = 以给定对象为根、深度优先遍历得到的全部 blob 内容的拼接。这个语义决定了几个实用推论:

  • 对bup split -t得到的树 ID做 join,能得到整棵树的原始数据流;
  • 对bup split -n name得到的分支/提交做 join,能得到该次保存的全部数据;
  • 由于bup split按滚动校验和分块、按序写入仓库,拼接顺序与拆分顺序严格一致,因此 join 的结果与输入字节流逐字节相等——这也是split/join可逆性的保证。

对象内容本身由 git 对象管道CatPipe读取(本地仓库的cat实现为git.catpipe(...).get(ref),见 lib/bup/repo/local.py 与 lib/bup/git.py),它复用持久化的git cat-file --batch子进程按需拉取对象,避免每次调用都重启进程。

远程仓库:join的网络协议路径

当指定-r时,join走的是远程仓库实现。RemoteRepo.join直接委托给客户端协议(self.join = self.client.join,见 lib/bup/repo/remote.py)。

客户端侧 lib/bup/client.py 的 join 实现 值得注意:它发送的是cat命令而不是join命令,源码注释明确说明这样"可以兼容旧版本服务器";随后从连接中循环读取 4 字节大端长度前缀(struct.unpack('!I', ...)),为零则终止,否则按长度读取并产出数据块。

服务器侧 lib/bup/protocol.py 的 join 命令处理 则负责真正的遍历:服务器调用本地self.repo.join(id)逐个产出 blob,每个 blob 以 4 字节大端长度前缀封装后写入连接;若对象缺失(KeyError),写入\0\0\0\0终止并向客户端报错。同一函数还被cat = join这一别名复用(lib/bup/protocol.py),这也解释了为何客户端可以"用 cat 实现 join"。

由此可以勾勒出bup join -r host:path的完整数据流:

本地 bup join └─ RemoteRepo.join ──> Client.join(发送 cat 命令) └─ SSH/连接 ──> 远程 bup server └─ protocol.join ──> LocalRepo.join ──> vfs.join(递归遍历) └─ 按 4 字节长度前缀逐块回传 └─ 逐块写入 stdout / -o 文件

整个过程的关键点在于:遍历发生在服务器端,客户端只负责按帧读取与转发,因此大量数据不会在两端之间冗余往返,网络开销被控制在"每个块一个长度前缀"的水平。

实战用法示例

例 1:split 与 join 的完整闭环(使用树 ID)

来自 bup-join 手册的 EXAMPLES:

# 先把 /etc 打成 tar 流,交给 bup split 分块入库,并输出树 ID TREE=$(tar -cvf - /etc | bup split -t) # 用树 ID 把数据 join 回来,再用 tar 列出归档内容 bup join $TREE | tar -tf -

这里tar -cvf - /etc产出的是一个无结构的 tar 字节流,bup split -t只关心"分块并入库",输出树 ID;bup join按树 ID 还原同一字节流,tar -tf -再把它当作归档解读。因为split与join完全对称,中间隔着多少个备份周期都不影响还原的正确性。

例 2:多次备份与历史回退(分支加~1语法)

同样来自手册 EXAMPLES:

# 对同一份数据做两次备份,第二次会基于第一次去重 tar -cvf - /etc | bup split -n mybackup tar -cvf - /etc | bup split -n mybackup # mybackup~1 是 git 语法:分支 mybackup 上倒数第二次提交 bup join mybackup~1 | tar -tf -

要点如下:

  • bup split -n mybackup会把新数据集作为同名分支的后代提交,从而保留历史(详见 bup-split 手册:若 name 已存在,新数据集被视为旧 name 的后代;数据本身还以名为data的顶层文件暴露在 VFS 中,可通过bup fuse/bup ftp访问)。
  • 由于join接受任意 git 引用,mybackup~1(git 相对引用语法,表示"上一次提交")可以直接作为参数——这正是把 bup 建立在 git 对象模型之上的直接红利。
  • 第二次备份时,/etc中未变化的部分在 split 阶段即被去重,第二次备份占用的新增存储很小;而join依然能完整还原任一次快照。

例 3:远程仓库 + 管道消费

bup-split 手册的示例 展示了完整的远程闭环:

# 把 /etc 归档后远程备份到 myserver 的默认仓库(-r myserver: 表示 host 为 myserver、路径为空) tar -cf - /etc | bup split -r myserver: -n mybackup-tar # 从远程 join 回来并统计归档条目数 bup join -r myserver: mybackup-tar | tar -tf - | wc -l 1961

这里-r myserver:使用了host:后不跟路径的写法,表示使用服务器端默认仓库路径。

例 4:从 stdin 批量喂入多个引用

不需要在命令行写参数,逐行传入即可:

# 假设 ids 文件里每行是一个 commit/tree/blob ID bup join < ids > restored.bin

这正是"无参数时从 stdin 读取"的行为,也对应测试 test/ext/test-split-join 中的bup join <tags2.tmp >out2.tmp用法。

测试验证:split/join 的字节级可逆性

仓库中的端到端测试直接证明了"join 还原 = split 输入"的字节级等价:

  • test/ext/test-split-join:分别对命令行传入的树 ID(bup join $(cat tags1.tmp))、stdin 传入的 ID 列表、-o输出文件、远程仓库(bup join -r "-:$BUP_DIR")做 join,结果都与原始test/testfile1、test/testfile2逐字节一致(diff -u无差异);对空数据集 join 输出为空串。
  • test/ext/test-comparative-split-join:两个不同版本的 bup 互相 split/join,验证格式兼容性。
  • test/ext/test-on:bup join baz > restore-baz,验证与bup on远程执行配合的还原路径。

这些测试覆盖了本地、stdin、-o、远程四种输入路径,可作为读者验证自己安装版本行为是否正确的参照。

与其他命令的关系

  • bup split:join的天然对偶。split负责分块入库,输出-t(树 ID)、-c(提交 ID)、-b(blob ID 序列)或-n(命名分支)作为后续join的寻址凭证。
  • bup save / bup restore:面向文件系统层级的高层封装。若你希望按路径还原文件而非拼接原始字节流,应使用bup restore;join适合把仓库当作"对象流"直接消费的场景(例如还原到tar管道)。
  • bup cat-file / bup ls / bup ftp / bup fuse:同一仓库抽象RepoProtocol(lib/bup/repo/base.py)下的其他视图工具,分别提供单对象读取、目录浏览、FTP 与 FUSE 挂载等能力;join与它们共享底层的vfs遍历与CatPipe对象管道。

常见问题与提示

  • 引用找不到怎么办?若join报告引用不存在,请确认:仓库位置是否正确(未加-r时使用本地默认仓库,见 bup 主手册 的仓库定位规则);引用的拼写是否符合 git 语法(分支名、~N相对引用、完整/缩写哈希均可);对象是否已被bup gc清理。
  • 如何获得稳定可复用的寻址凭证?习惯性使用bup split -t/-c时保留输出;长期保留的备份用-n name命名分支,并借助name~1、name~2等 git 相对引用访问历史快照。
  • -r的路径规则记住三点:无路径时用远程默认仓库;user@host:path中@前全算用户名、host 后必须有冒号;通用场景优先写 URL(ssh://、bup://)。
  • 性能提示:本地join依赖仓库的 midx/idx 索引做对象定位,保持索引新鲜(bup midx)可加快大量 blob 的还原;远程join的遍历在服务器端完成,传输开销仅为逐块长度前缀帧,适合备份数据量较大的场景。

参考文档

  • 本命令手册:bup-join(1)
  • 对偶命令:bup-split(1)
  • 主手册(含 REMOTE OPTIONS 与仓库定位规则):bup(1)
  • 相关命令:bup-save(1)、bup-restore(1)、bup-cat-file(1)
  • 核心实现:lib/bup/cmd/join.py、lib/bup/vfs.py、lib/bup/repo/base.py、lib/bup/client.py、lib/bup/protocol.py
  • 端到端测试:test/ext/test-split-join、test/ext/test-comparative-split-join
  • 灾备
  • CLI
  • 存储

【免费下载链接】bup

Very efficient backup system based on the git packfile format, providing fast incremental saves and global deduplication (among and within files, including virtual machine images). Please post problems or patches to the mailing list for discussion (see the end of the README below).

项目地址:https://gitcode.com/gh_mirrors/bu/bup
点击查看免费下载
上一篇:BetterJoy高级功能实战:3种特殊按钮重映射与陀螺仪校准终极指南
下一篇:如何用CefFlashBrowser免费重温经典Flash游戏?完整教程指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询