diff --git a/prisma/migrations/20260722000000_mark_retained_manual_files/migration.sql b/prisma/migrations/20260722000000_mark_retained_manual_files/migration.sql new file mode 100644 index 0000000..b2b1ca8 --- /dev/null +++ b/prisma/migrations/20260722000000_mark_retained_manual_files/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "manual_upload_files" ADD COLUMN "retainedAt" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 679a74e..ce14213 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -937,6 +937,7 @@ model ManualUploadFile { filePath String // Path on shared volume fileSize BigInt packageId String? // Set after processing + retainedAt DateTime? upload ManualUpload @relation(fields: [uploadId], references: [id], onDelete: Cascade) diff --git a/scripts/backup/restore.sh b/scripts/backup/restore.sh index 3085baa..d96621a 100755 --- a/scripts/backup/restore.sh +++ b/scripts/backup/restore.sh @@ -180,12 +180,25 @@ verify_file_references() { local database_user="${POSTGRES_USER:-dragons}" local database_name="${POSTGRES_DB:-dragonsstash}" docker compose exec -T db psql --no-psqlrc --tuples-only --no-align --quiet \ + --field-separator=$'\t' \ --username "$database_user" --dbname "$database_name" \ - --command 'SELECT "filePath" FROM "manual_upload_files" ORDER BY "filePath"' | + --command "SELECT 'legacy', \"filePath\" FROM \"manual_upload_files\" WHERE \"retainedAt\" IS NULL + UNION ALL + SELECT 'retained', \"filePath\" FROM \"manual_upload_files\" WHERE \"retainedAt\" IS NOT NULL + ORDER BY 2" | docker compose --profile backup run --rm --no-deps -T --entrypoint bash \ -v "$uploads_volume:/data/uploads:ro" backup -ceu ' missing=0 - while IFS= read -r file_path; do + while IFS="$(printf "\t")" read -r retention file_path; do + if [[ "$retention" == "legacy" ]]; then + printf "Warning: legacy manual-upload file reference is not required because retainedAt is NULL: %s\\n" "$file_path" >&2 + continue + fi + if [[ "$retention" != "retained" ]]; then + printf "Unexpected retention status for database reference: %s\\n" "$file_path" >&2 + missing=1 + continue + fi case "$file_path" in /data/uploads/*) relative_path="${file_path#/data/uploads/}" ;; *) diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts index 225abee..4f9cc30 100644 --- a/src/app/api/uploads/route.ts +++ b/src/app/api/uploads/route.ts @@ -55,6 +55,7 @@ export async function POST(request: Request) { fileName: file.name, filePath, fileSize: BigInt(file.size), + retainedAt: new Date(), }, }); } diff --git a/worker/src/manual-upload.ts b/worker/src/manual-upload.ts index 3cef614..1757261 100644 --- a/worker/src/manual-upload.ts +++ b/worker/src/manual-upload.ts @@ -1,4 +1,3 @@ -import path from "path"; import { rm } from "fs/promises"; import { db } from "./db/client.js"; import { childLogger } from "./util/logger.js"; @@ -200,12 +199,4 @@ export async function processManualUpload(uploadId: string): Promise { data: { status: "FAILED", errorMessage: message }, }); } - - // Clean up uploaded files - try { - const uploadDir = path.join("/data/uploads", uploadId); - await rm(uploadDir, { recursive: true, force: true }); - } catch { - // Best-effort cleanup - } }