strtCoding
strtCoding

Reputation: 391

doing fs.unlink() in node to a file in a docker named volume, undeletes the object if a file with the same name is created

I'm having some weird behaviour with an application written in node.js running inside a docker container.

It sucessfully uploads and delete images using a react-admin (https://marmelab.com/react-admin/) front-end.

-rw-r--r--    1 root     root       87.2K Feb 13  2021 60283f6b7b33b304a4b6b428.webp
-rw-r--r--    1 root     root       32.2K Dec  7 01:54 60283f6b7b33b304a4b6b428_1.jpg

It even replaces a previously uploaded image with a new one if the extension is different.

-rw-r--r--    1 root     root       87.2K Feb 13  2021 60283f6b7b33b304a4b6b428.webp
-rw-r--r--    1 root     root      674.1K Dec  7 02:26 60283f6b7b33b304a4b6b428_1.png

But for some reason, if I upload an image, a totally different one than a previously uploaded, but with the same extension, resulting in a image with the same name than the first deleted image, then the said image will show, instead of the newer one.

-rw-r--r--    1 root     root       87.2K Feb 13  2021 60283f6b7b33b304a4b6b428.webp
-rw-r--r--    1 root     root       32.2K Dec  7 01:54 60283f6b7b33b304a4b6b428_1.jpg

The said application uploads files using multer as a middleware:

const multer = require("multer");
const path = require("path");
const {
  PATH_PRODUCT_TEMP_IMAGE
} = require("../services/config");

var storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, PATH_PRODUCT_TEMP_IMAGE),
  filename: (req, file, cb) => {
    if (file.fieldname === "picture")
      cb(null, `${req.params.id}${path.extname(file.originalname)}`);
    else if (file.fieldname === "picture1")
      cb(null, `${req.params.id}_1${path.extname(file.originalname)}`);
  },
});

// Filter files with multer
const multerFilter = (req, file, cb) => {
  if (file.mimetype.startsWith("image")) {
    cb(null, true);
  } else {
    cb("Not an image! Please upload only images.", false);
  }
};

const maxFileUploadSizeMb = 15;

var upload = multer({
  storage: storage,
  limits: { fileSize: maxFileUploadSizeMb * 1024 * 1024 },
  fileFilter: multerFilter,
});

module.exports = upload;

The API controller that manages the file upload is:

 router.put(
  "/:id",
  [
    auth,
    upload.fields([
      { name: "picture", maxCount: 1 },
      { name: "picture1", maxCount: 1 },
    ]),
  ],
  async (req, res) => {
    try {
      let objToUpdate = buildProduct(req.body);

      const product = await Product.findById(req.params.id);
      if (!product) throw new Error(`Product ${req.params.id} doesn't exist`);

      if (req.files.picture?.length > 0) {
        objToUpdate = {
          ...objToUpdate,
          primaryImage: req.files.picture[0].filename,
        };
        removeProductImage(product.primaryImage);
        resizeProductImage(objToUpdate.primaryImage);
      }
      if (req.files.picture1?.length > 0) {
        objToUpdate = {
          ...objToUpdate,
          image1: req.files.picture1[0].filename,
        };
        removeProductImage(product?.image1);
        resizeProductImage(objToUpdate.image1);
      }
      await product.updateOne(objToUpdate);
      res.send(product);
    } catch (error) {
      sendError500(res, error);
    }
  }
);

const removeProductImage = async (imageName) => {
  if (notNullOrEmpty(imageName))
    return await removeFile(path.join(PATH_PRODUCT_IMAGE, imageName));
};

const removeFile = async (filePathName) => {
  let result = false;
  try {
    await fs.unlink(filePathName, (error) => {
      if (error) throwError(error);
      else result = true;
    });
  } catch (error) {
    throwError(error);
  }
  return result;

  function throwError(error) {
    throw new Error(`Can't delete file: ${filePathName} - ${error.message}`);
  }
};

The entire project is running in docker using named volumes as an storage for images. Nevertheless using the same code base but working with bind mount, it works as expected.

EDIT: I've noticed that I forgot to publish a function involved in the process:

const resizeProductImage = async (imageName) => {
  if (!notNullOrEmpty(imageName)) return;

  const imageTemp = path.join(PATH_PRODUCT_TEMP_IMAGE, imageName);
  const imageFinal = path.join(PATH_PRODUCT_IMAGE, imageName);

  await resizeImage({
    imageFinal,
    imageTemp,
    imageFinalSize: +IMAGE_SIZE_PRODUCT,
  });
  await removeProductTempImage(imageName);
};
    
const resizeImage = async ({
  imageFinal,
  imageTemp,
  imageFinalSize = 1024,
}) => {
  try {
    switch (path.extname(imageTemp)) {
      case ".png":
        await sharp(imageTemp)
          .resize(imageFinalSize, null)
          .png({ adaptiveFiltering: true })
          .toFile(imageFinal, null);
        break;
      case ".webp":
      case ".jpg":
      case ".jpeg":
      default:
        await sharp(imageTemp)
          .resize(imageFinalSize, null)
          .toFile(imageFinal, null);
        break;
    }
  } catch (error) {
    try {
      await fs.rename(imageTemp, imageFinal);
      throw Error(
        `Can't resize image ${imageTemp}, moving directly to ${imageFinal}, ${error.message}`
      );
    } catch (error) {
      throw Error(
        `Can't resize image ${imageTemp}, neither move to ${imageFinal}, ${error.message}`
      );
    }
  }
};

Upvotes: 0

Views: 344

Answers (1)

strtCoding
strtCoding

Reputation: 391

I've finally figured out what the problem was.

What has been happening was a CACHE problem and not a permissions problem as I first guessed.

The problem was inside the resizeProductImage, that was using the resizeImage function, that was using the sharp node package (https://www.npmjs.com/package/sharp).

This component has a cache enabled by default, resulting in not processing (resizing in this case) a new image with the same name that was recently processed.

As the program requires to name the uploaded image with the product ID + the image extension, each time a new image with the same extension was uploaded, the sharp package used the previous, cached image instead of working with the new one.

Only one line of code, placed where the component is called, was enough to solve the problem:

sharp.cache(false);

Upvotes: 0

Related Questions