Files
dataroom/lib/files/delete-file-server.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

63 lines
1.8 KiB
TypeScript

import { DeleteObjectsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { DocumentStorageType } from "@prisma/client";
import { match } from "ts-pattern";
import { getS3ClientAndConfig } from "./aws-client";
export type DeleteFileOptions = {
type: DocumentStorageType;
data: string; // folder path in our object storage
teamId: string; // needed to resolve storage region
};
export const deleteFile = async ({ type, data, teamId }: DeleteFileOptions) => {
return await match(type)
.with(DocumentStorageType.S3_PATH, async () =>
deleteAllFilesFromS3Server(data, teamId),
)
.otherwise(() => {
return;
});
};
const deleteAllFilesFromS3Server = async (data: string, teamId: string) => {
// get docId from url with starts with "doc_" with regex
const dataMatch = data.match(/^(.*doc_[^\/]+)\//);
const folderPath = dataMatch ? dataMatch[1] : data;
const { client, config } = getS3ClientAndConfig();
try {
// List all objects in the folder
const listParams = {
Bucket: config.bucket,
Prefix: `${folderPath}/`, // Ensure this ends with a slash if it's a folder
};
const listedObjects = await client.send(
new ListObjectsV2Command(listParams),
);
if (!listedObjects.Contents) return;
if (listedObjects.Contents.length === 0) return;
// Prepare delete parameters
const deleteParams = {
Bucket: config.bucket,
Delete: {
Objects: listedObjects.Contents.map((file) => ({ Key: file.Key })),
},
};
// Delete the objects
await client.send(new DeleteObjectsCommand(deleteParams));
if (listedObjects.IsTruncated) {
// If there are more files than returned in a single request, recurse
await deleteAllFilesFromS3Server(folderPath, teamId);
}
} catch (error) {
console.error("Error deleting files:", error);
}
};