diff --git a/pages/_portal/shafaf/index.vue b/pages/_portal/shafaf/index.vue index 0acf506..b7b8386 100644 --- a/pages/_portal/shafaf/index.vue +++ b/pages/_portal/shafaf/index.vue @@ -36,7 +36,7 @@ v-for="(item, i) in items" :key="i" class="item"> - شفافیت + شفافیت
diff --git a/server/controllers/categoryController.js b/server/controllers/categoryController.js index c934ec8..e3f56f6 100644 --- a/server/controllers/categoryController.js +++ b/server/controllers/categoryController.js @@ -8,13 +8,19 @@ const { checkValidations, } = require("../plugins/controllersHelperFunctions"); const News = require("../models/News"); -const sharp = require("sharp"); -const fs = require("fs"); const { extname } = require("path"); +const { + deleteCategoryImage, + deleteShafafiatIcon, + uploadProcessedImage, + uploadShafafiatIcon, + uploadTermsFile, + deleteTermsFile, + getStoredImageName, +} = require("../plugins/categoryImageStorage"); // variables const titleFilterRegex = new RegExp("/", "g"); -const noCover = "/img/no-cover.jpg"; const coverNewsWidth = 400; const coverNewsHeight = 400; const coverQuality = 100; @@ -411,34 +417,13 @@ module.exports.addCategory = [ image.mimetype.split("/")[1] }`; try { - const target = sharp(image.data); - await target - .resize(coverNewsWidth, coverNewsHeight, { fit: "cover" }) - .jpeg({ quality: coverQuality }) - .toFile(`./static/uploads/images/category/${imageName}`); - - /** - * if exist old pic delete it from file - */ - // if (targetNews.cover !== noCover && !targetNews.imported) { - // let oldPic = `./static${targetNews.cover}`; - // - // if (await fs.existsSync(oldPic)) { - // fs.unlink(oldPic, (err) => { - // if (err) return res500(res, err?.message); - // }); - // } - // } - - // if (targetNews.thumbCover !== noCover && !targetNews.imported) { - // let oldPic = `./static${targetNews.thumbCover}`; - // - // if (await fs.existsSync(oldPic)) { - // fs.unlink(oldPic, (err) => { - // if (err) return res500(res, err?.message); - // }); - // } - // } + await uploadProcessedImage( + image.data, + imageName, + coverNewsWidth, + coverNewsHeight, + coverQuality + ); create.managerCover = imageName; await create.save(); @@ -461,33 +446,12 @@ module.exports.addCategory = [ const ext = extname(image.name); const imageName = `shafafiat_icon_${Date.now()}${ext}`; try { - await image.mv( - `${process.cwd()}/static/uploads/images/category/shafafiat/${imageName}` + await uploadShafafiatIcon( + image.data, + imageName, + image.mimetype ); - /** - * if exist old pic delete it from file - */ - // if (targetNews.cover !== noCover && !targetNews.imported) { - // let oldPic = `./static${targetNews.cover}`; - // - // if (await fs.existsSync(oldPic)) { - // fs.unlink(oldPic, (err) => { - // if (err) return res500(res, err?.message); - // }); - // } - // } - - // if (targetNews.thumbCover !== noCover && !targetNews.imported) { - // let oldPic = `./static${targetNews.thumbCover}`; - // - // if (await fs.existsSync(oldPic)) { - // fs.unlink(oldPic, (err) => { - // if (err) return res500(res, err?.message); - // }); - // } - // } - create.icon = imageName; await create.save(); } catch (e) { @@ -812,24 +776,15 @@ module.exports.updateCategory = [ image.mimetype.split("/")[1] }`; try { - const target = sharp(image.data); - await target - .resize(coverNewsWidth, coverNewsHeight, { fit: "cover" }) - .jpeg({ quality: coverQuality }) - .toFile(`./static/uploads/images/category/${imageName}`); + await deleteCategoryImage(targetCategory, "managerCover"); - /** - * if exist old pic delete it from file - */ - if (targetCategory.managerCover !== noCover) { - let oldPic = `./static${targetCategory.managerCover}`; - - if (await fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + coverNewsWidth, + coverNewsHeight, + coverQuality + ); targetCategory.managerCover = imageName; } catch (e) { @@ -839,7 +794,6 @@ module.exports.updateCategory = [ if (req.files?.icon) { const image = req.files.icon; - console.log(image); if (!_sr.supportedImageFormats.includes(image.mimetype.split("/")[1])) { return res.status(422).json({ validation: { @@ -852,30 +806,17 @@ module.exports.updateCategory = [ const ext = extname(image.name); const imageName = `shafafiat_icon_${Date.now()}${ext}`; try { - await image.mv( - `${process.cwd()}/static/uploads/images/category/shafafiat/${imageName}` + await deleteShafafiatIcon( + getStoredImageName(targetCategory, "icon") ); - // const target = sharp(image.data) - // await target - // .resize(coverNewsWidth, coverNewsHeight, {fit: 'cover'}) - // .jpeg({quality: coverQuality}) - // .toFile(`./static/uploads/images/category/shafafiat/${imageName}`); - /** - * if exist old pic delete it from file - */ - if (targetCategory.icon !== noCover) { - let oldPic = `./static${targetCategory.icon}`; - - if (await fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadShafafiatIcon( + image.data, + imageName, + image.mimetype + ); targetCategory.icon = imageName; - console.log(targetCategory); } catch (e) { return res500(res, e?.message); } @@ -1223,52 +1164,59 @@ module.exports.getParent = [ module.exports.addTermsFileToCategory = [ [body("termsTitle").notEmpty().withMessage(_sr["fa"].required.title)], checkValidations(validationResult), - (req, res) => { - if (!req.files?.file) - return res - .status(422) - .json({ validation: { file: { msg: _sr["fa"].required.file } } }); - if (req.files?.file.mimetype.split("/")[1] !== "pdf") - return res - .status(422) - .json({ validation: { file: { msg: "فقط فرمت PDF قابل قبول است" } } }); + async (req, res) => { + try { + if (!req.files?.file) + return res + .status(422) + .json({ validation: { file: { msg: _sr["fa"].required.file } } }); + if (req.files?.file.mimetype.split("/")[1] !== "pdf") + return res + .status(422) + .json({ validation: { file: { msg: "فقط فرمت PDF قابل قبول است" } } }); - const data = { title: req.body.termsTitle }; - const file = req.files.file; - const fileName = - req.body.termsTitle.replace(titleFilterRegex, "-") + - "_" + - Date.now() + - ".pdf"; - file.mv(`./static/uploads/files/terms/${fileName}`); - data.file = fileName; + const category = await Category.findById(req.params.categoryId); + if (!category) return res404(res, "category not found"); - Category.findById(req.params.categoryId, (err, category) => { - if (err || !category) return res404(res, err); - category.terms.push(data); - category.save((err) => { - if (err) return res500(res, err); - else return res.json({ message: _sr["fa"].response.success_save }); + const fileName = + req.body.termsTitle.replace(titleFilterRegex, "-") + + "_" + + Date.now() + + ".pdf"; + + await uploadTermsFile(req.files.file.data, fileName); + + category.terms.push({ + title: req.body.termsTitle, + file: fileName, }); - }); + await category.save(); + + return res.json({ message: _sr["fa"].response.success_save }); + } catch (err) { + return res500(res, err); + } }, ]; module.exports.removeTermsFileFromCategory = [ - (req, res) => { - Category.findOne({ "terms._id": req.params.fileId }, (err, category) => { - if (err || !category) return res404(res, err); - const file = category.terms.id(req.params.fileId); - if (!file) return res404(res, "not found"); - file.remove(); - fs.unlink(`./static/${file.file}`, (err) => { - if (err) console.log(err); - }); - category.save((err) => { - if (err) return res500(res, err); - else return res.json({ message: _sr["fa"].response.success_remove }); - }); - }); + async (req, res) => { + try { + const category = await Category.findOne({ "terms._id": req.params.fileId }); + if (!category) return res404(res, "not found"); + + const termsFile = category.terms.id(req.params.fileId); + if (!termsFile) return res404(res, "not found"); + + const fileName = getStoredImageName(termsFile, "file"); + termsFile.remove(); + await deleteTermsFile(fileName); + await category.save(); + + return res.json({ message: _sr["fa"].response.success_remove }); + } catch (err) { + return res500(res, err); + } }, ]; diff --git a/server/controllers/contentController.js b/server/controllers/contentController.js index 1d38889..c421fd7 100644 --- a/server/controllers/contentController.js +++ b/server/controllers/contentController.js @@ -1,9 +1,11 @@ const {body, validationResult} = require('express-validator'); -const sharp = require('sharp'); const {_sr} = require('../plugins/serverResponses'); const {res404, res500, hasWhiteSpaces, isLatinCharactersWithSymbol, generateRandomDigits} = require('../plugins/controllersHelperFunctions'); const Content = require('../models/Content'); -const fs = require('fs'); +const { + deleteContentImages, + uploadProcessedImage, +} = require('../plugins/contentImageStorage'); ////// variables @@ -85,26 +87,23 @@ module.exports.addContent = [ const imageName = `manager_${Date.now()}_${create._id}.${image.mimetype.split('/')[1]}`; const thumbName = 'thumb_' + imageName try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/content/${imageName}`) - await target - .resize(profileThumbWidth, profileThumbHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileThumbQuality - }) - .toFile(`./static/uploads/images/content/${thumbName}`) + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); + await uploadProcessedImage( + image.data, + thumbName, + profileThumbWidth, + profileThumbHeight, + profileThumbQuality + ); create.cover = imageName; - create.thumb = imageName; + create.thumb = thumbName; await create.save(); } catch (e) { @@ -180,42 +179,22 @@ module.exports.updateContent = [ const imageName = `manager_${Date.now()}_${targetContent._id}.${image.mimetype.split('/')[1]}`; const thumbName = 'thumb_' + imageName try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/content/${imageName}`) - await target - .resize(profileThumbWidth, profileThumbHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileThumbQuality - }) - .toFile(`./static/uploads/images/content/${thumbName}`) + await deleteContentImages(targetContent); - /** - * if exist old pic delete it from file - */ - if (targetContent.cover) { - const oldPic = `./static${targetContent.cover}` - const oldThumb = `./static${targetContent.thumb}` - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - if (fs.existsSync(oldThumb)) { - fs.unlink(oldThumb, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); + await uploadProcessedImage( + image.data, + thumbName, + profileThumbWidth, + profileThumbHeight, + profileThumbQuality + ); targetContent.cover = imageName; targetContent.thumb = thumbName; diff --git a/server/controllers/dataBackupController.js b/server/controllers/dataBackupController.js index acc791b..7e4c6b1 100644 --- a/server/controllers/dataBackupController.js +++ b/server/controllers/dataBackupController.js @@ -1,9 +1,15 @@ const fs = require('fs') +const {PassThrough} = require('stream') const archiver = require('archiver') const moment = require('moment') const jwt = require('jsonwebtoken') const {secretKey} = require('../authentication') const {_sr} = require('../plugins/serverResponses') +const { + appendS3UploadsToArchive, + uploadBackupArchive, + deleteOldBackups, +} = require('../plugins/backupStorage') // models const Category = require('../models/Category') const Comment = require('../models/Comment') @@ -21,6 +27,8 @@ const Slider = require('../models/Slider') const User = require('../models/User') const YearBanner = require('../models/YearBanner') +const hostFullError = '

فضای هاست پر میباشد.لطفا نسبت به پاک کردن بکاپ های قبلی یا افزایش فضای هاست اقدام فرمایید.

' + module.exports.createBackUp = [ // check user token and permission to backup async (req, res, next) => { @@ -85,86 +93,72 @@ module.exports.createBackUp = [ const [categories, comments, contactus, contents, galleries, homepage, links, meetings, news, newsFiles, services, sliders, users, yearBanner, occasion] = await Promise.all(dataBaseQueries) - if (await !fs.existsSync('./backup/database')) await fs.mkdirSync('./backup/database') - - await Promise.all([ - fs.writeFileSync(`./backup/database/categories_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(categories)), - fs.writeFileSync(`./backup/database/comments_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(comments)), - fs.writeFileSync(`./backup/database/contactus_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(contactus)), - fs.writeFileSync(`./backup/database/contents_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(contents)), - fs.writeFileSync(`./backup/database/galleries_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(galleries)), - fs.writeFileSync(`./backup/database/homepage_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(homepage)), - fs.writeFileSync(`./backup/database/links_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(links)), - fs.writeFileSync(`./backup/database/meetings_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(meetings)), - fs.writeFileSync(`./backup/database/news_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(news)), - fs.writeFileSync(`./backup/database/newsFiles_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(newsFiles)), - fs.writeFileSync(`./backup/database/services_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(services)), - fs.writeFileSync(`./backup/database/sliders_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(sliders)), - fs.writeFileSync(`./backup/database/users_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(users)), - fs.writeFileSync(`./backup/database/yearBanner_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(yearBanner)), - fs.writeFileSync(`./backup/database/occasion_${moment(Date.now()).format('YYYY-MM-DD')}.json`, JSON.stringify(occasion)) - ]) - - /////////////////// create backup zip file - - const backupName = 'backup_' + moment(Date.now()).format('YYYY-MM-DD') + '.zip' - // create a file to stream archive data to. - const output = fs.createWriteStream(`./backup/${backupName}`) + const dateStr = moment(Date.now()).format('YYYY-MM-DD') + const backupName = `backup_${dateStr}.zip` const archive = archiver('zip', { - zlib: {level: 9} // Sets the compression level. + zlib: {level: 9} }) + const output = new PassThrough() + const chunks = [] + output.on('data', (chunk) => chunks.push(chunk)) - // listen for all archive data to be written - // 'close' event is fired only when a file descriptor is involved - output.on('close', function () { - console.log(archive.pointer() + ' total bytes') - console.log('archiver has been finalized and the output file descriptor has closed.') - return res.download(`./backup/${backupName}`) - }) - - // This event is fired when the data source is drained no matter what was the data source. - // It is not part of this library but rather from the NodeJS Stream API. - // @see: https://nodejs.org/api/stream.html#stream_event_end - output.on('end', function () { - console.log('Data has been drained') - return res.status(500).send('

فضای هاست پر میباشد.لطفا نسبت به پاک کردن بکاپ های قبلی یا افزایش فضای هاست اقدام فرمایید.

') - }) - - // good practice to catch warnings (ie stat failures and other non-blocking errors) archive.on('warning', function (err) { - if (err.code === 'ENOENT') { - // log warning console.log('On archiver warning -- ENOENT error code', err) } else { - // throw error console.log('On archiver warning -- other error codes ', err) } - return res.status(500).send('

فضای هاست پر میباشد.لطفا نسبت به پاک کردن بکاپ های قبلی یا افزایش فضای هاست اقدام فرمایید.

') + return res.status(500).send(hostFullError) }) - // good practice to catch this error explicitly archive.on('error', function (err) { - // throw err console.log(err) return res.status(500).json({message: 'مشکلی پیش آمده'}) }) - // pipe archive data to the file - await archive.pipe(output) + archive.pipe(output) - // append files from a sub-directory and naming it `new-subdir` within the archive - await archive.directory(`./static/uploads/`, 'uploads') - await archive.directory(`./backup/database/`, 'database') + const databaseExports = [ + ['categories', categories], + ['comments', comments], + ['contactus', contactus], + ['contents', contents], + ['galleries', galleries], + ['homepage', homepage], + ['links', links], + ['meetings', meetings], + ['news', news], + ['newsFiles', newsFiles], + ['services', services], + ['sliders', sliders], + ['users', users], + ['yearBanner', yearBanner], + ['occasion', occasion], + ] + + for (const [name, data] of databaseExports) { + archive.append(JSON.stringify(data), {name: `database/${name}_${dateStr}.json`}) + } + + await appendS3UploadsToArchive(archive) - // finalize the archive (ie we are done appending files but streams have to finish yet) - // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand await archive.finalize() - await fs.rmdirSync('./backup/database', {recursive: true}) + await new Promise((resolve, reject) => { + output.on('end', resolve) + output.on('error', reject) + }) + + const buffer = Buffer.concat(chunks) + + await uploadBackupArchive(buffer, backupName) + + res.setHeader('Content-Type', 'application/zip') + res.setHeader('Content-Disposition', `attachment; filename="${backupName}"`) + return res.send(buffer) } catch (e) { - return res.status(500).send('

فضای هاست پر میباشد.لطفا نسبت به پاک کردن بکاپ های قبلی یا افزایش فضای هاست اقدام فرمایید.

') + return res.status(500).send(hostFullError) } } ] @@ -173,12 +167,18 @@ module.exports.createBackUp = [ module.exports.removeOldBackups = [ async (req, res) => { try { - const files = await fs.readdirSync('./backup') - for await (const file of files) { - const isDirectory = await fs.statSync(`./backup/${file}`).isDirectory() - if (isDirectory) await fs.rmdirSync(`./backup/${file}`, {recursive: true}) - else await fs.unlinkSync(`./backup/${file}`) + await deleteOldBackups() + + if (fs.existsSync('./backup')) { + const files = fs.readdirSync('./backup') + for (const file of files) { + const filePath = `./backup/${file}` + const isDirectory = fs.statSync(filePath).isDirectory() + if (isDirectory) fs.rmdirSync(filePath, {recursive: true}) + else fs.unlinkSync(filePath) + } } + return res.json({message: _sr['fa'].response.success_remove}) } catch (e) { return res.status(500).json({message: e}) diff --git a/server/controllers/electionController.js b/server/controllers/electionController.js index 4bc9f84..0f94486 100644 --- a/server/controllers/electionController.js +++ b/server/controllers/electionController.js @@ -10,8 +10,11 @@ const { hasWhiteSpaces, isLatinCharactersWithSymbol } = require('../plugins/controllersHelperFunctions'); -const fs = require('fs'); -const sharp = require('sharp'); +const { + deleteCandidateImage, + deleteElectionCandidateImages, + uploadProcessedImage, +} = require('../plugins/electionImageStorage'); ////// variables const profileWidth = 300; @@ -292,17 +295,7 @@ module.exports.deletOneElection = [ } if (deleteElection.candidates.length) { - const targetDir = `./static/uploads/images/election/`; - - const getFiles = fs.readdirSync(targetDir); - - for (const file of getFiles) { - if (file.includes(deleteElection._id)) { - fs.unlink(`./static/uploads/images/election/${file}`, (err) => { - if (err) console.log(err) - }); - } - } + await deleteElectionCandidateImages(deleteElection); } await deleteElection.deleteOne(); @@ -457,16 +450,13 @@ module.exports.addCandidate = [ } const imageName = 'candidate_' + Date.now() + '_' + targetElection._id + '.jpg'; data.profilePic = imageName; - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .toFormat('jpg') - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/election/${imageName}`) + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); targetElection.candidates.push(data); @@ -528,29 +518,16 @@ module.exports.updateCandidate = [ return res.status(422).json({validation: {profilePic: {msg: _sr['fa'].format.image}}}); } const imageName = 'candidate_' + Date.now() + '_' + targetElection._id + '.jpg'; - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .toFormat('jpg') - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/election/${imageName}`) + await deleteCandidateImage(candidate); - /** - * if exist old pic delete it from file - */ - if (candidate.profilePic) { - const oldPic = `./static${candidate.profilePic}`; + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } candidate.profilePic = imageName; } @@ -586,15 +563,7 @@ module.exports.deleteCandidate = [ if (!candidate) return res404(res, `کاندید مورد نظر پیدا نشد`); - if (candidate.profilePic) { - const oldPic = `./static${candidate.profilePic}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await deleteCandidateImage(candidate); await targetElection.candidates.id(candidateId).remove(); diff --git a/server/controllers/linksController.js b/server/controllers/linksController.js index 3e3902a..e282d68 100644 --- a/server/controllers/linksController.js +++ b/server/controllers/linksController.js @@ -3,7 +3,6 @@ const { body, validationResult } = require('express-validator'); -const sharp = require('sharp'); const { _sr } = require('../plugins/serverResponses'); @@ -14,7 +13,10 @@ const { isLatinCharactersWithSymbol, generateRandomDigits } = require('../plugins/controllersHelperFunctions'); -const fs = require('fs'); +const { + deleteLinksImage, + uploadProcessedImage, +} = require('../plugins/linksImageStorage'); ////// variables const profileWidth = 50; @@ -108,15 +110,13 @@ module.exports.addLinks = [ } const imageName = `links_${Date.now()}_${createLinks._id}.${image.mimetype.split('/')[1]}`; try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/links/${imageName}`); + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); createLinks.icon = imageName; @@ -243,28 +243,15 @@ module.exports.updateLinks = [ } const imageName = `links_${Date.now()}_${targetLinks._id}.${image.mimetype.split('/')[1]}`; try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/links/${imageName}`); + await deleteLinksImage(targetLinks, 'icon'); - /** - * if exist old pic delete it from file - */ - if (targetLinks.icon) { - var oldPic = `./static${targetLinks.icon}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); targetLinks.icon = imageName; targetLinks.linksStatus = true; @@ -312,15 +299,7 @@ module.exports.deletOneLinks = [ return res500(res, `شما اجازه حذف این پیوند را ندارید`); } - if (targetLinks.icon) { - var oldPic = `./static${targetLinks.icon}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (error) => { - if (error) return res500(res, error?.message); - }); - } - } + await deleteLinksImage(targetLinks, 'icon'); /** delete links */ await targetLinks.deleteOne(); diff --git a/server/controllers/occasionController.js b/server/controllers/occasionController.js index c368fe4..8d42ae3 100644 --- a/server/controllers/occasionController.js +++ b/server/controllers/occasionController.js @@ -2,8 +2,10 @@ const Occasion = require('../models/Occasion'); const {body, validationResult} = require('express-validator'); const {_sr} = require('../plugins/serverResponses'); const {res404, res500} = require('../plugins/controllersHelperFunctions'); -const fs = require('fs'); -const sharp = require('sharp'); +const { + deleteOccasionImage, + uploadProcessedImage, +} = require('../plugins/occasionImageStorage'); const moment = require('moment-jalaali'); ////// variables @@ -63,16 +65,13 @@ module.exports.addOccasion = [ } const imageName = `occasion_${addOccasion._id}_${Date.now()}.jpg`; - const target = sharp(image.data) - await target - .resize(occasionWidth, occasionHeight, { - fit: 'cover' - }) - .toFormat('jpg') - .jpeg({ - quality: occasionQuality - }) - .toFile(`./static/uploads/images/occasion/${imageName}`); + await uploadProcessedImage( + image.data, + imageName, + occasionWidth, + occasionHeight, + occasionQuality + ); addOccasion.status = true; addOccasion.mainCover = imageName; @@ -132,26 +131,15 @@ module.exports.updateOccasion = [ } const imageName = `occasion_${targetOccasion._id}_${Date.now()}.jpg`; - const target = sharp(image.data) - await target - .resize(occasionWidth, occasionHeight, { - fit: 'cover' - }) - .toFormat('jpg') - .jpeg({ - quality: occasionQuality - }) - .toFile(`./static/uploads/images/occasion/${imageName}`); + await deleteOccasionImage(targetOccasion, 'mainCover'); - if (targetOccasion.mainCover) { - var oldPic = `./static${targetOccasion.mainCover}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + occasionWidth, + occasionHeight, + occasionQuality + ); targetOccasion.status = true; targetOccasion.mainCover = imageName; @@ -184,15 +172,7 @@ module.exports.deleteOccasion = [ return res404(res, `مناسبت مورد نظر پیدا نشد`); } - if (targetOccasion.mainCover) { - var oldPic = `./static${targetOccasion.mainCover}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await deleteOccasionImage(targetOccasion, 'mainCover'); await targetOccasion.deleteOne(); diff --git a/server/controllers/pdfController.js b/server/controllers/pdfController.js index 1d1d971..9aae71c 100644 --- a/server/controllers/pdfController.js +++ b/server/controllers/pdfController.js @@ -1,109 +1,148 @@ +const fs = require("fs/promises"); +const os = require("os"); +const path = require("path"); +const axios = require("axios"); const { body, validationResult } = require("express-validator"); const { _sr } = require("../plugins/serverResponses"); const { checkValidations } = require("../plugins/controllersHelperFunctions"); const Book = require("../models/Book"); -var convertapi = require('convertapi')('6zZB0nquI5V3P3Vg'); +const { + uploadPdf, + uploadBookSlide, + deletePdf, + deleteBookSlide, +} = require("../plugins/bookImageStorage"); +const convertapi = require("convertapi")("6zZB0nquI5V3P3Vg"); module.exports.AddPDF = [ - [ - body('title') - .notEmpty().withMessage(_sr['fa'].required.title) - ], - checkValidations(validationResult), - async (req, res) => { - try { - const firstbook = []; - - if (!req.files?.file) { - return res.status(422).json({ validation: { file: { msg: _sr['fa'].required.file } } }); - } - - if (req.files?.file.mimetype.split('/')[1] !== 'pdf') { - return res.status(422).json({ validation: { file: { msg: 'فقط فرمت PDF قابل قبول است' } } }); - } - - const book = await Book.findById(req.body.bookId); - const bookId = book ? book._id : null; - - const data = { title: req.body.title }; - const file = req.files.file; - const fileName = req.body.title.replace(Date.now(), '-') + '_' + Date.now() + '.pdf'; - const imageName = req.body.title.replace(Date.now(), '-') + '_' + Date.now(); - - const filePath = `./static/uploads/pdf/${fileName}`; - - await file.mv(filePath); - data.file = fileName; - - const convertPromise = convertapi.convert('jpg', { - File: filePath - }, 'pdf'); - - const result = await convertPromise; - const savedFiles = await result.saveFiles(`./static/uploads/books/${imageName}.jpeg`); - - const imagePath = `https://ostan-mr.ir/uploads/books/${imageName}.jpeg`; - firstbook.push(imagePath); - - if (bookId) { - const update = await Book.findByIdAndUpdate( - {_id:bookId}, - { $push: { slides: imagePath } }, - { upsert: true } - ); - return res.status(201).json({ msg: "اسلاید کتاب اضافه شد", Data: update }); - } else { - const newBook = new Book({ - title: req.body.title, - uri: `https://ostan-mr.ir/uploads/pdf/${fileName}`, - slides: firstbook - }); - await newBook.save(); - return res.status(201).json({ msg: "کتاب ایجاد شد", Data: newBook }); - } - - } catch (error) { - console.error(error); - return res.status(500).json({ msg: "خطایی در پردازش فایل رخ داد", error: error.message }); - } + [body("title").notEmpty().withMessage(_sr["fa"].required.title)], + checkValidations(validationResult), + async (req, res) => { + let tmpDir; + + try { + const firstbook = []; + + if (!req.files?.file) { + return res + .status(422) + .json({ validation: { file: { msg: _sr["fa"].required.file } } }); + } + + if (req.files?.file.mimetype.split("/")[1] !== "pdf") { + return res + .status(422) + .json({ validation: { file: { msg: "فقط فرمت PDF قابل قبول است" } } }); + } + + const book = await Book.findById(req.body.bookId); + const bookId = book ? book._id : null; + + const file = req.files.file; + const fileName = + req.body.title.replace(Date.now(), "-") + "_" + Date.now() + ".pdf"; + const imageName = + req.body.title.replace(Date.now(), "-") + "_" + Date.now(); + + const { url: pdfUrl } = await uploadPdf(file.data, fileName); + + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "book-")); + const tmpPdfPath = path.join(tmpDir, fileName); + await fs.writeFile(tmpPdfPath, file.data); + + const result = await convertapi.convert( + "jpg", + { File: tmpPdfPath }, + "pdf" + ); + + const convertedResponse = await axios.get(result.files[0].url, { + responseType: "arraybuffer", + }); + const imagePath = await uploadBookSlide( + Buffer.from(convertedResponse.data), + imageName + ); + firstbook.push(imagePath); + + if (bookId) { + const update = await Book.findByIdAndUpdate( + { _id: bookId }, + { $push: { slides: imagePath } }, + { upsert: true } + ); + return res + .status(201) + .json({ msg: "اسلاید کتاب اضافه شد", Data: update }); + } + + const newBook = new Book({ + title: req.body.title, + uri: pdfUrl, + slides: firstbook, + }); + await newBook.save(); + return res.status(201).json({ msg: "کتاب ایجاد شد", Data: newBook }); + } catch (error) { + console.error(error); + return res.status(500).json({ + msg: "خطایی در پردازش فایل رخ داد", + error: error.message, + }); + } finally { + if (tmpDir) { + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } } - - - + }, ]; module.exports.GetAll = [ - async (req, res,next) => { - try { - const Books = await Book.find({}); - return res.status(200).json({ Books }); - } catch (error) { - next(error) - } + async (req, res, next) => { + try { + const Books = await Book.find({}); + return res.status(200).json({ Books }); + } catch (error) { + next(error); } + }, ]; module.exports.GetOne = [ - async (req, res,next) => { - try { - const id = req.params.id - const book = await Book.findById(id); - return res.status(200).json({ msg: book }); - } catch (error) { - next(error) - } + async (req, res, next) => { + try { + const id = req.params.id; + const book = await Book.findById(id); + return res.status(200).json({ msg: book }); + } catch (error) { + next(error); } + }, ]; + module.exports.DeleteOne = [ - async(req,res,next)=>{ - try { - const book = await Book.deleteOne({_id:req.params.id}) - if(!book){ - return res.status(200).json({ msg: 'کتاب وجود ندارد ' }); - } - return res.status(200).json({ msg: 'کتاب حذف شد' }); - } catch (error) { - next(error) - } + async (req, res, next) => { + try { + const book = await Book.findById(req.params.id); + if (!book) { + return res.status(200).json({ msg: "کتاب وجود ندارد " }); + } + + const uri = book.get("uri", null, { getters: false }); + if (uri) { + await deletePdf(uri.split("/").pop()); + } + + const slides = book.get("slides", [], { getters: false }) || []; + for (const slide of slides) { + const imageName = slide.split("/").pop().replace(/\.jpeg$/i, ""); + await deleteBookSlide(imageName); + } + + await book.deleteOne(); + return res.status(200).json({ msg: "کتاب حذف شد" }); + } catch (error) { + next(error); } -] + }, +]; diff --git a/server/controllers/servicesController.js b/server/controllers/servicesController.js index e99b9dd..c326c08 100644 --- a/server/controllers/servicesController.js +++ b/server/controllers/servicesController.js @@ -3,7 +3,6 @@ const { body, validationResult } = require('express-validator'); -const sharp = require('sharp'); const { _sr } = require('../plugins/serverResponses'); @@ -14,7 +13,10 @@ const { isLatinCharactersWithSymbol, generateRandomDigits } = require('../plugins/controllersHelperFunctions'); -const fs = require('fs'); +const { + deleteSliderImage, + uploadProcessedImage, +} = require('../plugins/sliderImageStorage'); ////// variables const profileWidth = 600; @@ -113,15 +115,13 @@ module.exports.addSlider = [ } const imageName = `slider_${Date.now()}_${createSlider._id}.${image.mimetype.split('/')[1]}`; try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/slider/${imageName}`); + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); createSlider.cover = imageName; @@ -253,28 +253,15 @@ module.exports.updateSlider = [ } const imageName = `slider_${Date.now()}_${targetSlider._id}.${image.mimetype.split('/')[1]}`; try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/slider/${imageName}`); + await deleteSliderImage(targetSlider, "cover"); - /** - * if exist old pic delete it from file - */ - if (targetSlider.cover) { - var oldPic = `./static${targetSlider.cover}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); targetSlider.cover = imageName; } catch (e) { @@ -322,15 +309,7 @@ module.exports.deletOneSlider = [ return res500(res, `شما اجازه حذف این اسلایدر را ندارید`); } - if (targetSlider.cover) { - var oldPic = `./static${targetSlider.cover}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (error) => { - if (error) return res500(res, error?.message); - }); - } - } + await deleteSliderImage(targetSlider, "cover"); /** delete slider */ await targetSlider.deleteOne(); diff --git a/server/controllers/sliderController.js b/server/controllers/sliderController.js index 4f5892d..d78eb6f 100644 --- a/server/controllers/sliderController.js +++ b/server/controllers/sliderController.js @@ -3,7 +3,6 @@ const { body, validationResult } = require('express-validator'); -const sharp = require('sharp'); const { _sr } = require('../plugins/serverResponses'); @@ -14,7 +13,10 @@ const { isLatinCharactersWithSymbol, generateRandomDigits } = require('../plugins/controllersHelperFunctions'); -const fs = require('fs'); +const { + deleteSliderImage, + uploadProcessedImage, +} = require('../plugins/sliderImageStorage'); ////// variables const profileWidth = 880; @@ -113,15 +115,13 @@ module.exports.addSlider = [ } const imageName = `slider_${Date.now()}_${createSlider._id}.${image.mimetype.split('/')[1]}`; try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/slider/${imageName}`); + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); createSlider.cover = imageName; @@ -253,28 +253,15 @@ module.exports.updateSlider = [ } const imageName = `slider_${Date.now()}_${targetSlider._id}.${image.mimetype.split('/')[1]}`; try { - const target = sharp(image.data) - await target - .resize(profileWidth, profileHeight, { - fit: 'cover' - }) - .jpeg({ - quality: profileQuality - }) - .toFile(`./static/uploads/images/slider/${imageName}`); + await deleteSliderImage(targetSlider, "cover"); - /** - * if exist old pic delete it from file - */ - if (targetSlider.cover) { - var oldPic = `./static${targetSlider.cover}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); targetSlider.cover = imageName; } catch (e) { @@ -322,15 +309,7 @@ module.exports.deletOneSlider = [ return res500(res, `شما اجازه حذف این اسلایدر را ندارید`); } - if (targetSlider.cover) { - var oldPic = `./static${targetSlider.cover}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (error) => { - if (error) return res500(res, error?.message); - }); - } - } + await deleteSliderImage(targetSlider, "cover"); /** delete slider */ await targetSlider.deleteOne(); diff --git a/server/controllers/userController.js b/server/controllers/userController.js index d181d0b..5dda2a5 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -4,7 +4,6 @@ const { rateLimit } = require("express-rate-limit"); const bcrypt = require("bcryptjs"); const jwt = require("jsonwebtoken"); const crypto = require("crypto"); -const sharp = require("sharp"); const { secretKey } = require("./../authentication"); const { _sr } = require("../plugins/serverResponses"); const { @@ -14,7 +13,10 @@ const { isLatinCharactersWithSymbol, generateRandomDigits, } = require("../plugins/controllersHelperFunctions"); -const fs = require("fs"); +const { + deleteUserImage, + uploadProcessedImage, +} = require("../plugins/userImageStorage"); const nodemailer = require("nodemailer"); const Category = require("../models/Category"); const Logger = require("../models/Logger"); @@ -566,15 +568,13 @@ module.exports.registerAdmin = [ image.mimetype.split("/")[1]; data.profilePic = imageName; try { - const target = sharp(image.data); - await target - .resize(profileWidth, profileHeight, { - fit: "cover", - }) - .jpeg({ - quality: profileQuality, - }) - .toFile(`./static/uploads/images/users/${imageName}`); + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); } catch (e) { return res500(res, e?.message); } @@ -713,18 +713,7 @@ module.exports.deleteOneUser = [ .json({ message: `شما نمیتوانید این کاربر را حذف کنید` }); } - /** - * if exist old pic delete it from file - */ - if (user.profilePic) { - var oldPic = `./static${user.profilePic}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await deleteUserImage(user, "profilePic"); /** delete user */ await user.deleteOne(); @@ -922,28 +911,15 @@ module.exports.updateUser = [ image.mimetype.split("/")[1]; try { - const target = sharp(image.data); - await target - .resize(profileWidth, profileHeight, { - fit: "cover", - }) - .jpeg({ - quality: profileQuality, - }) - .toFile(`./static/uploads/images/users/${imageName}`); + await deleteUserImage(user, "profilePic"); - /** - * if exist old pic delete it from file - */ - if (user.profilePic) { - var oldPic = `./static${user.profilePic}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); } catch (e) { return res500(res, e?.message); } @@ -1129,28 +1105,15 @@ module.exports.updateProfile = [ image.mimetype.split("/")[1]; try { - const target = sharp(image.data); - await target - .resize(profileWidth, profileHeight, { - fit: "cover", - }) - .jpeg({ - quality: profileQuality, - }) - .toFile(`./static/uploads/images/users/${imageName}`); + await deleteUserImage(user, "profilePic"); - /** - * if exist old pic delete it from file - */ - if (user.profilePic) { - var oldPic = `./static${user.profilePic}`; - - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) return res500(res, err?.message); - }); - } - } + await uploadProcessedImage( + image.data, + imageName, + profileWidth, + profileHeight, + profileQuality + ); } catch (e) { return res500(res, e?.message); } diff --git a/server/controllers/yearBannerController.js b/server/controllers/yearBannerController.js index 71aa1e5..503b3a7 100644 --- a/server/controllers/yearBannerController.js +++ b/server/controllers/yearBannerController.js @@ -2,8 +2,7 @@ const YearBanner = require('../models/YearBanner'); const {body, validationResult, checkSchema} = require('express-validator'); const {_sr} = require('../plugins/serverResponses'); const {res404, res500} = require('../plugins/controllersHelperFunctions'); -const fs = require('fs'); -const sharp = require('sharp'); +const {uploadProcessedImage} = require('../plugins/yearBannerImageStorage'); ////// variables const yearBannerWidth = 1920 @@ -51,12 +50,13 @@ module.exports.updateYearBanner = [ imageName = `mainPage_cover.jpg`; - const target = sharp(image.data) - await target - .resize(yearBannerWidth, yearBannerHeight, {fit: 'cover'}) - .toFormat('jpg') - .jpeg({quality: yearBannerQuality}) - .toFile(`./static/uploads/images/mainCover/${imageName}`); + await uploadProcessedImage( + image.data, + imageName, + yearBannerWidth, + yearBannerHeight, + yearBannerQuality + ); } if (yearBanner) { diff --git a/server/models/Book.js b/server/models/Book.js index d606581..84facae 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -1,16 +1,31 @@ const mongoose = require('mongoose'); const mongoosePaginate = require('mongoose-paginate-v2'); +const s3Storage = require('../plugins/s3Storage'); - -function PDF(pdf) { - if (pdf) return '/uploads/files/ReadOnly-PDF' + pdf +function pdfUri(uri) { + if (!uri) return null; + if (uri.startsWith('http://') || uri.startsWith('https://')) return uri; + const fileName = uri.startsWith('/') ? uri.split('/').pop() : uri; + return s3Storage.getPublicUrl(`uploads/pdf/${fileName}`); } + +function slideUrl(slide) { + if (!slide) return null; + if (slide.startsWith('http://') || slide.startsWith('https://')) return slide; + const imageName = slide.startsWith('/') ? slide.split('/').pop() : slide; + const key = imageName.endsWith('.jpeg') + ? `uploads/books/${imageName}` + : `uploads/books/${imageName}.jpeg`; + return s3Storage.getPublicUrl(key); +} + const BookSchema = mongoose.Schema({ title: String, - uri: String, - slides:{ - default:[], - type:Array + uri: { type: String, get: pdfUri, default: null }, + slides: { + default: [], + type: [String], + get: (slides) => (slides || []).map(slideUrl), } }); diff --git a/server/models/Category.js b/server/models/Category.js index 7190473..0f6f4e6 100644 --- a/server/models/Category.js +++ b/server/models/Category.js @@ -1,4 +1,5 @@ const mongoose = require("mongoose"); +const s3Storage = require("../plugins/s3Storage"); function autoPopulateSubs(next) { this.populate({ @@ -14,12 +15,21 @@ function autoPopulateSubs(next) { } function file(file) { - return "/uploads/files/terms/" + file; + if (!file) return null; + if (file.startsWith("http://") || file.startsWith("https://")) return file; + return s3Storage.getPublicUrl(`uploads/files/terms/${file}`); } function image(img) { - if (img) return "/uploads/images/category/" + img; - else return "/img/no-cover.jpg"; + if (!img) return "/img/no-cover.jpg"; + if (img.startsWith("http://") || img.startsWith("https://")) return img; + return s3Storage.getPublicUrl(`uploads/images/category/${img}`); +} + +function shafafiatIcon(icon) { + if (!icon) return null; + if (icon.startsWith("http://") || icon.startsWith("https://")) return icon; + return s3Storage.getPublicUrl(`uploads/images/category/shafafiat/${icon}`); } const CategoryTermsFile = mongoose.Schema({ @@ -71,7 +81,7 @@ const CategorySchema = mongoose.Schema({ managerName: String, managerDescription: String, /** if category was shafafiat this icon is for that */ - icon: String, + icon: { type: String, get: shafafiatIcon, default: null }, }); CategorySchema.index({ parent: 1, childs: 1 }); diff --git a/server/models/Content.js b/server/models/Content.js index 42f45ec..a3f4445 100644 --- a/server/models/Content.js +++ b/server/models/Content.js @@ -1,7 +1,10 @@ const mongoose = require('mongoose'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/content/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/content/${img}`); } diff --git a/server/models/Election.js b/server/models/Election.js index e626751..c0f0f83 100644 --- a/server/models/Election.js +++ b/server/models/Election.js @@ -1,8 +1,11 @@ const mongoose = require('mongoose'); const mongoosePaginate = require('mongoose-paginate-v2'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/election/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/election/${img}`); } const CandidatesSchema = mongoose.Schema({ diff --git a/server/models/Links.js b/server/models/Links.js index d099d6f..1f4ddc0 100644 --- a/server/models/Links.js +++ b/server/models/Links.js @@ -1,7 +1,10 @@ const mongoose = require('mongoose'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/links/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/links/${img}`); } const LinksSchema = mongoose.Schema({ diff --git a/server/models/Occasion.js b/server/models/Occasion.js index 6c300ec..c5e7a82 100644 --- a/server/models/Occasion.js +++ b/server/models/Occasion.js @@ -1,8 +1,11 @@ const mongoose = require('mongoose'); const mongoosePaginate = require('mongoose-paginate-v2'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/occasion/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/occasion/${img}`); } const OccasionSchema = mongoose.Schema({ diff --git a/server/models/Slider.js b/server/models/Slider.js index fd9f017..05ec6ba 100644 --- a/server/models/Slider.js +++ b/server/models/Slider.js @@ -1,8 +1,11 @@ const mongoose = require('mongoose'); const mongoosePaginate = require('mongoose-paginate-v2'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/slider/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/slider/${img}`); } const SliderSchema = mongoose.Schema({ diff --git a/server/models/User.js b/server/models/User.js index 7d35028..cc3574a 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,8 +1,11 @@ const mongoose = require('mongoose'); const mongoosePaginate = require('mongoose-paginate-v2'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/users/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/users/${img}`); } const AdminSchema = mongoose.Schema({ diff --git a/server/models/YearBanner.js b/server/models/YearBanner.js index 43fdbda..80e1919 100644 --- a/server/models/YearBanner.js +++ b/server/models/YearBanner.js @@ -1,7 +1,10 @@ const mongoose = require('mongoose'); +const s3Storage = require('../plugins/s3Storage'); function image(img) { - if (img) return '/uploads/images/mainCover/' + img + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`uploads/images/mainCover/${img}`); } const YearBannerSchema = mongoose.Schema({ diff --git a/server/plugins/backupStorage.js b/server/plugins/backupStorage.js new file mode 100644 index 0000000..e3a7fb6 --- /dev/null +++ b/server/plugins/backupStorage.js @@ -0,0 +1,33 @@ +const s3Storage = require("./s3Storage"); + +const BACKUP_PREFIX = "backup"; +const UPLOADS_PREFIX = "uploads"; + +const appendS3UploadsToArchive = async (archive) => { + const keys = await s3Storage.listObjects(UPLOADS_PREFIX); + + for (const key of keys) { + const buffer = await s3Storage.getObjectBuffer(key); + archive.append(buffer, { name: key }); + } +}; + +const uploadBackupArchive = async (buffer, fileName) => { + await s3Storage.uploadBuffer( + `${BACKUP_PREFIX}/${fileName}`, + buffer, + "application/zip" + ); +}; + +const deleteOldBackups = async () => { + await s3Storage.deleteObjectsByPrefix(BACKUP_PREFIX); +}; + +module.exports = { + BACKUP_PREFIX, + UPLOADS_PREFIX, + appendS3UploadsToArchive, + uploadBackupArchive, + deleteOldBackups, +}; diff --git a/server/plugins/bookImageStorage.js b/server/plugins/bookImageStorage.js new file mode 100644 index 0000000..e354bf0 --- /dev/null +++ b/server/plugins/bookImageStorage.js @@ -0,0 +1,48 @@ +const s3Storage = require("./s3Storage"); + +const PDF_PREFIX = "uploads/pdf"; +const BOOK_SLIDE_PREFIX = "uploads/books"; + +const uploadPdf = async (fileData, fileName) => { + const key = `${PDF_PREFIX}/${fileName}`; + await s3Storage.uploadBuffer(key, fileData, "application/pdf"); + return { + fileName, + url: s3Storage.getPublicUrl(key), + }; +}; + +const uploadBookSlide = async (buffer, imageName) => { + const key = `${BOOK_SLIDE_PREFIX}/${imageName}.jpeg`; + await s3Storage.uploadBuffer(key, buffer, "image/jpeg"); + return s3Storage.getPublicUrl(key); +}; + +const deletePdf = async (fileName) => { + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${PDF_PREFIX}/${fileName}`); + } catch (error) { + console.log("Failed to delete PDF from S3:", error?.message); + } +}; + +const deleteBookSlide = async (imageName) => { + if (!imageName) return; + + try { + await s3Storage.deleteObject(`${BOOK_SLIDE_PREFIX}/${imageName}.jpeg`); + } catch (error) { + console.log("Failed to delete book slide from S3:", error?.message); + } +}; + +module.exports = { + PDF_PREFIX, + BOOK_SLIDE_PREFIX, + uploadPdf, + uploadBookSlide, + deletePdf, + deleteBookSlide, +}; diff --git a/server/plugins/categoryImageStorage.js b/server/plugins/categoryImageStorage.js new file mode 100644 index 0000000..f45533c --- /dev/null +++ b/server/plugins/categoryImageStorage.js @@ -0,0 +1,94 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const CATEGORY_IMAGE_PREFIX = "uploads/images/category"; +const CATEGORY_SHAFAFIAT_PREFIX = "uploads/images/category/shafafiat"; +const TERMS_FILE_PREFIX = "uploads/files/terms"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteCategoryImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${CATEGORY_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const deleteShafafiatIcon = async (fileName) => { + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${CATEGORY_SHAFAFIAT_PREFIX}/${fileName}`); + } catch (error) { + console.log("Failed to delete shafafiat icon from S3:", error?.message); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${CATEGORY_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +const uploadShafafiatIcon = async (fileData, fileName, contentType) => { + await s3Storage.uploadBuffer( + `${CATEGORY_SHAFAFIAT_PREFIX}/${fileName}`, + fileData, + contentType + ); + + return fileName; +}; + +const uploadTermsFile = async (fileData, fileName) => { + await s3Storage.uploadBuffer( + `${TERMS_FILE_PREFIX}/${fileName}`, + fileData, + "application/pdf" + ); + + return fileName; +}; + +const deleteTermsFile = async (fileName) => { + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${TERMS_FILE_PREFIX}/${fileName}`); + } catch (error) { + console.log("Failed to delete terms file from S3:", error?.message); + } +}; + +module.exports = { + CATEGORY_IMAGE_PREFIX, + CATEGORY_SHAFAFIAT_PREFIX, + TERMS_FILE_PREFIX, + getStoredImageName, + deleteCategoryImage, + deleteShafafiatIcon, + uploadProcessedImage, + uploadShafafiatIcon, + uploadTermsFile, + deleteTermsFile, +}; diff --git a/server/plugins/contentImageStorage.js b/server/plugins/contentImageStorage.js new file mode 100644 index 0000000..5732bda --- /dev/null +++ b/server/plugins/contentImageStorage.js @@ -0,0 +1,52 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const CONTENT_IMAGE_PREFIX = "uploads/images/content"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteContentImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${CONTENT_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const deleteContentImages = async (doc) => { + await deleteContentImage(doc, "cover"); + await deleteContentImage(doc, "thumb"); +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${CONTENT_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + CONTENT_IMAGE_PREFIX, + getStoredImageName, + deleteContentImage, + deleteContentImages, + uploadProcessedImage, +}; diff --git a/server/plugins/electionImageStorage.js b/server/plugins/electionImageStorage.js new file mode 100644 index 0000000..06129cd --- /dev/null +++ b/server/plugins/electionImageStorage.js @@ -0,0 +1,58 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const ELECTION_IMAGE_PREFIX = "uploads/images/election"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteElectionImage = async (fileName) => { + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${ELECTION_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete election image from S3:`, error?.message); + } +}; + +const deleteCandidateImage = async (candidate) => { + const fileName = getStoredImageName(candidate, "profilePic"); + await deleteElectionImage(fileName); +}; + +const deleteElectionCandidateImages = async (election) => { + for (const candidate of election.candidates) { + await deleteCandidateImage(candidate); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${ELECTION_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + ELECTION_IMAGE_PREFIX, + getStoredImageName, + deleteElectionImage, + deleteCandidateImage, + deleteElectionCandidateImages, + uploadProcessedImage, +}; diff --git a/server/plugins/linksImageStorage.js b/server/plugins/linksImageStorage.js new file mode 100644 index 0000000..c6a753f --- /dev/null +++ b/server/plugins/linksImageStorage.js @@ -0,0 +1,46 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const LINKS_IMAGE_PREFIX = "uploads/images/links"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteLinksImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${LINKS_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${LINKS_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + LINKS_IMAGE_PREFIX, + getStoredImageName, + deleteLinksImage, + uploadProcessedImage, +}; diff --git a/server/plugins/occasionImageStorage.js b/server/plugins/occasionImageStorage.js new file mode 100644 index 0000000..6ee91a8 --- /dev/null +++ b/server/plugins/occasionImageStorage.js @@ -0,0 +1,46 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const OCCASION_IMAGE_PREFIX = "uploads/images/occasion"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteOccasionImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${OCCASION_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${OCCASION_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + OCCASION_IMAGE_PREFIX, + getStoredImageName, + deleteOccasionImage, + uploadProcessedImage, +}; diff --git a/server/plugins/s3Storage.js b/server/plugins/s3Storage.js index f16c02e..7a5c949 100644 --- a/server/plugins/s3Storage.js +++ b/server/plugins/s3Storage.js @@ -2,6 +2,8 @@ const { S3Client, PutObjectCommand, DeleteObjectCommand, + ListObjectsV2Command, + GetObjectCommand, } = require("@aws-sdk/client-s3"); const config = { @@ -85,9 +87,54 @@ const deleteObject = async (key) => { ); }; +const listObjects = async (prefix = "") => { + const keys = []; + let continuationToken; + + do { + const response = await getClient().send( + new ListObjectsV2Command({ + Bucket: config.bucket, + Prefix: normalizeKey(prefix), + ContinuationToken: continuationToken, + }) + ); + + for (const item of response.Contents || []) { + if (item.Key) keys.push(item.Key); + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + return keys; +}; + +const getObjectBuffer = async (key) => { + const response = await getClient().send( + new GetObjectCommand({ + Bucket: config.bucket, + Key: normalizeKey(key), + }) + ); + + return Buffer.from(await response.Body.transformToByteArray()); +}; + +const deleteObjectsByPrefix = async (prefix) => { + const keys = await listObjects(prefix); + + await Promise.all(keys.map((key) => deleteObject(key))); +}; + module.exports = { isConfigured, getPublicUrl, uploadBuffer, deleteObject, + listObjects, + getObjectBuffer, + deleteObjectsByPrefix, }; diff --git a/server/plugins/sliderImageStorage.js b/server/plugins/sliderImageStorage.js new file mode 100644 index 0000000..8387c3e --- /dev/null +++ b/server/plugins/sliderImageStorage.js @@ -0,0 +1,46 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const SLIDER_IMAGE_PREFIX = "uploads/images/slider"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteSliderImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${SLIDER_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${SLIDER_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + SLIDER_IMAGE_PREFIX, + getStoredImageName, + deleteSliderImage, + uploadProcessedImage, +}; diff --git a/server/plugins/userImageStorage.js b/server/plugins/userImageStorage.js new file mode 100644 index 0000000..4a6bddb --- /dev/null +++ b/server/plugins/userImageStorage.js @@ -0,0 +1,46 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const USER_IMAGE_PREFIX = "uploads/images/users"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteUserImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${USER_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${USER_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + USER_IMAGE_PREFIX, + getStoredImageName, + deleteUserImage, + uploadProcessedImage, +}; diff --git a/server/plugins/yearBannerImageStorage.js b/server/plugins/yearBannerImageStorage.js new file mode 100644 index 0000000..62c02db --- /dev/null +++ b/server/plugins/yearBannerImageStorage.js @@ -0,0 +1,46 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const YEAR_BANNER_IMAGE_PREFIX = "uploads/images/mainCover"; + +const getStoredImageName = (doc, field) => + doc.get(field, null, { getters: false }); + +const deleteYearBannerImage = async (doc, field) => { + const fileName = getStoredImageName(doc, field); + if (!fileName) return; + + try { + await s3Storage.deleteObject(`${YEAR_BANNER_IMAGE_PREFIX}/${fileName}`); + } catch (error) { + console.log(`Failed to delete ${field} from S3:`, error?.message); + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality +) => { + const buffer = await sharp(imageData) + .resize(width, height, { fit: "cover" }) + .jpeg({ quality }) + .toBuffer(); + + await s3Storage.uploadBuffer( + `${YEAR_BANNER_IMAGE_PREFIX}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +module.exports = { + YEAR_BANNER_IMAGE_PREFIX, + getStoredImageName, + deleteYearBannerImage, + uploadProcessedImage, +};