Yeah chunking is good but I tried to simplify things to be easier to
understand. When you understand the basics than you have foundation you can
build on ;)
---
Željko
http://skitanja.blogspot.com/

"you can't stay young forever...
but you can be immature for the rest of your life"
Unknown




2009/10/1 dk_mSigsgaard <[email protected]>

>
> It seems to me forming one request over multiple files is a bit
> overdoing it. If one goes wrong, both server and client may easily get
> confused about which it was.. Use chunking! Since from a blob you can
> use the blob.slice method, this will be easy.
>
> This implementation will send 100k parts of a file, validate it on
> server, send CONTINUE - and proceed until a file is complete. Then, if
> more files are present in the GearsUpl queue, we venture forth :)
>
> You might need to uncomment 'console.log'
> And you need to instantiate a the Gears, try this in a html script
> tag:
> if (!window.google) {
>        document.write("You must have <a href='http://
> gears.google.com/'>Google Gears</a> installed and enabled!");
> }
> else if (parseInt(window.google.gears.factory.version.replace(/\./
> gi,"")) < 05210) {
>        document.write("You must have Gears 0.5.21.0 or newer installed.");
> else {
>  var GUPL = new GearsUpload;
>  GUPL.addFile(/*GearsBlob*/ myfile);
>  GUPL.addFile(/*GearsBlob*/ myfile);
>  GUPL.addFile(/*GearsBlob*/ myfile);
>  // ...
> }
>
>
> Scripts should be work with somewhat like this:
>
> -= JS Parts =-
> -------------------
> GearsUpl = function() {
>  this.mayoverride = {};
>  this.CHUNKSIZE = 100000; // split up in 100k chunks
>  this.queue=[ ];
>
>  this.filefailed = function(server_says_why) {
>    console.log("Error with" + this.queue[0].name + " : " +
> server_says_why);
>    this.queue.splice(0,1);
>  }
>  this.addFile(fileObj) {
>    this.queue[this.queue.length] = {
>      "name":fileObj.name,
>      "file":fileObj,
>      "bytes":fileObj.blob.length,
>      "chunks":(fileObj.blob.length > this.CHUNK_BYTES)?Math.ceil
> (fileObj.blob.length/this.CHUNKSIZE):1,
>      "uploaded":0,
>      "retryChunk":0
>    };
>  }
>  this.processQueue = function() {
>    if(this.queue.length > 0) {
>      if(this.queue[0]['uploaded'] == this.queue[0]['chunks']) {
>        this.queue.splice(0,1);
>        console.log('File complete ['+this.queue.name+']');
>        this.processQueue();
>      } else {
>        var f = this.queue[0];
>        var start=0;
>        var offset=this.CHUNK_BYTES;
>        if(f['uploaded'] > 0) start = f['uploaded']*this.CHUNK_BYTES;
>        var end = start+this.CHUNK_BYTES-1;
>        if(end > f['bytes']) end = f['bytes']-1;
>
>        if( (start+offset) > (f['bytes']-1) ) offset = (f['bytes']-
> start);
>        var thischunk = f['file'].blob.slice(start,offset);
>        this.sendChunk(f['name'], thischunk,start,end,f['bytes']);
>      }
>    }
>  }
>  this.sendChunk(filename, chunk, start, end, total) {
>    var request = google.gears.factory.create('beta.httprequest');
>    request.open('POST', 'upload.php');
>    request.setRequestHeader('Content-Disposition', 'attachment;
> filename="' + filename + '"');
>    request.setRequestHeader('Content-Type', 'application/octet-
> stream');
>    // use the 'Content-Description' field to send special requests
> regarding file
>    if(typeof this.mayoverwrite[filename] != "undefined")
>      request.setRequestHeader('Content-Description',
> 'overwrite=true');
>    request.setRequestHeader('Content-Range', 'bytes '+start+'-'+end
> +'/'+total);
>
>    var self = this; // since its asynchrone, self reference is needed
>    // now handle the feedback server sends on each request
>    // I use this json format: { status : integer,
> statusmessage:'something textual' }
>                request.onreadystatechange = function() {
>                        if (request.readyState == 4) {
>                                // e.g. { 'status' : 100, 'statusmessage':'X
> Bytes received,
> Continue plz'}
>                                eval("var status = "+request.responseText);
>
>                                if(status['status'] == 100) {
>                                        if(self.queue[0]['retryChunk'] > 0)
> self.queue[0]['retryChunk']
> =0;
>                                        self.queue[0]['uploaded']+=1;
>                                        console.log("Chunk
> "+self.queue[0]['uploaded']+" / "+self.queue[0]
> ['chunks']+" uploaded. ");
>                                }
>                                else if(status['status'] == 101) {
>                                        // resume? FIXME: still a TODO since
> chunk offsets are calculated
> with zero as base
>                                        if(confirm("File exists and may be
> resumed, click 'yes' to
> continue, otherwise upload is discarded")) {
>
>  self.resumefileOffset[self.queue[0]['name']] = status['offset'];
>                                                return;
>                                        }
>                                }
>                                else if(status['status'] == 102) {
>                                        if(self.queue[0]['retryChunk'] < 4)
> {
>
>  self.queue[0]['retryChunk']++;
>                                                console.log("Chunk
> "+(self.queue[0]['uploaded']+1)+" /
> "+self.queue[0]['chunks']+" Failed to upload. Retry attempt
> #"+self.queue[0]['retryChunk']+".");
>                                        }
>                                        else {
>                                                self.fileFailed("Upload
> failed after five retries. Server says:
> " + status.statusmessage);
>                                        }
>                                }
>                                else if(status['status'] == 550) {
>                                        if(confirm("File allready exists
> ("+self.queue[0]['name']+"),
> want to force overwrite?"))
>
>  self.mayoverwrite[self.queue[0]['name']] = true;
>                                        else
> self.fileFailed(status['statusmessage']);
>                                }
>                                else {
>                                        self.fileFailed("[" +
> status['status'] + "] " + status
> ['statusmessage']);
>                                }
>                                self.processQueue();
>                        }
>                };
>    request.send( chunk );
>  }
> };
> -= PHP Parts =-
> <?php
> class GUP {
>
>        function __construct() {
>                $this->uploadDir = dirname(__FILE__)."/uploads/"; //
> remember
> trailing slash
>                $this->validExtensions = array
> ("png","gif","jpg","jpeg","zip","tar","gz","bz2","7z","rar");
>        }
>
>        var $uploadDir;
>        var $validExtensions;
>        var $messages = array(
>                'ERROR_UNKNOWN' => array(
>                        'status' =>             500,
>                        'statusmessage' =>      "There was an unexpected
> error."
>                ),
>                'ERROR_PERMISSION' => array(
>                        'status' =>                     405,
>                        'statusmessage' =>      "Permissions on server are
> inadequate."
>                ),
>                'ERROR_EXISTS' => array(
>                        'status' =>                     550,
>                        'statusmessage' =>      "File exists, request
> overwrite"
>                ),
>                'CONTINUE' => array(
>                        'status' =>                     100,
>                        'statusmessage'=>       "Partial upload complete."
>                ),
>                'COMPLETE' => array(
>                        'status' =>                     100,
>                        'statusmessage'=>       "Upload complete."
>                ),
>                'RESUME' => array(
>                        'status' =>                     101,
>                        'offset' =>                     0,
>                        'statusmessage'=>       "Resuming upload, started
> earlier in this
> browsersession."
>                ),
>                'INVALID_RANGE' => array(
>                        'status '=>                     102,
>                        'statusmessage'=>       "Received number of bytes
> does not match expected
> range."
>                ),
>                'INVALID_EXTENSION' => array(
>                        'status '=>                     103,
>                        'statusmessage'=>       "Filename does not match a
> valid extension."
>                )
>        );
>        function setExtensions($str) {
>                $this->validExtensions = explode(",", $str);
>                for($i = 0 ; $i < count($this->validExtensions); $i++)
>                        $this->validExtensions[$i] =
> trim($this->validExtensions[$i]);
>        }
>
>        function checkExtension($filename) {
>                if(!strstr($filename, ".")) return false;
>                $ext = substr($filename, strrpos($filename, ".")+1);
>                foreach($this->validExtensions as $valid)
>                        if($valid == $ext)
>                                return true;
>                return false;
>        }
>        function getByteRange() {
>                if(!isset($_SERVER['HTTP_CONTENT_RANGE'])) return null;
>                $range = preg_replace("@.*bytes\ ([.]*)@", "$1", $_SERVER
> ['HTTP_CONTENT_RANGE']);
>                $range = explode('/', $range);
>                $ret = explode('-', $range[0]);
>                $ret[] = $range[1];
>                return $ret; // [ start, end, total ]
>        }
>        function checkByteRange($receivedLength) {
>                $range = $this->getByteRange();
>                return ( $range == null ? false : ($range[1]-$range[0]) ==
> $receivedLength);
>        }
>        function checkComplete($targetFile) {
>                $filename = $this->uploadDir.$targetFile;
>                if(!file_exists($filename))
>                        return false;
>                else if(!isset($_SERVER['HTTP_CONTENT_RANGE']))
>                        return false;
>
>                $total = substr($_SERVER['HTTP_CONTENT_RANGE'],
> strrpos($_SERVER
> ['HTTP_CONTENT_RANGE'], '/')+1);
>                return (filesize($targetFile) == $total);
>        }
>        function getFileName() {
>                return preg_replace('@.*filename="([^"]*).*@', "$1",
> $_SERVER
> ["HTTP_CONTENT_DISPOSITION"]);
>        }
>        function getMayOverWrite() {
>                if(!strstr($_SERVER['HTTP_CONTENT_DESCRIPTION'],
> "overwrite"))
>                        return false;
>                $ok =
> preg_replace('@.*overwrite=(true|false|TRUE|FALSE|0|1).*@',
> "$1",
>                        $_SERVER['HTTP_CONTENT_DESCRIPTION']);
>                eval(" \$overwrite = ".(strlen($ok) == 0 ? "false" :
> $ok).";"); //
> evaluate
>                return !!$overwrite; // force boolean conversion
>        }
>        function getMayResume() {
>                if(!strstr($_SERVER['HTTP_CONTENT_DESCRIPTION'], "resume"))
>                        return false;
>                $ok =
> preg_replace('@.*resume=(true|false|TRUE|FALSE|0|1).*@',
> "$1",
>                        $_SERVER['HTTP_CONTENT_DESCRIPTION']);
>                eval(" \$overwrite = ".(strlen($ok) == 0 ? "false" :
> $ok).";"); //
> evaluate
>                return !!$overwrite; // force boolean conversion
>        }
>        function readFromInputStream($targetFile) {
>                $fd = @fopen("php://input", "r");
>                $received = 0;
>                ob_start();
>                while( $data = fread( $fd, 10000000 ) ) {
>                        $received = $received + file_put_contents(
> $this->uploadDir.
> $targetFile, $data, FILE_APPEND );
>                }
>                @fclose($fd);
>                if(strstr(ob_get_clean(), "permission"))
>                        { echo
> json_encode($this->messages['ERROR_PERMISSION']); exit; }
>                return $received;
>        }
>        function handleStream() {
>                if(strstr($_SERVER["HTTP_CONTENT_DISPOSITION"],
> "attachment")) {
>                        session_start();
>                        foreach($_SERVER as $key=>$val) if(strstr($key,
> "HTTP_")) echo
> "[ $key ] => $val\n";
>                        $ok = false;
>
>                        $name = $this->getFileName();
>                        if(!isset($_SESSION['uploadSession'][$name])) {
>                                // test if file exists
>                                if(file_exists($this->uploadDir.$name))
>                                {
>                                        if(!$this->getMayOverwrite()) {
>                                                echo
> json_encode($this->messages['ERROR_EXISTS']);
>                                        } else $ok = true;
>                                } else {
>                                        if(!$this->checkExtension($name))
>                                                echo
> json_encode($this->messages['INVALID_EXTENSION']);
>                                        else $ok = true;
>                                }
>
>
>                                if($ok) $_SESSION['uploadSession'][$name] =
> true;
>                        } else {
>                                // are we resuming?
>                                $range = $this->getByteRange();
>                                if(file_exists($this->uploadDir.$name)
>                                && filesize($this->uploadDir.$name) !=
> $range[0]) {
>                                        if(!$this->getMayOverwrite()) {
>
>  $this->messages['RESUME']['offset'] = filesize($this->uploadDir.
> $name)+1;
>                                                echo
> json_encode($this->messages['ERROR_EXISTS']);
>                                                // FIXME: NOT IMPLEMENTED IN
> JS echo json_encode($this->messages
> ['RESUME']);
>                                        }
>                                        else if($this->getMayResume()) $ok =
> true;
>                                }
>                        }
>
>                        if($ok) {
>                                $bytes = $this->readFromInputStream();
>                                if($this->checkComplete($name)) {
>
>  unset($_SESSION['uploadSession'][$name]);
>                                        echo
> json_encode($this->messages['COMPLETE']);
>                                } else
>                                        echo
> json_encode($this->messages['CONTINUE']);
>                        }
>                }
>        }
> }
>
>
> $uploadHandle = new GUP();
> $uploadHandle->handleStream();
> ?>
>
>
>
> As said, is not entirely tested, but it should give the basic idea..
> The most basic php-handling would be somewhat like this:
>                $fd = @fopen("php://input", "r");
>                while( $data = fread( $fd, 10000000 ) ) {file_put_contents
> ( GUP::getFileName(), $data, FILE_APPEND );}
>                fclose($fd);
>                echo '{"status":100, "statusmessage":"seed me!"}';
>
>
> Feel free to ask and subject bugs or errors, anytime!
>
> Best Regards
> Morten Sigsgaard
> -------------------
> On 28 Sep., 15:22, Željko Mitrović <[email protected]> wrote:
> > -- snip --
> > gearsHttpRequest.open('POST', myUrl);
> > gearsHttpRequest.setRequestHeader('content-type',
> > 'multipart/form-data;boundary=' + boundary);
> >
> > gearsHttpRequest.send(builder.getAsBlob());
> > -- snip --
>
> Antal <[email protected]>
> >
> >
> >
> > > Hello,
> >
> > > I have searched over a day now, but I still can't find a good solution
> > > to post my blobs from my local storage to a remote server. I only
> > > found ones that partially worked. Can somebody post me a good example
> > > that would help me understand how to do this?
> >
> > > I am using PHP server side, so the best solution would be something
> > > that could send data that would populate the $_FILES and the $_POST
> > > (send supplementary data with the file), but of course any other
> > > working solution would be good.
> >
> > > Thank you in advance,
> > > István
>

Reply via email to