diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5826402 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +composer.lock +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f60bbe0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - travis_retry composer self-update + - travis_retry composer install --prefer-source --no-interaction --dev + +script: phpunit diff --git a/assets/js/upload.js b/assets/js/upload.js new file mode 100644 index 0000000..8099861 --- /dev/null +++ b/assets/js/upload.js @@ -0,0 +1,80 @@ +function createUploader(uploader) +{ + var $uploader = $('#uploader-' + uploader); + + if (!$uploader || !$uploader.length) { + alert('Cannot find uploader'); + } + + var $filelist = $uploader.find('.filelist'), + $uploaded = $uploader.find('.uploaded'), + $uploadAction = $uploader.find('.upload-actions'), + $uploadBtn = $('#' + $uploader.data('uploadbtn')) || false, + options = $uploader.data('options') || {}, + autoStart = $uploader.data('autostart') || false; + + defaultOptions = { + init: { + PostInit: function(up) { + if (!autoStart && $uploadBtn) { + $uploadBtn.click(function() { + $uploadAction.hide(); + up.start(); + return false; + }); + } + }, + + FilesAdded: function(up, files) { + // $filelist.find('.alert-file button.close').trigger('click'); //limit uploading to 1 + // $uploaded.html(''); + // $uploadAction.hide(); + $.each(files, function(i, file){ + $filelist.append( + '
' + + '' + file.name + ' (' + plupload.formatSize(file.size) + ') ' + + '
'); + + $filelist.on('click', '#' + file.id + ' button.cancelUpload', function(){ + $uploadAction.show(); + }); + }); + up.refresh(); // Reposition Flash/Silverlight + if (autoStart) { + $uploadAction.hide(); + up.start(); + } + }, + + UploadProgress: function(up, file) { + //if(!$('#' + file.id + ' .progress').hasClass('progress-striped')){ + $('#' + file.id + ' .progress').addClass('active'); + $('#' + file.id + ' button.cancelUpload').hide(); + //} + $('#' + file.id + ' .progress .progress-bar').animate({width: file.percent + '%'}, 100, 'linear'); + }, + + Error: function(up, err) { + $filelist.append('
' + + 'Error: ' + err.code + ', Message: ' + err.message + + (err.file ? ', File: ' + err.file.name : '') + + "
" + ); + up.refresh(); // Reposition Flash/Silverlight + }, + + FileUploaded: function(up, file, info) { + var response = JSON.parse(info.response); + $('#' + file.id + ' .progress .progress-bar').animate({width: '100%'}, 100, 'linear'); + $('#' + file.id + ' .progress').removeClass('progress-striped').removeClass('active').fadeOut(); + $('#' + file.id + ' .filename').removeClass('hide').show(); + $('#' + file.id + ' button.cancelUpload').attr('data-id', response.id).show(); + } + } + }; + + $.extend(options, defaultOptions); + + var uploader = new plupload.Uploader(options); + uploader.init(); +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7b767b3 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "jenky/laravel-plupload", + "description": "", + "authors": [ + { + "name": "Linh Tran", + "email": "jenky.w0w@gmail.com" + } + ], + "require": { + "php": ">=5.4.0", + "illuminate/support": "5.0.*" + }, + "autoload": { + "psr-4": { + "Jenky\\LaravelPlupload": "src/" + } + }, + "minimum-stability": "stable" +} diff --git a/config/plupload.php b/config/plupload.php new file mode 100644 index 0000000..501bd69 --- /dev/null +++ b/config/plupload.php @@ -0,0 +1,8 @@ + storage_path('plupload'), + + 'flash_swf_url' => asset("assets/plupload/js/Moxie.swf"), + 'silverlight_xap_url' => asset("assets/plupload/js/Moxie.xap"), +]; \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..d15e6c4 --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,5 @@ +request = $request; + $this->storage = app('filesystem'); + } + + /** + * Get chuck upload path + * + * @return string + */ + public function getChunkPath() + { + $path = config('plupload.chunk_path'); + + if (!$this->storage->isDirectory($path)) + { + $this->storage->makeDirectory($path, 0777, true); + } + + return $path; + } + + /** + * Process uploaded files + * + * @param string $name + * @param closure $closure + * + * @return array + */ + public function process($name, Closure $closure) + { + $response = []; + $response['jsonrpc'] = "2.0"; + + if ($this->hasChunks()) + { + $result = $this->chunks($name, $closure); + } + else + { + $result = $this->single($name, $closure); + } + + $response['result'] = $result; + + return $response; + } + + /** + * Handle single uploaded file + * + * @param string $name + * @param closure $closure + * + * @return mixed + */ + public function single($name, Closure $closure) + { + if ($this->request->hasFile($name)) + { + return $closure($this->request->file($name)); + } + } + + /** + * Handle single uploaded file + * + * @param string $name + * @param closure $closure + * + * @return mixed + */ + public function chunks($name, Closure $closure) + { + $result = false; + + if ($this->request->hasFile($name)) + { + $file = $this->request->file($name); + + $chunk = (int) $this->request->get('chunk', false); + $chunks = (int) $this->request->get('chunks', false); + $originalName = $this->request->get('name'); + + $filePath = $this->getChunkPath() . '/' . $originalName . '.part'; + + $this->removeOldData($filePath); + $this->appendData($filePath, $file); + + if ($chunk == $chunks - 1) + { + $file = new UploadedFile($filePath, $originalName, 'blob', sizeof($filePath), UPLOAD_ERR_OK, true); + $result = $closure($file); + @unlink($filePath); + } + } + + return $result; + } + + /** + * Remove old chunks + */ + protected function removeOldData($filePath) + { + if ($this->storage->exists($filePath) && ($this->storage->lastModified($filePath) < time() - $this->maxFileAge)) + { + $this->storage->delete($filePath); + } + } + + /** + * Merge chunks + */ + protected function appendData($filePathPartial, UploadedFile $file) + { + if (!$out = @fopen($filePathPartial, "ab")) + { + throw new Exception("Failed to open output stream.", 102); + } + + if (!$in = @fopen($file->getPathname(), "rb")) + { + throw new Exception("Failed to open input stream", 101); + } + + while ($buff = fread($in, 4096)) + { + fwrite($out, $buff); + } + + @fclose($out); + @fclose($in); + } + + /** + * Check if request has chunks + * + * @return bool + */ + public function hasChunks() + { + return (bool) $this->request->get('chunks', false); + } +} \ No newline at end of file diff --git a/src/Html.php b/src/Html.php new file mode 100644 index 0000000..7ca4632 --- /dev/null +++ b/src/Html.php @@ -0,0 +1,187 @@ +id = $id; + $this->options['url'] = $url; + + $this->initDefaultOptions(); + } + + /** + * Set default uploader options + */ + protected function initDefaultOptions() + { + $options = ['flash_swf_url', 'silverlight_xap_url']; + + foreach ($options as $option) + { + $this->options[$option] = config('plupload.' . $option); + } + } + + /** + * Set default uploader buttons + * + * @param array $options + * + * @return void + */ + protected function initDefaultButtons(array $options) + { + if (!$this->pickFilesButton) + { + $this->pickFilesButton = ' + + Browse + + '; + } + + if (!$this->uploadButton) + { + $this->uploadButton = ' + + Upload + + '; + } + } + + protected function init() + { + if (!$this->data) + { + if (empty($this->options['url'])) + { + throw new Exception("Missing URL option.", 1); + } + + $options = []; + + if (empty($this->options['browse_button'])) + { + $options['browse_button'] = 'uploader-' . $this->id . '-pickfiles'; + } + + if (empty($this->options['container'])) + { + $options['container'] = 'uploader-' . $this->id . '-container'; + } + + $options = array_merge($this->options, $options); + + // csrf token + $options['multipart_params']['_token'] = csrf_token(); + + $this->initDefaultButtons($options); + + $id = $this->id; + $autoStart = $this->autoStart; + $buttons = [ + 'pickFiles' => $this->pickFilesButton, + 'upload' => $this->uploadButton + ]; + + $this->data = compact('options', 'id', 'autoStart', 'buttons'); + } + + return $this->data; + } + + /** + * Set uploader auto start + * + * @param bool $bool + * + * @return void + */ + public function setAutoStart($bool) + { + $this->autoStart = (bool) $bool; + + return $this; + } + + /** + * Set uploader options + * @see https://github.com/moxiecode/plupload/wiki/Options + * + * @param array $options + * + * @return void + */ + public function setOptions(array $options) + { + $options = array_except($options, ['url']); + $this->options = array_merge($this->options, $options); + + return $this; + } + + /** + * Set uploader pick files button + * + * @param string $button + * + * @return void + */ + public function setPickFilesButton($button) + { + $this->pickFilesButton = $button; + return $this; + } + + /** + * Set uploader upload button + * + * @param string $button + * + * @return void + */ + public function setUploadButton($button) + { + $this->uploadButton = $button; + return $this; + } + + public function render($view = 'plupload::uploader', array $extra = array()) + { + $this->init(); + + return view($view, $this->data); + } +} \ No newline at end of file diff --git a/src/Plupload.php b/src/Plupload.php new file mode 100644 index 0000000..65bdcc4 --- /dev/null +++ b/src/Plupload.php @@ -0,0 +1,47 @@ +request = $request; + } + + /** + * File upload handler + * + * @param string $name + * @param closure $closure + * + * @return void + */ + public function file($name, Closure $closure) + { + $fileHandler = new File($this->request); + + return $fileHandler->process($name, $closure); + } + + /** + * Html template handler + * + * @param string $id + * @param string $url + * + * @return void + */ + public function make($id, $url) + { + // $id = is_null($id) ? str_random() : $id; + + return new Html($id, $url); + } +} \ No newline at end of file diff --git a/src/PluploadServiceProvider.php b/src/PluploadServiceProvider.php new file mode 100644 index 0000000..a4049ea --- /dev/null +++ b/src/PluploadServiceProvider.php @@ -0,0 +1,61 @@ +mergeConfigFrom($configPath, 'plupload'); + + $this->app['plupload'] = $this->app->share(function($app) + { + return $app->make('Jenky\LaravelPlupload\Plupload', [ + 'request' => $app['request'] + ]); + }); + } + + /** + * Bootstrap the application events. + * + * @return void + */ + public function boot() + { + $configPath = __DIR__ . '/../config/plupload.php'; + $viewsPath = __DIR__.'/../views'; + + $this->loadViewsFrom($viewsPath, 'plupload'); + + $this->publishes([$configPath => config_path('plupload.php')], 'config'); + $this->publishes([ + $viewsPath => base_path('resources/views/vendor/plupload'), + ]); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return ['plupload']; + } + +} diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/views/uploader.blade.php b/views/uploader.blade.php new file mode 100644 index 0000000..7e6d2e5 --- /dev/null +++ b/views/uploader.blade.php @@ -0,0 +1,19 @@ +@if (!empty($options['url'])) +
+
+
+
+ {!! $buttons['pickFiles'] !!} + @if (!$autoStart) + {!! $buttons['upload'] !!} + @endif +
+
+
+
+@else + Missing URL option. +@endif \ No newline at end of file