322 lines
9.4 KiB
JavaScript
322 lines
9.4 KiB
JavaScript
const { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
|
|
function getEnv(name, fallback) {
|
|
const v = process.env[name];
|
|
if (v == null || String(v).trim() === '') return fallback;
|
|
return String(v).trim();
|
|
}
|
|
|
|
function sanitizeKeySegment(segment) {
|
|
const s = String(segment).trim();
|
|
return s.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
}
|
|
|
|
/** Activity folder names under {eventId}/… */
|
|
const PHOTO_ACTIVITIES = {
|
|
photoWall: 'photo-wall',
|
|
photoSearch: 'photo-search',
|
|
aiAvatar: 'ai-avatar',
|
|
socialShare: 'social-share',
|
|
engagement: 'engagement',
|
|
};
|
|
|
|
/** Kind folder names under {eventId}/{activity}/… */
|
|
const PHOTO_KINDS = {
|
|
originals: 'originals',
|
|
framed: 'framed',
|
|
gallery: 'gallery',
|
|
searchTmp: 'search-tmp',
|
|
uploads: 'uploads',
|
|
results: 'results',
|
|
};
|
|
|
|
/**
|
|
* Build standardized S3 key:
|
|
* {eventId}/{activity}/{kind}/{filename}
|
|
*/
|
|
function buildActivityPhotoKey({ eventId, activity, kind, filename }) {
|
|
const safeEventId = sanitizeKeySegment(eventId || 'unknown-event');
|
|
const safeActivity = sanitizeKeySegment(activity || 'misc');
|
|
const safeKind = sanitizeKeySegment(kind || 'uploads');
|
|
const safeFilename = sanitizeKeySegment(filename || 'file');
|
|
return `${safeEventId}/${safeActivity}/${safeKind}/${safeFilename}`;
|
|
}
|
|
|
|
/**
|
|
* Legacy helper — prefer buildActivityPhotoKey.
|
|
* Pattern: {eventId}/{filename} or {eventId}/{subdir}/{filename}
|
|
* When subdir contains `/`, each segment is sanitized separately.
|
|
*/
|
|
function buildEventPhotoKey({ eventId, filename, subdir }) {
|
|
const safeEventId = sanitizeKeySegment(eventId || 'unknown-event');
|
|
const safeFilename = sanitizeKeySegment(filename || 'file');
|
|
if (!subdir) return `${safeEventId}/${safeFilename}`;
|
|
const safeSubdir = String(subdir)
|
|
.split('/')
|
|
.map((part) => sanitizeKeySegment(part))
|
|
.filter(Boolean)
|
|
.join('/');
|
|
if (!safeSubdir) return `${safeEventId}/${safeFilename}`;
|
|
return `${safeEventId}/${safeSubdir}/${safeFilename}`;
|
|
}
|
|
|
|
/**
|
|
* Photo wall keys — standardized layout:
|
|
* - original: {eventId}/photo-wall/originals/{filename}
|
|
* - framed: {eventId}/photo-wall/framed/{filename}
|
|
*/
|
|
function buildPhotoWallKey({ eventId, filename, framed = false }) {
|
|
return buildActivityPhotoKey({
|
|
eventId,
|
|
activity: PHOTO_ACTIVITIES.photoWall,
|
|
kind: framed ? PHOTO_KINDS.framed : PHOTO_KINDS.originals,
|
|
filename,
|
|
});
|
|
}
|
|
|
|
function picturesCloudfrontBase() {
|
|
// Pictures CDN (eventbuzz-pictures + DIT). Do not fall back to AWS_URL (rapportapp media CDN).
|
|
return getEnv('AWS_URL_PICTURES', 'https://pictures.eventbuzz.live').replace(/\/+$/, '');
|
|
}
|
|
|
|
function mediaCloudfrontBase() {
|
|
return getEnv('AWS_URL', 'https://did94hhhcgq1d.cloudfront.net').replace(/\/+$/, '');
|
|
}
|
|
|
|
function hostnameFromBase(base) {
|
|
if (!base) return '';
|
|
try {
|
|
return new URL(base.includes('://') ? base : `https://${base}`).hostname.toLowerCase();
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function isPicturesHost(hostname) {
|
|
const picturesHost = hostnameFromBase(picturesCloudfrontBase());
|
|
const host = String(hostname || '').toLowerCase();
|
|
return Boolean(picturesHost && host === picturesHost);
|
|
}
|
|
|
|
/** Media-library keys from s3Media.js: eventbuzz/{orgId}/{eventId}/{module}/{file} */
|
|
function isLegacyMediaKey(key) {
|
|
return /^eventbuzz\//i.test(String(key || '').trim());
|
|
}
|
|
|
|
function parseAbsoluteUrl(urlOrKey) {
|
|
const raw = String(urlOrKey || '').trim();
|
|
if (!/^https?:\/\//i.test(raw)) return null;
|
|
try {
|
|
return new URL(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize a stored pictures url/key to an S3 object key.
|
|
* Strips absolute CDN hosts (custom domain + legacy CloudFront) and fit-in prefixes.
|
|
*/
|
|
function toPicturesKey(urlOrKey) {
|
|
let raw = String(urlOrKey || '').trim();
|
|
if (!raw) return '';
|
|
|
|
if (/^https?:\/\//i.test(raw)) {
|
|
try {
|
|
const u = new URL(raw);
|
|
raw = u.pathname || '';
|
|
} catch {
|
|
/* keep raw */
|
|
}
|
|
}
|
|
|
|
raw = raw.replace(/^\/+/, '');
|
|
// DIT Thumbor-style path: fit-in/400x400/{key}
|
|
raw = raw.replace(/^fit-in\/\d+x\d+\//i, '');
|
|
return raw;
|
|
}
|
|
|
|
/**
|
|
* Absolute CloudFront URL for a pictures-bucket key (original, no resize).
|
|
* Accepts a bare key or an absolute URL (rewritten onto current AWS_URL_PICTURES).
|
|
*/
|
|
function cloudfrontUrlForKey(key) {
|
|
const objectKey = toPicturesKey(key);
|
|
if (!objectKey) return '';
|
|
const base = picturesCloudfrontBase();
|
|
if (!base) {
|
|
console.warn('[s3EventPhotos] AWS_URL_PICTURES is not set — returning S3 key as url');
|
|
return objectKey;
|
|
}
|
|
return `${base}/${objectKey}`;
|
|
}
|
|
|
|
function mediaUrlForKey(key) {
|
|
const objectKey = String(key || '').replace(/^\/+/, '');
|
|
if (!objectKey) return '';
|
|
const base = mediaCloudfrontBase();
|
|
if (!base) return objectKey;
|
|
return `${base}/${objectKey}`;
|
|
}
|
|
|
|
/**
|
|
* Normalize stored url/framedUrl that may be a bare key (legacy / misconfigured env).
|
|
* Absolute URLs on the old media CDN (rapportapp / AWS_URL) are left unchanged —
|
|
* those files are not in the eventbuzz-pictures bucket.
|
|
*/
|
|
function resolvePhotoPublicUrl(urlOrKey) {
|
|
if (urlOrKey == null || String(urlOrKey).trim() === '') return null;
|
|
const raw = String(urlOrKey).trim();
|
|
|
|
const absolute = parseAbsoluteUrl(raw);
|
|
if (absolute) {
|
|
if (isPicturesHost(absolute.hostname)) {
|
|
return cloudfrontUrlForKey(raw) || raw;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
if (isLegacyMediaKey(raw)) {
|
|
return mediaUrlForKey(raw) || raw;
|
|
}
|
|
|
|
const url = cloudfrontUrlForKey(raw);
|
|
return url || null;
|
|
}
|
|
|
|
/**
|
|
* On-the-fly resized picture URL via Dynamic Image Transformation (Thumbor fit-in).
|
|
* Example: https://pictures.eventbuzz.live/fit-in/400x400/{key}
|
|
* Legacy media-CDN URLs cannot be resized on the pictures DIT; return the original.
|
|
*/
|
|
function resolvePhotoThumbUrl(urlOrKey, { w = 400, h = 400 } = {}) {
|
|
if (urlOrKey == null || String(urlOrKey).trim() === '') return null;
|
|
const raw = String(urlOrKey).trim();
|
|
|
|
const absolute = parseAbsoluteUrl(raw);
|
|
if (absolute && !isPicturesHost(absolute.hostname)) {
|
|
return raw;
|
|
}
|
|
if (!absolute && isLegacyMediaKey(raw)) {
|
|
return mediaUrlForKey(raw) || raw;
|
|
}
|
|
|
|
const objectKey = toPicturesKey(raw);
|
|
if (!objectKey) return null;
|
|
const width = Math.max(1, Math.min(4096, Math.round(Number(w) || 400)));
|
|
const height = Math.max(1, Math.min(4096, Math.round(Number(h) || 400)));
|
|
const base = picturesCloudfrontBase();
|
|
if (!base) return null;
|
|
return `${base}/fit-in/${width}x${height}/${objectKey}`;
|
|
}
|
|
|
|
let cachedS3Client = null;
|
|
function getS3Client() {
|
|
if (cachedS3Client) return cachedS3Client;
|
|
|
|
const region = getEnv('AWS_DEFAULT_REGION', 'ap-south-1');
|
|
const accessKeyId = getEnv('AWS_ACCESS_KEY_ID');
|
|
const secretAccessKey = getEnv('AWS_SECRET_ACCESS_KEY');
|
|
|
|
cachedS3Client = new S3Client({
|
|
region,
|
|
credentials: accessKeyId && secretAccessKey ? { accessKeyId, secretAccessKey } : undefined,
|
|
});
|
|
|
|
return cachedS3Client;
|
|
}
|
|
|
|
async function putPicturesObject({ key, buffer, contentType }) {
|
|
const bucket = getEnv('AWS_EVENT_PICTURES_BUCKET', 'eventbuzz-pictures');
|
|
if (!bucket) throw new Error('AWS_EVENT_PICTURES_BUCKET is not configured');
|
|
|
|
await getS3Client().send(
|
|
new PutObjectCommand({
|
|
Bucket: bucket,
|
|
Key: key,
|
|
Body: buffer,
|
|
ContentType: contentType || 'application/octet-stream',
|
|
}),
|
|
);
|
|
|
|
return { s3Key: key, url: cloudfrontUrlForKey(key) };
|
|
}
|
|
|
|
/**
|
|
* Upload a photo buffer to the eventbuzz-pictures S3 bucket.
|
|
* Prefer `{ activity, kind }` for the standardized layout:
|
|
* {eventId}/{activity}/{kind}/{filename}
|
|
* Legacy `{ subdir }` still supported (segment-safe).
|
|
*/
|
|
async function uploadEventPhotoToS3({ buffer, contentType, eventId, filename, activity, kind, subdir }) {
|
|
let key;
|
|
if (activity && kind) {
|
|
key = buildActivityPhotoKey({ eventId, activity, kind, filename });
|
|
} else {
|
|
key = buildEventPhotoKey({ eventId, filename, subdir });
|
|
}
|
|
return putPicturesObject({ key, buffer, contentType });
|
|
}
|
|
|
|
/**
|
|
* Upload a photo-wall image (original or framed composite) to the pictures bucket.
|
|
* Keys: {eventId}/photo-wall/originals|framed/{filename}
|
|
*/
|
|
async function uploadPhotoWallPhotoToS3({ buffer, contentType, eventId, filename, framed = false }) {
|
|
const key = buildPhotoWallKey({ eventId, filename, framed });
|
|
return putPicturesObject({ key, buffer, contentType });
|
|
}
|
|
|
|
/**
|
|
* Delete a photo from eventbuzz-pictures S3 bucket by key.
|
|
*/
|
|
async function deleteEventPhotoFromS3({ s3Key }) {
|
|
const bucket = getEnv('AWS_EVENT_PICTURES_BUCKET', 'eventbuzz-pictures');
|
|
if (!bucket) return;
|
|
if (!s3Key) return;
|
|
|
|
await getS3Client().send(
|
|
new DeleteObjectCommand({
|
|
Bucket: bucket,
|
|
Key: s3Key,
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a photo buffer from eventbuzz-pictures S3 bucket by key.
|
|
*/
|
|
async function getEventPhotoBufferFromS3(s3Key) {
|
|
const bucket = getEnv('AWS_EVENT_PICTURES_BUCKET', 'eventbuzz-pictures');
|
|
if (!bucket) throw new Error('AWS_EVENT_PICTURES_BUCKET is not configured');
|
|
if (!s3Key) throw new Error('s3Key required');
|
|
|
|
const response = await getS3Client().send(
|
|
new GetObjectCommand({
|
|
Bucket: bucket,
|
|
Key: s3Key,
|
|
}),
|
|
);
|
|
const chunks = [];
|
|
for await (const chunk of response.Body) {
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
module.exports = {
|
|
PHOTO_ACTIVITIES,
|
|
PHOTO_KINDS,
|
|
uploadEventPhotoToS3,
|
|
uploadPhotoWallPhotoToS3,
|
|
deleteEventPhotoFromS3,
|
|
getEventPhotoBufferFromS3,
|
|
buildActivityPhotoKey,
|
|
buildEventPhotoKey,
|
|
buildPhotoWallKey,
|
|
cloudfrontUrlForKey,
|
|
toPicturesKey,
|
|
resolvePhotoPublicUrl,
|
|
resolvePhotoThumbUrl,
|
|
};
|