import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const isDev = process.env.NODE_ENV === "development"; function getR2Client() { return new S3Client({ region: "auto", endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, }, }); } const DEV_BASE_URL = process.env.NEXTAUTH_URL ?? "http://localhost:3000"; export async function generateUploadUrl( key: string, contentType: string, expiresIn = 300 ): Promise { if (isDev) { return `${DEV_BASE_URL}/api/files/dev/${key}`; } const command = new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: key, ContentType: contentType, }); return getSignedUrl(getR2Client(), command, { expiresIn }); } export async function generateDownloadUrl( key: string, expiresIn = 3600 ): Promise { if (isDev) { return `${DEV_BASE_URL}/api/files/dev/${key}`; } const command = new GetObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: key }); return getSignedUrl(getR2Client(), command, { expiresIn }); } export function buildStorageKey( // Crewing adds "cv" (Phase 3a); "crew-document" / "contract" follow in later // phases — see Crewing-Implementation-Spec §4.5. type: "po-document" | "receipt" | "cv" | "crew-document" | "contract" | "po-pdf", ownerId: string, fileName: string ): string { const timestamp = Date.now(); const safe = fileName.replace(/[^a-zA-Z0-9._-]/g, "_"); return `${type}/${ownerId}/${timestamp}-${safe}`; } export function buildSignatureKey(userId: string, ext: string): string { return `signatures/${userId}.${ext}`; } /** * Deterministic key for a PO's rendered PDF (one object per PO, no timestamp) so * "Email to vendor" can reuse a previously rendered copy instead of re-rendering * and re-uploading on every send (see `prepareVendorEmail`). */ export function buildPoPdfKey(poId: string, fileName: string): string { const safe = fileName.replace(/[^a-zA-Z0-9._-]/g, "_"); return `po-pdf/${poId}/${safe}`; } /** * Storage key for a company branding asset (logo or stamp/seal). * Deterministic per company+type so a re-upload overwrites the previous file. */ export function buildCompanyAssetKey( companyId: string, type: "logo" | "stamp", ext: string ): string { return `company-assets/${companyId}/${type}.${ext}`; } /** * Upload a file buffer directly to storage (server-side). * In dev: writes to .dev-uploads/. In prod: PUTs to R2. */ export async function uploadBuffer( key: string, buffer: Buffer, contentType: string ): Promise { if (isDev) { const fs = await import("fs/promises"); const path = await import("path"); const dir = path.join(process.cwd(), ".dev-uploads", ...key.split("/").slice(0, -1)); const filePath = path.join(process.cwd(), ".dev-uploads", ...key.split("/")); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, buffer); } else { const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3"); const s3 = new S3Client({ region: "auto", endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, }, }); await s3.send(new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: key, Body: buffer, ContentType: contentType, })); } } /** * Lightweight existence/metadata check for a stored object (no body transfer). * Returns `{ lastModified }` when the object exists, or `null` when it doesn't. * Used to reuse a cached PO PDF when it's still current. */ export async function statObject(key: string): Promise<{ lastModified: Date } | null> { try { if (isDev) { const fs = await import("fs/promises"); const path = await import("path"); const filePath = path.join(process.cwd(), ".dev-uploads", ...key.split("/")); const s = await fs.stat(filePath); return { lastModified: s.mtime }; } const { S3Client, HeadObjectCommand } = await import("@aws-sdk/client-s3"); const s3 = new S3Client({ region: "auto", endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, }, }); const r = await s3.send(new HeadObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: key })); return { lastModified: r.LastModified ?? new Date(0) }; } catch { return null; // missing object (404/NotFound) or any access error → treat as absent } } /** * Fetch a stored file as a Buffer (server-side). */ export async function downloadBuffer(key: string): Promise { try { if (isDev) { const fs = await import("fs/promises"); const path = await import("path"); const filePath = path.join(process.cwd(), ".dev-uploads", ...key.split("/")); return await fs.readFile(filePath) as Buffer; } else { const url = await generateDownloadUrl(key, 60); const res = await fetch(url); if (!res.ok) return null; return Buffer.from(await res.arrayBuffer()); } } catch { return null; } }