version 1.0

This commit is contained in:
2021-07-07 10:24:05 +02:00
commit 941a149097
17 changed files with 1069 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
<?php
namespace Bluesquare\StorageBundle\Adaptors;
use Aws\S3\S3Client;
use Bluesquare\StorageBundle\Exceptions\InvalidStorageConfiguration;
use Symfony\Component\Config\Definition\Exception\Exception;
/**
* Interface de manipulation du stockage S3
* Usage: $storage = new S3Storage('my_storage_name', $config);
* @author Maxime Renou
*/
class S3Storage implements StorageAdaptor
{
const MODE_PRIVATE = 'private';
const MODE_PUBLIC = 'public-read';
protected $client;
protected $bucket;
protected $region;
public function __construct ($storage_name, $config)
{
if (!($this->configIsNormed($config)))
throw new InvalidStorageConfiguration("Invalid $storage_name storage configuration");
$this->config = $config;
$this->bucket = $config['bucket'];
$this->bucket_url = $config['bucket_url'];
$this->client = new S3Client([
'version' => isset($config['version']) ? $config['version'] : 'latest',
'region' => $config['region'],
'endpoint' => $config['endpoint'],
'credentials' => [
'key' => $config['credentials']['key'],
'secret' => $config['credentials']['secret']
]
]);
}
public function getClient()
{
return $this->client;
}
public function mode($name)
{
if ($name == 'public') return self::MODE_PUBLIC;
return self::MODE_PRIVATE;
}
private function configIsNormed($config)
{
return (
isset($config['type']) &&
isset($config['bucket']) &&
isset($config['bucket_url']) &&
isset($config['region']) &&
isset($config['endpoint']) &&
isset($config['credentials']) &&
isset($config['credentials']['key']) &&
isset($config['credentials']['secret'])
);
}
protected function getPrefix($prefix = null)
{
$ret = '';
if (isset($this->config['path']) && !empty($this->config['path']))
{
$ret = trim($this->config['path'], '/').'/';
if (trim($ret) == '/') $ret = '';
}
if (!is_null($prefix)) {
$ret = ltrim($prefix, '/');
}
return $ret;
}
public function index($prefix = null)
{
return ($this->client->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => $this->getPrefix($prefix)
]));
}
/**
* Permet d'obtenir l'URL vers une ressource S3
* Cette ressource n'est accessible que si le fichier a été stocké en mode public
* @param $target_path
* @return string
*/
public function url ($target_path)
{
return rtrim($this->bucket_url, '/').'/'.$this->getPrefix().ltrim($target_path, '/');
}
/**
* Permet de stocker un fichier dans S3
* @param $source_path
* @param $target_path
* @param string $permissions
* @return \Aws\Result
*/
public function store ($source_path, $target_path, $permissions = self::MODE_PRIVATE)
{
return $this->client->putObject([
'Bucket' => $this->bucket,
'Path' => $this->getPrefix().$target_path,
'Key' => $this->getPrefix().$target_path,
'SourceFile' => $source_path, // Fix memory allocation
//'Body' => file_get_contents($source_path),
'ACL' => $permissions
]);
}
/**
* Permet de récupérer un fichier dans S3 pour le stocker en local
* @param $distant_path
* @param $local_path
*/
public function retrieve ($distant_path, $local_path)
{
$file_stream = fopen($local_path, 'w');
$aws_stream = $this->client->getObject([
'Bucket' => $this->bucket,
'Key' => $this->getPrefix().$distant_path,
'Path' => $this->getPrefix().$distant_path,
])->get('Body')->detach();
stream_copy_to_stream($aws_stream, $file_stream);
fclose($file_stream);
}
/**
* Ouvre le stream d'un fichier stocké dans S3
* @param $distant_path
* @param $target_stream
*/
public function getStream ($distant_path)
{
return $this->client->getObject([
'Bucket' => $this->bucket,
'Key' => $this->getPrefix().$distant_path,
'Path' => $this->getPrefix().$distant_path,
])->get('Body')->detach();
}
/**
* Permet de stream un fichier stocké dans S3
* @param $distant_path
* @param $target_stream
*/
public function stream ($distant_path, $target_stream)
{
stream_copy_to_stream($this->getStream($distant_path), $target_stream);
}
/**
* Permet de supprimer un fichier stocké dans S3
* @param $distant_path
*/
public function delete ($distant_path)
{
$this->client->deleteObject([
'Bucket' => $this->bucket,
'Path' => $this->getPrefix().$distant_path,
'Key' => $this->getPrefix().$distant_path,
]);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Bluesquare\StorageBundle\Adaptors;
interface StorageAdaptor
{
public function index();
public function mode($name);
public function store($source_path, $target_path);
public function retrieve($distant_path, $local_path);
public function stream($distant_path, $target_stream);
public function delete($distant_path);
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Bluesquare\StorageBundle\Annotations;
use Doctrine\ORM\Mapping as ORM;
/**
* @Annotation
*/
class Storage implements ORM\Annotation
{
/**
* @var string
*/
public $name;
/**
* @var mixed
*/
public $prefix = null;
/**
* @var mixed
*/
public $mode = null;
/**
* @var mixed
*/
public $mime = null;
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Bluesquare\StorageBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree builder.
*
* @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('storage');
$root = $treeBuilder->root('storage');
$root->useAttributeAsKey('storage_name')
->prototype('array')
->children()
->scalarNode('type')->isRequired()->cannotBeEmpty()->end()
->scalarNode('bucket')->end()
->scalarNode('bucket_url')->end()
->scalarNode('region')->end()
->scalarNode('endpoint')->end()
->arrayNode('credentials')
->children()
->scalarNode('key')->end()
->scalarNode('secret')->end()
->end()
->end()
->scalarNode('version')->end()
->scalarNode('path')->end()
->end()
->end()
->end();
return ($treeBuilder);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Bluesquare\StorageBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class StorageExtension extends Extension
{
/**
* Loads a specific configuration.
*
* @throws \InvalidArgumentException When provided tag is not defined in this extension
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Ressources/config'));
$loader->load('services.yaml');
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$definition = $container->getDefinition('bluesquare.storage');
$definition->setArgument(0, $config);
return $config;
}
public function getAlias()
{
// return ('bluesquare\\storage');
return parent::getAlias(); // TODO: Change the autogenerated stub
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bluesquare\StorageBundle\Exceptions;
class InvalidFileException extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bluesquare\StorageBundle\Exceptions;
class InvalidStorageConfiguration extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bluesquare\StorageBundle\Exceptions;
class MimeTypeException extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bluesquare\StorageBundle\Exceptions;
class MissingStorageAnnotation extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bluesquare\StorageBundle\Exceptions;
class UnknownStorage extends \Exception
{
}

View File

@@ -0,0 +1,2 @@
imports:
- { ressource: '%kernel.root_dir%/config/packages/bluesquare/storage.yaml' }

View File

@@ -0,0 +1,7 @@
services:
bluesquare.storage:
class: Bluesquare\StorageBundle\Storage
autowire: true
public: true
arguments: ['user_storage']
Bluesquare\StorageBundle\Storage: '@bluesquare.storage'

166
StorageBundle/Storage.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
namespace Bluesquare\StorageBundle;
use Aws\S3\S3Client;
use Bluesquare\StorageBundle\Adaptors\S3Storage;
use Bluesquare\StorageBundle\Exceptions\InvalidFileException;
use Bluesquare\StorageBundle\Exceptions\MimeTypeException;
use Bluesquare\StorageBundle\Exceptions\MissingStorageAnnotation;
use Bluesquare\StorageBundle\Exceptions\UnknownStorage;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Bluesquare\StorageBundle\Annotations\Storage as StorageAnnotation;
use Symfony\Component\DependencyInjection\Container;
use Doctrine\Common\Annotations\AnnotationReader;
/**
* Interface de manipulation des stockages préconfigurés
* Usage par injection
*/
class Storage
{
private $config_storage = [];
public function __construct(array $user_config = [])
{
// dump($user_config); die;
$this->config_storage = $user_config;
}
public function get($storage_name)
{
if (array_key_exists($storage_name, $this->config_storage))
{
$config = $this->config_storage[$storage_name];
switch ($config['type'])
{
case 's3': return (new S3Storage($storage_name, $config));
}
}
return (null);
}
protected function getStorageAnnotation($entity, $attribute)
{
$reflection = new \ReflectionProperty($entity, $attribute);
$reader = new AnnotationReader();
$annotations = $reader->getPropertyAnnotations($reflection);
$storage_annotation = null;
foreach ($annotations as $annotation)
{
if ($annotation instanceof StorageAnnotation) {
$storage_annotation = $annotation;
}
}
if (is_null($storage_annotation))
{
throw new MissingStorageAnnotation("Missing Storage annotation for $attribute in ".get_class($entity));
}
return $storage_annotation;
}
public function getStorageFor($entity, $attribute)
{
$annotation = $this->getStorageAnnotation($entity, $attribute);
return $annotation->name;
}
public function url($entity, $attribute, $class = null)
{
$annotation = $this->getStorageAnnotation(!is_null($class) ? $class : $entity, $attribute);
$storage = $this->get($annotation->name);
$prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/';
$camel = ucfirst(Container::camelize($attribute));
return $storage->url("$prefix{$entity->{"get".$camel}()}");
}
public function delete($entity, $attribute)
{
$annotation = $this->getStorageAnnotation($entity, $attribute);
$storage = $this->get($annotation->name);
$prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/';
$camel = ucfirst(Container::camelize($attribute));
return $storage->delete("$prefix{$entity->{"get".$camel}()}");
}
public function retrieve($entity, $attribute, $local_path)
{
$annotation = $this->getStorageAnnotation($entity, $attribute);
$storage = $this->get($annotation->name);
$prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/';
$camel = ucfirst(Container::camelize($attribute));
return $storage->retrieve("$prefix{$entity->{"get".$camel}()}", $local_path);
}
public function stream($entity, $attribute, $target_stream = null)
{
$annotation = $this->getStorageAnnotation($entity, $attribute);
$storage = $this->get($annotation->name);
$prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/';
$camel = ucfirst(Container::camelize($attribute));
if (is_null($target_stream)) {
return $storage->getStream("$prefix{$entity->{"get".$camel}()}");
}
return $storage->stream("$prefix{$entity->{"get".$camel}()}", $target_stream);
}
public function store($entity, $attribute, $file)
{
$storage_annotation = $this->getStorageAnnotation($entity, $attribute);
$file_hash = hash('sha256', time().$attribute.uniqid());
$storage = $this->get($storage_annotation->name);
if (is_null($storage))
{
throw new UnknownStorage("Unknown storage {$storage_annotation->name} for $attribute in ".get_class($entity));
}
$prefix = is_null($storage_annotation->prefix) || empty($storage_annotation->prefix) ? '' : trim($storage_annotation->prefix, '/').'/';
$mode = $storage->mode($storage_annotation->mode);
$camel = ucfirst(Container::camelize($attribute));
if ($file instanceof UploadedFile) {
$file_hash .= strlen($file->getClientOriginalExtension()) > 0 ? '.'.$file->getClientOriginalExtension() : '';
if (!is_null($storage_annotation->mime))
{
$valid = true;
if (count(explode('/', $storage_annotation->mime)) > 1) {
$valid = strtolower($storage_annotation->mime) == $file->getMimeType();
}
else {
$valid = strtolower($storage_annotation->mime) == explode('/', $file->getMimeType())[0];
}
if (!$valid) {
throw new MimeTypeException("Invalid mime type");
}
}
$storage->store($file->getRealPath(), "$prefix$file_hash", $mode);
$previous_file_hash = $entity->{"get$camel"}();
if (!is_null($previous_file_hash) && !empty($previous_file_hash)) {
$storage->delete("$prefix$previous_file_hash");
}
}
elseif (is_string($file) && file_exists($file)) {
$storage->store($file, "$prefix$file_hash", $mode);
}
else {
throw new InvalidFileException("Invalid file argument");
}
$entity->{"set$camel"}($file_hash);
return $file_hash;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Bluesquare\StorageBundle;
use Bluesquare\StorageBundle\DependencyInjection\StorageExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class StorageBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
}
public function getContainerExtension()
{
if (null === $this->extension)
$this->extension = new StorageExtension();
return $this->extension;
}
}