Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Последние темы форума

Показать новые сообщения »
реклама
nerohelp.info

Почтовая рассылка

Подписчиков: 11784
Последний выпуск: 19.06.2015

HTML5 Ajax upload очень больших файлов

HTML5 Ajax upload очень больших файлов

Автор: Mikhail Krivyy
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 код работает на стороне клиента (в браузере), считывает файл по частям и отправляет его на сервер.

Код:
// Для начала определим метод XMLHttpRequest.sendAsBinary(),
// если он не определен (Например, для браузера 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 документа. Здесь, для примера, подменен идентификатор загрузки. Вы можете генерировать его на свое усмотрение.

Код:
<?php
$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)

Код:
<style type="text/css">
#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>

Подключение класса загрузки. Антикеш на всякий случай.

Код:
<script type="text/javascript" src="./fileuploader.js?nc=<?php print time();?>"></script>

Форма загрузки. Изначально она скрыта, отображается только после успешной инициализации объекта-загрузчика.

Код:
</head>
<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="Загрузить &gt;&gt;" />

</form>

Индикатор прогресса (PorgressBar) и подсказки

Код:
<div id="cnuploader_progressbar"></div>
<div id="cnuploader_progresscomplete"></div>

<p>Вы сможете добавить название и описание после того как видео будет загружено на сервер. </p>

<p>Мы принимаем видео, не нарушающее требования российского законодательства (не содержащее порнографию, ненормативную лексику, призыв к насилию и т.д.) и не содержащее рекламу.</p>

<p>Используется загрузчик HTML5</p>

</div>

<script type="text/javascript">
Код:
function ShowForm() {

    // Создаем объект - 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';

            }
        }
    }
Код:
</script>

</body>
</html>

Ссылки по теме:

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 

Комментарии

1.
94K
29 октября 2014 года
Eduard Pilipenko
0 / / 29.10.2014
+3 / -0
Мне нравитсяМне не нравится
29 октября 2014, 04:05:39
Приветствую!
Можно ли как нибудь реализовать дозагрузку, например, если случайно закрылся браузер, то файл можно загрузить с того же места..
Так же при загрузке больших файлов ближе к середине загрузки выходит ошибка "No input file specified"
2.
87K
11 декабря 2012 года
Rostislav Tymkiv
0 / / 11.12.2012
+7 / -1
Мне нравитсяМне не нравится
11 декабря 2012, 15:52:42
А как сделать так что имя файла на сервере совпадало с именем файла на компе?
3.
80K
03 февраля 2012 года
FreeOwl
0 / / 03.02.2012
+4 / -0
Мне нравитсяМне не нравится
3 февраля 2012, 15:25:10
Код:
// Считаем порцию в объект 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']);

                }

            }
4.
71K
24 апреля 2011 года
ZX-Virus
0 / / 24.04.2011
+5 / -2
Мне нравитсяМне не нравится
25 сентября 2011, 20:52:21
ошибка в коде
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']);
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог