]> Raphaël G. Git Repositories - packbundle/commitdiff
Version 0.5.7 master 0.5.7
authorRaphaël Gertz <git@rapsys.eu>
Tue, 14 Oct 2025 15:02:54 +0000 (17:02 +0200)
committerRaphaël Gertz <git@rapsys.eu>
Tue, 14 Oct 2025 15:02:54 +0000 (17:02 +0200)
13 files changed:
Controller.php
DependencyInjection/Configuration.php
Form/CaptchaType.php
Form/ContactType.php [new file with mode: 0644]
Package/PathPackage.php
Parser/TokenParser.php
README.md
RapsysPackBundle.php
Util/FileUtil.php [new file with mode: 0644]
Util/ImageUtil.php
Util/IntlUtil.php
config/packages/rapsyspack.yaml
config/routes/rapsyspack.yaml

index 77a3e14e5a10d1c4d1e2c64e9c89f062e96766a1..7728f2e4410327d74bd97cdb1537d7e88dd57deb 100644 (file)
@@ -220,6 +220,57 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                return $response;
        }
 
                return $response;
        }
 
+       /**
+        * Return download file
+        *
+        * @param Request $request The Request instance
+        * @param string $hash The hash
+        * @param string $path The image path
+        * @return Response The rendered image
+        */
+       public function download(Request $request, string $hash, string $path/*, string $_format*/): Response {
+               //Without matching hash
+               if ($hash !== $this->slugger->hash($path)) {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Invalid download hash');
+               //Without valid format
+               #} elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+               #       //Throw new exception
+               #       throw new NotFoundHttpException('Invalid download format');
+               }
+
+               //Unshort path
+               $path = $this->slugger->unshort($short = $path);
+
+               //Without file
+               if (!is_file($path) || !($mtime = stat($path)['mtime'])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Unable to get thumb file');
+               }
+
+               //Read thumb from cache
+               $response = new BinaryFileResponse($path);
+
+               //Set file name
+               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($path));
+
+               //Set etag
+               //TODO: set etag to file content md5 ? cache it ?
+               $response->setEtag(md5($hash));
+
+               //Set last modified
+               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
+
+               //Set as public
+               $response->setPublic();
+
+               //Return 304 response if not modified
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
+       }
+
        /**
         * Return facebook image
         *
        /**
         * Return facebook image
         *
@@ -257,6 +308,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'facebook-'.$hash.'.'.$_format);
 
                //Set etag
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'facebook-'.$hash.'.'.$_format);
 
                //Set etag
+               //TODO: set etag to file content md5 ? cache it ?
                $response->setEtag(md5($hash));
 
                //Set last modified
                $response->setEtag(md5($hash));
 
                //Set last modified
@@ -443,6 +495,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
 
                //Set etag
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
 
                //Set etag
+               //TODO: set etag to file content md5 ? cache it ?
                $response->setEtag(md5(serialize([$height, $width, $zoom, $latitude, $longitude])));
 
                //Set last modified
                $response->setEtag(md5(serialize([$height, $width, $zoom, $latitude, $longitude])));
 
                //Set last modified
@@ -708,6 +761,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
 
                //Set etag
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
 
                //Set etag
+               //TODO: set etag to file content md5 ? cache it ?
                $response->setEtag(md5(serialize([$height, $width, $zoom, $coordinate])));
 
                //Set last modified
                $response->setEtag(md5(serialize([$height, $width, $zoom, $coordinate])));
 
                //Set last modified
@@ -751,16 +805,16 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                $path = $this->slugger->unshort($short = $path);
 
                //Set thumb
                $path = $this->slugger->unshort($short = $path);
 
                //Set thumb
-               $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$_format;
+               $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$width.'x'.$height.'.'.$_format;
 
                //Without file
 
                //Without file
-               if (!is_file($path) || !($updated = stat($path)['mtime'])) {
+               if (!is_file($path) || !($mtime = stat($path)['mtime'])) {
                        //Throw new exception
                        throw new NotFoundHttpException('Unable to get thumb file');
                }
 
                //Without thumb up to date file
                        //Throw new exception
                        throw new NotFoundHttpException('Unable to get thumb file');
                }
 
                //Without thumb up to date file
-               if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) {
+               if (!is_file($thumb) || !($updated = stat($thumb)['mtime']) || $updated < $mtime) {
                        //Without existing thumb path
                        if (!is_dir($dir = dirname($thumb))) {
                                //Create filesystem object
                        //Without existing thumb path
                        if (!is_dir($dir = dirname($thumb))) {
                                //Create filesystem object
@@ -803,8 +857,8 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                                throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
                        }
 
                                throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
                        }
 
-                       //Set mtime
-                       $mtime = stat($thumb)['mtime'];
+                       //Set updated
+                       $updated = stat($thumb)['mtime'];
                }
 
                //Read thumb from cache
                }
 
                //Read thumb from cache
@@ -814,10 +868,11 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.$hash.'.'.$_format);
 
                //Set etag
                $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.$hash.'.'.$_format);
 
                //Set etag
+               //TODO: set etag to file content md5 ? cache it ?
                $response->setEtag(md5($hash));
 
                //Set last modified
                $response->setEtag(md5($hash));
 
                //Set last modified
-               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
+               $response->setLastModified(\DateTime::createFromFormat('U', strval($updated)));
 
                //Set as public
                $response->setPublic();
 
                //Set as public
                $response->setPublic();
index 2f292095b8bbbc76efa7377dcab18eb8e6345e47..be4bb2a45070bf3ab1b5cfa98daa8016824f5fe5 100644 (file)
@@ -110,6 +110,16 @@ class Configuration implements ConfigurationInterface {
                                'lemon' => dirname(__DIR__).'/public/woff2/lemon.woff2',
                                'notoemoji' => dirname(__DIR__).'/public/woff2/notoemoji.woff2'
                        ],
                                'lemon' => dirname(__DIR__).'/public/woff2/lemon.woff2',
                                'notoemoji' => dirname(__DIR__).'/public/woff2/notoemoji.woff2'
                        ],
+                       'formats' => [
+                               'ico',
+                               'jpeg',
+                               'jpg',
+                               'png',
+                               'svg',
+                               'tiff',
+                               'webm',
+                               'webp',
+                       ],
                        'map' => [
                                'border' => '#00c3f9',
                                'fill' => '#cff',
                        'map' => [
                                'border' => '#00c3f9',
                                'fill' => '#cff',
@@ -166,6 +176,7 @@ class Configuration implements ConfigurationInterface {
                                'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'
                        ],
                        'thumb' => [
                                'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'
                        ],
                        'thumb' => [
+                               'format' => 'jpeg',
                                'height' => 128,
                                'width' => 128
                        ],
                                'height' => 128,
                                'width' => 128
                        ],
@@ -310,6 +321,11 @@ class Configuration implements ConfigurationInterface {
                                                ->defaultValue($defaults['fonts'])
                                                ->scalarPrototype()->end()
                                        ->end()
                                                ->defaultValue($defaults['fonts'])
                                                ->scalarPrototype()->end()
                                        ->end()
+                                       ->arrayNode('formats')
+                                               ->treatNullLike([])
+                                               ->defaultValue($defaults['formats'])
+                                               ->scalarPrototype()->end()
+                                       ->end()
                                        ->arrayNode('map')
                                                ->addDefaultsIfNotSet()
                                                ->children()
                                        ->arrayNode('map')
                                                ->addDefaultsIfNotSet()
                                                ->children()
@@ -371,6 +387,7 @@ class Configuration implements ConfigurationInterface {
                                                ->addDefaultsIfNotSet()
                                                ->children()
                                                        ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['thumb']['height'])->end()
                                                ->addDefaultsIfNotSet()
                                                ->children()
                                                        ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['thumb']['height'])->end()
+                                                       ->scalarNode('format')->cannotBeEmpty()->defaultValue($defaults['thumb']['format'])->end()
                                                        ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['thumb']['width'])->end()
                                                ->end()
                                        ->end()
                                                        ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['thumb']['width'])->end()
                                                ->end()
                                        ->end()
index f8cf3d024deba7465841d157d49a34981c7947e4..9d9db16e5d2a09eefd1cc09109c1dbe8ec0bf9e0 100644 (file)
@@ -37,8 +37,9 @@ class CaptchaType extends AbstractType {
         * @param ?ImageUtil $image The image instance
         * @param ?SluggerUtil $slugger The slugger instance
         * @param ?TranslatorInterface $translator The translator instance
         * @param ?ImageUtil $image The image instance
         * @param ?SluggerUtil $slugger The slugger instance
         * @param ?TranslatorInterface $translator The translator instance
+        * @param bool $enable Use captcha
         */
         */
-       public function __construct(protected ?ImageUtil $image = null, protected ?SluggerUtil $slugger = null, protected ?TranslatorInterface $translator = null) {
+       public function __construct(protected ?ImageUtil $image = null, protected ?SluggerUtil $slugger = null, protected ?TranslatorInterface $translator = null, protected bool $enable = false) {
        }
 
        /**
        }
 
        /**
@@ -56,7 +57,7 @@ class CaptchaType extends AbstractType {
                        $builder->add('_captcha_token', HiddenType::class, ['data' => $captcha['token'], 'empty_data' => $captcha['token'], 'mapped' => false]);
 
                        //Add captcha
                        $builder->add('_captcha_token', HiddenType::class, ['data' => $captcha['token'], 'empty_data' => $captcha['token'], 'mapped' => false]);
 
                        //Add captcha
-                       $builder->add('captcha', IntegerType::class, ['label_attr' => ['class' => 'captcha'], 'label' => '<img src="'.htmlentities($captcha['src']).'" alt="'.htmlentities($captcha['equation']).'" />', 'label_html' => true, 'mapped' => false, 'translation_domain' => false]);
+                       $builder->add('captcha', IntegerType::class, ['label_attr' => ['class' => 'captcha'], 'label' => '<img src="'.htmlentities($captcha['src']).'" alt="'.htmlentities($captcha['equation']).'" />', 'label_html' => true, 'mapped' => false, 'translation_domain' => false, 'required' => true]);
 
                        //Add event listener on captcha
                        $builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'validateCaptcha']);
 
                        //Add event listener on captcha
                        $builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'validateCaptcha']);
@@ -71,7 +72,7 @@ class CaptchaType extends AbstractType {
                parent::configureOptions($resolver);
 
                //Set defaults
                parent::configureOptions($resolver);
 
                //Set defaults
-               $resolver->setDefaults(['captcha' => false, 'error_bubbling' => true, 'translation_domain' => RapsysPackBundle::getAlias()]);
+               $resolver->setDefaults(['captcha' => $this->enable, 'error_bubbling' => true, 'translation_domain' => RapsysPackBundle::getAlias()]);
 
                //Add extra captcha option
                $resolver->setAllowedTypes('captcha', 'boolean');
 
                //Add extra captcha option
                $resolver->setAllowedTypes('captcha', 'boolean');
@@ -95,6 +96,8 @@ class CaptchaType extends AbstractType {
                //Without captcha
                if (empty($data['captcha'])) {
                        //Add error on captcha
                //Without captcha
                if (empty($data['captcha'])) {
                        //Add error on captcha
+                       //XXX: we need to add error on form
+                       //XXX: see https://github.com/symfony/symfony/issues/35831
                        $form->addError(new FormError($this->translator->trans('Captcha is empty')));
 
                        //Reset captcha token
                        $form->addError(new FormError($this->translator->trans('Captcha is empty')));
 
                        //Reset captcha token
@@ -105,6 +108,8 @@ class CaptchaType extends AbstractType {
                //With invalid captcha
                } elseif ($this->slugger->hash($data['captcha']) !== $data['_captcha_token']) {
                        //Add error on captcha
                //With invalid captcha
                } elseif ($this->slugger->hash($data['captcha']) !== $data['_captcha_token']) {
                        //Add error on captcha
+                       //XXX: we need to add error on form
+                       //XXX: see https://github.com/symfony/symfony/issues/35831
                        $form->addError(new FormError($this->translator->trans('Captcha is invalid')));
 
                        //Reset captcha token
                        $form->addError(new FormError($this->translator->trans('Captcha is invalid')));
 
                        //Reset captcha token
diff --git a/Form/ContactType.php b/Form/ContactType.php
new file mode 100644 (file)
index 0000000..b740921
--- /dev/null
@@ -0,0 +1,60 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle\Form;
+
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Validator\Constraints\Email;
+use Symfony\Component\Validator\Constraints\NotBlank;
+
+/**
+ * {@inheritdoc}
+ */
+class ContactType extends CaptchaType {
+       /**
+        * {@inheritdoc}
+        */
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
+               //Add fields
+               $builder
+                       ->add('name', TextType::class, ['attr' => ['placeholder' => 'Your name'], 'required' => true])
+                       ->add('subject', TextType::class, ['attr' => ['placeholder' => 'Subject'], 'required' => true])
+                       ->add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'required' => true, 'invalid_message' => 'Your mail doesn\'t seems to be valid'])
+                       ->add('message', TextareaType::class, ['attr' => ['placeholder' => 'Your message', 'cols' => 50, 'rows' => 15], 'required' => true])
+                       ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+
+               //Call parent
+               parent::buildForm($builder, $options);
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function configureOptions(OptionsResolver $resolver): void {
+               //Call parent configure options
+               parent::configureOptions($resolver);
+
+               //Set defaults
+               $resolver->setDefaults(['captcha' => true]);
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function getName() {
+               return 'contact_form';
+       }
+}
index c53b10fc268c74ef7fc179592db25c2ca2b47709..d5b4b6f04c1b7998a43656732996e2e682eacb7a 100644 (file)
@@ -61,7 +61,7 @@ class PathPackage extends Package {
         *
         * Returns an absolute or root-relative public path
         *
         *
         * Returns an absolute or root-relative public path
         *
-        * Transform @BundleBundle to bundle and remove /Resources/public fragment from path
+        * Transform @BundleBundle to bundle and remove [/Resources|]/public fragment from path
         * This bundle name conversion and bundle prefix are the same as in asset:install command
         *
         * @link https://symfony.com/doc/current/bundles.html#overridding-the-bundle-directory-structure
         * This bundle name conversion and bundle prefix are the same as in asset:install command
         *
         * @link https://symfony.com/doc/current/bundles.html#overridding-the-bundle-directory-structure
@@ -70,7 +70,7 @@ class PathPackage extends Package {
         */
        public function getUrl(string $path): string {
                //Match url starting with a bundle name
         */
        public function getUrl(string $path): string {
                //Match url starting with a bundle name
-               if (preg_match('%^@([A-Z][a-zA-Z]*?)(?:Bundle/Resources/public)?/(.*)$%', $path, $matches)) {
+               if (preg_match('%^@([A-Z][a-zA-Z]*?)(?:Bundle/(?:Resources/)?public)?/(.*)$%', $path, $matches)) {
                        //Handle empty or without replacement pattern basePath
                        if (empty($this->basePath) || strpos($this->basePath, '%s') === false) {
                                //Set path from hardcoded format
                        //Handle empty or without replacement pattern basePath
                        if (empty($this->basePath) || strpos($this->basePath, '%s') === false) {
                                //Set path from hardcoded format
index 7d73117b432129a70fbfbfcb6f2972530846de40..a1fe4c5f5adcdb7d14c3d05ca321f3a896588e56 100644 (file)
@@ -26,8 +26,10 @@ use Symfony\Component\Routing\Exception\RouteNotFoundException;
 use Symfony\Component\Routing\RouterInterface;
 
 use Twig\Error\Error;
 use Symfony\Component\Routing\RouterInterface;
 
 use Twig\Error\Error;
-use Twig\Node\Expression\AssignNameExpression;
+use Twig\Node\Expression\Variable\AssignContextVariable;
 use Twig\Node\Node;
 use Twig\Node\Node;
+use Twig\Node\Nodes;
+use Twig\Node\EmptyNode;
 use Twig\Node\SetNode;
 use Twig\Node\TextNode;
 use Twig\Source;
 use Twig\Node\SetNode;
 use Twig\Node\TextNode;
 use Twig\Source;
@@ -245,7 +247,7 @@ class TokenParser extends AbstractTokenParser {
                        #throw new Error('Empty inputs token', $token->getLine(), $stream->getSourceContext());
 
                        //Send an empty node without inputs
                        #throw new Error('Empty inputs token', $token->getLine(), $stream->getSourceContext());
 
                        //Send an empty node without inputs
-                       return new Node();
+                       return new EmptyNode();
                }
 
                //Check filters
                }
 
                //Check filters
@@ -329,13 +331,13 @@ class TokenParser extends AbstractTokenParser {
                }
 
                //Set name in context key
                }
 
                //Set name in context key
-               $ref = new AssignNameExpression($this->token, $token->getLine());
+               $ref = new AssignContextVariable($this->token, $token->getLine());
 
                //Set output in context value
                $value = new TextNode($asset, $token->getLine());
 
                //Send body with context set
 
                //Set output in context value
                $value = new TextNode($asset, $token->getLine());
 
                //Send body with context set
-               return new Node([
+               return new Nodes([
                        //This define name in twig template by prepending $context['<name>'] = '<output>';
                        new SetNode(true, $ref, $value, $token->getLine(), $this->getTag()),
                        //The tag captured body
                        //This define name in twig template by prepending $context['<name>'] = '<output>';
                        new SetNode(true, $ref, $value, $token->getLine(), $this->getTag()),
                        //The tag captured body
index 81cd07aba8a02da2386ecccc6928cb19ca374bc5..bd7ebc58b7fa2116cd7304559c4e4f6518c751d0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -101,8 +101,8 @@ class AppKernel extends Kernel
 
 ### Step 3: Configure the Bundle
 
 
 ### Step 3: Configure the Bundle
 
-Setup configuration file `config/packages/rapsys_pack.yaml` with the following
-content available in `Resources/config/packages/rapsys_pack.yaml`:
+Setup configuration file `config/packages/rapsyspack.yaml` with the following
+content available in `vendor/rapsys/packbundle/config/packages/rapsyspack.yaml`:
 
 ```yaml
 #Services configuration
 
 ```yaml
 #Services configuration
@@ -110,23 +110,23 @@ services:
     #Replace assets.packages definition
     assets.packages:
         class: 'Symfony\Component\Asset\Packages'
     #Replace assets.packages definition
     assets.packages:
         class: 'Symfony\Component\Asset\Packages'
-        arguments: [ '@rapsys_pack.path_package' ]
+        arguments: [ '@rapsyspack.path_package' ]
     #Replace assets.context definition
     assets.context:
         class: 'Rapsys\PackBundle\Context\RequestStackContext'
         arguments: [ '@request_stack', '%asset.request_context.base_path%', '%asset.request_context.secure%' ]
     #Register assets pack package
     #Replace assets.context definition
     assets.context:
         class: 'Rapsys\PackBundle\Context\RequestStackContext'
         arguments: [ '@request_stack', '%asset.request_context.base_path%', '%asset.request_context.secure%' ]
     #Register assets pack package
-    rapsys_pack.path_package:
+    rapsyspack.path_package:
         class: 'Rapsys\PackBundle\Package\PathPackage'
         arguments: [ '/', '@assets.empty_version_strategy', '@assets.context' ]
         public: true
     #Register twig pack extension
         class: 'Rapsys\PackBundle\Package\PathPackage'
         arguments: [ '/', '@assets.empty_version_strategy', '@assets.context' ]
         public: true
     #Register twig pack extension
-    rapsys_pack.pack_extension:
+    rapsyspack.pack_extension:
         class: 'Rapsys\PackBundle\Extension\PackExtension'
         class: 'Rapsys\PackBundle\Extension\PackExtension'
-        arguments: [ '@service_container', '@rapsys_pack.intl_util', '@file_locator', '@rapsys_pack.path_package', '@rapsys_pack.slugger_util' ]
+        arguments: [ '@service_container', '@rapsyspack.intl_util', '@file_locator', '@rapsyspack.path_package', '@rapsyspack.slugger_util' ]
         tags: [ 'twig.extension' ]
     #Register intl util service
         tags: [ 'twig.extension' ]
     #Register intl util service
-    rapsys_pack.intl_util:
+    rapsyspack.intl_util:
         class: 'Rapsys\PackBundle\Util\IntlUtil'
         public: true
     #Register facebook event subscriber
         class: 'Rapsys\PackBundle\Util\IntlUtil'
         public: true
     #Register facebook event subscriber
@@ -135,54 +135,54 @@ services:
         tags: [ 'kernel.event_subscriber' ]
     #Register intl util class alias
     Rapsys\PackBundle\Util\IntlUtil:
         tags: [ 'kernel.event_subscriber' ]
     #Register intl util class alias
     Rapsys\PackBundle\Util\IntlUtil:
-        alias: 'rapsys_pack.intl_util'
+        alias: 'rapsyspack.intl_util'
     #Register facebook util service
     #Register facebook util service
-    rapsys_pack.facebook_util:
+    rapsyspack.facebook_util:
         class: 'Rapsys\PackBundle\Util\FacebookUtil'
         class: 'Rapsys\PackBundle\Util\FacebookUtil'
-        arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ]
+        arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
         public: true
     #Register facebook util class alias
     Rapsys\PackBundle\Util\FacebookUtil:
         public: true
     #Register facebook util class alias
     Rapsys\PackBundle\Util\FacebookUtil:
-        alias: 'rapsys_pack.facebook_util'
+        alias: 'rapsyspack.facebook_util'
     #Register image util service
     #Register image util service
-    rapsys_pack.image_util:
+    rapsyspack.image_util:
         class: 'Rapsys\PackBundle\Util\ImageUtil'
         class: 'Rapsys\PackBundle\Util\ImageUtil'
-        arguments: [ '@router', '@rapsys_pack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ]
+        arguments: [ '@router', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
         public: true
     #Register image util class alias
     Rapsys\PackBundle\Util\ImageUtil:
         public: true
     #Register image util class alias
     Rapsys\PackBundle\Util\ImageUtil:
-        alias: 'rapsys_pack.image_util'
+        alias: 'rapsyspack.image_util'
     #Register map util service
     #Register map util service
-    rapsys_pack.map_util:
+    rapsyspack.map_util:
         class: 'Rapsys\PackBundle\Util\MapUtil'
         class: 'Rapsys\PackBundle\Util\MapUtil'
-        arguments: [ '@router', '@rapsys_pack.slugger_util' ]
+        arguments: [ '@router', '@rapsyspack.slugger_util' ]
         public: true
     #Register map util class alias
     Rapsys\PackBundle\Util\MapUtil:
         public: true
     #Register map util class alias
     Rapsys\PackBundle\Util\MapUtil:
-        alias: 'rapsys_pack.map_util'
+        alias: 'rapsyspack.map_util'
     #Register slugger util service
     #Register slugger util service
-    rapsys_pack.slugger_util:
+    rapsyspack.slugger_util:
         class: 'Rapsys\PackBundle\Util\SluggerUtil'
         arguments: [ '%kernel.secret%' ]
         public: true
     #Register slugger util class alias
     Rapsys\PackBundle\Util\SluggerUtil:
         class: 'Rapsys\PackBundle\Util\SluggerUtil'
         arguments: [ '%kernel.secret%' ]
         public: true
     #Register slugger util class alias
     Rapsys\PackBundle\Util\SluggerUtil:
-        alias: 'rapsys_pack.slugger_util'
+        alias: 'rapsyspack.slugger_util'
     #Register image controller
     Rapsys\PackBundle\Controller\ImageController:
     #Register image controller
     Rapsys\PackBundle\Controller\ImageController:
-        arguments: [ '@service_container', '@rapsys_pack.image_util', '@rapsys_pack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ]
+        arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
         tags: [ 'controller.service_arguments' ]
     #Register map controller
     Rapsys\PackBundle\Controller\MapController:
         tags: [ 'controller.service_arguments' ]
     #Register map controller
     Rapsys\PackBundle\Controller\MapController:
-        arguments: [ '@service_container', '@rapsys_pack.map_util', '@rapsys_pack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ]
+        arguments: [ '@service_container', '@rapsyspack.map_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
         tags: [ 'controller.service_arguments' ]
     Rapsys\PackBundle\Form\CaptchaType:
         tags: [ 'controller.service_arguments' ]
     Rapsys\PackBundle\Form\CaptchaType:
-        arguments: [ '@rapsys_pack.image_util', '@rapsys_pack.slugger_util', '@translator' ]
+        arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ]
         tags: [ 'form.type' ]
 ```
 
 Setup configuration file `config/packages/myproject.yaml` with the following
         tags: [ 'form.type' ]
 ```
 
 Setup configuration file `config/packages/myproject.yaml` with the following
-content available in `Resources/config/packages/rapsys_pack.yaml`:
+content available in `vendor/rapsys/packbundle/config/packages/rapsyspack.yaml`:
 
 ```yaml
 #Services configuration
 
 ```yaml
 #Services configuration
@@ -192,9 +192,9 @@ services:
         arguments: [ '@router', [ 'en', 'en_gb', 'en_us', 'fr', 'fr_fr' ] ]
         tags: [ 'kernel.event_subscriber' ]
     #Register facebook util service
         arguments: [ '@router', [ 'en', 'en_gb', 'en_us', 'fr', 'fr_fr' ] ]
         tags: [ 'kernel.event_subscriber' ]
     #Register facebook util service
-    rapsys_blog.facebook_util:
+    myproject.facebook_util:
         class: 'Rapsys\PackBundle\Util\FacebookUtil'
         class: 'Rapsys\PackBundle\Util\FacebookUtil'
-        arguments: [ '@router',  '%kernel.project_dir%/var/cache', '%rapsys_pack.path%', 'facebook', '%kernel.project_dir%/public/png/facebook.png' ]
+        arguments: [ '@router',  '%kernel.project_dir%/var/cache', '%rapsyspack.path%', 'facebook', '%kernel.project_dir%/public/png/facebook.png' ]
         public: true
 ```
 
         public: true
 ```
 
@@ -214,7 +214,7 @@ $ bin/console debug:config RapsysPackBundle
 
 ### Step 4: Use the twig extension in your Template
 
 
 ### Step 4: Use the twig extension in your Template
 
-You can use a template like this to generate your first `rapsys_pack` enabled
+You can use a template like this to generate your first `rapsyspack` enabled
 template:
 
 ```twig
 template:
 
 ```twig
@@ -223,7 +223,7 @@ template:
        <head>
                <meta charset="UTF-8" />
                <title>{% block title %}Welcome!{% endblock %}</title>
        <head>
                <meta charset="UTF-8" />
                <title>{% block title %}Welcome!{% endblock %}</title>
-               {% stylesheet '//fonts.googleapis.com/css?family=Irish+Grover|La+Belle+Aurore' '@NamedBundle/Resources/public/css/{reset,screen}.css' '@Short/css/example.css' %}
+               {% stylesheet '//fonts.googleapis.com/css?family=Irish+Grover|La+Belle+Aurore' '@NamedBundle/public/css/{reset,screen}.css' '@Short/css/example.css' %}
                        <link rel="stylesheet" type="text/css" href="{{ asset_url }}" />
                {% endstylesheet %}
        </head>
                        <link rel="stylesheet" type="text/css" href="{{ asset_url }}" />
                {% endstylesheet %}
        </head>
@@ -273,9 +273,9 @@ namespace Rapsys\PackBundle\Filter;
 
 use Twig\Error\Error;
 
 
 use Twig\Error\Error;
 
-//This class will be defined in the parameter rapsys_pack.filters.(css|img|js).[x].class string
+//This class will be defined in the parameter rapsyspack.filters.(css|img|js).[x].class string
 class MyPackFilter implements FilterInterface {
 class MyPackFilter implements FilterInterface {
-       //The constructor arguments ... will be replaced with values defined in the parameter rapsys_pack.filters.(css|img|js).[x].args array
+       //The constructor arguments ... will be replaced with values defined in the parameter rapsyspack.filters.(css|img|js).[x].args array
        public function __construct(string $fileName, int $line, string $bin = 'mypack', ...) {
                //Set fileName
                $this->fileName = $fileName;
        public function __construct(string $fileName, int $line, string $bin = 'mypack', ...) {
                //Set fileName
                $this->fileName = $fileName;
index 08360b08cb3f0623ec668f6de293c6916ee3a98a..7844d73f6e5fa4ea00f158b570002e26a5a3e4ed 100644 (file)
@@ -64,6 +64,6 @@ class RapsysPackBundle extends Bundle {
         */
        public static function getVersion(): string {
                //Return version
         */
        public static function getVersion(): string {
                //Return version
-               return '0.5.4';
+               return '0.5.7';
        }
 }
        }
 }
diff --git a/Util/FileUtil.php b/Util/FileUtil.php
new file mode 100644 (file)
index 0000000..814892e
--- /dev/null
@@ -0,0 +1,309 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle\Util;
+
+use Psr\Container\ContainerInterface;
+
+use Rapsys\PackBundle\RapsysPackBundle;
+use Rapsys\PackBundle\Util\ImageUtil;
+use Rapsys\PackBundle\Util\IntlUtil;
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RouterInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+/**
+ * Manages file informations
+ */
+class FileUtil {
+       /**
+        * Alias string
+        */
+       protected string $alias;
+
+       /**
+        * Config array
+        */
+       protected array $config;
+
+       /**
+        * Creates a new file util
+        *
+        * @param ContainerInterface $container The container instance
+        * @param ImageUtil $image The ImageUtil instance
+        * @param IntlUtil $intl The IntlUtil instance
+        * @param RouterInterface $router The router instance
+        * @param SluggerUtil $slugger The SluggerUtil instance
+        * @param TranslatorInterface $translator The translator instance
+        */
+       public function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected IntlUtil $intl, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator) {
+               //Retrieve config
+               $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
+       }
+
+       /**
+        * Get file infos
+        *
+        * @param string $path The base path
+        * @param string $realpath The real path
+        * @param string $name The route name
+        * @param array $parameters The route parameters
+        * @param bool $si Use si unit
+        */
+       public function getFile(string $path, string $realpath, string $name, array $parameters = [], bool $si = true): array {
+               //Set file
+               $file = new \SplFileObject($realpath);
+
+               //Set size
+               $size = $file->getSize();
+
+               //Set unit
+               $unit = $si ? 1000 : 1024;
+
+               //Set index
+               $index = [ '', $si ? 'k' : 'K', 'M', 'G', 'T', 'P', 'E' ];
+
+               //Get exp
+               $exp = intval((log($size) / log($unit)));
+
+               //Rebase number
+               $number = round($size / pow($unit, $exp), 2);
+
+               //Set ext, height, image, mime, preview, thumb and width
+               $ext = $height = $image = $mime = $preview = $thumb = $width = false;
+
+               //With supporter format extension
+               if (($pos = strrpos($realpath, '.')) && ($ext = substr($realpath, $pos + 1)) && in_array($ext, $this->config['formats'])) {
+                       //With mime type
+                       //XXX: getimagesize is too slow to run against all files
+                       if (($image = getimagesize($realpath)) !== false) {
+                               //Set ext
+                               if (($ext = image_type_to_extension($image[2], false)) === false) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Unable to get "%s" file image extension', $realpath));
+                               }
+
+                               //Set height
+                               $height = $image[1];
+
+                               //Set mime
+                               $mime = image_type_to_mime_type($image[2]);
+
+                               //Set width
+                               $width = $image[0];
+
+                               //Set preview
+                               $preview = $this->image->getThumb($realpath, $height, $width, $ext);
+
+                               //Set preview
+                               $thumb = $this->image->getThumb($realpath, $this->config['thumb']['height'], $this->config['thumb']['weight'], $ext);
+                       }
+               }
+
+               //Return file infos
+               return [
+                       'created' => (new \DateTime())->setTimestamp($file->getCTime()),
+                       'height' => $height,
+                       'intlsize' => $intlsize = $this->intl->number($number),
+                       'intlunit' => $intlunit = $this->translator->trans($index[$exp].($si ? '' : 'i').'B'),
+                       'link' => $this->router->generate($name, ['path' => substr($realpath, strlen($path) + 1)] + $parameters),
+                       'download' => $this->router->generate('rapsyspack_download', ['path' => $short = $this->slugger->short($realpath), 'hash' => $this->slugger->hash($short), 'u' => $mtime = $file->getMTime()]),
+                       'mime' => $mime,
+                       'modified' => (new \DateTime())->setTimestamp($mtime),
+                       'name' => basename($realpath).' ('.$intlsize.' '.$intlunit.')',
+                       'preview' => $preview,
+                       'size' => $size,
+                       'thumb' => $thumb,
+                       'width' => $width,
+                       //TODO: preview for images ?
+                       //TODO: extra fields ? (preview, miniature, etc ?)
+                       //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?)
+                       //TODO: XXX: finish this !!!
+               ];
+       }
+
+       /**
+        * Get path infos
+        *
+        * @param string $path The base path
+        * @param string $realpath The real path
+        * @param string $name The route name
+        * @param array $parameters The route parameters
+        * @param bool $si Use si unit
+        */
+       //Route object instead of shitty array ?
+       public function getInfos(string $path, string $realpath, string $slug, string $name, array $parameters = []) {
+               //Set result
+               $result = [
+                       'breadcrumbs' => [/*$breadcrumb*/],
+                       'directories' => [],
+                       'file' => [],
+                       'files' => [],
+               ];
+
+               //Set base
+               $base = '';
+
+               //Iterate on breadcrumbs
+               foreach (explode('/', substr($realpath, strlen($path))) as $value) {
+                       $result['breadcrumbs'][] = [
+                               'name' => $value ? '/ '.$value : $slug,
+                               'link' => $this->router->generate($name, ['path' => ($base .= ($base == '' ? '' : '/').$value)] + $parameters)
+                       ];
+               }
+
+               //With file
+               if (is_file($realpath)) {
+                       //Set file
+                       $result['file'] = $this->getFile($path, $realpath, $name, $parameters);
+               //With directory
+               //TODO: for pagination, files and directories are to be placed in a single array
+               //TODO: only get informations on files and directories inside the pagination window !
+               } elseif (is_dir($realpath)) {
+                       //Set dir
+                       $dir = dir($realpath);
+
+                       //Iterate on directory
+                       while (($item = $dir->read()) !== false) {
+                               //Skip ., .., .git, .svn, .htpasswd, .htgroup and .*.un~ items
+                               //TODO: set this regexp in config ?
+                               if (preg_match('/^(\.|\.\.|\.git|\.svn|\.ht(access|passwd|group)|\..*\.un~)$/', $item)) {
+                                       //Skip it
+                                       continue;
+                               }
+
+                               //Check item
+                               if (
+                                       //Without item realpath
+                                       !($itempath = realpath($realpath.'/'.$item)) ||
+                                       //Item realpath not matching album path
+                                       //XXX: skip files outside of album/element path (symlink outside of tree)
+                                       //TODO: make it configurable ? by album/element/admin ?
+                                       $path !== substr($itempath, 0, strlen($path))
+                               ) {
+                                       //Skip it
+                                       continue;
+                               }
+
+                               //With directory
+                               if (is_dir($itempath)) {
+                                       //Append directory
+                                       //TODO: use this structure or revert to old $item.'/' => $link form
+                                       $result['directories'][] = [
+                                               'name' => $item.'/',
+                                               'link' => $this->router->generate($name, ['path' => substr($itempath, strlen($path) + 1)] + $parameters)
+                                               //TODO: add stats here ? like number of files ?
+                                       ];
+                               //With file
+                               } elseif (is_file($itempath)) {
+                                       //Set file
+                                       $result['files'][] = $this->getFile($path, $itempath, $name, $parameters);
+                               //With unknown type
+                               } else {
+                                       //Throw 404
+                                       throw new \Exception(sprintf('Unknown file "%s" type', $itempath));
+                               }
+                       }
+               //With unknown type
+               } else {
+                       //Throw 404
+                       throw new \Exception(sprintf('Unknown file "%s" type', $realpath));
+               }
+
+               //Return result
+               return $result;
+       }
+
+       /**
+        * Get file infos
+        *
+        * @param string $path The file path
+        * @param bool $si Use si units
+        * @return array The file infos
+        * /
+       public function infos(string $path, bool $si = true): array {
+               //Stat file
+               $stat = stat($path);
+
+               //Set unit
+               $unit = $si ? 1000 : 1024;
+
+               //Set index
+               $index = [ '', $si ? 'k' : 'K', 'M', 'G', 'T', 'P', 'E' ];
+
+               //Get exp
+               $exp = intval((log($stat['size']) / log($unit)));
+
+               //Rebase number
+               $number = round($stat['size'] / pow($unit, $exp), 2);
+
+               //Set file infos
+               $fileinfos = [
+                       'intlsize' => $intlsize = $this->intl->number($number),
+                       'intlunit' => $intlunit = $this->translator->trans($index[$exp].($si ? '' : 'i').'B'),
+                       'name' => basename($path).' ('.$intlsize.' '.$intlunit.')',
+                       'size' => $stat['size'],
+                       'ctime' => $stat['ctime'],
+                       'mtime' => $stat['mtime'],
+                       //TODO: preview for images ?
+                       //TODO: extra fields ? (preview, miniature, etc ?)
+                       //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?)
+                       //TODO: XXX: finish this !!!
+               ];
+
+               //With mimetype
+               if (($mimetype = mime_content_type($path)) !== false) {
+                       //Set file mimetype
+                       $fileinfos['mimetype'] = $mimetype;
+
+                       //TODO: with image preview, movie webm preview, others a imagemagick file with format initials ?
+
+                       //With image
+                       if (
+                               $mimetype == 'image/jpeg' ||
+                               $mimetype == 'image/png' ||
+                               $mimetype == 'image/bmp' ||
+                               $mimetype == 'image/tiff' ||
+                               $mimetype == 'image/svg+xml'
+                       ) {
+                               //Get image width and height
+                               if (($fileinfos['image'] = getimagesize($path)) === false) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Unable to get "%s" file image size', $path));
+                               }
+
+                               //Set thumb
+                               $fileinfos['thumb'] = $this->image->getThumb($path, 64, 64);
+                       }
+               }
+               /*
+                       'src' => 
+                               //With location user source image
+                               if (($isFile = is_file($source = $this->config['path'].'/location/'.$location['id'].'/'.$id.'.png')) && ($mtime = stat($source)['mtime'])) {
+                                       //Set location image
+                                       $this->context['locations'][$locationId]['image'] = $this->image->getThumb($location['miniature'], $mtime, $source);
+
+                       //With image mimetype
+                       if (in_array($fileinfos['mimetype'], [ 'image/jpeg', 'image/png', 'image/webp' ])) {
+                               header('Content-Type: text/plain');
+                               var_dump($fileinfos);
+                               exit;
+                               $file['thumb'] = $this->router->generate('rapsyspack_thumb', [ 'hash' => $this->slugger->hash(''), 'path' => $path, 'slug' => $slug ]);
+                               #public function thumb(Request $request, string $hash, int $updated, string $path, int $width, int $height): Response {
+                       }
+                * /
+
+               //Return file infos
+               return $fileinfos;
+       }*/
+}
index 44afe28bb7234221c685061b8c4c54d44d287024..8a93c5285f62916c99611f9b2edeea51b86ef015 100644 (file)
@@ -73,10 +73,10 @@ class ImageUtil {
                $a = $random % 10;
 
                //Set b
                $a = $random % 10;
 
                //Set b
-               $b = $random / 10 % 10;
+               $b = intval($random / 10) % 10;
 
                //Set c
 
                //Set c
-               $c = $random / 100 % 10;
+               $c = intval($random / 100) % 10;
 
                //Set equation
                $equation = $a.' * '.$b.' + '.$c;
 
                //Set equation
                $equation = $a.' * '.$b.' + '.$c;
@@ -597,9 +597,16 @@ class ImageUtil {
         * @param string $path The path
         * @param ?int $height The height
         * @param ?int $width The width
         * @param string $path The path
         * @param ?int $height The height
         * @param ?int $width The width
+        * @param ?string $format The format
         * @return array The thumb data
         */
         * @return array The thumb data
         */
-       public function getThumb(string $path, ?int $height = null, ?int $width = null): array {
+       public function getThumb(string $path, ?int $height = null, ?int $width = null, ?string $format = null): array {
+               //Without format
+               if ($format === null || !in_array($format, $this->config['formats'])) {
+                       //Set format from config
+                       $format = $this->config['thumb']['format'];
+               }
+
                //Without height
                if ($height === null) {
                        //Set height from config
                //Without height
                if ($height === null) {
                        //Set height from config
@@ -624,9 +631,9 @@ class ImageUtil {
 
                //Return array
                return [
 
                //Return array
                return [
-                       'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width]),
-                       'width' => $width,
-                       'height' => $height
+                       'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, '_format' => $format]),
+                       'height' => $height,
+                       'width' => $width
                ];
        }
 
                ];
        }
 
index 80b95308255e4a719ea39e00414650adced75366..52bd546a7091ca13c8588a1d705724dd8906dd67 100644 (file)
@@ -123,7 +123,7 @@ class IntlUtil {
                //Set styles
                static $styles = [
                        'decimal' => \NumberFormatter::DECIMAL,
                //Set styles
                static $styles = [
                        'decimal' => \NumberFormatter::DECIMAL,
-                       'currency' => \NumberFormatter::CURRENCY,
+                       #'currency' => \NumberFormatter::CURRENCY,
                        'percent' => \NumberFormatter::PERCENT,
                        'scientific' => \NumberFormatter::SCIENTIFIC,
                        'spellout' => \NumberFormatter::SPELLOUT,
                        'percent' => \NumberFormatter::PERCENT,
                        'scientific' => \NumberFormatter::SCIENTIFIC,
                        'spellout' => \NumberFormatter::SPELLOUT,
@@ -150,7 +150,7 @@ class IntlUtil {
                        'int32' => \NumberFormatter::TYPE_INT32,
                        'int64' => \NumberFormatter::TYPE_INT64,
                        'double' => \NumberFormatter::TYPE_DOUBLE,
                        'int32' => \NumberFormatter::TYPE_INT32,
                        'int64' => \NumberFormatter::TYPE_INT64,
                        'double' => \NumberFormatter::TYPE_DOUBLE,
-                       'currency' => \NumberFormatter::TYPE_CURRENCY
+                       #'currency' => \NumberFormatter::TYPE_CURRENCY
                ];
 
                //Get formatter
                ];
 
                //Get formatter
index 0efaa0bb2e7eb90e116ea82bbfae5ed75d10f928..d2bc1c5718e977e3d1f0b001c3e8d8de24db360c 100644 (file)
@@ -24,7 +24,7 @@ services:
     # Register file util service
     rapsyspack.file_util:
         class: 'Rapsys\PackBundle\Util\FileUtil'
     # Register file util service
     rapsyspack.file_util:
         class: 'Rapsys\PackBundle\Util\FileUtil'
-        arguments: [ '@rapsyspack.image_util', '@rapsyspack.intl_util', '@translator' ]
+        arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.intl_util', '@router', '@rapsyspack.slugger_util', '@translator' ]
         public: true
     # Register image util service
     rapsyspack.image_util:
         public: true
     # Register image util service
     rapsyspack.image_util:
index f23660bc6aa09886bdddce68915bc13c14ae79f9..e471166da87a6de5f77f83cb9ea5c5d0e1c4d709 100644 (file)
@@ -8,6 +8,11 @@ rapsyspack_css:
     path: '/bundles/rapsyspack/pack/css/{file<[a-zA-Z0-9]+>}.{!_format<css>?css}'
     methods: GET
 
     path: '/bundles/rapsyspack/pack/css/{file<[a-zA-Z0-9]+>}.{!_format<css>?css}'
     methods: GET
 
+rapsyspack_download:
+    path: '/download/{hash<[a-zA-Z0-9=_-]+>}/{path<[a-zA-Z0-9=_-]+>}'
+    controller: Rapsys\PackBundle\Controller::download
+    methods: GET
+
 rapsyspack_facebook:
     path: '/facebook/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}'
     controller: Rapsys\PackBundle\Controller::facebook
 rapsyspack_facebook:
     path: '/facebook/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}'
     controller: Rapsys\PackBundle\Controller::facebook