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:
@@ -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),
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
Reference in New Issue
Block a user