├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── pint.yml │ └── run-tests.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src └── DropboxAdapter.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: spatie 2 | custom: https://spatie.be/open-source/support-us 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://github.com/spatie/flysystem-dropbox/discussions/new?category=q-a 5 | about: Ask the community for help 6 | - name: Feature Request 7 | url: https://github.com/spatie/flysystem-dropbox/discussions/new?category=ideas 8 | about: Share ideas for new features 9 | -------------------------------------------------------------------------------- /.github/workflows/pint.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v3 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - name: Fix styling issues 16 | uses: aglipanci/laravel-pint-action@0.1.0 17 | 18 | - name: Commit changes 19 | uses: stefanzweifel/git-auto-commit-action@v4 20 | with: 21 | commit_message: Fix styling 22 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.2, 8.1, 8.0] 13 | dependency-version: [prefer-lowest, prefer-stable] 14 | 15 | name: P${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 26 | coverage: none 27 | 28 | - name: Setup Problem Matches 29 | run: | 30 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 31 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 32 | 33 | - name: Install dependencies 34 | run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 35 | 36 | - name: Execute tests 37 | run: vendor/bin/pest 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `flysystem-dropbox` will be documented in this file 4 | 5 | ## 2.0.4 - 2021-04-26 6 | 7 | - avoid listing the base directory itself in listContents calls (#73) 8 | 9 | ## 2.0.3 - 2021-04-25 10 | 11 | - make listing a non-created directory not throw an exception (#72) 12 | 13 | ## 2.0.2 - 2021-03-31 14 | 15 | - use generator in listContents call for upstream compliance (#66) 16 | 17 | ## 2.0.1 - 2021-03-31 18 | 19 | - fix bugs discovered after real-world use (#63) 20 | 21 | ## 2.0.0 - 2021-03-28 22 | 23 | - add support from Flysystem v2 24 | 25 | ## 1.2.3 - 2020-12-27 26 | 27 | - add support for PHP 8 28 | 29 | ## 1.2.2 - 2019-12-04 30 | 31 | - fix `createSharedLinkWithSettings` 32 | 33 | ## 1.2.1 - 2019-09-14 34 | 35 | - fix minimum dep 36 | 37 | ## 1.2.0 - 2019-09-13 38 | 39 | - add `getUrl` method 40 | 41 | ## 1.1.0 - 2019-05-31 42 | 43 | - add `createSharedLinkWithSettings` 44 | 45 | ## 1.0.6 - 2017-11-18 46 | 47 | - determine mimetype from filename 48 | 49 | ## 1.0.5 - 2017-10-21 50 | 51 | - do not throw an exception when listing a non-existing directory 52 | 53 | ## 1.0.4 - 2017-10-19 54 | 55 | - make sure all files are retrieved when calling `listContents` 56 | 57 | ## 1.0.3 - 2017-05-18 58 | 59 | - reverts changes made in 1.0.2 60 | 61 | ## 1.0.2 - 2017-05-18 62 | 63 | - fix for files with different casings not showing up 64 | 65 | ## 1.0.1 - 2017-05-09 66 | 67 | - add `size` key. `bytes` is deprecated and will be removed in the next major version. 68 | 69 | 70 | ## 1.0.0 - 2017-04-19 71 | 72 | - initial release 73 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flysystem adapter for the Dropbox API 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/flysystem-dropbox.svg?style=flat-square)](https://packagist.org/packages/spatie/flysystem-dropbox) 4 | [![Tests](https://github.com/spatie/flysystem-dropbox/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/flysystem-dropbox/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/flysystem-dropbox.svg?style=flat-square)](https://packagist.org/packages/spatie/flysystem-dropbox) 6 | 7 | This package contains a [Flysystem](https://flysystem.thephpleague.com/) adapter for Dropbox. Under the hood, the [Dropbox API v2](https://www.dropbox.com/developers/documentation/http/overview) is used. 8 | 9 | ## Using Flystem v1 10 | 11 | If you're using Flysystem v1, then use [v1 of flysystem-dropbox](https://github.com/spatie/flysystem-dropbox/tree/v1). 12 | 13 | ## Support us 14 | 15 | [](https://spatie.be/github-ad-click/flysystem-dropbox) 16 | 17 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 18 | 19 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 20 | 21 | ## Installation 22 | 23 | You can install the package via composer: 24 | 25 | ``` bash 26 | composer require spatie/flysystem-dropbox 27 | ``` 28 | 29 | ## Usage 30 | 31 | The first thing you need to do is to get an authorization token at Dropbox. A token can be generated in the [App Console](https://www.dropbox.com/developers/apps) for any Dropbox API app. You'll find more info at [the Dropbox Developer Blog](https://blogs.dropbox.com/developers/2014/05/generate-an-access-token-for-your-own-account/). 32 | 33 | ```php 34 | use League\Flysystem\Filesystem; 35 | use Spatie\Dropbox\Client; 36 | use Spatie\FlysystemDropbox\DropboxAdapter; 37 | 38 | $client = new Client($authorizationToken); 39 | 40 | $adapter = new DropboxAdapter($client); 41 | 42 | $filesystem = new Filesystem($adapter, ['case_sensitive' => false]); 43 | ``` 44 | 45 | Note: Because Dropbox is not case-sensitive you’ll need to set the 'case_sensitive' option to false. 46 | 47 | ## Changelog 48 | 49 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 50 | 51 | ## Testing 52 | 53 | ``` bash 54 | composer test 55 | ``` 56 | 57 | ## Contributing 58 | 59 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 60 | 61 | ## Security 62 | 63 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 64 | 65 | ## Postcardware 66 | 67 | You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 68 | 69 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 70 | 71 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 72 | 73 | ## Credits 74 | 75 | - [Alex Vanderbist](https://github.com/AlexVanderbist) 76 | - [Freek Van der Herten](https://github.com/freekmurze) 77 | - [All Contributors](../../contributors) 78 | 79 | ## License 80 | 81 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/flysystem-dropbox", 3 | "description": "Flysystem Adapter for the Dropbox v2 API", 4 | "keywords": [ 5 | "spatie", 6 | "flysystem-dropbox", 7 | "flysystem", 8 | "dropbox", 9 | "v2", 10 | "api" 11 | ], 12 | "homepage": "https://github.com/spatie/flysystem-dropbox", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Alex Vanderbist", 17 | "email": "alex.vanderbist@gmail.com", 18 | "homepage": "https://spatie.be", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.0", 24 | "league/flysystem": "^3.7.0", 25 | "spatie/dropbox-api": "^1.17.1" 26 | }, 27 | "require-dev": { 28 | "pestphp/pest": "^1.22", 29 | "phpspec/prophecy-phpunit": "^2.0.1", 30 | "phpunit/phpunit": "^9.5.4" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Spatie\\FlysystemDropbox\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Spatie\\FlysystemDropbox\\Test\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "vendor/bin/phpunit" 44 | }, 45 | "config": { 46 | "sort-packages": true, 47 | "allow-plugins": { 48 | "pestphp/pest-plugin": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DropboxAdapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 42 | $this->prefixer = new PathPrefixer($prefix); 43 | $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector; 44 | } 45 | 46 | public function getClient(): Client 47 | { 48 | return $this->client; 49 | } 50 | 51 | public function fileExists(string $path): bool 52 | { 53 | $location = $this->applyPathPrefix($path); 54 | 55 | try { 56 | $meta = $this->client->getMetadata($location); 57 | 58 | return $meta['.tag'] === 'file'; 59 | } catch (BadRequest) { 60 | return false; 61 | } 62 | } 63 | 64 | public function directoryExists(string $path): bool 65 | { 66 | $location = $this->applyPathPrefix($path); 67 | 68 | try { 69 | $meta = $this->client->getMetadata($location); 70 | 71 | return $meta['.tag'] === 'folder'; 72 | } catch (BadRequest) { 73 | return false; 74 | } 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function write(string $path, string $contents, Config $config): void 81 | { 82 | $location = $this->applyPathPrefix($path); 83 | 84 | try { 85 | $this->client->upload($location, $contents, 'overwrite'); 86 | } catch (BadRequest $exception) { 87 | throw UnableToWriteFile::atLocation($location, $exception->getMessage(), $exception); 88 | } 89 | } 90 | 91 | /** 92 | * {@inheritDoc} 93 | */ 94 | public function writeStream(string $path, $contents, Config $config): void 95 | { 96 | $location = $this->applyPathPrefix($path); 97 | 98 | try { 99 | $this->client->upload($location, $contents, 'overwrite'); 100 | } catch (BadRequest $exception) { 101 | throw UnableToWriteFile::atLocation($location, $exception->getMessage(), $exception); 102 | } 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | public function read(string $path): string 109 | { 110 | $object = $this->readStream($path); 111 | 112 | $contents = stream_get_contents($object); 113 | fclose($object); 114 | 115 | unset($object); 116 | 117 | return $contents; 118 | } 119 | 120 | /** 121 | * {@inheritDoc} 122 | */ 123 | public function readStream(string $path) 124 | { 125 | $location = $this->applyPathPrefix($path); 126 | 127 | try { 128 | $stream = $this->client->download($location); 129 | } catch (BadRequest $exception) { 130 | throw UnableToReadFile::fromLocation($location, $exception->getMessage(), $exception); 131 | } 132 | 133 | return $stream; 134 | } 135 | 136 | /** 137 | * {@inheritDoc} 138 | */ 139 | public function delete(string $path): void 140 | { 141 | $location = $this->applyPathPrefix($path); 142 | 143 | try { 144 | $this->client->delete($location); 145 | } catch (BadRequest $exception) { 146 | throw UnableToDeleteFile::atLocation($location, $exception->getMessage(), $exception); 147 | } 148 | } 149 | 150 | /** 151 | * {@inheritDoc} 152 | */ 153 | public function deleteDirectory(string $path): void 154 | { 155 | $location = $this->applyPathPrefix($path); 156 | 157 | try { 158 | $this->client->delete($location); 159 | } catch (UnableToDeleteFile $exception) { 160 | throw UnableToDeleteDirectory::atLocation($location, $exception->getPrevious()->getMessage(), $exception); 161 | } 162 | } 163 | 164 | /** 165 | * {@inheritDoc} 166 | */ 167 | public function createDirectory(string $path, Config $config): void 168 | { 169 | $location = $this->applyPathPrefix($path); 170 | 171 | try { 172 | $this->client->createFolder($location); 173 | } catch (BadRequest $exception) { 174 | throw UnableToCreateDirectory::atLocation($location, $exception->getMessage()); 175 | } 176 | } 177 | 178 | /** 179 | * {@inheritDoc} 180 | */ 181 | public function setVisibility(string $path, string $visibility): void 182 | { 183 | throw UnableToSetVisibility::atLocation($path, 'Adapter does not support visibility controls.'); 184 | } 185 | 186 | /** 187 | * {@inheritDoc} 188 | */ 189 | public function visibility(string $path): FileAttributes 190 | { 191 | // Noop 192 | return new FileAttributes($path); 193 | } 194 | 195 | /** 196 | * {@inheritDoc} 197 | */ 198 | public function mimeType(string $path): FileAttributes 199 | { 200 | return new FileAttributes( 201 | $path, 202 | null, 203 | null, 204 | null, 205 | $this->mimeTypeDetector->detectMimeTypeFromPath($path) 206 | ); 207 | } 208 | 209 | /** 210 | * {@inheritDoc} 211 | */ 212 | public function lastModified(string $path): FileAttributes 213 | { 214 | $location = $this->applyPathPrefix($path); 215 | 216 | try { 217 | $response = $this->client->getMetadata($location); 218 | } catch (BadRequest $exception) { 219 | throw UnableToRetrieveMetadata::lastModified($location, $exception->getMessage()); 220 | } 221 | 222 | $timestamp = (isset($response['server_modified'])) ? strtotime($response['server_modified']) : null; 223 | 224 | return new FileAttributes( 225 | $path, 226 | null, 227 | null, 228 | $timestamp 229 | ); 230 | } 231 | 232 | /** 233 | * {@inheritDoc} 234 | */ 235 | public function checksum(string $path, Config $config): string 236 | { 237 | $algo = $config->get('checksum_algo', 'sha256'); 238 | $location = $this->applyPathPrefix($path); 239 | 240 | try { 241 | $response = $this->client->getMetadata($location); 242 | } catch (BadRequest $exception) { 243 | throw new UnableToProvideChecksum( 244 | reason: 'Unable to retrieve metadata.', 245 | path: $path, 246 | previous: $exception, 247 | ); 248 | } 249 | 250 | if (empty($response['content_hash'])) { 251 | throw new UnableToProvideChecksum( 252 | reason: 'Content-Hash not provided by Dropbox metadata.', 253 | path: $path, 254 | ); 255 | } 256 | 257 | return $algo === 'sha256' 258 | ? $response['content_hash'] 259 | : hash($algo, $response['content_hash']); 260 | } 261 | 262 | /** 263 | * {@inheritDoc} 264 | */ 265 | public function fileSize(string $path): FileAttributes 266 | { 267 | $location = $this->applyPathPrefix($path); 268 | 269 | try { 270 | $response = $this->client->getMetadata($location); 271 | } catch (BadRequest $exception) { 272 | throw UnableToRetrieveMetadata::lastModified($location, $exception->getMessage()); 273 | } 274 | 275 | return new FileAttributes( 276 | $path, 277 | $response['size'] ?? null 278 | ); 279 | } 280 | 281 | /** 282 | * {@inheritDoc} 283 | */ 284 | public function listContents(string $path = '', bool $deep = false): iterable 285 | { 286 | foreach ($this->iterateFolderContents($path, $deep) as $entry) { 287 | $storageAttrs = $this->normalizeResponse($entry); 288 | 289 | // Avoid including the base directory itself 290 | if ($storageAttrs->isDir() && $storageAttrs->path() === $path) { 291 | continue; 292 | } 293 | 294 | yield $storageAttrs; 295 | } 296 | } 297 | 298 | protected function iterateFolderContents(string $path = '', bool $deep = false): Generator 299 | { 300 | $location = $this->applyPathPrefix($path); 301 | 302 | try { 303 | $result = $this->client->listFolder($location, $deep); 304 | } catch (BadRequest $exception) { 305 | return; 306 | } 307 | 308 | yield from $result['entries']; 309 | 310 | while ($result['has_more']) { 311 | $result = $this->client->listFolderContinue($result['cursor']); 312 | yield from $result['entries']; 313 | } 314 | } 315 | 316 | protected function normalizeResponse(array $response): StorageAttributes 317 | { 318 | $timestamp = (isset($response['server_modified'])) ? strtotime($response['server_modified']) : null; 319 | 320 | if ($response['.tag'] === 'folder') { 321 | $normalizedPath = ltrim($this->prefixer->stripDirectoryPrefix($response['path_display']), '/'); 322 | 323 | return new DirectoryAttributes( 324 | $normalizedPath, 325 | null, 326 | $timestamp 327 | ); 328 | } 329 | 330 | $normalizedPath = ltrim($this->prefixer->stripPrefix($response['path_display']), '/'); 331 | 332 | return new FileAttributes( 333 | $normalizedPath, 334 | $response['size'] ?? null, 335 | null, 336 | $timestamp, 337 | $this->mimeTypeDetector->detectMimeTypeFromPath($normalizedPath) 338 | ); 339 | } 340 | 341 | /** 342 | * {@inheritDoc} 343 | */ 344 | public function move(string $source, string $destination, Config $config): void 345 | { 346 | $path = $this->applyPathPrefix($source); 347 | $newPath = $this->applyPathPrefix($destination); 348 | 349 | try { 350 | $this->client->move($path, $newPath); 351 | } catch (BadRequest $exception) { 352 | throw UnableToMoveFile::fromLocationTo($path, $newPath, $exception); 353 | } 354 | } 355 | 356 | /** 357 | * {@inheritDoc} 358 | */ 359 | public function copy(string $source, string $destination, Config $config): void 360 | { 361 | $path = $this->applyPathPrefix($source); 362 | $newPath = $this->applyPathPrefix($destination); 363 | 364 | try { 365 | $this->client->copy($path, $newPath); 366 | } catch (BadRequest $e) { 367 | throw UnableToCopyFile::fromLocationTo($path, $newPath, $e); 368 | } 369 | } 370 | 371 | protected function applyPathPrefix($path): string 372 | { 373 | return '/'.trim($this->prefixer->prefixPath($path), '/'); 374 | } 375 | 376 | public function getUrl(string $path): string 377 | { 378 | return $this->client->getTemporaryLink($path); 379 | } 380 | } 381 | --------------------------------------------------------------------------------