320 lines
8.1 KiB
TypeScript
320 lines
8.1 KiB
TypeScript
import type {
|
|
BackupEstimate,
|
|
BackupJobItem,
|
|
BackupPackageItem,
|
|
BackupRestorePreview,
|
|
BackendSyncPreview
|
|
} from '~/types/backup'
|
|
|
|
const jobs = ref<BackupJobItem[]>([])
|
|
const jobsPage = ref(1)
|
|
const jobsPageSize = ref(10)
|
|
const jobsTotal = ref(0)
|
|
const jobsTotalPages = ref(1)
|
|
const jobsLoading = ref(false)
|
|
const activeJobState = ref<BackupJobItem | null>(null)
|
|
const estimate = ref<BackupEstimate | null>(null)
|
|
const packages = ref<BackupPackageItem[]>([])
|
|
const loadingEstimate = ref(false)
|
|
const polling = ref(false)
|
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
export function useBackup() {
|
|
const toast = useToast()
|
|
const { t } = useI18n()
|
|
|
|
function stopPolling() {
|
|
if (pollTimer) {
|
|
clearInterval(pollTimer)
|
|
pollTimer = null
|
|
}
|
|
polling.value = false
|
|
}
|
|
|
|
function hasActiveJob(): boolean {
|
|
return activeJobState.value !== null
|
|
}
|
|
|
|
function getActiveJob(): BackupJobItem | null {
|
|
return activeJobState.value
|
|
}
|
|
|
|
async function cancelJob(jobId: string) {
|
|
try {
|
|
await $fetch(`/api/admin/backup/jobs/${jobId}/cancel`, {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
})
|
|
await loadJobs()
|
|
toast.add({ title: t('storage.backup.cancelDone'), color: 'success' })
|
|
} catch (error: unknown) {
|
|
handleError(error, t('storage.backup.cancelFailed'))
|
|
}
|
|
}
|
|
|
|
async function loadJobs(options?: { page?: number, pageSize?: number }) {
|
|
if (options?.page !== undefined) jobsPage.value = options.page
|
|
if (options?.pageSize !== undefined) jobsPageSize.value = options.pageSize
|
|
jobsLoading.value = true
|
|
try {
|
|
const data = await $fetch<{
|
|
jobs: BackupJobItem[]
|
|
total: number
|
|
page: number
|
|
pageSize: number
|
|
totalPages: number
|
|
activeJob: BackupJobItem | null
|
|
}>('/api/admin/backup/jobs', {
|
|
credentials: 'include',
|
|
query: {
|
|
pageNum: jobsPage.value,
|
|
pageSize: jobsPageSize.value
|
|
}
|
|
})
|
|
jobs.value = data.jobs
|
|
jobsTotal.value = data.total
|
|
jobsPage.value = data.page
|
|
jobsPageSize.value = data.pageSize
|
|
jobsTotalPages.value = data.totalPages
|
|
activeJobState.value = data.activeJob
|
|
if (!data.activeJob) {
|
|
stopPolling()
|
|
void loadEstimate().catch(() => {})
|
|
}
|
|
return data.jobs
|
|
} finally {
|
|
jobsLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function setJobsPage(page: number) {
|
|
jobsPage.value = page
|
|
return loadJobs()
|
|
}
|
|
|
|
async function setJobsPageSize(pageSize: number) {
|
|
jobsPageSize.value = pageSize
|
|
jobsPage.value = 1
|
|
return loadJobs()
|
|
}
|
|
|
|
async function refreshJobsFirstPage() {
|
|
jobsPage.value = 1
|
|
return loadJobs()
|
|
}
|
|
|
|
async function loadEstimate() {
|
|
loadingEstimate.value = true
|
|
try {
|
|
const data = await $fetch<{
|
|
estimate: BackupEstimate
|
|
packages: BackupPackageItem[]
|
|
backupsDir: string
|
|
}>('/api/admin/backup/estimate', { credentials: 'include' })
|
|
estimate.value = data.estimate
|
|
packages.value = data.packages
|
|
return data
|
|
} finally {
|
|
loadingEstimate.value = false
|
|
}
|
|
}
|
|
|
|
function startPolling() {
|
|
if (pollTimer) return
|
|
polling.value = true
|
|
pollTimer = setInterval(() => {
|
|
void loadJobs().catch(() => {})
|
|
}, 2000)
|
|
}
|
|
|
|
async function createExport() {
|
|
try {
|
|
const data = await $fetch<{ job: BackupJobItem, estimate: BackupEstimate }>(
|
|
'/api/admin/backup/export',
|
|
{ method: 'POST', credentials: 'include' }
|
|
)
|
|
estimate.value = data.estimate
|
|
await refreshJobsFirstPage()
|
|
startPolling()
|
|
toast.add({ title: t('storage.backup.exportStarted'), color: 'success' })
|
|
return data.job
|
|
} catch (error: unknown) {
|
|
handleError(error, t('storage.backup.exportFailed'))
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function previewRestore(input: {
|
|
file?: File
|
|
archiveName?: string
|
|
}): Promise<BackupRestorePreview> {
|
|
if (input.file) {
|
|
const form = new FormData()
|
|
form.append('file', input.file)
|
|
form.append('dryRun', 'true')
|
|
const data = await $fetch<{ preview: BackupRestorePreview }>(
|
|
'/api/admin/backup/restore',
|
|
{ method: 'POST', credentials: 'include', body: form }
|
|
)
|
|
return data.preview
|
|
}
|
|
|
|
const data = await $fetch<{ preview: BackupRestorePreview }>(
|
|
'/api/admin/backup/restore',
|
|
{
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: {
|
|
archiveName: input.archiveName,
|
|
dryRun: true
|
|
}
|
|
}
|
|
)
|
|
return data.preview
|
|
}
|
|
|
|
async function confirmRestore(input: {
|
|
file?: File
|
|
archiveName?: string
|
|
conflictMode: 'skip' | 'overwrite'
|
|
}) {
|
|
try {
|
|
if (input.file) {
|
|
const form = new FormData()
|
|
form.append('file', input.file)
|
|
form.append('conflictMode', input.conflictMode)
|
|
const data = await $fetch<{ job: BackupJobItem }>(
|
|
'/api/admin/backup/restore',
|
|
{ method: 'POST', credentials: 'include', body: form }
|
|
)
|
|
await refreshJobsFirstPage()
|
|
startPolling()
|
|
toast.add({ title: t('storage.backup.restoreStarted'), color: 'success' })
|
|
return data.job
|
|
}
|
|
|
|
const data = await $fetch<{ job: BackupJobItem }>(
|
|
'/api/admin/backup/restore',
|
|
{
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: {
|
|
archiveName: input.archiveName,
|
|
conflictMode: input.conflictMode,
|
|
dryRun: false
|
|
}
|
|
}
|
|
)
|
|
await refreshJobsFirstPage()
|
|
startPolling()
|
|
toast.add({ title: t('storage.backup.restoreStarted'), color: 'success' })
|
|
return data.job
|
|
} catch (error: unknown) {
|
|
handleError(error, t('storage.backup.restoreFailed'))
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function previewSync(body: {
|
|
fromBackendId: string
|
|
toBackendId: string
|
|
}): Promise<BackendSyncPreview> {
|
|
const data = await $fetch<{ preview: BackendSyncPreview }>(
|
|
'/api/admin/storage/sync',
|
|
{
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: { ...body, dryRun: true }
|
|
}
|
|
)
|
|
return data.preview
|
|
}
|
|
|
|
async function startSync(body: {
|
|
fromBackendId: string
|
|
toBackendId: string
|
|
deleteSource?: boolean
|
|
setDefault?: boolean
|
|
}) {
|
|
try {
|
|
const data = await $fetch<{ job: BackupJobItem }>(
|
|
'/api/admin/storage/sync',
|
|
{
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body
|
|
}
|
|
)
|
|
await refreshJobsFirstPage()
|
|
startPolling()
|
|
toast.add({ title: t('storage.backup.syncStarted'), color: 'success' })
|
|
return data.job
|
|
} catch (error: unknown) {
|
|
handleError(error, t('storage.backup.syncFailed'))
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function retryJob(jobId: string) {
|
|
try {
|
|
await $fetch(`/api/admin/backup/jobs/${jobId}/retry`, {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
})
|
|
await refreshJobsFirstPage()
|
|
startPolling()
|
|
toast.add({ title: t('storage.backup.retryStarted'), color: 'success' })
|
|
} catch (error: unknown) {
|
|
handleError(error, t('storage.backup.retryFailed'))
|
|
}
|
|
}
|
|
|
|
function downloadUrl(jobId: string): string {
|
|
return `/api/admin/backup/jobs/${jobId}/download`
|
|
}
|
|
|
|
function handleError(error: unknown, fallback: string) {
|
|
const status = typeof error === 'object' && error !== null && 'statusCode' in error
|
|
? (error as { statusCode: number }).statusCode
|
|
: 0
|
|
if (status === 409) {
|
|
toast.add({ title: t('storage.backup.jobRunning'), color: 'warning' })
|
|
return
|
|
}
|
|
toast.add({ title: fallback, color: 'error' })
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
stopPolling()
|
|
})
|
|
|
|
return {
|
|
jobs,
|
|
jobsPage,
|
|
jobsPageSize,
|
|
jobsTotal,
|
|
jobsTotalPages,
|
|
jobsLoading,
|
|
estimate,
|
|
packages,
|
|
loadingEstimate,
|
|
polling,
|
|
hasActiveJob,
|
|
getActiveJob,
|
|
loadJobs,
|
|
setJobsPage,
|
|
setJobsPageSize,
|
|
loadEstimate,
|
|
startPolling,
|
|
stopPolling,
|
|
createExport,
|
|
previewRestore,
|
|
confirmRestore,
|
|
previewSync,
|
|
startSync,
|
|
retryJob,
|
|
cancelJob,
|
|
downloadUrl
|
|
}
|
|
}
|