const formidable = require('formidable'); const fs = require('fs'); const config = require('../config'); const path = require('path'); const { pipeline } = require('stream/promises'); const { v4: uuid } = require('uuid'); const { format } = require('util'); const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const ensureDirectoryExistence = (filePath) => { const dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { return true; } ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); }; const UPLOAD_SESSIONS_DIR = path.join(config.uploadDir, 'upload_sessions'); const UPLOAD_SESSION_TTL_MS = 24 * 60 * 60 * 1000; const sanitizeFolder = (folder) => { const value = String(folder || '') .trim() .replace(/^\/+|\/+$/g, ''); if (!value || value.includes('..')) { return null; } return value; }; const sanitizeFilename = (filename) => { const value = path.basename(String(filename || '').trim()); if (!value || value === '.' || value === '..') { return null; } return value; }; const getSessionDir = (sessionId) => path.join(UPLOAD_SESSIONS_DIR, sessionId); const getSessionMetaPath = (sessionId) => path.join(getSessionDir(sessionId), 'meta.json'); const getSessionChunksDir = (sessionId) => path.join(getSessionDir(sessionId), 'chunks'); const getSessionChunkPath = (sessionId, chunkIndex) => path.join(getSessionChunksDir(sessionId), `${String(chunkIndex)}.part`); const readSessionMeta = (sessionId) => { const metaPath = getSessionMetaPath(sessionId); if (!fs.existsSync(metaPath)) { return null; } const raw = fs.readFileSync(metaPath, 'utf8'); return JSON.parse(raw); }; const writeSessionMeta = (sessionId, payload) => { const metaPath = getSessionMetaPath(sessionId); ensureDirectoryExistence(metaPath); fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), 'utf8'); }; const removeUploadSession = (sessionId) => { const sessionDir = getSessionDir(sessionId); if (fs.existsSync(sessionDir)) { fs.rmSync(sessionDir, { recursive: true, force: true }); } }; const cleanupExpiredUploadSessions = () => { if (!fs.existsSync(UPLOAD_SESSIONS_DIR)) { return; } const now = Date.now(); const sessionIds = fs.readdirSync(UPLOAD_SESSIONS_DIR); sessionIds.forEach((sessionId) => { try { const meta = readSessionMeta(sessionId); if (!meta) { removeUploadSession(sessionId); return; } const updatedAt = new Date( meta.updatedAt || meta.createdAt || 0, ).getTime(); if (!updatedAt || now - updatedAt > UPLOAD_SESSION_TTL_MS) { removeUploadSession(sessionId); } } catch (error) { console.error(`Failed to cleanup upload session ${sessionId}`, error); removeUploadSession(sessionId); } }); }; const streamAppendFile = async (targetPath, sourcePath) => { await new Promise((resolve, reject) => { const writeStream = fs.createWriteStream(targetPath, { flags: 'a' }); const readStream = fs.createReadStream(sourcePath); writeStream.on('error', reject); readStream.on('error', reject); writeStream.on('finish', resolve); readStream.pipe(writeStream, { end: true }); }); }; // S3 session storage helpers const S3_UPLOAD_SESSIONS_PREFIX = '_upload_sessions'; const getS3SessionMetaKey = (prefix, sessionId) => { const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, ''); const sessionPath = `${S3_UPLOAD_SESSIONS_PREFIX}/${sessionId}/meta.json`; return cleanPrefix ? `${cleanPrefix}/${sessionPath}` : sessionPath; }; const getS3SessionChunkKey = (prefix, sessionId, chunkIndex) => { const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, ''); const chunkPath = `${S3_UPLOAD_SESSIONS_PREFIX}/${sessionId}/chunks/${chunkIndex}.part`; return cleanPrefix ? `${cleanPrefix}/${chunkPath}` : chunkPath; }; const getS3SessionPrefix = (prefix, sessionId) => { const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, ''); const sessionPath = `${S3_UPLOAD_SESSIONS_PREFIX}/${sessionId}/`; return cleanPrefix ? `${cleanPrefix}/${sessionPath}` : sessionPath; }; const readS3SessionMeta = async (client, bucket, prefix, sessionId) => { try { const key = getS3SessionMetaKey(prefix, sessionId); const output = await client.send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); if (!output || !output.Body) { return null; } const bodyStr = await output.Body.transformToString(); return JSON.parse(bodyStr); } catch (error) { if (error.name === 'NoSuchKey') { return null; } throw error; } }; const writeS3SessionMeta = async ( client, bucket, prefix, sessionId, payload, ) => { const key = getS3SessionMetaKey(prefix, sessionId); await client.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: JSON.stringify(payload, null, 2), ContentType: 'application/json', }), ); }; const uploadS3Chunk = async ( client, bucket, prefix, sessionId, chunkIndex, body, ) => { const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex); await client.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, }), ); }; const downloadS3Chunk = async ( client, bucket, prefix, sessionId, chunkIndex, ) => { const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex); try { const output = await client.send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); if (!output || !output.Body) { return null; } return output.Body; } catch (error) { if (error.name === 'NoSuchKey') { return null; } throw error; } }; const s3ChunkExists = async (client, bucket, prefix, sessionId, chunkIndex) => { const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex); try { await client.send( new GetObjectCommand({ Bucket: bucket, Key: key, Range: 'bytes=0-0' }), ); return true; } catch (error) { if (error.name === 'NoSuchKey') { return false; } throw error; } }; const removeS3UploadSession = async (client, bucket, prefix, sessionId) => { const sessionPrefix = getS3SessionPrefix(prefix, sessionId); try { const listResult = await client.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: sessionPrefix, }), ); if (!listResult.Contents || listResult.Contents.length === 0) { return; } const objectsToDelete = listResult.Contents.map((obj) => ({ Key: obj.Key, })); await client.send( new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: objectsToDelete }, }), ); } catch (error) { console.error(`Failed to remove S3 upload session ${sessionId}`, error); } }; const cleanupExpiredS3UploadSessions = async () => { const provider = getFileStorageProvider(); if (provider !== 's3') { return; } try { const { client, bucket, prefix } = initS3(); const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, ''); const sessionsPrefix = cleanPrefix ? `${cleanPrefix}/${S3_UPLOAD_SESSIONS_PREFIX}/` : `${S3_UPLOAD_SESSIONS_PREFIX}/`; const listResult = await client.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: sessionsPrefix, Delimiter: '/', }), ); if (!listResult.CommonPrefixes || listResult.CommonPrefixes.length === 0) { return; } const now = Date.now(); for (const prefixObj of listResult.CommonPrefixes) { const sessionPrefix = prefixObj.Prefix; const sessionId = sessionPrefix .replace(sessionsPrefix, '') .replace(/\/$/, ''); if (!sessionId) continue; try { const meta = await readS3SessionMeta(client, bucket, prefix, sessionId); if (!meta) { await removeS3UploadSession(client, bucket, prefix, sessionId); continue; } const updatedAt = new Date( meta.updatedAt || meta.createdAt || 0, ).getTime(); if (!updatedAt || now - updatedAt > UPLOAD_SESSION_TTL_MS) { await removeS3UploadSession(client, bucket, prefix, sessionId); } } catch (error) { console.error( `Failed to cleanup S3 upload session ${sessionId}`, error, ); await removeS3UploadSession(client, bucket, prefix, sessionId); } } } catch (error) { console.error('Failed to cleanup expired S3 upload sessions', error); } }; const uploadLocal = ( folder, validations = { entity: null, folderIncludesAuthenticationUid: false, }, ) => { return (req, res) => { if (!req.currentUser) { res.sendStatus(403); return; } if (validations.entity) { res.sendStatus(403); return; } if (validations.folderIncludesAuthenticationUid) { folder = folder.replace(':userId', req.currentUser.authenticationUid); if ( !req.currentUser.authenticationUid || !folder.includes(req.currentUser.authenticationUid) ) { res.sendStatus(403); return; } } const form = new formidable.IncomingForm(); form.uploadDir = config.uploadDir; form.parse(req, function (err, fields, files) { const filename = String(fields.filename); const fileTempUrl = files.file.path; if (!filename) { fs.unlinkSync(fileTempUrl); res.sendStatus(500); return; } const privateUrl = path.join(form.uploadDir, folder, filename); ensureDirectoryExistence(privateUrl); fs.renameSync(fileTempUrl, privateUrl); res.sendStatus(200); }); form.on('error', function (err) { res.status(500).send(err); }); }; }; const downloadLocal = async (req, res) => { const privateUrl = req.query.privateUrl; if (!privateUrl) { return res.sendStatus(404); } res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); res.download(path.join(config.uploadDir, privateUrl)); }; const deleteLocal = async (privateUrl) => { try { if (!privateUrl) { return; } const filePath = path.join(config.uploadDir, privateUrl); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch (error) { console.error(`Cannot delete local file ${privateUrl}`, error); throw error; } }; const initGCloud = () => { const processFile = require('../middlewares/upload'); const { Storage } = require('@google-cloud/storage'); const hash = config.gcloud.hash; const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n'); const storage = new Storage({ projectId: process.env.GC_PROJECT_ID, credentials: { client_email: process.env.GC_CLIENT_EMAIL, private_key: privateKey, }, }); const bucket = storage.bucket(config.gcloud.bucket); return { hash, bucket, processFile }; }; const getFileStorageProvider = () => { const provider = (process.env.FILE_STORAGE_PROVIDER || '') .trim() .toLowerCase(); if (provider) { return provider; } const hasS3Credentials = Boolean( config.s3.bucket && config.s3.region && config.s3.accessKeyId && config.s3.secretAccessKey, ); if (hasS3Credentials) { return 's3'; } const hasGCloudCredentials = Boolean( process.env.GC_PROJECT_ID && process.env.GC_CLIENT_EMAIL && process.env.GC_PRIVATE_KEY && config.gcloud.bucket && config.gcloud.hash, ); if (hasGCloudCredentials) { return 'gcloud'; } return 'local'; }; const initS3 = () => { const processFile = require('../middlewares/upload'); const client = new S3Client({ region: config.s3.region, credentials: { accessKeyId: config.s3.accessKeyId, secretAccessKey: config.s3.secretAccessKey, }, }); return { client, bucket: config.s3.bucket, region: config.s3.region, prefix: config.s3.prefix, processFile, }; }; const buildStoragePath = (prefix, privateUrl) => { const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, ''); const cleanPrivateUrl = String(privateUrl || '').replace(/^\/+/, ''); if (!cleanPrefix) { return cleanPrivateUrl; } return `${cleanPrefix}/${cleanPrivateUrl}`; }; const uploadGCloud = async (folder, req, res) => { try { const { hash, bucket, processFile } = initGCloud(); await processFile(req, res); let buffer = await req.file.buffer; let filename = await req.body.filename; if (!req.file) { return res.status(400).send({ message: 'Please upload a file!' }); } let path = `${hash}/${folder}/${filename}`; let blob = bucket.file(path); console.log(path); const blobStream = blob.createWriteStream({ resumable: false, }); blobStream.on('error', (err) => { console.log('Upload error'); console.log(err.message); res.status(500).send({ message: err.message }); }); console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); blobStream.on('finish', async () => { const publicUrl = format( `https://storage.googleapis.com/${bucket.name}/${blob.name}`, ); res.status(200).send({ message: 'Uploaded the file successfully: ' + path, url: publicUrl, }); }); blobStream.end(buffer); } catch (err) { console.log(err); res.status(500).send({ message: `Could not upload the file. ${err}`, }); } }; const downloadGCloud = async (req, res) => { try { const { hash, bucket } = initGCloud(); const privateUrl = await req.query.privateUrl; const filePath = `${hash}/${privateUrl}`; const file = bucket.file(filePath); const fileExists = await file.exists(); if (fileExists[0]) { res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); const stream = file.createReadStream(); stream.pipe(res); } else { res.status(404).send({ message: 'Could not download the file.', }); } } catch (err) { res.status(404).send({ message: 'Could not download the file. ' + err, }); } }; const uploadS3 = async (folder, req, res) => { try { const { client, bucket, region, prefix, processFile } = initS3(); await processFile(req, res); if (!req.file) { return res.status(400).send({ message: 'Please upload a file!' }); } const filename = req.body.filename; if (!filename) { return res.status(400).send({ message: 'Missing filename' }); } const privateUrl = `${folder}/${filename}`; const key = buildStoragePath(prefix, privateUrl); await client.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: req.file.buffer, ContentType: req.file.mimetype, }), ); return res.status(200).send({ message: `Uploaded the file successfully: ${privateUrl}`, url: `https://${bucket}.s3.${region}.amazonaws.com/${key}`, }); } catch (error) { console.error('S3 upload error', error); return res.status(500).send({ message: `Could not upload the file. ${error.message || error}`, }); } }; const downloadS3 = async (req, res) => { try { const privateUrl = req.query.privateUrl; if (!privateUrl) { return res.status(404).send({ message: 'Missing privateUrl' }); } const { client, bucket, prefix } = initS3(); const key = buildStoragePath(prefix, privateUrl); const output = await client.send( new GetObjectCommand({ Bucket: bucket, Key: key, }), ); if (!output || !output.Body) { return res.status(404).send({ message: 'Could not download the file.', }); } if (output.ContentType) { res.setHeader('Content-Type', output.ContentType); } res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); if (typeof output.Body.pipe === 'function') { output.Body.pipe(res); return; } if (typeof output.Body.transformToByteArray === 'function') { const bytes = await output.Body.transformToByteArray(); res.send(Buffer.from(bytes)); return; } return res.send(output.Body); } catch (error) { const statusCode = error && error.name === 'NoSuchKey' ? 404 : 500; return res.status(statusCode).send({ message: `Could not download the file. ${error.message || error}`, }); } }; const deleteGCloud = async (privateUrl) => { try { const { hash, bucket } = initGCloud(); const filePath = `${hash}/${privateUrl}`; const file = bucket.file(filePath); const fileExists = await file.exists(); if (fileExists[0]) { file.delete(); } } catch (err) { console.log(`Cannot find the file ${privateUrl}`); } }; const deleteS3 = async (privateUrl) => { try { if (!privateUrl) { return; } const { client, bucket, prefix } = initS3(); const key = buildStoragePath(prefix, privateUrl); await client.send( new DeleteObjectCommand({ Bucket: bucket, Key: key, }), ); } catch (error) { console.error(`Cannot delete S3 file ${privateUrl}`, error); throw error; } }; const uploadStreamToGCloud = async (privateUrl, sourcePath) => { const { hash, bucket } = initGCloud(); const fullPath = `${hash}/${privateUrl}`; const blob = bucket.file(fullPath); await pipeline( fs.createReadStream(sourcePath), blob.createWriteStream({ resumable: false }), ); return format(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); }; const initUploadSession = async (req, res) => { try { if (!req.currentUser || !req.currentUser.id) { return res.sendStatus(403); } const provider = getFileStorageProvider(); // Cleanup expired sessions (async for S3, sync for local) if (provider === 's3') { cleanupExpiredS3UploadSessions().catch((err) => console.error('S3 session cleanup failed', err), ); } else { cleanupExpiredUploadSessions(); } const folder = sanitizeFolder(req.body?.folder); const filename = sanitizeFilename(req.body?.filename); const totalChunks = Number(req.body?.totalChunks); const size = Number(req.body?.size); const contentType = String(req.body?.contentType || '').trim(); if (!folder || !filename) { return res.status(400).send({ message: 'Invalid folder or filename' }); } if (!Number.isInteger(totalChunks) || totalChunks <= 0) { return res.status(400).send({ message: 'Invalid totalChunks' }); } if (!Number.isFinite(size) || size < 0) { return res.status(400).send({ message: 'Invalid file size' }); } const sessionId = uuid(); const now = new Date().toISOString(); const session = { id: sessionId, userId: req.currentUser.id, folder, filename, totalChunks, size, contentType, uploadedChunks: [], status: 'active', createdAt: now, updatedAt: now, }; if (provider === 's3') { const { client, bucket, prefix } = initS3(); await writeS3SessionMeta(client, bucket, prefix, sessionId, session); } else { const chunksDir = getSessionChunksDir(sessionId); fs.mkdirSync(chunksDir, { recursive: true }); writeSessionMeta(sessionId, session); } return res.status(200).send({ sessionId, uploadedChunks: [], totalChunks, }); } catch (error) { console.error('Failed to initialize upload session', error); return res .status(500) .send({ message: 'Failed to initialize upload session' }); } }; const getUploadSession = async (req, res) => { try { if (!req.currentUser || !req.currentUser.id) { return res.sendStatus(403); } const sessionId = String(req.params.sessionId || ''); const provider = getFileStorageProvider(); let session; if (provider === 's3') { const { client, bucket, prefix } = initS3(); session = await readS3SessionMeta(client, bucket, prefix, sessionId); } else { session = readSessionMeta(sessionId); } if (!session) { return res.status(404).send({ message: 'Upload session not found' }); } if (session.userId !== req.currentUser.id) { return res.sendStatus(403); } return res.status(200).send({ sessionId: session.id, totalChunks: session.totalChunks, uploadedChunks: session.uploadedChunks || [], status: session.status, }); } catch (error) { console.error('Failed to get upload session', error); return res.status(500).send({ message: 'Failed to get upload session' }); } }; const uploadChunk = async (req, res) => { try { if (!req.currentUser || !req.currentUser.id) { return res.sendStatus(403); } const sessionId = String(req.params.sessionId || ''); const chunkIndex = Number(req.params.chunkIndex); if (!Number.isInteger(chunkIndex) || chunkIndex < 0) { return res.status(400).send({ message: 'Invalid chunk index' }); } const provider = getFileStorageProvider(); let session; let s3Client, s3Bucket, s3Prefix; if (provider === 's3') { const s3 = initS3(); s3Client = s3.client; s3Bucket = s3.bucket; s3Prefix = s3.prefix; session = await readS3SessionMeta( s3Client, s3Bucket, s3Prefix, sessionId, ); } else { session = readSessionMeta(sessionId); } if (!session) { return res.status(404).send({ message: 'Upload session not found' }); } if (session.userId !== req.currentUser.id) { return res.sendStatus(403); } if (chunkIndex >= Number(session.totalChunks)) { return res.status(400).send({ message: 'Chunk index is out of range' }); } if (provider === 's3') { // Collect chunk data from request stream const chunks = []; for await (const chunk of req) { chunks.push(chunk); } const chunkBuffer = Buffer.concat(chunks); // Upload chunk directly to S3 await uploadS3Chunk( s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex, chunkBuffer, ); } else { // Local storage - write to temp file then rename const chunkDir = getSessionChunksDir(sessionId); fs.mkdirSync(chunkDir, { recursive: true }); const chunkPath = getSessionChunkPath(sessionId, chunkIndex); const tempChunkPath = `${chunkPath}.tmp`; if (fs.existsSync(tempChunkPath)) { fs.unlinkSync(tempChunkPath); } await pipeline(req, fs.createWriteStream(tempChunkPath)); fs.renameSync(tempChunkPath, chunkPath); } const uploadedChunks = Array.from( new Set([...(session.uploadedChunks || []), chunkIndex]), ).sort((a, b) => a - b); session.uploadedChunks = uploadedChunks; session.updatedAt = new Date().toISOString(); if (provider === 's3') { await writeS3SessionMeta( s3Client, s3Bucket, s3Prefix, sessionId, session, ); } else { writeSessionMeta(sessionId, session); } return res.status(200).send({ sessionId, chunkIndex, uploadedChunks: uploadedChunks.length, totalChunks: session.totalChunks, }); } catch (error) { console.error('Failed to upload chunk', error); return res.status(500).send({ message: 'Failed to upload chunk' }); } }; const finalizeUploadSession = async (req, res) => { try { if (!req.currentUser || !req.currentUser.id) { return res.sendStatus(403); } const sessionId = String(req.params.sessionId || ''); const provider = getFileStorageProvider(); let session; let s3Client, s3Bucket, s3Prefix, s3Region; if (provider === 's3') { const s3 = initS3(); s3Client = s3.client; s3Bucket = s3.bucket; s3Prefix = s3.prefix; s3Region = s3.region; session = await readS3SessionMeta( s3Client, s3Bucket, s3Prefix, sessionId, ); } else { session = readSessionMeta(sessionId); } if (!session) { return res.status(404).send({ message: 'Upload session not found' }); } if (session.userId !== req.currentUser.id) { return res.sendStatus(403); } const totalChunks = Number(session.totalChunks); // Verify all chunks exist if (provider === 's3') { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { const exists = await s3ChunkExists( s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex, ); if (!exists) { return res.status(400).send({ message: `Missing chunk ${chunkIndex}`, missingChunk: chunkIndex, }); } } } else { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { const chunkPath = getSessionChunkPath(sessionId, chunkIndex); if (!fs.existsSync(chunkPath)) { return res.status(400).send({ message: `Missing chunk ${chunkIndex}`, missingChunk: chunkIndex, }); } } } // Create temp directory for assembly const tempDir = path.join(config.uploadDir, '_temp_assembly'); fs.mkdirSync(tempDir, { recursive: true }); const assembledPath = path.join(tempDir, `${sessionId}.bin`); if (fs.existsSync(assembledPath)) { fs.unlinkSync(assembledPath); } // Download and assemble chunks if (provider === 's3') { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { const chunkStream = await downloadS3Chunk( s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex, ); if (!chunkStream) { // Cleanup and return error if (fs.existsSync(assembledPath)) fs.unlinkSync(assembledPath); return res.status(400).send({ message: `Failed to download chunk ${chunkIndex}`, missingChunk: chunkIndex, }); } // Write chunk to assembled file await new Promise((resolve, reject) => { const writeStream = fs.createWriteStream(assembledPath, { flags: 'a', }); writeStream.on('error', reject); writeStream.on('finish', resolve); if (typeof chunkStream.pipe === 'function') { chunkStream.on('error', reject); chunkStream.pipe(writeStream, { end: true }); } else if (typeof chunkStream.transformToByteArray === 'function') { chunkStream .transformToByteArray() .then((bytes) => { writeStream.write(Buffer.from(bytes)); writeStream.end(); }) .catch(reject); } else { writeStream.write(chunkStream); writeStream.end(); } }); } } else { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { const chunkPath = getSessionChunkPath(sessionId, chunkIndex); await streamAppendFile(assembledPath, chunkPath); } } const assembledStats = fs.statSync(assembledPath); if ( Number.isFinite(Number(session.size)) && Number(session.size) !== assembledStats.size ) { // Cleanup if (fs.existsSync(assembledPath)) fs.unlinkSync(assembledPath); return res.status(400).send({ message: 'Assembled file size mismatch', }); } const privateUrl = `${session.folder}/${session.filename}`; let publicUrl = ''; if (provider === 's3') { // Upload assembled file to final S3 location const key = buildStoragePath(s3Prefix, privateUrl); await s3Client.send( new PutObjectCommand({ Bucket: s3Bucket, Key: key, Body: fs.createReadStream(assembledPath), ContentType: session.contentType || undefined, }), ); publicUrl = `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${key}`; // Cleanup S3 session await removeS3UploadSession(s3Client, s3Bucket, s3Prefix, sessionId); } else if (provider === 'gcloud') { publicUrl = await uploadStreamToGCloud(privateUrl, assembledPath); removeUploadSession(sessionId); } else { const destinationPath = path.join(config.uploadDir, privateUrl); ensureDirectoryExistence(destinationPath); fs.renameSync(assembledPath, destinationPath); publicUrl = `/file/download?privateUrl=${encodeURIComponent(privateUrl)}`; removeUploadSession(sessionId); } // Cleanup temp assembled file (except for local where we renamed it) if (provider !== 'local' && fs.existsSync(assembledPath)) { fs.unlinkSync(assembledPath); } return res.status(200).send({ message: `Uploaded the file successfully: ${privateUrl}`, privateUrl, url: publicUrl, }); } catch (error) { console.error('Failed to finalize upload session', error); return res .status(500) .send({ message: 'Failed to finalize upload session' }); } }; const uploadFile = async (folder, req, res) => { const provider = getFileStorageProvider(); if (provider === 's3') { return uploadS3(folder, req, res); } if (provider === 'gcloud') { return uploadGCloud(folder, req, res); } return uploadLocal(folder, { entity: null, folderIncludesAuthenticationUid: false, })(req, res); }; const downloadFile = async (req, res) => { const provider = getFileStorageProvider(); if (provider === 's3') { return downloadS3(req, res); } if (provider === 'gcloud') { return downloadGCloud(req, res); } return downloadLocal(req, res); }; const deleteFile = async (privateUrl) => { const provider = getFileStorageProvider(); if (provider === 's3') { return deleteS3(privateUrl); } if (provider === 'gcloud') { return deleteGCloud(privateUrl); } return deleteLocal(privateUrl); }; const PRESIGN_EXPIRY_SECONDS = 3600; // 1 hour /** * Generate presigned GET URLs for multiple assets. * For S3: returns direct S3 signed URLs. * For other providers: returns backend proxy URLs. * * @param {string[]} urls - Array of storage_key paths * @returns {Promise>} Map of original path to presigned/proxy URL */ const generatePresignedUrls = async (urls) => { const provider = getFileStorageProvider(); if (provider !== 's3') { // For non-S3 providers, return backend proxy URLs return urls.reduce((acc, url) => { acc[url] = `/api/file/download?privateUrl=${encodeURIComponent(url)}`; return acc; }, {}); } const { client, bucket, prefix } = initS3(); const presignedUrls = {}; await Promise.all( urls.map(async (url) => { const key = buildStoragePath(prefix, url); const command = new GetObjectCommand({ Bucket: bucket, Key: key }); presignedUrls[url] = await getSignedUrl(client, command, { expiresIn: PRESIGN_EXPIRY_SECONDS, }); }), ); return presignedUrls; }; module.exports = { initUploadSession, getUploadSession, uploadChunk, finalizeUploadSession, initGCloud, initS3, getFileStorageProvider, uploadFile, downloadFile, deleteFile, uploadLocal, downloadLocal, deleteLocal, deleteGCloud, deleteS3, uploadGCloud, downloadGCloud, uploadS3, downloadS3, generatePresignedUrls, };