File size: 4,132 Bytes
0349430 538f814 0349430 538f814 d9879cf 538f814 0349430 538f814 0349430 538f814 d9879cf 538f814 0349430 538f814 0349430 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | /**
* 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);
}
|