feat: 数据备份管理系统 v0.1.0

带 Web 操作界面的备份系统,支持:
- 数据源:MySQL 数据库(mysqldump)、服务器目录(tar.gz)
- 存储目标:本地目录、S3 兼容对象存储(MinIO/Ceph/AWS S3)
- 触发方式:手动 + Cron 定时调度(APScheduler)
- 备份还原:MySQL 库、目录
- JWT 登录认证 + Fernet 字段加密
- 保留策略:按数量 + 按天数双重清理

技术栈:FastAPI + SQLAlchemy 2 + APScheduler + aioboto3;
前端 Vite + React 18 + TypeScript + Ant Design 5 + Zustand。
Docker Compose 一键起。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
publ
2026-06-22 18:27:41 +08:00
commit 4e3a4b4602
92 changed files with 5290 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
import { api } from './client'
import type { MessageResponse, TokenResponse, User } from '../types'
export interface LoginPayload {
username: string
password: string
}
export const authApi = {
login: (payload: LoginPayload) =>
api.post<TokenResponse>('/auth/login', payload).then((r) => r.data),
me: () => api.get<User>('/auth/me').then((r) => r.data),
changePassword: (old_password: string, new_password: string) =>
api
.post<MessageResponse>('/auth/change-password', { old_password, new_password })
.then((r) => r.data),
}
+45
View File
@@ -0,0 +1,45 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { message } from 'antd'
import { useAuthStore } from '../stores/authStore'
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
export const api = axios.create({
baseURL,
timeout: 60_000,
})
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
api.interceptors.response.use(
(resp) => resp,
(error: AxiosError<{ detail?: string | { msg: string }[] }>) => {
const status = error.response?.status
const detail = error.response?.data?.detail
let msg: string
if (Array.isArray(detail)) {
msg = detail.map((d) => d.msg).join('; ')
} else if (typeof detail === 'string') {
msg = detail
} else {
msg = error.message
}
if (status === 401) {
useAuthStore.getState().logout()
if (window.location.pathname !== '/login') {
window.location.href = '/login'
}
} else if (status && status >= 400) {
message.error(msg)
}
return Promise.reject(error)
},
)
+21
View File
@@ -0,0 +1,21 @@
import { api } from './client'
import type {
JobCreate,
JobOut,
JobRunResponse,
JobUpdate,
JobWithLastRun,
} from '../types'
export const jobApi = {
list: () => api.get<JobWithLastRun[]>('/jobs').then((r) => r.data),
get: (id: number) => api.get<JobWithLastRun>(`/jobs/${id}`).then((r) => r.data),
create: (payload: JobCreate) => api.post<JobOut>('/jobs', payload).then((r) => r.data),
update: (id: number, payload: JobUpdate) =>
api.put<JobOut>(`/jobs/${id}`, payload).then((r) => r.data),
remove: (id: number) => api.delete(`/jobs/${id}`).then((r) => r.data),
run: (id: number) =>
api.post<JobRunResponse>(`/jobs/${id}/run`).then((r) => r.data),
toggle: (id: number) =>
api.post<JobOut>(`/jobs/${id}/toggle`).then((r) => r.data),
}
+17
View File
@@ -0,0 +1,17 @@
import { api } from './client'
import type {
RestoreOut,
RestoreRequest,
RestoreTargetsResponse,
} from '../types'
export const restoreApi = {
getTargets: (run_id: number) =>
api
.get<RestoreTargetsResponse>('/restore/targets', { params: { run_id } })
.then((r) => r.data),
create: (payload: RestoreRequest) =>
api.post<RestoreOut>('/restore', payload).then((r) => r.data),
history: (run_id?: number) =>
api.get<RestoreOut[]>('/restore/history', { params: { run_id } }).then((r) => r.data),
}
+11
View File
@@ -0,0 +1,11 @@
import { api } from './client'
import type { RunListResponse, RunLogOut, RunOut } from '../types'
export const runApi = {
list: (params?: { job_id?: number; status?: string; page?: number; page_size?: number }) =>
api.get<RunListResponse>('/runs', { params }).then((r) => r.data),
get: (id: number) => api.get<RunOut>(`/runs/${id}`).then((r) => r.data),
getLog: (id: number) => api.get<RunLogOut>(`/runs/${id}/log`).then((r) => r.data),
downloadUrl: (id: number) => `/api/runs/${id}/download`,
remove: (id: number) => api.delete(`/runs/${id}`).then((r) => r.data),
}
+19
View File
@@ -0,0 +1,19 @@
import { api } from './client'
import type {
StorageCreate,
StorageOut,
StorageTestResponse,
StorageUpdate,
} from '../types'
export const storageApi = {
list: () => api.get<StorageOut[]>('/storages').then((r) => r.data),
get: (id: number) => api.get<StorageOut>(`/storages/${id}`).then((r) => r.data),
create: (payload: StorageCreate) =>
api.post<StorageOut>('/storages', payload).then((r) => r.data),
update: (id: number, payload: StorageUpdate) =>
api.put<StorageOut>(`/storages/${id}`, payload).then((r) => r.data),
remove: (id: number) => api.delete(`/storages/${id}`).then((r) => r.data),
test: (id: number) =>
api.post<StorageTestResponse>(`/storages/${id}/test`).then((r) => r.data),
}