knightly

Blog Archives

Drag & drop Uploads with XMLHttpRequest2 and PHP Revised

A while back i wrote a post where i explained how to implement the new XMLHttpRequest2 object. The main point of the post was to use sendAsBinary() so we can stream file uploads from the client to the server.

The post i made showed some code snippets to make this possible. And the Javascript part is all fine and dandy. But last week i had an interesting mail conversation with Jean-Pierre Vincent about the memory consumption on the server side of things.

The result of some testing revealed the server would need at least the amount of memory equal to the file size. For small files this is no problem. But with bigger files this becomes a problem. Although i couldn’t reproduce Jean-Pierre’s results. I wasn’t very happy with the test results.

Upload 2.8 MB file results in 3.1 MB memory usage
Upload 29 MB file results in 30 MB memory usage

A bit more testing revealed that file_put_contents() was the culprit. Which seems logical if you think about it. It reads the file into memory and dumps it again. Not very elegant for big files. Besides we are trying to stream files. So why should we read them completely in to memory? We shouldn’t :)

Jean-Pierre decided to go with the DataForm object to solve the issue. I still want to look into that. But have not found any information about. Besides that i knew the problem was on the server side. So i rewrote the receive() method to be less memory intensive. The results are considerably better then before.

Upload 2.8 MB file results in 0.4 MB memory usage
Upload 29 MB file results in 0.4 MB memory usage

The memory usage was measured with memory_get_peak_usage(). And the new code is posted below:

public function receive()
{
    if (!$this->isValid()) {
        throw new Exception('No file uploaded!');
    }

    $fileReader = fopen('php://input', "r");
    $fileWriter = fopen($this->_destination . $this->_fileName, "w+");

    while(true) {
        $buffer = fgets($fileReader, 4096);
        if (strlen($buffer) == 0) {
            fclose($fileReader);
            fclose($fileWriter);
            return true;
        }

        fwrite($fileWriter, $buffer);
    }

    return false;
}

Drag & drop Uploads with XMLHttpRequest2 and PHP

I finally had some time to read through my ever growing list must read items and play with some new software. While reading up on the new Firefox 3.6 i noticed it came with the new XMLHttpRequest [2] object based on the new file API. And according to the new specs. This would allow for easy file uploads. Now there’s been some examples [2] on the web already. But i just wanted to get my hands dirty.

The new XMLHttpRequest object makes is possible to send files in a few different formats. The most important being the binary format. The code for sending a request with XMLHttpRequest2 looks the same as the previous version. Except for sendAsBinary() in this case.

var xhr = new XMLHttpRequest();

fileUpload = xhr.upload,
fileUpload.onload = function() {
    console.log("Sent!");
}

xhr.open("POST", "upload.php", true);
xhr.sendAsBinary(file.getAsBinary());

So let’s set things up for drag & drop. We need a div that will be the main drop point. And we need some event listeners to catch the drag * drop events. Let start by creating the drop zone. For this we use two simple divs. The outer div will listen for the drag & drop events. And the inner will catch the files.

Now let’s create our upload code.

var upload = {
    setup : function() {},
    uploadFiles : function() {event}
}
window.addEventListener("load", upload.setup, false);

The setup method will set all event listeners for drag & drop. And register the upload handler.

var container = document.getElementById('container');
var drop = document.getElementById('drop');

container.addEventListener("dragenter", function(event) {
        drop.innerHTML = '';
        event.stopPropagation();
        event.preventDefault();
    },
    false
);

container.addEventListener("dragover", function(event) {
        event.stopPropagation();
	event.preventDefault();
    },
    false
);

container.addEventListener("drop", upload.uploadFiles, false);

As you could see above. the uploadFiles() method gets a event returned from the drag & drop action. This is where the new file APi comes in play. To get to the file property we access the dataTransfer object.

var files = event.dataTransfer.files;

The actual uploading is easy as cake.

for (var x = 0; x < files.length; x++) {

    var file = files.item(x);
    var xhr = new XMLHttpRequest();

     fileUpload = xhr.upload,
     fileUpload.onload = function() {
         console.log("Sent!");
    }

    xhr.open("POST", "upload.php", true);

    xhr.setRequestHeader("Cache-Control", "no-cache");
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    xhr.setRequestHeader("X-File-Name", file.fileName);
    xhr.setRequestHeader("X-File-Size", file.fileSize);
    xhr.setRequestHeader("Content-Type", "multipart/form-data");

    xhr.sendAsBinary(file.getAsBinary());
}

That's it for the client side. There is however a small problem on the receiving side. When handling uploaded files in PHP we expect the $_FILES array to be populated. This is not the case when streaming files from the client to the server. To get the needed file information we set some headers on the client side X-File-Name and X-File-Size. And since the $_FILES are is empty. We need an other way to get the file contents. So we will use php://input streams for that.

The contents of upload.php look like this:

require_once('Streamer.php');

$ft = new File_Streamer();
$ft->setDestination('data/');
$ft->receive();

With setDestination() the destination path for the uploaded files is set. And recieve() listens for any incoming files. Most of the magic is done in the recieve() method. So here's the code.

public function receive()
{
    if (!$this->isValid()) {
        throw new Exception('No file uploaded!');
    }

    file_put_contents(
        $this->_destination . $this->_fileName,
        file_get_contents("php://input")
    );

    return true;
}

I am impressed! This promises a lot of good. And offers some interesting options. Let's hope all browsers implement this gem. I still have one issue though. I can't get this to work in firefox under linux. The drag & drop events do not seem to function properly with files being dragged from the desktop. anybody know why?

If you interested in the complete code. you can find it here

**Small update**
http://lenss.nl/2010/09/drag-drop-uploads-with-xmlhttprequest2-and-php-revised/

Stop ACTA