mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-25 18:23:37 +08:00
Compare commits
54 Commits
v1.4.0
...
docs/docum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3f336a54 | ||
|
|
f1349a1550 | ||
|
|
5827074334 | ||
|
|
00151e466c | ||
|
|
50ce6587d8 | ||
|
|
f7599dd9bd | ||
|
|
bf0e91db57 | ||
|
|
37092f3167 | ||
|
|
51e4b0b0ce | ||
|
|
493e1faff5 | ||
|
|
68bb964350 | ||
|
|
65cf3a04d4 | ||
|
|
90b58d58d6 | ||
|
|
f584a0802a | ||
|
|
992fc24150 | ||
|
|
386f12a11b | ||
|
|
f807ce10e6 | ||
|
|
a0d1e66199 | ||
|
|
74e29a0753 | ||
|
|
01ce536ca8 | ||
|
|
ef2e15f500 | ||
|
|
bdf68eef7a | ||
|
|
8747d6a21b | ||
|
|
04ad3c29f4 | ||
|
|
e63b8f0be8 | ||
|
|
45bc210313 | ||
|
|
0f30e7bf52 | ||
|
|
e4c52fd8f4 | ||
|
|
17f4ec63ae | ||
|
|
5a936ee162 | ||
|
|
d39335bdde | ||
|
|
7084d47c4b | ||
|
|
7a6ffd4ddd | ||
|
|
61709dd4c9 | ||
|
|
f6bd185b9f | ||
|
|
af0e8f5c1f | ||
|
|
63fde903d2 | ||
|
|
67a42b09ba | ||
|
|
bc8742977e | ||
|
|
1a699da8d6 | ||
|
|
1b73f19eb1 | ||
|
|
539e9e64c4 | ||
|
|
83bf5ec656 | ||
|
|
66373fa8e4 | ||
|
|
3a4c2edd9b | ||
|
|
a6dd8033ed | ||
|
|
81c9c042d6 | ||
|
|
3e90e0f8a8 | ||
|
|
827a5a2181 | ||
|
|
970eb154e1 | ||
|
|
d26753c44a | ||
|
|
4251eb9e15 | ||
|
|
94d5fb7286 | ||
|
|
8eb93b3dd9 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -6,6 +6,10 @@ on:
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
# 最小权限:构建/测试仅需读取仓库内容,显式声明以收敛默认的可写令牌。
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
name: Go Build & Test
|
||||
|
||||
63
.github/workflows/docs.yml
vendored
Normal file
63
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Deploy Docs
|
||||
|
||||
# 触发条件:
|
||||
# - 推送 main 时,如果 docs-site/ 或站点相关 README 有变化
|
||||
# - 手动触发(在 Actions 页面)
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs-site/**'
|
||||
- '.github/workflows/docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# 允许写入 Pages,用于发布到 github.com/Awuqing/BackupX 的 Pages 站点
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# 同时只保留一个部署任务
|
||||
concurrency:
|
||||
group: pages-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs-site
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: docs-site/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build site
|
||||
run: npm run build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs-site/build
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -110,13 +110,26 @@ jobs:
|
||||
cp -r web/dist "${ARCHIVE_NAME}/web"
|
||||
cp server/config.example.yaml "${ARCHIVE_NAME}/"
|
||||
cp deploy/install.sh "${ARCHIVE_NAME}/" 2>/dev/null || true
|
||||
cp deploy/backupx.service "${ARCHIVE_NAME}/" 2>/dev/null || true
|
||||
# v2.2+: 随发布包提供 Grafana dashboard 与 nginx.conf 模板
|
||||
if [ -d deploy/grafana ]; then
|
||||
cp -r deploy/grafana "${ARCHIVE_NAME}/grafana"
|
||||
fi
|
||||
cp deploy/nginx.conf "${ARCHIVE_NAME}/nginx.conf" 2>/dev/null || true
|
||||
tar czf "${ARCHIVE_NAME}.tar.gz" "${ARCHIVE_NAME}"
|
||||
cp "${ARCHIVE_NAME}.tar.gz" "backupx-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz"
|
||||
sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
|
||||
sha256sum "backupx-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" > "backupx-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz.sha256"
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.VERSION }}
|
||||
files: backupx-${{ env.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
files: |
|
||||
backupx-${{ env.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
backupx-${{ env.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz.sha256
|
||||
backupx-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
backupx-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz.sha256
|
||||
generate_release_notes: true
|
||||
|
||||
# ─── Job 3: Docker 多架构 → Docker Hub ───
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
web/node_modules/
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
server/bin/
|
||||
.claude/
|
||||
106
CONTRIBUTING.md
Normal file
106
CONTRIBUTING.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Contributing to BackupX
|
||||
|
||||
感谢你对 BackupX 的关注!本指南介绍如何搭建开发环境并提交贡献。
|
||||
Thanks for your interest in contributing to BackupX! This guide covers how to set up your environment and submit changes.
|
||||
|
||||
## 开发环境 / Development Setup
|
||||
|
||||
### 依赖 / Prerequisites
|
||||
|
||||
- **Go** 1.25+(见 `server/go.mod`)
|
||||
- **Node.js** 20+(CI 与 Docker 使用 Node 20)
|
||||
- **npm** 9+
|
||||
|
||||
### 快速开始 / Quick Start
|
||||
|
||||
分别在两个终端启动前后端(后端 :8340,前端 Vite HMR):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
|
||||
# 终端 1 —— 后端(默认 http://localhost:8340)
|
||||
make dev-server
|
||||
|
||||
# 终端 2 —— 前端(Vite 热更新,/api 代理到 8340)
|
||||
make dev-web
|
||||
```
|
||||
|
||||
### 构建 / Building
|
||||
|
||||
```bash
|
||||
make build # 同时构建后端与前端
|
||||
make build-server # 仅后端 → server/bin/backupx
|
||||
make build-web # 仅前端 → web/dist
|
||||
make docker # 构建 Docker 镜像
|
||||
make docker-cn # 国内镜像源加速构建
|
||||
```
|
||||
|
||||
> 后端会自动托管 `web/dist`(或 `server.web_root` 指定目录),因此本地裸机部署无需额外的反向代理即可访问控制台。
|
||||
|
||||
## 测试 / Testing
|
||||
|
||||
提交前请确保测试通过:
|
||||
|
||||
```bash
|
||||
make test # 后端 + 前端全部测试
|
||||
make test-server # 仅后端:cd server && go test ./...
|
||||
make test-web # 仅前端:cd web && npm run test(vitest)
|
||||
```
|
||||
|
||||
新增功能或修复缺陷时,请尽量补充对应测试。
|
||||
|
||||
## 提交信息规范 / Commit Messages
|
||||
|
||||
本项目采用 **Conventional Commits**,正文用中文撰写:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
```
|
||||
|
||||
| type | 说明 |
|
||||
|------|------|
|
||||
| `feat` | 新功能 |
|
||||
| `fix` | 缺陷修复 |
|
||||
| `docs` | 文档变更 |
|
||||
| `style` | 不影响逻辑的格式调整 |
|
||||
| `refactor` | 重构 |
|
||||
| `perf` | 性能优化 |
|
||||
| `test` | 测试相关 |
|
||||
| `chore` | 构建/依赖/工具链 |
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
feat(storage): 新增 Wasabi S3 后端支持
|
||||
fix(cluster): 修复跨节点恢复的终态处理
|
||||
docs: 补充 CONTRIBUTING 指南
|
||||
```
|
||||
|
||||
## Pull Request 流程
|
||||
|
||||
1. **Fork** 仓库并从最新的 `main` 切出特性分支;
|
||||
2. **开发**功能或修复,必要时补充测试;
|
||||
3. **自测**:确保 `make test` 通过;
|
||||
4. **提交**:使用上述 Conventional Commits(中文);
|
||||
5. **推送**并对着 `main` 发起 PR。
|
||||
|
||||
### PR 描述建议
|
||||
|
||||
- 清晰说明本 PR 做了什么;
|
||||
- 对新功能/修复,补充动机与背景;
|
||||
- 关联相关 Issue(如 `Closes #62`);
|
||||
- 纯文档 PR 可不附测试。
|
||||
|
||||
> 请保持分支基于较新的 `main`:基线过旧的分支容易产生大范围冲突,难以评审与合入。
|
||||
|
||||
## 编码规范 / Coding Conventions
|
||||
|
||||
- **Go**:所有错误必须处理(禁止 `_ = err`),日志使用项目已有库(`zap`),禁止 `fmt.Println`;提交前执行 `gofmt`。
|
||||
- **前端**:遵循项目 ESLint/Prettier/tsconfig 配置,不擅自引入新的 CSS 框架或 UI 库。
|
||||
- **包管理**:`web/` 使用 npm,请提交对应的 `package-lock.json`。
|
||||
|
||||
## License
|
||||
|
||||
向 BackupX 贡献即表示你同意你的贡献以 [Apache License 2.0](LICENSE) 授权。
|
||||
26
Dockerfile
26
Dockerfile
@@ -51,11 +51,11 @@ RUN if [ "$USE_CHINA_MIRROR" = "true" ]; then \
|
||||
sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories; \
|
||||
fi
|
||||
|
||||
# Database client binaries are required by MySQL and PostgreSQL backup tasks.
|
||||
RUN apk add --no-cache \
|
||||
nginx \
|
||||
tzdata \
|
||||
ca-certificates \
|
||||
# Required by mysql/postgresql backup tasks
|
||||
su-exec \
|
||||
mysql-client \
|
||||
postgresql16-client \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
@@ -69,24 +69,30 @@ COPY --from=server-builder /build/server/backupx /app/bin/backupx
|
||||
# Copy frontend static files
|
||||
COPY --from=web-builder /build/web/dist /app/web
|
||||
|
||||
# Copy nginx config
|
||||
COPY deploy/docker/nginx.conf /etc/nginx/http.d/default.conf
|
||||
|
||||
# Copy entrypoint
|
||||
COPY deploy/docker/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# Create data directories
|
||||
RUN mkdir -p /app/data /tmp/backupx && \
|
||||
chown -R backupx:backupx /app /tmp/backupx
|
||||
|
||||
# Nginx needs to write to these dirs
|
||||
RUN mkdir -p /var/lib/nginx/tmp /var/log/nginx && \
|
||||
chown -R backupx:backupx /var/lib/nginx /var/log/nginx /run/nginx
|
||||
touch /app/data/.backupx-owner-v2 && \
|
||||
chown -R backupx:backupx /app/data /tmp/backupx && \
|
||||
chmod 0750 /app/data /tmp/backupx
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 8340
|
||||
|
||||
VOLUME ["/app/data"]
|
||||
|
||||
ENV BACKUPX_SERVER_HOST=0.0.0.0 \
|
||||
BACKUPX_SERVER_PORT=8340 \
|
||||
BACKUPX_SERVER_WEB_ROOT=/app/web
|
||||
|
||||
USER root
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD su-exec backupx:backupx wget -q -T 3 -O /dev/null http://127.0.0.1:8340/ready || exit 1
|
||||
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
338
README.md
338
README.md
@@ -1,11 +1,11 @@
|
||||
<p align="right">
|
||||
<a href="README_EN.md">English</a> | <strong>中文</strong>
|
||||
<strong>English</strong> | <a href="README.zh-CN.md">中文</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<h1 align="center">BackupX</h1>
|
||||
<p align="center">
|
||||
<strong>自托管服务器备份管理平台</strong><br>
|
||||
一个二进制,一条命令,管好你所有服务器的备份。
|
||||
<strong>Self-hosted server backup management</strong><br>
|
||||
One binary, one command — manage every backup of every server.
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/Awuqing/BackupX/stargazers"><img src="https://img.shields.io/github/stars/Awuqing/BackupX?style=flat-square&color=f5c542" alt="Stars"></a>
|
||||
@@ -15,307 +15,119 @@
|
||||
<img src="https://img.shields.io/badge/SQLite-embedded-003B57?style=flat-square&logo=sqlite" alt="SQLite">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Awuqing/BackupX?style=flat-square" alt="License"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://awuqing.github.io/BackupX/"><strong>Docs</strong></a> ·
|
||||
<a href="https://github.com/Awuqing/BackupX/releases"><strong>Downloads</strong></a> ·
|
||||
<a href="https://hub.docker.com/r/awuqing/backupx"><strong>Docker Hub</strong></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="screenshots/dashboard.png" alt="仪表盘"></td>
|
||||
<td width="50%"><img src="screenshots/backup-tasks.png" alt="备份任务"></td>
|
||||
<td width="50%"><img src="screenshots/dashboard.png" alt="Dashboard"></td>
|
||||
<td width="50%"><img src="screenshots/backup-tasks.png" alt="Backup Tasks"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="screenshots/storage-targets.png" alt="存储目标"></td>
|
||||
<td><img src="screenshots/backup-records.png" alt="备份记录"></td>
|
||||
<td><img src="screenshots/storage-targets.png" alt="Storage Targets"></td>
|
||||
<td><img src="screenshots/backup-records.png" alt="Backup Records"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 功能亮点
|
||||
## Highlights
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **备份类型** | 文件/目录(多源路径)、MySQL、PostgreSQL、SQLite、SAP HANA |
|
||||
| **存储后端** | 阿里云 OSS、腾讯云 COS、七牛云、S3 兼容(AWS/MinIO/R2)、Google Drive、WebDAV、FTP/FTPS、本地磁盘 |
|
||||
| **自动调度** | Cron 定时 + 可视化编辑器 + 自动保留策略(按天数/份数清理) |
|
||||
| **多节点** | Master-Agent 集群,统一管理多台服务器的备份 |
|
||||
| **安全** | JWT + bcrypt + AES-256-GCM 加密配置 + 可选备份文件加密 + 审计日志 |
|
||||
| **通知** | 邮件 / Webhook / Telegram,备份成功或失败时自动推送 |
|
||||
| **部署** | 单二进制 + 内嵌 SQLite,Docker 一键启动,零外部依赖 |
|
||||
| Capability | Details |
|
||||
|-----------|---------|
|
||||
| **Backup Types** | Files/directories (multi-source), MySQL, PostgreSQL, SQLite, SAP HANA (full / incremental / differential / log + parallel channels + retry) |
|
||||
| **SAP HANA Backint Agent** | Built-in Backint protocol — HANA's native interface routes data directly to any BackupX storage backend |
|
||||
| **70+ Storage Backends** | Alibaba OSS, Tencent COS, Qiniu, S3, Google Drive, WebDAV, FTP + SFTP, Azure Blob, Dropbox, OneDrive and dozens more via rclone |
|
||||
| **Scheduling** | Cron + visual editor + auto-retention (by days/count + empty-directory cleanup) |
|
||||
| **Multi-Node Cluster** | Outbound-only Master-Agent polling with proxy, private-CA, and SSH-bastion support; Agents run tasks locally with no reverse connectivity required |
|
||||
| **Security** | JWT + bcrypt + AES-256-GCM encrypted config + optional backup encryption + full audit log |
|
||||
| **Notifications** | Email / Webhook / Telegram on success or failure |
|
||||
| **Observability** | Prometheus `/metrics` endpoint + `/health` + `/ready` probes + SLA breach gauge |
|
||||
| **Audit Webhook** | HMAC-SHA256 signed forwarding to SIEM / WORM storage for compliance (SOC2 / GDPR) |
|
||||
| **Flow Control** | Per-node bandwidth cap + per-node concurrency limit — tune big/small nodes independently |
|
||||
| **Deployment** | Single binary + embedded SQLite; no external control-plane database (database backup tools are required on the execution host) |
|
||||
|
||||
---
|
||||
## Quick Start
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装
|
||||
|
||||
**Docker(推荐,无需克隆仓库):**
|
||||
Docker Compose (recommended):
|
||||
|
||||
```bash
|
||||
# 创建 docker-compose.yml 后一键启动
|
||||
git clone --depth 1 https://github.com/Awuqing/BackupX.git
|
||||
cd BackupX
|
||||
docker compose up -d
|
||||
|
||||
# 或直接运行
|
||||
docker run -d --name backupx -p 8340:8340 -v backupx-data:/app/data awuqing/backupx:latest
|
||||
```
|
||||
|
||||
> Docker Hub 镜像:[`awuqing/backupx`](https://hub.docker.com/r/awuqing/backupx),支持 linux/amd64 和 linux/arm64。
|
||||
|
||||
<details>
|
||||
<summary>docker-compose.yml 参考</summary>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backupx:
|
||||
image: awuqing/backupx:latest
|
||||
container_name: backupx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8340:8340"
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
# 挂载需要备份的宿主机目录(按需添加):
|
||||
# - /var/www:/mnt/www:ro
|
||||
# - /etc/nginx:/mnt/nginx-conf:ro
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
|
||||
volumes:
|
||||
backupx-data:
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**预编译包(裸机部署):**
|
||||
|
||||
从 [Releases](https://github.com/Awuqing/BackupX/releases) 下载对应平台的压缩包:
|
||||
Prebuilt archive:
|
||||
|
||||
```bash
|
||||
tar xzf backupx-v*-linux-amd64.tar.gz && cd backupx-*
|
||||
sudo ./install.sh # 自动配置 systemd + Nginx
|
||||
```
|
||||
|
||||
**从源码构建:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make build # 构建前后端
|
||||
make docker-cn # 或用国内镜像构建 Docker(goproxy.cn / npmmirror / 阿里云 apk)
|
||||
```
|
||||
|
||||
### 2. 打开控制台
|
||||
|
||||
浏览器访问 `http://your-server:8340`,首次打开会引导创建管理员账户。
|
||||
|
||||
### 3. 添加存储目标
|
||||
|
||||
进入 **存储目标** 页面,点击 **添加**,选择存储类型并填写凭证:
|
||||
|
||||
| 存储类型 | 需要填写 |
|
||||
|---------|---------|
|
||||
| 阿里云 OSS | Region + AccessKey ID/Secret + Bucket |
|
||||
| 腾讯云 COS | Region + SecretId/SecretKey + Bucket(格式 `name-appid`) |
|
||||
| 七牛云 Kodo | Region + AccessKey/SecretKey + Bucket |
|
||||
| S3 兼容 | Endpoint + AccessKey + Bucket |
|
||||
| Google Drive | Client ID/Secret → 点击授权完成 OAuth |
|
||||
| WebDAV | 服务器地址 + 用户名/密码 |
|
||||
| FTP | 主机 + 端口 + 用户名/密码 |
|
||||
| 本地磁盘 | 目标目录路径 |
|
||||
|
||||
> 国内云厂商只需填 Region 和 AccessKey,系统自动组装 Endpoint。
|
||||
|
||||
添加后点击 **测试连接** 确认配置正确。
|
||||
|
||||
### 4. 创建备份任务
|
||||
|
||||
进入 **备份任务** 页面,点击 **新建**,三步完成:
|
||||
|
||||
1. **基础信息** — 任务名称、备份类型、Cron 表达式(留空则仅手动执行)
|
||||
2. **源配置** — 文件备份选择源路径(支持多个)、数据库备份填写连接信息
|
||||
3. **存储与策略** — 选择存储目标、压缩策略、保留天数、是否加密
|
||||
|
||||
保存后可以点击 **立即执行** 测试,在 **备份记录** 页面实时查看执行日志。
|
||||
|
||||
### 5. 配置通知(可选)
|
||||
|
||||
进入 **通知配置** 页面,支持邮件、Webhook、Telegram 三种方式,可分别配置成功/失败时是否推送。
|
||||
|
||||
---
|
||||
|
||||
## 部署指南
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
docker compose up -d # 使用上方的 docker-compose.yml
|
||||
```
|
||||
|
||||
备份宿主机目录时需要挂载路径(在 docker-compose.yml 的 `volumes` 中添加):
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
- /var/www:/mnt/www:ro # 挂载需要备份的目录
|
||||
- /etc/nginx:/mnt/nginx-conf:ro # 可以挂载多个
|
||||
```
|
||||
|
||||
通过环境变量调整配置:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- BACKUPX_LOG_LEVEL=debug
|
||||
- BACKUPX_BACKUP_MAX_CONCURRENT=4
|
||||
```
|
||||
|
||||
### 裸机部署
|
||||
|
||||
```bash
|
||||
# 使用预编译包
|
||||
tar xzf backupx-v*-linux-amd64.tar.gz && cd backupx-*
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz.sha256
|
||||
sha256sum -c backupx-linux-amd64.tar.gz.sha256
|
||||
tar xzf backupx-linux-amd64.tar.gz
|
||||
cd backupx-*-linux-amd64
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
# 或从源码
|
||||
Build and install on bare metal:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git
|
||||
cd BackupX
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
安装脚本自动完成:创建系统用户 → 安装二进制到 `/opt/backupx/` → 配置 systemd → 配置 Nginx 反向代理。
|
||||
For ARM64 hosts, use `backupx-linux-arm64.tar.gz`. The archive contains `backupx`, `web/`, `config.example.yaml`, and `install.sh`; run `install.sh` from the extracted directory.
|
||||
|
||||
### Nginx 反向代理(裸机部署时)
|
||||
The Compose quick start defaults to `latest` for evaluation. Pin `BACKUPX_IMAGE` to a release tag or digest and review the security and recovery guides before production.
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name backup.example.com;
|
||||
Open `http://your-server:8340`, choose English or Chinese on the setup screen, create the first administrator account, then follow the [5-minute Quick Start](https://awuqing.github.io/BackupX/docs/getting-started/quick-start).
|
||||
|
||||
location / {
|
||||
root /opt/backupx/web;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
## Documentation
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8340;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
The full docs live at **https://awuqing.github.io/BackupX/** — Getting Started, Deployment, Operations, SAP HANA, Multi-Node Cluster, API reference, and more. Switch to Chinese via the language dropdown in the top-right nav.
|
||||
|
||||
### 配置文件
|
||||
Quick links:
|
||||
|
||||
配置文件路径 `./config.yaml`,也可通过 `BACKUPX_` 前缀环境变量覆盖:
|
||||
- [Quick Start](https://awuqing.github.io/BackupX/docs/getting-started/quick-start) — first backup in five minutes
|
||||
- [Installation](https://awuqing.github.io/BackupX/docs/getting-started/installation) — Docker / bare metal / source
|
||||
- [Upgrade & Recovery](https://awuqing.github.io/BackupX/docs/operations/upgrade-recovery) — snapshots, upgrades, rollback, and disaster recovery
|
||||
- [Security Hardening](https://awuqing.github.io/BackupX/docs/operations/security) — production exposure, roles, and secrets
|
||||
- [Monitoring & Alerts](https://awuqing.github.io/BackupX/docs/operations/monitoring) — probes, metrics, and initial alerts
|
||||
- [Troubleshooting](https://awuqing.github.io/BackupX/docs/operations/troubleshooting) — Master, proxy, Agent, and task diagnostics
|
||||
- [Multi-Node Cluster](https://awuqing.github.io/BackupX/docs/features/multi-node) — deploy the Agent on remote servers
|
||||
- [SAP HANA Support](https://awuqing.github.io/BackupX/docs/features/sap-hana) — hdbsql Runner and native Backint
|
||||
- [API Reference](https://awuqing.github.io/BackupX/docs/reference/api) — REST endpoints
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8340
|
||||
database:
|
||||
path: "./data/backupx.db"
|
||||
security:
|
||||
jwt_secret: "" # 留空自动生成并持久化到数据库
|
||||
encryption_key: "" # 留空自动生成
|
||||
backup:
|
||||
temp_dir: "/tmp/backupx"
|
||||
max_concurrent: 2
|
||||
log:
|
||||
level: "info" # debug | info | warn | error
|
||||
file: "./data/backupx.log"
|
||||
```
|
||||
|
||||
### 密码重置
|
||||
|
||||
忘记管理员密码时通过 CLI 重置:
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# 裸机
|
||||
./backupx reset-password --username admin --password newpass123
|
||||
|
||||
# Docker
|
||||
docker exec -it backupx /app/bin/backupx reset-password --username admin --password newpass123
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make dev-server # Terminal 1 — backend (:8340)
|
||||
make dev-web # Terminal 2 — frontend (Vite HMR)
|
||||
make test # run all tests
|
||||
make build # produce server/bin/backupx + web/dist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多节点集群
|
||||
|
||||
BackupX 支持 Master-Agent 模式管理多台服务器:
|
||||
|
||||
1. Web 控制台 → **节点管理** → **添加节点**,系统生成 Token
|
||||
2. 在远程服务器部署 Agent 并使用 Token 连接 Master
|
||||
3. 创建备份任务时选择对应节点,Master 自动下发任务
|
||||
|
||||
创建文件备份任务时,可通过可视化目录浏览器远程选择 Agent 节点上的目录,无需手动输入路径。
|
||||
|
||||
---
|
||||
|
||||
## 开发指南
|
||||
|
||||
**环境要求:** Go >= 1.25 · Node.js >= 20 · npm
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
make dev-server # 终端 1:后端(默认 :8340)
|
||||
make dev-web # 终端 2:前端(Vite HMR)
|
||||
|
||||
# 测试
|
||||
make test # 运行全部测试
|
||||
|
||||
# 构建
|
||||
make build # 前后端一起构建
|
||||
make docker # Docker 构建
|
||||
make docker-cn # 国内 Docker 构建(镜像加速)
|
||||
```
|
||||
|
||||
### 发版
|
||||
|
||||
```bash
|
||||
git tag v1.2.3 && git push --tags
|
||||
# GitHub Actions 自动:编译双架构二进制 → 发布 GitHub Release → 推送 Docker Hub 镜像
|
||||
```
|
||||
|
||||
也可在 GitHub Actions 页面手动触发 Release workflow。
|
||||
|
||||
---
|
||||
|
||||
## API 参考
|
||||
|
||||
所有接口以 `/api` 为前缀,使用 JWT Bearer Token 认证。
|
||||
|
||||
| 模块 | 端点 | 说明 |
|
||||
|------|------|------|
|
||||
| **认证** | `POST /auth/setup` | 初始化管理员 |
|
||||
| | `POST /auth/login` | 登录 |
|
||||
| | `PUT /auth/password` | 修改密码 |
|
||||
| **备份任务** | `GET\|POST /backup/tasks` | 列表 / 创建 |
|
||||
| | `GET\|PUT\|DELETE /backup/tasks/:id` | 详情 / 更新 / 删除 |
|
||||
| | `PUT /backup/tasks/:id/toggle` | 启用/禁用 |
|
||||
| | `POST /backup/tasks/:id/run` | 手动执行 |
|
||||
| **备份记录** | `GET /backup/records` | 列表(支持筛选) |
|
||||
| | `GET /backup/records/:id/logs/stream` | 实时日志 (SSE) |
|
||||
| | `GET /backup/records/:id/download` | 下载 |
|
||||
| | `POST /backup/records/:id/restore` | 恢复 |
|
||||
| **存储目标** | `GET\|POST /storage-targets` | 列表 / 添加 |
|
||||
| | `POST /storage-targets/test` | 测试连接 |
|
||||
| **节点** | `GET\|POST /nodes` | 列表 / 添加 |
|
||||
| | `GET /nodes/:id/fs/list` | 目录浏览 |
|
||||
| **通知** | `GET\|POST /notifications` | 列表 / 添加 |
|
||||
| **仪表盘** | `GET /dashboard/stats` | 概览统计 |
|
||||
| **审计日志** | `GET /audit-logs` | 操作审计 |
|
||||
| **系统** | `GET /system/info` | 系统信息 |
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| **后端** | Go · Gin · GORM · SQLite · robfig/cron |
|
||||
| **前端** | React 18 · TypeScript · ArcoDesign · Vite · Zustand · ECharts |
|
||||
| **存储** | AWS SDK v2 · Google Drive API v3 · gowebdav · jlaffaye/ftp |
|
||||
| **安全** | JWT · bcrypt · AES-256-GCM |
|
||||
See the [development guide](https://awuqing.github.io/BackupX/docs/development/setup) for more.
|
||||
|
||||
## Contributing
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
Issues and pull requests welcome. Please read the [contributing guide](https://awuqing.github.io/BackupX/docs/development/contributing) before opening a PR — commit messages and PRs on this project are written in Chinese.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=Awuqing%2FBackupX&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=Awuqing/BackupX&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=Awuqing/BackupX&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=Awuqing/BackupX&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
|
||||
124
README.zh-CN.md
Normal file
124
README.zh-CN.md
Normal file
@@ -0,0 +1,124 @@
|
||||
<p align="right">
|
||||
<a href="README.md">English</a> | <strong>中文</strong>
|
||||
</p>
|
||||
<p align="center">
|
||||
<h1 align="center">BackupX</h1>
|
||||
<p align="center">
|
||||
<strong>自托管服务器备份管理平台</strong><br>
|
||||
一个二进制,一条命令,管好你所有服务器的备份。
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/Awuqing/BackupX/stargazers"><img src="https://img.shields.io/github/stars/Awuqing/BackupX?style=flat-square&color=f5c542" alt="Stars"></a>
|
||||
<a href="https://github.com/Awuqing/BackupX/releases"><img src="https://img.shields.io/github/v/release/Awuqing/BackupX?style=flat-square&color=brightgreen" alt="Release"></a>
|
||||
<img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat-square&logo=go" alt="Go">
|
||||
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
|
||||
<img src="https://img.shields.io/badge/SQLite-embedded-003B57?style=flat-square&logo=sqlite" alt="SQLite">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Awuqing/BackupX?style=flat-square" alt="License"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://awuqing.github.io/BackupX/zh-Hans/"><strong>文档</strong></a> ·
|
||||
<a href="https://github.com/Awuqing/BackupX/releases"><strong>下载</strong></a> ·
|
||||
<a href="https://hub.docker.com/r/awuqing/backupx"><strong>Docker Hub</strong></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="screenshots/dashboard.png" alt="仪表盘"></td>
|
||||
<td width="50%"><img src="screenshots/backup-tasks.png" alt="备份任务"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="screenshots/storage-targets.png" alt="存储目标"></td>
|
||||
<td><img src="screenshots/backup-records.png" alt="备份记录"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 功能亮点
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **备份类型** | 文件/目录(多源路径)、MySQL、PostgreSQL、SQLite、SAP HANA(完整/增量/差异/日志备份 + 并行通道 + 失败重试) |
|
||||
| **SAP HANA Backint 代理** | 内置 SAP HANA Backint 协议代理,HANA 原生备份接口可直接把数据路由到 BackupX 支持的任意存储后端 |
|
||||
| **70+ 存储后端** | 内置阿里云 OSS / 腾讯云 COS / 七牛云 / S3 / Google Drive / WebDAV / FTP + 通过 rclone 集成 SFTP、Azure Blob、Dropbox、OneDrive 等 70+ 后端 |
|
||||
| **自动调度** | Cron 定时 + 可视化编辑器 + 自动保留策略(按天数/份数清理,自动回收空目录) |
|
||||
| **多节点集群** | Master-Agent 模式,基于 Agent 主动出站的 HTTP 轮询跨服务器管理备份,支持代理、私有 CA 与 SSH 堡垒机,无需反向连通性 |
|
||||
| **安全** | JWT + bcrypt + AES-256-GCM 加密配置 + 可选备份文件加密 + 完整审计日志 |
|
||||
| **通知** | 邮件 / Webhook / Telegram,备份成功或失败时自动推送 |
|
||||
| **可观测性** | Prometheus `/metrics` 端点 + `/health` + `/ready` 探针 + SLA 违约监控 |
|
||||
| **审计外输** | HMAC-SHA256 签名 Webhook,对接 SIEM / WORM 存储满足 SOC2 / GDPR 合规 |
|
||||
| **流控** | 节点级带宽限速 + 节点级并发控制,大小节点分别配置,避免小内存 Agent 被挤爆 |
|
||||
| **部署** | 单二进制 + 内嵌 SQLite,无需外部控制面数据库(数据库备份工具需安装在任务执行主机) |
|
||||
|
||||
## 快速开始
|
||||
|
||||
Docker Compose(推荐):
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://github.com/Awuqing/BackupX.git
|
||||
cd BackupX
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
预编译包:
|
||||
|
||||
```bash
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz.sha256
|
||||
sha256sum -c backupx-linux-amd64.tar.gz.sha256
|
||||
tar xzf backupx-linux-amd64.tar.gz
|
||||
cd backupx-*-linux-amd64
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
从源码构建并裸机安装:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git
|
||||
cd BackupX
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
ARM64 主机请下载 `backupx-linux-arm64.tar.gz`。预编译包内包含 `backupx`、`web/`、`config.example.yaml` 和 `install.sh`,请在解压后的目录内执行 `install.sh`。
|
||||
|
||||
Compose 快速开始为便于体验默认使用 `latest`。生产环境应把 `BACKUPX_IMAGE` 固定到 Release 标签或摘要,并先阅读安全与恢复指南。
|
||||
|
||||
打开 `http://your-server:8340`,在初始化页选择中文或 English 并创建首个管理员账户,按 [5 分钟快速开始](https://awuqing.github.io/BackupX/zh-Hans/docs/getting-started/quick-start) 完成首次备份。
|
||||
|
||||
## 文档
|
||||
|
||||
完整文档见 **https://awuqing.github.io/BackupX/zh-Hans/** — 快速开始、部署、运维、SAP HANA、多节点集群、API 参考等。
|
||||
|
||||
快捷链接:
|
||||
|
||||
- [快速开始](https://awuqing.github.io/BackupX/zh-Hans/docs/getting-started/quick-start) — 五分钟跑通第一个备份
|
||||
- [安装](https://awuqing.github.io/BackupX/zh-Hans/docs/getting-started/installation) — Docker / 裸机 / 源码
|
||||
- [升级与恢复](https://awuqing.github.io/BackupX/zh-Hans/docs/operations/upgrade-recovery) — 快照、升级、回滚与灾难恢复
|
||||
- [安全加固](https://awuqing.github.io/BackupX/zh-Hans/docs/operations/security) — 生产暴露、角色与密钥
|
||||
- [监控与告警](https://awuqing.github.io/BackupX/zh-Hans/docs/operations/monitoring) — 探针、指标与初始告警
|
||||
- [故障排查](https://awuqing.github.io/BackupX/zh-Hans/docs/operations/troubleshooting) — Master、代理、Agent 与任务诊断
|
||||
- [多节点集群](https://awuqing.github.io/BackupX/zh-Hans/docs/features/multi-node) — 远程服务器部署 Agent
|
||||
- [SAP HANA 支持](https://awuqing.github.io/BackupX/zh-Hans/docs/features/sap-hana) — hdbsql Runner 与原生 Backint
|
||||
- [API 参考](https://awuqing.github.io/BackupX/zh-Hans/docs/reference/api) — REST 端点
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make dev-server # 终端 1:后端(:8340)
|
||||
make dev-web # 终端 2:前端(Vite HMR)
|
||||
make test # 运行全部测试
|
||||
make build # 产出 server/bin/backupx + web/dist
|
||||
```
|
||||
|
||||
更多细节见 [开发指南](https://awuqing.github.io/BackupX/zh-Hans/docs/development/setup)。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 与 Pull Request。提交 PR 前请先阅读 [贡献指南](https://awuqing.github.io/BackupX/zh-Hans/docs/development/contributing) — 本项目的 commit message 和 PR 正文均使用中文。
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
||||
318
README_EN.md
318
README_EN.md
@@ -1,318 +0,0 @@
|
||||
<p align="right">
|
||||
<strong>English</strong> | <a href="README.md">中文</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<h1 align="center">BackupX</h1>
|
||||
<p align="center">
|
||||
<strong>Self-hosted Server Backup Management Platform</strong><br>
|
||||
One binary, one command — manage all your server backups.
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/Awuqing/BackupX/stargazers"><img src="https://img.shields.io/github/stars/Awuqing/BackupX?style=flat-square&color=f5c542" alt="Stars"></a>
|
||||
<a href="https://github.com/Awuqing/BackupX/releases"><img src="https://img.shields.io/github/v/release/Awuqing/BackupX?style=flat-square&color=brightgreen" alt="Release"></a>
|
||||
<img src="https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat-square&logo=go" alt="Go">
|
||||
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
|
||||
<img src="https://img.shields.io/badge/SQLite-embedded-003B57?style=flat-square&logo=sqlite" alt="SQLite">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Awuqing/BackupX?style=flat-square" alt="License"></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="screenshots/dashboard.png" alt="Dashboard"></td>
|
||||
<td width="50%"><img src="screenshots/backup-tasks.png" alt="Backup Tasks"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="screenshots/storage-targets.png" alt="Storage Targets"></td>
|
||||
<td><img src="screenshots/backup-records.png" alt="Backup Records"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Highlights
|
||||
|
||||
| Capability | Details |
|
||||
|-----------|---------|
|
||||
| **Backup Types** | Files/Directories (multi-source), MySQL, PostgreSQL, SQLite, SAP HANA |
|
||||
| **Storage Backends** | Alibaba Cloud OSS, Tencent COS, Qiniu Kodo, S3-compatible (AWS/MinIO/R2), Google Drive, WebDAV, FTP/FTPS, Local Disk |
|
||||
| **Scheduling** | Cron-based scheduling + visual editor + auto-retention policy (by days/count) |
|
||||
| **Multi-Node** | Master-Agent cluster for managing backups across multiple servers |
|
||||
| **Security** | JWT + bcrypt + AES-256-GCM encrypted config + optional backup encryption + audit logs |
|
||||
| **Notifications** | Email / Webhook / Telegram — push on success or failure |
|
||||
| **Deployment** | Single binary + embedded SQLite, Docker one-click, zero external dependencies |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install
|
||||
|
||||
**Docker (recommended, no clone needed):**
|
||||
|
||||
```bash
|
||||
# Create a docker-compose.yml then start
|
||||
docker compose up -d
|
||||
|
||||
# Or run directly
|
||||
docker run -d --name backupx -p 8340:8340 -v backupx-data:/app/data awuqing/backupx:latest
|
||||
```
|
||||
|
||||
> Docker Hub: [`awuqing/backupx`](https://hub.docker.com/r/awuqing/backupx) — supports linux/amd64 and linux/arm64.
|
||||
|
||||
<details>
|
||||
<summary>docker-compose.yml reference</summary>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backupx:
|
||||
image: awuqing/backupx:latest
|
||||
container_name: backupx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8340:8340"
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
# Mount host directories to back up (add as needed):
|
||||
# - /var/www:/mnt/www:ro
|
||||
# - /etc/nginx:/mnt/nginx-conf:ro
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
|
||||
volumes:
|
||||
backupx-data:
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Pre-built binaries (bare metal):**
|
||||
|
||||
Download from [Releases](https://github.com/Awuqing/BackupX/releases):
|
||||
|
||||
```bash
|
||||
tar xzf backupx-v*-linux-amd64.tar.gz && cd backupx-*
|
||||
sudo ./install.sh # Auto-configures systemd + Nginx
|
||||
```
|
||||
|
||||
**Build from source:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make build # Build frontend + backend
|
||||
make docker-cn # Or Docker build with China mirrors (goproxy.cn / npmmirror / Aliyun apk)
|
||||
```
|
||||
|
||||
### 2. Open the Console
|
||||
|
||||
Visit `http://your-server:8340` in your browser. First-time access guides you through admin account creation.
|
||||
|
||||
### 3. Add a Storage Target
|
||||
|
||||
Go to **Storage Targets** → **Add**, choose a storage type and enter credentials:
|
||||
|
||||
| Storage Type | Required Fields |
|
||||
|-------------|----------------|
|
||||
| Alibaba Cloud OSS | Region + AccessKey ID/Secret + Bucket |
|
||||
| Tencent Cloud COS | Region + SecretId/SecretKey + Bucket (`name-appid`) |
|
||||
| Qiniu Cloud Kodo | Region + AccessKey/SecretKey + Bucket |
|
||||
| S3 Compatible | Endpoint + AccessKey + Bucket |
|
||||
| Google Drive | Client ID/Secret → click Authorize for OAuth |
|
||||
| WebDAV | Server URL + Username/Password |
|
||||
| FTP | Host + Port + Username/Password |
|
||||
| Local Disk | Target directory path |
|
||||
|
||||
Click **Test Connection** to verify.
|
||||
|
||||
### 4. Create a Backup Task
|
||||
|
||||
Go to **Backup Tasks** → **Create**, complete 3 steps:
|
||||
|
||||
1. **Basic Info** — Task name, backup type, Cron expression (leave empty for manual-only)
|
||||
2. **Source Config** — File backup: select source paths (supports multiple); Database: enter connection info
|
||||
3. **Storage & Policy** — Select storage target(s), compression, retention days, encryption toggle
|
||||
|
||||
Save, then click **Run Now** to test. View real-time logs in **Backup Records**.
|
||||
|
||||
### 5. Set Up Notifications (Optional)
|
||||
|
||||
Go to **Notifications** to configure Email, Webhook, or Telegram alerts for backup success/failure.
|
||||
|
||||
---
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker compose up -d # Using the docker-compose.yml above
|
||||
```
|
||||
|
||||
Mount host directories for file backup (add to `volumes` in docker-compose.yml):
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
- /var/www:/mnt/www:ro
|
||||
- /etc/nginx:/mnt/nginx-conf:ro
|
||||
```
|
||||
|
||||
Override config via environment variables:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- BACKUPX_LOG_LEVEL=debug
|
||||
- BACKUPX_BACKUP_MAX_CONCURRENT=4
|
||||
```
|
||||
|
||||
### Bare Metal
|
||||
|
||||
```bash
|
||||
# From pre-built package
|
||||
tar xzf backupx-v*-linux-amd64.tar.gz && cd backupx-*
|
||||
sudo ./install.sh
|
||||
|
||||
# Or from source
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
The install script creates a system user, installs to `/opt/backupx/`, configures systemd, and sets up Nginx reverse proxy.
|
||||
|
||||
### Nginx Reverse Proxy (bare metal)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name backup.example.com;
|
||||
|
||||
location / {
|
||||
root /opt/backupx/web;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8340;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Config file: `./config.yaml` (or override with `BACKUPX_` prefixed env vars):
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8340
|
||||
database:
|
||||
path: "./data/backupx.db"
|
||||
security:
|
||||
jwt_secret: "" # Auto-generated and persisted to DB
|
||||
encryption_key: "" # Auto-generated
|
||||
backup:
|
||||
temp_dir: "/tmp/backupx"
|
||||
max_concurrent: 2
|
||||
log:
|
||||
level: "info" # debug | info | warn | error
|
||||
file: "./data/backupx.log"
|
||||
```
|
||||
|
||||
### Password Reset
|
||||
|
||||
```bash
|
||||
# Bare metal
|
||||
./backupx reset-password --username admin --password newpass123
|
||||
|
||||
# Docker
|
||||
docker exec -it backupx /app/bin/backupx reset-password --username admin --password newpass123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Node Cluster
|
||||
|
||||
BackupX supports Master-Agent mode for managing multiple servers:
|
||||
|
||||
1. Web Console → **Node Management** → **Add Node** — system generates a Token
|
||||
2. Deploy Agent on remote server, connect using the Token
|
||||
3. Create backup tasks and assign to specific nodes — Master dispatches automatically
|
||||
|
||||
The visual directory browser lets you pick directories on remote Agent nodes — no manual path typing.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
**Requirements:** Go >= 1.25 · Node.js >= 20 · npm
|
||||
|
||||
```bash
|
||||
# Dev mode
|
||||
make dev-server # Terminal 1: backend (:8340)
|
||||
make dev-web # Terminal 2: frontend (Vite HMR)
|
||||
|
||||
# Test
|
||||
make test # Run all tests
|
||||
|
||||
# Build
|
||||
make build # Build frontend + backend
|
||||
make docker # Docker build
|
||||
make docker-cn # Docker build with China mirrors
|
||||
```
|
||||
|
||||
### Release
|
||||
|
||||
```bash
|
||||
git tag v1.2.3 && git push --tags
|
||||
# GitHub Actions: compile dual-arch binaries → publish GitHub Release → push Docker Hub image
|
||||
```
|
||||
|
||||
Or manually trigger the Release workflow from GitHub Actions page.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
All endpoints prefixed with `/api`, authenticated via JWT Bearer Token.
|
||||
|
||||
| Module | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| **Auth** | `POST /auth/setup` | Initialize admin |
|
||||
| | `POST /auth/login` | Login |
|
||||
| | `PUT /auth/password` | Change password |
|
||||
| **Backup Tasks** | `GET\|POST /backup/tasks` | List / Create |
|
||||
| | `GET\|PUT\|DELETE /backup/tasks/:id` | Detail / Update / Delete |
|
||||
| | `PUT /backup/tasks/:id/toggle` | Enable / Disable |
|
||||
| | `POST /backup/tasks/:id/run` | Manual run |
|
||||
| **Backup Records** | `GET /backup/records` | List (with filter) |
|
||||
| | `GET /backup/records/:id/logs/stream` | Real-time logs (SSE) |
|
||||
| | `GET /backup/records/:id/download` | Download |
|
||||
| | `POST /backup/records/:id/restore` | Restore |
|
||||
| **Storage Targets** | `GET\|POST /storage-targets` | List / Add |
|
||||
| | `POST /storage-targets/test` | Test connection |
|
||||
| **Nodes** | `GET\|POST /nodes` | List / Add |
|
||||
| | `GET /nodes/:id/fs/list` | Directory browser |
|
||||
| **Notifications** | `GET\|POST /notifications` | List / Add |
|
||||
| **Dashboard** | `GET /dashboard/stats` | Overview stats |
|
||||
| **Audit Logs** | `GET /audit-logs` | Operation audit |
|
||||
| **System** | `GET /system/info` | System info |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| **Backend** | Go · Gin · GORM · SQLite · robfig/cron |
|
||||
| **Frontend** | React 18 · TypeScript · ArcoDesign · Vite · Zustand · ECharts |
|
||||
| **Storage** | AWS SDK v2 · Google Drive API v3 · gowebdav · jlaffaye/ftp |
|
||||
| **Security** | JWT · bcrypt · AES-256-GCM |
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and Pull Requests are welcome!
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
||||
@@ -12,6 +12,7 @@ ExecStart=/opt/backupx/bin/backupx -config /etc/backupx/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
UMask=0027
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -eu
|
||||
|
||||
# Backend listens on internal port 8341, Nginx exposes 8340
|
||||
export BACKUPX_SERVER_PORT="${BACKUPX_SERVER_PORT_INTERNAL:-8341}"
|
||||
# 旧镜像曾以 root 写入数据卷。Master 启动时做一次所有权迁移,随后
|
||||
# 降权运行;Agent 模式由部署命令显式决定用户,以访问宿主机备份路径。
|
||||
if [ "$(id -u)" -eq 0 ] && [ "${1:-}" != "agent" ]; then
|
||||
chown backupx:backupx /app/data /tmp/backupx
|
||||
if [ ! -f /app/data/.backupx-owner-v2 ]; then
|
||||
chown -R backupx:backupx /app/data
|
||||
su-exec backupx:backupx touch /app/data/.backupx-owner-v2
|
||||
fi
|
||||
export HOME=/app
|
||||
exec su-exec backupx:backupx /app/bin/backupx "$@"
|
||||
fi
|
||||
|
||||
# Start Nginx in background
|
||||
nginx -g "daemon off;" &
|
||||
NGINX_PID=$!
|
||||
|
||||
# Start BackupX backend
|
||||
/app/bin/backupx &
|
||||
APP_PID=$!
|
||||
|
||||
# Trap signals for graceful shutdown
|
||||
trap 'kill $APP_PID $NGINX_PID 2>/dev/null; wait $APP_PID $NGINX_PID 2>/dev/null' SIGTERM SIGINT
|
||||
|
||||
echo "BackupX started — Nginx :8340 -> Backend :8341"
|
||||
|
||||
# Wait for either process to exit
|
||||
wait -n $APP_PID $NGINX_PID 2>/dev/null || true
|
||||
kill $APP_PID $NGINX_PID 2>/dev/null || true
|
||||
wait $APP_PID $NGINX_PID 2>/dev/null || true
|
||||
# Web 静态文件由 BackupX 后端直接托管。容器只运行一个前台进程,
|
||||
# 让 Docker 准确传递信号、收集退出码并执行健康检查。
|
||||
exec /app/bin/backupx "$@"
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
server {
|
||||
listen 8340;
|
||||
server_name _;
|
||||
|
||||
root /app/web;
|
||||
index index.html;
|
||||
|
||||
# API reverse proxy to backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8341/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Static assets cache
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
34
deploy/grafana/README.md
Normal file
34
deploy/grafana/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# BackupX Grafana Dashboard
|
||||
|
||||
对接 BackupX v2.1+ 暴露的 Prometheus `/metrics` 端点。
|
||||
|
||||
## 导入步骤
|
||||
|
||||
1. 在 Grafana 配置 Prometheus 数据源指向你的 Prometheus(例如 `http://prometheus:9090`)
|
||||
2. 在 Prometheus 配置抓取 BackupX:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'backupx'
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: ['backupx-master:8340']
|
||||
```
|
||||
|
||||
3. Grafana → Dashboards → Import → 上传 `backupx-dashboard.json` → 选 Prometheus 数据源 → Import
|
||||
|
||||
## 面板内容
|
||||
|
||||
- 当前运行任务数 / SLA 违约数 / 在线节点 / 24h 成功率 / 应用版本
|
||||
- 任务执行速率(按 success/failed 堆叠)
|
||||
- 任务耗时 P50/P95/P99(按任务类型)
|
||||
- 任务产出字节速率
|
||||
- 存储目标用量 TopN 柱状图
|
||||
- 节点在线状态表(红/绿标色)
|
||||
- 验证 / 恢复 / 复制的成功率时间线
|
||||
|
||||
## 自定义建议
|
||||
|
||||
- 将 `backupx_sla_breach_tasks > 0` 配为 AlertManager 告警
|
||||
- `sum(backupx_node_online) < N` 触发集群容量告警(N 为你集群的最少节点数)
|
||||
- P99 任务耗时突变可用于发现慢任务和资源压力
|
||||
193
deploy/grafana/backupx-dashboard.json
Normal file
193
deploy/grafana/backupx-dashboard.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {"type": "grafana", "uid": "-- Grafana --"},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "BackupX v2.1+ 核心指标面板。对接 /metrics 端点,抓取周期建议 30s(与服务端 Gauge collector 同步)。",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [
|
||||
{
|
||||
"title": "BackupX 文档",
|
||||
"url": "https://awuqing.github.io/BackupX/",
|
||||
"type": "link",
|
||||
"targetBlank": true
|
||||
}
|
||||
],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "正在运行的任务",
|
||||
"gridPos": {"h": 4, "w": 4, "x": 0, "y": 0},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "backupx_task_running", "refId": "A"}],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short",
|
||||
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}, {"color": "yellow", "value": 5}]}
|
||||
}
|
||||
},
|
||||
"options": {"colorMode": "value", "graphMode": "area", "textMode": "auto"}
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "SLA 违约任务数",
|
||||
"gridPos": {"h": 4, "w": 4, "x": 4, "y": 0},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "backupx_sla_breach_tasks", "refId": "A"}],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short",
|
||||
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}, {"color": "red", "value": 1}]}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "在线节点",
|
||||
"gridPos": {"h": 4, "w": 4, "x": 8, "y": 0},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "sum(backupx_node_online)", "refId": "A"}],
|
||||
"fieldConfig": {
|
||||
"defaults": {"unit": "short", "color": {"mode": "thresholds"}, "thresholds": {"steps": [{"color": "red", "value": null}, {"color": "green", "value": 1}]}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "24h 任务成功率",
|
||||
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{
|
||||
"expr": "sum(rate(backupx_task_run_total{status=\"success\"}[24h])) / sum(rate(backupx_task_run_total[24h])) * 100",
|
||||
"refId": "A"
|
||||
}],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent", "decimals": 2,
|
||||
"thresholds": {"mode": "absolute", "steps": [{"color": "red", "value": null}, {"color": "yellow", "value": 95}, {"color": "green", "value": 99}]}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "应用版本",
|
||||
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 0},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "backupx_app_info", "refId": "A", "format": "table", "instant": true}],
|
||||
"options": {"textMode": "value_and_name", "reduceOptions": {"calcs": ["last"], "fields": "/^version$/"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "任务执行速率(按状态)",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 4},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{
|
||||
"expr": "sum by (status) (rate(backupx_task_run_total[5m]))",
|
||||
"refId": "A",
|
||||
"legendFormat": "{{status}}"
|
||||
}],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ops",
|
||||
"custom": {"drawStyle": "line", "lineInterpolation": "smooth", "fillOpacity": 10, "stacking": {"mode": "normal"}}
|
||||
},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName", "options": "success"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}]},
|
||||
{"matcher": {"id": "byName", "options": "failed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "red"}}]}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "任务耗时 P50 / P95 / P99",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 4},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [
|
||||
{"expr": "histogram_quantile(0.50, sum(rate(backupx_task_run_duration_seconds_bucket[10m])) by (le, task_type))", "refId": "A", "legendFormat": "P50 {{task_type}}"},
|
||||
{"expr": "histogram_quantile(0.95, sum(rate(backupx_task_run_duration_seconds_bucket[10m])) by (le, task_type))", "refId": "B", "legendFormat": "P95 {{task_type}}"},
|
||||
{"expr": "histogram_quantile(0.99, sum(rate(backupx_task_run_duration_seconds_bucket[10m])) by (le, task_type))", "refId": "C", "legendFormat": "P99 {{task_type}}"}
|
||||
],
|
||||
"fieldConfig": {"defaults": {"unit": "s"}}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "任务产出字节速率",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 12},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "sum by (task_type) (rate(backupx_task_bytes_total[5m]))", "refId": "A", "legendFormat": "{{task_type}}"}],
|
||||
"fieldConfig": {"defaults": {"unit": "Bps"}}
|
||||
},
|
||||
{
|
||||
"type": "bargauge",
|
||||
"title": "存储目标用量 TopN",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 12},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "topk(10, backupx_storage_used_bytes)", "refId": "A", "legendFormat": "{{target_name}} ({{target_type}})"}],
|
||||
"fieldConfig": {"defaults": {"unit": "bytes"}},
|
||||
"options": {"orientation": "horizontal", "displayMode": "gradient"}
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"title": "节点在线状态",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 20},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [{"expr": "backupx_node_online", "refId": "A", "format": "table", "instant": true}],
|
||||
"transformations": [
|
||||
{"id": "organize", "options": {"excludeByName": {"Time": true, "__name__": true, "job": true, "instance": true}, "indexByName": {"node_name": 0, "role": 1, "Value": 2}, "renameByName": {"Value": "online"}}}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"overrides": [{
|
||||
"matcher": {"id": "byName", "options": "online"},
|
||||
"properties": [{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "离线", "color": "red"}, "1": {"text": "在线", "color": "green"}}}]}]
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "验证 / 恢复 / 复制成功率",
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 20},
|
||||
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
||||
"targets": [
|
||||
{"expr": "sum by (status) (rate(backupx_verify_run_total[15m]))", "refId": "A", "legendFormat": "verify {{status}}"},
|
||||
{"expr": "sum by (status) (rate(backupx_restore_run_total[15m]))", "refId": "B", "legendFormat": "restore {{status}}"},
|
||||
{"expr": "sum by (status) (rate(backupx_replication_run_total[15m]))", "refId": "C", "legendFormat": "replication {{status}}"}
|
||||
],
|
||||
"fieldConfig": {"defaults": {"unit": "ops"}}
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"tags": ["backupx", "backup", "sre"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
|
||||
"label": "Datasource",
|
||||
"name": "DS_PROMETHEUS",
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"type": "datasource"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {"from": "now-6h", "to": "now"},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "BackupX Overview",
|
||||
"uid": "backupx-overview",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -1,35 +1,94 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
PROJECT_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
PREFIX="${PREFIX:-/opt/backupx}"
|
||||
ETC_DIR="${ETC_DIR:-/etc/backupx}"
|
||||
SERVICE_NAME="backupx"
|
||||
APP_USER="backupx"
|
||||
APP_GROUP="backupx"
|
||||
BIN_SOURCE="${BIN_SOURCE:-$PROJECT_ROOT/server/backupx}"
|
||||
WEB_SOURCE="${WEB_SOURCE:-$PROJECT_ROOT/web/dist}"
|
||||
CONFIG_TEMPLATE="${CONFIG_TEMPLATE:-$PROJECT_ROOT/server/config.example.yaml}"
|
||||
SERVICE_SOURCE="${SERVICE_SOURCE:-$PROJECT_ROOT/deploy/backupx.service}"
|
||||
NGINX_SOURCE="${NGINX_SOURCE:-$PROJECT_ROOT/deploy/nginx.conf}"
|
||||
if [ -f "$SCRIPT_DIR/backupx" ] && [ -d "$SCRIPT_DIR/web" ]; then
|
||||
BIN_SOURCE="${BIN_SOURCE:-$SCRIPT_DIR/backupx}"
|
||||
WEB_SOURCE="${WEB_SOURCE:-$SCRIPT_DIR/web}"
|
||||
CONFIG_TEMPLATE="${CONFIG_TEMPLATE:-$SCRIPT_DIR/config.example.yaml}"
|
||||
NGINX_SOURCE="${NGINX_SOURCE:-$SCRIPT_DIR/nginx.conf}"
|
||||
SERVICE_SOURCE_DEFAULT="$SCRIPT_DIR/backupx.service"
|
||||
else
|
||||
SOURCE_BIN_DEFAULT="$PROJECT_ROOT/server/bin/backupx"
|
||||
# Keep compatibility with contributors who built the historical path by
|
||||
# hand, while matching the canonical `make build` output first.
|
||||
if [ ! -f "$SOURCE_BIN_DEFAULT" ] && [ -f "$PROJECT_ROOT/server/backupx" ]; then
|
||||
SOURCE_BIN_DEFAULT="$PROJECT_ROOT/server/backupx"
|
||||
fi
|
||||
BIN_SOURCE="${BIN_SOURCE:-$SOURCE_BIN_DEFAULT}"
|
||||
WEB_SOURCE="${WEB_SOURCE:-$PROJECT_ROOT/web/dist}"
|
||||
CONFIG_TEMPLATE="${CONFIG_TEMPLATE:-$PROJECT_ROOT/server/config.example.yaml}"
|
||||
NGINX_SOURCE="${NGINX_SOURCE:-$PROJECT_ROOT/deploy/nginx.conf}"
|
||||
SERVICE_SOURCE_DEFAULT="$PROJECT_ROOT/deploy/backupx.service"
|
||||
fi
|
||||
SERVICE_SOURCE_EXPLICIT=0
|
||||
if [ -n "${SERVICE_SOURCE:-}" ]; then
|
||||
SERVICE_SOURCE_EXPLICIT=1
|
||||
fi
|
||||
SERVICE_SOURCE="${SERVICE_SOURCE:-$SERVICE_SOURCE_DEFAULT}"
|
||||
INSTALL_NGINX="${INSTALL_NGINX:-0}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "请使用 root 或 sudo 执行安装脚本。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_install_path() {
|
||||
path_name="$1"
|
||||
path_value="$2"
|
||||
case "$path_value" in
|
||||
/*) ;;
|
||||
*) echo "$path_name 必须是绝对路径: $path_value" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$path_value" in
|
||||
/|*"//"*|*"/./"*|*"/."|*"/../"*|*"/.."|*[!A-Za-z0-9_./+-]*)
|
||||
echo "$path_name 必须是规范、安全且非根目录的绝对路径: $path_value" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
validate_install_path PREFIX "$PREFIX"
|
||||
validate_install_path ETC_DIR "$ETC_DIR"
|
||||
|
||||
if [ ! -f "$BIN_SOURCE" ]; then
|
||||
echo "未找到后端二进制:$BIN_SOURCE" >&2
|
||||
echo "请先执行:cd \"$PROJECT_ROOT/server\" && go build -o backupx ./cmd/backupx" >&2
|
||||
echo "Backend binary not found / 未找到后端二进制:$BIN_SOURCE" >&2
|
||||
echo "源码树安装请先在仓库根目录执行 make build(产物:server/bin/backupx)。" >&2
|
||||
echo "For a source install, run 'make build' in the repository root first." >&2
|
||||
echo "发布包安装请确认当前目录包含 ./backupx、./web 和 ./install.sh。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$WEB_SOURCE" ]; then
|
||||
echo "未找到前端构建产物:$WEB_SOURCE" >&2
|
||||
echo "请先执行:cd \"$PROJECT_ROOT/web\" && npm run build" >&2
|
||||
echo "源码树安装请先执行:cd \"$PROJECT_ROOT/web\" && npm run build" >&2
|
||||
echo "发布包安装请确认当前目录包含 ./web。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG_TEMPLATE" ]; then
|
||||
echo "未找到配置模板:$CONFIG_TEMPLATE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SERVICE_SOURCE_EXPLICIT" = "1" ] && [ ! -f "$SERVICE_SOURCE" ]; then
|
||||
echo "指定的 systemd unit 不存在:$SERVICE_SOURCE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for managed_path in "$PREFIX" "$PREFIX/bin" "$PREFIX/web" "$PREFIX/data" "$ETC_DIR"; do
|
||||
if [ -L "$managed_path" ]; then
|
||||
echo "拒绝通过符号链接写入受管目录:$managed_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! getent group "$APP_GROUP" >/dev/null 2>&1; then
|
||||
groupadd --system "$APP_GROUP"
|
||||
fi
|
||||
@@ -38,25 +97,103 @@ if ! id "$APP_USER" >/dev/null 2>&1; then
|
||||
useradd --system --gid "$APP_GROUP" --home-dir "$PREFIX" --shell /usr/sbin/nologin "$APP_USER"
|
||||
fi
|
||||
|
||||
install -d -o "$APP_USER" -g "$APP_GROUP" "$PREFIX" "$PREFIX/bin" "$PREFIX/web" "$PREFIX/data" "$ETC_DIR"
|
||||
install -m 0755 "$BIN_SOURCE" "$PREFIX/bin/backupx"
|
||||
install -d -o root -g root -m 0755 "$PREFIX" "$PREFIX/bin" "$PREFIX/web"
|
||||
install -d -o "$APP_USER" -g "$APP_GROUP" -m 0750 "$PREFIX/data"
|
||||
install -d -o root -g "$APP_GROUP" -m 0750 "$ETC_DIR"
|
||||
install -o root -g root -m 0755 "$BIN_SOURCE" "$PREFIX/bin/backupx.new"
|
||||
mv -f "$PREFIX/bin/backupx.new" "$PREFIX/bin/backupx"
|
||||
cp -R "$WEB_SOURCE/." "$PREFIX/web/"
|
||||
chown -R "$APP_USER:$APP_GROUP" "$PREFIX"
|
||||
chown -R root:root "$PREFIX/bin" "$PREFIX/web"
|
||||
find "$PREFIX/web" -type d -exec chmod 0755 {} \;
|
||||
find "$PREFIX/web" -type f -exec chmod 0644 {} \;
|
||||
chown -R "$APP_USER:$APP_GROUP" "$PREFIX/data"
|
||||
|
||||
if [ ! -f "$ETC_DIR/config.yaml" ]; then
|
||||
install -m 0640 "$CONFIG_TEMPLATE" "$ETC_DIR/config.yaml"
|
||||
install -o root -g "$APP_GROUP" -m 0640 "$CONFIG_TEMPLATE" "$ETC_DIR/config.yaml"
|
||||
fi
|
||||
# 服务账户只需读取配置,不应拥有修改 /etc 配置或可执行文件的权限。
|
||||
chown root:"$APP_GROUP" "$ETC_DIR/config.yaml"
|
||||
chmod 0640 "$ETC_DIR/config.yaml"
|
||||
|
||||
# 仓库 unit 使用标准路径;自定义 PREFIX/ETC_DIR 时动态生成以保持路径一致。
|
||||
# 显式传入 SERVICE_SOURCE 表示调用方已经审核其中的路径,始终优先使用。
|
||||
if [ -f "$SERVICE_SOURCE" ] && { [ "$SERVICE_SOURCE_EXPLICIT" = "1" ] || { [ "$PREFIX" = "/opt/backupx" ] && [ "$ETC_DIR" = "/etc/backupx" ]; }; }; then
|
||||
install -m 0644 "$SERVICE_SOURCE" "/etc/systemd/system/$SERVICE_NAME.service"
|
||||
else
|
||||
cat > "/etc/systemd/system/$SERVICE_NAME.service" <<UNIT
|
||||
[Unit]
|
||||
Description=BackupX API Service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$APP_USER
|
||||
Group=$APP_GROUP
|
||||
WorkingDirectory=$PREFIX
|
||||
ExecStart=$PREFIX/bin/backupx -config $ETC_DIR/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
UMask=0027
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
if ! systemctl enable "$SERVICE_NAME" || ! systemctl restart "$SERVICE_NAME"; then
|
||||
echo "BackupX systemd 服务启动失败。" >&2
|
||||
systemctl status "$SERVICE_NAME" --no-pager >&2 || true
|
||||
journalctl -u "$SERVICE_NAME" -n 50 --no-pager >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0644 "$SERVICE_SOURCE" "/etc/systemd/system/$SERVICE_NAME.service"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME"
|
||||
|
||||
if [ -d "/etc/nginx/conf.d" ]; then
|
||||
install -m 0644 "$NGINX_SOURCE" "/etc/nginx/conf.d/$SERVICE_NAME.conf"
|
||||
if command -v nginx >/dev/null 2>&1; then
|
||||
nginx -t
|
||||
systemctl reload nginx || true
|
||||
# systemctl may return before the process has opened its HTTP listener. Verify
|
||||
# the same unauthenticated endpoint used by the first-administrator screen so a
|
||||
# broken bare-metal install cannot print a false success message.
|
||||
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8340/api/auth/setup/status}"
|
||||
READY=0
|
||||
ATTEMPT=1
|
||||
while [ "$ATTEMPT" -le 30 ]; do
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
if wget -q -T 2 -O /dev/null "$HEALTH_URL"; then
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
else
|
||||
echo "Warning / 警告:未找到 curl 或 wget,仅验证 systemd 服务状态。" >&2
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$READY" -ne 1 ]; then
|
||||
echo "BackupX did not become ready at $HEALTH_URL / 服务未通过就绪检查。" >&2
|
||||
systemctl status "$SERVICE_NAME" --no-pager >&2 || true
|
||||
journalctl -u "$SERVICE_NAME" -n 50 --no-pager >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$INSTALL_NGINX" = "1" ]; then
|
||||
if [ ! -d "/etc/nginx/conf.d" ] || [ ! -f "$NGINX_SOURCE" ]; then
|
||||
echo "已请求安装 Nginx 配置,但未找到 /etc/nginx/conf.d 或配置模板。" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -o root -g root -m 0644 "$NGINX_SOURCE" "/etc/nginx/conf.d/$SERVICE_NAME.conf"
|
||||
command -v nginx >/dev/null 2>&1 || { echo "未找到 nginx 命令。" >&2; exit 1; }
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
fi
|
||||
|
||||
cat <<MESSAGE
|
||||
@@ -67,6 +204,20 @@ cat <<MESSAGE
|
||||
- 配置文件:$ETC_DIR/config.yaml
|
||||
- systemd 服务:/etc/systemd/system/$SERVICE_NAME.service
|
||||
|
||||
Web 控制台已由后端直接托管,无需额外的 nginx 反向代理即可访问:
|
||||
http://<本机IP>:8340
|
||||
|
||||
首次访问 / First sign-in:
|
||||
1. 打开上面的地址,并可在登录页右上角选择 中文 或 English。
|
||||
2. 页面显示“系统初始化 / System setup”时,创建首个管理员用户名和密码。
|
||||
3. 如果未显示初始化表单,请先检查:$HEALTH_URL
|
||||
|
||||
如需安装仓库提供的 Nginx 模板,请审核域名与 TLS 配置后重新执行:
|
||||
sudo INSTALL_NGINX=1 ./install.sh
|
||||
|
||||
排查:若服务未监听端口,请查看日志:
|
||||
journalctl -u "$SERVICE_NAME" -n 50 --no-pager
|
||||
|
||||
如需修改监听地址、数据库路径或日志级别,请编辑 "$ETC_DIR/config.yaml" 后执行:
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
MESSAGE
|
||||
|
||||
@@ -8,16 +8,38 @@ server {
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8340/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header Connection "";
|
||||
client_max_body_size 0;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# Agent 一键安装脚本路径(兼容 v2.0 及之前生成的命令)。
|
||||
# v2.1+ 新生成的命令走 /api/install/... 自动命中上面的 /api/ 代理。
|
||||
location /install/ {
|
||||
proxy_pass http://127.0.0.1:8340/install/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# 健康检查端点同样不走 SPA fallback。
|
||||
location = /health { proxy_pass http://127.0.0.1:8340/health; }
|
||||
location = /ready { proxy_pass http://127.0.0.1:8340/ready; }
|
||||
location = /metrics { proxy_pass http://127.0.0.1:8340/metrics; }
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
@@ -3,27 +3,46 @@
|
||||
# 快速启动:docker compose up -d
|
||||
# 访问地址:http://localhost:8340
|
||||
#
|
||||
# 如需从源码构建镜像(而非拉取线上镜像),取消注释 build 行并注释 image 行。
|
||||
# 生产环境建议在 .env 中固定 BACKUPX_IMAGE 版本,并通过 HTTPS 反向代理暴露服务。
|
||||
|
||||
services:
|
||||
backupx:
|
||||
image: awuqing/backupx:latest
|
||||
# build: . # 从源码构建时取消此行注释
|
||||
image: ${BACKUPX_IMAGE:-awuqing/backupx:latest}
|
||||
# build: .
|
||||
container_name: backupx
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
stop_grace_period: 30s
|
||||
ports:
|
||||
- "8340:8340"
|
||||
- "${BACKUPX_BIND_ADDRESS:-0.0.0.0}:${BACKUPX_PORT:-8340}:8340"
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
# 挂载需要备份的宿主机目录(按需添加,:ro 表示只读):
|
||||
# - /var/www:/mnt/www:ro
|
||||
# - /etc/nginx:/mnt/nginx-conf:ro
|
||||
# - /home/user/data:/mnt/data:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
# 仅用于旧数据卷迁移与降权;应用进程随后以 backupx 运行。
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- SETGID
|
||||
- SETUID
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# 通过 BACKUPX_ 前缀环境变量覆盖配置:
|
||||
# - BACKUPX_LOG_LEVEL=debug
|
||||
# - BACKUPX_BACKUP_MAX_CONCURRENT=4
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
# 远程 Agent 连接 Master 时,配置为所有节点可达的稳定 HTTPS URL:
|
||||
# BACKUPX_SERVER_EXTERNAL_URL: https://backup.example.com
|
||||
# BACKUPX_LOG_LEVEL: debug
|
||||
# BACKUPX_BACKUP_MAX_CONCURRENT: "4"
|
||||
healthcheck:
|
||||
test: ["CMD", "su-exec", "backupx:backupx", "wget", "-q", "-T", "3", "-O", "/dev/null", "http://127.0.0.1:8340/ready"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
backupx-data:
|
||||
|
||||
20
docs-site/.gitignore
vendored
Normal file
20
docs-site/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
28
docs-site/README.md
Normal file
28
docs-site/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# BackupX documentation site
|
||||
|
||||
The public documentation is a Docusaurus site with English source documents and a complete Simplified Chinese translation.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm start
|
||||
```
|
||||
|
||||
Use `npm start -- --locale zh-Hans` to preview the Chinese site. The public Chinese URL remains `/zh-Hans/`; its source files live under `i18n/zh-CN/` through the locale `path` mapping in `docusaurus.config.ts`.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
```
|
||||
|
||||
The production build renders both locales and fails on broken document links. GitHub Actions publishes `build/` to GitHub Pages after changes reach `main`; do not deploy the site manually from a feature branch.
|
||||
|
||||
When adding, renaming, or removing a document:
|
||||
|
||||
1. Apply the same change under `docs/` and `i18n/zh-CN/docusaurus-plugin-content-docs/current/`.
|
||||
2. Update `sidebars.ts` and the translated sidebar labels when a category changes.
|
||||
3. Use relative links for links between documents so both locale prefixes resolve correctly.
|
||||
4. Run the full verification commands before opening a pull request.
|
||||
121
docs-site/docs/deployment/bare-metal.md
Normal file
121
docs-site/docs/deployment/bare-metal.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Bare-metal Deployment
|
||||
description: Hardened systemd deployment from the prebuilt release tarball or source, with opt-in Nginx.
|
||||
---
|
||||
|
||||
# Bare-metal Deployment
|
||||
|
||||
## From prebuilt release
|
||||
|
||||
```bash
|
||||
# Download the matching tarball
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz.sha256
|
||||
sha256sum -c backupx-linux-amd64.tar.gz.sha256
|
||||
|
||||
# Extract and install
|
||||
tar xzf backupx-linux-amd64.tar.gz && cd backupx-*-linux-amd64
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
The installer performs these steps automatically:
|
||||
|
||||
1. Creates a system user `backupx`
|
||||
2. Copies the binary to `/opt/backupx/bin/backupx` and the web console to `/opt/backupx/web`
|
||||
3. Installs the default configuration at `/etc/backupx/config.yaml`
|
||||
4. Installs `backupx.service` (systemd), enabled at boot
|
||||
5. Leaves Nginx unchanged unless `INSTALL_NGINX=1` is explicitly requested
|
||||
6. Verifies the first-setup API before reporting success
|
||||
|
||||
The executable and web assets are owned by root; only `/opt/backupx/data` is writable by the `backupx` service account. `/etc/backupx/config.yaml` is installed as `root:backupx` with mode `0640`.
|
||||
|
||||
The bundled Nginx template is a starting point and may conflict with an existing default server. Review its hostname and TLS policy first, then opt in:
|
||||
|
||||
```bash
|
||||
sudo INSTALL_NGINX=1 ./install.sh
|
||||
```
|
||||
|
||||
For multi-node clusters, edit `/etc/backupx/config.yaml` after installation and set the Master URL that remote Agents can reach:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
external_url: "https://backup.example.com"
|
||||
```
|
||||
|
||||
Restart BackupX after changing it:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart backupx
|
||||
```
|
||||
|
||||
## From source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
`make build` compiles:
|
||||
|
||||
- `server/bin/backupx` (Go backend, no CGO)
|
||||
- `web/dist/` (React frontend, `npm run build`)
|
||||
|
||||
## systemd
|
||||
|
||||
The installed unit:
|
||||
|
||||
```ini title="/etc/systemd/system/backupx.service"
|
||||
[Unit]
|
||||
Description=BackupX API Service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=backupx
|
||||
Group=backupx
|
||||
WorkingDirectory=/opt/backupx
|
||||
ExecStart=/opt/backupx/bin/backupx -config /etc/backupx/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
UMask=0027
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Typical operations:
|
||||
|
||||
```bash
|
||||
sudo systemctl status backupx
|
||||
sudo journalctl -u backupx -f # live logs
|
||||
sudo systemctl restart backupx
|
||||
curl -fsS http://127.0.0.1:8340/api/auth/setup/status
|
||||
```
|
||||
|
||||
Open `http://your-server:8340`, switch to English if desired, and create the first administrator on the **System setup** screen. For a custom listen port, run the installer with a matching `HEALTH_URL`.
|
||||
|
||||
For production, expose BackupX through HTTPS or restrict port `8340` at the firewall. The installer does not make firewall changes.
|
||||
|
||||
Before replacing a release, snapshot `/etc/backupx`, `/opt/backupx/data`, the installed binary, and web assets while the service is stopped. Follow the versioned procedure in [Upgrade and Recovery](../operations/upgrade-recovery); running an older binary against a database already migrated by a newer release is not a safe rollback.
|
||||
|
||||
## Password reset
|
||||
|
||||
If the admin password is lost:
|
||||
|
||||
```bash
|
||||
/opt/backupx/bin/backupx reset-password \
|
||||
--username admin \
|
||||
--password 'newpass123' \
|
||||
--config /etc/backupx/config.yaml
|
||||
```
|
||||
|
||||
Docker equivalent:
|
||||
|
||||
```bash
|
||||
docker exec -it backupx /app/bin/backupx reset-password --username admin --password 'newpass123'
|
||||
```
|
||||
97
docs-site/docs/deployment/configuration.md
Normal file
97
docs-site/docs/deployment/configuration.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: Configuration Reference
|
||||
description: All config.yaml server keys with defaults and matching environment variables.
|
||||
---
|
||||
|
||||
# Configuration Reference
|
||||
|
||||
BackupX loads `./config.yaml` from the working directory by default. You can override the path with `--config`. Every key can also be set via a `BACKUPX_` prefixed environment variable.
|
||||
|
||||
## Full config reference
|
||||
|
||||
```yaml title="config.yaml"
|
||||
server:
|
||||
host: "0.0.0.0" # BACKUPX_SERVER_HOST
|
||||
port: 8340 # BACKUPX_SERVER_PORT
|
||||
mode: "release" # release | debug
|
||||
external_url: "" # BACKUPX_SERVER_EXTERNAL_URL — stable public Master URL
|
||||
trusted_proxies: # BACKUPX_SERVER_TRUSTED_PROXIES — exact proxy IPs/CIDRs
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
web_root: "" # BACKUPX_SERVER_WEB_ROOT — built frontend directory
|
||||
|
||||
database:
|
||||
path: "./data/backupx.db" # BACKUPX_DATABASE_PATH — embedded SQLite
|
||||
|
||||
security:
|
||||
jwt_secret: "" # BACKUPX_SECURITY_JWT_SECRET — auto-generated if empty
|
||||
jwt_expire: "24h" # BACKUPX_SECURITY_JWT_EXPIRE
|
||||
encryption_key: "" # AES-256-GCM key for storage config encryption
|
||||
|
||||
backup:
|
||||
temp_dir: "/tmp/backupx" # BACKUPX_BACKUP_TEMP_DIR
|
||||
max_concurrent: 2 # BACKUPX_BACKUP_MAX_CONCURRENT
|
||||
retries: 10 # Per-upload rclone low-level retries
|
||||
bandwidth_limit: "" # e.g. "10M" to cap transfers at 10 MB/s
|
||||
|
||||
log:
|
||||
level: "info" # debug | info | warn | error
|
||||
file: "./data/backupx.log"
|
||||
max_size: 100 # MB per log file
|
||||
max_backups: 3 # rotated files retained
|
||||
max_age: 30 # retention in days
|
||||
```
|
||||
|
||||
## Secret generation
|
||||
|
||||
If `jwt_secret` or `encryption_key` is empty on first start, BackupX generates a random value and persists it to the `system_configs` table. Keep a backup of `data/backupx.db` — losing it invalidates all existing encrypted storage configurations.
|
||||
|
||||
## Environment variables
|
||||
|
||||
The environment wins when both file and env are set. All dot-paths become underscores and uppercase:
|
||||
|
||||
| Config key | Env variable |
|
||||
|------------|--------------|
|
||||
| `server.port` | `BACKUPX_SERVER_PORT` |
|
||||
| `server.external_url` | `BACKUPX_SERVER_EXTERNAL_URL` |
|
||||
| `server.trusted_proxies` | `BACKUPX_SERVER_TRUSTED_PROXIES` (comma-separated for env) |
|
||||
| `security.jwt_secret` | `BACKUPX_SECURITY_JWT_SECRET` |
|
||||
| `security.jwt_expire` | `BACKUPX_SECURITY_JWT_EXPIRE` |
|
||||
| `security.encryption_key` | `BACKUPX_SECURITY_ENCRYPTION_KEY` |
|
||||
| `log.level` | `BACKUPX_LOG_LEVEL` |
|
||||
| `backup.max_concurrent` | `BACKUPX_BACKUP_MAX_CONCURRENT` |
|
||||
| `backup.temp_dir` | `BACKUPX_BACKUP_TEMP_DIR` |
|
||||
| `backup.retries` | `BACKUPX_BACKUP_RETRIES` |
|
||||
| `backup.bandwidth_limit` | `BACKUPX_BACKUP_BANDWIDTH_LIMIT` |
|
||||
| `log.max_size` | `BACKUPX_LOG_MAX_SIZE` |
|
||||
| `log.max_backups` | `BACKUPX_LOG_MAX_BACKUPS` |
|
||||
| `log.max_age` | `BACKUPX_LOG_MAX_AGE` |
|
||||
|
||||
## Master external URL
|
||||
|
||||
Set `server.external_url` when BackupX is behind Docker, Nginx, a load balancer, or any reverse proxy whose internal Host is not reachable by remote Agents:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
external_url: "https://backup.example.com"
|
||||
```
|
||||
|
||||
This value is used when BackupX renders one-click Agent install scripts and docker-compose snippets. It must be reachable from every Agent host. Leave it empty only when `X-Forwarded-Proto` / `X-Forwarded-Host` are reliable and point to the same URL that Agents can access.
|
||||
|
||||
The install wizard can set an Agent-specific URL for a proxy or SSH-bastion node. That override is used by both the target-side one-time install URL and the generated Agent runtime configuration, while the browser continues to use the normal public address.
|
||||
|
||||
## Trusted reverse proxies
|
||||
|
||||
BackupX trusts forwarded client-address headers only from `server.trusted_proxies`. The default permits loopback Nginx only. If a reverse proxy runs in another container or host, add its exact IP or subnet:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
trusted_proxies:
|
||||
- "127.0.0.1"
|
||||
- "172.18.0.0/16"
|
||||
```
|
||||
|
||||
Do not configure `0.0.0.0/0`: client addresses feed authentication throttling, install-token throttling, and audit records. Set an empty list when BackupX is exposed directly and should trust no forwarded headers.
|
||||
|
||||
Back up the complete data directory and configuration before changing security keys or database paths. See [Upgrade and Recovery](../operations/upgrade-recovery) for a tested snapshot and rollback sequence.
|
||||
98
docs-site/docs/deployment/docker.md
Normal file
98
docs-site/docs/deployment/docker.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Docker Deployment
|
||||
description: Hardened single-process Docker deployment with health checks and persistent data.
|
||||
---
|
||||
|
||||
# Docker Deployment
|
||||
|
||||
The official [`awuqing/backupx`](https://hub.docker.com/r/awuqing/backupx) image supports `linux/amd64` and `linux/arm64`.
|
||||
|
||||
## Compose file
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
backupx:
|
||||
image: ${BACKUPX_IMAGE:-awuqing/backupx:latest}
|
||||
container_name: backupx
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
stop_grace_period: 30s
|
||||
ports:
|
||||
- "${BACKUPX_BIND_ADDRESS:-0.0.0.0}:${BACKUPX_PORT:-8340}:8340"
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
# - /var/www:/mnt/www:ro
|
||||
# - /etc/nginx:/mnt/nginx-conf:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- SETGID
|
||||
- SETUID
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
# BACKUPX_SERVER_EXTERNAL_URL: https://backup.example.com
|
||||
BACKUPX_LOG_LEVEL: info
|
||||
BACKUPX_BACKUP_MAX_CONCURRENT: "2"
|
||||
healthcheck:
|
||||
test: ["CMD", "su-exec", "backupx:backupx", "wget", "-q", "-T", "3", "-O", "/dev/null", "http://127.0.0.1:8340/ready"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
backupx-data:
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
The entrypoint uses root only to migrate ownership of data written by older images, then starts one unprivileged `backupx` process. Compose retains only the ownership and UID/GID transition capabilities needed for that initialization. The backend serves both the API and built web assets; the image neither mounts the Docker socket nor bundles a Docker CLI. Pin `BACKUPX_IMAGE` to a release tag in production.
|
||||
|
||||
## Host-directory backups
|
||||
|
||||
Mount each source directory and use its container path in the task. The container's `backupx` user must be able to read it; restore destinations need a separate, narrowly scoped writable mount. Prefer a remote Agent for privileged host paths. If a Master-side task truly requires root, make that exception explicit with `user: "0:0"` and review every mount.
|
||||
|
||||
## Multi-node cluster
|
||||
|
||||
Set the stable URL that Agents can reach:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
BACKUPX_SERVER_EXTERNAL_URL: https://backup.example.com
|
||||
```
|
||||
|
||||
Use HTTPS across untrusted networks. Proxy, private-CA, and SSH-bastion deployments are covered in [Multi-Node Cluster](../features/multi-node).
|
||||
|
||||
If an external reverse proxy is in another container, add only its bridge subnet to `BACKUPX_SERVER_TRUSTED_PROXIES`, for example `172.18.0.0/16`. Do not trust every address.
|
||||
|
||||
## Environment overrides
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
BACKUPX_LOG_LEVEL: debug
|
||||
BACKUPX_BACKUP_MAX_CONCURRENT: "4"
|
||||
BACKUPX_BACKUP_TEMP_DIR: /tmp/backupx
|
||||
```
|
||||
|
||||
The image's internal port is fixed at `8340`; change only the published host port with `BACKUPX_PORT`.
|
||||
|
||||
## Upgrade prerequisites
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
Wait for `healthy` before switching traffic or removing an old deployment. Before upgrades, stop the Master for a file-level copy or take an atomic snapshot of the entire `backupx-data` volume. Keep exactly one active Master for a data volume; SQLite does not support multiple Master containers sharing `/app/data`.
|
||||
|
||||
Use a release tag or digest instead of `latest`, and keep the matching pre-upgrade data snapshot. The complete upgrade, rollback, and disaster-recovery procedure is in [Upgrade and Recovery](../operations/upgrade-recovery).
|
||||
82
docs-site/docs/deployment/nginx.md
Normal file
82
docs-site/docs/deployment/nginx.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Nginx Reverse Proxy
|
||||
description: Expose BackupX behind Nginx with HTTPS and SSE-friendly buffering disabled.
|
||||
---
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
|
||||
A minimal production-ready Nginx site for BackupX:
|
||||
|
||||
```nginx title="/etc/nginx/sites-available/backupx"
|
||||
server {
|
||||
listen 80;
|
||||
server_name backup.example.com;
|
||||
|
||||
# Static UI (served from /opt/backupx/web)
|
||||
location / {
|
||||
root /opt/backupx/web;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API reverse proxy
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8340;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Large uploads (restore flow)
|
||||
client_max_body_size 0;
|
||||
proxy_request_buffering off;
|
||||
|
||||
# Live log stream uses SSE — buffering must be off
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# Compatibility route for installers generated by older releases.
|
||||
# Current installers use /api/install/ through the API block above.
|
||||
location /install/ {
|
||||
proxy_pass http://127.0.0.1:8340/install/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# Keep probes and metrics out of the SPA fallback.
|
||||
location = /health { proxy_pass http://127.0.0.1:8340/health; }
|
||||
location = /ready { proxy_pass http://127.0.0.1:8340/ready; }
|
||||
location = /metrics { proxy_pass http://127.0.0.1:8340/metrics; }
|
||||
}
|
||||
```
|
||||
|
||||
`proxy_request_buffering off` is required for Master-relay cluster backups. Without it, Nginx writes the complete Agent upload to its temporary storage before BackupX receives it, defeating streaming and potentially filling the proxy disk.
|
||||
|
||||
If Nginx runs on another host or in another container, add only that proxy IP or subnet to `server.trusted_proxies`. Do not use `0.0.0.0/0`; BackupX uses the trusted client address for login throttling, install-token throttling, and audit records.
|
||||
|
||||
`/health`, `/ready`, and `/metrics` do not require BackupX authentication. Allow probe and Prometheus source networks explicitly, or keep these locations on an internal listener instead of exposing them to the Internet.
|
||||
|
||||
## HTTPS with certbot
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d backup.example.com
|
||||
```
|
||||
|
||||
Certbot rewrites the config to listen on 443 with auto-renewal.
|
||||
|
||||
:::caution Agent needs a stable URL
|
||||
If Master is behind HTTPS, remote Agent deployments must use the final HTTPS URL for `--master`; redirects are not followed. For a private CA, pre-provision its PEM certificate and use `--ca-cert /path/to/ca.pem`. Reserve `--insecure-tls` for short-lived testing.
|
||||
:::
|
||||
41
docs-site/docs/development/contributing.md
Normal file
41
docs-site/docs/development/contributing.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Contributing
|
||||
description: How to report issues, propose changes, and submit PRs.
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
BackupX is open-source under Apache License 2.0. Issues and pull requests are welcome.
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Open an issue at [github.com/Awuqing/BackupX/issues](https://github.com/Awuqing/BackupX/issues). Please include:
|
||||
|
||||
- BackupX version (`backupx --version`)
|
||||
- Your deployment mode (Docker / bare metal / from source)
|
||||
- Relevant backup task type and storage backend
|
||||
- Steps to reproduce
|
||||
- Stdout / `backupx.log` excerpt for the window around the problem
|
||||
|
||||
## Proposing changes
|
||||
|
||||
For significant features or refactors, open an issue first to align on scope before investing in a PR.
|
||||
|
||||
## Pull requests
|
||||
|
||||
1. Fork and create a topic branch (e.g. `fix/windows-path-escape`)
|
||||
2. Run `make test` and make sure everything passes
|
||||
3. Keep changes focused — one concern per PR
|
||||
4. Write commit messages in Chinese following `类型: 简要描述` — examples:
|
||||
- `功能: 新增审计日志模块`
|
||||
- `修复: 目录浏览器无法进入子目录`
|
||||
- `重构: 简化存储目标解密逻辑`
|
||||
- Types: `功能` / `修复` / `重构` / `文档` / `构建` / `测试`
|
||||
5. PR title and body in Chinese too. Describe the why and how, not just the what.
|
||||
|
||||
## Coding guidelines
|
||||
|
||||
- **Go** — handle every error (no `_ = err`); use the existing logger (`zap`); no `fmt.Println` in production paths
|
||||
- **TypeScript** — strict mode, no implicit any, follow existing ESLint/Prettier configs
|
||||
- **Commit scope** — one logical change per commit; don't mix drive-by cleanups with feature work
|
||||
83
docs-site/docs/development/setup.md
Normal file
83
docs-site/docs/development/setup.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Development Setup
|
||||
description: Get a BackupX dev environment running — backend, frontend, tests.
|
||||
---
|
||||
|
||||
# Development Setup
|
||||
|
||||
**Requirements:** Go ≥ 1.25, Node.js ≥ 20, npm.
|
||||
|
||||
## Clone & install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
cd web && npm install && cd ..
|
||||
```
|
||||
|
||||
## Dev servers
|
||||
|
||||
Run the backend and the Vite dev server in two terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1: backend on :8340
|
||||
make dev-server
|
||||
|
||||
# Terminal 2: Vite with HMR on :5173
|
||||
make dev-web
|
||||
```
|
||||
|
||||
The Vite config proxies `/api` to `http://127.0.0.1:8340` so you can open the UI at `http://localhost:5173`.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
make test # runs Go + Web test suites
|
||||
make test-server # Go only
|
||||
make test-web # Vitest only
|
||||
```
|
||||
|
||||
## Production build
|
||||
|
||||
```bash
|
||||
make build # server/bin/backupx + web/dist
|
||||
make docker # Docker image
|
||||
make docker-cn # Docker image with mainland China mirrors
|
||||
```
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Component | Stack |
|
||||
|-----------|-------|
|
||||
| **Backend** | Go · Gin · GORM · SQLite · robfig/cron · rclone |
|
||||
| **Frontend** | React 18 · TypeScript · ArcoDesign · Vite · Zustand · ECharts |
|
||||
| **Storage** | rclone (70+ backends) · AWS SDK v2 · Google Drive API v3 |
|
||||
| **Security** | JWT · bcrypt · AES-256-GCM |
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
BackupX/
|
||||
├── server/ # Go backend
|
||||
│ ├── cmd/backupx/ # Entry point + subcommands (agent, backint, reset-password)
|
||||
│ ├── internal/
|
||||
│ │ ├── agent/ # Agent CLI logic
|
||||
│ │ ├── app/ # Wiring (repositories → services → handlers)
|
||||
│ │ ├── backup/ # Backup runners (file / mysql / postgres / sqlite / saphana)
|
||||
│ │ ├── backint/ # SAP HANA Backint protocol
|
||||
│ │ ├── http/ # HTTP handlers + router
|
||||
│ │ ├── model/ # GORM models
|
||||
│ │ ├── repository/ # DB access
|
||||
│ │ ├── service/ # Business logic
|
||||
│ │ └── storage/ # Storage providers (rclone + direct SDKs)
|
||||
│ └── pkg/ # Generic utilities
|
||||
├── web/ # React frontend (Vite)
|
||||
│ └── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ ├── services/
|
||||
│ └── types/
|
||||
├── docs-site/ # This documentation site (Docusaurus)
|
||||
├── deploy/ # install.sh, systemd unit, nginx config
|
||||
└── Makefile
|
||||
```
|
||||
54
docs-site/docs/features/backup-types.md
Normal file
54
docs-site/docs/features/backup-types.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Backup Types
|
||||
description: File, MySQL, PostgreSQL, SQLite and SAP HANA — what they back up and what to configure.
|
||||
---
|
||||
|
||||
# Backup Types
|
||||
|
||||
BackupX supports five built-in backup types. Type determines which runner executes the job.
|
||||
|
||||
When a task is routed to a remote Agent, the source tools and paths are resolved on that Agent host. Multi-target uploads are still tracked per storage target; if at least one target succeeds, the backup record is marked successful and the per-target result table shows partial failures.
|
||||
|
||||
## File / Directory
|
||||
|
||||
File tasks offer three backup modes:
|
||||
|
||||
- **Full archive** — writes a self-contained tar artifact on every run
|
||||
- **Differential archive** — writes only changes since the current full baseline and periodically refreshes that baseline
|
||||
- **CDC repository** — splits content with stable 512 KiB / 1 MiB / 4 MiB boundaries, stores new chunks in immutable 32 MiB packs, and writes a small snapshot manifest for each run
|
||||
|
||||
The CDC repository deduplicates identical content across files and snapshots. Restore, selective restore, verification, download-as-tar, retention, and garbage collection all resolve data through the repository index. Compression and encryption are applied per chunk; encrypted repositories use keyed chunk IDs so plaintext hashes are not exposed.
|
||||
|
||||
Repository mode currently uses a single-writer index and therefore runs on the Master only. To keep repository copies on multiple backends, select multiple primary storage targets on the task. Object-level replication is intentionally disabled because a snapshot manifest without its shared packs and indexes is not a complete backup.
|
||||
|
||||
Common file-task options:
|
||||
|
||||
- **Source** accepts multiple paths — one per line in the UI
|
||||
- **Exclude patterns** accept gitignore-style globs
|
||||
- Supports following symlinks, preserving permissions
|
||||
- Full and differential modes output `.tar`, `.tar.gz`, or `.tar.zst` artifacts
|
||||
|
||||
## MySQL
|
||||
|
||||
Uses `mysqldump` under the hood. Requires `mysqldump` to be on `$PATH` of the host running the task (Master or Agent).
|
||||
|
||||
- **Host / port / user / password / database** — multi-database allowed (comma-separated)
|
||||
- Output: `.sql` or `.sql.gz`
|
||||
- Default flags: `--single-transaction --routines --triggers --events`
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Uses `pg_dump`. Same connection fields as MySQL plus database name.
|
||||
|
||||
## SQLite
|
||||
|
||||
Copies the database file directly (with a consistency snapshot). No external tool required.
|
||||
|
||||
## SAP HANA
|
||||
|
||||
Two modes are supported — see the dedicated [SAP HANA](./sap-hana) page.
|
||||
|
||||
## Deletion behavior
|
||||
|
||||
When a task is deleted, BackupX removes backup artifacts from every storage target but preserves backup records for audit. Task deletion also tears down the cron schedule entry.
|
||||
229
docs-site/docs/features/multi-node.md
Normal file
229
docs-site/docs/features/multi-node.md
Normal file
@@ -0,0 +1,229 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: Multi-Node Cluster
|
||||
description: Deploy BackupX Agents through direct HTTPS, forward proxies, or SSH bastions.
|
||||
---
|
||||
|
||||
# Multi-Node Cluster
|
||||
|
||||
BackupX uses a single active Master as the control plane and an Agent on each source server. Agents initiate every connection, report a heartbeat every 15 seconds, and poll for commands every 5 seconds. No inbound Agent port is required.
|
||||
|
||||
## Architecture and boundaries
|
||||
|
||||
```text
|
||||
[Web console] ────────> [Active Master + SQLite]
|
||||
^
|
||||
| outbound HTTP(S) polling
|
||||
+---------+---------+
|
||||
| | |
|
||||
[Agent B] [Agent C] [Agent D]
|
||||
| | |
|
||||
+----> storage targets
|
||||
```
|
||||
|
||||
- Each node has an independent Agent Token. The Agent never receives the Master's JWT or encryption key.
|
||||
- A node is marked offline after 45 seconds without a heartbeat.
|
||||
- The Master persists commands; an Agent claims and executes them locally.
|
||||
- Network storage is normally written directly by the Agent. A Master-local target can opt into authenticated streaming relay.
|
||||
|
||||
:::warning Single-active Master
|
||||
The embedded SQLite database is not a shared multi-writer database. Run exactly one active Master against a data directory. For control-plane recovery, use an active/passive host, persistent-volume snapshots, and a stable DNS name or virtual IP. Never scale multiple Master replicas over the same `/app/data` or `backupx.db`.
|
||||
:::
|
||||
|
||||
BackupX applies a five-second SQLite busy timeout and command-queue indexes to reduce contention from concurrent Agent polls and task updates. Keep the database on a local or block-backed filesystem. For a file-level control-plane backup, stop the Master before copying the whole data directory; do not copy only `backupx.db` while it is running.
|
||||
|
||||
## Choose a network path
|
||||
|
||||
| Scenario | Agent Master URL | Agent proxy URL | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Routed network or public service | `https://backup.example.com` | empty | Recommended; allow only outbound TCP 443 |
|
||||
| Corporate forward proxy | `https://backup.example.com` | `http://proxy.internal:3128` | HTTP(S) and SOCKS5(H) are supported |
|
||||
| SSH dynamic tunnel through a bastion | `https://backup.internal` | `socks5h://127.0.0.1:1080` | Preserves TLS hostname and resolves internal DNS through the tunnel |
|
||||
| SSH fixed local forward | `http://127.0.0.1:18340` | empty | The HTTP hop is protected by SSH; bind the forward to loopback only |
|
||||
|
||||
For private PKI, provide the absolute path of a pre-provisioned PEM CA certificate. Do not use `--insecure-tls` in production.
|
||||
|
||||
When no explicit proxy is configured, Agent-to-Master HTTP traffic follows `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. A system service does not normally inherit an interactive shell's environment, so set the proxy in the install wizard or Agent YAML for systemd deployments.
|
||||
|
||||
## Prepare the Master
|
||||
|
||||
Set a stable URL before generating commands:
|
||||
|
||||
```yaml title="/etc/backupx/config.yaml"
|
||||
server:
|
||||
external_url: "https://backup.example.com"
|
||||
trusted_proxies:
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
# Add the exact reverse-proxy IP or subnet when it is not local.
|
||||
# - "172.18.0.0/16"
|
||||
```
|
||||
|
||||
`external_url` is the default install and Agent runtime address. A restricted node can override both generated target-side URLs with its tunnel or internal address while the browser continues to use the public address.
|
||||
|
||||
Use HTTPS across untrusted networks. For Master-relay uploads, configure the reverse proxy with unlimited request body size and request buffering disabled; see [Nginx Reverse Proxy](../deployment/nginx).
|
||||
|
||||
Configure the Agent with the final API URL, not an HTTP-to-HTTPS redirect. The Agent deliberately does not follow redirects so its authentication Token cannot be forwarded to an unintended host.
|
||||
|
||||
## Deploy an Agent
|
||||
|
||||
Open **Node Management → Add Node**:
|
||||
|
||||
1. Enter one node name, or up to 50 names in batch mode.
|
||||
2. Select systemd, Docker, or foreground mode; architecture; Agent release; command TTL; and download source.
|
||||
3. Select **Direct** or **Proxy or bastion**. For the restricted path, set an Agent-specific Master URL, proxy URL, or private CA path.
|
||||
4. Copy the generated command to the target host and run it with root privileges.
|
||||
|
||||
Systemd is recommended for host-file backup and restore because the Agent needs access to arbitrary local paths. A Docker Agent sees only explicitly mounted paths; recreate it with read-only backup-source mounts and separately scoped writable restore destinations before assigning file tasks.
|
||||
|
||||
The URL-based command downloads a one-time installer and verifies its marker before execution. The wizard binds the selected Agent URL, explicit proxy, and private CA to that download command as well as to the installed Agent configuration. If the install endpoint is still unreachable, use the separately displayed embedded command. The embedded command contains the long-lived node Token and must be handled as a secret.
|
||||
|
||||
The installer:
|
||||
|
||||
1. Detects `linux/amd64` or `linux/arm64`.
|
||||
2. Downloads the selected Release archive through the explicit proxy when configured, otherwise using the host's normal direct/environment-proxy route, and verifies its SHA-256 sidecar when the release provides one.
|
||||
3. Writes `/etc/backupx-agent/config.yaml` and `/etc/backupx-agent/agent.token` with mode `0600`.
|
||||
4. Keeps the Token out of the systemd unit and Docker environment metadata.
|
||||
5. Starts the Agent and checks `/api/v1/agent/self` for up to 30 seconds.
|
||||
6. Returns non-zero with systemd or Docker diagnostics when the node does not become online.
|
||||
|
||||
Older releases without checksum sidecars remain installable with a warning. New releases should always publish and verify the sidecar.
|
||||
|
||||
### Installed systemd configuration
|
||||
|
||||
```yaml title="/etc/backupx-agent/config.yaml"
|
||||
master: "https://backup.example.com"
|
||||
tokenFile: "/etc/backupx-agent/agent.token"
|
||||
heartbeatInterval: "15s"
|
||||
pollInterval: "5s"
|
||||
tempDir: "/var/lib/backupx-agent/tmp"
|
||||
proxyUrl: ""
|
||||
caCertFile: ""
|
||||
```
|
||||
|
||||
```ini title="/etc/systemd/system/backupx-agent.service"
|
||||
[Unit]
|
||||
Description=BackupX Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=10
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/backupx-agent/backupx agent --config /etc/backupx-agent/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=30s
|
||||
UMask=0077
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
The Agent runs as root because file backup and restore paths may belong to arbitrary system users. Restrict who can create tasks and who can modify the root-owned Agent configuration.
|
||||
|
||||
## SSH bastion example
|
||||
|
||||
Prefer a SOCKS tunnel when the internal Master uses HTTPS: its hostname and certificate validation remain unchanged.
|
||||
|
||||
Create a dedicated SSH account and pre-provision its private key plus a verified `known_hosts` file. Then create:
|
||||
|
||||
```sshconfig title="/etc/backupx-agent/ssh_config"
|
||||
Host backupx-bastion
|
||||
HostName bastion.example.com
|
||||
User backupx-tunnel
|
||||
IdentityFile /etc/backupx-agent/tunnel_ed25519
|
||||
IdentitiesOnly yes
|
||||
BatchMode yes
|
||||
UserKnownHostsFile /etc/backupx-agent/known_hosts
|
||||
StrictHostKeyChecking yes
|
||||
DynamicForward 127.0.0.1:1080
|
||||
ExitOnForwardFailure yes
|
||||
ServerAliveInterval 30
|
||||
ServerAliveCountMax 3
|
||||
```
|
||||
|
||||
```ini title="/etc/systemd/system/backupx-agent-tunnel.service"
|
||||
[Unit]
|
||||
Description=BackupX Agent SSH tunnel
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=backupx-agent.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/ssh -NT -F /etc/backupx-agent/ssh_config backupx-bastion
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Add a drop-in so the Agent fails closed when the tunnel is unavailable:
|
||||
|
||||
```ini title="/etc/systemd/system/backupx-agent.service.d/tunnel.conf"
|
||||
[Unit]
|
||||
Requires=backupx-agent-tunnel.service
|
||||
After=backupx-agent-tunnel.service
|
||||
```
|
||||
|
||||
Reload and start both units:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now backupx-agent-tunnel backupx-agent
|
||||
```
|
||||
|
||||
In the wizard, keep the internal HTTPS Master URL and set the proxy to `socks5h://127.0.0.1:1080`. Verify the bastion host key out-of-band before enabling the service.
|
||||
|
||||
## Central storage data paths
|
||||
|
||||
| Destination | Data path |
|
||||
| --- | --- |
|
||||
| S3, WebDAV, FTP, cloud drive, or another network backend | Agent streams directly to the destination |
|
||||
| `local_disk` with **Relay remote backups through Master** enabled | Agent streams through the authenticated Master API; Master writes to its local mount |
|
||||
|
||||
The relay does not create a second complete temporary copy on the Master. Restore uses the reverse streaming path. Nginx request buffering must be disabled for this behavior to remain streaming.
|
||||
|
||||
## Operations
|
||||
|
||||
```bash
|
||||
sudo systemctl status backupx-agent
|
||||
sudo journalctl -u backupx-agent -n 100 --no-pager
|
||||
sudo /opt/backupx-agent/backupx agent --config /etc/backupx-agent/config.yaml
|
||||
```
|
||||
|
||||
Rotate a node Token from its action menu. Update `/etc/backupx-agent/agent.token` on the node and restart the service during the 24-hour overlap window.
|
||||
|
||||
Monitor these Prometheus metrics:
|
||||
|
||||
- `backupx_agent_command_queue_depth`
|
||||
- `backupx_agent_command_running`
|
||||
- `backupx_agent_command_timeout_total`
|
||||
- `backupx_node_online`
|
||||
|
||||
## CLI reference
|
||||
|
||||
```text
|
||||
backupx agent --help
|
||||
-master string Master URL
|
||||
-token string Agent authentication token
|
||||
-token-file string Read the Agent Token from a file
|
||||
-config string YAML configuration path
|
||||
-temp-dir string Local temporary directory
|
||||
-proxy-url string HTTP(S) or SOCKS5(H) proxy
|
||||
-ca-cert string PEM CA certificate used to verify the Master
|
||||
-insecure-tls Skip TLS verification (testing only)
|
||||
```
|
||||
|
||||
Environment variables: `BACKUPX_AGENT_MASTER`, `BACKUPX_AGENT_TOKEN`, `BACKUPX_AGENT_TOKEN_FILE`, `BACKUPX_AGENT_HEARTBEAT`, `BACKUPX_AGENT_POLL`, `BACKUPX_AGENT_TEMP_DIR`, `BACKUPX_AGENT_PROXY_URL`, `BACKUPX_AGENT_CA_CERT_FILE`, and `BACKUPX_AGENT_INSECURE_TLS`.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The Master is single-active because it uses embedded SQLite.
|
||||
- Encrypted backups are Master-only because Agents do not hold the Master encryption key.
|
||||
- Remote directory browsing is a synchronous queue RPC with a 15-second timeout.
|
||||
- Claimed commands that stop reporting progress are timed out according to the Master command monitor.
|
||||
49
docs-site/docs/features/notifications.md
Normal file
49
docs-site/docs/features/notifications.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
title: Notifications
|
||||
description: Email, webhook, and Telegram notifications on backup success or failure.
|
||||
---
|
||||
|
||||
# Notifications
|
||||
|
||||
BackupX supports three notification channels. Configure per-channel rules for success-only, failure-only, or both.
|
||||
|
||||
## Email (SMTP)
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| SMTP host / port | e.g. `smtp.gmail.com:587` |
|
||||
| Username / password | App-specific password recommended |
|
||||
| From address | Used in `From:` header |
|
||||
| Recipients | Comma-separated list |
|
||||
| Use TLS / StartTLS | Match your SMTP provider |
|
||||
|
||||
## Webhook
|
||||
|
||||
Send a JSON POST to an arbitrary URL. Body shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "backup_result",
|
||||
"task": {"id": 1, "name": "web-files", "type": "file"},
|
||||
"record": {"id": 42, "status": "success", "fileSize": 1048576, "durationSeconds": 12},
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
Useful for custom workflows: Slack incoming webhook, PagerDuty, your own API, etc.
|
||||
|
||||
## Telegram
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| Bot token | From [@BotFather](https://t.me/BotFather) |
|
||||
| Chat ID | Numeric — obtain via `/start` + bot's `getUpdates` |
|
||||
|
||||
## Event rules
|
||||
|
||||
Each notification configuration can be scoped to:
|
||||
|
||||
- **Success only** — quiet during normal runs, pings on first failure
|
||||
- **Failure only** — recommended for loud channels
|
||||
- **Both** — useful during initial setup to verify notifications flow
|
||||
79
docs-site/docs/features/sap-hana.md
Normal file
79
docs-site/docs/features/sap-hana.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: SAP HANA Support
|
||||
description: Two SAP HANA backup modes — managed hdbsql runner and native Backint protocol agent.
|
||||
---
|
||||
|
||||
# SAP HANA Support
|
||||
|
||||
BackupX provides two SAP HANA backup modes. Pick whichever fits your operations workflow.
|
||||
|
||||
## Mode 1: hdbsql Runner (console-managed)
|
||||
|
||||
Create a SAP HANA backup task in the Web console. The backend invokes `hdbsql` to execute the backup. Use this when BackupX should own the schedule.
|
||||
|
||||
**Source configuration supports:**
|
||||
|
||||
| Field | Options | Description |
|
||||
|-------|---------|-------------|
|
||||
| Backup type | `data` / `log` | Data or log backup |
|
||||
| Backup level | `full` / `incremental` / `differential` | Auto-disabled for log backups |
|
||||
| Parallel channels | `1 ~ 32` | Multi-path SQL (`BACKUP DATA USING FILE ('c1', 'c2', ...)`) |
|
||||
| Retry count | `1 ~ 10` | Exponential backoff (`5s × attempt²`) |
|
||||
| Instance number | Optional | Inferred from port or specified manually |
|
||||
|
||||
## Mode 2: Backint Protocol Agent (HANA native)
|
||||
|
||||
BackupX ships a built-in Backint Agent. SAP HANA calls it via the native `BACKUP DATA USING BACKINT` syntax, and data is routed automatically to any BackupX storage target (S3 / OSS / COS / WebDAV / 70+ backends).
|
||||
|
||||
### 1. Parameter file
|
||||
|
||||
```ini title="/opt/backupx/backint_params.ini"
|
||||
#STORAGE_TYPE = s3
|
||||
#STORAGE_CONFIG_JSON = /opt/backupx/storage.json
|
||||
#PARALLEL_FACTOR = 4
|
||||
#COMPRESS = true
|
||||
#KEY_PREFIX = hana-backup
|
||||
#CATALOG_DB = /opt/backupx/backint_catalog.db
|
||||
#LOG_FILE = /var/log/backupx/backint.log
|
||||
```
|
||||
|
||||
### 2. Storage config (same schema as storage targets)
|
||||
|
||||
```json title="/opt/backupx/storage.json"
|
||||
{
|
||||
"endpoint": "https://s3.amazonaws.com",
|
||||
"region": "us-east-1",
|
||||
"bucket": "hana-prod",
|
||||
"accessKeyId": "AKIA...",
|
||||
"secretAccessKey": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create the hdbbackint symlink
|
||||
|
||||
```bash
|
||||
ln -s /opt/backupx/backupx /usr/sap/<SID>/SYS/global/hdb/opt/hdbbackint
|
||||
```
|
||||
|
||||
### 4. Enable Backint in HANA `global.ini`
|
||||
|
||||
```ini
|
||||
[backup]
|
||||
data_backup_using_backint = true
|
||||
catalog_backup_using_backint = true
|
||||
log_backup_using_backint = true
|
||||
data_backup_parameter_file = /opt/backupx/backint_params.ini
|
||||
log_backup_parameter_file = /opt/backupx/backint_params.ini
|
||||
```
|
||||
|
||||
### 5. Manual CLI invocation (troubleshooting)
|
||||
|
||||
```bash
|
||||
backupx backint -f backup -i input.txt -o output.txt -p backint_params.ini
|
||||
backupx backint -f restore -i input.txt -o output.txt -p backint_params.ini
|
||||
backupx backint -f inquire -i input.txt -o output.txt -p backint_params.ini
|
||||
backupx backint -f delete -i input.txt -o output.txt -p backint_params.ini
|
||||
```
|
||||
|
||||
The Backint Agent maintains an `EBID ↔ object-key` catalog in a local SQLite DB. All operations follow the SAP HANA Backint protocol (`#PIPE` / `#SAVED` / `#RESTORED` / `#BACKUP` / `#NOTFOUND` / `#DELETED` / `#ERROR`).
|
||||
40
docs-site/docs/features/storage-backends.md
Normal file
40
docs-site/docs/features/storage-backends.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Storage Backends
|
||||
description: 70+ storage backends — built-in cloud providers plus any rclone backend.
|
||||
---
|
||||
|
||||
# Storage Backends
|
||||
|
||||
BackupX aims to accept any place you'd want to drop a backup file.
|
||||
|
||||
## Built-in providers
|
||||
|
||||
| Type | Required fields |
|
||||
|------|-----------------|
|
||||
| **Alibaba OSS** | Region + AccessKey ID/Secret + Bucket (endpoint auto-assembled) |
|
||||
| **Tencent COS** | Region + SecretId/SecretKey + Bucket (format `name-appid`) |
|
||||
| **Qiniu Kodo** | Region + AccessKey/SecretKey + Bucket |
|
||||
| **S3-compatible** | Endpoint + AccessKey + Bucket |
|
||||
| **Google Drive** | Client ID/Secret + OAuth authorization |
|
||||
| **WebDAV** | URL + username/password |
|
||||
| **FTP / FTPS** | Host + port + username/password |
|
||||
| **Local disk** | Target directory (absolute path) + optional Master relay for remote Agents |
|
||||
|
||||
New local-disk targets enable **Relay remote backups through Master** by default. This makes the configured path belong to the Master, so a storage server mounted there can collect backups from many source Agents. Turn the switch off when the path intentionally belongs to each Agent. Existing targets retain their previous Agent-local behavior until explicitly changed.
|
||||
|
||||
## Rclone backends
|
||||
|
||||
Every [rclone backend](https://rclone.org/overview/) is exposed as a first-class storage type — SFTP, Azure Blob, Dropbox, OneDrive, Backblaze B2, Wasabi, pCloud, HDFS, and many more.
|
||||
|
||||
- The form groups fields into **required** and **advanced** (advanced collapsed by default)
|
||||
- Validation and connection tests reuse rclone's built-in probe
|
||||
|
||||
## Multiple targets per task
|
||||
|
||||
A backup task can fan out to multiple targets in parallel. All targets receive the same artifact; a per-target status is recorded:
|
||||
|
||||
- Success: storage path + size
|
||||
- Failed: error message
|
||||
|
||||
If any target fails after retries, the record status is `failed` but successful targets are preserved (no rollback).
|
||||
74
docs-site/docs/getting-started/installation.md
Normal file
74
docs-site/docs/getting-started/installation.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Installation
|
||||
description: Install BackupX via Docker, prebuilt archive, or from source.
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
BackupX ships as a single static binary. Three ways to install, pick the one that matches your environment.
|
||||
|
||||
## Docker (recommended)
|
||||
|
||||
Download the canonical hardened Compose file and start the service:
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/Awuqing/BackupX/main/docker-compose.yml
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
The Compose definition enables init and graceful shutdown, persists `/app/data`, runs the application as an unprivileged user, drops unnecessary capabilities, and checks `/ready`. Images at [`awuqing/backupx`](https://hub.docker.com/r/awuqing/backupx) support `linux/amd64` and `linux/arm64`.
|
||||
|
||||
For production, create a protected `.env` and pin a release instead of relying on `latest`:
|
||||
|
||||
```dotenv
|
||||
BACKUPX_IMAGE=awuqing/backupx:vX.Y.Z
|
||||
BACKUPX_BIND_ADDRESS=127.0.0.1
|
||||
TZ=Asia/Shanghai
|
||||
```
|
||||
|
||||
Use the loopback binding when a reverse proxy runs on the same host. For direct access, choose the intended interface and enforce a firewall. Mount host backup sources read-only or deploy an Agent on the source host. See [Docker Deployment](../deployment/docker) for the full configuration.
|
||||
|
||||
## Prebuilt archive (bare metal)
|
||||
|
||||
Download from the [Releases page](https://github.com/Awuqing/BackupX/releases) and run the installer:
|
||||
|
||||
```bash
|
||||
sha256sum -c backupx-v*-linux-amd64.tar.gz.sha256
|
||||
tar xzf backupx-v*-linux-amd64.tar.gz && cd backupx-*
|
||||
sudo ./install.sh # creates system user, installs to /opt/backupx, sets up systemd
|
||||
```
|
||||
|
||||
The installer:
|
||||
|
||||
1. Creates a `backupx` system user
|
||||
2. Installs the binary to `/opt/backupx/bin/backupx` and the web console to `/opt/backupx/web`
|
||||
3. Creates `/etc/backupx/config.yaml` with safe defaults
|
||||
4. Installs and enables the `backupx.service` systemd unit
|
||||
5. Leaves Nginx unchanged unless `INSTALL_NGINX=1` is explicitly requested
|
||||
6. Waits for `/api/auth/setup/status`; if startup fails, prints systemd diagnostics and exits non-zero
|
||||
|
||||
## From source
|
||||
|
||||
Requires Go ≥ 1.25 and Node.js ≥ 20.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
After `make build`, the binary is at `server/bin/backupx` and the built web UI is at `web/dist/`.
|
||||
The installer consumes those exact paths, so no Docker runtime is required. If an existing configuration uses a non-default port, set `HEALTH_URL` for the readiness check, for example `sudo HEALTH_URL=http://127.0.0.1:9000/api/auth/setup/status ./deploy/install.sh`.
|
||||
|
||||
The Nginx template is opt-in because automatically installing a catch-all virtual host can intercept existing sites. Review `deploy/nginx.conf`, then use `sudo INSTALL_NGINX=1 ./deploy/install.sh` only when it matches the host.
|
||||
|
||||
## Verify the install
|
||||
|
||||
```bash
|
||||
/opt/backupx/bin/backupx --version
|
||||
curl -fsS http://127.0.0.1:8340/api/auth/setup/status
|
||||
```
|
||||
|
||||
Then open `http://your-server:8340`. Choose **English** or **中文** in the upper-right corner. A fresh database shows **System setup**, where you create the first administrator username and password. If that form does not appear, retry the status request above before attempting to sign in.
|
||||
62
docs-site/docs/getting-started/quick-start.md
Normal file
62
docs-site/docs/getting-started/quick-start.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Quick Start
|
||||
description: Set up BackupX, add a storage target, create your first backup task.
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
|
||||
After [installation](./installation), get a first backup running in five minutes.
|
||||
|
||||
## 1. Open the console
|
||||
|
||||
Browse to `http://your-server:8340`. The first time, you'll be guided through creating an admin account.
|
||||
|
||||
## 2. Add a storage target
|
||||
|
||||
Navigate to **Storage Targets → Add**. Pick a type and fill the required fields:
|
||||
|
||||
| Type | Fields |
|
||||
|------|--------|
|
||||
| Alibaba OSS | Region + AccessKey ID/Secret + Bucket |
|
||||
| Tencent COS | Region + SecretId/SecretKey + Bucket (format `name-appid`) |
|
||||
| Qiniu Kodo | Region + AccessKey/SecretKey + Bucket |
|
||||
| S3-compatible | Endpoint + AccessKey + Bucket |
|
||||
| Google Drive | Client ID/Secret → click "Authorize" for OAuth flow |
|
||||
| WebDAV | URL + username/password |
|
||||
| FTP | Host + port + username/password |
|
||||
| Local disk | Target directory |
|
||||
| SFTP / Azure / Dropbox / OneDrive | Type-specific required fields; advanced options collapsed |
|
||||
|
||||
:::tip
|
||||
For mainland China cloud vendors you only fill Region and AccessKey — BackupX assembles the endpoint automatically. Rclone-style providers separate required fields from advanced ones, with advanced collapsed by default.
|
||||
:::
|
||||
|
||||
Click **Test Connection** to verify.
|
||||
|
||||
## 3. Create a backup task
|
||||
|
||||
Go to **Backup Tasks → New**. Three steps:
|
||||
|
||||
1. **Basic info** — name, type, cron expression (leave empty for manual-only)
|
||||
2. **Source** — paths for file backup (multi-source supported), or connection info for databases
|
||||
3. **Storage & policy** — pick target(s), compression, retention days, encryption on/off
|
||||
|
||||
For Agent-routed tasks, encryption must stay off because the Agent never receives the Master's encryption key. BackupX rejects remote-node or node-pool tasks with encryption enabled during create/update.
|
||||
|
||||
Save, then click **Run Now** to trigger a test. Live logs stream on the **Backup Records** page.
|
||||
|
||||
:::note
|
||||
Deleting a task also removes remote backup files to prevent orphans, but records are kept for audit.
|
||||
:::
|
||||
|
||||
## 4. Configure notifications (optional)
|
||||
|
||||
**Notifications** page supports email, webhook, and Telegram. Configure per-channel rules for success/failure events.
|
||||
|
||||
## Next up
|
||||
|
||||
- Explore [backup types](/docs/features/backup-types) and [storage backends](/docs/features/storage-backends)
|
||||
- Before production, review [Security Hardening](/docs/operations/security), [Monitoring and Alerts](/docs/operations/monitoring), and [Upgrade and Recovery](/docs/operations/upgrade-recovery)
|
||||
- Running SAP HANA? See [SAP HANA Support](/docs/features/sap-hana)
|
||||
- Managing many servers? See [Multi-Node Cluster](/docs/features/multi-node)
|
||||
42
docs-site/docs/intro.md
Normal file
42
docs-site/docs/intro.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
id: intro
|
||||
slug: /intro
|
||||
sidebar_position: 1
|
||||
title: Introduction
|
||||
description: Overview of BackupX — a self-hosted server backup management platform.
|
||||
---
|
||||
|
||||
# BackupX
|
||||
|
||||
**BackupX** is a self-hosted server backup management platform. One static binary, one command, and every backup job for every server is under control.
|
||||
|
||||
- **Single binary + embedded SQLite** — no external database or orchestrator required
|
||||
- **Files, databases, SAP HANA** — in one place, with a visual scheduler
|
||||
- **70+ storage backends** — Alibaba OSS, Tencent COS, Qiniu, S3, Google Drive, WebDAV, FTP, plus SFTP / Azure Blob / Dropbox / OneDrive and dozens more via rclone
|
||||
- **Multi-node cluster** — Master-Agent mode manages backups across servers, agents run tasks locally and upload straight to storage
|
||||
- **Secure by default** — JWT auth, bcrypt, AES-256-GCM encrypted config, optional backup encryption, full audit log
|
||||
|
||||
## Architecture at a Glance
|
||||
|
||||
```
|
||||
[Web Console] ─── JWT ──→ [Master (backupx)]
|
||||
│
|
||||
│ outbound HTTP polling (token auth)
|
||||
▼
|
||||
[Agent (backupx agent)]
|
||||
│
|
||||
▼
|
||||
[70+ Storage Backends]
|
||||
```
|
||||
|
||||
Tasks routed to the local Master run in-process; tasks assigned to remote nodes are dispatched through a command queue and executed by the Agent locally. Agents only ever initiate outbound HTTP — no reverse connectivity required.
|
||||
|
||||
## Where to Next
|
||||
|
||||
- **New to BackupX?** Read the [Quick Start](/docs/getting-started/quick-start) first.
|
||||
- **Deploying to production?** See the [Deployment Guide](/docs/deployment/docker).
|
||||
- **Planning upgrades or recovery?** Follow [Upgrade and Recovery](/docs/operations/upgrade-recovery).
|
||||
- **Operating production?** Start with [Security Hardening](/docs/operations/security) and [Monitoring and Alerts](/docs/operations/monitoring).
|
||||
- **SAP HANA operator?** Both `hdbsql` Runner and native Backint are supported — see [SAP HANA](/docs/features/sap-hana).
|
||||
- **Managing multiple servers?** See [Multi-Node Cluster](/docs/features/multi-node).
|
||||
- **Integrating programmatically?** See the [API Reference](/docs/reference/api).
|
||||
149
docs-site/docs/operations/monitoring.md
Normal file
149
docs-site/docs/operations/monitoring.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Monitoring and Alerts
|
||||
description: Health probes, Prometheus metrics, initial alert rules, and operational validation.
|
||||
---
|
||||
|
||||
# Monitoring and Alerts
|
||||
|
||||
BackupX exposes low-cost health endpoints and a dedicated Prometheus registry. Monitor both the control plane and the outcome of backup, restore, verification, and replication work.
|
||||
|
||||
## Probes
|
||||
|
||||
| Endpoint | Meaning | Expected response |
|
||||
| --- | --- | --- |
|
||||
| `/health` | Liveness: the HTTP process can respond | HTTP 200 with `status: live` |
|
||||
| `/ready` | Readiness: the process can reach SQLite | HTTP 200 with `status: ready`; HTTP 503 on database failure |
|
||||
| `/api/health` | API-prefixed alias for liveness | Same as `/health` |
|
||||
| `/api/ready` | API-prefixed alias for readiness | Same as `/ready` |
|
||||
| `/metrics` | Prometheus exposition | HTTP 200 when metrics are enabled |
|
||||
|
||||
Use `/health` for a liveness probe and `/ready` for readiness or load-balancer traffic decisions. Do not restart a process only because an external storage provider is unavailable; storage health belongs in task and target alerts.
|
||||
|
||||
~~~bash
|
||||
curl -fsS http://127.0.0.1:8340/health
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
curl -fsS http://127.0.0.1:8340/metrics | head
|
||||
~~~
|
||||
|
||||
These endpoints are unauthenticated. Restrict them to orchestrator and monitoring networks.
|
||||
|
||||
## Prometheus scrape
|
||||
|
||||
~~~yaml
|
||||
scrape_configs:
|
||||
- job_name: backupx
|
||||
scheme: https
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets: [backup.example.com]
|
||||
~~~
|
||||
|
||||
When Nginx terminates TLS, allow the Prometheus source address to reach `/metrics` and deny other public clients. The internal collector refreshes storage, node, command-queue, and SLA gauges every 30 seconds.
|
||||
|
||||
## BackupX metrics
|
||||
|
||||
| Metric | Type | Labels | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `backupx_app_info` | gauge | `version` | Running release metadata |
|
||||
| `backupx_task_run_total` | counter | `status`, `task_type` | Backup outcomes |
|
||||
| `backupx_task_run_duration_seconds` | histogram | `task_type` | Backup duration distribution |
|
||||
| `backupx_task_bytes_total` | counter | `task_type` | Produced backup bytes |
|
||||
| `backupx_task_running` | gauge | none | Current backup concurrency |
|
||||
| `backupx_storage_used_bytes` | gauge | `target_name`, `target_type` | Recorded usage per target |
|
||||
| `backupx_node_online` | gauge | `node_name`, `role` | Node online state, 1 or 0 |
|
||||
| `backupx_agent_command_queue_depth` | gauge | `node_name`, `role` | Pending and dispatched commands |
|
||||
| `backupx_agent_command_running` | gauge | `node_name`, `role` | Long-running Agent commands |
|
||||
| `backupx_agent_command_timeout_total` | gauge | `node_name`, `role` | Snapshot of timed-out commands |
|
||||
| `backupx_verify_run_total` | counter | `status` | Verification outcomes |
|
||||
| `backupx_restore_run_total` | counter | `status` | Restore outcomes |
|
||||
| `backupx_replication_run_total` | counter | `status` | Replication outcomes |
|
||||
| `backupx_sla_breach_tasks` | gauge | none | Enabled tasks outside their configured RPO |
|
||||
|
||||
Standard Go runtime and process collectors are registered in the same endpoint.
|
||||
|
||||
## Initial alert rules
|
||||
|
||||
Tune windows and thresholds to the schedules and RPOs of each environment:
|
||||
|
||||
~~~yaml
|
||||
groups:
|
||||
- name: backupx
|
||||
rules:
|
||||
- alert: BackupXTargetDown
|
||||
expr: up{job="backupx"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: BackupX metrics endpoint is unreachable
|
||||
|
||||
- alert: BackupXNotReady
|
||||
expr: probe_success{job="backupx-ready"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: BackupX readiness check is failing
|
||||
|
||||
- alert: BackupXBackupFailure
|
||||
expr: sum(increase(backupx_task_run_total{status="failed"}[15m])) > 0
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: A BackupX backup failed
|
||||
|
||||
- alert: BackupXSLABreach
|
||||
expr: backupx_sla_breach_tasks > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: One or more backup tasks are outside RPO
|
||||
|
||||
- alert: BackupXAgentOffline
|
||||
expr: backupx_node_online{role="agent"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: BackupX Agent is offline
|
||||
|
||||
- alert: BackupXAgentQueueBacklog
|
||||
expr: backupx_agent_command_queue_depth > 20
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: BackupX Agent command queue is growing
|
||||
~~~
|
||||
|
||||
The `BackupXNotReady` example assumes a blackbox probe job named `backupx-ready`. If no blackbox exporter is used, alert from the load balancer or orchestrator readiness signal instead.
|
||||
|
||||
## Operational dashboard
|
||||
|
||||
Track these views together:
|
||||
|
||||
- Success and failure rate by task type.
|
||||
- P50, P95, and maximum run duration relative to the backup window.
|
||||
- Bytes produced compared with the expected data-change rate.
|
||||
- Current running tasks versus `backup.max_concurrent`.
|
||||
- Offline Agents, queue depth, running commands, and timeout-count changes.
|
||||
- Storage growth, free capacity from the storage provider, and retention cleanup.
|
||||
- SLA breach count and age of the most recent successful backup for critical tasks.
|
||||
- Verification, restore, and replication success rates.
|
||||
|
||||
Prometheus storage usage is based on BackupX record metadata, not necessarily the provider's billable capacity. Monitor provider quota and filesystem free space separately.
|
||||
|
||||
## Post-deployment validation
|
||||
|
||||
After installation, upgrade, proxy changes, or recovery:
|
||||
|
||||
1. Check liveness and readiness locally and through the public proxy.
|
||||
2. Confirm Prometheus sees one active Master and the expected version label.
|
||||
3. Verify every expected Agent reports `backupx_node_online == 1`.
|
||||
4. Run a small backup and confirm the success counter increases.
|
||||
5. Run a verification or isolated restore and confirm its counter increases.
|
||||
6. Trigger a test notification and verify the alert delivery path.
|
||||
|
||||
Continue with [Troubleshooting](./troubleshooting) when a probe or metric is abnormal.
|
||||
102
docs-site/docs/operations/security.md
Normal file
102
docs-site/docs/operations/security.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Security Hardening
|
||||
description: Production controls for network exposure, roles, secrets, Agents, containers, and public endpoints.
|
||||
---
|
||||
|
||||
# Security Hardening
|
||||
|
||||
BackupX coordinates access to source files, database credentials, storage credentials, and restore destinations. Deploy the Master as a security-sensitive control plane, not as a general public web application.
|
||||
|
||||
## Recommended exposure model
|
||||
|
||||
| Component | Inbound access | Outbound access |
|
||||
| --- | --- | --- |
|
||||
| Master | HTTPS from administrators and Agents; metrics only from monitoring networks | Storage providers, notification endpoints, release checks |
|
||||
| Agent | No inbound port required | Master HTTPS endpoint and assigned storage targets |
|
||||
| SQLite data | Local or block-backed filesystem only | None |
|
||||
|
||||
Bind Docker to `127.0.0.1` when a reverse proxy runs on the same host:
|
||||
|
||||
~~~dotenv
|
||||
BACKUPX_BIND_ADDRESS=127.0.0.1
|
||||
~~~
|
||||
|
||||
For bare metal, set `server.host` to loopback when only a local proxy should reach BackupX. Otherwise restrict TCP 8340 with the host or network firewall.
|
||||
|
||||
## TLS and reverse proxies
|
||||
|
||||
- Use HTTPS across every untrusted network segment.
|
||||
- Set `server.external_url` to the stable URL that Agents can reach.
|
||||
- Add only the exact proxy IP or subnet to `server.trusted_proxies`. Never trust `0.0.0.0/0`.
|
||||
- Send the final HTTPS URL to Agents; the Agent does not follow redirects.
|
||||
- For private PKI, install a PEM CA on the Agent and configure `caCertFile` or `--ca-cert`.
|
||||
- Use `--insecure-tls` only for temporary testing.
|
||||
- Keep Nginx request and response buffering disabled for relay uploads and SSE logs.
|
||||
|
||||
When an SSH bastion is required, bind tunnels to loopback, verify host keys, use a dedicated account and key, and make the Agent service depend on the tunnel. See [Multi-Node Cluster](../features/multi-node).
|
||||
|
||||
## Roles and API keys
|
||||
|
||||
| Role | Intended access |
|
||||
| --- | --- |
|
||||
| `viewer` | Read dashboards, tasks, records, reports, and audit data; cannot browse node filesystems or mutate resources |
|
||||
| `operator` | Viewer access plus task, storage, notification, backup, restore, verification, and file-browse operations |
|
||||
| `admin` | Operator access plus users, API keys, settings, node lifecycle, install tokens, and token rotation |
|
||||
|
||||
Create separate named users instead of sharing the initial administrator. Enable two-factor authentication or passkeys for privileged accounts. Review trusted devices and recovery codes periodically.
|
||||
|
||||
User JWTs are stateless. Logout removes the client copy but does not revoke a token that was already copied elsewhere. Set `security.jwt_expire` to the shortest practical lifetime, protect Bearer tokens, and rotate the JWT secret when all active sessions must be invalidated.
|
||||
|
||||
API keys use the same role checks as interactive users. Their plaintext is shown only once; the database stores a keyed hash. Give automation the lowest role it needs, set an expiry, keep the key in a secret manager, and revoke unused keys. Avoid administrator API keys for monitoring.
|
||||
|
||||
## Protect control-plane secrets
|
||||
|
||||
- Restrict `/etc/backupx/config.yaml` to `root:backupx` mode `0640` and the data directory to the service account.
|
||||
- If `jwt_secret` and `encryption_key` are empty, generated values are persisted in the SQLite database. Back up the complete data directory.
|
||||
- Losing or replacing the encryption key makes saved storage credentials unreadable.
|
||||
- The database includes password hashes, configuration secrets, Agent tokens, API-key hashes, trusted-device state, and audit data. Encrypt snapshots and control their retention.
|
||||
- Do not put tokens in shell history, issue text, screenshots, or support bundles.
|
||||
|
||||
Each node has an independent long-lived Agent token. The systemd installer stores it in `/etc/backupx-agent/agent.token` with mode `0600`. Rotate a token after personnel changes, host compromise, or accidental disclosure, update the token file during the overlap window, then restart the Agent.
|
||||
|
||||
One-time install URLs are valid for 5 minutes to 24 hours and are consumed after use. Treat the URL and the embedded fallback command as secrets: the generated installation material provisions the long-lived node token.
|
||||
|
||||
## Container and host permissions
|
||||
|
||||
The canonical Compose deployment drops all capabilities and adds back only those needed to repair legacy volume ownership and switch to the unprivileged `backupx` user. Keep `no-new-privileges` enabled and do not mount the Docker socket.
|
||||
|
||||
Mount backup sources read-only. Add a separate, narrowly scoped writable mount only when a restore destination requires it. Prefer a host Agent over running the Master container as root for privileged filesystem access.
|
||||
|
||||
The systemd Master runs as `backupx`. The Agent normally runs as root because it may back up or restore files belonging to arbitrary system users. Limit who can create tasks and protect the root-owned Agent configuration.
|
||||
|
||||
## Public endpoints
|
||||
|
||||
The following endpoints intentionally do not use BackupX JWT or API-key authentication:
|
||||
|
||||
- `/health` and `/api/health`
|
||||
- `/ready` and `/api/ready`
|
||||
- `/metrics`
|
||||
- one-time `/install/:token` and `/api/install/:token` routes
|
||||
|
||||
Health responses expose status, version, uptime, timestamp, and readiness checks; a failed readiness check can include database error detail. `/metrics` also includes node and storage-target labels. Restrict metrics and probes to monitoring networks at the firewall or reverse proxy. Do not cache or log full install-token URLs.
|
||||
|
||||
## Backup encryption boundary
|
||||
|
||||
Encrypted backup tasks run on the Master because remote Agents never receive the Master's encryption key. Do not work around this boundary by copying the Master key to Agents. For Agent-routed tasks, rely on transport encryption and the destination provider's server-side encryption when required.
|
||||
|
||||
Test restores for encrypted backups after every key-management change. A backup whose key is unavailable is not recoverable.
|
||||
|
||||
## Audit and incident response
|
||||
|
||||
BackupX records privileged actions in the audit log and can forward signed audit events to an external webhook. Send high-value audit records to a separately administered SIEM or append-only store so a compromised Master cannot erase the only copy.
|
||||
|
||||
After suspected compromise:
|
||||
|
||||
1. Isolate the Master without deleting evidence.
|
||||
2. Revoke exposed API keys and rotate affected Agent tokens and storage credentials.
|
||||
3. Replace JWT and encryption keys only with a planned migration; changing the encryption key invalidates saved encrypted configuration.
|
||||
4. Review user, trusted-device, API-key, node, settings, restore, and deletion events.
|
||||
5. Recover from a known-good control-plane snapshot when integrity cannot be established.
|
||||
|
||||
Use [Upgrade and Recovery](./upgrade-recovery) for the paired application-and-database recovery procedure.
|
||||
160
docs-site/docs/operations/troubleshooting.md
Normal file
160
docs-site/docs/operations/troubleshooting.md
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: Troubleshooting
|
||||
description: A safe diagnostic sequence for the Master, reverse proxy, Agents, backup tools, and SQLite.
|
||||
---
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
Start with the first failing boundary and preserve evidence. Avoid deleting the database, recreating volumes, rotating every token, or reinstalling until the failure is understood.
|
||||
|
||||
## Fast triage
|
||||
|
||||
| Symptom | First check | Likely boundary |
|
||||
| --- | --- | --- |
|
||||
| Web console unavailable | Local `/health`, then proxy `/health` | Process, listener, firewall, proxy, or static assets |
|
||||
| `/health` works but `/ready` is 503 | Service logs, database path, disk space, ownership | SQLite or data filesystem |
|
||||
| Login loops or client IP is wrong | Forwarded headers and `trusted_proxies` | Reverse-proxy trust |
|
||||
| Live logs stop updating | Nginx response buffering and timeout | SSE proxy path |
|
||||
| Relay upload stalls or proxy disk fills | Request buffering and body-size limit | Reverse proxy |
|
||||
| Agent offline | Agent service logs, final Master URL, proxy, DNS, CA | Agent-to-Master path |
|
||||
| Backup starts but fails | Record log, source path, native database tool | Task runner or permissions |
|
||||
| Restore fails | Record log, destination mount and write access | Storage read or destination permissions |
|
||||
|
||||
## Collect status without secrets
|
||||
|
||||
Docker Master:
|
||||
|
||||
~~~bash
|
||||
docker compose ps
|
||||
docker compose logs --tail=200 backupx
|
||||
curl -i http://127.0.0.1:8340/health
|
||||
curl -i http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
Bare-metal Master:
|
||||
|
||||
~~~bash
|
||||
sudo systemctl status backupx --no-pager
|
||||
sudo journalctl -u backupx -n 200 --no-pager
|
||||
sudo ss -lntp | grep 8340
|
||||
curl -i http://127.0.0.1:8340/health
|
||||
curl -i http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
Systemd Agent:
|
||||
|
||||
~~~bash
|
||||
sudo systemctl status backupx-agent --no-pager
|
||||
sudo journalctl -u backupx-agent -n 200 --no-pager
|
||||
sudo systemctl status backupx-agent-tunnel --no-pager
|
||||
~~~
|
||||
|
||||
The tunnel command is relevant only to bastion deployments. Before sharing output, remove Authorization headers, API keys, Agent tokens, install URLs, database passwords, storage credentials, proxy credentials, and private paths that reveal sensitive topology.
|
||||
|
||||
## Web console or first setup
|
||||
|
||||
Check the unauthenticated setup endpoint:
|
||||
|
||||
~~~bash
|
||||
curl -fsS http://127.0.0.1:8340/api/auth/setup/status
|
||||
~~~
|
||||
|
||||
If the API works but the browser receives a blank page or JSON:
|
||||
|
||||
- Confirm the release contains web assets.
|
||||
- Bare metal: verify `/opt/backupx/web` is readable and `server.web_root` is correct when explicitly set.
|
||||
- Docker: confirm the official image is running and no custom mount hides the packaged web directory.
|
||||
- Nginx static mode: confirm `root /opt/backupx/web` and SPA fallback are present.
|
||||
- Clear an old service-worker or browser cache after a release change.
|
||||
|
||||
For authentication failures, verify system time before diagnosing TOTP or passkeys. Confirm the browser origin matches the final HTTPS host, and inspect the audit log for throttling, disabled users, or revoked trusted devices.
|
||||
|
||||
## Reverse proxy
|
||||
|
||||
Validate and reload Nginx:
|
||||
|
||||
~~~bash
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
curl -i https://backup.example.com/health
|
||||
curl -i https://backup.example.com/ready
|
||||
~~~
|
||||
|
||||
Common corrections:
|
||||
|
||||
- HTTP 413: set `client_max_body_size 0` for the API route.
|
||||
- Relay uploads fill proxy temporary storage: set `proxy_request_buffering off`.
|
||||
- SSE logs arrive in bursts or disconnect: set `proxy_buffering off`, disable proxy cache, and increase read timeout.
|
||||
- One-click installer returns HTML: proxy `/api/` and retain the legacy `/install/` route.
|
||||
- Agent receives a redirect: configure the final HTTPS Master URL instead of an HTTP URL.
|
||||
- Audit shows the proxy address for every user: add only the real proxy IP or subnet to `server.trusted_proxies`.
|
||||
|
||||
Use the complete [Nginx configuration](../deployment/nginx) as the comparison baseline.
|
||||
|
||||
## Agent offline
|
||||
|
||||
An Agent normally heartbeats every 15 seconds and is marked offline after 45 seconds.
|
||||
|
||||
1. Confirm the Agent and optional tunnel services are active.
|
||||
2. Verify the configured Master URL has no trailing redirect and resolves from the Agent host.
|
||||
3. Check the explicit `proxyUrl`. Use `socks5h://` when DNS must resolve through an SSH dynamic tunnel.
|
||||
4. Confirm the private CA path exists and is readable. Do not switch permanently to insecure TLS.
|
||||
5. Check outbound firewall access to the Master and assigned storage backends.
|
||||
6. Verify `/etc/backupx-agent/agent.token` exists with mode `0600`.
|
||||
7. If a token was rotated, install the new value during the overlap window and restart the Agent.
|
||||
|
||||
Do not paste the token into a diagnostic command that will be saved in shell history. A 401 in Agent logs usually indicates a missing, expired-overlap, or mismatched node token; repeated connection errors indicate URL, DNS, proxy, tunnel, firewall, or CA problems.
|
||||
|
||||
## Backup task failures
|
||||
|
||||
Open the backup record and inspect its complete log before changing the task.
|
||||
|
||||
- File tasks resolve paths on the selected Master or Agent. Confirm the path exists in that host's namespace.
|
||||
- Docker sees only mounted paths. Backup mounts should normally be read-only.
|
||||
- MySQL requires `mysqldump` on the execution host's `PATH`.
|
||||
- PostgreSQL requires `pg_dump` on the execution host's `PATH`.
|
||||
- SAP HANA runner mode requires its configured client tools and environment.
|
||||
- Confirm the service account can read sources and write the temporary directory.
|
||||
- Test the selected storage target from the console.
|
||||
- Check DNS, egress policy, provider quota, clock skew, and proxy settings for remote storage.
|
||||
|
||||
If multiple targets are configured, inspect the per-target result instead of assuming every copy failed. Preserve successful remote artifacts while correcting the failing target.
|
||||
|
||||
## Restore, download, or verification failures
|
||||
|
||||
- Confirm the remote artifact still exists and the storage credentials can read it.
|
||||
- Check that the destination is mounted on the host that performs the restore.
|
||||
- Use a separate writable restore path; do not make every backup-source mount writable.
|
||||
- Check free space in the destination and Agent temporary directory.
|
||||
- For encrypted backups, confirm the original Master encryption key is available.
|
||||
- For CDC repositories, keep manifests, indexes, and shared packs together; a manifest alone is not a complete backup.
|
||||
|
||||
Prefer an isolated restore destination during diagnosis. Do not repeatedly restore over the production source.
|
||||
|
||||
## SQLite and readiness failures
|
||||
|
||||
When `/health` is 200 but `/ready` is 503:
|
||||
|
||||
1. Read the exact database error from service logs.
|
||||
2. Check free disk space, inode availability, path ownership, and mount state.
|
||||
3. Confirm only one Master process or container uses the data directory.
|
||||
4. Keep SQLite on a local or block-backed filesystem, not a shared multi-writer or unreliable network filesystem.
|
||||
5. Check whether an external backup or antivirus process is holding files for long periods.
|
||||
|
||||
BackupX uses a five-second SQLite busy timeout, but that does not make SQLite a clustered database. Do not fix lock errors by starting another Master. For a file-level copy, stop the service and copy the whole data directory.
|
||||
|
||||
## Escalation package
|
||||
|
||||
When opening an issue, include:
|
||||
|
||||
- BackupX version, installation method, operating system, and architecture.
|
||||
- Whether the failure affects the Master, Agent, proxy, storage target, or one task.
|
||||
- Redacted service logs covering the first failure.
|
||||
- HTTP status and response body from `/health` and `/ready`.
|
||||
- A minimal reproduction and whether it began after an upgrade or configuration change.
|
||||
- Relevant proxy configuration with hostnames, credentials, and private addresses redacted.
|
||||
|
||||
Never attach `backupx.db`, `.env`, full configuration files, Agent token files, API keys, install commands, or storage credentials to a public issue.
|
||||
|
||||
If integrity or rollback is involved, stop making destructive changes and follow [Upgrade and Recovery](./upgrade-recovery).
|
||||
153
docs-site/docs/operations/upgrade-recovery.md
Normal file
153
docs-site/docs/operations/upgrade-recovery.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Upgrade and Recovery
|
||||
description: Back up the control plane, upgrade safely, roll back as a unit, and recover a failed Master.
|
||||
---
|
||||
|
||||
# Upgrade and Recovery
|
||||
|
||||
Backup artifacts and the BackupX control plane are different recovery domains. Object storage may still contain every archive while a lost Master database removes users, encrypted storage credentials, schedules, records, node tokens, and audit history. Protect both.
|
||||
|
||||
## Non-negotiable rules
|
||||
|
||||
1. Run exactly one active Master against a data directory or SQLite database.
|
||||
2. Snapshot the complete data directory and configuration while the Master is stopped, or use a storage-level atomic snapshot.
|
||||
3. Keep the old application version and its pre-upgrade data snapshot together. Schema migration happens at startup, so switching only the binary or image back is not a safe rollback.
|
||||
4. Store control-plane snapshots outside the Master host and test restoring them.
|
||||
5. Let active backup and restore jobs finish before stopping the Master.
|
||||
|
||||
| Deployment | Persistent control-plane data | Configuration and release state |
|
||||
| --- | --- | --- |
|
||||
| Docker | `/app/data` in the `backupx-data` volume | Compose file, protected `.env`, pinned image tag or digest |
|
||||
| Bare metal | `/opt/backupx/data` | `/etc/backupx`, `/opt/backupx/bin`, `/opt/backupx/web`, systemd unit |
|
||||
|
||||
The SQLite database contains generated JWT and encryption keys when they are not supplied in configuration. Treat every control-plane snapshot as a secret.
|
||||
|
||||
## Change checklist
|
||||
|
||||
Before an upgrade, host migration, or security-key change:
|
||||
|
||||
- Record the current BackupX version and the exact image digest or release checksum.
|
||||
- Confirm `/ready` returns HTTP 200 and review recent failures.
|
||||
- Wait for running backup, restore, verification, and replication work to finish.
|
||||
- Test at least one storage target and confirm Agents are online.
|
||||
- Create a full control-plane snapshot and copy it off-host.
|
||||
- Optionally export task definitions for human review. Task export excludes database passwords and storage credentials, so it is not a replacement for the database snapshot.
|
||||
- Define the rollback decision and maintenance-window deadline before starting.
|
||||
|
||||
## Snapshot a Docker deployment
|
||||
|
||||
This example creates a consistent file-level copy without requiring access to Docker's volume directory:
|
||||
|
||||
~~~bash
|
||||
snapshot="backupx-control-plane-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
install -d -m 0700 "$snapshot"
|
||||
|
||||
docker compose stop backupx
|
||||
docker cp backupx:/app/data "$snapshot/data"
|
||||
cp docker-compose.yml "$snapshot/"
|
||||
if [ -f .env ]; then cp .env "$snapshot/"; fi
|
||||
docker compose start backupx
|
||||
|
||||
tar -czf "$snapshot.tar.gz" "$snapshot"
|
||||
sha256sum "$snapshot.tar.gz" > "$snapshot.tar.gz.sha256"
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
If copying fails, start the stopped service before investigating. Protect the archive because `.env` and the database can contain credentials. A block-volume or storage-provider snapshot is also valid when it is atomic across the whole volume.
|
||||
|
||||
## Snapshot a bare-metal deployment
|
||||
|
||||
~~~bash
|
||||
snapshot="/var/backups/backupx/backupx-control-plane-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
|
||||
sudo install -d -m 0700 /var/backups/backupx
|
||||
|
||||
sudo systemctl stop backupx
|
||||
sudo tar --acls --xattrs -C / -czf "$snapshot" \
|
||||
etc/backupx \
|
||||
etc/systemd/system/backupx.service \
|
||||
opt/backupx/bin \
|
||||
opt/backupx/web \
|
||||
opt/backupx/data
|
||||
sudo systemctl start backupx
|
||||
|
||||
sudo sha256sum "$snapshot" | sudo tee "$snapshot.sha256"
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
Copy the archive and checksum to protected off-host storage. Do not copy only `backupx.db` while the service is running.
|
||||
|
||||
## Upgrade Docker
|
||||
|
||||
1. Put a release tag or immutable digest in `BACKUPX_IMAGE`. Do not use `latest` for a controlled production upgrade.
|
||||
2. Create and verify the pre-upgrade snapshot.
|
||||
3. Pull and recreate the service:
|
||||
|
||||
~~~bash
|
||||
docker compose pull backupx
|
||||
docker compose up -d backupx
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 backupx
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
4. Sign in, test a storage target, confirm Agent heartbeats, and run one small backup plus a restore or verification drill.
|
||||
5. Keep the old image reference and snapshot until the observation window ends.
|
||||
|
||||
Upgrade Agents after the Master, in small batches. Keep the node-specific proxy, private-CA, token-file, and bastion configuration unchanged unless that configuration is the purpose of the change.
|
||||
|
||||
## Upgrade bare metal
|
||||
|
||||
Download the target release and checksum, verify them, then extract the archive. The installer preserves an existing `/etc/backupx/config.yaml`, replaces the binary, web assets, and systemd unit, and restarts the service.
|
||||
|
||||
~~~bash
|
||||
sha256sum -c backupx-vX.Y.Z-linux-amd64.tar.gz.sha256
|
||||
tar xzf backupx-vX.Y.Z-linux-amd64.tar.gz
|
||||
cd backupx-vX.Y.Z-linux-amd64
|
||||
sudo ./install.sh
|
||||
|
||||
sudo systemctl status backupx --no-pager
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
Create the stopped-service snapshot before running the installer. Use the same post-upgrade application checks as Docker.
|
||||
|
||||
## Roll back
|
||||
|
||||
Rollback is a paired operation: restore both the previous application release and the snapshot created immediately before the upgrade.
|
||||
|
||||
For Docker, preserve the failed volume for analysis and restore the snapshot into a new empty volume. Point Compose at that volume and the previous image tag, then start exactly one Master. For bare metal, stop the service, preserve the failed state, restore the old configuration, binary, web assets, data directory, and unit from the same archive, reload systemd, and start the service.
|
||||
|
||||
After rollback:
|
||||
|
||||
~~~bash
|
||||
curl -fsS http://127.0.0.1:8340/health
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
Then verify login, storage access, schedules, Agent heartbeats, a backup, and a non-destructive restore drill. Do not delete the failed state until the incident is understood.
|
||||
|
||||
## Recover a lost Master
|
||||
|
||||
1. Provision a replacement host with the same architecture and the exact application version recorded with the snapshot.
|
||||
2. Keep the replacement isolated from production traffic and ensure the old Master cannot start.
|
||||
3. Restore configuration and the complete data directory with their original permissions.
|
||||
4. Start one Master and check `/ready` locally.
|
||||
5. Move the stable DNS name or virtual IP only after local validation.
|
||||
6. Confirm users, storage targets, tasks, records, notifications, and audit history.
|
||||
7. Existing Agents reconnect automatically when the restored database contains their matching tokens. Investigate and rotate tokens that may have been exposed.
|
||||
8. Run a small backup and a restore or verification drill before ending the incident.
|
||||
|
||||
External backup artifacts are not recreated by restoring the control plane; they remain on their configured storage targets. Conversely, task JSON export is useful for rebuilding schedules but omits secrets, storage definitions, and some node bindings. Use it only as an additional recovery aid.
|
||||
|
||||
## Test the recovery plan
|
||||
|
||||
At least quarterly, restore a recent snapshot into an isolated network, start the recorded BackupX version, and verify:
|
||||
|
||||
- `/ready` becomes healthy without contacting the production Master.
|
||||
- An administrator can sign in and encrypted storage configurations can be read.
|
||||
- Task, node, record, and audit counts are plausible.
|
||||
- A storage target can be tested without writing production data.
|
||||
- A selected backup can be verified or restored to an isolated destination.
|
||||
|
||||
Record restore duration and the newest recoverable snapshot time. Those measured values are the real control-plane RTO and RPO.
|
||||
268
docs-site/docs/reference/api.md
Normal file
268
docs-site/docs/reference/api.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: API Reference
|
||||
description: BackupX REST endpoints, authentication methods, role boundaries, streaming responses, and public probes.
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
The interactive API is rooted at `/api`. Most endpoints accept either a user JWT or an API key; Agent protocol endpoints use a node-specific token. Public probes and one-time installers are listed separately.
|
||||
|
||||
## Authentication
|
||||
|
||||
### User JWT
|
||||
|
||||
Obtain a JWT through `POST /api/auth/login` and send it as a Bearer token:
|
||||
|
||||
~~~bash
|
||||
curl -H "Authorization: Bearer $BACKUPX_TOKEN" \
|
||||
https://backup.example.com/api/backup/tasks
|
||||
~~~
|
||||
|
||||
The login flow may require OTP, TOTP, recovery code, a trusted-device token, or WebAuthn depending on account and system settings.
|
||||
|
||||
### API key
|
||||
|
||||
An administrator creates API keys in the console or through `POST /api/api-keys`. The plaintext `bax_...` value is returned only once.
|
||||
|
||||
~~~bash
|
||||
curl -H "X-Api-Key: $BACKUPX_API_KEY" \
|
||||
https://backup.example.com/api/dashboard/stats
|
||||
~~~
|
||||
|
||||
`Authorization: Bearer bax_...` is also accepted. API keys carry an `admin`, `operator`, or `viewer` role and can be disabled or given an expiry.
|
||||
|
||||
### Agent token
|
||||
|
||||
Agent protocol handlers authenticate the node token supplied in `X-Agent-Token`. This token is not a user credential and must not be used with the interactive resource API.
|
||||
|
||||
### Access labels
|
||||
|
||||
The tables use these labels:
|
||||
|
||||
| Label | Required access |
|
||||
| --- | --- |
|
||||
| Public | No JWT or API key; an install route still requires its one-time token |
|
||||
| Auth | Any authenticated `viewer`, `operator`, or `admin` |
|
||||
| Operator | `operator` or `admin` |
|
||||
| Admin | `admin` only |
|
||||
| Agent | Valid node-specific Agent token |
|
||||
|
||||
Viewers can use read endpoints except node filesystem browsing. Operators can run and mutate backup resources. Administrators additionally manage users, API keys, settings, nodes, install tokens, and node-token rotation. A rejected role returns HTTP 403.
|
||||
|
||||
## Authentication and account security
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/auth/setup/status` | Public | Check whether first-admin setup is required |
|
||||
| `POST` | `/api/auth/setup` | Public | Create the first administrator when no user exists |
|
||||
| `POST` | `/api/auth/login` | Public | Complete password or MFA login and obtain a JWT |
|
||||
| `POST` | `/api/auth/otp/send` | Public | Send a configured login OTP |
|
||||
| `POST` | `/api/auth/webauthn/login/options` | Public | Begin passkey login |
|
||||
| `POST` | `/api/auth/logout` | Auth | Acknowledge logout; the client must discard its stateless JWT |
|
||||
| `GET` | `/api/auth/profile` | Auth | Read the current account |
|
||||
| `PUT` | `/api/auth/password` | Auth | Change the current account password |
|
||||
| `POST` | `/api/auth/2fa/setup` | Auth | Prepare TOTP enrollment |
|
||||
| `POST` | `/api/auth/2fa/enable` | Auth | Enable TOTP after verification |
|
||||
| `POST` | `/api/auth/2fa/recovery-codes` | Auth | Regenerate recovery codes |
|
||||
| `DELETE` | `/api/auth/2fa` | Auth | Disable TOTP |
|
||||
| `PUT` | `/api/auth/otp/config` | Auth | Update OTP login configuration |
|
||||
| `POST` | `/api/auth/webauthn/register/options` | Auth | Begin passkey registration |
|
||||
| `POST` | `/api/auth/webauthn/register/finish` | Auth | Finish passkey registration |
|
||||
| `GET` | `/api/auth/webauthn/credentials` | Auth | List passkeys |
|
||||
| `DELETE` | `/api/auth/webauthn/credentials/:id` | Auth | Delete a passkey |
|
||||
| `GET` | `/api/auth/trusted-devices` | Auth | List trusted devices |
|
||||
| `DELETE` | `/api/auth/trusted-devices/:id` | Auth | Revoke a trusted device |
|
||||
|
||||
Use an interactive JWT, not an automation API key, for account-security endpoints.
|
||||
|
||||
## System and storage targets
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/system/info` | Auth | Version and system information |
|
||||
| `GET` | `/api/system/update-check` | Auth | Check available releases |
|
||||
| `GET` | `/api/storage-targets` | Auth | List storage targets |
|
||||
| `POST` | `/api/storage-targets` | Operator | Create a target |
|
||||
| `POST` | `/api/storage-targets/test` | Operator | Test an unsaved configuration |
|
||||
| `GET` | `/api/storage-targets/rclone/backends` | Auth | List available rclone backends |
|
||||
| `POST` | `/api/storage-targets/google-drive/auth-url` | Operator | Start Google Drive authorization |
|
||||
| `POST` | `/api/storage-targets/google-drive/complete` | Operator | Complete Google Drive authorization |
|
||||
| `GET` | `/api/storage-targets/google-drive/callback` | Auth | Handle the OAuth callback |
|
||||
| `GET` | `/api/storage-targets/:id` | Auth | Read a target |
|
||||
| `PUT` | `/api/storage-targets/:id` | Operator | Update a target |
|
||||
| `DELETE` | `/api/storage-targets/:id` | Operator | Delete a target |
|
||||
| `PUT` | `/api/storage-targets/:id/star` | Operator | Toggle favorite state |
|
||||
| `POST` | `/api/storage-targets/:id/test` | Operator | Test a saved target |
|
||||
| `GET` | `/api/storage-targets/:id/usage` | Auth | Read recorded usage |
|
||||
| `GET` | `/api/storage-targets/:id/google-drive/profile` | Auth | Read the connected Google Drive profile |
|
||||
|
||||
## Backup tasks
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/backup/tasks` | Auth | List tasks |
|
||||
| `GET` | `/api/backup/tasks/tags` | Auth | List task tags |
|
||||
| `GET` | `/api/backup/tasks/export` | Auth | Download all task definitions, or select them with `?ids=1,2` |
|
||||
| `POST` | `/api/backup/tasks/import` | Operator | Import task definitions, up to 1 MiB |
|
||||
| `POST` | `/api/backup/tasks/batch/toggle` | Operator | Enable or disable tasks in bulk |
|
||||
| `POST` | `/api/backup/tasks/batch/delete` | Operator | Delete tasks in bulk |
|
||||
| `POST` | `/api/backup/tasks/batch/run` | Operator | Run tasks in bulk |
|
||||
| `GET` | `/api/backup/tasks/:id` | Auth | Read a task |
|
||||
| `POST` | `/api/backup/tasks` | Operator | Create a task |
|
||||
| `PUT` | `/api/backup/tasks/:id` | Operator | Update a task |
|
||||
| `DELETE` | `/api/backup/tasks/:id` | Operator | Delete a task |
|
||||
| `PUT` | `/api/backup/tasks/:id/toggle` | Operator | Enable or disable a task |
|
||||
| `POST` | `/api/backup/tasks/:id/run` | Operator | Trigger a backup |
|
||||
| `POST` | `/api/backup/tasks/:id/verify` | Operator | Trigger verification from a task |
|
||||
|
||||
Task export intentionally excludes database passwords and storage credentials. It is useful for migration and review, not a complete control-plane backup.
|
||||
|
||||
## Backup and restore records
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/backup/records` | Auth | List and filter backup records |
|
||||
| `POST` | `/api/backup/records/batch-delete` | Operator | Delete records in bulk |
|
||||
| `GET` | `/api/backup/records/:id` | Auth | Read a backup record |
|
||||
| `GET` | `/api/backup/records/:id/logs/stream` | Auth | Stream logs with server-sent events |
|
||||
| `GET` | `/api/backup/records/:id/download` | Auth | Download an artifact |
|
||||
| `GET` | `/api/backup/records/:id/contents` | Auth | Browse artifact contents where supported |
|
||||
| `POST` | `/api/backup/records/:id/restore` | Operator | Start a restore |
|
||||
| `POST` | `/api/backup/records/:id/replicate` | Operator | Replicate an existing artifact |
|
||||
| `POST` | `/api/backup/records/:id/verify` | Operator | Verify an existing artifact |
|
||||
| `PUT` | `/api/backup/records/:id/lock` | Operator | Set retention lock state |
|
||||
| `DELETE` | `/api/backup/records/:id` | Operator | Delete a record and its managed artifact |
|
||||
| `GET` | `/api/restore/records` | Auth | List restore records |
|
||||
| `GET` | `/api/restore/records/:id` | Auth | Read a restore record |
|
||||
| `GET` | `/api/restore/records/:id/logs/stream` | Auth | Stream restore logs |
|
||||
| `GET` | `/api/replication/records` | Auth | List replication records |
|
||||
| `GET` | `/api/replication/records/:id` | Auth | Read a replication record |
|
||||
| `GET` | `/api/verify/records` | Auth | List verification records |
|
||||
| `GET` | `/api/verify/records/:id` | Auth | Read a verification record |
|
||||
| `GET` | `/api/verify/records/:id/logs/stream` | Auth | Stream verification logs |
|
||||
|
||||
## Templates, reports, and dashboard
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/task-templates` | Auth | List task templates |
|
||||
| `GET` | `/api/task-templates/:id` | Auth | Read a task template |
|
||||
| `POST` | `/api/task-templates` | Operator | Create a template |
|
||||
| `PUT` | `/api/task-templates/:id` | Operator | Update a template |
|
||||
| `DELETE` | `/api/task-templates/:id` | Operator | Delete a template |
|
||||
| `POST` | `/api/task-templates/:id/apply` | Operator | Create tasks from a template |
|
||||
| `GET` | `/api/reports/compliance` | Auth | Read compliance evidence |
|
||||
| `GET` | `/api/reports/compliance/export` | Auth | Export compliance evidence as CSV |
|
||||
| `GET` | `/api/dashboard/stats` | Auth | Summary statistics |
|
||||
| `GET` | `/api/dashboard/timeline` | Auth | Recent activity |
|
||||
| `GET` | `/api/dashboard/sla` | Auth | RPO and SLA status |
|
||||
| `GET` | `/api/dashboard/cluster` | Auth | Cluster summary |
|
||||
| `GET` | `/api/dashboard/breakdown` | Auth | Task and record breakdown |
|
||||
| `GET` | `/api/dashboard/node-performance` | Auth | Per-node performance |
|
||||
|
||||
## Notifications, settings, and administration
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/notifications` | Auth | List notification channels |
|
||||
| `GET` | `/api/notifications/:id` | Auth | Read a channel |
|
||||
| `POST` | `/api/notifications` | Operator | Create a channel |
|
||||
| `PUT` | `/api/notifications/:id` | Operator | Update a channel |
|
||||
| `DELETE` | `/api/notifications/:id` | Operator | Delete a channel |
|
||||
| `POST` | `/api/notifications/test` | Operator | Test an unsaved configuration |
|
||||
| `POST` | `/api/notifications/:id/test` | Operator | Test a saved channel |
|
||||
| `GET` | `/api/settings` | Auth | Read system settings |
|
||||
| `PUT` | `/api/settings` | Admin | Update system settings |
|
||||
| `GET` | `/api/users` | Admin | List users |
|
||||
| `POST` | `/api/users` | Admin | Create a user |
|
||||
| `PUT` | `/api/users/:id` | Admin | Update a user |
|
||||
| `POST` | `/api/users/:id/2fa/reset` | Admin | Reset a user's second factor |
|
||||
| `DELETE` | `/api/users/:id` | Admin | Delete a user |
|
||||
| `GET` | `/api/api-keys` | Admin | List API keys without plaintext values |
|
||||
| `POST` | `/api/api-keys` | Admin | Create an API key and return its plaintext once |
|
||||
| `PUT` | `/api/api-keys/:id/toggle` | Admin | Enable or disable an API key |
|
||||
| `DELETE` | `/api/api-keys/:id` | Admin | Revoke an API key |
|
||||
|
||||
## Audit, events, search, and discovery
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/audit-logs` | Auth | List and filter audit records |
|
||||
| `GET` | `/api/audit-logs/export` | Auth | Export audit records |
|
||||
| `GET` | `/api/events/stream` | Auth | Stream real-time application events with SSE |
|
||||
| `GET` | `/api/search` | Auth | Search supported resources |
|
||||
| `POST` | `/api/database/discover` | Auth | Discover databases from supplied connection details |
|
||||
|
||||
## Nodes
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/nodes` | Auth | List nodes |
|
||||
| `GET` | `/api/nodes/:id` | Auth | Read a node |
|
||||
| `GET` | `/api/nodes/:id/fs/list` | Operator | Browse the selected node filesystem |
|
||||
| `POST` | `/api/nodes` | Admin | Create a node |
|
||||
| `POST` | `/api/nodes/batch` | Admin | Create up to 50 nodes |
|
||||
| `PUT` | `/api/nodes/:id` | Admin | Update a node |
|
||||
| `DELETE` | `/api/nodes/:id` | Admin | Delete an unreferenced node |
|
||||
| `POST` | `/api/nodes/:id/install-tokens` | Admin | Create a one-time installer |
|
||||
| `GET` | `/api/nodes/:id/install-script-preview` | Admin | Preview generated install material |
|
||||
| `POST` | `/api/nodes/:id/rotate-token` | Admin | Rotate the long-lived node token |
|
||||
|
||||
## Agent protocol
|
||||
|
||||
These routes are for the `backupx agent` process and authenticate inside the handler with the node token.
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `POST` | `/api/agent/heartbeat` | Agent | Report liveness and node state |
|
||||
| `POST` | `/api/agent/commands/poll` | Agent | Claim a pending command |
|
||||
| `POST` | `/api/agent/commands/:id/result` | Agent | Report a command result |
|
||||
| `GET` | `/api/agent/tasks/:id` | Agent | Fetch a runnable task specification |
|
||||
| `POST` | `/api/agent/records/:id` | Agent | Append logs or update backup state |
|
||||
| `PUT` | `/api/agent/records/:id/artifacts/:targetId` | Agent | Stream a relayed artifact to the Master |
|
||||
| `GET` | `/api/agent/restores/:id/spec` | Agent | Fetch restore instructions |
|
||||
| `GET` | `/api/agent/restores/:id/artifact` | Agent | Stream a restore artifact |
|
||||
| `POST` | `/api/agent/restores/:id` | Agent | Update restore state |
|
||||
| `GET` | `/api/v1/agent/self` | Agent | Validate node identity during installation |
|
||||
|
||||
## Public operational and install routes
|
||||
|
||||
| Method | Endpoint | Access | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/health` | Public | Liveness |
|
||||
| `GET` | `/api/health` | Public | API-prefixed liveness alias |
|
||||
| `GET` | `/ready` | Public | SQLite readiness |
|
||||
| `GET` | `/api/ready` | Public | API-prefixed readiness alias |
|
||||
| `GET` | `/metrics` | Public | Prometheus metrics |
|
||||
| `GET` | `/install/:token` | Public | Consume a one-time Agent installer token |
|
||||
| `GET` | `/api/install/:token` | Public | API-prefixed installer route |
|
||||
| `GET` | `/install/:token/compose.yml` | Public | Render a Docker Agent Compose file |
|
||||
| `GET` | `/api/install/:token/compose.yml` | Public | API-prefixed Docker Compose route |
|
||||
|
||||
Restrict probes and metrics to monitoring networks. Install tokens are single-use, time-limited secrets and must not be written to public logs.
|
||||
|
||||
## Response formats
|
||||
|
||||
Most JSON successes use:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
~~~
|
||||
|
||||
Errors use an HTTP 4xx or 5xx status plus a stable application code:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"code": "BACKUP_TASK_NOT_FOUND",
|
||||
"message": "备份任务不存在"
|
||||
}
|
||||
~~~
|
||||
|
||||
Clients should branch on the HTTP status and `code`, not the localized `message`.
|
||||
|
||||
Artifact downloads, task JSON export, audit or compliance exports, installer responses, and `/metrics` return their native content types instead of the JSON envelope. Log and event streams use `text/event-stream`; reverse proxies must keep response buffering disabled.
|
||||
80
docs-site/docs/reference/cli.md
Normal file
80
docs-site/docs/reference/cli.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: CLI Reference
|
||||
description: backupx subcommands — server, agent, backint, reset-password.
|
||||
---
|
||||
|
||||
# CLI Reference
|
||||
|
||||
The `backupx` binary ships several subcommands. Running `backupx` with no subcommand starts the main server process.
|
||||
|
||||
## `backupx` (default: server)
|
||||
|
||||
```bash
|
||||
backupx --config /opt/backupx/config.yaml
|
||||
backupx --version
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--config <path>` | Explicit config YAML path; omitted uses the search paths below |
|
||||
| `--version` | Print version and exit |
|
||||
|
||||
When `--config` is omitted, the server searches `./config.yaml`, `./server/config.yaml`, and `/etc/backupx/config.yaml`. `BACKUPX_*` environment variables override matching server configuration keys. See [Configuration Reference](../deployment/configuration).
|
||||
|
||||
## `backupx agent`
|
||||
|
||||
Run in Agent mode, connecting to a Master. See [Multi-Node Cluster](../features/multi-node).
|
||||
|
||||
```bash
|
||||
backupx agent --master https://backup.example.com --token-file /etc/backupx-agent/agent.token
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--master <url>` | Master URL |
|
||||
| `--token <token>` | Agent auth token |
|
||||
| `--token-file <path>` | Read the Agent Token from a file; preferred for services and containers |
|
||||
| `--config <path>` | Load Agent YAML; when present, environment-based Agent config is not loaded |
|
||||
| `--temp-dir <path>` | Local temp directory (default `/var/lib/backupx-agent/tmp`) |
|
||||
| `--proxy-url <url>` | Explicit HTTP(S) or SOCKS5(H) proxy |
|
||||
| `--ca-cert <path>` | PEM CA certificate used to verify the Master |
|
||||
| `--insecure-tls` | Skip TLS verification (testing only) |
|
||||
|
||||
Agent precedence is explicit CLI flags over a YAML file. If `--config` is not supplied, Agent settings are loaded from `BACKUPX_AGENT_MASTER`, `BACKUPX_AGENT_TOKEN`, `BACKUPX_AGENT_TOKEN_FILE`, `BACKUPX_AGENT_HEARTBEAT`, `BACKUPX_AGENT_POLL`, `BACKUPX_AGENT_TEMP_DIR`, `BACKUPX_AGENT_PROXY_URL`, `BACKUPX_AGENT_CA_CERT_FILE`, and `BACKUPX_AGENT_INSECURE_TLS`. When no explicit proxy URL is set, the Agent also honors `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`.
|
||||
|
||||
`--token` overrides `--token-file`. Keep long-lived tokens in a root-readable file rather than command history. A private CA and `--insecure-tls` cannot be enabled together.
|
||||
|
||||
## `backupx backint`
|
||||
|
||||
SAP HANA Backint protocol agent. See [SAP HANA Support](../features/sap-hana).
|
||||
|
||||
```bash
|
||||
backupx backint -f <function> -i <input> -o <output> -p <params>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-f <fn>` | `backup` / `restore` / `inquire` / `delete` |
|
||||
| `-i <path>` | Input file |
|
||||
| `-o <path>` | Output file |
|
||||
| `-p <path>` | Parameter file |
|
||||
| `-u / -c / -l / -v` | Accepted and ignored for SAP compatibility |
|
||||
|
||||
The `-p` file must define `STORAGE_TYPE` and either `STORAGE_CONFIG_JSON` or `STORAGE_CONFIG`. Optional keys include `PARALLEL_FACTOR`, `COMPRESS`, `LOG_FILE`, `CATALOG_DB`, and `KEY_PREFIX`.
|
||||
|
||||
## `backupx reset-password`
|
||||
|
||||
Reset an admin password directly in the SQLite database. No server restart needed.
|
||||
|
||||
```bash
|
||||
backupx reset-password --username admin --password 'newpass123' [--config /path/to/config.yaml]
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--username` | Target username (default: `admin`) |
|
||||
| `--password` | New password (min 8 chars, required) |
|
||||
| `--config` | Config path (used to locate the database file) |
|
||||
|
||||
Run this command on the Master host with access to the configured SQLite path. Avoid placing the new password directly in retained shell history.
|
||||
159
docs-site/docusaurus.config.ts
Normal file
159
docs-site/docusaurus.config.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import {themes as prismThemes} from 'prism-react-renderer';
|
||||
import type {Config} from '@docusaurus/types';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
|
||||
// BackupX 官方站点 — 托管在 GitHub Pages
|
||||
// https://awuqing.github.io/BackupX/
|
||||
const config: Config = {
|
||||
title: 'BackupX',
|
||||
tagline: 'Self-hosted backup orchestration for servers, databases, storage targets and remote agents',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
future: {
|
||||
v4: true,
|
||||
},
|
||||
|
||||
url: 'https://awuqing.github.io',
|
||||
baseUrl: '/BackupX/',
|
||||
|
||||
organizationName: 'Awuqing',
|
||||
projectName: 'BackupX',
|
||||
deploymentBranch: 'gh-pages',
|
||||
trailingSlash: false,
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
markdown: {
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'zh-Hans'],
|
||||
localeConfigs: {
|
||||
en: {label: 'English', direction: 'ltr', htmlLang: 'en-US'},
|
||||
// Keep the published /zh-Hans/ URL while loading the existing zh-CN
|
||||
// translation tree. Without path, Docusaurus silently falls back to the
|
||||
// English documents because i18n/zh-Hans does not exist.
|
||||
'zh-Hans': {label: '简体中文', direction: 'ltr', htmlLang: 'zh-CN', path: 'zh-CN'},
|
||||
},
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
{
|
||||
docs: {
|
||||
sidebarPath: './sidebars.ts',
|
||||
editUrl: 'https://github.com/Awuqing/BackupX/edit/main/docs-site/',
|
||||
editLocalizedFiles: true,
|
||||
},
|
||||
blog: false,
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
image: 'img/social-card.png',
|
||||
colorMode: {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
title: 'BackupX',
|
||||
logo: {
|
||||
alt: 'BackupX Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'docs',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/Awuqing/BackupX/releases',
|
||||
label: 'Downloads',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
to: '/community',
|
||||
label: 'Community',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
to: '/sponsors',
|
||||
label: 'Sponsors',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/Awuqing/BackupX',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{label: 'Introduction', to: '/docs/intro'},
|
||||
{label: 'Quick Start', to: '/docs/getting-started/quick-start'},
|
||||
{label: 'Upgrade & Recovery', to: '/docs/operations/upgrade-recovery'},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Features',
|
||||
items: [
|
||||
{label: 'SAP HANA', to: '/docs/features/sap-hana'},
|
||||
{label: 'Multi-Node Cluster', to: '/docs/features/multi-node'},
|
||||
{label: 'API Reference', to: '/docs/reference/api'},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{label: 'GitHub', href: 'https://github.com/Awuqing/BackupX'},
|
||||
{label: 'Releases', href: 'https://github.com/Awuqing/BackupX/releases'},
|
||||
{label: 'Docker Hub', href: 'https://hub.docker.com/r/awuqing/backupx'},
|
||||
{label: 'Issues', href: 'https://github.com/Awuqing/BackupX/issues'},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{label: 'Contributors', href: 'https://github.com/Awuqing/BackupX/graphs/contributors'},
|
||||
{label: 'Pull Requests', href: 'https://github.com/Awuqing/BackupX/pulls'},
|
||||
{label: 'Sponsor', to: '/sponsors'},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sponsors',
|
||||
items: [
|
||||
{label: 'Sponsor BackupX', href: 'https://github.com/sponsors/Awuqing'},
|
||||
{label: 'Partnership', href: 'https://github.com/Awuqing/BackupX/issues/new/choose'},
|
||||
{label: 'Sponsor tiers', to: '/sponsors'},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} BackupX · Apache License 2.0`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
additionalLanguages: ['bash', 'yaml', 'ini', 'json', 'go', 'sql', 'nginx'],
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
160
docs-site/i18n/zh-CN/code.json
Normal file
160
docs-site/i18n/zh-CN/code.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"home.badge": {
|
||||
"message": "开源备份控制平面 · v2.2.1",
|
||||
"description": "Version badge on the hero"
|
||||
},
|
||||
"home.title.part1": {
|
||||
"message": "面向自托管服务器的",
|
||||
"description": "Hero title, first line"
|
||||
},
|
||||
"home.title.part2": {
|
||||
"message": "备份编排平台。",
|
||||
"description": "Hero title accent second line"
|
||||
},
|
||||
"home.tagline": {
|
||||
"message": "在一个清爽控制台中管理文件、数据库、SAP HANA 和远程节点备份。控制平面自己掌握,存储后端灵活选择。",
|
||||
"description": "Tagline on the home page"
|
||||
},
|
||||
"home.pageTitle": {
|
||||
"message": "面向自托管服务器的备份编排",
|
||||
"description": "Page <title> element on the home page"
|
||||
},
|
||||
"home.getStarted": {
|
||||
"message": "快速开始",
|
||||
"description": "Primary CTA on the home page"
|
||||
},
|
||||
"home.metric.backends": {
|
||||
"message": "存储后端",
|
||||
"description": "Hero metric label: storage backends"
|
||||
},
|
||||
"home.metric.backupTypes": {
|
||||
"message": "远程执行",
|
||||
"description": "Hero metric label: backup types"
|
||||
},
|
||||
"home.metric.license": {
|
||||
"message": "开源协议",
|
||||
"description": "Hero metric label: license"
|
||||
},
|
||||
"home.visual.eyebrow": {"message": "BackupX 控制台"},
|
||||
"home.visual.title": {"message": "运维概览"},
|
||||
"home.visual.status": {"message": "健康"},
|
||||
"home.visual.success": {"message": "成功率"},
|
||||
"home.visual.nodes": {"message": "活跃节点"},
|
||||
"home.visual.targets": {"message": "存储目标"},
|
||||
"home.visual.row1.title": {"message": "PostgreSQL 夜间备份"},
|
||||
"home.visual.row1.desc": {"message": "加密归档已上传至 S3"},
|
||||
"home.visual.row2.title": {"message": "SAP HANA 快照"},
|
||||
"home.visual.row2.desc": {"message": "正在 agent-shanghai-02 上运行"},
|
||||
"home.visual.row3.title": {"message": "保留策略清理"},
|
||||
"home.visual.row3.desc": {"message": "下一次执行在 4 小时后"},
|
||||
"home.command.title": {"message": "使用 Docker 启动"},
|
||||
|
||||
"section.features.tag": {
|
||||
"message": "核心能力",
|
||||
"description": "FEATURES section tag"
|
||||
},
|
||||
"section.features.title": {
|
||||
"message": "该有的都有,多余的没有",
|
||||
"description": "Features section title"
|
||||
},
|
||||
"section.features.subtitle": {
|
||||
"message": "备份 Runner、存储 Provider、调度、集群 — 每一块都经过打磨。",
|
||||
"description": "Features section subtitle"
|
||||
},
|
||||
|
||||
"feat.types.title": {"message": "多种备份类型"},
|
||||
"feat.types.desc": {"message": "文件与目录(支持多源路径),以及 MySQL、PostgreSQL、SQLite、SAP HANA 统一管理。"},
|
||||
"feat.storage.title": {"message": "70+ 存储后端"},
|
||||
"feat.storage.desc": {"message": "内置阿里云 OSS、腾讯云 COS、七牛、S3、Google Drive、WebDAV、FTP,以及 SFTP、Azure Blob、Dropbox 等 rclone 后端。"},
|
||||
"feat.scheduling.title": {"message": "调度与保留策略"},
|
||||
"feat.scheduling.desc": {"message": "基于 Cron 的可视化调度编辑器,支持按天数/份数自动保留和空目录清理。"},
|
||||
"feat.cluster.title": {"message": "多节点集群"},
|
||||
"feat.cluster.desc": {"message": "Master-Agent 采用 Agent 主动出站轮询。支持代理、私有 CA 与 SSH 堡垒机,无需反向连通性。"},
|
||||
"feat.security.title": {"message": "默认安全"},
|
||||
"feat.security.desc": {"message": "JWT 认证、bcrypt、AES-256-GCM 加密配置、可选备份加密、完整审计日志。"},
|
||||
"feat.deploy.title": {"message": "部署轻量"},
|
||||
"feat.deploy.desc": {"message": "单个静态二进制 + 内嵌 SQLite。Docker 一键启动或裸机 — 零外部依赖。"},
|
||||
"feat.learnMore": {"message": "了解更多"},
|
||||
|
||||
"showcase.tag": {"message": "产品界面"},
|
||||
"showcase.title": {"message": "精心打磨的控制台,而非 DIY 脚本"},
|
||||
"showcase.subtitle": {"message": "每个页面都为运维而生 — 可观测优先,可配置次之。"},
|
||||
"showcase.tab.dashboard": {"message": "仪表盘"},
|
||||
"showcase.tab.tasks": {"message": "备份任务"},
|
||||
"showcase.tab.storage": {"message": "存储目标"},
|
||||
"showcase.tab.nodes": {"message": "多节点"},
|
||||
"showcase.dashboard.title": {"message": "一眼掌握全局"},
|
||||
"showcase.dashboard.desc": {"message": "备份成功率、存储使用量、最近执行记录、即将触发的计划 — 一页实时数据。"},
|
||||
"showcase.tasks.title": {"message": "可视化任务编辑器"},
|
||||
"showcase.tasks.desc": {"message": "文件、MySQL、PostgreSQL、SQLite、SAP HANA — 三步完成。Cron 编辑器、多目标分发、保留策略、压缩、加密 — 点击即用。"},
|
||||
"showcase.storage.title": {"message": "70+ 后端,统一体验"},
|
||||
"showcase.storage.desc": {"message": "阿里云 OSS、腾讯云 COS、S3、Google Drive、WebDAV — 加上每一种 rclone 后端。测试连接、收藏、查看实时容量。"},
|
||||
"showcase.nodes.title": {"message": "几分钟搭起 Master-Agent"},
|
||||
"showcase.nodes.desc": {"message": "创建节点、复制令牌、在任意远程主机启动 Agent。路由到节点的任务在本地执行并直接上传到存储 — 无需反向连通性。"},
|
||||
"showcase.cta": {"message": "开始阅读文档"},
|
||||
|
||||
"community.tag": {"message": "社区"},
|
||||
"community.pageTitle": {"message": "社区、赞助商与贡献者"},
|
||||
"community.pageDescription": {"message": "赞助 BackupX,了解贡献者,并找到务实的参与方式。"},
|
||||
"community.title": {"message": "开放协作,面向长期运维"},
|
||||
"community.subtitle": {"message": "备份软件的信任来自透明发布、真实部署反馈,以及足够务实的贡献路径。"},
|
||||
"community.sponsor.kicker": {"message": "赞助商"},
|
||||
"community.sponsor.wallTitle": {"message": "赞助商"},
|
||||
"community.sponsor.title": {"message": "支持你依赖的备份基础设施"},
|
||||
"community.sponsor.cta": {"message": "赞助 BackupX"},
|
||||
"community.sponsor.openSlot": {"message": "赞助席位开放"},
|
||||
"community.sponsor.logo.project": {"message": "项目赞助"},
|
||||
"community.sponsor.logo.cloud": {"message": "云服务伙伴"},
|
||||
"community.sponsor.logo.object": {"message": "对象存储"},
|
||||
"community.sponsor.logo.cdn": {"message": "CDN 伙伴"},
|
||||
"community.sponsor.logo.database": {"message": "数据库伙伴"},
|
||||
"community.sponsor.logo.security": {"message": "安全审计"},
|
||||
"community.sponsor.logo.agent": {"message": "远程节点实验室"},
|
||||
"community.sponsor.logo.docs": {"message": "文档赞助"},
|
||||
"community.sponsor.logo.release": {"message": "发布赞助"},
|
||||
"community.sponsor.logo.s3": {"message": "S3 兼容"},
|
||||
"community.sponsor.logo.webdav": {"message": "WebDAV 伙伴"},
|
||||
"community.sponsor.logo.sftp": {"message": "SFTP 伙伴"},
|
||||
"community.sponsor.logo.docker": {"message": "容器伙伴"},
|
||||
"community.sponsor.logo.mirror": {"message": "镜像伙伴"},
|
||||
"community.sponsor.logo.restore": {"message": "恢复演练"},
|
||||
"community.sponsor.logo.qa": {"message": "测试实验室"},
|
||||
"community.sponsor.logo.oss": {"message": "开源支持"},
|
||||
"community.sponsor.logo.open": {"message": "赞助席位开放"},
|
||||
"community.sponsor.infrastructure.label": {"message": "基础设施"},
|
||||
"community.sponsor.infrastructure.title": {"message": "云与存储生态伙伴"},
|
||||
"community.sponsor.infrastructure.desc": {"message": "帮助 BackupX 覆盖对象存储、WebDAV、SFTP 以及区域云平台的真实验证。"},
|
||||
"community.sponsor.security.label": {"message": "安全"},
|
||||
"community.sponsor.security.title": {"message": "审计与可靠性支持者"},
|
||||
"community.sponsor.security.desc": {"message": "支持加密、恢复演练、发布签名和运维检查等强化工作。"},
|
||||
"community.sponsor.community.label": {"message": "社区"},
|
||||
"community.sponsor.community.title": {"message": "开源支持者"},
|
||||
"community.sponsor.community.desc": {"message": "支持文档、示例、平台测试和贡献者引导。"},
|
||||
"community.sponsor.tier.backer.name": {"message": "Backer"},
|
||||
"community.sponsor.tier.backer.amount": {"message": "适合个人与小团队"},
|
||||
"community.sponsor.tier.backer.desc": {"message": "支持文档、Issue 分流、兼容性测试和小型体验改进。"},
|
||||
"community.sponsor.tier.partner.name": {"message": "Partner"},
|
||||
"community.sponsor.tier.partner.amount": {"message": "适合存储与基础设施厂商"},
|
||||
"community.sponsor.tier.partner.desc": {"message": "支持 Provider 验证、部署示例、基准说明和集成指南。"},
|
||||
"community.sponsor.tier.enterprise.name": {"message": "Enterprise"},
|
||||
"community.sponsor.tier.enterprise.amount": {"message": "适合生产环境使用方"},
|
||||
"community.sponsor.tier.enterprise.desc": {"message": "赞助恢复演练、发布加固、审计和长期维护等可靠性工作。"},
|
||||
"community.contributor.kicker": {"message": "贡献者"},
|
||||
"community.contributor.all": {"message": "查看全部"},
|
||||
"community.contributor.source": {"message": "浏览器端通过 GitHub contributors API 获取。"},
|
||||
"community.contributor.botRole": {"message": "自动化贡献者"},
|
||||
"community.contributor.githubRole": {"message": "GitHub 贡献者"},
|
||||
"community.contributor.contributions": {"message": "{count} 次贡献"},
|
||||
"community.path.kicker": {"message": "贡献路径"},
|
||||
"community.path.issues.title": {"message": "反馈生产问题"},
|
||||
"community.path.issues.desc": {"message": "提交日志、部署拓扑和恢复预期。"},
|
||||
"community.path.docs.title": {"message": "完善文档与示例"},
|
||||
"community.path.docs.desc": {"message": "贡献存储、Agent 和数据库部署指南。"},
|
||||
"community.path.code.title": {"message": "提交聚焦的 PR"},
|
||||
"community.path.code.desc": {"message": "保持改动小而可测,并贴合现有架构。"},
|
||||
"sponsors.pageTitle": {"message": "赞助商"},
|
||||
"sponsors.pageDescription": {"message": "赞助 BackupX 的可靠性、文档、存储兼容性和长期维护。"},
|
||||
"sponsors.tag": {"message": "赞助商"},
|
||||
"sponsors.title": {"message": "赞助 BackupX 生态"},
|
||||
"sponsors.subtitle": {"message": "赞助帮助 BackupX 更贴近真实运维:经过验证的存储 Provider、可靠发布、恢复信心和更完善的文档。"}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version.label": {"message": "Next"},
|
||||
"sidebar.docs.category.Getting Started": {"message": "快速开始"},
|
||||
"sidebar.docs.category.Deployment": {"message": "部署"},
|
||||
"sidebar.docs.category.Operations": {"message": "运维"},
|
||||
"sidebar.docs.category.Features": {"message": "功能特性"},
|
||||
"sidebar.docs.category.Reference": {"message": "参考"},
|
||||
"sidebar.docs.category.Development": {"message": "开发"}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 裸机部署
|
||||
description: 从预编译包或源码加固部署 BackupX,Nginx 改为显式启用。
|
||||
---
|
||||
|
||||
# 裸机部署
|
||||
|
||||
## 使用预编译包
|
||||
|
||||
```bash
|
||||
# 下载对应平台的压缩包
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz
|
||||
curl -LO https://github.com/Awuqing/BackupX/releases/latest/download/backupx-linux-amd64.tar.gz.sha256
|
||||
sha256sum -c backupx-linux-amd64.tar.gz.sha256
|
||||
|
||||
# 解压并安装
|
||||
tar xzf backupx-linux-amd64.tar.gz && cd backupx-*-linux-amd64
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
安装脚本自动完成以下步骤:
|
||||
|
||||
1. 创建系统用户 `backupx`
|
||||
2. 复制二进制到 `/opt/backupx/bin/backupx`,并把 Web 控制台复制到 `/opt/backupx/web`
|
||||
3. 把默认配置安装到 `/etc/backupx/config.yaml`
|
||||
4. 安装并启用 `backupx.service` systemd 单元
|
||||
5. 默认不修改 Nginx;只有显式设置 `INSTALL_NGINX=1` 时才安装模板
|
||||
6. 验证首次初始化接口就绪后才报告安装成功
|
||||
|
||||
可执行文件与前端资源由 root 所有,只有 `/opt/backupx/data` 允许 `backupx` 服务账户写入。`/etc/backupx/config.yaml` 以 `root:backupx`、`0640` 权限安装。
|
||||
|
||||
仓库提供的 Nginx 模板只是起点,可能与现有默认站点冲突。先审核域名与 TLS 策略,再显式启用:
|
||||
|
||||
```bash
|
||||
sudo INSTALL_NGINX=1 ./install.sh
|
||||
```
|
||||
|
||||
如果要部署多节点集群,安装后请编辑 `/etc/backupx/config.yaml`,设置远程 Agent 可访问到的 Master URL:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
external_url: "https://backup.example.com"
|
||||
```
|
||||
|
||||
修改后重启 BackupX:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart backupx
|
||||
```
|
||||
|
||||
## 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
`make build` 会产出:
|
||||
|
||||
- `server/bin/backupx`(Go 后端,无 CGO)
|
||||
- `web/dist/`(React 前端,执行 `npm run build`)
|
||||
|
||||
## systemd
|
||||
|
||||
安装后的 service 文件:
|
||||
|
||||
```ini title="/etc/systemd/system/backupx.service"
|
||||
[Unit]
|
||||
Description=BackupX API Service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=backupx
|
||||
Group=backupx
|
||||
WorkingDirectory=/opt/backupx
|
||||
ExecStart=/opt/backupx/bin/backupx -config /etc/backupx/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
UMask=0027
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
sudo systemctl status backupx
|
||||
sudo journalctl -u backupx -f # 实时日志
|
||||
sudo systemctl restart backupx
|
||||
curl -fsS http://127.0.0.1:8340/api/auth/setup/status
|
||||
```
|
||||
|
||||
访问 `http://your-server:8340`,可按需切换到 English,然后在“系统初始化 / System setup”页面创建首个管理员。若监听端口不是默认值,请为安装脚本传入对应的 `HEALTH_URL`。
|
||||
|
||||
生产环境应通过 HTTPS 暴露 BackupX,或在防火墙限制 `8340` 端口。安装器不会自动修改防火墙。
|
||||
|
||||
替换版本前,应在服务停止时同时快照 `/etc/backupx`、`/opt/backupx/data`、已安装二进制和前端文件。请按[升级与恢复](../operations/upgrade-recovery)中的版本化流程操作;让旧版本二进制直接读取已由新版本迁移的数据库并不是安全回滚。
|
||||
|
||||
## 密码重置
|
||||
|
||||
忘记管理员密码时:
|
||||
|
||||
```bash
|
||||
/opt/backupx/bin/backupx reset-password \
|
||||
--username admin \
|
||||
--password 'newpass123' \
|
||||
--config /etc/backupx/config.yaml
|
||||
```
|
||||
|
||||
Docker 等效命令:
|
||||
|
||||
```bash
|
||||
docker exec -it backupx /app/bin/backupx reset-password --username admin --password 'newpass123'
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: 配置参考
|
||||
description: config.yaml 全部服务端配置项及对应的环境变量。
|
||||
---
|
||||
|
||||
# 配置参考
|
||||
|
||||
BackupX 默认从工作目录加载 `./config.yaml`,可通过 `--config` 指定其他路径。所有配置项都可通过 `BACKUPX_` 前缀环境变量覆盖。
|
||||
|
||||
## 完整配置
|
||||
|
||||
```yaml title="config.yaml"
|
||||
server:
|
||||
host: "0.0.0.0" # BACKUPX_SERVER_HOST
|
||||
port: 8340 # BACKUPX_SERVER_PORT
|
||||
mode: "release" # release | debug
|
||||
external_url: "" # BACKUPX_SERVER_EXTERNAL_URL — 稳定的 Master 对外 URL
|
||||
trusted_proxies: # BACKUPX_SERVER_TRUSTED_PROXIES — 准确的代理 IP/CIDR
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
web_root: "" # BACKUPX_SERVER_WEB_ROOT — 前端构建目录
|
||||
|
||||
database:
|
||||
path: "./data/backupx.db" # BACKUPX_DATABASE_PATH — 内嵌 SQLite
|
||||
|
||||
security:
|
||||
jwt_secret: "" # BACKUPX_SECURITY_JWT_SECRET — 留空自动生成
|
||||
jwt_expire: "24h" # BACKUPX_SECURITY_JWT_EXPIRE
|
||||
encryption_key: "" # 用于加密存储配置的 AES-256-GCM 密钥
|
||||
|
||||
backup:
|
||||
temp_dir: "/tmp/backupx" # BACKUPX_BACKUP_TEMP_DIR
|
||||
max_concurrent: 2 # BACKUPX_BACKUP_MAX_CONCURRENT
|
||||
retries: 10 # 单次上传的 rclone 底层重试次数
|
||||
bandwidth_limit: "" # 例如 "10M" 表示限速 10 MB/s
|
||||
|
||||
log:
|
||||
level: "info" # debug | info | warn | error
|
||||
file: "./data/backupx.log"
|
||||
max_size: 100 # 单个日志文件上限,单位 MB
|
||||
max_backups: 3 # 保留的轮转文件数
|
||||
max_age: 30 # 保留天数
|
||||
```
|
||||
|
||||
## 密钥生成
|
||||
|
||||
如果首次启动时 `jwt_secret` 或 `encryption_key` 为空,BackupX 会自动生成随机值并写入 `system_configs` 表。请妥善备份 `data/backupx.db`,一旦丢失将导致所有已加密的存储配置失效。
|
||||
|
||||
## 环境变量
|
||||
|
||||
文件和环境变量同时存在时,环境变量优先。配置路径转换规则:小写字母下划线 → 大写字母下划线:
|
||||
|
||||
| 配置项 | 环境变量 |
|
||||
|--------|----------|
|
||||
| `server.port` | `BACKUPX_SERVER_PORT` |
|
||||
| `server.external_url` | `BACKUPX_SERVER_EXTERNAL_URL` |
|
||||
| `server.trusted_proxies` | `BACKUPX_SERVER_TRUSTED_PROXIES`(环境变量使用逗号分隔) |
|
||||
| `security.jwt_secret` | `BACKUPX_SECURITY_JWT_SECRET` |
|
||||
| `security.jwt_expire` | `BACKUPX_SECURITY_JWT_EXPIRE` |
|
||||
| `security.encryption_key` | `BACKUPX_SECURITY_ENCRYPTION_KEY` |
|
||||
| `log.level` | `BACKUPX_LOG_LEVEL` |
|
||||
| `backup.max_concurrent` | `BACKUPX_BACKUP_MAX_CONCURRENT` |
|
||||
| `backup.temp_dir` | `BACKUPX_BACKUP_TEMP_DIR` |
|
||||
| `backup.retries` | `BACKUPX_BACKUP_RETRIES` |
|
||||
| `backup.bandwidth_limit` | `BACKUPX_BACKUP_BANDWIDTH_LIMIT` |
|
||||
| `log.max_size` | `BACKUPX_LOG_MAX_SIZE` |
|
||||
| `log.max_backups` | `BACKUPX_LOG_MAX_BACKUPS` |
|
||||
| `log.max_age` | `BACKUPX_LOG_MAX_AGE` |
|
||||
|
||||
## Master 对外 URL
|
||||
|
||||
当 BackupX 部署在 Docker、Nginx、负载均衡或多层反向代理后面,且后端收到的内部 Host 不是远程 Agent 可访问地址时,请配置 `server.external_url`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
external_url: "https://backup.example.com"
|
||||
```
|
||||
|
||||
BackupX 会用这个地址渲染一键 Agent 安装脚本和 docker-compose 片段。该地址必须能被所有 Agent 主机访问。只有在 `X-Forwarded-Proto` / `X-Forwarded-Host` 可靠且正好指向 Agent 可访问地址时,才建议留空。
|
||||
|
||||
代理或 SSH 堡垒机场景可在安装向导中为单个 Agent 设置覆盖地址。目标侧的一次性安装链接与生成的 Agent 运行配置都会使用这个地址,浏览器仍使用正常的公开地址。
|
||||
|
||||
## 可信反向代理
|
||||
|
||||
BackupX 只接受 `server.trusted_proxies` 中来源提供的客户端转发头。默认仅允许本机 Nginx。代理运行在其他容器或主机时,加入准确 IP 或网段:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
trusted_proxies:
|
||||
- "127.0.0.1"
|
||||
- "172.18.0.0/16"
|
||||
```
|
||||
|
||||
不要配置 `0.0.0.0/0`,因为登录限流、安装令牌限流和审计日志都依赖客户端地址。BackupX 直接暴露且不应信任任何转发头时可设置空列表。
|
||||
|
||||
修改安全密钥或数据库路径前,应同时备份完整数据目录和配置文件。经过验证的快照与回滚流程见[升级与恢复](../operations/upgrade-recovery)。
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Docker 部署
|
||||
description: 带健康检查和持久化数据的加固单进程 Docker 部署。
|
||||
---
|
||||
|
||||
# Docker 部署
|
||||
|
||||
官方镜像 [`awuqing/backupx`](https://hub.docker.com/r/awuqing/backupx) 支持 `linux/amd64` 和 `linux/arm64`。
|
||||
|
||||
## Compose 文件
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
backupx:
|
||||
image: ${BACKUPX_IMAGE:-awuqing/backupx:latest}
|
||||
container_name: backupx
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
stop_grace_period: 30s
|
||||
ports:
|
||||
- "${BACKUPX_BIND_ADDRESS:-0.0.0.0}:${BACKUPX_PORT:-8340}:8340"
|
||||
volumes:
|
||||
- backupx-data:/app/data
|
||||
# - /var/www:/mnt/www:ro
|
||||
# - /etc/nginx:/mnt/nginx-conf:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- SETGID
|
||||
- SETUID
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
# BACKUPX_SERVER_EXTERNAL_URL: https://backup.example.com
|
||||
BACKUPX_LOG_LEVEL: info
|
||||
BACKUPX_BACKUP_MAX_CONCURRENT: "2"
|
||||
healthcheck:
|
||||
test: ["CMD", "su-exec", "backupx:backupx", "wget", "-q", "-T", "3", "-O", "/dev/null", "http://127.0.0.1:8340/ready"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
backupx-data:
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
入口脚本仅以 root 完成旧镜像数据的所有权迁移,随后只运行一个非 root `backupx` 进程;Compose 仅保留初始化所需的所有权与 UID/GID 切换能力。后端同时提供 API 与前端静态文件,默认不挂载 Docker Socket,也不打包 Docker CLI。生产环境应把 `BACKUPX_IMAGE` 固定到明确 Release 标签。
|
||||
|
||||
## 备份宿主机目录
|
||||
|
||||
按需挂载源目录,并在任务中使用容器内路径。容器中的 `backupx` 用户必须拥有读取权限;恢复目标应使用单独且范围受限的可写挂载。特权路径优先通过远程 Agent 处理;确实需要 Master 以 root 读取时,应显式设置 `user: "0:0"` 并审核每一个挂载。
|
||||
|
||||
## 多节点集群
|
||||
|
||||
设置所有 Agent 可达的稳定地址:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
BACKUPX_SERVER_EXTERNAL_URL: https://backup.example.com
|
||||
```
|
||||
|
||||
跨不可信网络必须使用 HTTPS。代理、私有 CA 和 SSH 堡垒机场景见 [多节点集群](../features/multi-node)。
|
||||
|
||||
外部反向代理运行在其他容器时,只把准确的 Docker 网桥网段加入 `BACKUPX_SERVER_TRUSTED_PROXIES`,例如 `172.18.0.0/16`,不要信任所有地址。
|
||||
|
||||
## 环境变量覆盖
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
BACKUPX_LOG_LEVEL: debug
|
||||
BACKUPX_BACKUP_MAX_CONCURRENT: "4"
|
||||
BACKUPX_BACKUP_TEMP_DIR: /tmp/backupx
|
||||
```
|
||||
|
||||
镜像内部端口固定为 `8340`,只通过 `BACKUPX_PORT` 修改宿主机发布端口。
|
||||
|
||||
## 升级前提
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
等待状态变为 `healthy` 后再切换流量或移除旧部署。升级前应停止 Master 后做文件级复制,或对整个 `backupx-data` 卷创建原子快照。同一个数据卷必须只运行一个活动 Master;SQLite 不支持多个 Master 容器共享 `/app/data`。
|
||||
|
||||
生产环境应使用发布标签或镜像摘要而不是 `latest`,并保留与旧版本匹配的升级前数据快照。完整的升级、回滚和灾难恢复流程见[升级与恢复](../operations/upgrade-recovery)。
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Nginx 反向代理
|
||||
description: 通过 Nginx 发布 BackupX(HTTPS + SSE 友好的缓冲配置)。
|
||||
---
|
||||
|
||||
# Nginx 反向代理
|
||||
|
||||
生产环境可用的 Nginx 站点模板:
|
||||
|
||||
```nginx title="/etc/nginx/sites-available/backupx"
|
||||
server {
|
||||
listen 80;
|
||||
server_name backup.example.com;
|
||||
|
||||
# 静态 UI(由 /opt/backupx/web 提供)
|
||||
location / {
|
||||
root /opt/backupx/web;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API 反向代理
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8340;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# 大文件上传(用于恢复流程)
|
||||
client_max_body_size 0;
|
||||
proxy_request_buffering off;
|
||||
|
||||
# 实时日志使用 SSE,必须关闭缓冲
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# 兼容旧版本生成的安装地址;新版本通过上面的 /api/install/ 访问。
|
||||
location /install/ {
|
||||
proxy_pass http://127.0.0.1:8340/install/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# 避免探针和指标请求落入 SPA fallback。
|
||||
location = /health { proxy_pass http://127.0.0.1:8340/health; }
|
||||
location = /ready { proxy_pass http://127.0.0.1:8340/ready; }
|
||||
location = /metrics { proxy_pass http://127.0.0.1:8340/metrics; }
|
||||
}
|
||||
```
|
||||
|
||||
集群使用 Master 中转备份时必须保留 `proxy_request_buffering off`。否则 Nginx 会先把 Agent 上传的完整备份写入代理临时目录,再交给 BackupX,既失去流式传输优势,也可能占满代理磁盘。
|
||||
|
||||
如果 Nginx 运行在另一台主机或另一个容器,只把该代理的 IP 或网段加入 `server.trusted_proxies`,不要配置 `0.0.0.0/0`。登录限流、安装令牌限流和审计日志都依赖可信的客户端地址。
|
||||
|
||||
`/health`、`/ready` 和 `/metrics` 不需要 BackupX 认证。应只放行探针与 Prometheus 来源网段,或把这些 location 放在内部监听端口,避免直接暴露到互联网。
|
||||
|
||||
## certbot 配置 HTTPS
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d backup.example.com
|
||||
```
|
||||
|
||||
certbot 会自动改写配置监听 443 并设置续期。
|
||||
|
||||
:::caution Agent 需要稳定的 URL
|
||||
如果 Master 部署在 HTTPS 后面,远程 Agent 的 `--master` 必须使用最终 HTTPS 地址,Agent 不会跟随重定向。私有 CA 应预先下发 PEM 证书并使用 `--ca-cert /path/to/ca.pem`;`--insecure-tls` 只用于短期测试。
|
||||
:::
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 贡献指南
|
||||
description: 如何反馈问题、提出改进、提交 PR。
|
||||
---
|
||||
|
||||
# 贡献指南
|
||||
|
||||
BackupX 使用 Apache License 2.0 开源,欢迎提交 Issue 与 Pull Request。
|
||||
|
||||
## 报告 Bug
|
||||
|
||||
在 [github.com/Awuqing/BackupX/issues](https://github.com/Awuqing/BackupX/issues) 提交 Issue,请附上:
|
||||
|
||||
- BackupX 版本(`backupx --version`)
|
||||
- 部署方式(Docker / 裸机 / 源码)
|
||||
- 相关的备份任务类型和存储后端
|
||||
- 复现步骤
|
||||
- 问题发生时段的 stdout / `backupx.log` 片段
|
||||
|
||||
## 提议改动
|
||||
|
||||
对于重要功能或重构,建议先开 Issue 对齐方案,避免 PR 大改动后被 Review 回退。
|
||||
|
||||
## 提交 PR
|
||||
|
||||
1. Fork 仓库,创建主题分支(如 `fix/windows-path-escape`)
|
||||
2. 执行 `make test` 确认本地全通过
|
||||
3. 保持每个 PR 只做一件事
|
||||
4. Commit message 使用中文,格式 `类型: 简要描述`:
|
||||
- `功能: 新增审计日志模块`
|
||||
- `修复: 目录浏览器无法进入子目录`
|
||||
- `重构: 简化存储目标解密逻辑`
|
||||
- 类型:`功能` / `修复` / `重构` / `文档` / `构建` / `测试`
|
||||
5. PR 标题和正文同样使用中文,描述"为什么"和"怎么做",而非仅仅"做了什么"
|
||||
|
||||
## 代码规范
|
||||
|
||||
- **Go** — 所有错误必须处理(禁止 `_ = err`),日志使用现有 `zap`,禁止生产路径中出现 `fmt.Println`
|
||||
- **TypeScript** — 严格模式,禁止隐式 any,遵循现有 ESLint/Prettier 配置
|
||||
- **Commit 粒度** — 每个 commit 一件事,不要把顺手的小修改和功能代码混在一起
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 开发环境
|
||||
description: 搭建 BackupX 本地开发环境 — 后端、前端、测试。
|
||||
---
|
||||
|
||||
# 开发环境
|
||||
|
||||
**环境要求:** Go ≥ 1.25,Node.js ≥ 20,npm。
|
||||
|
||||
## 克隆与依赖
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
cd web && npm install && cd ..
|
||||
```
|
||||
|
||||
## 开发服务
|
||||
|
||||
开两个终端分别跑后端和前端:
|
||||
|
||||
```bash
|
||||
# 终端 1:后端,监听 :8340
|
||||
make dev-server
|
||||
|
||||
# 终端 2:Vite HMR,监听 :5173
|
||||
make dev-web
|
||||
```
|
||||
|
||||
Vite 配置了 `/api` 代理到 `http://127.0.0.1:8340`,浏览器直接访问 `http://localhost:5173`。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
make test # 运行 Go + Web 全部测试
|
||||
make test-server # 仅 Go
|
||||
make test-web # 仅 Vitest
|
||||
```
|
||||
|
||||
## 生产构建
|
||||
|
||||
```bash
|
||||
make build # server/bin/backupx + web/dist
|
||||
make docker # Docker 镜像
|
||||
make docker-cn # 国内镜像加速构建
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| **后端** | Go · Gin · GORM · SQLite · robfig/cron · rclone |
|
||||
| **前端** | React 18 · TypeScript · ArcoDesign · Vite · Zustand · ECharts |
|
||||
| **存储** | rclone(70+ 后端)· AWS SDK v2 · Google Drive API v3 |
|
||||
| **安全** | JWT · bcrypt · AES-256-GCM |
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
BackupX/
|
||||
├── server/ # Go 后端
|
||||
│ ├── cmd/backupx/ # 入口 + 子命令(agent / backint / reset-password)
|
||||
│ ├── internal/
|
||||
│ │ ├── agent/ # Agent CLI 逻辑
|
||||
│ │ ├── app/ # 装配(repo → service → handler)
|
||||
│ │ ├── backup/ # 备份 runner(file / mysql / postgres / sqlite / saphana)
|
||||
│ │ ├── backint/ # SAP HANA Backint 协议
|
||||
│ │ ├── http/ # HTTP handler + router
|
||||
│ │ ├── model/ # GORM 模型
|
||||
│ │ ├── repository/ # 数据访问
|
||||
│ │ ├── service/ # 业务逻辑
|
||||
│ │ └── storage/ # 存储 provider(rclone + 直接 SDK)
|
||||
│ └── pkg/ # 通用工具
|
||||
├── web/ # React 前端(Vite)
|
||||
│ └── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ ├── services/
|
||||
│ └── types/
|
||||
├── docs-site/ # 文档站(Docusaurus)
|
||||
├── deploy/ # install.sh / systemd unit / nginx config
|
||||
└── Makefile
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 备份类型
|
||||
description: 文件、MySQL、PostgreSQL、SQLite 和 SAP HANA — 各自的能力与配置说明。
|
||||
---
|
||||
|
||||
# 备份类型
|
||||
|
||||
BackupX 支持五种内置备份类型,类型决定了用哪个 runner 执行。
|
||||
|
||||
当任务路由到远程 Agent 时,源路径和外部工具都会在该 Agent 主机上解析。多存储目标上传仍会逐目标记录结果;只要至少一个目标上传成功,备份记录即为成功,详情中的目标结果表会展示部分失败。
|
||||
|
||||
## 文件 / 目录
|
||||
|
||||
文件任务提供三种备份模式:
|
||||
|
||||
- **全量归档** — 每次生成一份可独立使用的 tar 产物
|
||||
- **差异归档** — 只保存相对当前全量基线的变化,并按周期刷新全量基线
|
||||
- **CDC 去重仓库** — 按稳定的 512 KiB / 1 MiB / 4 MiB 内容边界切块,将新块合并到不可变的 32 MiB pack,每次运行只新增一份小型快照清单
|
||||
|
||||
CDC 仓库会在不同文件、不同快照之间复用相同内容。完整恢复、选择性恢复、完整性校验、下载为 tar、保留策略和垃圾回收都通过仓库索引定位分块。压缩与加密按块执行;启用加密时使用带密钥的块 ID,不暴露明文哈希。
|
||||
|
||||
当前仓库索引采用单写者模型,因此 CDC 模式仅在 Master 本机执行。如需保存多份完整仓库,请在任务中直接多选主存储目标。对象级副本复制会被禁用,因为只有快照清单、没有共享 pack 与索引并不是完整备份。
|
||||
|
||||
文件任务的通用选项:
|
||||
|
||||
- **源路径** 支持多个(UI 中每行一个)
|
||||
- **排除模式** 支持 gitignore 风格的通配符
|
||||
- 可选跟随符号链接、保留权限
|
||||
- 全量与差异模式输出 `.tar`、`.tar.gz` 或 `.tar.zst`
|
||||
|
||||
## MySQL
|
||||
|
||||
底层使用 `mysqldump`,需要执行任务的主机(Master 或 Agent)的 `$PATH` 中有 `mysqldump`。
|
||||
|
||||
- **主机 / 端口 / 用户 / 密码 / 数据库** — 支持多库(英文逗号分隔)
|
||||
- 输出:`.sql` 或 `.sql.gz`
|
||||
- 默认参数:`--single-transaction --routines --triggers --events`
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
底层使用 `pg_dump`,连接字段与 MySQL 一致加数据库名。
|
||||
|
||||
## SQLite
|
||||
|
||||
直接复制数据库文件(使用一致性快照),无需外部工具。
|
||||
|
||||
## SAP HANA
|
||||
|
||||
支持两种模式 — 详见 [SAP HANA](./sap-hana) 专题页。
|
||||
|
||||
## 删除行为
|
||||
|
||||
删除备份任务时,BackupX 会从所有存储目标上移除备份产物,但保留备份记录以供审计。删除任务同时拆除其 Cron 定时调度。
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: 多节点集群
|
||||
description: 通过直连 HTTPS、正向代理或 SSH 堡垒机部署 BackupX Agent。
|
||||
---
|
||||
|
||||
# 多节点集群
|
||||
|
||||
BackupX 使用一个单活 Master 作为控制面,在每台源服务器运行 Agent。所有连接都由 Agent 主动发起:每 15 秒上报心跳,每 5 秒轮询命令,不需要为 Agent 开放入站端口。
|
||||
|
||||
## 架构与边界
|
||||
|
||||
```text
|
||||
[Web 控制台] ────────> [单活 Master + SQLite]
|
||||
^
|
||||
| Agent 主动 HTTP(S) 轮询
|
||||
+---------+---------+
|
||||
| | |
|
||||
[Agent B] [Agent C] [Agent D]
|
||||
| | |
|
||||
+----> 存储目标
|
||||
```
|
||||
|
||||
- 每个节点有独立 Agent Token,Agent 不持有 Master 的 JWT 密钥或配置加密密钥。
|
||||
- Master 超过 45 秒未收到心跳即把节点标记为离线。
|
||||
- Master 持久化命令,Agent 领取后在本机执行。
|
||||
- 网络存储通常由 Agent 直传;Master 本地存储可显式启用认证流式中转。
|
||||
|
||||
:::warning Master 只能单活
|
||||
内置 SQLite 不是共享多写数据库。同一个数据目录只能运行一个 Master。控制面高可用应采用主备主机、持久卷快照以及稳定 DNS 或虚拟 IP,故障时确保旧 Master 停止后再启动备用实例。不要让多个 Master 副本同时挂载 `/app/data` 或同一个 `backupx.db`。
|
||||
:::
|
||||
|
||||
BackupX 会设置 5 秒 SQLite busy timeout,并为命令队列建立查询索引,降低 Agent 并发轮询及任务更新时的锁竞争。数据库应位于本地文件系统或块存储。采用文件复制备份控制面时,先停止 Master 再复制整个数据目录;运行期间不要只复制 `backupx.db`。
|
||||
|
||||
## 选择网络路径
|
||||
|
||||
| 场景 | Agent Master 地址 | Agent 代理 URL | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 可路由内网或公网服务 | `https://backup.example.com` | 留空 | 推荐,只需放行出站 TCP 443 |
|
||||
| 企业正向代理 | `https://backup.example.com` | `http://proxy.internal:3128` | 支持 HTTP(S) 与 SOCKS5(H) |
|
||||
| 通过堡垒机建立 SSH 动态转发 | `https://backup.internal` | `socks5h://127.0.0.1:1080` | 保留 TLS 主机名,并通过隧道解析内网 DNS |
|
||||
| SSH 固定本地转发 | `http://127.0.0.1:18340` | 留空 | HTTP 链路位于 SSH 内,只能绑定回环地址 |
|
||||
|
||||
私有 PKI 场景请填写目标节点上预置的 PEM CA 证书绝对路径。生产环境不要使用 `--insecure-tls`。
|
||||
|
||||
未配置显式代理时,Agent 到 Master 的 HTTP 流量会遵循 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY`。systemd 服务通常不会继承交互式 Shell 环境,因此 systemd 部署应在安装向导或 Agent YAML 中明确配置代理。
|
||||
|
||||
## 准备 Master
|
||||
|
||||
生成命令前先设置稳定地址:
|
||||
|
||||
```yaml title="/etc/backupx/config.yaml"
|
||||
server:
|
||||
external_url: "https://backup.example.com"
|
||||
trusted_proxies:
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
# 代理不在本机时,只加入准确的代理 IP 或网段。
|
||||
# - "172.18.0.0/16"
|
||||
```
|
||||
|
||||
`external_url` 是默认安装入口和 Agent 运行地址。受限节点可以让目标机侧生成的安装 URL 与 Agent 运行地址同时改用隧道或内网地址,浏览器仍继续访问公网地址。
|
||||
|
||||
跨不可信网络必须使用 HTTPS。Master 中转上传还要求反向代理关闭请求缓冲并允许大请求体,详见 [Nginx 反向代理](../deployment/nginx)。
|
||||
|
||||
Agent 必须直接配置最终 API 地址,不能依赖 HTTP 跳转到 HTTPS。Agent 会主动拒绝重定向,避免认证 Token 被转发到非预期主机。
|
||||
|
||||
## 部署 Agent
|
||||
|
||||
打开 **节点管理 → 添加节点**:
|
||||
|
||||
1. 输入单个节点名,或在批量模式输入最多 50 个名称。
|
||||
2. 选择 systemd、Docker 或前台模式,以及架构、Agent Release、命令有效期和下载源。
|
||||
3. 选择 **直连** 或 **代理或堡垒机**。受限网络可填写节点专用 Master 地址、代理 URL 或私有 CA 路径。
|
||||
4. 把生成的命令复制到目标机,以 root 权限执行。
|
||||
|
||||
备份和恢复宿主机文件时推荐 systemd,因为 Agent 需要访问任意本地路径。Docker Agent 只能看到显式挂载的目录;分配文件任务前,应使用只读备份源 volume,并为恢复目标单独配置范围受限的可写挂载。
|
||||
|
||||
主命令通过一次性入口下载安装器,并在执行前校验脚本标记。向导会把所选 Agent 地址、显式代理和私有 CA 同时绑定到下载命令与安装后的 Agent 配置。如果目标网络仍无法访问安装入口,使用页面单独展示的嵌入式备用命令。嵌入式命令包含长期节点 Token,必须按密钥管理。
|
||||
|
||||
安装器会:
|
||||
|
||||
1. 检测 `linux/amd64` 或 `linux/arm64`。
|
||||
2. 配置显式代理时始终通过该代理下载 Release;否则使用主机的正常直连或环境代理路径,并在该版本提供 SHA-256 旁车文件时进行校验。
|
||||
3. 以 `0600` 权限写入 `/etc/backupx-agent/config.yaml` 和 `/etc/backupx-agent/agent.token`。
|
||||
4. 不把 Token 写入 systemd unit 或 Docker 环境元数据。
|
||||
5. 启动 Agent,并在 30 秒内轮询 `/api/v1/agent/self`。
|
||||
6. 节点未上线时返回非零状态,并输出 systemd 或 Docker 排查命令。
|
||||
|
||||
旧版本如果没有校验文件,会显示兼容性警告后继续安装;新版本应始终发布并校验该文件。
|
||||
|
||||
### systemd 安装结果
|
||||
|
||||
```yaml title="/etc/backupx-agent/config.yaml"
|
||||
master: "https://backup.example.com"
|
||||
tokenFile: "/etc/backupx-agent/agent.token"
|
||||
heartbeatInterval: "15s"
|
||||
pollInterval: "5s"
|
||||
tempDir: "/var/lib/backupx-agent/tmp"
|
||||
proxyUrl: ""
|
||||
caCertFile: ""
|
||||
```
|
||||
|
||||
```ini title="/etc/systemd/system/backupx-agent.service"
|
||||
[Unit]
|
||||
Description=BackupX Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=10
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/backupx-agent/backupx agent --config /etc/backupx-agent/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=30s
|
||||
UMask=0077
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Agent 以 root 运行,因为文件备份和恢复路径可能属于任意系统用户。应严格限制谁能创建任务,以及谁能修改 root 所有的 Agent 配置。
|
||||
|
||||
## SSH 堡垒机示例
|
||||
|
||||
内网 Master 使用 HTTPS 时优先采用 SOCKS 隧道,这样 Master 主机名与证书校验保持不变。
|
||||
|
||||
先创建专用 SSH 账户,预置私钥和已经人工核对指纹的 `known_hosts`,再创建:
|
||||
|
||||
```sshconfig title="/etc/backupx-agent/ssh_config"
|
||||
Host backupx-bastion
|
||||
HostName bastion.example.com
|
||||
User backupx-tunnel
|
||||
IdentityFile /etc/backupx-agent/tunnel_ed25519
|
||||
IdentitiesOnly yes
|
||||
BatchMode yes
|
||||
UserKnownHostsFile /etc/backupx-agent/known_hosts
|
||||
StrictHostKeyChecking yes
|
||||
DynamicForward 127.0.0.1:1080
|
||||
ExitOnForwardFailure yes
|
||||
ServerAliveInterval 30
|
||||
ServerAliveCountMax 3
|
||||
```
|
||||
|
||||
```ini title="/etc/systemd/system/backupx-agent-tunnel.service"
|
||||
[Unit]
|
||||
Description=BackupX Agent SSH tunnel
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=backupx-agent.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/ssh -NT -F /etc/backupx-agent/ssh_config backupx-bastion
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
再添加依赖覆写,让隧道不可用时 Agent 关闭失败而不是绕过堡垒机:
|
||||
|
||||
```ini title="/etc/systemd/system/backupx-agent.service.d/tunnel.conf"
|
||||
[Unit]
|
||||
Requires=backupx-agent-tunnel.service
|
||||
After=backupx-agent-tunnel.service
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now backupx-agent-tunnel backupx-agent
|
||||
```
|
||||
|
||||
在安装向导中保留内网 HTTPS Master 地址,把代理填写为 `socks5h://127.0.0.1:1080`。启用服务前必须通过独立渠道核对堡垒机 Host Key。
|
||||
|
||||
## 集中存储数据路径
|
||||
|
||||
| 目标 | 数据路径 |
|
||||
| --- | --- |
|
||||
| S3、WebDAV、FTP、云盘或其他网络后端 | Agent 直接流式上传到目标 |
|
||||
| 启用 **远程备份经 Master 中转** 的 `local_disk` | Agent 通过认证 Master API 流式上传,Master 写入本地挂载 |
|
||||
|
||||
中转不会在 Master 上额外创建一份完整临时副本,恢复时走反向流式通道。Nginx 必须关闭请求缓冲,才能保持该特性。
|
||||
|
||||
## 运维
|
||||
|
||||
```bash
|
||||
sudo systemctl status backupx-agent
|
||||
sudo journalctl -u backupx-agent -n 100 --no-pager
|
||||
sudo /opt/backupx-agent/backupx agent --config /etc/backupx-agent/config.yaml
|
||||
```
|
||||
|
||||
从节点操作菜单轮换 Token 后,在 24 小时重叠窗口内更新 `/etc/backupx-agent/agent.token` 并重启服务。
|
||||
|
||||
建议监控:
|
||||
|
||||
- `backupx_agent_command_queue_depth`
|
||||
- `backupx_agent_command_running`
|
||||
- `backupx_agent_command_timeout_total`
|
||||
- `backupx_node_online`
|
||||
|
||||
## CLI 参考
|
||||
|
||||
```text
|
||||
backupx agent --help
|
||||
-master string Master 地址
|
||||
-token string Agent Token
|
||||
-token-file string 从文件读取 Agent Token
|
||||
-config string YAML 配置文件路径
|
||||
-temp-dir string 本地临时目录
|
||||
-proxy-url string HTTP(S) 或 SOCKS5(H) 代理
|
||||
-ca-cert string 用于校验 Master 的 PEM CA 证书
|
||||
-insecure-tls 跳过 TLS 校验(仅测试)
|
||||
```
|
||||
|
||||
环境变量:`BACKUPX_AGENT_MASTER`、`BACKUPX_AGENT_TOKEN`、`BACKUPX_AGENT_TOKEN_FILE`、`BACKUPX_AGENT_HEARTBEAT`、`BACKUPX_AGENT_POLL`、`BACKUPX_AGENT_TEMP_DIR`、`BACKUPX_AGENT_PROXY_URL`、`BACKUPX_AGENT_CA_CERT_FILE`、`BACKUPX_AGENT_INSECURE_TLS`。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- Master 使用内置 SQLite,只支持单活。
|
||||
- 加密备份仅支持 Master 本机执行,因为 Agent 不持有 Master 加密密钥。
|
||||
- 远程目录浏览是同步队列 RPC,默认超时 15 秒。
|
||||
- Agent 领取后长期不更新的命令会由 Master 超时监控处理。
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
title: 通知
|
||||
description: 备份成功或失败时通过邮件、Webhook、Telegram 推送通知。
|
||||
---
|
||||
|
||||
# 通知
|
||||
|
||||
BackupX 支持三种通知渠道,可为每个渠道单独配置成功/失败事件是否推送。
|
||||
|
||||
## 邮件(SMTP)
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| SMTP 主机 / 端口 | 如 `smtp.gmail.com:587` |
|
||||
| 用户名 / 密码 | 建议使用专用应用密码 |
|
||||
| 发件人地址 | 邮件 `From:` 头 |
|
||||
| 收件人列表 | 英文逗号分隔 |
|
||||
| 使用 TLS / StartTLS | 按 SMTP 提供方要求选择 |
|
||||
|
||||
## Webhook
|
||||
|
||||
向任意 URL 发送 JSON POST,请求体结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "backup_result",
|
||||
"task": {"id": 1, "name": "web-files", "type": "file"},
|
||||
"record": {"id": 42, "status": "success", "fileSize": 1048576, "durationSeconds": 12},
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
适合自定义场景:Slack incoming webhook、PagerDuty、自建 API 等。
|
||||
|
||||
## Telegram
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| Bot Token | 在 [@BotFather](https://t.me/BotFather) 创建 |
|
||||
| Chat ID | 数字型,可通过 `/start` 后调 Bot 的 `getUpdates` 获取 |
|
||||
|
||||
## 事件规则
|
||||
|
||||
每个通知配置可以指定触发范围:
|
||||
|
||||
- **仅成功** — 正常运行时静默
|
||||
- **仅失败** — 适合高噪敏感通道
|
||||
- **全部** — 初始化配置时用于验证链路
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: SAP HANA 支持
|
||||
description: 两种 SAP HANA 备份模式 — 控制台托管的 hdbsql Runner 和原生 Backint 协议代理。
|
||||
---
|
||||
|
||||
# SAP HANA 支持
|
||||
|
||||
BackupX 提供两种 SAP HANA 备份模式,按实际运维流程选择。
|
||||
|
||||
## 模式一:hdbsql Runner(控制台托管)
|
||||
|
||||
通过 Web 控制台创建 SAP HANA 备份任务,后端调用 `hdbsql` 执行备份。适合希望 BackupX 来管理调度的场景。
|
||||
|
||||
**源配置支持:**
|
||||
|
||||
| 字段 | 可选值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 备份类型 | `data` / `log` | 数据备份或日志备份 |
|
||||
| 备份级别 | `full` / `incremental` / `differential` | 日志备份时自动禁用 |
|
||||
| 并行通道数 | `1 ~ 32` | 多路径 SQL(`BACKUP DATA USING FILE ('c1', 'c2', ...)`) |
|
||||
| 失败重试次数 | `1 ~ 10` | 指数退避(`5s × 尝试次数²`) |
|
||||
| 实例编号 | 可选 | 从端口推断或手动指定 |
|
||||
|
||||
## 模式二:Backint 协议代理(HANA 原生接口)
|
||||
|
||||
BackupX 内置 Backint Agent,SAP HANA 通过原生 `BACKUP DATA USING BACKINT` 语法调用,数据自动路由到任意 BackupX 存储目标(S3 / OSS / COS / WebDAV / 70+ 后端)。
|
||||
|
||||
### 1. 参数文件
|
||||
|
||||
```ini title="/opt/backupx/backint_params.ini"
|
||||
#STORAGE_TYPE = s3
|
||||
#STORAGE_CONFIG_JSON = /opt/backupx/storage.json
|
||||
#PARALLEL_FACTOR = 4
|
||||
#COMPRESS = true
|
||||
#KEY_PREFIX = hana-backup
|
||||
#CATALOG_DB = /opt/backupx/backint_catalog.db
|
||||
#LOG_FILE = /var/log/backupx/backint.log
|
||||
```
|
||||
|
||||
### 2. 存储配置(与存储目标 schema 相同)
|
||||
|
||||
```json title="/opt/backupx/storage.json"
|
||||
{
|
||||
"endpoint": "https://s3.amazonaws.com",
|
||||
"region": "us-east-1",
|
||||
"bucket": "hana-prod",
|
||||
"accessKeyId": "AKIA...",
|
||||
"secretAccessKey": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建 hdbbackint 软链接
|
||||
|
||||
```bash
|
||||
ln -s /opt/backupx/backupx /usr/sap/<SID>/SYS/global/hdb/opt/hdbbackint
|
||||
```
|
||||
|
||||
### 4. 在 HANA `global.ini` 中启用
|
||||
|
||||
```ini
|
||||
[backup]
|
||||
data_backup_using_backint = true
|
||||
catalog_backup_using_backint = true
|
||||
log_backup_using_backint = true
|
||||
data_backup_parameter_file = /opt/backupx/backint_params.ini
|
||||
log_backup_parameter_file = /opt/backupx/backint_params.ini
|
||||
```
|
||||
|
||||
### 5. CLI 手动调用(用于排查)
|
||||
|
||||
```bash
|
||||
backupx backint -f backup -i input.txt -o output.txt -p backint_params.ini
|
||||
backupx backint -f restore -i input.txt -o output.txt -p backint_params.ini
|
||||
backupx backint -f inquire -i input.txt -o output.txt -p backint_params.ini
|
||||
backupx backint -f delete -i input.txt -o output.txt -p backint_params.ini
|
||||
```
|
||||
|
||||
Backint Agent 使用本地 SQLite 维护 `EBID ↔ 对象键` 目录,所有操作遵循 SAP HANA Backint 协议(`#PIPE` / `#SAVED` / `#RESTORED` / `#BACKUP` / `#NOTFOUND` / `#DELETED` / `#ERROR`)。
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 存储后端
|
||||
description: 70+ 存储后端 — 内置云服务商 + 任意 rclone 后端。
|
||||
---
|
||||
|
||||
# 存储后端
|
||||
|
||||
BackupX 的目标是接入任何你想放置备份文件的地方。
|
||||
|
||||
## 内置后端
|
||||
|
||||
| 类型 | 必填字段 |
|
||||
|------|---------|
|
||||
| **阿里云 OSS** | Region + AccessKey ID/Secret + Bucket(endpoint 自动组装) |
|
||||
| **腾讯云 COS** | Region + SecretId/SecretKey + Bucket(格式 `name-appid`) |
|
||||
| **七牛云 Kodo** | Region + AccessKey/SecretKey + Bucket |
|
||||
| **S3 兼容** | Endpoint + AccessKey + Bucket |
|
||||
| **Google Drive** | Client ID/Secret + OAuth 授权 |
|
||||
| **WebDAV** | 地址 + 用户名/密码 |
|
||||
| **FTP / FTPS** | 主机 + 端口 + 用户名/密码 |
|
||||
| **本地磁盘** | 目标目录(绝对路径)+ 可选的远程 Agent 经 Master 中转 |
|
||||
|
||||
新建本地磁盘目标默认开启 **远程备份经 Master 中转**。开启时,配置目录属于 Master,挂载到 Master 的存储服务器可集中接收多台源 Agent 的备份;如果该路径本就属于各 Agent,请关闭此选项。升级前已有目标保持原来的 Agent 本机落盘行为,只有显式开启后才会切换。
|
||||
|
||||
## Rclone 后端
|
||||
|
||||
每一种 [rclone 后端](https://rclone.org/overview/) 都作为一等公民暴露 — SFTP、Azure Blob、Dropbox、OneDrive、Backblaze B2、Wasabi、pCloud、HDFS 等。
|
||||
|
||||
- 表单字段分为 **必填** 和 **高级**(高级默认折叠)
|
||||
- 校验与连接测试复用 rclone 自带的探测
|
||||
|
||||
## 一个任务多个目标
|
||||
|
||||
一个备份任务可以并行上传到多个存储目标。每个目标获得相同的产物,每目标的状态会单独记录:
|
||||
|
||||
- 成功:storage_path + 文件大小
|
||||
- 失败:错误信息
|
||||
|
||||
如果任一目标在重试后仍失败,整条记录的状态为 `failed`,但已成功的目标产物会被保留(不回滚)。
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 安装
|
||||
description: 通过 Docker、预编译包或源码安装 BackupX。
|
||||
---
|
||||
|
||||
# 安装
|
||||
|
||||
BackupX 以单个静态二进制发布。三种安装方式,按实际环境选一种。
|
||||
|
||||
## Docker(推荐)
|
||||
|
||||
下载仓库中的正式加固 Compose 文件并启动:
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/Awuqing/BackupX/main/docker-compose.yml
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
该 Compose 配置启用 init 与优雅停止,持久化 `/app/data`,以非特权用户运行应用,删除不必要能力,并通过 `/ready` 检查健康。[`awuqing/backupx`](https://hub.docker.com/r/awuqing/backupx) 镜像支持 `linux/amd64` 和 `linux/arm64`。
|
||||
|
||||
生产环境应创建受保护的 `.env`,固定 Release 而不是依赖 `latest`:
|
||||
|
||||
```dotenv
|
||||
BACKUPX_IMAGE=awuqing/backupx:vX.Y.Z
|
||||
BACKUPX_BIND_ADDRESS=127.0.0.1
|
||||
TZ=Asia/Shanghai
|
||||
```
|
||||
|
||||
反向代理位于同一主机时使用回环绑定;需要直接访问时,应选择明确的监听接口并配置防火墙。宿主机备份源应只读挂载,或在源主机部署 Agent。完整配置见 [Docker 部署](../deployment/docker)。
|
||||
|
||||
## 预编译包(裸机)
|
||||
|
||||
从 [Releases 页面](https://github.com/Awuqing/BackupX/releases) 下载对应平台的压缩包,执行安装脚本:
|
||||
|
||||
```bash
|
||||
sha256sum -c backupx-v*-linux-amd64.tar.gz.sha256
|
||||
tar xzf backupx-v*-linux-amd64.tar.gz && cd backupx-*
|
||||
sudo ./install.sh # 创建系统用户、安装到 /opt/backupx、配置 systemd
|
||||
```
|
||||
|
||||
安装脚本会自动:
|
||||
|
||||
1. 创建 `backupx` 系统用户
|
||||
2. 安装二进制到 `/opt/backupx/bin/backupx`,并把 Web 控制台安装到 `/opt/backupx/web`
|
||||
3. 生成 `/etc/backupx/config.yaml`(含安全默认值)
|
||||
4. 注册并启用 `backupx.service` systemd 单元
|
||||
5. 默认不修改 Nginx;只有显式设置 `INSTALL_NGINX=1` 时才安装模板
|
||||
6. 等待 `/api/auth/setup/status` 就绪;启动失败时输出 systemd 诊断并返回非零状态
|
||||
|
||||
## 从源码构建
|
||||
|
||||
依赖:Go ≥ 1.25,Node.js ≥ 20。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
|
||||
make build
|
||||
sudo ./deploy/install.sh
|
||||
```
|
||||
|
||||
`make build` 完成后,二进制位于 `server/bin/backupx`,构建好的 Web UI 位于 `web/dist/`。
|
||||
安装脚本会直接使用这两个路径,不需要 Docker 运行时。如果已有配置修改了默认端口,可覆盖就绪检查地址,例如:`sudo HEALTH_URL=http://127.0.0.1:9000/api/auth/setup/status ./deploy/install.sh`。
|
||||
|
||||
自动安装兜底虚拟主机可能接管现有站点,因此 Nginx 模板改为显式启用。请先审核 `deploy/nginx.conf`,确认适合当前主机后再执行 `sudo INSTALL_NGINX=1 ./deploy/install.sh`。
|
||||
|
||||
## 验证安装
|
||||
|
||||
```bash
|
||||
/opt/backupx/bin/backupx --version
|
||||
curl -fsS http://127.0.0.1:8340/api/auth/setup/status
|
||||
```
|
||||
|
||||
打开浏览器访问 `http://your-server:8340`,可在右上角选择 **中文** 或 **English**。全新数据库会显示“系统初始化 / System setup”,在这里创建首个管理员用户名和密码。如果没有出现初始化表单,请先重试上面的状态接口,不要直接尝试登录。
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 快速开始
|
||||
description: 部署 BackupX、添加存储目标、创建第一个备份任务。
|
||||
---
|
||||
|
||||
# 快速开始
|
||||
|
||||
完成 [安装](./installation) 后,花五分钟跑通第一个备份。
|
||||
|
||||
## 1. 打开控制台
|
||||
|
||||
浏览器访问 `http://your-server:8340`。首次打开会引导创建管理员账户。
|
||||
|
||||
## 2. 添加存储目标
|
||||
|
||||
进入 **存储目标 → 添加**,选择类型并填写凭证:
|
||||
|
||||
| 类型 | 需要填写 |
|
||||
|------|---------|
|
||||
| 阿里云 OSS | Region + AccessKey ID/Secret + Bucket |
|
||||
| 腾讯云 COS | Region + SecretId/SecretKey + Bucket(格式 `name-appid`) |
|
||||
| 七牛云 Kodo | Region + AccessKey/SecretKey + Bucket |
|
||||
| S3 兼容 | Endpoint + AccessKey + Bucket |
|
||||
| Google Drive | Client ID/Secret → 点击「授权」完成 OAuth |
|
||||
| WebDAV | 服务器地址 + 用户名/密码 |
|
||||
| FTP | 主机 + 端口 + 用户名/密码 |
|
||||
| 本地磁盘 | 目标目录路径 |
|
||||
| SFTP / Azure / Dropbox / OneDrive 等 | 选择对应类型后填写必填项,高级配置默认折叠 |
|
||||
|
||||
:::tip
|
||||
国内云厂商只需填 Region 和 AccessKey,系统自动组装 Endpoint。Rclone 类型的配置项按"必填 / 可选"分层展示,高级选项默认折叠。
|
||||
:::
|
||||
|
||||
添加后点击 **测试连接** 确认配置正确。
|
||||
|
||||
## 3. 创建备份任务
|
||||
|
||||
进入 **备份任务 → 新建**,三步完成:
|
||||
|
||||
1. **基础信息** — 任务名称、备份类型、Cron 表达式(留空则仅手动执行)
|
||||
2. **源配置** — 文件备份选择源路径(支持多个),数据库备份填写连接信息
|
||||
3. **存储与策略** — 选择存储目标(支持多个)、压缩策略、保留天数、是否加密
|
||||
|
||||
对于路由到 Agent 的任务,加密必须关闭,因为 Agent 不会拿到 Master 的加密密钥。BackupX 会在创建/更新阶段拒绝开启加密的远程节点或节点池任务。
|
||||
|
||||
保存后可点击 **立即执行** 测试,**备份记录** 页面实时查看执行日志。
|
||||
|
||||
:::note
|
||||
删除备份任务时会自动清理远端存储上的备份文件,但保留备份记录以供审计追溯。
|
||||
:::
|
||||
|
||||
## 4. 配置通知(可选)
|
||||
|
||||
**通知配置** 页面支持邮件、Webhook、Telegram 三种方式,可分别配置成功/失败时是否推送。
|
||||
|
||||
## 继续阅读
|
||||
|
||||
- 了解 [备份类型](/docs/features/backup-types) 和 [存储后端](/docs/features/storage-backends)
|
||||
- 上线生产前阅读[安全加固](/docs/operations/security)、[监控与告警](/docs/operations/monitoring)和[升级与恢复](/docs/operations/upgrade-recovery)
|
||||
- 使用 SAP HANA?参考 [SAP HANA 支持](/docs/features/sap-hana)
|
||||
- 管理多台服务器?参考 [多节点集群](/docs/features/multi-node)
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
id: intro
|
||||
slug: /intro
|
||||
sidebar_position: 1
|
||||
title: 项目简介
|
||||
description: BackupX——自托管服务器备份管理平台概览。
|
||||
---
|
||||
|
||||
# BackupX
|
||||
|
||||
**BackupX** 是一款自托管的服务器备份管理平台:一个二进制,一条命令,管好所有服务器的所有备份。
|
||||
|
||||
- **单二进制 + 内嵌 SQLite** — 不依赖外部数据库或编排器
|
||||
- **文件、数据库、SAP HANA** — 统一管理,可视化调度
|
||||
- **70+ 存储后端** — 阿里云 OSS、腾讯云 COS、七牛、S3、Google Drive、WebDAV、FTP,以及通过 rclone 接入的 SFTP / Azure Blob / Dropbox / OneDrive 等数十种
|
||||
- **多节点集群** — Master-Agent 模式跨服务器管理备份,Agent 在本地执行并直接上传到存储
|
||||
- **默认安全** — JWT 认证、bcrypt、AES-256-GCM 加密配置、可选备份加密、完整审计日志
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
[Web 控制台] ─── JWT ──→ [Master (backupx)]
|
||||
│
|
||||
│ Agent 主动 HTTP 轮询(Token 认证)
|
||||
▼
|
||||
[Agent (backupx agent)]
|
||||
│
|
||||
▼
|
||||
[70+ 存储后端]
|
||||
```
|
||||
|
||||
路由到本机的任务在 Master 进程内直接执行;派到远程节点的任务通过命令队列下发,由 Agent 在本地执行。Agent 只发起出站 HTTP 连接 — 不需要任何反向连通性。
|
||||
|
||||
## 下一步
|
||||
|
||||
- **第一次使用 BackupX?** 先看 [快速开始](/docs/getting-started/quick-start)
|
||||
- **生产部署?** 参考 [部署指南](/docs/deployment/docker)
|
||||
- **规划升级或灾备?** 按[升级与恢复](/docs/operations/upgrade-recovery)执行
|
||||
- **生产运维?** 先阅读[安全加固](/docs/operations/security)与[监控和告警](/docs/operations/monitoring)
|
||||
- **SAP HANA 用户?** 支持 `hdbsql` Runner 和原生 Backint 两种模式 — 详见 [SAP HANA](/docs/features/sap-hana)
|
||||
- **管理多台服务器?** 参考 [多节点集群](/docs/features/multi-node)
|
||||
- **程序化集成?** 参考 [API 参考](/docs/reference/api)
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: 监控与告警
|
||||
description: 健康探针、Prometheus 指标、初始告警规则和运维验证。
|
||||
---
|
||||
|
||||
# 监控与告警
|
||||
|
||||
BackupX 提供低开销健康端点和独立 Prometheus Registry。监控既要覆盖控制面,也要覆盖备份、恢复、验证和复制的实际结果。
|
||||
|
||||
## 探针
|
||||
|
||||
| 端点 | 含义 | 预期响应 |
|
||||
| --- | --- | --- |
|
||||
| `/health` | 存活:HTTP 进程可响应 | HTTP 200,`status: live` |
|
||||
| `/ready` | 就绪:进程可访问 SQLite | 正常为 HTTP 200 与 `status: ready`;数据库故障为 HTTP 503 |
|
||||
| `/api/health` | 带 API 前缀的存活别名 | 与 `/health` 相同 |
|
||||
| `/api/ready` | 带 API 前缀的就绪别名 | 与 `/ready` 相同 |
|
||||
| `/metrics` | Prometheus 指标 | 指标启用时为 HTTP 200 |
|
||||
|
||||
`/health` 用作 liveness,`/ready` 用作 readiness 或负载均衡流量判断。外部存储暂时不可用不应直接触发进程重启,应通过任务和存储目标告警处理。
|
||||
|
||||
~~~bash
|
||||
curl -fsS http://127.0.0.1:8340/health
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
curl -fsS http://127.0.0.1:8340/metrics | head
|
||||
~~~
|
||||
|
||||
这些端点不需要认证,只允许编排器和监控网段访问。
|
||||
|
||||
## Prometheus 抓取
|
||||
|
||||
~~~yaml
|
||||
scrape_configs:
|
||||
- job_name: backupx
|
||||
scheme: https
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets: [backup.example.com]
|
||||
~~~
|
||||
|
||||
Nginx 终止 TLS 时,应只放行 Prometheus 源地址访问 `/metrics`。内部采集器每 30 秒刷新存储、节点、命令队列和 SLA Gauge。
|
||||
|
||||
## BackupX 指标
|
||||
|
||||
| 指标 | 类型 | 标签 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `backupx_app_info` | gauge | `version` | 当前版本元数据 |
|
||||
| `backupx_task_run_total` | counter | `status`、`task_type` | 备份结果 |
|
||||
| `backupx_task_run_duration_seconds` | histogram | `task_type` | 备份耗时分布 |
|
||||
| `backupx_task_bytes_total` | counter | `task_type` | 备份产出字节数 |
|
||||
| `backupx_task_running` | gauge | 无 | 当前备份并发 |
|
||||
| `backupx_storage_used_bytes` | gauge | `target_name`、`target_type` | 按目标记录的使用量 |
|
||||
| `backupx_node_online` | gauge | `node_name`、`role` | 节点在线状态,1 或 0 |
|
||||
| `backupx_agent_command_queue_depth` | gauge | `node_name`、`role` | 待处理与已派发命令 |
|
||||
| `backupx_agent_command_running` | gauge | `node_name`、`role` | Agent 长任务数 |
|
||||
| `backupx_agent_command_timeout_total` | gauge | `node_name`、`role` | 超时命令数快照 |
|
||||
| `backupx_verify_run_total` | counter | `status` | 验证结果 |
|
||||
| `backupx_restore_run_total` | counter | `status` | 恢复结果 |
|
||||
| `backupx_replication_run_total` | counter | `status` | 复制结果 |
|
||||
| `backupx_sla_breach_tasks` | gauge | 无 | 超出已配置 RPO 的启用任务数 |
|
||||
|
||||
同一端点还注册了标准 Go Runtime 与进程指标。
|
||||
|
||||
## 初始告警规则
|
||||
|
||||
应根据各环境计划与 RPO 调整窗口和阈值:
|
||||
|
||||
~~~yaml
|
||||
groups:
|
||||
- name: backupx
|
||||
rules:
|
||||
- alert: BackupXTargetDown
|
||||
expr: up{job="backupx"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: BackupX metrics endpoint is unreachable
|
||||
|
||||
- alert: BackupXNotReady
|
||||
expr: probe_success{job="backupx-ready"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: BackupX readiness check is failing
|
||||
|
||||
- alert: BackupXBackupFailure
|
||||
expr: sum(increase(backupx_task_run_total{status="failed"}[15m])) > 0
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: A BackupX backup failed
|
||||
|
||||
- alert: BackupXSLABreach
|
||||
expr: backupx_sla_breach_tasks > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: One or more backup tasks are outside RPO
|
||||
|
||||
- alert: BackupXAgentOffline
|
||||
expr: backupx_node_online{role="agent"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: BackupX Agent is offline
|
||||
|
||||
- alert: BackupXAgentQueueBacklog
|
||||
expr: backupx_agent_command_queue_depth > 20
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: BackupX Agent command queue is growing
|
||||
~~~
|
||||
|
||||
`BackupXNotReady` 示例假定存在名为 `backupx-ready` 的 Blackbox 探针任务。未部署 Blackbox Exporter 时,应改用负载均衡或编排器的 readiness 信号。
|
||||
|
||||
## 运维仪表盘
|
||||
|
||||
建议同时展示:
|
||||
|
||||
- 按任务类型统计成功率与失败率。
|
||||
- P50、P95、最大执行时长及其与备份窗口的关系。
|
||||
- 产出字节数与预期数据变化率。
|
||||
- 当前任务数与 `backup.max_concurrent`。
|
||||
- 离线 Agent、队列深度、运行命令和超时数变化。
|
||||
- 存储增长、提供商剩余容量和保留策略清理。
|
||||
- SLA 违约数及关键任务最近成功备份时间。
|
||||
- 验证、恢复和复制成功率。
|
||||
|
||||
Prometheus 存储使用量来自 BackupX 记录元数据,不一定等同于提供商计费容量,应另行监控提供商配额和文件系统剩余空间。
|
||||
|
||||
## 部署后验证
|
||||
|
||||
安装、升级、代理变更或恢复后:
|
||||
|
||||
1. 分别从本机和公开代理检查存活与就绪。
|
||||
2. 确认 Prometheus 只看到一个活动 Master,并带有预期版本标签。
|
||||
3. 确认所有预期 Agent 的 `backupx_node_online == 1`。
|
||||
4. 执行小型备份并确认成功 Counter 增长。
|
||||
5. 执行验证或隔离恢复并确认对应 Counter 增长。
|
||||
6. 触发测试通知并验证告警投递链路。
|
||||
|
||||
探针或指标异常时继续参考[故障排查](./troubleshooting)。
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 安全加固
|
||||
description: 生产环境的网络暴露、角色、密钥、Agent、容器和公开端点控制。
|
||||
---
|
||||
|
||||
# 安全加固
|
||||
|
||||
BackupX 统一接触源文件、数据库凭据、存储凭据和恢复目标,应把 Master 作为安全敏感的控制面部署,而不是普通的公开 Web 应用。
|
||||
|
||||
## 推荐暴露模型
|
||||
|
||||
| 组件 | 入站访问 | 出站访问 |
|
||||
| --- | --- | --- |
|
||||
| Master | 管理员与 Agent 的 HTTPS;指标只对监控网段开放 | 存储提供商、通知端点、版本检查 |
|
||||
| Agent | 不需要入站端口 | Master HTTPS 与分配的存储目标 |
|
||||
| SQLite 数据 | 仅本地或块存储文件系统 | 无 |
|
||||
|
||||
反向代理与 Docker 位于同一主机时,把 Docker 绑定到 `127.0.0.1`:
|
||||
|
||||
~~~dotenv
|
||||
BACKUPX_BIND_ADDRESS=127.0.0.1
|
||||
~~~
|
||||
|
||||
裸机仅允许本机代理访问时,把 `server.host` 设置为回环地址;其他情况应使用主机或网络防火墙限制 TCP 8340。
|
||||
|
||||
## TLS 与反向代理
|
||||
|
||||
- 所有不可信网络段都使用 HTTPS。
|
||||
- `server.external_url` 设置为 Agent 可访问的稳定地址。
|
||||
- `server.trusted_proxies` 只加入准确代理 IP 或网段,禁止信任 `0.0.0.0/0`。
|
||||
- Agent 使用最终 HTTPS 地址,不要依赖重定向。
|
||||
- 私有 PKI 应向 Agent 下发 PEM CA,并配置 `caCertFile` 或 `--ca-cert`。
|
||||
- `--insecure-tls` 只用于临时测试。
|
||||
- Master 中转上传和 SSE 日志需要关闭 Nginx 请求与响应缓冲。
|
||||
|
||||
必须经过 SSH 堡垒机时,将隧道绑定到回环地址,严格校验主机密钥,使用专用账号与密钥,并让 Agent 服务依赖隧道。详见[多节点集群](../features/multi-node)。
|
||||
|
||||
## 角色与 API Key
|
||||
|
||||
| 角色 | 预期权限 |
|
||||
| --- | --- |
|
||||
| `viewer` | 读取仪表盘、任务、记录、报表与审计数据;不能浏览节点文件系统或修改资源 |
|
||||
| `operator` | viewer 权限,加上任务、存储、通知、备份、恢复、验证和文件浏览操作 |
|
||||
| `admin` | operator 权限,加上用户、API Key、设置、节点生命周期、安装令牌和 Token 轮换 |
|
||||
|
||||
为每位人员创建独立命名账号,不共享初始管理员。特权账号应启用双因素认证或通行密钥,并定期检查可信设备与恢复码。
|
||||
|
||||
用户 JWT 是无状态令牌。登出只会删除客户端副本,无法撤销已被复制到其他位置的 Token。应把 `security.jwt_expire` 设置为可接受的最短时长,保护 Bearer Token;必须使全部会话失效时轮换 JWT 密钥。
|
||||
|
||||
API Key 与交互式用户使用相同的角色检查。明文只在创建时显示一次,数据库只保存带密钥哈希。自动化应使用最低必要角色、设置有效期、保存在密钥管理系统,并及时撤销闲置 Key。监控不应使用管理员 Key。
|
||||
|
||||
## 保护控制面密钥
|
||||
|
||||
- `/etc/backupx/config.yaml` 应为 `root:backupx`、模式 `0640`,数据目录只允许服务账号访问。
|
||||
- `jwt_secret` 和 `encryption_key` 留空时,自动生成值会写入 SQLite 数据库,因此必须备份完整数据目录。
|
||||
- 加密密钥丢失或替换后,已有存储凭据将无法解密。
|
||||
- 数据库包含密码哈希、配置密钥、Agent Token、API Key 哈希、可信设备状态和审计数据。快照应加密并设置保留策略。
|
||||
- 不要把 Token 写入 shell 历史、Issue、截图或支持包。
|
||||
|
||||
每个节点有独立的长期 Agent Token。systemd 安装器把它保存到 `/etc/backupx-agent/agent.token`,模式为 `0600`。人员变更、主机入侵或意外泄露后应轮换 Token,在重叠窗口内更新文件并重启 Agent。
|
||||
|
||||
一次性安装 URL 有效期为 5 分钟至 24 小时,使用后立即失效。URL 与内嵌备用命令都应视为秘密,因为生成的安装材料会配置长期节点 Token。
|
||||
|
||||
## 容器与主机权限
|
||||
|
||||
正式 Compose 会删除全部能力,只添加旧数据卷所有权迁移与切换到非特权 `backupx` 用户所需的能力。保留 `no-new-privileges`,不要挂载 Docker Socket。
|
||||
|
||||
备份源应只读挂载;只有恢复目标确实需要时才添加独立、范围明确的可写挂载。需要高权限文件访问时,优先部署宿主机 Agent,而不是让 Master 容器以 root 运行。
|
||||
|
||||
systemd Master 以 `backupx` 运行。Agent 通常以 root 运行,因为它可能备份或恢复属于任意系统用户的文件。应限制任务创建权限并保护 root 所有的 Agent 配置。
|
||||
|
||||
## 公开端点
|
||||
|
||||
以下端点有意不使用 BackupX JWT 或 API Key 认证:
|
||||
|
||||
- `/health` 与 `/api/health`
|
||||
- `/ready` 与 `/api/ready`
|
||||
- `/metrics`
|
||||
- 一次性 `/install/:token` 与 `/api/install/:token` 路由
|
||||
|
||||
健康响应包含状态、版本、运行时间、时间戳和就绪检查;就绪失败时可能带有数据库错误细节。`/metrics` 还会包含节点与存储目标标签。应在防火墙或反向代理只允许监控网段访问探针与指标,不要缓存或记录完整安装令牌 URL。
|
||||
|
||||
## 备份加密边界
|
||||
|
||||
加密备份任务只能在 Master 执行,因为远程 Agent 不会收到 Master 加密密钥。不要通过复制 Master 密钥到 Agent 来绕过这个边界。Agent 任务需要加密时,应根据要求使用传输层加密和存储提供商的服务端加密。
|
||||
|
||||
每次调整密钥管理后都应验证加密备份恢复。缺少密钥的备份不可恢复。
|
||||
|
||||
## 审计与事故响应
|
||||
|
||||
BackupX 会记录特权操作,并可把签名审计事件转发到外部 Webhook。高价值审计记录应发送到独立管理的 SIEM 或追加写存储,避免受损 Master 删除唯一副本。
|
||||
|
||||
怀疑入侵时:
|
||||
|
||||
1. 隔离 Master,但不要删除证据。
|
||||
2. 撤销泄露的 API Key,轮换受影响的 Agent Token 与存储凭据。
|
||||
3. JWT 与加密密钥只能按计划迁移;直接更换加密密钥会使已保存加密配置失效。
|
||||
4. 审查用户、可信设备、API Key、节点、设置、恢复与删除事件。
|
||||
5. 无法确认完整性时,从已知可信的控制面快照恢复。
|
||||
|
||||
应用与数据库配套恢复流程见[升级与恢复](./upgrade-recovery)。
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: 故障排查
|
||||
description: Master、反向代理、Agent、备份工具和 SQLite 的安全诊断顺序。
|
||||
---
|
||||
|
||||
# 故障排查
|
||||
|
||||
从最先失败的边界开始并保留证据。在确认原因前,不要删除数据库、重建卷、一次性轮换所有 Token 或重新安装。
|
||||
|
||||
## 快速分流
|
||||
|
||||
| 现象 | 首项检查 | 可能边界 |
|
||||
| --- | --- | --- |
|
||||
| Web 控制台不可用 | 本机 `/health`,再检查代理 `/health` | 进程、监听、防火墙、代理或静态文件 |
|
||||
| `/health` 正常但 `/ready` 为 503 | 服务日志、数据库路径、磁盘、权限 | SQLite 或数据文件系统 |
|
||||
| 登录循环或客户端 IP 错误 | 转发头与 `trusted_proxies` | 反向代理信任 |
|
||||
| 实时日志停止更新 | Nginx 响应缓冲与超时 | SSE 代理路径 |
|
||||
| 中转上传停滞或代理磁盘占满 | 请求缓冲与 Body 上限 | 反向代理 |
|
||||
| Agent 离线 | Agent 日志、最终 Master URL、代理、DNS、CA | Agent 到 Master 网络 |
|
||||
| 备份启动后失败 | 记录日志、源路径、数据库原生工具 | Runner 或权限 |
|
||||
| 恢复失败 | 记录日志、目标挂载与写权限 | 存储读取或目标权限 |
|
||||
|
||||
## 无敏感信息的状态采集
|
||||
|
||||
Docker Master:
|
||||
|
||||
~~~bash
|
||||
docker compose ps
|
||||
docker compose logs --tail=200 backupx
|
||||
curl -i http://127.0.0.1:8340/health
|
||||
curl -i http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
裸机 Master:
|
||||
|
||||
~~~bash
|
||||
sudo systemctl status backupx --no-pager
|
||||
sudo journalctl -u backupx -n 200 --no-pager
|
||||
sudo ss -lntp | grep 8340
|
||||
curl -i http://127.0.0.1:8340/health
|
||||
curl -i http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
systemd Agent:
|
||||
|
||||
~~~bash
|
||||
sudo systemctl status backupx-agent --no-pager
|
||||
sudo journalctl -u backupx-agent -n 200 --no-pager
|
||||
sudo systemctl status backupx-agent-tunnel --no-pager
|
||||
~~~
|
||||
|
||||
最后一条只适用于堡垒机部署。共享输出前,移除 Authorization 头、API Key、Agent Token、安装 URL、数据库密码、存储凭据、代理凭据以及会暴露敏感拓扑的私有路径。
|
||||
|
||||
## Web 控制台或首次初始化
|
||||
|
||||
检查无需认证的初始化端点:
|
||||
|
||||
~~~bash
|
||||
curl -fsS http://127.0.0.1:8340/api/auth/setup/status
|
||||
~~~
|
||||
|
||||
API 正常但浏览器出现空白页或 JSON 时:
|
||||
|
||||
- 确认 Release 包含前端文件。
|
||||
- 裸机检查 `/opt/backupx/web` 可读;显式配置时确认 `server.web_root` 正确。
|
||||
- Docker 确认运行正式镜像,且自定义挂载未覆盖镜像内前端目录。
|
||||
- Nginx 静态模式确认 `root /opt/backupx/web` 和 SPA fallback 存在。
|
||||
- 版本变更后清理旧 Service Worker 或浏览器缓存。
|
||||
|
||||
认证失败先校验系统时间,再排查 TOTP 或通行密钥。确认浏览器 Origin 与最终 HTTPS 主机一致,并从审计日志检查限流、禁用用户或已撤销可信设备。
|
||||
|
||||
## 反向代理
|
||||
|
||||
验证并重载 Nginx:
|
||||
|
||||
~~~bash
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
curl -i https://backup.example.com/health
|
||||
curl -i https://backup.example.com/ready
|
||||
~~~
|
||||
|
||||
常见修正:
|
||||
|
||||
- HTTP 413:API 路由设置 `client_max_body_size 0`。
|
||||
- 中转上传占满代理临时目录:设置 `proxy_request_buffering off`。
|
||||
- SSE 日志批量到达或断开:设置 `proxy_buffering off`、关闭代理缓存并增加读取超时。
|
||||
- 一键安装返回 HTML:代理 `/api/` 并保留旧版 `/install/` 路由。
|
||||
- Agent 收到重定向:配置最终 HTTPS Master URL,不使用 HTTP 地址。
|
||||
- 审计中所有用户都是代理 IP:只把真实代理 IP 或网段加入 `server.trusted_proxies`。
|
||||
|
||||
以完整的 [Nginx 配置](../deployment/nginx)作为对照基线。
|
||||
|
||||
## Agent 离线
|
||||
|
||||
Agent 通常每 15 秒发送一次心跳,45 秒无心跳后会被标记离线。
|
||||
|
||||
1. 确认 Agent 与可选隧道服务运行。
|
||||
2. 确认 Master URL 没有重定向,且可在 Agent 主机解析。
|
||||
3. 检查显式 `proxyUrl`;DNS 必须经过 SSH 动态隧道时使用 `socks5h://`。
|
||||
4. 确认私有 CA 路径存在且可读,不要长期改为跳过 TLS。
|
||||
5. 检查到 Master 与分配存储后端的出站防火墙。
|
||||
6. 确认 `/etc/backupx-agent/agent.token` 存在且模式为 `0600`。
|
||||
7. Token 已轮换时,在重叠窗口内写入新值并重启 Agent。
|
||||
|
||||
不要把 Token 直接放进会保存到 shell 历史的诊断命令。Agent 日志中的 401 通常表示 Token 缺失、重叠期已结束或节点不匹配;连续连接错误通常来自 URL、DNS、代理、隧道、防火墙或 CA。
|
||||
|
||||
## 备份任务失败
|
||||
|
||||
修改任务前先打开备份记录并阅读完整日志。
|
||||
|
||||
- 文件任务路径在所选 Master 或 Agent 上解析,确认路径存在于该主机命名空间。
|
||||
- Docker 只能看到已挂载路径,备份源通常应只读。
|
||||
- MySQL 要求执行主机 `PATH` 中存在 `mysqldump`。
|
||||
- PostgreSQL 要求执行主机 `PATH` 中存在 `pg_dump`。
|
||||
- SAP HANA Runner 模式要求对应客户端工具与环境。
|
||||
- 确认服务账号可读源路径并可写临时目录。
|
||||
- 从控制台测试所选存储目标。
|
||||
- 远端存储应检查 DNS、出站策略、提供商配额、时钟偏差和代理。
|
||||
|
||||
配置多个目标时,应查看逐目标结果,不要假定所有副本都失败。修复失败目标时保留已经成功的远端产物。
|
||||
|
||||
## 恢复、下载或验证失败
|
||||
|
||||
- 确认远端产物仍存在且存储凭据可读取。
|
||||
- 确认执行恢复的主机挂载了目标路径。
|
||||
- 使用独立可写恢复目录,不要把所有备份源都改为可写。
|
||||
- 检查目标与 Agent 临时目录剩余空间。
|
||||
- 加密备份必须能取得原 Master 加密密钥。
|
||||
- CDC 仓库的 Manifest、索引和共享 Pack 必须一起保留,单独 Manifest 不是完整备份。
|
||||
|
||||
诊断时优先恢复到隔离目录,不要反复覆盖生产源。
|
||||
|
||||
## SQLite 与就绪故障
|
||||
|
||||
`/health` 为 200 而 `/ready` 为 503 时:
|
||||
|
||||
1. 从服务日志读取准确数据库错误。
|
||||
2. 检查磁盘空间、inode、路径所有权和挂载状态。
|
||||
3. 确认数据目录只被一个 Master 进程或容器使用。
|
||||
4. SQLite 应位于本地或块存储文件系统,不放在共享多写或不可靠网络文件系统。
|
||||
5. 检查外部备份或防病毒进程是否长期占用文件。
|
||||
|
||||
BackupX 使用 5 秒 SQLite busy timeout,但这不会把 SQLite 变成集群数据库。不能通过启动另一个 Master 解决锁冲突。文件级复制应先停服,再复制整个数据目录。
|
||||
|
||||
## 升级问题材料
|
||||
|
||||
提交 Issue 时提供:
|
||||
|
||||
- BackupX 版本、安装方式、操作系统和架构。
|
||||
- 故障影响 Master、Agent、代理、存储目标还是单个任务。
|
||||
- 覆盖首次失败时段的脱敏日志。
|
||||
- `/health` 与 `/ready` 的 HTTP 状态和响应体。
|
||||
- 最小复现步骤,以及是否始于升级或配置变更。
|
||||
- 脱敏后的相关代理配置。
|
||||
|
||||
不要向公开 Issue 附加 `backupx.db`、`.env`、完整配置、Agent Token 文件、API Key、安装命令或存储凭据。
|
||||
|
||||
涉及完整性或回滚时,应停止破坏性变更并参考[升级与恢复](./upgrade-recovery)。
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 升级与恢复
|
||||
description: 备份控制面、安全升级、配套回滚并恢复故障 Master。
|
||||
---
|
||||
|
||||
# 升级与恢复
|
||||
|
||||
备份产物与 BackupX 控制面属于两个不同的恢复域。对象存储中可能仍保留全部归档,但 Master 数据库丢失会同时丢失用户、加密后的存储凭据、计划、记录、节点 Token 和审计历史,因此两者都必须保护。
|
||||
|
||||
## 必须遵守的规则
|
||||
|
||||
1. 同一个数据目录或 SQLite 数据库只能运行一个活动 Master。
|
||||
2. 停止 Master 后快照完整数据目录与配置,或使用覆盖整个存储卷的原子快照。
|
||||
3. 旧应用版本必须与其升级前数据快照配套保留。启动时会执行数据库迁移,只切回旧二进制或旧镜像不是安全回滚。
|
||||
4. 控制面快照应保存到 Master 主机之外,并定期验证恢复。
|
||||
5. 停止 Master 前,先等待正在运行的备份、恢复、验证和复制任务结束。
|
||||
|
||||
| 部署方式 | 持久化控制面数据 | 配置与版本状态 |
|
||||
| --- | --- | --- |
|
||||
| Docker | `backupx-data` 卷中的 `/app/data` | Compose 文件、受保护的 `.env`、固定的镜像标签或摘要 |
|
||||
| 裸机 | `/opt/backupx/data` | `/etc/backupx`、`/opt/backupx/bin`、`/opt/backupx/web`、systemd unit |
|
||||
|
||||
配置未显式提供 JWT 与加密密钥时,自动生成的值保存在 SQLite 数据库中。所有控制面快照都应按敏感数据管理。
|
||||
|
||||
## 变更前检查
|
||||
|
||||
升级、迁移主机或修改安全密钥前:
|
||||
|
||||
- 记录当前 BackupX 版本以及准确的镜像摘要或 Release 校验和。
|
||||
- 确认 `/ready` 返回 HTTP 200,并检查近期失败记录。
|
||||
- 等待正在运行的备份、恢复、验证和复制结束。
|
||||
- 测试至少一个存储目标,并确认 Agent 在线。
|
||||
- 创建完整控制面快照,校验后复制到异机。
|
||||
- 可额外导出任务定义供人工审阅。任务导出不包含数据库密码与存储凭据,不能代替数据库快照。
|
||||
- 开始前确定回滚条件与维护窗口截止时间。
|
||||
|
||||
## 快照 Docker 部署
|
||||
|
||||
下面的示例无需直接访问 Docker 卷目录,即可生成一致的文件级副本:
|
||||
|
||||
~~~bash
|
||||
snapshot="backupx-control-plane-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
install -d -m 0700 "$snapshot"
|
||||
|
||||
docker compose stop backupx
|
||||
docker cp backupx:/app/data "$snapshot/data"
|
||||
cp docker-compose.yml "$snapshot/"
|
||||
if [ -f .env ]; then cp .env "$snapshot/"; fi
|
||||
docker compose start backupx
|
||||
|
||||
tar -czf "$snapshot.tar.gz" "$snapshot"
|
||||
sha256sum "$snapshot.tar.gz" > "$snapshot.tar.gz.sha256"
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
复制失败时,应先恢复已停止的服务,再继续排查。归档中的 `.env` 和数据库可能包含凭据,必须限制访问。如果块存储或云平台快照能原子覆盖整个卷,也可以直接使用。
|
||||
|
||||
## 快照裸机部署
|
||||
|
||||
~~~bash
|
||||
snapshot="/var/backups/backupx/backupx-control-plane-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
|
||||
sudo install -d -m 0700 /var/backups/backupx
|
||||
|
||||
sudo systemctl stop backupx
|
||||
sudo tar --acls --xattrs -C / -czf "$snapshot" \
|
||||
etc/backupx \
|
||||
etc/systemd/system/backupx.service \
|
||||
opt/backupx/bin \
|
||||
opt/backupx/web \
|
||||
opt/backupx/data
|
||||
sudo systemctl start backupx
|
||||
|
||||
sudo sha256sum "$snapshot" | sudo tee "$snapshot.sha256"
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
把归档及校验和复制到受保护的异机存储。不要在服务运行时只复制 `backupx.db`。
|
||||
|
||||
## 升级 Docker
|
||||
|
||||
1. 在 `BACKUPX_IMAGE` 中使用 Release 标签或不可变摘要,受控生产升级不要使用 `latest`。
|
||||
2. 创建并验证升级前快照。
|
||||
3. 拉取并重建服务:
|
||||
|
||||
~~~bash
|
||||
docker compose pull backupx
|
||||
docker compose up -d backupx
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 backupx
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
4. 登录后测试存储目标,确认 Agent 心跳,并执行一个小型备份以及一次恢复或验证演练。
|
||||
5. 观察窗口结束前保留旧镜像引用与快照。
|
||||
|
||||
Master 完成后再小批量升级 Agent。除非变更目标就是网络配置,否则不要改动节点专用代理、私有 CA、Token 文件和堡垒机参数。
|
||||
|
||||
## 升级裸机
|
||||
|
||||
下载目标 Release 与校验和,完成校验后解压。安装器会保留已有的 `/etc/backupx/config.yaml`,替换二进制、前端文件和 systemd unit,并重启服务。
|
||||
|
||||
~~~bash
|
||||
sha256sum -c backupx-vX.Y.Z-linux-amd64.tar.gz.sha256
|
||||
tar xzf backupx-vX.Y.Z-linux-amd64.tar.gz
|
||||
cd backupx-vX.Y.Z-linux-amd64
|
||||
sudo ./install.sh
|
||||
|
||||
sudo systemctl status backupx --no-pager
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
运行安装器前必须先创建停服快照。升级后的业务检查与 Docker 相同。
|
||||
|
||||
## 回滚
|
||||
|
||||
回滚是配套操作:必须同时恢复旧应用版本和紧邻升级前创建的数据快照。
|
||||
|
||||
Docker 应保留故障卷用于分析,把快照恢复到新的空卷,Compose 同时指向该卷与旧镜像标签,然后只启动一个 Master。裸机应停止服务并保留故障现场,从同一归档恢复旧配置、二进制、前端、数据目录和 unit,重新加载 systemd 后启动。
|
||||
|
||||
回滚后检查:
|
||||
|
||||
~~~bash
|
||||
curl -fsS http://127.0.0.1:8340/health
|
||||
curl -fsS http://127.0.0.1:8340/ready
|
||||
~~~
|
||||
|
||||
随后验证登录、存储访问、计划、Agent 心跳、一次备份和一次非破坏性恢复演练。在事故原因明确前不要删除故障现场。
|
||||
|
||||
## 恢复丢失的 Master
|
||||
|
||||
1. 按快照记录准备同架构主机与完全相同的应用版本。
|
||||
2. 替代主机先与生产流量隔离,并确保旧 Master 无法再次启动。
|
||||
3. 按原权限恢复配置和完整数据目录。
|
||||
4. 只启动一个 Master,在本机检查 `/ready`。
|
||||
5. 本地验证完成后再切换稳定 DNS 名称或虚拟 IP。
|
||||
6. 检查用户、存储目标、任务、记录、通知和审计历史。
|
||||
7. 数据库内 Token 与节点一致时,已有 Agent 会自动重连;可能泄露的 Token 必须调查并轮换。
|
||||
8. 执行小型备份及恢复或验证演练后再结束事故处理。
|
||||
|
||||
恢复控制面不会重新生成外部备份产物,它们仍位于原存储目标。反过来,任务 JSON 导出只适合辅助重建计划,不包含密钥、存储定义和部分节点绑定,不能作为完整灾备。
|
||||
|
||||
## 验证恢复计划
|
||||
|
||||
至少每季度把近期快照恢复到隔离网络,启动快照记录的 BackupX 版本并验证:
|
||||
|
||||
- 不接触生产 Master 时,`/ready` 能恢复正常。
|
||||
- 管理员可登录,已加密的存储配置可读取。
|
||||
- 任务、节点、记录和审计数量合理。
|
||||
- 可以测试一个存储目标而不写入生产数据。
|
||||
- 选定备份可验证,或可恢复到隔离目录。
|
||||
|
||||
记录恢复耗时和最新可恢复快照时间,这两个实测值才是控制面的真实 RTO 与 RPO。
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: API 参考
|
||||
description: BackupX REST 端点、认证方式、角色边界、流式响应和公开探针。
|
||||
---
|
||||
|
||||
# API 参考
|
||||
|
||||
交互式 API 以 `/api` 为根路径。大多数端点接受用户 JWT 或 API Key;Agent 协议使用节点专用 Token。公开探针和一次性安装器在文末单列。
|
||||
|
||||
## 认证
|
||||
|
||||
### 用户 JWT
|
||||
|
||||
通过 `POST /api/auth/login` 获取 JWT,并作为 Bearer Token 发送:
|
||||
|
||||
~~~bash
|
||||
curl -H "Authorization: Bearer $BACKUPX_TOKEN" \
|
||||
https://backup.example.com/api/backup/tasks
|
||||
~~~
|
||||
|
||||
根据账号和系统设置,登录过程还可能要求邮件或短信 OTP、TOTP、恢复码、可信设备 Token 或 WebAuthn。
|
||||
|
||||
### API Key
|
||||
|
||||
管理员可在控制台或通过 `POST /api/api-keys` 创建 API Key。明文 `bax_...` 只返回一次。
|
||||
|
||||
~~~bash
|
||||
curl -H "X-Api-Key: $BACKUPX_API_KEY" \
|
||||
https://backup.example.com/api/dashboard/stats
|
||||
~~~
|
||||
|
||||
也支持 `Authorization: Bearer bax_...`。API Key 带有 `admin`、`operator` 或 `viewer` 角色,可禁用并可设置有效期。
|
||||
|
||||
### Agent Token
|
||||
|
||||
Agent 协议 Handler 从 `X-Agent-Token` 验证节点 Token。它不是用户凭据,不能用于交互式资源 API。
|
||||
|
||||
### 权限标记
|
||||
|
||||
下表使用这些标记:
|
||||
|
||||
| 标记 | 所需权限 |
|
||||
| --- | --- |
|
||||
| 公开 | 不需要 JWT 或 API Key;安装路由仍要求一次性 Token |
|
||||
| 已认证 | 任意 `viewer`、`operator` 或 `admin` |
|
||||
| 运维 | `operator` 或 `admin` |
|
||||
| 管理员 | 仅 `admin` |
|
||||
| Agent | 有效的节点专用 Agent Token |
|
||||
|
||||
viewer 可使用读取端点,但不能浏览节点文件系统;operator 可以执行和修改备份资源;admin 还可管理用户、API Key、设置、节点、安装令牌和节点 Token 轮换。角色不满足时返回 HTTP 403。
|
||||
|
||||
## 认证与账号安全
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/auth/setup/status` | 公开 | 查询是否需要创建首个管理员 |
|
||||
| `POST` | `/api/auth/setup` | 公开 | 系统无用户时创建首个管理员 |
|
||||
| `POST` | `/api/auth/login` | 公开 | 完成密码或 MFA 登录并获取 JWT |
|
||||
| `POST` | `/api/auth/otp/send` | 公开 | 发送已配置的登录 OTP |
|
||||
| `POST` | `/api/auth/webauthn/login/options` | 公开 | 开始通行密钥登录 |
|
||||
| `POST` | `/api/auth/logout` | 已认证 | 确认登出;客户端必须丢弃无状态 JWT |
|
||||
| `GET` | `/api/auth/profile` | 已认证 | 读取当前账号 |
|
||||
| `PUT` | `/api/auth/password` | 已认证 | 修改当前账号密码 |
|
||||
| `POST` | `/api/auth/2fa/setup` | 已认证 | 准备 TOTP 注册 |
|
||||
| `POST` | `/api/auth/2fa/enable` | 已认证 | 验证后启用 TOTP |
|
||||
| `POST` | `/api/auth/2fa/recovery-codes` | 已认证 | 重新生成恢复码 |
|
||||
| `DELETE` | `/api/auth/2fa` | 已认证 | 停用 TOTP |
|
||||
| `PUT` | `/api/auth/otp/config` | 已认证 | 更新 OTP 登录配置 |
|
||||
| `POST` | `/api/auth/webauthn/register/options` | 已认证 | 开始注册通行密钥 |
|
||||
| `POST` | `/api/auth/webauthn/register/finish` | 已认证 | 完成通行密钥注册 |
|
||||
| `GET` | `/api/auth/webauthn/credentials` | 已认证 | 列出通行密钥 |
|
||||
| `DELETE` | `/api/auth/webauthn/credentials/:id` | 已认证 | 删除通行密钥 |
|
||||
| `GET` | `/api/auth/trusted-devices` | 已认证 | 列出可信设备 |
|
||||
| `DELETE` | `/api/auth/trusted-devices/:id` | 已认证 | 撤销可信设备 |
|
||||
|
||||
账号安全端点应使用交互式 JWT,不应使用自动化 API Key。
|
||||
|
||||
## 系统与存储目标
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/system/info` | 已认证 | 版本与系统信息 |
|
||||
| `GET` | `/api/system/update-check` | 已认证 | 检查可用 Release |
|
||||
| `GET` | `/api/storage-targets` | 已认证 | 存储目标列表 |
|
||||
| `POST` | `/api/storage-targets` | 运维 | 创建目标 |
|
||||
| `POST` | `/api/storage-targets/test` | 运维 | 测试未保存配置 |
|
||||
| `GET` | `/api/storage-targets/rclone/backends` | 已认证 | 可用 rclone 后端 |
|
||||
| `POST` | `/api/storage-targets/google-drive/auth-url` | 运维 | 开始 Google Drive 授权 |
|
||||
| `POST` | `/api/storage-targets/google-drive/complete` | 运维 | 完成 Google Drive 授权 |
|
||||
| `GET` | `/api/storage-targets/google-drive/callback` | 已认证 | 处理 OAuth 回调 |
|
||||
| `GET` | `/api/storage-targets/:id` | 已认证 | 读取目标 |
|
||||
| `PUT` | `/api/storage-targets/:id` | 运维 | 更新目标 |
|
||||
| `DELETE` | `/api/storage-targets/:id` | 运维 | 删除目标 |
|
||||
| `PUT` | `/api/storage-targets/:id/star` | 运维 | 切换收藏 |
|
||||
| `POST` | `/api/storage-targets/:id/test` | 运维 | 测试已保存目标 |
|
||||
| `GET` | `/api/storage-targets/:id/usage` | 已认证 | 读取已记录用量 |
|
||||
| `GET` | `/api/storage-targets/:id/google-drive/profile` | 已认证 | 读取已连接 Google Drive 账号 |
|
||||
|
||||
## 备份任务
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/backup/tasks` | 已认证 | 任务列表 |
|
||||
| `GET` | `/api/backup/tasks/tags` | 已认证 | 任务标签 |
|
||||
| `GET` | `/api/backup/tasks/export` | 已认证 | 下载全部任务 JSON,或用 `?ids=1,2` 选择任务 |
|
||||
| `POST` | `/api/backup/tasks/import` | 运维 | 导入任务,最大 1 MiB |
|
||||
| `POST` | `/api/backup/tasks/batch/toggle` | 运维 | 批量启用或停用 |
|
||||
| `POST` | `/api/backup/tasks/batch/delete` | 运维 | 批量删除 |
|
||||
| `POST` | `/api/backup/tasks/batch/run` | 运维 | 批量执行 |
|
||||
| `GET` | `/api/backup/tasks/:id` | 已认证 | 读取任务 |
|
||||
| `POST` | `/api/backup/tasks` | 运维 | 创建任务 |
|
||||
| `PUT` | `/api/backup/tasks/:id` | 运维 | 更新任务 |
|
||||
| `DELETE` | `/api/backup/tasks/:id` | 运维 | 删除任务 |
|
||||
| `PUT` | `/api/backup/tasks/:id/toggle` | 运维 | 启用或停用 |
|
||||
| `POST` | `/api/backup/tasks/:id/run` | 运维 | 触发备份 |
|
||||
| `POST` | `/api/backup/tasks/:id/verify` | 运维 | 从任务触发验证 |
|
||||
|
||||
任务导出会主动排除数据库密码与存储凭据,适合迁移和审阅,不是完整控制面备份。
|
||||
|
||||
## 备份与恢复记录
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/backup/records` | 已认证 | 列出并筛选备份记录 |
|
||||
| `POST` | `/api/backup/records/batch-delete` | 运维 | 批量删除记录 |
|
||||
| `GET` | `/api/backup/records/:id` | 已认证 | 读取备份记录 |
|
||||
| `GET` | `/api/backup/records/:id/logs/stream` | 已认证 | 通过 SSE 输出日志 |
|
||||
| `GET` | `/api/backup/records/:id/download` | 已认证 | 下载产物 |
|
||||
| `GET` | `/api/backup/records/:id/contents` | 已认证 | 浏览支持类型的产物内容 |
|
||||
| `POST` | `/api/backup/records/:id/restore` | 运维 | 启动恢复 |
|
||||
| `POST` | `/api/backup/records/:id/replicate` | 运维 | 复制已有产物 |
|
||||
| `POST` | `/api/backup/records/:id/verify` | 运维 | 验证已有产物 |
|
||||
| `PUT` | `/api/backup/records/:id/lock` | 运维 | 设置保留锁 |
|
||||
| `DELETE` | `/api/backup/records/:id` | 运维 | 删除记录及受管产物 |
|
||||
| `GET` | `/api/restore/records` | 已认证 | 恢复记录列表 |
|
||||
| `GET` | `/api/restore/records/:id` | 已认证 | 恢复记录详情 |
|
||||
| `GET` | `/api/restore/records/:id/logs/stream` | 已认证 | 恢复日志 SSE |
|
||||
| `GET` | `/api/replication/records` | 已认证 | 复制记录列表 |
|
||||
| `GET` | `/api/replication/records/:id` | 已认证 | 复制记录详情 |
|
||||
| `GET` | `/api/verify/records` | 已认证 | 验证记录列表 |
|
||||
| `GET` | `/api/verify/records/:id` | 已认证 | 验证记录详情 |
|
||||
| `GET` | `/api/verify/records/:id/logs/stream` | 已认证 | 验证日志 SSE |
|
||||
|
||||
## 模板、报表与仪表盘
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/task-templates` | 已认证 | 任务模板列表 |
|
||||
| `GET` | `/api/task-templates/:id` | 已认证 | 读取任务模板 |
|
||||
| `POST` | `/api/task-templates` | 运维 | 创建模板 |
|
||||
| `PUT` | `/api/task-templates/:id` | 运维 | 更新模板 |
|
||||
| `DELETE` | `/api/task-templates/:id` | 运维 | 删除模板 |
|
||||
| `POST` | `/api/task-templates/:id/apply` | 运维 | 从模板创建任务 |
|
||||
| `GET` | `/api/reports/compliance` | 已认证 | 合规证据 |
|
||||
| `GET` | `/api/reports/compliance/export` | 已认证 | 导出合规 CSV |
|
||||
| `GET` | `/api/dashboard/stats` | 已认证 | 汇总统计 |
|
||||
| `GET` | `/api/dashboard/timeline` | 已认证 | 最近活动 |
|
||||
| `GET` | `/api/dashboard/sla` | 已认证 | RPO 与 SLA 状态 |
|
||||
| `GET` | `/api/dashboard/cluster` | 已认证 | 集群概览 |
|
||||
| `GET` | `/api/dashboard/breakdown` | 已认证 | 任务与记录分布 |
|
||||
| `GET` | `/api/dashboard/node-performance` | 已认证 | 节点性能 |
|
||||
|
||||
## 通知、设置与管理
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/notifications` | 已认证 | 通知渠道列表 |
|
||||
| `GET` | `/api/notifications/:id` | 已认证 | 读取渠道 |
|
||||
| `POST` | `/api/notifications` | 运维 | 创建渠道 |
|
||||
| `PUT` | `/api/notifications/:id` | 运维 | 更新渠道 |
|
||||
| `DELETE` | `/api/notifications/:id` | 运维 | 删除渠道 |
|
||||
| `POST` | `/api/notifications/test` | 运维 | 测试未保存配置 |
|
||||
| `POST` | `/api/notifications/:id/test` | 运维 | 测试已保存渠道 |
|
||||
| `GET` | `/api/settings` | 已认证 | 读取系统设置 |
|
||||
| `PUT` | `/api/settings` | 管理员 | 更新系统设置 |
|
||||
| `GET` | `/api/users` | 管理员 | 用户列表 |
|
||||
| `POST` | `/api/users` | 管理员 | 创建用户 |
|
||||
| `PUT` | `/api/users/:id` | 管理员 | 更新用户 |
|
||||
| `POST` | `/api/users/:id/2fa/reset` | 管理员 | 重置用户第二因素 |
|
||||
| `DELETE` | `/api/users/:id` | 管理员 | 删除用户 |
|
||||
| `GET` | `/api/api-keys` | 管理员 | API Key 列表,不返回明文 |
|
||||
| `POST` | `/api/api-keys` | 管理员 | 创建 API Key,明文仅返回一次 |
|
||||
| `PUT` | `/api/api-keys/:id/toggle` | 管理员 | 启用或停用 API Key |
|
||||
| `DELETE` | `/api/api-keys/:id` | 管理员 | 撤销 API Key |
|
||||
|
||||
## 审计、事件、搜索与发现
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/audit-logs` | 已认证 | 列出并筛选审计记录 |
|
||||
| `GET` | `/api/audit-logs/export` | 已认证 | 导出审计记录 |
|
||||
| `GET` | `/api/events/stream` | 已认证 | 通过 SSE 输出实时应用事件 |
|
||||
| `GET` | `/api/search` | 已认证 | 搜索支持的资源 |
|
||||
| `POST` | `/api/database/discover` | 已认证 | 按提供的连接信息发现数据库 |
|
||||
|
||||
## 节点
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/nodes` | 已认证 | 节点列表 |
|
||||
| `GET` | `/api/nodes/:id` | 已认证 | 节点详情 |
|
||||
| `GET` | `/api/nodes/:id/fs/list` | 运维 | 浏览所选节点文件系统 |
|
||||
| `POST` | `/api/nodes` | 管理员 | 创建节点 |
|
||||
| `POST` | `/api/nodes/batch` | 管理员 | 批量创建最多 50 个节点 |
|
||||
| `PUT` | `/api/nodes/:id` | 管理员 | 更新节点 |
|
||||
| `DELETE` | `/api/nodes/:id` | 管理员 | 删除未被引用的节点 |
|
||||
| `POST` | `/api/nodes/:id/install-tokens` | 管理员 | 创建一次性安装器 |
|
||||
| `GET` | `/api/nodes/:id/install-script-preview` | 管理员 | 预览安装材料 |
|
||||
| `POST` | `/api/nodes/:id/rotate-token` | 管理员 | 轮换长期节点 Token |
|
||||
|
||||
## Agent 协议
|
||||
|
||||
这些路由供 `backupx agent` 使用,Handler 内部通过节点 Token 认证。
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `POST` | `/api/agent/heartbeat` | Agent | 上报心跳与节点状态 |
|
||||
| `POST` | `/api/agent/commands/poll` | Agent | 领取待执行命令 |
|
||||
| `POST` | `/api/agent/commands/:id/result` | Agent | 上报命令结果 |
|
||||
| `GET` | `/api/agent/tasks/:id` | Agent | 获取可执行任务规格 |
|
||||
| `POST` | `/api/agent/records/:id` | Agent | 追加日志或更新备份状态 |
|
||||
| `PUT` | `/api/agent/records/:id/artifacts/:targetId` | Agent | 向 Master 流式中转产物 |
|
||||
| `GET` | `/api/agent/restores/:id/spec` | Agent | 获取恢复指令 |
|
||||
| `GET` | `/api/agent/restores/:id/artifact` | Agent | 流式读取恢复产物 |
|
||||
| `POST` | `/api/agent/restores/:id` | Agent | 更新恢复状态 |
|
||||
| `GET` | `/api/v1/agent/self` | Agent | 安装时校验节点身份 |
|
||||
|
||||
## 公开运维与安装路由
|
||||
|
||||
| 方法 | 端点 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/health` | 公开 | 存活检查 |
|
||||
| `GET` | `/api/health` | 公开 | 带 API 前缀的存活别名 |
|
||||
| `GET` | `/ready` | 公开 | SQLite 就绪检查 |
|
||||
| `GET` | `/api/ready` | 公开 | 带 API 前缀的就绪别名 |
|
||||
| `GET` | `/metrics` | 公开 | Prometheus 指标 |
|
||||
| `GET` | `/install/:token` | 公开 | 消费一次性 Agent 安装令牌 |
|
||||
| `GET` | `/api/install/:token` | 公开 | 带 API 前缀的安装路由 |
|
||||
| `GET` | `/install/:token/compose.yml` | 公开 | 生成 Docker Agent Compose |
|
||||
| `GET` | `/api/install/:token/compose.yml` | 公开 | 带 API 前缀的 Docker Compose 路由 |
|
||||
|
||||
探针与指标应只对监控网段开放。安装 Token 是单次、限时秘密,不能写入公开日志。
|
||||
|
||||
## 响应格式
|
||||
|
||||
大多数 JSON 成功响应为:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
~~~
|
||||
|
||||
错误使用 HTTP 4xx 或 5xx,并带稳定业务码:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"code": "BACKUP_TASK_NOT_FOUND",
|
||||
"message": "备份任务不存在"
|
||||
}
|
||||
~~~
|
||||
|
||||
客户端应按 HTTP 状态和 `code` 分支,不要依赖本地化的 `message`。
|
||||
|
||||
产物下载、任务 JSON 导出、审计或合规导出、安装器响应和 `/metrics` 使用各自原生 Content-Type,不使用 JSON Envelope。日志与事件流使用 `text/event-stream`,反向代理必须关闭响应缓冲。
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: CLI 参考
|
||||
description: backupx 子命令 — server / agent / backint / reset-password。
|
||||
---
|
||||
|
||||
# CLI 参考
|
||||
|
||||
`backupx` 二进制内置多个子命令。无子命令时默认启动主服务进程。
|
||||
|
||||
## `backupx`(默认:服务进程)
|
||||
|
||||
```bash
|
||||
backupx --config /opt/backupx/config.yaml
|
||||
backupx --version
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--config <path>` | 显式配置文件路径;省略时使用下方查找路径 |
|
||||
| `--version` | 打印版本后退出 |
|
||||
|
||||
未提供 `--config` 时,服务端依次查找 `./config.yaml`、`./server/config.yaml` 和 `/etc/backupx/config.yaml`。`BACKUPX_*` 环境变量会覆盖对应服务端配置项,详见[配置参考](../deployment/configuration)。
|
||||
|
||||
## `backupx agent`
|
||||
|
||||
以 Agent 模式运行,连接到 Master。详见 [多节点集群](../features/multi-node)。
|
||||
|
||||
```bash
|
||||
backupx agent --master https://backup.example.com --token-file /etc/backupx-agent/agent.token
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--master <url>` | Master URL |
|
||||
| `--token <token>` | Agent 认证令牌 |
|
||||
| `--token-file <path>` | 从文件读取 Agent Token,服务与容器部署推荐使用 |
|
||||
| `--config <path>` | 加载 Agent YAML;提供后不再加载基于环境变量的 Agent 配置 |
|
||||
| `--temp-dir <path>` | 本地临时目录(默认 `/var/lib/backupx-agent/tmp`) |
|
||||
| `--proxy-url <url>` | 显式 HTTP(S) 或 SOCKS5(H) 代理 |
|
||||
| `--ca-cert <path>` | 用于校验 Master 的 PEM CA 证书 |
|
||||
| `--insecure-tls` | 跳过 TLS 校验(仅测试用) |
|
||||
|
||||
Agent 配置优先级为显式 CLI 参数高于 YAML。未提供 `--config` 时,配置从 `BACKUPX_AGENT_MASTER`、`BACKUPX_AGENT_TOKEN`、`BACKUPX_AGENT_TOKEN_FILE`、`BACKUPX_AGENT_HEARTBEAT`、`BACKUPX_AGENT_POLL`、`BACKUPX_AGENT_TEMP_DIR`、`BACKUPX_AGENT_PROXY_URL`、`BACKUPX_AGENT_CA_CERT_FILE` 和 `BACKUPX_AGENT_INSECURE_TLS` 加载。未设置显式代理时,Agent 还会遵循 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY`。
|
||||
|
||||
`--token` 优先于 `--token-file`。长期 Token 应放在仅 root 可读的文件中,不要进入命令历史。私有 CA 与 `--insecure-tls` 不能同时启用。
|
||||
|
||||
## `backupx backint`
|
||||
|
||||
SAP HANA Backint 协议代理,详见 [SAP HANA 支持](../features/sap-hana)。
|
||||
|
||||
```bash
|
||||
backupx backint -f <function> -i <input> -o <output> -p <params>
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-f <fn>` | `backup` / `restore` / `inquire` / `delete` |
|
||||
| `-i <path>` | 输入文件 |
|
||||
| `-o <path>` | 输出文件 |
|
||||
| `-p <path>` | 参数文件 |
|
||||
| `-u / -c / -l / -v` | 接收但忽略(兼容 SAP 约定) |
|
||||
|
||||
`-p` 参数文件必须定义 `STORAGE_TYPE`,并提供 `STORAGE_CONFIG_JSON` 或 `STORAGE_CONFIG`。可选项包括 `PARALLEL_FACTOR`、`COMPRESS`、`LOG_FILE`、`CATALOG_DB` 和 `KEY_PREFIX`。
|
||||
|
||||
## `backupx reset-password`
|
||||
|
||||
直接在 SQLite 中重置管理员密码,无需重启服务。
|
||||
|
||||
```bash
|
||||
backupx reset-password --username admin --password 'newpass123' [--config /path/to/config.yaml]
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--username` | 目标用户名(默认 `admin`) |
|
||||
| `--password` | 新密码(最少 8 字符,必填) |
|
||||
| `--config` | 配置文件路径(用于定位数据库文件) |
|
||||
|
||||
该命令应在可访问配置中 SQLite 路径的 Master 主机执行。不要把新密码直接写入长期保留的 shell 历史。
|
||||
23
docs-site/i18n/zh-CN/docusaurus-theme-classic/footer.json
Normal file
23
docs-site/i18n/zh-CN/docusaurus-theme-classic/footer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"link.title.Docs": {"message": "文档"},
|
||||
"link.title.Features": {"message": "功能"},
|
||||
"link.title.More": {"message": "更多"},
|
||||
"link.title.Community": {"message": "社区"},
|
||||
"link.title.Sponsors": {"message": "赞助商"},
|
||||
"link.item.label.Introduction": {"message": "简介"},
|
||||
"link.item.label.Quick Start": {"message": "快速开始"},
|
||||
"link.item.label.Upgrade & Recovery": {"message": "升级与恢复"},
|
||||
"link.item.label.SAP HANA": {"message": "SAP HANA"},
|
||||
"link.item.label.Multi-Node Cluster": {"message": "多节点集群"},
|
||||
"link.item.label.API Reference": {"message": "API 参考"},
|
||||
"link.item.label.GitHub": {"message": "GitHub"},
|
||||
"link.item.label.Releases": {"message": "Releases"},
|
||||
"link.item.label.Docker Hub": {"message": "Docker Hub"},
|
||||
"link.item.label.Issues": {"message": "Issues"},
|
||||
"link.item.label.Contributors": {"message": "贡献者"},
|
||||
"link.item.label.Pull Requests": {"message": "Pull Requests"},
|
||||
"link.item.label.Sponsor": {"message": "赞助"},
|
||||
"link.item.label.Sponsor BackupX": {"message": "赞助 BackupX"},
|
||||
"link.item.label.Partnership": {"message": "合作伙伴"},
|
||||
"link.item.label.Sponsor tiers": {"message": "赞助层级"}
|
||||
}
|
||||
22
docs-site/i18n/zh-CN/docusaurus-theme-classic/navbar.json
Normal file
22
docs-site/i18n/zh-CN/docusaurus-theme-classic/navbar.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"item.label.Docs": {
|
||||
"message": "文档",
|
||||
"description": "Navbar item: Docs"
|
||||
},
|
||||
"item.label.Downloads": {
|
||||
"message": "下载",
|
||||
"description": "Navbar item: Downloads"
|
||||
},
|
||||
"item.label.Community": {
|
||||
"message": "社区",
|
||||
"description": "Navbar item: Community"
|
||||
},
|
||||
"item.label.Sponsors": {
|
||||
"message": "赞助商",
|
||||
"description": "Navbar item: Sponsors"
|
||||
},
|
||||
"item.label.GitHub": {
|
||||
"message": "GitHub",
|
||||
"description": "Navbar item: GitHub"
|
||||
}
|
||||
}
|
||||
19445
docs-site/package-lock.json
generated
Normal file
19445
docs-site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
docs-site/package.json
Normal file
49
docs-site/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "backupx-docs",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.10.2",
|
||||
"@docusaurus/faster": "3.10.2",
|
||||
"@docusaurus/preset-classic": "3.10.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.10.2",
|
||||
"@docusaurus/tsconfig": "3.10.2",
|
||||
"@docusaurus/types": "3.10.2",
|
||||
"@types/react": "^19.0.0",
|
||||
"typescript": "~6.0.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
}
|
||||
65
docs-site/sidebars.ts
Normal file
65
docs-site/sidebars.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
const sidebars: SidebarsConfig = {
|
||||
docs: [
|
||||
'intro',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Getting Started',
|
||||
collapsed: false,
|
||||
items: [
|
||||
'getting-started/installation',
|
||||
'getting-started/quick-start',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Deployment',
|
||||
items: [
|
||||
'deployment/docker',
|
||||
'deployment/bare-metal',
|
||||
'deployment/nginx',
|
||||
'deployment/configuration',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Operations',
|
||||
items: [
|
||||
'operations/upgrade-recovery',
|
||||
'operations/security',
|
||||
'operations/monitoring',
|
||||
'operations/troubleshooting',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Features',
|
||||
items: [
|
||||
'features/backup-types',
|
||||
'features/storage-backends',
|
||||
'features/sap-hana',
|
||||
'features/multi-node',
|
||||
'features/notifications',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Reference',
|
||||
items: [
|
||||
'reference/api',
|
||||
'reference/cli',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Development',
|
||||
items: [
|
||||
'development/setup',
|
||||
'development/contributing',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
329
docs-site/src/components/HomepageCommunity/index.tsx
Normal file
329
docs-site/src/components/HomepageCommunity/index.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import Heading from '@theme/Heading';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Link from '@docusaurus/Link';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type SponsorSlot = {
|
||||
brand: ReactNode;
|
||||
name: ReactNode;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
type Contributor = {
|
||||
login: string;
|
||||
avatarUrl?: string;
|
||||
contributions: number;
|
||||
type: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type GitHubContributor = {
|
||||
login: string;
|
||||
avatar_url?: string;
|
||||
contributions?: number;
|
||||
html_url?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type CommunityPath = {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
href: string;
|
||||
};
|
||||
|
||||
const SPONSOR_SLOTS: SponsorSlot[] = [
|
||||
{
|
||||
brand: 'BackupX',
|
||||
name: <Translate id="community.sponsor.logo.project">Project backer</Translate>,
|
||||
href: 'https://github.com/sponsors/Awuqing',
|
||||
},
|
||||
{
|
||||
brand: 'Cloud',
|
||||
name: <Translate id="community.sponsor.logo.cloud">Cloud partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Object',
|
||||
name: <Translate id="community.sponsor.logo.object">Object storage</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'CDN',
|
||||
name: <Translate id="community.sponsor.logo.cdn">CDN partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'DB',
|
||||
name: <Translate id="community.sponsor.logo.database">Database partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Security',
|
||||
name: <Translate id="community.sponsor.logo.security">Security audit</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Agent',
|
||||
name: <Translate id="community.sponsor.logo.agent">Remote node lab</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Docs',
|
||||
name: <Translate id="community.sponsor.logo.docs">Docs sponsor</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Release',
|
||||
name: <Translate id="community.sponsor.logo.release">Release sponsor</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'S3',
|
||||
name: <Translate id="community.sponsor.logo.s3">S3 compatible</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'WebDAV',
|
||||
name: <Translate id="community.sponsor.logo.webdav">WebDAV partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'SFTP',
|
||||
name: <Translate id="community.sponsor.logo.sftp">SFTP partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Docker',
|
||||
name: <Translate id="community.sponsor.logo.docker">Container partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Mirror',
|
||||
name: <Translate id="community.sponsor.logo.mirror">Mirror partner</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Restore',
|
||||
name: <Translate id="community.sponsor.logo.restore">Restore drill</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'QA',
|
||||
name: <Translate id="community.sponsor.logo.qa">Test lab</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'OSS',
|
||||
name: <Translate id="community.sponsor.logo.oss">Open source</Translate>,
|
||||
},
|
||||
{
|
||||
brand: 'Open Slot',
|
||||
name: <Translate id="community.sponsor.logo.open">Sponsor slot open</Translate>,
|
||||
},
|
||||
];
|
||||
|
||||
const FALLBACK_CONTRIBUTORS: Contributor[] = [
|
||||
{
|
||||
login: 'Awuqing',
|
||||
contributions: 0,
|
||||
type: 'User',
|
||||
href: 'https://github.com/Awuqing',
|
||||
},
|
||||
{
|
||||
login: 'dependabot[bot]',
|
||||
contributions: 0,
|
||||
type: 'Bot',
|
||||
href: 'https://github.com/dependabot',
|
||||
},
|
||||
];
|
||||
|
||||
const COMMUNITY_PATHS: CommunityPath[] = [
|
||||
{
|
||||
title: <Translate id="community.path.issues.title">Report production issues</Translate>,
|
||||
description: <Translate id="community.path.issues.desc">Share logs, deployment topology and restore expectations.</Translate>,
|
||||
href: 'https://github.com/Awuqing/BackupX/issues',
|
||||
},
|
||||
{
|
||||
title: <Translate id="community.path.docs.title">Improve docs and examples</Translate>,
|
||||
description: <Translate id="community.path.docs.desc">Contribute deployment guides for storage, agents and databases.</Translate>,
|
||||
href: '/docs/development/contributing',
|
||||
},
|
||||
{
|
||||
title: <Translate id="community.path.code.title">Ship focused PRs</Translate>,
|
||||
description: <Translate id="community.path.code.desc">Keep changes small, tested and aligned with the existing architecture.</Translate>,
|
||||
href: 'https://github.com/Awuqing/BackupX/pulls',
|
||||
},
|
||||
];
|
||||
|
||||
function SponsorLogoCard({brand, name, href}: SponsorSlot) {
|
||||
return (
|
||||
<Link className={styles.sponsorLogoTile} to={href ?? 'https://github.com/sponsors/Awuqing'}>
|
||||
<span className={styles.sponsorLogoMark}>{brand}</span>
|
||||
<span className={styles.sponsorLogoName}>{name}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function getInitials(login: string): string {
|
||||
return login
|
||||
.replace(/\[bot\]$/i, '')
|
||||
.split(/[-_\s]/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map(part => part[0]?.toUpperCase())
|
||||
.join('') || login.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function normalizeContributor(contributor: GitHubContributor): Contributor | null {
|
||||
if (!contributor.login) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
login: contributor.login,
|
||||
avatarUrl: contributor.avatar_url,
|
||||
contributions: contributor.contributions ?? 0,
|
||||
type: contributor.type ?? 'User',
|
||||
href: contributor.html_url ?? `https://github.com/${contributor.login}`,
|
||||
};
|
||||
}
|
||||
|
||||
function useGitHubContributors(): Contributor[] {
|
||||
const [contributors, setContributors] = useState<Contributor[]>(FALLBACK_CONTRIBUTORS);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch('https://api.github.com/repos/Awuqing/BackupX/contributors?per_page=12', {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub contributors request failed: ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<GitHubContributor[]>;
|
||||
})
|
||||
.then(payload => {
|
||||
const nextContributors = payload
|
||||
.map(normalizeContributor)
|
||||
.filter((contributor): contributor is Contributor => Boolean(contributor));
|
||||
|
||||
if (nextContributors.length > 0) {
|
||||
setContributors(nextContributors);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
console.warn(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
return contributors;
|
||||
}
|
||||
|
||||
function ContributorCard({login, avatarUrl, contributions, type, href}: Contributor) {
|
||||
return (
|
||||
<Link className={styles.contributorCard} to={href}>
|
||||
{avatarUrl ? (
|
||||
<img className={styles.avatarImage} src={avatarUrl} alt="" loading="lazy" />
|
||||
) : (
|
||||
<span className={styles.avatar} aria-hidden="true">{getInitials(login)}</span>
|
||||
)}
|
||||
<span className={styles.contributorBody}>
|
||||
<strong>{login}</strong>
|
||||
<span>
|
||||
{type === 'Bot' ? (
|
||||
<Translate id="community.contributor.botRole">Automation contributor</Translate>
|
||||
) : (
|
||||
<Translate id="community.contributor.githubRole">GitHub contributor</Translate>
|
||||
)}
|
||||
</span>
|
||||
<em>
|
||||
<Translate id="community.contributor.contributions" values={{count: contributions}}>
|
||||
{'{count} contributions'}
|
||||
</Translate>
|
||||
</em>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomepageSponsors(): ReactNode {
|
||||
return (
|
||||
<div className={styles.sponsorWall}>
|
||||
<div className={styles.sponsorWallHeader}>
|
||||
<Heading as="h3" className={styles.sponsorWallTitle}>
|
||||
<Translate id="community.sponsor.wallTitle">Sponsors</Translate>
|
||||
</Heading>
|
||||
<Link className={styles.sponsorWallAction} to="https://github.com/sponsors/Awuqing">
|
||||
<Translate id="community.sponsor.cta">Sponsor BackupX</Translate>
|
||||
<span aria-hidden="true">-></span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.sponsorLogoGrid}>
|
||||
{SPONSOR_SLOTS.map((slot, index) => (
|
||||
<SponsorLogoCard key={index} {...slot} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageCommunity(): ReactNode {
|
||||
const contributors = useGitHubContributors();
|
||||
|
||||
return (
|
||||
<section id="community" className={styles.section}>
|
||||
<div className="container">
|
||||
<div className={styles.sectionHead}>
|
||||
<div className={styles.sectionTag}>
|
||||
<Translate id="community.tag">COMMUNITY</Translate>
|
||||
</div>
|
||||
<Heading as="h2" className={styles.sectionTitle}>
|
||||
<Translate id="community.title">Built in the open, ready for long-term operators</Translate>
|
||||
</Heading>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
<Translate id="community.subtitle">
|
||||
Backup software earns trust through transparent releases, real deployment feedback and a contributor path that stays practical.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HomepageSponsors />
|
||||
|
||||
<div className={styles.communityGrid}>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span>
|
||||
<Translate id="community.contributor.kicker">Contributors</Translate>
|
||||
</span>
|
||||
<Link to="https://github.com/Awuqing/BackupX/graphs/contributors">
|
||||
<Translate id="community.contributor.all">View all</Translate>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.panelNote}>
|
||||
<Translate id="community.contributor.source">Loaded from GitHub contributors API in the browser.</Translate>
|
||||
</div>
|
||||
<div className={styles.contributorList}>
|
||||
{contributors.map(contributor => (
|
||||
<ContributorCard key={contributor.login} {...contributor} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<span>
|
||||
<Translate id="community.path.kicker">Contributor paths</Translate>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.pathList}>
|
||||
{COMMUNITY_PATHS.map((path, index) => (
|
||||
<Link key={index} className={styles.pathItem} to={path.href}>
|
||||
<span className={styles.pathIndex}>{String(index + 1).padStart(2, '0')}</span>
|
||||
<span>
|
||||
<strong>{path.title}</strong>
|
||||
<em>{path.description}</em>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
429
docs-site/src/components/HomepageCommunity/styles.module.css
Normal file
429
docs-site/src/components/HomepageCommunity/styles.module.css
Normal file
@@ -0,0 +1,429 @@
|
||||
.section {
|
||||
padding: 5.5rem 0 6rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 247, 250, 0) 0%, rgba(245, 247, 250, 0.86) 100%),
|
||||
var(--ifm-background-color);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .section {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 17, 21, 0) 0%, rgba(255, 255, 255, 0.03) 100%),
|
||||
var(--ifm-background-color);
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
max-width: 760px;
|
||||
margin: 0 auto 2.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sectionTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 4px 10px;
|
||||
color: #00a870;
|
||||
background: rgba(0, 180, 42, 0.1);
|
||||
border: 1px solid rgba(0, 180, 42, 0.18);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 2.35rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
margin: 0;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 1.04rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.sponsorWall {
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--ifm-background-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 28px rgba(29, 33, 41, 0.06);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sponsorWall {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sponsorWallHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
min-height: 60px;
|
||||
padding: 0 1.25rem;
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sponsorWallHeader {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sponsorWallTitle {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.sponsorWallTitle::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
content: "";
|
||||
background: #52c41a;
|
||||
border-radius: 3px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sponsorWallAction {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
color: #52c41a;
|
||||
background: rgba(82, 196, 26, 0.08);
|
||||
border: 1px solid rgba(82, 196, 26, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none !important;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sponsorWallAction:hover,
|
||||
.sponsorWallAction:focus-visible {
|
||||
color: #389e0d;
|
||||
background: rgba(82, 196, 26, 0.14);
|
||||
border-color: #52c41a;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sponsorLogoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
gap: 1px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sponsorLogoGrid {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sponsorLogoTile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
min-height: 106px;
|
||||
padding: 14px 10px;
|
||||
flex-direction: column;
|
||||
color: inherit;
|
||||
background: var(--ifm-background-color);
|
||||
text-align: center;
|
||||
text-decoration: none !important;
|
||||
transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sponsorLogoTile {
|
||||
background: rgba(15, 17, 21, 0.78);
|
||||
}
|
||||
|
||||
.sponsorLogoTile:hover,
|
||||
.sponsorLogoTile:focus-visible {
|
||||
z-index: 1;
|
||||
color: inherit;
|
||||
background: rgba(82, 196, 26, 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgba(82, 196, 26, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sponsorLogoMark {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--ifm-color-primary);
|
||||
font-size: 1.45rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.sponsorLogoTile:nth-child(2n) .sponsorLogoMark {
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
.sponsorLogoTile:nth-child(3n) .sponsorLogoMark {
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
.sponsorLogoTile:nth-child(4n) .sponsorLogoMark {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.sponsorLogoTile:nth-child(5n) .sponsorLogoMark {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.sponsorLogoName {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--ifm-background-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 28px rgba(29, 33, 41, 0.06);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .panel {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.communityGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.panelHeader a {
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.panelNote {
|
||||
margin: -0.35rem 0 1rem;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.contributorList,
|
||||
.pathList {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.contributorCard,
|
||||
.pathItem {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
color: inherit;
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
text-decoration: none !important;
|
||||
transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.contributorCard:hover,
|
||||
.contributorCard:focus-visible,
|
||||
.pathItem:hover,
|
||||
.pathItem:focus-visible {
|
||||
color: inherit;
|
||||
background: var(--ifm-background-color);
|
||||
border-color: var(--ifm-color-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .contributorCard,
|
||||
[data-theme='dark'] .pathItem {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.contributorCard {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
background: #165dff;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.contributorCard:nth-child(2) .avatar {
|
||||
background: #00a870;
|
||||
}
|
||||
|
||||
.contributorCard:nth-child(3) .avatar {
|
||||
background: #ff7d00;
|
||||
}
|
||||
|
||||
.contributorBody {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.contributorBody strong {
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.contributorBody span {
|
||||
color: var(--ifm-color-content);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.contributorBody em,
|
||||
.pathItem em {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.82rem;
|
||||
font-style: normal;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pathItem {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.pathIndex {
|
||||
color: var(--ifm-color-primary);
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pathItem strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
@media (max-width: 996px) {
|
||||
.section {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.communityGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sponsorLogoGrid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sponsorLogoTile {
|
||||
min-height: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.section {
|
||||
padding: 3.25rem 0;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.sponsorWallHeader {
|
||||
display: grid;
|
||||
min-height: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sponsorWallAction {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sponsorLogoGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sponsorLogoMark {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sponsorWallAction,
|
||||
.sponsorLogoTile,
|
||||
.contributorCard,
|
||||
.pathItem {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
172
docs-site/src/components/HomepageFeatures/index.tsx
Normal file
172
docs-site/src/components/HomepageFeatures/index.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import Heading from '@theme/Heading';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Link from '@docusaurus/Link';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type FeatureItem = {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
icon: ReactNode;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const DatabaseIcon = () => (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||||
<path d="M3 5v6c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
||||
<path d="M3 11v6c0 1.66 4 3 9 3s9-1.34 9-3v-6" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CloudIcon = () => (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M18 10h-1.26A8 8 0 109 20h9a5 5 0 000-10z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ClockIcon = () => (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const NetworkIcon = () => (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<rect x="9" y="2" width="6" height="6" rx="1" />
|
||||
<rect x="2" y="16" width="6" height="6" rx="1" />
|
||||
<rect x="16" y="16" width="6" height="6" rx="1" />
|
||||
<path d="M12 8v4" />
|
||||
<path d="M12 12H5v4" />
|
||||
<path d="M12 12h7v4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ShieldIcon = () => (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 2l9 4v6c0 5-3.5 9.5-9 10-5.5-.5-9-5-9-10V6l9-4z" />
|
||||
<polyline points="9 12 11 14 15 10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const RocketIcon = () => (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z" />
|
||||
<path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const FEATURES: FeatureItem[] = [
|
||||
{
|
||||
title: <Translate id="feat.types.title">Many Backup Types</Translate>,
|
||||
description: (
|
||||
<Translate id="feat.types.desc">
|
||||
Files and directories with multi-path sources, plus MySQL, PostgreSQL, SQLite, and SAP HANA — all in one place.
|
||||
</Translate>
|
||||
),
|
||||
icon: <DatabaseIcon />,
|
||||
link: '/docs/features/backup-types',
|
||||
},
|
||||
{
|
||||
title: <Translate id="feat.storage.title">70+ Storage Backends</Translate>,
|
||||
description: (
|
||||
<Translate id="feat.storage.desc">
|
||||
Native Alibaba OSS, Tencent COS, Qiniu, S3, Google Drive, WebDAV, FTP — plus SFTP, Azure Blob, Dropbox and more via rclone.
|
||||
</Translate>
|
||||
),
|
||||
icon: <CloudIcon />,
|
||||
link: '/docs/features/storage-backends',
|
||||
},
|
||||
{
|
||||
title: <Translate id="feat.scheduling.title">Scheduling & Retention</Translate>,
|
||||
description: (
|
||||
<Translate id="feat.scheduling.desc">
|
||||
Cron-based schedules with a visual editor and auto-retention (by days or count), plus empty-directory cleanup.
|
||||
</Translate>
|
||||
),
|
||||
icon: <ClockIcon />,
|
||||
},
|
||||
{
|
||||
title: <Translate id="feat.cluster.title">Multi-Node Cluster</Translate>,
|
||||
description: (
|
||||
<Translate id="feat.cluster.desc">
|
||||
Outbound-only Master-Agent polling with proxy, private-CA, and SSH-bastion support. No reverse connectivity is required.
|
||||
</Translate>
|
||||
),
|
||||
icon: <NetworkIcon />,
|
||||
link: '/docs/features/multi-node',
|
||||
},
|
||||
{
|
||||
title: <Translate id="feat.security.title">Secure by Default</Translate>,
|
||||
description: (
|
||||
<Translate id="feat.security.desc">
|
||||
JWT auth, bcrypt passwords, AES-256-GCM encrypted config, optional backup encryption, and a full audit log.
|
||||
</Translate>
|
||||
),
|
||||
icon: <ShieldIcon />,
|
||||
},
|
||||
{
|
||||
title: <Translate id="feat.deploy.title">Painless Deployment</Translate>,
|
||||
description: (
|
||||
<Translate id="feat.deploy.desc">
|
||||
Single static binary with embedded SQLite. Docker one-click or bare-metal — zero external dependencies.
|
||||
</Translate>
|
||||
),
|
||||
icon: <RocketIcon />,
|
||||
link: '/docs/getting-started/installation',
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({title, description, icon, link}: FeatureItem) {
|
||||
const content = (
|
||||
<>
|
||||
<div className={styles.iconWrap}>{icon}</div>
|
||||
<Heading as="h3" className={styles.featureTitle}>{title}</Heading>
|
||||
<p className={styles.featureDesc}>{description}</p>
|
||||
{link && (
|
||||
<span className={styles.featureLink}>
|
||||
<Translate id="feat.learnMore">Learn more</Translate>
|
||||
<span className={styles.featureArrow} aria-hidden="true">-></span>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (link) {
|
||||
return (
|
||||
<Link to={link} className={styles.featureCardLink}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <div className={styles.featureCard}>{content}</div>;
|
||||
}
|
||||
|
||||
export default function HomepageFeatures(): ReactNode {
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div className="container">
|
||||
<div className={styles.sectionHead}>
|
||||
<div className={styles.sectionTag}>
|
||||
<Translate id="section.features.tag">FEATURES</Translate>
|
||||
</div>
|
||||
<Heading as="h2" className={styles.sectionTitle}>
|
||||
<Translate id="section.features.title">Everything you need, nothing you don't</Translate>
|
||||
</Heading>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
<Translate id="section.features.subtitle">
|
||||
Battle-tested building blocks — backup runners, storage providers, scheduling, and clustering.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.grid}>
|
||||
{FEATURES.map((feat, idx) => (
|
||||
<Feature key={idx} {...feat} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
169
docs-site/src/components/HomepageFeatures/styles.module.css
Normal file
169
docs-site/src/components/HomepageFeatures/styles.module.css
Normal file
@@ -0,0 +1,169 @@
|
||||
.section {
|
||||
padding: 5.5rem 0 4.25rem;
|
||||
background: var(--ifm-background-color);
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
text-align: center;
|
||||
max-width: 720px;
|
||||
margin: 0 auto 3rem;
|
||||
}
|
||||
|
||||
.sectionTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
color: var(--ifm-color-primary);
|
||||
padding: 4px 12px;
|
||||
background: rgba(22, 93, 255, 0.08);
|
||||
border: 1px solid rgba(22, 93, 255, 0.16);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sectionTag {
|
||||
background: rgba(96, 126, 255, 0.18);
|
||||
color: var(--ifm-color-primary-lighter);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 2.35rem;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
font-weight: 750;
|
||||
margin: 0 0 1rem;
|
||||
color: var(--ifm-heading-color);
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.65;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 996px) {
|
||||
.section {
|
||||
padding: 3.5rem 0 2rem;
|
||||
}
|
||||
.sectionTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 997px) and (max-width: 1200px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.featureCard,
|
||||
.featureCardLink {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.75rem;
|
||||
background: var(--ifm-background-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
text-decoration: none !important;
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.featureCardLink:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 12px 30px -8px rgba(22, 93, 255, 0.18);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .featureCard,
|
||||
[data-theme='dark'] .featureCardLink {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .featureCardLink:hover {
|
||||
background: rgba(64, 128, 255, 0.05);
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 12px 30px -8px rgba(64, 128, 255, 0.25);
|
||||
}
|
||||
|
||||
.iconWrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(22, 93, 255, 0.1) 0%, rgba(20, 201, 201, 0.12) 100%);
|
||||
color: var(--ifm-color-primary);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .iconWrap {
|
||||
background: linear-gradient(135deg, rgba(96, 126, 255, 0.15) 0%, rgba(20, 201, 201, 0.12) 100%);
|
||||
color: var(--ifm-color-primary-lighter);
|
||||
}
|
||||
|
||||
.featureTitle {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.6rem;
|
||||
color: var(--ifm-heading-color);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.featureDesc {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.featureLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 1rem;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.featureArrow {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.featureCardLink:hover .featureArrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.sectionTitle {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.featureCard,
|
||||
.featureCardLink,
|
||||
.featureArrow {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
120
docs-site/src/components/HomepageShowcase/index.tsx
Normal file
120
docs-site/src/components/HomepageShowcase/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Heading from '@theme/Heading';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import Link from '@docusaurus/Link';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type Tab = {
|
||||
id: string;
|
||||
label: ReactNode;
|
||||
image: string;
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
};
|
||||
|
||||
function useTabs(): Tab[] {
|
||||
return [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: <Translate id="showcase.tab.dashboard">Dashboard</Translate>,
|
||||
image: useBaseUrl('/img/screenshots/dashboard.png'),
|
||||
title: <Translate id="showcase.dashboard.title">Know at a glance</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.dashboard.desc">
|
||||
Backup success rates, storage usage, recent runs and upcoming schedules — all on one page with live data.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'tasks',
|
||||
label: <Translate id="showcase.tab.tasks">Backup Tasks</Translate>,
|
||||
image: useBaseUrl('/img/screenshots/backup-tasks.png'),
|
||||
title: <Translate id="showcase.tasks.title">Visual task editor</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.tasks.desc">
|
||||
Files, MySQL, PostgreSQL, SQLite and SAP HANA with a three-step wizard. Cron editor, multi-target dispatch, retention, compression and encryption — point and click.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
label: <Translate id="showcase.tab.storage">Storage Targets</Translate>,
|
||||
image: useBaseUrl('/img/screenshots/storage-targets.png'),
|
||||
title: <Translate id="showcase.storage.title">70+ backends, one flow</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.storage.desc">
|
||||
Alibaba OSS, Tencent COS, S3, Google Drive, WebDAV — plus every rclone backend behind a uniform form. Test connection, favourite, and view live usage.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
label: <Translate id="showcase.tab.nodes">Multi-Node</Translate>,
|
||||
image: useBaseUrl('/img/screenshots/nodes.png'),
|
||||
title: <Translate id="showcase.nodes.title">Master-Agent in minutes</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.nodes.desc">
|
||||
Create a node, copy the token, start the Agent on any remote host. Tasks routed to a node run locally there and upload directly to storage — no reverse connectivity required.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function HomepageShowcase(): ReactNode {
|
||||
const tabs = useTabs();
|
||||
const [active, setActive] = useState(tabs[0].id);
|
||||
const current = tabs.find(t => t.id === active) ?? tabs[0];
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div className="container">
|
||||
<div className={styles.sectionHead}>
|
||||
<div className={styles.sectionTag}>
|
||||
<Translate id="showcase.tag">PRODUCT</Translate>
|
||||
</div>
|
||||
<Heading as="h2" className={styles.sectionTitle}>
|
||||
<Translate id="showcase.title">A polished console, not a DIY script</Translate>
|
||||
</Heading>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
<Translate id="showcase.subtitle">
|
||||
Every screen designed for day-2 operations — visibility first, configuration second.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.tabs}>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
className={clsx(styles.tabBtn, active === tab.id && styles.tabBtnActive)}
|
||||
onClick={() => setActive(tab.id)}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.stage}>
|
||||
<div className={styles.browser}>
|
||||
<div className={styles.browserBar}>
|
||||
<span className={clsx(styles.browserDot, styles.browserDotRed)} />
|
||||
<span className={clsx(styles.browserDot, styles.browserDotYellow)} />
|
||||
<span className={clsx(styles.browserDot, styles.browserDotGreen)} />
|
||||
<div className={styles.browserUrl}>backupx.local</div>
|
||||
</div>
|
||||
<img src={current.image} alt="" className={styles.screenshot} />
|
||||
</div>
|
||||
<div className={styles.caption}>
|
||||
<Heading as="h3" className={styles.captionTitle}>{current.title}</Heading>
|
||||
<p className={styles.captionDesc}>{current.description}</p>
|
||||
<Link to="/docs/getting-started/quick-start" className={styles.captionLink}>
|
||||
<Translate id="showcase.cta">Explore the docs</Translate>
|
||||
<span aria-hidden="true"> -></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
247
docs-site/src/components/HomepageShowcase/styles.module.css
Normal file
247
docs-site/src/components/HomepageShowcase/styles.module.css
Normal file
@@ -0,0 +1,247 @@
|
||||
.section {
|
||||
padding: 4.5rem 0 5.5rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 247, 250, 0) 0%, rgba(245, 247, 250, 0.72) 100%),
|
||||
var(--ifm-background-color);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .section {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 17, 21, 0) 0%, rgba(255, 255, 255, 0.03) 100%),
|
||||
var(--ifm-background-color);
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
text-align: center;
|
||||
max-width: 720px;
|
||||
margin: 0 auto 2.5rem;
|
||||
}
|
||||
|
||||
.sectionTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
color: #0e7490;
|
||||
padding: 4px 12px;
|
||||
background: rgba(20, 201, 201, 0.1);
|
||||
border: 1px solid rgba(20, 201, 201, 0.2);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sectionTag {
|
||||
background: rgba(20, 201, 201, 0.16);
|
||||
color: #67e8f9;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 2.35rem;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
font-weight: 750;
|
||||
margin: 0 0 1rem;
|
||||
color: var(--ifm-heading-color);
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.65;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px;
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tabBtn {
|
||||
min-height: 40px;
|
||||
padding: 8px 18px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.tabBtn:hover {
|
||||
color: var(--ifm-color-primary);
|
||||
background: var(--ifm-background-color);
|
||||
}
|
||||
|
||||
.tabBtnActive,
|
||||
.tabBtnActive:hover {
|
||||
background: var(--ifm-background-color);
|
||||
color: var(--ifm-color-primary) !important;
|
||||
border-color: rgba(22, 93, 255, 0.18);
|
||||
box-shadow: 0 6px 16px rgba(22, 93, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Stage */
|
||||
.stage {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: 3rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 996px) {
|
||||
.stage {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.browser {
|
||||
background: var(--ifm-background-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 24px 58px -22px rgba(22, 93, 255, 0.28),
|
||||
0 0 0 1px var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .browser {
|
||||
box-shadow:
|
||||
0 30px 60px -20px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.browserBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .browserBar {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.browserDot {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.browserDotRed { background: #ff5f56; }
|
||||
.browserDotYellow { background: #ffbd2e; }
|
||||
.browserDotGreen { background: #27c93f; }
|
||||
|
||||
.browserUrl {
|
||||
margin: 0 auto;
|
||||
padding: 3px 14px;
|
||||
background: var(--ifm-background-color);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-family: 'SFMono-Regular', Menlo, monospace;
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .browserUrl {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.screenshot {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.caption {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 996px) {
|
||||
.caption {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.captionTitle {
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
font-weight: 750;
|
||||
margin: 0 0 1rem;
|
||||
color: var(--ifm-heading-color);
|
||||
}
|
||||
|
||||
.captionDesc {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.7;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.captionLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(22, 93, 255, 0.18);
|
||||
border-radius: 8px;
|
||||
font-weight: 650;
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: none !important;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.captionLink:hover {
|
||||
color: var(--ifm-color-primary-dark);
|
||||
background: rgba(22, 93, 255, 0.06);
|
||||
border-color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 996px) {
|
||||
.sectionTitle {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.section {
|
||||
padding: 3.25rem 0 4rem;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.tabBtn {
|
||||
flex: 1 1 130px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tabBtn,
|
||||
.captionLink {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
264
docs-site/src/css/custom.css
Normal file
264
docs-site/src/css/custom.css
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* BackupX 官方文档站样式
|
||||
* 灵感:Ant Design / Arco Design
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Primary palette (Arco blue) */
|
||||
--ifm-color-primary: #165dff;
|
||||
--ifm-color-primary-dark: #0e4fe6;
|
||||
--ifm-color-primary-darker: #0b4bd9;
|
||||
--ifm-color-primary-darkest: #093eb3;
|
||||
--ifm-color-primary-light: #2f6cff;
|
||||
--ifm-color-primary-lighter: #3d75ff;
|
||||
--ifm-color-primary-lightest: #668eff;
|
||||
|
||||
/* Surfaces */
|
||||
--ifm-background-color: #ffffff;
|
||||
--ifm-background-surface-color: #ffffff;
|
||||
--ifm-color-emphasis-100: #f5f7fa;
|
||||
--ifm-color-emphasis-200: #e5e6eb;
|
||||
--ifm-color-emphasis-300: #c9cdd4;
|
||||
--ifm-color-emphasis-400: #a9aeb8;
|
||||
|
||||
/* Typography */
|
||||
--ifm-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
--ifm-font-family-monospace: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
--ifm-heading-font-weight: 700;
|
||||
--ifm-code-font-size: 92%;
|
||||
--ifm-h1-font-size: 2.25rem;
|
||||
--ifm-h2-font-size: 1.75rem;
|
||||
--ifm-h3-font-size: 1.35rem;
|
||||
--ifm-line-height-base: 1.7;
|
||||
|
||||
--ifm-color-content: #1d2129;
|
||||
--ifm-color-content-secondary: #4e5969;
|
||||
--ifm-heading-color: #1d2129;
|
||||
--ifm-global-radius: 8px;
|
||||
|
||||
/* Navbar */
|
||||
--ifm-navbar-height: 64px;
|
||||
--ifm-navbar-background-color: rgba(255, 255, 255, 0.9);
|
||||
--ifm-navbar-link-color: #4e5969;
|
||||
--ifm-navbar-link-hover-color: var(--ifm-color-primary);
|
||||
|
||||
/* Sidebar */
|
||||
--ifm-menu-color: #4e5969;
|
||||
--ifm-menu-color-background-active: rgba(22, 93, 255, 0.08);
|
||||
--ifm-menu-color-background-hover: var(--ifm-color-emphasis-100);
|
||||
|
||||
/* Code */
|
||||
--ifm-code-background: rgba(22, 93, 255, 0.06);
|
||||
--docusaurus-highlighted-code-line-bg: rgba(22, 93, 255, 0.08);
|
||||
|
||||
/* Hero background helper (consumed in index.module.css) */
|
||||
--bx-hero-bg: transparent;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #4080ff;
|
||||
--ifm-color-primary-dark: #3371f2;
|
||||
--ifm-color-primary-darker: #2c6ae6;
|
||||
--ifm-color-primary-darkest: #2359c7;
|
||||
--ifm-color-primary-light: #5a93ff;
|
||||
--ifm-color-primary-lighter: #74a5ff;
|
||||
--ifm-color-primary-lightest: #9dbfff;
|
||||
|
||||
--ifm-background-color: #0f1115;
|
||||
--ifm-background-surface-color: #16181d;
|
||||
--ifm-color-emphasis-100: #1d2129;
|
||||
--ifm-color-emphasis-200: #272e3b;
|
||||
--ifm-color-emphasis-300: #384252;
|
||||
--ifm-color-emphasis-400: #4e5969;
|
||||
|
||||
--ifm-color-content: #e6e9ef;
|
||||
--ifm-color-content-secondary: #9aa3b2;
|
||||
--ifm-heading-color: #f0f2f5;
|
||||
|
||||
--ifm-navbar-background-color: rgba(15, 17, 21, 0.9);
|
||||
--ifm-navbar-link-color: #c9d1db;
|
||||
|
||||
--ifm-menu-color: #c9d1db;
|
||||
--ifm-menu-color-background-active: rgba(64, 128, 255, 0.15);
|
||||
--ifm-menu-color-background-hover: rgba(255, 255, 255, 0.04);
|
||||
|
||||
--ifm-code-background: rgba(64, 128, 255, 0.14);
|
||||
--docusaurus-highlighted-code-line-bg: rgba(64, 128, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Frosted-glass navbar */
|
||||
.navbar {
|
||||
backdrop-filter: saturate(180%) blur(10px);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(10px);
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.navbar__title {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.navbar__link {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar__link,
|
||||
.button,
|
||||
a {
|
||||
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-radius: 8px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--ifm-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Sidebar tweaks */
|
||||
.menu__link {
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.menu__link--active,
|
||||
.menu__link--active:hover {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-doc-sidebar-container {
|
||||
border-right: 1px solid var(--ifm-color-emphasis-200) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .theme-doc-sidebar-container {
|
||||
border-right-color: rgba(255, 255, 255, 0.06) !important;
|
||||
}
|
||||
|
||||
/* Article: better heading rhythm */
|
||||
.markdown h2 {
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .markdown h2 {
|
||||
border-top-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.markdown table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px var(--ifm-color-emphasis-200);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.markdown table thead tr {
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.markdown table th,
|
||||
.markdown table td {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.markdown table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
code {
|
||||
background: var(--ifm-code-background);
|
||||
border: none;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
/* Admonitions: softer */
|
||||
.theme-admonition {
|
||||
border-radius: 8px;
|
||||
border-width: 1px;
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
--ifm-footer-background-color: #141720;
|
||||
--ifm-footer-color: #9aa3b2;
|
||||
--ifm-footer-link-color: #c9d1db;
|
||||
--ifm-footer-link-hover-color: #ffffff;
|
||||
--ifm-footer-title-color: #f0f2f5;
|
||||
padding: 3.5rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.footer__title {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer__link-item {
|
||||
font-size: 14px;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.footer__bottom {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding-top: 2rem;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--ifm-color-emphasis-300);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--ifm-color-emphasis-400);
|
||||
}
|
||||
|
||||
[data-theme='dark'] ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
19
docs-site/src/pages/community.tsx
Normal file
19
docs-site/src/pages/community.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import Layout from '@theme/Layout';
|
||||
import HomepageCommunity from '@site/src/components/HomepageCommunity';
|
||||
|
||||
export default function Community(): ReactNode {
|
||||
return (
|
||||
<Layout
|
||||
title={translate({id: 'community.pageTitle', message: 'Community, sponsors and contributors'})}
|
||||
description={translate({
|
||||
id: 'community.pageDescription',
|
||||
message: 'Sponsor BackupX, meet contributors, and find practical ways to contribute.',
|
||||
})}>
|
||||
<main>
|
||||
<HomepageCommunity />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
470
docs-site/src/pages/index.module.css
Normal file
470
docs-site/src/pages/index.module.css
Normal file
@@ -0,0 +1,470 @@
|
||||
/* Hero */
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 7rem 0 5.5rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(22, 93, 255, 0.08) 0%, rgba(255, 255, 255, 0) 72%),
|
||||
linear-gradient(90deg, rgba(20, 201, 201, 0.08) 0%, rgba(250, 173, 20, 0.08) 100%),
|
||||
var(--ifm-background-color);
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(22, 93, 255, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(22, 93, 255, 0.06) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.75), transparent 82%);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .hero {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(64, 128, 255, 0.16) 0%, rgba(15, 17, 21, 0) 72%),
|
||||
linear-gradient(90deg, rgba(20, 201, 201, 0.1) 0%, rgba(250, 173, 20, 0.08) 100%),
|
||||
var(--ifm-background-color);
|
||||
}
|
||||
|
||||
.heroInner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(420px, 0.9fr);
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heroContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 5px 12px;
|
||||
color: var(--ifm-color-primary);
|
||||
background: rgba(22, 93, 255, 0.09);
|
||||
border: 1px solid rgba(22, 93, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .badge {
|
||||
background: rgba(64, 128, 255, 0.16);
|
||||
border-color: rgba(64, 128, 255, 0.3);
|
||||
color: var(--ifm-color-primary-lighter);
|
||||
}
|
||||
|
||||
.badgeDot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #00b42a;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4px rgba(0, 180, 42, 0.12);
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
margin: 0;
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 3.45rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.heroTitleAccent {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.heroSubtitle {
|
||||
max-width: 640px;
|
||||
margin: 0;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.72;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.primaryBtn,
|
||||
.secondaryBtn {
|
||||
min-height: 46px;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.primaryBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
background: #165dff;
|
||||
border: 1px solid #165dff;
|
||||
box-shadow: 0 10px 24px rgba(22, 93, 255, 0.24);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.primaryBtn:hover,
|
||||
.primaryBtn:focus-visible {
|
||||
color: #fff;
|
||||
background: #0e4fe6;
|
||||
border-color: #0e4fe6;
|
||||
box-shadow: 0 14px 30px rgba(22, 93, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btnArrow {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.primaryBtn:hover .btnArrow,
|
||||
.primaryBtn:focus-visible .btnArrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.secondaryBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--ifm-font-color-base);
|
||||
background: var(--ifm-background-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.secondaryBtn:hover,
|
||||
.secondaryBtn:focus-visible {
|
||||
color: var(--ifm-color-primary);
|
||||
border-color: var(--ifm-color-primary);
|
||||
background: var(--ifm-background-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 1.35rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metricLabel {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.35;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.metricDivider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background: var(--ifm-color-emphasis-300);
|
||||
}
|
||||
|
||||
/* Product visual */
|
||||
.heroVisual {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.consolePanel {
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(22, 93, 255, 0.16);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 24px 60px rgba(29, 33, 41, 0.12);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .consolePanel {
|
||||
background: rgba(22, 24, 29, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
.consoleHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .consoleHeader {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.consoleHeader strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.consoleEyebrow {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.consoleStatus {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 4px 10px;
|
||||
color: #00a870;
|
||||
background: rgba(0, 180, 42, 0.1);
|
||||
border: 1px solid rgba(0, 180, 42, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.consoleGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .consoleGrid {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.consoleGrid > div {
|
||||
min-width: 0;
|
||||
padding: 1.1rem 1.25rem;
|
||||
border-right: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .consoleGrid > div {
|
||||
border-right-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.consoleGrid > div:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.consoleGrid strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.consoleLabel {
|
||||
display: block;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.timelineRow {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .timelineRow {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.timelineRow:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.timelineRow strong,
|
||||
.timelineRow span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.timelineRow strong {
|
||||
color: var(--ifm-heading-color);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.timelineRow span {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.timelineRow em {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-style: normal;
|
||||
font-weight: 650;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timelineDotOk,
|
||||
.timelineDotInfo,
|
||||
.timelineDotWarn {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.timelineDotOk {
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
.timelineDotInfo {
|
||||
background: #165dff;
|
||||
}
|
||||
|
||||
.timelineDotWarn {
|
||||
background: #ff7d00;
|
||||
}
|
||||
|
||||
.commandCard {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 1rem 1.1rem;
|
||||
background: #111827;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 16px 34px rgba(17, 24, 39, 0.18);
|
||||
}
|
||||
|
||||
.commandTitle {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.commandCard code {
|
||||
overflow-x: auto;
|
||||
color: #e5e7eb;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 996px) {
|
||||
.hero {
|
||||
padding: 4.5rem 0 3.5rem;
|
||||
}
|
||||
|
||||
.heroInner {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2.25rem;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: 2.45rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero {
|
||||
padding: 3.75rem 0 2.75rem;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: 2.05rem;
|
||||
}
|
||||
|
||||
.heroSubtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primaryBtn,
|
||||
.secondaryBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
gap: 0.85rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metricDivider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.consoleHeader,
|
||||
.timelineRow {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.consoleGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.consoleGrid > div {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.consoleGrid > div:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .consoleGrid > div {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.primaryBtn,
|
||||
.secondaryBtn,
|
||||
.btnArrow {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
169
docs-site/src/pages/index.tsx
Normal file
169
docs-site/src/pages/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Layout from '@theme/Layout';
|
||||
import Heading from '@theme/Heading';
|
||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||
import HomepageShowcase from '@site/src/components/HomepageShowcase';
|
||||
import HomepageCommunity from '@site/src/components/HomepageCommunity';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
function HomepageHeader() {
|
||||
return (
|
||||
<header className={styles.hero}>
|
||||
<div className={clsx('container', styles.heroInner)}>
|
||||
<div className={styles.heroContent}>
|
||||
<div className={styles.badge}>
|
||||
<span className={styles.badgeDot} />
|
||||
<Translate id="home.badge">Open-source backup control plane · v2.2.1</Translate>
|
||||
</div>
|
||||
<Heading as="h1" className={styles.heroTitle}>
|
||||
<Translate id="home.title.part1">Backup orchestration</Translate>
|
||||
<span className={styles.heroTitleAccent}>
|
||||
<Translate id="home.title.part2">for self-hosted servers.</Translate>
|
||||
</span>
|
||||
</Heading>
|
||||
<p className={styles.heroSubtitle}>
|
||||
<Translate id="home.tagline">
|
||||
Run file, database, SAP HANA and remote-node backups from one clean console. Keep the control plane yours, keep the storage flexible.
|
||||
</Translate>
|
||||
</p>
|
||||
<div className={styles.actions}>
|
||||
<Link className={clsx('button button--primary button--lg', styles.primaryBtn)} to="/docs/getting-started/quick-start">
|
||||
<Translate id="home.getStarted">Get Started</Translate>
|
||||
<span className={styles.btnArrow} aria-hidden="true">-></span>
|
||||
</Link>
|
||||
<Link className={clsx('button button--lg', styles.secondaryBtn)} to="https://github.com/Awuqing/BackupX">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style={{marginRight: 6}}>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 005.47 7.59c.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
GitHub
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.metrics}>
|
||||
<div className={styles.metric}>
|
||||
<div className={styles.metricValue}>70+</div>
|
||||
<div className={styles.metricLabel}>
|
||||
<Translate id="home.metric.backends">Storage backends</Translate>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.metricDivider} />
|
||||
<div className={styles.metric}>
|
||||
<div className={styles.metricValue}>Agent</div>
|
||||
<div className={styles.metricLabel}>
|
||||
<Translate id="home.metric.backupTypes">Remote execution</Translate>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.metricDivider} />
|
||||
<div className={styles.metric}>
|
||||
<div className={styles.metricValue}>Apache 2.0</div>
|
||||
<div className={styles.metricLabel}>
|
||||
<Translate id="home.metric.license">License</Translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.heroVisual}>
|
||||
<div className={styles.consolePanel}>
|
||||
<div className={styles.consoleHeader}>
|
||||
<div>
|
||||
<span className={styles.consoleEyebrow}>
|
||||
<Translate id="home.visual.eyebrow">BackupX Console</Translate>
|
||||
</span>
|
||||
<strong>
|
||||
<Translate id="home.visual.title">Operations overview</Translate>
|
||||
</strong>
|
||||
</div>
|
||||
<span className={styles.consoleStatus}>
|
||||
<Translate id="home.visual.status">Healthy</Translate>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.consoleGrid}>
|
||||
<div>
|
||||
<span className={styles.consoleLabel}>
|
||||
<Translate id="home.visual.success">Success rate</Translate>
|
||||
</span>
|
||||
<strong>99.4%</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.consoleLabel}>
|
||||
<Translate id="home.visual.nodes">Active nodes</Translate>
|
||||
</span>
|
||||
<strong>12</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.consoleLabel}>
|
||||
<Translate id="home.visual.targets">Storage targets</Translate>
|
||||
</span>
|
||||
<strong>8</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.timeline}>
|
||||
<div className={styles.timelineRow}>
|
||||
<span className={styles.timelineDotOk} />
|
||||
<div>
|
||||
<strong>
|
||||
<Translate id="home.visual.row1.title">PostgreSQL nightly</Translate>
|
||||
</strong>
|
||||
<span>
|
||||
<Translate id="home.visual.row1.desc">Encrypted archive uploaded to S3</Translate>
|
||||
</span>
|
||||
</div>
|
||||
<em>02:10</em>
|
||||
</div>
|
||||
<div className={styles.timelineRow}>
|
||||
<span className={styles.timelineDotInfo} />
|
||||
<div>
|
||||
<strong>
|
||||
<Translate id="home.visual.row2.title">SAP HANA snapshot</Translate>
|
||||
</strong>
|
||||
<span>
|
||||
<Translate id="home.visual.row2.desc">Running on agent-shanghai-02</Translate>
|
||||
</span>
|
||||
</div>
|
||||
<em>68%</em>
|
||||
</div>
|
||||
<div className={styles.timelineRow}>
|
||||
<span className={styles.timelineDotWarn} />
|
||||
<div>
|
||||
<strong>
|
||||
<Translate id="home.visual.row3.title">Retention cleanup</Translate>
|
||||
</strong>
|
||||
<span>
|
||||
<Translate id="home.visual.row3.desc">Next run in 4 hours</Translate>
|
||||
</span>
|
||||
</div>
|
||||
<em>queued</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.commandCard}>
|
||||
<div className={styles.commandTitle}>
|
||||
<Translate id="home.command.title">Start with Docker</Translate>
|
||||
</div>
|
||||
<code>docker run -d -p 8340:8340 awuqing/backupx:v2.2.1</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home(): ReactNode {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={translate({id: 'home.pageTitle', message: 'Backup orchestration for self-hosted servers'})}
|
||||
description={siteConfig.tagline}>
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
<HomepageShowcase />
|
||||
<HomepageCommunity />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
39
docs-site/src/pages/sponsors.tsx
Normal file
39
docs-site/src/pages/sponsors.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Layout from '@theme/Layout';
|
||||
import Heading from '@theme/Heading';
|
||||
import {HomepageSponsors} from '@site/src/components/HomepageCommunity';
|
||||
import styles from '@site/src/components/HomepageCommunity/styles.module.css';
|
||||
|
||||
export default function Sponsors(): ReactNode {
|
||||
return (
|
||||
<Layout
|
||||
title={translate({id: 'sponsors.pageTitle', message: 'Sponsors'})}
|
||||
description={translate({
|
||||
id: 'sponsors.pageDescription',
|
||||
message: 'Sponsor BackupX reliability, documentation, storage compatibility and long-term maintenance.',
|
||||
})}>
|
||||
<main>
|
||||
<section className={styles.section}>
|
||||
<div className="container">
|
||||
<div className={styles.sectionHead}>
|
||||
<div className={styles.sectionTag}>
|
||||
<Translate id="sponsors.tag">SPONSORS</Translate>
|
||||
</div>
|
||||
<Heading as="h1" className={styles.sectionTitle}>
|
||||
<Translate id="sponsors.title">Sponsor the BackupX ecosystem</Translate>
|
||||
</Heading>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
<Translate id="sponsors.subtitle">
|
||||
Sponsorship helps keep BackupX practical for real operators: tested storage providers, reliable releases, restore confidence and better documentation.
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
<HomepageSponsors />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
0
docs-site/static/.nojekyll
Normal file
0
docs-site/static/.nojekyll
Normal file
BIN
docs-site/static/img/favicon.ico
Normal file
BIN
docs-site/static/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
11
docs-site/static/img/logo.svg
Normal file
11
docs-site/static/img/logo.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<defs>
|
||||
<linearGradient id="bxg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2f6cff"/>
|
||||
<stop offset="100%" stop-color="#0b3eb3"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="40" height="40" rx="8" fill="url(#bxg)"/>
|
||||
<path d="M16 14h10a5 5 0 0 1 0 10H16V14z" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 24h12a5 5 0 0 1 0 10H16V24z" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 639 B |
BIN
docs-site/static/img/screenshots/backup-tasks.png
Normal file
BIN
docs-site/static/img/screenshots/backup-tasks.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
BIN
docs-site/static/img/screenshots/dashboard.png
Normal file
BIN
docs-site/static/img/screenshots/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
docs-site/static/img/screenshots/nodes.png
Normal file
BIN
docs-site/static/img/screenshots/nodes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
docs-site/static/img/screenshots/storage-targets.png
Normal file
BIN
docs-site/static/img/screenshots/storage-targets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
12
docs-site/tsconfig.json
Normal file
12
docs-site/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
// This file is not used by "docusaurus start/build" commands.
|
||||
// It is here to improve your IDE experience (type-checking, autocompletion...),
|
||||
// and can also run the package.json "typecheck" script manually.
|
||||
{
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"strict": true
|
||||
},
|
||||
"exclude": [".docusaurus", "build"]
|
||||
}
|
||||
84
server/cmd/backupx/agent.go
Normal file
84
server/cmd/backupx/agent.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"backupx/server/internal/agent"
|
||||
)
|
||||
|
||||
// runAgent 是 `backupx agent` 子命令入口。
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// backupx agent --master http://master:8340 --token <token>
|
||||
// backupx agent --config /etc/backupx-agent.yaml
|
||||
//
|
||||
// 配置优先级:CLI 参数 > 配置文件 > 环境变量
|
||||
func runAgent(args []string) {
|
||||
fs := flag.NewFlagSet("agent", flag.ExitOnError)
|
||||
configPath := fs.String("config", "", "path to agent config YAML (optional)")
|
||||
master := fs.String("master", "", "master URL, e.g. http://master.example.com:8340")
|
||||
token := fs.String("token", "", "agent authentication token")
|
||||
tokenFile := fs.String("token-file", "", "read the agent authentication token from a file")
|
||||
tempDir := fs.String("temp-dir", "", "local temp directory for backup artifacts")
|
||||
proxyURL := fs.String("proxy-url", "", "HTTP(S) or SOCKS5 proxy used to reach the master")
|
||||
caCertFile := fs.String("ca-cert", "", "PEM CA certificate used to verify the master")
|
||||
insecureTLS := fs.Bool("insecure-tls", false, "skip TLS verification (testing only)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cfg, err := loadAgentConfig(*configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "agent: load config: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
cfg.ApplyOverrides(agent.Overrides{
|
||||
Master: *master,
|
||||
Token: *token,
|
||||
TokenFile: *tokenFile,
|
||||
TempDir: *tempDir,
|
||||
ProxyURL: *proxyURL,
|
||||
CACertFile: *caCertFile,
|
||||
})
|
||||
if *insecureTLS {
|
||||
cfg.InsecureSkipTLSVerify = true
|
||||
}
|
||||
if err := cfg.ResolveToken(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "agent: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "agent: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
a, err := agent.New(cfg, version)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "agent: init: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "backupx agent %s starting (master=%s)\n", version, cfg.Master)
|
||||
if err := a.Run(ctx); err != nil && err != context.Canceled {
|
||||
fmt.Fprintf(os.Stderr, "agent: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// loadAgentConfig 按优先级加载配置:如果提供了 --config 就用文件,否则走环境变量。
|
||||
func loadAgentConfig(configPath string) (*agent.Config, error) {
|
||||
if configPath != "" {
|
||||
return agent.LoadConfigFile(configPath)
|
||||
}
|
||||
return agent.LoadConfigFromEnv()
|
||||
}
|
||||
98
server/cmd/backupx/backint.go
Normal file
98
server/cmd/backupx/backint.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"backupx/server/internal/backint"
|
||||
)
|
||||
|
||||
// runBackint 是 `backupx backint` 子命令入口。
|
||||
//
|
||||
// CLI 参数遵循 SAP HANA Backint 规范:
|
||||
//
|
||||
// backupx backint -f <function> -i <input-file> -o <output-file> -p <param-file>
|
||||
// [-u <user>] [-c <config-prefix>] [-l <log-file>] [-v <version>]
|
||||
//
|
||||
// 除 -f / -i / -o / -p 外其余参数接受但忽略(兼容 SAP 调用约定)。
|
||||
func runBackint(args []string) {
|
||||
fs := flag.NewFlagSet("backint", flag.ExitOnError)
|
||||
fnStr := fs.String("f", "", "function: backup | restore | inquire | delete")
|
||||
inputPath := fs.String("i", "", "input file path")
|
||||
outputPath := fs.String("o", "", "output file path")
|
||||
paramFile := fs.String("p", "", "parameter file path")
|
||||
|
||||
// 以下参数仅为兼容 SAP 调用约定,当前未使用
|
||||
_ = fs.String("u", "", "user (ignored)")
|
||||
_ = fs.String("c", "", "config-prefix (ignored)")
|
||||
_ = fs.String("l", "", "log file override (ignored, use LOG_FILE in params)")
|
||||
_ = fs.String("v", "", "backint version (ignored)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *fnStr == "" || *inputPath == "" || *outputPath == "" || *paramFile == "" {
|
||||
fmt.Fprintln(os.Stderr, "backint: -f, -i, -o, -p are required")
|
||||
fs.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
fn, err := backint.ParseFunction(*fnStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "backint: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cfg, err := backint.LoadConfigFile(*paramFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "backint: load config: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// 配置日志重定向(如果指定 LOG_FILE)
|
||||
restoreLog, err := redirectStderr(cfg.LogFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "backint: open log: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
defer restoreLog()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
agent, err := backint.NewAgent(ctx, cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "backint: init agent: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = agent.Close() }()
|
||||
|
||||
if err := agent.Run(ctx, fn, *inputPath, *outputPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "backint: run: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// redirectStderr 将 stderr 重定向到指定日志文件,返回恢复函数。
|
||||
// 空字符串表示保持原样。
|
||||
func redirectStderr(path string) (func(), error) {
|
||||
if path == "" {
|
||||
return func() {}, nil
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orig := os.Stderr
|
||||
os.Stderr = f
|
||||
return func() {
|
||||
os.Stderr = orig
|
||||
_ = f.Close()
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ func main() {
|
||||
runResetPassword(os.Args[2:])
|
||||
return
|
||||
}
|
||||
// 子命令分发:backint(SAP HANA Backint Agent 模式)
|
||||
if len(os.Args) > 1 && os.Args[1] == "backint" {
|
||||
runBackint(os.Args[2:])
|
||||
return
|
||||
}
|
||||
// 子命令分发:agent(远程节点 Agent 模式)
|
||||
if len(os.Args) > 1 && os.Args[1] == "agent" {
|
||||
runAgent(os.Args[2:])
|
||||
return
|
||||
}
|
||||
|
||||
var configPath string
|
||||
var showVersion bool
|
||||
|
||||
@@ -3,6 +3,12 @@ server:
|
||||
host: "0.0.0.0"
|
||||
port: 8340
|
||||
mode: "release" # debug | release
|
||||
external_url: "" # 可选:Master 对 Agent 可达的 URL,例如 https://backup.example.com
|
||||
trusted_proxies: # 仅这些代理可提供 X-Forwarded-For;跨容器代理需加入其网段
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
web_root: "" # 前端静态目录;留空自动探测(./web、/opt/backupx/web 等)。
|
||||
# 命中后后端直接托管 Web 控制台,无需额外 nginx 反向代理。
|
||||
|
||||
database:
|
||||
path: "./data/backupx.db" # SQLite 数据库路径
|
||||
|
||||
@@ -6,14 +6,18 @@ require (
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/rclone/rclone v1.73.3
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rclone/rclone v1.73.5
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/viper v1.20.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
google.golang.org/api v0.255.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
@@ -26,10 +30,10 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.3 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.2-0.20251110135918-10b7b7e7cd26 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.37 // indirect
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.38 // indirect
|
||||
github.com/Files-com/files-sdk-go/v3 v3.2.264 // indirect
|
||||
github.com/IBM/go-sdk-core/v5 v5.18.5 // indirect
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
|
||||
@@ -46,25 +50,26 @@ require (
|
||||
github.com/anchore/go-lzo v0.1.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/aws/smithy-go v1.25.1 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
@@ -106,7 +111,7 @@ require (
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -145,7 +150,6 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7 // indirect
|
||||
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988 // indirect
|
||||
github.com/koofr/go-koofrclient v0.0.0-20221207135200-cbd7fc9ad6a6 // indirect
|
||||
@@ -179,8 +183,6 @@ require (
|
||||
github.com/pkg/xattr v0.4.12 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.2 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
@@ -223,29 +225,28 @@ require (
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/arch v0.14.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/image v0.32.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
122
server/go.sum
122
server/go.sum
@@ -51,8 +51,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehw
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.3 h1:sxgSqOB9CDToiaVFpxuvb5wGgGqWa3lCShcm5o0n3bE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.3/go.mod h1:XdED8i399lEVblYHTZM8eXaP07gv4Z58IL6ueMlVlrg=
|
||||
github.com/Azure/go-ntlmssp v0.0.2-0.20251110135918-10b7b7e7cd26 h1:gy/jrlpp8EfSyA73a51fofoSfhp5rPNQAUvDr4Dm91c=
|
||||
github.com/Azure/go-ntlmssp v0.0.2-0.20251110135918-10b7b7e7cd26/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
@@ -61,8 +61,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.37 h1:W8S9TrAyZ4//3PXsU6+Bi+fe/6uIL986GyS7PVzIDL4=
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.37/go.mod h1:0cBhKXQg49XbKZZfk5TCDa3sVLP+xMxZTWL+7KY0XR0=
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.38 h1:YCyHs3wUXEe2BEWn40vcoyaQ2ruHNmNwkasfo3Th16A=
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.38/go.mod h1:0cBhKXQg49XbKZZfk5TCDa3sVLP+xMxZTWL+7KY0XR0=
|
||||
github.com/Files-com/files-sdk-go/v3 v3.2.264 h1:lMHTplAYI9FtmCo/QOcpRxmPA5REVAct1r2riQmDQKw=
|
||||
github.com/Files-com/files-sdk-go/v3 v3.2.264/go.mod h1:wGqkOzRu/ClJibvDgcfuJNAqI2nLhe8g91tPlDKRCdE=
|
||||
github.com/IBM/go-sdk-core/v5 v5.18.5 h1:g0JRl3sYXJczB/yuDlrN6x22LJ6jIxhp0Sa4ARNW60c=
|
||||
@@ -102,44 +102,46 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc h1:LoL75er+LKDHDUfU5tRvFwxH0LjPpZN8OoG8Ll+liGU=
|
||||
github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc/go.mod h1:w648aMHEgFYS6xb0KVMMtZ2uMeemhiKCuD2vj6gY52A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 h1:2fjfz3/G9BRvIKuNZ655GwzpklC2kEH0cowZQGO7uBg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4/go.mod h1:Ymws824lvMypLFPwyyUXM52SXuGgxpu0+DISLfKvB+c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.1 h1:IbWiN670htmBioc+Zj32vSpJgQ2+OYSlvTvfQ1nCORQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.1/go.mod h1:tw/B596EUhBWDFGdDGuLC21fVU4A3s4/5Efy8S39W18=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -256,8 +258,8 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 h1:JnrjqG5iR07/8k7NqrLNilRsl3s1EPRQEGvbPyOce68=
|
||||
github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348/go.mod h1:Czxo/d1g948LtrALAZdL04TL/HnkopquAjxYUuI02bo=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -530,8 +532,8 @@ github.com/rclone/Proton-API-Bridge v1.0.1-0.20260127174007-77f974840d11 h1:4MI2
|
||||
github.com/rclone/Proton-API-Bridge v1.0.1-0.20260127174007-77f974840d11/go.mod h1:3HLX7dwZgvB7nt+Yl/xdzVPcargQ1yBmJEUg3n+jMKM=
|
||||
github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18 h1:Lc+d3ISfQaMJKWZOE7z4ZSY4RVmdzbn1B0IM8xN18qM=
|
||||
github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18/go.mod h1:LB2kCEaZMzNn3ocdz+qYfxXmuLxxN0ka62KJd2x53Bc=
|
||||
github.com/rclone/rclone v1.73.3 h1:XKlobcnXxxzxnB6UBSVtRB+UeZmYDV9B4QExVSSGoAY=
|
||||
github.com/rclone/rclone v1.73.3/go.mod h1:QJDWatpAY9sKGXfpKZUXbThvtHoeo78DcFP2+/cbkvc=
|
||||
github.com/rclone/rclone v1.73.5 h1:r8a9JHYIWUqk7hNRJuMJ3cROkKfB2zmfkADg8ZLYh6I=
|
||||
github.com/rclone/rclone v1.73.5/go.mod h1:WVv8gvA/lEl/Y37e8I8yosm7ZY+Szq7ujXbJS8Ol63o=
|
||||
github.com/relvacode/iso8601 v1.7.0 h1:BXy+V60stMP6cpswc+a93Mq3e65PfXCgDFfhvNNGrdo=
|
||||
github.com/relvacode/iso8601 v1.7.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
@@ -651,16 +653,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
@@ -688,8 +690,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -700,12 +702,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -730,8 +732,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -771,8 +773,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -796,8 +798,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -847,8 +849,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -860,8 +862,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -877,8 +879,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -931,8 +933,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
293
server/internal/agent/agent.go
Normal file
293
server/internal/agent/agent.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"backupx/server/internal/backup"
|
||||
)
|
||||
|
||||
// Agent 是 Agent 进程的主控制器。
|
||||
type Agent struct {
|
||||
cfg *Config
|
||||
client *MasterClient
|
||||
executor *Executor
|
||||
version string
|
||||
|
||||
mu sync.Mutex
|
||||
started bool
|
||||
}
|
||||
|
||||
// New 构造 Agent。
|
||||
func New(cfg *Config, version string) (*Agent, error) {
|
||||
if err := cfg.ResolveToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := NewMasterClient(cfg.Master, cfg.Token, cfg.InsecureSkipTLSVerify)
|
||||
if err := client.ConfigureTransport(cfg.ProxyURL, cfg.CACertFile); err != nil {
|
||||
return nil, fmt.Errorf("configure master connection: %w", err)
|
||||
}
|
||||
executor := NewExecutor(client, cfg.TempDir)
|
||||
return &Agent{
|
||||
cfg: cfg,
|
||||
client: client,
|
||||
executor: executor,
|
||||
version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run 启动 Agent 主循环,阻塞直到 ctx 被取消。
|
||||
func (a *Agent) Run(ctx context.Context) error {
|
||||
a.mu.Lock()
|
||||
if a.started {
|
||||
a.mu.Unlock()
|
||||
return fmt.Errorf("agent already started")
|
||||
}
|
||||
a.started = true
|
||||
a.mu.Unlock()
|
||||
|
||||
hbInterval := parseDuration(a.cfg.HeartbeatInterval, 15*time.Second)
|
||||
pollInterval := parseDuration(a.cfg.PollInterval, 5*time.Second)
|
||||
|
||||
// 首次握手:通过一次心跳确认 token 有效
|
||||
if err := a.heartbeatOnce(ctx); err != nil {
|
||||
return fmt.Errorf("initial heartbeat failed: %w", err)
|
||||
}
|
||||
log.Printf("[agent] connected to master %s", a.cfg.Master)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
a.heartbeatLoop(ctx, hbInterval)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
a.pollLoop(ctx, pollInterval)
|
||||
}()
|
||||
wg.Wait()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// heartbeatLoop 定期发送心跳。
|
||||
func (a *Agent) heartbeatLoop(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := a.heartbeatOnce(ctx); err != nil {
|
||||
log.Printf("[agent] heartbeat failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) heartbeatOnce(ctx context.Context) error {
|
||||
hostname, _ := os.Hostname()
|
||||
req := HeartbeatRequest{
|
||||
Hostname: hostname,
|
||||
IPAddress: detectLocalIP(),
|
||||
AgentVersion: a.version,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
}
|
||||
_, err := a.client.Heartbeat(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// pollLoop 定期拉取并处理待执行命令。
|
||||
func (a *Agent) pollLoop(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
a.pollAndHandleOnce(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) pollAndHandleOnce(ctx context.Context) {
|
||||
cmd, err := a.client.PollCommand(ctx)
|
||||
if err != nil {
|
||||
log.Printf("[agent] poll command failed: %v", err)
|
||||
return
|
||||
}
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("[agent] received command #%d type=%s", cmd.ID, cmd.Type)
|
||||
switch cmd.Type {
|
||||
case "run_task":
|
||||
a.handleRunTask(ctx, cmd)
|
||||
case "list_dir":
|
||||
a.handleListDir(ctx, cmd)
|
||||
case "restore_record":
|
||||
a.handleRestoreRecord(ctx, cmd)
|
||||
case "discover_db":
|
||||
a.handleDiscoverDB(ctx, cmd)
|
||||
case "delete_storage_object":
|
||||
a.handleDeleteStorageObject(ctx, cmd)
|
||||
default:
|
||||
msg := fmt.Sprintf("unknown command type: %s", cmd.Type)
|
||||
log.Printf("[agent] %s", msg)
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, msg, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRunTask 处理 run_task 命令
|
||||
func (a *Agent) handleRunTask(ctx context.Context, cmd *CommandPayload) {
|
||||
var payload struct {
|
||||
TaskID uint `json:"taskId"`
|
||||
RecordID uint `json:"recordId"`
|
||||
}
|
||||
if err := json.Unmarshal(cmd.Payload, &payload); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "invalid payload: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
if err := a.executor.ExecuteRunTask(ctx, payload.TaskID, payload.RecordID); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, true, "", map[string]any{
|
||||
"taskId": payload.TaskID,
|
||||
"recordId": payload.RecordID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleRestoreRecord 处理 restore_record 命令
|
||||
func (a *Agent) handleRestoreRecord(ctx context.Context, cmd *CommandPayload) {
|
||||
var payload struct {
|
||||
RestoreRecordID uint `json:"restoreRecordId"`
|
||||
}
|
||||
if err := json.Unmarshal(cmd.Payload, &payload); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "invalid payload: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
if payload.RestoreRecordID == 0 {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "restoreRecordId is required", nil)
|
||||
return
|
||||
}
|
||||
if err := a.executor.ExecuteRestore(ctx, payload.RestoreRecordID); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, true, "", map[string]any{
|
||||
"restoreRecordId": payload.RestoreRecordID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteStorageObject 处理 delete_storage_object 命令:在 Agent 侧删除指定存储对象。
|
||||
// 用于跨节点 local_disk 场景下的远程备份文件清理。
|
||||
func (a *Agent) handleDeleteStorageObject(ctx context.Context, cmd *CommandPayload) {
|
||||
var payload struct {
|
||||
TargetType string `json:"targetType"`
|
||||
TargetConfig map[string]any `json:"targetConfig"`
|
||||
StoragePath string `json:"storagePath"`
|
||||
}
|
||||
if err := json.Unmarshal(cmd.Payload, &payload); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "invalid payload: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(payload.StoragePath) == "" {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "storagePath is required", nil)
|
||||
return
|
||||
}
|
||||
provider, err := a.executor.storageRegistry.Create(ctx, payload.TargetType, payload.TargetConfig)
|
||||
if err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "create provider: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
if err := provider.Delete(ctx, payload.StoragePath); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "delete object: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, true, "", map[string]any{"deleted": true})
|
||||
}
|
||||
|
||||
// handleDiscoverDB 处理 discover_db 命令:在 Agent 本机执行 mysql/psql 列出数据库。
|
||||
func (a *Agent) handleDiscoverDB(ctx context.Context, cmd *CommandPayload) {
|
||||
var payload struct {
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.Unmarshal(cmd.Payload, &payload); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "invalid payload: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
databases, err := backup.DiscoverDatabases(ctx, backup.NewOSCommandExecutor(), backup.DiscoverRequest{
|
||||
Type: payload.Type,
|
||||
Host: payload.Host,
|
||||
Port: payload.Port,
|
||||
User: payload.User,
|
||||
Password: payload.Password,
|
||||
})
|
||||
if err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, true, "", map[string]any{"databases": databases})
|
||||
}
|
||||
|
||||
// handleListDir 处理 list_dir 命令(阶段四实现)
|
||||
func (a *Agent) handleListDir(ctx context.Context, cmd *CommandPayload) {
|
||||
var payload struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.Unmarshal(cmd.Payload, &payload); err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, "invalid payload: "+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
entries, err := listLocalDir(payload.Path)
|
||||
if err != nil {
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
_ = a.client.SubmitCommandResult(ctx, cmd.ID, true, "", map[string]any{"entries": entries})
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
|
||||
func parseDuration(s string, fallback time.Duration) time.Duration {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return fallback
|
||||
}
|
||||
if d, err := time.ParseDuration(s); err == nil {
|
||||
return d
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func detectLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
368
server/internal/agent/client.go
Normal file
368
server/internal/agent/client.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MasterClient 是 Agent 调用 Master HTTP API 的封装。
|
||||
type MasterClient struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewMasterClient 构造 Master 客户端。
|
||||
func NewMasterClient(baseURL, token string, insecureTLS bool) *MasterClient {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
if transport.TLSClientConfig != nil {
|
||||
tlsConfig = transport.TLSClientConfig.Clone()
|
||||
tlsConfig.MinVersion = tls.VersionTLS12
|
||||
}
|
||||
// 仅用于用户显式开启的测试模式。生产环境应配置受信 CA。
|
||||
tlsConfig.InsecureSkipVerify = insecureTLS // #nosec G402
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
return &MasterClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
token: token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
Transport: transport,
|
||||
// Agent Token 是自定义认证头。禁止自动重定向,避免代理或错误
|
||||
// 配置把它转发到另一个主机;Master URL 必须直接指向 API。
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureTransport 应用显式代理和私有 CA。默认 Transport 已保留
|
||||
// ProxyFromEnvironment,因此 ProxyURL 留空时 HTTP_PROXY/HTTPS_PROXY/NO_PROXY 生效。
|
||||
func (c *MasterClient) ConfigureTransport(proxyURL, caCertFile string) error {
|
||||
transport, ok := c.httpClient.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
return errors.New("agent http transport has unexpected type")
|
||||
}
|
||||
if strings.TrimSpace(proxyURL) != "" {
|
||||
parsedProxy, err := url.Parse(strings.TrimSpace(proxyURL))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse proxy URL: %w", err)
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(parsedProxy)
|
||||
}
|
||||
if strings.TrimSpace(caCertFile) == "" {
|
||||
return nil
|
||||
}
|
||||
pemData, err := os.ReadFile(strings.TrimSpace(caCertFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read CA certificate: %w", err)
|
||||
}
|
||||
roots, err := x509.SystemCertPool()
|
||||
if err != nil || roots == nil {
|
||||
roots = x509.NewCertPool()
|
||||
}
|
||||
if !roots.AppendCertsFromPEM(pemData) {
|
||||
return errors.New("CA certificate file does not contain a valid PEM certificate")
|
||||
}
|
||||
transport.TLSClientConfig.RootCAs = roots
|
||||
return nil
|
||||
}
|
||||
|
||||
// HeartbeatRequest Agent 上报心跳的请求
|
||||
type HeartbeatRequest struct {
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
IPAddress string `json:"ipAddress,omitempty"`
|
||||
AgentVersion string `json:"agentVersion,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
}
|
||||
|
||||
// HeartbeatResponse Master 返回的心跳响应
|
||||
type HeartbeatResponse struct {
|
||||
Status string `json:"status"`
|
||||
NodeID uint `json:"nodeId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Heartbeat 上报心跳并获取节点元信息
|
||||
func (c *MasterClient) Heartbeat(ctx context.Context, req HeartbeatRequest) (*HeartbeatResponse, error) {
|
||||
var resp HeartbeatResponse
|
||||
if err := c.do(ctx, http.MethodPost, "/api/agent/heartbeat", req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CommandPayload 与 service.AgentCommandPayload 对齐
|
||||
type CommandPayload struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// PollCommandResponse 轮询响应:无命令时 Command 为 nil
|
||||
type PollCommandResponse struct {
|
||||
Command *CommandPayload `json:"command"`
|
||||
}
|
||||
|
||||
// PollCommand 拉取下一条待执行命令
|
||||
func (c *MasterClient) PollCommand(ctx context.Context) (*CommandPayload, error) {
|
||||
var resp PollCommandResponse
|
||||
if err := c.do(ctx, http.MethodPost, "/api/agent/commands/poll", nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Command, nil
|
||||
}
|
||||
|
||||
// SubmitCommandResult 上报命令执行结果
|
||||
func (c *MasterClient) SubmitCommandResult(ctx context.Context, cmdID uint, success bool, errorMsg string, result any) error {
|
||||
var resultJSON json.RawMessage
|
||||
if result != nil {
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal result: %w", err)
|
||||
}
|
||||
resultJSON = data
|
||||
}
|
||||
payload := map[string]any{
|
||||
"success": success,
|
||||
"errorMessage": errorMsg,
|
||||
}
|
||||
if resultJSON != nil {
|
||||
payload["result"] = resultJSON
|
||||
}
|
||||
path := fmt.Sprintf("/api/agent/commands/%d/result", cmdID)
|
||||
return c.do(ctx, http.MethodPost, path, payload, nil)
|
||||
}
|
||||
|
||||
// TaskSpec 与 service.AgentTaskSpec 对齐
|
||||
type TaskSpec struct {
|
||||
TaskID uint `json:"taskId"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
SourcePaths string `json:"sourcePaths"`
|
||||
ExcludePatterns string `json:"excludePatterns"`
|
||||
DBHost string `json:"dbHost"`
|
||||
DBPort int `json:"dbPort"`
|
||||
DBUser string `json:"dbUser"`
|
||||
DBPassword string `json:"dbPassword"`
|
||||
DBName string `json:"dbName"`
|
||||
DBPath string `json:"dbPath"`
|
||||
ExtraConfig string `json:"extraConfig"`
|
||||
Compression string `json:"compression"`
|
||||
Encrypt bool `json:"encrypt"`
|
||||
StorageTargets []StorageTargetConfig `json:"storageTargets"`
|
||||
}
|
||||
|
||||
// StorageTargetConfig 与 service.AgentStorageTargetConfig 对齐
|
||||
type StorageTargetConfig struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
TransferMode string `json:"transferMode"`
|
||||
}
|
||||
|
||||
// GetTaskSpec 拉取任务规格
|
||||
func (c *MasterClient) GetTaskSpec(ctx context.Context, taskID uint) (*TaskSpec, error) {
|
||||
var spec TaskSpec
|
||||
path := fmt.Sprintf("/api/agent/tasks/%d", taskID)
|
||||
if err := c.do(ctx, http.MethodGet, path, nil, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// RecordUpdate 与 service.AgentRecordUpdate 对齐
|
||||
type RecordUpdate struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
FileName string `json:"fileName,omitempty"`
|
||||
FileSize int64 `json:"fileSize,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
StoragePath string `json:"storagePath,omitempty"`
|
||||
StorageTargetID uint `json:"storageTargetId,omitempty"`
|
||||
StorageTransferMode string `json:"storageTransferMode,omitempty"`
|
||||
StorageUploadResults []StorageResultItem `json:"storageUploadResults,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
LogAppend string `json:"logAppend,omitempty"`
|
||||
}
|
||||
|
||||
type StorageResultItem struct {
|
||||
StorageTargetID uint `json:"storageTargetId"`
|
||||
StorageTargetName string `json:"storageTargetName"`
|
||||
Status string `json:"status"`
|
||||
StoragePath string `json:"storagePath,omitempty"`
|
||||
FileSize int64 `json:"fileSize,omitempty"`
|
||||
TransferMode string `json:"transferMode,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateRecord 上报备份记录的状态/日志
|
||||
func (c *MasterClient) UpdateRecord(ctx context.Context, recordID uint, update RecordUpdate) error {
|
||||
path := fmt.Sprintf("/api/agent/records/%d", recordID)
|
||||
return c.do(ctx, http.MethodPost, path, update, nil)
|
||||
}
|
||||
|
||||
// UploadArtifact streams an artifact through the Master for storage targets
|
||||
// that are not directly reachable from the Agent.
|
||||
func (c *MasterClient) UploadArtifact(ctx context.Context, recordID, targetID uint, objectKey string, size int64, checksum string, reader io.Reader) error {
|
||||
path := fmt.Sprintf("/api/agent/records/%d/artifacts/%d", recordID, targetID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL+path, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The executor owns and closes the artifact file. Prevent net/http from
|
||||
// closing that underlying reader when it finishes the request body.
|
||||
req.Body = io.NopCloser(reader)
|
||||
req.ContentLength = size
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("X-Agent-Token", c.token)
|
||||
req.Header.Set("X-BackupX-Object-Key", objectKey)
|
||||
req.Header.Set("X-BackupX-SHA256", checksum)
|
||||
client := *c.httpClient
|
||||
client.Timeout = 0
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("relay artifact to Master: %w", err)
|
||||
}
|
||||
data, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
closeErr := resp.Body.Close()
|
||||
if readErr != nil || closeErr != nil {
|
||||
return errors.Join(readErr, closeErr)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("relay artifact to Master: http %d: %s", resp.StatusCode, string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreSpec 与 service.AgentRestoreSpec 对齐
|
||||
type RestoreSpec struct {
|
||||
RestoreRecordID uint `json:"restoreRecordId"`
|
||||
BackupRecordID uint `json:"backupRecordId"`
|
||||
TaskID uint `json:"taskId"`
|
||||
TaskName string `json:"taskName"`
|
||||
Type string `json:"type"`
|
||||
SourcePath string `json:"sourcePath,omitempty"`
|
||||
SourcePaths []string `json:"sourcePaths,omitempty"`
|
||||
DBHost string `json:"dbHost,omitempty"`
|
||||
DBPort int `json:"dbPort,omitempty"`
|
||||
DBUser string `json:"dbUser,omitempty"`
|
||||
DBPassword string `json:"dbPassword,omitempty"`
|
||||
DBName string `json:"dbName,omitempty"`
|
||||
DBPath string `json:"dbPath,omitempty"`
|
||||
ExtraConfig string `json:"extraConfig,omitempty"`
|
||||
Compression string `json:"compression"`
|
||||
Encrypt bool `json:"encrypt"`
|
||||
Storage StorageTargetConfig `json:"storage"`
|
||||
StoragePath string `json:"storagePath"`
|
||||
FileName string `json:"fileName"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// RestoreUpdate 与 service.AgentRestoreUpdate 对齐
|
||||
type RestoreUpdate struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
LogAppend string `json:"logAppend,omitempty"`
|
||||
}
|
||||
|
||||
// GetRestoreSpec 拉取恢复规格
|
||||
func (c *MasterClient) GetRestoreSpec(ctx context.Context, restoreRecordID uint) (*RestoreSpec, error) {
|
||||
var spec RestoreSpec
|
||||
path := fmt.Sprintf("/api/agent/restores/%d/spec", restoreRecordID)
|
||||
if err := c.do(ctx, http.MethodGet, path, nil, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
func (c *MasterClient) DownloadRestoreArtifact(ctx context.Context, restoreRecordID uint) (io.ReadCloser, error) {
|
||||
path := fmt.Sprintf("/api/agent/restores/%d/artifact", restoreRecordID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Agent-Token", c.token)
|
||||
client := *c.httpClient
|
||||
client.Timeout = 0
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download relayed artifact from Master: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return resp.Body, nil
|
||||
}
|
||||
data, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
closeErr := resp.Body.Close()
|
||||
return nil, errors.Join(fmt.Errorf("download relayed artifact from Master: http %d: %s", resp.StatusCode, string(data)), readErr, closeErr)
|
||||
}
|
||||
|
||||
// UpdateRestore 上报恢复记录的状态/日志
|
||||
func (c *MasterClient) UpdateRestore(ctx context.Context, restoreRecordID uint, update RestoreUpdate) error {
|
||||
path := fmt.Sprintf("/api/agent/restores/%d", restoreRecordID)
|
||||
return c.do(ctx, http.MethodPost, path, update, nil)
|
||||
}
|
||||
|
||||
// do 是通用 HTTP 调用。所有 Agent API 都统一走 JSON + X-Agent-Token。
|
||||
func (c *MasterClient) do(ctx context.Context, method, path string, body any, out any) error {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(data)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Agent-Token", c.token)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", method, path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("%s %s: http %d: %s", method, path, resp.StatusCode, string(data))
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
// BackupX API 统一封装成 {code, data, message} 形式,需要解出 data 字段
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &envelope); err == nil && envelope.Data != nil {
|
||||
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
||||
return fmt.Errorf("decode data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 兼容直接返回对象的情况
|
||||
return json.Unmarshal(data, out)
|
||||
}
|
||||
93
server/internal/agent/client_test.go
Normal file
93
server/internal/agent/client_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMasterClientKeepsEnvironmentProxySupport(t *testing.T) {
|
||||
client := NewMasterClient("https://master.example.com", "token", false)
|
||||
transport := client.httpClient.Transport.(*http.Transport)
|
||||
if transport.Proxy == nil {
|
||||
t.Fatal("default transport proxy function must be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterClientConfiguresExplicitProxy(t *testing.T) {
|
||||
client := NewMasterClient("https://master.example.com", "token", false)
|
||||
if err := client.ConfigureTransport("socks5h://127.0.0.1:1080", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transport := client.httpClient.Transport.(*http.Transport)
|
||||
requestURL, _ := url.Parse("https://master.example.com")
|
||||
proxyURL, err := transport.Proxy(&http.Request{URL: requestURL})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if proxyURL == nil || proxyURL.String() != "socks5h://127.0.0.1:1080" {
|
||||
t.Fatalf("proxy URL = %v", proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterClientRejectsInvalidCACertificate(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "invalid.pem")
|
||||
if err := os.WriteFile(path, []byte("not a certificate"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := NewMasterClient("https://master.example.com", "token", false)
|
||||
if err := client.ConfigureTransport("", path); err == nil {
|
||||
t.Fatal("expected invalid CA certificate error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterClientDoesNotForwardTokenThroughRedirects(t *testing.T) {
|
||||
receivedToken := ""
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedToken = r.Header.Get("X-Agent-Token")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer target.Close()
|
||||
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer redirector.Close()
|
||||
|
||||
client := NewMasterClient(redirector.URL, "secret-agent-token", false)
|
||||
if _, err := client.Heartbeat(context.Background(), HeartbeatRequest{}); err == nil {
|
||||
t.Fatal("redirect response should not be accepted as a Master API response")
|
||||
}
|
||||
if receivedToken != "" {
|
||||
t.Fatalf("Agent token leaked through redirect: %q", receivedToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeatSendsTokenOnlyInAuthenticationHeader(t *testing.T) {
|
||||
requestBody := ""
|
||||
receivedHeader := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
requestBody = string(body)
|
||||
receivedHeader = r.Header.Get("X-Agent-Token")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":{"status":"ok","nodeId":1,"name":"node"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewMasterClient(server.URL, "secret-agent-token", false)
|
||||
if _, err := client.Heartbeat(context.Background(), HeartbeatRequest{Hostname: "node"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if receivedHeader != "secret-agent-token" {
|
||||
t.Fatalf("authentication header = %q", receivedHeader)
|
||||
}
|
||||
if strings.Contains(requestBody, "secret-agent-token") || strings.Contains(requestBody, `"token"`) {
|
||||
t.Fatalf("heartbeat body exposed the Agent token: %s", requestBody)
|
||||
}
|
||||
}
|
||||
196
server/internal/agent/config.go
Normal file
196
server/internal/agent/config.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// Package agent 实现 BackupX 远程 Agent。
|
||||
//
|
||||
// Agent 是一个独立的 Go 进程,部署在远程服务器上,通过 HTTP 轮询的方式
|
||||
// 与 Master 通信:定期上报心跳、拉取 Master 下发的命令、本地执行备份、
|
||||
// 把执行结果和日志回报给 Master。
|
||||
//
|
||||
// 通信协议见 server/internal/http/agent_handler.go。
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config 是 Agent 的运行时配置。
|
||||
type Config struct {
|
||||
// Master BackupX Master 的 HTTP 基础地址,例如 http://master.example.com:8340
|
||||
Master string `yaml:"master"`
|
||||
// Token 节点认证令牌(在 Master 创建节点时生成)
|
||||
Token string `yaml:"token"`
|
||||
// TokenFile 从文件读取节点认证令牌;适合 systemd 凭据和容器 secret。
|
||||
// Token 与 TokenFile 同时设置时优先使用 Token。
|
||||
TokenFile string `yaml:"tokenFile"`
|
||||
// HeartbeatInterval 心跳间隔,默认 15s
|
||||
HeartbeatInterval string `yaml:"heartbeatInterval"`
|
||||
// PollInterval 命令轮询间隔,默认 5s
|
||||
PollInterval string `yaml:"pollInterval"`
|
||||
// TempDir 备份临时目录,默认 /var/lib/backupx-agent/tmp
|
||||
TempDir string `yaml:"tempDir"`
|
||||
// ProxyURL Agent 访问 Master 使用的显式代理。留空时遵循
|
||||
// HTTP_PROXY、HTTPS_PROXY 与 NO_PROXY;支持 http(s) 和 socks5(h)。
|
||||
ProxyURL string `yaml:"proxyUrl"`
|
||||
// CACertFile 私有 CA 的 PEM 文件路径,用于安全连接内网 HTTPS Master。
|
||||
CACertFile string `yaml:"caCertFile"`
|
||||
// InsecureSkipTLSVerify 测试环境允许跳过 TLS 证书校验
|
||||
InsecureSkipTLSVerify bool `yaml:"insecureSkipTlsVerify"`
|
||||
}
|
||||
|
||||
// Overrides 表示命令行显式提供的 Agent 配置覆盖项。
|
||||
type Overrides struct {
|
||||
Master string
|
||||
Token string
|
||||
TokenFile string
|
||||
TempDir string
|
||||
ProxyURL string
|
||||
CACertFile string
|
||||
}
|
||||
|
||||
// LoadConfigFile 从 YAML 文件加载 Agent 配置。
|
||||
func LoadConfigFile(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read agent config: %w", err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse agent config: %w", err)
|
||||
}
|
||||
return applyConfigDefaults(&cfg)
|
||||
}
|
||||
|
||||
// LoadConfigFromEnv 从环境变量加载 Agent 配置。优先级低于 --config 文件。
|
||||
//
|
||||
// 支持的环境变量:
|
||||
// - BACKUPX_AGENT_MASTER Master URL
|
||||
// - BACKUPX_AGENT_TOKEN 节点认证令牌
|
||||
// - BACKUPX_AGENT_TOKEN_FILE 节点认证令牌文件
|
||||
// - BACKUPX_AGENT_HEARTBEAT 心跳间隔(如 15s)
|
||||
// - BACKUPX_AGENT_POLL 命令轮询间隔(如 5s)
|
||||
// - BACKUPX_AGENT_TEMP_DIR 临时目录
|
||||
// - BACKUPX_AGENT_PROXY_URL 显式 HTTP(S)/SOCKS5 代理
|
||||
// - BACKUPX_AGENT_CA_CERT_FILE 私有 CA PEM 文件
|
||||
// - BACKUPX_AGENT_INSECURE_TLS true / 1 跳过 TLS 校验
|
||||
func LoadConfigFromEnv() (*Config, error) {
|
||||
cfg := &Config{
|
||||
Master: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_MASTER")),
|
||||
Token: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_TOKEN")),
|
||||
TokenFile: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_TOKEN_FILE")),
|
||||
HeartbeatInterval: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_HEARTBEAT")),
|
||||
PollInterval: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_POLL")),
|
||||
TempDir: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_TEMP_DIR")),
|
||||
ProxyURL: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_PROXY_URL")),
|
||||
CACertFile: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_CA_CERT_FILE")),
|
||||
InsecureSkipTLSVerify: strings.EqualFold(os.Getenv("BACKUPX_AGENT_INSECURE_TLS"), "true") || os.Getenv("BACKUPX_AGENT_INSECURE_TLS") == "1",
|
||||
}
|
||||
return applyConfigDefaults(cfg)
|
||||
}
|
||||
|
||||
// ApplyOverrides 把命令行覆盖值合并入配置(非空覆盖)。
|
||||
func (c *Config) ApplyOverrides(overrides Overrides) {
|
||||
if strings.TrimSpace(overrides.Master) != "" {
|
||||
c.Master = strings.TrimSpace(overrides.Master)
|
||||
}
|
||||
tokenProvided := strings.TrimSpace(overrides.Token) != ""
|
||||
if strings.TrimSpace(overrides.TokenFile) != "" {
|
||||
c.TokenFile = strings.TrimSpace(overrides.TokenFile)
|
||||
if !tokenProvided {
|
||||
// An explicit --token-file must override a token inherited from YAML.
|
||||
c.Token = ""
|
||||
}
|
||||
}
|
||||
if tokenProvided {
|
||||
c.Token = strings.TrimSpace(overrides.Token)
|
||||
}
|
||||
if strings.TrimSpace(overrides.TempDir) != "" {
|
||||
c.TempDir = strings.TrimSpace(overrides.TempDir)
|
||||
}
|
||||
if strings.TrimSpace(overrides.ProxyURL) != "" {
|
||||
c.ProxyURL = strings.TrimSpace(overrides.ProxyURL)
|
||||
}
|
||||
if strings.TrimSpace(overrides.CACertFile) != "" {
|
||||
c.CACertFile = strings.TrimSpace(overrides.CACertFile)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveToken 在所有配置源合并完成后读取 token 文件。
|
||||
func (c *Config) ResolveToken() error {
|
||||
if strings.TrimSpace(c.Token) != "" || strings.TrimSpace(c.TokenFile) == "" {
|
||||
c.Token = strings.TrimSpace(c.Token)
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(strings.TrimSpace(c.TokenFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read agent token file: %w", err)
|
||||
}
|
||||
c.Token = strings.TrimSpace(string(data))
|
||||
if c.Token == "" {
|
||||
return errors.New("agent token file is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 校验必填字段。
|
||||
func (c *Config) Validate() error {
|
||||
masterURL, err := url.Parse(strings.TrimSpace(c.Master))
|
||||
if strings.TrimSpace(c.Master) == "" {
|
||||
return errors.New("master url is required (set via --master, BACKUPX_AGENT_MASTER or config file)")
|
||||
}
|
||||
if err != nil || (masterURL.Scheme != "http" && masterURL.Scheme != "https") || masterURL.Host == "" || masterURL.User != nil || masterURL.RawQuery != "" || masterURL.Fragment != "" {
|
||||
return errors.New("master url must be an absolute http(s) URL without credentials, query or fragment")
|
||||
}
|
||||
if strings.TrimSpace(c.Token) == "" {
|
||||
return errors.New("token is required (set via --token, --token-file, environment or config file)")
|
||||
}
|
||||
if c.ProxyURL != "" {
|
||||
proxyURL, proxyErr := url.Parse(c.ProxyURL)
|
||||
if proxyErr != nil || proxyURL.Host == "" {
|
||||
return errors.New("proxy url must be an absolute URL")
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case "http", "https", "socks5", "socks5h":
|
||||
default:
|
||||
return errors.New("proxy url scheme must be http, https, socks5 or socks5h")
|
||||
}
|
||||
if proxyURL.RawQuery != "" || proxyURL.Fragment != "" || (proxyURL.Path != "" && proxyURL.Path != "/") {
|
||||
return errors.New("proxy url must not contain a path, query or fragment")
|
||||
}
|
||||
}
|
||||
if c.CACertFile != "" && c.InsecureSkipTLSVerify {
|
||||
return errors.New("ca cert file and insecure TLS cannot be enabled together")
|
||||
}
|
||||
for name, value := range map[string]string{
|
||||
"heartbeat interval": c.HeartbeatInterval,
|
||||
"poll interval": c.PollInterval,
|
||||
} {
|
||||
duration, durationErr := time.ParseDuration(value)
|
||||
if durationErr != nil || duration <= 0 {
|
||||
return fmt.Errorf("%s must be a positive duration", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyConfigDefaults(cfg *Config) (*Config, error) {
|
||||
if cfg.HeartbeatInterval == "" {
|
||||
cfg.HeartbeatInterval = "15s"
|
||||
}
|
||||
if cfg.PollInterval == "" {
|
||||
cfg.PollInterval = "5s"
|
||||
}
|
||||
if cfg.TempDir == "" {
|
||||
cfg.TempDir = "/var/lib/backupx-agent/tmp"
|
||||
}
|
||||
cfg.Master = strings.TrimRight(strings.TrimSpace(cfg.Master), "/")
|
||||
cfg.Token = strings.TrimSpace(cfg.Token)
|
||||
cfg.TokenFile = strings.TrimSpace(cfg.TokenFile)
|
||||
cfg.ProxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||
cfg.CACertFile = strings.TrimSpace(cfg.CACertFile)
|
||||
return cfg, nil
|
||||
}
|
||||
147
server/internal/agent/config_test.go
Normal file
147
server/internal/agent/config_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfigFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "agent.yaml")
|
||||
content := `master: http://master.example.com:8340/
|
||||
token: abc123
|
||||
tokenFile: /run/secrets/backupx_agent_token
|
||||
heartbeatInterval: 20s
|
||||
pollInterval: 3s
|
||||
tempDir: /var/backupx-agent
|
||||
proxyUrl: socks5h://127.0.0.1:1080
|
||||
caCertFile: /etc/backupx-agent/ca.pem
|
||||
insecureSkipTlsVerify: true
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := LoadConfigFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.Master != "http://master.example.com:8340" {
|
||||
t.Errorf("trailing slash should be trimmed: %q", cfg.Master)
|
||||
}
|
||||
if cfg.Token != "abc123" {
|
||||
t.Errorf("token: %q", cfg.Token)
|
||||
}
|
||||
if cfg.HeartbeatInterval != "20s" || cfg.PollInterval != "3s" {
|
||||
t.Errorf("intervals: %+v", cfg)
|
||||
}
|
||||
if !cfg.InsecureSkipTLSVerify {
|
||||
t.Errorf("insecure should be true")
|
||||
}
|
||||
if cfg.ProxyURL != "socks5h://127.0.0.1:1080" || cfg.CACertFile != "/etc/backupx-agent/ca.pem" {
|
||||
t.Errorf("connection options not loaded: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigDefaults(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "agent.yaml")
|
||||
if err := os.WriteFile(path, []byte("master: http://m\ntoken: t\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := LoadConfigFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.HeartbeatInterval != "15s" || cfg.PollInterval != "5s" {
|
||||
t.Errorf("default intervals not applied: %+v", cfg)
|
||||
}
|
||||
if cfg.TempDir != "/var/lib/backupx-agent/tmp" {
|
||||
t.Errorf("default tempdir: %q", cfg.TempDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", Config{Master: "http://m", Token: "t"}, false},
|
||||
{"missing master", Config{Token: "t"}, true},
|
||||
{"missing token", Config{Master: "http://m"}, true},
|
||||
{"invalid master scheme", Config{Master: "ssh://m", Token: "t"}, true},
|
||||
{"master credentials rejected", Config{Master: "https://user:pass@m", Token: "t"}, true},
|
||||
{"valid socks proxy", Config{Master: "https://m", Token: "t", ProxyURL: "socks5h://127.0.0.1:1080"}, false},
|
||||
{"invalid proxy", Config{Master: "https://m", Token: "t", ProxyURL: "ftp://proxy"}, true},
|
||||
{"proxy path rejected", Config{Master: "https://m", Token: "t", ProxyURL: "http://proxy/connect"}, true},
|
||||
{"invalid heartbeat", Config{Master: "https://m", Token: "t", HeartbeatInterval: "never"}, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, _ = applyConfigDefaults(&c.cfg)
|
||||
err := c.cfg.Validate()
|
||||
if (err != nil) != c.wantErr {
|
||||
t.Errorf("%s: err=%v wantErr=%v", c.name, err, c.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeWithFlags(t *testing.T) {
|
||||
cfg := &Config{Master: "http://old", Token: "old"}
|
||||
cfg.ApplyOverrides(Overrides{Master: "http://new", TempDir: "/tmp/x", ProxyURL: "http://proxy:3128"})
|
||||
if cfg.Master != "http://new" {
|
||||
t.Errorf("master not overridden: %q", cfg.Master)
|
||||
}
|
||||
if cfg.Token != "old" {
|
||||
t.Errorf("empty flag should not override: %q", cfg.Token)
|
||||
}
|
||||
if cfg.TempDir != "/tmp/x" {
|
||||
t.Errorf("tempDir: %q", cfg.TempDir)
|
||||
}
|
||||
if cfg.ProxyURL != "http://proxy:3128" {
|
||||
t.Errorf("proxyUrl: %q", cfg.ProxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenFileOverrideReplacesConfiguredToken(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "agent.token")
|
||||
if err := os.WriteFile(path, []byte("file-token\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &Config{Token: "yaml-token", TokenFile: "/old/token"}
|
||||
cfg.ApplyOverrides(Overrides{TokenFile: " " + path + " "})
|
||||
if err := cfg.ResolveToken(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Token != "file-token" || cfg.TokenFile != path {
|
||||
t.Fatalf("token file override was not applied: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromEnv(t *testing.T) {
|
||||
t.Setenv("BACKUPX_AGENT_MASTER", "http://env-master")
|
||||
t.Setenv("BACKUPX_AGENT_TOKEN", "env-token")
|
||||
t.Setenv("BACKUPX_AGENT_PROXY_URL", "http://env-proxy:8080")
|
||||
t.Setenv("BACKUPX_AGENT_INSECURE_TLS", "true")
|
||||
cfg, err := LoadConfigFromEnv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Master != "http://env-master" || cfg.Token != "env-token" || cfg.ProxyURL != "http://env-proxy:8080" || !cfg.InsecureSkipTLSVerify {
|
||||
t.Errorf("env not picked up: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTokenFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "agent.token")
|
||||
if err := os.WriteFile(path, []byte(" file-token\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &Config{TokenFile: path}
|
||||
if err := cfg.ResolveToken(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Token != "file-token" {
|
||||
t.Fatalf("token = %q", cfg.Token)
|
||||
}
|
||||
}
|
||||
546
server/internal/agent/executor.go
Normal file
546
server/internal/agent/executor.go
Normal file
@@ -0,0 +1,546 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backupx/server/internal/backup"
|
||||
"backupx/server/internal/storage"
|
||||
storageRclone "backupx/server/internal/storage/rclone"
|
||||
"backupx/server/pkg/compress"
|
||||
)
|
||||
|
||||
// Executor 负责在 Agent 本地执行命令。
|
||||
type Executor struct {
|
||||
client *MasterClient
|
||||
tempDir string
|
||||
backupRegistry *backup.Registry
|
||||
storageRegistry *storage.Registry
|
||||
}
|
||||
|
||||
// NewExecutor 构造执行器。预先初始化 backup runner 与 storage registry。
|
||||
func NewExecutor(client *MasterClient, tempDir string) *Executor {
|
||||
backupRegistry := backup.NewRegistry(
|
||||
backup.NewFileRunner(),
|
||||
backup.NewSQLiteRunner(),
|
||||
backup.NewMySQLRunner(nil),
|
||||
backup.NewPostgreSQLRunner(nil),
|
||||
backup.NewSAPHANARunner(nil),
|
||||
backup.NewMongoDBRunner(nil),
|
||||
)
|
||||
storageRegistry := storage.NewRegistry(
|
||||
storageRclone.NewLocalDiskFactory(),
|
||||
storageRclone.NewS3Factory(),
|
||||
storageRclone.NewWebDAVFactory(),
|
||||
storageRclone.NewGoogleDriveFactory(),
|
||||
storageRclone.NewAliyunOSSFactory(),
|
||||
storageRclone.NewTencentCOSFactory(),
|
||||
storageRclone.NewQiniuKodoFactory(),
|
||||
storageRclone.NewFTPFactory(),
|
||||
storageRclone.NewRcloneFactory(),
|
||||
)
|
||||
storageRclone.RegisterAllBackends(storageRegistry)
|
||||
return &Executor{
|
||||
client: client,
|
||||
tempDir: tempDir,
|
||||
backupRegistry: backupRegistry,
|
||||
storageRegistry: storageRegistry,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteRunTask 处理 run_task 命令:拉规格 → 执行 runner → 压缩 → 上传 → 上报记录。
|
||||
//
|
||||
// 注意:Agent 当前不支持 Encrypt=true(加密密钥不下发到 Agent,避免密钥扩散)。
|
||||
// 遇到启用加密的任务会向 Master 上报失败并返回错误。
|
||||
func (e *Executor) ExecuteRunTask(ctx context.Context, taskID, recordID uint) error {
|
||||
if err := e.ensureTempDir(); err != nil {
|
||||
e.reportRecordFailure(ctx, recordID, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 1) 拉取任务规格
|
||||
spec, err := e.client.GetTaskSpec(ctx, taskID)
|
||||
if err != nil {
|
||||
e.reportRecordFailure(ctx, recordID, fmt.Sprintf("拉取任务规格失败: %v", err))
|
||||
return err
|
||||
}
|
||||
if spec.Encrypt {
|
||||
msg := "Agent 不支持加密备份(加密密钥仅在 Master 端持有)"
|
||||
e.reportRecordFailure(ctx, recordID, msg)
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
e.appendLog(ctx, recordID, fmt.Sprintf("[agent] 开始执行任务 %s (type=%s)\n", spec.Name, spec.Type))
|
||||
|
||||
// 2) 构造 backup.TaskSpec 并找对应 runner
|
||||
startedAt := time.Now().UTC()
|
||||
backupSpec := buildBackupTaskSpec(spec, startedAt, e.tempDir)
|
||||
runner, err := e.backupRegistry.Runner(backupSpec.Type)
|
||||
if err != nil {
|
||||
e.reportRecordFailure(ctx, recordID, fmt.Sprintf("不支持的备份类型: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 3) 运行 runner
|
||||
logger := newRecordLogger(ctx, e.client, recordID)
|
||||
result, err := runner.Run(ctx, backupSpec, logger)
|
||||
if err != nil {
|
||||
e.reportRecordFailure(ctx, recordID, err.Error())
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
// 4) 可选 gzip 压缩
|
||||
finalPath := result.ArtifactPath
|
||||
if strings.EqualFold(spec.Compression, "gzip") && !strings.HasSuffix(strings.ToLower(finalPath), ".gz") {
|
||||
e.appendLog(ctx, recordID, "[agent] 开始压缩备份文件\n")
|
||||
compressedPath, compressErr := compress.GzipFile(finalPath)
|
||||
if compressErr != nil {
|
||||
e.reportRecordFailure(ctx, recordID, fmt.Sprintf("压缩失败: %v", compressErr))
|
||||
return compressErr
|
||||
}
|
||||
finalPath = compressedPath
|
||||
} else if strings.EqualFold(spec.Compression, "zstd") && !strings.HasSuffix(strings.ToLower(finalPath), ".zst") {
|
||||
e.appendLog(ctx, recordID, "[agent] 开始压缩备份文件(zstd)\n")
|
||||
compressedPath, compressErr := compress.ZstdFile(finalPath)
|
||||
if compressErr != nil {
|
||||
e.reportRecordFailure(ctx, recordID, fmt.Sprintf("压缩失败: %v", compressErr))
|
||||
return compressErr
|
||||
}
|
||||
finalPath = compressedPath
|
||||
}
|
||||
info, err := os.Stat(finalPath)
|
||||
if err != nil {
|
||||
e.reportRecordFailure(ctx, recordID, fmt.Sprintf("获取文件信息失败: %v", err))
|
||||
return err
|
||||
}
|
||||
fileName := filepath.Base(finalPath)
|
||||
fileSize := info.Size()
|
||||
storagePath := backup.BuildRecordStorageKey(spec.Type, startedAt, recordID, fileName)
|
||||
|
||||
// 5) 计算 checksum(一次读一次)并上传到所有目标
|
||||
checksum, err := computeFileSHA256(finalPath)
|
||||
if err != nil {
|
||||
e.reportRecordFailure(ctx, recordID, fmt.Sprintf("计算 checksum 失败: %v", err))
|
||||
return err
|
||||
}
|
||||
if len(spec.StorageTargets) == 0 {
|
||||
e.reportRecordFailure(ctx, recordID, "没有关联的存储目标")
|
||||
return fmt.Errorf("no storage targets")
|
||||
}
|
||||
uploadResults := make([]StorageResultItem, 0, len(spec.StorageTargets))
|
||||
selectedStorageTargetID := uint(0)
|
||||
selectedStorageTransferMode := ""
|
||||
var uploadErrors []string
|
||||
for _, target := range spec.StorageTargets {
|
||||
if err := e.uploadToTarget(ctx, recordID, target, finalPath, storagePath, fileSize, checksum, spec.TaskID); err != nil {
|
||||
uploadResults = append(uploadResults, StorageResultItem{
|
||||
StorageTargetID: target.ID,
|
||||
StorageTargetName: target.Name,
|
||||
Status: "failed",
|
||||
TransferMode: target.TransferMode,
|
||||
Error: err.Error(),
|
||||
})
|
||||
uploadErrors = append(uploadErrors, fmt.Sprintf("%s: %v", target.Name, err))
|
||||
e.appendLog(ctx, recordID, fmt.Sprintf("[agent] 上传到存储目标 %s 失败: %v\n", target.Name, err))
|
||||
continue
|
||||
}
|
||||
if selectedStorageTargetID == 0 {
|
||||
selectedStorageTargetID = target.ID
|
||||
selectedStorageTransferMode = target.TransferMode
|
||||
}
|
||||
uploadResults = append(uploadResults, StorageResultItem{
|
||||
StorageTargetID: target.ID,
|
||||
StorageTargetName: target.Name,
|
||||
Status: "success",
|
||||
StoragePath: storagePath,
|
||||
FileSize: fileSize,
|
||||
TransferMode: target.TransferMode,
|
||||
})
|
||||
e.appendLog(ctx, recordID, fmt.Sprintf("[agent] 已上传到存储目标 %s\n", target.Name))
|
||||
}
|
||||
if selectedStorageTargetID == 0 {
|
||||
msg := strings.Join(uploadErrors, "; ")
|
||||
if msg == "" {
|
||||
msg = "所有存储目标上传均失败"
|
||||
}
|
||||
e.reportRecordFailureWithUploadResults(ctx, recordID, msg, uploadResults)
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
// 6) 上报最终成功
|
||||
return e.client.UpdateRecord(ctx, recordID, RecordUpdate{
|
||||
Status: "success",
|
||||
FileName: fileName,
|
||||
FileSize: fileSize,
|
||||
Checksum: checksum,
|
||||
StoragePath: storagePath,
|
||||
StorageTargetID: selectedStorageTargetID,
|
||||
StorageTransferMode: selectedStorageTransferMode,
|
||||
StorageUploadResults: uploadResults,
|
||||
LogAppend: fmt.Sprintf("[agent] 任务完成,总计 %d 字节\n", fileSize),
|
||||
})
|
||||
}
|
||||
|
||||
// uploadToTarget 上传单个目标。为保持简化不做上传级重试(rclone 本身已有 low-level 重试)。
|
||||
func (e *Executor) uploadToTarget(ctx context.Context, recordID uint, target StorageTargetConfig, filePath, objectKey string, fileSize int64, checksum string, taskID uint) error {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open artifact: %w", err)
|
||||
}
|
||||
if target.TransferMode == storage.TransferModeMasterRelay {
|
||||
uploadErr := e.client.UploadArtifact(ctx, recordID, target.ID, objectKey, fileSize, checksum, f)
|
||||
return errors.Join(uploadErr, f.Close())
|
||||
}
|
||||
var rawConfig map[string]any
|
||||
if len(target.Config) > 0 {
|
||||
// DecodeRawConfig 通过 json 解析
|
||||
if err := jsonUnmarshalMap(target.Config, &rawConfig); err != nil {
|
||||
return errors.Join(fmt.Errorf("parse storage config: %w", err), f.Close())
|
||||
}
|
||||
}
|
||||
provider, err := e.storageRegistry.Create(ctx, target.Type, rawConfig)
|
||||
if err != nil {
|
||||
closeErr := f.Close()
|
||||
return errors.Join(fmt.Errorf("create provider: %w", err), closeErr)
|
||||
}
|
||||
meta := map[string]string{
|
||||
"taskId": fmt.Sprintf("%d", taskID),
|
||||
"recordId": fmt.Sprintf("%d", recordID),
|
||||
}
|
||||
uploadErr := provider.Upload(ctx, objectKey, f, fileSize, meta)
|
||||
return errors.Join(uploadErr, f.Close())
|
||||
}
|
||||
|
||||
// appendLog 追加日志到 Master 记录(尽力而为,失败不中断主流程)
|
||||
func (e *Executor) appendLog(ctx context.Context, recordID uint, line string) {
|
||||
_ = e.client.UpdateRecord(ctx, recordID, RecordUpdate{LogAppend: line})
|
||||
}
|
||||
|
||||
// reportRecordFailure 上报失败状态
|
||||
func (e *Executor) reportRecordFailure(ctx context.Context, recordID uint, msg string) {
|
||||
e.reportRecordFailureWithUploadResults(ctx, recordID, msg, nil)
|
||||
}
|
||||
|
||||
func (e *Executor) reportRecordFailureWithUploadResults(ctx context.Context, recordID uint, msg string, uploadResults []StorageResultItem) {
|
||||
_ = e.client.UpdateRecord(ctx, recordID, RecordUpdate{
|
||||
Status: "failed",
|
||||
ErrorMessage: msg,
|
||||
StorageUploadResults: uploadResults,
|
||||
LogAppend: fmt.Sprintf("[agent] 错误: %s\n", msg),
|
||||
})
|
||||
}
|
||||
|
||||
// buildBackupTaskSpec 把 AgentTaskSpec 转换为 backup.TaskSpec。
|
||||
func buildBackupTaskSpec(spec *TaskSpec, startedAt time.Time, tempDir string) backup.TaskSpec {
|
||||
sourcePaths := parseStringListField(spec.SourcePaths)
|
||||
excludes := parseStringListField(spec.ExcludePatterns)
|
||||
return backup.TaskSpec{
|
||||
ID: spec.TaskID,
|
||||
Name: spec.Name,
|
||||
Type: spec.Type,
|
||||
SourcePath: spec.SourcePath,
|
||||
SourcePaths: sourcePaths,
|
||||
ExcludePatterns: excludes,
|
||||
Database: backup.DatabaseSpec{
|
||||
Host: spec.DBHost,
|
||||
Port: spec.DBPort,
|
||||
User: spec.DBUser,
|
||||
Password: spec.DBPassword,
|
||||
Path: spec.DBPath,
|
||||
Names: splitCommaOrNewline(spec.DBName),
|
||||
},
|
||||
Compression: spec.Compression,
|
||||
Encrypt: spec.Encrypt,
|
||||
StartedAt: startedAt,
|
||||
TempDir: tempDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) ensureTempDir() error {
|
||||
if err := os.MkdirAll(e.tempDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create agent temp dir: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseStringListField(value string) []string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" || trimmed == "[]" {
|
||||
return nil
|
||||
}
|
||||
var jsonItems []string
|
||||
if err := json.Unmarshal([]byte(trimmed), &jsonItems); err == nil {
|
||||
return compactStringList(jsonItems)
|
||||
}
|
||||
return compactStringList(strings.FieldsFunc(trimmed, func(r rune) bool {
|
||||
return r == '\n' || r == '\r'
|
||||
}))
|
||||
}
|
||||
|
||||
func compactStringList(items []string) []string {
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
if trimmed := strings.TrimSpace(item); trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// recordLogger 把 runner 日志回传到 Master 记录。
|
||||
// 实现 backup.LogWriter,每条日志追加到 record.log_content。
|
||||
type recordLogger struct {
|
||||
ctx context.Context
|
||||
client *MasterClient
|
||||
recordID uint
|
||||
}
|
||||
|
||||
func newRecordLogger(ctx context.Context, client *MasterClient, recordID uint) *recordLogger {
|
||||
return &recordLogger{ctx: ctx, client: client, recordID: recordID}
|
||||
}
|
||||
|
||||
func (l *recordLogger) WriteLine(message string) {
|
||||
_ = l.client.UpdateRecord(l.ctx, l.recordID, RecordUpdate{LogAppend: message + "\n"})
|
||||
}
|
||||
|
||||
// restoreLogger 把 runner 日志回传到 Master 恢复记录。
|
||||
type restoreLogger struct {
|
||||
ctx context.Context
|
||||
client *MasterClient
|
||||
restoreID uint
|
||||
}
|
||||
|
||||
func newRestoreLogger(ctx context.Context, client *MasterClient, restoreID uint) *restoreLogger {
|
||||
return &restoreLogger{ctx: ctx, client: client, restoreID: restoreID}
|
||||
}
|
||||
|
||||
func (l *restoreLogger) WriteLine(message string) {
|
||||
_ = l.client.UpdateRestore(l.ctx, l.restoreID, RestoreUpdate{LogAppend: message + "\n"})
|
||||
}
|
||||
|
||||
// DeleteStorageObject 在 Agent 本机上删除指定存储对象(供跨节点清理调用)。
|
||||
func (e *Executor) DeleteStorageObject(ctx context.Context, targetType string, targetConfig map[string]any, storagePath string) error {
|
||||
provider, err := e.storageRegistry.Create(ctx, targetType, targetConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create provider: %w", err)
|
||||
}
|
||||
return provider.Delete(ctx, storagePath)
|
||||
}
|
||||
|
||||
// ExecuteRestore 处理 restore_record 命令:拉规格 → 下载 → 解压 → 执行 runner.Restore → 上报结果。
|
||||
//
|
||||
// 与 ExecuteRunTask 对称,但方向相反:
|
||||
// - 下载:直连共享存储,或通过 Master 中转其本地磁盘对象
|
||||
// - 解密:当前 Agent 不支持加密恢复(密钥未下发),spec.Encrypt=true 会直接失败
|
||||
// - 执行:backup.Registry.Runner(spec.Type).Restore
|
||||
// - 上报:通过 UpdateRestore(status/logAppend)
|
||||
func (e *Executor) ExecuteRestore(ctx context.Context, restoreRecordID uint) error {
|
||||
if err := e.ensureTempDir(); err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
spec, err := e.client.GetRestoreSpec(ctx, restoreRecordID)
|
||||
if err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("拉取恢复规格失败: %v", err))
|
||||
return err
|
||||
}
|
||||
if spec.Encrypt {
|
||||
msg := "Agent 不支持加密恢复(加密密钥仅在 Master 端持有)"
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, msg)
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
e.appendRestoreLog(ctx, restoreRecordID, fmt.Sprintf("[agent] 开始恢复 %s (type=%s)\n", spec.TaskName, spec.Type))
|
||||
|
||||
tmpDir, err := os.MkdirTemp(e.tempDir, "restore-*")
|
||||
if err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("创建恢复临时目录失败: %v", err))
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// 1) 下载
|
||||
fileName := spec.FileName
|
||||
if strings.TrimSpace(fileName) == "" {
|
||||
fileName = filepath.Base(spec.StoragePath)
|
||||
}
|
||||
artifactPath := filepath.Join(tmpDir, filepath.Base(fileName))
|
||||
e.appendRestoreLog(ctx, restoreRecordID, fmt.Sprintf("[agent] 下载备份文件 %s\n", spec.StoragePath))
|
||||
var reader io.ReadCloser
|
||||
if spec.Storage.TransferMode == storage.TransferModeMasterRelay {
|
||||
reader, err = e.client.DownloadRestoreArtifact(ctx, restoreRecordID)
|
||||
} else {
|
||||
var rawConfig map[string]any
|
||||
if len(spec.Storage.Config) > 0 {
|
||||
if err := jsonUnmarshalMap(spec.Storage.Config, &rawConfig); err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("解析存储配置失败: %v", err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
provider, providerErr := e.storageRegistry.Create(ctx, spec.Storage.Type, rawConfig)
|
||||
if providerErr != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("创建存储客户端失败: %v", providerErr))
|
||||
return providerErr
|
||||
}
|
||||
reader, err = provider.Download(ctx, spec.StoragePath)
|
||||
}
|
||||
if err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("下载备份失败: %v", err))
|
||||
return err
|
||||
}
|
||||
if err := writeReaderToLocal(artifactPath, reader); err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("写入备份文件失败: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 2.5) 完整性校验:还原前比对下载对象的 SHA-256(与 Master 本地恢复路径一致)。
|
||||
// 拒绝还原损坏或被篡改的备份;早期无 checksum 的备份跳过(向后兼容)。
|
||||
if strings.TrimSpace(spec.Checksum) != "" {
|
||||
e.appendRestoreLog(ctx, restoreRecordID, "[agent] 校验备份完整性(SHA-256)\n")
|
||||
actual, sumErr := computeFileSHA256(artifactPath)
|
||||
if sumErr != nil {
|
||||
msg := fmt.Sprintf("计算校验和失败: %v", sumErr)
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, msg)
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
if !strings.EqualFold(actual, spec.Checksum) {
|
||||
msg := "备份文件完整性校验失败:SHA-256 不匹配,文件可能已损坏或被篡改"
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, msg)
|
||||
return fmt.Errorf("%s(期望 %s,实际 %s)", msg, spec.Checksum, actual)
|
||||
}
|
||||
e.appendRestoreLog(ctx, restoreRecordID, "[agent] 完整性校验通过\n")
|
||||
}
|
||||
|
||||
// 3) 解压(Agent 不支持加密,遇到 .enc 会直接失败)
|
||||
preparedPath := artifactPath
|
||||
if strings.HasSuffix(strings.ToLower(preparedPath), ".enc") {
|
||||
msg := "检测到加密后缀,Agent 不支持加密恢复"
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, msg)
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(preparedPath), ".gz") {
|
||||
e.appendRestoreLog(ctx, restoreRecordID, "[agent] 解压 gzip 压缩\n")
|
||||
decompressed, err := compress.GunzipFile(preparedPath)
|
||||
if err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("解压失败: %v", err))
|
||||
return err
|
||||
}
|
||||
preparedPath = decompressed
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(preparedPath), ".zst") {
|
||||
e.appendRestoreLog(ctx, restoreRecordID, "[agent] 解压 zstd 压缩\n")
|
||||
decompressed, err := compress.UnzstdFile(preparedPath)
|
||||
if err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("解压失败: %v", err))
|
||||
return err
|
||||
}
|
||||
preparedPath = decompressed
|
||||
}
|
||||
|
||||
// 4) 运行 runner.Restore
|
||||
taskSpec := buildRestoreBackupTaskSpec(spec, time.Now().UTC(), tmpDir)
|
||||
runner, err := e.backupRegistry.Runner(taskSpec.Type)
|
||||
if err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, fmt.Sprintf("不支持的备份类型: %v", err))
|
||||
return err
|
||||
}
|
||||
logger := newRestoreLogger(ctx, e.client, restoreRecordID)
|
||||
if err := runner.Restore(ctx, taskSpec, preparedPath, logger); err != nil {
|
||||
e.reportRestoreFailure(ctx, restoreRecordID, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 5) 上报成功
|
||||
return e.client.UpdateRestore(ctx, restoreRecordID, RestoreUpdate{
|
||||
Status: "success",
|
||||
LogAppend: "[agent] 恢复执行完成\n",
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Executor) appendRestoreLog(ctx context.Context, restoreID uint, line string) {
|
||||
_ = e.client.UpdateRestore(ctx, restoreID, RestoreUpdate{LogAppend: line})
|
||||
}
|
||||
|
||||
func (e *Executor) reportRestoreFailure(ctx context.Context, restoreID uint, msg string) {
|
||||
_ = e.client.UpdateRestore(ctx, restoreID, RestoreUpdate{
|
||||
Status: "failed",
|
||||
ErrorMessage: msg,
|
||||
LogAppend: fmt.Sprintf("[agent] 错误: %s\n", msg),
|
||||
})
|
||||
}
|
||||
|
||||
// buildRestoreBackupTaskSpec 把 RestoreSpec 转成 backup.TaskSpec。
|
||||
func buildRestoreBackupTaskSpec(spec *RestoreSpec, startedAt time.Time, tempDir string) backup.TaskSpec {
|
||||
return backup.TaskSpec{
|
||||
ID: spec.TaskID,
|
||||
Name: spec.TaskName,
|
||||
Type: spec.Type,
|
||||
SourcePath: spec.SourcePath,
|
||||
SourcePaths: spec.SourcePaths,
|
||||
ExcludePatterns: nil,
|
||||
Database: backup.DatabaseSpec{
|
||||
Host: spec.DBHost,
|
||||
Port: spec.DBPort,
|
||||
User: spec.DBUser,
|
||||
Password: spec.DBPassword,
|
||||
Path: spec.DBPath,
|
||||
Names: splitCommaOrNewline(spec.DBName),
|
||||
},
|
||||
Compression: spec.Compression,
|
||||
Encrypt: spec.Encrypt,
|
||||
StartedAt: startedAt,
|
||||
TempDir: tempDir,
|
||||
}
|
||||
}
|
||||
|
||||
// writeReaderToLocal 把 reader 写到本地文件(Agent 侧工具函数)。
|
||||
func writeReaderToLocal(targetPath string, reader io.ReadCloser) (err error) {
|
||||
defer func() {
|
||||
err = errors.Join(err, reader.Close())
|
||||
}()
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, copyErr := io.Copy(file, reader)
|
||||
return errors.Join(copyErr, file.Close())
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
|
||||
func computeFileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func splitCommaOrNewline(s string) []string {
|
||||
var result []string
|
||||
for _, part := range strings.FieldsFunc(s, func(r rune) bool {
|
||||
return r == ',' || r == '\n' || r == ';'
|
||||
}) {
|
||||
if p := strings.TrimSpace(part); p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
380
server/internal/agent/executor_test.go
Normal file
380
server/internal/agent/executor_test.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backupx/server/internal/storage"
|
||||
)
|
||||
|
||||
func TestBuildBackupTaskSpecParsesJSONSourcePaths(t *testing.T) {
|
||||
spec := &TaskSpec{
|
||||
TaskID: 7,
|
||||
Name: "root-files",
|
||||
Type: "file",
|
||||
SourcePaths: `["/root","/etc"]`,
|
||||
ExcludePatterns: `["*.log","tmp"]`,
|
||||
}
|
||||
|
||||
got := buildBackupTaskSpec(spec, time.Unix(0, 0), "/var/lib/backupx-agent/tmp")
|
||||
|
||||
if !reflect.DeepEqual(got.SourcePaths, []string{"/root", "/etc"}) {
|
||||
t.Fatalf("source paths = %#v", got.SourcePaths)
|
||||
}
|
||||
if !reflect.DeepEqual(got.ExcludePatterns, []string{"*.log", "tmp"}) {
|
||||
t.Fatalf("exclude patterns = %#v", got.ExcludePatterns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringListFieldKeepsLegacyLineFormat(t *testing.T) {
|
||||
got := parseStringListField("/root\n /etc \n")
|
||||
want := []string{"/root", "/etc"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("paths = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRunTaskRecordsPerTargetUploadResults(t *testing.T) {
|
||||
sourceDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "index.html"), []byte("hello"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
var finalUpdate RecordUpdate
|
||||
var updates []RecordUpdate
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/tasks/1":
|
||||
writeAgentEnvelope(t, w, TaskSpec{
|
||||
TaskID: 1,
|
||||
Name: "site",
|
||||
Type: "file",
|
||||
SourcePath: sourceDir,
|
||||
Compression: "gzip",
|
||||
StorageTargets: []StorageTargetConfig{
|
||||
{ID: 11, Name: "broken", Type: "agent_test_storage", Config: json.RawMessage(`{"name":"broken"}`)},
|
||||
{ID: 12, Name: "ok", Type: "agent_test_storage", Config: json.RawMessage(`{"name":"ok"}`)},
|
||||
},
|
||||
})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/records/99":
|
||||
var update RecordUpdate
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
t.Fatalf("Decode update returned error: %v", err)
|
||||
}
|
||||
updates = append(updates, update)
|
||||
if update.Status != "" {
|
||||
finalUpdate = update
|
||||
}
|
||||
writeAgentEnvelope(t, w, map[string]string{"status": "ok"})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
executor := NewExecutor(NewMasterClient(server.URL, "token", false), filepath.Join(t.TempDir(), "tmp"))
|
||||
executor.storageRegistry = storage.NewRegistry(&agentTestStorageFactory{
|
||||
providers: map[string]*agentTestStorageProvider{
|
||||
"broken": {name: "broken", failUpload: true},
|
||||
"ok": {name: "ok", objects: map[string][]byte{}},
|
||||
},
|
||||
})
|
||||
|
||||
if err := executor.ExecuteRunTask(context.Background(), 1, 99); err != nil {
|
||||
t.Fatalf("ExecuteRunTask returned error: %v", err)
|
||||
}
|
||||
if len(updates) == 0 || finalUpdate.Status != "success" {
|
||||
t.Fatalf("expected final success update, got updates=%#v final=%#v", updates, finalUpdate)
|
||||
}
|
||||
if finalUpdate.StorageTargetID != 12 {
|
||||
t.Fatalf("expected first successful target 12, got %d", finalUpdate.StorageTargetID)
|
||||
}
|
||||
if len(finalUpdate.StorageUploadResults) != 2 {
|
||||
t.Fatalf("expected two upload results, got %#v", finalUpdate.StorageUploadResults)
|
||||
}
|
||||
if finalUpdate.StorageUploadResults[0].Status != "failed" || finalUpdate.StorageUploadResults[1].Status != "success" {
|
||||
t.Fatalf("unexpected upload results: %#v", finalUpdate.StorageUploadResults)
|
||||
}
|
||||
if finalUpdate.StoragePath == "" || finalUpdate.FileSize <= 0 || finalUpdate.Checksum == "" {
|
||||
t.Fatalf("expected artifact metadata in final update, got %#v", finalUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRunTaskRelaysMasterLocalDiskTarget(t *testing.T) {
|
||||
sourceDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "index.html"), []byte("centralize me"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
var relayed []byte
|
||||
var finalUpdate RecordUpdate
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/tasks/1":
|
||||
writeAgentEnvelope(t, w, TaskSpec{
|
||||
TaskID: 1,
|
||||
Name: "remote-source",
|
||||
Type: "file",
|
||||
SourcePath: sourceDir,
|
||||
Compression: "gzip",
|
||||
StorageTargets: []StorageTargetConfig{{
|
||||
ID: 11, Name: "master-disk", Type: storage.TypeLocalDisk, TransferMode: storage.TransferModeMasterRelay,
|
||||
}},
|
||||
})
|
||||
case r.Method == http.MethodPut && r.URL.Path == "/api/agent/records/99/artifacts/11":
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll relayed body: %v", err)
|
||||
}
|
||||
digest := sha256.Sum256(body)
|
||||
if got := r.Header.Get("X-BackupX-SHA256"); got != fmt.Sprintf("%x", digest[:]) {
|
||||
t.Fatalf("relay checksum header = %q", got)
|
||||
}
|
||||
objectKey := r.Header.Get("X-BackupX-Object-Key")
|
||||
if !strings.Contains(objectKey, "/records/99/") || r.ContentLength != int64(len(body)) {
|
||||
t.Fatalf("invalid relay metadata: key=%q length=%d body=%d", objectKey, r.ContentLength, len(body))
|
||||
}
|
||||
relayed = append([]byte(nil), body...)
|
||||
writeAgentEnvelope(t, w, map[string]string{"status": "ok"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/records/99":
|
||||
var update RecordUpdate
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
t.Fatalf("Decode update returned error: %v", err)
|
||||
}
|
||||
if update.Status != "" {
|
||||
finalUpdate = update
|
||||
}
|
||||
writeAgentEnvelope(t, w, map[string]string{"status": "ok"})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
executor := NewExecutor(NewMasterClient(server.URL, "token", false), filepath.Join(t.TempDir(), "tmp"))
|
||||
if err := executor.ExecuteRunTask(context.Background(), 1, 99); err != nil {
|
||||
t.Fatalf("ExecuteRunTask returned error: %v", err)
|
||||
}
|
||||
if len(relayed) == 0 {
|
||||
t.Fatal("expected artifact bytes to be streamed through Master")
|
||||
}
|
||||
if finalUpdate.Status != "success" || finalUpdate.StorageTransferMode != storage.TransferModeMasterRelay {
|
||||
t.Fatalf("unexpected final relay update: %#v", finalUpdate)
|
||||
}
|
||||
if len(finalUpdate.StorageUploadResults) != 1 || finalUpdate.StorageUploadResults[0].TransferMode != storage.TransferModeMasterRelay {
|
||||
t.Fatalf("unexpected relay target result: %#v", finalUpdate.StorageUploadResults)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRestoreDownloadsMasterRelayedArtifact(t *testing.T) {
|
||||
var archive bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&archive)
|
||||
content := []byte("restored through Master")
|
||||
header := &tar.Header{Name: "site/index.html", Mode: 0o644, Size: int64(len(content)), Typeflag: tar.TypeReg}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
t.Fatalf("WriteHeader returned error: %v", err)
|
||||
}
|
||||
if _, err := tarWriter.Write(content); err != nil {
|
||||
t.Fatalf("Write returned error: %v", err)
|
||||
}
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
t.Fatalf("Close returned error: %v", err)
|
||||
}
|
||||
artifact := append([]byte(nil), archive.Bytes()...)
|
||||
digest := sha256.Sum256(artifact)
|
||||
restoreRoot := t.TempDir()
|
||||
restoreSource := filepath.Join(restoreRoot, "site")
|
||||
artifactRequests := 0
|
||||
var finalUpdate RestoreUpdate
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/restores/77/spec":
|
||||
writeAgentEnvelope(t, w, RestoreSpec{
|
||||
RestoreRecordID: 77,
|
||||
BackupRecordID: 99,
|
||||
TaskID: 1,
|
||||
TaskName: "remote-source",
|
||||
Type: "file",
|
||||
SourcePath: restoreSource,
|
||||
Storage: StorageTargetConfig{
|
||||
ID: 11, Name: "master-disk", Type: storage.TypeLocalDisk, TransferMode: storage.TransferModeMasterRelay,
|
||||
},
|
||||
StoragePath: "BackupX/file/site.tar",
|
||||
FileName: "site.tar",
|
||||
Checksum: fmt.Sprintf("%x", digest[:]),
|
||||
})
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/restores/77/artifact":
|
||||
artifactRequests++
|
||||
if r.Header.Get("X-Agent-Token") != "token" {
|
||||
t.Fatalf("missing Agent token on relay download")
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(artifact)))
|
||||
_, _ = w.Write(artifact)
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/restores/77":
|
||||
var update RestoreUpdate
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
t.Fatalf("Decode update returned error: %v", err)
|
||||
}
|
||||
if update.Status != "" {
|
||||
finalUpdate = update
|
||||
}
|
||||
writeAgentEnvelope(t, w, map[string]string{"status": "ok"})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
executor := NewExecutor(NewMasterClient(server.URL, "token", false), filepath.Join(t.TempDir(), "tmp"))
|
||||
if err := executor.ExecuteRestore(context.Background(), 77); err != nil {
|
||||
t.Fatalf("ExecuteRestore returned error: %v", err)
|
||||
}
|
||||
if artifactRequests != 1 {
|
||||
t.Fatalf("expected one relay artifact request, got %d", artifactRequests)
|
||||
}
|
||||
restored, err := os.ReadFile(filepath.Join(restoreRoot, "site", "index.html"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile returned error: %v", err)
|
||||
}
|
||||
if string(restored) != string(content) {
|
||||
t.Fatalf("restored content = %q, want %q", restored, content)
|
||||
}
|
||||
if finalUpdate.Status != "success" {
|
||||
t.Fatalf("unexpected final restore update: %#v", finalUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRunTaskReportsPerTargetUploadResultsWhenAllTargetsFail(t *testing.T) {
|
||||
sourceDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "index.html"), []byte("hello"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
var finalUpdate RecordUpdate
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/tasks/1":
|
||||
writeAgentEnvelope(t, w, TaskSpec{
|
||||
TaskID: 1,
|
||||
Name: "site",
|
||||
Type: "file",
|
||||
SourcePath: sourceDir,
|
||||
Compression: "gzip",
|
||||
StorageTargets: []StorageTargetConfig{
|
||||
{ID: 11, Name: "broken-a", Type: "agent_test_storage", Config: json.RawMessage(`{"name":"broken-a"}`)},
|
||||
{ID: 12, Name: "broken-b", Type: "agent_test_storage", Config: json.RawMessage(`{"name":"broken-b"}`)},
|
||||
},
|
||||
})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/records/99":
|
||||
var update RecordUpdate
|
||||
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||
t.Fatalf("Decode update returned error: %v", err)
|
||||
}
|
||||
if update.Status != "" {
|
||||
finalUpdate = update
|
||||
}
|
||||
writeAgentEnvelope(t, w, map[string]string{"status": "ok"})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
executor := NewExecutor(NewMasterClient(server.URL, "token", false), filepath.Join(t.TempDir(), "tmp"))
|
||||
executor.storageRegistry = storage.NewRegistry(&agentTestStorageFactory{
|
||||
providers: map[string]*agentTestStorageProvider{
|
||||
"broken-a": {name: "broken-a", failUpload: true},
|
||||
"broken-b": {name: "broken-b", failUpload: true},
|
||||
},
|
||||
})
|
||||
|
||||
if err := executor.ExecuteRunTask(context.Background(), 1, 99); err == nil {
|
||||
t.Fatal("expected ExecuteRunTask to return upload failure")
|
||||
}
|
||||
if finalUpdate.Status != "failed" {
|
||||
t.Fatalf("expected final failed update, got %#v", finalUpdate)
|
||||
}
|
||||
if len(finalUpdate.StorageUploadResults) != 2 {
|
||||
t.Fatalf("expected failed update to keep per-target results, got %#v", finalUpdate.StorageUploadResults)
|
||||
}
|
||||
for _, item := range finalUpdate.StorageUploadResults {
|
||||
if item.Status != "failed" || item.Error == "" {
|
||||
t.Fatalf("unexpected upload result: %#v", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type agentTestStorageFactory struct {
|
||||
providers map[string]*agentTestStorageProvider
|
||||
}
|
||||
|
||||
func (f *agentTestStorageFactory) Type() storage.ProviderType {
|
||||
return "agent_test_storage"
|
||||
}
|
||||
|
||||
func (f *agentTestStorageFactory) New(_ context.Context, config map[string]any) (storage.StorageProvider, error) {
|
||||
name, _ := config["name"].(string)
|
||||
provider := f.providers[name]
|
||||
if provider == nil {
|
||||
return nil, fmt.Errorf("unknown provider %q", name)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
type agentTestStorageProvider struct {
|
||||
name string
|
||||
failUpload bool
|
||||
objects map[string][]byte
|
||||
}
|
||||
|
||||
func (p *agentTestStorageProvider) Type() storage.ProviderType { return "agent_test_storage" }
|
||||
func (p *agentTestStorageProvider) TestConnection(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
func (p *agentTestStorageProvider) Upload(_ context.Context, objectKey string, reader io.Reader, _ int64, _ map[string]string) error {
|
||||
if p.failUpload {
|
||||
return fmt.Errorf("upload failed for %s", p.name)
|
||||
}
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if p.objects == nil {
|
||||
p.objects = map[string][]byte{}
|
||||
}
|
||||
p.objects[objectKey] = data
|
||||
return nil
|
||||
}
|
||||
func (p *agentTestStorageProvider) Download(_ context.Context, objectKey string) (io.ReadCloser, error) {
|
||||
data, ok := p.objects[objectKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("object %s not found", objectKey)
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(string(data))), nil
|
||||
}
|
||||
func (p *agentTestStorageProvider) Delete(_ context.Context, objectKey string) error {
|
||||
delete(p.objects, objectKey)
|
||||
return nil
|
||||
}
|
||||
func (p *agentTestStorageProvider) List(context.Context, string) ([]storage.ObjectInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func writeAgentEnvelope(t *testing.T, w http.ResponseWriter, data any) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(map[string]any{"code": "OK", "data": data}); err != nil {
|
||||
t.Fatalf("Encode response returned error: %v", err)
|
||||
}
|
||||
}
|
||||
50
server/internal/agent/fs.go
Normal file
50
server/internal/agent/fs.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DirEntry Agent 返回给 Master 的目录项。
|
||||
type DirEntry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// listLocalDir 列出 Agent 所在机器的指定路径。
|
||||
func listLocalDir(path string) ([]DirEntry, error) {
|
||||
cleaned := filepath.Clean(strings.TrimSpace(path))
|
||||
if strings.TrimSpace(path) == "" || cleaned == "." {
|
||||
cleaned = "/"
|
||||
}
|
||||
entries, err := os.ReadDir(cleaned)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read dir: %w", err)
|
||||
}
|
||||
result := make([]DirEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
info, _ := entry.Info()
|
||||
size := int64(0)
|
||||
if info != nil && !entry.IsDir() {
|
||||
size = info.Size()
|
||||
}
|
||||
result = append(result, DirEntry{
|
||||
Name: entry.Name(),
|
||||
Path: filepath.Join(cleaned, entry.Name()),
|
||||
IsDir: entry.IsDir(),
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].IsDir != result[j].IsDir {
|
||||
return result[i].IsDir
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user