How to File Uploads in CompoundJS with Express – NodeJS

mars 18th, 2012

For those who do not already know CompoundJS, Compound is the Node.JS MVC framework based on ExpressJS, fully ExpressJS-compatible. It allows you to build web applications in a similar manner as in Ruby On Rails. Handle file uploads with CompoundJS is pretty simple, now, with ExpressJS. In the beginning there were middlewares such as formidable or connect-form-sync which made things increasingly easy. But, the time as come now where uploading files do not require any extra module to work.

ExpressJS has bodyParser middleware which will parse json request bodies (as well as others), and place the result in req.body. And recently added, req.files which contains details about the files you have uploaded.

Environment file (environment.js)

Since Express is include in CompoundJS, if you don’t want to specify the upload directory, you are all set to start processing file uploads without any additional changes to environment.js. The files are uploaded to /tmp by default. If you want to make sure the files are uploaded to a specific directory, you change this

app.use(express.bodyParser());

to

app.use(express.bodyParser({uploadDir:'./uploads'}));

Here’s the full environment.js file :


var express    = require('express');

app.configure(function(){
    var cwd = process.cwd();
   
    app.use(express.static(cwd + '/public', {maxAge: 86400000}));
    app.set('views', cwd + '/app/views');
    app.set('view engine', 'ejs');
    app.set('view options', {complexNames: true});
    app.set('jsDirectory', '/javascripts/');
    app.set('cssDirectory', '/stylesheets/');
    app.use(express.bodyParser({uploadDir:'./uploads'}));
    app.use(express.cookieParser('secret'));
    app.use(express.session({secret: 'secret'}));
    app.use(express.methodOverride());
    app.use(app.router);
});

The environment.js file can be found in config directory in the CompoundJS created project.

Form file (new.ejs)

To get a better idea about how the file upload and POST submission can be handled, let’s create a form :


<% form_for(user, {action: path_to.user(user), method: 'POST', id: "file_user_form", enctype: 'multipart/form-data'}, function (form) { %>
  <div class="clearfix">
    <%- form.input('photo', {type: 'file'}) %>
    <%- form.input('id', {type: 'hidden', value: user.id }) %>
  </div>
  <div class="actions">
    <%- form.submit('Upload Photo', {class: 'btn primary'}) %> or
    <%- link_to('Cancel', path_to.user(user)+param, {class: 'btn'}) %>
  </div>
<% });%>

Routes file (routes.js)

To handle the form submission, let’s create a user route in routes.js :

map.resources('users');

Controller file (users_controller.js)

In the above example, we can access the file from the req.files.User.photo object. The object name is dependent what you set the name attribute to in your HTML form. In our case we set it as User[‘photo’], so we access it from req.files.User.photo instead of the default req.files.photo.

A file object has these commonly useful properties: size – size of the file in bytes path – the uploaded location of the file (files are renamed after uploading) name – original name of the file (as on your hard disk) type – the type of the file

To know the size of the file in our case: req.files.User.photo.size To know the original name of the file in our case: req.files.User.photo.name etc.

Note that the uploads directory is just the temporary upload directory; you will need to move the files to their respective intended locations once they are uploaded there. Following is an example of doing that. In this case we will be uploading the files to the /public/images directory.

Here is the action to handle this process in the users_controller.js file :


action('create', function () {
    uploadPhoto();
    User.create(req.body.User, function (err, user) {
        if (err) {
            flash('error', 'User can not be created');
            render('new', {
                user: user,
                title: 'New user'
            });
        } else {
            flash('info', 'User created');
            redirect(path_to.users);
        }
    });
});

function uploadPhoto() {
  if(req.files){   
    this.file = new File();
    var tmpFile = req.files.User.photo;
    var ext = tmpFile.name.substring(tmpFile.name.lastIndexOf('.')+1, tmpFile.name.length);

    this.filename = (params.id+'.'+ext.toLowerCase());
    body.User['photo'] = this.filename;

    this.file.upload(this.filename, tmpFile.path, function (err) {
        if (err) {
            console.log(err);
            this.title = 'New Photo';
            flash('error', 'Photo can not be created');
        } else {
            flash('info', 'Photo created');
        }
    }.bind(this));     
  }
}

Here we append the file extension to the user id to name the file. And we save the picture name to the user.photo field in db.

Model file (files.js)

All methods for the file upload are located in the models directory in the files.js models :


var fs = require('fs');

function File(name) {
    this.id = this.name = name;
}

File.find = function (cb) {
    var files = [];
    fs.readdir(app.root + '/public/images', function (err, fileNames) {
        fileNames.forEach(function (file) {
            files.push(new File(file));
        });
        cb(err, files);
    });
};

File.findById = function (id, cb) {
    cb(null, new File(id));
};

File.prototype.remove = function (cb) {
    fs.unlink(this.filename(), cb);
};

File.prototype.filename = function () {
    return app.root + '/public/images/' + this.name;
};

File.prototype.upload = function (name, path, cb) {
    this.name = name;
    fs.rename(path, this.filename(), cb);
};

File.prototype.rename = function (name, cb) {
    var oldPath = this.filename();
    this.name = name;
    fs.rename(oldPath, this.filename(), cb);
};

module.exports = File

Make sure to publish the class with publish(File); otherwise you will run into a ReferenceError: File is not defined.

To conclude

That’s all! Handling file uploads in CompoundJS is very easy with Express now. You need of course to run the latest version of Express. You can update all dependencies of your Compound in your project with NPM :

npm -l update

Have fun with CompoundJS !