chore(repo): 完善仓库维护与自动化 (#108)

统一 Node.js 24 LTS、CI、文档和发布工作流配置。

完善仓库维护规范、Dependabot 与贡献文档,清理生成产物并统一既有 Go 代码格式。
This commit is contained in:
Wu Qing
2026-08-09 21:18:19 +08:00
committed by GitHub
parent bdd16dafa8
commit 9080a47703
46 changed files with 645 additions and 320 deletions

25
.editorconfig Normal file
View File

@@ -0,0 +1,25 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{go,mod,sum}]
indent_style = tab
indent_size = 4
[Makefile]
indent_style = tab
[*.{js,jsx,ts,tsx,json,yml,yaml,css,scss,html,md,mdx}]
indent_style = space
indent_size = 2
[*.sh]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false

15
.gitattributes vendored Normal file
View File

@@ -0,0 +1,15 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.gif binary
*.ico binary
*.jpg binary
*.jpeg binary
*.png binary
*.webp binary
*.woff binary
*.woff2 binary
*.gz binary
*.zip binary

View File

@@ -1,14 +1,14 @@
---
name: "\U0001F41B Bug 报告"
about: "报告一个 Bug帮助我们改进 BackupX"
name: "Bug 报告"
about: "提交可复现的缺陷报告"
title: "[Bug] "
labels: ["bug"]
assignees: []
---
## 描述
## 问题描述
<!-- 清晰简洁地描述这个 Bug -->
<!-- 清晰说明发生了什么,以及它为什么是缺陷。 -->
## 复现步骤
@@ -18,26 +18,39 @@ assignees: []
## 期望行为
<!-- 描述你期望发生什么 -->
<!-- 描述正确结果。 -->
## 实际行为
<!-- 描述实际发生了什么 -->
<!-- 描述实际结果,包括稳定复现还是偶发。 -->
## 环境信息
- **OS**: <!-- 例如 Ubuntu 22.04 -->
- **Go 版本**: <!-- 例如 1.21 -->
- **Node.js 版本**: <!-- 例如 18.17 -->
- **浏览器**: <!-- 例如 Chrome 120 -->
- **BackupX 版本**: <!-- 例如 v1.0.0 -->
- BackupX 版本或提交:
- 部署方式Docker、裸机、集群
- 操作系统与架构:
- 数据库或备份类型:
- 浏览器版本(仅界面问题):
- 是否使用代理、私有 CA 或堡垒机:
## 最小配置
<!-- 仅保留复现所需字段。删除密码、Token、域名、IP 和存储凭据。 -->
```yaml
```
## 相关日志
```
<!-- 粘贴相关日志输出 -->
<!-- 附问题发生前后的最小日志片段,并先删除所有敏感信息。 -->
```text
```
## 截图
## 补充信息
<!-- 如有截图请附上 -->
<!-- 可附截图、相关 Issue、临时缓解方式或其他上下文。 -->
请勿提交数据库文件、完整配置、备份数据、API Key、Agent Token、安装命令或未脱敏日志。安全漏洞请使用私密漏洞报告入口。

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 💬 讨论区 / Discussions
url: https://github.com/Awuqing/GoogleDriverBackupEveryDay/discussions
about: 提问、讨论功能想法或分享使用经验
- name: 安全漏洞 / Security vulnerability
url: https://github.com/Awuqing/BackupX/security/advisories/new
about: 请通过私密渠道报告安全问题,不要创建公开 Issue。

View File

@@ -1,23 +1,36 @@
---
name: "\U0001F680 功能请求"
about: "建议一个新功能或改进"
name: "功能请求"
about: "建议一个可落地的新功能或改进"
title: "[Feature] "
labels: ["enhancement"]
assignees: []
---
## 需求描述
<!-- 清晰简洁地描述你希望的功能 -->
## 使用场景
<!-- 描述在什么场景下需要这个功能 -->
<!-- 描述在什么环境中遇到了什么问题。 -->
## 建议的解决方案
## 期望结果
<!-- 描述你认为应该如何实现(可选) -->
<!-- 说明用户最终需要完成的任务,不要只描述界面元素。 -->
## 建议方案
<!-- 可选:描述 API、配置、工作流或界面交互。 -->
## 替代方案
<!-- 说明当前绕过方式,以及它为什么不足。 -->
## 兼容性与风险
<!-- 是否影响现有配置、数据格式、部署方式、权限或集群节点。 -->
## 验收标准
- [ ]
- [ ]
## 补充信息
<!-- 任何其他相关信息、截图或上下文 -->
<!-- 可附相关文档、日志、截图或同类实现。不要附敏感信息。 -->

View File

@@ -1,29 +1,48 @@
## Pull Request
## 变更说明 / Summary
### 变更类型 / Type of Change
<!-- 说明做了什么、为什么需要,以及对用户或运维人员的影响。 -->
- [ ] 🐛 Bug 修复 (非破坏性变更)
- [ ] ✨ 新功能 (非破坏性变更)
- [ ] 💥 破坏性变更 (修复或功能导致现有功能变更)
- [ ] 📝 文档更新
- [ ] ♻️ 代码重构 (不影响功能)
- [ ] ⚡ 性能优化
## 变更类型 / Type of Change
### 描述 / Description
- [ ] 缺陷修复
- [ ] 新功能
- [ ] 破坏性变更
- [ ] 性能优化
- [ ] 重构或工程化
- [ ] 文档更新
<!-- 描述你做了什么更改以及为什么 -->
## 相关 Issue / Related Issue
### 相关 Issue / Related Issue
<!-- 例如Closes #123。没有关联 Issue 时写“无”。 -->
<!-- 关联的 Issue 编号,如 Fixes #123 -->
## 验证 / Validation
### 测试 / Testing
<!-- 只勾选实际执行过的项目,并在下方补充无法执行的原因。 -->
- [ ] 后端测试通过 (`go test ./...`)
- [ ] 前端测试通过 (`npm run test`)
- [ ] 前端构建成功 (`npm run build`)
- [ ] 已在本地环境手动测试
- [ ] 后端格式、静态检查与测试通过
- [ ] 前端测试与生产构建通过
- [ ] 文档类型检查与中英文生产构建通过
- [ ] 配置、迁移或部署兼容性已验证
- [ ] 已完成与变更范围相符的手动验证
### 截图 / Screenshots
验证命令与结果:
<!-- 如有 UI 变更请附上截图 -->
```text
```
## 风险与回滚 / Risk and Rollback
<!-- 说明兼容性风险、数据迁移、配置变化和回滚方法。无风险时写“无”。 -->
## 界面变更 / UI Changes
<!-- 有界面变更时附前后截图;无界面变更时写“无”。 -->
## 提交前检查 / Checklist
- [ ] 提交信息符合 Conventional Commits
- [ ] 未提交密钥、Token、真实配置、数据库或日志
- [ ] 新行为已补充测试和中英文文档
- [ ] 前端未使用 Emoji、渐变、阴影、独立字体或超过 4px 的圆角
- [ ] 没有夹带与本 PR 无关的格式化或重构

50
.github/SECURITY.md vendored
View File

@@ -1,34 +1,40 @@
# 安全漏洞披露政策 / Security Policy
# 安全策 / Security Policy
## 支持的版本 / Supported Versions
## 支持范围 / Supported Versions
| Version | Supported |
|---------|--------------------|
| latest | ✅ |
| < latest | ❌ |
安全修复面向最新稳定版本和 `main` 分支。旧版本不会单独维护安全补丁;升级前请先阅读对应 Release Notes 和升级恢复文档。
## 报告安全漏洞 / Reporting a Vulnerability
Security fixes target the latest stable release and the `main` branch. Older versions do not receive separate security patches. Review the release notes and upgrade documentation before updating.
如果您发现了安全漏洞,**请不要通过公开 Issue 报告**。
| Version | Status |
|---------|--------|
| Latest stable release | Supported |
| `main` | Development support |
| Older releases | Unsupported |
请发送邮件至项目维护者,包含以下信息:
## 报告漏洞 / Reporting a Vulnerability
1. 漏洞描述
2. 复现步骤
3. 受影响的版本
4. 可能的影响范围
请勿通过公开 Issue、Discussion 或 Pull Request 披露安全漏洞。使用 GitHub 的[私密漏洞报告入口](https://github.com/Awuqing/BackupX/security/advisories/new)提交报告。
我们会在 48 小时内确认收到并开始处理。
Do not disclose vulnerabilities in a public Issue, Discussion, or Pull Request. Submit the report through GitHub's [private vulnerability reporting form](https://github.com/Awuqing/BackupX/security/advisories/new).
---
报告应包含:
If you discover a security vulnerability, **please do NOT open a public issue**.
1. 受影响版本或提交;
2. 漏洞描述、攻击前提和影响范围;
3. 最小复现步骤或验证代码;
4. 已知缓解措施;
5. 希望使用的署名信息。
Instead, email the project maintainer with the following details:
不要上传真实密钥、Token、备份数据、数据库或包含客户信息的日志。必要时请先脱敏并使用最小化测试数据。
1. Description of the vulnerability
2. Steps to reproduce
3. Affected versions
4. Potential impact
Do not upload real credentials, tokens, backup data, databases, or logs containing customer information. Redact sensitive values and use minimal test data.
We will acknowledge receipt within 48 hours and begin working on a fix.
## 处理流程 / Response Process
- 维护者会尽快确认报告并进行初步分级;
- 修复期间请保持细节私密,避免影响仍未升级的部署;
- 修复发布后会在安全公告或 Release Notes 中说明受影响范围、缓解措施和升级版本;
- 披露时间由报告者与维护者协调确定。
The maintainer will acknowledge and triage the report as soon as practical. Details should remain private until a fix and coordinated disclosure are ready.

86
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: "03:00"
timezone: Asia/Shanghai
open-pull-requests-limit: 5
commit-message:
prefix: chore(deps)
groups:
github-actions:
patterns:
- "*"
- package-ecosystem: gomod
directory: /server
schedule:
interval: weekly
day: monday
time: "03:10"
timezone: Asia/Shanghai
open-pull-requests-limit: 5
commit-message:
prefix: chore(deps)
groups:
go-minor-and-patch:
patterns:
- "*"
update-types:
- minor
- patch
- package-ecosystem: npm
directory: /web
schedule:
interval: weekly
day: monday
time: "03:20"
timezone: Asia/Shanghai
open-pull-requests-limit: 5
commit-message:
prefix: chore(deps)
groups:
web-minor-and-patch:
patterns:
- "*"
update-types:
- minor
- patch
- package-ecosystem: npm
directory: /docs-site
schedule:
interval: weekly
day: monday
time: "03:30"
timezone: Asia/Shanghai
open-pull-requests-limit: 5
commit-message:
prefix: chore(deps)
groups:
docs-minor-and-patch:
patterns:
- "*"
update-types:
- minor
- patch
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: monday
time: "03:40"
timezone: Asia/Shanghai
open-pull-requests-limit: 5
commit-message:
prefix: chore(deps)
groups:
container-images:
patterns:
- "*"

View File

@@ -2,18 +2,24 @@ name: CI
on:
push:
branches: [main, master]
branches: [main]
pull_request:
branches: [main, master]
branches: [main]
workflow_dispatch:
# 最小权限:构建/测试仅需读取仓库内容,显式声明以收敛默认的可写令牌。
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
backend:
name: Go Build & Test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -23,6 +29,23 @@ jobs:
go-version: '1.25'
cache-dependency-path: server/go.sum
- name: Verify modules
working-directory: server
run: go mod verify
- name: Check formatting
working-directory: server
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
printf '%s\n' "$unformatted"
exit 1
fi
- name: Vet
working-directory: server
run: go vet ./...
- name: Build
working-directory: server
run: go build ./...
@@ -34,13 +57,14 @@ jobs:
frontend:
name: React Build & Test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: web/package-lock.json
@@ -48,14 +72,10 @@ jobs:
working-directory: web
run: npm ci
- name: Type Check
working-directory: web
run: npx tsc --noEmit -p tsconfig.json
- name: Test
working-directory: web
run: npm run test
- name: Build
- name: Type Check & Build
working-directory: web
run: npm run build

View File

@@ -1,9 +1,16 @@
name: Deploy Docs
# 触发条件:
# - 推送 main 时,如果 docs-site/ 或站点相关 README 有变化
# - PR 修改 docs-site/ 时执行类型检查和构建
# - 推送 main 时构建并部署 GitHub Pages
# - 手动触发(在 Actions 页面)
on:
pull_request:
branches:
- main
paths:
- 'docs-site/**'
- '.github/workflows/docs.yml'
push:
branches:
- main
@@ -12,20 +19,20 @@ on:
- '.github/workflows/docs.yml'
workflow_dispatch:
# 允许写入 Pages用于发布到 github.com/Awuqing/BackupX 的 Pages 站点
# 默认只读Pages 写权限仅授予部署任务。
permissions:
contents: read
pages: write
id-token: write
# 同时只保留一个部署任务
# 同一分支只保留最新一次构建,避免旧提交覆盖新结果。
concurrency:
group: pages-${{ github.ref }}
group: docs-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Docs
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: docs-site
@@ -36,24 +43,34 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: docs-site/package-lock.json
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Build site
run: npm run build
- name: Upload artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: docs-site/build
deploy:
name: Deploy Docs
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

View File

@@ -27,25 +27,85 @@ on:
type: string
permissions:
contents: write
packages: write
contents: read
# 统一版本号tag 推送取 ref_name手动触发取 inputs.version
env:
VERSION: ${{ github.event.inputs.version || github.ref_name }}
concurrency:
group: release-${{ github.ref }}-${{ github.event.inputs.version || 'tag' }}
cancel-in-progress: false
jobs:
# ─── Job 0: 校验版本与测试 ───
verify:
name: Validate Release
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- name: Validate release input
shell: bash
env:
RELEASE_VERSION: ${{ env.VERSION }}
RELEASE_EVENT: ${{ github.event_name }}
RELEASE_REF: ${{ github.ref }}
run: |
if [[ ! "$RELEASE_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then
echo "Version must be a SemVer tag such as v1.2.3 or v1.2.3-rc.1"
exit 1
fi
if [[ "$RELEASE_EVENT" == "workflow_dispatch" && "$RELEASE_REF" != "refs/heads/main" ]]; then
echo "Manual releases must run from the main branch"
exit 1
fi
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache-dependency-path: server/go.sum
- name: Verify backend
working-directory: server
run: |
go mod verify
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
printf '%s\n' "$unformatted"
exit 1
fi
go vet ./...
go test ./...
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Verify frontend
working-directory: web
run: |
npm ci
npm run test
# ─── Job 1: 构建前端 ───
build-web:
name: Build Frontend
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: web/package-lock.json
@@ -67,6 +127,9 @@ jobs:
name: Build ${{ matrix.goarch }}
needs: build-web
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
strategy:
matrix:
include:
@@ -98,13 +161,13 @@ jobs:
run: |
go build \
-trimpath \
-ldflags "-s -w -X main.version=${{ env.VERSION }}" \
-ldflags "-s -w -X main.version=${VERSION}" \
-o ../backupx \
./cmd/backupx
- name: Package release
run: |
ARCHIVE_NAME="backupx-${{ env.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}"
ARCHIVE_NAME="backupx-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}"
mkdir -p "${ARCHIVE_NAME}"
cp backupx "${ARCHIVE_NAME}/"
cp -r web/dist "${ARCHIVE_NAME}/web"
@@ -130,6 +193,7 @@ jobs:
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
fail_on_unmatched_files: true
generate_release_notes: true
# ─── Job 3: Docker 多架构 → Docker Hub ───
@@ -137,6 +201,7 @@ jobs:
name: Build & Push Docker
needs: build-web
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

36
.gitignore vendored
View File

@@ -1,4 +1,38 @@
# Dependencies
web/node_modules/
docs-site/node_modules/
# Build and test artifacts
web/dist/
docs-site/build/
docs-site/.docusaurus/
server/bin/
.claude/
coverage/
*.out
*.tsbuildinfo
web/vite.config.js
web/vite.config.d.ts
# Runtime data and local configuration
data/
tmp/
temp/
*.db
*.db-shm
*.db-wal
*.log
.env
.env.*
!.env*.example
# Editors, operating systems, and local tools
.idea/
.vscode/
.DS_Store
Thumbs.db
desktop.ini
*.swp
*.swo
*~
.claude/
.codex/

1
.node-version Normal file
View File

@@ -0,0 +1 @@
24

View File

@@ -8,8 +8,8 @@ Thanks for your interest in contributing to BackupX! This guide covers how to se
### 依赖 / Prerequisites
- **Go** 1.25+(见 `server/go.mod`
- **Node.js** 20+CI 与 Docker 使用 Node 20
- **npm** 9+
- **Node.js** 24 LTS`.node-version`CI 与 Docker 使用同一主版本
- **npm** 11+
### 快速开始 / Quick Start
@@ -17,6 +17,8 @@ Thanks for your interest in contributing to BackupX! This guide covers how to se
```bash
git clone https://github.com/Awuqing/BackupX.git && cd BackupX
npm --prefix web ci
npm --prefix docs-site ci
# 终端 1 —— 后端(默认 http://localhost:8340
make dev-server
@@ -31,6 +33,7 @@ make dev-web
make build # 同时构建后端与前端
make build-server # 仅后端 → server/bin/backupx
make build-web # 仅前端 → web/dist
make build-docs # 仅文档站 → docs-site/build
make docker # 构建 Docker 镜像
make docker-cn # 国内镜像源加速构建
```
@@ -39,15 +42,17 @@ make docker-cn # 国内镜像源加速构建
## 测试 / Testing
提交前请确保测试通过
提交前应执行与 CI 一致的完整验证
```bash
make test # 后端 + 前端全部测试
make verify # 格式、依赖、静态检查、测试与三端构建
make test # 仅后端 + 前端测试
make test-server # 仅后端cd server && go test ./...
make test-web # 仅前端cd web && npm run testvitest
make check-docs # 文档类型检查 + 中英文站点构建
```
新增功能或修复缺陷时,请尽量补充对应测试。
新增功能或修复缺陷时,请补充对应测试。文档变更也必须通过严格断链检查,不能只依赖合并后的 Pages 部署结果。
## 提交信息规范 / Commit Messages
@@ -82,7 +87,7 @@ docs: 补充 CONTRIBUTING 指南
1. **Fork** 仓库并从最新的 `main` 切出特性分支;
2. **开发**功能或修复,必要时补充测试;
3. **自测**:确保 `make test` 通过;
3. **自测**:确保 `make verify` 通过;
4. **提交**:使用上述 Conventional Commits中文
5. **推送**并对着 `main` 发起 PR。
@@ -91,15 +96,17 @@ docs: 补充 CONTRIBUTING 指南
- 清晰说明本 PR 做了什么;
- 对新功能/修复,补充动机与背景;
- 关联相关 Issue`Closes #62`
- 纯文档 PR 可不附测试
- 纯文档 PR 至少应附上 `make check-docs` 的结果
> 请保持分支基于较新的 `main`:基线过旧的分支容易产生大范围冲突,难以评审与合入。
## 编码规范 / Coding Conventions
- **Go**所有错误必须处理(禁止 `_ = err`),日志使用项目已有库(`zap`,禁止 `fmt.Println`提交前执行 `gofmt`
- **前端**遵循项目 ESLint/Prettier/tsconfig 配置,不擅自引入新的 CSS 框架或 UI 库。
- **包管理**`web/` 使用 npm请提交对应的 `package-lock.json`
- **Go**遵循现有 handler → service → repository 分层;避免把连续业务流程拆成大量无复用价值的独立函数。所有错误必须处理,日志使用 `zap`,禁止 `fmt.Println`提交前执行 `gofmt``go vet`
- **前端组件**复用并组合现有组件,不在页面内复制同类交互逻辑,不擅自改变共享组件的基础样式,不引入新的 CSS 框架或 UI 库。
- **前端视觉**:不使用 Emoji、渐变和 `box-shadow`;图标统一使用项目 SVG 图标组件;圆角为 04px不引入独立字体不用加粗字体制造层级减少 Card不使用数据左侧装饰竖线
- **包管理**`web/``docs-site/` 均使用 npm依赖变更必须同步提交对应的 `package-lock.json`
- **文档**:英文源文件与 `zh-CN` 翻译应同步维护;新增页面必须加入侧边栏并通过两种语言的生产构建。
## License

View File

@@ -10,7 +10,7 @@ ARG USE_CHINA_MIRROR=false
# ---- Stage 1: Build frontend ----
FROM node:20-alpine AS web-builder
FROM node:24-alpine AS web-builder
ARG USE_CHINA_MIRROR
# 国内镜像npm 使用淘宝源

View File

@@ -1,4 +1,6 @@
.PHONY: build dev test clean docker docker-cn
.PHONY: build build-server build-web build-docs dev-server dev-web \
test test-server test-web check-docs format-check vet-server \
verify verify-server verify-web verify-docs clean docker docker-cn
# 自动获取版本号(从 git tag 或 commit hash
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
@@ -12,6 +14,9 @@ build-server:
build-web:
cd web && npm run build
build-docs:
cd docs-site && npm run build
# ── 开发模式(分别在两个终端运行)──
dev-server:
cd server && go run ./cmd/backupx
@@ -28,6 +33,28 @@ test-server:
test-web:
cd web && npm run test
check-docs:
cd docs-site && npm run typecheck && npm run build
format-check:
@unformatted="$$(gofmt -l server)"; \
if [ -n "$$unformatted" ]; then \
printf '%s\n' "$$unformatted"; \
exit 1; \
fi
vet-server:
cd server && go mod verify && go vet ./...
# ── 提交前完整验证 ──
verify: verify-server verify-web verify-docs
verify-server: format-check vet-server test-server build-server
verify-web: test-web build-web
verify-docs: check-docs
# ── Docker 构建 ──
docker:
docker build --build-arg VERSION=$(VERSION) -t backupx:$(VERSION) -t backupx:latest .

View File

@@ -10,6 +10,8 @@
<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>
<a href="https://github.com/Awuqing/BackupX/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Awuqing/BackupX/ci.yml?branch=main&style=flat-square" alt="CI"></a>
<a href="https://github.com/Awuqing/BackupX/actions/workflows/docs.yml"><img src="https://img.shields.io/github/actions/workflow/status/Awuqing/BackupX/docs.yml?branch=main&style=flat-square&label=docs" alt="Docs"></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">

View File

@@ -10,6 +10,8 @@
<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>
<a href="https://github.com/Awuqing/BackupX/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Awuqing/BackupX/ci.yml?branch=main&style=flat-square" alt="CI"></a>
<a href="https://github.com/Awuqing/BackupX/actions/workflows/docs.yml"><img src="https://img.shields.io/github/actions/workflow/status/Awuqing/BackupX/docs.yml?branch=main&style=flat-square&label=docs" alt="Docs"></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">

View File

@@ -6,7 +6,7 @@ description: Get a BackupX dev environment running — backend, frontend, tests.
# Development Setup
**Requirements:** Go ≥ 1.25, Node.js ≥ 20, npm.
**Requirements:** Go ≥ 1.25, Node.js 24 LTS, npm 11 or later.
## Clone & install

View File

@@ -51,7 +51,7 @@ The installer:
## From source
Requires Go ≥ 1.25 and Node.js ≥ 20.
Requires Go ≥ 1.25, Node.js 24 LTS, and npm 11 or later.
```bash
git clone https://github.com/Awuqing/BackupX.git && cd BackupX

View File

@@ -6,7 +6,7 @@ description: 搭建 BackupX 本地开发环境 — 后端、前端、测试。
# 开发环境
**环境要求:** Go ≥ 1.25Node.js ≥ 20npm
**环境要求:** Go ≥ 1.25Node.js 24 LTS、npm 11 或更高版本
## 克隆与依赖

View File

@@ -51,7 +51,7 @@ sudo ./install.sh # 创建系统用户、安装到 /opt/backupx、配置
## 从源码构建
依赖Go ≥ 1.25Node.js ≥ 20
依赖Go ≥ 1.25Node.js 24 LTS、npm 11 或更高版本
```bash
git clone https://github.com/Awuqing/BackupX.git && cd BackupX

View File

@@ -25,7 +25,7 @@
"typescript": "~6.0.2"
},
"engines": {
"node": ">=20.0"
"node": ">=24.0"
}
},
"node_modules/@11ty/gray-matter": {

View File

@@ -44,6 +44,6 @@
]
},
"engines": {
"node": ">=20.0"
"node": ">=24.0"
}
}

View File

@@ -95,4 +95,3 @@ func redirectStderr(path string) (func(), error) {
_ = f.Close()
}, nil
}

View File

@@ -357,4 +357,3 @@ func buildStorageRegistry() *storage.Registry {
storageRclone.RegisterAllBackends(registry)
return registry
}

View File

@@ -21,10 +21,10 @@ import (
type Function string
const (
FunctionBackup Function = "backup"
FunctionRestore Function = "restore"
FunctionInquire Function = "inquire"
FunctionDelete Function = "delete"
FunctionBackup Function = "backup"
FunctionRestore Function = "restore"
FunctionInquire Function = "inquire"
FunctionDelete Function = "delete"
)
// BackupRequest 是 BACKUP 操作的单条请求。

View File

@@ -160,7 +160,7 @@ func TestFileRunnerSelectiveRestore(t *testing.T) {
}
diffAssertContent(t, filepath.Join(restoreSrc, "a.txt"), "alpha")
diffAssertContent(t, filepath.Join(restoreSrc, "sub", "c.txt"), "charlie") // 选中目录 → 子项一并恢复
diffAssertAbsent(t, filepath.Join(restoreSrc, "b.txt")) // 未选中 → 不恢复
diffAssertAbsent(t, filepath.Join(restoreSrc, "b.txt")) // 未选中 → 不恢复
}
// TestFileRunnerDifferentialWithoutBaseIsFull 验证无基线时差异请求回退为全量(产出清单、含全部文件)。

View File

@@ -160,4 +160,3 @@ func formatFileSize(size int64) string {
return fmt.Sprintf("%d B", size)
}
}

View File

@@ -32,8 +32,8 @@ func writeTestTar(t *testing.T, entries map[string][]byte) string {
func TestVerifyTarArchive_Valid(t *testing.T) {
path := writeTestTar(t, map[string][]byte{
"readme.md": []byte("hello"),
"data.bin": []byte("world!!!"),
"readme.md": []byte("hello"),
"data.bin": []byte("world!!!"),
})
report, err := VerifyTarArchive(path, "")
if err != nil {

View File

@@ -24,10 +24,11 @@ type MaintenanceWindow struct {
// 简化语法:多个窗口以 ';' 分隔,每个窗口按 "[days=xxx;]time=HH:MM-HH:MM" 格式。
// Days 缺省 = 全周;若不合法,跳过该段而非抛错(让调用方尽力工作)。
// 示例:
// "time=01:00-05:00" 每天 1 点到 5 点
// "days=sat,sun;time=00:00-23:59" 仅周末全天
// "time=22:00-06:00" 每天跨夜
// "days=mon,tue,wed,thu,fri;time=22:00-06:00" 工作日跨夜
//
// "time=01:00-05:00" 每天 1 点到 5 点
// "days=sat,sun;time=00:00-23:59" 仅周末全天
// "time=22:00-06:00" 每天跨夜
// "days=mon,tue,wed,thu,fri;time=22:00-06:00" 工作日跨夜
func ParseMaintenanceWindows(value string) []MaintenanceWindow {
v := strings.TrimSpace(value)
if v == "" {

View File

@@ -6,12 +6,12 @@ import "time"
// 任一 Notification 可订阅多个事件EventTypes 字段存 CSV。
// 空 EventTypes + OnSuccess/OnFailure=true 时沿用旧语义(仅备份成功/失败)。
const (
NotificationEventBackupSuccess = "backup_success"
NotificationEventBackupFailed = "backup_failed"
NotificationEventBackupSuccess = "backup_success"
NotificationEventBackupFailed = "backup_failed"
NotificationEventRestoreSuccess = "restore_success"
NotificationEventRestoreFailed = "restore_failed"
NotificationEventVerifyFailed = "verify_failed"
NotificationEventSLAViolation = "sla_violation"
NotificationEventVerifyFailed = "verify_failed"
NotificationEventSLAViolation = "sla_violation"
// NotificationEventStorageUnhealthy 存储目标连接失败(后台健康扫描触发)。
NotificationEventStorageUnhealthy = "storage_unhealthy"
// NotificationEventReplicationFailed 备份复制失败。
@@ -23,13 +23,13 @@ const (
)
type Notification struct {
ID uint `gorm:"primaryKey" json:"id"`
Type string `gorm:"size:20;index;not null" json:"type"`
Name string `gorm:"size:100;uniqueIndex;not null" json:"name"`
ConfigCiphertext string `gorm:"column:config_ciphertext;type:text;not null" json:"-"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
OnSuccess bool `gorm:"column:on_success;not null;default:false" json:"onSuccess"`
OnFailure bool `gorm:"column:on_failure;not null;default:true" json:"onFailure"`
ID uint `gorm:"primaryKey" json:"id"`
Type string `gorm:"size:20;index;not null" json:"type"`
Name string `gorm:"size:100;uniqueIndex;not null" json:"name"`
ConfigCiphertext string `gorm:"column:config_ciphertext;type:text;not null" json:"-"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
OnSuccess bool `gorm:"column:on_success;not null;default:false" json:"onSuccess"`
OnFailure bool `gorm:"column:on_failure;not null;default:true" json:"onFailure"`
// EventTypes 逗号分隔,订阅的事件类型。
// 空 = 仅监听备份成功/失败(兼容旧配置);非空则严格按订阅触发。
EventTypes string `gorm:"column:event_types;size:500" json:"eventTypes"`

View File

@@ -24,19 +24,19 @@ type ReplicationRecord struct {
SourceTargetID uint `gorm:"column:source_target_id;index;not null" json:"sourceTargetId"`
SourceTarget StorageTarget `gorm:"foreignKey:SourceTargetID;references:ID" json:"sourceTarget,omitempty"`
// DestTargetID 目标存储(复制过去)
DestTargetID uint `gorm:"column:dest_target_id;index;not null" json:"destTargetId"`
DestTarget StorageTarget `gorm:"foreignKey:DestTargetID;references:ID" json:"destTarget,omitempty"`
Status string `gorm:"size:20;index;not null" json:"status"`
StoragePath string `gorm:"column:storage_path;size:500" json:"storagePath"`
FileSize int64 `gorm:"column:file_size;not null;default:0" json:"fileSize"`
Checksum string `gorm:"column:checksum;size:64" json:"checksum"`
ErrorMessage string `gorm:"column:error_message;size:2000" json:"errorMessage"`
DurationSeconds int `gorm:"column:duration_seconds;not null;default:0" json:"durationSeconds"`
TriggeredBy string `gorm:"column:triggered_by;size:100" json:"triggeredBy"`
StartedAt time.Time `gorm:"column:started_at;index;not null" json:"startedAt"`
CompletedAt *time.Time `gorm:"column:completed_at;index" json:"completedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DestTargetID uint `gorm:"column:dest_target_id;index;not null" json:"destTargetId"`
DestTarget StorageTarget `gorm:"foreignKey:DestTargetID;references:ID" json:"destTarget,omitempty"`
Status string `gorm:"size:20;index;not null" json:"status"`
StoragePath string `gorm:"column:storage_path;size:500" json:"storagePath"`
FileSize int64 `gorm:"column:file_size;not null;default:0" json:"fileSize"`
Checksum string `gorm:"column:checksum;size:64" json:"checksum"`
ErrorMessage string `gorm:"column:error_message;size:2000" json:"errorMessage"`
DurationSeconds int `gorm:"column:duration_seconds;not null;default:0" json:"durationSeconds"`
TriggeredBy string `gorm:"column:triggered_by;size:100" json:"triggeredBy"`
StartedAt time.Time `gorm:"column:started_at;index;not null" json:"startedAt"`
CompletedAt *time.Time `gorm:"column:completed_at;index" json:"completedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (ReplicationRecord) TableName() string {

View File

@@ -11,10 +11,10 @@ import "time"
// - name
// - sourcePath / sourcePaths 中的 {{.Host}} / {{.Env}} 等占位符
type TaskTemplate struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:128;uniqueIndex;not null" json:"name"`
Description string `gorm:"size:500" json:"description"`
TaskType string `gorm:"column:task_type;size:20;not null" json:"taskType"`
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:128;uniqueIndex;not null" json:"name"`
Description string `gorm:"size:500" json:"description"`
TaskType string `gorm:"column:task_type;size:20;not null" json:"taskType"`
// Payload JSON存完整 BackupTaskUpsertInput 的序列化
Payload string `gorm:"type:text;not null" json:"payload"`
CreatedBy string `gorm:"column:created_by;size:128" json:"createdBy"`

View File

@@ -126,14 +126,14 @@ func (s *DashboardService) Timeline(ctx context.Context, days int) ([]repository
// 判定规则:任务设置了 SLAHoursRPO > 0且距最近一次 success 备份的时间 > SLAHoursRPO。
// 从未成功过的任务LastSuccessAt = nil若启用也视为违约from createdAt 起算)。
type SLAViolation struct {
TaskID uint `json:"taskId"`
TaskName string `json:"taskName"`
NodeID uint `json:"nodeId"`
NodeName string `json:"nodeName,omitempty"`
SLAHoursRPO int `json:"slaHoursRpo"`
LastSuccessAt *time.Time `json:"lastSuccessAt,omitempty"`
HoursSinceLastSuccess float64 `json:"hoursSinceLastSuccess"`
NeverSucceeded bool `json:"neverSucceeded"`
TaskID uint `json:"taskId"`
TaskName string `json:"taskName"`
NodeID uint `json:"nodeId"`
NodeName string `json:"nodeName,omitempty"`
SLAHoursRPO int `json:"slaHoursRpo"`
LastSuccessAt *time.Time `json:"lastSuccessAt,omitempty"`
HoursSinceLastSuccess float64 `json:"hoursSinceLastSuccess"`
NeverSucceeded bool `json:"neverSucceeded"`
}
// SLAComplianceReport Dashboard 的 SLA 合规概览。
@@ -204,15 +204,15 @@ func roundHours(value float64) float64 {
// ClusterNodeSummary 集群节点简报Dashboard 用)。
type ClusterNodeSummary struct {
ID uint `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
Status string `json:"status"`
IsLocal bool `json:"isLocal"`
AgentVersion string `json:"agentVersion"`
VersionStatus string `json:"versionStatus"` // current | outdated | unknown
LastSeen time.Time `json:"lastSeen"`
TaskCount int64 `json:"taskCount"`
ID uint `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
Status string `json:"status"`
IsLocal bool `json:"isLocal"`
AgentVersion string `json:"agentVersion"`
VersionStatus string `json:"versionStatus"` // current | outdated | unknown
LastSeen time.Time `json:"lastSeen"`
TaskCount int64 `json:"taskCount"`
}
// ClusterOverview Dashboard 集群概览卡片。
@@ -312,9 +312,9 @@ func (s *DashboardService) Breakdown(ctx context.Context, days int) (*BreakdownS
}
}
result := &BreakdownStats{
ByType: makeBreakdown(typeCounts, typeLabel),
ByNode: makeBreakdownByUint(nodeCounts, nodeNames, "节点 #"),
ByStatus: []BreakdownItem{},
ByType: makeBreakdown(typeCounts, typeLabel),
ByNode: makeBreakdownByUint(nodeCounts, nodeNames, "节点 #"),
ByStatus: []BreakdownItem{},
ByStorage: []BreakdownItem{},
}
// 按状态(最近 days 天记录)

View File

@@ -74,11 +74,11 @@ func (s *SystemService) CheckUpdate(ctx context.Context) (*UpdateCheckResult, er
}
var release struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Body string `json:"body"`
Published string `json:"published_at"`
Assets []struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Body string `json:"body"`
Published string `json:"published_at"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
@@ -132,4 +132,3 @@ func (s *SystemService) GetInfo(_ context.Context) *SystemInfo {
}
return info
}

View File

@@ -14,9 +14,9 @@ import (
// TaskExportService 管理备份任务的 JSON 导入 / 导出。
// 用途:
// 1. 集群迁移(旧 Master → 新 Master 的任务配置搬迁)
// 2. 灾备恢复任务配置本地文件化Master 宕机后重建)
// 3. 配置审计(版本化 Git 管理 JSON 快照)
// 1. 集群迁移(旧 Master → 新 Master 的任务配置搬迁)
// 2. 灾备恢复任务配置本地文件化Master 宕机后重建)
// 3. 配置审计(版本化 Git 管理 JSON 快照)
//
// 出于安全考虑,导出/导入不包含任何敏感字段:
// - 数据库密码DBPasswordCiphertext跳过导入后需人工填补
@@ -40,35 +40,35 @@ func NewTaskExportService(
// ExportedTask 导出格式:按名称引用存储/节点,不含敏感数据。
type ExportedTask struct {
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
CronExpr string `json:"cronExpr,omitempty"`
SourcePath string `json:"sourcePath,omitempty"`
SourcePaths []string `json:"sourcePaths,omitempty"`
ExcludePatterns []string `json:"excludePatterns,omitempty"`
DBHost string `json:"dbHost,omitempty"`
DBPort int `json:"dbPort,omitempty"`
DBUser string `json:"dbUser,omitempty"`
DBName string `json:"dbName,omitempty"`
DBPath string `json:"dbPath,omitempty"`
ExtraConfig map[string]any `json:"extraConfig,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
CronExpr string `json:"cronExpr,omitempty"`
SourcePath string `json:"sourcePath,omitempty"`
SourcePaths []string `json:"sourcePaths,omitempty"`
ExcludePatterns []string `json:"excludePatterns,omitempty"`
DBHost string `json:"dbHost,omitempty"`
DBPort int `json:"dbPort,omitempty"`
DBUser string `json:"dbUser,omitempty"`
DBName string `json:"dbName,omitempty"`
DBPath string `json:"dbPath,omitempty"`
ExtraConfig map[string]any `json:"extraConfig,omitempty"`
// 按名称引用:导入时按名称查找对应 ID
StorageTargetNames []string `json:"storageTargetNames"`
ReplicationTargetNames []string `json:"replicationTargetNames,omitempty"`
NodeName string `json:"nodeName,omitempty"`
DependsOnTaskNames []string `json:"dependsOnTaskNames,omitempty"`
Tags string `json:"tags,omitempty"`
Compression string `json:"compression,omitempty"`
Encrypt bool `json:"encrypt,omitempty"`
RetentionDays int `json:"retentionDays,omitempty"`
MaxBackups int `json:"maxBackups,omitempty"`
VerifyEnabled bool `json:"verifyEnabled,omitempty"`
VerifyCronExpr string `json:"verifyCronExpr,omitempty"`
VerifyMode string `json:"verifyMode,omitempty"`
SLAHoursRPO int `json:"slaHoursRpo,omitempty"`
AlertOnConsecutiveFails int `json:"alertOnConsecutiveFails,omitempty"`
MaintenanceWindows string `json:"maintenanceWindows,omitempty"`
StorageTargetNames []string `json:"storageTargetNames"`
ReplicationTargetNames []string `json:"replicationTargetNames,omitempty"`
NodeName string `json:"nodeName,omitempty"`
DependsOnTaskNames []string `json:"dependsOnTaskNames,omitempty"`
Tags string `json:"tags,omitempty"`
Compression string `json:"compression,omitempty"`
Encrypt bool `json:"encrypt,omitempty"`
RetentionDays int `json:"retentionDays,omitempty"`
MaxBackups int `json:"maxBackups,omitempty"`
VerifyEnabled bool `json:"verifyEnabled,omitempty"`
VerifyCronExpr string `json:"verifyCronExpr,omitempty"`
VerifyMode string `json:"verifyMode,omitempty"`
SLAHoursRPO int `json:"slaHoursRpo,omitempty"`
AlertOnConsecutiveFails int `json:"alertOnConsecutiveFails,omitempty"`
MaintenanceWindows string `json:"maintenanceWindows,omitempty"`
}
// ExportPayload 导出整体结构,带元信息。
@@ -233,67 +233,67 @@ func (s *TaskExportService) toExported(item *model.BackupTask, targetNames, node
nodeName = nodeNames[item.NodeID]
}
return ExportedTask{
Name: item.Name,
Type: item.Type,
Enabled: item.Enabled,
CronExpr: item.CronExpr,
SourcePath: item.SourcePath,
SourcePaths: sourcePaths,
ExcludePatterns: excludes,
DBHost: item.DBHost,
DBPort: item.DBPort,
DBUser: item.DBUser,
DBName: item.DBName,
DBPath: item.DBPath,
ExtraConfig: extra,
StorageTargetNames: storageNames,
ReplicationTargetNames: replicationNames,
NodeName: nodeName,
DependsOnTaskNames: dependsOnNames,
Tags: item.Tags,
Compression: item.Compression,
Encrypt: item.Encrypt,
RetentionDays: item.RetentionDays,
MaxBackups: item.MaxBackups,
VerifyEnabled: item.VerifyEnabled,
VerifyCronExpr: item.VerifyCronExpr,
VerifyMode: item.VerifyMode,
SLAHoursRPO: item.SLAHoursRPO,
Name: item.Name,
Type: item.Type,
Enabled: item.Enabled,
CronExpr: item.CronExpr,
SourcePath: item.SourcePath,
SourcePaths: sourcePaths,
ExcludePatterns: excludes,
DBHost: item.DBHost,
DBPort: item.DBPort,
DBUser: item.DBUser,
DBName: item.DBName,
DBPath: item.DBPath,
ExtraConfig: extra,
StorageTargetNames: storageNames,
ReplicationTargetNames: replicationNames,
NodeName: nodeName,
DependsOnTaskNames: dependsOnNames,
Tags: item.Tags,
Compression: item.Compression,
Encrypt: item.Encrypt,
RetentionDays: item.RetentionDays,
MaxBackups: item.MaxBackups,
VerifyEnabled: item.VerifyEnabled,
VerifyCronExpr: item.VerifyCronExpr,
VerifyMode: item.VerifyMode,
SLAHoursRPO: item.SLAHoursRPO,
AlertOnConsecutiveFails: item.AlertOnConsecutiveFails,
MaintenanceWindows: item.MaintenanceWindows,
MaintenanceWindows: item.MaintenanceWindows,
}
}
func (s *TaskExportService) toUpsertInput(t ExportedTask, targetsByName, nodesByName map[string]uint, deps []uint) BackupTaskUpsertInput {
return BackupTaskUpsertInput{
Name: t.Name,
Type: t.Type,
Enabled: t.Enabled,
CronExpr: t.CronExpr,
SourcePath: t.SourcePath,
SourcePaths: t.SourcePaths,
ExcludePatterns: t.ExcludePatterns,
DBHost: t.DBHost,
DBPort: t.DBPort,
DBUser: t.DBUser,
DBName: t.DBName,
DBPath: t.DBPath,
ExtraConfig: t.ExtraConfig,
StorageTargetIDs: idsFromNames(t.StorageTargetNames, targetsByName),
ReplicationTargetIDs: idsFromNames(t.ReplicationTargetNames, targetsByName),
NodeID: nodesByName[t.NodeName],
Tags: t.Tags,
Compression: t.Compression,
Encrypt: t.Encrypt,
RetentionDays: t.RetentionDays,
MaxBackups: t.MaxBackups,
VerifyEnabled: t.VerifyEnabled,
VerifyCronExpr: t.VerifyCronExpr,
VerifyMode: t.VerifyMode,
SLAHoursRPO: t.SLAHoursRPO,
Name: t.Name,
Type: t.Type,
Enabled: t.Enabled,
CronExpr: t.CronExpr,
SourcePath: t.SourcePath,
SourcePaths: t.SourcePaths,
ExcludePatterns: t.ExcludePatterns,
DBHost: t.DBHost,
DBPort: t.DBPort,
DBUser: t.DBUser,
DBName: t.DBName,
DBPath: t.DBPath,
ExtraConfig: t.ExtraConfig,
StorageTargetIDs: idsFromNames(t.StorageTargetNames, targetsByName),
ReplicationTargetIDs: idsFromNames(t.ReplicationTargetNames, targetsByName),
NodeID: nodesByName[t.NodeName],
Tags: t.Tags,
Compression: t.Compression,
Encrypt: t.Encrypt,
RetentionDays: t.RetentionDays,
MaxBackups: t.MaxBackups,
VerifyEnabled: t.VerifyEnabled,
VerifyCronExpr: t.VerifyCronExpr,
VerifyMode: t.VerifyMode,
SLAHoursRPO: t.SLAHoursRPO,
AlertOnConsecutiveFails: t.AlertOnConsecutiveFails,
MaintenanceWindows: t.MaintenanceWindows,
DependsOnTaskIDs: deps,
MaintenanceWindows: t.MaintenanceWindows,
DependsOnTaskIDs: deps,
}
}

View File

@@ -43,7 +43,7 @@ type LocalDiskFactory struct{}
func NewLocalDiskFactory() LocalDiskFactory { return LocalDiskFactory{} }
func (LocalDiskFactory) Type() storage.ProviderType { return storage.ProviderTypeLocalDisk }
func (LocalDiskFactory) SensitiveFields() []string { return nil }
func (LocalDiskFactory) SensitiveFields() []string { return nil }
func (LocalDiskFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
cfg, err := storage.DecodeConfig[storage.LocalDiskConfig](rawConfig)
@@ -66,7 +66,7 @@ type S3Factory struct{}
func NewS3Factory() S3Factory { return S3Factory{} }
func (S3Factory) Type() storage.ProviderType { return storage.ProviderTypeS3 }
func (S3Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
func (S3Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
func (S3Factory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
cfg, err := storage.DecodeConfig[storage.S3Config](rawConfig)
@@ -116,7 +116,7 @@ type WebDAVFactory struct{}
func NewWebDAVFactory() WebDAVFactory { return WebDAVFactory{} }
func (WebDAVFactory) Type() storage.ProviderType { return storage.ProviderTypeWebDAV }
func (WebDAVFactory) SensitiveFields() []string { return []string{"username", "password"} }
func (WebDAVFactory) SensitiveFields() []string { return []string{"username", "password"} }
func (WebDAVFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
cfg, err := storage.DecodeConfig[storage.WebDAVConfig](rawConfig)
@@ -187,7 +187,7 @@ type FTPFactory struct{}
func NewFTPFactory() FTPFactory { return FTPFactory{} }
func (FTPFactory) Type() storage.ProviderType { return storage.ProviderTypeFTP }
func (FTPFactory) SensitiveFields() []string { return []string{"username", "password"} }
func (FTPFactory) SensitiveFields() []string { return []string{"username", "password"} }
func (FTPFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
cfg, err := storage.DecodeConfig[storage.FTPConfig](rawConfig)
@@ -228,7 +228,7 @@ type AliyunOSSFactory struct{}
func NewAliyunOSSFactory() AliyunOSSFactory { return AliyunOSSFactory{} }
func (AliyunOSSFactory) Type() storage.ProviderType { return storage.ProviderTypeAliyunOSS }
func (AliyunOSSFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
func (AliyunOSSFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
// AliyunConfig 是阿里云 OSS 的用户配置。
type AliyunConfig struct {
@@ -269,7 +269,9 @@ type TencentCOSFactory struct{}
func NewTencentCOSFactory() TencentCOSFactory { return TencentCOSFactory{} }
func (TencentCOSFactory) Type() storage.ProviderType { return storage.ProviderTypeTencentCOS }
func (TencentCOSFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
func (TencentCOSFactory) SensitiveFields() []string {
return []string{"accessKeyId", "secretAccessKey"}
}
// TencentConfig 是腾讯云 COS 的用户配置。
type TencentConfig struct {
@@ -305,7 +307,7 @@ type QiniuKodoFactory struct{}
func NewQiniuKodoFactory() QiniuKodoFactory { return QiniuKodoFactory{} }
func (QiniuKodoFactory) Type() storage.ProviderType { return storage.ProviderTypeQiniuKodo }
func (QiniuKodoFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
func (QiniuKodoFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} }
// QiniuConfig 是七牛云 Kodo 的用户配置。
type QiniuConfig struct {
@@ -355,7 +357,9 @@ type RcloneFactory struct{}
func NewRcloneFactory() RcloneFactory { return RcloneFactory{} }
func (RcloneFactory) Type() storage.ProviderType { return storage.ProviderTypeRclone }
func (RcloneFactory) SensitiveFields() []string { return []string{"pass", "password", "secret_access_key", "client_secret", "token"} }
func (RcloneFactory) SensitiveFields() []string {
return []string{"pass", "password", "secret_access_key", "client_secret", "token"}
}
func (RcloneFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
backend, _ := rawConfig["backend"].(string)
@@ -462,8 +466,10 @@ func NewBackendFactory(backendType string) GenericBackendFactory {
return GenericBackendFactory{backendType: backendType, sensitive: sensitive}
}
func (f GenericBackendFactory) Type() storage.ProviderType { return storage.ProviderType(f.backendType) }
func (f GenericBackendFactory) SensitiveFields() []string { return f.sensitive }
func (f GenericBackendFactory) Type() storage.ProviderType {
return storage.ProviderType(f.backendType)
}
func (f GenericBackendFactory) SensitiveFields() []string { return f.sensitive }
func (f GenericBackendFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
root, _ := rawConfig["root"].(string)

19
web/package-lock.json generated
View File

@@ -23,7 +23,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.13.10",
"@types/node": "^24.13.3",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@vitejs/plugin-react": "^4.3.4",
@@ -31,6 +31,9 @@
"typescript": "^5.7.3",
"vite": "^6.4.3",
"vitest": "^3.2.7"
},
"engines": {
"node": ">=24.0"
}
},
"node_modules/@adobe/css-tools": {
@@ -1529,13 +1532,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~7.18.0"
}
},
"node_modules/@types/prop-types": {
@@ -3468,9 +3471,9 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},

View File

@@ -3,9 +3,13 @@
"private": true,
"version": "0.1.0",
"type": "module",
"engines": {
"node": ">=24.0"
},
"scripts": {
"dev": "vite",
"build": "tsc --noEmit -p tsconfig.json && vite build",
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json",
"build": "npm run typecheck && vite build",
"preview": "vite preview",
"test": "vitest run"
},
@@ -25,7 +29,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.13.10",
"@types/node": "^24.13.3",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@vitejs/plugin-react": "^4.3.4",

View File

@@ -29,6 +29,5 @@
"src/pages/login/page.tsx",
"src/pages/system-info/page.tsx",
"src/stores/auth.test.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
]
}

View File

@@ -1,11 +1,11 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"types": ["node"]
},
"include": ["vite.config.ts"]

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"root":["./src/rootapp.tsx","./src/main.tsx","./src/components/authbootstrap.tsx","./src/components/fullpageloading.tsx","./src/layouts/applayout.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/system-info/systeminfopage.tsx","./src/router/protectedroute.tsx","./src/router/index.tsx","./src/services/auth.ts","./src/services/http.ts","./src/services/system.ts","./src/stores/auth.ts","./src/test/setup.ts","./src/types/api.ts","./src/types/auth.ts","./src/types/system.ts"],"version":"5.9.3"}

View File

@@ -1,2 +0,0 @@
declare const _default: import("vite").UserConfig;
export default _default;

View File

@@ -1,32 +0,0 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8340',
changeOrigin: true,
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-arco': ['@arco-design/web-react'],
'vendor-echarts': ['echarts', 'echarts-for-react'],
},
},
},
},
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
globals: true,
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
exclude: ['src/components/auth-guard.test.tsx', 'src/stores/auth.test.ts'],
},
});