CognxSafeTrack
chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)
d9879cf | /** | |
| * Storage Service β Cloudflare R2 (S3-compatible) with local /tmp fallback | |
| * | |
| * Required env vars for R2 mode: | |
| * R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_URL | |
| * | |
| * Falls back to saving files in /tmp in development (returns a file:// path). | |
| */ | |
| import fs from 'fs/promises'; | |
| import path from 'path'; | |
| import crypto from 'crypto'; | |
| // βββ R2 / S3 Upload βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function uploadToR2(buffer: Buffer, filename: string, contentType: string): Promise<string> { | |
| const accountId = process.env.R2_ACCOUNT_ID!; | |
| const bucket = process.env.R2_BUCKET!; | |
| const accessKeyId = process.env.R2_ACCESS_KEY_ID!; | |
| const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY!; | |
| const publicUrl = process.env.R2_PUBLIC_URL!; // e.g. https://pub-xxx.r2.dev | |
| // Use native fetch with AWS Signature V4 (no SDK dependency needed for simple PUT) | |
| const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3'); | |
| const client = new S3Client({ | |
| region: 'auto', | |
| endpoint: `https://${accountId}.r2.cloudflarestorage.com`, | |
| credentials: { accessKeyId, secretAccessKey }, | |
| }); | |
| await client.send(new PutObjectCommand({ | |
| Bucket: bucket, | |
| Key: filename, | |
| Body: buffer, | |
| ContentType: contentType, | |
| })); | |
| const finalUrl = `${publicUrl.replace(/\/$/, "")}/${filename}`; | |
| // βββ Production Hardening: Verify Public Access βββββββββ | |
| try { | |
| const check = await fetch(finalUrl, { method: 'HEAD' }); | |
| if (!check.ok) { | |
| console.warn(`[Storage] β οΈ Public access check failed for ${finalUrl} (Status: ${check.status})`); | |
| } else { | |
| console.log(`[Storage] β Verified public access: ${finalUrl}`); | |
| } | |
| } catch (err: unknown) { | |
| console.warn(`[Storage] β οΈ Could not verify public access for ${finalUrl}: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`); | |
| } | |
| return finalUrl; | |
| } | |
| // βββ Local /tmp Fallback ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function saveLocally(buffer: Buffer, filename: string): Promise<string> { | |
| const tmpPath = path.join('/tmp', filename); | |
| await fs.writeFile(tmpPath, buffer); | |
| console.warn(`[Storage] R2 not configured β file saved locally to ${tmpPath}`); | |
| return `file://${tmpPath}`; | |
| } | |
| // βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function isR2Configured(): boolean { | |
| return !!( | |
| process.env.R2_ACCOUNT_ID && | |
| process.env.R2_ACCESS_KEY_ID && | |
| process.env.R2_SECRET_ACCESS_KEY && | |
| process.env.R2_BUCKET && | |
| process.env.R2_PUBLIC_URL | |
| ); | |
| } | |
| /** | |
| * Upload a buffer to Cloudflare R2 (or save locally in dev) and return the public URL. | |
| * @param buffer - File contents | |
| * @param filename - Filename with extension (e.g. "onepager-1234.pdf") | |
| * @param contentType - MIME type (e.g. "application/pdf") | |
| */ | |
| export async function uploadFile(buffer: Buffer, originalFilename: string, contentType: string): Promise<string> { | |
| // βββ Production Hardening: Unique Filenames βββββββββββββ | |
| const ext = path.extname(originalFilename); | |
| const uniqueName = `${crypto.randomUUID()}-${Date.now()}${ext}`; | |
| if (isR2Configured()) { | |
| try { | |
| return await uploadToR2(buffer, uniqueName, contentType); | |
| } catch (err: unknown) { | |
| console.error(`[Storage] R2 Upload Failed: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}. Falling back to local.`); | |
| } | |
| } | |
| return saveLocally(buffer, uniqueName); | |
| } | |