备份与恢复
This commit is contained in:
1 parent
5d5fecde1b
commit
b4535d6aea
6 files changed
+252
-27
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user