├── .github └── workflows │ └── main.yml ├── .gitignore ├── .php-cs-fixer.php ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Commands │ ├── SetCORSHeaders.php │ └── SetTempUrlKey.php ├── Composer │ └── Scripts.php ├── OVHConfiguration.php ├── OVHServiceProvider.php └── OVHSwiftAdapter.php └── tests ├── Functional ├── ExpiringObjectsTest.php └── UrlGenerationTest.php └── TestCase.php /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI laravel-ovh 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: [8.1, '8.2'] 15 | illuminate: ['10.*'] 16 | dependency-version: [prefer-stable] 17 | 18 | name: P${{ matrix.php }} - L${{ matrix.illuminate }} - ${{ matrix.dependency-version }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Detect Composer Cache Directory 25 | id: composer-cache 26 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 27 | 28 | - name: Setup Composer Cache 29 | uses: actions/cache@v2 30 | with: 31 | path: ${{ steps.composer-cache.outputs.dir }} 32 | key: dependencies-illuminate-${{ matrix.illuminate }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | coverage: none 39 | 40 | - name: Install Composer Packages 41 | run: | 42 | composer require "illuminate/support:${{ matrix.illuminate }}" --no-interaction --no-update 43 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 44 | 45 | - name: Execute PHP Linter 46 | run: composer test:lint 47 | 48 | - name: Execute PHPUnit Tests 49 | run: composer test:unit 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | laravel-ovh.sublime-project 4 | laravel-ovh.sublime-workspace 5 | composer.lock 6 | .idea/ 7 | cghooks.lock 8 | .php_cs.cache 9 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'operators' => ['=>' => null] 9 | ], 10 | 'blank_line_after_namespace' => true, 11 | 'blank_line_after_opening_tag' => true, 12 | 'blank_line_before_statement' => true, 13 | 'braces' => true, 14 | 'cast_spaces' => true, 15 | 'class_definition' => true, 16 | 'concat_space' => true, 17 | 'declare_equal_normalize' => true, 18 | 'elseif' => true, 19 | 'encoding' => true, 20 | 'full_opening_tag' => true, 21 | 'function_declaration' => true, 22 | 'function_typehint_space' => true, 23 | 'single_line_comment_style' => [ 24 | 'comment_types' => ['hash'] 25 | ], 26 | 'heredoc_to_nowdoc' => true, 27 | 'include' => true, 28 | 'indentation_type' => true, 29 | 'linebreak_after_opening_tag' => true, 30 | 'lowercase_cast' => true, 31 | 'constant_case' => ['case' => 'lower'], 32 | 'lowercase_keywords' => true, 33 | 'magic_constant_casing' => true, 34 | 'method_argument_space' => true, 35 | 'class_attributes_separation' => true, 36 | 'visibility_required' => true, 37 | 'native_function_casing' => true, 38 | 'no_alias_functions' => true, 39 | 'no_blank_lines_after_class_opening' => true, 40 | 'no_blank_lines_after_phpdoc' => true, 41 | 'no_closing_tag' => true, 42 | 'no_empty_phpdoc' => true, 43 | 'no_empty_statement' => true, 44 | 'no_extra_blank_lines' => true, 45 | 'no_leading_import_slash' => true, 46 | 'no_leading_namespace_whitespace' => true, 47 | 'no_multiline_whitespace_around_double_arrow' => true, 48 | 'multiline_whitespace_before_semicolons' => true, 49 | 'no_short_bool_cast' => true, 50 | 'no_singleline_whitespace_before_semicolons' => true, 51 | 'no_spaces_after_function_name' => true, 52 | 'no_spaces_around_offset' => true, 53 | 'no_spaces_inside_parenthesis' => true, 54 | 'no_trailing_comma_in_list_call' => true, 55 | 'no_trailing_comma_in_singleline_array' => true, 56 | 'no_trailing_whitespace' => true, 57 | 'no_trailing_whitespace_in_comment' => true, 58 | 'no_unneeded_control_parentheses' => true, 59 | 'no_unreachable_default_argument_value' => true, 60 | 'no_useless_return' => true, 61 | 'no_whitespace_before_comma_in_array' => true, 62 | 'no_whitespace_in_blank_line' => true, 63 | 'normalize_index_brace' => true, 64 | 'not_operator_with_successor_space' => false, 65 | 'object_operator_without_whitespace' => true, 66 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 67 | 'phpdoc_indent' => true, 68 | 'general_phpdoc_tag_rename' => true, 69 | 'phpdoc_inline_tag_normalizer' => true, 70 | 'phpdoc_tag_type' => true, 71 | 'phpdoc_no_access' => true, 72 | 'phpdoc_no_package' => true, 73 | 'phpdoc_no_useless_inheritdoc' => true, 74 | 'phpdoc_scalar' => true, 75 | 'phpdoc_single_line_var_spacing' => true, 76 | 'phpdoc_summary' => true, 77 | 'phpdoc_to_comment' => true, 78 | 'phpdoc_trim' => true, 79 | 'phpdoc_types' => true, 80 | 'phpdoc_var_without_name' => true, 81 | 'increment_style' => ['style' => 'post'], 82 | 'no_mixed_echo_print' => true, 83 | 'psr_autoloading' => true, 84 | 'self_accessor' => true, 85 | 'array_syntax' => ['syntax' => 'short'], 86 | 'short_scalar_cast' => true, 87 | 'simplified_null_return' => false, 88 | 'single_blank_line_at_eof' => true, 89 | 'single_blank_line_before_namespace' => true, 90 | 'single_class_element_per_statement' => true, 91 | 'single_import_per_statement' => true, 92 | 'single_line_after_imports' => true, 93 | 'single_quote' => true, 94 | 'space_after_semicolon' => true, 95 | 'standardize_not_equals' => true, 96 | 'switch_case_semicolon_to_colon' => true, 97 | 'switch_case_space' => true, 98 | 'ternary_operator_spaces' => true, 99 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 100 | 'trim_array_spaces' => true, 101 | 'unary_operator_spaces' => true, 102 | 'line_ending' => true, 103 | 'whitespace_after_comma_in_array' => true, 104 | 'lowercase_static_reference' => true, // added from Symfony 105 | 'magic_method_casing' => true, // added from Symfony 106 | 'fully_qualified_strict_types' => true, // added, 107 | 'no_unused_imports' => true, 108 | ]; 109 | 110 | $finder = Finder::create() 111 | ->notPath('bootstrap') 112 | ->notPath('storage') 113 | ->notPath('vendor') 114 | ->in(getcwd()) 115 | ->name('*.php') 116 | ->notName('*.blade.php') 117 | ->notName('index.php') 118 | ->notName('server.php') 119 | ->notName('_ide_helper.php') 120 | ->ignoreDotFiles(true) 121 | ->ignoreVCS(true); 122 | 123 | return (new Config) 124 | ->setFinder($finder) 125 | ->setRules($rules) 126 | ->setRiskyAllowed(true) 127 | ->setUsingCache(true); 128 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | 4 | checks: 5 | php: 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 sausin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel OVH Object Storage driver 2 | 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/sausin/laravel-ovh.svg?style=flat-square)](https://packagist.org/packages/sausin/laravel-ovh) 5 | [![Continuous Integration](https://github.com/sausin/laravel-ovh/workflows/CI%20laravel-ovh/badge.svg?branch=master)](https://github.com/sausin/laravel-ovh/actions?query=workflow%3A%22CI+laravel-ovh%22) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/sausin/laravel-ovh.svg?style=flat-square)](https://scrutinizer-ci.com/g/sausin/laravel-ovh) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/sausin/laravel-ovh.svg?style=flat-square)](https://packagist.org/packages/sausin/laravel-ovh) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) 9 | 10 | 11 | Laravel `Storage` facade provides support for many different filesystems. 12 | 13 | This is a wrapper to provide support in Laravel for [OVH Object Storage](https://www.ovh.ie/public-cloud/storage/object-storage/). 14 | 15 | # Installation 16 | 17 | Install via composer: 18 | ``` 19 | composer require sausin/laravel-ovh 20 | ``` 21 | 22 | Please see below for the details on various branches. You can choose the version of the package which is suitable for your development. 23 | Also, take note of the upgrade. 24 | 25 | | Package version | PHP compatibility | Laravel versions | Special features of OVH | Status | 26 | | --------------- | :---------------: | :--------------: | :---------------------------------------: | :--------- | 27 | | `1.2.x` | `^7.0 - ^7.1` | `>=5.4`, `<=5.8` | Temporary Url Support | Deprecated | 28 | | `2.x` | `>=7.1` | `>=5.4`, `<=6.x` | Above + Expiring Objects + Custom Domains | Deprecated | 29 | | `3.x` | `>=7.1` | `>=5.4`, `<=7.x` | Above + Keystone v3 API | Deprecated | 30 | | `4.x` | `>=7.2` | `>=5.4` | Above + Set private key on container | Deprecated | 31 | | `5.x` | `>=7.4` | `>=5.8` | Above + Config-based Expiring Objects + Form Post Signature + Prefix | Maintained | 32 | | `6.x` | `>=7.4` | `>=7.x` | PHP 8 support | Active | 33 | | `7.x` | `>=8.0` | `>=9.x` | Laravel 9+ support | Active | 34 | 35 | If you are using Laravel versions older than 5.5, add the service provider to the `providers` array in `config/app.php`: 36 | ```php 37 | Sausin\LaravelOvh\OVHServiceProvider::class 38 | ``` 39 | 40 | Define the ovh driver in the `config/filesystems.php` 41 | as below 42 | ```php 43 | 'ovh' => [ 44 | 'driver' => 'ovh', 45 | 'authUrl' => env('OS_AUTH_URL', 'https://auth.cloud.ovh.net/v3/'), 46 | 'projectId' => env('OS_PROJECT_ID'), 47 | 'region' => env('OS_REGION_NAME'), 48 | 'userDomain' => env('OS_USER_DOMAIN_NAME', 'Default'), 49 | 'username' => env('OS_USERNAME'), 50 | 'password' => env('OS_PASSWORD'), 51 | 'containerName' => env('OS_CONTAINER_NAME'), 52 | 53 | // Since v1.2 54 | // Optional variable and only if you are using temporary signed urls. 55 | // You can also set a new key using the command 'php artisan ovh:set-temp-url-key'. 56 | 'tempUrlKey' => env('OS_TEMP_URL_KEY'), 57 | 58 | // Since v2.1 59 | // Optional variable and only if you have setup a custom endpoint. 60 | 'endpoint' => env('OS_CUSTOM_ENDPOINT'), 61 | 62 | // Optional variables for handling large objects. 63 | // Defaults below are 300MB threshold & 100MB segments. 64 | 'swiftLargeObjectThreshold' => env('OS_LARGE_OBJECT_THRESHOLD', 300 * 1024 * 1024), 65 | 'swiftSegmentSize' => env('OS_SEGMENT_SIZE', 100 * 1024 * 1024), 66 | 'swiftSegmentContainer' => env('OS_SEGMENT_CONTAINER', null), 67 | 68 | // Optional variable and only if you would like to DELETE all uploaded object by DEFAULT. 69 | // This allows you to set an 'expiration' time for every new uploaded object to 70 | // your container. This will not affect objects already in your container. 71 | // 72 | // If you're not willing to DELETE uploaded objects by DEFAULT, leave it empty. 73 | // Really, if you don't know what you're doing, you should leave this empty as well. 74 | 'deleteAfter' => env('OS_DEFAULT_DELETE_AFTER', null), 75 | 76 | // Optional variable to cache your storage objects in memory 77 | // You must require league/flysystem-cached-adapter to enable caching 78 | // This option is not available on laravel-ovh >= 7.0 79 | 'cache' => true, // Defaults to false 80 | 81 | // Optional variable to set a prefix on all paths 82 | 'prefix' => null, 83 | ], 84 | ``` 85 | Define the correct env variables above in your .env file (to correspond to the values above), 86 | and you should now have a working OVH Object Storage setup :smile:. 87 | 88 | The environment variable `OS_AUTH_URL` is normally not going to be any different for OVH users and hence doesn't need to 89 | be specified. To get the values for remaining variables (like `OS_USERNAME`, `OS_REGION_NAME`, `OS_CONTAINER_NAME`, 90 | etc...), you can download the configuration file with details from OVH's Horizon or Control Panel: 91 | - **OVH Control Panel**: `Public cloud -> Project Management -> Users & Roles -> Download Openstack's RC file` 92 | - **OVH Horizon**: `Project -> API Access -> Download OpenStack RC File -> Identity API v3` 93 | 94 | Be sure to clear your app's config cache after finishing this library's configuration: 95 | ```sh 96 | php artisan config:cache 97 | ``` 98 | 99 | **NOTE**: Downloading your RC config file from **OVH Control Panel** will provide **Identity v2** variable names. 100 | However, for this package, the following variables are equivalent: 101 | 102 | | `laravel-ovh` variable name | OVH's RC variable name | 103 | | --------------------------- | ---------------------- | 104 | | `OS_PROJECT_ID` | `OS_TENANT_ID` | 105 | | `OS_PROJECT_NAME` | `OS_TENANT_NAME` | 106 | 107 | You can safely place the values from the Identity v2 variables and place them in the corresponding variable for this package. 108 | 109 | ## Upgrade Notes 110 | 111 | ### From 3.x to 4.x 112 | Starting with `4.x` branch, the variables to be defined in the `.env` file 113 | have been renamed to reflect the names used by OpenStack in their configuration file. This is to 114 | remove any discrepancy in understanding which variable should go where. This also means that 115 | the package might fail to work unless the variable names in the `.env` file are updated. 116 | 117 | ### From 4.x to 5.x 118 | Starting with `5.x` branch, the variables to be defined in the `config/filesystems.php` 119 | file have been renamed to better correspond with the names used by OpenStack in their configuration file. This 120 | is intended to give the developer a better understanding of the contents of each configuration 121 | key. If you're coming from `3.x`, updating the variable names in the `.env` might be essential to prevent failure. 122 | 123 | ### From 5.x/6.x to 7.x 124 | Starting with `7.x` branch, only Laravel 9 and PHP 8 are supported. The `cache` option should be removed from 125 | your config if you previously used it since Flysystem no longer supports "cached adapters". 126 | 127 | # Usage 128 | 129 | Refer to the extensive [Laravel Storage Documentation](https://laravel.com/docs/filesystem) for usage guidelines. 130 | 131 | **NOTE:** This package includes support for the following additional methods: 132 | ```php 133 | Storage::url() 134 | Storage::temporaryUrl() 135 | ``` 136 | 137 | The `temporaryUrl()` method is relevant for private containers where files are not publicly accessible 138 | under normal conditions. This generates a temporary signed url. For more details, please refer 139 | to [OVH's Temporary URL Documentation](https://docs.ovh.com/gb/en/public-cloud/share_an_object_via_a_temporary_url/). 140 | 141 | Remember that this functionality requires the container to have a proper key stored. 142 | The key in the header should match the `tempUrlKey` specified in `config/filesystems.php`. 143 | For more details on how to set up the header on your OVH container, please refer to 144 | [Generate the temporary address (_tempurl_)](https://docs.ovh.com/gb/en/public-cloud/share_an_object_via_a_temporary_url/#generate-the-temporary-address-tempurl). 145 | 146 | Alternatively, since version 4.x you can use the following commands: 147 | ```sh 148 | # Automatically generate a key 149 | php artisan ovh:set-temp-url-key 150 | 151 | # Generate a key for a specific disk 152 | php artisan ovh:set-temp-url-key --disk="other-ovh-disk" 153 | 154 | # Set a specific key 155 | php artisan ovh:set-temp-url-key --key=your-private-key 156 | ``` 157 | The package will then set the relevant key on your container and present it to you. If a key 158 | has already been set up previously, the package will warn you before overriding the existing 159 | key. If you'd like to force a new key anyway, you may use the `--force` flag with the command. 160 | 161 | Once you got your key configured in your container, you must add it to your `.env` file: 162 | ```dotenv 163 | OS_TEMP_URL_KEY='your-private-key' 164 | ``` 165 | 166 | ## Configuring a Custom Domain Name (Custom Endpoint) 167 | 168 | OVH's Object Storage allows you to point a Custom Domain Name or Endpoint to an individual 169 | container. For this, you must setup some records with your DNS provider, which will authorize 170 | the forwarded requests coming from your Endpoint to OVH's servers. 171 | 172 | In order to use a Custom Domain Name, you must specify it in your `.env` file: 173 | ```dotenv 174 | OS_CUSTOM_ENDPOINT="http://my-endpoint.example.com" 175 | ``` 176 | 177 | For more information, please refer to [OVH's Custom Domain Documentation](https://docs.ovh.com/gb/en/storage/pcs/link-domain/). 178 | 179 | ## Uploading Automatically Expiring Objects 180 | 181 | This library allows you to add expiration time to uploaded objects. There are 2 ways to do it: 182 | 183 | 1. Specifying expiration time programmatically: 184 | - You can either specify the number of seconds after which the uploaded object should be deleted: 185 | ```php 186 | // Automatically expire after 1 hour of being uploaded. 187 | Storage::disk('ovh')->put('path/to/file.jpg', $contents, ['deleteAfter' => 60*60]); 188 | ``` 189 | - Or, you can also specify a timestamp after which the uploaded object should be deleted: 190 | ```php 191 | // Automatically delete at the beginning of next month. 192 | Storage::disk('ovh')->put('path/to/file.jpg', $contents, ['deleteAt' => now()->addMonth()->startOfMonth()]) 193 | ``` 194 | 195 | 2. Specifying default expiration time via `.env` file. This will set an expiration time (in seconds) 196 | to every newly uploaded object by default: 197 | ```dotenv 198 | # Delete every object after 3 days of being uploaded 199 | OS_DELETE_AFTER=259200 200 | ``` 201 | 202 | For more information about these variables, please refer to 203 | [OVH's Automatic Object Deletion Documentation](https://docs.ovh.com/gb/en/storage/configure_automatic_object_deletion/) 204 | 205 | ## Large Object Support 206 | 207 | This library can help you optimize the upload speeds of large objects (such as videos or disk images) 208 | automatically by detecting file size thresholds and splitting the file into lighter segments. This will 209 | improve upload speeds by writing multiple segments into multiple Object Storage nodes simultaneously. 210 | 211 | By default, the size threshold to detect a Large Object is set to 300MB, and the segment size to split 212 | the file is set to 100MB. If you would like to change these values, you must specify the following 213 | variables in your `.env` file (in Bytes): 214 | ```dotenv 215 | # Set size threshold to 1GB 216 | OS_LARGE_OBJECT_THRESHOLD=1073741824 217 | # Set segment size to 200MB 218 | OS_SEGMENT_SIZE=209715200 219 | ``` 220 | 221 | If you would like to use a separate container for storing your Large Object Segments, 222 | you can do so by specifing the following variable in your `.env` file: 223 | ```dotenv 224 | OS_SEGMENT_CONTAINER="large-object-container-name" 225 | ``` 226 | 227 | Using a separate container for storing the segments of your Large Objects can be beneficial in 228 | some cases, to learn more about this, please refer to 229 | [OpenStack's Last Note on Using Swift for Large Objects](https://docs.openstack.org/swift/stein/overview_large_objects.html#using-swift) 230 | 231 | To learn more about segmented uploads for large objects, please refer to: 232 | - [OVH's Optimizing Large Object Uploads Documentation](https://docs.ovh.com/gb/en/storage/optimised_method_for_uploading_files_to_object_storage/) 233 | - [OpenStack's Large Object Support Documentation](https://docs.openstack.org/swift/latest/overview_large_objects.html) 234 | 235 | ## Form Post Middleware 236 | 237 | While this feature in not documented by the OVH team, it's explained in the 238 | [OpenStack's Documentation](https://docs.openstack.org/swift/latest/api/form_post_middleware.html). 239 | 240 | This feature allows for uploading of files _directly_ to the OVH servers rather than going through the application servers 241 | (thus improving the efficiency in the upload cycle). 242 | 243 | You must generate a valid FormPost signature, for which you can use the following function: 244 | ```php 245 | Storage::disk('ovh')->getAdapter()->getFormPostSignature($path, $expiresAt, $redirect, $maxFileCount, $maxFileSize); 246 | ``` 247 | Where: 248 | - `$path` is the directory path in which you would like to store the files. 249 | - `$expiresAt` is a `DateTimeInterface` object that specifies a date in which the FormPost signature will expire. 250 | - `$redirect` is the URL to which the user will be redirected once all files finish uploading. Defaults to `null` to prevent redirects. 251 | - `$maxFileCount` is the max quantity of files that the user will be able to upload using the signature. Defaults to `1` file. 252 | - `$maxFileSize` is the size limit that each uploaded file can have. Defaults to `25 MB` (`25*1024*1024`). 253 | 254 | After obtaining the signature, you need to pass the signature data to your HTML form: 255 | ```blade 256 |
257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 |
266 | ``` 267 | 268 | > **NOTE**: The upload method in the form _must_ be type of `POST`. 269 | 270 | > **NOTE**: As this will be a cross origin request, appropriate headers are needed on the container. See the use of command `php artisan ovh:set-cors-headers` further. 271 | 272 | The `$url` variable refers to the path URL to your container, you can get it by passing the path to the adapter `getUrl`: 273 | ```php 274 | $url = Storage::disk('ovh')->getAdapter()->getUrl($path); 275 | ``` 276 | 277 | > **NOTE**: If you've setup a custom domain for your Object Storage container, you can use that domain (along with the corresponding path) 278 | > to upload your files without exposing your OVH's URL scheme. 279 | 280 | ### Examples 281 | 282 | ```php 283 | // Generate a signature that allows an upload to the 'images' directory for the next 10 minutes. 284 | Storage::disk('ovh')->getAdapter()->getFormPostSignature('images', now()->addMinutes(10)); 285 | 286 | // Generate a signature that redirects to a url after successful file upload to the root of the container. 287 | Storage::disk('ovh')->getAdapter()->getFormPostSignature('', now()->addMinutes(5), route('file-uploaded')); 288 | 289 | // Generate a signature that allows upload of 3 files until next day. 290 | Storage::disk('ovh')->getAdapter()->getFormPostSignature('', now()->addDay(), null, 3); 291 | 292 | // Generate a signature that allows to upload 1 file of 1GB until the next hour. 293 | Storage::disk('ovh')->getAdapter()->getFormPostSignature('', now()->addHour(), null, 1, 1 * 1024 * 1024 * 1024); 294 | ``` 295 | ## Setting up Access Control headers on the container 296 | For the setup above to work correctly, the container must have the correct headers set on it. This package provides a convenient way to set them up using the below command 297 | ```php 298 | php artisan ovh:set-cors-headers 299 | ``` 300 | By default this will allow all origins to be able to upload on the container. However, if you would like to allow only specific origin(s) you may use the `--origins` flag. 301 | 302 | If these headers were already set previously, the command will seek confirmation before overriding the existing headers. 303 | 304 | ## Prefix & Multi-tenancy 305 | 306 | As noted above, `prefix` parameter was introduced in release 5.3.0. This means that any path specified when using the package will be prefixed with the given string. Nothing is added by default (or if the parameter has not been set at all). 307 | 308 | For example, when `prefix` has been set as `foo` in the config, the following command: 309 | ```php 310 | Storage::disk('ovh')->url('/'); 311 | ``` 312 | will generate a url as if it was requested with a path of `/foo` (i.e. the specified prefix has been used). 313 | 314 | This is particularly powerful in a multi-tenant setup. The same container can be used for all tenants and yet each tenant can have its own folder, almost automatically. The middleware where the tenant is being set can be updated, and using the below command: 315 | ```php 316 | Config::set('filesystems.disks.ovh.prefix', 'someprefixvalue') 317 | ``` 318 | a separate custom prefix will be set for each tenant! 319 | 320 | Both examples above assume the disk has been named as `ovh` in the config. Replace with the correct name for your case. 321 | 322 | # Credits 323 | - ThePHPLeague for the awesome [Flysystem](https://github.com/thephpleague/flysystem)! 324 | - [Chris Harvey](https://github.com/chrisnharvey) for the [Flysystem OpenStack SwiftAdapter](https://github.com/nimbusoftltd/flysystem-openstack-swift). 325 | - Rackspace for maintaining the [PHP OpenStack Repo](https://github.com/php-opencloud/openstack). 326 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sausin/laravel-ovh", 3 | "description": "OVH Object Storage driver for laravel", 4 | "keywords": [ 5 | "filesystem", 6 | "filesystems", 7 | "files", 8 | "storage", 9 | "flysystem", 10 | "openstack", 11 | "opencloud", 12 | "swift", 13 | "ovh", 14 | "laravel", 15 | "driver" 16 | ], 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Saurabh Singhvi", 21 | "email": "saurabh.singhvi@gmail.com" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-4": { 26 | "Sausin\\LaravelOvh\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Sausin\\LaravelOvh\\Tests\\": "tests/" 32 | } 33 | }, 34 | "require": { 35 | "php": "^8.1", 36 | "illuminate/console": "^10.0|^11.0|^12.0", 37 | "illuminate/support": "^10.0|^11.0|^12.0", 38 | "nimbusoft/flysystem-openstack-swift": "^1.5" 39 | }, 40 | "require-dev": { 41 | "phpunit/phpunit": "^9.5.10|^10.5|^11.5.3", 42 | "mockery/mockery": "^1.4.4", 43 | "friendsofphp/php-cs-fixer": "^3.14", 44 | "orchestra/testbench": "^8.0|^9.0|^10.0" 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Sausin\\LaravelOvh\\OVHServiceProvider" 50 | ] 51 | }, 52 | "hooks": { 53 | "pre-commit": [ 54 | "composer lint", 55 | "git update-index --again" 56 | ], 57 | "pre-push": [ 58 | "composer test" 59 | ], 60 | "post-merge": [ 61 | "composer install" 62 | ] 63 | } 64 | }, 65 | "scripts": { 66 | "post-install-cmd": [ 67 | "Sausin\\LaravelOvh\\Composer\\Scripts::devOnly", 68 | "# cghooks add --ignore-lock" 69 | ], 70 | "post-update-cmd": [ 71 | "Sausin\\LaravelOvh\\Composer\\Scripts::devOnly", 72 | "# cghooks update" 73 | ], 74 | "lint": "php-cs-fixer fix", 75 | "test:lint": "@lint --dry-run", 76 | "test:unit": "phpunit", 77 | "test": [ 78 | "@test:lint", 79 | "@test:unit" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | ./tests/Functional 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Commands/SetCORSHeaders.php: -------------------------------------------------------------------------------- 1 | getDisk(); 54 | 55 | $adapter = Storage::disk($disk)->getAdapter(); 56 | 57 | $this->container = $adapter->getContainer(); 58 | } catch (InvalidArgumentException $e) { 59 | $this->error($e->getMessage()); 60 | 61 | return; 62 | } 63 | 64 | $this->containerMeta = $this->container->getMetadata(); 65 | 66 | if ($this->option('force') || $this->askIfShouldOverrideExistingParams()) { 67 | $this->setHeaders(); 68 | } 69 | } 70 | 71 | /** 72 | * If there's no existing Temp URL Key present in the Container, continue. 73 | * 74 | * Otherwise, if there's already an existing Temp URL Key present in the 75 | * Container, the User will be prompted to choose if we should override it 76 | * or not. 77 | * 78 | * @return bool 79 | */ 80 | protected function askIfShouldOverrideExistingParams(): bool 81 | { 82 | $metaKeys = ['Access-Control-Allow-Origin', 'Access-Control-Max-Age']; 83 | 84 | if (count(array_intersect($metaKeys, array_keys($this->containerMeta))) === 0) { 85 | return true; 86 | } 87 | 88 | return $this->confirm( 89 | 'Some CORS Meta keys are already set on the container. Do you want to override them?', 90 | false 91 | ); 92 | } 93 | 94 | /** 95 | * Updates the Temp URL Key for the Container. 96 | * 97 | * @return void 98 | */ 99 | protected function setHeaders(): void 100 | { 101 | $origins = '*'; 102 | 103 | if (count($this->option('origins')) !== 0) { 104 | $origins = implode(' ', $this->option('origins')); 105 | } 106 | 107 | $maxAge = $this->option('max-age'); 108 | $meta = ['Access-Control-Allow-Origin' => $origins, 'Access-Control-Max-Age' => $maxAge]; 109 | 110 | if (array_key_exists('Temp-Url-Key', $this->containerMeta)) { 111 | $meta += ['Temp-Url-Key' => $this->containerMeta['Temp-Url-Key']]; 112 | } 113 | 114 | try { 115 | $this->container->resetMetadata($meta); 116 | 117 | $this->info('CORS meta keys successfully set on the container'); 118 | } catch (Exception $e) { 119 | $this->error($e->getMessage()); 120 | } 121 | } 122 | 123 | /** 124 | * Check if selected disk is correct. If not, provide options to user. 125 | * 126 | * @return string 127 | */ 128 | public function getDisk(): string 129 | { 130 | $available = array_keys(array_filter(Config::get('filesystems.disks'), function ($d) { 131 | return $d['driver'] === 'ovh'; 132 | })); 133 | 134 | $selected = $this->option('disk'); 135 | 136 | if (in_array($selected, $available)) { 137 | return $selected; 138 | } 139 | 140 | return $this->choice( 141 | 'Selected disk not correct. Please choose from below options:', 142 | $available, 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Commands/SetTempUrlKey.php: -------------------------------------------------------------------------------- 1 | getDisk(); 56 | 57 | $adapter = Storage::disk($disk)->getAdapter(); 58 | 59 | $this->container = $adapter->getContainer(); 60 | } catch (InvalidArgumentException $e) { 61 | $this->error($e->getMessage()); 62 | 63 | return; 64 | } 65 | 66 | $this->containerMeta = $this->container->getMetadata(); 67 | 68 | if ($this->option('force') || $this->askIfShouldOverrideExistingKey()) { 69 | $this->setContainerKey(); 70 | } 71 | } 72 | 73 | /** 74 | * If there's no existing Temp URL Key present in the Container, continue. 75 | * 76 | * Otherwise, if there's already an existing Temp URL Key present in the 77 | * Container, the User will be prompted to choose if we should override it 78 | * or not. 79 | * 80 | * @return bool 81 | */ 82 | protected function askIfShouldOverrideExistingKey(): bool 83 | { 84 | if (!array_key_exists('Temp-Url-Key', $this->containerMeta)) { 85 | return true; // Yeah, override the non-existing key. 86 | } 87 | 88 | return $this->confirm( 89 | 'A Temp URL Key already exists in your container, would you like to override it?', 90 | false 91 | ); 92 | } 93 | 94 | /** 95 | * Generates a random Temp URL Key. 96 | * 97 | * For more details, please refer to: 98 | * - https://docs.ovh.com/gb/en/public-cloud/share_an_object_via_a_temporary_url/#generate-the-temporary-address-tempurl 99 | * 100 | * @return string 101 | */ 102 | protected function getRandomKey(): string 103 | { 104 | return hash('sha512', time()); 105 | } 106 | 107 | /** 108 | * Updates the Temp URL Key for the Container. 109 | * 110 | * @return void 111 | */ 112 | protected function setContainerKey(): void 113 | { 114 | $key = $this->option('key') ?? $this->getRandomKey(); 115 | $meta = $this->getMeta(); 116 | 117 | try { 118 | $this->container->resetMetadata($meta + ['Temp-Url-Key' => $key]); 119 | 120 | $this->info('Successfully set Temp URL Key to: '.$key); 121 | } catch (Exception $e) { 122 | $this->error($e->getMessage()); 123 | } 124 | } 125 | 126 | /** 127 | * If other meta keys exist, get them. 128 | * 129 | * @return array 130 | */ 131 | protected function getMeta(): array 132 | { 133 | $meta = []; 134 | $metaKeys = ['Access-Control-Allow-Origin', 'Access-Control-Max-Age']; 135 | 136 | foreach ($metaKeys as $key) { 137 | if (array_key_exists($key, $this->containerMeta)) { 138 | $meta += [$key => $this->containerMeta[$key]]; 139 | } 140 | } 141 | 142 | return $meta; 143 | } 144 | 145 | /** 146 | * Check if selected disk is correct. If not, provide options to user. 147 | * 148 | * @return string 149 | */ 150 | public function getDisk(): string 151 | { 152 | $available = array_keys(array_filter(Config::get('filesystems.disks'), function ($d) { 153 | return $d['driver'] === 'ovh'; 154 | })); 155 | 156 | $selected = $this->option('disk'); 157 | 158 | if (in_array($selected, $available)) { 159 | return $selected; 160 | } 161 | 162 | return $this->choice( 163 | 'Selected disk not correct. Please choose from below options:', 164 | $available, 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Composer/Scripts.php: -------------------------------------------------------------------------------- 1 | isDevMode()) { 17 | $event->stopPropagation(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OVHConfiguration.php: -------------------------------------------------------------------------------- 1 | 0) { 166 | throw new BadMethodCallException('The following keys must be provided: '.implode(', ', $missingKeys)); 167 | } 168 | 169 | return (new self()) 170 | ->setAuthUrl($config['authUrl']) 171 | ->setProjectId($config['projectId']) 172 | ->setRegion($config['region']) 173 | ->setUserDomain($config['userDomain']) 174 | ->setUsername($config['username']) 175 | ->setPassword($config['password']) 176 | ->setContainerName($config['containerName']) 177 | ->setTempUrlKey($config['tempUrlKey'] ?? null) 178 | ->setEndpoint($config['endpoint'] ?? null) 179 | ->setSwiftLargeObjectThreshold($config['swiftLargeObjectThreshold'] ?? null) 180 | ->setSwiftSegmentSize($config['swiftSegmentSize'] ?? null) 181 | ->setSwiftSegmentContainer($config['swiftSegmentContainer'] ?? null) 182 | ->setDeleteAfter($config['deleteAfter'] ?? null) 183 | ->setPrefix($config['prefix'] ?? null); 184 | } 185 | 186 | /** 187 | * @return string 188 | */ 189 | public function getAuthUrl(): string 190 | { 191 | return $this->authUrl; 192 | } 193 | 194 | /** 195 | * @param string $authUrl 196 | * @return OVHConfiguration 197 | */ 198 | public function setAuthUrl(string $authUrl): self 199 | { 200 | $this->authUrl = $authUrl; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * @return string 207 | */ 208 | public function getProjectId(): string 209 | { 210 | return $this->projectId; 211 | } 212 | 213 | /** 214 | * @param string $projectId 215 | * @return OVHConfiguration 216 | */ 217 | public function setProjectId(string $projectId): self 218 | { 219 | $this->projectId = $projectId; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * @return string 226 | */ 227 | public function getRegion(): string 228 | { 229 | return $this->region; 230 | } 231 | 232 | /** 233 | * @param string $region 234 | * @return OVHConfiguration 235 | */ 236 | public function setRegion(string $region): self 237 | { 238 | $this->region = $region; 239 | 240 | return $this; 241 | } 242 | 243 | /** 244 | * @return string 245 | */ 246 | public function getUserDomain(): string 247 | { 248 | return $this->userDomain; 249 | } 250 | 251 | /** 252 | * @param string $userDomain 253 | * @return OVHConfiguration 254 | */ 255 | public function setUserDomain(string $userDomain): self 256 | { 257 | $this->userDomain = $userDomain; 258 | 259 | return $this; 260 | } 261 | 262 | /** 263 | * @return string 264 | */ 265 | public function getUsername(): string 266 | { 267 | return $this->username; 268 | } 269 | 270 | /** 271 | * @param string $username 272 | * @return OVHConfiguration 273 | */ 274 | public function setUsername(string $username): self 275 | { 276 | $this->username = $username; 277 | 278 | return $this; 279 | } 280 | 281 | /** 282 | * @return string 283 | */ 284 | public function getPassword(): string 285 | { 286 | return $this->password; 287 | } 288 | 289 | /** 290 | * @param string $password 291 | * @return OVHConfiguration 292 | */ 293 | public function setPassword(string $password): self 294 | { 295 | $this->password = $password; 296 | 297 | return $this; 298 | } 299 | 300 | /** 301 | * @return string 302 | */ 303 | public function getContainerName(): string 304 | { 305 | return $this->containerName; 306 | } 307 | 308 | /** 309 | * @param string $containerName 310 | * @return OVHConfiguration 311 | */ 312 | public function setContainerName(string $containerName): self 313 | { 314 | $this->containerName = $containerName; 315 | 316 | return $this; 317 | } 318 | 319 | /** 320 | * @return string|null 321 | */ 322 | public function getTempUrlKey(): ?string 323 | { 324 | return $this->tempUrlKey; 325 | } 326 | 327 | /** 328 | * @param string|null $tempUrlKey 329 | * @return OVHConfiguration 330 | */ 331 | public function setTempUrlKey(?string $tempUrlKey): self 332 | { 333 | $this->tempUrlKey = $tempUrlKey; 334 | 335 | return $this; 336 | } 337 | 338 | /** 339 | * @return string|null 340 | */ 341 | public function getEndpoint(): ?string 342 | { 343 | return $this->endpoint; 344 | } 345 | 346 | /** 347 | * @param string|null $endpoint 348 | * @return OVHConfiguration 349 | */ 350 | public function setEndpoint(?string $endpoint): self 351 | { 352 | $this->endpoint = $endpoint; 353 | 354 | return $this; 355 | } 356 | 357 | /** 358 | * @return string|null 359 | */ 360 | public function getSwiftLargeObjectThreshold(): ?string 361 | { 362 | return $this->swiftLargeObjectThreshold; 363 | } 364 | 365 | /** 366 | * @param string|null $swiftLargeObjectThreshold 367 | * @return OVHConfiguration 368 | */ 369 | public function setSwiftLargeObjectThreshold(?string $swiftLargeObjectThreshold): self 370 | { 371 | $this->swiftLargeObjectThreshold = $swiftLargeObjectThreshold; 372 | 373 | return $this; 374 | } 375 | 376 | /** 377 | * @return int|null 378 | */ 379 | public function getSwiftSegmentSize(): ?int 380 | { 381 | return $this->swiftSegmentSize; 382 | } 383 | 384 | /** 385 | * @param int|null $swiftSegmentSize 386 | * @return OVHConfiguration 387 | */ 388 | public function setSwiftSegmentSize(?int $swiftSegmentSize): self 389 | { 390 | $this->swiftSegmentSize = $swiftSegmentSize; 391 | 392 | return $this; 393 | } 394 | 395 | /** 396 | * @return string|null 397 | */ 398 | public function getSwiftSegmentContainer(): ?string 399 | { 400 | return $this->swiftSegmentContainer; 401 | } 402 | 403 | /** 404 | * @param string|null $swiftSegmentContainer 405 | * @return OVHConfiguration 406 | */ 407 | public function setSwiftSegmentContainer(?string $swiftSegmentContainer): self 408 | { 409 | $this->swiftSegmentContainer = $swiftSegmentContainer; 410 | 411 | return $this; 412 | } 413 | 414 | /** 415 | * @return int|null 416 | */ 417 | public function getDeleteAfter(): ?int 418 | { 419 | return $this->deleteAfter; 420 | } 421 | 422 | /** 423 | * @param int|null $deleteAfter 424 | * @return OVHConfiguration 425 | */ 426 | public function setDeleteAfter(?int $deleteAfter): self 427 | { 428 | $this->deleteAfter = $deleteAfter; 429 | 430 | return $this; 431 | } 432 | 433 | /** 434 | * @return string|null 435 | */ 436 | public function getPrefix(): ?string 437 | { 438 | return $this->prefix; 439 | } 440 | 441 | /** 442 | * @param string|null $prefix 443 | * @return OVHConfiguration 444 | */ 445 | public function setPrefix(?string $prefix): self 446 | { 447 | $this->prefix = $prefix; 448 | 449 | return $this; 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/OVHServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureCommands(); 23 | 24 | $this->configureStorage(); 25 | } 26 | 27 | /** 28 | * Configures available commands. 29 | */ 30 | protected function configureCommands(): void 31 | { 32 | if (!$this->app->runningInConsole()) { 33 | return; 34 | } 35 | 36 | $this->commands([ 37 | Commands\SetTempUrlKey::class, 38 | Commands\SetCORSHeaders::class, 39 | ]); 40 | } 41 | 42 | /** 43 | * Configures extended filesystem storage for interaction with OVH Object Storage. 44 | */ 45 | protected function configureStorage(): void 46 | { 47 | Storage::extend('ovh', function ($app, array $config) { 48 | // Creates a Configuration instance. 49 | $this->config = OVHConfiguration::make($config); 50 | 51 | $client = $this->makeOpenStackClient(); 52 | 53 | // Get the Object Storage container. 54 | $container = $client->objectStoreV1()->getContainer($this->config->getContainerName()); 55 | 56 | $adapter = $this->makeAdapter($container); 57 | 58 | $filesystem = $this->makeFileSystem($adapter); 59 | 60 | return new FilesystemAdapter($filesystem, $adapter, $config); 61 | }); 62 | } 63 | 64 | /** 65 | * Creates an OpenStack client instance, needed for interaction with OVH OpenStack. 66 | * 67 | * @return OpenStack 68 | */ 69 | protected function makeOpenStackClient(): OpenStack 70 | { 71 | return new OpenStack([ 72 | 'authUrl' => $this->config->getAuthUrl(), 73 | 'region' => $this->config->getRegion(), 74 | 'user' => [ 75 | 'name' => $this->config->getUsername(), 76 | 'password' => $this->config->getPassword(), 77 | 'domain' => [ 78 | 'name' => $this->config->getUserDomain(), 79 | ], 80 | ], 81 | 'scope' => [ 82 | 'project' => [ 83 | 'id' => $this->config->getProjectId(), 84 | ], 85 | ], 86 | ]); 87 | } 88 | 89 | protected function makeAdapter(Container $container) : OVHSwiftAdapter 90 | { 91 | return new OVHSwiftAdapter($container, $this->config, $this->config->getPrefix()); 92 | } 93 | 94 | /** 95 | * Creates a Filesystem instance for interaction with the Object Storage. 96 | * 97 | * @param OVHSwiftAdapter $adapter 98 | * @return Filesystem 99 | */ 100 | protected function makeFileSystem(OVHSwiftAdapter $adapter): Filesystem 101 | { 102 | return new Filesystem( 103 | $adapter, 104 | array_filter([ 105 | 'swiftLargeObjectThreshold' => $this->config->getSwiftLargeObjectThreshold(), 106 | 'swiftSegmentSize' => $this->config->getSwiftSegmentSize(), 107 | 'swiftSegmentContainer' => $this->config->getSwiftSegmentContainer(), 108 | ]) 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/OVHSwiftAdapter.php: -------------------------------------------------------------------------------- 1 | config = $config; 31 | } 32 | 33 | /** 34 | * Gets the endpoint url of the bucket. 35 | * 36 | * @param string|null $path 37 | * @return string 38 | */ 39 | protected function getEndpoint(?string $path = null): string 40 | { 41 | $url = !empty($this->config->getEndpoint()) 42 | // Allows assigning custom endpoint url 43 | ? rtrim($this->config->getEndpoint(), '/').'/' 44 | // If no custom endpoint assigned, use traditional swift v1 endpoint 45 | : sprintf( 46 | 'https://storage.%s.cloud.ovh.net/v1/AUTH_%s/%s/', 47 | $this->config->getRegion(), 48 | $this->config->getProjectId(), 49 | $this->config->getContainerName() 50 | ); 51 | 52 | if (!empty($path)) { 53 | $url .= $this->prefixer->prefixPath($path); 54 | } 55 | 56 | return $url; 57 | } 58 | 59 | /** 60 | * Custom function to comply with the Storage::url() function in laravel 61 | * without checking the existence of a file (faster). 62 | * 63 | * @param string $path 64 | * @return string 65 | */ 66 | public function getUrl($path) 67 | { 68 | return $this->getEndpoint($path); 69 | } 70 | 71 | /** 72 | * Custom function to get an url with confirmed file existence. 73 | * 74 | * @param string $path 75 | * @return string 76 | * @throws UnableToReadFile|UnableToCheckFileExistence 77 | */ 78 | public function getUrlConfirm($path): string 79 | { 80 | // check if object exists 81 | if (!$this->fileExists($path)) { 82 | throw UnableToReadFile::fromLocation($path, 'File does not exist.'); 83 | } 84 | 85 | return $this->getEndpoint($path); 86 | } 87 | 88 | /** 89 | * Generate a temporary URL for private containers. 90 | * 91 | 92 | * For more information, refer to OpenStack's documentation on Temporary URL middleware: 93 | * https://docs.openstack.org/swift/stein/api/temporary_url_middleware.html 94 | * 95 | * @param string $path 96 | * @param DateTimeInterface $expiresAt 97 | * @param array $options 98 | 99 | * @return string 100 | */ 101 | public function getTemporaryUrl(string $path, DateTimeInterface $expiresAt, array $options = []): string 102 | { 103 | // Ensure Temp URL Key is provided for the Disk 104 | if (empty($this->config->getTempUrlKey())) { 105 | throw new InvalidArgumentException("No Temp URL Key provided for container '".$this->container->name."'"); 106 | } 107 | 108 | // Get the method 109 | $method = $options['method'] ?? 'GET'; 110 | 111 | // The url on the OVH host 112 | $codePath = sprintf( 113 | '/v1/AUTH_%s/%s/%s', 114 | $this->config->getProjectId(), 115 | $this->config->getContainerName(), 116 | $this->prefixer->prefixPath($path) 117 | ); 118 | 119 | // Body for the HMAC hash 120 | $body = sprintf("%s\n%s\n%s", $method, $expiresAt->getTimestamp(), $codePath); 121 | 122 | // The actual hash signature 123 | $signature = hash_hmac('sha1', $body, $this->config->getTempUrlKey()); 124 | 125 | // Return signed url 126 | return sprintf( 127 | '%s?temp_url_sig=%s&temp_url_expires=%s', 128 | $this->getEndpoint($path), 129 | $signature, 130 | $expiresAt->getTimestamp() 131 | ); 132 | } 133 | 134 | /** 135 | * Generate a FormPost signature to upload files directly to your OVH container. 136 | * 137 | * For more information, refer to OpenStack's documentation on FormPost middleware: 138 | * https://docs.openstack.org/swift/stein/api/form_post_middleware.html 139 | * 140 | * @param string $path 141 | * @param DateTimeInterface $expiresAt 142 | * @param string|null $redirect 143 | * @param int $maxFileCount 144 | * @param int $maxFileSize Defaults to 25MB (25 * 1024 * 1024) 145 | * @return string 146 | */ 147 | public function getFormPostSignature(string $path, DateTimeInterface $expiresAt, ?string $redirect = null, int $maxFileCount = 1, int $maxFileSize = 26214400): string 148 | { 149 | // Ensure Temp URL Key is provided for the Disk 150 | if (empty($this->config->getTempUrlKey())) { 151 | throw new InvalidArgumentException("No Temp URL Key provided for container '".$this->container->name."'"); 152 | } 153 | 154 | // Ensure that 'expires' timestamp is in the future 155 | if ((new DateTime()) >= $expiresAt) { 156 | throw new InvalidArgumentException('Expiration time of FormPost signature must be in the future.'); 157 | } 158 | 159 | // Ensure $path doesn't begin with a slash 160 | $path = $this->prefixer->prefixPath($path); 161 | 162 | // The url on the OVH host 163 | $codePath = sprintf( 164 | '/v1/AUTH_%s/%s/%s', 165 | $this->config->getProjectId(), 166 | $this->config->getContainerName(), 167 | $path 168 | ); 169 | 170 | // Body for the HMAC hash 171 | $body = sprintf("%s\n%s\n%s\n%s\n%s", $codePath, $redirect ?? '', $maxFileSize, $maxFileCount, $expiresAt->getTimestamp()); 172 | 173 | // The actual hash signature 174 | return hash_hmac('sha1', $body, $this->config->getTempUrlKey()); 175 | } 176 | 177 | /** 178 | * Expose the container to allow for modification to metadata. 179 | * 180 | * @return Container 181 | */ 182 | public function getContainer(): Container 183 | { 184 | return $this->container; 185 | } 186 | 187 | /** 188 | * Include support for object deletion. 189 | * 190 | * @param string $path 191 | * @param Config $config 192 | * @return array 193 | * @see SwiftAdapter 194 | */ 195 | protected function getWriteData(string $path, Config $config): array 196 | { 197 | $data = parent::getWriteData($path, $config); 198 | 199 | if (null !== $config->get('deleteAfter')) { 200 | // Apply object expiration timestamp if given 201 | $data['deleteAfter'] = $config->get('deleteAfter'); 202 | } elseif (null !== $config->get('deleteAt')) { 203 | // Apply object expiration time if given 204 | $data['deleteAt'] = $config->get('deleteAt'); 205 | } elseif (!empty($this->config->getDeleteAfter())) { 206 | // Apply default object expiration time from package config 207 | $data['deleteAfter'] = $this->config->getDeleteAfter(); 208 | } 209 | 210 | return $data; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /tests/Functional/ExpiringObjectsTest.php: -------------------------------------------------------------------------------- 1 | flySystemConfig = new Config(); 17 | } 18 | 19 | public function testCanBeDeletedAtTimestamp() 20 | { 21 | $deleteAt = new \DateTime('2012-12-21'); 22 | 23 | $this->container->shouldReceive('createObject')->once()->with([ 24 | 'name' => 'hello', 25 | 'content' => 'world', 26 | 'deleteAt' => $deleteAt->getTimestamp(), 27 | ])->andReturn($this->object); 28 | 29 | $this->flySystemConfig = $this->flySystemConfig->extend([ 30 | 'deleteAt' => $deleteAt->getTimestamp(), 31 | ]); 32 | 33 | $this->adapter->write('hello', 'world', $this->flySystemConfig); 34 | 35 | // Prevent "no assertion error", we're just checking that the deleteAt is correctly passed to the container 36 | $this->assertTrue(true); 37 | } 38 | 39 | public function testCanBeDeletedAfterSpecificTime() 40 | { 41 | $this->container->shouldReceive('createObject')->once()->with([ 42 | 'name' => 'hello', 43 | 'content' => 'world', 44 | 'deleteAfter' => 60, 45 | ])->andReturn($this->object); 46 | 47 | $this->flySystemConfig = $this->flySystemConfig->extend([ 48 | 'deleteAfter' => 60, 49 | ]); 50 | 51 | $this->adapter->write('hello', 'world', $this->flySystemConfig); 52 | 53 | // Prevent "no assertion error", we're just checking that the deleteAfter is correctly passed to the container 54 | $this->assertTrue(true); 55 | } 56 | 57 | public function testCanBeDeleteAfterSpecificTimeFromGlobalConfig() 58 | { 59 | $this->container->shouldReceive('createObject')->once()->with([ 60 | 'name' => 'hello', 61 | 'content' => 'world', 62 | 'deleteAfter' => 1800, 63 | ])->andReturn($this->object); 64 | 65 | $this->config->setDeleteAfter(1800); 66 | 67 | $this->adapter->write('hello', 'world', $this->flySystemConfig); 68 | 69 | // Prevent "no assertion error", we're just checking that the deleteAfter is correctly passed to the container 70 | $this->assertTrue(true); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Functional/UrlGenerationTest.php: -------------------------------------------------------------------------------- 1 | object->shouldNotReceive('retrieve', 'getObject'); 14 | 15 | $url = $this->adapter->getUrl('hello'); 16 | 17 | $this->assertEquals('https://storage.TestingGround.cloud.ovh.net/v1/AUTH_AwesomeProject/my-container/hello', $url); 18 | } 19 | 20 | public function testCanGenerateUrlOnCustomEndpoint() 21 | { 22 | $this->config->setEndpoint('http://custom.endpoint'); 23 | 24 | $this->object->shouldNotReceive('retrieve', 'getObject'); 25 | 26 | $url = $this->adapter->getUrl('hello'); 27 | 28 | $this->assertEquals('http://custom.endpoint/hello', $url); 29 | } 30 | 31 | public function testCanGenerateUrlWithFileConfirmation() 32 | { 33 | $this->container 34 | ->shouldReceive('objectExists') 35 | ->once() 36 | ->with('hello') 37 | ->andReturnTrue(); 38 | 39 | $url = $this->adapter->getUrlConfirm('hello'); 40 | 41 | $this->assertEquals('https://storage.TestingGround.cloud.ovh.net/v1/AUTH_AwesomeProject/my-container/hello', $url); 42 | } 43 | 44 | public function testCanGenerateUrlWithFileConfirmationOnCustomEndpoint() 45 | { 46 | $this->config->setEndpoint('http://custom.endpoint'); 47 | 48 | $this->container 49 | ->shouldReceive('objectExists') 50 | ->once() 51 | ->with('hello') 52 | ->andReturnTrue(); 53 | 54 | $url = $this->adapter->getUrlConfirm('hello'); 55 | 56 | $this->assertEquals('http://custom.endpoint/hello', $url); 57 | } 58 | 59 | public function testCanGenerateTemporaryUrl() 60 | { 61 | $this->config->setTempUrlKey('my-key'); 62 | 63 | $this->object->shouldNotReceive('retrieve', 'getObject'); 64 | 65 | $url = $this->adapter->getTemporaryUrl('hello.jpg', new DateTime('2004-09-22')); 66 | 67 | $this->assertNotNull($url); 68 | } 69 | 70 | public function testCanGenerateTemporaryUrlOnCustomEndpoint() 71 | { 72 | $this->config 73 | ->setEndpoint('http://custom.endpoint') 74 | ->setTempUrlKey('my-key'); 75 | 76 | $this->object->shouldNotReceive('retrieve', 'getObject'); 77 | 78 | $url = $this->adapter->getTemporaryUrl('hello.jpg', new DateTime('2015-10-21')); 79 | 80 | $this->assertNotNull($url); 81 | } 82 | 83 | public function testTemporaryUrlWillFailIfNoKeyProvided() 84 | { 85 | $this->expectException('InvalidArgumentException'); 86 | 87 | $this->adapter->getTemporaryUrl('hello.jpg', new DateTime('1979-06-13')); 88 | } 89 | 90 | public function testCanGenerateFormPostSignature() 91 | { 92 | $this->config->setTempUrlKey('my-key'); 93 | 94 | $this->object->shouldNotReceive('retrieve', 'getObject'); 95 | 96 | $signature = $this->adapter->getFormPostSignature('images', (new DateTime())->add(new DateInterval('PT5M'))); 97 | 98 | $this->assertNotNull($signature); 99 | } 100 | 101 | public function testFormPostSignatureWillFailIfNoKeyProvided() 102 | { 103 | $this->expectException('InvalidArgumentException'); 104 | 105 | $this->adapter->getFormPostSignature('images', new DateTime()); 106 | } 107 | 108 | public function testFormPostWillFailIfExpirationIsNotInTheFuture() 109 | { 110 | $this->config->setTempUrlKey('my-key'); 111 | 112 | $this->expectException('InvalidArgumentException'); 113 | 114 | $this->adapter->getFormPostSignature('images', new DateTime('2010-07-28')); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | config = OVHConfiguration::make([ 29 | 'authUrl' => '', 30 | 'projectId' => 'AwesomeProject', 31 | 'region' => 'TestingGround', 32 | 'userDomain' => 'Default', 33 | 'username' => '', 34 | 'password' => '', 35 | 'containerName' => 'my-container', 36 | ]); 37 | 38 | $this->container = Mockery::mock('OpenStack\ObjectStore\v1\Models\Container'); 39 | 40 | $this->container->name = $this->config->getContainerName(); 41 | $this->object = Mockery::mock('OpenStack\ObjectStore\v1\Models\StorageObject'); 42 | $this->adapter = new OVHSwiftAdapter($this->container, $this->config); 43 | } 44 | 45 | public function tearDown(): void 46 | { 47 | Mockery::close(); 48 | } 49 | } 50 | --------------------------------------------------------------------------------