Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jason Dean and Mark Drew
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jason Dean ( @JasonPDean ) Mark Drew ( @markdrew )

Storing Per-File Multipart-Params In The Plupload Queue

By on
Tags:

Last week, I looked at uploading files directly to Amazon S3 using the Plupload HTML5 uploader. In that demo, I used the "BeforeUpload" event in order to generate unique filenames for each upload. This go me thinking - if I can inject per-file settings at the time of upload, can I also store per-file settings at the time in which the file is added to the Plupload queue?

Plupload will only upload one file at a time. As such, it keeps a queue of pending files in memory. As files are added to this queue, they pass through the "FilesAdded" event handler. This handler is given access to the array of files that have been selected by the user. These files are represented using plain-old JavaScript objects; so, I wonder, can they be augmented with context-sensitive information to be later used at the time of upload?

As it turns out, Yes. I was able to inject temporal data into each File instance as it was added to the queue. Then, in the "BeforeUpload" event handler, I was able to update to the multipart-params in order to reflect the data stored in each file.

To test this, I created a form that could upload images with a "convert to grayscale" checkbox. As you can guess, if the checkbox is checked, the images are converted to grayscale on the server; if the checkbox is not checked, the images are kept in full color. I then built up a file queue and toggled this checkbox on an off before adding groups of files to the queue. What I found was that each file maintained the state of the checkbox that was relevant at the time the file was added to the queue.

Here is my demo code - pay special attention to the "FilesAdded" and "BeforeUpload" event handlers; you will see that these handlers inject settings, and then subsequently read those settings out of each File instance.

<!--- Reset the output buffer and set the page encoding. --->
<cfcontent type="text/html; charset=utf-8" />

<cfoutput>

	<!doctype html>
	<html>
	<head>
		<meta charset="utf-8" />

		<title>
			Storing Per-File Multipart-Params In The Plupload Queue
		</title>

		<link rel="stylesheet" type="text/css" href="./assets/css/styles.css"></link>
	</head>
	<body>

		<h1>
			Storing Per-File Multipart-Params In The Plupload Queue
		</h1>

		<div id="uploader" class="uploader">

			<a id="selectFiles" href="##">

				<span class="label">
					Select Files
				</span>

				<span class="standby">
					Waiting for files...
				</span>

				<span class="progress">
					Uploading - <span class="percent"></span>%
				</span>

			</a>

		</div>

		<label>
			<input type="checkbox" name="convertToGrayscale" value="1" />
			Convert Image to Grayscale
		</label>

		<ul class="uploads">
			<!--
				Will be populated dynamically with LI/IMG tags by the
				uploader success handler.
			-->
		</ul>


		<!-- Load and initialize scripts. -->
		<script type="text/javascript" src="./assets/jquery/jquery-2.0.3.min.js"></script>
		<script type="text/javascript" src="./assets/plupload/js/plupload.full.js"></script>
		<script type="text/javascript">

			(function( $, plupload ) {


				// Find and cache the DOM elements we'll be using.
				var dom = {
					uploader: $( "##uploader" ),
					percent: $( "##uploader span.percent" ),
					uploads: $( "ul.uploads" ),
					checkbox: $( "input[ type = 'checkbox' ]")
				};


				// Instantiate the Plupload uploader.
				var uploader = new plupload.Uploader({

					// Try to load the HTML5 engine and then, if that's
					// not supported, the Flash fallback engine.
					runtimes: "html5,flash",

					// The upload URL.
					url: "./upload.cfm",

					// The ID of the drop-zone element.
					drop_element: "uploader",

					// To enable click-to-select-files, you can provide
					// a browse button. We can use the same one as the
					// drop zone.
					browse_button: "selectFiles",

					// For the Flash engine, we have to define the ID
					// of the node into which Pluploader will inject the
					// <OBJECT> tag for the flash movie.
					container: "uploader",

					// The URL for the SWF file for the Flash upload
					// engine for browsers that don't support HTML5.
					flash_swf_url: "./assets/plupload/js/plupload.flash.swf",

					// Needed for the Flash environment to work.
					urlstream_upload: true,

					// Set up the multi-part data. For now, just leave
					// the params blank - we'll be adding to them as
					// we start to upload files.
					multipart: true,
					multipart_params: {}
				});


				// Set up the event handlers for the uploader.
				uploader.bind( "Init", handlePluploadInit );
				uploader.bind( "Error", handlePluploadError );
				uploader.bind( "FilesAdded", handlePluploadFilesAdded );
				uploader.bind( "QueueChanged", handlePluploadQueueChanged );
				uploader.bind( "BeforeUpload", handlePluploadBeforeUpload );
				uploader.bind( "UploadProgress", handlePluploadUploadProgress );
				uploader.bind( "FileUploaded", handlePluploadFileUploaded );
				uploader.bind( "StateChanged", handlePluploadStateChanged );

				// Initialize the uploader (it is only after the
				// initialization is complete that we will know which
				// runtime load: html5 vs. Flash).
				uploader.init();


				// ------------------------------------------ //
				// ------------------------------------------ //


				// I handle the before upload event where the meta data
				// can be edited right before the upload of a specific
				// file, allowing for per-file settings.
				function handlePluploadBeforeUpload( uploader, file ) {

					params = uploader.settings.multipart_params;

					// Set the convert-to-grayscale data that was available
					// at the time the file was ADDED to the queue (which
					// may be different from the checkbox-state now).
					params.convertToGrayscale = file.convertToGrayscale;

				}


				// I handle the init event. At this point, we will know
				// which runtime has loaded, and whether or not drag-
				// drop functionality is supported.
				function handlePluploadInit( uploader, params ) {

					console.info( "Drag-drop supported:", !! uploader.features.dragdrop );

				}


				// I handle any errors raised during uploads.
				function handlePluploadError() {

					console.warn( "Error during upload." );

				}


				// I handle the files-added event. This is different
				// that the queue-changed event. At this point, we
				// have an opportunity to reject files from the queue.
				function handlePluploadFilesAdded( uploader, files ) {

					var isChecked = dom.checkbox.is( ":checked" );

					// Store the current convert-to-grascale steting
					// with the given FILE object; this way, when the
					// file is popped off the queue and ready to
					// upload, we'll be able to update the multipart
					// params to contains the appropriate settings.
					for ( var i = 0 ; i < files.length ; i++ ) {

						files[ i ].convertToGrayscale = isChecked;

					}

				}


				// I handle the queue changed event. When the queue
				// changes, it gives us an opportunity to programmatically
				// start the upload process.
				function handlePluploadQueueChanged( uploader ) {

					if ( uploader.files.length && isNotUploading() ){

						uploader.start();

					}

				}


				// I handle the upload progress event. This gives us
				// the progress of the given file, NOT of the entire
				// upload queue.
				function handlePluploadUploadProgress( uploader, file ) {

					dom.percent.text( file.percent );

				}


				// I handle the file-uploaded event. At this point,
				// the image has been uploaded and thumbnailed - we
				// can now load that image in our uploads list.
				function handlePluploadFileUploaded( uploader, file, response ) {

					var li = $( "<li><img /></li>" );

					var imageSource = $.trim( response.response );

					li.find( "img" ).prop( "src", imageSource );

					dom.uploads.prepend( li );

				}


				// I handle the change in state of the uploader.
				function handlePluploadStateChanged( uploader ) {

					if ( isUploading() ) {

						dom.uploader.addClass( "uploading" );

					} else {

						dom.uploader.removeClass( "uploading" );

					}

				}


				// I determine if the upload is currently inactive.
				function isNotUploading() {

					var currentState = uploader.state;

					return( currentState === plupload.STOPPED );

				}


				// I determine if the uploader is currently uploading a
				// file (or if it is inactive).
				function isUploading() {

					var currentState = uploader.state;

					return( currentState === plupload.STARTED );

				}


			})( jQuery, plupload );


		</script>

	</body>
	</html>

</cfoutput>

On the server-side, my upload target read in the images, scaled them down, and then, if desired by the user, converted the thumbnails to grayscale. Since I am working on a local development environment, I had to add a sleep() call in order to be able to build up an actual queue of files (without the sleep, the uploads were instantaneous).

<cfscript>


	// These are the two fields that Plupload will post by default.
	param name="form.name" type="string";
	param name="form.file" type="string";

	// This is the black-and-white flag that we want to send through
	// on a per-file-upload basis.
	param name="form.convertToGrayscale" type="boolean" default=false;


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// Read in the TMP file as an image.
	upload = imageNew( fileReadBinary( form.file ) );

	// Scale the image down for the demo.
	imageScaleToFit( upload, 150, 150, "highPerformance" );

	// Convert to gray-scale ONLY if requested by the client.
	if ( form.convertToGrayscale ) {

		imageGrayscale( upload );

	}

	serverFileName = ( createUUID() & ".png" );

	imageWrite(
		upload,
		expandPath( "./uploads/#serverFileName#" )
	);


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// NOTE: This sleep() is here to slow down the plupload queue
	// such that we can actually get a queue to appear for a
	// reasonable / actionable amount of time.
	sleep( 2 * 1000 );


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// Write the server file path back to the client.
	// --
	// NOTE: There will be white-space around this in the response;
	// the client will have to trim it ... I'm just being lazy for
	// this demo.
	writeOutput( "./uploads/#serverFileName#" );


</cfscript>

From an application standpoint, this finding is pretty exciting! It means that I can keep a Plupload queue loaded at all times; then, just adjust per-file settings as the state of my application changes. I tested this in both the HTML5 and Flash runtimes and both contexts seemed to work properly.

Want to use code from this post? Check out the license.

Reader Comments

15,674 Comments

@John,

It's pretty cool. One thing that I really want to be able to do is have a "global" uploader that can persist as app-views change. Then, just change the settings with each file.

16 Comments

@Ben

Change the settings based on? 'where' the uploaded the file from e.g.: "/mysite/about/" or based on file type? Curious.

And that leads to this question… "global" uploader… could you expound on that a bit. I'm using this thing for a photo uploader and the "uploader" can be instantiated from any page in my site. Wondering what your thinking about.

Also, just got this working nicely. Big super thanks for the post.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel