Reputation: 391
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
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