Using HTML5 file uploads with AJAX and jQuery

206,933

Solution 1

It's not too hard. Firstly, take a look at FileReader Interface.

So, when the form is submitted, catch the submission process and

var file = document.getElementById('fileBox').files[0]; //Files[0] = 1st file
var reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = shipOff;
//reader.onloadstart = ...
//reader.onprogress = ... <-- Allows you to update a progress bar.
//reader.onabort = ...
//reader.onerror = ...
//reader.onloadend = ...


function shipOff(event) {
    var result = event.target.result;
    var fileName = document.getElementById('fileBox').files[0].name; //Should be 'picture.jpg'
    $.post('/myscript.php', { data: result, name: fileName }, continueSubmission);
}

Then, on the server side (i.e. myscript.php):

$data = $_POST['data'];
$fileName = $_POST['name'];
$serverFile = time().$fileName;
$fp = fopen('/uploads/'.$serverFile,'w'); //Prepends timestamp to prevent overwriting
fwrite($fp, $data);
fclose($fp);
$returnData = array( "serverFile" => $serverFile );
echo json_encode($returnData);

Or something like it. I may be mistaken (and if I am, please, correct me), but this should store the file as something like 1287916771myPicture.jpg in /uploads/ on your server, and respond with a JSON variable (to a continueSubmission() function) containing the fileName on the server.

Check out fwrite() and jQuery.post().

On the above page it details how to use readAsBinaryString(), readAsDataUrl(), and readAsArrayBuffer() for your other needs (e.g. images, videos, etc).

Solution 2

With jQuery (and without FormData API) you can use something like this:

function readFile(file){
   var loader = new FileReader();
   var def = $.Deferred(), promise = def.promise();

   //--- provide classic deferred interface
   loader.onload = function (e) { def.resolve(e.target.result); };
   loader.onprogress = loader.onloadstart = function (e) { def.notify(e); };
   loader.onerror = loader.onabort = function (e) { def.reject(e); };
   promise.abort = function () { return loader.abort.apply(loader, arguments); };

   loader.readAsBinaryString(file);

   return promise;
}

function upload(url, data){
    var def = $.Deferred(), promise = def.promise();
    var mul = buildMultipart(data);
    var req = $.ajax({
        url: url,
        data: mul.data,
        processData: false,
        type: "post",
        async: true,
        contentType: "multipart/form-data; boundary="+mul.bound,
        xhr: function() {
            var xhr = jQuery.ajaxSettings.xhr();
            if (xhr.upload) {

                xhr.upload.addEventListener('progress', function(event) {
                    var percent = 0;
                    var position = event.loaded || event.position; /*event.position is deprecated*/
                    var total = event.total;
                    if (event.lengthComputable) {
                        percent = Math.ceil(position / total * 100);
                        def.notify(percent);
                    }                    
                }, false);
            }
            return xhr;
        }
    });
    req.done(function(){ def.resolve.apply(def, arguments); })
       .fail(function(){ def.reject.apply(def, arguments); });

    promise.abort = function(){ return req.abort.apply(req, arguments); }

    return promise;
}

var buildMultipart = function(data){
    var key, crunks = [], bound = false;
    while (!bound) {
        bound = $.md5 ? $.md5(new Date().valueOf()) : (new Date().valueOf());
        for (key in data) if (~data[key].indexOf(bound)) { bound = false; continue; }
    }

    for (var key = 0, l = data.length; key < l; key++){
        if (typeof(data[key].value) !== "string") {
            crunks.push("--"+bound+"\r\n"+
                "Content-Disposition: form-data; name=\""+data[key].name+"\"; filename=\""+data[key].value[1]+"\"\r\n"+
                "Content-Type: application/octet-stream\r\n"+
                "Content-Transfer-Encoding: binary\r\n\r\n"+
                data[key].value[0]);
        }else{
            crunks.push("--"+bound+"\r\n"+
                "Content-Disposition: form-data; name=\""+data[key].name+"\"\r\n\r\n"+
                data[key].value);
        }
    }

    return {
        bound: bound,
        data: crunks.join("\r\n")+"\r\n--"+bound+"--"
    };
};

//----------
//---------- On submit form:
var form = $("form");
var $file = form.find("#file");
readFile($file[0].files[0]).done(function(fileData){
   var formData = form.find(":input:not('#file')").serializeArray();
   formData.file = [fileData, $file[0].files[0].name];
   upload(form.attr("action"), formData).done(function(){ alert("successfully uploaded!"); });
});

With FormData API you just have to add all fields of your form to FormData object and send it via $.ajax({ url: url, data: formData, processData: false, contentType: false, type:"POST"})

Share:
206,933
Joshua Cody
Author by

Joshua Cody

Updated on July 08, 2022

Comments

  • Joshua Cody
    Joshua Cody almost 2 years

    Admittedly, there are similar questions lying around on Stack Overflow, but it seems none quite meet my requirements.

    Here is what I'm looking to do:

    • Upload an entire form of data, one piece of which is a single file
    • Work with Codeigniter's file upload library

    Up until here, all is well. The data gets in my database as I need it. But I'd also like to submit my form via an AJAX post:

    • Using the native HTML5 File API, not flash or an iframe solution
    • Preferably interfacing with the low-level .ajax() jQuery method

    I think I could imagine how to do this by auto-uploading the file when the field's value changes using pure javascript, but I'd rather do it all in one fell swoop on for submit in jQuery. I'm thinking it's not possible to do via query strings as I need to pass the entire file object, but I'm a little lost on what to do at this point.

    Can this be achieved?

  • Joshua Cody
    Joshua Cody over 13 years
    Hey Clark, am I understanding correctly? This will send the uploaded file as soon as it's loaded into the FileReader constructor from the file system, bypassing jQuery's low-level .ajax handler. Then the rest of the form will submit as normal?
  • Joshua Cody
    Joshua Cody over 13 years
    All right, so I was wrong in my understanding before. Now I'm taking the readAsDataUrl of an image, adding it to my datastring in .ajax, and submitting all my info together. My previous solution involved CodeIgniter's default file input class which grabbed data from $_FILES['field'], so it looks like I'll need to switch to a different solution for parsing the base64 image data. Any advice on that is welcomed, upvoting your answer here, and once I finish implementation, I'll mark it as correct.
  • clarkf
    clarkf over 13 years
    @Joshua Cody - I updated the answer to give a little more detail. You'll have to forgive that I haven't used CodeIgniter in many moons and couldn't tell you how to integrate this into their codebase. I'm not sure why you need to upload the file before submission, but this should at least give you a clue. (You could also insert the image into a database if that's better for you).
  • Joshua Cody
    Joshua Cody over 13 years
    @Clarkf, I don't need to upload before submission, I was misunderstanding your previous example :) After SO went down and I spent some time on the w3 and HTML5Rocks, I started to understand. I'll give this a shot and be back here.
  • Joshua Cody
    Joshua Cody over 13 years
    All right, been messing with this all morning. PHP seems to be returning badly-formatted files. See two images, one rendered immediately and the other after a $_POST to the server and immediate echo. A diff on the two elements reveals this, that apparently PHP is stripping all "+" characters. A str_replace fixes this for immediate return, but the file that's saved is still corrupt and can't be opened via my file system. Also, going ahead and marking this as correct. Thanks so much for your help so far.
  • clarkf
    clarkf over 13 years
    @Joshua Cody - Ah, well, it seems the problem lay in the encoding aspect. There are two different ways to encode a space character (' ') for use in a query string - '%20' and '+'. jQuery's .post() method utilizes their .param() method, which formats according to the +. Running a quick test, $.param({hi: 'HELLO + WORLD'}) yields hi=HELLO+%2B+WORLD, meaning the literal + is encoded as %2B, and for some reason PHP is double-unserializing the data.
  • Joshua Cody
    Joshua Cody over 13 years
    Hey Clark, I got the solution. Turns out I needed to URIEncode what I was passing, then do a preg_replace to pull out the file's metadata, then base64_decode what that left me, then write those contents to disk. Perfect! Thanks so much for all of your help!
  • aroth
    aroth about 11 years
    Question; would it not be better to bind the onload handler first, before calling readAsText? JavaScript is single-threaded so it probably doesn't matter, but is there any specific reason why the example code sets the onload handler second?
  • clarkf
    clarkf about 11 years
    @aroth: Nope. As a matter of fact, it probably would be better to bind all callbacks before setting the process in motion. In practice, I'm sure you would not miss any events, but, theoretically, the FileReader could trigger an event before the next tick, where the handlers would be bound.
  • Admin
    Admin almost 11 years
    This solution does not address the limitation that XMLHttpRequest.send() imposes upon the data funnelled through it. When passed a string (such as your multipart), send() does not support binary data. Your multipart here will be treated as a utf-8 string, and will choke on or corrupt binary data that is not valid utf-8. If you really need to avoid FormData, you need to use XMLHttpRequest.sendAsBinary() (polyfill available. Unfortunately this means that using jQuery for the ajax call becomes much more difficult.