CodeNet / Языки программирования / Java Script / Формы
CodeNet / Веб программирование / HTML
HTML5 Ajax upload очень больших файлов
HTML5 Ajax upload очень больших файлов
7 сентября 2011 года
В этой небольшой заметке рассмотрен способ загрузки очень больших файлов (больше 2Гб) через браузер. Метод приведенный в этой заметке хорош тем что:
- Файл загружается асинхронно. В это время на странице отображается индикатор загрузки (ProgressBar);
- Файл загружается по частям и не считывается целиком в память ни сервером ни браузером клиента;
- Браузер повторяет загрузку секции, если предыдущая попытка закончилась неудачно. Это очень полезное свойство при загрузке больших файлов по HTTP.
Плох этот методом тем, что требует использования браузера на базе Gecko 2.0 и выше или WebKit. На текущий момент, это такие браузеры как FireFox 4.0+, SeaMonkey 2.1+ или Google Chrome 11+.
Основная идея
Файл считывается по частям с помощью методов slice(), mozSlice() или webkitSlice() объекта Blob [2]. После этого, каждая часть отправляется на сервер с помощью sendAsBinary() объекта XMLHttpRequest [1]. Если часть по какой-то причине не загрузилась, то производится попытка загрузить часть снова.
Стоит обратить внимание, что метод slice() объекта Blob устарел начиная с версии Gecko 2.0 (FireFox 4), метод mozSlice() добавлен в Gecko 5.0 (FireFox 5) и метод webkitSlice() добавлен Google Chrome 11.
Еще в Google Chrome нет метода sendAsBinary() объекта XMLHttpRequest.
Скачать в архиве (ZIP; 5.7 Кб)
Код
fileuploader.js
JavaScript код работает на стороне клиента (в браузере), считывает файл по частям и отправляет его на сервер.
// если он не определен (Например, для браузера Google Chrome).
if (!XMLHttpRequest.prototype.sendAsBinary) {
XMLHttpRequest.prototype.sendAsBinary = function(datastr) {
function byteValue(x) {
return x.charCodeAt(0) & 0xff;
}
var ords = Array.prototype.map.call(datastr, byteValue);
var ui8a = new Uint8Array(ords);
this.send(ui8a.buffer);
}
}
/**
* Класс FileUploader.
* @param ioptions Ассоциативный массив опций загрузки
*/
function FileUploader(ioptions) {
// Позиция, с которой будем загружать файл
this.position=0;
// Размер загружаемого файла
this.filesize=0;
// Объект Blob или File (FileList[i])
this.file = null;
// Ассоциативный массив опций
this.options=ioptions;
// Если не определена опция uploadscript, то возвращаем null. Нельзя
// продолжать, если эта опция не определена.
if (this.options['uploadscript']==undefined) return null;
/*
* Проверка, поддерживает ли браузер необходимые объекты
* @return true, если браузер поддерживает все необходимые объекты
*/
this.CheckBrowser=function() {
if (window.File && window.FileReader && window.FileList && window.Blob) return true; else return false;
}
/*
* Загрузка части файла на сервер
* @param from Позиция, с которой будем загружать файл
*/
this.UploadPortion=function(from) {
// Объект FileReader, в него будем считывать часть загружаемого файла
var reader = new FileReader();
// Текущий объект
var that=this;
// Позиция с которой будем загружать файл
var loadfrom=from;
// Объект Blob, для частичного считывания файла
var blob=null;
// Таймаут для функции setTimeout. С помощью этой функции реализована повторная попытка загрузки
// по таймауту (что не совсем корректно)
var xhrHttpTimeout=null;
/*
* Событие срабатывающее после чтения части файла в FileReader
* @param evt Событие
*/
reader.onloadend = function(evt) {
if (evt.target.readyState == FileReader.DONE) {
// Создадим объект XMLHttpRequest, установим адрес скрипта для POST
// и необходимые заголовки HTTP запроса.
var xhr = new XMLHttpRequest();
xhr.open('POST', that.options['uploadscript'], true);
xhr.setRequestHeader("Content-Type", "application/x-binary; charset=x-user-defined");
// Идентификатор загрузки (чтобы знать на стороне сервера что с чем склеивать)
xhr.setRequestHeader("Upload-Id", that.options['uploadid']);
// Позиция начала в файле
xhr.setRequestHeader("Portion-From", from);
// Размер порции
xhr.setRequestHeader("Portion-Size", that.options['portion']);
// Установим таймаут
that.xhrHttpTimeout=setTimeout(function() {
xhr.abort();
},that.options['timeout']);
/*
* Событие XMLHttpRequest.onProcess. Отрисовка ProgressBar.
* @param evt Событие
*/
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
// Посчитаем количество закаченного в процентах (с точность до 0.1)
var percentComplete = Math.round((loadfrom+evt.loaded) * 1000 / that.filesize);percentComplete/=10;
// Посчитаем ширину синей полоски ProgressBar
var width=Math.round((loadfrom+evt.loaded) * 300 / that.filesize);
// Изменим свойства элементом ProgressBar'а, добавим к нему текст
var div1=document.getElementById('cnuploader_progressbar');
var div2=document.getElementById('cnuploader_progresscomplete');
div1.style.display='block';
div2.style.display='block';
div2.style.width=width+'px';
if (percentComplete<30) {
div2.textContent='';
div1.textContent=percentComplete+'%';
}
else {
div2.textContent=percentComplete+'%';
div1.textContent='';
}
}
}, false);
/*
* Событие XMLHttpRequest.onLoad. Окончание загрузки порции.
* @param evt Событие
*/
xhr.addEventListener("load", function(evt) {
// Очистим таймаут
clearTimeout(that.xhrHttpTimeout);
// Если сервер не вернул HTTP статус 200, то выведем окно с сообщением сервера.
if (evt.target.status!=200) {
alert(evt.target.responseText);
return;
}
// Добавим к текущей позиции размер порции.
that.position+=that.options['portion'];
// Закачаем следующую порцию, если файл еще не кончился.
if (that.filesize>that.position) {
that.UploadPortion(that.position);
}
else {
// Если все порции загружены, сообщим об этом серверу. XMLHttpRequest, метод GET,
// PHP скрипт тот-же.
var gxhr = new XMLHttpRequest();
gxhr.open('GET', that.options['uploadscript']+'?action=done', true);
// Установим идентификатор загруки.
gxhr.setRequestHeader("Upload-Id", that.options['uploadid']);
/*
* Событие XMLHttpRequest.onLoad. Окончание загрузки сообщения об окончании загрузки файла :).
* @param evt Событие
*/
gxhr.addEventListener("load", function(evt) {
// Если сервер не вернул HTTP статус 200, то выведем окно с сообщением сервера.
if (evt.target.status!=200) {
alert(evt.target.responseText.toString());
return;
}
// Если все нормально, то отправим пользователя дальше. Там может быть сообщение
// об успешной загрузке или следующий шаг формы с дополнительным полями.
else window.parent.location=that.options['redirect_success'];
}, false);
// Отправим HTTP GET запрос
gxhr.sendAsBinary('');
}
}, false);
/*
* Событие XMLHttpRequest.onError. Ошибка при загрузке
* @param evt Событие
*/
xhr.addEventListener("error", function(evt) {
// Очистим таймаут
clearTimeout(that.xhrHttpTimeout);
// Сообщим серверу об ошибке во время загруке, сервер сможет удалить уже загруженные части.
// XMLHttpRequest, метод GET, PHP скрипт тот-же.
var gxhr = new XMLHttpRequest();
gxhr.open('GET', that.options['uploadscript']+'?action=abort', true);
// Установим идентификатор загруки.
gxhr.setRequestHeader("Upload-Id", that.options['uploadid']);
/*
* Событие XMLHttpRequest.onLoad. Окончание загрузки сообщения об ошибке загрузки :).
* @param evt Событие
*/
gxhr.addEventListener("load", function(evt) {
// Если сервер не вернул HTTP статус 200, то выведем окно с сообщением сервера.
if (evt.target.status!=200) {
alert(evt.target.responseText);
return;
}
}, false);
// Отправим HTTP GET запрос
gxhr.sendAsBinary('');
// Отобразим сообщение об ошибке
if (that.options['message_error']==undefined) alert("There was an error attempting to upload the file."); else alert(that.options['message_error']);
}, false);
/*
* Событие XMLHttpRequest.onAbort. Если по какой-то причине передача прервана, повторим попытку.
* @param evt Событие
*/
xhr.addEventListener("abort", function(evt) {
clearTimeout(that.xhrHttpTimeout);
that.UploadPortion(that.position);
}, false);
// Отправим порцию методом POST
xhr.sendAsBinary(evt.target.result);
}
};
that.blob=null;
// Считаем порцию в объект Blob. Три условия для трех возможных определений Blob.[.*]slice().
if (this.file.slice) that.blob=this.file.slice(from,from+that.options['portion']);
else {
if (this.file.webkitSlice) that.blob=this.file.webkitSlice(from,from+that.options['portion']);
else {
if (this.file.mozSlice) that.blob=this.file.mozSlice(from,from+that.options['portion']);
}
}
// Считаем Blob (часть файла) в FileReader
reader.readAsBinaryString(that.blob);
}
/*
* Загрузка файла на сервер
* return Число. Если не 0, то произошла ошибка
*/
this.Upload=function() {
// Скроем форму, чтобы пользователь не отправил файл дважды
var e=document.getElementById(this.options['form']);
if (e) e.style.display='none';
if (!this.file) return -1;
else {
// Если размер файла больше размера порциии ограничимся одной порцией
if (this.filesize>this.options['portion']) this.UploadPortion(0,this.options['portion']);
// Иначе отправим файл целиком
else this.UploadPortion(0,this.filesize);
}
}
if (this.CheckBrowser()) {
// Установим значения по умолчанию
if (this.options['portion']==undefined) this.options['portion']=1048576;
if (this.options['timeout']==undefined) this.options['timeout']=15000;
var that = this;
// Добавим обработку события выбора файла
document.getElementById(this.options['formfiles']).addEventListener('change', function (evt) {
var files=evt.target.files;
// Выберем только первый файл
for (var i = 0, f; f = files[i]; i++) {
that.filesize=f.size;
that.file = f;
break;
}
}, false);
// Добавим обработку события onSubmit формы
document.getElementById(this.options['form']).addEventListener('submit', function (evt) {
that.Upload();
(arguments[0].preventDefault)? arguments[0].preventDefault(): arguments[0].returnValue = false;
}, false);
}
}
upload.php
PHP скрипт, работает на стороне сервера. Его задача - принимать и склеивать порции. После окончания передачи всех порций скрипт переименовывает файл и создает флаг сигнализирующий о том что файл готов к обработке. В моем случае отдельный демон проверяет этот флаг и берет файлы "в оборот".
$uploaddir="./uploaddir";
// Идентификатор загрузки (аплоада). Для генерации идентификатора я обычно использую функцию md5()
$hash=$_SERVER["HTTP_UPLOAD_ID"];
// Информацию о ходе загрузки сохраним в системный лог, это позволить решать проблемы оперативнее
openlog("html5upload.php", LOG_PID | LOG_PERROR, LOG_LOCAL0);
// Проверим корректность идентификатора
if (preg_match("/^[0123456789abcdef]{32}$/i",$hash)) {
// Если HTTP запрос сделан методом GET, то это не загрузка порции, а пост-обработка
if ($_SERVER["REQUEST_METHOD"]=="GET") {
// abort - сотрем загружаемый файл. Загрузка не удалась.
if ($_GET["action"]=="abort") {
if (is_file($uploaddir."/".$hash.".html5upload")) unlink($uploaddir."/".$hash.".html5upload");
print "ok abort";
return;
}
// done - загрузка завершена успешно. Переименуем файл и создадим файл-флаг.
if ($_GET["action"]=="done") {
syslog(LOG_INFO, "Finished for hash ".$hash);
// Если файл существует, то удалим его
if (is_file($uploaddir."/".$hash.".original")) unlink($uploaddir."/".$hash.".original");
// Переименуем загружаемый файл
rename($uploaddir."/".$hash.".html5upload",$uploaddir."/".$hash.".original");
// Создадим файл-флаг
$fw=fopen($uploaddir."/".$hash.".original_ready","wb");if ($fw) fclose($fw);
}
}
// Если HTTP запрос сделан методом POST, то это загрузка порции
elseif ($_SERVER["REQUEST_METHOD"]=="POST") {
syslog(LOG_INFO, "Uploading chunk. Hash ".$hash." (".intval($_SERVER["HTTP_PORTION_FROM"])."-".intval($_SERVER["HTTP_PORTION_FROM"]+$_SERVER["HTTP_PORTION_SIZE"]).", size: ".intval($_SERVER["HTTP_PORTION_SIZE"]).")");
// Имя файла получим из идентификатора загрузки
$filename=$uploaddir."/".$hash.".html5upload";
// Если загружается первая порция, то откроем файл для записи, если не первая, то для дозаписи.
if (intval($_SERVER["HTTP_PORTION_FROM"])==0)
$fout=fopen($filename,"wb");
else
$fout=fopen($filename,"ab");
// Если не смогли открыть файл на запись, то выдаем сообщение об ошибке
if (!$fout) {
syslog(LOG_INFO, "Can't open file for writing: ".$filename);
header("HTTP/1.0 500 Internal Server Error");
print "Can't open file for writing.";
return;
}
// Из stdin читаем данные отправленные методом POST - это и есть содержимое порций
$fin = fopen("php://input", "rb");
if ($fin) {
while (!feof($fin)) {
// Считаем 1Мб из stdin
$data=fread($fin, 1024*1024);
// Сохраним считанные данные в файл
fwrite($fout,$data);
}
fclose($fin);
}
fclose($fout);
}
// Все нормально, вернем HTTP 200 и тело ответа "ok"
header("HTTP/1.0 200 OK");
print "ok\n";
}
else {
// Если неверный идентификатор загрузку, то вернем HTTP 500 и сообщение об ошибке
syslog(LOG_INFO, "Uploading chunk. Wrong hash ".$hash);
header("HTTP/1.0 500 Internal Server Error");
print "Wrong session hash.";
}
// Закроем syslog лог
closelog();
index.php
Файл с HTML формой и инициализацией скрипта загрузки.
Заголовок HTML документа. Здесь, для примера, подменен идентификатор загрузки. Вы можете генерировать его на свое усмотрение.
$hash=htmlspecialchars(stripslashes($_GET["hash"]));
$hash=md5("test");
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head>
<title>video.novgorod.ru</title>
<meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
Таблица стилей для индикатора прогресса (PorgressBar)
#cnuploader_progressbar {display:none;margin-top:10px;height:16px;font-family:sans-serif;font-size:12px;padding:3px;width:300px;position:absolute;text-align:center;color:black;border:1px solid black;display:hidden;}
#cnuploader_progresscomplete {display:none;margin-top:10px;height:16px;font-family:sans-serif;font-size:12px;padding:3px;width:0;text-align:center;background-color:blue;color:white;border:1px solid transparent;display:hidden;}
</style>
Подключение класса загрузки. Антикеш на всякий случай.
Форма загрузки. Изначально она скрыта, отображается только после успешной инициализации объекта-загрузчика.
<body onload="ShowForm();">
<div>
<p>Максимальный размер файла при загрузке через браузер - <b>4Гб</b>.</p>
<form action="./" method="post" id="uploadform" onsubmit="return false;" style="display:none;">
<table cellspacing=1>
<tr><td><div id="message">Выберите файл:</div></td><td><input type="file" id="files" name="files[]" /></td></t>
</table>
<input type="submit" value="Загрузить >>" />
</form>
Индикатор прогресса (PorgressBar) и подсказки
<div id="cnuploader_progresscomplete"></div>
<p>Вы сможете добавить название и описание после того как видео будет загружено на сервер. </p>
<p>Мы принимаем видео, не нарушающее требования российского законодательства (не содержащее порнографию, ненормативную лексику, призыв к насилию и т.д.) и не содержащее рекламу.</p>
<p>Используется загрузчик HTML5</p>
</div>
<script type="text/javascript">
// Создаем объект - FileUploader. Задаем опции.
var uploader=new FileUploader( {
// Сообщение об ошибке
message_error: 'Ошибка при загрузке файла',
// ID элемента формы
form: 'uploadform',
// ID элемента <input type=file
formfiles: 'files',
// Идентификатор загрузки. В нашему случе хэш.
uploadid: '<?php print $hash;?>',
// URL скрипта загрузки (описан выше).
uploadscript: './upload.php',
// URL, куда перенаправить пользователя при успешной загрузке
redirect_success: './step2.php?hash=<?php print $hash;?>',
// URL, куда отправить пользователя при ошибке загрузки
redirect_abort: './abort.php?hash=<?php print $hash;?>',
// Размер порции. 2 Мб
portion: 1024*1024*2
});
// Если не удалось создать объект, то перенаправим пользователя на простую форму загруки.
if (!uploader) document.location='/upload/simple.php?hash=<?php print $hash;?>';
else {
// Если браузер не поддерживается, то перенаправим пользователя на простую форму загруки.
if (!uploader.CheckBrowser()) document.location='/upload/simple.php?hash=<?php print $hash;?>';
else {
// Если все нормально, то отобразим форму (по умолчанию она скрыта)
var e=document.getElementById('uploadform');
if (e) e.style.display='block';
}
}
}
</body>
</html>
Ссылки по теме:
- [1] MDN XMLHttpRequest;
- [2] MDN Blob;
- [3] MDN FileList.
Оставить комментарий
Комментарии
попробовал ajax вставить и им - не получается, видимо где-то конфликтует с текущими скриптами...
У меня была проблема с ограничением в максимально передаваемом размере файла на виртуальном хостинге, при этом требовалось, чтоб пользователь мог использовать мой модуль на Опенкарт для миграции сайта из v1.5 в v3+ без каких-либо дополнительных манипуляций с настройками php и htaccess,
для это подходи способ передачи больших файла частями.
В итоге кастомизировал этот код под свою задачу - теперь на сервер передаются файлы любого размера (в рамках разумного конечно)
Можно ли как нибудь реализовать дозагрузку, например, если случайно закрылся браузер, то файл можно загрузить с того же места..
Так же при загрузке больших файлов ближе к середине загрузки выходит ошибка "No input file specified"
if (this.file.slice) that.blob=this.file.slice(from,from+that.options['portion']);
else {
if (this.file.webkitSlice) that.blob=this.file.webkitSlice(from,from+that.options['portion']);
else {
if (this.file.mozSlice) that.blob=this.file.mozSlice(from,from+that.options['portion']);
}
}
if (this.file.slice) that.blob=this.file.slice(from,from+that.options['portion']);
здесь метод slice в качестве второго параметра передает номер байта в файле, а длину читаемого блоба
т.е. должно быть так:
if (this.file.slice) that.blob=this.file.slice(from, that.options['portion']);