Files
dataroom/lib/files/put-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

101 lines
2.4 KiB
TypeScript

import { PutObjectCommand } from "@aws-sdk/client-s3";
import { DocumentStorageType } from "@prisma/client";
import path from "node:path";
import { match } from "ts-pattern";
import { newId } from "@/lib/id-helper";
import { safeSlugify } from "@/lib/utils";
import { SUPPORTED_DOCUMENT_MIME_TYPES } from "../constants";
import { getS3ClientAndConfig } from "./aws-client";
// `File` is a web API type and not available server-side, so we need to define our own type
type File = {
name: string;
type: string;
buffer: Buffer;
};
export const putFileServer = async ({
file,
teamId,
docId,
restricted = true,
}: {
file: File;
teamId: string;
docId?: string;
restricted?: boolean;
}) => {
// One object store, so no transport to choose. The match this replaced keyed
// on NEXT_PUBLIC_UPLOAD_TRANSPORT, which the deployment does not set — so it
// fell to `.otherwise()` and returned { type: null, data: null }, i.e. every
// server-side upload reported success while storing nothing.
const { type, data } = await putFileInS3Server({
file,
teamId,
docId,
restricted,
});
return { type, data };
};
const putFileInS3Server = async ({
file,
teamId,
docId,
restricted = true,
}: {
file: File;
teamId: string;
docId?: string;
restricted?: boolean;
}) => {
if (!docId) {
docId = newId("doc");
}
if (
restricted &&
file.type !== "image/png" &&
file.type !== "image/jpeg" &&
file.type !== "application/pdf"
) {
throw new Error("Only PNG, JPEG, PDF or MP4 files are supported");
}
if (!restricted && !SUPPORTED_DOCUMENT_MIME_TYPES.includes(file.type)) {
throw new Error("Unsupported file type");
}
const { client, config } = getS3ClientAndConfig();
// Get the basename and extension for the file
const { name, ext } = path.parse(file.name);
const slugifiedName = safeSlugify(name) + ext;
const originalFileName = `${name}${ext}`;
const key = `${teamId}/${docId}/${slugifiedName}`;
const params = {
Bucket: config.bucket,
Key: key,
Body: file.buffer,
ContentType: file.type,
ContentDisposition: `attachment; filename="${slugifiedName}"; filename*=UTF-8''${encodeURIComponent(originalFileName)}`,
};
// Create a new instance of the PutObjectCommand with the parameters
const command = new PutObjectCommand(params);
// Send the command to S3
await client.send(command);
return {
type: DocumentStorageType.S3_PATH,
data: key,
};
};