const crypto = require("crypto"); const path = require("path"); const { PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); const s3 = require("../../config/s3.config"); const bucket = () => process.env.AWS_S3_BUCKET_NAME; const extensionFor = (mime) => ({ "image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "application/pdf": ".pdf", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx" }[mime]); const sanitizeFilename = (name = "file") => path.basename(name).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 180); const createObjectKey = ({ ownerId = "system", mimeType, purpose = "upload", now = new Date(), id = crypto.randomUUID() }) => { const ext = extensionFor(mimeType); if (!ext) throw new Error("Unsupported file type"); const root = purpose === "document" ? "documents" : "uploads"; const owner = String(ownerId).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80); return `${root}/${owner}/${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}/${id}${ext}`; }; const uploadBuffer = async ({ buffer, objectKey, mimeType, checksum }) => { await s3.send(new PutObjectCommand({ Bucket: bucket(), Key: objectKey, Body: buffer, ContentType: mimeType, ChecksumSHA256: checksum ? Buffer.from(checksum, "hex").toString("base64") : undefined })); return objectKey; }; const deleteObject = (objectKey) => s3.send(new DeleteObjectCommand({ Bucket: bucket(), Key: objectKey })); const objectExists = async (objectKey) => { try { await s3.send(new HeadObjectCommand({ Bucket: bucket(), Key: objectKey })); return true; } catch (error) { if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) return false; throw error; } }; const createSignedDownloadUrl = (objectKey, expiresIn = Number(process.env.S3_SIGNED_URL_TTL_SECONDS || 900)) => getSignedUrl(s3, new GetObjectCommand({ Bucket: bucket(), Key: objectKey }), { expiresIn }); module.exports = { uploadBuffer, deleteObject, objectExists, createSignedDownloadUrl, createObjectKey, extensionFor, sanitizeFilename };