const File = require('./fileModel') const AppError = require('../../globalErrorHandler/AppError') const util = require('util') const fs = require('fs') const { uploadObject, deleteObject, listOfObjects, signedUrl, copyObject, removeObject, } = require('../../utils/upload/bucket') const deleteTempFile = util.promisify(fs.unlink) const bucketSingleUpload = async (file, bucketName) => { const fileData = { originalName: file.originalname, filename: file.filename, path: file.path, } const result = await uploadObject(fileData, bucketName) const data = { fileURL: result.fileURL, fileName: result.fileName, fileEndpoint: result.fileEndpoint, bucketName, } // Delete file from uploads folder await deleteTempFile(file.path) const fileRecord = await createFile(data) return { status: 'success', fileRecord, } } const bucketMultiUpload = async (files, bucketName) => { const results = [] for (let file of files) { const result = await bucketSingleUpload(file, bucketName) results.push(result) } return results } const bucketDeleteFile = async (fileName, bucketName) => { let raw = typeof fileName === 'object' && fileName !== null ? (fileName.fileName || fileName.url || fileName.name || '') : fileName let exportedFileName = getLastUrlSegment(String(raw || '')) if (exportedFileName) { await deleteObject(exportedFileName, bucketName).catch((err) => { console.error('Error deleting object from MinIO bucket:', err) }) await deleteFile(exportedFileName).catch((err) => { console.error('Error deleting file record from DB:', err) }) } return { fileName: exportedFileName } } const getLastUrlSegment = (url) => { if (!url || typeof url !== 'string') { return '' } if (!url.includes('/')) { return url } const cleanUrl = url.endsWith('/') ? url.slice(0, -1) : url const segments = cleanUrl.split('/') return segments[segments.length - 1] } const getAllFiles = async () => { return File.find({}, null, null) } const getFile = async (fileId) => { const file = await File.findById(fileId, null, null) if (!file) { throw new AppError('NoFileFound', 404, {}) } return file } const createFile = async (data) => { return File.create(data, null) } const updateFile = async (fileId, data) => { const file = await File.findOneAndUpdate({ _id: fileId }, data, null) if (!file) { throw AppError('NoFileFound', 404, {}) } return file } const deleteFile = async (fileName) => { await File.findOneAndDelete({ fileName }, null) return null } const bucketGetFiles = async (bucketName) => { return listOfObjects(bucketName) } const parseFileAndBucket = (input, defaultBucket = 'properties') => { if (!input) return { fileName: '', bucketName: defaultBucket } const raw = typeof input === 'object' && input !== null ? (input.fileName || input.name || input.url || '') : String(input || '') const clean = raw.split('?')[0] const match = clean.match(/(?:^|\/)(properties|documents|temp|belongings|belonging-images|belonging-documents|property-images|property-documents)\/([^/]+)$/i) if (match) { let b = match[1] if (b === 'property-images' || b === 'belonging-images') b = 'properties' if (b === 'property-documents' || b === 'belonging-documents') b = 'documents' return { bucketName: b, fileName: decodeURIComponent(match[2]) } } const parts = clean.split('/') const fileName = decodeURIComponent(parts.pop() || '') return { bucketName: defaultBucket, fileName } } const getSignedUrl = async (input, defaultBucket = 'properties') => { if (!input) { return null } const { fileName, bucketName } = parseFileAndBucket(input, defaultBucket) if (!fileName) { return null } try { return await signedUrl(bucketName, fileName) } catch (err) { console.error(`Error generating signed URL for ${bucketName}/${fileName}:`, err.message) return null } } const getMultipleSignedUrls = async (fileNameArray, defaultBucket = 'properties') => { if (!fileNameArray || !Array.isArray(fileNameArray)) { return [] } const results = await Promise.all( fileNameArray.map(async (item) => { const { fileName, bucketName } = parseFileAndBucket(item, defaultBucket) if (!fileName) return null const url = await getSignedUrl(item, bucketName) return url ? { url, fileName, bucketName } : null }) ) return results.filter(Boolean) } const moveFromTempToMain = async (files, targetBucket) => { const tempBucket = 'temp' if (!files || !Array.isArray(files) || files.length === 0) { return [] } const { minioClient } = require('../../utils/upload/bucket') const updatedUrls = await Promise.all( files.map(async (item) => { const rawStr = typeof item === 'object' && item !== null ? (item.url || item.fileName || item.name || '') : String(item || '') const fileName = getLastUrlSegment(rawStr) if (!fileName) { return null } try { // Check if object exists in temp bucket const existsInTemp = await minioClient.statObject(tempBucket, fileName).then(() => true).catch(() => false) if (existsInTemp) { // Copy object from temp to targetBucket await copyObject(targetBucket, fileName, `/${tempBucket}/${fileName}`).catch((err) => { console.error(`Failed to copy object "${fileName}" to "${targetBucket}":`, err.message) }) // Remove from temp bucket await removeObject(tempBucket, fileName).catch((err) => { console.error(`Failed to remove object "${fileName}" from "${tempBucket}":`, err.message) }) // Update File record in database if exists await File.findOneAndUpdate({ fileName }, { bucketName: targetBucket }, null).catch(() => {}) } // Construct new URL for targetBucket const protocol = process.env.AWS_USE_SSL === 'true' ? 'https://' : 'http://' const endpoint = process.env.AWS_ENDPOINT || 'localhost' const hostPort = (endpoint.includes(':') || !process.env.AWS_PORT) ? endpoint : `${endpoint}:${process.env.AWS_PORT}` const fullHost = hostPort.startsWith('http://') || hostPort.startsWith('https://') ? hostPort : `${protocol}${hostPort}` const newUrl = `${fullHost.replace(/\/$/, '')}/${targetBucket}/${fileName}` return newUrl } catch (err) { console.error(`Failed to move file "${fileName}" from ${tempBucket} to ${targetBucket}:`, err.message) if (rawStr.includes(`/${tempBucket}/`)) { return rawStr.replace(`/${tempBucket}/`, `/${targetBucket}/`) } return rawStr } }) ) return updatedUrls.filter(Boolean) } const getImage = async (bucketName, fileName) => { if (!fileName) { throw new AppError('NoFileFound', 404, {}) } // Get signed URL from the file service const signedUrl = await getSignedUrl(fileName, bucketName); if (!signedUrl) { return null; } // Fetch the image from the signed URL const response = await fetch(signedUrl); if (!response.ok) { throw new AppError('NoFileFound', 404, {}) } // Get the content type from the response headers const contentType = response.headers.get('content-type'); // Convert the response to a buffer const buffer = await response.arrayBuffer(); // Return both the buffer and content type return { buffer: Buffer.from(buffer), contentType }; }; module.exports = { moveFromTempToMain, getMultipleSignedUrls, getSignedUrl, createFile, updateFile, deleteFile, getAllFiles, getFile, bucketSingleUpload, bucketMultiUpload, bucketDeleteFile, bucketGetFiles, getImage, }