Files
dataroom/lib/trigger/cleanup-expired-exports.ts
zeekayandHanzo Dev 2c4319f61e storage: one object store, ours — and the gate that shut it off is gone
`getS3ClientAndConfig` threw "Invalid upload transport" unless
NEXT_PUBLIC_UPLOAD_TRANSPORT was "s3". The deployment does not set that variable
— it configures storage through NEXT_PRIVATE_UPLOAD_* (transport s3, bucket
dataroom, endpoint http://s3:9000, credentials) — so every call through that
helper threw in production while a correctly configured bucket sat beside it.
The two put paths keyed on the same unset variable and fell to `.otherwise()`,
returning { type: null, data: null }: a server-side upload reported success and
stored nothing.

There is one object store now, so nothing chooses a transport. The VERCEL_BLOB
arms are gone from get/put/copy/delete, CSV exports write through putFileServer,
and branding + expired-export deletion go through deleteFile.

Safe to remove rather than migrate: the database has no rows to orphan —
Document, DocumentVersion, Team and User are all 0, so nothing is stored under a
VERCEL_BLOB path.

The cleanup queue now carries teamId, which deleteFile needs to resolve the
storage region, and its item is a `path` rather than a `blobUrl` — the scheduler
was testing `startsWith("https://")` to decide whether to enqueue, which a
storage path never matches.

Verified with npx tsc --noEmit: 100 -> 99. The one fixed is get-file.ts, whose
ts-pattern match is no longer there to be uncallable. Nothing introduced: the
two duplicate.ts files that looked new carry identical error counts before and
after and were never touched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 11:23:32 -07:00

82 lines
2.3 KiB
TypeScript

import { logger, schedules } from "@trigger.dev/sdk/v3";
import { jobStore } from "@/lib/kv-job-store";
import { deleteFile } from "@/lib/files/delete-file-server";
import { DocumentStorageType } from "@prisma/client";
export const cleanupExpiredExports = schedules.task({
id: "cleanup-expired-exports",
// Run daily at 2 AM UTC
cron: "0 2 * * *",
run: async (payload) => {
logger.info("Starting cleanup of expired exports", {
timestamp: payload.timestamp,
});
try {
// Get all blob URLs that are due for cleanup
const expired = await jobStore.getExpiredForCleanup();
if (expired.length === 0) {
logger.info("Nothing due for cleanup");
return { deletedCount: 0 };
}
logger.info(`Found ${expired.length} to delete`);
// Delete from our object storage
const deletionResults = await Promise.allSettled(
expired.map(async (item) => {
try {
await deleteFile({
type: DocumentStorageType.S3_PATH,
data: item.path,
teamId: item.teamId,
});
// Remove from cleanup queue after successful deletion
await jobStore.removeFromCleanupQueue(item.path, item.jobId);
logger.info("Deleted", {
path: item.path,
jobId: item.jobId,
});
return { path: item.path, success: true };
} catch (error) {
logger.error("Failed to delete", {
path: item.path,
jobId: item.jobId,
error: error instanceof Error ? error.message : String(error),
});
return { path: item.path, success: false, error };
}
}),
);
const successCount = deletionResults.filter(
(result) => result.status === "fulfilled" && result.value.success,
).length;
const failureCount = deletionResults.length - successCount;
logger.info("Cleanup completed", {
totalBlobs: expired.length,
successCount,
failureCount,
});
return {
deletedCount: successCount,
failureCount,
totalProcessed: expired.length,
};
} catch (error) {
logger.error("Cleanup task failed", {
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
},
});