├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── composer.json ├── phpunit.hhvm.xml ├── phpunit.php ├── phpunit.xml ├── readme.md ├── src └── DropboxAdapter.php └── tests └── DropboxAdapterTests.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | bin 3 | composer.lock 4 | coverage 5 | coverage.xml -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | checks: 4 | php: 5 | code_rating: true 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 | tools: 20 | external_code_coverage: 21 | timeout: 600 22 | runs: 3 23 | php_code_coverage: false 24 | php_code_sniffer: 25 | config: 26 | standard: PSR2 27 | filter: 28 | paths: ['src'] 29 | php_loc: 30 | enabled: true 31 | excluded_dirs: [vendor, spec, stubs] 32 | php_cpd: 33 | enabled: true 34 | excluded_dirs: [vendor, spec, stubs] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | 9 | install: 10 | - travis_retry composer install --no-interaction --prefer-source 11 | 12 | script: 13 | - bin/phpunit 14 | 15 | after_script: 16 | - wget https://scrutinizer-ci.com/ocular.phar 17 | - bash -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.xml; fi;' 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/flysystem-dropbox", 3 | "description": "Flysystem adapter for Dropbox", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Frank de Jonge", 8 | "email": "info@frenky.net" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.4.0", 13 | "league/flysystem": "~1.0", 14 | "dropbox/dropbox-sdk": "~1.1" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "~4.8" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "League\\Flysystem\\Dropbox\\": "src/" 22 | } 23 | }, 24 | "config": { 25 | "bin-dir": "bin" 26 | }, 27 | "min-stability": "dev", 28 | "prefer-stable": true, 29 | "extra": { 30 | "branch-alias": { 31 | "dev-master": "1.0-dev" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.hhvm.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Flysystem Adapter for Dropbox 2 | 3 | [![Author](http://img.shields.io/badge/author-@frankdejonge-blue.svg?style=flat-square)](https://twitter.com/frankdejonge) 4 | [![Build Status](https://img.shields.io/travis/thephpleague/flysystem-dropbox/master.svg?style=flat-square)](https://travis-ci.org/thephpleague/flysystem-dropbox) 5 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/flysystem-dropbox.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/flysystem-dropbox/code-structure) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/flysystem-dropbox.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/flysystem-dropbox) 7 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 8 | [![Packagist Version](https://img.shields.io/packagist/v/league/flysystem-dropbox.svg?style=flat-square)](https://packagist.org/packages/league/flysystem-dropbox) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/league/flysystem-dropbox.svg?style=flat-square)](https://packagist.org/packages/league/flysystem-dropbox) 10 | 11 | 12 | ## Installation 13 | 14 | ```bash 15 | composer require league/flysystem-dropbox 16 | ``` 17 | 18 | ## Usage 19 | 20 | Visit https://www.dropbox.com/developers/apps and get your "App secret". 21 | 22 | You can also generate OAuth access token for testing using the Dropbox App Console without going through the authorization flow. 23 | 24 | ~~~ php 25 | use League\Flysystem\Dropbox\DropboxAdapter; 26 | use League\Flysystem\Filesystem; 27 | use Dropbox\Client; 28 | 29 | $client = new Client($accessToken, $appSecret); 30 | $adapter = new DropboxAdapter($client, [$prefix]); 31 | 32 | $filesystem = new Filesystem($adapter); 33 | ~~~ 34 | -------------------------------------------------------------------------------- /src/DropboxAdapter.php: -------------------------------------------------------------------------------- 1 | 'size', 23 | 'mime_type' => 'mimetype', 24 | ]; 25 | 26 | /** 27 | * @var Client 28 | */ 29 | protected $client; 30 | 31 | /** 32 | * Constructor. 33 | * 34 | * @param Client $client 35 | * @param string $prefix 36 | */ 37 | public function __construct(Client $client, $prefix = null) 38 | { 39 | $this->client = $client; 40 | $this->setPathPrefix($prefix); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function has($path) 47 | { 48 | return $this->getMetadata($path); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function write($path, $contents, Config $config) 55 | { 56 | return $this->upload($path, $contents, WriteMode::add()); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function writeStream($path, $resource, Config $config) 63 | { 64 | return $this->uploadStream($path, $resource, WriteMode::add()); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function update($path, $contents, Config $config) 71 | { 72 | return $this->upload($path, $contents, WriteMode::force()); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function updateStream($path, $resource, Config $config) 79 | { 80 | return $this->uploadStream($path, $resource, WriteMode::force()); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function read($path) 87 | { 88 | if ( ! $object = $this->readStream($path)) { 89 | return false; 90 | } 91 | 92 | $object['contents'] = stream_get_contents($object['stream']); 93 | fclose($object['stream']); 94 | unset($object['stream']); 95 | 96 | return $object; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function readStream($path) 103 | { 104 | $stream = fopen('php://temp', 'w+'); 105 | $location = $this->applyPathPrefix($path); 106 | 107 | if ( ! $this->client->getFile($location, $stream)) { 108 | fclose($stream); 109 | 110 | return false; 111 | } 112 | 113 | rewind($stream); 114 | 115 | return compact('stream'); 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function rename($path, $newpath) 122 | { 123 | $path = $this->applyPathPrefix($path); 124 | $newpath = $this->applyPathPrefix($newpath); 125 | 126 | try { 127 | $this->client->move($path, $newpath); 128 | } catch (Exception $e) { 129 | return false; 130 | } 131 | 132 | return true; 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function copy($path, $newpath) 139 | { 140 | $path = $this->applyPathPrefix($path); 141 | $newpath = $this->applyPathPrefix($newpath); 142 | 143 | try { 144 | $this->client->copy($path, $newpath); 145 | } catch (Exception $e) { 146 | return false; 147 | } 148 | 149 | return true; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function delete($path) 156 | { 157 | $location = $this->applyPathPrefix($path); 158 | $result = $this->client->delete($location); 159 | 160 | return isset($result['is_deleted']) ? $result['is_deleted'] : false; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function deleteDir($path) 167 | { 168 | return $this->delete($path); 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function createDir($path, Config $config) 175 | { 176 | $location = $this->applyPathPrefix($path); 177 | $result = $this->client->createFolder($location); 178 | 179 | if ($result === null) { 180 | return false; 181 | } 182 | 183 | return $this->normalizeResponse($result, $path); 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function getMetadata($path) 190 | { 191 | $location = $this->applyPathPrefix($path); 192 | 193 | try { 194 | $object = $this->client->getMetadata($location); 195 | } catch(Exception_BadResponseCode $e) { 196 | if ($e->getStatusCode() === 301) { 197 | return false; 198 | } 199 | 200 | throw $e; 201 | } 202 | 203 | if ( ! $object) { 204 | return false; 205 | } 206 | 207 | return $this->normalizeResponse($object, $path); 208 | } 209 | 210 | /** 211 | * {@inheritdoc} 212 | */ 213 | public function getMimetype($path) 214 | { 215 | return $this->getMetadata($path); 216 | } 217 | 218 | /** 219 | * {@inheritdoc} 220 | */ 221 | public function getSize($path) 222 | { 223 | return $this->getMetadata($path); 224 | } 225 | 226 | /** 227 | * {@inheritdoc} 228 | */ 229 | public function getTimestamp($path) 230 | { 231 | return $this->getMetadata($path); 232 | } 233 | 234 | /** 235 | * {@inheritdoc} 236 | */ 237 | public function getClient() 238 | { 239 | return $this->client; 240 | } 241 | 242 | /** 243 | * {@inheritdoc} 244 | */ 245 | public function listContents($directory = '', $recursive = false) 246 | { 247 | $listing = []; 248 | $directory = trim($directory, '/.'); 249 | $location = $this->applyPathPrefix($directory); 250 | 251 | if ( ! $result = $this->client->getMetadataWithChildren($location)) { 252 | return []; 253 | } 254 | 255 | foreach ($result['contents'] as $object) { 256 | $path = $this->removePathPrefix($object['path']); 257 | $listing[] = $this->normalizeResponse($object, $path); 258 | 259 | if ($recursive && $object['is_dir']) { 260 | $listing = array_merge($listing, $this->listContents($path, true)); 261 | } 262 | } 263 | 264 | return $listing; 265 | } 266 | 267 | /** 268 | * Apply the path prefix. 269 | * 270 | * @param string $path 271 | * 272 | * @return string prefixed path 273 | */ 274 | public function applyPathPrefix($path) 275 | { 276 | 277 | $path = parent::applyPathPrefix($path); 278 | 279 | return '/' . ltrim(rtrim($path, '/'), '/'); 280 | } 281 | 282 | /** 283 | * Do the actual upload of a string file. 284 | * 285 | * @param string $path 286 | * @param string $contents 287 | * @param WriteMode $mode 288 | * 289 | * @return array|false file metadata 290 | */ 291 | protected function upload($path, $contents, WriteMode $mode) 292 | { 293 | $location = $this->applyPathPrefix($path); 294 | 295 | if ( ! $result = $this->client->uploadFileFromString($location, $mode, $contents)) { 296 | return false; 297 | } 298 | 299 | return $this->normalizeResponse($result, $path); 300 | } 301 | 302 | /** 303 | * Do the actual upload of a file resource. 304 | * 305 | * @param string $path 306 | * @param resource $resource 307 | * @param WriteMode $mode 308 | * 309 | * @return array|false file metadata 310 | */ 311 | protected function uploadStream($path, $resource, WriteMode $mode) 312 | { 313 | $location = $this->applyPathPrefix($path); 314 | 315 | // If size is zero, consider it unknown. 316 | $size = Util::getStreamSize($resource) ?: null; 317 | 318 | if ( ! $result = $this->client->uploadFile($location, $mode, $resource, $size)) { 319 | return false; 320 | } 321 | 322 | return $this->normalizeResponse($result, $path); 323 | } 324 | 325 | /** 326 | * Normalize a Dropbox response. 327 | * 328 | * @param array $response 329 | * 330 | * @return array 331 | */ 332 | protected function normalizeResponse(array $response) 333 | { 334 | $result = ['path' => ltrim($this->removePathPrefix($response['path']), '/')]; 335 | 336 | if (isset($response['modified'])) { 337 | $result['timestamp'] = strtotime($response['modified']); 338 | } 339 | 340 | $result = array_merge($result, Util::map($response, static::$resultMap)); 341 | $result['type'] = $response['is_dir'] ? 'dir' : 'file'; 342 | 343 | return $result; 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /tests/DropboxAdapterTests.php: -------------------------------------------------------------------------------- 1 | prophesize('Dropbox\Client'); 14 | 15 | return [ 16 | [new Dropbox($mock->reveal(), 'prefix'), $mock], 17 | ]; 18 | } 19 | 20 | /** 21 | * @dataProvider dropboxProvider 22 | */ 23 | public function testWrite($adapter, $mock) 24 | { 25 | $mock->uploadFileFromString(Argument::any(), Argument::any(), Argument::any())->willReturn([ 26 | 'is_dir' => false, 27 | 'modified' => '10 September 2000', 28 | 'path' => '/prefix/something', 29 | ], false); 30 | 31 | $result = $adapter->write('something', 'contents', new Config()); 32 | $this->assertInternalType('array', $result); 33 | $this->assertArrayHasKey('type', $result); 34 | $this->assertEquals('file', $result['type']); 35 | $this->assertFalse($adapter->write('something', 'something', new Config())); 36 | } 37 | 38 | /** 39 | * @dataProvider dropboxProvider 40 | */ 41 | public function testUpdate(Dropbox $adapter, $mock) 42 | { 43 | $mock->uploadFileFromString(Argument::any(), Argument::any(), Argument::any())->willReturn([ 44 | 'is_dir' => false, 45 | 'modified' => '10 September 2000', 46 | 'path' => '/prefix/something' 47 | ], false); 48 | 49 | $result = $adapter->update('something', 'contents', new Config()); 50 | $this->assertInternalType('array', $result); 51 | $this->assertArrayHasKey('type', $result); 52 | $this->assertEquals('file', $result['type']); 53 | $this->assertFalse($adapter->update('something', 'something', new Config())); 54 | } 55 | 56 | /** 57 | * @dataProvider dropboxProvider 58 | */ 59 | public function testWriteStream(Dropbox $adapter, $mock) 60 | { 61 | $mock->uploadFile(Argument::any(), Argument::any(), Argument::any(), null)->willReturn([ 62 | 'is_dir' => false, 63 | 'modified' => '10 September 2000', 64 | 'path' => '/prefix/something' 65 | ], false); 66 | 67 | $result = $adapter->writeStream('something', tmpfile(), new Config()); 68 | $this->assertInternalType('array', $result); 69 | $this->assertArrayHasKey('type', $result); 70 | $this->assertEquals('file', $result['type']); 71 | $this->assertFalse($adapter->writeStream('something', tmpfile(), new Config())); 72 | } 73 | 74 | /** 75 | * @dataProvider dropboxProvider 76 | */ 77 | public function testUpdateStream(Dropbox $adapter, $mock) 78 | { 79 | $mock->uploadFile(Argument::any(), Argument::any(), Argument::any(), null)->willReturn([ 80 | 'is_dir' => false, 81 | 'modified' => '10 September 2000', 82 | 'path' => '/prefix/something' 83 | ], false); 84 | 85 | $result = $adapter->updateStream('something', tmpfile(), new Config()); 86 | $this->assertInternalType('array', $result); 87 | $this->assertArrayHasKey('type', $result); 88 | $this->assertEquals('file', $result['type']); 89 | $this->assertFalse($adapter->updateStream('something', tmpfile(), new Config())); 90 | } 91 | 92 | public function metadataProvider() 93 | { 94 | return [ 95 | ['getMetadata'], 96 | ['getMimetype'], 97 | ['getTimestamp'], 98 | ['getSize'], 99 | ['has'], 100 | ]; 101 | } 102 | 103 | /** 104 | * @dataProvider metadataProvider 105 | */ 106 | public function testMetadataCalls($method) 107 | { 108 | $mock = $this->prophesize('Dropbox\Client'); 109 | $mock->getMetadata('/one')->willReturn([ 110 | 'is_dir' => false, 111 | 'modified' => '10 September 2000', 112 | 'path' => '/one' 113 | ], false); 114 | 115 | $adapter = new Dropbox($mock->reveal()); 116 | $this->assertInternalType('array', $adapter->{$method}('one', 'two')); 117 | $this->assertFalse($adapter->{$method}('one', 'two')); 118 | } 119 | 120 | public function testMetadataFileWasMovedFailure() 121 | { 122 | $mock = $this->prophesize('Dropbox\Client'); 123 | $mock->getMetadata('/one')->willThrow(new Exception_BadResponseCode('ERROR', 301)); 124 | 125 | $adapter = new Dropbox($mock->reveal()); 126 | $this->assertFalse($adapter->has('one')); 127 | } 128 | 129 | public function testMetadataFileWasNotMovedFailure() 130 | { 131 | $this->setExpectedException('Dropbox\Exception_BadResponseCode'); 132 | $mock = $this->prophesize('Dropbox\Client'); 133 | $mock->getMetadata('/one')->willThrow(new Exception_BadResponseCode('ERROR', 500)); 134 | 135 | (new Dropbox($mock->reveal()))->has('one'); 136 | } 137 | 138 | /** 139 | * @dataProvider dropboxProvider 140 | */ 141 | public function testRead($adapter, $mock) 142 | { 143 | $stream = tmpfile(); 144 | fwrite($stream, 'something'); 145 | $mock->getFile(Argument::any(), Argument::any())->willReturn($stream, false); 146 | $this->assertInternalType('array', $adapter->read('something')); 147 | $this->assertFalse($adapter->read('something')); 148 | fclose($stream); 149 | } 150 | 151 | /** 152 | * @dataProvider dropboxProvider 153 | */ 154 | public function testReadStream(Dropbox $adapter, $mock) 155 | { 156 | $stream = tmpfile(); 157 | fwrite($stream, 'something'); 158 | $mock->getFile(Argument::any(), Argument::any())->willReturn($stream, false); 159 | $this->assertInternalType('array', $adapter->readStream('something')); 160 | $this->assertFalse($adapter->readStream('something')); 161 | fclose($stream); 162 | } 163 | 164 | /** 165 | * @dataProvider dropboxProvider 166 | */ 167 | public function testDelete(Dropbox $adapter, $mock) 168 | { 169 | $mock->delete('/prefix/something')->willReturn(['is_deleted' => true]); 170 | $this->assertTrue($adapter->delete('something')); 171 | $this->assertTrue($adapter->deleteDir('something')); 172 | } 173 | 174 | /** 175 | * @dataProvider dropboxProvider 176 | */ 177 | public function testCreateDir(Dropbox $adapter, $mock) 178 | { 179 | $mock->createFolder('/prefix/fail/please')->willReturn(null); 180 | $mock->createFolder('/prefix/pass/please')->willReturn([ 181 | 'is_dir' => true, 182 | 'path' => '/prefix/pass/please', 183 | ]); 184 | $this->assertFalse($adapter->createDir('fail/please', new Config())); 185 | $expected = ['path' => 'pass/please', 'type' => 'dir']; 186 | $this->assertEquals($expected, $adapter->createDir('pass/please', new Config())); 187 | } 188 | 189 | /** 190 | * @dataProvider dropboxProvider 191 | */ 192 | public function testListContents(Dropbox $adapter, $mock) 193 | { 194 | $mock->getMetadataWithChildren(Argument::type('string'))->willReturn( 195 | ['contents' => [ 196 | ['is_dir' => true, 'path' => 'dirname'], 197 | ]], 198 | ['contents' => [ 199 | ['is_dir' => false, 'path' => 'dirname/file'], 200 | ]], 201 | false 202 | ); 203 | 204 | $result = $adapter->listContents('', true); 205 | $this->assertCount(2, $result); 206 | $this->assertEquals([], $adapter->listContents('', false)); 207 | } 208 | 209 | /** 210 | * @dataProvider dropboxProvider 211 | */ 212 | public function testRename($adapter, $mock) 213 | { 214 | $mock->move(Argument::type('string'), Argument::type('string'))->willReturn(['is_dir' => false, 'path' => 'something']); 215 | $this->assertTrue($adapter->rename('something', 'something')); 216 | } 217 | 218 | /** 219 | * @dataProvider dropboxProvider 220 | */ 221 | public function testRenameFail($adapter, $mock) 222 | { 223 | $mock->move('/prefix/something', '/prefix/something')->willThrow(new \Dropbox\Exception('Message')); 224 | 225 | $this->assertFalse($adapter->rename('something', 'something')); 226 | } 227 | 228 | /** 229 | * @dataProvider dropboxProvider 230 | */ 231 | public function testCopy($adapter, $mock) 232 | { 233 | $mock->copy(Argument::type('string'), Argument::type('string'))->willReturn(['is_dir' => false, 'path' => 'something']); 234 | $this->assertTrue($adapter->copy('something', 'something')); 235 | } 236 | 237 | /** 238 | * @dataProvider dropboxProvider 239 | */ 240 | public function testCopyFail($adapter, $mock) 241 | { 242 | $mock->copy(Argument::any(), Argument::any())->willThrow(new \Dropbox\Exception('Message')); 243 | 244 | $this->assertFalse($adapter->copy('something', 'something')); 245 | } 246 | 247 | /** 248 | * @dataProvider dropboxProvider 249 | */ 250 | public function testGetClient($adapter) 251 | { 252 | $this->assertInstanceOf('Dropbox\Client', $adapter->getClient()); 253 | } 254 | } 255 | --------------------------------------------------------------------------------