Last Updated:

HTML5 Ajax upload very large files

This short article discusses how to download very large files (more than 2GB) through a browser. The method given in this note is good because:

  • The file is loaded asynchronously. At this time, a progress bar (ProgressBar) is displayed on the page;
  • The file is downloaded in parts and is not read entirely into memory by either the server or the client's browser;
  • The browser retries the loading of the section if the previous attempt failed. This is a very useful property when downloading large files over HTTP.

The bad thing about this method is that it requires the use of a browser based on Gecko 2.0 and above or WebKit. At the moment, these are browsers such as FireFox 4.0+, SeaMonkey 2.1+ or Google Chrome 11+.

Basic idea

The file is read in parts using the slice(), mozSlice() or webkitSlice() methods of the Blob object [2]. After that, each part is sent to the server using the sendAsBinary() object XMLHttpRequest [1]. If the part does not load for some reason, then an attempt is made to download the part again.

Note that the slice() method of the Blob object has been deprecated since Gecko 2.0 (FireFox 4), the mozSlice() method was added to Gecko 5.0 (FireFox 5), and the webkitSlice() method was added by Google Chrome 11.

Even in Google Chrome, there is no sendAsBinary() method of the XMLHttpRequest object.

Code

fileuploader.js

JavaScript code works on the client side (in the browser), reads the file in parts and sends it to the server.

 
Code:
First, let's define the XMLHttpRequest.sendAsBinary()
method if it is not defined (for example, for the Google Chrome browser).

if (! XMLHttpRequest. prototype. sendAsBinary) {

XMLHttpRequest. prototype. sendAsBinary = function(datastr) {
function byteValue(x) {
return x.charCodeAt(0) & 0xff;
}
var ords = Array. prototype. map. call(datastr, byteValue);
var ui8a = new Uint8Array(ords);
this. send(ui8a. buffer);
}
}

/**
* FileUploader class.
* @param ioptions Associative array of loading
options */
function FileUploader(ioptions) {

Position from which we will load this file
. position=0;

The size of the downloaded file
is this. filesize=0;

A Blob or File (FileList[i])
this object. file = null;

An associative array of
options this. options=ioptions;

If the uploadscript option is not defined, we return null. You cannot
continue if this option is not defined.
if (this. options['uploadscript']==undefined) return null;

/*
* Checking whether the browser supports the necessary objects
* @return true if the browser supports all the necessary objects
*/
this. CheckBrowser=function() {
if (window. File && window. FileReader && window. FileList && window. Blob) return true; else return false;
}


/*
* Upload part of a file to a servop
* @param from The position from which we will load the file
*/
this. UploadPortion=function(from) {

FileReader object, in it we will read part of the loaded file
var reader = new FileReader();

The current var object
is that=this;

The position from which we will load the var loadfrom=from file
;

A Blob object to partially read the var file
blob=null;

Timeout for the setTimeout function. This function has implemented a time-out retry
(which is not entirely correct)
var xhrHttpTimeout=null;

/*
* Event triggered after reading part of a file in FileReader
* @param evt Event
*/
reader. onloadend = function(evt) {
if (evt. target. readyState == FileReader. DONE) {

Create an XMLHttpRequest object, set the script address for POST
and the required HTTP request headers.
var xhr = new XMLHttpRequest();
xhr. open('POST', that. options['uploadscript'], true);
xhr. setRequestHeader("Content-Type", "application/x-binary; charset=x-user-defined");

The download ID (to know on the server side what to stick to)
xhr. setRequestHeader("Upload-Id", that. options['uploadid']);
The start position in the xhr file
. setRequestHeader("Portion-From", from);
Serving size
xhr. setRequestHeader("Portion-Size", that. options['portion']);

Let's set a timeout to
that. xhrHttpTimeout=setTimeout(function() {
xhr. abort();
},that. options['timeout']);

/*
* XMLHttpRequest.onProcess Event Rendering ProgressBar.
* @param evt Event
*/
xhr. upload. addEventListener("progress", function(evt) {
if (evt. lengthComputable) {

Let's calculate the amount of downloaded percentage (with an accuracy of 0.1)
var percentComplete = Math.round((loadfrom+evt. loaded) * 1000 / that. filesize); percentComplete/=10;

Calculate the width of the blue strip ProgressBar
var width=Math.round((loadfrom+evt. loaded) * 300 / that. filesize);

Let's change the properties of the ProgressBar element, add the text
var div1=document to it. getElementById('cnuploader_progressbar');
var div2=document. getElementById('cnuploader_progresscomplete');

div1. style. display='block';
div2. style. display='block';
div2. style. width=width+'px';
if (percentComplete<30) {
div2. textContent='';
div1. textContent=percentComplete+'%';
}
else {
div2. textContent=percentComplete+'%';
div1. textContent='';
}
}

}, false);



/*
* XMLHttpRequest.onLoad event. Batch finish loading.
* @param evt Event
*/
xhr. addEventListener("load", function(evt) {

Clear cleartimeout
clearTimeout(that. xhrHttpTimeout);

If the server has not returned HTTP status 200, then display a window with the server message.
if (evt. target. status!=200) {
alert(evt. target. responseText);
return;
}

Add the portion size to the current position.
that. position+=that. options['portion'];

Download the next portion if the file has not yet ended.
if (that. filesize>that. position) {
that. UploadPortion(that. position);
}
else {
If all portions are loaded, let's let the server know. XMLHttpRequest, GET method,
PHP script is the same.
var gxhr = new XMLHttpRequest();
gxhr. open('GET', that. options['uploadscript']+'?action=done', true);

Let's set the ID to the puzzle.
gxhr. setRequestHeader("Upload-Id", that. options['uploadid']);

/*
* XMLHttpRequest.onLoad Event Completion of download of the file completion message :).
* @param evt Event
*/
gxhr. addEventListener("load", function(evt) {

If the server does not return http status 200, then display a window with the server message.
if (evt. target. status!=200) {
alert(evt. target. responseText. toString());
return;
}
If everything is fine, then send the user further. There may be a message
about the successful download or the next step of the form with additional fields.
else window. parent. location=that. options['redirect_success'];
}, false);

Send an HTTP GET request
to gxhr. sendAsBinary('');
}
}, false);

/*
* XMLHttpRequest.onError Event: Error loading
* @param evt Event
*/
xhr. addEventListener("error", function(evt) {

Clear
cleartimeout(that. xhrHttpTimeout);

Let's inform the server about the error during the boot, the server will be able to delete the already downloaded parts.
XMLHttpRequest, GET method, PHP script is the same.
var gxhr = new XMLHttpRequest();

gxhr. open('GET', that. options['uploadscript']+'?action=abort', true);

Let's set the ID to the puzzle.
gxhr. setRequestHeader("Upload-Id", that. options['uploadid']);

/*
* XMLHttpRequest.onLoad Event Completion of Loading error message :).
* @param evt Event
*/
gxhr. addEventListener("load", function(evt) {

If the server does not return http status 200, then display a window with the server message.
if (evt. target. status!=200) {
alert(evt. target. responseText);
return;
}
}, false);

Send HTTP GET request
gxhr. sendAsBinary('');

Let's display an error
message if (that. options['message_error']==undefined) alert("There was an error attempting to upload the file." ); else alert(that. options['message_error']);
}, false);

/*
* XMLHttpRequest.onAbort Event: If for some reason the transfer is interrupted, please try again.
* @param evt Event
*/
xhr. addEventListener("abort", function(evt) {
clearTimeout(that. xhrHttpTimeout);
that. UploadPortion(that. position);
}, false);

Let's send a portion using the POST
xhr method. sendAsBinary(evt. target. result);
}
};

that. blob=null;

Count the portion into a Blob object. Three conditions for three possible Blob definitions.[. *]slice().
if (this. file. slice) that. blob=this. file. slice(from,from+that. options['portion']);
else {
if (this. file. webkitSlice) that. blob=this. file. webkitSlice(from,from+that. options['portion']);
else {
if (this. file. mozSlice) that. blob=this. file. mozSlice(from,from+that. options['portion']);
}
}

Count Blob (part fayla) in fileReader
reader. readAsBinaryString(that. blob);
}


/*
* Uploading a file to the server
* return Number. If not 0, then an error
*/
this has occurred. Upload=function() {

Hide the form so that the user does not send the file twice
var e=document. getElementById(this. options['form']);
if (e) e.style. display='none';

if (! this. file) return -1;
else {

If the file size is larger than the portion size, we will limit ourselves to one portion of
if (this. filesize>this. options['portion']) this. UploadPortion(0,this. options['portion']);

Otherwise, we will send the
entire file else this. UploadPortion(0,this. filesize);
}
}



if (this. CheckBrowser()) {

Let's set the default
values if (this. options['portion']==undefined) this. options['portion']=1048576;
if (this. options['timeout']==undefined) this. options['timeout']=15000;

var that = this;

// Let's add handling of the document file
selection event. getElementById(this. options['formfiles']). addEventListener('change', function (evt) {

var files=evt. target. files;

Select only the first for file
(var i = 0, f; f = files[i]; i++) {
that. filesize=f.size;
that. file = f;
break;
}
}, false);

Let's add handle to the onSubmit event of the
document form. getElementById(this. options['form']). addEventListener('submit', function (evt) {
that. Upload();
(arguments[0]. preventDefault)? arguments[0]. preventDefault(): arguments[0]. returnValue = false;
}, false
);
        }


    }
 

upload.php

PHP script works on the server side. Its task is to accept and glue portions. After the transfer of all portions is finished, the script renames the file and creates a flag signaling that the file is ready for processing. In my case, a separate daemon checks this flag and takes the files "into circulation".

 
Code:
The directory where the file
will be uploaded $uploaddir="./uploaddir";

Boot ID (apollo). To generate the ID, I typically use the md5()
function $hash=$_SERVER["HTTP_UPLOAD_ID"];

Information about the progress of the download will be saved in the system log, this will allow you to solve problems more quickly
openlog("html5upload.php", LOG_PID | LOG_PERROR, LOG_LOCAL0);

Let's check the correctness of
the if identifier (preg_match("/^[0123456789abcdef]{32}$/i",$hash)) {


If the HTTP request is made by the GET method, then this is not loading a portion, but post-processing
if ($_SERVER ["REQUEST_METHOD"]=="GET") {

abort - erase the downloaded file. Download failed.
if ($_GET["action"]=="abort") {
if (is_file($uploaddir. "/". $hash. ".html5upload")) unlink($uploaddir. "/". $hash. ".html5upload");
print "ok abort";
return;
}

done - the download completed successfully. Rename the file and create a flag file.
if ($_GET["action"]=="done") {
syslog(LOG_INFO, "Finished for hash ". $hash);

If the file exists, then delete it
if (is_file($uploaddir. "/". $hash. ".original")) unlink($uploaddir. "/". $hash. ".original");

Rename the download file
rename($uploaddir. "/". $hash. ".html5upload",$uploaddir. "/". $hash. ".original");

Let's create a flag
file $fw=fopen($uploaddir. "/". $hash. ".original_ready","wb"); if ($fw) fclose($fw);
}
}

If an HTTP request is made by the POST method, it is loading a portion of
elseif ($_SERVER["REQUEST_METHOD"]=="POST") {

syslog(LOG_INFO , "Uploading chunk. Hash ". $hash. " (". intval($_SERVER["HTTP_PORTION_FROM"]). "-". intval($_SERVER["HTTP_PORTION_FROM"]+$_SERVER["HTTP_PORTION_SIZE"]). ", size: ". intval($_SERVER["HTTP_PORTION_SIZE"]). ")");

We get the file name from the download
ID $filename=$uploaddir. "/". $hash. ".html5upload";

If the first portion is loaded, then we will open the file for recording, if not the first, then for additional recording.
if (intval($_SERVER["HTTP_PORTION_FROM"])==0)
$fout=fopen($filename,"wb");
else
$fout=fopen($filename,"ab");

If you could not open the file for writing, we give an error
message if (! $fout) {
syslog(LOG_INFO, "Can't open file for writing: ". $filename);
header("HTTP/1.0 500 Internal Server Error");
print "Can't open file for writing." ;
return;
}

From stdin we read the data sent by the POST method - this is the content of
the portions $fin = fopen("php://input", "rb");
if ($fin) {
while (! feof($fin)) {
Count 1Mb of stdin
$data=fread($fin, 1024*1024);
Save the read data to the file
fwrite($fout,$data);
}
fclose($fin);
}

fclose($fout);
} Everything is

fine, return HTTP 200 and the response body "ok"
header("HTTP/1.0 200 OK");
print "ok\n";
}
else {
If the download ID is incorrect, then return HTTP 500 and the error
message syslog(LOG_INFO, "Uploading chunk. Wrong hash ". $hash);
header("HTTP/1.0 500 Internal Server Error");
print "Wrong session hash." ;
}

Close syslog log
closelog
();
 

index.php

A file with an HTML form and the initialization of the loading script.

The title of the HTML document. Here, for example, the download ID is replaced. You can generate it at your discretion.

 
Code:
 
<?php
$hash=htmlspecialchars(stripslashes($_GET["hash"]));
$hash=md5("test");

> <! DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head>
<title>video.novgorod.ru</title>
<meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
 

Style sheet for progress bar (PorgressBar)

 
Code:
 
<style type="text/css">
#cnuploader_progressbar {display:none;margin-top:10px;height:16px;font-family:sans-serif;font-size:12px;padding:3px;width:300px;position:absolute;text-align:center;color:black;border:1px solid black;display:hidden;}
#cnuploader_progresscomplete {display:none;margin-top:10px;height:16px;font-family:sans-serif;font-size:12px;padding:3px;width:0;text-align:center;background-color:blue;color:white;border:1px solid transparent;display:hidden;}
</style>
 

Connect the boot class. Anti-cash just in case.

 
Code:
 
<script type="text/javascript" src="./fileuploader.js?nc=<?php print time();? >"></script>

Upload form. Initially, it is hidden, it is displayed only after the boot loader object has been successfully initialized.

 
Code:
</head>
<body onload="ShowForm();"
> <div>

<p>The maximum file size when downloaded via a browser is <b>4Gb</b>. </p>

<form action="./" method="post" id="uploadform" onsubmit="return false;" style="display:none;"

> <table cellspacing=1>
<tr><td><div id="message">Select file:</div></td><td><input type="file" id= "files" name="files[]" /></td></t>
</table>
<input type="submit" value="Load >>" />

</ form>
 

Progress indicator (PorgressBar) and hints

 
Code:
 
<div id="cnuploader_progressbar"></div>
<div id="cnuploader_progresscomplete"></div>

<p>You can add a title and description after the video is uploaded to the server. </p>

<p>We accept videos that do not violate the requirements of Russian legislation (not containing pornography, profanity, incitement to violence, etc.) and do not contain advertising. </p>
<p>The HTML5</p> </div>
<script type="text/javascript">
function ShowForm() {

Create an object - FileUploader. Specify the options.
var uploader=new FileUploader( {


error message message_error: 'Error loading file',


form element ID: 'uploadform',

element ID <input type=file
formfiles: 'files',

Download ID. In our case, the hash.
uploadid: '<?php print $hash;? >',

the URL of the download script (described above).
uploadscript: './upload.php',

URL, where to redirect the user if
the redirect_success successful download: './step2.php?hash=<?php print $hash;? >',

the URL where to send the user if the download fails
redirect_abort: './abort.php?hash=<?php print $hash;? >',


Serving Size. 2 MB
portion: 1024*1024*2
});

If the object could not be created, we will redirect the user to a simple form of the puzzle.

if (! uploader) document. location='/upload/simple.php?hash=<?php print $hash;? >';
else {
If the browser is not supported, we will redirect the user to a simple form of gloom.
if (! uploader. CheckBrowser()) document. location='/upload/simple.php?hash=<?php print $hash;? >';
else {
If everything is fine, then display the form (by default it is hidden)
var e=document. getElementById('uploadform');
if (e) e.style. display='block'
;

            }
        }
    }
</script>

</body>
</html>