package ru.inspirit.net { import flash.errors.IllegalOperationError; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.HTTPStatusEvent; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.net.URLLoader; import flash.net.URLLoaderDataFormat; import flash.net.URLRequest; import flash.net.URLRequestHeader; import flash.net.URLRequestMethod; import flash.utils.ByteArray; import flash.utils.Dictionary; import flash.utils.Endian; import flash.utils.setTimeout; import flash.utils.clearInterval; import ru.inspirit.net.events.MultipartURLLoaderEvent; /** * Multipart URL Loader * * Original idea by Marston Development Studio - http://marstonstudio.com/?p=36 * * History * 2009.01.15 version 1.0 * Initial release * * 2009.01.19 version 1.1 * Added options for MIME-types (default is application/octet-stream) * * 2009.01.20 version 1.2 * Added clearVariables and clearFiles methods * Small code refactoring * Public methods documentaion * * 2009.02.09 version 1.2.1 * Changed 'useWeakReference' to false (thanx to zlatko) * It appears that on some servers setting 'useWeakReference' to true * completely disables this event * * 2009.03.05 version 1.3 * Added Async property. Now you can prepare data asynchronous before sending it. * It will prevent flash player from freezing while constructing request data. * You can specify the amount of bytes to write per iteration through BLOCK_SIZE static property. * Added events for asynchronous method. * Added dataFormat property for returned server data. * Removed 'Cache-Control' from headers and added custom requestHeaders array property. * Added getter for the URLLoader class used to send data. * * @author Eugene Zatepyakin * @version 1.3 * @link http://blog.inspirit.ru/ */ public class MultipartURLLoader extends EventDispatcher { public static var BLOCK_SIZE:uint = 64 * 1024; private var _loader:URLLoader; private var _boundary:String; private var _variableNames:Array; private var _fileNames:Array; private var _variables:Dictionary; private var _files:Dictionary; private var _async:Boolean = false; private var _path:String; private var _data:ByteArray; private var _prepared:Boolean = false; private var asyncWriteTimeoutId:Number; private var asyncFilePointer:uint = 0; private var totalFilesSize:uint = 0; private var writtenBytes:uint = 0; public var requestHeaders:Array; public function MultipartURLLoader() { _fileNames = new Array(); _files = new Dictionary(); _variableNames = new Array(); _variables = new Dictionary(); _loader = new URLLoader(); requestHeaders = new Array(); } /** * Start uploading data to specified path * * @param path The server script path * @param async Set to true if you are uploading huge amount of data */ public function load(path:String, async:Boolean = false):void { if (path == null || path == '') throw new IllegalOperationError('You cant load without specifing PATH'); _path = path; _async = async; if (_async) { if(!_prepared){ constructPostDataAsync(); } else { doSend(); } } else { _data = constructPostData(); doSend(); } } /** * Start uploading data after async prepare */ public function startLoad():void { if ( _path == null || _path == '' || _async == false ) throw new IllegalOperationError('You can use this method only if loading asynchronous.'); if ( !_prepared && _async ) throw new IllegalOperationError('You should prepare data before sending when using asynchronous.'); doSend(); } /** * Prepare data before sending (only if you use asynchronous) */ public function prepareData():void { constructPostDataAsync(); } /** * Stop loader action */ public function close():void { try { _loader.close(); } catch( e:Error ) { } } /** * Add string variable to loader * If you have already added variable with the same name it will be overwritten * * @param name Variable name * @param value Variable value */ public function addVariable(name:String, value:Object = ''):void { if (_variableNames.indexOf(name) == -1) { _variableNames.push(name); } _variables[name] = value; _prepared = false; } /** * Add file part to loader * If you have already added file with the same fileName it will be overwritten * * @param fileContent File content encoded to ByteArray * @param fileName Name of the file * @param dataField Name of the field containg file data * @param contentType MIME type of the uploading file */ public function addFile(fileContent:ByteArray, fileName:String, dataField:String = 'Filedata', contentType:String = 'application/octet-stream'):void { if (_fileNames.indexOf(fileName) == -1) { _fileNames.push(fileName); _files[fileName] = new FilePart(fileContent, fileName, dataField, contentType); totalFilesSize += fileContent.length; } else { var f:FilePart = _files[fileName] as FilePart; totalFilesSize -= f.fileContent.length; f.fileContent = fileContent; f.fileName = fileName; f.dataField = dataField; f.contentType = contentType; totalFilesSize += fileContent.length; } _prepared = false; } /** * Remove all variable parts */ public function clearVariables():void { _variableNames = new Array(); _variables = new Dictionary(); _prepared = false; } /** * Remove all file parts */ public function clearFiles():void { for each(var name:String in _fileNames) { (_files[name] as FilePart).dispose(); } _fileNames = new Array(); _files = new Dictionary(); totalFilesSize = 0; _prepared = false; } /** * Dispose all class instance objects */ public function dispose(): void { clearInterval(asyncWriteTimeoutId); removeListener(); close(); _loader = null; _boundary = null; _variableNames = null; _variables = null; _fileNames = null; _files = null; requestHeaders = null; _data = null; } /** * Generate random boundary * @return Random boundary */ public function getBoundary():String { if (_boundary == null) { _boundary = ''; for (var i:int = 0; i < 0x20; i++ ) { _boundary += String.fromCharCode( int( 97 + Math.random() * 25 ) ); } } return _boundary; } public function get ASYNC():Boolean { return _async; } public function get PREPARED():Boolean { return _prepared; } public function get dataFormat():String { return _loader.dataFormat; } public function set dataFormat(format:String):void { if (format != URLLoaderDataFormat.BINARY && format != URLLoaderDataFormat.TEXT && format != URLLoaderDataFormat.VARIABLES) { throw new IllegalOperationError('Illegal URLLoader Data Format'); } _loader.dataFormat = format; } public function get loader():URLLoader { return _loader; } private function doSend():void { var urlRequest:URLRequest = new URLRequest(); urlRequest.url = _path; urlRequest.contentType = 'multipart/form-data; boundary=' + getBoundary(); urlRequest.method = URLRequestMethod.POST; urlRequest.data = _data; if (requestHeaders.length && requestHeaders != null){ urlRequest.requestHeaders = requestHeaders.concat(); } addListener(); _loader.load(urlRequest); } private function constructPostDataAsync():void { clearInterval(asyncWriteTimeoutId); _data = new ByteArray(); _data.endian = Endian.BIG_ENDIAN; _data = constructVariablesPart(_data); asyncFilePointer = 0; writtenBytes = 0; _prepared = false; if (_fileNames.length) { nextAsyncLoop(); } else { _data = closeDataObject(_data); _prepared = true; dispatchEvent( new MultipartURLLoaderEvent(MultipartURLLoaderEvent.DATA_PREPARE_COMPLETE) ); } } private function constructPostData():ByteArray { var postData:ByteArray = new ByteArray(); postData.endian = Endian.BIG_ENDIAN; postData = constructVariablesPart(postData); postData = constructFilesPart(postData); postData = closeDataObject(postData); return postData; } private function closeDataObject(postData:ByteArray):ByteArray { postData = BOUNDARY(postData); postData = DOUBLEDASH(postData); return postData; } private function constructVariablesPart(postData:ByteArray):ByteArray { var i:uint; var bytes:String; for each(var name:String in _variableNames) { postData = BOUNDARY(postData); postData = LINEBREAK(postData); bytes = 'Content-Disposition: form-data; name="' + name + '"'; for ( i = 0; i < bytes.length; i++ ) { postData.writeByte( bytes.charCodeAt(i) ); } postData = LINEBREAK(postData); postData = LINEBREAK(postData); postData.writeUTFBytes(_variables[name]); postData = LINEBREAK(postData); } return postData; } private function constructFilesPart(postData:ByteArray):ByteArray { var i:uint; var bytes:String; if(_fileNames.length){ for each(var name:String in _fileNames) { postData = getFilePartHeader(postData, _files[name] as FilePart); postData = getFilePartData(postData, _files[name] as FilePart); postData = LINEBREAK(postData); } postData = closeFilePartsData(postData); } return postData; } private function closeFilePartsData(postData:ByteArray):ByteArray { var i:uint; var bytes:String; postData = LINEBREAK(postData); postData = BOUNDARY(postData); postData = LINEBREAK(postData); bytes = 'Content-Disposition: form-data; name="Upload"'; for ( i = 0; i < bytes.length; i++ ) { postData.writeByte( bytes.charCodeAt(i) ); } postData = LINEBREAK(postData); postData = LINEBREAK(postData); bytes = 'Submit Query'; for ( i = 0; i < bytes.length; i++ ) { postData.writeByte( bytes.charCodeAt(i) ); } postData = LINEBREAK(postData); return postData; } private function getFilePartHeader(postData:ByteArray, part:FilePart):ByteArray { var i:uint; var bytes:String; postData = BOUNDARY(postData); postData = LINEBREAK(postData); bytes = 'Content-Disposition: form-data; name="Filename"'; for ( i = 0; i < bytes.length; i++ ) { postData.writeByte( bytes.charCodeAt(i) ); } postData = LINEBREAK(postData); postData = LINEBREAK(postData); postData.writeUTFBytes(part.fileName); postData = LINEBREAK(postData); postData = BOUNDARY(postData); postData = LINEBREAK(postData); bytes = 'Content-Disposition: form-data; name="' + part.dataField + '"; filename="'; for ( i = 0; i < bytes.length; i++ ) { postData.writeByte( bytes.charCodeAt(i) ); } postData.writeUTFBytes(part.fileName); postData = QUOTATIONMARK(postData); postData = LINEBREAK(postData); bytes = 'Content-Type: ' + part.contentType; for ( i = 0; i < bytes.length; i++ ) { postData.writeByte( bytes.charCodeAt(i) ); } postData = LINEBREAK(postData); postData = LINEBREAK(postData); return postData; } private function getFilePartData(postData:ByteArray, part:FilePart):ByteArray { postData.writeBytes(part.fileContent, 0, part.fileContent.length); return postData; } private function onProgress( event: ProgressEvent ): void { dispatchEvent( event ); } private function onComplete( event: Event ): void { removeListener(); dispatchEvent( event ); } private function onIOError( event: IOErrorEvent ): void { removeListener(); dispatchEvent( event ); } private function onSecurityError( event: SecurityErrorEvent ): void { removeListener(); dispatchEvent( event ); } private function onHTTPStatus( event: HTTPStatusEvent ): void { dispatchEvent( event ); } private function addListener(): void { _loader.addEventListener( Event.COMPLETE, onComplete, false, 0, false ); _loader.addEventListener( ProgressEvent.PROGRESS, onProgress, false, 0, false ); _loader.addEventListener( IOErrorEvent.IO_ERROR, onIOError, false, 0, false ); _loader.addEventListener( HTTPStatusEvent.HTTP_STATUS, onHTTPStatus, false, 0, false ); _loader.addEventListener( SecurityErrorEvent.SECURITY_ERROR, onSecurityError, false, 0, false ); } private function removeListener(): void { _loader.removeEventListener( Event.COMPLETE, onComplete ); _loader.removeEventListener( ProgressEvent.PROGRESS, onProgress ); _loader.removeEventListener( IOErrorEvent.IO_ERROR, onIOError ); _loader.removeEventListener( HTTPStatusEvent.HTTP_STATUS, onHTTPStatus ); _loader.removeEventListener( SecurityErrorEvent.SECURITY_ERROR, onSecurityError ); } private function BOUNDARY(p:ByteArray):ByteArray { var l:int = getBoundary().length; p = DOUBLEDASH(p); for (var i:int = 0; i < l; i++ ) { p.writeByte( _boundary.charCodeAt( i ) ); } return p; } private function LINEBREAK(p:ByteArray):ByteArray { p.writeShort(0x0d0a); return p; } private function QUOTATIONMARK(p:ByteArray):ByteArray { p.writeByte(0x22); return p; } private function DOUBLEDASH(p:ByteArray):ByteArray { p.writeShort(0x2d2d); return p; } private function nextAsyncLoop():void { var fp:FilePart; if (asyncFilePointer < _fileNames.length) { fp = _files[_fileNames[asyncFilePointer]] as FilePart; _data = getFilePartHeader(_data, fp); asyncWriteTimeoutId = setTimeout(writeChunkLoop, 10, _data, fp.fileContent, 0); asyncFilePointer ++; } else { _data = closeFilePartsData(_data); _data = closeDataObject(_data); _prepared = true; dispatchEvent( new MultipartURLLoaderEvent(MultipartURLLoaderEvent.DATA_PREPARE_PROGRESS, totalFilesSize, totalFilesSize) ); dispatchEvent( new MultipartURLLoaderEvent(MultipartURLLoaderEvent.DATA_PREPARE_COMPLETE) ); } } private function writeChunkLoop(dest:ByteArray, data:ByteArray, p:uint = 0):void { var len:uint = Math.min(BLOCK_SIZE, data.length - p); dest.writeBytes(data, p, len); if (len < BLOCK_SIZE || p + len >= data.length) { // Finished writing file bytearray dest = LINEBREAK(dest); nextAsyncLoop(); return; } p += len; writtenBytes += len; if ( writtenBytes % BLOCK_SIZE * 2 == 0 ) { dispatchEvent( new MultipartURLLoaderEvent(MultipartURLLoaderEvent.DATA_PREPARE_PROGRESS, writtenBytes, totalFilesSize) ); } asyncWriteTimeoutId = setTimeout(writeChunkLoop, 10, dest, data, p); } } } internal class FilePart { public var fileContent:flash.utils.ByteArray; public var fileName:String; public var dataField:String; public var contentType:String; public function FilePart(fileContent:flash.utils.ByteArray, fileName:String, dataField:String = 'Filedata', contentType:String = 'application/octet-stream') { this.fileContent = fileContent; this.fileName = fileName; this.dataField = dataField; this.contentType = contentType; } public function dispose():void { fileContent = null; fileName = null; dataField = null; contentType = null; } }