Use power of symfony binary file response

Add commands

Fix typo

Remove extra line
This commit is contained in:
Nicolas Le Goff
2013-06-26 15:37:51 +02:00
committed by Romain Neutron
parent 7fc9eb3010
commit 01a36ee9f7
16 changed files with 423 additions and 118 deletions

View File

@@ -99,6 +99,7 @@ use Alchemy\Phrasea\Core\Provider\TaskManagerServiceProvider;
use Alchemy\Phrasea\Core\Provider\TemporaryFilesystemServiceProvider;
use Alchemy\Phrasea\Core\Provider\TokensServiceProvider;
use Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider;
use Alchemy\Phrasea\Core\Provider\XSendFileMappingServiceProvider;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Twig\JSUniqueID;
use Alchemy\Phrasea\Twig\Camelize;
@@ -306,6 +307,12 @@ class Application extends SilexApplication
$this->register(new ValidatorServiceProvider());
$this->register(new XPDFServiceProvider());
$this->register(new XSendFileMappingServiceProvider(), array(
'xsendfile.mapping' => array(
$this['root.path'] . '/tmp/download/' => '/download/',
$this['root.path'] . '/tmp/lazaret/' => '/lazaret/'
)
));
$this->register(new FileServeServiceProvider());
$this['phraseanet.exception_handler'] = $this->share(function ($app) {

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Setup;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command dumps XsendFile Apache condifuration
*/
class XSendFileMappingApacheDumper extends Command
{
public function __construct()
{
parent::__construct('xsendfile:dump-apache');
$this->setDescription('Dump XSendFile mapping for Apache web server');
}
/**
* {@inheritdoc}
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$mapper = $this->container['phraseanet.xsendfile-mapping'];
$output->writeln('<info>Apache XSendfile configuration</info>');
$output->writeln('');
$output->writeln('<IfModule mod_xsendfile.c>');
$output->writeln(' <Files *>');
$output->writeln(' XSendFile on');
foreach ($this->container['phraseanet.xsendfile-mapping']->getMapping() as $entry) {
$output->writeln(' XSendFilePath ' . $mapper->sanitizePath($entry['directory']));
}
$output->writeln(' </Files>');
$output->writeln('</IfModule>');
$output->writeln('');
return 1;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Setup;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command dumps XSendFile Nginx configuration
*/
class XSendFileMappingNginxDumper extends Command
{
public function __construct()
{
parent::__construct('xsendfile:dump-nginx');
$this->setDescription('Dump xsendfile mapping for Nginx and Apache web server');
}
/**
* {@inheritdoc}
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$mapper = $this->container['phraseanet.xsendfile-mapping'];
$output->writeln('<info>Nginx XSendfile configuration</info>');
$output->writeln('');
foreach ($this->container['phraseanet.xsendfile-mapping']->getMapping() as $entry) {
$output->writeln(' location ' . $mapper->sanitizeMountPoint($entry['mount-point']) . ' {');
$output->writeln(' internal;');
$output->writeln(' alias ' . $mapper->sanitizePath($entry['directory']));
$output->writeln(' }');
$output->writeln('');
}
return 1;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Event\Subscriber;
use Silex\Application;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class XSendFileSubscriber implements EventSubscriberInterface
{
private $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array('applyHeaders', 16),
);
}
public function applyHeaders(GetResponseEvent $event)
{
if ($this->app['phraseanet.configuration']['xsendfile']['enable']) {
$request = $event->getRequest();
$request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', (string) $this->app['phraseanet.xsendfile-mapping']);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Alchemy\Phrasea\XSendFile\Mapping;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Silex\Application;
use Silex\ServiceProviderInterface;
class XSendFileMappingServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
if (!isset($app['xsendfile.mapping'])) {
$app['xsendfile.mapping'] = array();
}
if (!is_array($app['xsendfile.mapping'])) {
throw new InvalidArgumentException('XSendFile mapping must be an array');
}
$app['phraseanet.xsendfile-mapping'] = $app->share(function($app) {
$mapping = array();
foreach($app['xsendfile.mapping'] as $path => $mountPoint) {
$mapping[] = array(
'directory' => $path,
'mount-point' => $mountPoint,
);
}
return Mapping::create($app, $mapping);
});
}
public function boot(Application $app)
{
}
}

View File

@@ -13,32 +13,17 @@ namespace Alchemy\Phrasea\Response;
use Alchemy\Phrasea\Response\DeliverDataInterface;
use Alchemy\Phrasea\Application;
use Psr\Log\LoggerInterface,
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ServeFileResponseFactory implements DeliverDataInterface
{
private $xSendFileEnable = false;
private $mappings;
private $unicode;
private $logger;
public function __construct($enableXSendFile, $xAccelMappings, \unicode $unicode, LoggerInterface $logger = null)
public function __construct($enableXSendFile, \unicode $unicode)
{
$this->logger = $logger;
$this->xSendFileEnable = $enableXSendFile;
$mappings = array();
foreach ($xAccelMappings as $path => $mountPoint) {
if (is_dir($path) && '' !== $mountPoint) {
$mappings[$this->sanitizeXAccelPath($path)] = $this->sanitizeXAccelMountPoint($mountPoint);
}
}
$this->mappings = $mappings;
$this->unicode = $unicode;
}
@@ -49,12 +34,9 @@ class ServeFileResponseFactory implements DeliverDataInterface
public static function create(Application $app)
{
return new self(
$app['phraseanet.registry']->get('GV_modxsendfile'),
array(
$app['phraseanet.registry']->get('GV_X_Accel_Redirect') => $app['phraseanet.registry']->get('GV_X_Accel_Redirect_mount_point'),
$app['root.path'] . '/tmp/download/' => '/download/',
$app['root.path'] . '/tmp/lazaret/' => '/lazaret/'
), new \unicode(), $app['logger']);
$app['phraseanet.configuration']['xsendfile']['enable'],
$app['unicode']
);
}
/**
@@ -66,11 +48,7 @@ class ServeFileResponseFactory implements DeliverDataInterface
$response->setContentDisposition($disposition, $this->sanitizeFilename($filename), $this->sanitizeFilenameFallback($filename));
if ($this->isXSendFileEnable()) {
if ($this->isMappedFile($file)) {
$response->headers->set('X-Accel-Redirect', $this->xAccelRedirectMapping($file));
} else if (null !== $this->logger) {
$this->logger->warning(sprintf('%s is not located under a nginx xAccelPath'));
}
BinaryFileResponse::trustXSendfileTypeHeader();
}
if (null !== $mimeType) {
@@ -104,16 +82,6 @@ class ServeFileResponseFactory implements DeliverDataInterface
return $this->xSendFileEnable;
}
private function sanitizeXAccelPath($path)
{
return sprintf('%s/', rtrim($path, '/'));
}
private function sanitizeXAccelMountPoint($mountPoint)
{
return sprintf('/%s/', rtrim(ltrim($mountPoint, '/'), '/'));
}
private function sanitizeFilename($filename)
{
return str_replace(array('/', '\\'), '', $filename);
@@ -123,20 +91,4 @@ class ServeFileResponseFactory implements DeliverDataInterface
{
return $this->unicode->remove_nonazAZ09($filename, true, true, true);
}
private function xAccelRedirectMapping($file)
{
return str_replace(array_keys($this->mappings), array_values($this->mappings), $file);
}
private function isMapped($file)
{
foreach (array_keys($this->mappings) as $path) {
if (false !== strpos($file, $path)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\XSendFile;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
class Mapping
{
private $mapping;
/**
* @param array $mapping
* @throws \InvalidArgumentException
*/
public function __construct(array $mapping)
{
$this->validate($mapping);
$this->mapping = $mapping;
}
public function __toString()
{
$final = array();
foreach($this->mapping as $entry) {
if (!is_dir($entry['directory']) || '' === $entry['mount-point']) {
continue;
}
$final[] = sprintf('%s=%s', $this->sanitizeMountPoint($entry['mount-point']), $this->sanitizePath(realpath($entry['directory'])));
}
return implode(',', $final);
}
public function getMapping()
{
return $this->mapping;
}
public static function create(Application $app, array $mapping = array())
{
if (isset($app['phraseanet.configuration']['xsendfile']['mapping'])) {
$confMapping = $app['phraseanet.configuration']['xsendfile']['mapping'];
if (!is_array($confMapping)) {
throw new InvalidArgumentException('XSendFile mapping configuration must be an array');
}
foreach($confMapping as $entry) {
$mapping[] = $entry;
}
}
return new Mapping($mapping);
}
public function sanitizePath($path)
{
return sprintf('/%s', rtrim(ltrim($path, '/'),'/'));
}
public function sanitizeMountPoint($mountPoint)
{
return sprintf('/%s', rtrim(ltrim($mountPoint, '/'), '/'));
}
private function validate(array $mapping)
{
foreach($mapping as $entry) {
if (!is_array($entry)) {
throw new \InvalidArgumentException('XSendFile mapping entry must be an array');
}
if (!isset($entry['directory']) && !isset($entry['mount-point'])) {
throw new \InvalidArgumentException('XSendFile mapping entry must contain at least two keys "directory" and "mounbt-point"');
}
}
}
}