Skip to content

Instantly share code, notes, and snippets.

@mirie
Created September 22, 2016 00:20
Show Gist options
  • Save mirie/45f75fe58479c142a4cf54e9a2559fe6 to your computer and use it in GitHub Desktop.
Save mirie/45f75fe58479c142a4cf54e9a2559fe6 to your computer and use it in GitHub Desktop.
flysystem classes
<?php
namespace Drupal\flysystem_s3;
use Aws\CacheInterface;
use Drupal\Core\Cache\CacheBackendInterface;
/**
* A Drupal cache adapter for use with the AWS PHP SDK.
*/
class AwsCacheAdapter implements CacheInterface {
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
private $cache;
/**
* The cache prefix.
*
* @var string
*/
private $prefix;
/**
* Constructs an AwsCacheAdapter object.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The Drupal cache backend.
* @param string $prefix
* (Optional) The prefix to use for cache items. Defaults to an empty
* string.
*/
public function __construct(CacheBackendInterface $cache, $prefix = '') {
$this->cache = $cache;
$this->prefix = $prefix;
}
/**
* {@inheritdoc}
*/
public function get($key) {
if ($item = $this->cache->get($this->prefix . $key)) {
return $item->data;
}
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = 0) {
$ttl = (int) $ttl;
$ttl = $ttl === 0 ? CacheBackendInterface::CACHE_PERMANENT : time() + $ttl;
$this->cache->set($this->prefix . $key, $value, $ttl);
}
/**
* {@inheritdoc}
*/
public function remove($key) {
$this->cache->delete($this->prefix . $key);
}
}
<?php
namespace Drupal\flysystem\Controller;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\flysystem\ImageStyleCopier;
use Drupal\image\ImageStyleInterface;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* Defines an image style controller that serves from temporary, then redirects.
*/
class ImageStyleRedirectController extends ImageStyleDownloadController {
/**
* The file entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $fileStorage;
/**
* The file system.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The image style copier.
*
* @var \Drupal\flysystem\ImageStyleCopier
*/
protected $imageStyleCopier;
/**
* Constructs an ImageStyleRedirectController.
*
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
* @param \Drupal\Core\Entity\EntityStorageInterface $file_storage
* The file entity storage.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system.
* @param \Drupal\flysystem\ImageStyleCopier $image_style_copier
* The image style copier.
*/
public function __construct(LockBackendInterface $lock, ImageFactory $image_factory, EntityStorageInterface $file_storage, FileSystemInterface $file_system, ImageStyleCopier $image_style_copier) {
parent::__construct($lock, $image_factory);
$this->fileStorage = $file_storage;
$this->fileSystem = $file_system;
$this->imageStyleCopier = $image_style_copier;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('lock'),
$container->get('image.factory'),
$container->get('entity.manager')->getStorage('file'),
$container->get('file_system'),
$container->get('flysystem_image_style_copier')
);
}
/**
* {@inheritdoc}
*/
public function deliver(Request $request, $scheme, ImageStyleInterface $image_style) {
$target = $request->query->get('file');
$source_uri = $scheme . '://' . $target;
$this->validateRequest($request, $image_style, $scheme, $target);
// Don't try to generate file if source is missing.
try {
$source_uri = $this->validateSource($source_uri);
}
catch (FileNotFoundException $e) {
$derivative_uri = $image_style->buildUri($source_uri);
$this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', ['%source_image_path' => $source_uri, '%derivative_path' => $derivative_uri]);
return new Response($this->t('Error generating image, missing source file.'), 404);
}
// If the image already exists on the adapter, deliver it instead.
try {
return $this->redirectAdapterImage($source_uri, $image_style);
}
catch (FileNotFoundException $e) {
return $this->deliverTemporary($scheme, $target, $image_style);
}
}
/**
* Generate a temporary image for an image style.
*
* @param string $scheme
* The file scheme, defaults to 'private'.
* @param string $source_path
* The image file to generate the temporary image for.
* @param \Drupal\image\ImageStyleInterface $image_style
* The image style to generate.
*
* @throws \RuntimeException
* Thrown when generate() failed to generate an image.
*
* @return \Drupal\file\Entity\File
* The temporary image that was generated.
*/
protected function generateTemporaryImage($scheme, $source_path, ImageStyleInterface $image_style) {
$image_uri = "$scheme://$source_path";
$destination_temp = $image_style->buildUri("temporary://flysystem/$scheme/$source_path");
// Try to generate the temporary image, watching for other threads that may
// also be trying to generate the temporary image.
try {
$success = $this->generate($image_style, $image_uri, $destination_temp);
if (!$success) {
throw new \RuntimeException('The temporary image could not be generated');
}
}
catch (ServiceUnavailableHttpException $e) {
// This exception is only thrown if the lock could not be acquired.
$tries = 0;
do {
if (file_exists($destination_temp)) {
break;
}
// The file still doesn't exist.
usleep(250000);
$tries++;
} while ($tries < 4);
// We waited for more than 1 second for the temporary image to appear.
// Since local image generation should be fast, fail out here to try to
// limit PHP process demands.
if ($tries >= 4) {
throw $e;
}
}
return $destination_temp;
}
/**
* Flushes the output buffer and copies the temporary images to the adapter.
*/
protected function flushCopy() {
// We have to call both of these to actually flush the image.
Response::closeOutputBuffers(0, TRUE);
flush();
$this->imageStyleCopier->processCopyTasks();
}
/**
* Redirects to to an adapter hosted image, if it exists.
*
* @param string $source_uri
* The URI to the source image.
* @param \Drupal\image\ImageStyleInterface $image_style
* The image style to redirect to.
*
* @throws \Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException
* Thrown if the derivative does not exist on the adapter.
*
* @return \Drupal\Core\Routing\TrustedRedirectResponse
* A redirect to the image if it exists.
*/
protected function redirectAdapterImage($source_uri, ImageStyleInterface $image_style) {
$derivative_uri = $image_style->buildUri($source_uri);
if (file_exists($derivative_uri)) {
// We can't just return TrustedRedirectResponse because core throws an
// exception about missing cache metadata.
// https://www.drupal.org/node/2638686
// https://www.drupal.org/node/2630808
// http://drupal.stackexchange.com/questions/187086/trustedresponseredirect-failing-how-to-prevent-cache-metadata
// @todo Figure out why caching this response leads to stale images being
// served.
// $url = Url::fromUri($image_style->buildUrl($source_uri))->toString(TRUE);
// $response = new TrustedRedirectResponse($url->getGeneratedUrl());
// $response->addCacheableDependency($url);
$response = new TrustedRedirectResponse($image_style->buildUrl($source_uri));
$response->addCacheableDependency(0);
return $response;
}
throw new FileNotFoundException(sprintf('%derivative_uri does not exist', $derivative_uri));
}
/**
* Delivers a generate an image, deliver it, and upload it to the adapter.
*
* @param string $scheme
* The scheme of the source image.
* @param string $source_path
* The path of the source image.
* @param \Drupal\image\ImageStyleInterface $image_style
* The image style to generate.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
* The image response, or an error response if image generation failed.
*/
protected function deliverTemporary($scheme, $source_path, ImageStyleInterface $image_style) {
$source_uri = $scheme . '://' . $source_path;
$derivative_uri = $image_style->buildUri($source_uri);
try {
$temporary_uri = $this->generateTemporaryImage($scheme, $source_path, $image_style);
}
catch (\RuntimeException $e) {
$this->logger->notice('Unable to generate the derived image located at %path.', ['%path' => $derivative_uri]);
return new Response($this->t('Error generating image.'), 500);
}
// Register a copy task with the kernel terminate handler.
$this->imageStyleCopier->addCopyTask($temporary_uri, $source_uri, $image_style);
// Symfony's kernel terminate handler is documented to only executes after
// flushing with fastcgi, and not with mod_php or regular CGI. However,
// it appears to work with mod_php. We assume it doesn't and register a
// shutdown handler unless we know we are under fastcgi. If images have
// been previously flushed and uploaded, this call will do nothing.
//
// https://github.com/symfony/symfony-docs/issues/6520
if (!function_exists('fastcgi_finish_request')) {
drupal_register_shutdown_function(function () {
$this->flushCopy();
});
}
return $this->send($scheme, $temporary_uri);
}
}
<?php
namespace Drupal\flysystem_s3\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\file\Entity\File;
use Drupal\flysystem\FlysystemFactory;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Aws\S3\PostObjectV4;
/**
* Defines a controller to respond to S3 CORS upload AJAX requests.
*/
class S3CorsUploadAjaxController extends ControllerBase {
/**
* The form builder.
*
* @var \Drupal\flysystem\FlysystemFactory
*/
protected $flysystemFactory;
/**
* The file system.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('flysystem_factory'),
$container->get('file_system')
);
}
/**
* Constructs an S3CorsUploadAjaxController object.
*
* @param \Drupal\flysystem\FlysystemFactory $flysystem_factory
* The Flysystem factory.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system.
*/
public function __construct(FlysystemFactory $flysystem_factory, FileSystemInterface $file_system) {
$this->flysystemFactory = $flysystem_factory;
$this->fileSystem = $file_system;
}
/**
* Returns the signed request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JsonResponse object.
*/
public function signRequest(Request $request) {
$post = $request->request->all();
/** @var \Drupal\flysystem_s3\Flysystem\Adapter\S3Adapter $adapter */
$scheme = $this->fileSystem->uriScheme($post['destination']);
$adapter = $this->flysystemFactory->getPlugin($scheme)->getAdapter();
$client = $adapter->getClient();
$bucket = $adapter->getBucket();
$options = [
['acl' => $post['acl']],
['bucket' => $bucket],
['starts-with', '$key', file_uri_target($post['destination']) . '/'],
];
$uri = file_create_filename($post['filename'], $post['destination']);
$post['key'] = file_uri_target($uri);
// Create a temporary file to return with a file ID in the response.
$file = File::create([
'uri' => $uri,
'filesize' => $post['filesize'],
'filename' => $post['filename'],
'filemime' => $post['filemime'],
'uid' => \Drupal::currentUser()->getAccount()->id(),
]);
$file->save();
// Remove values not necessary for the request to Amazon.
unset($post['destination']);
unset($post['filename']);
unset($post['filemime']);
unset($post['filesize']);
// @todo Make this interval configurable.
$expiration = '+5 hours';
$postObject = new PostObjectV4($client, $bucket, $post, $options, $expiration);
$data = [];
$data['attributes'] = $postObject->getFormAttributes();
$data['inputs'] = $postObject->getFormInputs();
$data['options'] = $options;
$data['fid'] = $file->id();
return new JsonResponse($data);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment