From c73027540a1c993e222ad605c0d1976ac8f53f77 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 19 Jun 2026 12:35:49 +0330 Subject: [PATCH] S3 gallery --- server/controllers/galleryController.js | 278 +++++++----------------- server/models/Gallery.js | 13 +- server/plugins/galleryImageStorage.js | 191 ++++++++++++++++ 3 files changed, 276 insertions(+), 206 deletions(-) create mode 100644 server/plugins/galleryImageStorage.js diff --git a/server/controllers/galleryController.js b/server/controllers/galleryController.js index 6cf94fb..0db9940 100644 --- a/server/controllers/galleryController.js +++ b/server/controllers/galleryController.js @@ -1,12 +1,20 @@ const Gallery = require("../models/Gallery"); const { body, validationResult } = require("express-validator"); -const sharp = require("sharp"); const crypto = require("crypto"); const { _sr } = require("../plugins/serverResponses"); const { res404, res500 } = require("../plugins/controllersHelperFunctions"); -const fs = require("fs"); const moment = require("moment-jalaali"); const Category = require("./../models/Category"); +const { + deleteGalleryFile, + uploadGalleryFile, + uploadProcessedImage, + deleteAllGalleryFiles, + deleteGalleryCover, + getStoredFileName, + getStoredArray, + GALLERY_IMAGE_PREFIX, +} = require("../plugins/galleryImageStorage"); ////// variables const picsWidth = 1920; @@ -45,139 +53,40 @@ const imageLimit = 2; const graphicLimit = 5; const videoLimit = 20; ////// functions -const _upload = async (fileType, file, galleryId, Hash) => { - try { - const fileName = `gallery_${galleryId}_${Date.now()}.${ - file.mimetype.split("/")[1] - }`; - const uploadPath = `./static/uploads/${fileType}s/gallery/${fileName}`; - const thumbUploadPath = `./static/uploads/${fileType}s/gallery/thumb_${fileName}`; - - switch (fileType) { - case "image": - try { - const target = sharp(file.data); - await target - .resize(picsWidth, picsHeight, { - fit: "cover", - }) - .toFormat("jpg") - .jpeg({ - quality: picsQuality, - }) - .toFile(uploadPath); - - await target - .resize(thumbPicsWidth, thumbPicsHeight, { - fit: "cover", - }) - .toFormat("jpg") - .jpeg({ - quality: thumbPicsQuality, - }) - .toFile(thumbUploadPath); - } catch (error) { - console.log(error); - } - break; - case "graphic": - try { - const target = sharp(file.data); - await target - // .resize(graphicWidth, graphicHeight, { - // fit: 'cover' - // }) - .toFormat("jpg") - .jpeg({ - quality: graphicQuality, - }) - .toFile(uploadPath); - - await target - .resize(thumbGraphicWidth, thumbGraphicHeight, { - fit: "cover", - }) - .toFormat("jpg") - .jpeg({ - quality: thumbGraphicQuality, - }) - .toFile(thumbUploadPath); - } catch (error) { - console.log(error); - } - break; - case "video": - await file.mv(uploadPath); - break; - } - - return fileName; - } catch (error) { - console.log(error); - return null; +const _upload = async (fileType, file, galleryId) => { + if (fileType === "graphic") { + return uploadGalleryFile("graphic", file, galleryId, { + mainQuality: graphicQuality, + thumbWidth: thumbGraphicWidth, + thumbHeight: thumbGraphicHeight, + thumbQuality: thumbGraphicQuality, + }); } + + if (fileType === "video") { + return uploadGalleryFile("video", file, galleryId, {}); + } + + return uploadGalleryFile("image", file, galleryId, { + mainWidth: picsWidth, + mainHeight: picsHeight, + mainQuality: picsQuality, + thumbWidth: thumbPicsWidth, + thumbHeight: thumbPicsHeight, + thumbQuality: thumbPicsQuality, + }); }; const _deleteOne = async (fileType, name) => { try { - let targetFile = `./static/uploads/${fileType}s/gallery/${name}`; - let thumbTargetFile = `./static/uploads/${fileType}s/gallery/thumb_${name}`; - - if (fs.existsSync(targetFile)) { - fs.unlink(targetFile, (err) => { - if (err) console.log(err); - }); - if (fileType !== "video") { - fs.unlink(thumbTargetFile, (err) => { - if (err) console.log(err); - }); - } - return name; - } else { - return null; - } + await deleteGalleryFile(fileType, name); + return name; } catch (error) { console.log(error); return null; } }; -const _deleteAll = async (galleryId, galleryType) => { - try { - const targetDir = `./static/uploads/${galleryType}s/gallery/`; - - const getFiles = fs.readdirSync(targetDir); - - for (const file of getFiles) { - if (file.includes(galleryId)) { - fs.unlink(`./static/uploads/${galleryType}s/gallery/${file}`, (err) => { - if (err) console.log(err); - }); - } - } - } catch (error) { - console.log(error); - } -}; - -const _deleteCover = async (galleryId) => { - try { - const targetDir = `./static/uploads/images/gallery/`; - - const getFiles = fs.readdirSync(targetDir); - - for (const file of getFiles) { - if (file.includes(galleryId) && file.includes("cover")) { - fs.unlink(`./static/uploads/images/gallery/${file}`, (err) => { - if (err) console.log(err); - }); - } - } - } catch (error) { - console.log(err); - } -}; - const _randomGallery = async (queryObject) => { let setSkip; try { @@ -599,7 +508,7 @@ module.exports.updateGallery = [ } if (targetGallery.galleryType !== galleryType) { - await _deleteAll(targetGallery._id, targetGallery.galleryType); + await deleteAllGalleryFiles(targetGallery); targetGallery.images = []; targetGallery.thumbImages = []; targetGallery.video = null; @@ -722,8 +631,8 @@ module.exports.deletOneGallery = [ return res500(res, `شما اجازه پاک کردن این گالری را ندارید`); } - await _deleteAll(targetGallery._id, targetGallery.galleryType); - await _deleteCover(targetGallery._id); + await deleteAllGalleryFiles(targetGallery); + await deleteGalleryCover(targetGallery); /** delete gallery */ await targetGallery.deleteOne(); @@ -1037,27 +946,20 @@ module.exports.uploadPDF = [ targetGallery.thumbImages.push(`thumb_${fileName}`); break; case "video": - if ( - targetGallery.video && - (await fs.existsSync(`./static/${targetGallery?.video}`)) - ) { - fs.unlink(`./static/${targetGallery.video}`, (err) => { - if (err) return res.status(500).json({ msg: err.message }); - }); + if (getStoredFileName(targetGallery, "video")) { + await deleteGalleryFile( + "video", + getStoredFileName(targetGallery, "video") + ); } targetGallery.video = fileName; break; case "graphic": - if ( - targetGallery.graphic && - (await fs.existsSync(`./static/${targetGallery?.graphic}`)) - ) { - fs.unlink(`./static/${targetGallery.graphic}`, (err) => { - if (err) return res.status(500).json({ msg: err.message }); - }); - fs.unlink(`./static/${targetGallery.thumbGraphic}`, (err) => { - if (err) return res.status(500).json({ msg: err.message }); - }); + if (getStoredFileName(targetGallery, "graphic")) { + await deleteGalleryFile( + "graphic", + getStoredFileName(targetGallery, "graphic") + ); } targetGallery.graphic = fileName; targetGallery.thumbGraphic = `thumb_${fileName}`; @@ -1130,9 +1032,7 @@ module.exports.uploadFile = [ return res.status(404).json({ msg: `گالری پیدا نشد` }); } - const Hash = targetGallery.uidImage.map((path) => - path.replace("/uploads/images/gallery/", "") - ); + const Hash = getStoredArray(targetGallery, "uidImage"); if (Hash.includes(fileHash)) { return res.status(200).json({ msg: `این فایل قبلا بارگذاری شده` }); } @@ -1206,27 +1106,20 @@ module.exports.uploadFile = [ targetGallery.thumbImages.push(`thumb_${fileName}`); break; case "video": - if ( - targetGallery.video && - (await fs.existsSync(`./static/${targetGallery?.video}`)) - ) { - fs.unlink(`./static/${targetGallery.video}`, (err) => { - if (err) return res.status(500).json({ msg: err.message }); - }); + if (getStoredFileName(targetGallery, "video")) { + await deleteGalleryFile( + "video", + getStoredFileName(targetGallery, "video") + ); } targetGallery.video = fileName; break; case "graphic": - if ( - targetGallery.graphic && - (await fs.existsSync(`./static/${targetGallery?.graphic}`)) - ) { - fs.unlink(`./static/${targetGallery.graphic}`, (err) => { - if (err) return res.status(500).json({ msg: err.message }); - }); - fs.unlink(`./static/${targetGallery.thumbGraphic}`, (err) => { - if (err) return res.status(500).json({ msg: err.message }); - }); + if (getStoredFileName(targetGallery, "graphic")) { + await deleteGalleryFile( + "graphic", + getStoredFileName(targetGallery, "graphic") + ); } targetGallery.graphic = fileName; targetGallery.thumbGraphic = `thumb_${fileName}`; @@ -1743,47 +1636,26 @@ module.exports.uploadCover = [ const imageName = `cover_gallery_${Date.now()}_${targetGallery._id}.jpg`; const thumbImg = `thumb_${imageName}`; try { - const target = sharp(file.data); + await uploadProcessedImage( + file.data, + imageName, + coverWidth, + coverHeight, + coverQuality, + GALLERY_IMAGE_PREFIX + ); - await target - .resize(coverWidth, coverHeight, { - fit: "cover", - }) - .toFormat("jpg") - .jpeg({ - quality: coverQuality, - }) - .toFile(`./static/uploads/images/gallery/${imageName}`); + await uploadProcessedImage( + file.data, + thumbImg, + thumbCoverWidth, + thumbCoverHeight, + thumbCoverQuality, + GALLERY_IMAGE_PREFIX + ); - await target - .resize(thumbCoverWidth, thumbCoverHeight, { - fit: "cover", - }) - .toFormat("jpg") - .jpeg({ - quality: thumbCoverQuality, - }) - .toFile(`./static/uploads/images/gallery/${thumbImg}`); - - /** - * if exist old pic delete it from file - */ - if (targetGallery.cover) { - let oldPic = `./static/uploads/images/gallery/${targetGallery.cover}`; - if (fs.existsSync(oldPic)) { - fs.unlink(oldPic, (err) => { - if (err) console.log("Error deleting old cover:", err); - }); - } - } - - if (targetGallery.thumbCover) { - let oldThumbPic = `./static/uploads/images/gallery/${targetGallery.thumbCover}`; - if (fs.existsSync(oldThumbPic)) { - fs.unlink(oldThumbPic, (err) => { - if (err) console.log("Error deleting old thumb cover:", err); - }); - } + if (getStoredFileName(targetGallery, "cover")) { + await deleteGalleryCover(targetGallery); } // Update gallery record with new cover images diff --git a/server/models/Gallery.js b/server/models/Gallery.js index 4235e07..f9ca9ed 100644 --- a/server/models/Gallery.js +++ b/server/models/Gallery.js @@ -1,16 +1,23 @@ const mongoose = require('mongoose'); const mongoosePaginate = require('mongoose-paginate-v2'); +const s3Storage = require('../plugins/s3Storage'); + +function galleryUrl(img, prefix) { + if (!img) return null; + if (img.startsWith('http://') || img.startsWith('https://')) return img; + return s3Storage.getPublicUrl(`${prefix}/${img}`); +} function image(img) { - if (img) return '/uploads/images/gallery/' + img + return galleryUrl(img, 'uploads/images/gallery'); } function graphic(img) { - if (img) return '/uploads/graphics/gallery/' + img + return galleryUrl(img, 'uploads/graphics/gallery'); } function video(vdo) { - if (vdo) return '/uploads/videos/gallery/' + vdo + return galleryUrl(vdo, 'uploads/videos/gallery'); } const GallerySchema = mongoose.Schema({ diff --git a/server/plugins/galleryImageStorage.js b/server/plugins/galleryImageStorage.js new file mode 100644 index 0000000..f877c2f --- /dev/null +++ b/server/plugins/galleryImageStorage.js @@ -0,0 +1,191 @@ +const sharp = require("sharp"); +const s3Storage = require("./s3Storage"); + +const GALLERY_IMAGE_PREFIX = "uploads/images/gallery"; +const GALLERY_GRAPHIC_PREFIX = "uploads/graphics/gallery"; +const GALLERY_VIDEO_PREFIX = "uploads/videos/gallery"; + +const getPrefixForFileType = (fileType) => { + switch (fileType) { + case "graphic": + return GALLERY_GRAPHIC_PREFIX; + case "video": + return GALLERY_VIDEO_PREFIX; + default: + return GALLERY_IMAGE_PREFIX; + } +}; + +const getGalleryObjectKey = (fileType, fileName) => + `${getPrefixForFileType(fileType)}/${fileName}`; + +const getStoredFileName = (doc, field) => + doc.get(field, null, { getters: false }); + +const getStoredArray = (doc, field) => { + const value = doc.get(field, null, { getters: false }); + return Array.isArray(value) ? value : []; +}; + +const deleteGalleryFile = async (fileType, fileName) => { + if (!fileName) return; + + const baseName = fileName.replace(/^thumb_/, ""); + + try { + await s3Storage.deleteObject(getGalleryObjectKey(fileType, baseName)); + } catch (error) { + console.log(`Failed to delete ${baseName} from S3:`, error?.message); + } + + if (fileType !== "video") { + try { + await s3Storage.deleteObject( + getGalleryObjectKey(fileType, `thumb_${baseName}`) + ); + } catch (error) { + console.log(`Failed to delete thumb_${baseName} from S3:`, error?.message); + } + } +}; + +const uploadProcessedImage = async ( + imageData, + imageName, + width, + height, + quality, + prefix = GALLERY_IMAGE_PREFIX +) => { + let pipeline = sharp(imageData); + + if (width && height) { + pipeline = pipeline.resize(width, height, { fit: "cover" }); + } + + const buffer = await pipeline.jpeg({ quality }).toBuffer(); + + await s3Storage.uploadBuffer( + `${prefix}/${imageName}`, + buffer, + "image/jpeg" + ); + + return imageName; +}; + +const uploadGalleryVideo = async (fileData, fileName, contentType) => { + await s3Storage.uploadBuffer( + `${GALLERY_VIDEO_PREFIX}/${fileName}`, + fileData, + contentType + ); + + return fileName; +}; + +const uploadGalleryFile = async (fileType, file, galleryId, options = {}) => { + const fileName = `gallery_${galleryId}_${Date.now()}.${ + file.mimetype.split("/")[1] + }`; + const prefix = getPrefixForFileType(fileType); + + switch (fileType) { + case "image": + await uploadProcessedImage( + file.data, + fileName, + options.mainWidth, + options.mainHeight, + options.mainQuality, + prefix + ); + await uploadProcessedImage( + file.data, + `thumb_${fileName}`, + options.thumbWidth, + options.thumbHeight, + options.thumbQuality, + prefix + ); + break; + case "graphic": + await uploadProcessedImage( + file.data, + fileName, + null, + null, + options.mainQuality, + prefix + ); + await uploadProcessedImage( + file.data, + `thumb_${fileName}`, + options.thumbWidth, + options.thumbHeight, + options.thumbQuality, + prefix + ); + break; + case "video": + await uploadGalleryVideo(file.data, fileName, file.mimetype); + break; + } + + return fileName; +}; + +const deleteAllGalleryFiles = async (gallery) => { + const galleryType = getStoredFileName(gallery, "galleryType"); + + if (galleryType === "image") { + const images = getStoredArray(gallery, "images"); + for (const fileName of images) { + await deleteGalleryFile("image", fileName); + } + return; + } + + if (galleryType === "graphic") { + await deleteGalleryFile("graphic", getStoredFileName(gallery, "graphic")); + return; + } + + if (galleryType === "video") { + await deleteGalleryFile("video", getStoredFileName(gallery, "video")); + } +}; + +const deleteGalleryCover = async (gallery) => { + const cover = getStoredFileName(gallery, "cover"); + const thumbCover = getStoredFileName(gallery, "thumbCover"); + + if (cover) { + try { + await s3Storage.deleteObject(`${GALLERY_IMAGE_PREFIX}/${cover}`); + } catch (error) { + console.log(`Failed to delete cover from S3:`, error?.message); + } + } + + if (thumbCover) { + try { + await s3Storage.deleteObject(`${GALLERY_IMAGE_PREFIX}/${thumbCover}`); + } catch (error) { + console.log(`Failed to delete thumb cover from S3:`, error?.message); + } + } +}; + +module.exports = { + GALLERY_IMAGE_PREFIX, + GALLERY_GRAPHIC_PREFIX, + GALLERY_VIDEO_PREFIX, + getStoredFileName, + getStoredArray, + deleteGalleryFile, + uploadProcessedImage, + uploadGalleryFile, + deleteAllGalleryFiles, + deleteGalleryCover, +};