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);
}