Fragmented Thought

HTML Drag and Drop File Upload With PHP Backend

By

Published:

Lance Gliser

Heads up! This content is more than six months old. Take some time to verify everything still works as expected.

This one fought me, for hours yesterday. A client wanted drag and drop implemented for file on their server. This can already be done in most modern browsers by simply dropping the files onto the file input, but that's a very awkward UI, likely to cause issues. Most users aren't experienced enough to know they can do that. So you have three reasonable choices:

  • At a minimum, prevent drag and drop on non-registered elements causing the page to load that image. Force them to use the native handling of files dropped onto file inputs.
  • A little more reasonable would be to create a region that makes dragging obvious, and a large target for them to aim for that lights up when they are in place.
  • Or, perhaps the best if you only have a single file input, and it's the most important thing on the page anyway, make the entire page listen for a dropped file and handle it.

My particular scenario yesterday was #3, but #2 would be done in roughly the same way. I'm going to post some slightly more than bare bones code for javascript, and php. I'll leave the css and html up to you. It's still a bit raw, the goal was a functioning prototype for working scenarios.

Javascript

Drag and Drop Event Listeners, FileReaders, and XmlHttpRequest objects. All without an ounce of jQuery.

example.upload = { // Support flag, for reuse dndSupported: false, // Testable initilization flag behaviors: false, // Pointer to our <progress> element progress: null, init: function () { // Ensure we have drag and drop var div = document.createElement("div"); // Simple test of capability similar to moderizer example.upload.dndSupported = "draggable" in div || ("ondragstart" in div && "ondrop" in div); if (!example.upload.dndSupported) { alert("Drag and Drop not supported"); } else { this.bindBehaviors(); } }, bindBehaviors: function () { if (!example.upload.behaviors) { // Binding to the entire document // You could target anything instead here var doc = document.documentElement; doc.ondragover = function () { this.className = "hover"; return false; }; doc.ondragend = function () { this.className = ""; return false; }; doc.ondrop = function (event) { example.upload.onDrop(event); }; example.upload.progress = document.getElementById("progress"); example.upload.preview.target = document.getElementById("preview"); example.upload.behaviors = true; example.upload.reset(); } }, hideFields: function () { document.getElementById("fields-wrapper").style.display = "none"; }, showFields: function () { document.getElementById("fields-wrapper").style.display = "inline-block"; }, reset: function () { example.upload.showFields(); example.upload.updateProgress(0); example.upload.preview.clear(); }, onDrop: function (event) { example.upload.hideFields(); example.upload.preview.clear(); example.upload.updateProgress(0); this.className = ""; // Preventing default behavior (viewing file directly) event.preventDefault && event.preventDefault(); var files = event.dataTransfer.files; if (files.length > 1) { alert("Only one file may be uploaded."); } else if (files.length == 0) { return; } var file = files[0]; // Display a preview if applicable example.upload.preview.display(file); var xhr = new XMLHttpRequest(); // Put is a little more resilient with large files xhr.open("put", "upload-file", true); // Some variables to use saving the file xhr.setRequestHeader("X-File-Name", file.name); xhr.setRequestHeader("X-File-Size", file.size); // File upload finished behavior xhr.onload = function () { // just in case we get stuck around 99% example.upload.updateProgress(100); console.log(xhr); if (xhr.status === 200) { console.log("all done: " + xhr.status); } else { example.upload.reset(); console.log("Something went terribly wrong..."); } }; // Progress update behavior xhr.upload.onprogress = function (event) { if (event.lengthComputable) { example.upload.updateProgress(((event.loaded / event.total) * 100) | 0); } }; // Send just the file contents xhr.send(file); return false; }, updateProgress: function (percentComplete) { if (percentComplete == 0) { example.upload.progress.style.display = "none"; } else { example.upload.progress.style.display = "inline-block"; } example.upload.progress.value = example.upload.progress.innerHTML = percentComplete; }, preview: { target: null, display: function (file) { example.upload.preview.clear(); // Creating a div with the filename nameDiv = document.createElement("div"); nameDiv.innerHTML = file.name; nameDiv.className = "file-name"; example.upload.preview.target.appendChild(nameDiv); // Allow previews of certain file types switch (file.type) { case "image/png": case "image/jpeg": case "image/gif": // Creates an image on the fly with a generated src var reader = new FileReader(); reader.onload = function (event) { var image = new Image(); image.src = event.target.result; image.width = 100; // a fake resize example.upload.preview.target.appendChild(image); }; reader.readAsDataURL(file); break; } }, clear: function () { example.upload.preview.target.innerHTML = ""; }, }, }; // A document ready call. Build your own as you like ready(function () { example.upload.init(); });

Php File Saving Code

// Read the input directly, there is no post, get, data $in = fopen('php://input','r'); // Open any file you like for reading $out = fopen(__DIR__./uploads/' . $_SERVER['HTTP_X_FILE_NAME'], "w+"); // Stream the data out to a file while($data = fread($in, 1024)) fwrite($out, $data); // Close handlers fclose($in); fclose($out);

Are there security concerns building it this way? Of course! The php code above should scare the hell out of you. But, security is your own pass, outside this example. Please, please make sure you review your own and implement a solution that works for your needs. Remember, no matter what the browser passes you, it's important to validate.