Google Chrome 21 added a feature to upload directories, Firefox also didn’t get too long far behind and added that feature in version 42. Here is a code that will work in both browsers, with help from jQuery and FormData object. It will also work with files larger then the limit:
function Uploader(path, limit) { this.path = path; this.upload_max_filesize = limit; } Uploader.prototype.upload_tree = function upload_tree(tree, path) { var defered = $.Deferred(); var self = this; path = path || self.path; function process(entries, callback) { entries = entries.slice(); (function recur() { var entry = entries.shift(); if (entry) { callback(entry).then(recur).fail(function() { defered.reject(); }); } else { defered.resolve(); } })(); } function upload_files(entries) { process(entries, function(entry) { return self.upload_tree(entry, path + "/" + tree.name); }); } function upload_file(file) { self.upload(file, path).then(function() { defered.resolve(); }).fail(function() { defered.reject(); }); } if (typeof Directory != 'undefined' && tree instanceof Directory) { // firefox tree.getFilesAndDirectories().then(function(entries) { upload_files(entries); }); } else if (typeof File != 'undefined' && tree instanceof File) { // firefox upload_file(tree); } else if (tree.isFile) { // chrome tree.file(upload_file); } else if (tree.isDirectory) { // chrome var dirReader = tree.createReader(); dirReader.readEntries(function(entries) { upload_files(entries); }); } return defered.promise(); }; Uploader.prototype.upload = function upload(file, path) { var self = this; var defered = $.Deferred(); var file_name = path + '/' + file.name; if (file.size > self.upload_max_filesize) { if (!(file.slice || file.webkitSlice)) { console.log('Exceeded filesize limit.'); defered.resolve(); // next file } else { self.upload_by_chunks(file, path).then(function() { defered.resolve(); }).fail(function() { defered.reject(); }); } } else { self.upload_file(file, path).then(function() { defered.resolve(); }).fail(function() { defered.reject(); }); } return defered.promise(); }; Uploader.prototype.upload_by_chunks = function upload_by_chunks(file, path, chunk_size) { var self = this; chunk_size = chunk_size || 1048576; // 1MB var defered = $.Deferred(); function slice(start, end) { if (file.slice) { return file.slice(start, end); } else if (file.webkitSlice) { return file.webkitSlice(start, end); } } var i = 0; function process(start, end) { if (start < file.size) { var chunk = slice(start, end); var formData = new FormData(); formData.append('file', chunk, file.name); formData.append('token', self.token); formData.append('path', path); $.ajax({ url: 'lib/upload.php?append=1', type: 'POST', success: function(response) { if (response.error) { console.log(response.error); defered.reject(); } else { process(end, end+chunk_size); } }, error: function(jxhr, error, status) { console.log(jxhr.statusText); defered.reject(); }, data: formData, cache: false, contentType: false, processData: false }); } else { console.log('File "' + file.name + '" uploaded.'); defered.resolve(); } } var fname = path + '/' + file.name; // we need to remove the file if already there $.ajax({ url: 'delete.php', data: {file: fname}, success: function(response) { if (response.result) { process(0, chunk_size); } else if (response.error) { console.log(response.error); defered.reject(); } } }); return defered.promise(); }; Uploader.prototype.upload_file = function upload_file(file, path) { var self = this; var defered = $.Deferred(); var formData = new FormData(); formData.append('file', file); formData.append('path', path); $.ajax({ url: 'lib/upload.php', type: 'POST', success: function(response) { if (response.error) { console.log(response.error); defered.reject(); } else { console.log('File "' + file.name + '" ' + 'uploaded.'); defered.resolve(); } }, error: function(jxhr, error, status) { console.log(jxhr.statusText); defered.reject(); }, data: formData, cache: false, contentType: false, processData: false }); return defered.promise(); };
Here is drag and drop code that use that “class”:
$('div').on('drop', function(e) { e.preventDefault(); var org = e.originalEvent; var uploader = new Uploader('/home/user', FILE_LIMIT_TAKEN_FROM_SERVER); var items; if (org.dataTransfer.items) { items = [].slice.call(org.dataTransfer.items); } var files = (org.dataTransfer.files || org.target.files); if (files) { files = [].slice.call(files); } if (items && items.length) { if (items[0].webkitGetAsEntry) { var entries = []; items.forEach(function(item) { var entry = item.webkitGetAsEntry(); if (entry) entries.push(entry); }); (function upload() { var entry = entries.shift(); if (entry) { uploader.upload_tree(entry).then(upload) } else { console.log('Finish'); } })(); } } else if (files && files.length) { (function upload() { var file = files.shift(); if (file) { uploader.upload(file).then(upload) } else { console.log('Finish'); } }); } else if (org.dataTransfer.getFilesAndDirectories) { org.dataTransfer.getFilesAndDirectories().then(function(items) { (function upload() { var item = items.shift(); if (item) { uploader.upload_tree(item).then(upload); } else { console.log('Finish'); } })(); }); } }).on('dragover', function(e) { e.preventDefault(); }).on('dragenter', function(e) { e.preventDefault(); });
upload.php
can look like this:
<?php header('Content-type: application/json'); if (!isset($_FILES['file'])) { echo json_encode(array('error' => 'No File')); } else if (!isset($_POST['path'])) { echo json_encode(array('error' => 'Wrong request, no path')); } else { $fname = basename($_FILES['file']['name']); switch ($_FILES['file']['error']) { case UPLOAD_ERR_OK: $path = ''; // create directories if don't exists foreach (explode("/", $_POST['path']) as $folder) { if (!is_dir($path . DIRECTORY_SEPARATOR . $folder)) { mkdir($path . DIRECTORY_SEPARATOR . $folder); } $path .= DIRECTORY_SEPARATOR . $folder; } $full_name = $_POST['path'] . '/' . $fname; if (file_exists($full_name) && !is_writable($full_name)) { echo json_encode(array( 'error' => 'File "'. $fname . '" is not writable' )); } else { if (isset($_GET['append'])) { $contents = file_get_contents($_FILES['file']['tmp_name']); $file = fopen($full_name, 'a+'); if (!$file) { echo json_encode(array('error' => 'Can\'t save file.')); } else if (fwrite($file, $contents) != strlen($contents)) { echo json_encode(array('error' => 'Not all bytes saved.')); } else { echo json_encode(array('success' => true)); } fclose($file); } else { if (!move_uploaded_file($_FILES['file']['tmp_name'], $full_name)) { echo json_encode(array('error' => 'Can\'t save file.')); } else { echo json_encode(array('success' => true)); } } } break; case UPLOAD_ERR_NO_FILE: echo json_encode(array('error' => 'File not sent.')); break; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: echo json_encode(array('error' => 'Exceeded filesize limit.')); break; default: echo json_encode(array('error' => 'Unknown error.')); } } ?>
The code for delete can just call unlink function if file exist.
Another thing that can be added to the code, is to ask a question if overwrite a file if it exists. This is left as a exercise to the reader.