├── .editorconfig ├── .gitignore ├── .scrutinizer.yml ├── .semver ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── composer.json ├── config ├── Migrations │ ├── 20141213004653_initial_migration.php │ ├── 20160302083933_fixing_mime_type_field.php │ ├── 20170222133412_updating_length_of_file_extension_field.php │ └── 20200714095531_AddingVariantsAndMetadataFields.php └── routes.php ├── docs ├── Documentation │ ├── Getting-a-File-Path-and-URL.md │ ├── How-To-Use.md │ ├── How-it-works.md │ ├── Image-Storage-And-Versioning.md │ ├── Installation.md │ ├── List-of-included-Adapters.md │ ├── The-Image-Helper.md │ ├── The-Image-Version-Shell.md │ └── images │ │ └── file-storage-flowchart.jpg ├── README.md └── Tutorials │ ├── Quick-Start.md │ ├── Replacing-Files.md │ └── Validating-File-Uploads.md ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml.dist ├── pmip └── pmip.rb ├── psalm.xml ├── rector.yml ├── src ├── FileStorage │ ├── DataTransformer.php │ └── DataTransformerInterface.php ├── Model │ ├── Behavior │ │ └── FileStorageBehavior.php │ ├── Entity │ │ ├── FileStorage.php │ │ └── FileStorageEntityInterface.php │ └── Table │ │ └── FileStorageTable.php ├── Plugin.php ├── Shell │ ├── ImageVersionShell.php │ ├── StorageShell.php │ └── Task │ │ └── ImageTask.php ├── Utility │ └── StorageUtils.php └── View │ └── Helper │ └── ImageHelper.php └── tests ├── Fixture ├── File │ ├── cake.icon.png │ └── titus.jpg ├── FileStorageFixture.php └── ItemFixture.php ├── TestCase ├── FileStorageTestCase.php ├── FileStorageTestTable.php ├── Model │ ├── Behavior │ │ └── FileStorageBehaviorTest.php │ ├── Entity │ │ └── FileStorageTest.php │ └── Table │ │ └── FileStorageTableTest.php └── View │ └── Helper │ └── ImageHelperTest.php └── bootstrap.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | charset = utf-8 13 | 14 | [*.bat] 15 | end_of_line = crlf 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor 3 | /tmp 4 | /nbproject 5 | /config/Migrations/schema-dump-default.lock 6 | /clover.xml 7 | /composer.lock 8 | /.phpunit.result.cache 9 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: 3 | code_rating: true 4 | remove_extra_empty_lines: true 5 | remove_php_closing_tag: true 6 | remove_trailing_whitespace: true 7 | tools: 8 | php_code_coverage: false 9 | php_loc: 10 | enabled: true 11 | excluded_dirs: [vendor, tests, config, docs] 12 | php_cpd: 13 | enabled: true 14 | excluded_dirs: [vendor, tests, config, docs] 15 | filter: 16 | excluded_paths: [src/Event/, src/Lib/, tests, vendor, docs] 17 | build: 18 | environment: 19 | php: 20 | version: 7.2 21 | project_setup: 22 | before: 23 | - mysql -e "CREATE DATABASE test" 24 | 25 | tests: 26 | override: 27 | - 28 | command: './vendor/bin/phpunit --coverage-clover=coverage.xml' 29 | coverage: 30 | file: 'coverage.xml' 31 | format: 'php-clover' 32 | -------------------------------------------------------------------------------- /.semver: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 3 3 | :minor: 0 4 | :patch: 0 5 | :special: '' 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.4 5 | 6 | env: 7 | matrix: 8 | - GENERIC=1 9 | 10 | global: 11 | - DEFAULT=1 12 | 13 | matrix: 14 | fast_finish: true 15 | include: 16 | - php: 7.4 17 | env: COVERAGE=1 DEFAULT=0 18 | 19 | - php: 7.4 20 | env: PHPCS=1 DEFAULT=0 21 | 22 | - php: 7.4 23 | env: PHPSTAN=1 DEFAULT=0 24 | 25 | before_script: 26 | - composer self-update 27 | - composer install --prefer-dist --no-interaction 28 | - sh -c "if [ '$COVERALLS' = '1' ]; then mkdir -p build/logs; fi" 29 | - phpenv config-rm xdebug.ini 30 | - if [[ $PHPSTAN = 1 ]]; then composer stan-setup; fi 31 | 32 | script: 33 | - vendor/bin/phpunit 34 | - if [[ $DEFAULT = 1 ]]; then composer test; fi 35 | - if [[ $COVERAGE = 1 ]]; then composer coverage-test; fi 36 | - if [[ $PHPCS = 1 ]]; then composer cs-check; fi 37 | - if [[ $PHPSTAN = 1 ]]; then composer stan; fi 38 | 39 | after_success: 40 | - if [[ $COVERAGE = 1 ]]; then bash <(curl -s https://codecov.io/bash); fi 41 | 42 | notifications: 43 | email: false 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased](https://github.com/burzum/cakephp-file-storage/compare/2.0.0-rc1...2.0) 8 | ### Added 9 | 10 | ### Changed 11 | 12 | ### Fixed 13 | 14 | ## [2.0.0](https://github.com/burzum/cakephp-file-storage/releases/tag/2.0.0) - 2018-09-07 15 | ### Changed 16 | - Added this change log 17 | - Improved documentation 18 | 19 | ## [2.0.0-rc1](https://github.com/burzum/cakephp-file-storage/releases/tag/2.0.0-rc1) - 2018-09-07 20 | ### Changed 21 | - Updated dependencies 22 | 23 | ## [2.0.0-beta2](https://github.com/burzum/cakephp-file-storage/releases/tag/2.0.0-beta2) - 2018-09-07 24 | ### Changed 25 | - Improved documentation 26 | - Updated dependencies 27 | - Upgraded to CakePHP 3.6 28 | - Removed upload validation methods as they are part of the CakePHP core 29 | - Increased the length of the extension field in the DB #157 30 | - Updated Travis CI configuration, remove PHP 5.6, add PHP 7.1, 7.2 & nightly 31 | 32 | ### Fixed 33 | - Fixed Travis CI builds 34 | 35 | ## [2.0.0-beta1](https://github.com/burzum/cakephp-file-storage/releases/tag/2.0.0-beta1) - 2017-11-25 36 | ### Added 37 | - Added Flysystem support to StorageManager 38 | - Added pre- and post processing callbacks for image processing 39 | 40 | ### Changed 41 | - Updated CI 42 | - Updated dependencies 43 | - Improved documentation 44 | - Removed `UploadValidationBehavior` 45 | - Removed the `ImageStorageTable` class 46 | - Removed the `ImageStorage.beforeSave` event 47 | - Removed the `ImageStorage.afterSave` event 48 | - Removed the `ImageStorage.beforeDelete` event 49 | - Removed the `ImageStorage.afterDelete` event 50 | - Removed deprecated method calls 51 | - `FileStorage::deleteOldFileOnSave()` is no longer called automatically in the `FileStorage::afterSave()` callback 52 | - Renamed the DB field `file_storage.model` to `file_storage.identifier` 53 | - Renamed The DB field `file_storage.adapter` to `file_storage.adapter_config` 54 | - Refactored image processing 55 | 56 | ### Fixed 57 | - Fixing a bug in StorageManager 58 | - Fixed tests 59 | 60 | ## [1.2.1](https://github.com/burzum/cakephp-file-storage/releases/tag/1.2.1) - 2017-02-28 61 | ### Changed 62 | - Refactored the shells 63 | 64 | ### Fixed 65 | - Fixed issue with image version shell 66 | - Fixed auto-rotating photos based on exif data #142 67 | 68 | ## [1.2.0](https://github.com/burzum/cakephp-file-storage/releases/tag/1.2.0) - 2017-02-16 69 | ### Changed 70 | - Removed passed image parameter for auto rotate 71 | - Updated documentation 72 | - Updated access to properties to getters and setters 73 | - Updated dependencies 74 | - Refactored the storage manager 75 | 76 | ### Fixed 77 | - Fixed passing the subject to the event object in `ImageVersionsTrait` 78 | - Fixed issue with CakePHP 3.4 79 | - Fixed tests 80 | 81 | ## [1.1.6](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.6) - 2016-06-14 82 | ### Added 83 | - Added `LegacyPathBuilder` to rebuild the cakePHP 2.x version 84 | 85 | ### Changed 86 | - Removed PHP 5.5 from Travis 87 | - Updated documentation 88 | - Made it possible to use a callable as hash for `randomPath()` 89 | 90 | ## [1.1.5](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.5) - 2016-06-13 91 | ### Changed 92 | - Updated documentation 93 | - Updated dependencies 94 | - Set the primary key for the `FileStorageTable` instead of auto-detecting it 95 | 96 | ### Fixed 97 | - Fixed S3 Path Builder 98 | - Fixed "First arg must be a non empty string!" exception #112 99 | - Changed the length of the mime type field #126 100 | 101 | ## [1.1.4](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.4) - 2016-01-21 102 | ### Added 103 | - Added `BaseListener` 104 | - Introduced a new shell command to store file via command line 105 | 106 | ### Changed 107 | - Improved the Quick Start Tutorial 108 | - Throw exception if listener can't work with a specific adapter 109 | 110 | ## [1.1.3](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.3) - 2016-01-14 111 | ### Added 112 | - Added `fileToUploadArray` for uploadArray, kept uploadArray as alias 113 | - Added new events to prepare for future changes 114 | 115 | ### Changed 116 | - Improved the `BasePathBuilderTest` 117 | - Improved the StorageException throwing 118 | 119 | ## [1.1.2](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.2) - 2016-01-11 120 | ### Fixed 121 | - Fixed the broken `StorageUtils::normalizeGlobalFilesArray()` 122 | 123 | ## [1.1.1](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.1) - 2016-01-06 124 | ### Changed 125 | - Updated Documentation 126 | - Improved argument handling of the `StorageTrait` 127 | - Refined the path building 128 | - Improved tests 129 | 130 | ### Fixed 131 | - Fixed an issue when saving again without adapter and model 132 | 133 | ## [1.1.0](https://github.com/burzum/cakephp-file-storage/releases/tag/1.1.0) - 2015-12-18 134 | ### Added 135 | - PathBuilders have been introduced 136 | 137 | ### Changed 138 | - The whole image processing system has been refactored 139 | - Everything in `Burzum\FileStorage\Event` has been deprecated 140 | - Everything in `Burzum\FileStorage\Lib` has been deprecated 141 | 142 | ## [1.0.0](https://github.com/burzum/cakephp-file-storage/releases/tag/1.0.0) - 2015-08-25 143 | ### Added 144 | - Initial stable release 145 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 - 2014 by Florian Krämer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FileStorage Plugin for CakePHP 2 | ============================== 3 | 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) 5 | [![Build Status](https://img.shields.io/travis/burzum/cakephp-file-storage/3.0.svg?style=flat-square)](https://travis-ci.org/burzum/cakephp-file-storage) 6 | [![Coverage Status](https://img.shields.io/coveralls/burzum/cakephp-file-storage.svg?branch=3.0.svg?style=flat-square)](https://coveralls.io/r/burzum/cakephp-file-storage) 7 | [![Code Quality](https://img.shields.io/scrutinizer/g/burzum/cakephp-file-storage.svg?branch=3.0?style=flat-square)](https://coveralls.io/r/burzum/cakephp-file-storage) 8 | 9 | **If you're upgrading from CakePHP 2.x please read [the migration guide](docs/Documentation/Migrating-from-CakePHP-2.md).** 10 | 11 | The **File Storage** plugin is giving you the possibility to upload and store files in virtually any kind of storage backend. The plugin features the [Gaufrette](https://github.com/KnpLabs/Gaufrette) **and** [FlySystem](https://github.com/thephpleague/flysystem) library in a CakePHP fashion and provides a simple way to use the storage adapters through the [StorageManager](src/Storage/StorageManager.php) class. 12 | 13 | Storage adapters are an unified interface that allow you to store file data to your local file system, in memory, in a database or into a zip file and remote systems. There is a database table keeping track of what you stored where. You can always write your own adapter or extend and overload existing ones. 14 | 15 | How it works 16 | ------------ 17 | 18 | The whole plugin is build with clear [Separation of Concerns (SoC)](https://en.wikipedia.org/wiki/Separation_of_concerns) in mind: A file is *always* an entry in the `file_storage` table from the app perspective. The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. Storing the path to a file inside an arbitrary table along other data is considered as *bad practice* because it doesn't respect SoC from an architecture perspective but many people do it this way for some reason. 19 | 20 | You associate the `file_storage` table with your model using the FileStorage or ImageStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events, the listeners listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a path builder class. 21 | 22 | List of supported Adapters 23 | -------------------------- 24 | 25 | * Apc 26 | * Amazon S3 27 | * ACL Aware Amazon S3 28 | * Azure 29 | * Doctrine DBAL 30 | * Dropbox 31 | * Ftp 32 | * Grid FS 33 | * In Memory 34 | * Local File System 35 | * MogileFS 36 | * Open Cloud 37 | * Rackspace Cloudfiles 38 | * Sftp 39 | * Zip File 40 | 41 | Supported CakePHP Versions 42 | -------------------------- 43 | 44 | * CakePHP 4.x -> 3.0 Branch 45 | * CakePHP 3.x -> 2.0 Branch 46 | * CakePHP 2.x -> 1.0 Branch 47 | 48 | Requirements 49 | ------------ 50 | 51 | * PHP 7.2+ 52 | * CakePHP 4.x 53 | * Gaufrette Storage Library 0.7.x 54 | 55 | Optional but required if you want image processing out of the box: 56 | 57 | * The [Imagine Image processing plugin](https://github.com/burzum/cakephp-imagine-plugin) if you want to process and store images. 58 | * [FlySystem](https://github.com/thephpleague/flysystem) as alternative library over Gaufrette 59 | 60 | You can still implement whatever file processing you want very easy. It's not tied to Imagine. 61 | 62 | Documentation 63 | ------------- 64 | 65 | For documentation, as well as tutorials, see the [docs](docs/README.md) directory of this repository. 66 | 67 | Support 68 | ------- 69 | 70 | For bugs and feature requests, please use the [issues](https://github.com/burzum/cakephp-file-storage/issues) section of this repository. 71 | 72 | Contributing 73 | ------------ 74 | 75 | To contribute to this plugin please follow a few basic rules. 76 | 77 | * Pull requests must be send to the branch that reflects the version you want to contribute to. 78 | * [Unit tests](http://book.cakephp.org/4.0/en/development/testing.html) are required. 79 | 80 | License 81 | ------- 82 | 83 | Copyright Florian Krämer 84 | 85 | Licensed under The MIT License 86 | Redistributions of files must retain the above copyright notice. 87 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "burzum/cakephp-file-storage", 3 | "type": "cakephp-plugin", 4 | "description": "This plugin is giving you the possibility to store files in virtually any kind of storage backend. This plugin is wrapping the Gaufrette library (https://github.com/KnpLabs/Gaufrette) library in a CakePHP fashion and provides a simple way to use the storage adapters through the StorageManager class.", 5 | "keywords": ["file", "filesystem", "media", "abstraction", "upload", "cakephp", "storage"], 6 | "homepage": "https://github.com/burzum/cakephp-file-storage-plugin", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Florian Krämer", 11 | "homepage": "https://florian-kraemer.net" 12 | }, 13 | { 14 | "name": "Other contributors", 15 | "homepage": "https://github.com/burzum/cakephp-file-storage/graphs/contributors", 16 | "role": "Contributors" 17 | } 18 | ], 19 | "prefer-stable": true, 20 | "minimum-stability": "dev", 21 | "require": { 22 | "php": ">=7.4", 23 | "cakephp/cakephp": "^4.0", 24 | "phauthentic/file-storage": "dev-develop" 25 | }, 26 | "require-dev": { 27 | "cakephp/plugin-installer": "^1.3.0", 28 | "phauthentic/file-storage-image-processor": "dev-develop", 29 | "phpunit/phpunit": "^8.0", 30 | "spryker/code-sniffer": "^0.15.6", 31 | "vlucas/phpdotenv": "^3.3" 32 | }, 33 | "repositories": [ 34 | { 35 | "type": "git", 36 | "url": "https://github.com/skie/cakephp-imagine-plugin" 37 | } 38 | ], 39 | "autoload": { 40 | "psr-4": { 41 | "Burzum\\FileStorage\\": "src", 42 | "Burzum\\FileStorage\\Test\\Fixture\\": "tests\\Fixture" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Cake\\Test\\": "/vendor/cakephp/cakephp/tests", 48 | "Burzum\\FileStorage\\Test\\": "tests" 49 | } 50 | }, 51 | "suggest": { 52 | "phauthentic/file-storage-image-processor": "Required if you want to use the image processing feature of FileStorage" 53 | }, 54 | "scripts": { 55 | "check": [ 56 | "@cs-check", 57 | "@test", 58 | "@stan" 59 | ], 60 | "cs-check": "phpcs -p", 61 | "cs-fix": "phpcbf -p", 62 | "stan": "phpstan analyse src/ && psalm --show-info=false", 63 | "stan-test": "phpstan analyse tests/", 64 | "psalm": "psalm --show-info=false", 65 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^0.12 vimeo/psalm:^3.0 && mv composer.backup composer.json", 66 | "rector": "rector process src/", 67 | "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:^0.4.11 && mv composer.backup composer.json", 68 | "test": "phpunit", 69 | "coverage-test": "phpunit --stderr --coverage-clover=clover.xml" 70 | }, 71 | "config": { 72 | "sort-packages": true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /config/Migrations/20141213004653_initial_migration.php: -------------------------------------------------------------------------------- 1 | table('file_storage', ['id' => false, 'primary_key' => 'id']) 11 | ->addColumn('id', 'char', ['limit' => 36]) 12 | ->addColumn('user_id', 'char', ['limit' => 36, 'null' => true, 'default' => null]) 13 | ->addColumn('foreign_key', 'char', ['limit' => 36, 'null' => true, 'default' => null]) 14 | ->addColumn('model', 'string', ['limit' => 128, 'null' => true, 'default' => null]) 15 | ->addColumn('filename', 'string', ['limit' => 255, 'null' => true, 'default' => null]) 16 | ->addColumn('filesize', 'integer', ['limit' => 16, 'null' => true, 'default' => null]) 17 | ->addColumn('mime_type', 'string', ['limit' => 32, 'null' => true, 'default' => null]) 18 | ->addColumn('extension', 'string', ['limit' => 5, 'null' => true, 'default' => null]) 19 | ->addColumn('hash', 'string', ['limit' => 64, 'null' => true, 'default' => null]) 20 | ->addColumn('path', 'string', ['null' => true, 'default' => null]) 21 | ->addColumn('adapter', 'string', ['limit' => 32, 'null' => true, 'default' => null]) 22 | ->addColumn('created', 'datetime', ['null' => true, 'default' => null]) 23 | ->addColumn('modified', 'datetime', ['null' => true, 'default' => null]) 24 | ->create(); 25 | } 26 | 27 | /** 28 | * Migrate Down. 29 | */ 30 | public function down() { 31 | $this->dropTable('file_storage'); 32 | } 33 | } -------------------------------------------------------------------------------- /config/Migrations/20160302083933_fixing_mime_type_field.php: -------------------------------------------------------------------------------- 1 | and SHOULD be limited to 64 characters. 18 | */ 19 | class FixingMimeTypeField extends AbstractMigration { 20 | 21 | /** 22 | * Change Method. 23 | * Write your reversible migrations using this method. 24 | * More information on writing migrations is available here: 25 | * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class 26 | * The following commands can be used in this method and Phinx will 27 | * automatically reverse them when rolling back: 28 | * createTable 29 | * renameTable 30 | * addColumn 31 | * renameColumn 32 | * addIndex 33 | * addForeignKey 34 | * Remember to call "create()" or "update()" and NOT "save()" when working 35 | * with the Table class. 36 | */ 37 | public function change() { 38 | $this->table('file_storage', ['id' => false, 'primary_key' => 'id']) 39 | ->changeColumn('mime_type', 'string', ['limit' => 128, 'null' => true, 'default' => null]) 40 | ->update(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/Migrations/20170222133412_updating_length_of_file_extension_field.php: -------------------------------------------------------------------------------- 1 | and SHOULD be limited to 64 characters. 18 | * 19 | * @link https://github.com/burzum/cakephp-file-storage/issues/157 20 | */ 21 | class UpdatingLengthOfFileExtensionField extends AbstractMigration { 22 | 23 | /** 24 | * Change Method. 25 | * Write your reversible migrations using this method. 26 | * More information on writing migrations is available here: 27 | * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class 28 | * The following commands can be used in this method and Phinx will 29 | * automatically reverse them when rolling back: 30 | * createTable 31 | * renameTable 32 | * addColumn 33 | * renameColumn 34 | * addIndex 35 | * addForeignKey 36 | * Remember to call "create()" or "update()" and NOT "save()" when working 37 | * with the Table class. 38 | */ 39 | public function change() { 40 | $this->table('file_storage', ['id' => false, 'primary_key' => 'id']) 41 | ->changeColumn('extension', 'string', ['limit' => 32, 'null' => true, 'default' => null]) 42 | ->update(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/Migrations/20200714095531_AddingVariantsAndMetadataFields.php: -------------------------------------------------------------------------------- 1 | table('file_storage') 21 | ->addColumn('variants', 'json', ['null' => true]) 22 | ->addColumn('metadata', 'json', ['null' => true]) 23 | ->update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | fallbacks(); 6 | }); -------------------------------------------------------------------------------- /docs/Documentation/Getting-a-File-Path-and-URL.md: -------------------------------------------------------------------------------- 1 | # Getting a file path and URL 2 | 3 | The path and filename of a stored file in the storage backend that was used to store the file is generated by a [path builder](Path-Builders.md). The event listener that stored your file has used a path builder to generate the path based on the entity data. This means that if you have the entity and instantiate a path builder you can build the path to it in any place. 4 | 5 | The plugin already provides you with several convenience short cuts to do that. 6 | 7 | Be aware that whenever you use a path builder somewhere, you **must** use the same path builder and options as when the entity was created. They're usually the same as configured in your event listener. 8 | 9 | ## Getting it from an entity 10 | 11 | If you're using an entity from this plugin, or extending it they'll implement the PathBuilderTrait. This enables you to set and get the path builder on the entities. 12 | 13 | Due to some [limitations of the CakePHP core](http://api.cakephp.org/3.1/source-class-Cake.ORM.Table.html#1965) you can't pass options to the entity when calling `Table::newEntity()`. 14 | 15 | There are two workarounds for that issue. Either you'll have to set it manually on the entity instance: 16 | 17 | ```php 18 | $entity->pathBuilder('PathBuilderName', ['options-array' => 'goes-here']); 19 | $entity->path(); // Gets you the path in the used storage backend to the file 20 | $entity->url(); // Gets you the URL to the file if possible 21 | ``` 22 | 23 | Or do it in the constructor of the entity. Pay attention to the two properties `_pathBuilderClass` and `_pathBuilderOptions`. Set whatever you need here. If you're inheriting `Burzum\FileStorage\Model\Entity\FileStorage` these options and the code below will be already present. 24 | 25 | ```php 26 | namespace App\Model\Entity; 27 | 28 | use Burzum\FileStorage\Storage\PathBuilder\PathBuilderTrait; 29 | 30 | class SomeEntityInYourApp extends Entity { 31 | 32 | use PathBuilderTrait; 33 | 34 | /** 35 | * Path Builder Class. 36 | * 37 | * This is named $_pathBuilderClass because $_pathBuilder is already used by 38 | * the trait to store the path builder instance. 39 | * 40 | * @param array 41 | */ 42 | protected $_pathBuilderClass = null; 43 | 44 | /** 45 | * Path Builder options 46 | * 47 | * @param array 48 | */ 49 | protected $_pathBuilderOptions = []; 50 | 51 | /** 52 | * Constructor 53 | * 54 | * @param array $properties hash of properties to set in this entity 55 | * @param array $options list of options to use when creating this entity 56 | */ 57 | public function __construct(array $properties = [], array $options = []) { 58 | $options += [ 59 | 'pathBuilder' => $this->_pathBuilderClass, 60 | 'pathBuilderOptions' => $this->_pathBuilderOptions 61 | ]; 62 | 63 | parent::__construct($properties, $options); 64 | 65 | if (!empty($options['pathBuilder'])) { 66 | $this->pathBuilder( 67 | $options['pathBuilder'], 68 | $options['pathBuilderOptions'] 69 | ); 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | If you want to use path builders depending on the kind of file or the identifier which is stored in the `model` field of the `file_storage` table, you can implement that logic as well there and use the entities data to determine the path builder class or options. 76 | 77 | ## Getting it using the storage helper 78 | 79 | The storage helper is basically just a proxy to a path builder. The helper takes two configuration options: 80 | 81 | * **pathBuilder**: Name of the path builder to use. 82 | * **pathBuilderOptions**: The options passed to the path builders constructor. 83 | 84 | Make sure that the options you pass and the path builder are the same you've used when you uploaded the file! Otherwise you end up with a different path! 85 | 86 | ```php 87 | // Load the helper 88 | $this->loadHelper('Burzum/FileStorage.Storage', [ 89 | 'pathBuilder' => 'Base', 90 | // The builder options must match the options and builder class that were used to store the file! 91 | 'pathBuilderOptions' => [ 92 | 'modelFolder' => true, 93 | ] 94 | ]); 95 | 96 | // Use it in your views 97 | $url = $this->Storage->url($yourEntity); 98 | 99 | // Change the path builder at run time 100 | // Be careful, this will change the path builder instance in the helper! 101 | $this->Storage->pathBuilder('SomePathBuilder', ['options' => 'here']); 102 | ``` 103 | -------------------------------------------------------------------------------- /docs/Documentation/How-To-Use.md: -------------------------------------------------------------------------------- 1 | How to Use It 2 | ============= 3 | 4 | Before you continue to read this page it is recommended that you have read about [the Storage Manager](The-Storage-Manager.md) before. 5 | 6 | The following text is going to describe two ways to store a file. Which of both you choose depends at the end on your use case but it is recommended to use the events because they automate the whole process much more. 7 | 8 | The basic idea of this plugin is that files are always handled as separate entities and are associated to other models. The reason for that is simple. A file has multiple properties like size, mime type and other entities in the system can have more than one file for example. It is considered as *bad* practice to store lots of file paths as reference in a table together with other data. 9 | 10 | This plugin resolves that issue by handling each file as a completely separate entity in the application. There is just one table `file_storage` that will keep the reference to all your files, no matter where they're stored. 11 | 12 | Preparing the File Upload 13 | ------------------------- 14 | 15 | This section is going to show how to store a file using the Storage Manager directly. 16 | 17 | For example you have a `reports` table and want to save a PDF to it, you would then create an association like: 18 | 19 | ```php 20 | public function initialize(array $config) 21 | { 22 | parent::initialize($config); 23 | $this->table('reports'); 24 | 25 | $this->hasOne('PdfFiles', [ 26 | 'className' => 'Burzum/FileStorage.PdfFiles', 27 | 'foreignKey' => 'foreign_key', 28 | 'conditions' => [ 29 | 'PdfFiles.model' => 'Reports' 30 | ] 31 | ]); 32 | } 33 | ``` 34 | 35 | In your `add.ctp` or `edit.ctp` views you would add something like this. 36 | 37 | ```php 38 | echo $this->Form->create($report, ['type' => 'file']); 39 | echo $this->Form->input('title'); 40 | echo $this->Form->file('pdf_files.file'); // Pay attention here! 41 | echo $this->Form->input('description'); 42 | echo $this->Form->submit(__('Submit')); 43 | echo $this->Form->end(); 44 | ``` 45 | 46 | [Make sure your form is using the right HTTP method](http://book.cakephp.org/3.0/en/views/helpers/form.html#changing-the-http-method-for-a-form)! 47 | 48 | Store an uploaded file using Events 49 | ----------------------------------- 50 | 51 | The **FileStorage** plugin comes with a class that acts just as a listener to some of the events in this plugin. Take a look at [ImageProcessingListener.php](../../src/Event/ImageProcessingListener.php). 52 | 53 | This class will listen to all the ImageStorage model events and save the uploaded image and then create the versions for that image and storage adapter. 54 | 55 | It is important to understand that nearly each storage adapter requires a little different handling: Most of the time you can't treat a local file the same as a file you store in a cloud service. 56 | The interface that this plugin and Gaufrette provide is the same but not the internals. So a path that works for your local file system might not work for your remote storage system because it has other requirements or limitations. 57 | 58 | So if you want to store a file using Amazon S3 you would have to store it, 59 | create all the versions of that image locally and then upload each of them 60 | and then delete the local temp files. The good news is the plugin can already take care of that. 61 | 62 | When you create a new listener it is important that you check the `model` field and 63 | the event subject object (usually a table object inheriting `\Cake\ORM\Table`) if it 64 | matches what you expect. 65 | Using the event system you could create any kind of storage and upload behavior without 66 | inheriting or touching the model code. Just write a listener class and attach it to the global EventManager. 67 | 68 | List of events 69 | -------------- 70 | 71 | Events triggered in the `ImageStorage` model: 72 | 73 | * ImageVersion.createVersion 74 | * ImageVersion.removeVersion 75 | * ImageStorage.beforeSave 76 | * ImageStorage.afterSave 77 | * ImageStorage.beforeDelete 78 | * ImageStorage.afterDelete 79 | 80 | Events triggered in the `FileStorage` model: 81 | 82 | * FileStorage.beforeSave 83 | * FileStorage.afterSave 84 | * FileStorage.afterDelete 85 | 86 | Event Listeners 87 | --------------- 88 | 89 | See [this page](Included-Event-Listeners.md) for the event listeners that are included in the plugin. 90 | 91 | 92 | Handling the File Upload Manually 93 | --------------------------------- 94 | 95 | You'll have to customize it a little but its just a matter for a few lines. 96 | 97 | Note the Listener expects a request data key `file` present in the form, so use `echo $this->Form->input('file');` to allow the Marshaller pick the right data from the uploaded file. 98 | 99 | Lets go by this scenario inside the report table, assuming there is an add() method: 100 | 101 | ```php 102 | public function add() { 103 | $entity = $this->newEntity($postData); 104 | $saved = $this->save($entity); 105 | if ($saved) { 106 | $key = 'your-file-name'; 107 | if (StorageManager::get('Local')->write($key, file_get_contents($this->data['pdf_files']['file']['tmp_name']))) { 108 | $postData['pdf_files']['foreign_key'] = $saved->id; 109 | $postData['pdf_files']['model'] = 'Reports'; 110 | $postData['pdf_files']['path'] = $key; 111 | $postData['pdf_files']['adapter'] = 'Local'; 112 | $this->PdfDocuments->save($this->PdfDocuments->newEntity($postData)); 113 | } 114 | } 115 | return $entity; 116 | } 117 | ``` 118 | 119 | Later, when you want to delete the file, for example in the beforeDelete() or afterDelete() callback of your Report model, you'll know the adapter you have used to store the attached PdfFile and can get an instance of this adapter configuration using the StorageManager. By having the path or key available you can then simply call: 120 | 121 | ```php 122 | StorageManager::get($data['PdfFile']['adapter'])->delete($data['PdfFile']['path']); 123 | ``` 124 | 125 | Insted of doing all of this in the table object that has the files associated to it you can also simply extend the FileStorage table from the plugin and add your storage logic there and use that table for your association. 126 | 127 | Why is it done like this? 128 | ------------------------- 129 | 130 | Every developer might want to store the file at a different point or apply other operations on the file before or after it is stored. Based on different circumstances you might want to save an associated file even before you created the record its going to get attached to, in other scenarios like in this documentation you might want to do it after. 131 | 132 | The ``$key`` is also a key aspect of it: Different adapters might expect a different key. A key for the Local adapter of Gaufrette is usually a path and a file name under which the data gets stored. That's also the reason why you use `file_get_contents()` instead of simply passing the tmp path as it is. 133 | 134 | It is up to you how you want to generate the key and build your path. You can customize the way paths and file names are build by writing a custom event listener for that. 135 | 136 | It is highly recommended to read the Gaufrette documentation for the read() and write() methods of the adapters. 137 | -------------------------------------------------------------------------------- /docs/Documentation/How-it-works.md: -------------------------------------------------------------------------------- 1 | How it works 2 | ------------ 3 | 4 | The whole plugin is build with clear [Separation of Concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) in mind: A file is *always* an entry in the `file_storage` table from the app perspective. 5 | 6 | The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. You associate the `file_storage` table with your model using the FileStorage or ImageStorage model from the plugin via hasOne, hasMany or HABTM. 7 | 8 | When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events. [The listeners](../../src/Storage/Listener) listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a [path builder](Path-Builders.md) class. 9 | 10 | ![File Storage abstract flowchart](./images/file-storage-flowchart.jpg) 11 | -------------------------------------------------------------------------------- /docs/Documentation/Image-Storage-And-Versioning.md: -------------------------------------------------------------------------------- 1 | Image Versioning 2 | ================ 3 | 4 | You can set up automatic image processing for the ImageStorage table. To make the magic happen you have to use the ImageStorage table (it extends the FileStorage table) for image file saving. 5 | 6 | All you need to do is basically use the image model and configure versions on a per model basis. When you save an ImageStorage table entity it is important to have the 'model' field filled so that the script can find the correct versions for that model. 7 | 8 | ```php 9 | Configure::write('FileStorage', array( 10 | 'imageSizes' => array( 11 | 'GalleryImage' => array( 12 | 'c50' => array( 13 | 'crop' => array( 14 | 'width' => 50, 'height' => 50 15 | ) 16 | ), 17 | 't120' => array( 18 | 'thumbnail' => array( 19 | 'width' => 120, 'height' => 120 20 | ) 21 | ), 22 | 't800' => array( 23 | 'thumbnail' => array( 24 | 'width' => 800, 'height' => 600 25 | ) 26 | ) 27 | ), 28 | 'User' => array( 29 | 'c50' => array( 30 | 'crop' => array( 31 | 'width' => 50, 'height' => 50 32 | ) 33 | ), 34 | 't150' => array( 35 | 'crop' => array( 36 | 'width' => 150, 'height' => 150) 37 | ) 38 | ), 39 | ) 40 | ) 41 | ); 42 | 43 | \Burzum\FileStorage\Lib\FileStorageUtils::generateHashes(); 44 | ``` 45 | 46 | Calling ```generateHashes()``` is important, it will create the hash values for each versioned image and store them in Media.imageHashes in the configuration. 47 | 48 | If you don't want to have the script to generate the hashes each time it's executed, it is up to you to store it persistent. This plugin just provides you the tools. 49 | 50 | Image files will end up wherever you have configured your base path. 51 | 52 | ``` 53 | /ModelName/51/21/63/4c0f128f91fc48749662761d407888cc/4c0f128f91fc48749662761d407888cc.jpg 54 | ``` 55 | 56 | The versioned image files will be in the same folder, which is the id of the record, as the original image and have the truncated hash of the version attached but before the extension. 57 | 58 | ``` 59 | /ModelName/51/21/63/4c0f128f91fc48749662761d407888cc/4c0f128f91fc48749662761d407888cc.f91fsc.jpg 60 | ``` 61 | 62 | You should symlink your image root folder to APP/webroot/images for example to avoid that images go through php and are send directly instead. 63 | 64 | Extending and Changing Image Versioning 65 | --------------------------------------- 66 | 67 | It is possible to totally change the way image versions are created. You'll just have to extend or create new listeners and attach them to the global EventManager. 68 | -------------------------------------------------------------------------------- /docs/Documentation/Installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Make sure you've checked the [requirements](Requirements.md) first! 5 | 6 | Using Composer 7 | -------------- 8 | 9 | Installing the plugin via [Composer](https://getcomposer.org/) is very simple, just run in your project folder: 10 | 11 | ``` 12 | composer require burzum/file-storage:^4.0 13 | ``` 14 | 15 | Database Setup 16 | -------------- 17 | 18 | You need to setup the plugin database using [the official migrations plugin for CakePHP](https://github.com/cakephp/migrations). 19 | 20 | ``` 21 | cake migrations migrate -p Burzum/FileStorage 22 | ``` 23 | 24 | If you're coming from the CakePHP 2.0 version of the plugin, the support for the CakeDC Migrations plugin has been dropped in favor of [the official migrations plugin](https://github.com/cakephp/migrations). 25 | 26 | CakePHP Bootstrap 27 | ----------------- 28 | 29 | Add the following part to your applications ```config/bootstrap.php```. 30 | 31 | ```php 32 | use Cake\Event\EventManager; 33 | use Burzum\FileStorage\Lib\FileStorageUtils; 34 | use Burzum\FileStorage\Lib\StorageManager; 35 | use Burzum\FileStorage\Event\ImageProcessingListener; 36 | use Burzum\FileStorage\Event\LocalFileStorageListener; 37 | 38 | // Only required if you're *NOT* using composer or another autoloader! 39 | spl_autoload_register(__NAMESPACE__ .'\FileStorageUtils::gaufretteLoader'); 40 | 41 | $listener = new LocalFileStorageListener(); 42 | EventManager::instance()->on($listener); 43 | 44 | // For automated image processing you'll have to attach this listener as well 45 | $listener = new ImageProcessingListener(); 46 | EventManager::instance()->on($listener); 47 | ``` 48 | 49 | Adapter Specific Configuration 50 | ------------------------------ 51 | 52 | Depending on the storage backend of your choice, for example Amazon S3 or Dropbox, you'll very likely need additional vendor libs and extended adapter configuration. 53 | 54 | Please see the [Specific Adapter Configuration](Specific-Adapter-Configurations.md) page of the documentation for more information about then. It is also worth checking the Gaufrette documentation for additonal adapters. 55 | 56 | Running Tests 57 | ------------- 58 | 59 | The plugin tests are set up in a way that you can run them without putting the plugin into a CakePHP3 application. All you need to do is to go into the FileStorage folder and run these commands: 60 | 61 | ``` 62 | cd 63 | composer update 64 | phpunit 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/Documentation/List-of-included-Adapters.md: -------------------------------------------------------------------------------- 1 | Included storage adapters 2 | ------------------------- 3 | 4 | The following adapters are coming along with the Gaufrette library that is used by this plugin as a base. 5 | 6 | * Apc 7 | * Amazon S3 8 | * ACL Aware Amazon S3 9 | * Azure 10 | * Doctrine DBAL 11 | * Dropbox 12 | * Ftp 13 | * Grid FS 14 | * In Memory 15 | * Local File System 16 | * MogileFS 17 | * Open Cloud 18 | * Rackspace Cloudfiles 19 | * Sftp 20 | * Zip File 21 | 22 | Check [the adapter folder](https://github.com/KnpLabs/Gaufrette/tree/master/src/Gaufrette/Adapter) of the Gaufrette lib for a complete list. 23 | 24 | If you need another adapter that is not included you can implement it yourself or try searching the web first. 25 | -------------------------------------------------------------------------------- /docs/Documentation/The-Image-Helper.md: -------------------------------------------------------------------------------- 1 | The Image Helper 2 | ================ 3 | 4 | The plugin comes with an Image helper that makes it easy to display the images generated by the ImageStorage model and the events that process the images. 5 | 6 | ```php 7 | namespace App\View; 8 | class AppView extends View { 9 | public function initialize() { 10 | parent::initialize(); 11 | $this->loadHelper('Burzum/FileStorage.Image'); 12 | } 13 | } 14 | ``` 15 | 16 | In your views you can now access all your image versions, which you have declared before in your config through the helper. 17 | 18 | ```php 19 | echo $this->Image->display($product['image'], 'small'); 20 | ``` 21 | 22 | If you want the original image just call the display() method without a version. 23 | 24 | ```php 25 | echo $this->Image->display($product['image']); 26 | ``` 27 | 28 | If you want to get only the URL to an image you can call ```imageUrl()```. 29 | 30 | ```php 31 | $imageUrl = $this->Image->imageUrl($product['image'], 'small'); 32 | echo $this->Html->image($imageUrl); 33 | ``` 34 | 35 | Options for display() and imageUrl() 36 | ------------------------------------ 37 | 38 | The third argument of both methods is an option array, right now it has only one option to set. 39 | 40 | * **fallback:** Optional, can be boolean true or string. If boolean true it will use ```placeholder/.jpg``` as place holder. If string it will use that as image. 41 | -------------------------------------------------------------------------------- /docs/Documentation/The-Image-Version-Shell.md: -------------------------------------------------------------------------------- 1 | The Image Version Shell 2 | ======================= 3 | 4 | The shell comes with three pretty much self explaining commands: `generate`, `remove` and `regenerate`. 5 | 6 | Generate 7 | -------- 8 | 9 | **Arguments** 10 | 11 | * **model (required)**: Value of the model property of the images to generate. 12 | * **version (required)**: Image version to generate. 13 | 14 | **Options** 15 | 16 | * **storageTable**: The storage table for image processing you want to use. 17 | * **limit**: Limits the amount of records to be processed in one batch. 18 | * **keep-old-versions**: Use this switch if you do not want to overwrite existing versions. 19 | 20 | Example: 21 | 22 | ```sh 23 | bin\cake imageVersion generate Avatar t150 24 | ``` 25 | 26 | Remove 27 | ------ 28 | 29 | **Arguments** 30 | 31 | * **model (required)**: Value of the model property of the images to generate. 32 | * **version (required)**: Image version to generate. 33 | 34 | **Options** 35 | 36 | * **storageTable**: The storage table for image processing you want to use. 37 | * **limit**: Limits the amount of records to be processed in one batch. 38 | 39 | Example: 40 | 41 | ```sh 42 | bin\cake imageVersion remove Avatar t150 43 | ``` 44 | 45 | Regenerate 46 | ---------- 47 | 48 | **Arguments** 49 | 50 | * **model (required)**: Value of the model property of the images to generate. 51 | 52 | **Options** 53 | 54 | * **storageTable**: The storage table for image processing you want to use. 55 | * **limit**: Limits the amount of records to be processed in one batch. 56 | * **keep-old-versions**: Use this switch if you do not want to overwrite existing versions. 57 | 58 | Example: 59 | 60 | ```sh 61 | bin\cake imageVersion regenerate Avatar 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/Documentation/images/file-storage-flowchart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burzum/cakephp-file-storage/dec9ab1241e3ab36ec55c834ba6cfe9361b3b88f/docs/Documentation/images/file-storage-flowchart.jpg -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | The **File Storage** plugin is giving you the possibility to store files in virtually any kind of storage backend. 4 | This plugin is wrapping the [Gaufrette](https://github.com/KnpLabs/Gaufrette) library in a CakePHP fashion 5 | and provides a simple way to use the storage adapters through the [StorageManager](../Lib/StorageManager.php) class. 6 | 7 | [See this list of included storage adapters.](Docs/Documentation/List-of-included-Adapters.md) 8 | 9 | Storage adapters are an unified interface that allow you to store file data to your local file system, in memory, in a database or into a zip file and remote systems. There is a database table keeping track of what you stored were. You can always write your own adapter or extend and overload existing ones. 10 | 11 | ## Documentation 12 | 13 | * [Installation](Documentation/Installation.md) 14 | * [How it works](Documentation/How-it-works.md) 15 | * [How to Use it](Documentation/How-To-Use.md) 16 | * [Migrating from File Storage v1 to v2](Migrating-from-File-Storage-v1-to-v2.md) 17 | * [The Storage Manager](Documentation/The-Storage-Manager.md) 18 | * [Included Event Listeners](Documentation/Included-Event-Listeners.md) 19 | * [Path Builders](Documentation/Path-Builders.md) 20 | * [Getting a file path and URL](Documentation/Getting-a-File-Path-and-URL.md) 21 | * Image processing 22 | * [Image Storage and Versioning](Documentation/Image-Storage-And-Versioning.md) 23 | * [The Image Version Shell](Documentation/The-Image-Version-Shell.md) 24 | * [The Image Helper](Documentation/The-Image-Helper.md) 25 | 26 | ## Tutorials 27 | 28 | * [Quick Start](Tutorials/Quick-Start.md) 29 | -------------------------------------------------------------------------------- /docs/Tutorials/Quick-Start.md: -------------------------------------------------------------------------------- 1 | Quick-Start Tutorial 2 | ==================== 3 | 4 | It is required that you have at least a basic understanding of how the event system of CakePHP work works. If you're unsure it is recommended to read about it first. It is expected that you take the time to try to actually *understand* what you're doing instead of just copy and pasting the code. Understanding OOP and namespaces in php is required for this tutorial. 5 | 6 | This tutorial will assume that we're going to add an avatar image upload for our users. 7 | 8 | For image processing you'll need the Imagine plugin. If you don't have it already added, add it now: 9 | 10 | ```sh 11 | composer require burzum/cakephp-imagine-plugin 12 | ``` 13 | 14 | In your applications `config/bootstrap.php` load the plugins: 15 | 16 | ```php 17 | Plugin::load('Burzum/FileStorage', [ 18 | 'bootstrap' => true 19 | ]); 20 | Plugin::load('Burzum/Imagine'); 21 | ``` 22 | 23 | This will load the `bootstrap.php` of the File Storage plugin. The default configuration in there will load the LocalStorage listener and the ImageProcessing listener. You can also skip that bootstrap part and configure your own listeners in your apps bootstrap.php or a new file. 24 | 25 | To make image processing work you'll have to add this to your application's bootstrap.php as well: 26 | 27 | ```php 28 | /** 29 | * Image resizing configuration 30 | */ 31 | Configure::write('FileStorage', array( 32 | 'imageSizes' => [ 33 | 'Avatar' => [ 34 | 'crop180' => [ 35 | 'squareCenterCrop' => [ 36 | 'size' => 180 37 | ] 38 | ], 39 | 'crop100' => [ 40 | 'squareCenterCrop' => [ 41 | 'size' => 100 42 | ] 43 | ], 44 | 'crop40' => [ 45 | 'squareCenterCrop' => [ 46 | 'size' => 40 47 | ] 48 | ] 49 | ] 50 | ] 51 | )); 52 | ``` 53 | 54 | We now assume that you have a table called `Users` and that you want to attach an avatar image to your users. 55 | 56 | In your `App\Model\Table\UsersTable.php` file is a method called `inititalize()`. Add the avatar file assocation there: 57 | 58 | ```php 59 | $this->hasOne('Avatars', [ 60 | 'className' => 'Burzum/FileStorage.FileStorage', 61 | 'foreignKey' => 'foreign_key', 62 | 'conditions' => [ 63 | 'Avatars.model' => 'Avatar' 64 | ] 65 | ]); 66 | ``` 67 | 68 | Especially pay attention to the `conditions` key in the config array of the association. You must specify this here or File Storage won't be able to identify that kind of file properly. 69 | 70 | Either save it through the association along with your users save call or save it separate. However, whatever you do, it is important that you set the `foreign_key` and `model` field for the associated file storage entity. 71 | 72 | If you don't specify the model field it will use the file storage tables table name by default and your has one association won't find it. 73 | 74 | Inside the `edit.ctp` view file of your users edit method: 75 | 76 | ```php 77 | echo $this->Form->create($user); 78 | echo $this->Form->input('username'); 79 | // That's the important line / field 80 | echo $this->Form->file('avatar.file'); 81 | echo $this->Form->submit(__('Submit')); 82 | echo $this->Form->end(); 83 | ``` 84 | 85 | You **must** use the `file` field for the uploaded file. The plugin will check the entity for this field. 86 | 87 | Your users controller `edit()` method: 88 | 89 | ```php 90 | /** 91 | * Assuming you've loaded: 92 | * 93 | * - AuthComponent 94 | * - FlashComponent 95 | */ 96 | class UsersController extends AppController { 97 | 98 | public function edit() { 99 | $userId = $this->Auth->user('id'); 100 | $user = $this->Users->get($userId); 101 | 102 | if ($this->request->is(['post', 'put'])) { 103 | $user = $this->Users->patchEntity($user, $this->request->data()); 104 | if (!empty($users->avatar->file)) { 105 | $users->avatar->set('user_id', $userId); // Optional 106 | $users->avatar->set('model', 'Avatars'); 107 | } 108 | 109 | if ($this->Users->save($user)) { 110 | $this->Flash->success('User saved'); 111 | } else { 112 | $this->Flash->error('There was a problem saving the user.'); 113 | } 114 | } 115 | 116 | $this->set('user', $user); 117 | } 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/Tutorials/Replacing-Files.md: -------------------------------------------------------------------------------- 1 | Replacing Files 2 | =============== 3 | 4 | **Don't** use Table::deleteAll() if you don't want to end up with orphaned files! The reason for that is that deleteAll() doesn't fire the callbacks. So the events that will remove the files won't get fired. 5 | -------------------------------------------------------------------------------- /docs/Tutorials/Validating-File-Uploads.md: -------------------------------------------------------------------------------- 1 | Validating File Uploads 2 | ======================= 3 | 4 | You can validate your uploads by extending `Burzum\FileStorage\Storage\Listener\ValidationListener` and implement your validation methods just like you do in table objects: 5 | 6 | ```php 7 | use Burzum\FileStorage\Storage\Listener\ValidationListener; 8 | use Cake\Validation\Validator; 9 | 10 | class TestValidationListener extends ValidationListener { 11 | 12 | public function validationAvatar(Validator $validator) { 13 | $validator->add('file', 'mimeType', [ 14 | 'rule' => ['mimeType', ['image/jpg', 'image/jpeg', 'image/png']] 15 | ]); 16 | 17 | $validator->add('file', 'imageSize', [ 18 | 'rule' => ['imageSize', [ 19 | 'height' => ['>=', 200], 20 | 'width' => ['>=', 200] 21 | ]] 22 | ]); 23 | 24 | return $validator; 25 | } 26 | } 27 | 28 | EventManager::instance()->on(new FileValidationListener()); 29 | ``` 30 | 31 | This will attach the listener to the `Model.initialize()` event and add your configured validators to the FileStorage table. 32 | 33 | References: 34 | 35 | * [Using A Different Validation Set](http://book.cakephp.org/3.0/en/orm/validation.html#using-a-different-validation-set) 36 | * [Table::validator()](http://api.cakephp.org/3.3/class-Cake.Validation.ValidatorAwareTrait.html#_validator) 37 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | src/ 5 | tests/ 6 | 7 | 8 | 0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 7 3 | checkMissingIterableValueType: false 4 | checkGenericClassInNonGenericObjectType: false 5 | bootstrapFiles: 6 | - tests/bootstrap.php 7 | earlyTerminatingMethodCalls: 8 | Cake\Console\Shell: 9 | - abort 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./tests/TestCase 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ./src 52 | 53 | ./src/Storage/PathBuilder/PathBuilderInterface.php 54 | ./vendor 55 | ./tests 56 | ./src/Model/Entity 57 | ./src/Event 58 | ./src/Lib 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /pmip/pmip.rb: -------------------------------------------------------------------------------- 1 | puts 'Hello PMIP 0.3.2! - Please see http://code.google.com/p/pmip/ for full instructions and plugin helper bundles.' 2 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /rector.yml: -------------------------------------------------------------------------------- 1 | # rector.yaml 2 | services: 3 | Rector\Php\Rector\FunctionLike\ParamTypeDeclarationRector: ~ 4 | Rector\Php\Rector\FunctionLike\ReturnTypeDeclarationRector: ~ 5 | -------------------------------------------------------------------------------- /src/FileStorage/DataTransformer.php: -------------------------------------------------------------------------------- 1 | table = $table; 25 | } 26 | 27 | /** 28 | * @param \Cake\Datasource\EntityInterface $entity 29 | * 30 | * @return \Phauthentic\Infrastructure\Storage\FileInterface 31 | */ 32 | public function entityToFileObject(EntityInterface $entity): FileInterface 33 | { 34 | $file = File::create( 35 | (string)$entity->get('filename'), 36 | (int)$entity->get('filesize'), 37 | (string)$entity->get('mime_type'), 38 | (string)$entity->get('adapter'), 39 | (string)$entity->get('identifier'), 40 | (string)$entity->get('model'), 41 | (string)$entity->get('foreign_key'), 42 | (array)$entity->get('variants'), 43 | (array)$entity->get('metadata') 44 | ); 45 | 46 | $file = $file->withUuid((string)$entity->get('id')); 47 | 48 | if ($entity->has('path')) { 49 | $file = $file->withPath($entity->get('path')); 50 | } 51 | 52 | if ($entity->has('file')) { 53 | /** @var \Psr\Http\Message\UploadedFileInterface|array $uploadedFile */ 54 | $uploadedFile = $entity->get('file'); 55 | if (!is_array($uploadedFile)) { 56 | $filename = $uploadedFile->getStream()->getMetadata('uri'); 57 | } else { 58 | $filename = $uploadedFile['tmp_name']; 59 | } 60 | 61 | $file = $file->withFile($filename); 62 | } 63 | 64 | return $file; 65 | } 66 | 67 | /** 68 | * @param \Phauthentic\Infrastructure\Storage\FileInterface $file 69 | * @param \Cake\Datasource\EntityInterface|null $entity 70 | * 71 | * @return \Cake\Datasource\EntityInterface 72 | */ 73 | public function fileObjectToEntity(FileInterface $file, ?EntityInterface $entity): EntityInterface 74 | { 75 | $data = [ 76 | 'id' => $file->uuid(), 77 | 'model' => $file->model(), 78 | 'foreign_key' => $file->modelId(), 79 | 'filesize' => $file->filesize(), 80 | 'filename' => $file->filename(), 81 | 'mime_type' => $file->mimeType(), 82 | 'variants' => $file->variants(), 83 | 'metadata' => $file->metadata(), 84 | 'adapter' => $file->storage(), 85 | 'path' => $file->path(), 86 | ]; 87 | 88 | return $entity 89 | ? $this->table->patchEntity($entity, $data, ['validate' => false, 'guard' => false]) 90 | : $this->table->newEntity($data, ['validate' => false, 'guard' => false]); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/FileStorage/DataTransformerInterface.php: -------------------------------------------------------------------------------- 1 | 'Local', 55 | 'ignoreEmptyFile' => true, 56 | 'fileField' => 'file', 57 | 'fileStorage' => null, 58 | 'imageProcessor' => null, 59 | ]; 60 | 61 | /** 62 | * @var array 63 | */ 64 | protected array $processors = []; 65 | 66 | /** 67 | * @inheritDoc 68 | * 69 | * @throws \RuntimeException 70 | */ 71 | public function initialize(array $config): void 72 | { 73 | parent::initialize($config); 74 | 75 | if ($this->getConfig('fileStorage') instanceof FileStorage) { 76 | $this->fileStorage = $this->getConfig('fileStorage'); 77 | } else { 78 | throw new RuntimeException( 79 | 'Missing or invalid fileStorage config key' 80 | ); 81 | } 82 | 83 | if (!$this->getConfig('dataTransformer') instanceof DataTransformerInterface) { 84 | $this->transformer = new DataTransformer( 85 | $this->getTable() 86 | ); 87 | } 88 | } 89 | 90 | /** 91 | * @param string $configName 92 | * 93 | * @return \League\Flysystem\AdapterInterface 94 | */ 95 | public function getStorageAdapter(string $configName): AdapterInterface 96 | { 97 | return $this->fileStorage->getStorage($configName); 98 | } 99 | 100 | /** 101 | * Checks if a file upload is present. 102 | * 103 | * @param \Cake\Datasource\EntityInterface|\ArrayObject $entity 104 | * 105 | * @return bool 106 | */ 107 | protected function isFileUploadPresent($entity) 108 | { 109 | $field = $this->getConfig('fileField'); 110 | if ($this->getConfig('ignoreEmptyFile') === true) { 111 | if (!isset($entity[$field])) { 112 | return false; 113 | } 114 | 115 | /** @var \Psr\Http\Message\UploadedFileInterface|array $file */ 116 | $file = $entity[$field]; 117 | if (!is_array($file)) { 118 | return $file->getError() !== UPLOAD_ERR_NO_FILE; 119 | } 120 | 121 | return $file['error'] !== UPLOAD_ERR_NO_FILE; 122 | } 123 | 124 | return true; 125 | } 126 | 127 | /** 128 | * beforeMarshal callback 129 | * 130 | * @param \Cake\Event\EventInterface $event 131 | * @param \ArrayObject $data 132 | * @param \ArrayObject $options 133 | * 134 | * @return void 135 | */ 136 | public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options): void 137 | { 138 | if ($this->isFileUploadPresent($data)) { 139 | $this->getFileInfoFromUpload($data); 140 | } 141 | } 142 | 143 | /** 144 | * beforeSave callback 145 | * 146 | * @param \Cake\Event\EventInterface $event 147 | * @param \Cake\Datasource\EntityInterface $entity 148 | * @param \ArrayObject $options 149 | * 150 | * @return void 151 | */ 152 | public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void 153 | { 154 | if (!$this->isFileUploadPresent($entity)) { 155 | return; 156 | } 157 | 158 | $this->checkEntityBeforeSave($entity); 159 | 160 | $this->dispatchEvent('FileStorage.beforeSave', [ 161 | 'entity' => $entity, 162 | 'storageAdapter' => $this->getStorageAdapter($entity->get('adapter')), 163 | ], $this->getTable()); 164 | } 165 | 166 | /** 167 | * afterSave callback 168 | * 169 | * @param \Cake\Event\EventInterface $event 170 | * @param \Cake\Datasource\EntityInterface $entity 171 | * @param \ArrayObject $options 172 | * 173 | * @throws \Exception 174 | * 175 | * @return void 176 | */ 177 | public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void 178 | { 179 | if (!$this->isFileUploadPresent($entity)) { 180 | return; 181 | } 182 | 183 | if ($entity->isNew()) { 184 | try { 185 | $file = $this->entityToFileObject($entity); 186 | $file = $this->fileStorage->store($file); 187 | $file = $this->processImages($file, $entity); 188 | 189 | foreach ($this->processors as $processor) { 190 | $file = $processor->process($file); 191 | } 192 | 193 | $entity = $this->fileObjectToEntity($file, $entity); 194 | $this->getTable()->save( 195 | $entity, 196 | ['callbacks' => false] 197 | ); 198 | } catch (Throwable $exception) { 199 | $this->getTable()->delete($entity); 200 | 201 | throw $exception; 202 | } 203 | } 204 | 205 | $this->dispatchEvent('FileStorage.afterSave', [ 206 | 'entity' => $entity, 207 | 'storageAdapter' => $this->getStorageAdapter($entity->get('adapter')), 208 | ], $this->getTable()); 209 | } 210 | 211 | /** 212 | * checkEntityBeforeSave 213 | * 214 | * @param \Cake\Datasource\EntityInterface $entity 215 | * 216 | * @return void 217 | */ 218 | protected function checkEntityBeforeSave(EntityInterface $entity) 219 | { 220 | if ($entity->isNew()) { 221 | if (!$entity->has('model')) { 222 | $entity->set('model', $this->getTable()->getTable()); 223 | } 224 | 225 | if (!$entity->has('adapter')) { 226 | $entity->set('adapter', $this->getConfig('defaultStorageConfig')); 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * afterDelete callback 233 | * 234 | * @param \Cake\Event\EventInterface $event 235 | * @param \Cake\Datasource\EntityInterface $entity 236 | * @param \ArrayObject $options 237 | * 238 | * @return void 239 | */ 240 | public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void 241 | { 242 | $this->dispatchEvent('FileStorage.afterDelete', [ 243 | 'entity' => $entity, 244 | ], $this->getTable()); 245 | 246 | $file = $this->entityToFileObject($entity); 247 | $this->fileStorage->remove($file); 248 | } 249 | 250 | /** 251 | * Gets information about the file that is being uploaded. 252 | * 253 | * - gets the file size 254 | * - gets the mime type 255 | * - gets the extension if present 256 | * 257 | * @param array|\ArrayAccess $upload 258 | * @param string $field 259 | * 260 | * @return void 261 | */ 262 | protected function getFileInfoFromUpload(&$upload, $field = 'file') 263 | { 264 | /** @var \Psr\Http\Message\UploadedFileInterface|array $uploadedFile */ 265 | $uploadedFile = $upload[$field]; 266 | if (!is_array($uploadedFile)) { 267 | $upload['filesize'] = $uploadedFile->getSize(); 268 | $upload['mime_type'] = $uploadedFile->getClientMediaType(); 269 | $upload['extension'] = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION); 270 | $upload['filename'] = $uploadedFile->getClientFilename(); 271 | } else { 272 | $upload['filesize'] = $uploadedFile['size']; 273 | $upload['mime_type'] = $uploadedFile['type']; 274 | $upload['extension'] = pathinfo($uploadedFile['name'], PATHINFO_EXTENSION); 275 | $upload['filename'] = $uploadedFile['name']; 276 | } 277 | } 278 | 279 | /** 280 | * Don't use Table::deleteAll() if you don't want to end up with orphaned 281 | * files! The reason for that is that deleteAll() doesn't fire the 282 | * callbacks. So the events that will remove the files won't get fired. 283 | * 284 | * @param array $conditions Query::where() array structure. 285 | * 286 | * @return int Number of deleted records / files 287 | */ 288 | public function deleteAllFiles($conditions) 289 | { 290 | $table = $this->getTable(); 291 | 292 | $results = $table->find() 293 | ->select((array)$table->getPrimaryKey()) 294 | ->where($conditions) 295 | ->all(); 296 | 297 | if ($results->count() > 0) { 298 | foreach ($results as $result) { 299 | $table->delete($result); 300 | } 301 | } 302 | 303 | return $results->count(); 304 | } 305 | 306 | /** 307 | * @param \Cake\Datasource\EntityInterface $entity Entity 308 | * 309 | * @return \Phauthentic\Infrastructure\Storage\FileInterface 310 | */ 311 | public function entityToFileObject(EntityInterface $entity): FileInterface 312 | { 313 | return $this->transformer->entityToFileObject($entity); 314 | } 315 | 316 | /** 317 | * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File 318 | * @param \Cake\Datasource\EntityInterface|null $entity 319 | * 320 | * @return \Cake\Datasource\EntityInterface 321 | */ 322 | public function fileObjectToEntity(FileInterface $file, ?EntityInterface $entity) 323 | { 324 | return $this->transformer->fileObjectToEntity($file, $entity); 325 | } 326 | 327 | /** 328 | * Processes images 329 | * 330 | * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File 331 | * @param \Cake\Datasource\EntityInterface $entity 332 | * 333 | * @return \Phauthentic\Infrastructure\Storage\FileInterface 334 | */ 335 | public function processImages(FileInterface $file, EntityInterface $entity): FileInterface 336 | { 337 | $imageSizes = Configure::read('FileStorage.imageVariants'); 338 | $model = $file->model(); 339 | $identifier = $entity->get('identifier'); 340 | 341 | if (!isset($imageSizes[$model][$identifier])) { 342 | return $file; 343 | } 344 | 345 | $file = $file->withVariants($imageSizes[$model][$identifier]); 346 | $file = $this->imageProcessor->process($file); 347 | 348 | return $file; 349 | } 350 | 351 | /** 352 | * @throws \RuntimeException 353 | * 354 | * @return \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface 355 | */ 356 | protected function getImageProcessor(): ProcessorInterface 357 | { 358 | if ($this->imageProcessor !== null) { 359 | return $this->imageProcessor; 360 | } 361 | 362 | if ($this->getConfig('imageProcessor') instanceof ProcessorInterface) { 363 | $this->imageProcessor = $this->getConfig('imageProcessor'); 364 | } 365 | 366 | if ($this->imageProcessor === null) { 367 | throw new RuntimeException('No image processor found'); 368 | } 369 | 370 | return $this->imageProcessor; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/Model/Entity/FileStorage.php: -------------------------------------------------------------------------------- 1 | true, 25 | ]; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected $_virtual = [ 31 | 'variantUrls', 32 | ]; 33 | 34 | /** 35 | * @param string $variant Variant 36 | * 37 | * @return string|null 38 | */ 39 | public function getVariantUrl(string $variant): ?string 40 | { 41 | $variants = (array)$this->get('variants'); 42 | if (!isset($variants[$variant]['url'])) { 43 | return null; 44 | } 45 | 46 | return $variants[$variant]['url']; 47 | } 48 | 49 | /** 50 | * @param string $variant Variant 51 | * 52 | * @return string|null 53 | */ 54 | public function getVariantPath(string $variant): ?string 55 | { 56 | $variants = (array)$this->get('variants'); 57 | if (!isset($variants[$variant]['path'])) { 58 | return null; 59 | } 60 | 61 | return $variants[$variant]['path']; 62 | } 63 | 64 | /** 65 | * Making it backward compatible 66 | * 67 | * @return array 68 | */ 69 | protected function _getVariantUrls() 70 | { 71 | $variants = (array)$this->get('variants'); 72 | $list = [ 73 | 'original' => $this->get('url'), 74 | ]; 75 | 76 | foreach ($variants as $name => $data) { 77 | if (!empty($data['url'])) { 78 | $list[$name] = $data['url']; 79 | } 80 | } 81 | 82 | return $list; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Model/Entity/FileStorageEntityInterface.php: -------------------------------------------------------------------------------- 1 | addColumn('variants', 'json'); 37 | $schema->addColumn('metadata', 'json'); 38 | 39 | return parent::_initializeSchema($schema); 40 | } 41 | 42 | /** 43 | * Initialize 44 | * 45 | * @param array $config 46 | * 47 | * @return void 48 | */ 49 | public function initialize(array $config): void 50 | { 51 | parent::initialize($config); 52 | 53 | $this->setTable('file_storage'); 54 | $this->setPrimaryKey('id'); 55 | $this->setDisplayField('filename'); 56 | 57 | $this->addBehavior('Timestamp'); 58 | $this->addBehavior( 59 | 'Burzum/FileStorage.FileStorage', 60 | (array)Configure::read('FileStorage.behaviorConfig'), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | setDescription([ 46 | __d('file_storage', 'Shell command for generating and removing image versions.'), 47 | ]); 48 | $parser->addOption('storageTable', [ 49 | 'short' => 's', 50 | 'help' => __d('file_storage', 'The storage table for image processing you want to use.'), 51 | 'default' => 'Burzum/FileStorage.FileStorage', 52 | ]); 53 | $parser->addOption('limit', [ 54 | 'short' => 'l', 55 | 'help' => __d('file_storage', 'Limits the amount of records to be processed in one batch'), 56 | ]); 57 | $parser->addSubcommands([ 58 | 'generate' => [ 59 | 'help' => __d('file_storage', ' Generate a new image version'), 60 | 'parser' => [ 61 | 'arguments' => [ 62 | 'model' => [ 63 | 'help' => __d('file_storage', 'Value of the model property of the images to generate'), 64 | 'required' => true, 65 | ], 66 | 'identifier' => [ 67 | 'help' => __d('file_storage', 'The image identifier (`model` field in `file_storage` table).'), 68 | 'required' => true, 69 | ], 70 | ], 71 | 'options' => [ 72 | 'adapter' => [ 73 | 'short' => 'a', 74 | 'help' => __('The adapter config name to use.'), 75 | 'default' => 'Local', 76 | ], 77 | 'storageTable' => [ 78 | 'short' => 's', 79 | 'help' => __d('file_storage', 'The storage table for image processing you want to use.'), 80 | 'default' => 'Burzum/FileStorage.FileStorage', 81 | ], 82 | 'limit' => [ 83 | 'short' => 'l', 84 | 'help' => __d('file_storage', 'Limits the amount of records to be processed in one batch'), 85 | ], 86 | 'keep-old-versions' => [ 87 | 'short' => 'k', 88 | 'help' => __d('file_storage', 'Use this switch if you do not want to overwrite existing versions.'), 89 | 'boolean' => true, 90 | ], 91 | ], 92 | ], 93 | ], 94 | 'remove' => [ 95 | 'help' => __d('file_storage', ' Remove an new image version'), 96 | 'parser' => [ 97 | 'arguments' => [ 98 | 'model' => [ 99 | 'help' => __d('file_storage', 'Value of the model property of the images to remove'), 100 | 'required' => true, 101 | ], 102 | 'version' => [ 103 | 'help' => __d('file_storage', 'Image version to remove'), 104 | 'required' => true, 105 | ], 106 | ], 107 | 'options' => [ 108 | 'adapter' => [ 109 | 'short' => 'a', 110 | 'help' => __('The adapter config name to use.'), 111 | 'default' => 'Local', 112 | ], 113 | 'storageTable' => [ 114 | 'short' => 's', 115 | 'help' => __d('file_storage', 'The storage table for image processing you want to use.'), 116 | 'default' => 'Burzum/FileStorage.FileStorage', 117 | ], 118 | 'limit' => [ 119 | 'short' => 'l', 120 | 'help' => __d('file_storage', 'Limits the amount of records to be processed in one batch'), 121 | ], 122 | ], 123 | ], 124 | ], 125 | 'regenerate' => [ 126 | 'help' => __d('file_storage', ' Generates all image versions.'), 127 | 'parser' => [ 128 | 'arguments' => [ 129 | 'model' => [ 130 | 'help' => __d('file_storage', 'Value of the model property of the images to generate'), 131 | 'required' => true, 132 | ], 133 | ], 134 | 'options' => [ 135 | 'adapter' => [ 136 | 'short' => 'a', 137 | 'help' => __('The adapter config name to use.'), 138 | 'default' => 'Local', 139 | ], 140 | 'storageTable' => [ 141 | 'short' => 's', 142 | 'help' => __d('file_storage', 'The storage table for image processing you want to use.'), 143 | 'default' => 'Burzum/FileStorage.FileStorage', 144 | ], 145 | 'limit' => [ 146 | 'short' => 'l', 147 | 'help' => __d('file_storage', 'Limits the amount of records to be processed in one batch'), 148 | ], 149 | 'keep-old-versions' => [ 150 | 'short' => 'k', 151 | 'help' => __d('file_storage', 'Use this switch if you do not want to overwrite existing versions.'), 152 | 'boolean' => true, 153 | ], 154 | ], 155 | ], 156 | ], 157 | ]); 158 | 159 | return $parser; 160 | } 161 | 162 | /** 163 | * @inheritDoc 164 | */ 165 | public function startup(): void 166 | { 167 | parent::startup(); 168 | 169 | $storageTable = $this->params['storageTable']; 170 | 171 | try { 172 | $this->Table = TableRegistry::getTableLocator()->get($storageTable); 173 | } catch (Exception $e) { 174 | $this->abort($e->getMessage()); 175 | } 176 | 177 | if (isset($this->params['limit'])) { 178 | if (!is_numeric($this->params['limit'])) { 179 | $this->abort(__d('file_storage', '--limit must be an integer!')); 180 | } 181 | $this->limit = (int)$this->params['limit']; 182 | } 183 | } 184 | 185 | /** 186 | * Generate all image versions. 187 | * 188 | * @return void 189 | */ 190 | public function regenerate(): void 191 | { 192 | $operations = Configure::read('FileStorage.imageSizes.' . $this->args[0]); 193 | $options = [ 194 | 'overwrite' => !$this->params['keep-old-versions'], 195 | ]; 196 | 197 | if (empty($operations)) { 198 | $this->abort(__d('file_storage', 'Invalid table or version.')); 199 | } 200 | 201 | foreach ($operations as $version => $operation) { 202 | try { 203 | $this->_loop($this->command, $this->args[0], [$version => $operation], $options); 204 | } catch (Exception $e) { 205 | $this->abort($e->getMessage()); 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * Generate a given image version. 212 | * 213 | * @param string $model 214 | * @param string $version 215 | * 216 | * @return void 217 | */ 218 | public function generate(string $model, string $version): void 219 | { 220 | $operations = Configure::read('FileStorage.imageVariants.' . $model . '.' . $version); 221 | $options = [ 222 | 'overwrite' => !$this->params['keep-old-versions'], 223 | ]; 224 | 225 | if (empty($operations)) { 226 | $this->out(__d('file_storage', 'Invalid table or version.')); 227 | $this->_stop(); 228 | } 229 | 230 | try { 231 | $this->_loop('generate', $model, [$version => $operations], $options); 232 | } catch (Exception $e) { 233 | $this->abort($e->getMessage()); 234 | } 235 | } 236 | 237 | /** 238 | * Remove a given image version. 239 | * 240 | * @param string $model 241 | * @param string $version 242 | * 243 | * @return void 244 | */ 245 | public function remove(string $model, string $version): void 246 | { 247 | $operations = Configure::read('FileStorage.imageSizes.' . $model . '.' . $version); 248 | 249 | if (empty($operations)) { 250 | $this->out(__d('file_storage', 'Invalid table or version.')); 251 | $this->_stop(); 252 | } 253 | 254 | try { 255 | $this->_loop('remove', $model, [$version => $operations]); 256 | } catch (Exception $e) { 257 | $this->out($e->getMessage()); 258 | $this->_stop(); 259 | } 260 | } 261 | 262 | /** 263 | * Loops through image records and performs requested operation on them. 264 | * 265 | * @param string $action 266 | * @param string $model 267 | * @param array $operations 268 | * @param array $options 269 | * 270 | * @return void 271 | */ 272 | protected function _loop(string $action, $model, array $operations = [], array $options = []): void 273 | { 274 | if (!in_array($action, ['generate', 'remove', 'regenerate'])) { 275 | $this->_stop(); 276 | } 277 | 278 | $totalImageCount = $this->_getCount($model); 279 | 280 | if ($totalImageCount === 0) { 281 | $this->out(__d('file_storage', 'No Images for model {0} found', $model)); 282 | $this->_stop(); 283 | } 284 | 285 | $this->out(__d('file_storage', '{0} image file(s) will be processed' . "\n", $totalImageCount)); 286 | 287 | $offset = 0; 288 | $limit = $this->limit; 289 | 290 | /** @var \Phauthentic\Infrastructure\Storage\FileStorage|null $storage */ 291 | $storage = Configure::read('FileStorage.behaviorConfig.fileStorage'); 292 | if (!$storage) { 293 | $this->abort(sprintf('Invalid adapter config `%s` provided!', $this->params['adapter'])); 294 | } 295 | $adapter = $storage->getStorage($this->params['adapter']); 296 | 297 | do { 298 | $images = $this->_getRecords($model, $limit, $offset); 299 | if ($images->count()) { 300 | foreach ($images as $image) { 301 | $payload = [ 302 | 'entity' => $image, 303 | 'storage' => $adapter, 304 | 'operations' => $operations, 305 | 'versions' => array_keys($operations), 306 | 'table' => $this->Table, 307 | 'options' => $options, 308 | ]; 309 | 310 | if ($action === 'generate' || $action === 'regenerate') { 311 | $Event = new Event('ImageVersion.createVersion', $this->Table, $payload); 312 | EventManager::instance()->dispatch($Event); 313 | } 314 | 315 | if ($action === 'remove') { 316 | $Event = new Event('ImageVersion.removeVersion', $this->Table, $payload); 317 | EventManager::instance()->dispatch($Event); 318 | } 319 | 320 | $this->out(__('{0} processed', $image->id)); 321 | } 322 | } 323 | $offset += $limit; 324 | } while ($images->count() > 0); 325 | } 326 | 327 | /** 328 | * Gets the amount of images for a model in the DB. 329 | * 330 | * @param string $identifier 331 | * @param array $extensions 332 | * 333 | * @return int 334 | */ 335 | protected function _getCount(string $identifier, array $extensions = ['jpg', 'png', 'jpeg']): int 336 | { 337 | return $this->Table 338 | ->find() 339 | ->where(['model' => $identifier]) 340 | ->andWhere(['extension IN' => $extensions]) 341 | ->count(); 342 | } 343 | 344 | /** 345 | * Gets the chunk of records for the image processing 346 | * 347 | * @param string $identifier 348 | * @param int $limit 349 | * @param int $offset 350 | * @param array $extensions 351 | * 352 | * @return \Cake\Datasource\ResultSetInterface 353 | */ 354 | protected function _getRecords(string $identifier, int $limit, int $offset, array $extensions = ['jpg', 'png', 'jpeg']): ResultSetInterface 355 | { 356 | return $this->Table 357 | ->find() 358 | ->where(['model' => $identifier]) 359 | ->andWhere(['extension IN' => $extensions]) 360 | ->limit($limit) 361 | ->offset($offset) 362 | ->all(); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/Shell/StorageShell.php: -------------------------------------------------------------------------------- 1 | addOption('adapter', [ 35 | 'short' => 'a', 36 | 'help' => __('The adapter config name to use.'), 37 | 'default' => 'Local', 38 | ]); 39 | $parser->addOption('identifier', [ 40 | 'short' => 'i', 41 | 'help' => __('The file identifier (`model` field in `file_storage` table).'), 42 | 'default' => null, 43 | ]); 44 | $parser->addOption('model', [ 45 | 'short' => 'm', 46 | 'help' => __('The model / table to use.'), 47 | 'default' => 'Burzum/FileStorage.FileStorage', 48 | ]); 49 | $parser->addSubcommand('image', [ 50 | 'help' => __('Image Processing Task.'), 51 | 'parser' => $this->Image->getOptionParser(), 52 | ]); 53 | $parser->addSubcommand('store', [ 54 | 'help' => __('Stores a file in the DB.'), 55 | ]); 56 | $parser->addSubcommand('attach', [ 57 | 'help' => __('Attach a file to a record.'), 58 | ]); 59 | 60 | return $parser; 61 | } 62 | 63 | /** 64 | * Does the arg and params checks for store(). 65 | * 66 | * @return void 67 | */ 68 | protected function _storePreCheck(): void 69 | { 70 | if (empty($this->args[0])) { 71 | $this->abort('No file provided!'); 72 | } 73 | 74 | if (!file_exists($this->args[0])) { 75 | $this->abort('The file does not exist!'); 76 | } 77 | 78 | /** @var \Phauthentic\Infrastructure\Storage\FileStorage|null $storage */ 79 | $storage = Configure::read('FileStorage.behaviorConfig.fileStorage'); 80 | if (!$storage) { 81 | $this->abort(sprintf('Invalid adapter config `%s` provided!', $this->params['adapter'])); 82 | } 83 | //$adapter = $storage->getStorage($this->params['adapter']); 84 | } 85 | 86 | /** 87 | * Store a local file via command line in any storage backend. 88 | * 89 | * @return void 90 | */ 91 | public function store(): void 92 | { 93 | $this->_storePreCheck(); 94 | $model = $this->loadModel($this->params['model']); 95 | if (Configure::read('App.uploadedFilesAsObjects', true)) { 96 | $fileData = StorageUtils::fileToUploadedFileObject($this->args[0]); 97 | } else { 98 | $fileData = StorageUtils::fileToUploadedFileArray($this->args[0]); 99 | } 100 | $entity = $model->newEntity([ 101 | 'adapter' => $this->params['adapter'], 102 | 'file' => $fileData, 103 | 'filename' => is_array($fileData) ? $fileData['name'] : $fileData->getClientFilename(), 104 | ]); 105 | if ($entity->getErrors()) { 106 | $this->abort('Validation failed: ' . print_r($entity->getErrors(), true)); 107 | } 108 | 109 | if (!$model->save($entity)) { 110 | $this->abort('Failed to save the file.'); 111 | } 112 | 113 | $this->out('File successfully saved!'); 114 | $this->out('ID: ' . $entity->get('id')); 115 | $this->out('Path: ' . $entity->get('path')); 116 | $this->out('Size: ' . $entity->get('filesize')); 117 | } 118 | 119 | /** 120 | * Store a local file via command line in any storage backend. 121 | * 122 | * @return void 123 | */ 124 | public function attach(): void 125 | { 126 | $this->_storePreCheck(); 127 | $model = $this->loadModel($this->params['model']); 128 | if (Configure::read('App.uploadedFilesAsObjects', true)) { 129 | $fileData = StorageUtils::fileToUploadedFileObject($this->args[0]); 130 | } else { 131 | $fileData = StorageUtils::fileToUploadedFileArray($this->args[0]); 132 | } 133 | $entity = $model->newEntity([ 134 | 'adapter' => $this->params['adapter'], 135 | 'file' => $fileData, 136 | 'filename' => is_array($fileData) ? $fileData['name'] : $fileData->getClientFilename(), 137 | 'model' => 'X', 138 | 'foreign_key' => '1', 139 | ]); 140 | if ($entity->getErrors()) { 141 | $this->abort('Validation failed: ' . print_r($entity->getErrors(), true)); 142 | } 143 | 144 | if (!$model->save($entity)) { 145 | $this->abort('Failed to save the file.'); 146 | } 147 | 148 | $this->out('File successfully attached to record ``!'); 149 | $this->out('ID: ' . $entity->get('id')); 150 | $this->out('Path: ' . $entity->get('path')); 151 | $this->out('Size: ' . $entity->get('filesize')); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Shell/Task/ImageTask.php: -------------------------------------------------------------------------------- 1 | 24 | * bin\cake Burzum/FileStorage.storage image remove ProfilePicture "thumb60, crop50" 25 | */ 26 | class ImageTask extends Shell 27 | { 28 | use EventDispatcherTrait; 29 | 30 | /** 31 | * @var \Cake\ORM\Table 32 | */ 33 | protected $Table; 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function initialize(): void 39 | { 40 | parent::initialize(); 41 | $this->Table = TableRegistry::getTableLocator()->get('Burzum/FileStorage.ImageStorage'); 42 | } 43 | 44 | /** 45 | * Remove image versions. 46 | * 47 | * @return void 48 | */ 49 | public function remove(): void 50 | { 51 | $this->_loop($this->args[0], explode(',', $this->args[1]), 'remove'); 52 | } 53 | 54 | /** 55 | * Create image versions. 56 | * 57 | * @return void 58 | */ 59 | public function generate(): void 60 | { 61 | $this->_loop($this->args[0], explode(',', $this->args[1]), 'generate'); 62 | } 63 | 64 | /** 65 | * Loops through image records and performs requested operations on them. 66 | * 67 | * @param string $identifier 68 | * @param array $options 69 | * @param string $action 70 | * 71 | * @return void 72 | */ 73 | protected function _loop(string $identifier, $options, $action): void 74 | { 75 | $count = $this->_getCount($identifier); 76 | $offset = 0; 77 | $limit = $this->params['limit']; 78 | 79 | $this->out(__d('file_storage', '{0} record(s) will be processed.' . "\n", $count)); 80 | 81 | do { 82 | $records = $this->_getRecords($identifier, $limit, $offset); 83 | if ($records->count()) { 84 | foreach ($records as $record) { 85 | $method = '_' . $action . 'Image'; 86 | try { 87 | $this->{$method}($record, $options); 88 | } catch (StorageException $e) { 89 | $this->err($e->getMessage()); 90 | } 91 | } 92 | } 93 | $offset += $limit; 94 | $this->out(__d('file_storage', '{0} of {1} records processed.', [$limit, $count])); 95 | } while ($records->count() > 0); 96 | } 97 | 98 | /** 99 | * Triggers the event to remove image versions. 100 | * 101 | * @param \Cake\ORM\Entity $record 102 | * @param array $options 103 | * 104 | * @return void 105 | */ 106 | protected function _removeImage($record, $options): void 107 | { 108 | $Event = new Event('ImageVersion.removeVersion', $this->Table, [ 109 | 'entity' => $record, 110 | 'operations' => $options, 111 | ]); 112 | EventManager::instance()->dispatch($Event); 113 | } 114 | 115 | /** 116 | * Triggers the event to generate the new images. 117 | * 118 | * @param \Cake\ORM\Entity $record 119 | * @param array $options 120 | * 121 | * @return void 122 | */ 123 | protected function _generateImage($record, $options): void 124 | { 125 | $Event = new Event('ImageVersion.createVersion', $this->Table, [ 126 | 'entity' => $record, 127 | 'operations' => $options, 128 | ]); 129 | EventManager::instance()->dispatch($Event); 130 | } 131 | 132 | /** 133 | * Gets the records for the loop. 134 | * 135 | * @param string $identifier Identifier. 136 | * @param int $limit Records limit. 137 | * @param int $offset Records offset. 138 | * 139 | * @return \Cake\Datasource\ResultSetInterface 140 | */ 141 | public function _getRecords(string $identifier, int $limit, int $offset): ResultSetInterface 142 | { 143 | return $this->Table 144 | ->find() 145 | ->where([$this->Table->getAlias() . '.model' => $identifier]) 146 | ->limit($limit) 147 | ->offset($offset) 148 | ->all(); 149 | } 150 | 151 | /** 152 | * Gets the amount of records for an identifier in the DB. 153 | * 154 | * @param string $identifier 155 | * 156 | * @return int 157 | */ 158 | protected function _getCount(string $identifier): int 159 | { 160 | $count = $this->_getCountQuery($identifier)->count(); 161 | if ($count === 0) { 162 | $this->out(__d('file_storage', 'No records for identifier "{0}" found.', $identifier)); 163 | $this->_stop(); 164 | } 165 | 166 | return $count; 167 | } 168 | 169 | /** 170 | * Gets the query object for the count. 171 | * 172 | * @param string $identifier 173 | * 174 | * @return \Cake\ORM\Query 175 | */ 176 | protected function _getCountQuery(string $identifier): Query 177 | { 178 | return $this->Table 179 | ->find() 180 | ->where([ 181 | $this->Table->getAlias() . '.model' => $identifier, 182 | ]); 183 | } 184 | 185 | /** 186 | * @inheritDoc 187 | */ 188 | public function getOptionParser(): ConsoleOptionParser 189 | { 190 | $parser = parent::getOptionParser(); 191 | $parser->addOption('model', [ 192 | 'short' => 'm', 193 | 'help' => __('The model to use.'), 194 | 'default' => 'Burzum/FileStorage.ImageStorage', 195 | ]); 196 | 197 | $parser->addOption('limit', [ 198 | 'short' => 'l', 199 | 'help' => __('The limit of records to process in a batch.'), 200 | 'default' => 50, 201 | ]); 202 | 203 | $parser->addArguments([ 204 | 'identifier' => ['help' => 'The identifier to process', 'required' => true], 205 | 'versions' => ['help' => 'The versions to process', 'required' => true], 206 | ]); 207 | 208 | return $parser; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Utility/StorageUtils.php: -------------------------------------------------------------------------------- 1 | $filename, 39 | 'size' => filesize($filename), 40 | 'error' => UPLOAD_ERR_OK, 41 | 'name' => basename($filename), 42 | 'type' => $mimeType, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/View/Helper/ImageHelper.php: -------------------------------------------------------------------------------- 1 | '', 36 | ]; 37 | 38 | /** 39 | * Generates an image url based on the image record data and the used Gaufrette adapter to store it 40 | * 41 | * @param \Burzum\FileStorage\Model\Entity\FileStorageEntityInterface|null $image FileStorage entity or whatever else table that matches this helpers needs without 42 | * the model, we just want the record fields 43 | * @param string|null $version Image version string 44 | * @param array $options HtmlHelper::image(), 2nd arg options array 45 | * 46 | * @return string 47 | */ 48 | public function display(?FileStorageEntityInterface $image, ?string $version = null, array $options = []): string 49 | { 50 | if ($image === null) { 51 | return $this->fallbackImage($options, $version); 52 | } 53 | 54 | $url = $this->imageUrl($image, $version, $options); 55 | if ($url !== null) { 56 | return $this->Html->image($url, $options); 57 | } 58 | 59 | return $this->fallbackImage($options, $version); 60 | } 61 | 62 | /** 63 | * URL 64 | * 65 | * @param \Burzum\FileStorage\Model\Entity\FileStorageEntityInterface $image FileStorage entity or whatever else table that matches this helpers needs without 66 | * the model, we just want the record fields 67 | * @param string|null $variant Image version string 68 | * @param array $options HtmlHelper::image(), 2nd arg options array 69 | * 70 | * @throws \Phauthentic\Infrastructure\Storage\Processor\Exception\VariantDoesNotExistException 71 | * 72 | * @return string|null 73 | */ 74 | public function imageUrl(FileStorageEntityInterface $image, ?string $variant = null, array $options = []): ?string 75 | { 76 | if ($variant === null) { 77 | $url = (string)$image->get('url'); 78 | if ($url) { 79 | return $url; 80 | } 81 | $path = (string)$image->get('path'); 82 | } else { 83 | $url = $image->getVariantUrl($variant); 84 | if ($url) { 85 | return $url; 86 | } 87 | $path = $image->getVariantPath($variant); 88 | } 89 | 90 | if (!$path) { 91 | throw VariantDoesNotExistException::withName($variant); 92 | } 93 | 94 | $options = array_merge($this->getConfig(), $options); 95 | if (!empty($options['pathPrefix'])) { 96 | $url = $options['pathPrefix'] . $path; 97 | } 98 | 99 | return $this->normalizePath((string)$url); 100 | } 101 | 102 | /** 103 | * Provides a fallback image if the image record is empty 104 | * 105 | * @param array $options 106 | * @param string|null $version 107 | * 108 | * @return string 109 | */ 110 | public function fallbackImage(array $options = [], ?string $version = null): string 111 | { 112 | if (isset($options['fallback'])) { 113 | if ($options['fallback'] === true) { 114 | $imageFile = 'placeholder/' . $version . '.jpg'; 115 | } else { 116 | $imageFile = $options['fallback']; 117 | } 118 | unset($options['fallback']); 119 | 120 | return $this->Html->image($imageFile, $options); 121 | } 122 | 123 | return ''; 124 | } 125 | 126 | /** 127 | * Turns the windows \ into / so that the path can be used in an url 128 | * 129 | * @param string $path 130 | * 131 | * @return string 132 | */ 133 | protected function normalizePath(string $path): string 134 | { 135 | return str_replace('\\', '/', $path); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Fixture/File/cake.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burzum/cakephp-file-storage/dec9ab1241e3ab36ec55c834ba6cfe9361b3b88f/tests/Fixture/File/cake.icon.png -------------------------------------------------------------------------------- /tests/Fixture/File/titus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burzum/cakephp-file-storage/dec9ab1241e3ab36ec55c834ba6cfe9361b3b88f/tests/Fixture/File/titus.jpg -------------------------------------------------------------------------------- /tests/Fixture/FileStorageFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], 32 | 'user_id' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], 33 | 'foreign_key' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], 34 | 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], 35 | 'filename' => ['type' => 'string', 'null' => false, 'default' => null], 36 | 'filesize' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 16], 37 | 'mime_type' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 38 | 'extension' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 39 | 'hash' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], 40 | 'path' => ['type' => 'string', 'null' => true, 'default' => null], 41 | 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32, 'comment' => 'Gaufrette Storage Adapter Class'], 42 | 'variants' => ['type' => 'json', 'null' => true, 'default' => null], 43 | 'metadata' => ['type' => 'json', 'null' => true, 'default' => null], 44 | 'created' => ['type' => 'datetime', 'null' => true, 'default' => null], 45 | 'modified' => ['type' => 'datetime', 'null' => true, 'default' => null], 46 | '_constraints' => [ 47 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 48 | ], 49 | ]; 50 | 51 | /** 52 | * Records 53 | * 54 | * @var array 55 | */ 56 | public $records = [ 57 | [ 58 | 'id' => 'file-storage-1', 59 | 'user_id' => 'user-1', 60 | 'foreign_key' => 'item-1', 61 | 'model' => 'Item', 62 | 'filename' => 'cake.icon.png', 63 | 'filesize' => '', 64 | 'mime_type' => 'image/png', 65 | 'extension' => 'png', 66 | 'hash' => '', 67 | 'path' => '', 68 | 'adapter' => 'Local', 69 | 'variants' => '{}', 70 | 'metadata' => '{}', 71 | 'created' => '2012-01-01 12:00:00', 72 | 'modified' => '2012-01-01 12:00:00', 73 | ], 74 | [ 75 | 'id' => 'file-storage-2', 76 | 'user_id' => 'user-1', 77 | 'foreign_key' => 'item-1', 78 | 'model' => 'Item', 79 | 'filename' => 'titus-bienebek-bridle.jpg', 80 | 'filesize' => '', 81 | 'mime_type' => 'image/jpg', 82 | 'extension' => 'jpg', 83 | 'hash' => '', 84 | 'path' => '', 85 | 'adapter' => 'Local', 86 | 'variants' => '{}', 87 | 'metadata' => '{}', 88 | 'created' => '2012-01-01 12:00:00', 89 | 'modified' => '2012-01-01 12:00:00', 90 | ], 91 | [ 92 | 'id' => 'file-storage-3', 93 | 'user_id' => 'user-1', 94 | 'foreign_key' => 'item-2', 95 | 'model' => 'Item', 96 | 'filename' => 'titus.jpg', 97 | 'filesize' => '335872', 98 | 'mime_type' => 'image/jpg', 99 | 'extension' => 'jpg', 100 | 'hash' => '', 101 | 'path' => '', 102 | 'adapter' => 'Local', 103 | 'variants' => '{}', 104 | 'metadata' => '{}', 105 | 'created' => '2012-01-01 12:00:00', 106 | 'modified' => '2012-01-01 12:00:00', 107 | ], 108 | [ 109 | 'id' => 'file-storage-4', 110 | 'user_id' => 'user-1', 111 | 'foreign_key' => 'item-4', 112 | 'model' => 'Item', 113 | 'filename' => 'titus.jpg', 114 | 'filesize' => '335872', 115 | 'mime_type' => 'image/jpg', 116 | 'extension' => 'jpg', 117 | 'hash' => '09d82a31', 118 | 'path' => null, 119 | 'adapter' => 'S3', 120 | 'variants' => '{}', 121 | 'metadata' => '{}', 122 | 'created' => '2012-01-01 12:00:00', 123 | 'modified' => '2012-01-01 12:00:00', 124 | ], 125 | ]; 126 | } 127 | -------------------------------------------------------------------------------- /tests/Fixture/ItemFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], 32 | 'name' => ['type' => 'string', 'null' => true, 'default' => null], 33 | 'path' => ['type' => 'string', 'null' => true, 'default' => null], 34 | 'filename' => ['type' => 'string', 'null' => true, 'default' => null], 35 | '_constraints' => [ 36 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 37 | ], 38 | ]; 39 | 40 | /** 41 | * Records 42 | * 43 | * @var array 44 | */ 45 | public $records = [ 46 | [ 47 | 'id' => 'item-1', 48 | 'name' => 'Cake', 49 | ], 50 | [ 51 | 'id' => 'item-2', 52 | 'name' => 'More Cake', 53 | ], 54 | [ 55 | 'id' => 'item-3', 56 | 'name' => 'A lot Cake', 57 | ], 58 | ]; 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestCase/FileStorageTestCase.php: -------------------------------------------------------------------------------- 1 | testPath = TMP . 'file-storage-test' . DS; 71 | $this->fileFixtures = Plugin::path('Burzum/FileStorage') . 'tests' . DS . 'Fixture' . DS . 'File' . DS; 72 | 73 | if (!is_dir($this->testPath)) { 74 | mkdir($this->testPath); 75 | } 76 | 77 | $this->prepareDependencies(); 78 | $this->configureImageVariants(); 79 | 80 | $this->FileStorage = $this 81 | ->getTableLocator() 82 | ->get(FileStorageTestTable::class); 83 | } 84 | 85 | /** 86 | * @return void 87 | */ 88 | private function configureImageVariants(): void 89 | { 90 | Configure::write('FileStorage.imageVariants', [ 91 | 'Test' => [ 92 | 't50' => [ 93 | 'thumbnail' => [ 94 | 'mode' => 'outbound', 95 | 'width' => 50, 96 | 'height' => 50, 97 | ], 98 | ], 99 | 't150' => [ 100 | 'thumbnail' => [ 101 | 'mode' => 'outbound', 102 | 'width' => 150, 103 | 'height' => 150, 104 | ], 105 | ], 106 | ], 107 | 'UserAvatar' => [ 108 | 'small' => [ 109 | 'thumbnail' => [ 110 | 'mode' => 'inbound', 111 | 'width' => 80, 112 | 'height' => 80, 113 | ], 114 | ], 115 | ], 116 | ]); 117 | } 118 | 119 | /** 120 | * @return void 121 | */ 122 | private function prepareDependencies(): void 123 | { 124 | $pathBuilder = new PathBuilder([ 125 | 'pathTemplate' => '{model}{ds}{collection}{ds}{randomPath}{ds}{strippedId}{ds}{strippedId}.{extension}', 126 | 'variantPathTemplate' => '{model}{ds}{collection}{ds}{randomPath}{ds}{strippedId}{ds}{strippedId}.{hashedVariant}.{extension}', 127 | ]); 128 | 129 | $storageService = new StorageService( 130 | new StorageAdapterFactory() 131 | ); 132 | 133 | $storageService->setAdapterConfigFromArray([ 134 | 'Local' => [ 135 | 'class' => LocalFactory::class, 136 | 'options' => [ 137 | 'root' => $this->testPath, true, 138 | ], 139 | ], 140 | ]); 141 | 142 | $fileStorage = new FileStorage( 143 | $storageService, 144 | $pathBuilder 145 | ); 146 | 147 | $imageManager = new ImageManager([ 148 | 'driver' => 'gd', 149 | ]); 150 | 151 | $imageProcessor = new ImageProcessor( 152 | $fileStorage, 153 | $pathBuilder, 154 | $imageManager 155 | ); 156 | 157 | Configure::write('FileStorage.behaviorConfig', [ 158 | 'fileStorage' => $fileStorage, 159 | 'imageProcessor' => $imageProcessor, 160 | ]); 161 | } 162 | 163 | /** 164 | * Cleanup test files 165 | * 166 | * @return void 167 | */ 168 | public function tearDown(): void 169 | { 170 | parent::tearDown(); 171 | 172 | $this->getTableLocator()->clear(); 173 | $Folder = new Folder($this->testPath); 174 | $Folder->delete(); 175 | } 176 | 177 | /** 178 | * Creates a file 179 | * 180 | * @param string $file File path and name, relative to FileStorageTestCase::$testPath 181 | * 182 | * @return string 183 | */ 184 | protected function _createMockFile(string $file): string 185 | { 186 | if (DS === '/') { 187 | $file = str_replace('\\', DS, $file); 188 | } else { 189 | $file = str_replace('/', DS, $file); 190 | } 191 | 192 | $path = dirname($file); 193 | if (!is_dir($this->testPath . $path)) { 194 | mkdir($this->testPath . $path, 0777, true); 195 | } 196 | 197 | if (!file_exists($this->testPath . $file)) { 198 | touch($this->testPath . $file); 199 | } 200 | 201 | return $this->testPath . $file; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/TestCase/FileStorageTestTable.php: -------------------------------------------------------------------------------- 1 | addColumn('variants', 'json'); 23 | $schema->addColumn('metadata', 'json'); 24 | 25 | return parent::_initializeSchema($schema); 26 | } 27 | 28 | /** 29 | * Initialize 30 | * 31 | * @param array $config 32 | * 33 | * @return void 34 | */ 35 | public function initialize(array $config): void 36 | { 37 | parent::initialize($config); 38 | $this->setTable('file_storage'); 39 | $this->setAlias('FileStorage'); 40 | $this->setEntityClass(FileStorage::class); 41 | $this->setDisplayField('filename'); 42 | 43 | $this->addBehavior( 44 | 'Burzum/FileStorage.FileStorage', 45 | Configure::read('FileStorage.behaviorConfig') 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php: -------------------------------------------------------------------------------- 1 | getTableLocator()->clear(); 46 | $this->FileStorage = $this->getTableLocator()->get(FileStorageTestTable::class); 47 | 48 | $this->FileStorage->addBehavior( 49 | 'Burzum/FileStorage.FileStorage', 50 | Configure::read('FileStorage.behaviorConfig') 51 | ); 52 | 53 | $this->testFilePath = Plugin::path('Burzum/FileStorage') . 'Test' . DS . 'Fixture' . DS . 'File' . DS; 54 | } 55 | 56 | /** 57 | * endTest 58 | * 59 | * @return void 60 | */ 61 | public function tearDown(): void 62 | { 63 | parent::tearDown(); 64 | unset($this->FileStorage); 65 | $this->getTableLocator()->clear(); 66 | } 67 | 68 | /** 69 | * testAfterDelete 70 | * 71 | * @return void 72 | */ 73 | public function testAfterDelete() 74 | { 75 | $file = $this->_createMockFile('/Item/00/14/90/filestorage1/filestorage1.png'); 76 | $this->assertFileExists($file); 77 | 78 | $entity = $this->FileStorage->get('file-storage-1'); 79 | $entity->adapter = 'Local'; 80 | $entity->path = '/Item/00/14/90/filestorage1/filestorage1.png'; 81 | 82 | $event = new Event('FileStorage.afterDelete', $this->FileStorage, [ 83 | 'entity' => $entity, 84 | 'adapter' => 'Local', 85 | ]); 86 | 87 | $this->FileStorage->behaviors()->FileStorage->afterDelete( 88 | $event, 89 | $entity, 90 | new ArrayObject([]) 91 | ); 92 | 93 | $this->assertFileNotExists($file); 94 | } 95 | 96 | /** 97 | * testBeforeSave 98 | * 99 | * @return void 100 | */ 101 | public function testBeforeSave() 102 | { 103 | $file = new UploadedFile( 104 | $this->fileFixtures . 'titus.jpg', 105 | filesize($this->fileFixtures . 'titus.jpg'), 106 | UPLOAD_ERR_OK, 107 | 'titus.png', 108 | 'image/jpeg' 109 | ); 110 | 111 | $entity = $this->FileStorage->newEntity([ 112 | 'file' => $file, 113 | ], [ 114 | 'accessibleFields' => ['*' => true], 115 | ]); 116 | 117 | $event = new Event('Model.beforeSave', $this->FileStorage, [ 118 | 'entity' => $entity, 119 | ]); 120 | 121 | $this->FileStorage->behaviors()->FileStorage->beforeSave($event, $entity, new ArrayObject([])); 122 | 123 | $this->assertSame($entity->adapter, 'Local'); 124 | $this->assertSame($entity->filesize, 332643); 125 | $this->assertSame($entity->mime_type, 'image/jpeg'); 126 | $this->assertSame($entity->model, 'file_storage'); 127 | } 128 | 129 | /** 130 | * @return void 131 | */ 132 | public function testBeforeSaveArray() 133 | { 134 | $entity = $this->FileStorage->newEntity([ 135 | 'file' => [ 136 | 'error' => UPLOAD_ERR_OK, 137 | 'tmp_name' => $this->fileFixtures . 'titus.jpg', 138 | 'size' => filesize($this->fileFixtures . 'titus.jpg'), 139 | 'name' => 'titus.png', 140 | 'type' => 'image/jpeg', 141 | ], 142 | ], [ 143 | 'accessibleFields' => ['*' => true], 144 | ]); 145 | 146 | $event = new Event('Model.beforeSave', $this->FileStorage, [ 147 | 'entity' => $entity, 148 | ]); 149 | 150 | $this->FileStorage->behaviors()->FileStorage->beforeSave($event, $entity, new ArrayObject([])); 151 | 152 | $this->assertSame($entity->adapter, 'Local'); 153 | $this->assertSame($entity->filesize, 332643); 154 | $this->assertSame($entity->mime_type, 'image/jpeg'); 155 | $this->assertSame($entity->model, 'file_storage'); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Entity/FileStorageTest.php: -------------------------------------------------------------------------------- 1 | getVariantUrl('nonexistent'); 27 | $this->assertNull($result); 28 | } 29 | 30 | /** 31 | * @return void 32 | */ 33 | public function testGetVariantPath(): void 34 | { 35 | $fileStorage = new FileStorage(); 36 | 37 | $result = $fileStorage->getVariantPath('nonexistent'); 38 | $this->assertNull($result); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/FileStorageTableTest.php: -------------------------------------------------------------------------------- 1 | FileStorage); 28 | $this->getTableLocator()->clear(); 29 | } 30 | 31 | /** 32 | * testInitialization 33 | * 34 | * @return void 35 | */ 36 | public function testInitialize() 37 | { 38 | $this->assertEquals($this->FileStorage->getTable(), 'file_storage'); 39 | $this->assertEquals($this->FileStorage->getDisplayField(), 'filename'); 40 | } 41 | 42 | /** 43 | * Testing a complete save call 44 | * 45 | * @link https://github.com/burzum/cakephp-file-storage/issues/85 46 | * 47 | * @return void 48 | */ 49 | public function testFileSaving() 50 | { 51 | $entity = $this->FileStorage->newEntity([ 52 | 'model' => 'Document', 53 | 'adapter' => 'Local', 54 | 'file' => new UploadedFile( 55 | $this->fileFixtures . 'titus.jpg', 56 | filesize($this->fileFixtures . 'titus.jpg'), 57 | UPLOAD_ERR_OK, 58 | 'tituts.jpg', 59 | 'image/jpeg' 60 | ), 61 | ], ['accessibleFields' => ['*' => true]]); 62 | $this->assertSame([], $entity->getErrors()); 63 | 64 | $this->FileStorage->saveOrFail($entity); 65 | } 66 | 67 | /** 68 | * Testing a complete save call 69 | * 70 | * @link https://github.com/burzum/cakephp-file-storage/issues/85 71 | * 72 | * @return void 73 | */ 74 | public function testFileSavingArray() 75 | { 76 | $entity = $this->FileStorage->newEntity([ 77 | 'model' => 'Document', 78 | 'adapter' => 'Local', 79 | 'file' => [ 80 | 'error' => UPLOAD_ERR_OK, 81 | 'size' => filesize($this->fileFixtures . 'titus.jpg'), 82 | 'type' => 'image/jpeg', 83 | 'name' => 'tituts.jpg', 84 | 'tmp_name' => $this->fileFixtures . 'titus.jpg', 85 | ], 86 | ], ['accessibleFields' => ['*' => true]]); 87 | $this->assertSame([], $entity->getErrors()); 88 | 89 | $this->FileStorage->saveOrFail($entity); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/ImageHelperTest.php: -------------------------------------------------------------------------------- 1 | view = new View($null); 48 | $this->helper = new ImageHelper($this->view); 49 | $this->helper->Html = new HtmlHelper($this->view); 50 | 51 | $request = (new Request(['url' => 'contacts/add'])) 52 | ->withAttribute('webroot', '/') 53 | ->withAttribute('base', '/'); 54 | 55 | $this->helper->Html->getView()->setRequest($request); 56 | } 57 | 58 | /** 59 | * End Test 60 | * 61 | * @return void 62 | */ 63 | public function tearDown(): void 64 | { 65 | parent::tearDown(); 66 | unset($this->helper); 67 | } 68 | 69 | /** 70 | * testImageUrl 71 | * 72 | * @return void 73 | */ 74 | public function testImageUrl() 75 | { 76 | $image = $this->FileStorage->newEntity([ 77 | 'id' => 'e479b480-f60b-11e1-a21f-0800200c9a66', 78 | 'filename' => 'testimage.jpg', 79 | 'model' => 'Test', 80 | 'path' => 'test/path/testimage.jpg', 81 | 'extension' => 'jpg', 82 | 'adapter' => 'Local', 83 | 'variants' => [ 84 | 't150' => [ 85 | 'path' => 'test/path/testimage.c3f33c2a.jpg', 86 | 'url' => '', 87 | ], 88 | ], 89 | ], ['accessibleFields' => ['*' => true]]); 90 | 91 | $result = $this->helper->imageUrl($image, 't150', ['pathPrefix' => '/src/']); 92 | $this->assertEquals('/src/test/path/testimage.c3f33c2a.jpg', $result); 93 | 94 | $result = $this->helper->imageUrl($image, null, ['pathPrefix' => '/src/']); 95 | $this->assertEquals('/src/test/path/testimage.jpg', $result); 96 | } 97 | 98 | /** 99 | * testImage 100 | * 101 | * @return void 102 | */ 103 | public function testImageUrlInvalidArgumentException() 104 | { 105 | $this->expectException(VariantDoesNotExistException::class); 106 | $image = $this->FileStorage->newEntity([ 107 | 'id' => 'e479b480-f60b-11e1-a21f-0800200c9a66', 108 | 'filename' => 'testimage.jpg', 109 | 'model' => 'Test', 110 | 'path' => 'test/path/', 111 | 'extension' => 'jpg', 112 | 'adapter' => 'Local', 113 | ], ['accessibleFields' => ['*' => true]]); 114 | 115 | $this->helper->imageUrl($image, 'invalid-version!'); 116 | } 117 | 118 | /** 119 | * testFallbackImage 120 | * 121 | * @return void 122 | */ 123 | public function testFallbackImage() 124 | { 125 | Configure::write('Media.fallbackImages.Test.t150', 't150fallback.png'); 126 | 127 | $result = $this->helper->fallbackImage(['fallback' => true], 't150'); 128 | $this->assertSame('', $result); 129 | 130 | $result = $this->helper->fallbackImage(['fallback' => 'something.png'], 't150'); 131 | $this->assertSame('', $result); 132 | 133 | $result = $this->helper->fallbackImage([], 't150'); 134 | $this->assertSame('', $result); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | setPsr4('Cake\\', './vendor/cakephp/cakephp/src'); 31 | $loader->setPsr4('Cake\Test\\', './vendor/cakephp/cakephp/tests'); 32 | $loader->setPsr4('Burzum\Imagine\\', './vendor/burzum/cakephp-imagine-plugin/src'); 33 | 34 | $config = [ 35 | 'path' => dirname(__FILE__, 2) . DS, 36 | ]; 37 | Plugin::getCollection()->add(new \Burzum\FileStorage\Plugin($config)); 38 | --------------------------------------------------------------------------------