Веб-разработка

индекс
236,88

Загрузка браузером нескольких файлов

Если нужно дать пользователю возможность загрузки нескольких файлов, традиционное решение на данный момент — использовать для этой цели Flash (реже — Java applet или ActiveX). В случае, если соответствующий плагин недоступен, пользователю, как правило, показывают стандартный HTML-элемент для загрузки файла.

Последнюю ситуацию можно улучшить, если использовать встроенную в браузеры возможность множественной загрузки файлов. Из всех браузеров сейчас данную возможность не поддерживает только Internet Explorer (впрочем, мы ещё не видели девятую версию, может там что-то изменится), остальные браузеры — Opera 9 и выше (а так же версии 3.5—6.05), Firefox 3.6+, Chrome 3.0.191.0+ и Safari 4.0.1+ такую возможность предоставляют.

Достаточно написать что-то вроде
Copy Source | Copy HTML
  1. <form enctype="multipart/form-data" method="post">
  2.    <input type="file" min="1" max="9999" name="file[]" multiple="true" />
  3.    <input type="submit" name="submit" />
  4. </form>
PHP оказался готов к такой конструкции (именно для него в параметре «name» стоят квадратные скобки), он просто разложит загружаемые файлы по элементам массива $_FILES, если только мы не используем «Оперу».

К сожалению, «Опера» (ещё с версии 3.5) отправляет, при использовании мультизагрузки, файлы в контейнере «multipart/mixed», который PHP не понимает.

Я попробовал исправить эту ситуацию.

К счастью для нас, PHP, приняв такой запрос, поместит его содержимое в массив $_POST (в данном случае он попадёт в $_POST['file'][0], дальше остаётся только распарсить его и переложить в $_FILES (надеюсь, директива magic_quotes_gpc у вас отключена).

В качестве парсера я использовал PECL-модуль mailparse (есть бинарник для Windows).

У меня в примере ожидается параметр «file», но это значение легко вынести в настройку. Код мне кажется достаточно простым, чтобы его не комментировать, но, если что-то не понятно, спросите, я добавлю комментарии.
Copy Source | Copy HTML
  1. if (isset($_POST['file'], $_POST['file'][ 0])) {
  2.  
  3.     if ($idx = strpos($_POST['file'][ 0], "\n")) {
  4.         $bound = substr($_POST['file'][ 0], 2, $idx-2);
  5.  
  6.         $body = "MIME-Version: 1.0\nContent-type: multipart/form-data; boundary={$bound}\n\n".
  7.                  $_POST['file'][ 0];
  8.  
  9.         unset($_POST['file'][ 0]);
  10.         $f = &$_FILES['file'];
  11.  
  12.         $f['name'] = $f['type'] = $f['tmp_name'] = $f['error'] = $f['size'];
  13.  
  14.         $msg = mailparse_msg_create();
  15.  
  16.         if (mailparse_msg_parse($msg, $body)) {
  17.             $i =  0;
  18.  
  19.             foreach(mailparse_msg_get_structure($msg) as $st) {
  20.  
  21.                 $section = mailparse_msg_get_part($msg, $st);
  22.  
  23.                 $data = mailparse_msg_get_part_data($section);
  24.  
  25.                 if ($data['content-type'] == 'multipart/form-data') {
  26.                     continue;
  27.                 }
  28.  
  29.                 ob_start();
  30.                 if (mailparse_msg_extract_part($section, $body)) {
  31.                     $tmp = tempnam(sys_get_temp_dir(), 'php');
  32.                     file_put_contents($tmp, ob_get_clean());
  33.  
  34.                     $f['name'][$i] = $data['disposition-filename'];
  35.                     $f['type'][$i] = $data['content-type'];
  36.                     $f['tmp_name'][$i] = $tmp;
  37.                     $f['error'][$i] =  0;
  38.                     $f['size'][$i] = filesize($tmp);
  39.  
  40.                     $i++;
  41.                 } else {
  42.                     ob_end_clean();
  43.                 }
  44.             }
  45.         }
  46.         unset($f);
  47.  
  48.         mailparse_msg_free($msg);
  49.     }
  50. }
Я не совсем уверен насчёт публикации этой статьи в блог «PHP», возможно «HTML» подошёл бы больше, с другой стороны, здесь рассматривается способ использования множественной загрузки вместе в PHP.

P.S. перенёс в «Веб-разработка», как предложили в комментариях, действительно блог к теме намного ближе.
+95
30 ноября 2009, 04:25
162

комментарии (67)

–9
ChiPer #
Спасибо за статью.
–9
jiexaspb #
интересно. спасибо
p.s. опере не зачет
–10
MaXyC #
не пользовался оперой никогда, но когда пришлось. нашел очень много недостатков
0
corp #
список в студию ;) или, как обычно, «пользовался» = «верстал под»?
+12
Goodkat #
При переходе на любой непривычный браузер сразу видишь кучу недостатков.
По памяти недостатки на момент перехода, в скобках состояние на сегодня:
ie->opera — проблемы с вёрсткой некоторых сайтов
opera->ff — тормоза, падает (уже не падает), жрёт много оперативки (уже меньше)
ff->opera — проблемы с rich webapplications (уже лучше, но кое-где глюки есть)
ff->safari — нет плагинов, постоянно падал (уже стабилен)
ff->chrome — нет плагинов (уже есть в хромиуме), неудобен для вебразработки
chrome->ff, safari->ff — жуткие тормоза
0
z0rg #
на FF поставьте плагин AFOM :)
–5
MaXyC #
самые ярые поклонники оперы отстучали минусами не только по камменту но и в карму. чо злые то такие все. я ни единого плохого слова про вашу малышку не сказал и не сказал что фф рулит вроде, а остался при своем мнении. а товарищь Goodkat выше очень хорошо сказал мыслю по этому поводу )
+3
deseven #
Просто ваш коммент бессмысленен и к обсуждению никакого отношения не имеет. Ни аргументации, ни полезности.
+2
MikeGav #
Да уж, из-за этой особенности оперы я уж было начал грешить на PHP в чем сейчас каюсь :)
0
w999d #
эммм… а поточнее можно? сам оперой практически не пользуюсь — она в случае использования [] в аттрибуте name меняет enctype на multipart/mixed??
0
bolk #
В случае загрузки нескольких файлов, шлёт их вместе внутри контейнера multipart/mixed.
0
bolk #
«Нескольких», в смысле, из одного контрола, конечно.
+6
kmike #
Опера действует полностью в соответствии со стандартами в этом случае, стандарт тут не поддерживает php, так что не кайтесь, грешить нужно на php, а не на оперу.
+1
habraname #
может, в Web-разработку?
0
bolk #
Точно! Перенёс.
0
SibProgrammer #
К сожалению на текущий момент суммарная аудитория всех перечисленных браузеров едва дотягивает до 10% на среднестатистическом сайте. Но FF 3.6 не за горами, т.ч. через 3-4 месяца эффект от этой фичи уже должен быть ощутим :)
+3
beatlejute #
Странные вы данные какие-то даете, интересно, на чем они основаны? И почему на таком пейзаже уныния, FF 3.6 сделает что-то ощутимое?
–1
SibProgrammer #
У вас есть другие данные? Приведите плиз ссылки (только не для вашего личного проекта, а для среднестатистического).
Я же основываюсь на www.w3schools.com/browsers/browsers_stats.asp
На нескольких собственных проекта, к которым у меня есть доступ, картина еще печальней — в среднем: 50% IE, 48% FF, 2% все остальное.
+1
beatlejute #
Странно, но если судить по данным с вашей ссылки, за октябрь, например, Фаерфокс занимает 47.5%, а все ИЕ вместе 37.5%…
Тем более, «суммарная аудитория всех перечисленных браузеров» — это аудитория и ФФ тоже…
Не знаю какой сайт можно считать среднестатистическим, тем более, что вы подразумеваете.
+2
SibProgrammer #
Вы невнимательно читаете — в 47.5% FF _не попадает_ FF 3.6. Потому что он незарелизен еще. Т.е. если вы сейчас добавите эту фичу, то текущие пользователи FF не смогут ей воспользоваться.
0
beatlejute #
Все, понял, спасибо)
+1
HDg #
Ну вы конечно молодец. Смотреть статистику браузеров на сайте для разработчиков :)
+1
bolk #
Прелесть ситуации в том, что она уже плоха, а данный метод позволяет её исправить. То есть, если у пользователя нет поддержки Flash (ActiveX, Java) и недостаточно новый браузер, то он просто загрузит файлы «по старинке».

Кстати, есть область где этот способ выручает сильно — интранет. Там доля таких браузером может резко отличаться.
+1
bolk #
В России всё не так плохо суммарная доля одних только Opera 9 и 10 может достигать 30%.
0
Nc_Soft #
А почему у input есть атрибут multiple?
Я вот тут его что-то не наблюдаю
www.w3.org/TR/html401/interact/forms.html#h-17.4
+2
beatlejute #
+2
youROCK #
+22
anight #
а на самом деле надо было
1) покурить стандарты и разобраться, имеет ли право опера присылать в таком виде, и если нет, то зарепортить им баг
2) если имеет — зарепортить feature request в php

+2
Dlussky #
Если покурить стандарты, то multiple — это фишка HTML5, и разное поведение браузеров в такой ситуации — вполне легально.
Хотя, если вебкит и гекко уже сделали юзабельный вариант — поведение оперы непонятно. Кстати, интересно было бы протестить пример на мобильной опере для вин-тач-девайсов, там ядро 2.3 все-таки. Может быть есть отличия в поведении. Думаю, если все-таки отписать им багрепорт — есть большой шанс включения исправленя в ядро 2.4.
Самое обидное — это если после копания в стандартах выяснится, что опера как раз ведет себя как положено, а остальные браузеры нарушают стандарты в угоду совместимости. Хотя пока я даже не вижу такого типа данных в стандарте на enctype(
+3
razetdinov #
Поведение Оперы станет понятно, если знать, что она реализовала Web Forms 2.0 задолго до того, как он влился в HTML5. В том черновике атрибут multiple есть только у select, а в input за это отвечают атрибуты min и max.
+2
betal #
У оперы тоже есть приятные фичи. Помню очень порадовало, когда случайно увидел там атрибут required, хотя в других браузерах его еще не было :)

+4
bolk #
Баг зарепортен аж два раза.
+2
bolk #
Обычно я стараюсь анализировать за что минусы. А вот тут даже предположить не могу.
+4
arty #
RFC2388 www.ietf.org/rfc/rfc2388.txt
Returning Values from Forms: multipart/form-data, L. Masinter. IETF, August 1998.

4.2 Sets of files

If the value of a form field is a set of files rather than a single
file, that value can be transferred together using the
«multipart/mixed» format.
0
bolk #
Ага, спасибо!
0
arty #
запрос к php отмечен как bogus: bugs.php.net/bug.php?id=47789

в списках рассылки php обсуждения нет, нужно завести
–1
eugeneorlov #
Заголовок исправьте: загрузка «массивом» нескольких файлов за один раз — стандартная фича, описанная в мануале PHP. Здесь скорее «Выбор в диалоговом окне...» и решение соответствующих проблем. Иначе думаешь о пересказе мануала или об асинхронной загрузке.

Теперь о статье: «Из всех браузеров сейчас данную возможность не поддерживает только Internet Explorer» — таким образом велосипед нафиг никому не нужен. Не парьтесь, используйте множественные поля для файлов.
+3
moroz1999 #
Я бы поспорил насчет «ненужен». Автоматизируем это дело с помощью библиотеки, оформляем в IE по-старинке, а в других браузерах — как описано в статье. В результате так или иначе работать будет у всех, а как только такая фича появится на крупном сайте типа gmail или facebook, это неминуемо окажет некоторое давление на MS.
Альтернативные браузеры нередко так и распространяются среди простых пользователей — один Вася Пупкин увидит удобную фишку у другого, и, не найдя её у себя в IE, задастся вопросом :)
+2
bolk #
О каком велосипеде идёт речь? Я описал конкретную (и частую ситуацию) — загрузка через Flash (Java, ActiveX) и degradation до стандартного поля. Эту ситуацию можно улучшить.
0
elsinor #
Вопрос по-существу от не-программиста. На виртуальном хостинге этой mailparse может и не быть, как я понимаю?
0
bolk #
Может и не быть, но никто не мешает взять библиотеку, написанную на PHP.
+1
Dlussky #
Интересно, что если специально задать enctype='multipart/mixed', опера его проигнорирует и будет использовать дефолтный application/x-www-form-urlencoded
0
bolk #
Где-то даже есть страница где описаны поведения всех браузеров с различными enctype.
0
mshakhan #
Я может что-то не так понимаю, но почему бы просто не использовать джаваскрипт и генерить несколько инпутов?
НЛО прилетело и опубликовало эту надпись здесь
0
DenVdmj #
Оперы 9.5 и 10 отдают файлы с Content-Type: multipart/form-data;. Но беда с php имеется. Может поясните как надо понимать фразу:

К сожалению, «Опера» (ещё с версии 3.5) отправляет, в этом случае, запрос в формате «multipart/mixed», который PHP не понимает.
+2
bolk #
Содержимое внутри multipart/form-data отдаётся в multipart/mixed.
–2
OmeZ #
интересная ситуация получается, если опера — делать одно, если FF другое, если IE — третье, а то и просто так показывать стандартную форму с обычной загрузки файла. Если все так весело, зачем я должен отказываться от проверенного временем flash-загрузчика и типичной формы с обычным file для ситуаций когда флэша нет? Зачем плодить сущности для такой опреации, если она не гарантировано и не всегда корректно отображается в разных браузерах?
С CSS уже намучились и продолжаем, так зачем те же грабли переносить и на backend
0
bolk #
А разве я предложил отказаться от Flash? Процитируйте это место, пожалуйста.
–1
OmeZ #
имеется ввиду что на данный момент бессмысленно разрабатывать такой подход для закачки, т.к. он не описан в стандартах. Это грабли которые от версии к версии браузеров будет меняться, поддерживается не четко и по большому счету код становится заложником людей которые ту же оперу пишут. Даже учитывая что «плагин недоступен», стандартный загрузчик из 3 полей типа file будет лучше чем грабли с multiple, которые большинство пользователей даже не будут пытаться использовать, они не знают что в выпавшем окошке можно выбрать 2-3 файла а не только один.
+1
bolk #
имеется ввиду что на данный момент бессмысленно разрабатывать такой подход для закачки, т.к. он не описан в стандартах
Этот подход описан в стандартах. Это часть стандарта WebForms2, который входит в HTML5.
лучше чем грабли с multiple, которые большинство пользователей даже не будут пытаться использовать, они не знают что в выпавшем окошке можно выбрать 2-3 файла а не только один.
Во флешовом загрузчике они до этого же как-то догадываются :)
0
razetdinov #
Уточню, что Web Forms так и остался в состояние черновика, в вот HTML5 уже в статусе Last Call.
+1
Splurov #
Кто придумает как проверить поддержку браузером multiple="true"/min-max без явного if (navigator.userAgent.match(...)) {...}?
+1
bolk #
Ничего сложного, все эти атрибуты отображаются в DOM браузера, как свойства, надо проверить их существование.
0
Splurov #
Да, точно, всё так и есть.
0
Nashev #
А поясните, к слову — атрибуты тега в его элемент DOM попадают только если браузер их понимает как использовать?
+1
bolk #
Там есть разница — атрибуты тега и свойства объекта. В атрибуты тега значения попадут всегда, в свойства объекта — нет.
0
lany #
Приятно, что в скором будущем этим можно будет пользоваться. На данный момент мы просто разрешаем загружать .zip/.tar/.tar.gz в тех местах, где по логике пользователю удобно притащить много файлов. На сервере обёртка разбирается с форматом, при необходимости сама распаковывает и в дальнейший код передаёт список файлов (список из одного файла, если загружали не архив). Но, конечно, это костыль и нормально работает только для небольших файлов. С видеохостингами, например, такой вариант сразу отпадает.
0
recompileme #
Кстати, мультиаплоад в ФФ наконец-то появился во многом благодаря хабру.
0
bolk #
Я знаю, сам голосовал.
+1
david_mz #
Спасибо. К сожалению, малоприменимо в реальном мире, особенно в случае с Оперой: мультиаплоад обычно нужен для загрузки больших файлов, а тут надо все полученные данные в памяти PHP-скрипта держать…
0
bolk #
У нас в интранете юзкейсы самые разные, россыпь фоток по 1—2 мегабайта залить, например.
0
acia #
Извиняюсь, что поднимаю столь старую тему) У меня проблема — файлы через оперу загружаются, переносятся в $_FILES с помощью вашей функции, но содержимое искажается. magic_quotes_gps отключен, mbstring стоит. В чем может быть проблема, может, сталкивались?
Any help much appreciated :)
0
bolk #
Трудно предположить. Может быть, у вас русский Apache?
0
acia #
апача вообще нет, только nginx и php-fastcgi
попрбовал с Mail_mimeDecode — точно такой же результат. Не понимаю…
0
bolk #
Посмотрите сырые данные и попробуйте сравнить то, что отсылаете с тем, что приходит. Там можно сделать какие-то выводы.
0
acia #
Как раз этим сейчас занимаюсь :)
Файлы одинакового размера, но содержимое местами различается

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