Доброго времени суток, коллеги. В этой статье я опишу опыт создание многопоточного загрузчика файлов (с ограниченной нагрузкой на сервер) на JS (jQuery).
Совсем недавно у меня появилась задача (опытом решение которой я и хочу с вами поделится): сделать, в админке, возможность выбирать и загружать более одного файла за один раз. Задание вроде тривиальное и не сложное, но в итоге мое решение показалось мне довольно таки интересным, так как не было найдено аналогов.
Решение №1
Так как задача была простенькая, и, естественно, решений уже написано предостаточно, я обратился за помощью к гуглу. Но, поиск положительных результатов не дал – большинство предлагаемых решений используют Flash (что из-за некоторых специфик не позволяло использовать такие решения мне) или же написанные библиотеки на JS были сильно громадными и что самое прискорбное – нерабочими. Пришлось собирать велосипед.
Решение №2
Задача была срочной, поэтому нужно было срочное решение. Не долго думаю я прикрутил к полю инпут атрибут multiple (доступен с HTML5).
<form id='FilesupLoadForm'>
<input type='file' id=’fileinput’ name='files' multiple="multiple" >
<input type="submit" value='upload'>
</form>
Далее следовали маленькие изменения обработчика на получение не одного файла, а массива файлов – и задача решена! (наивности (неопытности) моей не было придела).
Как многие со смехом уже додумали, что на первую, нормальную, партию файлов nginx ответил пятьсот третей. Надо было думать дальше.
Решение №3
Так как прошлое решение было сделано красиво и удобно для админов, было решено отталкиваться от него. Нужно было решить проблему ошибки №503, которую возвращал nginx из-за длительной обработки файлов.
Полминуты на обдумывание и появляется новое решение: будем отправлять ajax-ом не сразу все файлы, а по одному.
Решение, примерно, имело следующий вид:
jQuery.each($('#fileinput')[0].files, function(i, f) {
var file = new FormData();
file.append('file', f);
$.ajax({
url: 'uploader.php',
data: file,
async : false,
contentType: false,
processData: false,
dataType: "JSON",
type: 'POST',
beforeSend: function() {},
complete: function(event, request, settings) {},
success: function(data){ }
});
});
Все просто: перебираем массив файлов (который находится в нужном инпуте), создаем экземпляр класса для работы с файлами (об этом дальше) и ajax-ом отправляем запрос на сервер. Стоит обратить внимание на параметр "async: false" — тут мы задаем синхронное выполнение ajax-запроса, так как асинхронное создаст нам множество запросов на сервер, чем мы с легкостью его сами и положим.
Решение работает, ошибки нет, но вот одна проблема – работает то оно медленно. И тут мне пришла в голову идея еще одного решения поставленной задачи.
Решение №4
Для ускорения загрузки файлов на сервер можно увеличить количество запросов, в которых будут передаваться файлы. Такое решение упереться в решение двух проблем:
1).Большим количеством запросов я быстро полажу к чертям свой сервер.
2) Большой объем одновременно загружаемых файлов забьет нам весь канал.
Судя по проблемам наш загрузчик должен считать количество запущенных запросов и объем передаваемых данных и при некоторых условиях ожидать окончания одних запросов, для начала других.
Приступим к коду:
FilesUploader = {
dataArr : new Array(),
fStek : new Array(),
vStek : 0,
deley : 100,
debug : true,
maxFilesSize : 1024*1024*10,
maxThreads : 10,
Легенда:
dataArr – массив с данными для отправки.
fStek – сюда записываем идентификаторы таймаутов, для дальнейшей остановки рекурсии и очистки памяти от незавершенных функций.
vStek – количество вызванных потоков.
deley – задержка рекурсии функции, проверяющей потоки и объемы.
debug – режим дэбага. Нужно для отладки, но в этом примере все её признаки я удалил.
maxFilesSize – максимальная сума объемов загружаемых файлов
maxThreads – максимальное количество потоков.
Полную ясность в переменных (особенно fStek и deley) внесет вторая рассматриваемая функция FilesUploader.controller(). А пока что перейдем к инициализации класса:
run : function() {
jQuery.each($('#fileinput')[0].files, function(i, f) {
FilesUploader.dataArr.push(f);
});
FilesUploader.controller();
},
Именно на эту функцию вешается обработка события клика кнопки в форме. Работа функции просто: пробегаем по файлам (jQuery.each), занесенным в инпут, и добавляем (FilesUploader.dataArr.push(f)) запись о каждом в массив. Далее вызываем контроллер, который есть важнейшим и сложнейшим звеном системы:
controller : function() {
FilesUploader.fStek.push(setTimeout(FilesUploader.controller, FilesUploader.deley));
if(FilesUploader.vStek>=this.maxThreads) { return; }
item = FilesUploader.dataArr.pop();
if(item) {
if(FilesUploader.maxFilesSize-item.size < 0) {
FilesUploader.dataArr.push(item);
return;
}
FilesUploader.maxFilesSize-=item.size;
FilesUploader.vStek++;
FilesUploader.worker(item);
} else clearTimeout(FilesUploader.fStek.pop());
},
В первой строчке функции мы асинхронно вызываем (через некоторый период времени) эту же функцию (т.е. создаем рекурсию), и заносим идентификатор вызванной функции в переменную, для возможности прервать её выполнение.
Далее идет условие проверки потоков.
После получение файл из массива (FilesUploader.dataArr.pop()) проверяем его на наличие.
1. Если файла нет — тогда «убиваем» вызванные функции, по их идентификатору (clearTimeout(FilesUploader.fStek.pop()));
2. Если файл существует, делаем проверку на объем загружаемых файлов, и если он превышен – возвращаем файл обратно в стек и выходим из функции, иначе, если не превышен: отнимаем объем, увеличиваем счетчик запушенных потоков и вызываем следующую функцию (FilesUploader.worker(item)).
worker : function(item) {
var file = new FormData();
file.append('file', item);
$.ajax({
url: 'uploader.php',
data: file,
contentType: false,
processData: false,
dataType: "JSON",
type: 'POST',
beforeSend: function() {},
complete: function(event, request, settings) {
FilesUploader.maxFilesSize+=this.fileData.size;
FilesUploader.vStek--;
},
success: function(data){ }
});
},
Для отправки файла на сервер средствами ajax, нужно поместить в него данные о файле (file.append()) в экземпляр класса FormData.
Далее уже вызываем функцию $.ajax, которая передаст наш файл загрузчику на сервер. По завершению каждого запроса (функция complete()) нужно увеличить допустимый объем и уменьшить количество исполняемых потоков (что и делается в строках “FilesUploader.maxFilesSize+=this.fileData.size” и “FilesUploader.vStek—“).
И последний штрих – функция вывода в консоль и закрывающая скобка:
out : function(message) {
if(console.log && this.debug) console.log(message);
}
}
Вот и все – класс для многопоточной загрузки файлов на сервер готов. Далее уже следует выставить, в зависимости от конфигурации сервера, допустимое количество потоков и объем одновременно загружаемых файлов – и можно работать.
Автор: IgaIst