Showing posts with label nodejs. Show all posts
Showing posts with label nodejs. Show all posts

Sunday, August 25, 2013

Uninstall NodeJS from MacOSX

Open your terminal.

Find where nodejs is installed by:
which node
In my case, it's in /usr/local/bin/node

Go to the folder that contains /bin/node
cd /usr/local
Remove all node related stuffs
sudo rm -rf bin/node bin/node-waf include/node lib/node lib/pkgconfig/nodejs.pc share/man/man1/node.1

Saturday, April 27, 2013

NodeJS - simulating a multi-file upload request


Story:

I was building a NodeJS client application that connects to a backend Rest API. One of the functionalities requires uploading multiple files to the server.


Concept:

We will need to upload the files via a POST request to the server. The request will look like the following:


POST /cgi-bin/qtest HTTP/1.1
Host: aram
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://aram/~martind/banner.htm
Content-Type: multipart/form-data; boundary=---------------------------287032381131322
Content-Length: 582

-----------------------------287032381131322
Content-Disposition: form-data; name="datafile1"; filename="r.gif"
Content-Type: image/gif

GIF87a.............,...........D..;
-----------------------------287032381131322
Content-Disposition: form-data; name="datafile2"; filename="g.gif"
Content-Type: image/gif

GIF87a.............,...........D..;
-----------------------------287032381131322
Content-Disposition: form-data; name="datafile3"; filename="b.gif"
Content-Type: image/gif

GIF87a.............,...........D..;
-----------------------------287032381131322--

I took this above example from What should a Multipart HTTP request with multiple files look like?


The number 287032381131322 is a random number.

-----------------------------287032381131322 is a boundary that separates individual data fields.

Notice that there's always a "\r\n" appending the -----------------------------287032381131322.

The last -----------------------------287032381131322 will contain an extra "--" at the end.


Implementation:

We will create a function to generate the above request. The files that will be uploaded are called thumbnail, bigThumbnail, and photo.

I will be using the NodeJS library "Sequence" to make the code to follow a sequential order. Don't worry about it if you don't understand it.

Here's the code:

FileProvider.prototype.save = function(req, callback) {

var files = req.files;
var title = req.body.title;

if(files.length == 0) {
console.log("nothing uploaded");
return callback(500, null);
}

var thumbnail = files.thumbnail;
        var bigThumbnail = files.bigThumbnail;
var photo = files.photo;
 
var boundary = Math.random();

var post_data = [];
 
post_data.push(new Buffer(httpRequest.encodeFieldPart(boundary, 'title', title), 'ascii'));

  var sequence = Futures.sequence();

  sequence.then(function(next) {
  post_data.push(new Buffer(httpRequest.encodeFilePart(boundary, thumbnail.type, 'thumbnail', thumbnail.name), 'ascii'));
  var file_reader = fs.createReadStream(thumbnail.path, {encoding: 'binary'});
  var file_contents = '';

  file_reader.on('data', function(data){
    file_contents += data;
  });

  file_reader.on('end', function(){

    post_data.push(new Buffer(file_contents, 'binary'));
    post_data.push(new Buffer("--" + boundary + "\r\n"), 'ascii');

  next();
  });
})
.then(function(next) {
post_data.push(new Buffer(httpRequest.encodeFilePart(boundary, photo.type, 'digitalImage', photo.name), 'ascii'));
  var file_reader = fs.createReadStream(photo.path, {encoding: 'binary'});
  var file_contents = '';

  file_reader.on('data', function(data){
    file_contents += data;
  });

  file_reader.on('end', function(){

    post_data.push(new Buffer(file_contents, 'binary'));
    post_data.push(new Buffer("--" + boundary + "--\r\n"), 'ascii');

  next();
  });
})
.then(function(next) {

var dataLength = 0;
for(var i = 0; i < post_data.length; i++) {
    dataLength += post_data[i].length;
  }

var options = {
host: constants.SERVER_HOST,
port: constants.SERVER_PORT,
path: constants.SERVER_UPLOAD_PATH,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': dataLength,
            'Cookie': req.session.user.authCookie
}
};

   httpRequest.post(req, options, post_data, function(res, data) {
    var response = null;
if(res.statusCode == 200) {
try {
response = JSON.parse(data);
} catch (err) {
console.log("FileProvider.save error: " + err);
}
callback(res.statusCode, response);
} else {
callback(res.statusCode, null);
}
});
});

};


Helper Library:


HttpRequest.prototype.post = function(request, options, postData, callback) {

// disable socket pooling
options.agent = false;

var req = http.request(options, function(res) {

console.log('RESPONSE STATUS: ' + res.statusCode);
console.log('RESPONSE HEADERS: ' + JSON.stringify(res.headers));

res.setEncoding('utf8');

var responseData = '';

res.on('data', function (data) {
responseData += data;
});

res.on('end', function () {



callback(res, responseData);
});

});

req.setTimeout(100000, function() {
console.log('problem with request: timeout');
callback(408, null);
});

req.on('error', function(e) {
  console.log('problem with request: ' + e.message);
  callback(599, null);
});

if(postData instanceof Array) {
for (var i = 0; i < postData.length; i++) {
    req.write(postData[i]);
   }
} else {
req.write(postData);
}

req.end();

};

HttpRequest.prototype.encodeFieldPart = function(boundary,name,value) {
    var return_part = "--" + boundary + "\r\n";
    return_part += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n";
    return_part += value + "\r\n";
    return return_part;
}

HttpRequest.prototype.encodeFilePart = function(boundary,type,name,filename) {
    var return_part = "--" + boundary + "\r\n";
    return_part += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n";
    return_part += "Content-Type: " + type + "\r\n\r\n";
    return return_part;
}

Wednesday, February 13, 2013

NodeJS and ExpressJS - Tools and Code Structure

The purpose of this post is to present a set of tools and frameworks for rapid web development and a way to organize your code.

Tools and Frameworks

Development Tools
  • Sublime Text for coding

Frameworks
  • NodeJS
  • ExpressJS - API for web development

View/Template Technology
  • ejs - very close to html and easy learning curve
  • ejs-locals - supports decorator pattern

Libraries
  • futures - NodeJS is an event driven single threaded framework and it's easy to have multiple nested callbacks that make things look very spaghetti
  • i18next - localization both server and client sides
  • express-validator - provides validation and santization
  • string - provides common string operations like truncate
  • nodetime - NodeJS profiling

Code Organization

The following is the project structure I usually use. The "/" suffice means that it's a folder.  "-->" means it's inside a folder, while "---->" means it's inside the 2nd level of a folder.

root_folder/
-->providers/
-->models/
-->routers/
-->views/
---->partials/
---->mobile/
---->layouts/
->utils/
-->locales/
-->public/
---->images/
---->javascripts/
---->stylesheets/
-->app.js
-->app_mobile.js

The NodeJS application is located in the root_folder/ above.

providers/ is the business logic or data access layer.

models/ stores object entities.

routers/ stores controllers. I could have called this controllers/.

views/ stores the templates. All the default templates are stored inside this parent folder. I store the mobile templates inside the views/mobile/ folder. views/partials/ stores components like header, footer,  or templates that both the web and mobile share, etc.

utils/ stores utility functions that can be used anything in the application. (Ex. HttpRequest, string operations)

locales/ stores files for i18n.

public/ stores static assets like images, javascripts, and css stylesheets.

app.js and app_mobile.js store routers and global configurations for the web and mobile versions of the applications respectively.


Application Entry Point Configuration:

In the above folder structure, app.js and app_mobile.js controls the application flow. I have put some comments in the important parts of the code snippet.

// include the default libraries

var express = require('express');
  engine = require('ejs-locals'),
  i18n = require("i18next");

// include your routers/controllers

var loginRouter = require('./routers/loginRouter');
var postRouter = require('./routers/PostRouter');

// i18n settings

var i18nOptions = {
  fallbackLng: 'en',
  resSetPath: 'locales/__lng__/__ns__.json',
  detectLngFromPath: 0,
  saveMissing: true,
  debug: false
};

i18n.init(i18nOptions);

// Server Configuration

var app = module.exports = express();
app.engine('ejs', engine);

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.set('view options', {layout: 'mobile/layout.ejs'});
  app.set('mobile_prefix', 'mobile/');
  app.use(express.bodyParser());
  app.use(i18n.handle);
  app.use(expressValidator);
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({ secret: 'your_secret_code', cookie: { maxAge: 31536000000} }));

  app.use(flash());

  app.use(function(req, res, next){

    res.locals.currentUser = req.session.user;
    res.locals.currentHost = req.header('host');
    res.locals.currentUrl = req.url;
    res.locals.currentLocale = '/' + i18n.lng();
    res.locals.isMobile = true;
    
    /* 
    if your image assets are language dependent, you can save them in folders locales/en/, locales/es
    */
    if(i18n.lng() == 'en') {
      res.locals.currentImageDir = '';
    } else {
      res.locals.currentImageDir = '/' + i18n.lng();
    }

    next();
  });

  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

i18n.registerAppHelper(app)
    .serveDynamicResources(app);

i18n.serveWebTranslate(app, {
    i18nextWTOptions: {
      languages: ['fr', 'en'],
      }
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

var requireRole = function(role) {
  return function(req, res, next) {
    if(req.session.user != null && req.session.user.role === role)
      next();
    else
      res.redirect('/' + req.i18n.lng() + '/login');
  }
};

var requireAuth = function() {
  return function(req, res, next) {

    if(req.session.user != null) {
      next();
    } else {
      console.log('Redirect link: ' + req.path);
      req.session.redirect_to = req.path;
      res.redirect('/' + req.i18n.lng() + '/login');
    }
  }
};

app.get('/:lng/login', loginRouter.login);
app.post('/:lng/login', loginRouter.submitLogin);
app.get('/:lng/posts', requireAuth(), postRouter.listAllPosts);

app.configure('production', function(){
  app.use(express.errorHandler());
});

http.createServer(app).listen(8080, function(){
  console.log("Server listening on port %d in %s mode", 8080, app.settings.env);
});

Tuesday, January 22, 2013

How to build a NodeJS AMI on EC2

This demo will provide guidelines on how to configure a NodeJS EC2 instance and create a NodeJS AMI on Ubuntu.

Specs:

Ubuntu Server 12.04.1 LTS 64-bit


Create a Ubuntu Server 12.04.1 LTS 64-bit t1.micro instance


Uncheck delete on termination for the EBS-root disc.

Create a Security Group called Node JS Production (or anything you want).

Add port 22, 80, 443, 3000 to the Security Group. (I am adding port 3000 because I run the app from port 3000)

Launch the instance.

In the AWS Management Console, Volumes -> Create Volume.

Make the volume with
  • type = Standard
  • Size = 20GB
  • Availability Zone must match the EC2's Availability Zone
  • make the drive name xvdf

Associate this EBS with the EC2 instance we just created.

ssh into your instance.
Ex. ssh -i {key} ubuntu@{ec2-address}.compute-1.amazonaws.com
sudo apt-get update

We are going to format the xvdf with XFS file system. Refer to Amazon EC2 - Mounting a EBS drive.


Install NodeJS and other dependencies

sudo apt-get -y nodejs npm

If you run "node --version", you will find the node version is 0.6.12. We want to use 0.8.18, since it's a lot faster.

sudo npm install -g n
sudo n 0.8.18

Now "sudo node --version" will show version 0.8.18 while "node --version" will show 0.6.12


Install Git and fetch your code (Optional)

sudo apt-get install git -y
mkdir /vol/src
cd /vol/src

git config --global user.name "your_name"
git config --global user.email "your_email"
git config --global github.user "your_github_login"
git clone ssh://git@github.com/username/repo.git

You will want to establish a connection with Github using ssh rather than https because if you are building an image that can be used for auto-scaling you don't want to input the username and password every time. See Generating SSH Keys for more details.

Test your application by running

sudo node {your_app}


Making the NodeJS start on boot

To make a robust image, we want the NodeJS app to start on boot and respawn when crashed. We will write a simple service. All service scripts are located in /etc/init.

Let's create the file /etc/init/{your_app_name}_service.conf

sudo vi http://upstart.ubuntu.com/wiki/Stanzas

Put the following into the file:

#######################

#!upstart

description "my NodeJS server"
author      "Some Dude"

# start on startup
start on started networking
stop on shutdown

# Automatically Respawn:
respawn
respawn limit 5 60

script
    cd /vol/src/{your_app}
    exec sudo node /vol/src/{your_app}/app.js >> /vol/log/app_`date +"%Y%m%d_%H%M%S"`.log 2>&1
end script

post-start script
   # Optionally put a script here that will notifiy you node has (re)started
   # /root/bin/hoptoad.sh "node.js has started!"
end script
#######################


Refer to upstart stanzas for more details about what each field mean.

Create the directory to store NodeJS outputs:

sudo mkdir /vol/log

I have marked each log file with the start time of the app. You will probably want to change this to create logs daily.

To check if the services are running:

initctl list | grep {your_app_name}_service.conf

To start a service:

sudo service {your_app_name}_service.conf start

To stop a service:

sudo service {your_app_name}_service.conf stop


Now reboot your EC2 instance in the AWS console.

Test if your site is started.


Create a NodeJS AMI


In the AWS Management Console, click instances at the left sidebar.

Right click on the Wordpress instance created above and click on Create Image.

Fill in the image name. I like to name things in a convention that is systematic. If you are planning to write deploy scripts and do auto-scaling, it is easier to identify what an image is. I use the following convention:

{namespace}_{what_is_it}_{date}

Ex. mycompany_blog_20130118

You will want the date because you may create an image every time you deploy new codes.

Leave the other options as default, and click on Create Image.

On the left sidebar, click on AMIs under Images.

You can see the status of the AMI we just created.

You should launch an instance from this AMI and test all the data is there.

Monday, January 21, 2013

Switching NodeJS versions on Ubuntu

This tutorial will demo how you can have multiple versions of NodeJS and switch them any time.

Specs:
  • Ubuntu Server 12.04.1 LTS

Steps:

> sudo apt-get install nodejs npm

> sudo npm install -g n

> sudo n {version}
Ex. sudo n 0.8.18
If the version is not installed, the n cmd will automatically install the version for you. If the version is installed, then it will simply switch to that version.

NodeJS default install location on Mac OSX

Both the node and node_modules are located in:
/usr/local/lib/