├── tests
├── Functional
│ ├── .gitignore
│ ├── routes.yml
│ ├── Twig
│ │ ├── PathResolver.php
│ │ ├── path_resolver_config.yml
│ │ ├── PathResolverTest.php
│ │ └── FilesExtensionTest.php
│ ├── Preview
│ │ ├── config_no_messenger.yml
│ │ ├── config_messenger.yml
│ │ ├── config.yml
│ │ ├── PreviewGeneratorMessengerTest.php
│ │ └── PreviewGeneratorTest.php
│ ├── Entity
│ │ ├── File.php
│ │ ├── EmbeddableFilePersistentPath.php
│ │ ├── Preview.php
│ │ ├── PersistentPathFile.php
│ │ ├── FileWithPreview.php
│ │ └── News.php
│ ├── Repository
│ │ ├── FileRepository.php
│ │ ├── PreviewRepository.php
│ │ ├── FileWithPreviewRepository.php
│ │ └── PersistentPathFileRepository.php
│ ├── config_base.yml
│ ├── config.yml
│ ├── LiipImagine
│ │ ├── config.yml
│ │ └── FileFilterPathResolverTest.php
│ ├── NamingStrategy
│ │ ├── PersistentPathStrategyTest.php
│ │ ├── PersistPathStrategy
│ │ │ └── config.yml
│ │ └── AbstractStrategyTest.php
│ ├── Kernel.php
│ └── AbstractFunctionalTest.php
├── files
│ ├── lorem-ipsum.txt
│ ├── image1.jpg
│ ├── image2.jpg
│ ├── lorem-ipsum.pdf
│ └── image1_preview.jpg
├── VirtualFile.php
├── StringableFile.php
├── File.php
├── File2.php
├── File3.php
├── MutableFile.php
├── FileRepository.php
├── NamingStrategy
│ ├── DateStrategyTest.php
│ ├── NamingStrategyTestCase.php
│ ├── SplitHashStrategyTest.php
│ ├── NullDirectoryStrategyTest.php
│ ├── AppendExtensionStrategyTest.php
│ ├── DirectoryChunkSplitStrategyTest.php
│ ├── UuidV4StrategyTest.php
│ ├── UuidV5StrategyTest.php
│ ├── AbstractStrategyTest.php
│ ├── PersistentPathStrategyTest.php
│ └── DirectoryPrefixStrategyTest.php
├── AbstractModelFactoryTest.php
├── PersistentPathFile.php
├── Repository
│ └── ORMTest.php
├── Model
│ └── DecoratedFileTest.php
├── PathResolver
│ ├── AssetsPathResolverTest.php
│ ├── CachePathResolverTest.php
│ ├── AwsS3PathResolverTest.php
│ ├── DelegatingPathResolverTest.php
│ ├── AwsPreSignedS3PathResolverTest.php
│ ├── AzureBlobStoragePathResolverTest.php
│ └── AzureBlobStorageSASParametersTest.php
├── DependencyInjection
│ └── StorageFactoryTest.php
├── Validator
│ └── Constraint
│ │ └── FileTest.php
├── FileDownloaderTest.php
├── Command
│ ├── MigrateNamingStrategyCommandTest.php
│ └── VerifyConsistencyCommandTest.php
├── Preview
│ └── DimensionTest.php
├── MigratorTest.php
├── Utility
│ └── DownloadUtilityTest.php
└── Form
│ └── Type
│ └── FileTypeTest.php
├── .idea
├── encodings.xml
├── codeStyles
│ └── codeStyleConfig.xml
├── misc.xml
├── vcs.xml
├── symfony2.xml
├── .gitignore
├── phpunit.xml
├── modules.xml
├── deployment.xml
├── php-test-framework.xml
├── runConfigurations
│ └── phpunit_xml_dist.xml
└── files.iml
├── src
├── Event
│ ├── PostMove.php
│ ├── PreMove.php
│ ├── PostUpdate.php
│ ├── PostUpload.php
│ ├── PreRemove.php
│ ├── PreUpdate.php
│ └── AbstractFileEvent.php
├── UnableToResolvePath.php
├── ArxyFilesBundle.php
├── Entity
│ ├── File.php
│ ├── EmbeddableFile.php
│ └── MutableFile.php
├── Model
│ ├── PathAwareFile.php
│ ├── MutablePathAware.php
│ ├── File.php
│ ├── MutableFile.php
│ ├── AbstractFile.php
│ └── DecoratedFile.php
├── Preview
│ ├── DimensionInterface.php
│ ├── PreviewGeneratorInterface.php
│ ├── GeneratePreviewMessage.php
│ ├── PreviewableFile.php
│ ├── NoPreviewGeneratorFound.php
│ ├── GeneratePreviewMessageHandler.php
│ ├── Dimension.php
│ ├── PreviewGeneratorMessengerListener.php
│ ├── PreviewGeneratorListener.php
│ ├── PreviewGenerator.php
│ └── ImagePreviewGenerator.php
├── MigratorInterface.php
├── PathResolver
│ ├── AzureBlobStorageSASParametersFactory.php
│ ├── AssetsPathResolver.php
│ ├── AwsS3PathResolver.php
│ ├── AzureBlobStoragePathResolver.php
│ ├── DelegatingPathResolver.php
│ ├── CachePathResolver.php
│ ├── AwsS3PreSignedPathResolver.php
│ └── AzureBlobStorageSASPathResolver.php
├── PathResolver.php
├── Utility
│ ├── FileUtility.php
│ ├── NamingStrategyUtility.php
│ ├── FileDownloader.php
│ ├── DownloadableFile.php
│ └── DownloadUtility.php
├── Repository.php
├── Twig
│ ├── PathResolverExtension.php
│ ├── FilesRuntime.php
│ ├── PathResolverRuntime.php
│ └── FilesExtension.php
├── NamingStrategy.php
├── ModelFactory.php
├── MigrateableStorage.php
├── LiipImagine
│ ├── FileFilter.php
│ └── FileFilterPathResolver.php
├── Resources
│ ├── config
│ │ └── doctrine
│ │ │ ├── MutableFile.orm.xml
│ │ │ ├── File.orm.xml
│ │ │ └── EmbeddableFile.orm.xml
│ └── views
│ │ └── form
│ │ └── file.html.twig
├── UnableToUpload.php
├── NamingStrategy
│ ├── UuidV4Strategy.php
│ ├── UuidV5Strategy.php
│ ├── NullDirectoryStrategy.php
│ ├── DateStrategy.php
│ ├── AppendExtensionStrategy.php
│ ├── PersistentPathStrategy.php
│ ├── SplitHashStrategy.php
│ ├── DirectoryPrefixStrategy.php
│ └── DirectoryChunkSplitStrategy.php
├── DependencyInjection
│ ├── StorageFactory.php
│ └── Configuration.php
├── AbstractModelFactory.php
├── Repository
│ └── ORM.php
├── Storage.php
├── Migrator.php
├── EventListener
│ ├── PathAwareListener.php
│ └── DoctrineORMListener.php
├── ErrorHandler.php
├── FileException.php
├── ManagerInterface.php
├── Form
│ ├── EventListener
│ │ └── FileUploadListener.php
│ └── Type
│ │ └── FileType.php
├── Command
│ └── MigrateNamingStrategyCommand.php
├── PathResolverManager.php
├── Validator
│ └── Constraint
│ │ ├── File.php
│ │ └── FileValidator.php
├── Storage
│ └── FlysystemStorage.php
├── FileMap.php
└── DelegatingManager.php
├── .github
├── workflows
│ ├── psalm.yml
│ ├── codeball.yml
│ ├── phpcs.yml
│ └── phpunit.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── infection.json.dist
├── psalm.xml.dist
├── .gitignore
├── LICENSE
├── phpunit.xml.dist
├── phpcs.xml
└── composer.json
/tests/Functional/.gitignore:
--------------------------------------------------------------------------------
1 | var
--------------------------------------------------------------------------------
/tests/files/lorem-ipsum.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolar sit amet
2 |
--------------------------------------------------------------------------------
/tests/files/image1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Warxcell/files/HEAD/tests/files/image1.jpg
--------------------------------------------------------------------------------
/tests/files/image2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Warxcell/files/HEAD/tests/files/image2.jpg
--------------------------------------------------------------------------------
/tests/files/lorem-ipsum.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Warxcell/files/HEAD/tests/files/lorem-ipsum.pdf
--------------------------------------------------------------------------------
/tests/Functional/routes.yml:
--------------------------------------------------------------------------------
1 | _liip_imagine:
2 | resource: "@LiipImagineBundle/Resources/config/routing.yaml"
--------------------------------------------------------------------------------
/tests/files/image1_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Warxcell/files/HEAD/tests/files/image1_preview.jpg
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/Event/PostMove.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/symfony2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/src/ArxyFilesBundle.php:
--------------------------------------------------------------------------------
1 | getId();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.idea/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/MigratorInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/PathResolver.php:
--------------------------------------------------------------------------------
1 | getHash();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/File.php:
--------------------------------------------------------------------------------
1 | id;
14 | }
15 |
16 | public function setId(?int $id): void
17 | {
18 | $this->id = $id;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Preview/PreviewGeneratorInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Model/File.php:
--------------------------------------------------------------------------------
1 | file = $file;
14 | }
15 |
16 | public function getFile(): PreviewableFile
17 | {
18 | return $this->file;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Repository.php:
--------------------------------------------------------------------------------
1 | id;
16 | }
17 |
18 | public function setId(?int $id): void
19 | {
20 | $this->id = $id;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/File3.php:
--------------------------------------------------------------------------------
1 | id;
16 | }
17 |
18 | public function setId(?int $id): void
19 | {
20 | $this->id = $id;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Twig/PathResolverExtension.php:
--------------------------------------------------------------------------------
1 | id;
16 | }
17 |
18 | public function setId(int $id): void
19 | {
20 | $this->id = $id;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Preview/PreviewableFile.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | interface MigrateableStorage extends Storage
14 | {
15 | /**
16 | * @param T $file
17 | * @throws FileException when file cannot be migrated for some reason
18 | */
19 | public function migrate(File $file, string $oldPathname, string $newPathname): bool;
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/psalm.yml:
--------------------------------------------------------------------------------
1 | name: Psalm Static analysis
2 |
3 | on: [ push, pull_request ]
4 |
5 | jobs:
6 | psalm:
7 | name: Psalm
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout code
11 | uses: actions/checkout@v2
12 |
13 | - name: Psalm
14 | uses: docker://vimeo/psalm-github-actions
15 | with:
16 | composer_require_dev: true
17 | composer_ignore_platform_reqs: false
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "./infection/infection.log",
9 | "summary": "./infection/summary.log",
10 | "json": "./infection/infection-log.json",
11 | "perMutator": "./infection/per-mutator.md",
12 | "github": true,
13 | "badge": {
14 | "branch": "master"
15 | }
16 | },
17 | "mutators": {
18 | "@default": true
19 | }
20 | }
--------------------------------------------------------------------------------
/tests/Functional/Preview/config_messenger.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - {resource: "config.yml"}
3 |
4 | framework:
5 | messenger: ~
6 |
7 | services:
8 | _defaults:
9 | public: true
10 | autowire: true # Automatically injects dependencies in your services.
11 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
12 |
13 | Arxy\FilesBundle\Preview\GeneratePreviewMessageHandler: ~
14 | Arxy\FilesBundle\Preview\PreviewGeneratorMessengerListener: ~
--------------------------------------------------------------------------------
/src/Utility/NamingStrategyUtility.php:
--------------------------------------------------------------------------------
1 | getDirectoryName($file) ?? "") . $namingStrategy->getFileName($file);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/phpunit_xml_dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/psalm.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/Functional/Entity/File.php:
--------------------------------------------------------------------------------
1 | id;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/LiipImagine/FileFilter.php:
--------------------------------------------------------------------------------
1 | filter = $filter;
18 | }
19 |
20 | public function getFilter(): string
21 | {
22 | return $this->filter;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Resources/config/doctrine/MutableFile.orm.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Preview/NoPreviewGeneratorFound.php:
--------------------------------------------------------------------------------
1 |
5 | {% trans %}Original filename{% endtrans %}: {{ form.vars.data.originalFilename }}
6 | {% trans %}Size{% endtrans %}: {{ form.vars.data.size|format_bytes }}
7 | {% trans %}Mime Type{% endtrans %}: {{ form.vars.data.mimeType }}
8 | {% trans %}Created{% endtrans %}: {{ form.vars.data.createdAt|format_datetime }}
9 |
10 | {% endif %}
11 |
12 | {{ form_widget(form) }}
13 | {% endblock %}
--------------------------------------------------------------------------------
/src/Twig/FilesRuntime.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
18 | }
19 |
20 | public function readContent(File $file): string
21 | {
22 | return $this->manager->read($file);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/UnableToUpload.php:
--------------------------------------------------------------------------------
1 | relatedFile = $file;
19 | }
20 |
21 | public function getRelatedFile(): SplFileInfo
22 | {
23 | return $this->relatedFile;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Functional/Repository/FileRepository.php:
--------------------------------------------------------------------------------
1 | pathResolver = $pathResolver;
18 | }
19 |
20 | public function filePath(File $file): string
21 | {
22 | return $this->pathResolver->getPath($file);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Functional/Repository/PreviewRepository.php:
--------------------------------------------------------------------------------
1 | bytes($bytes)->format($precision, ' ')
20 | ),
21 | new TwigFilter('file_content', [FilesRuntime::class, 'readContent']),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Functional/Repository/FileWithPreviewRepository.php:
--------------------------------------------------------------------------------
1 | namespace = $namespace;
18 | }
19 |
20 | public function getDirectoryName(File $file): ?string
21 | {
22 | return null;
23 | }
24 |
25 | public function getFileName(File $file): string
26 | {
27 | return (string)Uuid::v5($this->namespace, $file->getHash());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.idea/files.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/NamingStrategy/NullDirectoryStrategy.php:
--------------------------------------------------------------------------------
1 | originalStrategy = $originalStrategy;
17 | }
18 |
19 | public function getDirectoryName(File $file): ?string
20 | {
21 | return null;
22 | }
23 |
24 | public function getFileName(File $file): string
25 | {
26 | return $this->originalStrategy->getFileName($file);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Event/AbstractFileEvent.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
19 | $this->file = $file;
20 | }
21 |
22 | public function getManager(): ManagerInterface
23 | {
24 | return $this->manager;
25 | }
26 |
27 | public function getFile(): File
28 | {
29 | return $this->file;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Preview/GeneratePreviewMessageHandler.php:
--------------------------------------------------------------------------------
1 | generator = $generator;
16 | }
17 |
18 | public function __invoke(GeneratePreviewMessage $message): void
19 | {
20 | $file = $message->getFile();
21 | try {
22 | $file->setPreview($this->generator->generate($file));
23 | } catch (NoPreviewGeneratorFound $exception) {
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/phpcs.yml:
--------------------------------------------------------------------------------
1 | name: PHP CodeSniffer
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: "Install PHP"
11 | uses: "shivammathur/setup-php@v2"
12 | with:
13 | coverage: "none"
14 | php-version: "8.0"
15 | tools: "cs2pr"
16 |
17 | - name: "Install dependencies with Composer"
18 | uses: "ramsey/composer-install@v1"
19 |
20 | # https://github.com/doctrine/.github/issues/3
21 | - name: "Run PHP_CodeSniffer"
22 | run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr"
23 |
--------------------------------------------------------------------------------
/src/DependencyInjection/StorageFactory.php:
--------------------------------------------------------------------------------
1 | create(new SplFileInfo(__DIR__ . '/files/image1.jpg'), 'name', 12345, 'hash', 'mimeType');
17 |
18 | self::assertSame('name', $file->getOriginalFilename());
19 | self::assertSame(12345, $file->getSize());
20 | self::assertSame('hash', $file->getHash());
21 | self::assertSame('mimeType', $file->getMimeType());
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/Functional/Entity/EmbeddableFilePersistentPath.php:
--------------------------------------------------------------------------------
1 | pathname;
24 | }
25 |
26 | public function setPathname(string $pathname): void
27 | {
28 | $this->pathname = $pathname;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/NamingStrategy/DateStrategy.php:
--------------------------------------------------------------------------------
1 | format = $format;
18 | }
19 |
20 | public function getDirectoryName(File $file): ?string
21 | {
22 | return $file->getCreatedAt()->format($this->format) . DIRECTORY_SEPARATOR;
23 | }
24 |
25 | public function getFileName(File $file): string
26 | {
27 | return $file->getHash();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Functional/config_base.yml:
--------------------------------------------------------------------------------
1 | framework:
2 | test: ~
3 | secret: ydslvbxkcen473w89r8qkaponbcyd
4 | router:
5 | resource: '%kernel.project_dir%/routes.yml'
6 | utf8: true
7 |
8 | doctrine:
9 | dbal:
10 | driver: pdo_sqlite
11 | memory: true
12 | charset: UTF8
13 | orm:
14 | auto_generate_proxy_classes: "%kernel.debug%"
15 | naming_strategy: doctrine.orm.naming_strategy.underscore
16 | auto_mapping: true
17 | mappings:
18 | ArxyFilesBundleTestsFunctionalEntity:
19 | type: annotation
20 | dir: '%kernel.project_dir%/Entity'
21 | is_bundle: false
22 | prefix: Arxy\FilesBundle\Tests\Functional\Entity
23 |
24 | flysystem:
25 | storages:
26 | in_memory:
27 | adapter: 'memory'
--------------------------------------------------------------------------------
/src/AbstractModelFactory.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class AbstractModelFactory implements ModelFactory
15 | {
16 | /**
17 | * @var class-string
18 | */
19 | private string $class;
20 |
21 | /**
22 | * @param class-string $class
23 | */
24 | public function __construct(string $class)
25 | {
26 | $this->class = $class;
27 | }
28 |
29 | public function create(
30 | SplFileInfo $file,
31 | string $originalFilename,
32 | int $size,
33 | string $hash,
34 | string $mimeType
35 | ): File {
36 | return new $this->class($originalFilename, $size, $hash, $mimeType);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Repository/ORM.php:
--------------------------------------------------------------------------------
1 | findOneBy(
15 | [
16 | 'hash' => $hash,
17 | 'size' => $size,
18 | ]
19 | );
20 | }
21 |
22 | abstract public function findOneBy(array $criteria, array $orderBy = null);
23 |
24 | public function findAllForBatchProcessing(): iterable
25 | {
26 | $query = $this->createQueryBuilder('file')->getQuery();
27 |
28 | return $query->toIterable();
29 | }
30 |
31 | /** @return QueryBuilder */
32 | abstract public function createQueryBuilder($alias, $indexBy = null);
33 | }
34 |
--------------------------------------------------------------------------------
/src/Resources/config/doctrine/File.orm.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/PathResolver/AssetsPathResolver.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
21 | $this->packages = $packages;
22 | $this->package = $package;
23 | }
24 |
25 | public function getPath(File $file): string
26 | {
27 | return $this->packages->getUrl($this->manager->getPathname($file), $this->package);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/PathResolver/AwsS3PathResolver.php:
--------------------------------------------------------------------------------
1 | s3Client = $s3Client;
21 | $this->bucket = $bucket;
22 | $this->manager = $manager;
23 | }
24 |
25 | public function getPath(File $file): string
26 | {
27 | return $this->s3Client->getObjectUrl($this->bucket, $this->manager->getPathname($file));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Preview/Dimension.php:
--------------------------------------------------------------------------------
1 | width = $width;
24 | $this->height = $height;
25 | }
26 |
27 | public function getWidth(): int
28 | {
29 | return $this->width;
30 | }
31 |
32 | public function getHeight(): int
33 | {
34 | return $this->height;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Storage.php:
--------------------------------------------------------------------------------
1 | originalStrategy = $originalStrategy;
19 | }
20 |
21 | public function getDirectoryName(File $file): ?string
22 | {
23 | return $this->originalStrategy->getDirectoryName($file);
24 | }
25 |
26 | public function getFileName(File $file): string
27 | {
28 | $extension = pathinfo($file->getOriginalFilename(), PATHINFO_EXTENSION);
29 |
30 | return $this->originalStrategy->getFileName($file) . '.' . $extension;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/NamingStrategy/PersistentPathStrategy.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | final class PersistentPathStrategy implements NamingStrategy
18 | {
19 | public function getDirectoryName(File $file): ?string
20 | {
21 | $pathname = $file->getPathname();
22 |
23 | $directory = dirname($pathname);
24 | if ($directory === '.') {
25 | return null;
26 | }
27 |
28 | return $directory . DIRECTORY_SEPARATOR;
29 | }
30 |
31 | public function getFileName(File $file): string
32 | {
33 | $pathname = $file->getPathname();
34 |
35 | return basename($pathname);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Resources/config/doctrine/EmbeddableFile.orm.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/Functional/Entity/Preview.php:
--------------------------------------------------------------------------------
1 | id;
31 | }
32 |
33 | public function getPathname(): string
34 | {
35 | return $this->pathname;
36 | }
37 |
38 | public function setPathname(string $pathname): void
39 | {
40 | $this->pathname = $pathname;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/NamingStrategy/SplitHashStrategy.php:
--------------------------------------------------------------------------------
1 | splitLength = $splitLength;
24 | }
25 |
26 | public function getDirectoryName(File $file): ?string
27 | {
28 | return chunk_split($file->getHash(), $this->splitLength, DIRECTORY_SEPARATOR);
29 | }
30 |
31 | public function getFileName(File $file): string
32 | {
33 | return $file->getHash();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Functional/Entity/PersistentPathFile.php:
--------------------------------------------------------------------------------
1 | id;
31 | }
32 |
33 | public function getPathname(): string
34 | {
35 | return $this->pathname;
36 | }
37 |
38 | public function setPathname(string $pathname): void
39 | {
40 | $this->pathname = $pathname;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/LiipImagine/FileFilterPathResolver.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class FileFilterPathResolver implements PathResolver
16 | {
17 | private ManagerInterface $fileManager;
18 | private CacheManager $cacheManager;
19 |
20 | public function __construct(ManagerInterface $fileManager, CacheManager $cacheManager)
21 | {
22 | $this->fileManager = $fileManager;
23 | $this->cacheManager = $cacheManager;
24 | }
25 |
26 | public function getPath(File $file): string
27 | {
28 | return $this->cacheManager->getBrowserPath(
29 | $this->fileManager->getPathname($file->getDecorated()),
30 | $file->getFilter()
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/NamingStrategyTestCase.php:
--------------------------------------------------------------------------------
1 | file = $file;
18 | $this->expectedDirectoryName = $expectedDirectoryName;
19 | $this->expectedFilename = $expectedFilename;
20 | }
21 |
22 | public function getFile(): File
23 | {
24 | return $this->file;
25 | }
26 |
27 | public function getExpectedDirectoryName(): ?string
28 | {
29 | return $this->expectedDirectoryName;
30 | }
31 |
32 | public function getExpectedFilename(): string
33 | {
34 | return $this->expectedFilename;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Migrator.php:
--------------------------------------------------------------------------------
1 | storage = $storage;
22 | $this->old = $oldNamingStrategy;
23 | $this->new = $newNamingStrategy;
24 | }
25 |
26 | public function migrate(File $file): bool
27 | {
28 | $oldName = NamingStrategyUtility::getPathnameFromStrategy($this->old, $file);
29 | $newName = NamingStrategyUtility::getPathnameFromStrategy($this->new, $file);
30 |
31 | return $this->storage->migrate($file, $oldName, $newName);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yml:
--------------------------------------------------------------------------------
1 | name: PhpUnit
2 |
3 | on: [ push, pull_request ]
4 |
5 | jobs:
6 | tests:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | include:
11 | - php: '7.4'
12 | - php: '8.0'
13 | env:
14 | extensions: bcmath gd xdebug
15 | XDEBUG_MODE: coverage
16 | steps:
17 | - uses: actions/checkout@v2
18 | - uses: php-actions/composer@v6
19 | with:
20 | php_version: '${{ matrix.php }}'
21 | php_extensions: "${{ env.extensions }}"
22 | - uses: php-actions/phpunit@v3
23 | with:
24 | php_version: '${{ matrix.php }}'
25 | php_extensions: "${{ env.extensions }}"
26 | - name: Coverage Report
27 | uses: actions/upload-artifact@v2
28 | with:
29 | name: coverage
30 | path: coverage
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Cache and logs (Symfony2)
2 | /app/cache/*
3 | /app/logs/*
4 | !app/cache/.gitkeep
5 | !app/logs/.gitkeep
6 |
7 | # Email spool folder
8 | /app/spool/*
9 |
10 | # Cache, session files and logs (Symfony3)
11 | /var/cache/*
12 | /var/logs/*
13 | /var/sessions/*
14 | !var/cache/.gitkeep
15 | !var/logs/.gitkeep
16 | !var/sessions/.gitkeep
17 |
18 | # Parameters
19 | /app/config/parameters.yml
20 | /app/config/parameters.ini
21 |
22 | # Managed by Composer
23 | /app/bootstrap.php.cache
24 | /var/bootstrap.php.cache
25 | /bin/*
26 | !bin/console
27 | !bin/symfony_requirements
28 | /vendor/
29 |
30 | # Assets and user uploads
31 | /web/bundles/
32 | /web/uploads/
33 |
34 | # PHPUnit
35 | /app/phpunit.xml
36 | /phpunit.xml
37 |
38 | # Build data
39 | /build/
40 |
41 | # Composer PHAR
42 | /composer.phar
43 |
44 | # Backup entities generated with doctrine:generate:entities command
45 | **/Entity/*~
46 |
47 | # Embedded web-server pid file
48 | /.web-server-pid
49 |
50 | composer.lock
51 |
52 | .phpunit.result.cache
53 | .php_cs.cache
54 | .phpunit.cache
55 | infection
56 | clover.xml
57 | coverage
58 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/SplitHashStrategyTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidArgumentException::class);
30 | $this->expectExceptionMessage('$splitLength parameter must be modulus of 32');
31 |
32 | new NamingStrategy\SplitHashStrategy(6);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Bozhidar Hristov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/PersistentPathFile.php:
--------------------------------------------------------------------------------
1 | pathname = $pathname;
24 | }
25 |
26 | public function getId(): ?int
27 | {
28 | return $this->id;
29 | }
30 |
31 | public function setId(?int $id): void
32 | {
33 | $this->id = $id;
34 | }
35 |
36 | public function getPathname(): string
37 | {
38 | return $this->pathname;
39 | }
40 |
41 | public function setPathname(string $pathname): void
42 | {
43 | $this->pathname = $pathname;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/EventListener/PathAwareListener.php:
--------------------------------------------------------------------------------
1 | namingStrategy = $namingStrategy;
20 | }
21 |
22 | public static function getSubscribedEvents(): array
23 | {
24 | return [
25 | PostUpload::class => 'onUpload',
26 | ];
27 | }
28 |
29 | public function onUpload(PostUpload $event): void
30 | {
31 | $entity = $event->getFile();
32 |
33 | if ($entity instanceof MutablePathAware) {
34 | $entity->setPathname(NamingStrategyUtility::getPathnameFromStrategy($this->namingStrategy, $entity));
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Functional/Twig/PathResolverTest.php:
--------------------------------------------------------------------------------
1 | manager->upload(new SplFileObject($pathname));
30 |
31 | /** @var Environment $twig */
32 | $twig = self::getContainer()->get(Environment::class);
33 | $formatted = $twig->render($twig->createTemplate('{{ file_path(file) }}'), ['file' => $file]);
34 |
35 | self::assertSame(md5_file($pathname), $formatted);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | client = $client;
21 | $this->container = $container;
22 | $this->manager = $manager;
23 | }
24 |
25 | public function getPath(File $file): string
26 | {
27 | return $this->client->getBlobUrl($this->getContainer(), $this->getBlob($file));
28 | }
29 |
30 | public function getContainer(): string
31 | {
32 | return $this->container;
33 | }
34 |
35 | public function getBlob(File $file): string
36 | {
37 | return $this->manager->getPathname($file);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/NullDirectoryStrategyTest.php:
--------------------------------------------------------------------------------
1 | originalStrategy = $originalStrategy;
20 | $this->prefix = rtrim($prefix, DIRECTORY_SEPARATOR);
21 | }
22 |
23 | public function getDirectoryName(File $file): ?string
24 | {
25 | $directory = $this->originalStrategy->getDirectoryName($file);
26 | if ($directory === null) {
27 | return $this->prefix . DIRECTORY_SEPARATOR;
28 | }
29 |
30 | return rtrim($this->prefix . DIRECTORY_SEPARATOR . $directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
31 | }
32 |
33 | public function getFileName(File $file): string
34 | {
35 | return $this->originalStrategy->getFileName($file);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/AppendExtensionStrategyTest.php:
--------------------------------------------------------------------------------
1 | setId(12345);
33 | yield new NamingStrategyTestCase($file, '1/2/3/', '123.jpg');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
21 |
22 | src
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/Functional/config.yml:
--------------------------------------------------------------------------------
1 | arxy_files:
2 | managers:
3 | public:
4 | driver: orm
5 | class: 'Arxy\FilesBundle\Tests\Functional\Entity\File'
6 | storage: 'in_memory'
7 | naming_strategy: 'Arxy\FilesBundle\NamingStrategy\SplitHashStrategy'
8 | repository: 'Arxy\FilesBundle\Tests\Functional\Repository\FileRepository'
9 | embeddable_manager:
10 | driver: orm
11 | class: 'Arxy\FilesBundle\Entity\EmbeddableFile'
12 | storage: 'in_memory'
13 | naming_strategy: 'Arxy\FilesBundle\NamingStrategy\SplitHashStrategy'
14 |
15 | services:
16 | _defaults:
17 | public: true
18 | autowire: true # Automatically injects dependencies in your services.
19 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
20 |
21 | Arxy\FilesBundle\Tests\Functional\Repository\FileRepository: ~
22 | Arxy\FilesBundle\NamingStrategy\SplitHashStrategy: ~
23 |
24 | Arxy\FilesBundle\NamingStrategy\NullDirectoryStrategy:
25 | decorates: 'Arxy\FilesBundle\NamingStrategy\SplitHashStrategy'
26 | arguments:
27 | - '@.inner'
28 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/DirectoryChunkSplitStrategyTest.php:
--------------------------------------------------------------------------------
1 | , PathResolver> */
14 | private array $resolvers;
15 |
16 | /**
17 | * @param array, PathResolver> $resolvers
18 | */
19 | public function __construct(array $resolvers)
20 | {
21 | $this->resolvers = $resolvers;
22 | }
23 |
24 | public function getPath(File $file): string
25 | {
26 | return $this->getResolver($file)->getPath($file);
27 | }
28 |
29 | /**
30 | * @template T of File
31 | * @param T $file
32 | * @return PathResolver
33 | * @throws LogicException if no Resolver is found for $file
34 | */
35 | private function getResolver(File $file): PathResolver
36 | {
37 | $class = get_class($file);
38 | if (!isset($this->resolvers[$class])) {
39 | throw new LogicException('No resolver for ' . $class);
40 | }
41 |
42 | return $this->resolvers[$class];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/NamingStrategy/DirectoryChunkSplitStrategy.php:
--------------------------------------------------------------------------------
1 | originalStrategy = $originalStrategy;
23 | $this->offset = $offset;
24 | $this->length = $length;
25 | $this->chunkSplit = $chunkSplit;
26 | }
27 |
28 | public function getDirectoryName(File $file): ?string
29 | {
30 | $filename = $this->originalStrategy->getFileName($file);
31 |
32 | return chunk_split(substr($filename, $this->offset, $this->length), $this->chunkSplit, DIRECTORY_SEPARATOR);
33 | }
34 |
35 | public function getFileName(File $file): string
36 | {
37 | return $this->originalStrategy->getFileName($file);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Functional/LiipImagine/config.yml:
--------------------------------------------------------------------------------
1 | arxy_files:
2 | managers:
3 | public:
4 | driver: orm
5 | class: 'Arxy\FilesBundle\Tests\Functional\Entity\File'
6 | storage: 'in_memory'
7 | naming_strategy: 'Arxy\FilesBundle\NamingStrategy\SplitHashStrategy'
8 |
9 | services:
10 | _defaults:
11 | public: true
12 | autowire: true # Automatically injects dependencies in your services.
13 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
14 |
15 | Arxy\FilesBundle\Tests\Functional\Repository\FileRepository: ~
16 | Arxy\FilesBundle\NamingStrategy\SplitHashStrategy: ~
17 | Arxy\FilesBundle\LiipImagine\FileFilterPathResolver: ~
18 |
19 | liip_imagine:
20 | driver: "gd"
21 | loaders:
22 | default:
23 | flysystem:
24 | filesystem_service: 'in_memory'
25 | data_loader: default
26 | resolvers:
27 | default:
28 | web_path: ~
29 | filter_sets:
30 | thumbnail:
31 | quality: 75
32 | filters:
33 | thumbnail: {size: [120, 90], mode: outbound}
34 | background: {size: [124, 94], position: center, color: '#000000'}
35 |
--------------------------------------------------------------------------------
/src/Utility/FileDownloader.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
23 | }
24 |
25 | /**
26 | * @throws \ErrorException
27 | */
28 | public function downloadAsSplFile(File $file): SplFileInfo
29 | {
30 | $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('file-downloader', true);
31 |
32 | ErrorHandler::wrap(static fn (): bool => mkdir($tempDir));
33 |
34 | $tmpFile = $tempDir . DIRECTORY_SEPARATOR . $file->getOriginalFilename();
35 |
36 | $destinationStream = ErrorHandler::wrap(static fn () => fopen($tmpFile, 'w'));
37 | ErrorHandler::wrap(fn () => stream_copy_to_stream($this->manager->readStream($file), $destinationStream));
38 |
39 | fclose($destinationStream);
40 |
41 | return new SplFileInfo($tmpFile);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Model/AbstractFile.php:
--------------------------------------------------------------------------------
1 | originalFilename = $originalFilename;
20 | $this->size = $size;
21 | $this->hash = $hash;
22 | $this->mimeType = $mimeType;
23 | $this->createdAt = new DateTimeImmutable();
24 | }
25 |
26 | public function getOriginalFilename(): string
27 | {
28 | return $this->originalFilename;
29 | }
30 |
31 | public function getSize(): int
32 | {
33 | return $this->size;
34 | }
35 |
36 | public function getHash(): string
37 | {
38 | return $this->hash;
39 | }
40 |
41 | public function getCreatedAt(): DateTimeImmutable
42 | {
43 | return $this->createdAt;
44 | }
45 |
46 | public function getMimeType(): string
47 | {
48 | return $this->mimeType;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Model/DecoratedFile.php:
--------------------------------------------------------------------------------
1 | decorated = $decorated;
25 | }
26 |
27 | /**
28 | * @return T
29 | */
30 | public function getDecorated(): File
31 | {
32 | return $this->decorated;
33 | }
34 |
35 | public function getOriginalFilename(): string
36 | {
37 | return $this->decorated->getOriginalFilename();
38 | }
39 |
40 | public function getSize(): int
41 | {
42 | return $this->decorated->getSize();
43 | }
44 |
45 | public function getHash(): string
46 | {
47 | return $this->decorated->getHash();
48 | }
49 |
50 | public function getCreatedAt(): DateTimeImmutable
51 | {
52 | return $this->decorated->getCreatedAt();
53 | }
54 |
55 | public function getMimeType(): string
56 | {
57 | return $this->decorated->getMimeType();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Functional/NamingStrategy/PersistentPathStrategyTest.php:
--------------------------------------------------------------------------------
1 | $manager */
27 | $manager = self::getContainer()->get('embeddable');
28 | $news = new News();
29 |
30 | $news->setEmbeddableFilePersistentPath(
31 | $manager->upload(new SplFileObject(__DIR__ . '/../../files/image1.jpg'))
32 | );
33 | $this->entityManager->persist($news);
34 | $this->entityManager->flush();
35 |
36 | self::assertTrue($this->doesFileExists($news->getEmbeddableFilePersistentPath()));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/UuidV4StrategyTest.php:
--------------------------------------------------------------------------------
1 | getStrategy()->getDirectoryName($this->getFile()));
31 | }
32 |
33 | public function testGetFilename(): void
34 | {
35 | $strategy = $this->getStrategy();
36 |
37 | $filename1 = $strategy->getFileName($this->getFile());
38 | self::assertTrue(uuid_is_valid($filename1));
39 |
40 | $filename2 = $strategy->getFileName($this->getFile());
41 | self::assertTrue(uuid_is_valid($filename2));
42 |
43 | self::assertNotSame($filename1, $filename2);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | src
5 | tests
6 |
7 | tests/Functional/var/*
8 |
9 |
10 | error
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Entity/MutableFile.php:
--------------------------------------------------------------------------------
1 | modifiedAt = new DateTimeImmutable();
17 | }
18 |
19 | public function setOriginalFilename(string $originalFilename): void
20 | {
21 | $this->originalFilename = $originalFilename;
22 | }
23 |
24 | public function setSize(int $size): void
25 | {
26 | $this->size = $size;
27 | }
28 |
29 | public function setHash(string $hash): void
30 | {
31 | $this->hash = $hash;
32 | }
33 |
34 | public function getModifiedAt(): DateTimeImmutable
35 | {
36 | return $this->modifiedAt;
37 | }
38 |
39 | public function setModifiedAt(DateTimeImmutable $modifiedAt): void
40 | {
41 | $this->modifiedAt = $modifiedAt;
42 | }
43 |
44 | public function setMimeType(string $mimeType): void
45 | {
46 | $this->mimeType = $mimeType;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/FileException.php:
--------------------------------------------------------------------------------
1 | relatedFile = $file;
19 | }
20 |
21 | public static function unableToRead(File $file, Throwable $exception): self
22 | {
23 | return new self($file, 'Unable to read file', $exception);
24 | }
25 |
26 | public static function unableToWrite(File $file, Throwable $exception): self
27 | {
28 | return new self($file, 'Unable to write file', $exception);
29 | }
30 |
31 | public static function unableToMove(File $file, Throwable $exception): self
32 | {
33 | return new self($file, 'Unable to move file', $exception);
34 | }
35 |
36 | public static function unableToRemove(File $file, Throwable $exception): self
37 | {
38 | return new self($file, 'Unable to remove file', $exception);
39 | }
40 |
41 | public function getRelatedFile(): File
42 | {
43 | return $this->relatedFile;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Repository/ORMTest.php:
--------------------------------------------------------------------------------
1 | getMockForTrait(ORM::class);
18 |
19 | $mock->expects($this->once())
20 | ->method('findOneBy')
21 | ->with(['hash' => 'hash', 'size' => 123456]);
22 |
23 | $mock->findByHashAndSize('hash', 123456);
24 | }
25 |
26 | public function testFindAllForBatchProcessing(): void
27 | {
28 | $mock = $this->getMockForTrait(ORM::class);
29 |
30 | $queryMock = $this->createMock(AbstractQuery::class);
31 | $queryMock->expects($this->once())->method('toIterable');
32 |
33 | $qbMock = $this->createMock(QueryBuilder::class);
34 | $qbMock->expects($this->once())->method('getQuery')->willReturn($queryMock);
35 |
36 | $mock->expects($this->once())
37 | ->method('createQueryBuilder')
38 | ->willReturn($qbMock);
39 |
40 | $mock->findAllForBatchProcessing();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Functional/NamingStrategy/PersistPathStrategy/config.yml:
--------------------------------------------------------------------------------
1 | arxy_files:
2 | managers:
3 | public:
4 | driver: orm
5 | class: 'Arxy\FilesBundle\Tests\Functional\Entity\PersistentPathFile'
6 | storage: 'in_memory'
7 | naming_strategy: 'Arxy\FilesBundle\NamingStrategy\PersistentPathStrategy'
8 | embeddable:
9 | driver: orm
10 | class: 'Arxy\FilesBundle\Tests\Functional\Entity\EmbeddableFilePersistentPath'
11 | storage: 'in_memory'
12 | naming_strategy: 'Arxy\FilesBundle\NamingStrategy\PersistentPathStrategy'
13 |
14 | services:
15 | _defaults:
16 | public: true
17 | autowire: true # Automatically injects dependencies in your services.
18 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
19 |
20 | Arxy\FilesBundle\Tests\Functional\Repository\FileRepository: ~
21 | Arxy\FilesBundle\NamingStrategy\PersistentPathStrategy: ~
22 | Arxy\FilesBundle\NamingStrategy: '@Arxy\FilesBundle\NamingStrategy\PersistentPathStrategy'
23 |
24 | Arxy\FilesBundle\NamingStrategy\SplitHashStrategy: ~
25 |
26 | Arxy\FilesBundle\EventListener\PathAwareListener:
27 | arguments:
28 | $namingStrategy: '@Arxy\FilesBundle\NamingStrategy\SplitHashStrategy'
29 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/UuidV5StrategyTest.php:
--------------------------------------------------------------------------------
1 | bus = $bus;
19 | }
20 |
21 | public static function getSubscribedEvents(): array
22 | {
23 | return [
24 | PostUpload::class => 'postUpload',
25 | PostUpdate::class => 'postUpdate',
26 | ];
27 | }
28 |
29 | public function postUpload(PostUpload $event): void
30 | {
31 | $file = $event->getFile();
32 |
33 | if ($file instanceof PreviewableFile) {
34 | $this->bus->dispatch(new GeneratePreviewMessage($file));
35 | }
36 | }
37 |
38 | public function postUpdate(PostUpdate $event): void
39 | {
40 | $file = $event->getFile();
41 |
42 | if ($file instanceof PreviewableFile) {
43 | $this->bus->dispatch(new GeneratePreviewMessage($file));
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/PathResolver/CachePathResolver.php:
--------------------------------------------------------------------------------
1 | pathResolver = $pathResolver;
26 | $this->cache = $cache;
27 | $this->expiresAfter = $expiresAfter;
28 | }
29 |
30 | /**
31 | * @throws InvalidArgumentException
32 | */
33 | public function getPath(File $file): string
34 | {
35 | $key = $file->getHash();
36 | $item = $this->cache->getItem($key);
37 |
38 | if (!$item->isHit()) {
39 | $item->expiresAfter($this->expiresAfter);
40 | $item->set($this->pathResolver->getPath($file));
41 | $this->cache->save($item);
42 | }
43 |
44 | return $item->get();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/PathResolver/AwsS3PreSignedPathResolver.php:
--------------------------------------------------------------------------------
1 | s3Client = $s3Client;
28 | $this->bucket = $bucket;
29 | $this->manager = $manager;
30 | $this->expiry = $expiry;
31 | }
32 |
33 | public function getPath(File $file): string
34 | {
35 | $cmd = $this->s3Client->getCommand(
36 | 'GetObject',
37 | [
38 | 'Bucket' => $this->bucket,
39 | 'Key' => $this->manager->getPathname($file),
40 | ]
41 | );
42 |
43 | $now = new DateTimeImmutable();
44 | $request = $this->s3Client->createPresignedRequest($cmd, $now->add($this->expiry)->getTimestamp());
45 |
46 | return (string)$request->getUri();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Model/DecoratedFileTest.php:
--------------------------------------------------------------------------------
1 | setId(98);
23 | $this->createdAt = $file->getCreatedAt();
24 | $this->decorator = new VirtualFile($file);
25 | }
26 |
27 | public function testGetOriginalFilename(): void
28 | {
29 | self::assertSame('filename', $this->decorator->getOriginalFilename());
30 | }
31 |
32 | public function testGetSize(): void
33 | {
34 | self::assertSame(1234, $this->decorator->getSize());
35 | }
36 |
37 | public function testGetHash(): void
38 | {
39 | self::assertSame('hash', $this->decorator->getHash());
40 | }
41 |
42 | public function testGetMimeType(): void
43 | {
44 | self::assertSame('mimeType', $this->decorator->getMimeType());
45 | }
46 |
47 | public function testGetCreatedAt(): void
48 | {
49 | self::assertSame($this->createdAt, $this->decorator->getCreatedAt());
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Utility/DownloadableFile.php:
--------------------------------------------------------------------------------
1 | name = $name;
27 | $this->forceDownload = $forceDownload;
28 | $this->expireAt = $expireAt;
29 | }
30 |
31 | public function getModifiedAt(): DateTimeImmutable
32 | {
33 | if ($this->decorated instanceof MutableFile) {
34 | return $this->decorated->getModifiedAt();
35 | } else {
36 | return $this->decorated->getCreatedAt();
37 | }
38 | }
39 |
40 | public function getName(): ?string
41 | {
42 | return $this->name;
43 | }
44 |
45 | public function isForceDownload(): bool
46 | {
47 | return $this->forceDownload;
48 | }
49 |
50 | public function getExpireAt(): ?DateTimeInterface
51 | {
52 | return $this->expireAt;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Functional/Entity/FileWithPreview.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class FileWithPreview extends BaseFile implements PreviewableFile, MutablePathAware
17 | {
18 | /**
19 | * @ORM\Id()
20 | * @ORM\Column(type="integer")
21 | * @ORM\GeneratedValue()
22 | */
23 | private ?int $id = null;
24 |
25 | /**
26 | * @ORM\OneToOne(targetEntity=Preview::class, cascade={"PERSIST"})
27 | */
28 | private ?Preview $preview = null;
29 |
30 | /**
31 | * @ORM\Column(type="string")
32 | */
33 | private string $pathname;
34 |
35 | public function getId(): ?int
36 | {
37 | return $this->id;
38 | }
39 |
40 | public function getPreview(): ?\Arxy\FilesBundle\Model\File
41 | {
42 | return $this->preview;
43 | }
44 |
45 | public function setPreview(?\Arxy\FilesBundle\Model\File $file): void
46 | {
47 | $this->preview = $file;
48 | }
49 |
50 | public function getPathname(): string
51 | {
52 | return $this->pathname;
53 | }
54 |
55 | public function setPathname(string $pathname): void
56 | {
57 | $this->pathname = $pathname;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Preview/PreviewGeneratorListener.php:
--------------------------------------------------------------------------------
1 | previewGenerator = $previewGenerator;
18 | }
19 |
20 | public static function getSubscribedEvents(): array
21 | {
22 | return [
23 | PostUpload::class => 'postUpload',
24 | PostUpdate::class => 'postUpdate',
25 | ];
26 | }
27 |
28 | public function postUpload(PostUpload $event): void
29 | {
30 | $entity = $event->getFile();
31 |
32 | if ($entity instanceof PreviewableFile) {
33 | $this->generatePreview($entity);
34 | }
35 | }
36 |
37 | public function postUpdate(PostUpdate $event): void
38 | {
39 | $entity = $event->getFile();
40 |
41 | if ($entity instanceof PreviewableFile) {
42 | $this->generatePreview($entity);
43 | }
44 | }
45 |
46 | private function generatePreview(PreviewableFile $file): void
47 | {
48 | try {
49 | $file->setPreview($this->previewGenerator->generate($file));
50 | } catch (NoPreviewGeneratorFound $exception) {
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/AbstractStrategyTest.php:
--------------------------------------------------------------------------------
1 | getTestCases() as $testCase) {
21 | yield [$testCase->getFile(), $testCase->getExpectedDirectoryName()];
22 | }
23 | }
24 |
25 | final public function filenameTestData(): iterable
26 | {
27 | foreach ($this->getTestCases() as $testCase) {
28 | yield [$testCase->getFile(), $testCase->getExpectedFilename()];
29 | }
30 | }
31 |
32 | /** @dataProvider directoryTestData */
33 | final public function testDirectoryName(File $file, ?string $expected): void
34 | {
35 | self::assertEquals(
36 | $expected,
37 | $this->getStrategy()->getDirectoryName($file)
38 | );
39 | }
40 |
41 | /** @dataProvider filenameTestData */
42 | final public function testFilename(File $file, string $expected): void
43 | {
44 | self::assertEquals(
45 | $expected,
46 | $this->getStrategy()->getFileName($file)
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/PathResolver/AssetsPathResolverTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
27 |
28 | $this->pathResolver = new PathResolver\AssetsPathResolver(
29 | $this->manager,
30 | new Packages(
31 | new PathPackage(
32 | '/media',
33 | new EmptyVersionStrategy()
34 | )
35 | )
36 | );
37 | }
38 |
39 | public function testGetPath(): void
40 | {
41 | $file = new File('original_filename.jpg', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
42 | $this->manager->expects($this->once())->method('getPathname')->with($file)->willReturn('directory/5');
43 | self::assertSame('/media/directory/5', $this->pathResolver->getPath($file));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/StorageFactoryTest.php:
--------------------------------------------------------------------------------
1 | [$this->createMock(Storage::class), Storage::class];
20 | yield 'flysystem' => [$this->createMock(FilesystemOperator::class), Storage\FlysystemStorage::class];
21 | }
22 |
23 | /**
24 | * @dataProvider factoryProvider
25 | * @param class-string $expected
26 | */
27 | public function testFactory(object $object, string $expected): void
28 | {
29 | self::assertInstanceOf($expected, StorageFactory::factory($object));
30 | }
31 |
32 | public function factoryExceptionProvider(): iterable
33 | {
34 | yield 'non-supported class' => [new stdClass(), LogicException::class, 'Class stdClass not supported'];
35 | }
36 |
37 | /**
38 | * @dataProvider factoryExceptionProvider
39 | * @param class-string $expected
40 | */
41 | public function testFactoryException(object $object, string $expected, string $expectedMessage): void
42 | {
43 | self::expectException($expected);
44 | self::expectExceptionMessage($expectedMessage);
45 | StorageFactory::factory($object);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Functional/LiipImagine/FileFilterPathResolverTest.php:
--------------------------------------------------------------------------------
1 | file = $this->manager->upload(new SplFileObject(__DIR__ . '/../../files/image1.jpg'));
22 | $this->entityManager->persist($this->file);
23 | $this->entityManager->flush();
24 | }
25 |
26 | protected static function getConfig(): string
27 | {
28 | return __DIR__ . '/config.yml';
29 | }
30 |
31 | protected static function getBundles(): array
32 | {
33 | return [new LiipImagineBundle()];
34 | }
35 |
36 | public function testFilter(): void
37 | {
38 | $pathResolver = self::getContainer()->get(FileFilterPathResolver::class);
39 | assert($pathResolver instanceof FileFilterPathResolver);
40 |
41 | $path = $pathResolver->getPath(new FileFilter($this->file, 'thumbnail'));
42 | self::assertSame(
43 | 'http://localhost/media/cache/resolve/thumbnail/9aa1c5fc/7c938816/6d7ce7fd/46648dd1/9aa1c5fc7c9388166d7ce7fd46648dd1',
44 | $path
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/PathResolver/CachePathResolverTest.php:
--------------------------------------------------------------------------------
1 | cache = new ArrayAdapter();
26 | $this->decoratedPathResolver = $this->createMock(PathResolver::class);
27 | $this->pathResolver = new PathResolver\CachePathResolver(
28 | $this->decoratedPathResolver,
29 | $this->cache
30 | );
31 | }
32 |
33 | public function testGetPath(): void
34 | {
35 | $file = new File('original_filename.jpg', 125, '1234567', 'image/jpeg');
36 | $file->setId(1);
37 |
38 | $this->decoratedPathResolver->expects($this->once())->method('getPath')->with($file)->willReturn('path');
39 |
40 | self::assertFalse($this->cache->hasItem('1234567'));
41 | self::assertSame('path', $this->pathResolver->getPath($file));
42 | self::assertTrue($this->cache->hasItem('1234567'));
43 | self::assertSame('path', $this->pathResolver->getPath($file));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/PersistentPathStrategyTest.php:
--------------------------------------------------------------------------------
1 |
66 | */
67 | public function getClass(): string;
68 |
69 | /**
70 | * Clears internal FileMap of pending files.
71 | */
72 | public function clear(): void;
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Validator/Constraint/FileTest.php:
--------------------------------------------------------------------------------
1 | maxSize);
18 | }
19 |
20 | public function testNormalizeKb(): void
21 | {
22 | $file = new File(maxSize: '1k');
23 |
24 | self::assertSame(1000, $file->maxSize);
25 | }
26 |
27 | public function testNormalizeMb(): void
28 | {
29 | $file = new File(maxSize: '1M');
30 |
31 | self::assertSame(1000000, $file->maxSize);
32 | }
33 |
34 | public function testNormalizeKi(): void
35 | {
36 | $file = new File(maxSize: '1Ki');
37 |
38 | self::assertSame(1024, $file->maxSize);
39 | }
40 |
41 | public function testNormalizeMi(): void
42 | {
43 | $file = new File(maxSize: '1Mi');
44 |
45 | self::assertSame(1048576, $file->maxSize);
46 | }
47 |
48 | public function testInvalid(): void
49 | {
50 | $this->expectException(ConstraintDefinitionException::class);
51 | $this->expectExceptionMessage('"1 gigabyte" is not a valid maximum size.');
52 | $this->expectExceptionCode(0);
53 |
54 | new File(maxSize: '1 gigabyte');
55 | }
56 |
57 | public function testSingleMimeType(): void
58 | {
59 | $file = new File(mimeTypes: 'image/jpg');
60 |
61 | self::assertCount(1, $file->mimeTypes);
62 | self::assertSame('image/jpg', $file->mimeTypes[0]);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Form/EventListener/FileUploadListener.php:
--------------------------------------------------------------------------------
1 | fileManager = $fileManager;
22 | $this->multiple = $multiple;
23 | }
24 |
25 | public static function getSubscribedEvents(): array
26 | {
27 | return [
28 | FormEvents::SUBMIT => 'submit',
29 | ];
30 | }
31 |
32 | public function submit(FormEvent $event): void
33 | {
34 | /** @var SplFileInfo|SplFileInfo[]|null $uploadedFile */
35 | $uploadedFile = $event->getForm()->get('file')->getData();
36 |
37 | if ($uploadedFile !== null) {
38 | $event->setData($this->transform($uploadedFile));
39 | }
40 | }
41 |
42 | /**
43 | * @param SplFileInfo|SplFileInfo[] $data
44 | * @return File|File[]
45 | */
46 | private function transform($data)
47 | {
48 | if ($this->multiple) {
49 | /** @var SplFileInfo[] $data */
50 | return array_map(
51 | fn (SplFileInfo $file): File => $this->fileManager->upload($file),
52 | $data
53 | );
54 | } else {
55 | /** @var SplFileInfo $data */
56 | return $this->fileManager->upload($data);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/PathResolver/AwsS3PathResolverTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
29 | $this->s3Client = new S3Client([
30 | 'region' => 'us-west-2',
31 | 'version' => '2006-03-01',
32 | 'credentials' => static function (): PromiseInterface {
33 | return Create::promiseFor(
34 | new Credentials('key', 'secret')
35 | );
36 | },
37 | ]);
38 |
39 | $this->pathResolver = new PathResolver\AwsS3PathResolver($this->s3Client, 'bucket', $this->manager);
40 | }
41 |
42 | public function testGetPath(): void
43 | {
44 | $file = new File('original_filename.jpg', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
45 | $this->manager->expects($this->once())->method('getPathname')->with($file)->willReturn('pathname');
46 | self::assertSame('https://bucket.s3.us-west-2.amazonaws.com/pathname', $this->pathResolver->getPath($file));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Command/MigrateNamingStrategyCommand.php:
--------------------------------------------------------------------------------
1 | migrator = $migrator;
27 | $this->repository = $repository;
28 | }
29 |
30 | protected function execute(InputInterface $input, OutputInterface $output): int
31 | {
32 | $io = new SymfonyStyle($input, $output);
33 | $progressBar = $io->createProgressBar();
34 |
35 | $totalMigrated = 0;
36 | $totalFailed = 0;
37 |
38 | $files = $this->repository->findAllForBatchProcessing();
39 | foreach ($progressBar->iterate($files) as $file) {
40 | $migrated = $this->migrator->migrate($file);
41 | if ($migrated) {
42 | $totalMigrated++;
43 | $io->success('File ' . $file->getHash() . ' migrated');
44 | } else {
45 | $totalFailed++;
46 | $io->warning('File ' . $file->getHash() . ' not migrated');
47 | }
48 | }
49 |
50 | $io->note('Migrated: ' . (string)$totalMigrated . '. Failures: ' . (string)$totalFailed . '.');
51 |
52 | return 0;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Preview/PreviewGenerator.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
30 | $this->generators = $generators;
31 | $this->dimension = $dimension;
32 | }
33 |
34 | /**
35 | * @throws NoPreviewGeneratorFound
36 | */
37 | public function generate(File $file): File
38 | {
39 | $preview = $this->manager->upload($this->generatePreview($file));
40 |
41 | if ($preview instanceof MutableFile) {
42 | $filename = pathinfo($file->getOriginalFilename(), PATHINFO_FILENAME);
43 | $extension = pathinfo($file->getOriginalFilename(), PATHINFO_EXTENSION);
44 | $preview->setOriginalFilename(sprintf('%s_preview.%s', $filename, $extension));
45 | }
46 |
47 | return $preview;
48 | }
49 |
50 | private function generatePreview(File $file): SplFileInfo
51 | {
52 | foreach ($this->generators as $generator) {
53 | if ($generator->supports($file)) {
54 | return $generator->generate($file, $this->dimension);
55 | }
56 | }
57 |
58 | throw NoPreviewGeneratorFound::instance($file);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/PathResolverManager.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
19 | $this->pathResolver = $pathResolver;
20 | }
21 |
22 | public function upload(SplFileInfo $splFileInfo): File
23 | {
24 | return $this->manager->upload($splFileInfo);
25 | }
26 |
27 | public function getPathname(File $file): string
28 | {
29 | return $this->manager->getPathname($file);
30 | }
31 |
32 | public function read(File $file): string
33 | {
34 | return $this->manager->read($file);
35 | }
36 |
37 | public function readStream(File $file)
38 | {
39 | return $this->manager->readStream($file);
40 | }
41 |
42 | public function write(MutableFile $file, SplFileInfo $splFileInfo): void
43 | {
44 | $this->manager->write($file, $splFileInfo);
45 | }
46 |
47 | public function moveFile(File $file): void
48 | {
49 | $this->manager->moveFile($file);
50 | }
51 |
52 | public function remove(File $file): void
53 | {
54 | $this->manager->remove($file);
55 | }
56 |
57 | public function getClass(): string
58 | {
59 | return $this->manager->getClass();
60 | }
61 |
62 | public function getPath(File $file): string
63 | {
64 | return $this->pathResolver->getPath($file);
65 | }
66 |
67 | public function clear(): void
68 | {
69 | $this->manager->clear();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Preview/ImagePreviewGenerator.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
32 | $this->imagine = $imagine;
33 | $this->format = $format;
34 | $this->transformation = $transformation;
35 | }
36 |
37 | public function supports(File $file): bool
38 | {
39 | return stripos($file->getMimeType(), 'image/') !== false;
40 | }
41 |
42 | public function generate(File $file, DimensionInterface $dimension): SplFileInfo
43 | {
44 | $image = $this->imagine->read($this->manager->readStream($file));
45 | $image = $image->thumbnail(new Box($dimension->getWidth(), $dimension->getHeight()));
46 |
47 | if ($this->transformation !== null) {
48 | $this->transformation->apply($image);
49 | }
50 |
51 | $preview = new SplTempFileObject();
52 | $preview->fwrite($image->get($this->getFormat($file)));
53 |
54 | return $preview;
55 | }
56 |
57 | private function getFormat(File $file): string
58 | {
59 | return $this->format ?? str_replace('image/', '', $file->getMimeType());
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/Functional/NamingStrategy/AbstractStrategyTest.php:
--------------------------------------------------------------------------------
1 | namingStrategy = self::getContainer()->get(NamingStrategy::class);
21 | }
22 |
23 | final public function doesFileExists(File $file): bool
24 | {
25 | $filepath = ($this->namingStrategy->getDirectoryName($file) ?? "") . $this->namingStrategy->getFileName($file);
26 |
27 | return $this->flysystem->fileExists($filepath);
28 | }
29 |
30 | final public function testFileAfterCreation(): File
31 | {
32 | $file = $this->manager->upload(new SplFileObject(__DIR__ . '/../../files/image1.jpg'));
33 |
34 | $this->entityManager->persist($file);
35 | $this->entityManager->flush();
36 | $this->entityManager->clear();
37 |
38 | self::assertTrue($this->doesFileExists($file));
39 |
40 | return $file;
41 | }
42 |
43 | final public function testFileAfterDeletion(): void
44 | {
45 | $file = $this->testFileAfterCreation();
46 |
47 | $file = $this->entityManager->find($this->manager->getClass(), $file->getId());
48 |
49 | $filepath = NamingStrategyUtility::getPathnameFromStrategy($this->namingStrategy, $file);
50 |
51 | $this->entityManager->remove($file);
52 | $this->assertTrue($this->flysystem->fileExists($filepath));
53 |
54 | $this->entityManager->flush();
55 | self::assertFalse($this->flysystem->fileExists($filepath));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Form/Type/FileType.php:
--------------------------------------------------------------------------------
1 | fileManager = $fileManager;
22 | }
23 |
24 | public function buildForm(FormBuilderInterface $builder, array $options): void
25 | {
26 | $fileOptions = $options['input_options'];
27 | $fileOptions['mapped'] = false;
28 | $fileOptions['label'] = false;
29 | $fileOptions['multiple'] = $options['multiple'];
30 | $fileOptions['required'] = $options['required'];
31 |
32 | $builder->add('file', SymfonyFileType::class, $fileOptions);
33 |
34 | $builder->addEventSubscriber(new FileUploadListener($options['manager'], $options['multiple']));
35 | }
36 |
37 | public function configureOptions(OptionsResolver $resolver): void
38 | {
39 | $resolver->setDefault(
40 | 'data_class',
41 | static fn (Options $options): ?string => $options['multiple'] ? null : $options['manager']->getClass()
42 | );
43 | $resolver->setDefault('empty_data', null);
44 | $resolver->setDefault('input_options', []);
45 | $resolver->setDefault('multiple', false);
46 | $resolver->setDefault('manager', $this->fileManager);
47 | $resolver->setAllowedTypes('manager', ManagerInterface::class);
48 | }
49 |
50 | public function getBlockPrefix(): string
51 | {
52 | return 'arxy_file';
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Functional/Twig/FilesExtensionTest.php:
--------------------------------------------------------------------------------
1 | get(Environment::class);
46 | $formatted = $twig->render($twig->createTemplate('{{ bytes|format_bytes }}'), ['bytes' => $bytes]);
47 |
48 | self::assertEquals($expected, $formatted);
49 | }
50 |
51 | public function testGetContent(): void
52 | {
53 | $pathname = __DIR__ . '/../../files/lorem-ipsum.txt';
54 | $file = $this->manager->upload(new SplFileObject($pathname));
55 |
56 | /** @var Environment $twig */
57 | $twig = self::getContainer()->get(Environment::class);
58 | $formatted = $twig->render($twig->createTemplate('{{ file|file_content }}'), ['file' => $file]);
59 | self::assertSame(file_get_contents($pathname), $formatted);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/PathResolver/DelegatingPathResolverTest.php:
--------------------------------------------------------------------------------
1 | pathResolver1 = $this->createMock(PathResolver::class);
26 | $this->pathResolver2 = $this->createMock(PathResolver::class);
27 | $this->pathResolver = new DelegatingPathResolver(
28 | [
29 | File::class => $this->pathResolver1,
30 | File2::class => $this->pathResolver2,
31 | ]
32 | );
33 | }
34 |
35 | public function testGetPath(): void
36 | {
37 | $file1 = new File('original_filename.jpg', 125, '1234567', 'image/jpeg');
38 | $this->pathResolver1->expects($this->once())->method('getPath')->with($file1)->willReturn(File::class);
39 | self::assertSame(File::class, $this->pathResolver->getPath($file1));
40 |
41 | $file2 = new File2('original_filename.jpg', 125, '1234567', 'image/jpeg');
42 | $this->pathResolver2->expects($this->once())->method('getPath')->with($file2)->willReturn(File2::class);
43 | self::assertSame(File2::class, $this->pathResolver->getPath($file2));
44 | }
45 |
46 | public function testNotManagedFile(): void
47 | {
48 | $this->expectException(LogicException::class);
49 | $this->expectExceptionMessage('No resolver for ' . File3::class);
50 | $this->pathResolver->getPath(new File3('original_filename.jpg', 125, '1234567', 'image/jpeg'));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/NamingStrategy/DirectoryPrefixStrategyTest.php:
--------------------------------------------------------------------------------
1 | getFile(), 'cache/1/2/3/', '123');
33 | }
34 |
35 | private function getFile(): \Arxy\FilesBundle\Model\File
36 | {
37 | $file = new File('original_filename.jpg', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
38 | $file->setId(12345);
39 |
40 | return $file;
41 | }
42 |
43 | public function testIfDecoratedStrategyIsNull(): void
44 | {
45 | $strategy = new NamingStrategy\DirectoryPrefixStrategy(
46 | new class () implements NamingStrategy {
47 | public function getDirectoryName(\Arxy\FilesBundle\Model\File $file): ?string
48 | {
49 | return null;
50 | }
51 |
52 | public function getFileName(\Arxy\FilesBundle\Model\File $file): string
53 | {
54 | return '123';
55 | }
56 | },
57 | 'cache/'
58 | );
59 |
60 | self::assertSame('cache/', $strategy->getDirectoryName($this->getFile()));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/PathResolver/AwsPreSignedS3PathResolverTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
30 | $this->s3Client = new S3Client([
31 | 'region' => 'us-west-2',
32 | 'version' => '2006-03-01',
33 | 'credentials' => static function (): PromiseInterface {
34 | return Create::promiseFor(
35 | new Credentials('key', 'secret')
36 | );
37 | },
38 | ]);
39 |
40 | $this->pathResolver = new PathResolver\AwsS3PreSignedPathResolver(
41 | $this->s3Client,
42 | 'bucket',
43 | $this->manager,
44 | new DateInterval(
45 | 'P1D'
46 | )
47 | );
48 | }
49 |
50 | public function testGetPath(): void
51 | {
52 | $file = new File('original_filename.jpg', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
53 | $this->manager->expects($this->once())->method('getPathname')->with($file)->willReturn('pathname');
54 | self::assertStringContainsString(
55 | 'https://bucket.s3.us-west-2.amazonaws.com/pathname',
56 | $this->pathResolver->getPath($file)
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Validator/Constraint/File.php:
--------------------------------------------------------------------------------
1 | */
26 | public array $mimeTypes = [];
27 |
28 | /**
29 | * @param array|string $mimeTypes
30 | * @param array|null $groups
31 | */
32 | public function __construct(
33 | int|string|null $maxSize = null,
34 | public string $maxSizeMessage = 'The file is too large ({{ size }}). Allowed maximum size is {{ limit }}.',
35 | array|string $mimeTypes = [],
36 | public string $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.',
37 | array $groups = null,
38 | ) {
39 | if (is_string($maxSize)) {
40 | $maxSize = $this->normalizeBinaryFormat($maxSize);
41 | }
42 | $this->maxSize = $maxSize;
43 |
44 | if (!is_array($mimeTypes)) {
45 | $mimeTypes = [$mimeTypes];
46 | }
47 | $this->mimeTypes = $mimeTypes;
48 | parent::__construct(groups: $groups);
49 | }
50 |
51 | private function normalizeBinaryFormat(string $maxSize): int
52 | {
53 | $original = $maxSize;
54 | try {
55 | if (stripos($maxSize, 'B') === false) {
56 | $maxSize .= 'B';
57 | }
58 |
59 | return (int)parse($maxSize)->numberOfBytes();
60 | } catch (Exception $e) {
61 | throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size.', $original), 0, $e);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/Functional/Kernel.php:
--------------------------------------------------------------------------------
1 | testCase = $testCase;
28 | }
29 |
30 | public function config(string $config): void
31 | {
32 | $this->config = realpath($config);
33 | }
34 |
35 | public function bundles(array $bundles): void
36 | {
37 | $this->additionalBundles = $bundles;
38 | }
39 |
40 | public function getProjectDir(): string
41 | {
42 | return __DIR__;
43 | }
44 |
45 | private function getVarDir(): string
46 | {
47 | return __DIR__ . '/var/files-bundle-' . md5($this->testCase);
48 | }
49 |
50 | public function getCacheDir()
51 | {
52 | return $this->getVarDir() . '/cache';
53 | }
54 |
55 | public function getLogDir()
56 | {
57 | return $this->getVarDir() . '/log';
58 | }
59 |
60 | public function registerBundles(): array
61 | {
62 | return array_merge(
63 | [
64 | new FrameworkBundle(),
65 | new DoctrineBundle(),
66 | new FlysystemBundle(),
67 | new ArxyFilesBundle(),
68 | ],
69 | $this->additionalBundles
70 | );
71 | }
72 |
73 | public function registerContainerConfiguration(LoaderInterface $loader): void
74 | {
75 | $loader->load(__DIR__ . '/config_base.yml');
76 | $loader->load($this->config);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tests/PathResolver/AzureBlobStoragePathResolverTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
26 | $this->blobRestProxy = $this->createMock(BlobRestProxy::class);
27 | $this->pathResolver = new PathResolver\AzureBlobStoragePathResolver(
28 | $this->blobRestProxy,
29 | 'azure-container',
30 | $this->manager,
31 | );
32 | }
33 |
34 | public function testGetPath(): void
35 | {
36 | $file = new File('original_filename.jpg', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
37 |
38 | $this->manager->expects($this->once())->method('getPathname')->with($file)->willReturn('pathname');
39 |
40 | $this->blobRestProxy->expects($this->once())
41 | ->method('getBlobUrl')
42 | ->with('azure-container', 'pathname')
43 | ->willReturn('all good');
44 |
45 | self::assertSame('all good', $this->pathResolver->getPath($file));
46 | }
47 |
48 | public function testGetContainer(): void
49 | {
50 | self::assertSame('azure-container', $this->pathResolver->getContainer());
51 | }
52 |
53 | public function testGetBlob(): void
54 | {
55 | $file = new File('original_filename.jpg', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
56 |
57 | $this->manager->expects($this->once())->method('getPathname')->with($file)->willReturn('all good');
58 |
59 | self::assertSame('all good', $this->pathResolver->getBlob($file));
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/Functional/Entity/News.php:
--------------------------------------------------------------------------------
1 | id;
39 | }
40 |
41 | public function setId(?int $id): void
42 | {
43 | $this->id = $id;
44 | }
45 |
46 | public function getFile(): ?File
47 | {
48 | return $this->file;
49 | }
50 |
51 | public function setFile(?File $file): void
52 | {
53 | $this->file = $file;
54 | }
55 |
56 | public function getEmbeddableFile(): ?EmbeddableFile
57 | {
58 | return $this->embeddableFile;
59 | }
60 |
61 | public function setEmbeddableFile(?EmbeddableFile $embeddableFile): void
62 | {
63 | $this->embeddableFile = $embeddableFile;
64 | }
65 |
66 | public function getEmbeddableFile1(): ?EmbeddableFile
67 | {
68 | return $this->embeddableFile1;
69 | }
70 |
71 | public function setEmbeddableFile1(?EmbeddableFile $embeddableFile1): void
72 | {
73 | $this->embeddableFile1 = $embeddableFile1;
74 | }
75 |
76 | public function getEmbeddableFilePersistentPath(): ?EmbeddableFilePersistentPath
77 | {
78 | return $this->embeddableFilePersistentPath;
79 | }
80 |
81 | public function setEmbeddableFilePersistentPath(?EmbeddableFilePersistentPath $embeddableFilePersistentPath): void
82 | {
83 | $this->embeddableFilePersistentPath = $embeddableFilePersistentPath;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Storage/FlysystemStorage.php:
--------------------------------------------------------------------------------
1 | flysystem = $flysystem;
21 | }
22 |
23 | public function read(File $file, string $pathname): string
24 | {
25 | try {
26 | return $this->flysystem->read($pathname);
27 | } catch (FilesystemException $exception) {
28 | throw FileException::unableToRead($file, $exception);
29 | }
30 | }
31 |
32 | public function readStream(File $file, string $pathname)
33 | {
34 | try {
35 | return $this->flysystem->readStream($pathname);
36 | } catch (FilesystemException $exception) {
37 | throw FileException::unableToRead($file, $exception);
38 | }
39 | }
40 |
41 | public function write(File $file, string $pathname, $stream): void
42 | {
43 | try {
44 | $this->flysystem->writeStream($pathname, $stream);
45 | } catch (FilesystemException $exception) {
46 | throw FileException::unableToWrite($file, $exception);
47 | }
48 | }
49 |
50 | public function remove(File $file, string $pathname): void
51 | {
52 | try {
53 | $this->flysystem->delete($pathname);
54 | } catch (FilesystemException $exception) {
55 | throw FileException::unableToMove($file, $exception);
56 | }
57 | }
58 |
59 | public function migrate(File $file, string $oldPathname, string $newPathname): bool
60 | {
61 | try {
62 | if (!$this->flysystem->fileExists($oldPathname)) {
63 | return false;
64 | }
65 |
66 | $this->flysystem->move($oldPathname, $newPathname);
67 |
68 | return true;
69 | } catch (FilesystemException $exception) {
70 | throw new FileException($file, 'Unable to migrate file', $exception);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/PathResolver/AzureBlobStorageSASPathResolver.php:
--------------------------------------------------------------------------------
1 | pathResolver = $pathResolver;
27 | $this->signatureHelper = $signatureHelper;
28 | $this->parametersFactory = $factory;
29 | }
30 |
31 | public function getPath(File $file): string
32 | {
33 | return $this->pathResolver->getPath($file) . '?' . $this->generateSas($file);
34 | }
35 |
36 | private function generateSas(File $file): string
37 | {
38 | $parameters = $this->parametersFactory->create($file);
39 | $expiry = $parameters->getExpiry();
40 |
41 | $expiry = DateTime::createFromImmutable($expiry);
42 |
43 | $start = $parameters->getStart();
44 | if ($start !== null) {
45 | $start = DateTime::createFromImmutable($start);
46 | }
47 |
48 | return $this->signatureHelper->generateBlobServiceSharedAccessSignatureToken(
49 | Resources::RESOURCE_TYPE_BLOB,
50 | sprintf('%s/%s', $this->pathResolver->getContainer(), $this->pathResolver->getBlob($file)),
51 | 'r',
52 | $expiry,
53 | $start ?? "",
54 | $parameters->getIp() ?? "",
55 | 'https',
56 | $parameters->getIdentifier() ?? "",
57 | $parameters->getCacheControl() ?? "",
58 | $parameters->getContentDisposition() ?? "",
59 | $parameters->getContentEncoding() ?? "",
60 | $parameters->getContentLanguage() ?? "",
61 | $parameters->getContentType() ?? "",
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/FileDownloaderTest.php:
--------------------------------------------------------------------------------
1 | filesystem = new Filesystem(new InMemoryFilesystemAdapter());
29 |
30 | $this->manager = new Manager(
31 | File::class,
32 | new FlysystemStorage($this->filesystem),
33 | /** @implements NamingStrategy */
34 | new class implements NamingStrategy {
35 | public function getDirectoryName(\Arxy\FilesBundle\Model\File $file): ?string
36 | {
37 | return null;
38 | }
39 |
40 | public function getFileName(\Arxy\FilesBundle\Model\File $file): string
41 | {
42 | return (string)$file->getId();
43 | }
44 | },
45 | new FileRepository(),
46 | );
47 |
48 | $this->downloader = new FileDownloader($this->manager);
49 | }
50 |
51 | public function testDownloadAsSplFilePersisted(): void
52 | {
53 | $file = $this->manager->upload(new \SplFileObject(__DIR__ . '/files/image1.jpg'));
54 | $this->manager->moveFile($file);
55 |
56 | $splFile = $this->downloader->downloadAsSplFile($file);
57 |
58 | self::assertFileEquals($splFile->getPathname(), __DIR__ . '/files/image1.jpg');
59 | }
60 |
61 | public function testDownloadAsSplFileNotPersisted(): void
62 | {
63 | $file = $this->manager->upload(new \SplFileObject(__DIR__ . '/files/image1.jpg'));
64 |
65 | $splFile = $this->downloader->downloadAsSplFile($file);
66 |
67 | self::assertFileEquals($splFile->getPathname(), __DIR__ . '/files/image1.jpg');
68 | }
69 | }
--------------------------------------------------------------------------------
/tests/Functional/Preview/PreviewGeneratorMessengerTest.php:
--------------------------------------------------------------------------------
1 | manager->upload(new SplFileInfo(__DIR__ . '/../../files/image1.jpg'));
26 | assert($file instanceof FileWithPreview);
27 |
28 | $this->entityManager->persist($file);
29 | $this->entityManager->flush();
30 |
31 | self::assertNotNull($file->getPreview());
32 | // $previewManager = self::getContainer()->get('preview');
33 |
34 | // $expectedFilename = __DIR__.'/../../files/image1_preview.jpg';
35 | //
36 | // $expectedMd5 = md5_file($expectedFilename);
37 | // self::assertSame($expectedMd5, md5($previewManager->read($file->getPreview())));
38 | // self::assertSame($expectedMd5, $file->getPreview()->getHash());
39 | //
40 | //
41 | // $expectedFilesize = filesize($expectedFilename);
42 | // self::assertSame($expectedFilesize, strlen($previewManager->read($file->getPreview())));
43 | // self::assertSame($expectedFilesize, $file->getPreview()->getSize());
44 |
45 | // self::assertSame('image1_preview.jpg', $file->getPreview()->getOriginalFilename());
46 | }
47 |
48 | public function testPreviewWrite(): void
49 | {
50 | $file = $this->manager->upload(new SplFileInfo(__DIR__ . '/../../files/image1.jpg'));
51 | assert($file instanceof FileWithPreview);
52 |
53 | $this->entityManager->persist($file);
54 | $this->entityManager->flush();
55 |
56 | $preview1 = $file->getPreview();
57 | self::assertNotNull($preview1);
58 |
59 | $this->manager->write($file, new SplFileInfo(__DIR__ . '/../../files/image2.jpg'));
60 |
61 | self::assertNotSame($preview1, $file->getPreview());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Validator/Constraint/FileValidator.php:
--------------------------------------------------------------------------------
1 | maxSize !== null) {
30 | $limitInBytes = $constraint->maxSize;
31 | $sizeInBytes = $value->getSize();
32 |
33 | if ($sizeInBytes > $limitInBytes) {
34 | $this->context->buildViolation($constraint->maxSizeMessage)
35 | ->setParameter('{{ size }}', $this->humanizeBytes($sizeInBytes))
36 | ->setParameter('{{ limit }}', $this->humanizeBytes($limitInBytes))
37 | ->addViolation();
38 | }
39 | }
40 |
41 | if (count($constraint->mimeTypes) > 0) {
42 | $mimeTypes = $constraint->mimeTypes;
43 | $mime = $value->getMimeType();
44 |
45 | foreach ($mimeTypes as $mimeType) {
46 | if ($mimeType === $mime) {
47 | return;
48 | }
49 |
50 | if ($discrete = strstr($mimeType, '/*', true)) {
51 | if (strstr($mime, '/', true) === $discrete) {
52 | return;
53 | }
54 | }
55 | }
56 |
57 | $this->context->buildViolation($constraint->mimeTypesMessage)
58 | ->setParameter('{{ type }}', $this->formatValue($mime))
59 | ->setParameter('{{ types }}', $this->formatValues($mimeTypes))
60 | ->addViolation();
61 | }
62 | }
63 |
64 | private function humanizeBytes(int $bytes): string
65 | {
66 | return bytes($bytes)->format(2, ' ');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/Functional/AbstractFunctionalTest.php:
--------------------------------------------------------------------------------
1 | buildDb($kernel);
26 |
27 | $this->entityManager = static::getContainer()
28 | ->get('doctrine')
29 | ->getManager();
30 |
31 | $this->manager = static::getContainer()->get('public');
32 | $this->flysystem = static::getContainer()->get('in_memory');
33 | }
34 |
35 | protected function tearDown(): void
36 | {
37 | parent::tearDown();
38 |
39 | $this->entityManager->close();
40 | unset($this->entityManager);
41 |
42 | $this->manager->clear();
43 | unset($this->manager);
44 |
45 | unset($this->flysystem);
46 | }
47 |
48 | protected static function getKernelClass()
49 | {
50 | return Kernel::class;
51 | }
52 |
53 | protected static function createKernel(array $options = [])
54 | {
55 | $kernel = parent::createKernel($options);
56 | assert($kernel instanceof Kernel);
57 | $kernel->config(static::getConfig());
58 | $kernel->bundles(static::getBundles());
59 | $kernel->setTestCase(static::class);
60 |
61 | return $kernel;
62 | }
63 |
64 | abstract protected static function getConfig(): string;
65 |
66 | abstract protected static function getBundles(): array;
67 |
68 | private function buildDb($kernel): void
69 | {
70 | $application = new Application($kernel);
71 | $application->setAutoExit(false);
72 |
73 | $application->run(
74 | new ArrayInput(
75 | [
76 | 'doctrine:schema:create',
77 | ]
78 | ),
79 | new ConsoleOutput()
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/Command/MigrateNamingStrategyCommandTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
31 | $this->repository = $this->createMock(Repository::class);
32 | $this->migrator = $this->createMock(MigratorInterface::class);
33 |
34 | $this->command = new MigrateNamingStrategyCommand($this->migrator, $this->repository);
35 | }
36 |
37 | public function testExecute(): void
38 | {
39 | $file1 = new File('filename', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
40 | $file1->setId(1);
41 |
42 | $file2 = new File('filename', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
43 | $file2->setId(2);
44 | $this->repository->expects($this->once())->method('findAllForBatchProcessing')->willReturn([$file1, $file2]);
45 |
46 | $this->migrator
47 | ->expects($this->exactly(2))
48 | ->method('migrate')->withConsecutive(
49 | [$this->identicalTo($file1)],
50 | [$this->identicalTo($file2)]
51 | )
52 | ->will($this->onConsecutiveCalls(true, false));
53 |
54 | $commandTester = new CommandTester($this->command);
55 | self::assertSame(0, $commandTester->execute([]));
56 |
57 | $output = $commandTester->getDisplay();
58 |
59 | self::assertStringContainsString('File 098f6bcd4621d373cade4e832627b4f6 migrated', $output);
60 | self::assertStringContainsString('File 098f6bcd4621d373cade4e832627b4f6 not migrated', $output);
61 | self::assertStringContainsString('Migrated: 1. Failures: 1.', $output);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/Preview/DimensionTest.php:
--------------------------------------------------------------------------------
1 | expectNotToPerformAssertions();
26 | new Dimension($width, $height);
27 | }
28 |
29 | public function constructorExceptionsDataProvider(): iterable
30 | {
31 | yield [
32 | 0,
33 | 1,
34 | InvalidArgumentException::class,
35 | 'Length of either side cannot be 0 or negative, current size is 0x1',
36 | ];
37 | yield [
38 | 1,
39 | 0,
40 | InvalidArgumentException::class,
41 | 'Length of either side cannot be 0 or negative, current size is 1x0',
42 | ];
43 | yield [
44 | 0,
45 | 0,
46 | InvalidArgumentException::class,
47 | 'Length of either side cannot be 0 or negative, current size is 0x0',
48 | ];
49 | yield [
50 | -1,
51 | 1,
52 | InvalidArgumentException::class,
53 | 'Length of either side cannot be 0 or negative, current size is -1x1',
54 | ];
55 | yield [
56 | 1,
57 | -1,
58 | InvalidArgumentException::class,
59 | 'Length of either side cannot be 0 or negative, current size is 1x-1',
60 | ];
61 | yield [
62 | -1,
63 | -1,
64 | InvalidArgumentException::class,
65 | 'Length of either side cannot be 0 or negative, current size is -1x-1',
66 | ];
67 | yield [null, 1, TypeError::class];
68 | yield [1, null, TypeError::class];
69 | }
70 |
71 | /**
72 | * @dataProvider constructorExceptionsDataProvider
73 | */
74 | public function testConstructorExceptions(
75 | ?int $width,
76 | ?int $height,
77 | string $expectException,
78 | ?string $expectExceptionMessage = null
79 | ): void {
80 | $this->expectException($expectException);
81 | $this->expectExceptionMessage($expectExceptionMessage);
82 |
83 | new Dimension($width, $height);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Utility/DownloadUtility.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
26 | }
27 |
28 | public function createResponse(File $file): StreamedResponse
29 | {
30 | $response = new StreamedResponse();
31 | $response->headers->set('Content-Type', $file->getMimeType());
32 | $response->setPublic();
33 | $response->setEtag($file->getHash());
34 |
35 | if ($file instanceof DownloadableFile) {
36 | $expireAt = $file->getExpireAt();
37 | $response->setExpires($expireAt);
38 | $response->setLastModified($file->getModifiedAt());
39 |
40 | $contentDisposition = HeaderUtils::makeDisposition(
41 | $file->isForceDownload() ? HeaderUtils::DISPOSITION_ATTACHMENT : HeaderUtils::DISPOSITION_INLINE,
42 | $file->getName() ?? u($file->getOriginalFilename())->ascii()->toString()
43 | );
44 | } else {
45 | $expireAt = new DateTimeImmutable("+30 days");
46 | $response->setExpires($expireAt);
47 | $response->setLastModified($file->getCreatedAt());
48 |
49 | $contentDisposition = HeaderUtils::makeDisposition(
50 | HeaderUtils::DISPOSITION_ATTACHMENT,
51 | u($file->getOriginalFilename())->ascii()->toString()
52 | );
53 | }
54 |
55 | $response->headers->set('Content-Length', (string)$file->getSize());
56 | $response->headers->set('Content-Disposition', $contentDisposition);
57 |
58 | $response->setCallback(
59 | function () use ($file): void {
60 | $stream = $this->manager->readStream($file);
61 |
62 | $out = ErrorHandler::wrap(static fn () => fopen('php://output', 'wb'));
63 | ErrorHandler::wrap(static fn (): int => stream_copy_to_stream($stream, $out));
64 | ErrorHandler::wrap(static fn (): bool => fclose($out));
65 | ErrorHandler::wrap(static fn (): bool => fclose($stream));
66 | }
67 | );
68 |
69 | return $response;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Functional/Preview/PreviewGeneratorTest.php:
--------------------------------------------------------------------------------
1 | manager->upload(new SplFileInfo(__DIR__ . '/../../files/image1.jpg'));
26 | assert($file instanceof FileWithPreview);
27 |
28 | $this->entityManager->persist($file);
29 | $this->entityManager->flush();
30 |
31 | self::assertNotNull($file->getPreview());
32 | // $previewManager = self::getContainer()->get('preview');
33 | //
34 | // $expectedFilename = __DIR__.'/../../files/image1_preview.jpg';
35 |
36 | // $expectedMd5 = md5_file($expectedFilename);
37 | // self::assertSame($expectedMd5, md5($previewManager->read($file->getPreview())));
38 | // self::assertSame($expectedMd5, $file->getPreview()->getHash());
39 |
40 | // $expectedFilesize = filesize($expectedFilename);
41 | // self::assertSame($expectedFilesize, strlen($previewManager->read($file->getPreview())));
42 | // self::assertSame($expectedFilesize, $file->getPreview()->getSize());
43 |
44 | // self::assertSame('image1_preview.jpg', $file->getPreview()->getOriginalFilename());
45 | }
46 |
47 | public function testPreviewWrite(): void
48 | {
49 | $file = $this->manager->upload(new SplFileInfo(__DIR__ . '/../../files/image1.jpg'));
50 | assert($file instanceof FileWithPreview);
51 |
52 | $this->entityManager->persist($file);
53 | $this->entityManager->flush();
54 |
55 | $preview1 = $file->getPreview();
56 | self::assertNotNull($preview1);
57 |
58 | $this->manager->write($file, new SplFileInfo(__DIR__ . '/../../files/image2.jpg'));
59 |
60 | self::assertNotSame($preview1, $file->getPreview());
61 | }
62 |
63 | public function testNotSupportedFile(): void
64 | {
65 | $file = $this->manager->upload(new SplFileInfo(__DIR__ . '/../../files/lorem-ipsum.pdf'));
66 | assert($file instanceof FileWithPreview);
67 |
68 | $this->entityManager->persist($file);
69 | $this->entityManager->flush();
70 |
71 | self::assertNull($file->getPreview());
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/FileMap.php:
--------------------------------------------------------------------------------
1 | */
24 | private array $map = [];
25 | /** @var array */
26 | private array $pendingFiles = [];
27 |
28 | /**
29 | * @param array $files
30 | * @param array $splFiles
31 | */
32 | public function __construct(array $files = [], array $splFiles = [])
33 | {
34 | $this->map = $files;
35 | $this->pendingFiles = $splFiles;
36 | }
37 |
38 | /**
39 | * @return T|null
40 | */
41 | public function findByHashAndSize(string $hash, int $size): ?File
42 | {
43 | foreach ($this->pendingFiles as $file) {
44 | if ($file->getHash() === $hash && $file->getSize() === $size) {
45 | return $file;
46 | }
47 | }
48 |
49 | return null;
50 | }
51 |
52 | /**
53 | * @param T $file
54 | * @param S $fileInfo
55 | */
56 | public function put(File $file, SplFileInfo $fileInfo): void
57 | {
58 | $id = $this->getObjectId($file);
59 | $this->map[$id] = $fileInfo;
60 | $this->pendingFiles[$id] = $file;
61 | }
62 |
63 | /**
64 | * @param T $file
65 | * @return S
66 | */
67 | public function get(File $file): SplFileInfo
68 | {
69 | if (!$this->has($file)) {
70 | throw new OutOfBoundsException(
71 | sprintf(
72 | 'File %s not found in map',
73 | FileUtility::toString($file)
74 | )
75 | );
76 | }
77 |
78 | return $this->map[$this->getObjectId($file)];
79 | }
80 |
81 | /**
82 | * @param T $file
83 | */
84 | public function has(File $file): bool
85 | {
86 | return isset($this->map[$this->getObjectId($file)]);
87 | }
88 |
89 | /**
90 | * @param T $file
91 | */
92 | public function remove(File $file): void
93 | {
94 | $id = $this->getObjectId($file);
95 | unset($this->map[$id]);
96 | unset($this->pendingFiles[$id]);
97 | }
98 |
99 | /**
100 | * @param T $file
101 | */
102 | private function getObjectId(File $file): int
103 | {
104 | return spl_object_id($file);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "arxy/files",
3 | "type": "symfony-bundle",
4 | "authors": [
5 | {
6 | "name": "Bozhidar Hristov",
7 | "email": "warxcell@gmail.com"
8 | }
9 | ],
10 | "require": {
11 | "php": "^8.0",
12 | "league/mime-type-detection": "^1.7",
13 | "gabrielelana/byte-units": "^0.5.0"
14 | },
15 | "require-dev": {
16 | "symfony/validator": "*",
17 | "doctrine/orm": "*",
18 | "symfony/form": "*",
19 | "symfony/http-foundation": "*",
20 | "twig/twig": "*",
21 | "symfony/cache": "*",
22 | "symfony/asset": "*",
23 | "microsoft/azure-storage-blob": "*",
24 | "aws/aws-sdk-php": "*",
25 | "league/flysystem-memory": "^2.0",
26 | "phpunit/phpunit": "^9.5",
27 | "symfony/http-kernel": "*",
28 | "symfony/dependency-injection": "^5.2",
29 | "infection/infection": "^0.21.4",
30 | "symfony/symfony": "^4.4 | ^5.2",
31 | "doctrine/doctrine-bundle": "^2.3",
32 | "liip/imagine-bundle": "^2.6",
33 | "vimeo/psalm": "^4.7",
34 | "league/flysystem-bundle": "^2.0",
35 | "symfony/uid": "*",
36 | "imagine/imagine": "^1.2",
37 | "symfony/messenger": "*",
38 | "league/flysystem": "^2.0 | ^3.0",
39 | "squizlabs/php_codesniffer": "*",
40 | "slevomat/coding-standard": "^7.0"
41 | },
42 | "suggest": {
43 | "league/flysystem": "Use FlySystem Storage to store contents of file",
44 | "symfony/validator": "Validate files against constraints",
45 | "symfony/form": "Integrate file upload with forms",
46 | "doctrine/orm": "Use Doctrine ORM as persistence layer",
47 | "twig/twig": "Use Arxy\\Files\\Twig\\FilesExtension::formatBytes to get formatted KiB/MiB/GB instead. Also for form template",
48 | "symfony/uid": "Required to use UuidV5Strategy/UuidV4Strategy Naming Strategy",
49 | "imagine/imagine": "Use ImagePreviewGenerator",
50 | "symfony/messenger": "Generate Previews asynchronous.",
51 | "symfony/event-dispatcher": "Usage of events.",
52 | "liip/imagine-bundle": "Generate image thumbnails",
53 | "aws/aws-sdk-php": "Generate file urls for files hosted on AWS S3",
54 | "microsoft/azure-storage-blob": "Generate file urls for files hosted on Microsoft Azure Blob Storage"
55 | },
56 | "autoload": {
57 | "psr-4": {
58 | "Arxy\\FilesBundle\\": "src/"
59 | }
60 | },
61 | "autoload-dev": {
62 | "psr-4": {
63 | "Arxy\\FilesBundle\\Tests\\": "tests/"
64 | }
65 | },
66 | "config": {
67 | "allow-plugins": {
68 | "dealerdirect/phpcodesniffer-composer-installer": true,
69 | "infection/extension-installer": true
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/MigratorTest.php:
--------------------------------------------------------------------------------
1 | filesystem = $this->createMock(FilesystemOperator::class);
27 |
28 | $this->oldNamingStrategy = new class implements NamingStrategy {
29 | public function getDirectoryName(\Arxy\FilesBundle\Model\File $file): ?string
30 | {
31 | return null;
32 | }
33 |
34 | public function getFileName(\Arxy\FilesBundle\Model\File $file): string
35 | {
36 | return 'old_' . $file->getHash();
37 | }
38 | };
39 |
40 | $this->newNamingStrategy = new class implements NamingStrategy {
41 | public function getDirectoryName(\Arxy\FilesBundle\Model\File $file): ?string
42 | {
43 | return null;
44 | }
45 |
46 | public function getFileName(\Arxy\FilesBundle\Model\File $file): string
47 | {
48 | return 'new_' . $file->getHash();
49 | }
50 | };
51 |
52 | $this->migrator = new Migrator(
53 | new FlysystemStorage($this->filesystem),
54 | $this->oldNamingStrategy,
55 | $this->newNamingStrategy
56 | );
57 | }
58 |
59 | public function testNotMigrated(): void
60 | {
61 | $file = new File('image2.jpg', 24053, '9aa1c5fc7c9388166d7ce7fd46648dd1', 'image/jpeg');
62 |
63 | $this->filesystem->expects(self::once())->method('fileExists')->with('old_9aa1c5fc7c9388166d7ce7fd46648dd1')
64 | ->willReturn(true);
65 |
66 | $this->filesystem->expects(self::once())->method('move')->with(
67 | 'old_9aa1c5fc7c9388166d7ce7fd46648dd1',
68 | 'new_9aa1c5fc7c9388166d7ce7fd46648dd1'
69 | );
70 |
71 | self::assertTrue($this->migrator->migrate($file));
72 | }
73 |
74 | public function testMigrated(): void
75 | {
76 | $file = new File('image2.jpg', 24053, '9aa1c5fc7c9388166d7ce7fd46648dd1', 'image/jpeg');
77 |
78 | $this->filesystem->expects(self::once())->method('fileExists')->with('old_9aa1c5fc7c9388166d7ce7fd46648dd1')
79 | ->willReturn(false);
80 |
81 | self::assertFalse($this->migrator->migrate($file));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/EventListener/DoctrineORMListener.php:
--------------------------------------------------------------------------------
1 | class = $manager->getClass();
25 | $this->manager = $manager;
26 |
27 | $this->move = static function (File $file) use ($manager): void {
28 | $manager->moveFile($file);
29 | };
30 | $this->remove = static function (File $file) use ($manager): void {
31 | $manager->remove($file);
32 | };
33 | }
34 |
35 | public function postPersist(LifecycleEventArgs $eventArgs): void
36 | {
37 | $entity = $eventArgs->getEntity();
38 | $entityManager = $eventArgs->getEntityManager();
39 | if ($this->supports($entity)) {
40 | ($this->move)($entity);
41 | }
42 | $this->handleEmbeddable($entityManager, $entity, $this->move);
43 | }
44 |
45 | public function postRemove(LifecycleEventArgs $eventArgs): void
46 | {
47 | $entity = $eventArgs->getEntity();
48 | $entityManager = $eventArgs->getEntityManager();
49 |
50 | if ($this->supports($entity)) {
51 | ($this->remove)($entity);
52 | }
53 | $this->handleEmbeddable($entityManager, $entity, $this->remove);
54 | }
55 |
56 | public function onClear(): void
57 | {
58 | $this->manager->clear();
59 | }
60 |
61 | private function supports(object $entity): bool
62 | {
63 | return $entity instanceof $this->class;
64 | }
65 |
66 | private function handleEmbeddable(
67 | EntityManagerInterface $entityManager,
68 | object $entity,
69 | Closure $action
70 | ): void {
71 | $classMetadata = $entityManager->getClassMetadata(ClassUtils::getClass($entity));
72 |
73 | foreach ($classMetadata->embeddedClasses as $property => $embeddedClass) {
74 | if (!is_a($embeddedClass['class'], $this->class, true)) {
75 | continue;
76 | }
77 |
78 | $refl = new ReflectionObject($entity);
79 | $reflProperty = $refl->getProperty($property);
80 | $reflProperty->setAccessible(true);
81 | /** @var File|null $file */
82 | $file = $reflProperty->getValue($entity);
83 |
84 | if ($file === null) {
85 | continue;
86 | }
87 | $action($file);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/PathResolver/AzureBlobStorageSASParametersTest.php:
--------------------------------------------------------------------------------
1 | parameters = new PathResolver\AzureBlobStorageSASParameters(new DateTimeImmutable());
19 | }
20 |
21 | public function testWithExpiry(): void
22 | {
23 | $expiry = new DateTimeImmutable();
24 |
25 | $new = $this->parameters->withExpiry($expiry);
26 |
27 | self::assertNotSame($new, $this->parameters);
28 | self::assertSame($expiry, $new->getExpiry());
29 | }
30 |
31 | public function testWithStart(): void
32 | {
33 | $expiry = new DateTimeImmutable();
34 |
35 | $new = $this->parameters->withStart($expiry);
36 |
37 | self::assertNotSame($new, $this->parameters);
38 | self::assertSame($expiry, $new->getStart());
39 | }
40 |
41 | public function testWithIp(): void
42 | {
43 | $new = $this->parameters->withIp('127.0.0.1');
44 |
45 | self::assertNotSame($new, $this->parameters);
46 | self::assertSame('127.0.0.1', $new->getIp());
47 | }
48 |
49 | public function testWithIdentifier(): void
50 | {
51 | $new = $this->parameters->withIdentifier('id');
52 |
53 | self::assertNotSame($new, $this->parameters);
54 | self::assertSame('id', $new->getIdentifier());
55 | }
56 |
57 | public function testWithCacheControl(): void
58 | {
59 | $new = $this->parameters->withCacheControl('cache-control');
60 |
61 | self::assertNotSame($new, $this->parameters);
62 | self::assertSame('cache-control', $new->getCacheControl());
63 | }
64 |
65 | public function testWithContentDisposition(): void
66 | {
67 | $new = $this->parameters->withContentDisposition('content-disposition');
68 |
69 | self::assertNotSame($new, $this->parameters);
70 | self::assertSame('content-disposition', $new->getContentDisposition());
71 | }
72 |
73 | public function testWithContentEncoding(): void
74 | {
75 | $new = $this->parameters->withContentEncoding('content-encoding');
76 |
77 | self::assertNotSame($new, $this->parameters);
78 | self::assertSame('content-encoding', $new->getContentEncoding());
79 | }
80 |
81 | public function testWithContentLanguage(): void
82 | {
83 | $new = $this->parameters->withContentLanguage('content-language');
84 |
85 | self::assertNotSame($new, $this->parameters);
86 | self::assertSame('content-language', $new->getContentLanguage());
87 | }
88 |
89 | public function testWithContentType(): void
90 | {
91 | $new = $this->parameters->withContentType('content-type');
92 |
93 | self::assertNotSame($new, $this->parameters);
94 | self::assertSame('content-type', $new->getContentType());
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
17 | assert($rootNode instanceof ArrayNodeDefinition);
18 |
19 | // @formatter:off
20 | /**
21 | * @psalm-suppress PossiblyNullReference
22 | * @psalm-suppress PossiblyUndefinedMethod
23 | */
24 | $rootNode
25 | ->fixXmlConfig('manager')
26 | ->children()
27 | ->booleanNode('form')->defaultTrue()->end()
28 | ->booleanNode('twig')->defaultTrue()->end()
29 | ->arrayNode('managers')
30 | ->useAttributeAsKey('name')
31 | ->arrayPrototype()
32 | ->performNoDeepMerging()
33 | ->children()
34 | ->enumNode('driver')->values(['orm'])->isRequired()->end()
35 | ->scalarNode('class')->isRequired()->end()
36 | ->arrayNode('storage')
37 | ->isRequired()
38 | ->beforeNormalization()
39 | ->ifString()
40 | ->then(static fn (string $value): array => ['service_id' => $value])
41 | ->end()
42 | ->children()
43 | ->scalarNode('service_id')->isRequired()->cannotBeEmpty()
44 | ->end()
45 | ->end()
46 | ->end()
47 | ->arrayNode('naming_strategy')
48 | ->isRequired()
49 | ->beforeNormalization()
50 | ->ifString()
51 | ->then(static fn (string $value): array => ['service_id' => $value])
52 | ->end()
53 | ->children()
54 | ->scalarNode('service_id')->isRequired()->cannotBeEmpty()
55 | ->end()
56 | ->end()
57 | ->end()
58 | ->scalarNode('repository')->defaultNull()->end()
59 | ->scalarNode('mime_type_detector')->defaultNull()->end()
60 | ->scalarNode('model_factory')->defaultNull()->end()
61 | ->end()
62 | ->end()
63 | ->defaultValue([])
64 | ->end()
65 | ->end();
66 | // @formatter:on
67 | return $treeBuilder;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/DelegatingManager.php:
--------------------------------------------------------------------------------
1 | , ManagerInterface> */
20 | private array $managers = [];
21 | private ManagerInterface $manager;
22 |
23 | /**
24 | * @param ManagerInterface[] $managers
25 | */
26 | public function __construct(array $managers, ManagerInterface $manager = null)
27 | {
28 | if (count($managers) === 0) {
29 | throw new InvalidArgumentException('You should pass at least one manager!');
30 | }
31 | if ($manager !== null) {
32 | $this->manager = $manager;
33 | } else {
34 | $this->manager = reset($managers);
35 | }
36 |
37 | foreach ($managers as $manager) {
38 | $this->managers[$manager->getClass()] = $manager;
39 | }
40 | }
41 |
42 | /**
43 | * @param class-string $class
44 | * @return ManagerInterface
45 | * @throws LogicException if not manager is found for $class
46 | * @template T of File
47 | */
48 | public function getManagerFor(string $class): ManagerInterface
49 | {
50 | if (!isset($this->managers[$class])) {
51 | throw new LogicException('No manager for ' . $class);
52 | }
53 |
54 | return $this->managers[$class];
55 | }
56 |
57 | public function upload(SplFileInfo $splFileInfo): File
58 | {
59 | return $this->manager->upload($splFileInfo);
60 | }
61 |
62 | public function getPathname(File $file): string
63 | {
64 | return $this->getManagerForFile($file)->getPathname($file);
65 | }
66 |
67 | public function read(File $file): string
68 | {
69 | return $this->getManagerForFile($file)->read($file);
70 | }
71 |
72 | public function readStream(File $file)
73 | {
74 | return $this->getManagerForFile($file)->readStream($file);
75 | }
76 |
77 | public function write(MutableFile $file, SplFileInfo $splFileInfo): void
78 | {
79 | $this->getManagerForFile($file)->write($file, $splFileInfo);
80 | }
81 |
82 | public function moveFile(File $file): void
83 | {
84 | $this->getManagerForFile($file)->moveFile($file);
85 | }
86 |
87 | public function remove(File $file): void
88 | {
89 | $this->getManagerForFile($file)->remove($file);
90 | }
91 |
92 | public function getClass(): string
93 | {
94 | return $this->manager->getClass();
95 | }
96 |
97 | public function clear(): void
98 | {
99 | foreach (array_merge($this->managers, [$this->manager->getClass() => $this->manager]) as $manager) {
100 | $manager->clear();
101 | }
102 | }
103 |
104 | /**
105 | * @param T $file
106 | * @return ManagerInterface
107 | * @throws LogicException if not manager is found for $file
108 | * @template T of File
109 | */
110 | private function getManagerForFile(File $file): ManagerInterface
111 | {
112 | foreach ($this->managers as $class => $manager) {
113 | if ($file instanceof $class) {
114 | return $manager;
115 | }
116 | }
117 | throw new LogicException('No manager for ' . get_class($file));
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tests/Command/VerifyConsistencyCommandTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
40 | $this->flysystem = $this->createMock(FilesystemOperator::class);
41 | $this->repository = $this->createMock(Repository::class);
42 |
43 | $this->command = new VerifyConsistencyCommand(
44 | new FlysystemStorage($this->flysystem),
45 | $this->manager,
46 | $this->repository
47 | );
48 | }
49 |
50 | public function testExecute(): void
51 | {
52 | $file1 = new File('filename.txt', 5, '5a105e8b9d40e1329780d62ea2265d8a', 'text/plain2');
53 | $file1->setId(1);
54 |
55 | $file2 = new File('filename', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
56 | $file2->setId(2);
57 |
58 | $files = [$file1, $file2];
59 | $this->repository->expects(self::once())->method('findAllForBatchProcessing')->willReturn($files);
60 |
61 | $this->manager
62 | ->expects(self::exactly(2))
63 | ->method('getPathname')
64 | ->withConsecutive(...array_map(static fn (File $file): array => [self::identicalTo($file)], $files))
65 | ->will(self::onConsecutiveCalls('file1path', 'file2path'));
66 |
67 | $content = 'test';
68 | $this->flysystem
69 | ->expects(self::exactly(2))
70 | ->method('readStream')
71 | ->withConsecutive(['file1path'])
72 | ->will(
73 | self::onConsecutiveCalls(
74 | fopen('data://text/plain;base64,' . base64_encode($content), 'r'),
75 | self::throwException(new FileException($file2, 'File not found'))
76 | )
77 | );
78 |
79 | $commandTester = new CommandTester($this->command);
80 | self::assertSame(0, $commandTester->execute([]));
81 |
82 | $output = str_replace(PHP_EOL, '', $commandTester->getDisplay());
83 | $output = preg_replace('/ +/', ' ', $output);
84 |
85 | self::assertStringContainsString('File file1path wrong size! Actual size: 4 bytes, expected 5 bytes!', $output);
86 | self::assertStringContainsString(
87 | 'File file1path wrong hash! Actual hash: 098f6bcd4621d373cade4e832627b4f6, expected 5a105e8b9d40e1329780d62ea2265d8a!',
88 | $output
89 | );
90 | self::assertStringContainsString(
91 | 'File file1path wrong mimeType! Actual mimeType: text/plain, expected text/plain2!',
92 | $output
93 | );
94 | self::assertStringContainsString('File file2path missing!', $output);
95 | self::assertStringContainsString('4 errors detected', $output);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/tests/Utility/DownloadUtilityTest.php:
--------------------------------------------------------------------------------
1 | pathname = __DIR__ . '/../files/image1.jpg';
31 | $flysystem = $this->createMock(FilesystemOperator::class);
32 | $flysystem->method('readStream')->willReturn(fopen($this->pathname, 'r'));
33 |
34 | $namingStrategy = $this->createMock(NamingStrategy::class);
35 | $this->manager = new Manager(File::class, new FlysystemStorage($flysystem), $namingStrategy);
36 | $this->downloadUtility = new DownloadUtility($this->manager);
37 | }
38 |
39 | public function createResponseProvider(): iterable
40 | {
41 | $file = new File('image1.jpg', 1234, '12345', 'image/jpeg');
42 | $expiresAt = new DateTimeImmutable('+30 days');
43 | yield [
44 | $file,
45 | 'attachment; filename=image1.jpg',
46 | 'image/jpeg',
47 | $expiresAt->format('D, d M Y H:i'),
48 | $file->getCreatedAt()->format('D, d M Y H:i:s') . ' GMT',
49 | 1234,
50 | ];
51 |
52 | yield [
53 | new DownloadableFile(
54 | $file,
55 | 'my_name.jpg',
56 | false,
57 | new DateTimeImmutable('2021-04-29 15:00:00')
58 | ),
59 | 'inline; filename=my_name.jpg',
60 | 'image/jpeg',
61 | 'Thu, 29 Apr 2021 15:00:00 GMT',
62 | $file->getCreatedAt()->format('D, d M Y H:i:s') . ' GMT',
63 | 1234,
64 | ];
65 |
66 | $mutableFile = new MutableFile('image1.jpg', 1234, '12345', 'image/jpeg');
67 | $mutableFile->setModifiedAt(new DateTimeImmutable('2021-04-30 15:00:00'));
68 | yield [
69 | new DownloadableFile(
70 | $mutableFile,
71 | 'my_name.jpg',
72 | false,
73 | new DateTimeImmutable('2021-05-29 15:00:00')
74 | ),
75 | 'inline; filename=my_name.jpg',
76 | 'image/jpeg',
77 | 'Sat, 29 May 2021 15:00:00 GMT',
78 | $mutableFile->getModifiedAt()->format('D, d M Y H:i:s') . ' GMT',
79 | 1234,
80 | ];
81 | }
82 |
83 | /**
84 | * @dataProvider createResponseProvider
85 | */
86 | public function testCreateResponse(
87 | \Arxy\FilesBundle\Model\File $file,
88 | string $expectedContentDisposition,
89 | string $expectedContentType,
90 | string $expectedExpires,
91 | string $expectedLastModified,
92 | int $expectedContentLength
93 | ): void {
94 | $response = $this->downloadUtility->createResponse($file);
95 |
96 | self::assertInstanceOf(StreamedResponse::class, $response);
97 | self::assertSame($expectedContentDisposition, $response->headers->get('Content-Disposition'));
98 | self::assertSame($expectedContentType, $response->headers->get('Content-Type'));
99 | self::assertStringContainsString($expectedExpires, $response->headers->get('Expires'));
100 | self::assertSame($expectedLastModified, $response->headers->get('Last-Modified'));
101 | self::assertSame((string)$expectedContentLength, $response->headers->get('Content-Length'));
102 |
103 | ob_start();
104 | $response->sendContent();
105 | $streamedContent = ob_get_clean();
106 |
107 | self::assertSame(file_get_contents($this->pathname), $streamedContent);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/tests/Form/Type/FileTypeTest.php:
--------------------------------------------------------------------------------
1 | manager = $this->createMock(ManagerInterface::class);
26 |
27 | parent::setUp();
28 | }
29 |
30 | protected function getExtensions(): array
31 | {
32 | $type = new FileType($this->manager);
33 |
34 | return [
35 | new PreloadedExtension([$type], []),
36 | ];
37 | }
38 |
39 | protected function getTypeExtensions(): array
40 | {
41 | return [
42 | new FormTypeHttpFoundationExtension(),
43 | ];
44 | }
45 |
46 | public function testSingleUpload(): void
47 | {
48 | $uploadedFile = new UploadedFile(__DIR__ . '/../../files/image1.jpg', 'image1.jpg');
49 |
50 | $file = new File('filename', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
51 |
52 | $this->manager->expects($this->once())->method('upload')->with($uploadedFile)->willReturn($file);
53 | $this->manager->expects($this->once())->method('getClass')->willReturn(File::class);
54 |
55 | $form = $this->factory->create(FileType::class, null);
56 |
57 | $form->submit(['file' => $uploadedFile]);
58 |
59 | $actual = $form->getData();
60 |
61 | self::assertTrue($form->isSynchronized());
62 | self::assertInstanceOf(File::class, $actual);
63 | self::assertSame($file, $actual);
64 | }
65 |
66 | public function testMultipleUpload(): void
67 | {
68 | $uploadedFile1 = new UploadedFile(__DIR__ . '/../../files/image1.jpg', 'image1.jpg');
69 | $uploadedFile2 = new UploadedFile(__DIR__ . '/../../files/image2.jpg', 'image2.jpg');
70 |
71 | $file1 = new File('filename', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
72 | $file2 = new File('filename', 125, '098f6bcd4621d373cade4e832627b4f6', 'image/jpeg');
73 |
74 | $this->manager->expects($this->exactly(2))
75 | ->method('upload')
76 | ->withConsecutive(
77 | [$this->identicalTo($uploadedFile1)],
78 | [$this->identicalTo($uploadedFile2)]
79 | )
80 | ->will($this->onConsecutiveCalls($file1, $file2));
81 |
82 | $this->manager->method('getClass')->willReturn(File::class);
83 |
84 | $form = $this->factory->create(
85 | FileType::class,
86 | null,
87 | [
88 | 'multiple' => true,
89 | ]
90 | );
91 |
92 | $form->submit(['file' => [$uploadedFile1, $uploadedFile2]]);
93 |
94 | $actual = $form->getData();
95 |
96 | self::assertTrue($form->isSynchronized());
97 | self::assertCount(2, $actual);
98 | self::assertSame($file1, $actual[0]);
99 | self::assertSame($file2, $actual[1]);
100 | }
101 |
102 | public function testInvalidManagerPassed(): void
103 | {
104 | $this->expectException(InvalidOptionsException::class);
105 | $this->expectExceptionMessage(
106 | 'The option "manager" with value stdClass is expected to be of type "Arxy\FilesBundle\ManagerInterface", but is of type "stdClass".'
107 | );
108 |
109 | $this->factory->create(
110 | FileType::class,
111 | null,
112 | [
113 | 'manager' => new stdClass(),
114 | ]
115 | );
116 | }
117 |
118 | public function testLabelOfFileIsFalse(): void
119 | {
120 | $form = $this->factory->create(
121 | FileType::class,
122 | null,
123 | [
124 | 'data_class' => File::class,
125 | ]
126 | );
127 |
128 | self::assertFalse($form->get('file')->getConfig()->getOption('label'));
129 | }
130 | }
131 |
--------------------------------------------------------------------------------