diff --git a/.docker/compose.yaml b/.docker/compose.yaml new file mode 100644 index 0000000..d273ca0 --- /dev/null +++ b/.docker/compose.yaml @@ -0,0 +1,14 @@ +x-build-args: &build-args + UID: "${UID:-1000}" + GID: "${GID:-1000}" + +name: cleverage-flysystem-process-bundle + +services: + php: + build: + context: php + args: + <<: *build-args + volumes: + - ../:/var/www diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 0000000..f98c3ba --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.2-fpm-alpine + +ARG UID +ARG GID + +RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" +COPY /conf.d/ "$PHP_INI_DIR/conf.d/" + +RUN apk update && apk add \ + tzdata \ + shadow \ + nano \ + bash \ + icu-dev \ + && docker-php-ext-configure intl \ + && docker-php-ext-install intl opcache \ + && docker-php-ext-enable opcache + +RUN ln -s /usr/share/zoneinfo/Europe/Paris /etc/localtime \ + && sed -i "s/^;date.timezone =.*/date.timezone = Europe\/Paris/" $PHP_INI_DIR/php.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN usermod -u $UID www-data \ + && groupmod -g $GID www-data + +USER www-data:www-data + +WORKDIR /var/www diff --git a/.docker/php/conf.d/dev.ini b/.docker/php/conf.d/dev.ini new file mode 100644 index 0000000..2a141be --- /dev/null +++ b/.docker/php/conf.d/dev.ini @@ -0,0 +1,5 @@ +display_errors = 1 +error_reporting = E_ALL + +opcache.validate_timestamps = 1 +opcache.revalidate_freq = 0 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..7711713 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..58db37d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml new file mode 100644 index 0000000..c9cd06b --- /dev/null +++ b/.github/workflows/notifications.yml @@ -0,0 +1,23 @@ +name: Rocket chat notifications + +# Controls when the action will run. +on: + push: + tags: + - '*' + +jobs: + notification: + runs-on: ubuntu-latest + + steps: + - name: Get the tag short reference + id: get_tag + run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + + - name: Rocket.Chat Notification + uses: madalozzo/Rocket.Chat.GitHub.Action.Notification@master + with: + type: success + job_name: "[cleverage/flysystem-process-bundle](https://github.com/cleverage/flysystem-process-bundle) : ${{ steps.get_tag.outputs.TAG }} has been released" + url: ${{ secrets.CLEVER_AGE_ROCKET_CHAT_WEBOOK_URL }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..9f1580f --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,62 @@ +name: Quality + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: PHPStan + run: vendor/bin/phpstan --no-progress --memory-limit=1G analyse --error-format=github + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: PHP-CS-Fixer + run: vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none + + rector: + name: Rector + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: Rector + run: vendor/bin/rector --no-progress-bar --dry-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2d7e7a4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + name: PHP ${{ matrix.php-version }} + ${{ matrix.dependencies }} + ${{ matrix.variant }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.allowed-to-fail }} + env: + SYMFONY_REQUIRE: ${{matrix.symfony-require}} + + strategy: + matrix: + php-version: + - '8.2' + - '8.3' + dependencies: [highest] + allowed-to-fail: [false] + symfony-require: [''] + variant: [normal] + include: + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.1.* + variant: symfony/symfony:"7.1.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.1.* + variant: symfony/symfony:"7.1.*" + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: pcov + tools: composer:v2, flex + - name: Add PHPUnit matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install variant + if: matrix.variant != 'normal' && !startsWith(matrix.variant, 'symfony/symfony') + run: composer require ${{ matrix.variant }} --no-update + - name: Install Composer dependencies (${{ matrix.dependencies }}) + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + - name: Run Tests with coverage + run: vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/logs/clover.xml + #- name: Send coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # files: build/logs/clover.xml diff --git a/.gitignore b/.gitignore index ff72e2d..ca08796 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ /composer.lock /vendor +.env +.idea +/phpunit.xml +.phpunit.result.cache +.phpunit.cache +.php-cs-fixer.cache +coverage-report diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..58730f9 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,46 @@ +setRules([ + '@PHP71Migration' => true, + '@PHP82Migration' => true, + '@PHPUnit75Migration:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'protected_to_private' => false, + 'native_constant_invocation' => ['strict' => false], + 'header_comment' => ['header' => $fileHeaderComment], + 'modernize_strpos' => true, + 'get_class_to_class_keyword' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->append([__FILE__]) + ) + ->setCacheFile('.php-cs-fixer.cache') +; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7d7d7c5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +v2.0 +------ + +## BC breaks + +* [#5](https://github.com/cleverage/flysystem-process-bundle/issues/5) Replace "oneup/flysystem-bundle": ">1.0,<4.0" by "league/flysystem-bundle": "^3.0" +* [#5](https://github.com/cleverage/flysystem-process-bundle/issues/5) Update Tasks for "league/flysystem-bundle": "^3.0" +* [#6](https://github.com/cleverage/flysystem-process-bundle/issues/6) Update services according to Symfony best practices. Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. + Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_flysystem_process` + +### Changes + +* [#3](https://github.com/cleverage/flysystem-process-bundle/issues/3) Add Makefile & .docker for local standalone usage +* [#3](https://github.com/cleverage/flysystem-process-bundle/issues/3) Add rector, phpstan & php-cs-fixer configurations & apply it +* [#4](https://github.com/cleverage/flysystem-process-bundle/issues/4) Remove `sidus/base-bundle` dependency + +### Fixes + +v1.0.1 +------ + +### Fixes + +* Fixed dependencies after removing sidus/base-bundle from the base process bundle + +v1.0.0 +------ + +* Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ef0dbe2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +Contributing +============ + +First of all, **thank you** for contributing, **you are awesome**! + +Here are a few rules to follow in order to ease code reviews, and discussions before +maintainers accept and merge your work. + +You MUST run the quality & test suites. + +You SHOULD write (or update) unit tests. + +You SHOULD write documentation. + +Please, write [commit messages that make sense](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), +and [rebase your branch](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) before submitting your Pull Request. + +One may ask you to [squash your commits](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +too. This is used to "clean" your Pull Request before merging it (we don't want +commits such as `fix tests`, `fix 2`, `fix 3`, etc.). + +Thank you! + +## Running the quality & test suites + +Tests suite uses Docker environments in order to be idempotent to OS's. More than this +PHP version is written inside the Dockerfile; this assures to test the bundle with +the same resources. No need to have PHP installed. + +You only need Docker set it up. + +To allow testing environments more smooth we implemented **Makefile**. +You have two commands available: + +```bash +make quality +``` + +```bash +make tests +``` + +## Deprecations notices + +When a feature should be deprecated, or when you have a breaking change for a future version, please : +* [Fill an issue](https://github.com/cleverage/flysystem-process-bundle/issues/new) +* Add TODO comments with the following format: `@TODO deprecated v2.0` +* Trigger a deprecation error: `@trigger_error('This feature will be deprecated in v2.0', E_USER_DEPRECATED);` + +You can check which deprecation notice is triggered in tests +* `make bash` +* `SYMFONY_DEPRECATIONS_HELPER=0 ./vendor/bin/phpunit` diff --git a/DependencyInjection/CleverAgeFlysystemProcessExtension.php b/DependencyInjection/CleverAgeFlysystemProcessExtension.php deleted file mode 100644 index 4ff925e..0000000 --- a/DependencyInjection/CleverAgeFlysystemProcessExtension.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @author Vincent Chalnot - * @author Madeline Veyrenc - */ -class CleverAgeFlysystemProcessExtension extends SidusBaseExtension -{ -} diff --git a/LICENSE b/LICENSE index fdc6131..045d824 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2015-2019 Clever-Age +Copyright (c) Clever-Age Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0a58e32 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +.ONESHELL: +SHELL := /bin/bash + +DOCKER_RUN_PHP = docker compose -f .docker/compose.yaml run --rm php "bash" "-c" +DOCKER_COMPOSE = docker compose -f .docker/compose.yaml + +start: upd #[Global] Start application + +src/vendor: #[Composer] install dependencies + $(DOCKER_RUN_PHP) "composer install --no-interaction" + +upd: #[Docker] Start containers detached + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans --detach + +up: #[Docker] Start containers + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans + +stop: #[Docker] Down containers + $(DOCKER_COMPOSE) stop + +down: #[Docker] Down containers + $(DOCKER_COMPOSE) down + +build: #[Docker] Build containers + $(DOCKER_COMPOSE) build + +ps: # [Docker] Show running containers + $(DOCKER_COMPOSE) ps + +bash: #[Docker] Connect to php container with current host user + $(DOCKER_COMPOSE) exec php bash + +logs: #[Docker] Show logs + $(DOCKER_COMPOSE) logs -f + +quality: phpstan php-cs-fixer rector #[Quality] Run all quality checks + +phpstan: #[Quality] Run PHPStan + $(DOCKER_RUN_PHP) "vendor/bin/phpstan --no-progress --memory-limit=1G analyse" + +php-cs-fixer: #[Quality] Run PHP-CS-Fixer + $(DOCKER_RUN_PHP) "vendor/bin/php-cs-fixer fix --diff --verbose" + +rector: #[Quality] Run Rector + $(DOCKER_RUN_PHP) "vendor/bin/rector" + +tests: phpunit #[Tests] Run all tests + +phpunit: #[Tests] Run PHPUnit + $(DOCKER_RUN_PHP) "vendor/bin/phpunit" diff --git a/README.md b/README.md index c1fa163..9b11f51 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,22 @@ CleverAge/FlysystemProcessBundle ======================= -See process bundle documentation +This bundle is a part of the [CleverAge/ProcessBundle](https://github.com/cleverage/process-bundle) project. +It provides [Flysystem](https://flysystem.thephpleague.com/docs/) library integration on Process bundle. + +Compatible with [Symfony stable version and latest Long-Term Support (LTS) release](https://symfony.com/releases). + +## Documentation + +For usage documentation, see: +[docs/index.md](docs/index.md) + +## Support & Contribution + +For general support and questions, please use [Github](https://github.com/cleverage/flysystem-process-bundle/issues). +If you think you found a bug or you have a feature idea to propose, feel free to open an issue after looking at the [contributing](CONTRIBUTING.md) guide. + +## License + +This bundle is under the MIT license. +For the whole copyright, see the [LICENSE](LICENSE) file distributed with this source code. diff --git a/Resources/config/services/task.yml b/Resources/config/services/task.yml deleted file mode 100644 index 6440b85..0000000 --- a/Resources/config/services/task.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - CleverAge\FlysystemProcessBundle\Task\: - resource: '../../../Task/*' - autowire: true - public: true - shared: false - tags: - - { name: monolog.logger, channel: cleverage_process_task } diff --git a/Task/FilesystemOptionTrait.php b/Task/FilesystemOptionTrait.php deleted file mode 100644 index 27bbdcc..0000000 --- a/Task/FilesystemOptionTrait.php +++ /dev/null @@ -1,49 +0,0 @@ -setRequired($optionName); - $resolver->setAllowedTypes($optionName, 'string'); - $resolver->setNormalizer($optionName, function (Options $options, $value) { - return $this->getMountManager()->getFilesystem($value); - }); - } - - protected function getFilesystem(ProcessState $state, $optionName): FilesystemInterface - { - return $this->getOption($state, $optionName); - } - - abstract protected function getMountManager(): MountManager; - - /** - * @see \CleverAge\ProcessBundle\Model\AbstractConfigurableTask::getOption - * - * @param ProcessState $state - * @param string $code - * - * @return mixed - */ - abstract protected function getOption(ProcessState $state, $code); -} diff --git a/Task/ListContentTask.php b/Task/ListContentTask.php deleted file mode 100644 index c16abc4..0000000 --- a/Task/ListContentTask.php +++ /dev/null @@ -1,111 +0,0 @@ -mountManager = $mountManager; - } - - protected function configureOptions(OptionsResolver $resolver) - { - $this->configureFilesystemOption($resolver, 'filesystem'); - - $resolver->setDefault('file_pattern', null); - $resolver->setAllowedTypes('file_pattern', ['null', 'string']); - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - if ($this->fsContent === null || key($this->fsContent) === null) { - $filesystem = $this->getFilesystem($state, 'filesystem'); - $pattern = $this->getOption($state, 'file_pattern'); - - $this->fsContent = $this->getFilteredFilesystemContents($filesystem, $pattern); - } - - if (key($this->fsContent) === null) { - $state->setSkipped(true); - $this->fsContent = null; - } else { - $state->setOutput(current($this->fsContent)); - } - } - - public function next(ProcessState $state) - { - if (!is_array($this->fsContent)) { - return false; - } - - next($this->fsContent); - - return key($this->fsContent) !== null; - } - - - /** - * @param FilesystemInterface $filesystem - * @param string|null $pattern - * - * @return array - */ - protected function getFilteredFilesystemContents(FilesystemInterface $filesystem, $pattern = null): array - { - $results = []; - foreach ($filesystem->listContents() as $item) { - if ($pattern === null || \preg_match($pattern, $item['path'])) { - $results[] = $item; - } - } - - return $results; - } - - /** - * @return MountManager - */ - protected function getMountManager(): MountManager - { - return $this->mountManager; - } - -} diff --git a/Task/RemoveFileTask.php b/Task/RemoveFileTask.php deleted file mode 100644 index 68ed0e3..0000000 --- a/Task/RemoveFileTask.php +++ /dev/null @@ -1,68 +0,0 @@ -mountManager = $mountManager; - $this->logger = $logger; - } - - protected function configureOptions(OptionsResolver $resolver) - { - $this->configureFilesystemOption($resolver, 'filesystem'); - } - - public function execute(ProcessState $state) - { - $filesystem = $this->getFilesystem($state, 'filesystem'); - $filePath = $state->getInput(); - - $success = $filesystem->delete($filePath); - - if ($success) { - $this->logger->info('Deleted input file', ['file' => $filePath]); - } else { - $this->logger->warning('Failed to deleted input file', ['file' => $filePath]); - } - } - - protected function getMountManager(): MountManager - { - return $this->mountManager; - } - -} diff --git a/composer.json b/composer.json index a25787b..208e9f4 100644 --- a/composer.json +++ b/composer.json @@ -34,15 +34,35 @@ ], "autoload": { "psr-4": { - "CleverAge\\FlysystemProcessBundle\\": "" + "CleverAge\\FlysystemProcessBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "CleverAge\\FlysystemProcessBundle\\Tests\\": "tests/" } }, "require": { - "cleverage/process-bundle": "3.*|dev-v3.0-dev", - "oneup/flysystem-bundle": ">1.0,<4.0", - "sidus/base-bundle": "~1.0" + "php": ">=8.1", + "cleverage/process-bundle": "^4.0", + "league/flysystem-bundle": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~6.4" + "friendsofphp/php-cs-fixer": "*", + "phpstan/extension-installer": "*", + "phpstan/phpstan": "*", + "phpstan/phpstan-symfony": "*", + "phpunit/phpunit": "<10.0", + "rector/rector": "*", + "roave/security-advisories": "dev-latest", + "symfony/test-pack": "^1.1" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "sort-packages": true } } diff --git a/config/services/task.yaml b/config/services/task.yaml new file mode 100644 index 0000000..f6d4d98 --- /dev/null +++ b/config/services/task.yaml @@ -0,0 +1,28 @@ +services: + _defaults: + public: false + tags: + - { name: monolog.logger, channel: cleverage_process_task } + + cleverage_flysystem_process.task.file_fetch: + class: CleverAge\FlysystemProcessBundle\Task\FileFetchTask + arguments: [!tagged_locator { tag: 'flysystem.storage', index_by: 'storage' }] + CleverAge\FlysystemProcessBundle\Task\FileFetchTask: + alias: cleverage_flysystem_process.task.file_fetch + public: true + + cleverage_flysystem_process.task.list_content: + class: CleverAge\FlysystemProcessBundle\Task\ListContentTask + arguments: [!tagged_locator { tag: 'flysystem.storage', index_by: 'storage' }] + CleverAge\FlysystemProcessBundle\Task\ListContentTask: + alias: cleverage_flysystem_process.task.list_content + public: true + + cleverage_flysystem_process.task.remove_file: + class: CleverAge\FlysystemProcessBundle\Task\RemoveFileTask + arguments: + - '@logger' + - !tagged_locator { tag: 'flysystem.storage', index_by: 'storage' } + CleverAge\FlysystemProcessBundle\Task\RemoveFileTask: + alias: cleverage_flysystem_process.task.remove_file + public: true diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..bf1ecdb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,42 @@ +## Prerequisite + +CleverAge/ProcessBundle must be [installed](https://github.com/cleverage/process-bundle/blob/main/docs/01-quick_start.md#installation. + +## Installation + +Make sure Composer is installed globally, as explained in the [installation chapter](https://getcomposer.org/doc/00-intro.md) +of the Composer documentation. + +Open a command console, enter your project directory and install it using composer: + +```bash +composer require cleverage/flysystem-process-bundle +``` + +Remember to add the following line to config/bundles.php (not required if Symfony Flex is used) + +```php +CleverAge\FlysystemProcessBundle\CleverAgeFlysystemProcessBundle::class => ['all' => true], +``` + +Configure at least one flysytem/storage into `config/packages/flysytem.yaml` + +```yaml +#config/packages/flysytem.yaml +flysystem: + storages: + storage.source: # This is the identifier of flysytem/storage + adapter: 'local' + options: + directory: '%kernel.project_dir%/var/storage/source' +``` + +See https://github.com/thephpleague/flysystem-bundle?tab=readme-ov-file for more sample configuration (sftp, ftp, amazon s3 ...) + + +## Reference + +- Tasks + - [FileFetchTask](reference/tasks/01-FileFetchTask.md) + - [ListContentTask](reference/tasks/02-ListContentTask.md) + - [RemoveFileTask](reference/tasks/03-RemoveFileTask.md) diff --git a/docs/reference/tasks/01-FileFetchTask.md b/docs/reference/tasks/01-FileFetchTask.md new file mode 100644 index 0000000..a5dca8a --- /dev/null +++ b/docs/reference/tasks/01-FileFetchTask.md @@ -0,0 +1,73 @@ +FileFetchTask +======== + +Perform copy between 2 flysystems storage + +Task reference +-------------- + +* **Service**: [`CleverAge\FlysystemProcessBundle\Task\FileFetchTask`](../src/Task/FileFetchTask.php) + +Accepted inputs +--------------- + +The filename or filenames to copy from. + +If the option `file_pattern` is not set the input is used as strict filename(s) to match. + +If input is set but not corresponding as any file into `source_filesystem` task failed with UnableToReadFile exception. + +If FileFetchTask is the fisrt task of you process and you wan to use input, don't forgive to set the `entry_point` task name at process level + +Possible outputs +---------------- + +Filename of copied file. + +Options +------- + +| Code | Type | Required | Default | Description | +|--------------------------|:----------:|:---------:|:---------:|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `source_filesystem` | `string` | **X** | | The source flysystem/storage.
See config/packages/flysystem.yaml to see configured flysystem/storages. | +| `destination_filesystem` | `string` | **X** | | The source flysystem/storage.
See config/packages/flysystem.yaml to see configured flysystem/storages. | +| `file_pattern` | `string` | | null | The file_parttern used in preg_match to match into `source_filesystem` list of files. If not set try to use input as strict filename to match | +| `remove_source` | `bool` | | false | If true delete source file after copy | + + +Examples +-------- + +* Simple fetch task configuration + - See config/packages/flysystem.yaml to see configured flysystems/storages. + - copy all .csv files from 'storage.source' to 'storage.destination' + - remove .csv from 'storage.source' after copy + - output will be filename of copied files +```yaml +# Task configuration level +code: + service: '@CleverAge\FlysystemProcessBundle\Task\FileFetchTask' + options: + source_filesystem: 'storage.source' + destination_filesystem: 'storage.destination' + file_pattern: '/.csv$/' + remove_source: true +``` + +* Simple fetch process configuration to cipy a specific file from --input option via
```bin/console cleverage:process:execute my_custom_process --input=foobar.csv -vv``` + - See config/packages/flysystem.yaml to see configured flysystems/storages. + - copy input file from 'storage.source' to 'storage.destination' + - remove .csv from 'storage.source' after copy + - output will be filename of copied file +```yaml +# Full process configuration to use input as filename with the following call +my_custom_process: + entry_point: copy_from_input + tasks: + copy_from_input: + service: '@CleverAge\FlysystemProcessBundle\Task\FileFetchTask' + options: + source_filesystem: 'storage.source' + destination_filesystem: 'storage.destination' + remove_source: true +``` diff --git a/docs/reference/tasks/02-ListContentTask.md b/docs/reference/tasks/02-ListContentTask.md new file mode 100644 index 0000000..036c0c3 --- /dev/null +++ b/docs/reference/tasks/02-ListContentTask.md @@ -0,0 +1,42 @@ +ListContentTask +======== + +List files of a flysystem storage + +Task reference +-------------- + +* **Service**: [`CleverAge\FlysystemProcessBundle\Task\ListContentTask`](../src/Task/ListContentTask.php) + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +League\Flysystem\StorageAttributes + +Options +------- + +| Code | Type | Required | Default | Description | +|----------------|:----------:|:---------:|:---------:|------------------------------------------------------------------------------------------------------------| +| `filesystem` | `string` | **X** | | The source flysystem/storage.
See config/packages/flysystem.yaml to see configured flysystem/storages. | +| `file_pattern` | `string` | | | The file_parttern used in preg_match to match into `filesystem` | + +Examples +-------- +* Simple list task configuration from a filesystem + - see config/packages/flysystem.yaml to see configured flysystems/storages. + - list all .csv files from 'storage.source' + - output will be League\Flysystem\StorageAttributes representation of copied files +```yaml +# Task configuration level +code: + service: '@CleverAge\FlysystemProcessBundle\Task\ListContentTask' + options: + filesystem: 'storage.source' + file_pattern: '/.csv$/' +``` diff --git a/docs/reference/tasks/03-RemoveFileTask.md b/docs/reference/tasks/03-RemoveFileTask.md new file mode 100644 index 0000000..4928f33 --- /dev/null +++ b/docs/reference/tasks/03-RemoveFileTask.md @@ -0,0 +1,46 @@ +RemoveFileTask +======== + +Remove a file from a flysystem storage + +Task reference +-------------- + +* **Service**: [`CleverAge\FlysystemProcessBundle\Task\RemoveFileTask`](../src/Task/RemoveFileTask.php) + +Accepted inputs +--------------- + +The filename of the file to remove on `filesystem`. + +When filename is deleted add a info log. + +If filename not found or cannot be deleted on `filesystem` add a warning log. + +Possible outputs +---------------- + +None + +Options +------- + +| Code | Type | Required | Default | Description | +|--------------|:----------:|:----------:|:---------:|-------------------------------------------------------------------------------------------------------------| +| `filesystem` | `string` | **X** | | The source flysystem/storage.
See config/packages/flysystem.yaml to see configured flysystem/storages. | + +Examples +-------- +* Simple process to remove a file on 'storage.source' via
```bin/console cleverage:process:execute my_custom_process --input=foobar.csv -vv``` + - see config/packages/flysystem.yaml to see configured flysystems/storages. + - remove file with name passed as input +```yaml +# +my_custom_process: + entry_point: remove_from_input + tasks: + remove_from_input: + service: '@CleverAge\FlysystemProcessBundle\Task\FileFetchTask' + options: + filesystem: 'storage.source' +``` diff --git a/docs/reference/tasks/_template.md b/docs/reference/tasks/_template.md new file mode 100644 index 0000000..ed1d4a5 --- /dev/null +++ b/docs/reference/tasks/_template.md @@ -0,0 +1,44 @@ +TaskName +======== + +_Describe main goal an use cases of the task_ + +Task reference +-------------- + +* **Service**: `ClassName` + +Accepted inputs +--------------- + +_Description of allowed types_ + +Possible outputs +---------------- + +_Description of possible types_ + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `code` | `type` | **X** _or nothing_ | `default value` _if available_ | _description_ | + +Examples +-------- + +_YAML samples and explanations_ + +* Example 1 + - details + - details + +```yaml +# Task configuration level +code: + service: '@service_ref' + options: + a: 1 + b: 2 +``` diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e9a9e7e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 10 + paths: + - src + - tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..766495c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..72a2408 --- /dev/null +++ b/rector.php @@ -0,0 +1,30 @@ +withPhpVersion(PhpVersion::PHP_82) + ->withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withPhpSets(php82: true) + // here we can define, what prepared sets of rules will be applied + ->withPreparedSets( + deadCode: true, + codeQuality: true + ) + ->withSets([ + LevelSetList::UP_TO_PHP_82, + SymfonySetList::SYMFONY_64, + SymfonySetList::SYMFONY_71, + SymfonySetList::SYMFONY_CODE_QUALITY, + SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, + SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, + ]) +; diff --git a/CleverAgeFlysystemProcessBundle.php b/src/CleverAgeFlysystemProcessBundle.php similarity index 55% rename from CleverAgeFlysystemProcessBundle.php rename to src/CleverAgeFlysystemProcessBundle.php index 778cb4d..0b3a69a 100644 --- a/CleverAgeFlysystemProcessBundle.php +++ b/src/CleverAgeFlysystemProcessBundle.php @@ -1,8 +1,11 @@ - - * @author Vincent Chalnot - * @author Madeline Veyrenc - */ class CleverAgeFlysystemProcessBundle extends Bundle { + public function getPath(): string + { + return \dirname(__DIR__); + } } diff --git a/src/DependencyInjection/CleverAgeFlysystemProcessExtension.php b/src/DependencyInjection/CleverAgeFlysystemProcessExtension.php new file mode 100644 index 0000000..3802c8c --- /dev/null +++ b/src/DependencyInjection/CleverAgeFlysystemProcessExtension.php @@ -0,0 +1,47 @@ +findServices($container, __DIR__.'/../../config/services'); + } + + /** + * Recursively import config files into container. + */ + protected function findServices(ContainerBuilder $container, string $path, string $extension = 'yaml'): void + { + $finder = new Finder(); + $finder->in($path) + ->name('*.'.$extension)->files(); + $loader = new YamlFileLoader($container, new FileLocator($path)); + foreach ($finder as $file) { + $loader->load($file->getFilename()); + } + } +} diff --git a/Task/FileFetchTask.php b/src/Task/FileFetchTask.php similarity index 50% rename from Task/FileFetchTask.php rename to src/Task/FileFetchTask.php index 422e3a4..b915e67 100644 --- a/Task/FileFetchTask.php +++ b/src/Task/FileFetchTask.php @@ -1,8 +1,11 @@ - + * Either get files using a file regexp, or take files from input. */ class FileFetchTask extends AbstractConfigurableTask implements IterableTaskInterface { - /** @var MountManager */ - protected $mountManager; - - /** @var FilesystemInterface */ - protected $sourceFS; + protected FilesystemOperator $sourceFS; - /** @var FilesystemInterface */ - protected $destinationFS; + protected FilesystemOperator $destinationFS; - /** @var array */ - protected $matchingFiles = []; + /** + * @var array + */ + protected array $matchingFiles = []; /** - * @param MountManager|null $mountManager + * @param ServiceLocator $storages */ - public function __construct(MountManager $mountManager = null) + public function __construct(protected readonly ServiceLocator $storages) { - $this->mountManager = $mountManager; } /** - * @param ProcessState $state - * * @throws \InvalidArgumentException - * @throws ExceptionInterface - * @throws FilesystemNotFoundException */ - public function initialize(ProcessState $state) + public function initialize(ProcessState $state): void { - if (!$this->mountManager) { - throw new ServiceNotFoundException('MountManager service not found, you need to install FlySystemBundle'); - } // Configure options parent::initialize($state); - $this->sourceFS = $this->mountManager->getFilesystem($this->getOption($state, 'source_filesystem')); - $this->destinationFS = $this->mountManager->getFilesystem($this->getOption($state, 'destination_filesystem')); + /** @var string $sourceFilesystemOption */ + $sourceFilesystemOption = $this->getOption($state, 'source_filesystem'); + $this->sourceFS = $this->storages->get($sourceFilesystemOption); + /** @var string $destinationFilesystemOption */ + $destinationFilesystemOption = $this->getOption($state, 'destination_filesystem'); + $this->destinationFS = $this->storages->get($destinationFilesystemOption); } /** - * @param ProcessState $state - * * @throws \InvalidArgumentException - * @throws ExceptionInterface * @throws \UnexpectedValueException - * @throws FilesystemNotFoundException - * @throws FileNotFoundException + * @throws FilesystemException */ public function execute(ProcessState $state): void { @@ -88,45 +75,44 @@ public function execute(ProcessState $state): void return; } - $this->doFileCopy($state, $file, $this->getOption($state, 'remove_source')); + /** @var bool $removeSourceOption */ + $removeSourceOption = $this->getOption($state, 'remove_source'); + $this->doFileCopy($state, $file, $removeSourceOption); $state->setOutput($file); } /** - * @param ProcessState $state - * * @throws \UnexpectedValueException - * @throws ExceptionInterface * @throws \InvalidArgumentException - * - * @return bool|mixed + * @throws FilesystemException */ - public function next(ProcessState $state) + public function next(ProcessState $state): bool { $this->findMatchingFiles($state); - return next($this->matchingFiles); + return false !== next($this->matchingFiles); } /** - * @param ProcessState $state - * * @throws \UnexpectedValueException * @throws \InvalidArgumentException - * @throws ExceptionInterface + * @throws FilesystemException */ - protected function findMatchingFiles(ProcessState $state) + protected function findMatchingFiles(ProcessState $state): void { + /** @var ?string $filePattern */ $filePattern = $this->getOption($state, 'file_pattern'); if ($filePattern) { foreach ($this->sourceFS->listContents('/') as $file) { - if ('file' === $file['type'] - && preg_match($filePattern, $file['path']) - && !\in_array($file['path'], $this->matchingFiles, true)) { - $this->matchingFiles[] = $file['path']; + if ('file' === $file->type() + && preg_match($filePattern, $file->path()) + && !\in_array($file->path(), $this->matchingFiles, true) + ) { + $this->matchingFiles[] = $file->path(); } } } else { + /** @var array|string|null $input */ $input = $state->getInput(); if (!$input) { throw new \UnexpectedValueException('No pattern neither input provided for the Task'); @@ -144,45 +130,32 @@ protected function findMatchingFiles(ProcessState $state) } /** - * @param ProcessState $state - * @param string $filename - * @param bool $removeSource - * - * @throws FileNotFoundException - * @throws ExceptionInterface - * @throws FilesystemNotFoundException * @throws \InvalidArgumentException - * - * @return mixed + * @throws FilesystemException */ - protected function doFileCopy(ProcessState $state, $filename, $removeSource) + protected function doFileCopy(ProcessState $state, string $filename, bool $removeSource): bool|string|null { - $prefixFrom = $this->getOption($state, 'source_filesystem'); - $prefixTo = $this->getOption($state, 'destination_filesystem'); - - $buffer = $this->mountManager->getFilesystem($prefixFrom)->readStream($filename); + $buffer = $this->sourceFS->readStream($filename); - if (false === $buffer) { - return false; + try { + $this->destinationFS->writeStream($filename, $buffer); + $result = true; + } catch (FilesystemException) { + $result = false; } - $result = $this->mountManager->getFilesystem($prefixTo)->putStream($filename, $buffer); - if (\is_resource($buffer)) { fclose($buffer); } if ($removeSource) { - $this->mountManager->delete(sprintf('%s://%s', $prefixFrom, $filename)); + $this->sourceFS->delete($filename); } return $result ? $filename : null; } - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired(['source_filesystem', 'destination_filesystem']); $resolver->setAllowedTypes('source_filesystem', 'string'); diff --git a/src/Task/ListContentTask.php b/src/Task/ListContentTask.php new file mode 100644 index 0000000..c804135 --- /dev/null +++ b/src/Task/ListContentTask.php @@ -0,0 +1,100 @@ +|null + */ + protected ?array $fsContent = null; + + /** + * @param ServiceLocator $storages + */ + public function __construct(protected readonly ServiceLocator $storages) + { + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('filesystem'); + $resolver->setAllowedTypes('filesystem', 'string'); + $resolver->setDefault('file_pattern', null); + $resolver->setAllowedTypes('file_pattern', ['null', 'string']); + } + + /** + * @throws \InvalidArgumentException + * @throws FilesystemException + */ + public function execute(ProcessState $state): void + { + if (null === $this->fsContent || null === key($this->fsContent)) { + /** @var string $filesystemOption */ + $filesystemOption = $this->getOption($state, 'filesystem'); + $filesystem = $this->storages->get($filesystemOption); + /** @var ?string $patternOption */ + $patternOption = $this->getOption($state, 'file_pattern'); + + $this->fsContent = $this->getFilteredFilesystemContents($filesystem, $patternOption); + } + + if (null === key($this->fsContent)) { + $state->setSkipped(true); + $this->fsContent = null; + } else { + $state->setOutput(current($this->fsContent)); + } + } + + public function next(ProcessState $state): bool + { + if (!\is_array($this->fsContent)) { + return false; + } + + next($this->fsContent); + + return null !== key($this->fsContent); + } + + /** + * @return list<\League\Flysystem\StorageAttributes> + * + * @throws FilesystemException + */ + protected function getFilteredFilesystemContents(FilesystemOperator $filesystem, ?string $pattern = null): array + { + $results = []; + foreach ($filesystem->listContents('') as $item) { + if (null === $pattern || preg_match($pattern, $item->path())) { + $results[] = $item; + } + } + + return $results; + } +} diff --git a/src/Task/RemoveFileTask.php b/src/Task/RemoveFileTask.php new file mode 100644 index 0000000..2f195f4 --- /dev/null +++ b/src/Task/RemoveFileTask.php @@ -0,0 +1,63 @@ + $storages + */ + public function __construct(protected LoggerInterface $logger, protected readonly ServiceLocator $storages) + { + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('filesystem'); + $resolver->setAllowedTypes('filesystem', 'string'); + } + + public function execute(ProcessState $state): void + { + /** @var string $filesystemOption */ + $filesystemOption = $this->getOption($state, 'filesystem'); + $filesystem = $this->storages->get($filesystemOption); + /** @var string $filePath */ + $filePath = $state->getInput(); + + try { + $filesystem->delete($filePath); + $result = true; + } catch (FilesystemException) { + $result = false; + } + + if ($result) { + $this->logger->info('Deleted input file', ['file' => $filePath]); + } else { + $this->logger->warning('Failed to deleted input file', ['file' => $filePath]); + } + } +} diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29