File size: 4,321 Bytes
de6a95b
0349430
 
 
 
 
 
 
 
 
 
 
538f814
0349430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538f814
 
 
 
 
 
de6a95b
538f814
de6a95b
538f814
d9879cf
de6a95b
538f814
 
 
0349430
 
 
 
 
 
de6a95b
0349430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6019d2d
0349430
6019d2d
0349430
6019d2d
 
538f814
6019d2d
 
538f814
0349430
538f814
 
d9879cf
6019d2d
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
95
96
97
import { logger } from '../logger';
/**
 * 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) {
            logger.warn(`[Storage] ⚠️ Public access check failed for ${finalUrl} (Status: ${check.status})`);
        } else {
            logger.info(`[Storage] βœ… Verified public access: ${finalUrl}`);
        }
    } catch (err: unknown) {
        logger.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);
    logger.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 originalFilename - Filename with extension (e.g. "onepager-1234.pdf")
 * @param contentType - MIME type (e.g. "application/pdf")
 * @param organizationId - Optional organization ID for folder isolation
 */
export async function uploadFile(buffer: Buffer, originalFilename: string, contentType: string, organizationId?: string): Promise<string> {
    // ─── Production Hardening: Unique Filenames with Org Isolation ───────────
    const ext = path.extname(originalFilename);
    const prefix = organizationId ? `orgs/${organizationId}/` : '';
    const uniqueName = `${prefix}${crypto.randomUUID()}-${Date.now()}${ext}`;

    if (isR2Configured()) {
        try {
            return await uploadToR2(buffer, uniqueName, contentType);
        } catch (err: unknown) {
            logger.error(`[Storage] R2 Upload Failed: ${(err instanceof Error ? err.message : String(err))}. Falling back to local.`);
        }
    }
    return saveLocally(buffer, uniqueName);
}