|
| 1 | +import { writeFileSync, mkdirSync, existsSync } from 'fs'; |
| 2 | +import { join, dirname } from 'path'; |
| 3 | +import { fileURLToPath } from 'url'; |
| 4 | +import { randomUUID } from 'crypto'; |
| 5 | + |
| 6 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 7 | +const LOCAL_DIR = process.env.MERCH_ARTWORK_LOCAL_DIR || |
| 8 | + join(__dirname, '..', 'data', 'artwork'); |
| 9 | + |
| 10 | +function getBaseUrl() { |
| 11 | + return (process.env.MERCH_ARTWORK_BASE_URL || '').replace(/\/$/, ''); |
| 12 | +} |
| 13 | + |
| 14 | +function ensureLocalDir() { |
| 15 | + if (!existsSync(LOCAL_DIR)) mkdirSync(LOCAL_DIR, { recursive: true }); |
| 16 | +} |
| 17 | + |
| 18 | +async function uploadLocal(buffer, ext = 'png') { |
| 19 | + ensureLocalDir(); |
| 20 | + const id = randomUUID(); |
| 21 | + const filename = `${id}.${ext}`; |
| 22 | + const filepath = join(LOCAL_DIR, filename); |
| 23 | + writeFileSync(filepath, buffer); |
| 24 | + const base = getBaseUrl(); |
| 25 | + if (!base) { |
| 26 | + return { id, filename, url: `/api/artwork/${filename}` }; |
| 27 | + } |
| 28 | + return { id, filename, url: `${base}/${filename}` }; |
| 29 | +} |
| 30 | + |
| 31 | +async function uploadS3(buffer, ext = 'png') { |
| 32 | + const bucket = process.env.MERCH_S3_BUCKET; |
| 33 | + const region = process.env.MERCH_S3_REGION || 'auto'; |
| 34 | + const endpoint = process.env.MERCH_S3_ENDPOINT; |
| 35 | + const accessKey = process.env.MERCH_S3_ACCESS_KEY; |
| 36 | + const secretKey = process.env.MERCH_S3_SECRET_KEY; |
| 37 | + const publicBase = getBaseUrl(); |
| 38 | + |
| 39 | + if (!bucket || !accessKey || !secretKey) { |
| 40 | + throw new Error('S3 storage requires MERCH_S3_BUCKET, MERCH_S3_ACCESS_KEY, MERCH_S3_SECRET_KEY'); |
| 41 | + } |
| 42 | + |
| 43 | + const id = randomUUID(); |
| 44 | + const key = `artwork/${id}.${ext}`; |
| 45 | + const host = endpoint || `https://${bucket}.s3.${region}.amazonaws.com`; |
| 46 | + const url = `${host.replace(/\/$/, '')}/${key}`; |
| 47 | + |
| 48 | + const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3'); |
| 49 | + const client = new S3Client({ |
| 50 | + region, |
| 51 | + endpoint: endpoint || undefined, |
| 52 | + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, |
| 53 | + forcePathStyle: !!endpoint |
| 54 | + }); |
| 55 | + |
| 56 | + await client.send(new PutObjectCommand({ |
| 57 | + Bucket: bucket, |
| 58 | + Key: key, |
| 59 | + Body: buffer, |
| 60 | + ContentType: `image/${ext}`, |
| 61 | + ACL: 'public-read' |
| 62 | + })); |
| 63 | + |
| 64 | + const publicUrl = publicBase ? `${publicBase}/${key}` : url; |
| 65 | + return { id, filename: key, url: publicUrl }; |
| 66 | +} |
| 67 | + |
| 68 | +export async function uploadArtwork(buffer, ext = 'png') { |
| 69 | + const mode = (process.env.MERCH_ARTWORK_STORAGE || 'local').toLowerCase(); |
| 70 | + if (mode === 's3') return uploadS3(buffer, ext); |
| 71 | + return uploadLocal(buffer, ext); |
| 72 | +} |
| 73 | + |
| 74 | +export function getLocalArtworkPath(filename) { |
| 75 | + if (!filename || filename.includes('..') || filename.includes('/')) return null; |
| 76 | + const filepath = join(LOCAL_DIR, filename); |
| 77 | + if (!existsSync(filepath)) return null; |
| 78 | + return filepath; |
| 79 | +} |
0 commit comments