备份与恢复

This commit is contained in:
Yeomai committed 2026-10-18 20:28:46 +08:00
1 parent 5d5fecde1b
commit b4535d6aea
6 files changed
+252 -27

No files matched your search

+19 -2
View File
@@ -54,6 +54,19 @@ function formatTime(value: string) {
return new Date(value).toLocaleString(locale.value, { hour12: false })
}
function getErrorMessage(error: unknown) {
const value = error as {
data?: { data?: { message?: string }, message?: string }
statusMessage?: string
message?: string
}
return value?.data?.data?.message
?? value?.data?.message
?? value?.statusMessage
?? value?.message
?? t('storage.backup.previewFailed')
}
function pickFile() {
fileInput.value?.click()
}
@@ -90,8 +103,12 @@ async function submit() {
archiveName: selectedPackage.value || undefined
})
confirmOpen.value = true
} catch {
toast.add({ title: t('storage.backup.previewFailed'), color: 'error' })
} catch (error: unknown) {
toast.add({
title: t('storage.backup.previewFailed'),
description: getErrorMessage(error),
color: 'error'
})
} finally {
previewLoading.value = false
}
+42 -2
View File
@@ -30,6 +30,48 @@ const conflictMode = ref<'skip' | 'overwrite'>('skip')
const direction = ref<SiteSyncDirection>('push')
const scope = ref<'all' | 'image' | 'video' | 'file'>('all')
/** 目标站点地址与 Token 自动保存 24 小时 */
const FORM_STORAGE_KEY = 'pichost.site-sync-form'
const FORM_TTL_MS = 24 * 60 * 60 * 1000
function loadSavedForm() {
if (!import.meta.client) return
try {
const raw = localStorage.getItem(FORM_STORAGE_KEY)
if (!raw) return
const data = JSON.parse(raw) as {
targetBaseUrl?: string
token?: string
savedAt?: number
}
if (!data.savedAt || Date.now() - data.savedAt > FORM_TTL_MS) {
localStorage.removeItem(FORM_STORAGE_KEY)
return
}
if (data.targetBaseUrl) targetBaseUrl.value = data.targetBaseUrl
if (data.token) token.value = data.token
} catch {
localStorage.removeItem(FORM_STORAGE_KEY)
}
}
function persistForm() {
if (!import.meta.client) return
try {
localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify({
targetBaseUrl: targetBaseUrl.value,
token: token.value,
savedAt: Date.now()
}))
} catch {
// 存储不可用时忽略
}
}
watch([targetBaseUrl, token], persistForm)
onMounted(loadSavedForm)
const scopeItems = computed(() => [
{ value: 'all' as const, label: t('storage.backup.siteSyncScopeAll') },
{ value: 'image' as const, label: t('storage.backup.siteSyncScopeImage') },
@@ -110,7 +152,6 @@ function submit(resume = false) {
scope: scope.value,
...(resume && props.resumableJob ? { resumeJobId: props.resumableJob.id } : {})
})
token.value = ''
}
function startNew() {
@@ -131,7 +172,6 @@ function abandonSync() {
direction: direction.value,
abandonJobId: props.resumableJob.id
})
token.value = ''
}
function setConflictMode(mode: 'skip' | 'overwrite') {
+19 -8
View File
@@ -204,12 +204,17 @@ export function useBackup() {
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 }
{
method: 'POST',
credentials: 'include',
body: input.file,
headers: {
'content-type': 'application/octet-stream',
'x-pichost-dry-run': 'true'
}
}
)
return data.preview
}
@@ -235,12 +240,18 @@ export function useBackup() {
}) {
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 }
{
method: 'POST',
credentials: 'include',
body: input.file,
headers: {
'content-type': 'application/octet-stream',
'x-pichost-dry-run': 'false',
'x-pichost-conflict-mode': input.conflictMode
}
}
)
await refreshJobsFirstPage()
startPolling()