const { S3Client, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, GetObjectCommand, } = require("@aws-sdk/client-s3"); const config = { bucket: process.env.BUCKET_NAME, region: process.env.BUCKET_REGION || "us-west-2", endpoint: process.env.PARSPACK_S3_ENDPOINT, accessKeyId: process.env.BUCKET_ACCESS_KEY, secretAccessKey: process.env.BUCKET_SECRET_KEY, publicUrl: process.env.BUCKET_PUBLIC_URL || process.env.PARSPACK_S3_ENDPOINT, }; let s3Client = null; const isConfigured = () => Boolean( config.bucket && config.endpoint && config.accessKeyId && config.secretAccessKey ); const getClient = () => { if (!isConfigured()) { throw new Error("S3 storage is not configured"); } if (!s3Client) { s3Client = new S3Client({ region: config.region, endpoint: config.endpoint, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, }, forcePathStyle: true, }); } return s3Client; }; const normalizeKey = (key) => key.replace(/^\/+/, ""); const getPublicUrl = (key) => { const normalizedKey = normalizeKey(key); const baseUrl = (config.publicUrl || config.endpoint).replace(/\/+$/, ""); return `${baseUrl}/${normalizedKey}`; }; const uploadBuffer = async ( key, buffer, contentType = "application/octet-stream" ) => { const normalizedKey = normalizeKey(key); await getClient().send( new PutObjectCommand({ Bucket: config.bucket, Key: normalizedKey, Body: buffer, ContentType: contentType, ACL: "public-read", }) ); return { key: normalizedKey, url: getPublicUrl(normalizedKey), }; }; const deleteObject = async (key) => { const normalizedKey = normalizeKey(key); await getClient().send( new DeleteObjectCommand({ Bucket: config.bucket, Key: normalizedKey, }) ); }; const listObjects = async (prefix = "") => { const keys = []; let continuationToken; do { const response = await getClient().send( new ListObjectsV2Command({ Bucket: config.bucket, Prefix: normalizeKey(prefix), ContinuationToken: continuationToken, }) ); for (const item of response.Contents || []) { if (item.Key) keys.push(item.Key); } continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; } while (continuationToken); return keys; }; const getObjectBuffer = async (key) => { const response = await getClient().send( new GetObjectCommand({ Bucket: config.bucket, Key: normalizeKey(key), }) ); return Buffer.from(await response.Body.transformToByteArray()); }; const deleteObjectsByPrefix = async (prefix) => { const keys = await listObjects(prefix); await Promise.all(keys.map((key) => deleteObject(key))); }; module.exports = { isConfigured, getPublicUrl, uploadBuffer, deleteObject, listObjects, getObjectBuffer, deleteObjectsByPrefix, };