Files
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

81 lines
2.1 KiB
TypeScript

import { DocumentStorageType } from "@prisma/client";
import { match } from "ts-pattern";
export type GetFileOptions = {
type: DocumentStorageType;
data: string;
isDownload?: boolean;
};
export const getFile = async ({
type,
data,
isDownload = false,
}: GetFileOptions): Promise<string> => {
// One backend. `type` stays on the signature because callers read it from the
// document row, but every row this app writes is S3_PATH.
return getFileFromS3(data);
};
const fetchPresignedUrl = async (
endpoint: string,
headers: Record<string, string>,
key: string,
): Promise<string> => {
const response = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({ key }),
});
if (!response.ok) {
const contentType = response.headers.get("content-type");
let errorMessage: string;
if (contentType && contentType.includes("application/json")) {
try {
const error = await response.json();
errorMessage =
error.message || `Request failed with status ${response.status}`;
} catch (parseError) {
const textError = await response.text();
errorMessage =
textError || `Request failed with status ${response.status}`;
}
} else {
const textError = await response.text();
errorMessage =
textError || `Request failed with status ${response.status}`;
}
throw new Error(errorMessage);
}
const { url } = (await response.json()) as { url: string };
return url;
};
const getFileFromS3 = async (key: string) => {
const isServer =
typeof window === "undefined" && !!process.env.INTERNAL_API_KEY;
if (isServer) {
return fetchPresignedUrl(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/file/s3/get-presigned-get-url`,
{
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.INTERNAL_API_KEY}`,
},
key,
);
} else {
return fetchPresignedUrl(
`/api/file/s3/get-presigned-get-url-proxy`,
{
"Content-Type": "application/json",
},
key,
);
}
};