import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const accountId = process.env.R2_ACCOUNT_ID ?? ""; const bucket = process.env.R2_BUCKET ?? "continuum"; export const s3 = new S3Client({ region: "auto", endpoint: process.env.R2_ENDPOINT ?? `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "", secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "", }, }); /** Presigned PUT for clients uploading a G-code/3MF file directly to R2. */ export async function getUploadUrl(key: string, contentType = "application/octet-stream", expiresInSeconds = 900) { const command = new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType }); return getSignedUrl(s3, command, { expiresIn: expiresInSeconds }); } /** Presigned GET for the edge proxy to download a job file over LAN via FTPS relay or direct fetch. */ export async function getDownloadUrl(key: string, expiresInSeconds = 900) { const command = new GetObjectCommand({ Bucket: bucket, Key: key }); return getSignedUrl(s3, command, { expiresIn: expiresInSeconds }); } /** * Whether the object is actually in the bucket, and how big it is. * * The scheduler needs this because `POST /jobs` returns a presigned PUT and * the client has not used it yet: the row exists before the file does. Rather * than trust a client to tell us it finished - a client that crashes * mid-upload would never say so, and one that lies would have a gateway * download an error document - this asks the bucket. * * Returns `undefined` for a missing object rather than throwing, because for * this caller "not uploaded yet" is the ordinary case on every tick between * queueing a job and the upload finishing. */ export async function headObject(key: string): Promise<{ size: number } | undefined> { try { const head = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); return { size: head.ContentLength ?? 0 }; } catch (err) { // 404/NotFound is the expected answer for a job whose file has not // arrived. Anything else - credentials, networking, a bucket that does // not exist - is a real problem and must not be reported as "no file", // or every job on a misconfigured deployment would sit queued forever // with nothing saying why. const name = (err as { name?: string }).name; const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode; if (name === "NotFound" || name === "NoSuchKey" || status === 404) return undefined; throw err; } } export function jobFileKey(farmId: string, jobId: string, fileName: string) { return `farms/${farmId}/jobs/${jobId}/${fileName}`; } export function frameKey(farmId: string, printerId: string, frameId: string) { return `farms/${farmId}/printers/${printerId}/frames/${frameId}.jpg`; }