1

When using Multer in an Express application, if maxCount for file uploads is set to 1:

Expected Behavior: When a single file is uploaded, req.files should be populated with the file, and the corresponding URL should be generated. Actual Behavior: When a single file is uploaded, req.files is either not populated correctly or is missing, leading to no URL being generated. However, when two files are uploaded, the URL for the first file is generated correctly.

//Controller Logic
// Allows a user to submit basic information during their first login
async function submitBasicInfo(req, res) {
  const userId = req.user.userId; // Extracted from JWT by authentication middleware
  const role = req.user.role; // Extracted from JWT by authentication middleware

  try {
    const user = await User.findById(userId);
    if (!user) {
      return res.status(404).json({ message: "User not found" });
    }

    // Log to verify incoming files
    console.log("Files received:", req.files);

    // Existing URLs
    const existingProfilePhotoUrls = user.profilePhotos || [];
    const existingProfileVideoUrls = user.profileVideos || [];
    const existingProfileDocumentUrls = user.profileDocuments || [];

    // New URLs
    const newProfilePhotoUrls = req.files.profilePhotos
      ? req.files.profilePhotos.map((file) =>
          path.join("profileUploads", role, userId.toString(), file.filename)
        )
      : [];

    const newProfileVideoUrls = req.files.profileVideos
      ? req.files.profileVideos.map((file) =>
          path.join("profileUploads", role, userId.toString(), file.filename)
        )
      : [];

    const newProfileDocumentUrls = req.files.profileDocuments
      ? req.files.profileDocuments.map((file) =>
          path.join("profileUploads", role, userId.toString(), file.filename)
        )
      : [];

    // Combine existing and new URLs
    const updatedProfilePhotoUrls =
      existingProfilePhotoUrls.concat(newProfilePhotoUrls);
    const updatedProfileVideoUrls =
      existingProfileVideoUrls.concat(newProfileVideoUrls);
    const updatedProfileDocumentUrls = existingProfileDocumentUrls.concat(
      newProfileDocumentUrls
    );

    // Log the constructed URLs for debugging
    console.log(
      `Backend - Constructed profilePhotoUrls: ${updatedProfilePhotoUrls}`
    );
    console.log(
      `Backend - Constructed profileVideoUrls: ${updatedProfileVideoUrls}`
    );
    console.log(
      `Backend - Constructed profileDocumentUrls: ${updatedProfileDocumentUrls}`
    );

    // Update user with the basic information from request body
    const updatedUser = await User.findByIdAndUpdate(
      userId,
      {
        $set: {
          profileInfo: req.body,
          profilePhotos: updatedProfilePhotoUrls,
          profileVideos: updatedProfileVideoUrls,
          profileDocuments: updatedProfileDocumentUrls,
          isFirstLogin: false,
          onboardingState: "basicInfoComplete", // Update the state to basicInfoComplete
        },
      },
      { new: true }
    );

    // Log the stored URLs and role for debugging
    console.log(`Stored profilePhotos: ${updatedUser.profilePhotos}`);
    console.log(`Stored profileVideos: ${updatedUser.profileVideos}`);
    console.log(`Stored profileDocuments: ${updatedUser.profileDocuments}`);
    console.log(`User role: ${role}`);

    // Log onboardingState for debugging
    console.log(
      `SBI- User ${updatedUser.email} updated onboardingState to: ${updatedUser.onboardingState}`
    );

    res.status(200).json({
      user: updatedUser,
      role: updatedUser.role, // Include the role in the response
      message: "Basic information updated successfully",
    });
  } catch (error) {
    console.error("Error updating user's basic information:", error);
    res.status(500).json({ message: "Internal server error" });
  }
}

//Multer Middleware

const multer = require("multer");
const path = require("path");
const fs = require("fs");

// Define upload limits for each file type
const uploadLimits = {
  profilePhotos: 1, // Max count for profilePhotos
  profileVideos: 1, // Max count for profileVideos
  profileDocuments: 1, // Max count for profileDocuments
};

// Define file size limits in bytes (1 MB = 1,000,000 bytes)
const fileSizeLimits = {
  profilePhotos: 5 * 1e6, // 5 MB for profile photos
  profileVideos: 100 * 1e6, // 100 MB for profile videos
  profileDocuments: 10 * 1e6, // 10 MB for documents
};

// Configure storage for multer
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const role = req.user.role;
    const userId = req.user.userId.toString();
    const uploadPath = path.join(
      __dirname,
      "..",
      "..",
      "mediaUploads",
      role,
      userId
    );

    // Ensure the upload directory exists
    fs.mkdirSync(uploadPath, { recursive: true });

    console.log(`Uploading file to: ${uploadPath}`);
    cb(null, uploadPath);
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
    const fileName = `${file.fieldname}-${uniqueSuffix}${path.extname(
      file.originalname
    )}`;
    console.log(`Saving file as: ${fileName}`);
    cb(null, fileName);
  },
});

// Multer middleware to handle file upload
const profileUploads = multer({
  storage,
  limits: {
    fileSize: Math.max(...Object.values(fileSizeLimits)), // Max size for the largest allowed file type
    files: Object.values(uploadLimits).reduce((a, b) => a + b, 0), // Total files limit
  },
}).fields([
  { name: "profilePhotos", maxCount: uploadLimits.profilePhotos },
  { name: "profileVideos", maxCount: uploadLimits.profileVideos },
  { name: "profileDocuments", maxCount: uploadLimits.profileDocuments },
]);

// Middleware to construct and store file URLs
const constructFileUrls = (req, res, next) => {
  try {
    const userId = req.user.userId.toString();
    const role = req.user.role;
    const basePath = "profileUploads";

    // Initialize arrays to store the file URLs
    req.body.profilePhotoUrls = [];
    req.body.profileVideoUrls = [];
    req.body.profileDocumentUrls = [];

    if (req.files) {
      // Check for and log received files
      console.log("Files received:", req.files);

      if (req.files.profilePhotos) {
        req.body.profilePhotoUrls = req.files.profilePhotos.map((file) => {
          const profilePhotoPath = path.join(
            basePath,
            role,
            userId,
            file.filename
          );
          return profilePhotoPath.replace(/\\/g, "/");
        });
        console.log(`Profile photos URLs: ${req.body.profilePhotoUrls}`);
      }

      if (req.files.profileVideos) {
        req.body.profileVideoUrls = req.files.profileVideos.map((file) => {
          const profileVideoPath = path.join(
            basePath,
            role,
            userId,
            file.filename
          );
          return profileVideoPath.replace(/\\/g, "/");
        });
        console.log(`Profile videos URLs: ${req.body.profileVideoUrls}`);
      }

      if (req.files.profileDocuments) {
        req.body.profileDocumentUrls = req.files.profileDocuments.map(
          (file) => {
            const profileDocumentPath = path.join(
              basePath,
              role,
              userId,
              file.filename
            );
            return profileDocumentPath.replace(/\\/g, "/");
          }
        );
        console.log(`Profile documents URLs: ${req.body.profileDocumentUrls}`);
      }
    }

    next();
  } catch (error) {
    console.error("Error in constructFileUrls middleware:", error);
    res
      .status(500)
      .json({ error: "Internal server error during URL construction" });
  }
};

// Middleware to validate file upload limits
const validateFileUploads = (req, res, next) => {
  const errors = [];
  const totalFiles = Object.keys(uploadLimits).reduce((count, field) => {
    return count + (req.files[field] ? req.files[field].length : 0);
  }, 0);

  if (totalFiles > Object.values(uploadLimits).reduce((a, b) => a + b, 0)) {
    errors.push(
      `Total file upload limit exceeded. Allowed: ${Object.values(
        uploadLimits
      ).reduce((a, b) => a + b, 0)}, Provided: ${totalFiles}`
    );
  }

  for (const [field, limit] of Object.entries(uploadLimits)) {
    if (req.files[field] && req.files[field].length > limit) {
      errors.push(`Field "${field}" exceeds its limit of ${limit} files.`);
    }
  }

  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }

  next();
};

// Error-handling middleware for Multer errors
const multerErrorHandler = (err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    console.error("Multer Error:", err);
    let message;
    if (err.code === "LIMIT_UNEXPECTED_FILE") {
      message = `Unexpected file field: ${err.field}`;
    } else if (err.code === "LIMIT_FILE_SIZE") {
      message = `File size limit exceeded for field: ${err.field}`;
    } else {
      message = "File upload error";
    }
    return res.status(400).json({ message, error: err.message });
  }
  console.error("Unexpected Error:", err);
  res.status(500).json({ message: "Something broke!", error: err.message });
};

module.exports = {
  profileUploads,
  constructFileUrls,
  validateFileUploads,
  multerErrorHandler,
};


router.post(
  "/submit-basic-info",
  authenticate,
  profileUploads,
  constructFileUrls,
  validateFileUploads,
  multerErrorHandler,
  submitBasicInfo
);


PostMan Keys and Values

enter image description here

When using Multer in an Express application, if maxCount for file uploads is set to 1:

Expected Behavior: When a single file is uploaded, req.files should be populated with the file, and the corresponding URL should be generated. Actual Behavior: When a single file is uploaded, req.files is either not populated correctly or is missing, leading to no URL being generated. However, when two files are uploaded, the URL for the first file is generated correctly.

2
  • Any chance that when it's one file, it's in req.file instead of req.files? Commented Aug 3, 2024 at 4:59
  • Hi, Did the answer below help ? Commented Aug 7, 2024 at 3:34

1 Answer 1

0

Please see below an MRE - minimal reproducible example. It has taken the same case of uploading a single file by setting maxCount : 1. The results show it works - it uploads the single file. Please look at the results of the test run below.

Proposal :

This MRE may rule out the chance of a bug in Multer when setting maxCount to 1. Therefore the reported bug may be answered here.

Now the actual cause of failure might be still hidden somewhere else in the code. Request you may please use this MRE to check your code again, and please revert in case the issue still persists.

server.js

const express = require('express');
const multer = require('multer');

const app = express();
const upload = multer({ dest: './uploads' });

app.post(
  '/upload',
  upload.fields([{ name: 'fileset1', maxCount: 1 }]),
  (req, res) => {
    console.log(req.files);
    res.send('done');
  }
);

const formdata = new FormData();
formdata.append('fileset1', new Blob(['some content'], { type: 'text/plain' }));

fetch('http://localhost:3000/upload', { method: 'POST', body: formdata });

app.listen(3000, () => console.log('l@3000'));

Test run

node server.js

Results:

[Object: null prototype] {
  fileset1: [
    {
      fieldname: 'fileset1',
      originalname: 'blob',
      encoding: '7bit',
      mimetype: 'text/plain',
      destination: './uploads',
      filename: '0141edfd8f0c889f36d4c99e2f1e2ecb',
      path: 'uploads/0141edfd8f0c889f36d4c99e2f1e2ecb',
      size: 12
    }
  ]
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.