简体   繁体   中英

How to Prevent an Image from Saving When Submitting Form Using Multer

I am working on a Nodejs project and I am trying to use multer to store images locally. I have made my form in HTML and was able to get everything working as it should. When images are saved, they are stored in an uploads folder i created. However, I am running into the issue of images from the form being stored in an uploads folder, even when there are errors such as empty fields that cause a redirection to the form page. Is there anyway to prevent the image from saving unless the form is properly completed? Heres the link to my repo: https://github.com/halsheik/RecipeWarehouse.git . Below are the edits made to add multer into project.

// Modules required to run the application
const express = require('express');
const multer = require('multer');
const crypto = require('crypto');
const path = require('path');
const { ensureAuthenticated } = require('../config/auth');

// Creates 'mini app'
const router = express.Router();

// Models
const Recipe = require('../models/Recipe'); // Recipe Model

// Set up storage engine
const storage = multer.diskStorage({
    destination: function(req, file, callback){
        callback(null, 'public/uploads');
    },

    filename: function(req, file, callback){
        crypto.pseudoRandomBytes(16, function(err, raw) {
            if (err) return callback(err);
          
            callback(null, raw.toString('hex') + path.extname(file.originalname));
        });
    }
});

const upload = multer({
    storage: storage
});

// My Recipes
router.get('/myRecipes', ensureAuthenticated, function(req, res){
    Recipe.find({}, function(err, recipes){
        if(err){
          console.log(err);
        } else {
          res.render('./home/myRecipes', {
            recipes: recipes,
            ingredients: recipes.ingredients,
            directions: recipes.directions
          });
        }
      });
});

// Create Recipe Page
router.get('/createRecipe', ensureAuthenticated, function(req, res){
    res.render('./home/createRecipe');
});

// Create Recipe
router.post('/createRecipe', upload.single('recipeImage'), ensureAuthenticated, function(req, res){
    const { recipeName, ingredients, directions } = req.body;
    let errors = [];

    // Checks that all fields are not empty
    if(!recipeName || !ingredients || !directions){
        errors.push({ msg: 'Please fill in all fields.' });
    }

    // Checks that an image is uploaded
    if(!req.file){
        errors.push({ msg: 'Please add an image of your recipe' });
    }

    // Checks for any errors and prevents recipe creation if any
    if(errors.length > 0){
        console.log(errors);
        res.render('./home/createRecipe', {
            errors,
            recipeName,
            ingredients,
            directions
        });
    } else {
        // Create a new 'Recipe' using our model
        const newRecipe = new Recipe({
            recipeName: recipeName,
            author: req.user._id,
            ingredients: ingredients,
            directions: directions,
        }); 

        // Saves recipe to mongoDB database
        newRecipe.save().then(function(){
            res.redirect('/recipes/myRecipes');
        }).catch(function(err){
            console.log(err);
        });
    }

});

module.exports = router;
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Homemade</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div class="newRecipeContainer">
        <form action="/recipes/createRecipe" method="POST" enctype="multipart/form-data">
            <div class="recipeNameContainer">
                <label class="recipeNameLabel">Title</label>
                <input type="text" name="recipeName">
            </div>

            <div class="recipeImage">
                <input type="file" accept="image/*" name="recipeImage" onchange="validateImageFile(this);"/> 
            </div>

            <div class="ingredientsContainer">
                <button class="addIngredientButton" type="button" @click="addIngredientForm">Add Another Ingredient</button>
        
                <div class="allIngredients" v-for="(ingredient, ingredientIndex) in ingredients">
                    <label class="ingredient">{{ ingredientIndex + 1 }}.)</label>
                    <input type="text" name="ingredients" v-model="ingredient.ingredient">
                    
                    <button class="deleteIngredientButton" type="button" v-if="ingredientIndex > 0" @click="deleteIngredientForm(ingredientIndex)">Delete Ingredient</button>
                </div>
            </div>

            <div class="directionsContainer">
                <button class="addDirectionButton" type="button" @click="addDirectionForm">Add Another Direction</button>
        
                <div class="allDirections" v-for="(direction, directionIndex) in directions">
                    <label class="direction">{{ directionIndex + 1 }}.)</label>
                    <input type="text" name="directions" v-model="direction.direction">
                    
                    <button class="deleteDirectionButton" type="button" v-if="directionIndex > 0" @click="deleteDirectionForm(directionIndex)">Delete Direction</button>
                </div>
            </div>

            <button class="createRecipeButton" type="submit">Create Recipe</button>
        </form>
    </div>

    <script src="/controls/newRecipeControl.js"></script>
</body>
</html>

Thanks for any help!

I had the same problem with this for a school project I did a month back. I solved it by using multers memory storage and then persisting it myself using the buffer that multer gives. a bit of a dumb workaround, but it did the trick for me, and since you seem to have the same problem as I did, it will work for you too.

check out their documentation on how to use it. also check out how to write the buffer to a file with fs module.

EDIT:

Ok, I've found the code:

export const validateRequest = (req, res, next, schema, fileExpected = false) => {
    const options = { abortEarly: false, allowUnknown: true, stripUnknown: true };
    const { error, value } = schema.validate(req.body, options);
    const validationErrors = [];

    if (fileExpected && req.file === undefined) validationErrors.push('"prod_image" is requiered.');
    if (error) error.details.forEach(x => validationErrors.push(x.message));
    
    if (validationErrors.length > 0) {
        res.status(400).json(validationErrors);
    } else {
        req.body = value;
        next();
    }
};

since multer populates req.file and req.body at the same time, and since it needs to run before joi to handle the multipart/form-data, this is how I validate the reqest. After this, all that is left is to persist the file to disk. I did it like so:

import fs from 'fs';
import path from 'path';
import multer from 'multer';
import { randomBytes } from 'crypto';
import { srcPath } from './../settings';


const storage = multer.memoryStorage();

const fileFilter = (req, file, cb) => {
    const ext = path.extname(file.originalname);
    if (ext !== '.jpg' && ext !== '.png') return cb(new Error('Invalid image extension.'));
    cb(null, true);
};

export const upload = multer({storage: storage, fileFilter: fileFilter });

export const persistImage = (file, cb) => {
    const ext = path.extname(file.originalname);
    const newName = randomBytes(16).toString('hex') + ext;
    const imagesFolderPath = srcPath + '/productImages/';
    const finalPath = path.join(imagesFolderPath, newName);
    fs.writeFile(finalPath, file.buffer, (err) => cb(err, newName));
};

export const removeImage = (imageName, cb) => {
    const imagesFolderPath = srcPath + '/productImages/';
    const finalPath = path.join(imagesFolderPath, imageName);
    fs.unlink(finalPath, (err) => cb(err));
};

The removeImage function is needed if saving data to the database fails. This is a really bad solution in my opinion, but it was a requirement for the class. My professor considers saving images in the database evil. In a real scenario you would want to save them to something like Azures blob storage or something akin to that. That would be ideal, but my project needed the files to be saved in the project folder, soooooo.....

Many things can go wrong when doing it like this. Hope this helps, cheers.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM