mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
`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]>
82 lines
2.3 KiB
TypeScript
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;
|
|
}
|
|
},
|
|
});
|