Search code examples
javascriptnode.jsmulter

Custom file name from frontend in Multer


I'm uploading a file using FormData and receiving it server-side using Multer. Everything works as expected, except since I'm using FileSystem API on the front-end (https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry), the files I'm uploading come from sub-directories. Multer seems to only see the filename, even if I explicitly set an alias for the file as I append it to form data (https://developer.mozilla.org/en-US/docs/Web/API/FormData/append). It also seems like Multer performs its logic prior to the rest of my request handler and does not see the parameters I set on the body. How do I get multer to see the full path?

Here is a simplified version of what I currently have setup:

Client (alias represents full name with path, file.name is the base name automatically set by FileSystem API):

function upload(file, alias) {
    let url = window.location.origin + '/upload';
    let xhr = new XMLHttpRequest();
    let formData = new FormData();
    xhr.open('POST', url, true);

    return new Promise(function (resolve, reject) {

        xhr.addEventListener('readystatechange', function(e) {
            if (xhr.readyState == 4 && xhr.status == 200) {
                resolve(file.name);
            }
            else if (xhr.readyState == 4 && xhr.status != 200) {
                reject(file.name);
            }
        })

        formData.append('file', file, alias || file.name); // this should in theory replace filename, but doesn't
        formData.append('alias', alias || file.name); // an extra field that I can't see in multer function at all
        xhr.send(formData);
    });
}

Server:

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, 'uploads/');
    },
    filename: function (req, file, cb) {
        // neither req nor file seems to contain any hint of the alias here
        cb(null, file.originalname);
    }
});
const upload = multer({storage: storage});
const bodyParser = require('body-parser');
app.use(bodyParser.json());

app.post('/upload', upload.single('file'), function (req, res, next) {
    // by this time the file seems to already be on disk with whatever name multer picked
    if (req.file) {
        res.status(200).end();
    } else {
        res.status(500).end();
    }
});

Solution

  • In order to get this to work, use the preservePath option when configuring multer. The following will work:

    const upload = multer({storage: storage, preservePath: true});
    

    However, it's important to note, multer will not create the directories or subdirectories. Those have to be created beforehand. (I tested this too. If directories are created and empty, upload succeeds, however, if directories do not exist, uploads fail).

    In their readme, they say: "Note: You are responsible for creating the directory when providing destination as a function. When passing a string, multer will make sure that the directory is created for you."

    A follow-up to that note would be: "you are responsible for creating any sub-directories too".

    The relative paths of files uploaded will be accessible in originalname property. So, backend would look like this: (as you had it, but with updated comments)

    const storage = multer.diskStorage({
        destination: function (req, file, cb) {
            cb(null, 'uploads/');
        },
        filename: function (req, file, cb) {
            // If you uploaded for example, the directory: myDir/myFile.txt,
            // file.originalname *would* be set to that (myDir/myFile.txt)
            // and myFile.txt would get saved to uploads/myDir
            // *provided that* uploads/myDir already exists.
            // (if it doesn't upload will fail)
            // /* if(  [ uploads/myDir doesn't exist ] ) { mkdir } */
            cb(null, file.originalname);
        }
    });
    

    Helpful tip: On the front end, I found it easier to test directory / subdirectory upload with: (tested on Chrome latest ok)

    <form action="/uploads/multipleFiles" method="post" enctype="multipart/form-data">
          <input type="file" name="multiple" webkitdirectory accept="text/*" onchange="console.log(this.files)" />
          <input type="text" name="tester" value="uploadTester" />
          <input type="submit"/>
    </form>