diff --git a/Imagine/Cache/CacheManager.php b/Imagine/Cache/CacheManager.php index 772b0add9..769e206e9 100644 --- a/Imagine/Cache/CacheManager.php +++ b/Imagine/Cache/CacheManager.php @@ -38,8 +38,10 @@ class CacheManager private $resolvers = array(); /** + * Constructs the cache manager to handle Resolvers based on the provided FilterConfiguration. + * * @param FilterConfiguration $filterConfig - * @param Filesystem $filesystem + * @param RouterInterface $router * @param string $webRoot * @param string $defaultResolver */ @@ -54,7 +56,7 @@ public function __construct(FilterConfiguration $filterConfig, RouterInterface $ /** * @param string $filter * @param ResolverInterface $resolver - * + * * @return void */ public function addResolver($filter, ResolverInterface $resolver) @@ -113,13 +115,7 @@ private function getResolver($filter) */ public function getBrowserPath($targetPath, $filter, $absolute = false) { - $params = array('path' => ltrim($targetPath, '/')); - - return str_replace( - urlencode($params['path']), - urldecode($params['path']), - $this->router->generate('_imagine_'.$filter, $params, $absolute) - ); + return $this->getResolver($filter)->getBrowserPath($targetPath, $filter, $absolute); } /** @@ -163,4 +159,17 @@ public function store(Response $response, $targetPath, $filter) return $response; } + + /** + * Remove a cached image from the storage. + * + * @param string $targetPath + * @param string $filter + * + * @return bool + */ + public function remove($targetPath, $filter) + { + return $this->getResolver($filter)->remove($targetPath, $filter); + } } diff --git a/Imagine/Cache/Resolver/AbstractFilesystemResolver.php b/Imagine/Cache/Resolver/AbstractFilesystemResolver.php index efdafee44..679783fd0 100644 --- a/Imagine/Cache/Resolver/AbstractFilesystemResolver.php +++ b/Imagine/Cache/Resolver/AbstractFilesystemResolver.php @@ -13,9 +13,9 @@ abstract class AbstractFilesystemResolver implements ResolverInterface protected $filesystem; /** - * Constructs cache web path resolver + * Constructs a filesystem based cache resolver. * - * @param Filesystem $filesystem + * @param Filesystem $filesystem */ public function __construct(Filesystem $filesystem) { @@ -23,7 +23,10 @@ public function __construct(Filesystem $filesystem) } /** + * Stores the content into a static file. + * * @throws \RuntimeException + * * @param Response $response * @param string $targetPath * @param string $filter diff --git a/Imagine/Cache/Resolver/AmazonS3Resolver.php b/Imagine/Cache/Resolver/AmazonS3Resolver.php new file mode 100644 index 000000000..1f9631dc7 --- /dev/null +++ b/Imagine/Cache/Resolver/AmazonS3Resolver.php @@ -0,0 +1,135 @@ +storage = $storage; + $this->storage->if_bucket_exists($bucket); + + $this->bucket = $bucket; + $this->acl = $acl; + } + + /** + * @param CacheManager $cacheManager + */ + public function setCacheManager(CacheManager $cacheManager) + { + $this->cacheManager = $cacheManager; + } + + /** + * {@inheritDoc} + */ + public function resolve(Request $request, $path, $filter) + { + return $this->getObjectPath($path, $filter); + } + + /** + * {@inheritDoc} + */ + public function store(Response $response, $targetPath, $filter) + { + $storageResponse = $this->storage->create_object($this->bucket, $targetPath, array( + 'body' => $response->getContent(), + 'contentType' => $response->headers->get('Content-Type'), + 'length' => strlen($response->getContent()), + 'acl' => $this->acl, + )); + + if ($storageResponse->isOK()) { + $response->setStatusCode(301); + $response->headers->set('Location', $this->storage->get_object_url($this->bucket, $targetPath)); + } + + return $response; + } + + /** + * {@inheritDoc} + */ + public function getBrowserPath($targetPath, $filter, $absolute = false) + { + $objectPath = $this->getObjectPath($targetPath, $filter); + if ($this->storage->if_object_exists($this->bucket, $objectPath)) { + return $this->storage->get_object_url($this->bucket, $objectPath); + } + + $params = array('path' => ltrim($targetPath, '/')); + + return str_replace( + urlencode($params['path']), + urldecode($params['path']), + $this->cacheManager->getRouter()->generate('_imagine_'.$filter, $params, $absolute) + ); + } + + /** + * {@inheritDoc} + */ + public function remove($targetPath, $filter) + { + $objectPath = $this->getObjectPath($targetPath, $filter); + if (!$this->storage->if_object_exists($this->bucket, $objectPath)) { + // A non-existing object to delete: done! + return true; + } + + return $this->storage->delete_object($this->bucket, $objectPath)->isOK(); + } + + /** + * Returns the object path within the bucket. + * + * @param string $path The base path of the resource. + * @param string $filter The name of the imagine filter in effect. + * + * @return string The path of the object on S3. + */ + protected function getObjectPath($path, $filter) + { + return $filter.'/'.$path; + } +} diff --git a/Imagine/Cache/Resolver/ResolverInterface.php b/Imagine/Cache/Resolver/ResolverInterface.php index 87538a270..28b9e991b 100644 --- a/Imagine/Cache/Resolver/ResolverInterface.php +++ b/Imagine/Cache/Resolver/ResolverInterface.php @@ -8,22 +8,49 @@ interface ResolverInterface { /** - * Resolves filtered path for rendering in the browser + * Resolves filtered path for rendering in the browser. * - * @param Request $request - * @param string $path - * @param string $filter + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException In case the path can not be resolved. * - * @return string target path + * @param Request $request The request made against a _imagine_* filter route. + * @param string $path The path where the resolved file is expected. + * @param string $filter The name of the imagine filter in effect. + * + * @return string|Response The target path to be used in other methods of this Resolver, + * a Response may be returned to avoid calling store upon resolution. */ function resolve(Request $request, $path, $filter); /** - * @param Response $response - * @param string $targetPath - * @param string $filter + * Stores the content of the given Response. + * + * @param Response $response The response provided by the _imagine_* filter route. + * @param string $targetPath The target path provided by the resolve method. + * @param string $filter The name of the imagine filter in effect. * - * @return Response + * @return Response The (modified) response to be sent to the browser. */ function store(Response $response, $targetPath, $filter); + + /** + * Returns a web accessible URL. + * + * @param string $targetPath The target path provided by the resolve method. + * @param string $filter The name of the imagine filter in effect. + * @param bool $absolute Whether to generate an absolute URL or a relative path is accepted. + * In case the resolver does not support relative paths, it may ignore this flag. + * + * @return string + */ + function getBrowserPath($targetPath, $filter, $absolute = false); + + /** + * Removes a stored image resource. + * + * @param string $targetPath The target path provided by the resolve method. + * @param string $filter The name of the imagine filter in effect. + * + * @return bool Whether the file has been removed successfully. + */ + function remove($targetPath, $filter); } diff --git a/Imagine/Cache/Resolver/WebPathResolver.php b/Imagine/Cache/Resolver/WebPathResolver.php index 8dc793cb8..dfdd7157e 100644 --- a/Imagine/Cache/Resolver/WebPathResolver.php +++ b/Imagine/Cache/Resolver/WebPathResolver.php @@ -2,17 +2,17 @@ namespace Liip\ImagineBundle\Imagine\Cache\Resolver; +use Liip\ImagineBundle\Imagine\Cache\CacheManagerAwareInterface, + Liip\ImagineBundle\Imagine\Cache\CacheManager; + use Symfony\Component\HttpFoundation\Request, Symfony\Component\HttpFoundation\RedirectResponse, Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Liip\ImagineBundle\Imagine\Cache\CacheManagerAwareInterface, - Liip\ImagineBundle\Imagine\Cache\CacheManager; - class WebPathResolver extends AbstractFilesystemResolver implements CacheManagerAwareInterface { /** - * @var CacheManager; + * @var CacheManager */ protected $cacheManager; @@ -25,37 +25,84 @@ public function setCacheManager(CacheManager $cacheManager) } /** - * Resolves filtered path for rendering in the browser + * {@inheritDoc} + */ + public function resolve(Request $request, $path, $filter) + { + $browserPath = $this->decodeBrowserPath($this->getBrowserPath($path, $filter)); + $targetPath = $this->getFilePath($path, $filter, $request->getBaseUrl()); + + // if the file has already been cached, we're probably not rewriting + // correctly, hence make a 301 to proper location, so browser remembers + if (file_exists($targetPath)) { + return new RedirectResponse($request->getBasePath().$browserPath); + } + + return $targetPath; + } + + /** + * {@inheritDoc} + */ + public function getBrowserPath($targetPath, $filter, $absolute = false) + { + $params = array('path' => ltrim($targetPath, '/')); + + return str_replace( + urlencode($params['path']), + urldecode($params['path']), + $this->cacheManager->getRouter()->generate('_imagine_'.$filter, $params, $absolute) + ); + } + + /** + * {@inheritDoc} + */ + public function remove($targetPath, $filter) + { + $filename = $this->getFilePath($targetPath, $filter); + $this->filesystem->remove($filename); + + return file_exists($filename); + } + + /** + * Return the local filepath. + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException * - * @param Request $request - * @param string $path - * @param string $filter + * @param string $path The resource path to convert. + * @param string $filter The name of the imagine filter. + * @param string $basePath An optional base path to remove from the path. * - * @return string target path + * @return string */ - public function resolve(Request $request, $path, $filter) + protected function getFilePath($path, $filter, $basePath = '') { - //TODO: find out why I need double urldecode to get a valid path - $browserPath = urldecode(urldecode($this->cacheManager->getBrowserPath($path, $filter))); + $browserPath = $this->decodeBrowserPath($this->getBrowserPath($path, $filter)); - // if cache path cannot be determined, return 404 + // if cache path cannot be determined, return 404 if (null === $browserPath) { throw new NotFoundHttpException('Image doesn\'t exist'); } - $basePath = $request->getBaseUrl(); - if (!empty($basePath) && 0 === strpos($browserPath, $basePath)) { - $browserPath = substr($browserPath, strlen($basePath)); + if (!empty($basePath) && 0 === strpos($browserPath, $basePath)) { + $browserPath = substr($browserPath, strlen($basePath)); } - $targetPath = $this->cacheManager->getWebRoot().$browserPath; - - // if the file has already been cached, we're probably not rewriting - // correctly, hence make a 301 to proper location, so browser remembers - if (file_exists($targetPath)) { - return new RedirectResponse($request->getBasePath().$browserPath); - } + return $this->cacheManager->getWebRoot().$browserPath; + } - return $targetPath; + /** + * Decodes the URL encoded browser path. + * + * @param string $browserPath + * + * @return string + */ + protected function decodeBrowserPath($browserPath) + { + //TODO: find out why I need double urldecode to get a valid path + return urldecode(urldecode($browserPath)); } } diff --git a/README.md b/README.md index c8f4208a8..5dd804b37 100644 --- a/README.md +++ b/README.md @@ -163,26 +163,26 @@ web_profiler: The default configuration for the bundle looks like this: ``` yaml -liip_imagine: - driver: gd - web_root: %kernel.root_dir%/../web - data_root: %liip_imagine.web_root% - cache_prefix: /media/cache - cache: web_path - data_loader: filesystem - controller_action: liip_imagine.controller:filterAction - formats: [] - filter_sets: +liip_imagine: + driver: gd + web_root: %kernel.root_dir%/../web + data_root: %liip_imagine.web_root% + cache_prefix: /media/cache + cache: web_path + data_loader: filesystem + controller_action: liip_imagine.controller:filterAction + formats: [] + filter_sets: # Prototype - name: - path: ~ - quality: 100 - format: ~ - cache: ~ - data_loader: ~ - controller_action: ~ - filters: + name: + path: ~ + quality: 100 + format: ~ + cache: ~ + data_loader: ~ + controller_action: ~ + filters: # Prototype name: [] @@ -332,7 +332,7 @@ For an example of a filter loader implementation, refer to ## Outside the web root When your setup requires your source images to live outside the web root, or if that's just the way you roll, -you can override the DataLoader service and define a custom path, as the third argument, that replaces +you can override the DataLoader service and define a custom path, as the third argument, that replaces `%liip_imagine.web_root%` (example here in XML): ``` xml @@ -344,13 +344,13 @@ you can override the DataLoader service and define a custom path, as the third a ``` -One way to override a service is by redefining it in the services configuration file of your bundle. +One way to override a service is by redefining it in the services configuration file of your bundle. Another way would be by modifying the service definition from your bundle's Dependency Injection Extension: ``` php $container->getDefinition('liip_imagine.data.loader.filesystem') ->replaceArgument(2, '%kernel.root_dir%/data/uploads'); -``` +``` ## Custom image loaders @@ -426,7 +426,7 @@ to `Liip\ImagineBundle\Imagine\Data\Transformer\PdfTransformer` as an example. ExtendedFileSystemLoader extends FileSystemLoader and takes, as argument, an array of transformers. In the example, when a file with the pdf extension is passed to the data loader, -PdfTransformer uses a php imagick object (injected via the service container) +PdfTransformer uses a php imagick object (injected via the service container) to extract the first page of the document and returns it to the data loader as a png image. To tell the bundle about the transformers, you have to register them as services @@ -444,7 +444,7 @@ services: class: Acme\ImagineBundle\Imagine\Data\Loader\MyCustomDataLoader tags: - { name: liip_imagine.data.loader, loader: custom_data_loader } - arguments: + arguments: - '@liip_imagine' - %liip_imagine.formats% - %liip_imagine.data_root% @@ -500,6 +500,57 @@ liip_imagine: For an example of a cache resolver implementation, refer to `Liip\ImagineBundle\Imagine\Cache\Resolver\WebPathResolver`. +### AmazonS3Resolver + +The AmazonS3Resolver requires the [aws-sdk-php](https://github.com/amazonwebservices/aws-sdk-for-php). + +You can add the SDK by adding those lines to your `deps` file. + +``` ini +[aws-sdk] + git=git://github.com/amazonwebservices/aws-sdk-for-php.git +``` + +Afterwards, you only need to configure some information regarding your AWS account and the bucket. + +``` yaml +parameters: + amazon_s3.key: 'your-aws-key' + amazon_s3.secret: 'your-aws-secret' + amazon_s3.bucket: 'your-bucket.example.com' +``` + +Now you can set up the services required: + +``` yaml +services: + acme.amazon_s3: + class: AmazonS3 + arguments: + - + key: %amazon_s3.key% + secret: %amazon_s3.secret% + # more S3 specific options, see \AmazonS3::__construct() + + acme.imagine.cache.resolver.amazon_s3: + class: Liip\ImagineBundle\Imagine\Cache\Resolver\AmazonS3Resolver + arguments: + - "@acme.amazon_s3" + - "%amazon_s3.bucket%" + tags: + - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' } +``` + +Now you are ready to use the `AmazonS3Resolver` by configuring the bundle. +The following example will configure the resolver is default. + +``` yaml +liip_imagine: + cache: 'amazon_s3' +``` + +If you want to use other buckets for other images, simply alter the parameter names and create additional services! + ## Dynamic filters With a custom data loader it is possible to dynamically modify the configuration that will