|
|
@@ -4,7 +4,6 @@ import { acquireBackupLock, releaseBackupLock } from "@/lib/database-backup/back
|
|
|
import { checkDatabaseConnection, executePgRestore } from "@/lib/database-backup/docker-executor";
|
|
|
import {
|
|
|
cleanupTempFile,
|
|
|
- createCleanupCallback,
|
|
|
generateTempFilePath,
|
|
|
registerTempFile,
|
|
|
} from "@/lib/database-backup/temp-file-manager";
|
|
|
@@ -40,32 +39,7 @@ export async function POST(request: Request) {
|
|
|
return new Response("Unauthorized", { status: 401 });
|
|
|
}
|
|
|
|
|
|
- // 2. 尝试获取分布式锁(防止并发操作)
|
|
|
- lockId = await acquireBackupLock("import");
|
|
|
- if (!lockId) {
|
|
|
- logger.warn({
|
|
|
- action: "database_import_lock_conflict",
|
|
|
- user: session.user.name,
|
|
|
- });
|
|
|
- return Response.json(
|
|
|
- {
|
|
|
- error: "其他管理员正在执行备份操作,请稍后重试",
|
|
|
- details: "为确保数据一致性,同一时间只能执行一个备份操作",
|
|
|
- },
|
|
|
- { status: 409 }
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 3. 检查数据库连接
|
|
|
- const isAvailable = await checkDatabaseConnection();
|
|
|
- if (!isAvailable) {
|
|
|
- logger.error({
|
|
|
- action: "database_import_connection_unavailable",
|
|
|
- });
|
|
|
- return Response.json({ error: "数据库连接不可用,请检查数据库服务状态" }, { status: 503 });
|
|
|
- }
|
|
|
-
|
|
|
- // 4. 解析表单数据
|
|
|
+ // 2. 解析并验证表单数据(在获取锁之前完成,避免验证失败时锁被占用)
|
|
|
const formData = await request.formData();
|
|
|
const file = formData.get("file") as File | null;
|
|
|
const cleanFirst = formData.get("cleanFirst") === "true";
|
|
|
@@ -75,12 +49,12 @@ export async function POST(request: Request) {
|
|
|
return Response.json({ error: "缺少备份文件" }, { status: 400 });
|
|
|
}
|
|
|
|
|
|
- // 5. 验证文件类型
|
|
|
+ // 3. 验证文件类型
|
|
|
if (!file.name.endsWith(".dump")) {
|
|
|
return Response.json({ error: "文件格式错误,仅支持 .dump 格式的备份文件" }, { status: 400 });
|
|
|
}
|
|
|
|
|
|
- // 6. 验证文件大小
|
|
|
+ // 4. 验证文件大小
|
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
|
logger.warn({
|
|
|
action: "database_import_file_too_large",
|
|
|
@@ -97,6 +71,42 @@ export async function POST(request: Request) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ // 5. 尝试获取分布式锁(防止并发操作)
|
|
|
+ lockId = await acquireBackupLock("import");
|
|
|
+ if (!lockId) {
|
|
|
+ logger.warn({
|
|
|
+ action: "database_import_lock_conflict",
|
|
|
+ user: session.user.name,
|
|
|
+ });
|
|
|
+ return Response.json(
|
|
|
+ {
|
|
|
+ error: "其他管理员正在执行备份操作,请稍后重试",
|
|
|
+ details: "为确保数据一致性,同一时间只能执行一个备份操作",
|
|
|
+ },
|
|
|
+ { status: 409 }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 检查数据库连接
|
|
|
+ const isAvailable = await checkDatabaseConnection();
|
|
|
+ if (!isAvailable) {
|
|
|
+ logger.error({
|
|
|
+ action: "database_import_connection_unavailable",
|
|
|
+ });
|
|
|
+ // 数据库不可用时释放锁
|
|
|
+ if (lockId) {
|
|
|
+ await releaseBackupLock(lockId, "import").catch((err) => {
|
|
|
+ logger.error({
|
|
|
+ action: "database_import_lock_release_error",
|
|
|
+ lockId,
|
|
|
+ reason: "connection_unavailable",
|
|
|
+ error: err.message,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return Response.json({ error: "数据库连接不可用,请检查数据库服务状态" }, { status: 503 });
|
|
|
+ }
|
|
|
+
|
|
|
logger.info({
|
|
|
action: "database_import_initiated",
|
|
|
filename: file.name,
|
|
|
@@ -120,8 +130,34 @@ export async function POST(request: Request) {
|
|
|
});
|
|
|
|
|
|
// 8. 监听请求取消(用户关闭浏览器)
|
|
|
- const abortCleanup = createCleanupCallback(tempFilePath, "aborted");
|
|
|
- request.signal.addEventListener("abort", abortCleanup);
|
|
|
+ // 创建一个综合的清理函数,同时处理临时文件和锁
|
|
|
+ const currentLockId = lockId;
|
|
|
+ const currentTempFilePath = tempFilePath;
|
|
|
+ const abortHandler = () => {
|
|
|
+ // 清理临时文件
|
|
|
+ if (currentTempFilePath) {
|
|
|
+ cleanupTempFile(currentTempFilePath, "aborted").catch((err) => {
|
|
|
+ logger.error({
|
|
|
+ action: "database_import_cleanup_error",
|
|
|
+ tempFilePath: currentTempFilePath,
|
|
|
+ reason: "aborted",
|
|
|
+ error: err instanceof Error ? err.message : String(err),
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 释放锁
|
|
|
+ if (currentLockId) {
|
|
|
+ releaseBackupLock(currentLockId, "import").catch((err) => {
|
|
|
+ logger.error({
|
|
|
+ action: "database_import_lock_release_error",
|
|
|
+ lockId: currentLockId,
|
|
|
+ reason: "request_cancelled",
|
|
|
+ error: err.message,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ request.signal.addEventListener("abort", abortHandler);
|
|
|
|
|
|
// 9. 执行 pg_restore,返回 SSE 流
|
|
|
const stream = executePgRestore(tempFilePath, cleanFirst, skipLogs);
|
|
|
@@ -133,21 +169,22 @@ export async function POST(request: Request) {
|
|
|
},
|
|
|
flush() {
|
|
|
// 流正常结束时清理
|
|
|
- if (tempFilePath) {
|
|
|
- cleanupTempFile(tempFilePath, "completed").catch((err) => {
|
|
|
+ if (currentTempFilePath) {
|
|
|
+ cleanupTempFile(currentTempFilePath, "completed").catch((err) => {
|
|
|
logger.error({
|
|
|
action: "database_import_cleanup_error",
|
|
|
- tempFilePath,
|
|
|
+ tempFilePath: currentTempFilePath,
|
|
|
error: err.message,
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- if (lockId) {
|
|
|
- releaseBackupLock(lockId, "import").catch((err) => {
|
|
|
+ if (currentLockId) {
|
|
|
+ releaseBackupLock(currentLockId, "import").catch((err) => {
|
|
|
logger.error({
|
|
|
action: "database_import_lock_release_error",
|
|
|
- lockId,
|
|
|
+ lockId: currentLockId,
|
|
|
+ reason: "stream_done",
|
|
|
error: err.message,
|
|
|
});
|
|
|
});
|