deploy-chabokan / deploy (push) Canceled after 0s
moveFromTempToMain was calling statObject/copy/remove against MinIO for every file already attached to a property on every save, even when the file was already in the target bucket and never touched temp. This added a blocking network round-trip per image/document on every property save. Now files already pointing at the target bucket are returned immediately without hitting MinIO.
278 lines
8.6 KiB
JavaScript
278 lines
8.6 KiB
JavaScript
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
|
|
}
|
|
|
|
// Already pointing at the target bucket (e.g. re-submitted unchanged on every
|
|
// property save) - skip the MinIO round-trips entirely, nothing to move.
|
|
if (rawStr.includes(`/${targetBucket}/`) && !rawStr.includes(`/${tempBucket}/`)) {
|
|
return rawStr.split('?')[0]
|
|
}
|
|
|
|
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,
|
|
}
|