DataExtract / artifacts /api-server /src /lib /articleImages.ts
Taha Saif
Deploy Ecom Hub API backend (Express + Drizzle + Clerk + R2)
74bfb65
Raw History Blame Contribute Delete
2.36 kB
import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { randomUUID } from 'node:crypto';
import { PassThrough, type Readable } from 'node:stream';
const bucket = process.env.R2_BUCKET;
const s3 = new S3Client({
region: 'auto',
endpoint: process.env.R2_ACCOUNT_ID ? `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com` : undefined,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
},
});
function objectKey(id: string) {
return `article-images/${id}`;
}
export function articleImageFile(id: string) {
if (!bucket) throw new Error('App Storage is not configured.');
const key = objectKey(id);
return {
async getMetadata() {
const head = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
return [{ contentType: head.ContentType }] as const;
},
createReadStream() {
const stream = new PassThrough();
s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }))
.then((object) => {
const body = object.Body as Readable;
body.on('error', (err) => stream.emit('error', err));
body.pipe(stream);
})
.catch((err) => stream.emit('error', err));
return stream;
},
async save(bytes: Buffer, options: { contentType: string }) {
await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: bytes, ContentType: options.contentType }));
},
};
}
// Reject renamed HTML/SVG and executable files, not just incorrect MIME labels.
export function imageMime(bytes: Buffer) {
if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return 'image/png';
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return 'image/jpeg';
if (/^GIF8[79]a$/.test(bytes.subarray(0, 6).toString())) return 'image/gif';
if (bytes.subarray(0, 4).toString() === 'RIFF' && bytes.subarray(8, 12).toString() === 'WEBP') return 'image/webp';
return null;
}
export async function saveArticleImage(bytes: Buffer) {
const contentType = imageMime(bytes);
if (!contentType) throw new Error('Choose a JPG, PNG, WEBP or GIF image.');
const id = randomUUID();
await articleImageFile(id).save(bytes, { contentType });
return `/api/article-images/${id}`;
}