restic REST Backend 协议详解:HTTP 仓库的 API 版本协商与全部端点规范
【免费下载链接】resticFast, secure, efficient backup program项目地址: https://gitcode.com/GitHub_Trending/re/restic
导读
restic 除了支持本地磁盘、SFTP、S3 等仓库,还允许连接任何"遵循固定 REST API"的 HTTP 服务器,将其作为备份仓库后端。本文以 doc/REST_backend.rst 为骨架,完整讲解该协议中{type}文件类型、v1/v2 两代 API 的版本协商机制、从仓库初始化到 blob 读写的全部 HTTP 端点,并结合 internal/backend/rest/rest.go 等客户端源码,说明 restic 端如何发起这些请求、如何解析响应。读完本文,你可以据此实现一个自定义 REST 仓库服务,或深刻理解 restic 访问 HTTP 仓库时的完整网络行为。
REST Backend 是什么
restic 可以通过 HTTP/HTTPS 与一个实现了下述 REST API 的服务器交互,这类服务器作为 restic 的远程仓库。相比 S3、Swift 等公有云对象存储协议,REST 协议是完全自定义的,因此需要先部署一个专门的 REST 服务器进程(例如 restic 官方配套的rest-server),再通过修改 URL scheme 让 restic 连上去,例如 doc/030_preparing_a_new_repo.rst 中给出的初始化命令:
$ restic -r rest:http://host:8000/ initrest:前缀后的地址决定协议与访问方式。该文档还给出了 HTTPS、密码保护、多仓库以及 Unix socket 等多种组合:
$ restic -r rest:https://host:8000/ init $ restic -r rest:https://user:pass@host:8000/ init $ restic -r rest:https://user:pass@host:8000/my_backup_repo/ init $ restic -r rest:http+unix:///tmp/rest.socket:/my_backup_repo/ init用户名与密码也可以放到环境变量里,避免出现在命令行中:
$ export RESTIC_REST_USERNAME=<MY_REST_SERVER_USERNAME> $ export RESTIC_REST_PASSWORD=<MY_REST_SERVER_PASSWORD>在客户端解析层,internal/backend/rest/config.go 的ParseConfig要求字符串必须以rest:开头,prepareURL会保证以/结尾;ApplyEnvironment则在 URL 中既未携带用户名也未携带密码时,才读取前缀加RESTIC_REST_USERNAME/RESTIC_REST_PASSWORD的环境变量填充认证信息(config.go)。
使用 TLS 时,restic 默认用系统 CA 证书校验证书;若服务端使用自签证书或自定义 CA,可通过--cacert指定证书文件,此后系统 CA 将不再参与校验。
一个值得注意的事实是:REST 服务器在磁盘上使用的目录结构与本地(local)后端完全一致(doc/030_preparing_a_new_repo.rst),因此同一份仓库既可以本地直接访问,也可以同时通过 HTTP 访问。
协议的核心要素:{type}、{path} 与目录布局
协议文档定义了两个占位符:
{type}表示文件/blob 的类型,合法取值包括:
| {type} 值 | 含义 |
|---|---|
data | 数据块(pack 文件) |
keys | 密钥文件 |
locks | 锁文件 |
snapshots | 快照文件 |
index | 索引文件 |
config | 仓库配置文件(比较特殊,见下) |
{path}是仓库在服务器上的路径,目的是让一个服务器同时托管多个相互独立的 restic 仓库。默认路径为/,且路径必须以/结尾。
客户端对类型与路径的拼接逻辑位于 internal/backend/layout/layout_rest.go。RESTLayout复用了与本地/SFTP 后端一致的defaultLayoutPaths映射(见 layout_default.go):data、snapshots、index、locks、keys各对应一个同名目录;只有config特例,其Filename固定为仓库根目录下的config(layout_rest.go)。也就是说,一个仓库形如:
{path}/data/… {path}/keys/… {path}/locks/… {path}/snapshots/… {path}/index/… {path}/config注意与本地后端的不同:本地后端(
DefaultLayout)为data目录再按文件名前两个字符拆出两层子目录(layout_default.go),而 REST 布局下data目录是扁平存放的,不做两字符子目录拆分。
API 版本协商机制:Accept 头与 Content-Type 头
REST 协议经历过一次演进(v1 → v2),关键差异在"列出 blob"端点的返回内容(见下文),而版本的选择完全依赖 HTTP 头完成,服务端无需通过不同 URL 区分版本:
- 请求头
Accept决定客户端想要的 API 版本:application/vnd.x.restic.rest.v1或留空:API 版本 1;application/vnd.x.restic.rest.v2:API 版本 2。
- 对应当返回 JSON 的请求,服务器在响应头
Content-Type中返回它支持的最高版本值;如果Content-Type是其他任何值,则视为 API 版本 1。
在客户端源码中这两个媒体类型是常量(rest.go):
const ( ContentTypeV1 = "application/vnd.x.restic.rest.v1" ContentTypeV2 = "application/vnd.x.restic.rest.v2" )restic 客户端在发出保存、读取、HEAD、删除、列举等绝大多数请求时都会设置Accept: application/vnd.x.restic.rest.v2(例如 rest.go、rest.go、rest.go)。而在List处理响应时,客户端会检查响应头Content-Type是否为 v2,据此决定走listv2还是listv1(rest.go)——这正是"Content-Type 响应头决定协议版本"的实现位置。
仓库级操作:初始化、删除与 config 文件
以下端点针对整个仓库({path}层面)操作。
POST {path}?create=true —— 创建仓库
用于在服务器上新建仓库。客户端以binary/octet-stream类型、空请求体发起 POST,并在 URL 查询参数中携带create=true;服务器成功创建结构(或仓库已存在)时返回200 OK,否则返回错误。
restic 在restic init时通过客户端Create完成这一调用(rest.go):它先以Stat检查 config 文件是否已存在,若已存在则直接报错 "config file already exists",防止误初始化覆盖已有仓库;随后构造带?create=true的 POST 请求,仅当响应状态为200 OK才视为成功。
DELETE {path} —— 删除仓库
在服务器端删除整个仓库。成功返回200 OK;若服务器未实现该功能返回501 Not Implemented,若服务器策略上拒绝删除则返回403 Forbidden。客户端将这类非 200 响应封装为restError(rest.go)。
config 文件相关端点
HEAD {path}/config:仓库存在配置文件时返回200 OK,否则返回 HTTP 错误。GET {path}/config:返回配置文件内容(若存在),否则返回 HTTP 错误。响应格式为binary/octet-stream。POST {path}/config:将请求体作为配置内容保存,成功返回200 OK,否则返回 HTTP 错误。
客户端Stat就是通过HEAD请求拿Content-Length头作为文件大小来实现的(rest.go);仓库锁(lock)流程中判断"仓库是否已初始化"也正是靠对 config 文件的探测。
blob(数据文件)级操作
除了 config 位于仓库根外,其余类型都存放在对应子目录中。下文统一写作{path}/{type}/{name},其中{name}是 blob 的文件名(restic 中通常是内容的 SHA-256 十六进制哈希)。
GET {path}/{type}/ —— 列出某类型全部文件
- API v1:返回纯 JSON 字符串数组,元素为文件名,例如:
[ "245bc4c430d393f74fbe7b13325e30dbde9fb0745e50caad57c446c93d20096b", "85b420239efa1132c41cea0065452a40ebc20c6f8e0b132a5b2f5848360973ec", "8e2006bb5931a520f3c7009fe278d1ebb87eb72c3ff92a50c30e90f1b8cf3e60", "e75c8c407ea31ba399ab4109f28dd18c4c68303d8d86cc275432820c42ce3649" ]- API v2:返回 JSON 对象数组,每个对象含
name(文件名)与size(字节大小)两个键,例如:
[ { "name": "245bc4c430d393f74fbe7b13325e30dbde9fb0745e50caad57c446c93d20096b", "size": 2341058 }, { "name": "85b420239efa1132c41cea0065452a40ebc20c6f8e0b132a5b2f5848360973ec", "size": 2908900 }, { "name": "8e2006bb5931a520f3c7009fe278d1ebb87eb72c3ff92a50c30e90f1b8cf3e60", "size": 3030712 }, { "name": "e75c8c407ea31ba399ab4109f28dd18c4c68303d8d86cc275432820c42ce3649", "size": 2804 } ]v2 引入的意义在客户端源码里非常清晰:listv1在拿到文件名字符串数组后,还必须对每一个文件额外发一次HEAD请求来获取大小(rest.go);而listv2在一次请求的响应中就直接拿到name与size(rest.go),大大减少了对小文件仓库做全量列举(如restic check、prune遍历索引)时的往返次数。List对目录不存在的处理也有细节:除rclone/服务器外,404被视为"目录缺失"直接忽略返回(rest.go)。
HEAD {path}/{type}/{name} —— 判断 blob 是否存在
若指定类型与名称的 blob 已存储,返回200 OK;否则返回404 Not Found。若 blob 存在,响应头Content-Length会被设置为文件大小。客户端Stat正是依赖此语义获取文件信息。
GET {path}/{type}/{name} —— 读取 blob 内容
若 blob 存在则返回其内容(响应格式binary/octet-stream),否则返回404。支持 Range 部分读取:如果请求带Range头字段,则响应状态码为206(而非200),响应体只包含请求的范围。
这是 restic 恢复与校验大量数据的基础能力。客户端Load/openReader中会把"偏移量 + 长度"构造成标准的 HTTP Range 头:不带 length 时为bytes={offset}-,带 length 时为bytes={offset}-{offset+length-1}(rest.go)。响应只接受200 OK或206 Partial Content两种状态码,并支持在开启BackendErrorRedesign特性时用416(RequestedRangeNotSatisfiable)标记越界读取。此外Load在读完数据后会等待一个EOF再关闭响应体,以避免 HTTP/2 流被过早关闭导致服务器端出现stream closed错误(rest.go)。
POST {path}/{type}/{name} —— 保存 blob
将请求体内容保存为指定类型与名称的 blob,成功返回200 OK,否则返回 HTTP 错误。请求格式为binary/octet-stream。
客户端Save使用application/octet-stream内容类型并显式设置Content-Length(req.ContentLength = rd.Length()),以避免使用 chunked 编码、让服务器提前知道数据规模(rest.go)。它还用RewindReader支持重试时的请求体重放。注意 REST 后端与对象存储不同:其Properties()中HasAtomicReplace为false,因为rest-server会阻止覆盖写(rest.go)。
DELETE {path}/{type}/{name} —— 删除 blob
从仓库中删除指定 blob,成功返回200 OK,否则返回 HTTP 错误。客户端Remove直接发送 HTTPDELETE,仅接受200 OK作为成功(rest.go);restic forget --prune等清理流程最终就依赖该端点回收空间。
完整的客户端方法映射
把上述端点与 rest.go 中的backend.Backend接口实现一一对应,可以快速建立"协议 ↔ 代码"的映射关系:
| REST 端点 | 客户端方法 | 源码位置 |
|---|---|---|
POST {path}?create=true | Create | rest.go |
HEAD {path}/{type}/{name} | Stat | rest.go |
GET {path}/{type}/{name}(含 Range) | Load/openReader | rest.go |
POST {path}/{type}/{name} | Save | rest.go |
DELETE {path}/{type}/{name} | Remove | rest.go |
GET {path}/{type}/ | List(v1/v2 自动探测) | rest.go |
| 仓库整体删除 | Delete(util.DefaultDelete) | rest.go |
错误语义上也做了归纳:restError在404时会被判为"文件不存在"(IsNotExist,配合errors.As),401、403、416、507(InsufficientStorage)等被视为永久错误(rest.go),从而驱动上层重试策略做出正确决策。
连接配置与并发控制
REST 后端支持通过 restic 的选项机制配置参数。在 config.go 中:
type Config struct { URL *url.URL Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"` }Connections注册在rest前缀下,默认值为 5(NewConfig中填充),它控制 restic 与 REST 服务器之间允许的最大并发 HTTP 连接数,实际通过Backend.Properties().Connections暴露给调度层(rest.go)。当你的备份并发度高或服务器连接数受限时,可在命令行用类似-o rest.connections=10的方式调整。
测试如何验证这套协议
仓库内置了两层测试,可作为协议实现者的"参考实现":
- internal/backend/rest/rest_int_test.go 用
httptest起本地 HTTP 服务,直接验证List在不同响应Content-Type(v1/v2/未知)下的解析分支——例如 v1 响应数组需要额外发出 N 次 HEAD 请求、v2 一次到位; - internal/backend/rest/rest_test.go 会探测系统是否安装了
rest-server可执行文件,若存在则以--no-auth --path <dir> --listen <addr>启动真实服务进程,对完整 REST 后端做与本地后端同等的后端一致性测试(Save/Load/Stat/Remove/List 全套),并通过backend/test套件校验实现符合backend.Backend契约。
如果你的目标是编写一个全新的 REST 仓库服务器,最稳妥的路径就是让上述两类测试全部通过。
小结
REST Backend 是 restic 面向"可自托管的任意 HTTP 服务"设计的精简协议:以Accept/Content-Type头完成 v1/v2 版本协商,用 6 种文件类型子目录组织仓库,通过 POST/GET/HEAD/DELETE 一组有限动词覆盖创建、读取、写入、删除与列举全部仓库数据,并借助 Range 头支持大文件的按需部分读取。本文给出的端点语义可直接作为服务端实现依据,而 internal/backend/rest 目录下的客户端代码与测试则是协议行为的权威参考;日常使用时可参考 doc/030_preparing_a_new_repo.rst 的 REST Server 小节完成rest:URL 的初始化与访问配置。
【免费下载链接】resticFast, secure, efficient backup program项目地址: https://gitcode.com/GitHub_Trending/re/restic
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考