"File" is a special type of data submitting to the server. It's encoded as multipart/form-data (i.e., binary data.) It does not like simple key/value pairs from text fields, which could be captured in params or data context of FeathersJS, we would need middleware to covert this to either params or data, and then be saved to the server.
Okay, let's build an Upload API with FeatherJS.
BTW - If you are new to FeathersJS, then please check their website here. Or a short tutorial I wrote a while ago.
As I said, we need middleware. Not sure if there is another choice, but I believe Multer is the best. Multer is a node.js middleware for handling multipart/form-data
, which is primarily used for uploading files.
Under your FeathersJS root directory, run
npm install multer
FeatherJS CLI is really a life-saver. If you have not used it before, then you must try it out. Under the root directory, run:
feathers generate service
Choose custom service, and named it something like, “upload”, and then http://localhost:3030/upload, will be your Upload API's endpoint.
If you name your service “upload”, then you should see the new folder created under “ROOT/src/services”. The next is modifying some fields to integrate with Multer.
Let's add middleware to transfer the received files to feathers.
const multer = require("multer");
const multipartMiddleware = multer();
module.exports = function (app) {
// some options which cand be defined in config/default.json or production.json
const options = {
fileServer: app.get("fileServer"),
filePath: app.get("filePath"),
imageMaxWidth: app.get("imageMaxWidth"), // for image upload only
imageMinWidth: app.get("imageMinWidth"), // for image upload only
};
// Initialize our service with any options it requires
app.use(
"/upload",
multipartMiddleware.array("files"),
// another middleware, this time to
// transfer the received files to feathers
function (req, res, next) {
req.feathers.files = req.files;
next();
},
createService(options)
);
// Get our initialized service so that we can register hooks
const service = app.service("upload");
service.hooks(hooks);
};
Note that, if you only want to support single file upload, then you can use multpartMiddleare.single("file")
. Obviously, multpartMiddleare.array("files")
would receive the multiple files and save them in an array.
Thanks to ‘Multer’, and now “files” are available in the params context. Note that req.feathers
set the properties on the params instead of data per FeathersJS doc
All middleware registered after the REST transport will have access to the
req.feathers
object to set properties on the service methodparams
If you really want the data of the files being saved in the data context, you can have a hook to convert it, but I don't see it's necessary.
Let's implement ‘create
’ function in the upload.class.js to access/save the files uploaded.
async create(data, params) {
const uploadedFiles = params.files; //note that the file in in params
const allPromises = uploadedFiles.map(async (dataFile) => {
const { originalname, buffer, mimetype } = dataFile;
const { fileServer, filePath, imageMaxWidth, imageMinWidth } =
this.options;
const ts = new Date().getTime();
const filename =
ts +
originalname
.toLowerCase()
.replace(/[^a-z0-9. -]/g, "") // remove invalid chars
.replace(/\s+/g, "-") // collapse whitespace and replace by -
.replace(/-+/g, "-");
try {
await uploadToLocal(
filePath,
filename,
buffer,
mimetype,
imageMaxWidth,
imageMinWidth
);
} catch (error) {
console.log("Upload Error");
}
return {
name: originalname,
url: fileServer + "/" + filename,
thumbnail: fileServer + "/sm-" + filename, //only True for image
};
});
const responses = Promise.all(allPromises);
return responses;
}
Note every single file received contains the following attributes: originalname, buffer, and mimetype
const { originalname, buffer, mimetype } = dataFile;
Then you can save ‘buffer’ to anywhere we like, local or cloud. And in my case, I return the filename and URL. Note that the thumbnail attribute is for the image only.
You see I use ‘uploadToLocal’ function. which is something like this for your reference:
const sharp = require("sharp");
const fs = require("fs").promises;
const uploadToLocal = async (
uploadFolderPath,
fileName,
fileBuffer,
fileType,
maxWidth,
minWidth
) => {
if (fileType && fileType.substr(0, 5).toLowerCase() === "image") {
try {
await sharp(fileBuffer, { withoutEnlargement: true })
.resize(maxWidth)
.toFile(uploadFolderPath + "/" + fileName);
await sharp(fileBuffer, { withoutEnlargement: true })
.resize(minWidth)
.toFile(uploadFolderPath + "/" + "sm-" + fileName);
} catch (error) {
console.log(error);
return false;
}
} else {
try {
await fs.writeFile(uploadFolderPath + "/" + fileName, fileBuffer);
} catch (error) {
console.log(error);
return false;
}
}
return true;
};
module.exports = uploadToLocal;
This should be straightforward, just note that my uploader also does resize for image files, which uses another NPM package - sharp
That's it, and you should be able to upload the File via this API now. Please leave the comments below if you have any questions or you would like to see how the client code looks like and I will write the article about that when I get a chance.