From 0afcf156ee53b1b7daabdc28495403225dc85ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20L=C3=A1szl=C3=B3?= <rlacko99@gmail.com> Date: Thu, 24 Dec 2020 15:01:43 +0100 Subject: [PATCH] upload, get file and update profile picture --- src/index.ts | 3 + src/middlewares/files/createFile.ts | 41 +++++++++++ .../files/handleFileValidationError.ts | 20 ------ .../files/{fileFilter.ts => imageFilter.ts} | 10 +-- src/middlewares/files/imageStorage.ts | 25 +++++++ .../files/profile/getProfilePicture.ts | 26 ------- .../files/profile/profilePictureStorage.ts | 19 ----- .../files/profile/uploadProfilePicture.ts | 45 ------------ src/middlewares/files/sendFile.ts | 25 +++++++ src/middlewares/files/updateProfilePicture.ts | 34 +++++++++ src/middlewares/files/updateTermCardBg.ts | 37 ++++++++++ src/routes/files.ts | 71 +++++++++++++++---- 12 files changed, 229 insertions(+), 127 deletions(-) create mode 100644 src/middlewares/files/createFile.ts delete mode 100644 src/middlewares/files/handleFileValidationError.ts rename src/middlewares/files/{fileFilter.ts => imageFilter.ts} (54%) create mode 100644 src/middlewares/files/imageStorage.ts delete mode 100644 src/middlewares/files/profile/getProfilePicture.ts delete mode 100644 src/middlewares/files/profile/profilePictureStorage.ts delete mode 100644 src/middlewares/files/profile/uploadProfilePicture.ts create mode 100644 src/middlewares/files/sendFile.ts create mode 100644 src/middlewares/files/updateProfilePicture.ts create mode 100644 src/middlewares/files/updateTermCardBg.ts diff --git a/src/index.ts b/src/index.ts index c1dba75c..64167195 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import expressSession from "express-session"; import filesRoute from "./routes/files"; import mongoose from "mongoose"; import morgan from "morgan"; +import multer from "multer"; import newsRoute from "./routes/news"; import notificationsRoute from "./routes/notifications"; import termsRoute from "./routes/terms"; @@ -81,6 +82,8 @@ app.use((err: any, req: Request, res: Response, next: NextFunction) => { if (err instanceof ErrorHandler) return handleError(err, res); if (err instanceof mongoose.Error.CastError) return handleError(new ErrorHandler(400, err.message), res); + if (err instanceof multer.MulterError) + return handleError(new ErrorHandler(400, err.message), res); //Flush out the stack to the console console.error(err.stack); diff --git a/src/middlewares/files/createFile.ts b/src/middlewares/files/createFile.ts new file mode 100644 index 00000000..6e0028ac --- /dev/null +++ b/src/middlewares/files/createFile.ts @@ -0,0 +1,41 @@ +import { NextFunction, Request, Response } from "express"; + +import { ErrorHandler } from "../utils/ErrorHandler"; +import File from "../../models/FileSchema"; +import Profile from "../../models/ProfileSchema"; +import fs from "fs"; + +/** + * Upload a new profile picture + */ +const createFile = () => async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const newFile = new File(); + + newFile.originalName = req.file.originalname; + newFile.uploadDate = new Date(); + newFile.path = req.file.path; + newFile.mimeType = req.file.mimetype; + try { + await newFile.save(); + } catch (e) { + fs.unlinkSync(req.file.path); + throw new ErrorHandler( + 500, + "There was an error while recording the file in the Database!" + ); + } + + res.data.value = newFile._id; + + next(); + } catch (err) { + next(err); + } +}; + +export default createFile; diff --git a/src/middlewares/files/handleFileValidationError.ts b/src/middlewares/files/handleFileValidationError.ts deleted file mode 100644 index 0bdf0f87..00000000 --- a/src/middlewares/files/handleFileValidationError.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextFunction, Request, Response } from "express"; - -/** - * Middleware that handles errors after uploading a file - */ -const handleFileValidationError = () => async ( - req: Request, - res: Response, - next: NextFunction -) => { - if (req.fileValidationError) { - return res.status(400).send(req.fileValidationError); - } - if (!req.file) { - return res.status(400).send("File wasn't provided!"); - } - next(); -}; - -export default handleFileValidationError; diff --git a/src/middlewares/files/fileFilter.ts b/src/middlewares/files/imageFilter.ts similarity index 54% rename from src/middlewares/files/fileFilter.ts rename to src/middlewares/files/imageFilter.ts index 28c4e66a..4e5d8885 100644 --- a/src/middlewares/files/fileFilter.ts +++ b/src/middlewares/files/imageFilter.ts @@ -1,20 +1,20 @@ -import multer, { FileFilterCallback, Multer } from "multer"; +import { ErrorHandler } from "../utils/ErrorHandler"; +import { FileFilterCallback } from "multer"; /** - * multer fileFilter that only lets png or jpeg to be uploaded + * multer imageFilter that only lets png or jpeg to be uploaded * * @param {Express.Request} req * @param {Express.Multer.File} file * @param {FileFilterCallback} cb */ -export function fileFilter( +export function imageFilter( req: Express.Request, file: Express.Multer.File, cb: FileFilterCallback ) { if (file.mimetype !== "image/png" && file.mimetype !== "image/jpeg") { - req.fileValidationError = "Invalid file type!"; - return cb(null, false); + return cb(new ErrorHandler(400, "Invalid file type!")); } else { cb(null, true); } diff --git a/src/middlewares/files/imageStorage.ts b/src/middlewares/files/imageStorage.ts new file mode 100644 index 00000000..abf680ce --- /dev/null +++ b/src/middlewares/files/imageStorage.ts @@ -0,0 +1,25 @@ +import multer from "multer"; +/** + * Multer DiskStorage configuration + * @param location where to save the file + */ +export const imageStorage = (location: string) => + multer.diskStorage({ + destination: function (req, file, callback) { + callback(null, location); + }, + filename: function (req, file, callback) { + const fileExtension = file.originalname.substring( + file.originalname.lastIndexOf(".") + ); + callback( + null, + Date.now() + + "-" + + Math.random().toString(36).substring(7) + + fileExtension + ); + }, + }); + +export default imageStorage; diff --git a/src/middlewares/files/profile/getProfilePicture.ts b/src/middlewares/files/profile/getProfilePicture.ts deleted file mode 100644 index 660beab1..00000000 --- a/src/middlewares/files/profile/getProfilePicture.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextFunction, Request, Response } from "express"; - -import File from "../../../models/FileSchema"; - -/** - * Get Profile picture file from res.data.profile - */ -const getProfilePicture = () => async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - if (!res.data.profile!.pictureId) - return res - .status(400) - .send({ message: "You dont have a profile picture!" }); - - const file = await File.findById(res.data.profile!.pictureId).lean().exec(); - return res.sendFile(file!.path, { root: "." }); - } catch (err) { - next(err); - } -}; - -export default getProfilePicture; diff --git a/src/middlewares/files/profile/profilePictureStorage.ts b/src/middlewares/files/profile/profilePictureStorage.ts deleted file mode 100644 index 3ce61b9e..00000000 --- a/src/middlewares/files/profile/profilePictureStorage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import multer from "multer"; -import path from "path"; - -export const profilePictureStorage = multer.diskStorage({ - destination: function (req, file, callback) { - callback(null, "uploads/profile_pictures/"); - }, - filename: function (req, file, callback) { - const fileExtension = file.originalname.substring( - file.originalname.lastIndexOf(".") - ); - callback( - null, - Date.now() + "-" + Math.random().toString(36).substring(7) + fileExtension - ); - }, -}); - -export default profilePictureStorage; diff --git a/src/middlewares/files/profile/uploadProfilePicture.ts b/src/middlewares/files/profile/uploadProfilePicture.ts deleted file mode 100644 index d1866729..00000000 --- a/src/middlewares/files/profile/uploadProfilePicture.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextFunction, Request, Response } from "express"; - -import { ErrorHandler } from "../../../middlewares/utils/ErrorHandler"; -import File from "../../../models/FileSchema"; -import Profile from "../../../models/ProfileSchema"; -import fs from "fs"; - -/** - * Upload a new profile picture to a given User in res.data.profile - */ -const uploadProfilePicture = () => async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - if (!res.data.profile) throw new ErrorHandler(400, "User not found!"); - if (res.data.profile.pictureId) { - const oldFile = await File.findByIdAndRemove(res.data.profile.pictureId) - .lean() - .exec(); - if (oldFile) await fs.unlinkSync(oldFile!.path); - } - - const newFile = new File(); - - newFile.originalName = req.file.originalname; - newFile.uploadDate = new Date(); - newFile.path = req.file.path; - newFile.mimeType = req.file.mimetype; - - await newFile.save(); - - await Profile.updateOne( - { _id: res.data.profile!._id }, - { pictureId: newFile._id } - ).exec(); - - return res.status(200).send(); - } catch (err) { - next(err); - } -}; - -export default uploadProfilePicture; diff --git a/src/middlewares/files/sendFile.ts b/src/middlewares/files/sendFile.ts new file mode 100644 index 00000000..4103093f --- /dev/null +++ b/src/middlewares/files/sendFile.ts @@ -0,0 +1,25 @@ +import { NextFunction, Request, Response } from "express"; + +import { ErrorHandler } from "../utils/ErrorHandler"; +import File from "../../models/FileSchema"; + +/** + * Return the file with id = res.data.value + */ +const sendFile = (): any => async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const file = await File.findById(res.data.value).lean().exec(); + + if (!file) throw new ErrorHandler(404, "File not found!"); + + return res.sendFile(file.path, { root: "." }); + } catch (err) { + next(err); + } +}; + +export default sendFile; diff --git a/src/middlewares/files/updateProfilePicture.ts b/src/middlewares/files/updateProfilePicture.ts new file mode 100644 index 00000000..1e432d01 --- /dev/null +++ b/src/middlewares/files/updateProfilePicture.ts @@ -0,0 +1,34 @@ +import { NextFunction, Request, Response } from "express"; + +import File from "../../models/FileSchema"; +import Profile from "../../models/ProfileSchema"; +import fs from "fs"; + +/** + * Upload a new profile picture + */ +const updateProfilePicture = () => async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + if (res.data.profile!.picture) { + const oldFile = await File.findByIdAndRemove(res.data.profile!.picture) + .lean() + .exec(); + if (oldFile) fs.unlinkSync(oldFile!.path); + } + + await Profile.updateOne( + { _id: res.data.profile!._id }, + { picture: res.data.value! } + ).exec(); + + next(); + } catch (err) { + next(err); + } +}; + +export default updateProfilePicture; diff --git a/src/middlewares/files/updateTermCardBg.ts b/src/middlewares/files/updateTermCardBg.ts new file mode 100644 index 00000000..8e7e5c0c --- /dev/null +++ b/src/middlewares/files/updateTermCardBg.ts @@ -0,0 +1,37 @@ +import { NextFunction, Request, Response } from "express"; + +import File from "../../models/FileSchema"; +import Profile from "../../models/ProfileSchema"; +import Term from "../../models/TermSchema"; +import fs from "fs"; + +/** + * Upload a new profile picture + */ +const uploadTermCardBg = () => async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + if (res.data.term!.backgroundFile) { + const oldFile = await File.findByIdAndRemove( + res.data.term!.backgroundFile + ) + .lean() + .exec(); + if (oldFile) fs.unlinkSync(oldFile!.path); + } + + await Term.updateOne( + { _id: res.data.term!._id }, + { backgroundFile: res.data.value! } + ).exec(); + + next(); + } catch (err) { + next(err); + } +}; + +export default uploadTermCardBg; diff --git a/src/routes/files.ts b/src/routes/files.ts index fe7fd3a9..bc88876a 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -1,23 +1,31 @@ +import profilePictureStorage, { + imageStorage, +} from "../middlewares/files/imageStorage"; + import { Application } from "express"; import cardImageStorage from "../middlewares/files/card/cardImageStorage"; +import createFile from "../middlewares/files/createFile"; import example from "../middlewares/example"; -import { fileFilter } from "../middlewares/files/fileFilter"; import getFieldValue from "../middlewares/utils/getFieldValue"; import getUser from "../middlewares/user/getUser"; +import { imageFilter } from "../middlewares/files/imageFilter"; import isLoggedIn from "../middlewares/auth/isLoggedIn"; import isRegistered from "../middlewares/auth/isRegistered"; import isStaffOrAdmin from "../middlewares/auth/isStaffOrAdmin"; import multer from "multer"; -import profilePictureStorage from "../middlewares/files/profile/profilePictureStorage"; +import noContentResponse from "../middlewares/utils/noContentResponse"; +import sendFile from "../middlewares/files/sendFile"; +import setOwnUserId from "../middlewares/user/setOwnUserId"; +import updateProfilePicture from "../middlewares/files/updateProfilePicture"; const profilePictureUpload = multer({ - storage: profilePictureStorage, - fileFilter, + storage: imageStorage("uploads/profile_pictures/"), + fileFilter: imageFilter, }); const CardImageUpload = multer({ - storage: cardImageStorage, - fileFilter, + storage: imageStorage("uploads/card_image/"), + fileFilter: imageFilter, }); const filesRoute = (prefix: string, app: Application): void => { @@ -28,22 +36,61 @@ const filesRoute = (prefix: string, app: Application): void => { isStaffOrAdmin(), getUser(), getFieldValue("profile", "picture"), - example() + sendFile() ); // Get a users accepted picture - app.get(`${prefix}/user/:userId/picture/accepted`, example()); + app.get( + `${prefix}/user/:userId/picture/accepted`, + isRegistered(), + isStaffOrAdmin(), + getUser(), + getFieldValue("profile", "acceptedPicture"), + sendFile() + ); // Get own picture - app.get(`${prefix}/me/picture`, example()); + app.get( + `${prefix}/me/picture`, + isRegistered(), + setOwnUserId(), + getUser(), + getFieldValue("profile", "picture"), + sendFile() + ); // Get own accepted picture - app.get(`${prefix}/me/picture/accepted`, example()); + app.get( + `${prefix}/me/picture/accepted`, + isRegistered(), + setOwnUserId(), + getUser(), + getFieldValue("profile", "acceptedPicture"), + sendFile() + ); // Get a users card in a Term app.get(`${prefix}/user/:userId/card/term/:termId`, example()); // Get own card in a Term app.get(`${prefix}/me/card/term/:termId`, example()); // Add a profile picture to a user - app.post(`${prefix}/user/:userId/picture`, example()); + app.post( + `${prefix}/user/:userId/picture`, + isRegistered(), + isStaffOrAdmin(), + getUser(), + profilePictureUpload.single("profile_picture"), + createFile(), + updateProfilePicture(), + noContentResponse() + ); // Add own profile picture - app.post(`${prefix}/me/picture`, example()); + app.post( + `${prefix}/me/picture`, + isRegistered(), + setOwnUserId(), + getUser(), + profilePictureUpload.single("profile_picture"), + createFile(), + updateProfilePicture(), + noContentResponse() + ); // Get a Term's card background image app.get(`${prefix}/card/bg/term/:termId`, example()); // Add a Term's card background image or update it -- GitLab