Custom Multer storage - Illegal operation when using Sharp - node.js

I'm using Multer together with Sharp to store images uploaded as part of an HTML form. I want to resize and transform the images before storing them on the disk and found this thread about how to do just that.
I thought I had set-up everything correctly, but when I try and upload an image I get:
Error: EISDIR: illegal operation on a directory, open 'C:\...\uploads'
Below is my code:
Routes.js:
var multer = require('multer');
var customStorage = require(path.join(__dirname, 'customStorage.js'));
var upload = multer({
storage: new customStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, 'uploads'));
},
filename: function (req, file, cb) {
cb(null, Date.now());
}
}),
limits: { fileSize: 5000000 }
});
...
app.use('/upload', upload.single('file'), (req, res) => { ... });
customStorage.js:
var fs = require('fs');
var sharp = require('sharp');
function getDestination (req, file, cb) {
cb(null, '/dev/null'); // >Implying I use loonix
};
function customStorage (opts) {
this.getDestination = (opts.destination || getDestination);
};
customStorage.prototype._handleFile = function _handleFile(req, file, cb) {
this.getDestination(req, file, function (err, path) {
if (err) return cb(err);
var outStream = fs.createWriteStream(path);
var transform = sharp().resize(200, 200).background('white').embed().jpeg();
file.stream.pipe(transform).pipe(outStream);
outStream.on('error', cb);
outStream.on('finish', function () {
cb(null, {
path: path,
size: outStream.bytesWritten
});
});
});
};
customStorage.prototype._removeFile = function _removeFile(req, file, cb) {
fs.unlink(file.path, cb);
};
module.exports = function (opts) {
return new customStorage(opts);
};

The error Error: EISDIR: illegal operation on a directory in this context indicates that you are setting Multer's destination to a directory when it should be the name of the destination file.
The destination is set in the line cb(null, path.join(__dirname, 'uploads')); in Routes.js. If you change this line to something like cb(null, path.join(__dirname, 'myDirectory\\mySubdirectory\\', myFilename + '.jpg')), it will work.

Related

How upload files and optimize them before upload using multer - NodeJs

I'm doing a social network using Nodejs, react and mongo. I'm using multer to upload images but I need optimize them before upload at the directory.
Multer
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, "./uploads/publications");
},
filename(req, file = {}, cb) {
const { originalname } = file;
const fileExtension = (originalname.match(/\.+[\S]+$/) || [])[0];
crypto.pseudoRandomBytes(16, function (err, raw) {
cb(null, raw.toString("hex") + Date.now() + fileExtension);
});
},
});
var mul_upload = multer({ dest: "./uploads/publications", storage });
Route
app.post(
"/publication",
[md_auth.ensureAuth, mul_upload.single("image")],
PublicationController.savePublication
);
Is it possible to compress and optimize the image before uploading it?
You need to use a npm package. sharp it can be very a very good option.
const sharp = require('sharp')
sharp(req.file).resize(200, 200).toBuffer(function(err, buf) {
if (err) return next(err)
// you can do anything with the buffer
})
With multer you can implement the custom storage function. You can check here how to do it. I am adding the sample code here:
var fs = require('fs')
function getDestination(req, file, cb) {
cb(null, '/dev/null')
}
function MyCustomStorage(opts) {
this.getDestination = (opts.destination || getDestination)
}
MyCustomStorage.prototype._handleFile = function _handleFile(req, file, cb) {
this.getDestination(req, file, function(err, path) {
if (err) return cb(err)
var outStream = fs.createWriteStream(path)
var resizer = sharp().resize(200, 200).png()
file.stream.pipe(resizer).pipe(outStream)
outStream.on('error', cb)
outStream.on('finish', function() {
cb(null, {
path: path,
size: outStream.bytesWritten
})
})
})
}
MyCustomStorage.prototype._removeFile = function _removeFile(req, file, cb) {
fs.unlink(file.path, cb)
}
module.exports = function(opts) {
return new MyCustomStorage(opts)
}
for reactJs
here is a npm package
npm i react-image-file-resizer
for NodeJs
npm i react-image-file-resizer

How to catch errors in multer

I have read documentation for multer. But the current set up I have in my code is different and this makes difficult for me to understand how to handle errors.
It is important now because it happened (only once) that a file was not stored on the server but the code continued saving info in database as if the storing of the file had worked. But it probably did not.
const multer = require('multer');
var docPath = "path_to_disk_where_to_store_files";
var storage = multer.diskStorage({
inMemory: true,
destination: function (request, file, callback) {
callback(null, docPath);
},
filename: function (request, file, callback) {
//Just a function that creates an unique name with timestamp
renamedFile = helpers.createUniqueName(file.originalname);
callback(null, renamedFile);
}
});
var fileFilter = function (req, file, cb) {
var path = require('path');
var ext = path.extname(file.originalname);
if (file.mimetype !== 'application/pdf' || ext.toLowerCase() != '.pdf') {
req.fileValidationError = 'goes wrong on the mimetype';
return cb(null, false, new Error('goes wrong on the mimetype'));
}
cb(null, true);
};
const multerUploader = multer({storage: storage, fileFilter: fileFilter, limits: { fileSize: maxSize }});
router.post('/save_document',[multerUploader.single('file'),saveDocumentInDb]);
I dont really understand where the if-statement that will check if the upload got an error would fit.
Please refer the following for error handling when using multer:
https://github.com/expressjs/multer#error-handling
Your implementation will be something like this:
const multerUploader = multer({storage: storage, fileFilter: fileFilter, limits: { fileSize: maxSize }});
const upload = multerUploader.single('file');
router.post('/save_document', function (req, res) {
upload(req, res, function (err) {
if (err instanceof multer.MulterError) {
// A Multer error occurred when uploading.
} else if (err) {
// An unknown error occurred when uploading.
}
// Everything went fine and save document in DB here.
})
})

How to resize image size in nodejs using multer

Multer have already limit size property. This property only restrict the image. Not resize the image. My question is suppose image is greater than "limit size", how to resize that image ?
var storageOptions = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'useravatars/')
},
filename: function (req, file, cb) {
cb(null, file.fieldname + '-' + Date.now())
}
});
var avatarUpload = multer({
storage: storageOptions,
limits: {
fileSize: 1000000
}
}).single("avatar");
It depends on whether you want to store the resized image as well.
In any case, you'll use a library to handle the resize operation. sharp is a very good option.
Resize in a route handler(after file is stored to disk):
sharp(req.file).resize(200, 200).toBuffer(function(err, buf) {
if (err) return next(err)
// Do whatever you want with `buf`
})
Other option would be creating your own storage engine, in this case you'll receive the file data, resize, then store to disk (copied from https://github.com/expressjs/multer/blob/master/StorageEngine.md):
var fs = require('fs')
function getDestination(req, file, cb) {
cb(null, '/dev/null')
}
function MyCustomStorage(opts) {
this.getDestination = (opts.destination || getDestination)
}
MyCustomStorage.prototype._handleFile = function _handleFile(req, file, cb) {
this.getDestination(req, file, function(err, path) {
if (err) return cb(err)
var outStream = fs.createWriteStream(path)
var resizer = sharp().resize(200, 200).png()
file.stream.pipe(resizer).pipe(outStream)
outStream.on('error', cb)
outStream.on('finish', function() {
cb(null, {
path: path,
size: outStream.bytesWritten
})
})
})
}
MyCustomStorage.prototype._removeFile = function _removeFile(req, file, cb) {
fs.unlink(file.path, cb)
}
module.exports = function(opts) {
return new MyCustomStorage(opts)
}
const path = require("path");
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, "/uploads"));
},
filename: function (req, file, cb) {
cb(null, uuid.v4() + `${path.extname(file.originalname)}`);
}
});
const limits = {
fields: 10,
fileSize: 500 * 1024,
files: 1,
};
const upload = multer({ storage, limits });
const baseUrl = "http://localhost:3000/files/";
router.post("/upload", upload.single("file"), async (ctx, next) => {
ctx.body = {
code: 1,
data: baseUrl + ctx.file.filename,
};
});

Rename uploaded images using MULTER

I am uploading the images using multer. They all are given random names (dec93b9f333c7a731723b06ce73c0bbc.jpg), which is very bad for SEO... Can you guys help me out, how to save the images with the pattern: 'fixed-name'+'random-name'.extension. Then at least part of the file would be readable for the google. Thanks!
app.set('images', '/var/www/images');
app.use(app.get('images'), express.static(app.get('images')));
var multerForImage = multer({
dest: app.get('images'),
onParseStart: function (file) {
console.log("Started parsing file stream", file);
},
onFileUploadStart: function (file) {
console.log('File recieved: ', file.originalname);
},
onFileUploadComplete: function (file, req, res) {
console.log("File upload complete");
var path = app.get('images') + "/" + file.name;
var user = req.session.user;
res.json({
success: true,
data: path
});
},
onFileUploadData: function (file, data, req, res) {
console.log('Data recieved for file upload');
},
onParseEnd: function (req, next) {
console.log("Parsing data end for file upload");
}
});
You can use the storage configuration.
app.set('images', '/var/www/images');
app.use(app.get('images'), express.static(app.get('images')));
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, app.get('images'))
},
filename: function (req, file, cb) {
const randomPart = uuidV4(); // use whatever random you want.
const extension = file.mimetype.split('/')[1];
cb(null, 'fixed-name'+ randomPart + `.${extension}`)
}
})
var multerForImage = multer({
storage: storage,
...

multer: dynamic destination path

I am writing a node application and I was looking for something to upload files on the server. I could get files to upload when there was only one static directory. But I need to make directories per user and then upload files to those, according to the user that's logged in. I looked stuff up but everything that I try ends in an Error: ENOENT: no such file or directory, open ... error. What I am trying to do currently is this -
let storage = multer.diskStorage({
destination: function(req, file, cb) {
let dest = path.join(__dirname, './documents', 'somenameigetfromtheuser');
let stat = null;
try {
stat = fs.statSync(dest);
}
catch (err) {
fs.mkdirSync(dest);
}
if (stat && !stat.isDirectory()) {
throw new Error('Directory cannot be created');
}
cb(null, dest);
}
});
let upload = multer({
storage: storage,
dest: 'documents/'
});
app.post('/testUpload', upload.single('testfile'), (req, res) => {
res.json({
test: 'test'
})
});
There is a similar question that has been answered but it doesn't work that way for me because I want the directory name from the request object.
When I remove the storage property in my multer initialization, the files are stored in the documents directory with a random name. I want the file to have its original name and I want it to be stored in a directory where I get the name of the directory from the req object.
Help a brother out, thanks!
edited
See https://github.com/expressjs/multer#diskstorage
Note that req.body might not have been fully populated yet. It depends on the order that the client transmits fields and files to the server.
Due to that, first write file in temp directory, read directory name from req and move file:
fs = require('fs-extra'); //npm install fs.extra
...
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '../tempDir/')
},
filename: function (req, file, cb) {
cb(null, file.originalname)
}
});
var upload = multer({
storage: storage
}).single('file');
upload(req, res, function (err) {
if (err) {
res.json({});
return;
}
var dir = JSON.parse(req.body.data).directory;
var filename = req.file.filename;
fs.move('../tempDir/' + fileName, '../tempDir/' + dir + '/' + fileName, function (err) {
if (err) {
return console.error(err);
}
res.json({});
});
});
Here's code for dynamic path by argument!
exports.upload = (folderName) => {
return imageUpload = multer({
storage: multer.diskStorage({
destination: function (req, file, cb) {
const path = `src/assets/uploads/${folderName}/`;
fs.mkdirSync(path, { recursive: true })
cb(null, path);
},
// By default, multer removes file extensions so let's add them back
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname));
}
}),
limits: { fileSize: 10000000 },
fileFilter: function (req, file, cb) {
if (!file.originalname.match(/\.(jpg|JPG|webp|jpeg|JPEG|png|PNG|gif|GIF|jfif|JFIF)$/)) {
req.fileValidationError = 'Only image files are allowed!';
return cb(null, false);
}
cb(null, true);
}
})
}
and call it
Make sure you append first the textfields on the client-side and only then do you append the files. In my case i had something like this:
`
for(let i=0; i<files.length;i++)
{
formData.append("files[]",files[i]);
}
formData.append("username",username);
`
The fix was to first append the textfield like so:
`
formData.append("username",username);
for(let i=0; i<files.length;i++)
{
formData.append("files[]",files[i]);
}
`
Here's what I do for uploading files to dynamic directories.
In frontend I use URL parameters to pass user IDs.
await axios({
method: 'post',
data: formData,
url: '/api/upload?userId=123',
headers: { 'content-type': 'multipart/form-data' }
})
In backend get that parameter and use for destination. Also create the directory if it doesn't exist.
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
const directory = `./public/uploads/${req.query.userId}`
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true })
}
cb(null, directory)
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`)
}
})
})
In my project I use multer as follow:
1.Store the file first in a common directory, like /tmp/.
2.Copy/move the file anywhere you want, to CDN in my case, and to a user folder in yours.
3.Remove the original file in /tmp if needed.
And maybe let upload = multer({
storage: storage,
dest: 'documents/'
}); you should remove the dest here since you specified dest in storage, right?
const storage = multer.diskStorage({
destination: function(req, file, callback){
callback(null, path.dirname('D:/') + 'Integra Qamba Site/');
},
filename: function(req, file, callback){
let data = new Date();
callback(null, dateTime +".jpg");
}
});

Resources