From 982b9e759f983ff4526f9fe133f38e92e369e284 Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Wed, 19 Aug 2026 06:30:00 +0330 Subject: [PATCH] fix(s3): generate and use seaweedfs signed urls for properties and documents --- components/file/fileService.js | 53 +++++++++++++++++++++----- components/property/belongingModel.js | 4 +- components/property/propertyModel.js | 4 +- components/property/propertyService.js | 12 ++++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/components/file/fileService.js b/components/file/fileService.js index 3fd4bb8..a1faa56 100644 --- a/components/file/fileService.js +++ b/components/file/fileService.js @@ -118,19 +118,54 @@ const bucketGetFiles = async (bucketName) => { return listOfObjects(bucketName) } -const getSignedUrl = async (fileName, bucketName) => { - if (!fileName) { - return +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]) } } - return signedUrl(bucketName, fileName) + const parts = clean.split('/') + const fileName = decodeURIComponent(parts.pop() || '') + return { bucketName: defaultBucket, fileName } } -const getMultipleSignedUrls = async (fileNameArray, bucketName) => { - const urls = [] - for (let fileName of fileNameArray) { - urls.push({ url: await getSignedUrl(fileName, bucketName), fileName: fileName}) +const getSignedUrl = async (input, defaultBucket = 'properties') => { + if (!input) { + return null } - return urls + 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) => { diff --git a/components/property/belongingModel.js b/components/property/belongingModel.js index f8fd5d6..c7b5f4b 100644 --- a/components/property/belongingModel.js +++ b/components/property/belongingModel.js @@ -68,11 +68,11 @@ const belongingSchema = new mongoose.Schema( belongingSchema.plugin(AutoIncrement, { inc_field: 'belongingCode' }) belongingSchema.methods.getDocumentsSignedUrls = function () { - return getMultipleSignedUrls(this.documentUrls, 'belonging-documents') + return getMultipleSignedUrls(this.documentUrls, 'documents') } belongingSchema.methods.getMainImagedSignedUrl = function () { - return getMultipleSignedUrls(this.mainImageUrls, 'belonging-images') + return getMultipleSignedUrls(this.mainImageUrls, 'properties') } belongingSchema.index({ location: '2dsphere' }) // Enable geospatial queries diff --git a/components/property/propertyModel.js b/components/property/propertyModel.js index 72b59cb..1593196 100644 --- a/components/property/propertyModel.js +++ b/components/property/propertyModel.js @@ -116,11 +116,11 @@ const propertySchema = new mongoose.Schema( propertySchema.plugin(AutoIncrement, { inc_field: 'propertyCode' }) propertySchema.methods.getDocumentsSignedUrls = function () { - return getMultipleSignedUrls(this.documentUrls, 'property-documents') + return getMultipleSignedUrls(this.documentUrls, 'documents') } propertySchema.methods.getMainImagedSignedUrl = function () { - return getMultipleSignedUrls(this.mainImageUrls, 'property-images') + return getMultipleSignedUrls(this.mainImageUrls, 'properties') } propertySchema.index({ location: '2dsphere' }) diff --git a/components/property/propertyService.js b/components/property/propertyService.js index 7be711c..2a7207b 100644 --- a/components/property/propertyService.js +++ b/components/property/propertyService.js @@ -4,7 +4,7 @@ const Building = require('./buildingModel') const AppError = require('../../globalErrorHandler/AppError') const { client } = require('../../setup/typesense') const TsApiFeatures = require('../../utils/TsApiFeatures') -const { moveFromTempToMain } = require('../file/fileService') +const { moveFromTempToMain, getSignedUrl } = require('../file/fileService') const getAllProperties = async () => { return Property.find({}, null, null).populate('category').populate('status') @@ -153,8 +153,12 @@ const searchPropertiesMongo = async (inputQuery = {}) => { .skip((page - 1) * per_page) .limit(per_page) - const items = properties.map((prop) => { + const items = await Promise.all(properties.map(async (prop) => { const combinedTags = Array.isArray(prop.tags) ? prop.tags.join(' ') : (prop.tags || '') + let signedThumb = null + if (prop.mainImageUrls && prop.mainImageUrls.length > 0) { + signedThumb = await getSignedUrl(prop.mainImageUrls[0], 'properties').catch(() => null) + } return { _id: prop._id.toString(), id: prop.propertyCode ? prop.propertyCode.toString() : prop._id.toString(), @@ -164,7 +168,7 @@ const searchPropertiesMongo = async (inputQuery = {}) => { nativeCategory: prop.category?.nativeName || '', categoryObj: prop.category, status: prop.status, - imageUrl: prop.mainImageUrls?.[0] || 'https://cloud.mbmanager.ir/images/item-no-image.png', + imageUrl: signedThumb || 'https://cloud.mbmanager.ir/images/item-no-image.png', mainImageUrls: prop.mainImageUrls || [], documents: prop.documentUrls || [], documentUrls: prop.documentUrls || [], @@ -183,7 +187,7 @@ const searchPropertiesMongo = async (inputQuery = {}) => { isActive: prop.isActive !== false, features: prop.features || [], } - }) + })) const pagesCount = Math.ceil(totalCount / per_page) || 1