同步处理

This commit is contained in:
tianmo committed 2026-10-17 18:15:42 +08:00
1 parent 9939943e31
commit 6494ad2918
26 files changed
+970 -40

No files matched your search

+4 -1
View File
@@ -3,11 +3,14 @@ import { createApiError } from '../../../utils/api-error'
import { serializeBackupJob } from '../../../utils/backup/api'
import { getBackupExportEstimate } from '../../../utils/backup/stats'
import { startFullExportJob } from '../../../utils/backup/runner'
import { resolveRuntimeAppVersion } from '../../../utils/app-version'
export default defineEventHandler(async (event) => {
const user = await requireAdminAuth(event)
const config = useRuntimeConfig(event)
const appVersion = String(config.appVersion ?? '0.0.0')
const appVersion = resolveRuntimeAppVersion(
String(config.appVersion ?? '0.0.0')
)
const estimate = await getBackupExportEstimate()
@@ -0,0 +1,14 @@
import { requireAdminAuth } from '../../../../utils/access'
import { setAllFinishedBackupJobsHidden } from '../../../../utils/backup/jobs'
interface Body {
hidden?: boolean
}
export default defineEventHandler(async (event) => {
const user = await requireAdminAuth(event)
const body = await readBody<Body>(event).catch(() => null)
const hidden = body?.hidden !== false
const updated = setAllFinishedBackupJobsHidden(user.id, hidden)
return { success: true, hidden, updated }
})
+25
View File
@@ -0,0 +1,25 @@
import { requireAdminAuth } from '../../../../utils/access'
import { createApiError } from '../../../../utils/api-error'
import {
getBackupJob,
setBackupJobHidden
} from '../../../../utils/backup/jobs'
interface Body {
jobId?: string
hidden?: boolean
}
export default defineEventHandler(async (event) => {
const user = await requireAdminAuth(event)
const body = await readBody<Body>(event).catch(() => null)
const jobId = body?.jobId?.trim() ?? ''
if (!jobId || !getBackupJob(jobId, user.id)) {
createApiError(event, 'INVALID_REQUEST', '任务不存在', 404)
}
const hidden = body?.hidden !== false
if (!setBackupJobHidden(jobId, user.id, hidden)) {
createApiError(event, 'INVALID_REQUEST', '更新任务显示状态失败', 400)
}
return { success: true, hidden }
})
+10 -2
View File
@@ -14,14 +14,21 @@ export default defineEventHandler(async (event) => {
recoverInterruptedBackupJobs()
}
const query = getQuery(event)
const includeHidden = query.includeHidden === 'true'
|| query.includeHidden === '1'
const pageSize = Math.min(50, Math.max(1, Number(query.pageSize) || 10))
const pageNum = Math.max(1, Number(query.pageNum) || Number(query.page) || 1)
const { jobs, total } = listBackupJobsPage(pageNum, pageSize, user.id)
const { jobs, total } = listBackupJobsPage(
pageNum,
pageSize,
user.id,
includeHidden
)
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const page = total > 0 ? Math.min(pageNum, totalPages) : 1
const slice = page === pageNum
? jobs
: listBackupJobsPage(page, pageSize, user.id).jobs
: listBackupJobsPage(page, pageSize, user.id, includeHidden).jobs
const activeJob = getActiveBackupJob(user.id)
@@ -31,6 +38,7 @@ export default defineEventHandler(async (event) => {
page,
pageSize,
totalPages,
includeHidden,
activeJob: activeJob ? serializeBackupJob(activeJob) : null
}
})
+3 -1
View File
@@ -1,7 +1,9 @@
import { resolveRuntimeAppVersion } from '../utils/app-version'
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event)
return {
status: 'ok',
version: config.appVersion
version: resolveRuntimeAppVersion(String(config.appVersion ?? ''))
}
})
@@ -0,0 +1,29 @@
import { requireSiteSyncAuth } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { validateStorageKey } from '../../utils/image-key'
import { finalizeSiteSyncChunk } from '../../utils/site-sync/chunks'
interface Body {
jobId?: string
key?: string
}
export default defineEventHandler(async (event) => {
await requireSiteSyncAuth(event)
const body = await readBody<Body>(event).catch(() => null)
const jobId = body?.jobId?.trim() ?? ''
const key = body?.key?.trim() ?? ''
if (!jobId || !validateStorageKey(key)) {
createApiError(event, 'INVALID_REQUEST', '分块完成参数无效', 400)
}
try {
return await finalizeSiteSyncChunk({ jobId, key })
} catch (error) {
createApiError(
event,
'INVALID_REQUEST',
error instanceof Error ? error.message : '分块组装失败',
400
)
}
})
+16
View File
@@ -0,0 +1,16 @@
import { getQuery } from 'h3'
import { requireSiteSyncAuth } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { validateStorageKey } from '../../utils/image-key'
import { getSiteSyncChunkStatus } from '../../utils/site-sync/chunks'
export default defineEventHandler(async (event) => {
await requireSiteSyncAuth(event)
const query = getQuery(event)
const jobId = typeof query.jobId === 'string' ? query.jobId.trim() : ''
const key = typeof query.key === 'string' ? query.key.trim() : ''
if (!jobId || !validateStorageKey(key)) {
createApiError(event, 'INVALID_REQUEST', '分块查询参数无效', 400)
}
return getSiteSyncChunkStatus({ jobId, key })
})
+54
View File
@@ -0,0 +1,54 @@
import { getHeader, readRawBody } from 'h3'
import { requireSiteSyncAuth } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { validateStorageKey } from '../../utils/image-key'
import {
isSiteSyncChunkMetadata,
SITE_SYNC_CHUNK_SIZE,
writeSiteSyncChunk
} from '../../utils/site-sync/chunks'
export default defineEventHandler(async (event) => {
await requireSiteSyncAuth(event)
const encoded = getHeader(event, 'x-pichost-chunk-metadata') ?? ''
let metadata: unknown
try {
metadata = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'))
} catch {
createApiError(event, 'INVALID_REQUEST', '分块元数据无效', 400)
}
if (
!isSiteSyncChunkMetadata(metadata)
|| !validateStorageKey(metadata.key)
|| !validateStorageKey(metadata.mediaKey)
) {
createApiError(event, 'INVALID_REQUEST', '分块元数据无效', 400)
}
const index = Number.parseInt(getHeader(event, 'x-pichost-chunk-index') ?? '', 10)
if (!Number.isInteger(index)) {
createApiError(event, 'INVALID_REQUEST', '分块序号无效', 400)
}
const body = await readRawBody(event, false)
if (!body?.length) {
createApiError(event, 'INVALID_REQUEST', '未收到分块数据', 400)
}
if (body.length > SITE_SYNC_CHUNK_SIZE) {
createApiError(event, 'FILE_TOO_LARGE', '分块超过 4 MB', 413)
}
try {
return await writeSiteSyncChunk({
metadata,
index,
bytes: new Uint8Array(body)
})
} catch (error) {
createApiError(
event,
'INVALID_REQUEST',
error instanceof Error ? error.message : '分块写入失败',
400
)
}
})
+3
View File
@@ -1,6 +1,7 @@
import { requireSiteSyncAuth } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { cleanupSiteSyncJobArtifacts } from '../../utils/site-sync/artifacts'
import { cleanupSiteSyncChunkSessions } from '../../utils/site-sync/chunks'
interface Body {
jobId?: string
@@ -13,8 +14,10 @@ export default defineEventHandler(async (event) => {
if (!jobId || jobId.length > 128) {
createApiError(event, 'INVALID_REQUEST', '缺少同步任务 ID', 400)
}
const removedChunkSessions = await cleanupSiteSyncChunkSessions(jobId)
return {
success: true,
removedChunkSessions,
...await cleanupSiteSyncJobArtifacts(jobId)
}
})
+2 -1
View File
@@ -7,6 +7,7 @@ import {
getVideoBaseUrl,
isHideFolderInUrl
} from '../../utils/env'
import { resolveRuntimeAppVersion } from '../../utils/app-version'
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
@@ -24,7 +25,7 @@ export default defineEventHandler(async (event) => {
videoBaseUrl: getVideoBaseUrl(event),
fileBaseUrl: getFileBaseUrl(event),
hideFolderInUrl: isHideFolderInUrl(event),
appVersion: config.appVersion as string
appVersion: resolveRuntimeAppVersion(String(config.appVersion ?? ''))
}
}
})
+5 -2
View File
@@ -1,14 +1,17 @@
import { requireAdminAuth } from '../../utils/access'
import {
fetchLatestGitHubRelease,
isVersionNewer
isVersionNewer,
resolveRuntimeAppVersion
} from '../../utils/app-version'
export default defineEventHandler(async (event) => {
await requireAdminAuth(event)
const config = useRuntimeConfig(event)
const currentVersion = String(config.appVersion ?? '')
const currentVersion = resolveRuntimeAppVersion(
String(config.appVersion ?? '')
)
const query = getQuery(event)
const force = query.refresh === '1' || query.refresh === 'true'
const latest = await fetchLatestGitHubRelease(force)