Merge branch 'fix/backup-chunked-storage' into release/v1.39.2

This commit is contained in:
Marc-André Bombeck committed 2026-09-26 01:49:22 +02:00
commit 39b56ee4e2
25 files changed
+1092 -240

No files matched your search

@@ -22,11 +22,9 @@ import { apiHandler, HttpError, requireAdmin } from "@/lib/api-handler";
import { apiError, getClientIp } from "@/lib/api-response";
import { auditLog } from "@/lib/auth/audit";
import {
BACKUP_UNDECRYPTABLE_CODE,
BACKUP_UNDECRYPTABLE_ERROR,
} from "@/lib/export/backup-blob";
import {
isStoredBackupReadError,
openStoredBackup,
storedBackupRefusal,
STORED_BACKUP_SELECT,
} from "@/lib/export/stored-backup";
import {
@@ -89,8 +87,9 @@ export const GET = apiHandler(
reason: err instanceof Error ? err.message : "decrypt_failed",
},
});
return apiError(BACKUP_UNDECRYPTABLE_ERROR, 422, {
errorCode: BACKUP_UNDECRYPTABLE_CODE,
const refusal = storedBackupRefusal(err);
return apiError(refusal.message, refusal.status, {
errorCode: refusal.code,
});
}
@@ -103,16 +102,25 @@ export const GET = apiHandler(
streamed = await readStreamedBackup(source);
parseBackupPayload(streamed.raw);
} catch (err) {
const readFailure = isStoredBackupReadError(err);
await auditLog("admin.backups.download.failed", {
userId: admin.id,
ipAddress: getClientIp(request),
details: {
backupId: id,
ownerId: backup.userId,
reason: "schema_invalid",
reason: readFailure ? "read_failed" : "schema_invalid",
message: err instanceof Error ? err.message : String(err),
},
});
if (readFailure) {
// The copy changed between opening and reading (replaced by the
// weekly run, or altered): the same answer the open gives.
const refusal = storedBackupRefusal(err);
return apiError(refusal.message, refusal.status, {
errorCode: refusal.code,
});
}
return apiError("Backup payload failed schema validation", 500);
}
@@ -24,12 +24,9 @@ import { apiHandler, HttpError, requireAdmin } from "@/lib/api-handler";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import {
BACKUP_UNDECRYPTABLE_CODE,
BACKUP_UNDECRYPTABLE_ERROR,
} from "@/lib/export/backup-blob";
import {
openStoredBackup,
storedBackupRefusal,
STORED_BACKUP_SELECT,
storedBackupIdentity,
} from "@/lib/export/stored-backup";
@@ -114,8 +111,9 @@ const handler = apiHandler(
reason: err instanceof Error ? err.message : "decrypt_failed",
},
});
return apiError(BACKUP_UNDECRYPTABLE_ERROR, 422, {
errorCode: BACKUP_UNDECRYPTABLE_CODE,
const refusal = storedBackupRefusal(err);
return apiError(refusal.message, refusal.status, {
errorCode: refusal.code,
});
}
@@ -17,11 +17,9 @@ import { prisma } from "@/lib/db";
import { apiHandler, HttpError, requireAdmin } from "@/lib/api-handler";
import { apiError, apiSuccess } from "@/lib/api-response";
import {
BACKUP_UNDECRYPTABLE_CODE,
BACKUP_UNDECRYPTABLE_ERROR,
} from "@/lib/export/backup-blob";
import {
isStoredBackupReadError,
openStoredBackup,
storedBackupRefusal,
STORED_BACKUP_SELECT,
} from "@/lib/export/stored-backup";
import {
@@ -62,12 +60,13 @@ export const GET = apiHandler(
let source: BackupSource;
try {
source = await openStoredBackup(prisma, backup);
} catch {
} catch (err) {
// Same refusal the restore and the download give, for the same reason:
// a copy this instance cannot open is bad stored input, and the preview
// is the first place an operator meets it.
return apiError(BACKUP_UNDECRYPTABLE_ERROR, 422, {
errorCode: BACKUP_UNDECRYPTABLE_CODE,
const refusal = storedBackupRefusal(err);
return apiError(refusal.message, refusal.status, {
errorCode: refusal.code,
});
}
@@ -77,7 +76,13 @@ export const GET = apiHandler(
const streamed = await readStreamedBackup(source);
measurementCount = streamed.measurementCount;
payload = parseBackupPayload(streamed.raw);
} catch {
} catch (err) {
if (isStoredBackupReadError(err)) {
const refusal = storedBackupRefusal(err);
return apiError(refusal.message, refusal.status, {
errorCode: refusal.code,
});
}
return apiError("Backup payload failed schema validation", 422);
}