Компания
1 355,29
рейтинг
10 февраля 2011 в 14:56

Разное → Загрузка файлов с помощью html5 File API, с преферансом и танцовщицами

Предисловие


Загрузка файлов всегда занимала особое место в веб-разработке.
О трудности оформления стилями <input type=file/> уже сказано немало, почитать об этом можно, например, по ссылкам раз, два, три, четыре, пять, шесть.
Но и сам процесс загрузки файлов нетривиален, есть много разных способов – и ни одного идеального.

Я уже писал о внедрении на нашем проекте Файлы@Mail.Ru silverlight-загрузчика полгода назад. На тот момент у нас подерживались iframe, flash, silverlight и обычная загрузка файлов. Но прогресс не стоит на месте, и вот уже последние бета-версии всеми горячо любимых браузеров в полной мере поддерживают html5 FileAPI (справедливости ради, стоит заметить, что, как обычно, некоторые поддерживают своеобразно, но об этом — ниже).

Пока писалась статья, Chrome 9 был объявлен stable и форсировано обновился уже на 75% установок 8 версии. Так, что празднуем поддержку File API первым стабильным браузером, ура!

Мы подумали, что не использовать такую технологию было бы преступлением против юзеров пользователей.
Подумали — и внедрили html5 загрузку в дополнение к уже существующим вариантам.
В итоге наши пользователи получили множество плюшек:
— прозрачная дозагрузка после обрыва соединения (и даже рестарта браузера!);
— очередь загрузки;
— прогресс-бар (пользователи MacOS и Safari наконец могут видеть прогресс без всяких инородных плагинов), возможность удаления файлов из очереди, если передумал.


Используя File API мы можем программно, из javascript-кода:
1. получить список выбранных в диалоге файлов, их размеры и mime-типы (на которые, к слову, не стоит рассчитывать, т.к. некоторые популярные типы файлов браузеры по расширению не определяют).
2. получить необходимый диапазон байтов из файла, не загружая целиком содержимое файла в память (в отличие от Flash и Firefox 3 – см. примечание 1).
3. загрузить на сервер как целый файл, так и его кусочек.
4. загружать файлы в один drag-n-drop.
5. загружать одновременно (параллельно) несколько файлов.
Т.е. нам не нужны никакие плагины для манипуляций с файлами, и это, безусловно, очень круто!

Фабула


Собственно загрузка файлов реализуется в File API всего в несколько строк, но мы добавили несколько приятных фич (очередь загрузки, дозагрузка при обрыве соединения) и код стал немного сложнее.
Код загрузчика на проекте Файлы@Mail.Ru доступен и не обфусцирован и его можно изучить, но он привязан к проекту и его особенностям, поэтому мы рассмотрим этот механизм загрузки в чистом виде на примере проекта lightweight uploader.

Итак, поехали…

Мы вешаем на input обработчик onchange.

oself.file_elm.onchange = function() {
	oself.onSelect(this); // 'this' is a DOM object here
}


Объект input поддерживает html5 атрибуты multiple для разрешения выбора нескольких файлов за раз в диалоге и accept (см. прим. 2), который производит фильтрацию файлов в диалоге согласно заданным mime-типам.

В методе onSelect пробегаемся по массиву files (который содержит сформированный браузером список выбранных файлов), выставляем дефолтные свойства и генерируем событие onSelect для каждого файла.
После этого пересоздаем кнопку, т.е. удаляем input и создаем его заново. Это делается для того, чтобы исключить повторную загрузку выбранных файлов при отправке формы на сервер в случае, когда кнопка находится внутри формы.
Инициатором начала загрузки в данном случае выступает слушатель события onSelect, вызывая метод объекта-загрузчика enqueueUpload.

/*
 n - номер загрузчика на странице, исключительно для удобства
 file - сам объект типа File
 idx - порядковый номер текущего файла
 cnt - общее количество выбранных за раз файлов
*/
function onSelect(n, file, idx, cnt) {
	if(file.size > 1 * 1024 * 1024) {
		alert("File is too big!\nMaximum size is 1 MB.");
		return;
	}
	var d = document.createElement('div');
	d.id = 'file_' + file.id + '_' + n;
	document.getElementById('file_list_' + n).appendChild(d);
	d.innerHTML = '<a href="#" id="file_' + file.id + '_cancel_' + n + '">X</a>' + file.name + ' (' + file.size + ') <span id="file_' + file.id + '_status_' + n + '">...</span>'
	document.getElementById('file_' + file.id + '_cancel_' + n).onclick = function() {
		window['up' + n].cancelUpload(file.id);
		return false;
	};
	window['up' + n].enqueueUpload(file, 'http://lwu.no-ip.org/upload', "arg1=val1&arg2=val2");
}


Метод enqueueUpload добавляет файл во внутреннюю очередь загрузчика, добавляет файл в очередь фронтенда (фронтенд — это сущность, взаимодействующая с пользователем и позволяющая ему выбирать файлы, т.е. либо input, либо плагин Flash или Silverlight) и вызывает метод startNextUpload, который либо сразу стартует загрузку этого файла, либо откладывает её, если уже одновременно загружается заданное при инициализации количество файлов.

При добавлении файла в очередь фронтенда, html5 фронтенд запускает механизм обсчета уникального хеша файла, с помощью которого [хеша] реализуется дозагрузка. Подробности можно посмотреть в статье про silverlight-загрузчик.
Да-да, хеш опять подсчитывается по алгоритму Adler32.

oself.addFile = function(fo) {
	upFE_html5.superclass.addFile.apply(oself, [fo]);
	oself.calcChunkSize(fo);
	oself.calcFileHash(); // run calculation for next file
};


После подсчета хеша происходит обращение к локальному хранилищу для проверки, есть ли там информация о предыдущей неудачной загрузке этого файла. Если информация находится — атрибуты файла url, sessionID и uploadedRange перезаписываются информацией из локального хранилища.
Локальное хранилище (оно же WebStorage) — это еще один элемент html5, который позволяет хранить произвольные данные в формате ключ-значение на стороне пользователя либо на время сессии (SessionStorage), либо постоянно (LocalStorage).
Когда доходит очередь до загрузки файла, вызывается метод загрузчика startUpload, который генерирует событие onStart и запускает загрузку.

oself.startUpload = function(id, url, data) {
	var fo = oself.getFile(id);
	fo.url = url; // at this moment url already fetched from localStorage if info presents
	fo.data = data;
	fo.full_url = fo.url + (fo.url.match(/\?/) ? '&' : '?') + fo.data;
	fo.retry = oself.opts.maxChunkRetries;
	oself.broadcast('onStart', fo);
	oself.uploadFile(fo);
};


Метод uploadFile производит непосредственную загрузку файла на сервер.

oself.uploadFile = function(fo) {
	oself.calcNextChunkRange(fo);
	var blob, simple_upload = 0;
	try {
		blob = fo.slice(fo.currentChunkStartPos, fo.currentChunkEndPos - fo.currentChunkStartPos + 1);
	} catch(e) { // Safari doesn't support Blob.slice method
		blob = new FormData();
		blob.append('Filedata', fo);
		simple_upload = 1;
	};
	fo.xhr = new XMLHttpRequest();
	fo.xhr.onreadystatechange = function() {
		if(this.readyState == 4) {
			try {
				if(this.status == 201) { // chunk was uploaded succesfully
					var range = this.responseText;
					try { // getResponseHeader throws exception during cross-domain upload, but this is most reliable variant
						range = this.getResponseHeader('Range');
					} catch(e) {};
					if(!range) {
						throw new Error('No range in 201 answer');
					}
					fo.uploadedRange = range; // store range for case of later retry
					fo.retry = oself.opts.maxChunkRetries; // restore retry counter
					userStorage.set(fo); // add or update file info in localStorage
					oself.uploadFile(fo);
				} else if(this.status == 200) {
					fo.responseText = this.responseText;
					fo.loaded = fo.size; // all bytes were uploaded
					userStorage.del(fo); // delete file info from localStorage
					oself.broadcast('onDone', fo, fo.responseText);
				} else if(this.status == 0 && fo.cancel == 1) {
					//t('Aborted uploading for id=' + fo.id);
				} else {
					throw new Error('Bad http answer code');
				}
			} catch(e) { // any exception means that we need to retry upload
				oself.retryUpload(fo);
			};
		}
	};
	fo.xhr.open("POST", fo.full_url, true);
	fo.xhr.upload.onprogress = function(evt) {
		fo.loaded = (simple_upload ? 0 : fo._loaded) + evt.loaded;
		oself.broadcast('onProgress', fo);
	};
	if(!simple_upload) {
		fo.xhr.setRequestHeader('Session-ID', fo.sessionID);
		fo.xhr.setRequestHeader('Content-Disposition', 'attachment; filename="' + encodeURI(fo.name) + '\"');
		fo.xhr.setRequestHeader('Content-Range', 'bytes ' + fo.currentChunkStartPos + '-' + fo.currentChunkEndPos + '/' + fo.size);
		fo.xhr.setRequestHeader('Content-Type', 'application/octet-stream');
	}
	fo.xhr.withCredentials = true; // allow cookies to be sent
	fo.xhr.send(blob);
};


Комментарии в коде ясно показывают неполную поддержку html5 File API в браузере Safari (по крайней мере, в OS Windows), см. прим. 3.
При возникновении ошибок запускается метод retryUpload, который повторно пытается загрузить файл указанное при инициализации загрузчика количество раз, увеличивая промежуток между попытками при каждой неудаче.
В случае исчерпания количества попыток генерируется событие onError.

oself.retryUpload = function(fo) {
	fo.retry--;
	if(fo.retry > 0) {
		var timeout = oself.opts.retryTimeoutBase * (oself.opts.maxChunkRetries - fo.retry);
		setTimeout(function(){oself.uploadFile(fo)}, timeout);
	} else {
		oself.broadcast('onError', fo. lwu.ERROR_CODES.OTHER_ERROR);
	}
};


Для работы всего этого чуда на сервере должен быть установлен nginx с upload-модулем. Чуть подробнее об этом было написано в предыдущей статье.

Вместо послесловия...


Хочется высказать несколько мыслей:
1. На данный момент FileAPI поддерживают Chrome 8 и выше, Firefox 4 beta и частично Safari 5. Про внедрение поддержки в InternetExplorer и Opera мне ничего не известно.
Однако, Chrome 8 мы отключили из-за досадного бага, из-за которого нельзя выбрать много файлов в диалоге.
Firefox 3 поддерживает FileAPI по-своему, там нет поддержки насущно необходимого объекта FormData, поэтому загрузка больших файлов невозможна, т.к. требует чтения всего содержимого файла в память компьютера.
2. Атрибут accept работает очень коряво, много mime-типов браузеры просто не понимают. Поэтому для меня остается загадкой, почему фильтрация сделана именно так, а не по списку расширений, как это сделано в Flash и Silverlight.
3. Браузер Safari не реализует объект FileReader и метод Blob.slice, поэтому в нём не работает дозагрузка средствами html5. Т.к., дозагрузка — это очень полезная «плюшка», то мы поменяли в Safari порядок вызова загрузчиков, сделав Silverlight более предпочтительным.
4. Не совсем очевидно, но при использовании битовых операций Javascript преобразует операнды к типу signed int32. А т.к. для подсчета контрольной суммы Adler32 нужны беззнаковые числа, пришлось отказаться от битового сдвига влево и использовать умножение на 65536.
5. Нужно делать URI-кодирование имени файла на клиенте и декодирование на сервере, т.к. имя попадает в заголовок Content-Disposition, а заголовки не должны по стандарту содержать не-ASCII символы.
6. Обязательно нужно предупреждать пользователей о необходимости отключения плагина Firebug или ему подобных и вот почему: Firebug на вкладке Сеть логирует всю сетевую активность и полностью сохраняет все запросы, а т.к. наши запросы небольшие по размеру, то встроенный ограничитель плагина не срабатывает и на больших файлах мы можем получить большое потребление памяти браузером.

Дмитрий Дедюхин, ведущий разработчик Файлы@Mail.Ru
Автор: @Demetros

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

  • +24
    Побольше бы таких публикаций на этом «новостном и развлекательном» портале
  • +7
    Печально, но слова «преферанс» и «танцовщицы» есть только в заголовке.
  • 0
    +1, конечно, но плак-плак-плак. Похоже, вскоре любимым w3m просторы Intenet бороздить не получится.
  • 0
    А чем ваш сервис лучше яндекс.диска? сравнил по опциям, нет ниодной превосходящей яд.
    пс: попробовал протестировать сервис и кинуть видео файл на 700мб в Опере 11, вылез алерт сразу и файл не стал даже пытаться загружаться, печально
    • +1
      Ну. Просто есть N пользователей, которые сидят в mail.ru и у них сразу же есть закладочка: файлы, и им не надо идти на yandex.direct.
      • –2
        по моему скромному мнению, мне кажется большинство сдешних обитателей пользуется скорее уж джимейлом, так что для них что открыть в один клик яд, что маилру будет равнозначно по усилиям, не так ли?
        • 0
          Ну. Ваше мнение не соответствует действительности. У mail.ru — огромная аудитория. Все мои не-компьютерные знакомые за единственным исключением почту держат именно на mail.ru.

          Ну. И ЯД открыть им сложнее. Про него надо 1 — знать, и 2 — найти, куда надо кликнуть. Многие люди даже отвыкли от того, чтобы чего-нибудь в address-bar набирать.
          • 0
            Читайте внимательнее, я написал про айтишников, которые обитают на хабре.
            Насколько мне известно на тот и другой сервис можно перейти по ссылке с морды поисковика, так что тогда будет играть ключевую роль при прочих очевидных плюсах?
            • +1
              О… Не воспринял слово «сдешних», но в этом отчасти и ваша вина. Как бы, здешние — вот верный вариант. Но mail.ru работает же не только на здешних обитателей
              • –1
                описался, бывает. Не надо цепляться.
                Ну так как пост на хабре этот обсуждаем, собственно и надо рассматривать тогда сервис с точки зрения хабра. Так что по существу?
                • 0
                  Хабр должен быть социально-ответственным Ъ! :)
                • 0
                  Не. Ну по существо это самое как раз. Mail.ru делает хранилку для своих пользователей. Ему нет никакой выгоды ставить у себя ссылку на Yandex. Хранилка mail.ru-шная вполне юзабельная. Yandex'овская, конечно, намного вместительней, да ещё и с высокой скоростью работает, потому что у нас Yandex есть в городе, и туда всё кэшируется. Но mail.ru для повседневных нужд вполне подходит: поделиться записью мозгового штурма, архивом фоток и прочими подобными штуками.
                • 0
                  По-моему, рассматривается не сервис, а механизм его реализации. Лично мне пост интересен и полезен, не смотря на то, что ни одним подобным сервисом не пользуюсь.
                  • 0
                    Именно так.
                    Тем более, я написал о некоторых «аномалиях», с которыми мы столкнулись при внедрении, описания которых я не встречал нигде.
                    • 0
                      Почему мне и нравятся ваши посты, в них часто описаны такие «аномалии» :)
    • +1
      У нас загрузка на любой вкус и браузер, в случае сильверлайта и хтмл5 есть дозагрузка, на яд это есть?
      Вы наверно были неавторизованным и вам в алерте написали, что нельзя загружать больше 100МБ?
      • –1
        Я вот Оперой пользуюсь в основном и честно говоря мне до лампочки через html5 или сирвелайт идет загрузка, так в данном случае не вижу минуса в отсутствии этих способов у яда он и так неплохо справляется с функциями своими.
        По поводу дозагрузки: да это приятно, но в наше время больших скоростей, а не диалапа большие файлы и так, в принципе, заливаются на раз.
        Не, я был залогинен как раз и перешел туда из соседней вкладки с почтой.
        ps: попробовал еще раз залить файл того же объема, скорость аплоада на уровне 160 кбс не порадовала, если честно. Передумал и отменил закачку.
        Уж извините, но по субъективным ощущениям вам еще не скоро удастся нагнать яд, не в плане применяемых технологий, а по таким обыденным параметрам как макс. объем заливаемых файлов, время хранения скорость закачки и т. д.
        Ведь рядовым обывателям плевать какие фичи используют создатели, им важны как раз эти простые параметры, которые на поверхности, понимаете?
      • +2
        «Сервелата» нет, но простым перетаскиванием файла уже давно умеет загружать.
  • +1
    но в наше время больших скоростей

    Вы в Питере живете? Не забывайте, что Москва и Питер — это далеко не вся Россия.
    • 0
      Да в Питере, понятно что не вся, но в ДС и ДС2 обитает большинство активных сетевых пользователей, верно?
      Ну а в целом по более менее крупным городам есть уже инет на более чем мегабитных скоростях, про цену не говорю
      • 0
        Инет-то есть. И даже скорости заявлены мегабитные.

        Но по факту работает это примерно так:
        в крупном городе мегабитная скорость — это 1-2 мегабита в секунду рублей за 500 в месяц. 5 мегабит стоит уже тысячи полторы. Да и то режется до 400-500 килобит в секунду по достижении относительно небольшого лимита: 10-20 гигабайт. То есть, 5-6 фильмов можно скачать еще мегабитом, а остальное уже лучше не качать. Ибо долго.

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

        Очень доступно, правда?
        Кстати, регион — один из самых продвинутых в России по части проникновения интернета.
        • 0
          Поддерживаю. Хорошо только в столицах. Живу в Екатеринбурге — всё замечательно просто с интернетом. Скорости даже до 10 мегабит бывают, при чём выдерживаются, как заявлено. Но уезжаешь в область, даже во вполне солидный и влиятельный областной центр, километров за 70 от областной столицы — и всё, полный швах. Ничего надёжнее GPRS нет.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Почему же рано? Проект Lightweight Uploader как раз и создан с целью применять File API тогда, когда оно доступно, а когда недоступно — flash и silverlight.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Ну первая ласточка уже есть — стабильный Chrome 9. Уже 7-9% пользователей.
          Firefox 4 уже совсем скоро, в начале этого года: beta 11 уже вышла, а 12-я будет Release Candidate. Это еще 26%.
          Итого ждем 35% аудитории через несколько месяцев.

          Опера и IE9 пока не торопятся поддерживать File API. Ну значит, продолжат терять долю ;)
    • 0
      Весной запускаем проект — «танцовщицы» только для Firefox 4 и Chrome 9+ То, что оба опенсорсные чистое совпадение, просто они лучше поддерживают HTML5, чем конкуренты. Остальным старый добрый HTML4 без JS фактически.
  • 0
    «с преферансом и танцовщицами» — после этого даже читать не стал — классику то не порте, преферанс и танцовщицы — это не реально както, сразу представляешь себе бендера в розовом и гея!, блекджек и шлюхи — вот в чем сила
    • 0
      Шлюхи бы резали глаз на главной странице.
      А смысл Вы и так поняли :)
  • 0
    Вопрос: планируете ли вы сделать API Файлов@Mail.Ru для анонимных / пользователей @mail.ru?
    • 0
      Анонимусы не доставляют, это всегда риск массовых автоматизированных закачек роботами. Так что об их интересах будем думать в последнюю очередь.
      Для авторизованных пользователей есть несколько интересных идей. API среди них не на первом месте, но мы думаем и об этом тоже.
  • 0
    Спасибо за замечательную статью!

    Вышел на неё случайно в момент решения странной проблемы: при некоторых пока неизвестных настройках браузера FileAPI не загружает файлы вообще (жалуются именно корпоративные клиенты кто сидит в нете через VPN). Танцы с бубном пока ни к чему не привели, но вдруг вам приходилось сталкиваться с похожими проблемами?

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

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