gadgetui.input.FileUploader example, Part II - the model.FileService server component

A FileUploader component does no good by itself. In order to provide upload capability, you need something on the server that can receive the uploaded files and process them. In Part I, I demonstrated a CFML page that handles the initial capture of the uploaded data and passes it to a component, model.FileService, for further processing. model.FileService isn't all that complicated, but it helps to walk through the process so you understand what is happening.

model.FileService.cfc


component output="false"{
	public function init( filePath, FilePartDAO ){
		variables.filePath = arguments.filePath;
		variables.viewFilePath = "/test/upload/";
		variables.FilePartDAO = arguments.FilePartDAO;
		variables.separator = iif(expandpath("./") contains "/",de("/"),de("\"));
		return this;
	}
	public function upload( required string id,
		required any temp_file,
		required numeric part,
		required numeric parts,
		required string filename,
		required string filesize ){

		local.result = {};
		local.addedPart = variables.FilePartDAO.create( fileid = arguments.id, filepath = arguments.temp_file, 
                                             filepart = arguments.part, parts = arguments.parts );

		if( arguments.part eq arguments.parts ){
			if( server.OS.name contains 'Windows' ){
				local.str = " copy /b ";
				local.files = variables.FileParDAO.readByFileId( fileid = arguments.id );
				local.str = local.str & local.files.filepath[ 1 ];
				for( local.ix = 2; local.ix lte local.files.recordcount; local.ix++ ){
					local.str = local.str & "+" & local.files.filepath[ local.ix ];
				}
				local.str = local.str & " ""#variables.filePath##arguments.filename#""";

				local.runtime = createObject("java", "java.lang.Runtime");
				local.process_runtime = local.runtime.getRuntime();
				local.process_exec = local.process_runtime.exec( javacast( "string[]", ["cmd.exe", "/c", local.str]) );
				local.exitCode = local.process_exec.waitFor();
			}else{
				local.str = " cat ";
				local.files = variables.FilePartDAO.readByFileId( fileid = arguments.id );
				for( local.ix = 1; local.ix lte local.files.recordcount; local.ix++ ){
					local.str = local.str & " " & local.files.filepath[ local.ix ];

				}
				local.str = local.str & " > ""#variables.filePath##arguments.filename#""";
				//writelog( file="upload", text="Concatenating: " & local.str );
				local.runtime = createObject("java", "java.lang.Runtime");
				local.process_runtime = local.runtime.getRuntime();
				local.process_exec = local.process_runtime.exec( javacast( "string[]", ["bash", "-c", local.str]) );
				local.exitCode = local.process_exec.waitFor();
			}
			//clean up
			for( local.ix = 1; local.ix lte local.files.recordcount; local.ix++ ){
				if( fileExists( local.files.filepath[ local.ix ] ) ){
					fileDelete( local.files.filepath[ local.ix ] );
				}
			}
			//clean up
			variables.FilePartDAO.delete( fileid = arguments.id );
		}

		local.result.path = variables.viewFilePath;
		//local.result.tags = arguments.tags;
		local.result.filename = arguments.filename;
		local.result.disabled = 0;
		local.result.filesize = arguments.filesize;
		local.result.mimetype = "application/octet-stream";
		local.result.created = now();

		return local.result;
	}
}

First let's check on the init() method that instantiates the component. Mostly it is fairly obvious what is going on- we configure the component with upload and view paths according to our server configuration. The only non-obvious configuration is FilePartDAO, which is just what its name implies - a DAO that manages persistence of filepart information to the database. Technically, we don't need a database to store this information, we could just store it in a session variable. However, persisting the data provides the underpinning for a broken upload resume feature that I am planning to add soon.

Next, let's review the upload() method. First, the method signature:

	public function upload( required string id,
		required any temp_file,
		required numeric part,
		required numeric parts,
		required string filename,
		required string filesize )

The signature shows what we are dealing with as input. An id (in this case, a generated UUID for each filepart), a path to the filepart on disk, the index and total number of parts, the original filename and filesize. Nothing too complicated there.

What you will notice next is that the method does almost nothing until you get to the last part of the fileparts in the upload. It basically just persists the filepart information to the database. Once you get to the last part, though, the upload method checks whether you are in a Windows or *nix environment, loops over the records of fileparts and assembles a CLI string to use to concatenate the fileparts into a single file. It then instantiates a Java Runtime object and invokes a CLI interpreter (cmd or bash for Windows or *nix) with the concatenate statement as an argument. 

What does that all mean? It means the page invokes a command line statement to put the file back together in the final upload destination. Personally, I have found this method to be an order of magnitude faster than trying to use native file commands in CFML to re-assemble the uploaded file. 

Once the file is re-assembled, the method goes through the temporary file parts and deletes them, then deletes the record of them in the database, then returns a struct with the final uploaded file's metadata. That information is then sent back to the browser as the final reply to the upload function

To be clear, this is a vanilla implementation of the upload function, meant mainly to demonstrate a basic way of capturing and storing the uploaded file and its metadata. I am available for consulting engagements if you would like a more complex implementation, such as storing the file in a NoSQL store like MongoDB.