Загрузка файлов с помощью HTML5 и сколько раз мы сказали нехорошие слова

Стояла задача: найти или создать загрузчик файлов на сервер, использующий возможности HTML5 для мультизагрузки. Загрузчик должен:
  • отправлять методом POST любые параметры вместе с файлом;
  • отправлять куки;
  • предоставлять возможность выбора сразу нескольких файлов (или нескольких тысяч – тут как пользователь захочет);
  • отправлять файлы группами;
  • файлы собираются в группы до определенного количества мегабайт, или до определенного количества файлов в группе; (это связано с тем, что на сервере есть ограничение на размер POST запроса и на количество файлов в одном пакете)

Именно по причине отсутствия опыта работы с флешем, для создания загрузчика был выбран только JavaScript.

Итак, что из этого получилось


Сначала был поиск: SWFupload, Uploadify и другие были изучены. Возможно, не слишком хорошо, но каждый из них не удовлетворял всем требованиям.

Баги, баги, баги. Некоторые версии Flash не могут управлять именем переменной. SWFupload не отправляет куки, но у него есть сборщик куков, и он их отправляет только методами GET или POST. Однако у нас CMS на сервере проверяет сессию пользователя именно на основании кук, поэтому Flash был отброшен и было выбрано решение, с использованием HTML5.
Поскольку FileApi поддерживается только FF 3.0+, Chrome и Safari 4+, то разберем тонкости работы с этими браузерами.

Тонкость №1

Получение содержимого файла в Chrome с помощью readAsBinaryString.
При использовании данного метода мы столкнулись с непонятной проблемой, что все файлы приходят битыми, размер которых примерно в 1,5 раза больше изначального размера. Победить эту проблему не получилось, поэтому для хрома ( а так же сафари) мы воспользовались FormData.

Чтобы отправить группу файлов + какие-то данные методом POST в FF (до 4.0) формируем multipart/form-data:
Content-Type: multipart/form-data; boundary=------multipartformboundary1295790618
\r\n
--------multipartformboundary1295790618
\r\n
Content-Disposition: form-data; name='user_files[]';filename='My_File1.jpg'
\r\n
Content-Type: application/octet-stream
\r\n
\r\n
содержимое файла
\r\n
--------multipartformboundary1295790618
\r\n
Content-Disposition: form-data; name='user_files[]';filename='My_File2.jpg'
\r\n
Content-Type: application/octet-stream
\r\n
\r\n
содержимое файла
\r\n
--------multipartformboundary1295790618
\r\n
Content-Disposition: form-data; name='my_param'
\r\n
\r\n
Param_value
\r\n
--------multipartformboundary1295790618
\r\n
Content-Disposition: form-data; name='my_param'
\r\n
\r\n
Param_value
\r\n
--------multipartformboundary1295790618--
\r\n


Тонкость №2

Определение поддержки FormData можно проверять следующим образом:
function isFormDataSupported() {
return (window.FormData);
}


Тонкость №3

В FF 3.0/3.5 и FF 3.6 используются разные функции для получения содержимого файлов:
FF 3.0/3.5: readAsBinary()
FF 3.6: getAsBinary()

Тонкость №4

Также есть разница в отправке файлов через XMLHttpRequest:
для FormData нужно отправлять методом send(), а для отправки файлов, полученных методом getAsBinary() или с помощью readAsBinaryString необходимо отправлять методом sendAsBinary().

Тонкость №5

FF редко вызывает событие progress у XMLHttpRequest, в отличии от хрома или Сафари. На деле у FF событие progress вообще иногда не отрабатывало при очень быстром соединении или на локалхосте.

Тонкость №6

При событии abort у XMLHttpRequest в FF также вызывается событие error. С этим нужно быть особо внимательным, если у вас там стоит обработчик на ошибки — это может вызвать дополнительные вопросы у пользователя.

Тонкость №7

Для PHP советую обратить внимание на следующие настройки в php.ini:

post_max_size — максимальный размер POST.
upload_max_filesize — максимальный размер файла.
max_file_uploads — максимальное количество загружаемых файлов.

Если будете использовать наш скрипт, не забудьте изменить скрипт под настройки своего сервера.

Демо для загрузки изображений (стоит фильтр по расширениям)
Скачать демо
Хорошая презентация на тему AJAX загрузки

UPDATE:

Тонкость №8 (спасибо soltpain за то, что заметил этот баг)

В Windows есть еще один нюанс в выборе множества файлов через [input type=file multiple]:
Количество доступных файлов для выбора зависит от общей длины имен выбранных файлов. Для хрома — 256 символов, для FF — 4096 (длина не одного файла, а общая длина имен, то есть если есть файлы, у которых имя состоит из 20 символов, то в хроме можно выбрать максимум 10 файлов). В линуксе все нормально.

Описание бага:
codereview.chromium.org/4198004
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 67
  • +20
    >загрузчик фалов на сервер
    Угловая скорость вращения дедушки Фрейда слегка увеличилась:)
    Исправьте пожалуйста.
    • +2
      спасибо, исправил)
    • 0
      в хроме невозможно выбрать более 10 файлов одновременно — можно это обойти?
      • 0
        Проблема заключается в том, что общее количество символов в именах файлов не должно превышать ~220 символов. Пока вроде такая проблема только в хроме на винде.

        codereview.chromium.org/4198004
      • 0
        Спасибо за информацию. Как раз щас предстоит делать загрузку файлов на HTML 5, в дополнение к Silverlight.
        • 0
          под виндовс действительно есть такая проблема в хроме, но бывает и выбирает больше) Пока не знаю, какая причина. В линуксе все нормально.
          • 0
            Непременно глянем-с!
            • +1
              А как вы решили проблему загрузки файлов с помощью Ajax в ИЕ и Опере?
              • +1
                флеш :)
                • +1
                  Свой загрузик пишите или какое-то решне выбрали? По универсальности мне больше этот нарвится www.plupload.com/example_custom.php, но уж слишком громоздкий.
                  • 0
                    Да, скорее всего plupload.

                    Но опять же, наверное, самый значимый для нас минус большинства загрузчиков — это то, что у загрузчика есть собственный гуи. Пусть можно изменять стили, пусть как-то его гуи можно настраивать — все таки, имхо, аплоадер должен только загружать файлы, а как ему эти файлы передадутся — уже не его задача.

                    Да и очень хочется самостоятельно написать на флеше аналогичный загрузчи просто для собственного развития) Но за это деньги не платят, а работать должно все «уже сейчас»).
                    • 0
                      Да, у plupload можно выкинуть весь его гуи и использовать только ядерное апи.

                      И поскорей бы все браузеры подтянулись с FormData.

              • +2
                На это тоже стоит взглянуть.
                • 0
                  Пересмотрел кучу плагинов и решений на HTML5. Valums — показался мне лучшим вариантом. Стилизовать легко, необходимые функции написаны, работает во всех браузерах.
                  • 0
                    Вот кстати пример с использованием Valums — dapmoed.ru/elfinder-demo/
                    • 0
                      Спасибо, elFinder-у действительно не хватало нормального мультиаплоадера.
                • –4
                  у меня завис ваш пример, что-то он корявый, я все же ухитрился бы флэш привязать. в свое время облез кучу всего, нужно было в реальном времени подгружать кучу файлов и чтобы они тут же парсились и появлялись на сайте в реалтайме, так вот пользовал и аякс на jquery и все что угодно, были проблемы пока полностью не перешел на флэш загрузку: быстро, удобно, красиво, есть прелоадер (а это важно было в моем проекте, ибо файлы иногда лились по 20-30 мегов и пользователь видел что сайт не завис). вобщем пришлось выучить флэш и написать свой загрузчик…
                  • +1
                    вобщем пришлось выучить флэш и написать свой загрузчик…

                    Разве мало готовых флеш-загрузчиков?
                  • 0
                    у меня завис ваш пример, что-то он корявый, я все же ухитрился бы флэш привязать. в свое время облез кучу всего, нужно было в реальном времени подгружать кучу файлов и чтобы они тут же парсились и появлялись на сайте в реалтайме, так вот пользовал и аякс на jquery и все что угодно, были проблемы пока полностью не перешел на флэш загрузку: быстро, удобно, красиво, есть прелоадер (а это важно было в моем проекте, ибо файлы иногда лились по 20-30 мегов и пользователь видел что сайт не завис). вобщем пришлось выучить флэш и написать свой загрузчик…
                    изначально, когда я еще писал на jquery мне очень помог iframeupld.js (его легко найти в инете)
                    • 0
                      Важно было отправлять с файлами данные POSTом, а так же загружать файлы пачками.

                      Собственно, сейчас и делаем флеш загрузчик.
                    • +1
                      Каждый изобретает свой велосипед.
                      Самым «вкусным» в виде Blob вы не воспользовались.


                      Нельзя использовать readAsBinary, на больших файлах это сожрет всю память юзера.

                      FF редко вызывает событие progress у XMLHttpRequest, в отличии от хрома или Сафари.

                      А вы пробовали вешать обработчик на xhr.upload.onprogress?
                      • 0
                        Прошу прощения, заглянул в код, там обработчик на xhr.upload.onprogress вешается.
                        Значит это глюк ФФ, по спецификации www.w3.org/TR/XMLHttpRequest2/#make-upload-progress-notifications событие должно вызываться либо раз в 50мс, либо на каждый байт, в зависимости от того, что происходит чаще.
                        • 0
                          факт в том, что получить 100% ни разу не удалось) Хотя иногда доходило до 99, но чаще всего останавливается в пределах 80
                      • +2
                        Не смотрели www.plupload.com/index.php?
                        • 0
                          смотрим) Но я так понял, он тоже по одному отправляет?
                          • 0
                            Я так и не понял, зачем вам отправлять файлы группами?
                            • 0
                              Для того, чтобы уменьшить количество соединений
                              • +1
                                Хм, очень странный аргумент.
                                А зачем вам уменьшать кол-во соединений? Вы уперлись во что-то? Или просто так захотели от балды?
                                • 0
                                  Ну, не могу дать точного ответа. Наверное, от балды — испугались возможной тысячи юзеров, одновременно решивших загрузить 1000 файлов)
                                  • 0
                                    А вы не пугайтесь, если конечно вы используете nginx или что-то подобное.
                                    Поверьте мне, ваши диски раньше начнут «втыкать», чем вы достигните тысяч соединений.
                                    • 0
                                      Ну да, но все же мало ли что, тем более было интересно, возможно ли это сделать)
                            • 0
                              Да, по очереди.
                          • 0
                            Кстати Опера тоже поддерживает Мультизагрузку файлов. В ней ваша форма застряла на
                            Output:

                            STARTING ...
                            • +2
                              Опера не поддерживает FormData и FileApi
                            • +2
                              использовал habrahabr.ru/blogs/webdev/109079/ — работает все хорошо, ждем когда в safari заработает
                              • 0
                                там используется FileReader, но в хроме столкнулись с такой проблемой, что файлы получаются битыми, не знаю, из-за чего это может быть.

                                Мне кажется, что FormData гораздо удобнее для отправки данных. FileReader больше подходит там, где нужно еще как-то взаимодействовать с файлами, кроме просто отправки на сервер, имхо.
                                • +1
                                  Я пошел по этому же пути и добавил в свой код использование FormData там, где это возможно. Получилось, наверное, почти как у вас. Тока я еще в jQuery-плагин все это завернул.
                                  • 0
                                    Лучше поздно, чем никогда :)

                                    Если интересно, то вот jQuery-плагин по мотивам моей статьи, использующий FormData там, где это возможно, и позволяющий управлять очередью загрузок. В самом файле есть описание его использования. Ссылку на работающий пример и кое-какие подробности можно увидеть в UPD той самой статьи.
                                    • 0
                                      посмотрим, спасибо)
                                  • +2
                                    Я немного доработал свой код, он в сафари работает нонче. Все не доходят руки на хабр выложить. Чуть позже дам линк.
                                  • 0
                                    Давать читать это всем, кто начинает писать статьи на Хабре! 5 минут на чтение — минус рабочая неделя на мозготоптательный дебаг…
                                    Статей по поводу работы с HTML5 немеряно, но статей с четкой постановкой задач и указанием на нюансы и без воды — единицы.

                                    Респект.
                                  • 0
                                    Используйте plupload либо мою альтернативу Lightweight Uploader
                                    Если хотите иметь регулярный секс — то, конечно, пишите своё )
                                    • 0
                                      да, мы остановимся на plupload, наверное. Но иметь регулярный секс полезно для здоровья все равно)
                                      • 0
                                        Кстати, хорошая у вас альтернатива. А для чего nginx используете при аплоаде?

                                        Будет ли ваше решение загружать файлы, если файлы будут закачиваться с адреса, например, site.ru на адрес u1.site.ru?
                                        • 0
                                          nginx вместе c upload-модулем используется:
                                          а) для того, чтобы бэкенд вступал в дело только после полного окончания загрузки; при этом не происходит копирование файла в STDIN бэкенда, ему передается лишь ссылка на уже сохраненный upload модулем временный файл
                                          б) для поддержки дозагрузки, которую дает upload модуль

                                          Будет ли ваше решение загружать файлы, если файлы будут закачиваться с адреса, например, site.ru на адрес u1.site.ru?

                                          Хороший вопрос )
                                          Будет (после небольшого фикса с моей стороны) и если использовать последнюю версию upload модуля из гита, которая обрабатывает метод OPTIONS с помощью директивы upload_add_header.
                                          • 0
                                            Я имел в виду, что фиксить надо html5 загрузку, конечно же.
                                            Flash и Silverlight загрузка будет работать между доменами при правильно настроенных файлах u1.site.ru/crossdomain.xml и u1.site.ru/clientaccesspolicy.xml (хотя по идее и первого будет достаточно).
                                            • 0
                                              Собственно, в гите уже есть фикс для кроссдоменной html5-загрузки.
                                              • 0
                                                Спасибо. На днях попробую внедрить вашу загрузку в свой проект.
                                              • 0
                                                А загрузка будет работать без upload-модуля? Так как если его ставить, то нужно собирать энджинкс в ручную, а это не есть debian way на продакшен сервере.
                                                • 0
                                                  Будет, если вы сами повторите протокол частичной загрузки на бекенде.
                                                  Сильверлайт очень плохо загружает большие файлы целиком, точнее сначала полностью загружает в память (видимо для высчитывания заголовка Content-Length, который вручную нельзя установить), и только потом начинает загружать на сервер.
                                                  • 0
                                                    А может дороботать ваше решение, что бы частичная загрузка работала опциоанльно. Что бы ее можно было оключить, если upload модуль не установлен. Тогда это решение будет более универсальным и не заточеным под энджинкс с его модулем.
                                                    • 0
                                                      В принципе, можно, правда в этом случае нельзя будет использовать сильверлайт-плагин, а т.к. html5 еще не особо распространен, то остается только флэш.
                                                      А по моей статистике, флэш — это наихудший способ загружать файлы, он создает больше всего ошибок.
                                                  • 0
                                                    А что такое «debian way»? И почему он не совместим с ручным билдом?
                                                    • 0
                                                      Это когда все программы ставятся из пакетов. И ручная сборка не приветствуется. Потому что пакеты имеют тенденцию обновляться закрывая баги или при выходе новых версий. И обновление всего ПО на сервере в этом случае приосходит с помощью одной одной/двух команд. А при ручной сборе, необходимо при выпуске новых версии все пересобирать в ручную и еще отслеживать версии. Короче гемор один и усложнение жизни админу. Поэтому в пакетных операционках не приветствуется ручная сборка пакетов.
                                                  • 0
                                                    Ошибаетесь.
                                                    dev.w3.org/2006/waf/access-control/ — это уже поддерживают Chrome, Safari и Firefox. Ребята из Opera как обычно заняты им одним ведомыми делами, но думаю скоро и они подтянутся.
                                                    • 0
                                                      Я сначала не заметил, что вы ответили. Но все равно не знал, спасибо)
                                              • 0
                                                Стоит упомянуть, что sendAsBinary — отсебятина Mozilla и нигде, кроме FF, не существует.
                                                • 0
                                                  Да. В презентации есть это, там еще кстати рассмотрены некоторые аспекты, которые в статье не упоминались.
                                                • +1
                                                  Если загрузить файл драг&дропом, а затем попытаться загрузить через кнопку, то в хроме выпадает ошибка Uncaught ReferenceError: onuploaded is not defined.
                                                  • 0
                                                    точняк, опечатка :\
                                                  • 0
                                                    Начал внедрять ваш аплоадер и оказалось, что он не отправляет и не получает кукисы. Если вам не сложно, то допишите пожалуйста работы с куками.
                                                    • 0
                                                      а вы кому именно?
                                                      • 0
                                                        Вам. Ваш аплоадер не работает с кукисами.
                                                    • 0
                                                      ну да
                                                      • 0
                                                        Знаю что прошел год, но ссылки померли.

                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.