среда, 22 мая 2013 г.

Fine Uploader. Асинхронно подкачиваем файлы и добавляем к элементам ИБ.

Мы тут вроде вступили недавно в 21 век. А файлы закачиваем всё также - инпутом с типом файл. Долго, не круто и вообще... 21 век же! Всякие гуглы и мейлы уже давно сделали асинхронную закачку файлов. И пока вы заполняете остальную форму - файлы спокойненько себе подкачиваются.

В интернетах гуляет множество решений, некоторые из которых мне совсем не по душе. Например с флеш-компонентами. Такое сразу отсекаем.
После некоторого времени, потраченного на поиски, выбираю решение fine-uploader. Проект более чем живой, обновляется и развивается. Хранится весь на ГитХабе, хотя и свой сайтец у проекта существует. Все ссылки в конце статьи.

UPD: Статья маленько устарела, если честно. Обращайте внимание на версию =)

Сегодня обсуждаем асинхронную подкачку файлов, решение fine-uploader и традиционно - применение к системе Битрикс: прикрепление нескольких асинхронно закаченных файлов к элементу ИБ. Разговор будет долгий и мозгодробительный, так что готовьтесь.



0. Общая логика


1. Есть клиент-форма, в которую включается jquery плагин, добавляющий свой инпут типа файл с обработчиками событий. Плагин очень гибко настраивается и имеет кучу настроек.
2. Пользователь нажимает Добавить и выбирает файл для закачки. Если файл по каким-то параметрам не подходит, то плагин об этом сообщает (первый уровень проверки).
3. По событию выбора файл асинхронно уходит к серверной части, к обработчику со стороны сервера (второй уровень проверки). На клиентской части в это время работает прогресс-бар. По завершении закачки этот самый серверный обработчик возвращает json-результат для клиентской части.
4. На клиентской форме отображается результат. На этом этапе я приписывала к форме скрытые инпуты с названием файла, чтобы потом по самбиту их обрабатывать.
5. По сабмиту обрабатываем стандартные инпуты и обрабатываем массив с именами файлов, которые нам пришли из формы.



1. Работа с jquery-плагином fine-uploader

Вообще сам плагин имеет дикое количество настроек и запутанную распределённую многоступенчатую документацию. Углубляться не буду, пройдусь по верхам и по использованным параметрам. Да и вообще внутрь не полезем, будем смотреть на части нашего сегодняшнего урока как на Чёрные Ящики.
У аплоадера есть две части - клиентская и серверная. Клиентская предоставляет функционал морды и первый уровень проверки закачиваемых файлов. Серверная часть соответственно принимает файл, обеспечивая более приближенный к реальности уровень проверки закачиваемого контента.
Рассмотрим клиентскую часть. После включения файлов jquery и самого плагина, переходим к подключению функционала аплоадера.
var uploader = $('#fine-uploader-files').fineUploader({ //ну, во-первых, надо завести div с таким id. это вы и без меня справитесь.
        request: {
            endpoint: '/new_order/jsonUploaderHandler.php', //путь к серверной части
            forceMultipart: false,
            inputName: 'qqfiles', //имя инпут поля типа file
            params: {
                action: 'files' //здесь можно указать доп. параметры, которые надо передать серверной части
            }
        },
        debug: true, //включаем дебаг в консоль браузера. 
        validation: {
            allowedExtensions:  ['jpg', 'png', 'xls', 'doc', 'docx', 'odt', 'pdf'], //разрешённые типы для первого уровня проверки
            sizeLimit: 31457280, //максимальный размер файла для первого уровня проверки
            itemLimit: 5 //максимальное количество файлов для текущего подключения плагина
        },
        dragAndDrop: {
            disableDefaultDropzone: true //отключаем дроп-зону
        },
        messages: { //русифицируем некоторые сообщения и кнопки
            typeError: "{file}: неверный тип файла. Принимаются только файлы форматов: {extensions}.",
            sizeError: "{file}: файл слишком большой. Максимальный размер: {sizeLimit}.",
            tooManyItemsError: "Вы пытаетесь закачать {netItems}-й файл. Максимальное количество: {itemLimit}."
        },
        text: {
            uploadButton: 'Прикрепить файлы',
            failUpload: 'Не закачан!'
        }
    })//обработка события
    .on('complete', function(event, id, fileName, responseJSON)
    {
        if (responseJSON.success) //если серверная часть вернула статус успешное завершение
            $('#form_order').append('');
    });
Для удобства я добавляю скрытое поле с именем только что добавленного файла. Кстати, можно не бояться, плагин следит за уникальностью имён.


2. Морда плагина

Собственно, вместо указанного элемента, получаем кнопочку, которая по нажатию вызывает диалоговое окно выбора файла. А там уже и оформляем по вкусу.


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

Собственно серверная часть представлена на нескольких языках, смотрите на хитхабе. Меня интересует php, смотрим дальше. Пример на гитхабе более чем исчерпывающий, надо только подставить свои данные =)
//защитимся-ка мы от самых умных
if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest')
{
 // Include the uploader class
 require_once 'uploads/qqFileUploader.php';

 $uploader = new qqFileUploader();

 //в зависимости от типа выставляем ограничение на закачки
 $uploader->allowedExtensions = array("jpg", "png", 'xls', 'doc', 'docx', 'odt', 'pdf');
 $uploader->sizeLimit = 31457280; // 30 Mb = 30 * 1024 * 1024
 $uploader->inputName = 'qqfiles';

 // Call handleUpload() with the name of the folder, relative to PHP's getcwd()
 $result = $uploader->handleUpload('../upload/problems_images');

 // To return a name used for uploaded file you can use the following line.
 $result['uploadName'] = $uploader->getUploadName();

 header("Content-Type: text/plain");
 echo json_encode($result);
}
Обратить внимание следует разве что на адрес, который передаётся для handleUpload. В комментарии там ясно написано, каким он должен быть. Файлы у меня сохранялись во временную папочку, все в одну кучу. При обработке сабмита формы эти файлы распределяются по нужным каталогам и из этой временной папочки удаляются.


4. Добавление элемента в ИБ

Все добавленные к форме "на лету" файлы были записаны в скрытые поля с именем files[], так что после самбита мы получаем массив с именами добавленных файлов. Прроверям пришедшие из формы данные, формируем массив файлов и делаем Add
 $arDeleteFiles = array();
 $el = new CIBlockElement;
 
 //свойства по умолчанию
 $arAdd['NAME'] = 'НОВОЕ ЗАДАНИЕ'; //имя элемента
 $arAdd['IBLOCK_ID'] = 3; //айдишка ИБ, куда добавляем элемент
 $arAdd['IBLOCK_SECTION_ID'] = false; // айдишка секции ИБ, куда добавляем элемент
 
 //дополнительные файлы
 if (isset($_REQUEST['files']) && count($_REQUEST['files'])) //в переменной files лежат имена добавленных к этой форме файлов
 {
  $counter = 0; //счётчик для множественного добавления файлов в свойство (см. код далее)
  foreach($_REQUEST['files'] as $file_name)
  {
   $file_filepath = $_SERVER["DOCUMENT_ROOT"].'/upload/problems_images/'.$file_name; //временная папка плюс имя файла
   if (file_exists($file_filepath))
   {
    $arAdd['PROPERTY_VALUES']['FILES']['n'.$counter] = CFile::MakeFileArray($file_filepath); //формируем массив, который поймёт функция добавления
    array_push($arDeleteFiles, $file_filepath); //формируем массив для удаления использованных файлов из временной директории
    $counter++;
   }
  }
 }
 //добавляем элемент или выводим ошибку
 if($PRODUCT_ID = $el->Add($arAdd))
  echo "New ID: ".$PRODUCT_ID;
 else
  echo "Error: ".$el->LAST_ERROR;
 
 //удаляем файлы из "временной" директории
 foreach($arDeleteFiles as $sFilePath)
  unlink($sFilePath); //@unlink($sFilePath);
ГитХаб: Клиентская часть
Версия, на которой писалась статья - 3.4.1
ГитХаб: Серверная часть
Сайт с примерами и всяким таким

15 комментариев:

  1. Спасибо за статью!
    Я вот так и не понял, плагин платный? При попытке скачать на сайте, мне говорят что это триал на 45 дней?
    У вас какая версия? Можете разместить плагин для скачивания?

    ОтветитьУдалить
    Ответы
    1. угу, а исходников то уже и нет... только демки...

      Удалить
    2. добавлена ссылочка на скачивание внизу статьи

      Удалить
    3. Почему же исходников нет?
      Скачал с сайта fineuploader.com, там версия 3.5.0 с такими же кодами как и в вашей 3.4.1.
      Вот только не понятно чего ожидать от этой триальной версии? Она что, перестанет работать через 45 дней? :)
      Ссылка на 3.5.0: http://bit.ly/10uJhGq

      Удалить
    4. а чёрт их знает, надо в код смотреть, что там через 45 дней будет =)
      они и сайт успели поменять, с тех пор как я статью начала писать (заготовка-то давно валялась...

      Удалить
    5. Сравнил исходные коды этих двух версий. Новые изменения по поводу триала или 45 дней не обнаружил :)

      Удалить
    6. https://github.com/Widen/fine-uploader/releases - здесь легко скачиваются версии клиента вплоть до 4.1.0, а здесь - https://github.com/Widen/fine-uploader-server/releases - версии сервера вплоть до 4.0.0.
      Лицензия GNU GPL v3 вроде не коммерческая. Или я что-то упускаю?

      Удалить
    7. статья мною писалась когда ребятки там закрыли вообще доступ к исходникам, оставив только триал.
      сейчас они повесили гну и покупку лицензию на освобождение от гну. ну чего могу сказать. круто.

      Удалить
  2. спасибо за последнюю бесплатную версию)

    ОтветитьУдалить
    Ответы
    1. пожалуйста; хорошо, что кому-то пригодилось.

      Удалить
    2. Да я уверен пригодилось многим, просто не пишет никто.

      Удалить
  3. Светлана, пожалуйста, заэскейпите $file_name перед использованием, а то сейчас можно просмотреть и удалить почти любой файл, доступный пользователю, от имени которого запущен php-скрипт.

    ОтветитьУдалить
    Ответы
    1. к счастью, мы уже некоторое время назад избавились от этого модуля =)

      Удалить
  4. Кстати, с помощью текущего примера серверной части FileUploader'a [url]https://github.com/Widen/fine-uploader-server/blob/master/php/traditional/handler.php[/url], тоже можно на сервере файлы поудалять или папок насоздавать в неожиданных местах - проследите за $_REQUEST['qqfilename'] и $_REQUEST['qquuid']:

    [code]
    public function getName(){
    if (isset($_REQUEST['qqfilename']))
    return $_REQUEST['qqfilename'];
    ...
    }
    ...
    public function handleUpload($uploadDirectory, $name = null){
    ...
    if ($name === null){
    $name = $this->getName();
    }

    ...
    $uuid = $_REQUEST['qquuid'];
    if (!file_exists($targetFolder)){
    mkdir($targetFolder);
    }
    ...
    $targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;

    $target = join(DIRECTORY_SEPARATOR, array($uploadDirectory, $uuid, $name));
    ...
    $target = fopen($target, 'wb');

    ...
    }
    [/code]

    ОтветитьУдалить