├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── code-style.yml │ ├── update-changelog.yml │ ├── static-analysis.yml │ ├── run-tests.yml │ └── dependabot-auto-merge.yml ├── src ├── TokenProvider.php ├── InMemoryTokenProvider.php ├── RefreshableTokenProvider.php ├── UploadSessionCursor.php ├── Exceptions │ └── BadRequest.php └── Client.php ├── phpunit.xml ├── LICENSE.md ├── composer.json ├── README.md └── CHANGELOG.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: spatie 2 | custom: https://spatie.be/open-source/support-us 3 | -------------------------------------------------------------------------------- /src/TokenProvider.php: -------------------------------------------------------------------------------- 1 | token; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/RefreshableTokenProvider.php: -------------------------------------------------------------------------------- 1 | getBody(), true); 15 | 16 | if ($body !== null) { 17 | if (isset($body['error']['.tag'])) { 18 | $this->dropboxCode = $body['error']['.tag']; 19 | } 20 | 21 | parent::__construct($body['error_summary']); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | ref: main 16 | 17 | - name: Update Changelog 18 | uses: stefanzweifel/changelog-updater-action@v1 19 | with: 20 | latest-version: ${{ github.event.release.name }} 21 | release-notes: ${{ github.event.release.body }} 22 | 23 | - name: Commit updated CHANGELOG 24 | uses: stefanzweifel/git-auto-commit-action@v5 25 | with: 26 | branch: main 27 | commit_message: Update CHANGELOG 28 | file_pattern: CHANGELOG.md 29 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | phpstan: 7 | name: PHPStan (PHP ${{ matrix.php-version }}) 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | php-version: 13 | - '8.2' 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | coverage: none 23 | php-version: ${{ matrix.php-version }} 24 | tools: cs2pr 25 | 26 | - name: Install Composer dependencies 27 | uses: ramsey/composer-install@v3 28 | 29 | - name: Run PHPStan 30 | run: 'vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr' 31 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | php: [8.2, 8.1] 12 | stability: [lowest, highest] 13 | 14 | name: PHP ${{ matrix.php }} - ${{ matrix.stability }} deps 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | coverage: none 25 | 26 | - name: Setup problem matchers 27 | run: | 28 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 29 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 30 | 31 | - name: Install dependencies 32 | uses: ramsey/composer-install@v3 33 | with: 34 | dependency-versions: ${{ matrix.stability }} 35 | 36 | - name: Execute tests 37 | run: vendor/bin/pest 38 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | src/ 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: dependabot-auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2.4.0 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | compat-lookup: true 20 | 21 | - name: Auto-merge Dependabot PRs for semver-minor updates 22 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 23 | run: gh pr merge --auto --merge "$PR_URL" 24 | env: 25 | PR_URL: ${{github.event.pull_request.html_url}} 26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | 28 | - name: Auto-merge Dependabot PRs for semver-patch updates 29 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 30 | run: gh pr merge --auto --merge "$PR_URL" 31 | env: 32 | PR_URL: ${{github.event.pull_request.html_url}} 33 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | - name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90% 36 | if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}} 37 | run: gh pr merge --auto --merge "$PR_URL" 38 | env: 39 | PR_URL: ${{github.event.pull_request.html_url}} 40 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/dropbox-api", 3 | "description": "A minimal implementation of Dropbox API v2", 4 | "keywords": [ 5 | "spatie", 6 | "dropbox-api", 7 | "dropbox", 8 | "api", 9 | "v2" 10 | ], 11 | "homepage": "https://github.com/spatie/dropbox-api", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Alex Vanderbist", 16 | "email": "alex.vanderbist@gmail.com", 17 | "homepage": "https://spatie.be", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "Freek Van der Herten", 22 | "email": "freek@spatie.be", 23 | "homepage": "https://spatie.be", 24 | "role": "Developer" 25 | } 26 | ], 27 | "require": { 28 | "php": "^8.1", 29 | "ext-json": "*", 30 | "graham-campbell/guzzle-factory": "^4.0.2|^5.0|^6.0|^7.0", 31 | "guzzlehttp/guzzle": "^6.2|^7.0" 32 | }, 33 | "require-dev": { 34 | "laravel/pint": "^1.10.1", 35 | "pestphp/pest": "^2.6.3", 36 | "phpstan/phpstan": "^1.10.16" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Spatie\\Dropbox\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Spatie\\Dropbox\\Tests\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/pest", 50 | "pint": "vendor/bin/pint", 51 | "phpstan": "vendor/bin/phpstan analyze" 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "pestphp/pest-plugin": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A minimal implementation of Dropbox API v2 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/dropbox-api.svg?style=flat-square)](https://packagist.org/packages/spatie/dropbox-api) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/dropbox-api/run-tests.yml?label=tests&style=flat-square)](https://github.com/spatie/dropbox-api/actions?query=workflow%3ATests+branch%3Amaster) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/spatie/dropbox-api/code-style.yml?label=code%20style&style=flat-square)](https://github.com/spatie/dropbox-api/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amaster) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/dropbox-api.svg?style=flat-square)](https://packagist.org/packages/spatie/dropbox-api) 7 | 8 | 9 | This is a minimal PHP implementation of the [Dropbox API v2](https://www.dropbox.com/developers/documentation/http/overview). It contains only the methods needed for [our flysystem-dropbox adapter](https://github.com/spatie/flysystem-dropbox). We are open however to PRs that add extra methods to the client. 10 | 11 | Here are a few examples on how you can use the package: 12 | 13 | ```php 14 | $client = new Spatie\Dropbox\Client($authorizationToken); 15 | 16 | //create a folder 17 | $client->createFolder($path); 18 | 19 | //list a folder 20 | $client->listFolder($path); 21 | 22 | //get a temporary link 23 | $client->getTemporaryLink($path); 24 | ``` 25 | 26 | ## Support us 27 | 28 | [](https://spatie.be/github-ad-click/dropbox-api) 29 | 30 | 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). 31 | 32 | 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). 33 | 34 | ## Installation 35 | 36 | You can install the package via composer: 37 | 38 | ``` bash 39 | composer require spatie/dropbox-api 40 | ``` 41 | 42 | ## Usage 43 | 44 | The first thing you need to do is get an authorization token at Dropbox. Unlike [other companies](https://google.com) Dropbox has made this very easy. You can just generate a token 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/). 45 | 46 | With an authorization token you can instantiate a `Spatie\Dropbox\Client`. 47 | 48 | ```php 49 | $client = new Spatie\Dropbox\Client($authorizationToken); 50 | ``` 51 | 52 | or alternatively you can implement `Spatie\Dropbox\TokenProvider` 53 | which will provide the access-token from its 54 | `TokenProvider->getToken(): string` method. 55 | 56 | If you use oauth2 to authenticate and to acquire refresh-tokens and access-tokens, 57 | (like [thephpleague/oauth2-client](https://github.com/thephpleague/oauth2-client)), 58 | you can create an adapter that internally takes care of token-expiration and refreshing tokens, 59 | and at runtime will supply the access-token via the `TokenProvider->getToken(): string` method. 60 | 61 | *(Dropbox announced they will be moving to short-lived access_tokens mid 2021).* 62 | 63 | 64 | ```php 65 | // implements Spatie\Dropbox\TokenProvider 66 | $tokenProvider = new AutoRefreshingDropBoxTokenService($refreshToken); 67 | $client = new Spatie\Dropbox\Client($tokenProvider); 68 | ``` 69 | 70 | 71 | 72 | or alternatively you can authenticate as an App using your App Key & Secret. 73 | 74 | ```php 75 | $client = new Spatie\Dropbox\Client([$appKey, $appSecret]); 76 | ``` 77 | 78 | If you only need to access the public endpoints you can instantiate `Spatie\Dropbox\Client` without any arguments. 79 | 80 | ```php 81 | $client = new Spatie\Dropbox\Client(); 82 | ``` 83 | 84 | ## Dropbox Endpoints 85 | 86 | Look in [the source code of `Spatie\Dropbox\Client`](https://github.com/spatie/dropbox-api/blob/master/src/Client.php) to discover the methods you can use. 87 | 88 | Here's an example: 89 | 90 | ```php 91 | $content = 'hello, world'; 92 | $client->upload('/dropboxpath/filename.txt', $content, $mode='add'); 93 | 94 | $from = '/dropboxpath/somefile.txt'; 95 | $to = '/dropboxpath/archive/somefile.txt'; 96 | $client->move($from, $to); 97 | ``` 98 | 99 | If the destination filename already exists, dropbox will throw an Exception with 'to/conflict/file/..' 100 | 101 | The ``upload()`` and ``move()`` methods have an optional extra 'autorename' argument 102 | to try and let dropbox automatically rename the file if there is such a conflict. 103 | 104 | Here's an example: 105 | 106 | ```php 107 | $from = '/dropboxpath/somefile.txt'; 108 | $to = '/dropboxpath/archive/somefile.txt'; 109 | $client->move($from, $to, $autorename=true); 110 | // with autorename results in 'somefile (1).txt' 111 | ``` 112 | 113 | 114 | If you do not find your favorite method, you can directly use the `contentEndpointRequest` and `rpcEndpointRequest` functions. 115 | 116 | ```php 117 | public function contentEndpointRequest(string $endpoint, array $arguments, $body): ResponseInterface 118 | 119 | public function rpcEndpointRequest(string $endpoint, array $parameters): array 120 | ``` 121 | 122 | Here's an example: 123 | 124 | ```php 125 | $client->rpcEndpointRequest('search', ['path' => '', 'query' => 'bat cave']); 126 | ``` 127 | 128 | If you need to change the subdomain of the endpoint URL used in the API request, you can prefix the endpoint path with `subdomain::`. 129 | 130 | Here's an example: 131 | 132 | ```php 133 | $client->rpcEndpointRequest('content::files/get_thumbnail_batch', $parameters); 134 | ``` 135 | 136 | ## Changelog 137 | 138 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 139 | 140 | ## Testing 141 | 142 | ``` bash 143 | composer test 144 | ``` 145 | 146 | ## Contributing 147 | 148 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 149 | 150 | You can run `composer pint` to run the code style fixer, and `composer phpstan` to run the static analysis. 151 | 152 | ## Security 153 | 154 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 155 | 156 | ## Postcardware 157 | 158 | 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. 159 | 160 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 161 | 162 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 163 | 164 | ## Credits 165 | 166 | - [Alex Vanderbist](https://github.com/AlexVanderbist) 167 | - [Freek Van der Herten](https://github.com/freekmurze) 168 | - [All Contributors](../../contributors) 169 | 170 | ## License 171 | 172 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 173 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `dropbox-api` will be documented in this file 4 | 5 | ## 1.23.0 - 2025-01-06 6 | 7 | ### What's Changed 8 | 9 | * Run tests using Pest by @jmsche in https://github.com/spatie/dropbox-api/pull/124 10 | * Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/spatie/dropbox-api/pull/125 11 | * Bump actions/checkout from 2 to 4 by @dependabot in https://github.com/spatie/dropbox-api/pull/126 12 | * Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/spatie/dropbox-api/pull/129 13 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/spatie/dropbox-api/pull/128 14 | * Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/dropbox-api/pull/131 15 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/spatie/dropbox-api/pull/133 16 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.2.0 by @dependabot in https://github.com/spatie/dropbox-api/pull/136 17 | * PHP 8.4 deprecations by @simoheinonen in https://github.com/spatie/dropbox-api/pull/138 18 | 19 | ### New Contributors 20 | 21 | * @simoheinonen made their first contribution in https://github.com/spatie/dropbox-api/pull/138 22 | 23 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.22.0...1.23.0 24 | 25 | ## 1.22.0 - 2023-06-08 26 | 27 | ### What's Changed 28 | 29 | - Cleanup and improvements by @jmsche in https://github.com/spatie/dropbox-api/pull/123 30 | - Drop support for old PHP versions 31 | 32 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.21.2...1.22.0 33 | 34 | ## 1.21.2 - 2023-06-07 35 | 36 | ### What's Changed 37 | 38 | - Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/spatie/dropbox-api/pull/115 39 | - Bump dependabot/fetch-metadata from 1.4.0 to 1.5.0 by @dependabot in https://github.com/spatie/dropbox-api/pull/117 40 | - Bump dependabot/fetch-metadata from 1.5.0 to 1.5.1 by @dependabot in https://github.com/spatie/dropbox-api/pull/118 41 | - Fix README badges by @jmsche in https://github.com/spatie/dropbox-api/pull/120 42 | - Allow graham-campbell/guzzle-factory v7 by @jmsche in https://github.com/spatie/dropbox-api/pull/119 43 | - Fix PHPUnit errors & failures by @jmsche in https://github.com/spatie/dropbox-api/pull/121 44 | 45 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.21.1...1.21.2 46 | 47 | ## 1.21.1 - 2023-03-17 48 | 49 | ### What's Changed 50 | 51 | - Add PHP 8.2 support by @patinthehat in https://github.com/spatie/dropbox-api/pull/110 52 | - Add dependabot automation by @patinthehat in https://github.com/spatie/dropbox-api/pull/109 53 | - Update Dependabot Automation by @patinthehat in https://github.com/spatie/dropbox-api/pull/112 54 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/dropbox-api/pull/113 55 | - Allow graham-campbell/guzzle-factory v6 by @jmsche in https://github.com/spatie/dropbox-api/pull/114 56 | 57 | ### New Contributors 58 | 59 | - @patinthehat made their first contribution in https://github.com/spatie/dropbox-api/pull/110 60 | - @dependabot made their first contribution in https://github.com/spatie/dropbox-api/pull/113 61 | 62 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.21.0...1.21.1 63 | 64 | ## 1.21.0 - 2022-09-27 65 | 66 | ### What's Changed 67 | 68 | - Add ability to set the namespace ID for requests by @rstefanic in https://github.com/spatie/dropbox-api/pull/105 69 | 70 | ### New Contributors 71 | 72 | - @rstefanic made their first contribution in https://github.com/spatie/dropbox-api/pull/105 73 | 74 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.20.2...1.21.0 75 | 76 | ## 1.20.2 - 2022-06-24 77 | 78 | ### What's Changed 79 | 80 | - uploadSessionStart and uploadSessionFinish can accept resource by @dmitryuk in https://github.com/spatie/dropbox-api/pull/102 81 | 82 | ### New Contributors 83 | 84 | - @dmitryuk made their first contribution in https://github.com/spatie/dropbox-api/pull/102 85 | 86 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.20.1...1.20.2 87 | 88 | ## 1.20.1 - 2022-03-29 89 | 90 | ## What's Changed 91 | 92 | - Fix refreshable token response by @einarsozols in https://github.com/spatie/dropbox-api/pull/100 93 | 94 | ## New Contributors 95 | 96 | - @einarsozols made their first contribution in https://github.com/spatie/dropbox-api/pull/100 97 | 98 | **Full Changelog**: https://github.com/spatie/dropbox-api/compare/1.20.0...1.20.1 99 | 100 | ## Unreleased 101 | 102 | - Added refreshable token provider interface. 103 | 104 | ## 1.19.1 - 2021-07-04 105 | 106 | - fix compability with guzzlehttp/psr7 2.0 (#91) 107 | 108 | ## 1.19.0 - 2021-06-18 109 | 110 | - add autoRename parameter for move() method (#89) 111 | 112 | ## 1.18.0 - 2021-05-27 113 | 114 | - add autorename option to upload method (#86) 115 | 116 | ## 1.17.1 - 2021-03-01 117 | 118 | - allow graham-campbell/guzzle-factory v5 (#79) 119 | 120 | ## 1.17.0 - 2020-12-08 121 | 122 | - `TokenProvider` interface for accesstokens (#76) 123 | 124 | ## 1.16.1 - 2020-11-27 125 | 126 | - allow PHP 8 127 | 128 | ## 1.16.0 - 2020-09-25 129 | 130 | - allow the Client to work with Dropbox business accounts 131 | 132 | ## 1.15.0 - 2020-07-09 133 | 134 | - allow Guzzle 7 (#70) 135 | 136 | ## 1.14.0 - 2020-05-11 137 | 138 | - add support for app authentication and no authentication 139 | 140 | ## 1.13.0 - 2020-05-03 141 | 142 | - added `downloadZip` (#66) 143 | 144 | ## 1.12.0 - 2020-02-04 145 | 146 | - add `search` method 147 | 148 | ## 1.11.1 - 2019-12-12 149 | 150 | - make compatible with PHP 7.4 151 | 152 | ## 1.11.0 - 2019-07-04 153 | 154 | - add `$response` to `BadRequest` 155 | 156 | ## 1.10.0 - 2019-07-01 157 | 158 | - move retry stuff to package 159 | 160 | ## 1.9.0 - 2019-05-21 161 | 162 | - make guzzle retry 5xx and 429 responses 163 | 164 | ## 1.8.0 - 2019-04-13 165 | 166 | - add `getEndpointUrl` 167 | - drop support for PHP 7.0 168 | 169 | ## 1.7.1 - 2019-02-13 170 | 171 | - fix for `createSharedLinkWithSettings` with empty settings 172 | 173 | ## 1.7.0 - 2019-02-06 174 | 175 | - add getter and setter for the access token 176 | 177 | ## 1.6.6 - 2018-07-19 178 | 179 | - fix for piped streams 180 | 181 | ## 1.6.5 - 2018-01-15 182 | 183 | - adjust `normalizePath` to allow id/rev/ns to be queried 184 | 185 | ## 1.6.4 - 2017-12-05 186 | 187 | - fix max chunk size 188 | 189 | ## 1.6.1 - 2017-07-28 190 | 191 | - fix for finishing upload session 192 | 193 | ## 1.6.0 - 2017-07-28 194 | 195 | - add various new methods to enable chuncked uploads 196 | 197 | ## 1.5.3 - 2017-07-28 198 | 199 | - use recommended `move_v2` method to move files 200 | 201 | ## 1.5.2 - 2017-07-17 202 | 203 | - add missing parameters to `listSharedLinks` method 204 | 205 | ## 1.5.1 - 2017-07-17 206 | 207 | - fix broken `revokeToken` and `getAccountInfo` 208 | 209 | ## 1.5.0 - 2017-07-11 210 | 211 | - add `revokeToken` and `getAccountInfo` 212 | 213 | ## 1.4.0 - 2017-07-11 214 | 215 | - add `listSharedLinks` 216 | 217 | ## 1.3.0 - 2017-07-04 218 | 219 | - add error code to thrown exception 220 | 221 | ## 1.2.0 - 2017-04-29 222 | 223 | - added `createSharedLinkWithSettings` 224 | 225 | ## 1.1.0 - 2017-04-22 226 | 227 | - added `listFolderContinue` 228 | 229 | ## 1.0.1 - 2017-04-19 230 | 231 | - Bugfix: set default value for request body 232 | 233 | ## 1.0.0 - 2017-04-19 234 | 235 | - initial release 236 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | |TokenProvider|null $accessTokenOrAppCredentials 54 | * @param int $maxChunkSize Set max chunk size per request (determines when to switch from "one shot upload" to upload session and defines chunk size for uploads via session). 55 | * @param int $maxUploadChunkRetries How many times to retry an upload session start or append after RequestException. 56 | * @param ?string $teamMemberId The team member ID to be specified for Dropbox business accounts 57 | */ 58 | public function __construct( 59 | string|array|TokenProvider|null $accessTokenOrAppCredentials = null, 60 | ?ClientInterface $client = null, 61 | int $maxChunkSize = self::MAX_CHUNK_SIZE, 62 | protected int $maxUploadChunkRetries = 0, 63 | protected ?string $teamMemberId = null, 64 | ) { 65 | if (is_array($accessTokenOrAppCredentials)) { 66 | [$this->appKey, $this->appSecret] = $accessTokenOrAppCredentials; 67 | } 68 | if ($accessTokenOrAppCredentials instanceof TokenProvider) { 69 | $this->tokenProvider = $accessTokenOrAppCredentials; 70 | } 71 | if (is_string($accessTokenOrAppCredentials)) { 72 | $this->tokenProvider = new InMemoryTokenProvider($accessTokenOrAppCredentials); 73 | } 74 | 75 | $this->client = $client ?? new GuzzleClient(['handler' => GuzzleFactory::handler()]); 76 | 77 | $this->maxChunkSize = ($maxChunkSize < self::MAX_CHUNK_SIZE ? max($maxChunkSize, 1) : self::MAX_CHUNK_SIZE); 78 | } 79 | 80 | /** 81 | * Copy a file or folder to a different location in the user's Dropbox. 82 | * 83 | * If the source path is a folder all its contents will be copied. 84 | * 85 | * @return array 86 | * 87 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-copy_v2 88 | */ 89 | public function copy(string $fromPath, string $toPath): array 90 | { 91 | $parameters = [ 92 | 'from_path' => $this->normalizePath($fromPath), 93 | 'to_path' => $this->normalizePath($toPath), 94 | ]; 95 | 96 | return $this->rpcEndpointRequest('files/copy_v2', $parameters); 97 | } 98 | 99 | /** 100 | * Create a folder at a given path. 101 | * 102 | * @return array 103 | * 104 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder 105 | */ 106 | public function createFolder(string $path): array 107 | { 108 | $parameters = [ 109 | 'path' => $this->normalizePath($path), 110 | ]; 111 | 112 | $object = $this->rpcEndpointRequest('files/create_folder', $parameters); 113 | 114 | $object['.tag'] = 'folder'; 115 | 116 | return $object; 117 | } 118 | 119 | /** 120 | * Create a shared link with custom settings. 121 | * 122 | * If no settings are given then the default visibility is RequestedVisibility.public. 123 | * The resolved visibility, though, may depend on other aspects such as team and 124 | * shared folder settings). Only for paid users. 125 | * 126 | * @param array $settings 127 | * @return array 128 | * 129 | * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-create_shared_link_with_settings 130 | */ 131 | public function createSharedLinkWithSettings(string $path, array $settings = []): array 132 | { 133 | $parameters = [ 134 | 'path' => $this->normalizePath($path), 135 | ]; 136 | 137 | if (count($settings)) { 138 | $parameters = array_merge(compact('settings'), $parameters); 139 | } 140 | 141 | return $this->rpcEndpointRequest('sharing/create_shared_link_with_settings', $parameters); 142 | } 143 | 144 | /** 145 | * Search a file or folder in the user's Dropbox. 146 | * 147 | * @return array 148 | * 149 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-search 150 | */ 151 | public function search(string $query, bool $includeHighlights = false): array 152 | { 153 | $parameters = [ 154 | 'query' => $query, 155 | 'include_highlights' => $includeHighlights, 156 | ]; 157 | 158 | return $this->rpcEndpointRequest('files/search_v2', $parameters); 159 | } 160 | 161 | /** 162 | * List shared links. 163 | * 164 | * For empty path returns a list of all shared links. For non-empty path 165 | * returns a list of all shared links with access to the given path. 166 | * 167 | * If direct_only is set true, only direct links to the path will be returned, otherwise 168 | * it may return link to the path itself and parent folders as described on docs. 169 | * 170 | * @return array 171 | * 172 | * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links 173 | */ 174 | public function listSharedLinks(?string $path = null, bool $direct_only = false, ?string $cursor = null): array 175 | { 176 | $parameters = [ 177 | 'path' => $path ? $this->normalizePath($path) : null, 178 | 'cursor' => $cursor, 179 | 'direct_only' => $direct_only, 180 | ]; 181 | 182 | $body = $this->rpcEndpointRequest('sharing/list_shared_links', $parameters); 183 | 184 | return $body['links']; 185 | } 186 | 187 | /** 188 | * Delete the file or folder at a given path. 189 | * 190 | * If the path is a folder, all its contents will be deleted too. 191 | * A successful response indicates that the file or folder was deleted. 192 | * 193 | * @return array 194 | * 195 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete 196 | */ 197 | public function delete(string $path): array 198 | { 199 | $parameters = [ 200 | 'path' => $this->normalizePath($path), 201 | ]; 202 | 203 | return $this->rpcEndpointRequest('files/delete', $parameters); 204 | } 205 | 206 | /** 207 | * Download a file from a user's Dropbox. 208 | * 209 | * 210 | * @return resource 211 | * 212 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download 213 | */ 214 | public function download(string $path) 215 | { 216 | $arguments = [ 217 | 'path' => $this->normalizePath($path), 218 | ]; 219 | 220 | $response = $this->contentEndpointRequest('files/download', $arguments); 221 | 222 | return StreamWrapper::getResource($response->getBody()); 223 | } 224 | 225 | /** 226 | * Download a folder from the user's Dropbox, as a zip file. 227 | * The folder must be less than 20 GB in size and have fewer than 10,000 total files. 228 | * The input cannot be a single file. Any single file must be less than 4GB in size. 229 | * 230 | * 231 | * @return resource 232 | * 233 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download_zip 234 | */ 235 | public function downloadZip(string $path) 236 | { 237 | $arguments = [ 238 | 'path' => $this->normalizePath($path), 239 | ]; 240 | 241 | $response = $this->contentEndpointRequest('files/download_zip', $arguments); 242 | 243 | return StreamWrapper::getResource($response->getBody()); 244 | } 245 | 246 | /** 247 | * Returns the metadata for a file or folder. 248 | * 249 | * Note: Metadata for the root folder is unsupported. 250 | * 251 | * @return array 252 | * 253 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata 254 | */ 255 | public function getMetadata(string $path): array 256 | { 257 | $parameters = [ 258 | 'path' => $this->normalizePath($path), 259 | ]; 260 | 261 | return $this->rpcEndpointRequest('files/get_metadata', $parameters); 262 | } 263 | 264 | /** 265 | * Get a temporary link to stream content of a file. 266 | * 267 | * This link will expire in four hours and afterwards you will get 410 Gone. 268 | * Content-Type of the link is determined automatically by the file's mime type. 269 | * 270 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link 271 | */ 272 | public function getTemporaryLink(string $path): string 273 | { 274 | $parameters = [ 275 | 'path' => $this->normalizePath($path), 276 | ]; 277 | 278 | $body = $this->rpcEndpointRequest('files/get_temporary_link', $parameters); 279 | 280 | return $body['link']; 281 | } 282 | 283 | /** 284 | * Get a thumbnail for an image. 285 | * 286 | * This method currently supports files with the following file extensions: 287 | * jpg, jpeg, png, tiff, tif, gif and bmp. 288 | * 289 | * Photos that are larger than 20MB in size won't be converted to a thumbnail. 290 | * 291 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail 292 | */ 293 | public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64'): string 294 | { 295 | $arguments = [ 296 | 'path' => $this->normalizePath($path), 297 | 'format' => $format, 298 | 'size' => $size, 299 | ]; 300 | 301 | $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments); 302 | 303 | return (string) $response->getBody(); 304 | } 305 | 306 | /** 307 | * Starts returning the contents of a folder. 308 | * 309 | * If the result's ListFolderResult.has_more field is true, call 310 | * list_folder/continue with the returned ListFolderResult.cursor to retrieve more entries. 311 | * 312 | * Note: auth.RateLimitError may be returned if multiple list_folder or list_folder/continue calls 313 | * with same parameters are made simultaneously by same API app for same user. If your app implements 314 | * retry logic, please hold off the retry until the previous request finishes. 315 | * 316 | * @return array 317 | * 318 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder 319 | */ 320 | public function listFolder(string $path = '', bool $recursive = false): array 321 | { 322 | $parameters = [ 323 | 'path' => $this->normalizePath($path), 324 | 'recursive' => $recursive, 325 | ]; 326 | 327 | return $this->rpcEndpointRequest('files/list_folder', $parameters); 328 | } 329 | 330 | /** 331 | * Once a cursor has been retrieved from list_folder, use this to paginate through all files and 332 | * retrieve updates to the folder, following the same rules as documented for list_folder. 333 | * 334 | * @return array 335 | * 336 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder-continue 337 | */ 338 | public function listFolderContinue(string $cursor = ''): array 339 | { 340 | return $this->rpcEndpointRequest('files/list_folder/continue', compact('cursor')); 341 | } 342 | 343 | /** 344 | * Move a file or folder to a different location in the user's Dropbox. 345 | * 346 | * If the source path is a folder all its contents will be moved. 347 | * 348 | * @return array 349 | * 350 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-move_v2 351 | */ 352 | public function move(string $fromPath, string $toPath, bool $autorename = false): array 353 | { 354 | $parameters = [ 355 | 'from_path' => $this->normalizePath($fromPath), 356 | 'to_path' => $this->normalizePath($toPath), 357 | 'autorename' => $autorename, 358 | ]; 359 | 360 | return $this->rpcEndpointRequest('files/move_v2', $parameters); 361 | } 362 | 363 | /** 364 | * The file should be uploaded in chunks if its size exceeds the 150 MB threshold 365 | * or if the resource size could not be determined (eg. a popen() stream). 366 | * 367 | * @param string|resource $contents 368 | */ 369 | protected function shouldUploadChunked(mixed $contents): bool 370 | { 371 | $size = is_string($contents) ? strlen($contents) : fstat($contents)['size']; 372 | 373 | if ($this->isPipe($contents)) { 374 | return true; 375 | } 376 | 377 | return $size > $this->maxChunkSize; 378 | } 379 | 380 | /** 381 | * Check if the contents is a pipe stream (not seekable, no size defined). 382 | * 383 | * @param string|resource $contents 384 | */ 385 | protected function isPipe(mixed $contents): bool 386 | { 387 | return is_resource($contents) && (fstat($contents)['mode'] & 010000) != 0; 388 | } 389 | 390 | /** 391 | * Create a new file with the contents provided in the request. 392 | * 393 | * Do not use this to upload a file larger than 150 MB. Instead, create an upload session with upload_session/start. 394 | * 395 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload 396 | * 397 | * @param string|resource $contents 398 | * @return array 399 | */ 400 | public function upload(string $path, mixed $contents, string $mode = 'add', bool $autorename = false): array 401 | { 402 | if ($this->shouldUploadChunked($contents)) { 403 | return $this->uploadChunked($path, $contents, $mode); 404 | } 405 | 406 | $arguments = [ 407 | 'path' => $this->normalizePath($path), 408 | 'mode' => $mode, 409 | 'autorename' => $autorename, 410 | ]; 411 | 412 | $response = $this->contentEndpointRequest('files/upload', $arguments, $contents); 413 | 414 | $metadata = json_decode($response->getBody(), true); 415 | 416 | $metadata['.tag'] = 'file'; 417 | 418 | return $metadata; 419 | } 420 | 421 | /** 422 | * Upload file split in chunks. This allows uploading large files, since 423 | * Dropbox API v2 limits the content size to 150MB. 424 | * 425 | * The chunk size will affect directly the memory usage, so be careful. 426 | * Large chunks tends to speed up the upload, while smaller optimizes memory usage. 427 | * 428 | * @param string|resource $contents 429 | * @return array 430 | */ 431 | public function uploadChunked(string $path, mixed $contents, string $mode = 'add', ?int $chunkSize = null): array 432 | { 433 | if ($chunkSize === null || $chunkSize > $this->maxChunkSize) { 434 | $chunkSize = $this->maxChunkSize; 435 | } 436 | 437 | $stream = $this->getStream($contents); 438 | 439 | $cursor = $this->uploadChunk(self::UPLOAD_SESSION_START, $stream, $chunkSize, null); 440 | 441 | while (! $stream->eof()) { 442 | $cursor = $this->uploadChunk(self::UPLOAD_SESSION_APPEND, $stream, $chunkSize, $cursor); 443 | } 444 | 445 | return $this->uploadSessionFinish('', $cursor, $path, $mode); 446 | } 447 | 448 | /** 449 | * @throws Exception 450 | */ 451 | protected function uploadChunk(int $type, StreamInterface &$stream, int $chunkSize, ?UploadSessionCursor $cursor = null): UploadSessionCursor 452 | { 453 | $maximumTries = $stream->isSeekable() ? $this->maxUploadChunkRetries : 0; 454 | $pos = $stream->tell(); 455 | 456 | $tries = 0; 457 | 458 | tryUpload: 459 | try { 460 | $tries++; 461 | 462 | $chunkStream = new Psr7\LimitStream($stream, $chunkSize, $stream->tell()); 463 | 464 | if ($type === self::UPLOAD_SESSION_START) { 465 | return $this->uploadSessionStart($chunkStream); 466 | } 467 | 468 | if ($type === self::UPLOAD_SESSION_APPEND && $cursor !== null) { 469 | return $this->uploadSessionAppend($chunkStream, $cursor); 470 | } 471 | 472 | throw new Exception('Invalid type'); 473 | } catch (RequestException $exception) { 474 | if ($tries < $maximumTries) { 475 | // rewind 476 | $stream->seek($pos, SEEK_SET); 477 | goto tryUpload; 478 | } 479 | 480 | throw $exception; 481 | } 482 | } 483 | 484 | /** 485 | * Upload sessions allow you to upload a single file in one or more requests, 486 | * for example where the size of the file is greater than 150 MB. 487 | * This call starts a new upload session with the given data. 488 | * 489 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start 490 | * 491 | * @param string|resource|StreamInterface $contents 492 | */ 493 | public function uploadSessionStart(mixed $contents, bool $close = false): UploadSessionCursor 494 | { 495 | $arguments = compact('close'); 496 | 497 | $response = json_decode( 498 | $this->contentEndpointRequest('files/upload_session/start', $arguments, $contents)->getBody(), 499 | true 500 | ); 501 | 502 | return new UploadSessionCursor($response['session_id'], ($contents instanceof StreamInterface ? $contents->tell() : strlen($contents))); 503 | } 504 | 505 | /** 506 | * Append more data to an upload session. 507 | * When the parameter close is set, this call will close the session. 508 | * A single request should not upload more than 150 MB. 509 | * 510 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append_v2 511 | */ 512 | public function uploadSessionAppend(string|StreamInterface $contents, UploadSessionCursor $cursor, bool $close = false): UploadSessionCursor 513 | { 514 | $arguments = compact('cursor', 'close'); 515 | 516 | $pos = $contents instanceof StreamInterface ? $contents->tell() : 0; 517 | $this->contentEndpointRequest('files/upload_session/append_v2', $arguments, $contents); 518 | 519 | $cursor->offset += $contents instanceof StreamInterface ? ($contents->tell() - $pos) : strlen($contents); 520 | 521 | return $cursor; 522 | } 523 | 524 | /** 525 | * Finish an upload session and save the uploaded data to the given file path. 526 | * A single request should not upload more than 150 MB. 527 | * 528 | * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish 529 | * 530 | * @param string|resource|StreamInterface $contents 531 | * @return array 532 | */ 533 | public function uploadSessionFinish(mixed $contents, UploadSessionCursor $cursor, string $path, string $mode = 'add', bool $autorename = false, bool $mute = false): array 534 | { 535 | $arguments = compact('cursor'); 536 | $arguments['commit'] = compact('path', 'mode', 'autorename', 'mute'); 537 | 538 | $response = $this->contentEndpointRequest( 539 | 'files/upload_session/finish', 540 | $arguments, 541 | ($contents == '') ? null : $contents 542 | ); 543 | 544 | $metadata = json_decode($response->getBody(), true); 545 | 546 | $metadata['.tag'] = 'file'; 547 | 548 | return $metadata; 549 | } 550 | 551 | /** 552 | * Get Account Info for current authenticated user. 553 | * 554 | * @return array 555 | * 556 | * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account 557 | */ 558 | public function getAccountInfo(): array 559 | { 560 | return $this->rpcEndpointRequest('users/get_current_account'); 561 | } 562 | 563 | /** 564 | * Revoke current access token. 565 | * 566 | * @link https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke 567 | */ 568 | public function revokeToken(): void 569 | { 570 | $this->rpcEndpointRequest('auth/token/revoke'); 571 | } 572 | 573 | protected function normalizePath(string $path): string 574 | { 575 | if (preg_match("/^id:.*|^rev:.*|^(ns:[0-9]+(\/.*)?)/", $path) === 1) { 576 | return $path; 577 | } 578 | 579 | $path = trim($path, '/'); 580 | 581 | return ($path === '') ? '' : '/'.$path; 582 | } 583 | 584 | protected function getEndpointUrl(string $subdomain, string $endpoint): string 585 | { 586 | if (count($parts = explode('::', $endpoint)) === 2) { 587 | [$subdomain, $endpoint] = $parts; 588 | } 589 | 590 | return "https://{$subdomain}.dropboxapi.com/2/{$endpoint}"; 591 | } 592 | 593 | /** 594 | * @param array $arguments 595 | * @param string|resource|StreamInterface $body 596 | * 597 | * @throws \Exception 598 | */ 599 | public function contentEndpointRequest(string $endpoint, array $arguments, mixed $body = '', bool $isRefreshed = false): ResponseInterface 600 | { 601 | $headers = ['Dropbox-API-Arg' => json_encode($arguments)]; 602 | 603 | if ($body !== '') { 604 | $headers['Content-Type'] = 'application/octet-stream'; 605 | } 606 | 607 | try { 608 | $response = $this->client->request('POST', $this->getEndpointUrl('content', $endpoint), [ 609 | 'headers' => $this->getHeaders($headers), 610 | 'body' => $body, 611 | ]); 612 | } catch (ClientException $exception) { 613 | if ( 614 | $isRefreshed 615 | || ! $this->tokenProvider instanceof RefreshableTokenProvider 616 | || ! $this->tokenProvider->refresh($exception) 617 | ) { 618 | throw $this->determineException($exception); 619 | } 620 | $response = $this->contentEndpointRequest($endpoint, $arguments, $body, true); 621 | } 622 | 623 | return $response; 624 | } 625 | 626 | /** 627 | * @param array>|null $parameters 628 | * @return array 629 | */ 630 | public function rpcEndpointRequest(string $endpoint, ?array $parameters = null, bool $isRefreshed = false): array 631 | { 632 | try { 633 | $options = ['headers' => $this->getHeaders()]; 634 | 635 | if ($parameters) { 636 | $options['json'] = $parameters; 637 | } 638 | 639 | $response = $this->client->request('POST', $this->getEndpointUrl('api', $endpoint), $options); 640 | } catch (ClientException $exception) { 641 | if ( 642 | $isRefreshed 643 | || ! $this->tokenProvider instanceof RefreshableTokenProvider 644 | || ! $this->tokenProvider->refresh($exception) 645 | ) { 646 | throw $this->determineException($exception); 647 | } 648 | 649 | return $this->rpcEndpointRequest($endpoint, $parameters, true); 650 | } 651 | 652 | return json_decode($response->getBody(), true) ?? []; 653 | } 654 | 655 | protected function determineException(ClientException $exception): Exception 656 | { 657 | if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) { 658 | return new BadRequest($exception->getResponse()); 659 | } 660 | 661 | return $exception; 662 | } 663 | 664 | /** 665 | * @param string|resource $contents 666 | */ 667 | protected function getStream(mixed $contents): StreamInterface 668 | { 669 | if ($this->isPipe($contents)) { 670 | /* @var resource $contents */ 671 | return new PumpStream(function ($length) use ($contents) { 672 | $data = fread($contents, $length); 673 | if (strlen($data) === 0) { 674 | return false; 675 | } 676 | 677 | return $data; 678 | }); 679 | } 680 | 681 | return Psr7\Utils::streamFor($contents); 682 | } 683 | 684 | /** 685 | * Get the access token. 686 | */ 687 | public function getAccessToken(): string 688 | { 689 | return $this->tokenProvider->getToken(); 690 | } 691 | 692 | /** 693 | * Set the access token. 694 | */ 695 | public function setAccessToken(string $accessToken): self 696 | { 697 | $this->tokenProvider = new InMemoryTokenProvider($accessToken); 698 | 699 | return $this; 700 | } 701 | 702 | /** 703 | * Set the namespace ID. 704 | */ 705 | public function setNamespaceId(string $namespaceId): self 706 | { 707 | $this->namespaceId = $namespaceId; 708 | 709 | return $this; 710 | } 711 | 712 | /** 713 | * Get the HTTP headers. 714 | * 715 | * @param array $headers 716 | * @return array 717 | */ 718 | protected function getHeaders(array $headers = []): array 719 | { 720 | $auth = []; 721 | if ($this->tokenProvider || ($this->appKey && $this->appSecret)) { 722 | $auth = $this->tokenProvider 723 | ? $this->getHeadersForBearerToken($this->tokenProvider->getToken()) 724 | : $this->getHeadersForCredentials(); 725 | } 726 | 727 | if ($this->teamMemberId) { 728 | $auth = array_merge( 729 | $auth, 730 | [ 731 | 'Dropbox-API-Select-User' => $this->teamMemberId, 732 | ] 733 | ); 734 | } 735 | 736 | if ($this->namespaceId) { 737 | $headers = array_merge( 738 | $headers, 739 | [ 740 | 'Dropbox-API-Path-Root' => json_encode([ 741 | '.tag' => 'namespace_id', 742 | 'namespace_id' => $this->namespaceId, 743 | ]), 744 | ] 745 | ); 746 | } 747 | 748 | return array_merge($auth, $headers); 749 | } 750 | 751 | /** 752 | * @return array{Authorization: string} 753 | */ 754 | protected function getHeadersForBearerToken(string $token): array 755 | { 756 | return [ 757 | 'Authorization' => "Bearer {$token}", 758 | ]; 759 | } 760 | 761 | /** 762 | * @return array{Authorization: string} 763 | */ 764 | protected function getHeadersForCredentials(): array 765 | { 766 | return [ 767 | 'Authorization' => 'Basic '.base64_encode("{$this->appKey}:{$this->appSecret}"), 768 | ]; 769 | } 770 | } 771 | --------------------------------------------------------------------------------