feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { prepareBackup, executeUploads, acquireBackupLock, releaseBackupLock } from '~~/server/services/autoBackupService'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
acquireBackupLock()
|
||||
try {
|
||||
const prepared = await prepareBackup()
|
||||
// 必须等待上传完成再返回响应:
|
||||
// Vercel Serverless 在响应返回后会冻结/回收函数实例,
|
||||
// 若以 fire-and-forget 后台执行,上传会被中途掐断(socket closed)。
|
||||
const result = await executeUploads(prepared)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.success ? '备份完成' : '备份部分或全部失败',
|
||||
backupId: prepared.historyId,
|
||||
results: result.results
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
throw createApiError(500, SERVER_ERROR_CODES.BACKUP_FAILED, error.message || '备份执行失败')
|
||||
} finally {
|
||||
releaseBackupLock()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { defineEventHandler, getRouterParam } from 'h3'
|
||||
import { eq, desc } from 'drizzle-orm'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { backupHistory } from '~/drizzle/schema'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
|
||||
/** 60 秒超时阈值 */
|
||||
const TIMEOUT_THRESHOLD_MS = 60_000
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = getRouterParam(event, 'id')
|
||||
|
||||
if (!idParam) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '缺少备份 ID')
|
||||
}
|
||||
|
||||
let record
|
||||
|
||||
if (idParam === 'latest') {
|
||||
const [latest] = await db
|
||||
.select()
|
||||
.from(backupHistory)
|
||||
.orderBy(desc(backupHistory.createdAt))
|
||||
.limit(1)
|
||||
record = latest
|
||||
} else {
|
||||
const backupId = parseInt(idParam, 10)
|
||||
if (isNaN(backupId)) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '无效的备份 ID')
|
||||
}
|
||||
const [found] = await db
|
||||
.select()
|
||||
.from(backupHistory)
|
||||
.where(eq(backupHistory.id, backupId))
|
||||
record = found
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
throw createApiError(404, 'BACKUP_NOT_FOUND', '备份记录不存在')
|
||||
}
|
||||
|
||||
const methods: Array<{ method: string; success: boolean; error?: string | null }> = JSON.parse(record.methods)
|
||||
const allCompleted = methods.every(m => m.success || m.error != null)
|
||||
const age = Date.now() - new Date(record.createdAt).getTime()
|
||||
const completedCount = methods.filter(m => m.success || m.error != null).length
|
||||
const succeededCount = methods.filter(m => m.success).length
|
||||
const failedCount = methods.filter(m => m.error != null).length
|
||||
|
||||
const status = allCompleted
|
||||
? (succeededCount === methods.length ? 'completed' : failedCount === methods.length ? 'failed' : 'partial')
|
||||
: (age > TIMEOUT_THRESHOLD_MS ? 'timeout' : 'running')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: record.id,
|
||||
filename: record.filename,
|
||||
status,
|
||||
totalRecords: record.totalRecords,
|
||||
backupSize: record.backupSize,
|
||||
createdAt: record.createdAt,
|
||||
progress: {
|
||||
total: methods.length,
|
||||
completed: completedCount,
|
||||
succeeded: succeededCount,
|
||||
failed: failedCount
|
||||
},
|
||||
methods
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user