diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 764db5df..dc9dd293 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,6 +1,13 @@ tools: php_code_sniffer: true php_cs_fixer: true - php_mess_detector: true + php_mess_detector: + config: + code_size_rules: + too_many_fields: false + too_many_methods: false + + design_rules: + coupling_between_objects: false php_analyzer: true sensiolabs_security_checker: true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0acf73bd..fc9e984a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ php: - 5.4 env: - - SYMFONY_VERSION=2.1.* - SYMFONY_VERSION=2.2.* - SYMFONY_VERSION=2.3.* diff --git a/Controller/AbstractChunkedController.php b/Controller/AbstractChunkedController.php index 7f930d4f..62f9df1f 100644 --- a/Controller/AbstractChunkedController.php +++ b/Controller/AbstractChunkedController.php @@ -48,19 +48,22 @@ protected function handleChunkedUpload(UploadedFile $file, ResponseInterface $re $request = $this->container->get('request'); $chunkManager = $this->container->get('oneup_uploader.chunk_manager'); - // reset uploaded to always have a return value - $uploaded = null; - // get information about this chunked request list($last, $uuid, $index, $orig) = $this->parseChunkedRequest($request); $chunk = $chunkManager->addChunk($uuid, $index, $file, $orig); - $this->dispatchChunkEvents($chunk, $response, $request, $last); + if (null !== $chunk) { + $this->dispatchChunkEvents($chunk, $response, $request, $last); + } if ($chunkManager->getLoadDistribution()) { $chunks = $chunkManager->getChunks($uuid); $assembled = $chunkManager->assembleChunks($chunks, true, $last); + + if (null === $chunk) { + $this->dispatchChunkEvents($assembled, $response, $request, $last); + } } // if all chunks collected and stored, proceed @@ -73,9 +76,7 @@ protected function handleChunkedUpload(UploadedFile $file, ResponseInterface $re $path = $assembled->getPath(); - // create a temporary uploaded file to meet the interface restrictions - $uploadedFile = new UploadedFile($assembled->getPathname(), $assembled->getBasename(), null, $assembled->getSize(), null, true); - $uploaded = $this->handleUpload($uploadedFile, $response, $request); + $this->handleUpload($assembled, $response, $request); $chunkManager->cleanup($path); } diff --git a/Controller/AbstractController.php b/Controller/AbstractController.php index 30b05197..03e14cb2 100644 --- a/Controller/AbstractController.php +++ b/Controller/AbstractController.php @@ -2,9 +2,10 @@ namespace Oneup\UploaderBundle\Controller; +use Oneup\UploaderBundle\Uploader\File\FileInterface; +use Oneup\UploaderBundle\Uploader\File\FilesystemFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\FileBag; @@ -99,12 +100,18 @@ protected function getFiles(FileBag $bag) * * Note: The return value differs when * - * @param UploadedFile The file to upload + * @param The file to upload * @param response A response object. * @param request The request object. */ - protected function handleUpload(UploadedFile $file, ResponseInterface $response, Request $request) + protected function handleUpload($file, ResponseInterface $response, Request $request) { + // wrap the file if it is not done yet which can only happen + // if it wasn't a chunked upload, in which case it is definitely + // on the local filesystem. + if (!($file instanceof FileInterface)) { + $file = new FilesystemFile($file); + } $this->validate($file); $this->dispatchPreUploadEvent($file, $response, $request); @@ -126,7 +133,7 @@ protected function handleUpload(UploadedFile $file, ResponseInterface $response, * @param response A response object. * @param request The request object. */ - protected function dispatchPreUploadEvent(UploadedFile $uploaded, ResponseInterface $response, Request $request) + protected function dispatchPreUploadEvent(FileInterface $uploaded, ResponseInterface $response, Request $request) { $dispatcher = $this->container->get('event_dispatcher'); @@ -161,10 +168,10 @@ protected function dispatchPostEvents($uploaded, ResponseInterface $response, Re } } - protected function validate(UploadedFile $file) + protected function validate(FileInterface $file) { $dispatcher = $this->container->get('event_dispatcher'); - $event = new ValidationEvent($file, $this->config, $this->type, $this->container->get('request')); + $event = new ValidationEvent($file, $this->container->get('request'), $this->config, $this->type); $dispatcher->dispatch(UploadEvents::VALIDATION, $event); } diff --git a/Controller/BlueimpController.php b/Controller/BlueimpController.php index f10f776c..64e1b880 100644 --- a/Controller/BlueimpController.php +++ b/Controller/BlueimpController.php @@ -61,7 +61,7 @@ protected function parseChunkedRequest(Request $request) $attachmentName = rawurldecode(preg_replace('/(^[^"]+")|("$)/', '', $request->headers->get('content-disposition'))); // split the header string to the appropriate parts - list($tmp, $startByte, $endByte, $totalBytes) = preg_split('/[^0-9]+/', $headerRange); + list(, $startByte, $endByte, $totalBytes) = preg_split('/[^0-9]+/', $headerRange); // getting information about chunks // note: We don't have a chance to get the last $total @@ -73,7 +73,6 @@ protected function parseChunkedRequest(Request $request) $size = ($endByte + 1 - $startByte); $last = ($endByte + 1) == $totalBytes; $index = $last ? \PHP_INT_MAX : floor($startByte / $size); - $total = ceil($totalBytes / $size); // it is possible, that two clients send a file with the // exact same filename, therefore we have to add the session diff --git a/Controller/DropzoneController.php b/Controller/DropzoneController.php index 17d6ba95..b7b6e946 100644 --- a/Controller/DropzoneController.php +++ b/Controller/DropzoneController.php @@ -18,7 +18,7 @@ public function upload() foreach ($files as $file) { try { - $uploaded = $this->handleUpload($file, $response, $request); + $this->handleUpload($file, $response, $request); } catch (UploadException $e) { $this->errorHandler->addException($response, $e); } diff --git a/Controller/FancyUploadController.php b/Controller/FancyUploadController.php index 8901b2ed..e36979e3 100644 --- a/Controller/FancyUploadController.php +++ b/Controller/FancyUploadController.php @@ -18,7 +18,7 @@ public function upload() foreach ($files as $file) { try { - $uploaded = $this->handleUpload($file, $response, $request); + $this->handleUpload($file, $response, $request); } catch (UploadException $e) { $this->errorHandler->addException($response, $e); } diff --git a/Controller/MooUploadController.php b/Controller/MooUploadController.php index 8b6cce5e..a33ee432 100644 --- a/Controller/MooUploadController.php +++ b/Controller/MooUploadController.php @@ -17,9 +17,6 @@ class MooUploadController extends AbstractChunkedController public function upload() { $request = $this->container->get('request'); - $dispatcher = $this->container->get('event_dispatcher'); - $translator = $this->container->get('translator'); - $response = new MooUploadResponse(); $headers = $request->headers; diff --git a/Controller/UploadifyController.php b/Controller/UploadifyController.php index a98f7e8a..8b8fcf37 100644 --- a/Controller/UploadifyController.php +++ b/Controller/UploadifyController.php @@ -18,7 +18,7 @@ public function upload() foreach ($files as $file) { try { - $uploaded = $this->handleUpload($file, $response, $request); + $this->handleUpload($file, $response, $request); } catch (UploadException $e) { $this->errorHandler->addException($response, $e); } diff --git a/Controller/YUI3Controller.php b/Controller/YUI3Controller.php index 8638ad61..05d3bcb9 100644 --- a/Controller/YUI3Controller.php +++ b/Controller/YUI3Controller.php @@ -18,7 +18,7 @@ public function upload() foreach ($files as $file) { try { - $uploaded = $this->handleUpload($file, $response, $request); + $this->handleUpload($file, $response, $request); } catch (UploadException $e) { $this->errorHandler->addException($response, $e); } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index e18268a4..a9efae75 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -18,7 +18,20 @@ public function getConfigTreeBuilder() ->addDefaultsIfNotSet() ->children() ->scalarNode('maxage')->defaultValue(604800)->end() - ->scalarNode('directory')->defaultNull()->end() + ->arrayNode('storage') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values(array('filesystem', 'gaufrette')) + ->defaultValue('filesystem') + ->end() + ->scalarNode('filesystem')->defaultNull()->end() + ->scalarNode('directory')->defaultNull()->end() + ->scalarNode('stream_wrapper')->defaultNull()->end() + ->scalarNode('sync_buffer_size')->defaultValue('100K')->end() + ->scalarNode('prefix')->defaultValue('chunks')->end() + ->end() + ->end() ->booleanNode('load_distribution')->defaultTrue()->end() ->end() ->end() @@ -38,7 +51,6 @@ public function getConfigTreeBuilder() ->children() ->enumNode('frontend') ->values(array('fineuploader', 'blueimp', 'uploadify', 'yui3', 'fancyupload', 'mooupload', 'plupload', 'dropzone', 'custom')) - ->defaultValue('fineuploader') ->end() ->arrayNode('custom_frontend') ->addDefaultsIfNotSet() @@ -57,15 +69,10 @@ public function getConfigTreeBuilder() ->end() ->scalarNode('filesystem')->defaultNull()->end() ->scalarNode('directory')->defaultNull()->end() + ->scalarNode('stream_wrapper')->defaultNull()->end() ->scalarNode('sync_buffer_size')->defaultValue('100K')->end() ->end() ->end() - ->arrayNode('allowed_extensions') - ->prototype('scalar')->end() - ->end() - ->arrayNode('disallowed_extensions') - ->prototype('scalar')->end() - ->end() ->arrayNode('allowed_mimetypes') ->prototype('scalar')->end() ->end() diff --git a/DependencyInjection/OneupUploaderExtension.php b/DependencyInjection/OneupUploaderExtension.php index fae90564..f6720a6a 100644 --- a/DependencyInjection/OneupUploaderExtension.php +++ b/DependencyInjection/OneupUploaderExtension.php @@ -13,11 +13,14 @@ class OneupUploaderExtension extends Extension { protected $storageServices = array(); + protected $container; + protected $config; public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); + $this->config = $this->processConfiguration($configuration, $configs); + $this->container = $container; $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('uploader.xml'); @@ -25,133 +28,216 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('validators.xml'); $loader->load('errorhandler.xml'); - if ($config['twig']) { + if ($this->config['twig']) { $loader->load('twig.xml'); } - $config['chunks']['directory'] = is_null($config['chunks']['directory']) ? - sprintf('%s/uploader/chunks', $container->getParameter('kernel.cache_dir')) : - $this->normalizePath($config['chunks']['directory']) + $this->createChunkStorageService(); + $this->processOrphanageConfig(); + + $container->setParameter('oneup_uploader.chunks', $this->config['chunks']); + $container->setParameter('oneup_uploader.orphanage', $this->config['orphanage']); + + $controllers = array(); + + // handle mappings + foreach ($this->config['mappings'] as $key => $mapping) { + $controllers[$key] = $this->processMapping($key, $mapping); + } + + $container->setParameter('oneup_uploader.controllers', $controllers); + } + + protected function processOrphanageConfig() + { + if ($this->config['chunks']['storage']['type'] === 'filesystem') { + $defaultDir = sprintf('%s/uploader/orphanage', $this->container->getParameter('kernel.cache_dir')); + } else { + $defaultDir = 'orphanage'; + } + + $this->config['orphanage']['directory'] = is_null($this->config['orphanage']['directory']) ? $defaultDir: + $this->normalizePath($this->config['orphanage']['directory']) ; + } - $config['orphanage']['directory'] = is_null($config['orphanage']['directory']) ? - sprintf('%s/uploader/orphanage', $container->getParameter('kernel.cache_dir')) : - $this->normalizePath($config['orphanage']['directory']) + protected function processMapping($key, &$mapping) + { + $mapping['max_size'] = $mapping['max_size'] < 0 ? + $this->getMaxUploadSize($mapping['max_size']) : + $mapping['max_size'] ; + $controllerName = $this->createController($key, $mapping); - $container->setParameter('oneup_uploader.chunks', $config['chunks']); - $container->setParameter('oneup_uploader.orphanage', $config['orphanage']); + $this->verifyPhpVersion($mapping); - $controllers = array(); + return array($controllerName, array( + 'enable_progress' => $mapping['enable_progress'], + 'enable_cancelation' => $mapping['enable_cancelation'] + )); + } - // handle mappings - foreach ($config['mappings'] as $key => $mapping) { - $mapping['max_size'] = $mapping['max_size'] < 0 ? - $this->getMaxUploadSize($mapping['max_size']) : - $mapping['max_size'] - ; + protected function createController($key, $config) + { + // create the storage service according to the configuration + $storageService = $this->createStorageService($config['storage'], $key, $config['use_orphanage']); - // create the storage service according to the configuration - $storageService = null; - - // if a service is given, return a reference to this service - // this allows a user to overwrite the storage layer if needed - if (!is_null($mapping['storage']['service'])) { - $storageService = new Reference($mapping['storage']['service']); - } else { - // no service was given, so we create one - $storageName = sprintf('oneup_uploader.storage.%s', $key); - - if ($mapping['storage']['type'] == 'filesystem') { - $mapping['storage']['directory'] = is_null($mapping['storage']['directory']) ? - sprintf('%s/../web/uploads/%s', $container->getParameter('kernel.root_dir'), $key) : - $this->normalizePath($mapping['storage']['directory']) - ; - - $container - ->register($storageName, sprintf('%%oneup_uploader.storage.%s.class%%', $mapping['storage']['type'])) - ->addArgument($mapping['storage']['directory']) - ; - } - - if ($mapping['storage']['type'] == 'gaufrette') { - if(!class_exists('Gaufrette\\Filesystem')) - throw new InvalidArgumentException('You have to install Gaufrette in order to use it as a storage service.'); - - if(strlen($mapping['storage']['filesystem']) <= 0) - throw new ServiceNotFoundException('Empty service name'); - - $container - ->register($storageName, sprintf('%%oneup_uploader.storage.%s.class%%', $mapping['storage']['type'])) - ->addArgument(new Reference($mapping['storage']['filesystem'])) - ->addArgument($this->getValueInBytes($mapping['storage']['sync_buffer_size'])) - ; - } - - $storageService = new Reference($storageName); - - if ($mapping['use_orphanage']) { - $orphanageName = sprintf('oneup_uploader.orphanage.%s', $key); - - // this mapping wants to use the orphanage, so create - // a masked filesystem for the controller - $container - ->register($orphanageName, '%oneup_uploader.orphanage.class%') - ->addArgument($storageService) - ->addArgument(new Reference('session')) - ->addArgument($config['orphanage']) - ->addArgument($key) - ; - - // switch storage of mapping to orphanage - $storageService = new Reference($orphanageName); - } - } + if ($config['frontend'] != 'custom') { + $controllerName = sprintf('oneup_uploader.controller.%s', $key); + $controllerType = sprintf('%%oneup_uploader.controller.%s.class%%', $config['frontend']); + } else { + $customFrontend = $config['custom_frontend']; - if ($mapping['frontend'] != 'custom') { - $controllerName = sprintf('oneup_uploader.controller.%s', $key); - $controllerType = sprintf('%%oneup_uploader.controller.%s.class%%', $mapping['frontend']); - } else { - $customFrontend = $mapping['custom_frontend']; + $controllerName = sprintf('oneup_uploader.controller.%s', $customFrontend['name']); + $controllerType = $customFrontend['class']; - $controllerName = sprintf('oneup_uploader.controller.%s', $customFrontend['name']); - $controllerType = $customFrontend['class']; + if(empty($controllerName) || empty($controllerType)) + throw new ServiceNotFoundException('Empty controller class or name. If you really want to use a custom frontend implementation, be sure to provide a class and a name.'); + } - if(empty($controllerName) || empty($controllerType)) - throw new ServiceNotFoundException('Empty controller class or name. If you really want to use a custom frontend implementation, be sure to provide a class and a name.'); - } + $errorHandler = $this->createErrorHandler($config); + + // create controllers based on mapping + $this->container + ->register($controllerName, $controllerType) + + ->addArgument(new Reference('service_container')) + ->addArgument($storageService) + ->addArgument($errorHandler) + ->addArgument($config) + ->addArgument($key) + + ->addTag('oneup_uploader.routable', array('type' => $key)) + ->setScope('request') + ; + + return $controllerName; + } + + protected function createErrorHandler($config) + { + return is_null($config['error_handler']) ? + new Reference('oneup_uploader.error_handler.'.$config['frontend']) : + new Reference($config['error_handler']); + } - $errorHandler = is_null($mapping['error_handler']) ? - new Reference('oneup_uploader.error_handler.'.$mapping['frontend']) : - new Reference($mapping['error_handler']); + protected function verifyPhpVersion($config) + { + if ($config['enable_progress'] || $config['enable_cancelation']) { + if (strnatcmp(phpversion(), '5.4.0') < 0) { + throw new InvalidArgumentException('You need to run PHP version 5.4.0 or above to use the progress/cancelation feature.'); + } + } + } - // create controllers based on mapping - $container - ->register($controllerName, $controllerType) + protected function createChunkStorageService() + { + $config = &$this->config['chunks']['storage']; - ->addArgument(new Reference('service_container')) - ->addArgument($storageService) - ->addArgument($errorHandler) - ->addArgument($mapping) - ->addArgument($key) + $storageClass = sprintf('%%oneup_uploader.chunks_storage.%s.class%%', $config['type']); + if ($config['type'] === 'filesystem') { + $config['directory'] = is_null($config['directory']) ? + sprintf('%s/uploader/chunks', $this->container->getParameter('kernel.cache_dir')) : + $this->normalizePath($config['directory']) + ; - ->addTag('oneup_uploader.routable', array('type' => $key)) - ->setScope('request') + $this->container + ->register('oneup_uploader.chunks_storage', sprintf('%%oneup_uploader.chunks_storage.%s.class%%', $config['type'])) + ->addArgument($config['directory']) ; + } else { + $this->registerGaufretteStorage( + 'oneup_uploader.chunks_storage', + $storageClass, $config['filesystem'], + $config['sync_buffer_size'], + $config['stream_wrapper'], + $config['prefix'] + ); + + $this->container->setParameter('oneup_uploader.orphanage.class', 'Oneup\UploaderBundle\Uploader\Storage\GaufretteOrphanageStorage'); + + // enforce load distribution when using gaufrette as chunk + // torage to avoid moving files forth-and-back + $this->config['chunks']['load_distribution'] = true; + } + } - if ($mapping['enable_progress'] || $mapping['enable_cancelation']) { - if (strnatcmp(phpversion(), '5.4.0') < 0) { - throw new InvalidArgumentException('You need to run PHP version 5.4.0 or above to use the progress/cancelation feature.'); - } + protected function createStorageService(&$config, $key, $orphanage = false) + { + $storageService = null; + + // if a service is given, return a reference to this service + // this allows a user to overwrite the storage layer if needed + if (!is_null($config['service'])) { + $storageService = new Reference($config['service']); + } else { + // no service was given, so we create one + $storageName = sprintf('oneup_uploader.storage.%s', $key); + $storageClass = sprintf('%%oneup_uploader.storage.%s.class%%', $config['type']); + + if ($config['type'] == 'filesystem') { + $config['directory'] = is_null($config['directory']) ? + sprintf('%s/../web/uploads/%s', $this->container->getParameter('kernel.root_dir'), $key) : + $this->normalizePath($config['directory']) + ; + + $this->container + ->register($storageName, $storageClass) + ->addArgument($config['directory']) + ; } - $controllers[$key] = array($controllerName, array( - 'enable_progress' => $mapping['enable_progress'], - 'enable_cancelation' => $mapping['enable_cancelation'] - )); + if ($config['type'] == 'gaufrette') { + $this->registerGaufretteStorage( + $storageName, + $storageClass, + $config['filesystem'], + $config['sync_buffer_size'], + $config['stream_wrapper'] + ); + } + + $storageService = new Reference($storageName); + + if ($orphanage) { + $orphanageName = sprintf('oneup_uploader.orphanage.%s', $key); + + // this mapping wants to use the orphanage, so create + // a masked filesystem for the controller + $this->container + ->register($orphanageName, '%oneup_uploader.orphanage.class%') + ->addArgument($storageService) + ->addArgument(new Reference('session')) + ->addArgument(new Reference('oneup_uploader.chunks_storage')) + ->addArgument($this->config['orphanage']) + ->addArgument($key) + ; + + // switch storage of mapping to orphanage + $storageService = new Reference($orphanageName); + } } - $container->setParameter('oneup_uploader.controllers', $controllers); + return $storageService; + } + + protected function registerGaufretteStorage($key, $class, $filesystem, $buffer, $streamWrapper = null, $prefix = '') + { + if(!class_exists('Gaufrette\\Filesystem')) + throw new InvalidArgumentException('You have to install Gaufrette in order to use it as a chunk storage service.'); + + if(strlen($filesystem) <= 0) + throw new ServiceNotFoundException('Empty service name'); + + $streamWrapper = $this->normalizeStreamWrapper($streamWrapper); + + $this->container + ->register($key, $class) + ->addArgument(new Reference($filesystem)) + ->addArgument($this->getValueInBytes($buffer)) + ->addArgument($streamWrapper) + ->addArgument($prefix) + ; } protected function getMaxUploadSize($input) @@ -182,4 +268,13 @@ protected function normalizePath($input) { return rtrim($input, '/'); } + + protected function normalizeStreamWrapper($input) + { + if (is_null($input)) { + return null; + } + + return rtrim($input, '/') . '/'; + } } diff --git a/Event/ValidationEvent.php b/Event/ValidationEvent.php index 0fa90b0f..15e1c18a 100644 --- a/Event/ValidationEvent.php +++ b/Event/ValidationEvent.php @@ -2,8 +2,8 @@ namespace Oneup\UploaderBundle\Event; +use Oneup\UploaderBundle\Uploader\File\FileInterface; use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; class ValidationEvent extends Event @@ -13,7 +13,7 @@ class ValidationEvent extends Event protected $type; protected $request; - public function __construct(UploadedFile $file, array $config, $type, Request $request = null) + public function __construct(FileInterface $file, Request $request, array $config, $type) { $this->file = $file; $this->config = $config; diff --git a/EventListener/AllowedExtensionValidationListener.php b/EventListener/AllowedExtensionValidationListener.php deleted file mode 100644 index 68c26e85..00000000 --- a/EventListener/AllowedExtensionValidationListener.php +++ /dev/null @@ -1,21 +0,0 @@ -getConfig(); - $file = $event->getFile(); - - $extension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION); - - if (count($config['allowed_extensions']) > 0 && !in_array($extension, $config['allowed_extensions'])) { - throw new ValidationException('error.whitelist'); - } - } -} diff --git a/EventListener/AllowedMimetypeValidationListener.php b/EventListener/AllowedMimetypeValidationListener.php index 195f6bdc..66aa7b91 100644 --- a/EventListener/AllowedMimetypeValidationListener.php +++ b/EventListener/AllowedMimetypeValidationListener.php @@ -16,8 +16,7 @@ public function onValidate(ValidationEvent $event) return; } - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mimetype = finfo_file($finfo, $file->getRealpath()); + $mimetype = $file->getMimeType(); if (!in_array($mimetype, $config['allowed_mimetypes'])) { throw new ValidationException('error.whitelist'); diff --git a/EventListener/DisallowedExtensionValidationListener.php b/EventListener/DisallowedExtensionValidationListener.php deleted file mode 100644 index 9cdac58e..00000000 --- a/EventListener/DisallowedExtensionValidationListener.php +++ /dev/null @@ -1,21 +0,0 @@ -getConfig(); - $file = $event->getFile(); - - $extension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION); - - if (count($config['disallowed_extensions']) > 0 && in_array($extension, $config['disallowed_extensions'])) { - throw new ValidationException('error.blacklist'); - } - } -} diff --git a/EventListener/DisallowedMimetypeValidationListener.php b/EventListener/DisallowedMimetypeValidationListener.php index 452eff61..c5ace443 100644 --- a/EventListener/DisallowedMimetypeValidationListener.php +++ b/EventListener/DisallowedMimetypeValidationListener.php @@ -16,8 +16,7 @@ public function onValidate(ValidationEvent $event) return; } - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mimetype = finfo_file($finfo, $file->getRealpath()); + $mimetype = $file->getExtension(); if (in_array($mimetype, $config['disallowed_mimetypes'])) { throw new ValidationException('error.blacklist'); diff --git a/README.md b/README.md index a8b708b1..0abe5c17 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ The entry point of the documentation can be found in the file `Resources/docs/in Upgrade Notes ------------- +* Version **v1.0.0** introduced some backward compatibility breaks. For a full list of changes, head to the [dedicated pull request](https://github.com/1up-lab/OneupUploaderBundle/pull/57). * If you're using chunked uploads consider upgrading from **v0.9.6** to **v0.9.7**. A critical issue was reported regarding the assembly of chunks. More information in ticket [#21](https://github.com/1up-lab/OneupUploaderBundle/issues/21#issuecomment-21560320). * Error management [changed](https://github.com/1up-lab/OneupUploaderBundle/pull/25) in Version **0.9.6**. You can now register an `ErrorHandler` per configured frontend. This comes bundled with some adjustments to the `blueimp` controller. More information is available in [the documentation](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/custom_error_handler.md). * Event dispatching [changed](https://github.com/1up-lab/OneupUploaderBundle/commit/a408548b241f47af3539b2137c1817a21a51fde9) in Version **0.9.5**. The dispatching is now handled in the `upload*` functions. So if you have created your own implementation, be sure to remove the call to the `dispatchEvents` function, otherwise it will be called twice. Furthermore no `POST_UPLOAD` event will be fired anymore after uploading a chunk. You can get more information on this topic in the [documentation](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/custom_logic.md#using-chunked-uploads). diff --git a/Resources/config/uploader.xml b/Resources/config/uploader.xml index 39502abd..c9709943 100644 --- a/Resources/config/uploader.xml +++ b/Resources/config/uploader.xml @@ -5,11 +5,13 @@ Oneup\UploaderBundle\Uploader\Chunk\ChunkManager + Oneup\UploaderBundle\Uploader\Chunk\Storage\GaufretteStorage + Oneup\UploaderBundle\Uploader\Chunk\Storage\FilesystemStorage Oneup\UploaderBundle\Uploader\Naming\UniqidNamer Oneup\UploaderBundle\Routing\RouteLoader Oneup\UploaderBundle\Uploader\Storage\GaufretteStorage Oneup\UploaderBundle\Uploader\Storage\FilesystemStorage - Oneup\UploaderBundle\Uploader\Storage\OrphanageStorage + Oneup\UploaderBundle\Uploader\Storage\FilesystemOrphanageStorage Oneup\UploaderBundle\Uploader\Orphanage\OrphanageManager Oneup\UploaderBundle\Controller\FineUploaderController Oneup\UploaderBundle\Controller\BlueimpController @@ -26,6 +28,7 @@ %oneup_uploader.chunks% + diff --git a/Resources/config/validators.xml b/Resources/config/validators.xml index 5b2ffe00..4c3fc731 100644 --- a/Resources/config/validators.xml +++ b/Resources/config/validators.xml @@ -8,14 +8,6 @@ - - - - - - - - diff --git a/Resources/doc/chunked_uploads.md b/Resources/doc/chunked_uploads.md index 4f5c5e1c..fd6d87a5 100644 --- a/Resources/doc/chunked_uploads.md +++ b/Resources/doc/chunked_uploads.md @@ -31,11 +31,53 @@ You can configure the `ChunkManager` by using the following configuration parame oneup_uploader: chunks: maxage: 86400 - directory: %kernel.cache_dir%/uploader/chunks + storage: + directory: %kernel.cache_dir%/uploader/chunks ``` You can choose a custom directory to save the chunks temporarily while uploading by changing the parameter `directory`. +## Use Gaufrette to store chunk files + +You can also use a Gaufrette filesystem as the chunk storage. A possible use case is to use chunked uploads behind non-session sticky load balancers. +To do this you must first set up [Gaufrette](gaufrette_storage.md). There are however some additional things to keep in mind. +The configuration for the Gaufrette chunk storage should look as the following: + +```yaml +oneup_uploader: + chunks: + maxage: 86400 + storage: + type: gaufrette + filesystem: gaufrette.gallery_filesystem + prefix: 'chunks' + stream_wrapper: 'gaufrette://gallery/' +``` + +> :exclamation: Setting the `stream_wrapper` is heavily recommended for better performance, see the reasons in the [gaufrette configuration](gaufrette_storage.md#configure-your-mappings) + +As you can see there are is an option, `prefix`. It represents the directory + *relative* to the filesystem's directory which the chunks are stored in. +Gaufrette won't allow it to be outside of the filesystem. +This will give you a better structured directory, +as the chunk's folders and the uploaded files won't mix with each other. +You can set it to an empty string (`''`), if you don't need it. Otherwise it defaults to `chunks`. + +> :exclamation: You can only use stream capable filesystems for the chunk storage, at the time of this writing +only the Local filesystem is capable of streaming directly. + +The chunks will be read directly from the temporary directory and appended to the already existing part on the given filesystem, +resulting in only one single read and one single write operation. + +> :exclamation: Do not use a Gaufrette filesystem for the chunk storage and a local filesystem for the mapping. This is not possible to check during container setup and will throw unexpected errors at runtime! + +You can achieve the biggest improvement if you use the same filesystem as your storage. If you do so, the assembled +file only has to be moved out of the chunk directory, which takes no time on a local filesystem. + +> The load distribution is forcefully turned on, if you use Gaufrette as the chunk storage. + +See the [Use Chunked Uploads behind Load Balancers](load_balancers.md) section in the documentation for a full configuration example. + ## Clean up The ChunkManager can be forced to clean up old and orphanaged chunks by using the command provided by the OneupUploaderBundle. diff --git a/Resources/doc/configuration_reference.md b/Resources/doc/configuration_reference.md index b75fee44..dcc5a20a 100644 --- a/Resources/doc/configuration_reference.md +++ b/Resources/doc/configuration_reference.md @@ -7,7 +7,13 @@ All available configuration options along with their default values are listed b oneup_uploader: chunks: maxage: 604800 - directory: ~ + storage: + type: filesystem + directory: ~ + filesystem: ~ + sync_buffer_size: 100K + stream_wrapper: ~ + prefix: 'chunks' load_distribution: true orphanage: maxage: 604800 @@ -26,9 +32,8 @@ oneup_uploader: type: filesystem filesystem: ~ directory: ~ + stream_wrapper: ~ sync_buffer_size: 100K - allowed_extensions: [] - disallowed_extensions: [] allowed_mimetypes: [] disallowed_mimetypes: [] error_handler: oneup_uploader.error_handler.noop diff --git a/Resources/doc/custom_namer.md b/Resources/doc/custom_namer.md index 4aaeabcb..9b59e584 100644 --- a/Resources/doc/custom_namer.md +++ b/Resources/doc/custom_namer.md @@ -12,19 +12,19 @@ First, create a custom namer which implements ```Oneup\UploaderBundle\Uploader\N namespace Acme\DemoBundle; -use Symfony\Component\HttpFoundation\File\UploadedFile; +use Oneup\UploaderBundle\Uploader\File\FileInterface; use Oneup\UploaderBundle\Uploader\Naming\NamerInterface; class CatNamer implements NamerInterface { - public function name(UploadedFile $file) + public function name(FileInterface $file) { return 'grumpycat.jpg'; } } ``` -To match the `NamerInterface` you have to implement the function `name()` which expects an `UploadedFile` and should return a string representing the name of the given file. The example above would name every file _grumpycat.jpg_ and is therefore not very useful. +To match the `NamerInterface` you have to implement the function `name()` which expects an `FileInterface` and should return a string representing the name of the given file. The example above would name every file _grumpycat.jpg_ and is therefore not very useful. Next, register your created namer as a service in your `services.xml` diff --git a/Resources/doc/frontend_blueimp.md b/Resources/doc/frontend_blueimp.md index f6935c25..db30fdf7 100644 --- a/Resources/doc/frontend_blueimp.md +++ b/Resources/doc/frontend_blueimp.md @@ -41,4 +41,14 @@ security: main: pattern: ^/ anonymous: true -``` \ No newline at end of file +``` + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/frontend_dropzone.md b/Resources/doc/frontend_dropzone.md index 0be52b31..66fc3188 100644 --- a/Resources/doc/frontend_dropzone.md +++ b/Resources/doc/frontend_dropzone.md @@ -21,4 +21,14 @@ oneup_uploader: frontend: dropzone ``` -Be sure to check out the [official manual](http://www.dropzonejs.com/) for details on the configuration. \ No newline at end of file +Be sure to check out the [official manual](http://www.dropzonejs.com/) for details on the configuration. + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/frontend_fancyupload.md b/Resources/doc/frontend_fancyupload.md index 02bef21b..09896f0e 100644 --- a/Resources/doc/frontend_fancyupload.md +++ b/Resources/doc/frontend_fancyupload.md @@ -28,13 +28,13 @@ window.addEvent('domready', function() debug: true, target: 'demo-browse' }); - + $('demo-browse').addEvent('click', function() { swiffy.browse(); return false; }); - + $('demo-select-images').addEvent('change', function() { var filter = null; @@ -71,7 +71,7 @@ window.addEvent('domready', function() - +

Browse Files | @@ -106,3 +106,13 @@ oneup_uploader: ``` Be sure to check out the [official manual](http://digitarald.de/project/fancyupload/) for details on the configuration. + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/frontend_fineuploader.md b/Resources/doc/frontend_fineuploader.md index b0248573..cd8a0198 100644 --- a/Resources/doc/frontend_fineuploader.md +++ b/Resources/doc/frontend_fineuploader.md @@ -32,6 +32,14 @@ oneup_uploader: frontend: fineuploader ``` -> Actually, `fineuploader` is the default value, so you don't have to provide it manually. - Be sure to check out the [official manual](https://github.com/Widen/fine-uploader/blob/master/readme.md) for details on the configuration. + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/frontend_mooupload.md b/Resources/doc/frontend_mooupload.md index 5c318680..6a1db2c8 100644 --- a/Resources/doc/frontend_mooupload.md +++ b/Resources/doc/frontend_mooupload.md @@ -32,4 +32,14 @@ oneup_uploader: frontend: mooupload ``` -Be sure to check out the [official manual](https://github.com/juanparati/MooUpload) for details on the configuration. \ No newline at end of file +Be sure to check out the [official manual](https://github.com/juanparati/MooUpload) for details on the configuration. + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/frontend_plupload.md b/Resources/doc/frontend_plupload.md index 34c1dc54..deb1ce0c 100644 --- a/Resources/doc/frontend_plupload.md +++ b/Resources/doc/frontend_plupload.md @@ -47,4 +47,14 @@ security: main: pattern: ^/ anonymous: true -``` \ No newline at end of file +``` + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/frontend_uploadify.md b/Resources/doc/frontend_uploadify.md index 072f22ca..ec7bcc99 100644 --- a/Resources/doc/frontend_uploadify.md +++ b/Resources/doc/frontend_uploadify.md @@ -16,7 +16,7 @@ $(document).ready(function() swf: "{{ asset('bundles/acmedemo/js/uploadify.swf') }}", uploader: "{{ oneup_uploader_endpoint('gallery') }}" }); - + }); @@ -34,4 +34,14 @@ oneup_uploader: frontend: uploadify ``` -Be sure to check out the [official manual](http://www.uploadify.com/documentation/) for details on the configuration. \ No newline at end of file +Be sure to check out the [official manual](http://www.uploadify.com/documentation/) for details on the configuration. + +Next steps +---------- + +After this setup, you can move on and implement some of the more advanced features. A full list is available [here](https://github.com/1up-lab/OneupUploaderBundle/blob/master/Resources/doc/index.md#next-steps). + +* [Process uploaded files using custom logic](custom_logic.md) +* [Return custom data to frontend](response.md) +* [Include your own Namer](custom_namer.md) +* [Configuration Reference](configuration_reference.md) diff --git a/Resources/doc/gaufrette_storage.md b/Resources/doc/gaufrette_storage.md index 6c933477..2e74a3cd 100644 --- a/Resources/doc/gaufrette_storage.md +++ b/Resources/doc/gaufrette_storage.md @@ -53,7 +53,7 @@ knp_gaufrette: local: directory: %kernel.root_dir%/../web/uploads create: true - + filesystems: gallery: adapter: gallery @@ -71,7 +71,7 @@ oneup_uploader: gallery: storage: type: gaufrette - filesystem: gaufrette.gallery_filesystem + filesystem: gaufrette.gallery_filesystem ``` You can specify the buffer size used for syncing files from your filesystem to the gaufrette storage by changing the property `sync_buffer_size`. @@ -84,7 +84,31 @@ oneup_uploader: gallery: storage: type: gaufrette - filesystem: gaufrette.gallery_filesystem + filesystem: gaufrette.gallery_filesystem sync_buffer_size: 1M ``` +You may also specify the stream wrapper protocol for your filesystem: +```yml +# app/config/config.yml + +oneup_uploader: + mappings: + gallery: + storage: + type: gaufrette + filesystem: gaufrette.gallery_filesystem + stream_wrapper: gaufrette://gallery/ +``` + +> This is only useful if you are using a stream-capable adapter. At the time of this writing, only +the local adapter is capable of streaming directly. + +The first part (`gaufrette`) in the example above `MUST` be the same as `knp_gaufrette.stream_wrapper.protocol`, +the second part (`gallery`) in the example, `MUST` be the key of the filesytem (`knp_gaufette.filesystems.key`). +It also must end with a slash (`/`). + +This is particularly useful if you want to get exact informations about your files. Gaufrette offers you every functionality +to do this without relying on the stream wrapper, however it will have to download the file and load it into memory +to operate on it. If `stream_wrapper` is specified, the bundle will try to open the file as streams when such operation +is requested. (e.g. getting the size of the file, the mime-type based on content) diff --git a/Resources/doc/index.md b/Resources/doc/index.md index fe69b15d..bf97ce0b 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -34,7 +34,7 @@ Add OneupUploaderBundle to your composer.json using the following construct: ```js { "require": { - "oneup/uploader-bundle": "0.9.*@dev" + "oneup/uploader-bundle": "1.0.*@dev" } } ``` @@ -71,7 +71,8 @@ This bundle was designed to just work out of the box. The only thing you have to oneup_uploader: mappings: - gallery: ~ + gallery: + frontend: blueimp # or any uploader you use in the frontend ``` To enable the dynamic routes, add the following to your routing configuration file. @@ -124,6 +125,7 @@ some more advanced features. * [Validate your uploads](custom_validator.md) * [General/Generic Events](events.md) * [Enable Session upload progress / upload cancelation](progress.md) +* [Use Chunked Uploads behind Load Balancers](load_balancers.md) * [Configuration Reference](configuration_reference.md) * [Testing this bundle](testing.md) diff --git a/Resources/doc/load_balancers.md b/Resources/doc/load_balancers.md new file mode 100644 index 00000000..c1786007 --- /dev/null +++ b/Resources/doc/load_balancers.md @@ -0,0 +1,41 @@ +Use Chunked Uploads behind Load Balancers +========================================= + +If you want to use Chunked Uploads behind load balancers that is not configured to use sticky sessions you'll eventually end up with a bunch of chunks on every instance and the bundle is not able to reassemble the file on the server. + +You can avoid this problem by using Gaufrette as an abstract filesystem. Check the following configuration as an example. + +```yaml +knp_gaufrette: + adapters: + gallery: + local: + directory: %kernel.root_dir%/../web/uploads + create: true + + filesystems: + gallery: + adapter: gallery + + stream_wrapper: ~ + +oneup_uploader: + chunks: + storage: + type: gaufrette + filesystem: gaufrette.gallery_filesystem + stream_wrapper: gaufrette://gallery/ + + mappings: + gallery: + frontend: fineuploader + storage: + type: gaufrette + filesystem: gaufrette.gallery_filesystem +``` + +> :exclamation: Event though it is possible to use two different Gaufrette filesystems - one for the the chunk storage - and one for the mapping, it is not recommended. + +> :exclamation: Do not use a Gaufrette filesystem for the chunk storage and a local filesystem one for the mapping. This is not possible to check during configuration and will throw unexpected errors! + +Using Gaufrette filesystems for chunked upload directories has some limitations. It is highly recommended to use a `Local` Gaufrette adapter as it is the only one that is able to `rename` a file but `move` it. Especially when working with bigger files this can have serious perfomance advantages as this way the file doesn't have to be moved entirely to memory! \ No newline at end of file diff --git a/Resources/doc/orphanage.md b/Resources/doc/orphanage.md index 59478cde..b4051a23 100644 --- a/Resources/doc/orphanage.md +++ b/Resources/doc/orphanage.md @@ -67,6 +67,12 @@ oneup_uploader: You can choose a custom directory to save the orphans temporarily while uploading by changing the parameter `directory`. +If you are using a gaufrette filesystem as the chunk storage, the ```directory``` specified above should be +relative to the filesystem's root directory. It will detect if you are using a gaufrette chunk storage +and default to ```orphanage```. + +> The orphanage and the chunk storage are forced to be on the same filesystem. + ## Clean up The `OrphanageManager` can be forced to clean up orphans by using the command provided by the OneupUploaderBundle. @@ -79,4 +85,4 @@ The `Orphanage` will save uploaded files in a directory like the following: %kernel.cache_dir%/uploader/orphanage/{session_id}/uploaded_file.ext -It is currently not possible to change the part after `%kernel.cache_dir%/uploader/orphanage` dynamically. This has some implications. If a user will upload files through your `gallery` mapping, and choose not to submit the form, but instead start over with a new form handled by the `gallery` mapping, the newly uploaded files are going to be moved in the same directory. Therefore you will get both the files uploaded the first time and the second time if you trigger the `uploadFiles` method. \ No newline at end of file +It is currently not possible to change the part after `%kernel.cache_dir%/uploader/orphanage` dynamically. This has some implications. If a user will upload files through your `gallery` mapping, and choose not to submit the form, but instead start over with a new form handled by the `gallery` mapping, the newly uploaded files are going to be moved in the same directory. Therefore you will get both the files uploaded the first time and the second time if you trigger the `uploadFiles` method. diff --git a/Tests/App/config/config.yml b/Tests/App/config/config.yml index 619cebc7..a350ddb8 100644 --- a/Tests/App/config/config.yml +++ b/Tests/App/config/config.yml @@ -23,8 +23,7 @@ oneup_uploader: max_size: 256 storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] @@ -38,8 +37,7 @@ oneup_uploader: max_size: 256 storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] @@ -53,8 +51,7 @@ oneup_uploader: max_size: 256 storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] @@ -68,8 +65,7 @@ oneup_uploader: max_size: 256 storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] @@ -83,8 +79,7 @@ oneup_uploader: max_size: 256 storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] @@ -98,8 +93,7 @@ oneup_uploader: max_size: 256 storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] @@ -115,8 +109,7 @@ oneup_uploader: storage: directory: %kernel.root_dir%/cache/%kernel.environment%/upload error_handler: oneup_uploader.error_handler.blueimp - allowed_extensions: [ "ok" ] - disallowed_extensions: [ "fail" ] + allowed_mimetypes: [ "image/jpg", "text/plain" ] disallowed_mimetypes: [ "image/gif" ] diff --git a/Tests/Controller/AbstractControllerTest.php b/Tests/Controller/AbstractControllerTest.php index 8909345a..c584b7fb 100644 --- a/Tests/Controller/AbstractControllerTest.php +++ b/Tests/Controller/AbstractControllerTest.php @@ -18,7 +18,7 @@ public function setUp() $this->helper = $this->container->get('oneup_uploader.templating.uploader_helper'); $this->createdFiles = array(); - $routes = $this->container->get('router')->getRouteCollection()->all(); + $this->container->get('router')->getRouteCollection()->all(); } abstract protected function getConfigKey(); diff --git a/Tests/Controller/AbstractValidationTest.php b/Tests/Controller/AbstractValidationTest.php index 01b0a6dc..77a5bac8 100644 --- a/Tests/Controller/AbstractValidationTest.php +++ b/Tests/Controller/AbstractValidationTest.php @@ -7,8 +7,6 @@ abstract class AbstractValidationTest extends AbstractControllerTest { - abstract protected function getFileWithCorrectExtension(); - abstract protected function getFileWithIncorrectExtension(); abstract protected function getFileWithCorrectMimeType(); abstract protected function getFileWithIncorrectMimeType(); abstract protected function getOversizedFile(); @@ -27,26 +25,6 @@ public function testAgainstMaxSize() $this->assertCount(0, $this->getUploadedFiles()); } - public function testAgainstCorrectExtension() - { - // assemble a request - $client = $this->client; - $endpoint = $this->helper->endpoint($this->getConfigKey()); - - $client->request('POST', $endpoint, $this->getRequestParameters(), array($this->getFileWithCorrectExtension())); - $response = $client->getResponse(); - - $this->assertTrue($response->isSuccessful()); - $this->assertEquals($response->headers->get('Content-Type'), 'application/json'); - $this->assertCount(1, $this->getUploadedFiles()); - - foreach ($this->getUploadedFiles() as $file) { - $this->assertTrue($file->isFile()); - $this->assertTrue($file->isReadable()); - $this->assertEquals(128, $file->getSize()); - } - } - public function testEvents() { $client = $this->client; @@ -56,11 +34,11 @@ public function testEvents() // event data $validationCount = 0; - $dispatcher->addListener(UploadEvents::VALIDATION, function(ValidationEvent $event) use (&$validationCount) { + $dispatcher->addListener(UploadEvents::VALIDATION, function() use (&$validationCount) { ++ $validationCount; }); - $client->request('POST', $endpoint, $this->getRequestParameters(), array($this->getFileWithCorrectExtension())); + $client->request('POST', $endpoint, $this->getRequestParameters(), array($this->getFileWithCorrectMimeType())); $this->assertEquals(1, $validationCount); } @@ -82,25 +60,11 @@ public function testIfRequestIsAvailableInEvent() ++ $validationCount; }); - $client->request('POST', $endpoint, $this->getRequestParameters(), array($this->getFileWithCorrectExtension())); + $client->request('POST', $endpoint, $this->getRequestParameters(), array($this->getFileWithCorrectMimeType())); $this->assertEquals(1, $validationCount); } - public function testAgainstIncorrectExtension() - { - // assemble a request - $client = $this->client; - $endpoint = $this->helper->endpoint($this->getConfigKey()); - - $client->request('POST', $endpoint, $this->getRequestParameters(), array($this->getFileWithIncorrectExtension())); - $response = $client->getResponse(); - - //$this->assertTrue($response->isNotSuccessful()); - $this->assertEquals($response->headers->get('Content-Type'), 'application/json'); - $this->assertCount(0, $this->getUploadedFiles()); - } - public function testAgainstCorrectMimeType() { // assemble a request diff --git a/Tests/Controller/BlueimpValidationTest.php b/Tests/Controller/BlueimpValidationTest.php index 4bbfa1c7..2aec2f7c 100644 --- a/Tests/Controller/BlueimpValidationTest.php +++ b/Tests/Controller/BlueimpValidationTest.php @@ -4,7 +4,6 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Oneup\UploaderBundle\Tests\Controller\AbstractValidationTest; -use Oneup\UploaderBundle\Event\ValidationEvent; use Oneup\UploaderBundle\UploadEvents; class BlueimpValidationTest extends AbstractValidationTest @@ -23,26 +22,6 @@ public function testAgainstMaxSize() $this->assertCount(0, $this->getUploadedFiles()); } - public function testAgainstCorrectExtension() - { - // assemble a request - $client = $this->client; - $endpoint = $this->helper->endpoint($this->getConfigKey()); - - $client->request('POST', $endpoint, $this->getRequestParameters(), $this->getFileWithCorrectExtension(), array('HTTP_ACCEPT' => 'application/json')); - $response = $client->getResponse(); - - $this->assertTrue($response->isSuccessful()); - $this->assertEquals($response->headers->get('Content-Type'), 'application/json'); - $this->assertCount(1, $this->getUploadedFiles()); - - foreach ($this->getUploadedFiles() as $file) { - $this->assertTrue($file->isFile()); - $this->assertTrue($file->isReadable()); - $this->assertEquals(128, $file->getSize()); - } - } - public function testEvents() { $client = $this->client; @@ -52,29 +31,15 @@ public function testEvents() // event data $validationCount = 0; - $dispatcher->addListener(UploadEvents::VALIDATION, function(ValidationEvent $event) use (&$validationCount) { + $dispatcher->addListener(UploadEvents::VALIDATION, function() use (&$validationCount) { ++ $validationCount; }); - $client->request('POST', $endpoint, $this->getRequestParameters(), $this->getFileWithCorrectExtension()); + $client->request('POST', $endpoint, $this->getRequestParameters(), $this->getFileWithCorrectMimeType()); $this->assertEquals(1, $validationCount); } - public function testAgainstIncorrectExtension() - { - // assemble a request - $client = $this->client; - $endpoint = $this->helper->endpoint($this->getConfigKey()); - - $client->request('POST', $endpoint, $this->getRequestParameters(), $this->getFileWithIncorrectExtension(), array('HTTP_ACCEPT' => 'application/json')); - $response = $client->getResponse(); - - //$this->assertTrue($response->isNotSuccessful()); - $this->assertEquals($response->headers->get('Content-Type'), 'application/json'); - $this->assertCount(0, $this->getUploadedFiles()); - } - public function testAgainstCorrectMimeType() { // assemble a request @@ -131,26 +96,6 @@ protected function getOversizedFile() ))); } - protected function getFileWithCorrectExtension() - { - return array('files' => array(new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ))); - } - - protected function getFileWithIncorrectExtension() - { - return array('files' => array(new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ))); - } - protected function getFileWithCorrectMimeType() { return array('files' => array(new UploadedFile( diff --git a/Tests/Controller/DropzoneValidationTest.php b/Tests/Controller/DropzoneValidationTest.php index 73cb5ff0..abe7f061 100644 --- a/Tests/Controller/DropzoneValidationTest.php +++ b/Tests/Controller/DropzoneValidationTest.php @@ -27,26 +27,6 @@ protected function getOversizedFile() ); } - protected function getFileWithCorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ); - } - - protected function getFileWithIncorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ); - } - protected function getFileWithCorrectMimeType() { return new UploadedFile( diff --git a/Tests/Controller/FancyUploadValidationTest.php b/Tests/Controller/FancyUploadValidationTest.php index 7e489649..1f782288 100644 --- a/Tests/Controller/FancyUploadValidationTest.php +++ b/Tests/Controller/FancyUploadValidationTest.php @@ -27,26 +27,6 @@ protected function getOversizedFile() ); } - protected function getFileWithCorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ); - } - - protected function getFileWithIncorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ); - } - protected function getFileWithCorrectMimeType() { return new UploadedFile( diff --git a/Tests/Controller/FineUploaderValidationTest.php b/Tests/Controller/FineUploaderValidationTest.php index d1961e43..a3b1a7e0 100644 --- a/Tests/Controller/FineUploaderValidationTest.php +++ b/Tests/Controller/FineUploaderValidationTest.php @@ -27,26 +27,6 @@ protected function getOversizedFile() ); } - protected function getFileWithCorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ); - } - - protected function getFileWithIncorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ); - } - protected function getFileWithCorrectMimeType() { return new UploadedFile( diff --git a/Tests/Controller/PluploadValidationTest.php b/Tests/Controller/PluploadValidationTest.php index 61ffc55c..c9e9be98 100644 --- a/Tests/Controller/PluploadValidationTest.php +++ b/Tests/Controller/PluploadValidationTest.php @@ -27,26 +27,6 @@ protected function getOversizedFile() ); } - protected function getFileWithCorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ); - } - - protected function getFileWithIncorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ); - } - protected function getFileWithCorrectMimeType() { return new UploadedFile( diff --git a/Tests/Controller/UploadifyValidationTest.php b/Tests/Controller/UploadifyValidationTest.php index 0f106503..086267f0 100644 --- a/Tests/Controller/UploadifyValidationTest.php +++ b/Tests/Controller/UploadifyValidationTest.php @@ -27,26 +27,6 @@ protected function getOversizedFile() ); } - protected function getFileWithCorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ); - } - - protected function getFileWithIncorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ); - } - protected function getFileWithCorrectMimeType() { return new UploadedFile( diff --git a/Tests/Controller/YUI3ValidationTest.php b/Tests/Controller/YUI3ValidationTest.php index ca4919c4..2325c617 100644 --- a/Tests/Controller/YUI3ValidationTest.php +++ b/Tests/Controller/YUI3ValidationTest.php @@ -27,26 +27,6 @@ protected function getOversizedFile() ); } - protected function getFileWithCorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.ok', - 'text/plain', - 128 - ); - } - - protected function getFileWithIncorrectExtension() - { - return new UploadedFile( - $this->createTempFile(128), - 'cat.fail', - 'text/plain', - 128 - ); - } - protected function getFileWithCorrectMimeType() { return new UploadedFile( diff --git a/Tests/DependencyInjection/OneupUploaderExtensionTest.php b/Tests/DependencyInjection/OneupUploaderExtensionTest.php index 1b09fbf5..85ac094b 100644 --- a/Tests/DependencyInjection/OneupUploaderExtensionTest.php +++ b/Tests/DependencyInjection/OneupUploaderExtensionTest.php @@ -28,6 +28,28 @@ public function testValueToByteTransformer() $this->assertEquals(2147483648, $method->invoke($mock, '2G')); } + public function testNormalizationOfStreamWrapper() + { + $mock = $this->getMockBuilder('Oneup\UploaderBundle\DependencyInjection\OneupUploaderExtension') + ->disableOriginalConstructor() + ->getMock() + ; + + $method = new \ReflectionMethod( + 'Oneup\UploaderBundle\DependencyInjection\OneupUploaderExtension', + 'normalizeStreamWrapper' + ); + $method->setAccessible(true); + + $output1 = $method->invoke($mock, 'gaufrette://gallery'); + $output2 = $method->invoke($mock, 'gaufrette://gallery/'); + $output3 = $method->invoke($mock, null); + + $this->assertEquals('gaufrette://gallery/', $output1); + $this->assertEquals('gaufrette://gallery/', $output2); + $this->assertNull($output3); + } + public function testGetMaxUploadSize() { $mock = $this->getMockBuilder('Oneup\UploaderBundle\DependencyInjection\OneupUploaderExtension') diff --git a/Tests/Uploader/Chunk/ChunkManagerTest.php b/Tests/Uploader/Chunk/Storage/ChunkStorageTest.php similarity index 63% rename from Tests/Uploader/Chunk/ChunkManagerTest.php rename to Tests/Uploader/Chunk/Storage/ChunkStorageTest.php index 4ce9c4aa..af1f32fc 100644 --- a/Tests/Uploader/Chunk/ChunkManagerTest.php +++ b/Tests/Uploader/Chunk/Storage/ChunkStorageTest.php @@ -1,32 +1,14 @@ mkdir($tmpDir); - - $this->tmpDir = $tmpDir; - } - - public function tearDown() - { - $system = new Filesystem(); - $system->remove($this->tmpDir); - } + protected $storage; public function testExistanceOfTmpDir() { @@ -49,16 +31,15 @@ public function testChunkCleanup() { // get a manager configured with a max-age of 5 minutes $maxage = 5 * 60; - $manager = $this->getManager($maxage); $numberOfFiles = 10; $finder = new Finder(); $finder->in($this->tmpDir); $this->fillDirectory($numberOfFiles); - $this->assertCount(10, $finder); + $this->assertCount($numberOfFiles, $finder); - $manager->clear(); + $this->storage->clear($maxage); $this->assertTrue(is_dir($this->tmpDir)); $this->assertTrue(is_writeable($this->tmpDir)); @@ -75,21 +56,12 @@ public function testClearIfDirectoryDoesNotExist() $filesystem = new Filesystem(); $filesystem->remove($this->tmpDir); - $manager = $this->getManager(10); - $manager->clear(); + $this->storage->clear(10); // yey, no exception $this->assertTrue(true); } - protected function getManager($maxage) - { - return new ChunkManager(array( - 'directory' => $this->tmpDir, - 'maxage' => $maxage - )); - } - protected function fillDirectory($number) { $system = new Filesystem(); diff --git a/Tests/Uploader/Chunk/Storage/FilesystemStorageTest.php b/Tests/Uploader/Chunk/Storage/FilesystemStorageTest.php new file mode 100644 index 00000000..8e54cc4d --- /dev/null +++ b/Tests/Uploader/Chunk/Storage/FilesystemStorageTest.php @@ -0,0 +1,31 @@ +mkdir($tmpDir); + + $this->tmpDir = $tmpDir; + $this->storage = new FilesystemStorage(array( + 'directory' => $this->tmpDir + )); + } + + public function tearDown() + { + $system = new Filesystem(); + $system->remove($this->tmpDir); + } +} diff --git a/Tests/Uploader/Chunk/Storage/GaufretteStorageTest.php b/Tests/Uploader/Chunk/Storage/GaufretteStorageTest.php new file mode 100644 index 00000000..375d4505 --- /dev/null +++ b/Tests/Uploader/Chunk/Storage/GaufretteStorageTest.php @@ -0,0 +1,43 @@ +mkdir($parentDir); + + $this->parentDir = $parentDir; + + $adapter = new Adapter($this->parentDir, true); + + $filesystem = new GaufretteFilesystem($adapter); + + $this->storage = new GaufretteStorage($filesystem, 100000, null, $this->chunkKey); + $this->tmpDir = $this->parentDir.'/'.$this->chunkKey; + + $system->mkdir($this->tmpDir); + + } + + public function tearDown() + { + $system = new Filesystem(); + $system->remove($this->parentDir); + } + +} diff --git a/Tests/Uploader/File/FileTest.php b/Tests/Uploader/File/FileTest.php new file mode 100644 index 00000000..2753089c --- /dev/null +++ b/Tests/Uploader/File/FileTest.php @@ -0,0 +1,43 @@ +assertEquals($this->pathname, $this->file->getPathname()); + } + + public function testGetPath() + { + $this->assertEquals($this->path, $this->file->getPath()); + } + + public function testGetBasename() + { + $this->assertEquals($this->basename, $this->file->getBasename()); + } + + public function testGetExtension() + { + $this->assertEquals($this->extension, $this->file->getExtension()); + } + + public function testGetSize() + { + $this->assertEquals($this->size, $this->file->getSize()); + } + + public function testGetMimeType() + { + $this->assertEquals($this->mimeType, $this->file->getMimeType()); + } +} diff --git a/Tests/Uploader/File/FilesystemFileTest.php b/Tests/Uploader/File/FilesystemFileTest.php new file mode 100644 index 00000000..0868cb91 --- /dev/null +++ b/Tests/Uploader/File/FilesystemFileTest.php @@ -0,0 +1,30 @@ +path = sys_get_temp_dir(). '/oneup_test_tmp'; + mkdir($this->path); + + $this->basename = 'test_file.txt'; + $this->pathname = $this->path .'/'. $this->basename; + $this->extension = 'txt'; + $this->size = 9; //something = 9 bytes + $this->mimeType = 'text/plain'; + + file_put_contents($this->pathname, 'something'); + + $this->file = new FilesystemFile(new UploadedFile($this->pathname, 'test_file.txt', null, null, null, true)); + } + + public function tearDown() + { + unlink($this->pathname); + rmdir($this->path); + } +} diff --git a/Tests/Uploader/File/GaufretteFileTest.php b/Tests/Uploader/File/GaufretteFileTest.php new file mode 100644 index 00000000..fc7e2f7b --- /dev/null +++ b/Tests/Uploader/File/GaufretteFileTest.php @@ -0,0 +1,44 @@ +set('oneup', $filesystem); + + StreamWrapper::register(); + + $this->storage = new GaufretteStorage($filesystem, 100000); + + $this->path = 'oneup_test_tmp'; + mkdir(sys_get_temp_dir().'/'.$this->path); + + $this->basename = 'test_file.txt'; + $this->pathname = $this->path .'/'. $this->basename; + $this->extension = 'txt'; + $this->size = 9; //something = 9 bytes + $this->mimeType = 'text/plain'; + + file_put_contents(sys_get_temp_dir() .'/' . $this->pathname, 'something'); + + $this->file = new GaufretteFile(new File($this->pathname, $filesystem), $filesystem, 'gaufrette://oneup/'); + } + + public function tearDown() + { + unlink(sys_get_temp_dir().'/'.$this->pathname); + rmdir(sys_get_temp_dir().'/'.$this->path); + } +} diff --git a/Tests/Uploader/Naming/UniqidNamerTest.php b/Tests/Uploader/Naming/UniqidNamerTest.php index 2ff1631a..d20df0ac 100644 --- a/Tests/Uploader/Naming/UniqidNamerTest.php +++ b/Tests/Uploader/Naming/UniqidNamerTest.php @@ -8,14 +8,14 @@ class UniqidNamerTest extends \PHPUnit_Framework_TestCase { public function testNamerReturnsName() { - $file = $this->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile') + $file = $this->getMockBuilder('Oneup\UploaderBundle\Uploader\File\FilesystemFile') ->disableOriginalConstructor() ->getMock() ; $file ->expects($this->any()) - ->method('guessExtension') + ->method('getExtension') ->will($this->returnValue('jpeg')) ; @@ -25,14 +25,14 @@ public function testNamerReturnsName() public function testNamerReturnsUniqueName() { - $file = $this->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile') + $file = $this->getMockBuilder('Oneup\UploaderBundle\Uploader\File\FilesystemFile') ->disableOriginalConstructor() ->getMock() ; $file ->expects($this->any()) - ->method('guessExtension') + ->method('getExtension') ->will($this->returnValue('jpeg')) ; diff --git a/Tests/Uploader/Storage/FilesystemOrphanageStorageTest.php b/Tests/Uploader/Storage/FilesystemOrphanageStorageTest.php new file mode 100644 index 00000000..d6478483 --- /dev/null +++ b/Tests/Uploader/Storage/FilesystemOrphanageStorageTest.php @@ -0,0 +1,52 @@ +numberOfPayloads = 5; + $this->tempDirectory = sys_get_temp_dir() . '/orphanage'; + $this->realDirectory = sys_get_temp_dir() . '/storage'; + $this->payloads = array(); + + $filesystem = new Filesystem(); + $filesystem->mkdir($this->tempDirectory); + $filesystem->mkdir($this->realDirectory); + + for ($i = 0; $i < $this->numberOfPayloads; $i ++) { + // create temporary file + $file = tempnam(sys_get_temp_dir(), 'uploader'); + + $pointer = fopen($file, 'w+'); + fwrite($pointer, str_repeat('A', 1024), 1024); + fclose($pointer); + + $this->payloads[] = new FilesystemFile(new UploadedFile($file, $i . 'grumpycat.jpeg', null, null, null, true)); + } + + // create underlying storage + $this->storage = new FilesystemStorage($this->realDirectory); + // is ignored anyways + $chunkStorage = new FilesystemChunkStorage('/tmp/'); + + // create orphanage + $session = new Session(new MockArraySessionStorage()); + $session->start(); + + $config = array('directory' => $this->tempDirectory); + + $this->orphanage = new FilesystemOrphanageStorage($this->storage, $session, $chunkStorage, $config, 'cat'); + } +} diff --git a/Tests/Uploader/Storage/FilesystemStorageTest.php b/Tests/Uploader/Storage/FilesystemStorageTest.php index 6f3e0114..1ae9964d 100644 --- a/Tests/Uploader/Storage/FilesystemStorageTest.php +++ b/Tests/Uploader/Storage/FilesystemStorageTest.php @@ -2,6 +2,7 @@ namespace Oneup\UploaderBundle\Tests\Uploader\Storage; +use Oneup\UploaderBundle\Uploader\File\FilesystemFile; use Symfony\Component\Finder\Finder; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -26,7 +27,7 @@ public function setUp() public function testUpload() { - $payload = new UploadedFile($this->file, 'grumpycat.jpeg', null, null, null, true); + $payload = new FilesystemFile(new UploadedFile($this->file, 'grumpycat.jpeg', null, null, null, true)); $storage = new FilesystemStorage($this->directory); $storage->upload($payload, 'notsogrumpyanymore.jpeg'); diff --git a/Tests/Uploader/Storage/GaufretteOrphanageStorageTest.php b/Tests/Uploader/Storage/GaufretteOrphanageStorageTest.php new file mode 100644 index 00000000..418298cb --- /dev/null +++ b/Tests/Uploader/Storage/GaufretteOrphanageStorageTest.php @@ -0,0 +1,118 @@ +numberOfPayloads = 5; + $this->realDirectory = sys_get_temp_dir() . '/storage'; + $this->chunkDirectory = $this->realDirectory .'/' . $this->chunksKey; + $this->tempDirectory = $this->realDirectory . '/' . $this->orphanageKey; + $this->payloads = array(); + + if (!$this->checkIfTempnameMatchesAfterCreation()) { + $this->markTestSkipped('Temporary directories do not match'); + } + + $filesystem = new \Symfony\Component\Filesystem\Filesystem(); + $filesystem->mkdir($this->realDirectory); + $filesystem->mkdir($this->chunkDirectory); + $filesystem->mkdir($this->tempDirectory); + + $adapter = new Adapter($this->realDirectory, true); + $filesystem = new GaufretteFilesystem($adapter); + + $this->storage = new GaufretteStorage($filesystem, 100000); + + $chunkStorage = new GaufretteChunkStorage($filesystem, 100000, null, 'chunks'); + + // create orphanage + $session = new Session(new MockArraySessionStorage()); + $session->start(); + + $config = array('directory' => 'orphanage'); + + $this->orphanage = new GaufretteOrphanageStorage($this->storage, $session, $chunkStorage, $config, 'cat'); + + for ($i = 0; $i < $this->numberOfPayloads; $i ++) { + // create temporary file as if it was reassembled by the chunk manager + $file = tempnam($this->chunkDirectory, 'uploader'); + + $pointer = fopen($file, 'w+'); + fwrite($pointer, str_repeat('A', 1024), 1024); + fclose($pointer); + + //gaufrette needs the key relative to it's root + $fileKey = str_replace($this->realDirectory, '', $file); + + $this->payloads[] = new GaufretteFile(new File($fileKey, $filesystem), $filesystem); + } + } + + public function testUpload() + { + for ($i = 0; $i < $this->numberOfPayloads; $i ++) { + $this->orphanage->upload($this->payloads[$i], $i . 'notsogrumpyanymore.jpeg'); + } + + $finder = new Finder(); + $finder->in($this->tempDirectory)->files(); + $this->assertCount($this->numberOfPayloads, $finder); + + $finder = new Finder(); + // exclude the orphanage and the chunks + $finder->in($this->realDirectory)->exclude(array($this->orphanageKey, $this->chunksKey))->files(); + $this->assertCount(0, $finder); + } + + public function testUploadAndFetching() + { + for ($i = 0; $i < $this->numberOfPayloads; $i ++) { + $this->orphanage->upload($this->payloads[$i], $i . 'notsogrumpyanymore.jpeg'); + } + + $finder = new Finder(); + $finder->in($this->tempDirectory)->files(); + $this->assertCount($this->numberOfPayloads, $finder); + + $finder = new Finder(); + $finder->in($this->realDirectory)->exclude(array($this->orphanageKey, $this->chunksKey))->files(); + $this->assertCount(0, $finder); + + $files = $this->orphanage->uploadFiles(); + + $this->assertTrue(is_array($files)); + $this->assertCount($this->numberOfPayloads, $files); + + $finder = new Finder(); + $finder->in($this->tempDirectory)->files(); + $this->assertCount(0, $finder); + + $finder = new Finder(); + $finder->in($this->realDirectory)->files(); + $this->assertCount($this->numberOfPayloads, $finder); + } + + public function checkIfTempnameMatchesAfterCreation() + { + return strpos(tempnam($this->chunkDirectory, 'uploader'), $this->chunkDirectory) === 0; + } +} diff --git a/Tests/Uploader/Storage/GaufretteStorageTest.php b/Tests/Uploader/Storage/GaufretteStorageTest.php index f9ecfe68..008e6180 100644 --- a/Tests/Uploader/Storage/GaufretteStorageTest.php +++ b/Tests/Uploader/Storage/GaufretteStorageTest.php @@ -2,6 +2,7 @@ namespace Oneup\UploaderBundle\Tests\Uploader\Storage; +use Oneup\UploaderBundle\Uploader\File\FilesystemFile; use Symfony\Component\Finder\Finder; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -33,7 +34,7 @@ public function setUp() public function testUpload() { - $payload = new UploadedFile($this->file, 'grumpycat.jpeg', null, null, null, true); + $payload = new FilesystemFile(new UploadedFile($this->file, 'grumpycat.jpeg', null, null, null, true)); $this->storage->upload($payload, 'notsogrumpyanymore.jpeg'); $finder = new Finder(); diff --git a/Tests/Uploader/Storage/OrphanageStorageTest.php b/Tests/Uploader/Storage/OrphanageTest.php similarity index 61% rename from Tests/Uploader/Storage/OrphanageStorageTest.php rename to Tests/Uploader/Storage/OrphanageTest.php index bcaf5251..836ab860 100644 --- a/Tests/Uploader/Storage/OrphanageStorageTest.php +++ b/Tests/Uploader/Storage/OrphanageTest.php @@ -4,14 +4,8 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; -use Oneup\UploaderBundle\Uploader\Storage\OrphanageStorage; -use Oneup\UploaderBundle\Uploader\Storage\FilesystemStorage; - -class OrphanageStorageTest extends \PHPUnit_Framework_TestCase +abstract class OrphanageTest extends \PHPUnit_Framework_Testcase { protected $tempDirectory; protected $realDirectory; @@ -20,40 +14,6 @@ class OrphanageStorageTest extends \PHPUnit_Framework_TestCase protected $payloads; protected $numberOfPayloads; - public function setUp() - { - $this->numberOfPayloads = 5; - $this->tempDirectory = sys_get_temp_dir() . '/orphanage'; - $this->realDirectory = sys_get_temp_dir() . '/storage'; - $this->payloads = array(); - - $filesystem = new Filesystem(); - $filesystem->mkdir($this->tempDirectory); - $filesystem->mkdir($this->realDirectory); - - for ($i = 0; $i < $this->numberOfPayloads; $i ++) { - // create temporary file - $file = tempnam(sys_get_temp_dir(), 'uploader'); - - $pointer = fopen($file, 'w+'); - fwrite($pointer, str_repeat('A', 1024), 1024); - fclose($pointer); - - $this->payloads[] = new UploadedFile($file, $i . 'grumpycat.jpeg', null, null, null, true); - } - - // create underlying storage - $this->storage = new FilesystemStorage($this->realDirectory); - - // create orphanage - $session = new Session(new MockArraySessionStorage()); - $session->start(); - - $config = array('directory' => $this->tempDirectory); - - $this->orphanage = new OrphanageStorage($this->storage, $session, $config, 'cat'); - } - public function testUpload() { for ($i = 0; $i < $this->numberOfPayloads; $i ++) { diff --git a/Uploader/Chunk/ChunkManager.php b/Uploader/Chunk/ChunkManager.php index 4f7733ba..49050c0b 100644 --- a/Uploader/Chunk/ChunkManager.php +++ b/Uploader/Chunk/ChunkManager.php @@ -2,111 +2,45 @@ namespace Oneup\UploaderBundle\Uploader\Chunk; -use Symfony\Component\HttpFoundation\File\File; +use Oneup\UploaderBundle\Uploader\Chunk\Storage\ChunkStorageInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\Finder\Finder; -use Symfony\Component\Filesystem\Filesystem; use Oneup\UploaderBundle\Uploader\Chunk\ChunkManagerInterface; class ChunkManager implements ChunkManagerInterface { - public function __construct($configuration) + protected $configuration; + protected $storage; + + public function __construct($configuration, ChunkStorageInterface $storage) { $this->configuration = $configuration; + $this->storage = $storage; } public function clear() { - $system = new Filesystem(); - $finder = new Finder(); - - try { - $finder->in($this->configuration['directory'])->date('<=' . -1 * (int) $this->configuration['maxage'] . 'seconds')->files(); - } catch (\InvalidArgumentException $e) { - // the finder will throw an exception of type InvalidArgumentException - // if the directory he should search in does not exist - // in that case we don't have anything to clean - return; - } - - foreach ($finder as $file) { - $system->remove($file); - } + $this->storage->clear($this->configuration['maxage']); } public function addChunk($uuid, $index, UploadedFile $chunk, $original) { - $filesystem = new Filesystem(); - $path = sprintf('%s/%s', $this->configuration['directory'], $uuid); - $name = sprintf('%s_%s', $index, $original); - - // create directory if it does not yet exist - if(!$filesystem->exists($path)) - $filesystem->mkdir(sprintf('%s/%s', $this->configuration['directory'], $uuid)); - - return $chunk->move($path, $name); + return $this->storage->addChunk($uuid, $index, $chunk, $original); } - public function assembleChunks(\IteratorAggregate $chunks, $removeChunk = true, $renameChunk = false) + public function assembleChunks($chunks, $removeChunk = true, $renameChunk = false) { - $iterator = $chunks->getIterator()->getInnerIterator(); - - $base = $iterator->current(); - $iterator->next(); - - while ($iterator->valid()) { - - $file = $iterator->current(); - - if (false === file_put_contents($base->getPathname(), file_get_contents($file->getPathname()), \FILE_APPEND | \LOCK_EX)) { - throw new \RuntimeException('Reassembling chunks failed.'); - } - - if ($removeChunk) { - $filesystem = new Filesystem(); - $filesystem->remove($file->getPathname()); - } - - $iterator->next(); - } - - $name = $base->getBasename(); - - if ($renameChunk) { - $name = preg_replace('/^(\d+)_/', '', $base->getBasename()); - } - - // remove the prefix added by self::addChunk - $assembled = new File($base->getRealPath()); - $assembled = $assembled->move($base->getPath(), $name); - - return $assembled; + return $this->storage->assembleChunks($chunks, $removeChunk, $renameChunk); } public function cleanup($path) { - // cleanup - $filesystem = new Filesystem(); - $filesystem->remove($path); - - return true; + return $this->storage->cleanup($path); } public function getChunks($uuid) { - $finder = new Finder(); - $finder - ->in(sprintf('%s/%s', $this->configuration['directory'], $uuid))->files()->sort(function(\SplFileInfo $a, \SplFileInfo $b) { - $t = explode('_', $a->getBasename()); - $s = explode('_', $b->getBasename()); - $t = (int) $t[0]; - $s = (int) $s[0]; - - return $s < $t; - }); - - return $finder; + return $this->storage->getChunks($uuid); } public function getLoadDistribution() diff --git a/Uploader/Chunk/ChunkManagerInterface.php b/Uploader/Chunk/ChunkManagerInterface.php index 0991a3e7..c89d9bfe 100644 --- a/Uploader/Chunk/ChunkManagerInterface.php +++ b/Uploader/Chunk/ChunkManagerInterface.php @@ -21,13 +21,13 @@ public function addChunk($uuid, $index, UploadedFile $chunk, $original); /** * Assembles the given chunks and return the resulting file. * - * @param \IteratorAggregate $chunks - * @param bool $removeChunk Remove the chunk file once its assembled. - * @param bool $renameChunk Rename the chunk file once its assembled. + * @param $chunks + * @param bool $removeChunk Remove the chunk file once its assembled. + * @param bool $renameChunk Rename the chunk file once its assembled. * * @return File */ - public function assembleChunks(\IteratorAggregate $chunks, $removeChunk = true, $renameChunk = false); + public function assembleChunks($chunks, $removeChunk = true, $renameChunk = false); /** * Get chunks associated with the given uuid. diff --git a/Uploader/Chunk/Storage/ChunkStorageInterface.php b/Uploader/Chunk/Storage/ChunkStorageInterface.php new file mode 100644 index 00000000..2be1146a --- /dev/null +++ b/Uploader/Chunk/Storage/ChunkStorageInterface.php @@ -0,0 +1,18 @@ +directory = $directory; + } + + public function clear($maxAge) + { + $system = new Filesystem(); + $finder = new Finder(); + + try { + $finder->in($this->directory)->date('<=' . -1 * (int) $maxAge . 'seconds')->files(); + } catch (\InvalidArgumentException $e) { + // the finder will throw an exception of type InvalidArgumentException + // if the directory he should search in does not exist + // in that case we don't have anything to clean + return; + } + + foreach ($finder as $file) { + $system->remove($file); + } + } + + public function addChunk($uuid, $index, UploadedFile $chunk, $original) + { + $filesystem = new Filesystem(); + $path = sprintf('%s/%s', $this->directory, $uuid); + $name = sprintf('%s_%s', $index, $original); + + // create directory if it does not yet exist + if(!$filesystem->exists($path)) + $filesystem->mkdir(sprintf('%s/%s', $this->directory, $uuid)); + + return $chunk->move($path, $name); + } + + public function assembleChunks($chunks, $removeChunk, $renameChunk) + { + if (!($chunks instanceof \IteratorAggregate)) { + throw new \InvalidArgumentException('The first argument must implement \IteratorAggregate interface.'); + } + + $iterator = $chunks->getIterator()->getInnerIterator(); + + $base = $iterator->current(); + $iterator->next(); + + while ($iterator->valid()) { + + $file = $iterator->current(); + + if (false === file_put_contents($base->getPathname(), file_get_contents($file->getPathname()), \FILE_APPEND | \LOCK_EX)) { + throw new \RuntimeException('Reassembling chunks failed.'); + } + + if ($removeChunk) { + $filesystem = new Filesystem(); + $filesystem->remove($file->getPathname()); + } + + $iterator->next(); + } + + $name = $base->getBasename(); + + if ($renameChunk) { + // remove the prefix added by self::addChunk + $name = preg_replace('/^(\d+)_/', '', $base->getBasename()); + } + + $assembled = new File($base->getRealPath()); + $assembled = $assembled->move($base->getPath(), $name); + + // the file is only renamed before it is uploaded + if ($renameChunk) { + // create an file to meet interface restrictions + $assembled = new FilesystemFile(new UploadedFile($assembled->getPathname(), $assembled->getBasename(), null, $assembled->getSize(), null, true)); + } + + return $assembled; + } + + public function cleanup($path) + { + // cleanup + $filesystem = new Filesystem(); + $filesystem->remove($path); + + return true; + } + + public function getChunks($uuid) + { + $finder = new Finder(); + $finder + ->in(sprintf('%s/%s', $this->directory, $uuid))->files()->sort(function(\SplFileInfo $a, \SplFileInfo $b) { + $t = explode('_', $a->getBasename()); + $s = explode('_', $b->getBasename()); + $t = (int) $t[0]; + $s = (int) $s[0]; + + return $s < $t; + }); + + return $finder; + } +} diff --git a/Uploader/Chunk/Storage/GaufretteStorage.php b/Uploader/Chunk/Storage/GaufretteStorage.php new file mode 100644 index 00000000..5c641d00 --- /dev/null +++ b/Uploader/Chunk/Storage/GaufretteStorage.php @@ -0,0 +1,159 @@ +getAdapter() instanceof StreamFactory)) { + throw new \InvalidArgumentException('The filesystem used as chunk storage must implement StreamFactory'); + } + $this->filesystem = $filesystem; + $this->bufferSize = $bufferSize; + $this->prefix = $prefix; + $this->streamWrapperPrefix = $streamWrapperPrefix; + } + + /** + * Clears files and folders older than $maxAge in $prefix + * $prefix must be passable so it can clean the orphanage too + * as it is forced to be the same filesystem. + * + * @param $maxAge + * @param null $prefix + */ + public function clear($maxAge, $prefix = null) + { + $prefix = $prefix ? :$this->prefix; + $matches = $this->filesystem->listKeys($prefix); + + $now = time(); + $toDelete = array(); + + // Collect the directories that are old, + // this also means the files inside are old + // but after the files are deleted the dirs + // would remain + foreach ($matches['dirs'] as $key) { + if ($maxAge <= $now-$this->filesystem->mtime($key)) { + $toDelete[] = $key; + } + } + // The same directory is returned for every file it contains + array_unique($toDelete); + foreach ($matches['keys'] as $key) { + if ($maxAge <= $now-$this->filesystem->mtime($key)) { + $this->filesystem->delete($key); + } + } + + foreach ($toDelete as $key) { + // The filesystem will throw exceptions if + // a directory is not empty + try { + $this->filesystem->delete($key); + } catch (\Exception $e) { + continue; + } + } + } + + /** + * Only saves the information about the chunk to avoid moving it + * forth-and-back to reassemble it. Load distribution is enforced + * for gaufrette based chunk storage therefore assembleChunks will + * be called in the same request. + * + * @param $uuid + * @param $index + * @param UploadedFile $chunk + * @param $original + */ + public function addChunk($uuid, $index, UploadedFile $chunk, $original) + { + $this->unhandledChunk = array( + 'uuid' => $uuid, + 'index' => $index, + 'chunk' => $chunk, + 'original' => $original + ); + } + + public function assembleChunks($chunks, $removeChunk, $renameChunk) + { + // the index is only added to be in sync with the filesystem storage + $path = $this->prefix.'/'.$this->unhandledChunk['uuid'].'/'; + $filename = $this->unhandledChunk['index'].'_'.$this->unhandledChunk['original']; + + if (empty($chunks)) { + $target = $filename; + $this->ensureRemotePathExists($path.$target); + } else { + /* + * The array only contains items with matching prefix until the filename + * therefore the order will be decided depending on the filename + * It is only case-insensitive to be overly-careful. + */ + sort($chunks, SORT_STRING | SORT_FLAG_CASE); + $target = pathinfo($chunks[0], PATHINFO_BASENAME); + } + + $dst = $this->filesystem->createStream($path.$target); + if ($this->unhandledChunk['index'] === 0) { + // if it's the first chunk overwrite the already existing part + // to avoid appending to earlier failed uploads + $this->openStream($dst, 'w'); + } else { + $this->openStream($dst, 'a'); + } + + + // Meet the interface requirements + $uploadedFile = new FilesystemFile($this->unhandledChunk['chunk']); + + $this->stream($uploadedFile, $dst); + + if ($renameChunk) { + $name = preg_replace('/^(\d+)_/', '', $target); + $this->filesystem->rename($path.$target, $path.$name); + $target = $name; + } + $uploaded = $this->filesystem->get($path.$target); + + if (!$renameChunk) { + return $uploaded; + } + + return new GaufretteFile($uploaded, $this->filesystem, $this->streamWrapperPrefix); + } + + public function cleanup($path) + { + $this->filesystem->delete($path); + } + + public function getChunks($uuid) + { + $results = $this->filesystem->listKeys($this->prefix.'/'.$uuid); + + return $results['keys']; + } + + public function getFilesystem() + { + return $this->filesystem; + } +} diff --git a/Uploader/File/FileInterface.php b/Uploader/File/FileInterface.php new file mode 100644 index 00000000..511169d7 --- /dev/null +++ b/Uploader/File/FileInterface.php @@ -0,0 +1,57 @@ +getPathname(), $file->getClientOriginalName(), $file->getClientMimeType(), $file->getClientSize(), $file->getError(), true); + } else { + parent::__construct($file->getPathname(), $file->getBasename(), $file->getMimeType(), $file->getSize(), 0, true); + } + + } + + public function getExtension() + { + return $this->getClientOriginalExtension(); + } +} diff --git a/Uploader/File/GaufretteFile.php b/Uploader/File/GaufretteFile.php new file mode 100644 index 00000000..0b6904b7 --- /dev/null +++ b/Uploader/File/GaufretteFile.php @@ -0,0 +1,104 @@ +getKey(), $filesystem); + $this->filesystem = $filesystem; + $this->streamWrapperPrefix = $streamWrapperPrefix; + } + + /** + * Returns the size of the file + * + * !! WARNING !! + * Calling this loads the entire file into memory, + * unless it is on a stream-capable filesystem. + * In case of bigger files this could throw exceptions, + * and will have heavy performance footprint. + * !! ------- !! + * + */ + public function getSize() + { + // This can only work on streamable files, so basically local files, + // still only perform it once even on local files to avoid bothering the filesystem.php g + if ($this->filesystem->getAdapter() instanceof StreamFactory && !$this->size) { + if ($this->streamWrapperPrefix) { + try { + $this->setSize(filesize($this->streamWrapperPrefix.$this->getKey())); + } catch (\Exception $e) { + // Fail gracefully if there was a problem with opening the file and + // let gaufrette load the file into memory allowing it to throw exceptions + // if that doesn't work either. + // Not empty to make the scrutiziner happy. + return parent::getSize(); + } + } + } + + return parent::getSize(); + } + + public function getPathname() + { + return $this->getKey(); + } + + public function getPath() + { + return pathinfo($this->getKey(), PATHINFO_DIRNAME); + } + + public function getBasename() + { + return pathinfo($this->getKey(), PATHINFO_BASENAME); + } + + /** + * @return string + */ + public function getMimeType() + { + // This can only work on streamable files, so basically local files, + // still only perform it once even on local files to avoid bothering the filesystem. + if ($this->filesystem->getAdapter() instanceof StreamFactory && !$this->mimeType) { + if ($this->streamWrapperPrefix) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $this->mimeType = finfo_file($finfo, $this->streamWrapperPrefix.$this->getKey()); + finfo_close($finfo); + } + } + + return $this->mimeType; + } + + /** + * Now that we may be able to get the mime-type the extension + * COULD be guessed based on that, but it would be even less + * accurate as mime-types can have multiple extensions + * + * @return mixed + */ + public function getExtension() + { + return pathinfo($this->getKey(), PATHINFO_EXTENSION); + } + + public function getFilesystem() + { + return $this->filesystem; + } + +} diff --git a/Uploader/Gaufrette/StreamManager.php b/Uploader/Gaufrette/StreamManager.php new file mode 100644 index 00000000..58868b88 --- /dev/null +++ b/Uploader/Gaufrette/StreamManager.php @@ -0,0 +1,59 @@ +createStream(); + } + + return new LocalStream($file->getPathname()); + } + + protected function ensureRemotePathExists($path) + { + // this is a somehow ugly workaround introduced + // because the stream-mode is not able to create + // subdirectories. + if(!$this->filesystem->has($path)) + $this->filesystem->write($path, '', true); + } + + protected function openStream(Stream $stream, $mode) + { + // always use binary mode + $mode = $mode.'b+'; + + return $stream->open(new StreamMode($mode)); + } + + protected function stream(FileInterface $file, Stream $dst) + { + $src = $this->createSourceStream($file); + + // always use reading only for the source + $this->openStream($src, 'r'); + + while (!$src->eof()) { + $data = $src->read($this->bufferSize); + $dst->write($data); + } + + $dst->close(); + $src->close(); + } + +} diff --git a/Uploader/Naming/NamerInterface.php b/Uploader/Naming/NamerInterface.php index d44b8177..fabd0178 100644 --- a/Uploader/Naming/NamerInterface.php +++ b/Uploader/Naming/NamerInterface.php @@ -2,15 +2,15 @@ namespace Oneup\UploaderBundle\Uploader\Naming; -use Symfony\Component\HttpFoundation\File\UploadedFile; +use Oneup\UploaderBundle\Uploader\File\FileInterface; interface NamerInterface { /** * Name a given file and return the name * - * @param UploadedFile $file + * @param FileInterface $file * @return string */ - public function name(UploadedFile $file); + public function name(FileInterface $file); } diff --git a/Uploader/Naming/UniqidNamer.php b/Uploader/Naming/UniqidNamer.php index 4c16b98f..34a71413 100644 --- a/Uploader/Naming/UniqidNamer.php +++ b/Uploader/Naming/UniqidNamer.php @@ -2,13 +2,12 @@ namespace Oneup\UploaderBundle\Uploader\Naming; -use Symfony\Component\HttpFoundation\File\UploadedFile; -use Oneup\UploaderBundle\Uploader\Naming\NamerInterface; +use Oneup\UploaderBundle\Uploader\File\FileInterface; class UniqidNamer implements NamerInterface { - public function name(UploadedFile $file) + public function name(FileInterface $file) { - return sprintf('%s.%s', uniqid(), $file->guessExtension()); + return sprintf('%s.%s', uniqid(), $file->getExtension()); } } diff --git a/Uploader/Orphanage/OrphanageManager.php b/Uploader/Orphanage/OrphanageManager.php index b09dd25c..6903db76 100644 --- a/Uploader/Orphanage/OrphanageManager.php +++ b/Uploader/Orphanage/OrphanageManager.php @@ -24,6 +24,14 @@ public function get($key) public function clear() { + // Really ugly solution to clearing the orphanage on gaufrette + $class = $this->container->getParameter('oneup_uploader.orphanage.class'); + if ($class === 'Oneup\UploaderBundle\Uploader\Storage\GaufretteOrphanageStorage') { + $chunkStorage = $this->container->get('oneup_uploader.chunks_storage '); + $chunkStorage->clear($this->config['maxage'], $this->config['directory']); + + return; + } $system = new Filesystem(); $finder = new Finder(); diff --git a/Uploader/Storage/OrphanageStorage.php b/Uploader/Storage/FilesystemOrphanageStorage.php similarity index 71% rename from Uploader/Storage/OrphanageStorage.php rename to Uploader/Storage/FilesystemOrphanageStorage.php index d0787d0e..e0026cca 100644 --- a/Uploader/Storage/OrphanageStorage.php +++ b/Uploader/Storage/FilesystemOrphanageStorage.php @@ -2,33 +2,36 @@ namespace Oneup\UploaderBundle\Uploader\Storage; +use Oneup\UploaderBundle\Uploader\Chunk\Storage\ChunkStorageInterface; +use Oneup\UploaderBundle\Uploader\File\FileInterface; +use Oneup\UploaderBundle\Uploader\File\FilesystemFile; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Finder\Finder; -use Symfony\Component\Filesystem\Filesystem; use Oneup\UploaderBundle\Uploader\Storage\FilesystemStorage; use Oneup\UploaderBundle\Uploader\Storage\StorageInterface; use Oneup\UploaderBundle\Uploader\Storage\OrphanageStorageInterface; -class OrphanageStorage extends FilesystemStorage implements OrphanageStorageInterface +class FilesystemOrphanageStorage extends FilesystemStorage implements OrphanageStorageInterface { protected $storage; protected $session; protected $config; protected $type; - public function __construct(StorageInterface $storage, SessionInterface $session, $config, $type) + public function __construct(StorageInterface $storage, SessionInterface $session, ChunkStorageInterface $chunkStorage, $config, $type) { parent::__construct($config['directory']); + // We can just ignore the chunkstorage here, it's not needed to access the files $this->storage = $storage; $this->session = $session; $this->config = $config; $this->type = $type; } - public function upload(File $file, $name, $path = null) + public function upload(FileInterface $file, $name, $path = null) { if(!$this->session->isStarted()) throw new \RuntimeException('You need a running session in order to run the Orphanage.'); @@ -38,14 +41,12 @@ public function upload(File $file, $name, $path = null) public function uploadFiles() { - $filesystem = new Filesystem(); - try { $files = $this->getFiles(); $return = array(); foreach ($files as $file) { - $return[] = $this->storage->upload(new File($file->getPathname()), str_replace($this->getFindPath(), '', $file)); + $return[] = $this->storage->upload(new FilesystemFile(new File($file->getPathname())), str_replace($this->getFindPath(), '', $file)); } return $return; diff --git a/Uploader/Storage/FilesystemStorage.php b/Uploader/Storage/FilesystemStorage.php index ed7931ec..ce626311 100644 --- a/Uploader/Storage/FilesystemStorage.php +++ b/Uploader/Storage/FilesystemStorage.php @@ -2,10 +2,7 @@ namespace Oneup\UploaderBundle\Uploader\Storage; -use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpFoundation\File\File; - -use Oneup\UploaderBundle\Uploader\Storage\StorageInterface; +use Oneup\UploaderBundle\Uploader\File\FileInterface; class FilesystemStorage implements StorageInterface { @@ -16,10 +13,8 @@ public function __construct($directory) $this->directory = $directory; } - public function upload(File $file, $name, $path = null) + public function upload(FileInterface $file, $name, $path = null) { - $filesystem = new Filesystem(); - $path = is_null($path) ? $name : sprintf('%s/%s', $path, $name); $path = sprintf('%s/%s', $this->directory, $path); diff --git a/Uploader/Storage/GaufretteOrphanageStorage.php b/Uploader/Storage/GaufretteOrphanageStorage.php new file mode 100644 index 00000000..ea5379fb --- /dev/null +++ b/Uploader/Storage/GaufretteOrphanageStorage.php @@ -0,0 +1,90 @@ +getFilesystem(), $chunkStorage->bufferSize, null, null); + + $this->storage = $storage; + $this->chunkStorage = $chunkStorage; + $this->session = $session; + $this->config = $config; + $this->type = $type; + } + + public function upload(FileInterface $file, $name, $path = null) + { + if(!$this->session->isStarted()) + throw new \RuntimeException('You need a running session in order to run the Orphanage.'); + + return parent::upload($file, $name, $this->getPath()); + } + + public function uploadFiles() + { + try { + $files = $this->getFiles(); + $return = array(); + + foreach ($files as $key => $file) { + try { + $return[] = $this->storage->upload($file, str_replace($this->getPath(), '', $key)); + } catch (\Exception $e) { + // well, we tried. + continue; + } + } + + return $return; + } catch (\Exception $e) { + return array(); + } + } + + public function getFiles() + { + $keys = $this->chunkStorage->getFilesystem()->listKeys($this->getPath()); + $keys = $keys['keys']; + $files = array(); + + foreach ($keys as $key) { + // gotta pass the filesystem to both as you can't get it out from one.. + $files[$key] = new GaufretteFile(new File($key, $this->chunkStorage->getFilesystem()), $this->chunkStorage->getFilesystem()); + } + + return $files; + } + + protected function getPath() + { + // the storage is initiated in the root of the filesystem, from where the orphanage directory + // should be relative. + return sprintf('%s/%s/%s', $this->config['directory'], $this->session->getId(), $this->type); + } + +} diff --git a/Uploader/Storage/GaufretteStorage.php b/Uploader/Storage/GaufretteStorage.php index 2fbd3491..7b9bc7e3 100644 --- a/Uploader/Storage/GaufretteStorage.php +++ b/Uploader/Storage/GaufretteStorage.php @@ -2,26 +2,25 @@ namespace Oneup\UploaderBundle\Uploader\Storage; -use Symfony\Component\HttpFoundation\File\File; -use Gaufrette\Stream\Local as LocalStream; -use Gaufrette\StreamMode; +use Oneup\UploaderBundle\Uploader\File\FileInterface; +use Oneup\UploaderBundle\Uploader\File\GaufretteFile; use Gaufrette\Filesystem; +use Symfony\Component\Filesystem\Filesystem as LocalFilesystem; use Gaufrette\Adapter\MetadataSupporter; +use Oneup\UploaderBundle\Uploader\Gaufrette\StreamManager; -use Oneup\UploaderBundle\Uploader\Storage\StorageInterface; - -class GaufretteStorage implements StorageInterface +class GaufretteStorage extends StreamManager implements StorageInterface { - protected $filesystem; - protected $bufferSize; + protected $streamWrapperPrefix; - public function __construct(Filesystem $filesystem, $bufferSize) + public function __construct(Filesystem $filesystem, $bufferSize, $streamWrapperPrefix = null) { $this->filesystem = $filesystem; $this->bufferSize = $bufferSize; + $this->streamWrapperPrefix = $streamWrapperPrefix; } - public function upload(File $file, $name, $path = null) + public function upload(FileInterface $file, $name, $path = null) { $path = is_null($path) ? $name : sprintf('%s/%s', $path, $name); @@ -29,26 +28,28 @@ public function upload(File $file, $name, $path = null) $this->filesystem->getAdapter()->setMetadata($name, array('contentType' => $file->getMimeType())); } - $src = new LocalStream($file->getPathname()); - $dst = $this->filesystem->createStream($path); + if ($file instanceof GaufretteFile) { + if ($file->getFilesystem() == $this->filesystem) { + $file->getFilesystem()->rename($file->getKey(), $path); - // this is a somehow ugly workaround introduced - // because the stream-mode is not able to create - // subdirectories. - if(!$this->filesystem->has($path)) - $this->filesystem->write($path, '', true); + return new GaufretteFile($this->filesystem->get($path), $this->filesystem, $this->streamWrapperPrefix); + } + } - $src->open(new StreamMode('rb+')); - $dst->open(new StreamMode('wb+')); + $this->ensureRemotePathExists($path); + $dst = $this->filesystem->createStream($path); - while (!$src->eof()) { - $data = $src->read($this->bufferSize); - $written = $dst->write($data); - } + $this->openStream($dst, 'w'); + $this->stream($file, $dst); - $dst->close(); - $src->close(); + if ($file instanceof GaufretteFile) { + $file->delete(); + } else { + $filesystem = new LocalFilesystem(); + $filesystem->remove($file->getPathname()); + } - return $this->filesystem->get($path); + return new GaufretteFile($this->filesystem->get($path), $this->filesystem, $this->streamWrapperPrefix); } + } diff --git a/Uploader/Storage/StorageInterface.php b/Uploader/Storage/StorageInterface.php index 79fb3ae9..31665d83 100644 --- a/Uploader/Storage/StorageInterface.php +++ b/Uploader/Storage/StorageInterface.php @@ -2,16 +2,16 @@ namespace Oneup\UploaderBundle\Uploader\Storage; -use Symfony\Component\HttpFoundation\File\File; +use Oneup\UploaderBundle\Uploader\File\FileInterface; interface StorageInterface { /** * Uploads a File instance to the configured storage. * - * @param File $file + * @param $file * @param string $name * @param string $path */ - public function upload(File $file, $name, $path = null); + public function upload(FileInterface $file, $name, $path = null); } diff --git a/composer.json b/composer.json index b4a4d153..a6be9f89 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ ], "require": { - "symfony/framework-bundle": "2.*", + "symfony/framework-bundle": ">=2.2", "symfony/finder": ">=2.2.0" },