Node.js Image Uploader using Express and jQuery Ajax
Reading Time:
Reading Time:
Note: The demo might take a second or two to load up sometimes, since the dyne might be at sleep.
To build this feature, we’ll use,
These packages are used at the backend, in the front-end of the web application Twitter Bootstrap and jQuery is used.
The application has the following structure,
app.js
– Contains the Node Express server code.app.js
– Contains the fron-end ajax file upload code.index.html
– Main index HTML file.There is also a package.json
file that contains the info about the project and it’s dependencies.
First all the required libraries are required and the app.js
script, which is empty for now.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Codedodle - Node.js image upload</title>
<meta name="description" content="Node.js image upload using express, jQuery using ajax." />
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<style type="text/css">body {background-color: #cccccc;}</style>
</head>
<body>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript" src="/assets/js/app.js"></script>
</body>
</html>
Let’s add the components that is needed for creating the application.
<!-- Photos upload Form -->
<form id="upload-photos" method="post" action="/upload_photos" enctype="multipart/form-data">
<div class="form-group">
<label for="photos-input">File input</label>
<input id="photos-input" type="file" name="photos[]" multiple="multiple" >
<p class="help-block">You can upload up to 3 files.</p>
</div>
<input type="hidden" name="csrf_token" value="just_a_text_field" />
<input class="btn btn-default" type="submit" name="Photo Uploads" value="Upload Photos" />
</form>
The Upload Form is the main component in the page. This is how the users selects the files to upload and other inputs. There is a hidden input field csrf_token
to demonstrate how other fields can be present in the form along with the file input. Nothing is done with that input though.
The file input has the attribute multiple=“multiple”
which accepts multiple input files.
<!-- Progress Bar -->
<div class="row">
<div class="col-md-12">
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<span class="sr-only"></span>
</div>
</div>
</div>
</div>
Next component in the page is the Progress Bar. This is a Bootstrap component which can be used directly without much code. To show the progress in the component, just adjust the width
css property of the element using jQuery.
<!-- Photos Album Container -->
<div id="album" class="row"></div>
This is where the photos appear after the upload has been finished successfully.
Complete markup,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Codedodle - Node.js image upload</title>
<meta name="description" content="Node.js image upload using express, jQuery using ajax." />
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<style type="text/css">body {background-color: #cccccc;}</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4">
<!-- Photos upload Form -->
<form id="upload-photos" method="post" action="/upload_photos" enctype="multipart/form-data">
<div class="form-group">
<label for="photos-input">File input</label>
<input id="photos-input" type="file" name="photos[]" multiple="multiple" >
<p class="help-block">You can upload up to 3 files.</p>
</div>
<input type="hidden" name="csrf_token" value="just_a_text_field" />
<input class="btn btn-default" type="submit" name="Photo Uploads" value="Upload Photos" />
</form>
<br/>
<!-- Progress Bar -->
<div class="row">
<div class="col-md-12">
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<span class="sr-only"></span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4"></div>
</div>
<!-- Photos Album Container -->
<div id="album" class="row"></div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript" src="/assets/js/app.js"></script>
</body>
</html>
Now to handle the file upload from the form and send those files using Ajax to the backend.
// On form submit, handle the file uploads.
$('#upload-photos').on('submit', function (event) {
event.preventDefault();
// Get the files from input, create new FormData.
var files = $('#photos-input').get(0).files,
formData = new FormData();
if (files.length === 0) {
alert('Select atleast 1 file to upload.');
return false;
}
if (files.length > 3) {
alert('You can only upload up to 3 files.');
return false;
}
// Append the files to the formData.
for (var i=0; i < files.length; i++) {
var file = files[i];
formData.append('photos[]', file, file.name);
}
// Note: We are only appending the file inputs to the FormData.
uploadFiles(formData);
});
In the above script, an on submit event listener is binded to the form element. When the form is submitted, the default form submission action is prevented by calling the event.preventDefault()
method, which prevents the form submission being sent to the server.
The input files is retrieved from the input file
field using jQuery and a new FormData
is created. To know more about FormData visit here.
Then the input file field is checked if any files are submitted and there not more than 3 file submitted for upload. Once the check is passed, we append the file inputs to the formData
Object we created before and pass it to the uploadFiles
function to send them to the server.
Let’s look at the uploadFiles
method next.
/**
* Upload the photos using ajax request.
*
* @param formData
*/
function uploadFiles(formData) {
$.ajax({
url: '/upload_photos',
method: 'post',
data: formData,
processData: false,
contentType: false,
xhr: function () {
var xhr = new XMLHttpRequest();
// Add progress event listener to the upload.
xhr.upload.addEventListener('progress', function (event) {
var progressBar = $('.progress-bar');
if (event.lengthComputable) {
var percent = (event.loaded / event.total) * 100;
progressBar.width(percent + '%');
if (percent === 100) {
progressBar.removeClass('active');
}
}
});
return xhr;
}
}).done(handleSuccess).fail(function (xhr, status) {
alert(status);
});
}
The uploadFiles
function takes in the formData
and creates a jQuery ajax request passing the formData
as the input data to the ajax request. The request is a post
request which is submitted to the /upload_photos
endpoint.
It is important to set the processData
and the contentType
fields to false
. View the jQuery ajax docs for more info.
The xhr
property of the jQuery ajax is used to create a custom ajax request object. Using this we provide our own ajax request object to which we add a upload event listener to the progress event. Then use this to update the progress components width and show the upload progress. Finally return the custom ajax xhr
object that is created.
handleSuccess
function is provided to handle the ajax response when the ajax request is successful. If the request fails, an alert of the status is shown to the user.
Next, let’s look at the handleSuccess
function.
Before looking into the handleSuccess
function, let look at the structure of the response that will be expected to be built at the backend. This will be a JSON response with the following structure.
[
{
"status":true,
"filename":"file1.jpg",
"type":"jpg",
"publicPath":"uploads/file1.jpg"
},
{
"status":true,
"filename":"file2.png",
"type":"png",
"publicPath":"uploads/file2.png"
},
{
"status":false,
"filename":"file3.zip",
"message":"Invalid file type"
}
]
The response contains an array of objects. Each object represents the uploaded file. If the uploaded file is of valid type, then it has a status
of true
, the uploaded filename
, the file type in type
and finally the public path in publicPath
.
If the file is invalid, then it will have the response as shown in the last object in the above structure.
/**
* Handle the upload response data from server and display them.
*
* @param data
*/
function handleSuccess(data) {
if (data.length > 0) {
var html = '';
for (var i=0; i < data.length; i++) {
var img = data[i];
if (img.status) {
html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail"><img src="' + img.publicPath + '" alt="' + img.filename + '"></a></div>';
} else {
html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail">Invalid file type - ' + img.filename + '</a></div>';
}
}
$('#album').html(html);
} else {
alert('No images were uploaded.')
}
}
The handleSuccess
function uses those object, loops through them and appends the HTML inside the Album Container component.
Also add a onChange
event to the file input to reset the progress bar whenever there is a new file upload is started.
// Set the progress bar to 0 when a file(s) is selected.
$('#photos-input').on('change', function () {
$('.progress-bar').width('0%');
});
That was our front-end code for handling the file upload and submitting them to the endpoint. Now let’s create the Node script to create the endpoints.
This will be our final app.js
code.
/**
* Upload the photos using ajax request.
*
* @param formData
*/
function uploadFiles(formData) {
$.ajax({
url: '/upload_photos',
method: 'post',
data: formData,
processData: false,
contentType: false,
xhr: function () {
var xhr = new XMLHttpRequest();
// Add progress event listener to the upload.
xhr.upload.addEventListener('progress', function (event) {
var progressBar = $('.progress-bar');
if (event.lengthComputable) {
var percent = (event.loaded / event.total) * 100;
progressBar.width(percent + '%');
if (percent === 100) {
progressBar.removeClass('active');
}
}
});
return xhr;
}
}).done(handleSuccess).fail(function (xhr, status) {
alert(status);
});
}
/**
* Handle the upload response data from server and display them.
*
* @param data
*/
function handleSuccess(data) {
if (data.length > 0) {
var html = '';
for (var i=0; i < data.length; i++) {
var img = data[i];
if (img.status) {
html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail"><img src="' + img.publicPath + '" alt="' + img.filename + '"></a></div>';
} else {
html += '<div class="col-xs-6 col-md-4"><a href="#" class="thumbnail">Invalid file type - ' + img.filename + '</a></div>';
}
}
$('#album').html(html);
} else {
alert('No images were uploaded.')
}
}
// Set the progress bar to 0 when a file(s) is selected.
$('#photos-input').on('change', function () {
$('.progress-bar').width('0%');
});
// On form submit, handle the file uploads.
$('#upload-photos').on('submit', function (event) {
event.preventDefault();
// Get the files from input, create new FormData.
var files = $('#photos-input').get(0).files,
formData = new FormData();
if (files.length === 0) {
alert('Select atleast 1 file to upload.');
return false;
}
if (files.length > 3) {
alert('You can only upload up to 3 files.');
return false;
}
// Append the files to the formData.
for (var i=0; i < files.length; i++) {
var file = files[i];
formData.append('photos[]', file, file.name);
}
// Note: We are only appending the file inputs to the FormData.
uploadFiles(formData);
});
There must be 2 endpoints created, one will be a get request to the page index which will return the index.html
that was created before. The second one will accept a post request method in which the uploaded files will be processed.
First, let’s install the required packages for the application using the Node Package Manager (npm).
npm install express formidable read-chunk file-type --save
Let’s setup the express server.
var express = require('express'),
path = require('path'),
fs = require('fs'),
formidable = require('formidable'),
readChunk = require('read-chunk'),
fileType = require('file-type');
var app = express();
app.set('port', (process.env.PORT || 5000));
// Tell express to serve static files from the following directories
app.use(express.static('public'));
app.use('/uploads', express.static('uploads'));
/************
Routes will be defined here.
*************/
app.listen(app.get('port'), function() {
console.log('Express started at port ' + app.get('port'));
});
Here is a basic skeleton of the express server. The script includes all the required packages needed along with the Node fs and path modules.
The express app is initialised. The port in which the application should run is got from the environment variable or it defaults to 5000.
Then the express middleware is used to server the static files from the public directory and the uploads directory. Visit express middleware to know more.
Finally the app is started using the app.listen
method.
The first route to be defined is the index route. This will be the default route that users view once they visit the application. This route will send the index.html
file that we created using the response objects res.sendFile
method.
/**
* Index route
*/
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, 'views/index.html'));
});
Inside this route a photos
array is created which will contain the list of photos objects and a new instance of the formidable.IncomingForm
is created and assigned to the form
variable.
Then the configure options for formidable
is set based on the application needs and a event listener is attached to process the uploaded files and other actions. To view all the options and events triggered look at the formidable documentation.
Let’s first set the options needed. Here the option that formidable must expect multiple files and also specify the default temp directory to which it must save the files to.
// Tells formidable that there will be multiple files sent.
form.multiples = true;
// Upload directory for the images
form.uploadDir = path.join(__dirname, 'tmp_uploads');
Then a listener to one of the triggers that formidable provides is added. It’s the file
trigger, which is called whenever a new file is received. It accepts a callback which will receive the name
and a file
object. Refer the formidable docs for the properties present in the file object.
// Invoked when a file has finished uploading.
form.on('file', function (name, file) {
// Allow only 3 files to be uploaded.
if (photos.length === 3) {
fs.unlink(file.path);
return true;
}
var buffer = null,
type = null,
filename = '';
// Read a chunk of the file.
buffer = readChunk.sync(file.path, 0, 262);
// Get the file type using the buffer read using read-chunk
type = fileType(buffer);
// Check the file type, must be either png,jpg or jpeg
if (type !== null && (type.ext === 'png' || type.ext === 'jpg' || type.ext === 'jpeg')) {
// Assign new file name
filename = Date.now() + '-' + file.name;
// Move the file with the new file name
fs.rename(file.path, path.join(__dirname, 'uploads/' + filename));
// Add to the list of photos
photos.push({
status: true,
filename: filename,
type: type.ext,
publicPath: 'uploads/' + filename
});
} else {
// If invalid file type, remove it and set status to false.
photos.push({
status: false,
filename: file.name,
message: 'Invalid file type'
});
fs.unlink(file.path);
}
});
Inside the callback function, we do the following.
photos
length is 3, since only 3 files are allowed. If there is already 3 photos, then delete the current file.readChunk
we get a chunk of the file, that is enough to identify the file type and pass that buffer to the file-type
to get the type of the file.Date.now
and the file.name
and move the file to the uploads directory. Finally that file is added to the photos
variable with the properties shown in the structure.false
object to the photos
variable and remove that file using fs.unlink
.And of-course the callback for error
event is also provided to log if any error occurs.
form.on('error', function(err) {
console.log('Error occurred during processing - ' + err);
});
// Invoked when all the fields have been processed.
form.on('end', function() {
console.log('All the request fields have been processed.');
});
Finally, the request is processed using the parse method and the photos
is sent back as a JSON response.
// Parse the incoming form fields.
form.parse(req, function (err, fields, files) {
res.status(200).json(photos);
});
Our final app.js
looks like this.
var express = require('express'),
path = require('path'),
fs = require('fs'),
formidable = require('formidable'),
readChunk = require('read-chunk'),
fileType = require('file-type');
var app = express();
app.set('port', (process.env.PORT || 5000));
// Tell express to serve static files from the following directories
app.use(express.static('public'));
app.use('/uploads', express.static('uploads'));
/**
* Index route
*/
app.get('/', function (req, res) {
// Don't bother about this :)
var filesPath = path.join(__dirname, 'uploads/');
fs.readdir(filesPath, function (err, files) {
if (err) {
console.log(err);
return;
}
files.forEach(function (file) {
fs.stat(filesPath + file, function (err, stats) {
if (err) {
console.log(err);
return;
}
var createdAt = Date.parse(stats.ctime),
days = Math.round((Date.now() - createdAt) / (1000*60*60*24));
if (days > 1) {
fs.unlink(filesPath + file);
}
});
});
});
res.sendFile(path.join(__dirname, 'views/index.html'));
});
/**
* Upload photos route.
*/
app.post('/upload_photos', function (req, res) {
var photos = [],
form = new formidable.IncomingForm();
// Tells formidable that there will be multiple files sent.
form.multiples = true;
// Upload directory for the images
form.uploadDir = path.join(__dirname, 'tmp_uploads');
// Invoked when a file has finished uploading.
form.on('file', function (name, file) {
// Allow only 3 files to be uploaded.
if (photos.length === 3) {
fs.unlink(file.path);
return true;
}
var buffer = null,
type = null,
filename = '';
// Read a chunk of the file.
buffer = readChunk.sync(file.path, 0, 262);
// Get the file type using the buffer read using read-chunk
type = fileType(buffer);
// Check the file type, must be either png,jpg or jpeg
if (type !== null && (type.ext === 'png' || type.ext === 'jpg' || type.ext === 'jpeg')) {
// Assign new file name
filename = Date.now() + '-' + file.name;
// Move the file with the new file name
fs.rename(file.path, path.join(__dirname, 'uploads/' + filename));
// Add to the list of photos
photos.push({
status: true,
filename: filename,
type: type.ext,
publicPath: 'uploads/' + filename
});
} else {
photos.push({
status: false,
filename: file.name,
message: 'Invalid file type'
});
fs.unlink(file.path);
}
});
form.on('error', function(err) {
console.log('Error occurred during processing - ' + err);
});
// Invoked when all the fields have been processed.
form.on('end', function() {
console.log('All the request fields have been processed.');
});
// Parse the incoming form fields.
form.parse(req, function (err, fields, files) {
res.status(200).json(photos);
});
});
app.listen(app.get('port'), function() {
console.log('Express started at port ' + app.get('port'));
});
Once all the files are in place, fire up the Express server using the node app.js
from within the root directory and load up the browser at http://localhost:5000.
Hope you guys found this useful and have fun exploring and learning.