Skip to content

3. GridFS File Storage

Jon Clausen edited this page Jan 6, 2016 · 19 revisions

CBMongoDB includes an Active Entity implementation for using MongoDB's GridFS for relational, distributed file storage. In the past, the storage of binary objects, especially large ones, in a database often caused serious performance problems when dealing with the flattened results of queries. MongoDB's implementation, along with the java driver, allows for excellent performance and relatively low system overhead by:

  1. Retreiving the binary data from storage only when it is explicitly called from within the query result object.
  2. Using buffered streaming to ensure that file operations don't require persistence of the full binary data in memory.

While certainly not as fast as disk I/O operations, MongoDB's GridFS provides a low-latency option for storing, managing and serving files using today's data distribution strategies.

Setup

Let's say, for example, that we want to associate profile images to our "Person" collection but we want those images to be available to all users of our database (i.e. - mobile app users, back-office apps, our website, etc.) without using a CDN solution or constraining our image data to file systems requiring I/O access, permissions, etc.

We can create a relational entity for our Person model that will be responsible for storing and retrieving profile pictures. Our component should extend cbmongodb.models.FileEntity. An additional component attribute may be specified with the name of the bucket used to store the file data. By default, Mongo uses a bucket named fs.

component name="ProfileImages" extends="cbmongodb.models.FileEntity" collection="profileImages" bucket="images" accessors=true{
	//normalize our person information, in case we need it for captions and alt information.
	//Note that we are using auto-normalization of the first_name and last_name keys on our person entity
	property name="person" schema=true normalize="Person" on="person.id" keys="first_name,last_name";
		property name="person.id" schema=true required=true;	
}

Because the querying syntax for GridFS files differs from our standard query syntax, entities use a standard MongoDB collection which connects to the GridFS file object. Every FileEntity schema contains a property fileId which allows for a one-to-one relationship to the GridFS file.

Storing files

We can load a file in directly from a local path, for example a file uploaded to a temporary directory:

var Person = getModel("Person").load("54e523599b67bde3c7b2f03d");

var ProfilePhoto = getModel("ProfileImages")
//Set and normalize our person model
ProfilePhoto.set('person.id',Person.get_id())
//Load our photo in to grid FS and pass the optional argument to delete the file after it has been loaded
ProfilePhoto.loadFile(filePath=expandPath('/includes/tmp/MyProfilePhoto.jpg'),deleteFile=true);
ProfilePhoto.create();

Working With Images and Files

Once we've created our profile photo, we can retrieve using our person relationship:

var ProfilePhoto = getModel("ProfileImages").where('person.id',Person.get_id()).find();

A simple example would be to retrieve the file and write it back out to the file system. The writeTo function, available in the FileEntity, returns the written file's path and will accept either a full path or a directory as it's destination argument. If only a directory is specified, the file will be written with the fileId string as the name of the file.

var imageFile = ProfilePhoto.writeTo(expandPath('/includes/images/profiles/MyProfileImage.jpg'));

For images, we can also provide a struct of arguments to resize or crop the image before writing it out to the file system.

  • width: The maximum width of the image. If crop x and y coordinates are specified, this will be the actual width. Otherwise, if only height and width are provided, the image will be scaled to the maximum height/width constraints.
  • height: Same as width, but for height
  • x: The horizontal offset of the cropped image from the top left corner of the original image
  • y: The vertical offset of the cropped image bounding box

Write a scaled image which will be no larger than 100x100 pixels wide:

var imageFile = ProfilePhoto.writeTo(
	Path=expandPath('/includes/images/profiles/MyProfileImage.jpg'),
	imageArgs={"height":100,"width":100}
);

Non-image files may also be written to the file system with the writeTo(Path) method, though image arguments will be ignored.

Additional Image Methods

CBMongoDB is bundled with the JavaXT Image library, which allows for advanced image manipulation. We can retrieve the modifiable JavaXT image object through the getImageObject() method. The example below retrieves the Image object, crops the image to 100x100 pixels from the center of the image and writes it out to a file:

var imgObject = ProfilePhoto.getImageObject();
var width = imgObject.getWidth();
var height = imgObject.getHeight();
//center calculation: coordinate = (originalWidth/2) - (targetWidth/2)
var x = (width/2)-50;
var y = (width/2)-50;
imgObject.crop(x,y,100,100);
imgObject.saveAs(expandPath('/includes/images/profiles/MyProfileImage.jpg'));

Easier still, we can also retreived a pre-scaled and/or cropped version of the image object by passing arguments in to the getImageObject method - note the use of the "center" passed to the x and y arguments, which will calculate the crop offsets automatically:

var imgObject = ProfilePhoto.getImageObject(height=100,width=100,x="center",y="center");
imgObject.saveAs(expandPath('/includes/images/profiles/MyProfileImage.jpg'));

You can accomplish a variety of image manipulation tasks using the JavaXT image object. For examples, see here.

You may also retrieve and work with GridFS images as a CFML native image. Here we'll use native CFML methods to accomplish the same task as above.

Note: From a performance and system resource standpoint, using the Java image object is the recommended method for dealing with large image files.

var img = ProfilePhoto.getCFImage();
var width = img.width;
var height = img.height;
//center calculation: coordinate = (originalWidth/2) - (targetWidth/2)
var x = (width/2)-50;
var y = (width/2)-50;
imageCrop(img,x,y,100,100);
imageWrite(img,expandPath('/includes/images/profiles/MyProfileImage.jpg'));

Writing Images Directly to The Browser

Images may also be written directly to the browser. Let's say have a handler that we want to dynamically serve images of different sizes for our views. We can set our handler action to serve our images directly:

component name="Images" extends="coldbox.system.EventHandler" {

	function index(event,rc,prc){
		var imageArgs = {};
		//handle our pixel argument (e.g. - 100x100)
		if(event.valueExists('p')){
			imageArgs["width"]=listFirst(rc.p,'x');
			imageArgs["height"]=listLast(rc.p,'x');
		}
		//we can use Coldbox's format detection to change the mimetype (e.g. - jpg to png)
		var imgFormats = ['jpg','jpeg','png','gif'];
		if( event.valueExists('format') && arrayFind(imgFormat,lcase(rc.format)) ){
			imageArgs['mimeType'] = 'image/' & lcase(rc.format);
		}
		//set an expiration time of 1 day so that our browser can cache the image
		imageArgs['expiration']=dateAdd('d',1,now());
		var Image = getModel("ProfileImage").load(rc.id);
		//calling writeImageToBrowser() will flush the image to the browser with the appropriate headers
		Image.writeImageToBrowser(argumentCollection=imageArgs);

	}
}

We could retrieve a 100 pixel scaled image directly, using the above, from the URL /images/[fileId].png?p=100x100, or a custom route could be created to handle additional parameters.

Configuration Options

A GridFS struct may be added to your MongoDB config struct in your config.Coldbox file. By default it is omitted. It includes image storage options for limiting the size of images and storing CFImage metadata:

GridFS = {
	"imagestorage":{
		//whether to store the cfimage metadata
		"metadata":true,
		//the max allowed width of images in the GridFS store
		"maxwidth":1000,
		//the max allowed height of images in the GridFS store
		"maxheight":1000,
		//The path within the site root with trailing slash to use for resizing images (required if maxheight or max width are specified)
		"tmpDirectory":"/includes/tmp/"
	}
}

Method Summary

  • loadFile(required string filePath,deleteFile=false): Loads a file in to GridFS and solidifies the relationship to the Entity
  • getFileObject(): returns the GridFS file object
  • getExtension(): returns the file extension (e.g. "jpg")
  • getMimeType(): returns the mimetype of the file (e.g. "image/jpeg")
  • writeTo(required string destination,required struct imageArgs={}): writes the stored GridFS file to a path with optional image transformation arguments
  • writeImageToBrowser(numeric width,numeric height,any x=0,any y=0,mimeType,expiration): writes an image directly to the browser as content and aborts the remainder of the request
  • getCFImage(numeric width,numeric height,any x=0,any y=0): returns a native CFML Image (e.g. "<cfimage>") from the GridFS file
  • getImageObject(numeric width,numeric height,any x=0,any y=0): returns a javaxt.io.Image object from the GridFS file
  • getBufferedImage(numeric width,numeric height,any x=0,any y=0): returns the buffered image object for accessing the raw pixels of an image