CodeIgniter → Загрузка нескольких файлов одновременно

Стала задача найти скрипт для загрузки нескольких файлов одновременно и интегрировать это решение с фреймворком CodeIgniter 2.

Требования:

  • выбор и загрузка нескольких файлов одновременно;
  • возможность перетаскивания (drag-and-drop) файлов;
  • возможность отменить загрузку в любой момент;
  • независимость от внешних фреймворков/библиотек;
  • работа без использования flash (!);
  • работоспособность под https;
  • поддержка клавиатуры;
  • прогресс-бар загрузки;
  • кроссбраузерность.

Скриптов для загрузки в сети оказалась масса, но большая часть не отвечала перечисленным выше требованиям. Выбор пал на: http://valums.com/ajax-upload/

На этом же сайте можно найти демо-версию а также краткую инструкцию по настроке.

Демо 

Я же остановлюсь на том как интегрировать этот загрузчить с CodeIgniter 2. Что признаться потребовало небольших усилий.

Этап 1. Скачиваем с официального сайта архив, далее копируем clientfileuploader.js в директорию со скриптами, clientfileuploader.css в директорию с CSS, clientupload_loading.gif в директорию с изображениями соответственно.

Подключаем файл стилей и файл со скриптом в представлении upload_view

...
<link href="style.css" rel="/css/fileuploader.css" type="text/css" />
... 
<div id="file-uploader">
<noscript>
<p>Please enable JavaScript to use file uploader.</p>        
<!-- or put a simple form for upload here -->
</noscript>
</div>
<script src="/js/fileuploader.js" type="text/javascript"></script>
<script>            
function createUploader() {
         var uploader = new qq.FileUploader({
             element: document.getElementById('file-uploader'),
             action: '/manuals/do_upload',
             multiple: true,
             debug: true
         });
}        
// in your app create uploader as soon as the DOM is ready    
// don't wait for the window to load      
window.onload = createUploader;     
</script>
...

Где:

  • element — блок в в который будет помещен код загрузчика;
  • action — обработчик;
  • multiple — выбор нескольких файлов (true/false);
  • debug — режим отладки (trur/false).

Этап 2. Правка контроллера:

public function upload() 
    {
        $data = array();
        $this->load->view('upload_view', $data);        
    }

    /**
    * Обработчик загрузки файлов 
    * 
    */
    public function do_upload() 
    {
        // Список поддерживаемых расширений, ex. array("jpeg", "xml", "bmp")
        $allowedExtensions = array("jpg", "png", "gif");
        // Максимально допустимый размер файла, в байтах
        $sizeLimit = 3 * 1024 * 1024;

        // библиотека в которой будет храниться код обработчика
        $this->load->library("qqfileuploader", array($allowedExtensions, $sizeLimit));
        // результат работы 'success' => 'true' при успешной загрузке
        $result = $this->qqfileuploader->handleUpload('upload/');

        echo htmlspecialchars(json_encode($result), ENT_NOQUOTES);
    }

Этап 3. Код самой библиотеки qqfileuploader которую мы подключаем в контроллере:

/**
 * Handle file uploads via XMLHttpRequest
 */
class qqUploadedFileXhr {

    /**
     * Save the file to the specified path
     * @return boolean TRUE on success
     */
    public function save($path) 
    {    
        // Читаем из потока во временный файл,
        // проверяем размер файла, если все норм
        // записываем в файл $path и возвращаем true

        $input = fopen("php://input", "r");
        $temp = tmpfile();
        $realSize = stream_copy_to_stream($input, $temp);
        fclose($input);

        if ($realSize != $this->getSize()){            
            return false;
        }

        $target = fopen($path, "w");        
        fseek($temp, 0, SEEK_SET);
        stream_copy_to_stream($temp, $target);
        fclose($target);

        return true;
    }

    function getName() {
        return $_GET['qqfile'];
    }

    function getSize() {
        if (isset($_SERVER["CONTENT_LENGTH"])){
            return (int)$_SERVER["CONTENT_LENGTH"];            
        } else {
            throw new Exception('Getting content length is not supported.');
        }      
    }   
}

/**
 * Handle file uploads via regular form post (uses the $_FILES array)
 */
class qqUploadedFileForm {  
    /**
     * Save the file to the specified path
     * @return boolean TRUE on success
     */
    function save($path) {
        if(!move_uploaded_file($_FILES['qqfile']['tmp_name'], $path)){
            return false;
        }
        return true;
    }

    function getName() {
        return $_FILES['qqfile']['name'];
    }

    function getSize() {
        return $_FILES['qqfile']['size'];
    }
}

class Qqfileuploader 
{
    private $allowedExtensions = array();
    private $sizeLimit = 10485760;
    private $file;

    function __construct(array $params = array())
    {     
        $allowedExtensions = $params[0];
        $sizeLimit = $params[1];

        $allowedExtensions = array_map("strtolower", $allowedExtensions);

        $this->allowedExtensions = $allowedExtensions;        
        $this->sizeLimit = $sizeLimit;

        // проверка post_max_size и upload_max_filesize со значением $sizeLimit
        $this->checkServerSettings();       

        if (isset($_GET['qqfile'])) {
            $this->file = new qqUploadedFileXhr();
        } elseif (isset($_FILES['qqfile'])) {
            $this->file = new qqUploadedFileForm();
        } else {
            echo "FALSE!";
            $this->file = false; 
        }
    }

    private function checkServerSettings(){        
        $postSize = $this->toBytes(ini_get('post_max_size'));
        $uploadSize = $this->toBytes(ini_get('upload_max_filesize'));        

        if ($postSize < $this->sizeLimit || $uploadSize < $this->sizeLimit){
            $size = max(1, $this->sizeLimit / 1024 / 1024) . 'M';             
            die("{'error':'increase post_max_size and upload_max_filesize to $size'}");    
        }        
    }

    private function toBytes($str){
        $val = trim($str);
        $last = strtolower($str[strlen($str)-1]);
        switch($last) {
            case 'g': $val *= 1024;
            case 'm': $val *= 1024;
            case 'k': $val *= 1024;        
        }
        return $val;
    }

    /**
     * Returns array('success'=>true) or array('error'=>'error message')
     */
    function handleUpload($uploadDirectory, $replaceOldFile = FALSE)
    {                                
        if (!is_writable($uploadDirectory)){
            return array('error' => "Ошибка сервера. Каталог для загрузки не доступен для записи.");
        }

        if (!$this->file){
            return array('error' => 'Ошибка, файл не был загружен. ');
        }

        $size = $this->file->getSize();

        if ($size == 0) {
            return array('error' => 'Файл пуст');
        }

        if ($size > $this->sizeLimit) {
            return array('error' => 'Файл слишком велик');
        }

        $pathinfo = pathinfo($this->file->getName());
        //$filename = $pathinfo['filename'];  // сохранить имя файла, 
        $filename = md5(uniqid());            // присвоить уникальное имя
        $ext = $pathinfo['extension'];

        // проверка на тип файлов по расширению
        if($this->allowedExtensions && !in_array(strtolower($ext), $this->allowedExtensions)){
            $these = implode(', ', $this->allowedExtensions);
            return array('error' => 'Файл имеет неверное расширение, оно должно быть: '. $these . '.');
        }

        if(!$replaceOldFile){
            /// don't overwrite previous files that were uploaded
            while (file_exists($uploadDirectory . $filename . '.' . $ext)) {
                $filename .= rand(10, 99);
            }
        }

        if ($this->file->save($uploadDirectory . $filename . '.' . $ext)){
            return array('success' => true);
        } else {
            return array('error'=> 'Could not save uploaded file.' .
                'The upload was cancelled, or server error encountered');
        }

    }    
}

Если возникнут проблемы с передачей параметров в скрипт может потребоваться правка самого скрипта, вот небольшая инструкция.