xyOps 备份格式(XYBK v1.0)完全指南:基于 NDJSON 的批量数据导出与导入机制
xyOps 备份格式(XYBK v1.0)完全指南:基于 NDJSON 的批量数据导出与导入机制
XYBK(xyOps Backup Format)是 xyOps 系统用于批量导出与导入数据的开放文件格式规范,支撑着管理界面中的 "Export Data" 与 "Import Data" 功能,可实现跨实例的数据迁移、备份归档与灾难恢复。读完本文,你将掌握 XYBK 的文件结构、三种 NDJSON 记录类型、各数据分区(Lists / 数据库索引 / 用户数据 / Buckets / Secrets / 任务与工单文件 / 监控时间线)的完整格式,以及导入导出的底层实现原理与安全特性。
XYBK 是什么
XYBK 是 xyOps 官方定义的备份格式,当前版本为 v1.0,发布日期为 2025 年 12 月 12 日,作者为 Joseph Huckaby(PixlCore)。其设计目标非常明确:
- 批量导出:管理员可从运行中的 xyOps 实例中按需选择数据类别(Lists、数据库索引、Extras)导出,或直接导出全部数据;
- 批量导入:将导出的数据文件恢复到同一实例或全新实例中,用于迁移、备份、审计或系统重建;
- 开放可读:文件本质是 UTF-8 文本(NDJSON 格式 + 注释行),并可选 Gzip 压缩传输,任何文本编辑器或命令行工具都能直接查看内容。
XYBK 主要被 Admin 的 "Export Data" 与 "Import Data" 两个功能消费:导出器将 Gzip 压缩的 NDJSON 文件流式发送给客户端;导入器则同时接受纯 NDJSON(.txt)或 Gzip 压缩的 NDJSON(.txt.gz)。
在 xyOps 的整体架构中,XYBK 是数据平面(存储与数据库)的"可移植快照协议":它统一了底层两类存储——pixl-server-storage 的 api_admin_export_data 与 api_admin_import_data 两个 API 处理器中。
文件结构:NDJSON 与三类行
一个 XYBK 文件是一行接一行的 UTF-8 文本序列,只允许出现三种行:
| 行类型 | 规则 | 导入器行为 |
|---|---|---|
| 注释 | 以 # 开头 |
忽略 |
| 空行 | 空行或仅含空白字符 | 忽略 |
| 记录 | 一行一个完整 JSON 对象(NDJSON) | 按顺序处理 |
文件开头是一段由导出器写出的短头部注释块(仅用于人类可读,导入器不会解析):
# xyOps Data Export v1.0
# Hostname: [host]
# Date/Time: [string]
# Format: NDJSON
在头部之后,文件包含一个或多个带标签的分区(注释行 + NDJSON 记录)。分区标题(Section Header)同样只是提示性信息,导入器会忽略它们——导入器只处理以 { 开头的 JSON 行。
这意味着:你可以手动编辑、裁剪、拼接 XYBK 文件而不破坏其语义,只要保证 JSON 记录行的顺序与结构正确即可。
三种记录类型(Record Types)
每一行 NDJSON 记录必须是且只能是以下三种形式之一,导入器据此分流处理。
Storage Put(键值写入)
{ "key": "<storage_key>", "value": /* json_or_base64 */ }
- 直接以 key/value "put" 方式写入 pixl-server-storage;
- 对于二进制键(binary key),
value是 Base64 字符串;导入时通过 key 模式自动识别二进制键,并解码回原始字节(详见下文导入实现的isBinaryKey处理); - 对于 JSON 键,
value是 JSON 对象,原样存储。
Storage Command(存储命令)
{ "cmd": "<method>", "args": [ /* arg1, arg2, ... */ ] }
- 调用 pixl-server-storage 上的某个存储 API 方法,例如
listDelete; - 参数原样传递,导入器在内部自行追加回调;
- 典型用途:导出器在重建列表前先删除目标列表("先删后建"),保证导入结果与导出源一致、不产生残留数据。
Database Record(数据库记录)
{ "index": "<index_id>", "id": "<record_id>", "record": { /* ... */ } }
- 通过
unbase.insert(index, id, record)向 Unbase 数据库索引插入一条记录; - 语义为"按 ID 创建或替换"(create or replace)——同一 ID 的记录被重复导入时,后者覆盖前者。
分区(Sections)与各类数据组织
导出器会以注释行添加分区标题,把相关记录分组,便于人工阅读。可遇到的分区标题如下:
# List: <key># Database Index: <index> (<query>)# User Data:# Bucket Data# Bucket Files# Encrypted Secret Data# Job Files (<query>)# Ticket Files (<query>)# Monitor Timeline Data (<query>)
Lists(列表)
xyOps 的许多子系统都被建模为存储"Lists"(分页数组),因此列表导出是 XYBK 文件中最常见的组成部分。导出内容包括列表元数据 + 所有分页,其格式固定为三步:
- 先删除目标列表,为导入的新列表腾出空间:
{ "cmd": "listDelete", "args": [ "<key>", false ] }
- 写入列表头(key/value 形式,包含分页元数据):
{ "key": "<key>", "value": { "page_size": 100, "first_page": 1, "last_page": 5, "length": 500, "type": "list" } }
- 依次写入每个分页(从
first_page到last_page含端点):
{ "key": "<key>/<page>", "value": { "type": "list_page", "items": [ /* ... */ ] } }
其中 items 数组即该页的实际列表条目。在导出实现中(lib/api/admin.js),导出器先用 storage.get(item.key) 取到列表头,随即写出 listDelete 命令与头记录,再以 async.whilst 从 list.first_page 迭代到 list.last_page,逐个读取 item.key + '/' + page_idx 分页键并写出。
在 global/ 命名空间下,典型的列表键包括:
alerts、api_keys、buckets、categories、channels、events、groups、monitors、plugins、secrets、tags、users、roles、web_hooks
这与 internal/ui.json 中 Admin UI 的 list_list 配置一一对应(该配置还包含每个列表的图标)。
注意:用户账户记录本身并不存储在
global/users列表中——该列表保存的是名册(roster)。真正的用户记录以users/<username>键导出,见下文"User Data"。
Database Indexes(数据库索引)
导出可包含完整的 Unbase 索引,且可选地通过查询(query)过滤。每条记录形如:
{ "index": "<index_id>", "id": "<record_id>", "record": { /* ... */ } }
常见的索引 ID 包括:alerts、jobs、servers、snapshots、activity、tickets,与 internal/ui.json 中 database_list 配置一致。
从源码看,索引导出使用底层索引器 API(self.storage.searchRecords(item.query, index_config, ...))以便获得更多控制权,结果按 ID 从新到旧排序,并支持 max_rows 截断(lib/api/admin.js)。默认查询为 '*'(匹配全部)。
User Data(用户数据)
用户账户记录以存储键导出:
users/<normalized_username>→{ ...user record... }
其中 <normalized_username> 是用户名的小写化、去除非单词字符的结果。导出实现(lib/api/admin.js)遍历 global/users 名册列表,为每个用户计算 user_path = 'users/' + username.toLowerCase().replace(/\W+/g, ''),然后写出用户记录。
若选择了 "User Avatars" extra,还可能包含以下二进制键(Base64 值):
users/<normalized_username>/avatar/64.pngusers/<normalized_username>/avatar/256.png
用户记录中的密码以加盐 bcrypt 哈希存储,并原样导出——绝不导出明文密码。
Buckets(Buckets 桶数据)
选择 Buckets 列表后,导出器还会包含桶数据,并视 extras 决定是否包含文件载荷:
key: buckets/<bucket_id>/data(JSON):每个桶的元数据/数据对象;key: buckets/<bucket_id>/files(JSON):以存储路径为键的每个文件载荷清单,文件载荷本身为 Base64。
在源码中,bucketData 遍历 global/buckets 列表读取 buckets/<id>/data;bucketFiles 则先读取 buckets/<id>/files 清单,逐个校验大小(默认上限 1 MB,max_size = 1024 * 1024),超限文件跳过,最后先写各文件载荷、再写清单(lib/api/admin.js)。
Secrets(加密机密数据)
Secret 保险库的元数据存放在 global/secrets 列表中(像普通列表一样导出)。而机密载荷本身导出为:
key: secrets/<secret_id>→ 值即存储的加密 blob
内容是 Base64 编码的加密数据——机密绝不会以明文导出(lib/api/admin.js)。
Job 与 Ticket 文件 / 日志
通过 extras 选择后,任务(job)与工单(ticket)附件按 key 导出,载荷为 Base64。任务还支持导出压缩日志:
- Job 文件:
key: <file_path>,即任务files[]列表中的每个文件(受大小上限约束); - Job 日志:
key: logs/jobs/<job_id>/log.txt.gz,若存在且低于大小上限; - Ticket 文件:
key: <file_path>,即工单files[]列表中的每个文件(受大小上限约束)。
源码细节(lib/api/admin.js):jobFiles 默认查询为 tags:_files(仅扫描带文件的任务),ticketFiles 默认查询为 '*';两者都按 1 MB 大小过滤;日志导出要求 job.log_file_size < max_size 且任务无 output 内联输出。
Monitor Timeline Data(监控时间线数据)
服务器监控的时间序列数据以列表形式存储在 timeline/<server_id>/<system_id> 键下。选中 monitor_data 后,导出器对每台服务器、每个监控系统(self.systems)构造时间线键,并复用列表导出逻辑(exportTypes.list)逐条导出,因此时间线数据在文件中表现为普通 List(lib/api/admin.js)。
数据选择:Lists、Indexes 与 Extras
Admin UI 提供三个选择分组,映射到导出的条目类型:
Lists(列表):global/ 下的一个或多个标准列表(见上文列表清单)。选择 users 会同时触发 users/<username> 的 "User Data" 导出;选择 buckets 触发 "Bucket Data";选择 secrets 触发 "Encrypted Secret Data"。
Indexes(索引):按 ID 选择一个或多个 Unbase 索引(可选查询过滤),如 jobs、servers、tickets 等。
Extras(附加项):可选的载荷与时间序列:
job_files— 任务附件job_logs— 任务压缩日志bucket_files— 桶文件载荷ticket_files— 工单附件monitor_data— 监控时间线stat_data— 统计历史(内部映射为global/stats列表导出)user_avatars— 用户头像
导出器可被指示在任一分组中选择 "all"。内部实现中(lib/api/admin.js),这些选择会被展开成一个由上述记录类型构成的条目流(items 数组),逐个执行。例如:选择列表 users + extra user_avatars 会生成 { type: 'users', avatars: true } 条目;选择 extras 中的 job_files/job_logs 会生成 { type: 'jobFiles', logs, files } 条目。
值得注意的是,导出的 API 还支持单次传输令牌(single-use transfer token):携带 params.token 时,服务端会校验 transferTokens 中对应的令牌,将其携带的会话与参数合并进本次请求后立即销毁,用于跨实例的安全数据传输(lib/api/admin.js)。同时,导出操作要求用户拥有 bulk_export 特权(lib/api/admin.js),该特权定义于 internal/ui.json 的权限清单中。
压缩与传输
导出文件以 Gzip 流式输出,文件名形如:
xyops-data-export-YYYY-MM-DD-<id>.txt.gz
例如 xyops-data-export-2025-12-12-aBcDeF.txt.gz。导入器则接受纯 .txt NDJSON 文件或 Gzip 压缩的 .txt.gz 文件均可。
源码证实:导出响应设置 Content-Type: application/gzip 与 Content-Disposition: attachment 头,通过 zlib.createGzip() 流管道直接写入 HTTP 响应对象,真正做到边读边导、边压边传(lib/api/admin.js);导入器则根据上传文件的文件名(\.gz$)或 MIME 类型(gzip)自动判断是否先 gunzip 解压到临时文件再逐行处理(lib/api/admin.js)。
导出结束时会写出 # End of file 注释标记文件结尾(lib/api/admin.js)。
安全特性
XYBK 在格式层面刻意保证敏感材料的"加密或哈希化":
- API Keys:只导出加盐哈希。明文 API 密钥材料永远不会出现在导出文件中;
key字段是创建时存储的加盐 SHA-256 摘要。 - Secrets:机密载荷以加密 blob(Base64)导出,明文永不导出。
- Users:密码以加盐 bcrypt 哈希存储与导出,不含任何明文密码。
这意味着 XYBK 文件本身是可以相对放心分发/归档的——即便拿到备份文件,也无法直接读取到密钥明文或密码原文。
完整示例
以下片段展示列表导出、API 密钥与一条数据库记录的组合(截取自文档示例):
# xyOps Data Export v1.0
# Hostname: joemax.xyops.io
# Date/Time: Tue Nov 18 2025 12:01:27 GMT-0800 (Pacific Standard Time)
# Format: NDJSON
# List: global/alerts
{"cmd":"listDelete","args":["global/alerts",false]}
{"key":"global/alerts","value":{"page_size":100,"first_page":0,"last_page":0,"length":5,"type":"list"}}
{"key":"global/alerts/0","value":{"type":"list_page","items":[{"id":"load_avg_high","title":"High CPU Load", ...}]}}
# List: global/api_keys
{"cmd":"listDelete","args":["global/api_keys",false]}
{"key":"global/api_keys","value":{"page_size":100,"first_page":0,"last_page":0,"length":2,"type":"list"}}
{"key":"global/api_keys/0","value":{"type":"list_page","items":[{"key":"<salted_sha256>","active":1, ...}]}}
# Database Index: tickets (*)
{"index":"tickets","id":"tmhzbmbagig","record":{"subject":"Alert: High Active Jobs on raspberrypi", ...}}
解析规则与导入实现细节
导入器的解析规则可总结为:
- 注释行与空行被忽略,只有以
{开头的行才会被解析; - 行按顺序处理——命令可能先准备状态(如删除列表),其后的 put 再写入数据;
- key/value 记录通过
storage.put(key, value)写入;二进制键在导入时自动 Base64 解码; - 数据库记录通过
unbase.insert(index, id, record)插入; - 存储命令会以给定参数调用存储引擎上的对应方法;
- 导入器逐行流式校验,最多收集 100 个错误用于报告,遇到非致命错误会继续处理。
在 lib/api/admin.js 的 api_admin_import_data 中,这些规则被完整落地:
- 单任务限制:同一时刻只允许一个维护任务(
type: 'maint')运行,否则拒绝导入; - 解压/移动:
.gz文件先解压到os.tmpdir()下的临时文件(xyops-import-temp-<id>.txt),防止处理过程中上传文件被清理;也支持params.format == 'cronicle'时的旧版 Cronicle 数据格式转换; - 预计数:先用
readline预扫文件,统计以{开头的行数,用于报告导入进度; - Danger 模式:传入
params.danger时(主要用于与外部系统同步的增量更新),跳过"停调度器"步骤;否则导入会自动:关闭调度器(scheduler.enabled状态置 false,警告提示需手动重新启用)、静默冲刷所有 queued 任务、中止所有运行中任务,并等待其全部结束; - 逐行处理:
Tools.fileEachLine以 32 KB 缓冲逐行读取,仅解析^\s*\{的行,并按三种记录类型分流:row.index && row.id && row.record→self.unbase.insert(...);row.key && row.value→ 若self.storage.isBinaryKey(row.key)为真,则Buffer.from(row.value, 'base64')解码,再self.storage.put(...);row.cmd && self.storage[row.cmd] && row.args→ 校验命令在白名单内后调用:put|copy|rename|delete|listCreate|listPush|listUnshift|listCopy|listRename|listDelete|hashCreate|hashPut|hashUpdate|hashCopy|hashRename|hashDelete|hashDeleteAll——白名单之外的命令被记作错误并跳过;- 其余行记 "Unknown format" 错误;
- 状态刷新:导入完成后,重新加载
ui.globalMemoryLists配置的所有全局列表进内存、重编译监控与告警表达式、重建 Secret 加密缓存、重新准备插件(Plugins)、向所有监控 Worker 广播更新,并强制所有已认证用户重连以获取新数据; - 报告:任务详情(job.details)中包含导入统计——处理行数(Lines Processed)、数据库记录数(DB Records Imported)、存储键写入数(Storage Keys Written)、命令执行数(Commands Executed),以及错误与警告清单(各最多展示 100 条),同时附上文件名、大小、MIME 类型与格式。
导出实现:从选择到流式文件
api_admin_export_data(lib/api/admin.js)的流程要点:
- 参数归一化:支持三种调用形态——
items数组(直接指定{ type, key|index, query? })、lists/indexes/extras数组、或将三者置为字符串"all"(此时分别展开为ui.list_list、ui.database_list、ui.extra_list的全部 ID); - 展开选择:按上文"数据选择"一节的映射关系,把 lists/indexes/extras 展开为具体条目类型(list、index、users、bucketData、bucketFiles、secretData、jobFiles、ticketFiles、monitorData),并做数组类型校验;
- 权限与会话:要求有效用户且具备
bulk_export特权; - 流式输出:启动内部任务(title "Custom bulk data export"),设置 HTTP 流响应头,
zlib.createGzip()管道直连响应对象,先写出四行头部注释(v1.0 / Hostname / Date/Time / Format); - 顺序迭代:
async.eachSeries按条目顺序执行各exportTypes.<type>处理器,每个处理器内部又以async.eachSeries/async.whilst串行写出该类型的所有记录,保证记录顺序稳定、内存占用可控(文件载荷按需加载为 Buffer 后转 Base64); - 中断处理:响应对象监听
error/close/finish事件——若客户端断连或出错,立即销毁 gzip 流、结束内部任务并停止后续迭代(export_aborted标志); - 收尾:全部条目完成后写出
# End of file并结束任务。
版本与兼容性
本规范定义了 XYBK v1.0。导出器在文件头部注释中始终写入 # xyOps Data Export v1.0 作为版本标识。
未来版本可能新增分区标题与记录形态,但导入器始终忽略注释,只依赖这里定义的三种 NDJSON 记录类型——Storage Put、Storage Command 与 Database Record。这意味着 XYBK v1.0 文件具备很强的向后兼容性:即便未来导出器写出新的分区与记录结构,v1.0 及之后的导入器也能正确处理既有文件,不会因新增内容而崩溃。这为跨版本迁移与长期归档提供了格式层面的保障。
延伸阅读
- 规范原文:docs/xybk.md
- 导入实现(含命令白名单、Danger 模式、报告统计):lib/api/admin.js
- 导出实现(含选择展开、流式 gzip、各类导出处理器):lib/api/admin.js
- UI 数据选择配置(Lists / Indexes / Extras 清单与图标):internal/ui.json
- 底层存储与数据库参考:pixl-server-storage(含 Lists 分页数组语义)与 pixl-server-unbase(Unbase 索引),其 Lists 内部结构决定了本文所述的"头记录 + 分页记录"导出形态。