同步修改

This commit is contained in:
tianmo committed 2026-10-17 17:52:08 +08:00
1 parent d647453085
commit 9939943e31
6 files changed
+60 -27

No files matched your search

+6 -2
View File
@@ -60,8 +60,12 @@ const isPull = computed(() => direction.value === 'pull')
const canAbandon = computed(() => const canAbandon = computed(() =>
!props.busy !props.busy
&& Boolean(props.resumableJob) && Boolean(props.resumableJob)
&& (isPull.value || Boolean(token.value.trim()))
) )
const abandonConfirm = computed(() => (
isPull.value || token.value.trim()
? t('storage.backup.siteSyncAbandonConfirm')
: t('storage.backup.siteSyncAbandonWithoutTokenConfirm')
))
const directionSubtitle = computed(() => ( const directionSubtitle = computed(() => (
isPull.value isPull.value
? t('storage.backup.siteSyncPullSubtitle') ? t('storage.backup.siteSyncPullSubtitle')
@@ -110,7 +114,7 @@ function resumeSync() {
function abandonSync() { function abandonSync() {
if (!canAbandon.value || !props.resumableJob) return if (!canAbandon.value || !props.resumableJob) return
if (!window.confirm(t('storage.backup.siteSyncAbandonConfirm'))) return if (!window.confirm(abandonConfirm.value)) return
emit('sync', { emit('sync', {
targetBaseUrl: targetBaseUrl.value.trim(), targetBaseUrl: targetBaseUrl.value.trim(),
token: token.value.trim(), token: token.value.trim(),
+10 -4
View File
@@ -268,11 +268,17 @@ export function useBackup() {
) )
await refreshJobsFirstPage() await refreshJobsFirstPage()
startPolling() startPolling()
const cleanupPending = Boolean(
body.abandonJobId
&& data.job.payload.cleanupPending === true
)
toast.add({ toast.add({
title: body.abandonJobId title: cleanupPending
? t('storage.backup.siteSyncAbandoned') ? t('storage.backup.siteSyncAbandonedPendingCleanup')
: t('storage.backup.siteSyncStarted'), : body.abandonJobId
color: 'success' ? t('storage.backup.siteSyncAbandoned')
: t('storage.backup.siteSyncStarted'),
color: cleanupPending ? 'warning' : 'success'
}) })
return data.job return data.job
} catch (error: unknown) { } catch (error: unknown) {
+2
View File
@@ -695,7 +695,9 @@
"siteSyncResume": "Resume last sync", "siteSyncResume": "Resume last sync",
"siteSyncAbandon": "Abandon and clean up", "siteSyncAbandon": "Abandon and clean up",
"siteSyncAbandonConfirm": "Abandon this sync and delete partial objects created by it that are not yet referenced by a media record?", "siteSyncAbandonConfirm": "Abandon this sync and delete partial objects created by it that are not yet referenced by a media record?",
"siteSyncAbandonWithoutTokenConfirm": "No target token is provided. Abandon the task now? Partial objects will be marked for later automatic cleanup on the target.",
"siteSyncAbandoned": "Sync abandoned and partial objects cleaned up", "siteSyncAbandoned": "Sync abandoned and partial objects cleaned up",
"siteSyncAbandonedPendingCleanup": "Sync abandoned; partial objects still need cleanup",
"siteSyncScanProgress": "Media scan progress", "siteSyncScanProgress": "Media scan progress",
"siteSyncFileProgress": "Synced file count", "siteSyncFileProgress": "Synced file count",
"siteSyncStarted": "Site-to-site sync started", "siteSyncStarted": "Site-to-site sync started",
+2
View File
@@ -695,7 +695,9 @@
"siteSyncResume": "继续上次同步", "siteSyncResume": "继续上次同步",
"siteSyncAbandon": "放弃并清理残留", "siteSyncAbandon": "放弃并清理残留",
"siteSyncAbandonConfirm": "确定放弃这次同步吗?将删除本次同步新建但尚未归入媒体记录的残留对象。", "siteSyncAbandonConfirm": "确定放弃这次同步吗?将删除本次同步新建但尚未归入媒体记录的残留对象。",
"siteSyncAbandonWithoutTokenConfirm": "未填写目标站 Token。确定只放弃任务吗?目标站残留将标记为待清理,之后可由目标站自动清理。",
"siteSyncAbandoned": "同步已放弃,残留对象已清理", "siteSyncAbandoned": "同步已放弃,残留对象已清理",
"siteSyncAbandonedPendingCleanup": "同步已放弃,残留待清理",
"siteSyncScanProgress": "媒体扫描进度", "siteSyncScanProgress": "媒体扫描进度",
"siteSyncFileProgress": "已同步文件数量", "siteSyncFileProgress": "已同步文件数量",
"siteSyncStarted": "两站点同步任务已开始", "siteSyncStarted": "两站点同步任务已开始",
+31 -17
View File
@@ -25,29 +25,41 @@ export async function abandonSiteSyncJob(
const targetBaseUrl = typeof job.payload.targetBaseUrl === 'string' const targetBaseUrl = typeof job.payload.targetBaseUrl === 'string'
? job.payload.targetBaseUrl.replace(/\/+$/, '') ? job.payload.targetBaseUrl.replace(/\/+$/, '')
: '' : ''
let cleanup: CleanupResult let cleanup: CleanupResult | null = null
let cleanupError: string | null = null
if (direction === 'push') { if (direction === 'push') {
if (!targetBaseUrl || !token) { if (targetBaseUrl && token) {
throw new Error('放弃推送任务并清理目标站残留时,需要目标站 Token') try {
} cleanup = await fetchSiteSyncJson<CleanupResult>(
cleanup = await fetchSiteSyncJson<CleanupResult>( `${targetBaseUrl}/api/site-sync/cleanup`,
`${targetBaseUrl}/api/site-sync/cleanup`, token,
token, {
{ method: 'POST',
method: 'POST', headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jobId })
body: JSON.stringify({ jobId }) }
)
} catch (error) {
cleanupError = error instanceof Error ? error.message : '目标站残留清理失败'
} }
) }
} else { } else {
cleanup = await cleanupSiteSyncJobArtifacts(jobId) try {
cleanup = await cleanupSiteSyncJobArtifacts(jobId)
} catch (error) {
cleanupError = error instanceof Error ? error.message : '本地残留清理失败'
}
} }
if (cleanup.failed > 0) { const cleanupPending = Boolean(
throw new Error(`同步残留清理失败:${cleanup.failed} 个对象`) cleanupError
} || !cleanup
|| cleanup.failed > 0
)
const message = `已放弃同步 · 清理 ${cleanup.deleted} 个残留对象` const message = cleanupPending
? `已放弃同步 · 残留待清理${cleanupError ? `:${cleanupError}` : ''}`
: `已放弃同步 · 清理 ${cleanup!.deleted} 个残留对象`
updateBackupJob(jobId, { updateBackupJob(jobId, {
status: 'failed', status: 'failed',
message, message,
@@ -58,6 +70,8 @@ export async function abandonSiteSyncJob(
resumable: false, resumable: false,
abandoned: true, abandoned: true,
cancelled: false, cancelled: false,
cleanupPending,
cleanupError,
failedKeys: [], failedKeys: [],
cleanup cleanup
} }
+9 -4
View File
@@ -1,6 +1,8 @@
import { basename } from 'node:path' import { basename } from 'node:path'
import { File as BufferFile } from 'node:buffer'
import { import {
Agent, Agent,
FormData as UndiciFormData,
fetch as undiciFetch fetch as undiciFetch
} from 'undici' } from 'undici'
import type { Readable } from 'node:stream' import type { Readable } from 'node:stream'
@@ -399,7 +401,7 @@ async function uploadObject(
getJobAbortSignal(jobId) getJobAbortSignal(jobId)
) )
throwIfJobCancellationRequested(jobId) throwIfJobCancellationRequested(jobId)
const form = new FormData() const form = new UndiciFormData()
const isPrimary = key === row.key const isPrimary = key === row.key
form.append('jobId', jobId) form.append('jobId', jobId)
form.append('mediaKey', row.key) form.append('mediaKey', row.key)
@@ -412,8 +414,11 @@ async function uploadObject(
form.append('contentType', contentType) form.append('contentType', contentType)
form.append( form.append(
'file', 'file',
new Blob([new Uint8Array(bytes)], { type: contentType }), new BufferFile(
basename(key) [new Uint8Array(bytes)],
basename(key),
{ type: contentType }
)
) )
const response = await undiciFetch( const response = await undiciFetch(
@@ -424,7 +429,7 @@ async function uploadObject(
headers: { 'Auth-Token': token }, headers: { 'Auth-Token': token },
signal: getJobAbortSignal(jobId), signal: getJobAbortSignal(jobId),
dispatcher: agents.upload dispatcher: agents.upload
} as Parameters<typeof undiciFetch>[1] }
) )
if (!response.ok) throw await responseError(response) if (!response.ok) throw await responseError(response)
await response.json().catch(() => null) await response.json().catch(() => null)