MongoDB: Using MongoDB as an Image Server
First, we should know that this may not always be the best alternative — you can still use Amazon’s S3 service. This guide serves only as an interesting alternative to consider.
In this example, we’ll be using:
- Express 4.x
- NodeJS 5.x
Storing the image in the database
In the example below, we have a service that receives the image in base64, converts it to binary, and saves it to the database:
var GridStore = require('mongodb').GridStore;
var randomstring = require('randomstring').generate;
// Regular expression used to remove the
// data:image/XXX part which isn't actually
// part of the base64 code
var BASE64_REGEX = /^data:image\/png;base64,|^data:image\/jpeg;base64,|^data:image\/jpg;base64,|^data:image\/bmp;base64,/;
var FileManager = {
nextFileId: function() {
return `${randomstring(4)}_${randomstring(8)}_${randomstring(6)}`;
},
upload: function(data, encoding, fileId) {
fileId = fileId || this.nextFileId();
var buf = new Buffer(data, encoding),
gridStore = new GridStore(db, fileId, 'w');
return gridStore.open().then(function(gridStore) {
return gridStore.write(buf);
})
.then(function() {
return gridStore.close();
})
.then(function() {
return fileId;
});
},
uploadImage: function(data) {
return this.upload(data.replace(BASE64_REGEX), 'base64');
}
};
var fs = require('fs');
function storeFileId (fileId) {
return db.collection('images').insertOne({ fileId: fileId });
}
FileManager.uploadImage(fs.readFileSync('./images/angularjs-vs-react-who-is-the-winner.jpg'))
.then(storeFileId);
Creating the GridStore instance
In the first parameter, we pass the database connection (which you’ll receive right after a require('mongodb').MongoClient.connect()). In the second parameter, we define the filename, and lastly we define what we’re going to do with this document — in our case, we’re going to write. So w stands for write :)
Generating the filename
You may notice on line 13 of the example above that the filename we’re going to store is created by the anonymous function in the nextFileId property of FileManager. It returns something like:
c9R9_B7xgEVxQ_u7nCsX
This is just to make sure no files end up with duplicate names and that we can put this in a URL without making it obvious that we’re using MongoDB:
https://api.domain.com/images/c9R9_B7xgEVxQ_u7nOX9.png
But nothing stops you from using an ObjectId — we’re talking about Mongo, right? :)
Writing to the file
On line 19, we store the image data in the instance, but if you don’t execute gridStore.close() as on line 22, it won’t be saved to the database and will remain only in the instance. So it’s always important to execute a gridStore.close() after gridStore.write(). The parameter we pass to write() can be a Buffer or a String.
Reading the previously stored image
To access the image we saved, we’ll need to use the GridStore class again, but this time we won’t need to create another instance. However, we’ll definitely need the file ID we stored, so it’s important to know that you need to keep the file identifier so you can access it later.
var Q = require('q'),
GridStore = require('mongodb').GridStore;
FileManager.get = function(id) {
return GridStore.exists(db, id).then(function(result) {
if(result) {
return GridStore.read(db, id);
}
return Q.reject(new Error('File does not exists'));
});
};
class AlbumController {
getUserAlbum(req) {
return db.collection('users').findOne({
_id: ObjectId(req.params.userId)
})
.then(function(user) {
var userId = user._id.toString();
return db.collection('albums').findOne({ userId: userId });
})
.then(function(album) {
return albums && album.images && Q.all(album.images.forEach(function(image) {
return FileManager.get(image.fileId);
}));
});
}
}
Creating an endpoint
I believe this is the simplest part of it all: creating an endpoint where you can request your image from your site.
var EXT_REMOVE_REGEX = /\.([A-z]+)$/;
app.get('images/:imageId', function(req, res) {
var ext, filename = req.params.id;
// Let's remove the file extension, in case you
// typed something like "Z97d_KLEJAcVI_kII2tZ.png" or
// "Z97d_KLEJAcVI_kII2tZ.jpg"
filename = filename.replace(EXT_REMOVE_REGEX, function(raw, $ext, index, filename) {
ext = $ext;
return '';
});
// Let's fill this variable so we can reuse it later
// when writing the response header.
ext = ext || 'png';
return FileManager.get(filename).then(function(buf) {
// Writing the header with the new image and
// the buffer length.
res.writeHead(200, {
'Content-Type': 'image/' + ext,
'Content-Length': buf.length
});
// Responding to the request with the image data,
// which should load normally in your browser.
res.end(buf, 'binary');
});
});
I hope you enjoyed it. Leave your thoughts and questions below. Until next time.