Powrót do kategorii
Backend
tagi
plupload, zend framework, zf2,

Upload dużych plików w Zend Framework 2 z wykorzystaniem Plupload

Tadeusz
Tadeusz, 09/01/2015

Na pewno każdy programista PHP spotkał się z problemem uploadu dużych plików na serwer (taki 1,2 3GB). Jak zatem ominąć ograniczenia serwera max_upload czy post_max_size? Pomocny w takich przypadkach okazuje się plugin plupload z opcją dzielenia pliku na części podczas jego przesyłania. Jego zasada działania jest prosta – wysyłany plik jest najpierw porcjowany na mniejsze (akceptowalne przez serwer) paczki i dopiero po kolei wysyłany na serwer.

Przykład wdrożenia uploadu dużych plików

Pobieramy plugin pludpload ze strony producenta http://www.plupload.com/download/
Dodajemy do layotutu wymagane pliki:

 ->prependFile($this->basePath() . '/js/plupload/i18n/'.\Model\Lang::getLang().'.js') 
 ->prependFile($this->basePath() . '/js/plupload/jquery.ui.plupload/jquery.ui.plupload.min.js') 
 ->prependFile($this->basePath() . '/js/plupload/plupload.full.min.js')

Następnie wdrażamy obsługę uploadu po stronie javascriptu

initPlupload: function () {
            $('.plUploadPlugin').each(function () {
                var box = $(this);
                var type = box.attr('data-type');
                box.find('.plProgress').hide();
                var uploader = new plupload.Uploader({
                    url: $('#' + box.attr('data-start')).attr('data-url'),
                    chunk_size: '1Mb', // maksymalny rozmiar części na jakie zostanie podzielony plik
                    max_retries: 3, // Liczba prób
                    unique_names: true,
                    multi_selection: false,
                    dragdrop: true,
                    drop_element: box.attr('data-drop'),
                    browse_button: box.attr('data-button'),
                });

                uploader.setOption('filters', [
                        {title: "Image files", extensions: "jpg,jpeg,gif,png"}
                ]);
                uploader.init();
                uploader.bind('Init', function (up, params) {
                    box.find('.plProgress').html(0 + "%");
                    $('#' + box.attr('data-drop')).on('dragenter', function () {
                        $(this).addClass('drop-file-active');
                    });

                    $('#' + box.attr('data-drop')).on('dragleave drop', function () {
                        $(this).removeClass('drop-file-active');
                    });
                });

                uploader.bind('FilesAdded', function (up, files) {
                    box.find('.plClearFile, .extraHtml').hide();
                    box.find('.plProgress').show();

                    $('#' + box.attr('data-status')).removeClass('afterUpload').addClass('beforeUpload');
                    var html = '';
                    plupload.each(files, function (file) {
                        html = '<li id="' + file.id + '">' + file.name + ' (' + plupload.formatSize(file.size) + ') <b class="blue"></b></li>';
                    });
                    document.getElementById(box.attr('data-filelist')).innerHTML = html;

                    $('#' + box.attr('data-status')).removeClass('beforeUpload').addClass('afterUpload');
                    uploader.start();
                });

                uploader.bind('UploadProgress', function (up, file) {

                    box.find('.imagePlaceholder').addClass('progress').find('img').remove();
                    Loading('show', box.find('.imagePlaceholder'));

                    $('#' + box.attr('data-status')).removeClass('beforeUpload').addClass('progressUpload');

                    box.find('.plProgress').html(file.percent + "%");
                });

                uploader.bind('Error', function (up, err) {
                    document.getElementById(box.attr('data-console')).innerHTML = "\nError #" + err.code + ": " + err.message;
                });

                uploader.bind('FileUploaded', function (up, file, response) { 
                    var obj = jQuery.parseJSON(response.response);


                    if (obj.error && obj.error.code != 200) {
                        box.parent().find('.error').removeClass('hide');
                        return;
                    }
                    $('#' + box.attr('data-status')).removeClass('progressUpload').removeClass('beforeUpload').addClass('afterUpload');
                    var size = box.attr('data-size');

                        Loading('show', box.find('.imagePlaceholder'));

                        if (size && obj.urls) {
                            box.find('.imagePlaceholder').find('.img').remove();
                            var url = obj.urls[size];
                        } else {
                            var url = obj.url;
                        }
                        box.find('.imagePlaceholder').find('img').remove();

                        var img = $('<img src="' + url + '"/>');
                        box.find('.imagePlaceholder').append(img).css("width", "auto");
                        img.load(function () {
                            Loading('hide', box.find('.imagePlaceholder'));
                            box.find('.imagePlaceholder').removeClass('progress');
                        });

                        box.find('.plClearFile, .extraHtml').removeClass('hide');
                        box.find('.plClearFile, .extraHtml').show();

                    box.find('.plProgress').hide();

                });

            })
        },

Kolejnym etapem jest dzielenie pliku na części oraz zapis całego pliku na serwerze.
W kontrolerze tworzymy akcję, na którą przekierowany jest upload w pliku js.

use Tesr\ServiceManager\StaticServiceManager;

use Model\File\File;

use Model\File\FileBuilder;

use Model\File\FileQuery;

use Zend\Http\PhpEnvironment\Response;

use Zend\Http\Response\Stream;

use Zend\Mvc\MvcEvent;

use Zend\Serializer\Adapter\Json;

use Zend\View\Model\JsonModel;

use Zend\View\Model\ViewModel;

use Zend\Http\Headers;


public function pluploadAction() 
    { 
 
        $config = $this->getServiceLocator()->get('Config'); 
        header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); 
        header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); 
        header("Cache-Control: no-store, no-cache, must-revalidate"); 
        header("Cache-Control: post-check=0, pre-check=0", false); 
        header("Pragma: no-cache"); 
        // 5 minut maksymalny czas wykonania
        @set_time_limit(300); 
        $fileType = $this->getRequest()->getQuery('type', false); 

        $fileName = md5($_REQUEST["name"] . $this->getRequest()->getQuery('box_id', uniqid())); 
        $fileShortPath = substr($fileName, 0, 2) . DIRECTORY_SEPARATOR . substr($fileName, 2, 2); 
        $targetDir = $config['file']['uploadPath'] . $fileShortPath;
        $cleanupTargetDir = true; // usuwa stare pliki
        $maxFileAge = 5 * 3600; // maksymalny czas przetrzymywania plików na dysku (mnożenie w wartości zmiennej jest tylko do celów prezentacyjnych)

        $tmp = $config['file']['uploadPath']; 
        foreach (explode(DIRECTORY_SEPARATOR, $fileShortPath) as $dir) { 
            $tmp .= DIRECTORY_SEPARATOR . $dir; 
            if (!is_dir($tmp)) { 
                @mkdir($tmp, 0777, true); 
                system("chmod 777 " . $tmp); 
            } 
 
        } 
        // Pobieranie nazwy pliku
        if (isset($_REQUEST["name"])) { 
            $fileName = $_REQUEST["name"]; 
        } elseif (!empty($_FILES)) { 
            $fileName = $_FILES["file"]["name"]; 
        } else { 
            $fileName = uniqid("file_"); 
        } 
        $fileName = \Medtube\Lib\Url::slug($fileName); 
        $filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName; 
 
        // Dzielenie pliku
        $chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0; 
        $chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0; 
  
        // Usuwanie tymczasowych plików
 
        if ($cleanupTargetDir) { 
            if (!is_dir($targetDir) || !$dir = opendir($targetDir)) { 
                die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}'); 
            } 
 
            while (($file = readdir($dir)) !== false) { 
                $tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file; 
         
                if ($tmpfilePath == "{$filePath}.part") { 
                    continue; 
                } 
                // Usunięcie starych tymczasowych plików
                if (preg_match('/\.part$/', $file) && (filemtime($tmpfilePath) < time() - $maxFileAge)) { 
                    @unlink($tmpfilePath); 
                } 
            } 
            closedir($dir); 
        } 
 

        if (!$out = @fopen("{$filePath}.part", $chunks ? "ab" : "wb")) { 
            $resp = ["jsonrpc" => "2.0", "error" => ["code" => 102, "message" => "Failed to open output stream."], "id" => "id"]; 
            return new JsonModel($resp); 
        } 
 
        if (!empty($_FILES)) { 
            if ($_FILES["file"]["error"] || !is_uploaded_file($_FILES["file"]["tmp_name"])) { 
                $resp = ["jsonrpc" => "2.0", "error" => ["code" => 103, "message" => "Failed to move uploaded file."], "id" => "id"]; 
                return new JsonModel($resp); 
            } 
            if (!$in = @fopen($_FILES["file"]["tmp_name"], "rb")) { 
                $resp = ["jsonrpc" => "2.0", "error" => ["code" => 101, "message" => "Failed to open input stream."], "id" => "id"]; 
                return new JsonModel($resp); 
            } 
        } else { 
            if (!$in = @fopen("php://input", "rb")) { 
                $resp = ["jsonrpc" => "2.0", "error" => ["code" => 101, "message" => "Failed to open input stream."], "id" => "id"]; 
                return new JsonModel($resp); 
            } 
        } 
 
        while ($buff = fread($in, 4096)) { 
            fwrite($out, $buff); 
        } 
 
        @fclose($out); 
        @fclose($in);


        // Zapis pliku
        if (!$chunks || $chunk == $chunks - 1) { 
            rename("{$filePath}.part", $filePath); 
 
            if (filesize($filePath) == 0) { 
                $resp = ["jsonrpc" => "2.0", "error" => ["code" => 104, "message" => "Failed to upload."], "id" => "id"]; 
                return new JsonModel($resp); 
            } 
 
 
            try { 
                $file = new File(); 
                $builder = new FileBuilder($file); 
                $builder->buildFile($fileShortPath, $fileName); 
 
                $file = $builder->getFile(); 
            } catch (\Exception $e) { 
                $resp = ["jsonrpc" => "2.0", "error" => ["code" => 104, "message" => "Failed to convert."], "id" => "id"]; 
                return new JsonModel($resp); 
            } 
 
            try { 
              $file->getBlobImage(['maxSize' => 100]); 
            } Catch (\Exception $e) { 
              $resp = ["jsonrpc" => "2.0", "error" => ["code" => 104, "message" => "Failed to convert."], "id" => "id"]; 
              return new JsonModel($resp); 
            } 
 
            $url = $this->url()->fromRoute('image', ['id' => $file->getId(), 'size' => 150]); 
 
            return new JsonModel(["jsonrpc" => "2.0", "result" => null, "id" => $file->getId(), 'url' => $url); 
 
        } 
        return new JsonModel(["jsonrpc" => "2.0", "result" => null, "id" => "id"]); 
 
    }

Teraz możemy wgrywać na serwer nawet wielo-gigabajtowe pliki, nadzorując progress uploadu. Bardziej zaawansowanym rozwiązaniem jest jeszcze wznawianie przerwanego uploadu (np. na wypadek zerwanego połączenia). Wymaga on jednak odrobinę więcej pracy (wink)

Podobne artykuły

Sesje w Zend Framework 2

O sposobach użycia i implementacji sesji.

Wykorzystanie Redis 3 jako systemu cache’ującego w Zend Framework 2

Optymalizacja wydajności aplikacji opartych na Zend Framework 2 z wykorzystaniem Redis.

Poznajmy się
Poznajmy się
Chcesz porozmawiać o start-upach, projektach lub programowaniu?

Hello World! Sp. z o.o.
ul. Twarda 18
00 -105 Warszawa

+48 22 378 47 27
GOGOmedia
GOGOmedia
Internet Software House

Jesteśmy internetową firmą technologiczną, dostarczamy kompletne rozwiązania informatyczne z zakresu web aplikacji. Kompleksowo obsługujemy klientów z różnych sektorów biznesu w zakresie dedykowanego oprogramowania. Prowadzimy szkolenia, doradzamy, wykonujemy specjalistyczne audyty i dzielimy się zdobytą przez lata wiedzą. Dla wielu jesteśmy partnerem, który pomaga osiągać wyznaczone cele biznesowe w najbardziej optymalny sposób.

Polecamy
Polecamy
narzędzia wspierające naszą codzienną pracę
  • New Relic
  • CloudFlare
  • JIRA
  • Bamboo
  • Axure
  • Zendesk
  • Microsoft Project