28,82
рейтинг
26 июля 2010 в 16:55

Разное → Закачка больших файлов или Как обойти ограничения дешевого виртуального хостинга

Однажды в очередной раз возникла задача о закачке относительно больших файлов. Говоря конкретно, клиент захотел заливать на сайт через админку видеоролики размером 20-40 мегабайт. Казалось бы, в наше просвещенное время подобный размер — это такая мелочь, о коей и говорить стыдно. Но внезапно все уперлось в настройки виртуального хостинга. Мы с ужасом обнаружили, что максимальный размер закачиваемого файла — 2M, и поменять эту цифру нет возможности. И менять хостинг по ряду причин нельзя — по крайней мере не сейчас.

Перед нами встает задача — обойти ограничения убогого виртуального хостинга. Сам принцип такого обхода очевиден: файл надо порезать на куски, залить частями, а на стороне сервера собрать в единое целое. Но делать это надо не вручную — пользователь должен выбрать файл и нажать на кнопку «Отправить». Как же это сделать?


Самая первая наша реакция — посмотреть возможности различных флеш-аплоадеров. Ведь не может быть, чтобы мировая техническая мысль не реализовала такой полезной вещи, как загрузка файла по частям. Перебираем последовательно Uploadify, SWFUpload, FancyUpload, jqUploader, jquery-transmit. Но все тщетно. Искомой фичи мы не видим. Вполне вероятно, что надо копать дальше, но время поджимает, и надо уже что-то делать…

Вышеописанное печально. Однако нам на руку играет тот факт, что это админка. Т.е. нам вовсе не нужно ориентироваться на кроссбраузерность. Достаточно того, что этот механизм будет работать на браузере клиента, каковым (о чудо!) является FF.

Тут же вспоминаем, что в FF последних версий есть возможность получить в строку содержимое файла, загруженного в поле file-upload. И в голову приходит желание разбивать эту строку на куски и закачивать частями, используя Ajax.

Клиентская часть


Сначала нарисуем необходимое в статическом HTML:

<form>
  <input type="file" id="myfile">
</form>

<a href="#" onClick="big_file_upload($('#myfile'))">Отправить</a>

Т.е. при нажатии на ссылку должна быть вызвана функция big_file_upload, в которую передается объект, из которого нужно взять содержимое файла. Обратите внимание на конструкуию $('#myfile'). Думаю, нет нужды подробнее останавливаться на необходимости подключения библиотеки jQuery, которую мы также будем использовать и для ajax-запросов при передачи файла на сервер.

Теперь нам надо написать ту самую функцию big_file_upload:

var upload_chunk_size = 120000;
// В этой переменной лежит размер кусков, на которые мы будем делить файл.
 
function big_file_upload(file) {
  // .....
}

Получение содержимого файла

Для получения содержимого файла будем использовать следующую конструкцию:

  var data = file.get(0).files.item(0).getAsDataURL();

Поясняю ее смысл:
  • file.get(0) — получение DOM-объекта из jQuery-объекта, переданного в функцию
  • files.item(0) — получение первого файла из списка. Здесь он у нас единственный, однако напомню, что уже есть возможность множественной закачки файлов из одного контрола.
  • getAsDataURL() — получение содержимого файла в формате Data:URL. Есть еще методы getAsText и getAsBinary, однако нам нужно передавать на сервер методом POST, поэтому желательно получить содержимое файла, закодированное в Base64.

Аналогичной конструкцией получаем имя файла:

  var filename = file.get(0).files.item(0).fileName;

Поскольку содержимое у нас в формате Data:URL, то неплохо было бы отрезать заголовочную часть, в которой находится информация о MIME-типе и способе кодирования. В более общем варианте нашей функции эту информацию надо бы использовать, но в данном примере она нам только будет мешаться при декодировании. Поэтому просто отрежем все по первую запятую (включительно), которой отделяется заголовок:

  var comma = data.indexOf(',');
  if (comma>0) data = data.substring(comma+1);

Отправка файла кусками

Здесь все банально:

 var pos = 0;
 
 while (pos<data.length) {
 
    $.post('/upload.php',{
      filename:filename,
      chunk:data.substring(pos,pos+upload_chunk_size)
    });
 
    pos += upload_chunk_size;
  }

Серверная часть


Теперь сделаем на сервере принимающий PHP-скрипт upload.php. В варианте для примера он также предельно прост:

  $filename = $_POST['filename'];
  $f = fopen("/dir/to/save/$filename","a");
  fputs($f,base64_decode($_POST['chunk']));
  fclose($f);

Файл открывается с опцией «а», т.е. предлагается не перезаписывать существующий файл, а дополнять его. Таким способом мы из кусков соберем целый файл.

Думаю, всем понятно, что данный скрипт должен лежать в закрытой админской части сайта чтобы к нему не имели доступа разные личности со стороны. Кроме того, имя файла и сам файл должны проверяться на валидность. Иначе — это даже не уязвимость, а… я и слова-то такого не знаю…

Пробуем запустить


Попробовали? Получилось? Держу пари, что получилось совсем не то, что ожидалось. Файл вроде бы закачался. Вроде бы даже длина правильная. А вот содержимое — какая-то каша.

Почему так получилось? Ответ прост: POST-запросы отправляются в асинхронном режиме по нескольку кусков одновременно, поэтому никто не гарантирует, что на сервер они будут приходить именно в той последовательности, в которой были поданы команды на отправку. Мало того, что никто не гарантирует, а я прямо-таки утверждаю, что никогда они не придут в нужной последовательности. Всегда будет получаться каша.

Поэтому, как это ни грустно, придется асинхронность отключить. Перед оправкой файла выполним следующее:

 $.ajaxSetup({async:false});

Теперь все нормально. Файл собирается в нужной последовательности, однако на время закачки исполнение прочих скриптов приостанавливается. Поэтому, чтобы не нервировать пользователя, было бы неплохо где-нибудь показывать процентики или прогресс-бар. Таким образом, работоспособный скрипт выглядит следующим образом:

var upload_chunk_size = 120000; // Размер куска
 
function big_file_upload(file) {
  var data = file.get(0).files.item(0).getAsDataURL(); // Получаем содержимое файла
  var filename = file.get(0).files.item(0).fileName; // Получаем имя файла
 
  var comma = data.indexOf(','); 
  if (comma>0) data = data.substring(comma+1); // Отрезаем заголовок Data::URL
 
  var pos = 0;
  $.ajaxSetup({async:false}); // Отключаем асинхронность
 
  while (pos<data.length) {
 
    $.post('/upload.php',{ // Отправляем POST
      filename:filename, // Имя файла
      chunk:data.substring(pos,pos+upload_chunk_size) // Кусок файлв
    });
 
    pos += upload_chunk_size;
 
    var p = Math.round(pos*100/data.length); // Вычисляем процент отправленного
    $('#progress').text(p+'%'); // Рисуем цифирь для спокойствия пользователя
  }
}

Недостатки


Как же без них…

  1. В данном примере предполагается, что метод getAsDataURL всегда возвращает данные, закодированные в base64. На самом деле я бы не стал биться об заклад, что так будет всегда. По-хорошему, заголовок надо не выкидывать, а передавать серверной части, которую, в свою очередь, научить обрабатывать данные, закодированные разными способами.
  2. Файл, отправленный два раза, запишется на сервере два раза. Причем дополнит сам себя. Чтобы избежать этого, видимо, нужно передавать кроме имени еще и какой-то уникальный идентификатор закачки. Но это, вообще говоря, вопрос способа формирования и передачи имени файла. Здесь универсального рецепта нет и быть не может.
  3. Клиентский скрипт исполняется долго (в зависимости от размера файла и толщины канала), и FF может даже поинтересоваться: вы, мол, уверены ли, что надо ждать окончания, или убить его чтоб не мучался?
  4. Кроссбраузерность. Получение содержимого файла, увы, работает только на FF. Проверено на 3.0, 3.5 и 3.6. На более ранних не проверялось за неимением таковых под рукой. Сами разработчики FF рекомендуют вместо это способа пользоваться FileAPI, однако оно появилось только в 3.6.
  5. Действительно большие файлы (сотни мегабайт, гигабайты) закачать таким способом не получится. Предел зависит от объема памяти, доступной браузеру.


Что сделать?


Вероятно, надо попробовать все-таки в асинхронном режиме. Для каждого куска передавать еще и информацию о его расположении внутри файла. При этом серверная часть серьезно усложнится.
Автор: @Straight
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Комментарии (46)

  • –2
    >> Отправить

    1. Почему не сабмит?
    2. ИМХО, даже в наш век тотального JS надо навешивать все обработчики уже после загрузки DOM, а наличие решетки в href'е — вообще ни в какие ворота (в идеале все должно работать без JS, т.е. элемент формы должен иметь нормальный работающий адрес скрипта).

    >> Думаю, всем понятно, что данный скрипт должен лежать в закрытой админской части сайта чтобы к нему не имели доступа разные личности со стороны.

    Проверку в самом скрипте все равно надо делать, никому нельзя верить.
    • +4
      Надеюсь, Вы понимаете, что в данной статье показаны всего лишь краткие примеры? (Это к вопросу о решетке и сабмите). А что касается проверки в самом скрипте, то об этом русским по белому написано и в статье. Надеюсь, Вы не ставите мне в вину тот факт, что я в примере не реализовал эту проверку, которая сама по себе достойна не одной статьи?
      • –1
        Я ставлю в вину даже отсутствие упоминания о подобного рода практике. Всяко лучше, чем спорное «Думаю, нет нужды подробнее останавливаться на необходимости подключения библиотеки jQuery».
        • 0
          «Кроме того, имя файла и сам файл должны проверяться на валидность» — это не упоминание о подобного рода практике?
  • +6
    офигеть. Я бы тупо сменил хостера.
    • +2
      Не всегда получается, обычно принятие решений о переезде на стороне клиента принимается неделю-две, а вот файл залить нужно прямо сейчас, вчера, позавчера, к нам звонят и спрашивают где годовой отчет.

      Так что решение имеет право на жизнь.
      • +1
        у вас автоматизация костыля, а для описанных вами случаев достаточно ручной загрузки, т.к. случаи эти должны быть исключением, а не правилом.
        • 0
          (скромно) ну так-то да, руками проще залить, если раз в полгода, и сменить хостера, если болит очень, тут вопросов нет.

          Но если клиент настаивает и хостинг сохранить, и чтобы файлы закачивались — почему бы не сделать работу, и не выставить счет. Как говорится, все ваши желания вами и оплачиваются.
    • +3
      я бы тупо по ftp залил :)
  • +2
    jumpLoader обладает нужной вам функцией закачки по частям.
    Вот пример загрузки больших файлов частями с проверкой контрольной суммы.
    • +1
      А тут более простой пример, для быстрого оценки фичи.
    • +1
      Ага… Это интересно, спасибо.
    • +1
      еще один plupload.
      Поддерживает сразу несколько способов (flash, silverlight etc)
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Ну, допустим, популярность явы здесь не столь критична: напоминаю, что речь идет об админке, причем даже не о серийном продукте — для одного единственного клиента. Лицензия напрягает больше.

        Данная возможность точно поддерживается в FF 3.0. Насчет более старых — не знаю. IE, Опера, Хром — не поддерживают. А что касается стандартов, то более перспективной фичей представляется FileAPI. Его уже поддерживает FF 3.6, на подходе Хром. Опера, думаю, подтянется. Всю малину, конечно же, испортит IE.
        • 0
          Не стоит сильно переживать насчет лицензии, как сказано на сайте «You may use this software for free».

          Покупать нужно для отключения лого, но я его там так и не нашел, этого лого (разве что в диалоге about, который никто не заставляет вас вызывать).
          Или для получения исходников. Этот вариант я считаю вполне справедливым, т.к. работа автором проделана немаленькая и вполне достойна той платы, что он посит.
      • 0
        Ну, как уже ответил автор топика, для админки не так критично ява это или флэш.

        А про закрытость лицензии не совсем верно. Пользовать вы его можете абсолютно бесплатно. За деньги вам предлагаются версии «без лого» и исходники.

        Думаю для большинства будет достаточно бесплатной версии, тем более что функционал этой бесплатностью никак не ограничивается.
  • 0
    Хм…
    а директивы в .htaccess
    php_value upload_max_filesize
    php_value post_max_size
    хостер тоже не поддерживает????
    • +1
      Неужто Вас удивляет, что такое бывает?
      • 0
        Честно-говоря удивило несколько…
        В свое время для самописного приложения делали что-то подобное через JSP…
        Но пример интересный — спасибо!
      • +2
        Значит там CGI и вам надо бы править свой php.ini :)
        Кстати, а что за хостер? )
        • +1
          Согласен с вами, товарищ!
          Тоже в свое время попался на эту фишку — даже в саппорт писал.
          А мне и говорят — это город Ленинград php.ini вам надо ковырять!
          Кстати, хостер-то был и есть Логол…
          • +1
            Ведь намного удобнее положить php.ini к скрипту, для которого надо изменить параметры, чем ковыряться в .htaccess, который еще и перекроет параметры, настроенные до этого.
  • НЛО прилетело и опубликовало эту надпись здесь
  • +2
    Вообще, конечно, перед тем как регистрируется хостинг — надо открыть страницу опций и хорошо их изучить, на предмет совместимости с желаниями клиентов ;) У меня была такая же ситуация, после непродолжительной переписки с хостером, ограничение для моего аккаунта было расширено до 30 мб. Насколько я понял, подобные ограничения — для защиты от нелегального распространения контента. Когда я объяснил что на хостинге сайт музыкальной группы и загружают они только свое видео и музыку, хостер не стал препятствовать и/или просить денег за это. Всегда можно договорится (если обосновать) вы это пробовали?
    • 0
      В данном случае все решается банальной сменой тарифного плана. Остается малость: убедить в этом клиента. А вот с этим проблема. Задача ведь решена? Решена. А если нет разницы — зачем платить больше?
  • 0
    Тоже сталкивался с похожей задачей и уперся в пределы памяти.

    Почему они не сделали простой произвольный доступ к содержимому файла, вместо единственно возможного чтения его целиком в память? Неудобно…

    Интересно, из флеша это можно обойти?
  • 0
    Подскажите, насколько ресурсоемки подобные операции (подсчет размера файла, перебор по частям) в javascript? Скажем, потянет ли средний клиент обсчет файла размером 1-2 Гб (не берем в расчет загрузку, речь только о клиентской стороне)?
    • 0
      Нет, гигабайты — это уже явно за пределами.
  • 0
    Как вариант — через промежуточный хост с нормальными настройками ;) Который бы уже бил файл и сливал на основной по кусочкам.
    • 0
      Боюсь, этот вариант еще более экзотичен. Кто будет оплачивать этот промежуточный хост? Клиент? Зачем тогда весь этот апофеоз, если в этом случае можно просто захостить сайт на этом хосте? Может быть, наша компания? Но что тогда будет если клиент прекратит сотрудничество с нами? Наша разработка прекратит функционировать, что не очень красиво.
  • НЛО прилетело и опубликовало эту надпись здесь
    • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      я сходу и прочитал как «Текарт». ЧЯДНТ?
    • 0
      >Теперь прочитаем — «Текарт».
      >Какое слово мозг подставит первым? =)

      Декарт
  • 0
    Вот вы извращенцы =)
  • 0
    А вот как преобразуется серверная часть в случае, если передача ведется в асинхронном режиме:

    $filename = "/dir/to/save/".$_POST['filename'];
    fclose(fopen($filename, 'a')); // создадим файл
    $f = fopen($filename,"r+");
    fseek($fp, $_POST['offset']);
    fputs($f,base64_decode($_POST['chunk']));
    fclose($f);


    А на клиенте, соответственно:

    $.post('/upload.php',{
    filename:filename,
    chunk:data.substring(pos,pos+upload_chunk_size),
    offset: pos
    });
    • 0
      Только одно «но» — если все-таки у вас работает декодирование base64 от обрезанной строки, то надо offset тоже правильно посылать, а именно — умножать на 3/4 (см. base64 на википедии). Т.е. либо на клиенте, либо на сервере нужно написать:

      offset: pos*3/4


      Или

      fseek($fp, $_POST['offset']*3/4);


      Извините, не сразу понял, что в base64 данные приходят :).
    • +5
      fclose(fopen($filename, 'a'));


      Шикарно. Есть такая функция, touch называется.
      • 0
        Угадайте, что она делает :). Так что я лично разницы не вижу.
        • +1
          Если для изготовления дырок в стене есть дрель, то какой смысл делать дырки забивая гвозди и потом вытаскивая их пассатижами?
  • 0
    Кстати… вместо отключения асинхронности лучше бы отправляли номер чанка и собирали при получении последнего
    • 0
      угу, серверная часть при этом не сложнее: открываем файл, перемещаемся на позицию X, пишем очередной чанк. Ну и можно заранее создавать нужный по размеру файл, забитый нулями.
  • 0
    Похожее решение, сделанное в недрах mail.ru
    habrahabr.ru/company/mailru/blog/102551/

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

Самое читаемое Разное