├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin └── clouddrive ├── box.json ├── composer.json ├── composer.lock └── src └── CloudDrive ├── Account.php ├── Cache.php ├── Cache ├── MySQL.php ├── SQL.php └── SQLite.php ├── CloudDrive.php ├── Commands ├── CatCommand.php ├── ClearCacheCommand.php ├── Command.php ├── ConfigCommand.php ├── DiskUsageCommand.php ├── DownloadCommand.php ├── FindCommand.php ├── InitCommand.php ├── ListCommand.php ├── ListPendingCommand.php ├── ListTrashCommand.php ├── MetadataCommand.php ├── MkdirCommand.php ├── MoveCommand.php ├── QuotaCommand.php ├── RenameCommand.php ├── RenewCommand.php ├── ResolveCommand.php ├── RestoreCommand.php ├── SyncCommand.php ├── TempLinkCommand.php ├── TrashCommand.php ├── TreeCommand.php ├── UploadCommand.php └── UsageCommand.php └── Node.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | /.cache/* 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `clouddrive-php` will be documented in this file 4 | 5 | ## 0.1.1 6 | 7 | ### Added 8 | - Indices to database tables for improved performance 9 | - `composer.json` now has PHP version requirement 10 | - `box.json` file added for packaging as `.phar` 11 | - JSON output in CLI is now considered `verbose` and is not outputted unless desired 12 | - 'Success' and 'failure' messages are colored accordingly 13 | - A `callable` is now accepted to be passed into the `upload` command instead of a resource stream for writing 14 | - Added config value to allow duplicate file uploads in different locations (suppress dedup check in API) 15 | - Added support of the `ls` command with a direct file node to just display its information (pass `-a` flag to show its child assets) 16 | - Added `link` command to generate pre-authenticated temp links to share files 17 | - Added config value to suppress trashed nodes in `ls` output 18 | - Method in the `Node` class `inTrash` returns if the node's status is in the trash or not 19 | - Ability to now download FOLDER nodes 20 | 21 | ### Deprecated 22 | - Passing in a resource stream into the `upload` method has been replaced by a callable (see 'added') 23 | 24 | ### Fixed 25 | - `config` command was not properly outputting `bool` values when reading an individual item 26 | - `PHP` shebang path is now more universal in the `bin` file 27 | - Error messages now to through `STDERR` 28 | - MD5 queries return an array since multiple files can have the same MD5 with duplicate uploads enabled 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/:package_name). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 13 | 14 | - **Create feature branches** - Don't ask us to pull from your master branch. 15 | 16 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 17 | 18 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 19 | 20 | **Happy coding**! 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 :author_name <:author_email> 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 | # CloudDrive PHP 2 | 3 | NOTE: This project is in active development. 4 | 5 | CloudDrive-PHP is an SDK and CLI application for interacting with Amazon's [Cloud Drive](https://www.amazon.com/clouddrive/home). 6 | 7 | The project originally started out as an application to manage storage in Cloud Drive from the command line but figured other's may want to take advantage of the API calls and develop their own software using it, so I made sure to build the library so it can be built upon and make the CLI application an included tool that could also be used as an example for implementation. 8 | 9 | ## Install 10 | 11 | Via Composer (for use in a project) 12 | 13 | ``` 14 | $ composer require alex-phillips/clouddrive 15 | ``` 16 | 17 | Install globally to run the CLI from any location (as long as the global composer `bin` directory is in your `$PATH`). 18 | 19 | ``` 20 | $ composer global require alex-phillips/clouddrive 21 | ``` 22 | 23 | ## CLI 24 | 25 | ### Setup 26 | 27 | The first run of the CLI needs to authenticate your Amazon Cloud Drive account with the application using your Amazon Cloud Drive credentials. Use the `config` command to set these credentials as well as the email associated with your Amazon account. 28 | 29 | ``` 30 | $ clouddrive config email me@example.com 31 | $ clouddrive config client-id CLIENT_ID 32 | $ clouddrive config client-secret CLIENT_SECRET 33 | ``` 34 | 35 | Once the credentials are set, simply run the `init` command. This will provide you with an authentication URL to visit. Paste the URL you are redirected to into the terminal and press enter. 36 | 37 | ``` 38 | $ clouddrive init 39 | Initial authorization required. 40 | Navigate to the following URL and paste in the redirect URL here. 41 | https://www.amazon.com/ap/oa?client_id=CLIENT_ID&scope=clouddrive%3Aread_all%20clouddrive%3Awrite&response_type=code&redirect_uri=http://localhost 42 | ... 43 | Successfully authenticated with Amazon CloudDrive. 44 | ``` 45 | 46 | After you have been authenticated, run the `sync` command to sync your local cache with the current state of your Cloud Drive. 47 | 48 | ### Usage 49 | 50 | The CLI application relies on your local cache being in sync with your Cloud Drive, so run the `sync` command periodically to retrieve any changes since the last sync. You can view all available commands and usage of each command using the `-h` flag at any point. 51 | 52 | ``` 53 | Cloud Drive version 0.1.0 54 | 55 | Usage: 56 | command [options] [arguments] 57 | 58 | Options: 59 | -h, --help Display this help message 60 | -q, --quiet Do not output any message 61 | -V, --version Display this application version 62 | --ansi Force ANSI output 63 | --no-ansi Disable ANSI output 64 | -n, --no-interaction Do not ask any interactive question 65 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 66 | 67 | Available commands: 68 | cat Output a file to the standard output stream 69 | clear-cache Clear the local cache 70 | clearcache Clear the local cache 71 | config Read, write, and remove config options 72 | download Download remote file or folder to specified local path 73 | du Display disk usage for the given node 74 | find Find nodes by name or MD5 checksum 75 | help Displays help for a command 76 | init Initialize the command line application for use with an Amazon account 77 | link Generate a temporary, pre-authenticated download link 78 | list Lists commands 79 | ls List all remote nodes inside of a specified directory 80 | metadata Retrieve the metadata (JSON) of a node by its path 81 | mkdir Create a new remote directory given a path 82 | mv Move a node to a new remote folder 83 | pending List the nodes that have a status of 'PENDING' 84 | quota Show Cloud Drive quota 85 | rename Rename remote node 86 | renew Renew authorization 87 | resolve Return a node's remote path by its ID 88 | restore Restore a remote node from the trash 89 | rm Move a remote Node to the trash 90 | sync Sync the local cache with Amazon CloudDrive 91 | trash List the nodes that are in trash 92 | tree Print directory tree of the given node 93 | upload Upload local file or folder to remote directory 94 | usage Show Cloud Drive usage 95 | ``` 96 | 97 | ## SDK 98 | 99 | ### SDK Responses 100 | 101 | All of the method calls return a reponse in a REST API-like structure with the exception of those methods that return `Node` objects. 102 | 103 | Example response: 104 | ```php 105 | [ 106 | 'result' => true, 107 | 'data' => [ 108 | 'message' => 'The response was successful' 109 | ] 110 | ] 111 | ``` 112 | 113 | Every API-like reponse will have at least 2 keys: `success` and `data`. `Success` is a boolean on whether or not the request was completed successfully and the data contains various information related to the request. 114 | 115 | ** NOTE: ** The response for `nodeExists` will return `success = true` if the node exists, `false` if the node doesn't exist. 116 | 117 | Various method calls that return `Node` objects such as `findNodeByPath` and `findNodeById` will return either a `Node` object or `null` if the node was not found. 118 | 119 | ### Getting started 120 | 121 | The first thing to do is create a new `CloudDrive` object using the email of the account you wish you talk with and the necessary API credentials for Cloud Drive and a `Cache` object for storing local data. (Currently there is only a SQLite cache store). 122 | 123 | ```php 124 | $clouddrive = new CloudDrive\CloudDrive($email, $clientId, $clientSecret, new \CloudDrive\Cache\SQLite($email)); 125 | $response = $clouddrive->getAccount()->authorize(); 126 | ``` 127 | 128 | The first time you go to authorize the account, the `response` will "fail" and the `data` key in the response will contain an `auth_url`. This is required during the initial authorization to grant access to your application with Cloud Drive. Simply navigate to the URL, you will be redirected to a "localhost" URL which will contain the necessary code for access. 129 | 130 | Next, call authorization once more with the redirected URL passed in: 131 | 132 | ```php 133 | $clouddrive = new CloudDrive\CloudDrive($email, $clientId, $clientSecret, new \CloudDrive\Cache\SQLite($email)); 134 | $response = $clouddrive->getAccount()->authorize($redirectUrl); 135 | ``` 136 | 137 | The response will now be successful and the access token will be stored in the cache. From now on, when the account needs to renew its authorization, it will do so automatically with its 'refresh token' inside of the `authorize` method. 138 | 139 | ### Local Cache 140 | 141 | There is currently support for MySQL (and MariaDB) and SQLite3 for the local cache store. Simply instantiate these with the necessary parameters. If you are using MySQL, make sure the database is created. The initialization of the cache store will automatically create the necessary tables. 142 | 143 | ``` 144 | $cacheStore = new \CloudDrive\Cache\SQLite('my-cache', './.cache'); 145 | ``` 146 | 147 | ### Node 148 | 149 | Once you have authenticated the `Account` object and created a local cache, initialize the `Node` object to utilize these. 150 | 151 | 152 | ``` 153 | Node::init($account, $cache); 154 | ``` 155 | 156 | Now all static `Node` methods will be available to retrieve, find, and manipulate `Node` objects. 157 | 158 | ``` 159 | $results = Node::loadByName('myfile.txt'); 160 | ``` 161 | 162 | Various `Node` methods will either return an array if multiple nodes are able to be returned and a `Node` object if only 1 is meant to be returned (i.e., lookup by ID). If no nodes are found, then the methods will return an empty array or `null` value respectively. 163 | 164 | ## Contributing 165 | 166 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 167 | 168 | ## License 169 | 170 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 171 | -------------------------------------------------------------------------------- /bin/clouddrive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | command(new \CloudDrive\Commands\MetadataCommand()); 27 | $app->command(new \CloudDrive\Commands\InitCommand()); 28 | $app->command(new \CloudDrive\Commands\SyncCommand()); 29 | $app->command(new \CloudDrive\Commands\ClearCacheCommand()); 30 | $app->command(new \CloudDrive\Commands\UploadCommand()); 31 | $app->command(new \CloudDrive\Commands\ListCommand()); 32 | $app->command(new \CloudDrive\Commands\DownloadCommand()); 33 | $app->command(new \CloudDrive\Commands\MkdirCommand()); 34 | $app->command(new \CloudDrive\Commands\TrashCommand()); 35 | $app->command(new \CloudDrive\Commands\RestoreCommand()); 36 | $app->command(new \CloudDrive\Commands\RenameCommand()); 37 | $app->command(new \CloudDrive\Commands\ListTrashCommand()); 38 | $app->command(new \CloudDrive\Commands\ResolveCommand()); 39 | $app->command(new \CloudDrive\Commands\MoveCommand()); 40 | $app->command(new \CloudDrive\Commands\FindCommand()); 41 | $app->command(new \CloudDrive\Commands\QuotaCommand()); 42 | $app->command(new \CloudDrive\Commands\UsageCommand()); 43 | $app->command(new \CloudDrive\Commands\ConfigCommand()); 44 | $app->command(new \CloudDrive\Commands\CatCommand()); 45 | $app->command(new \CloudDrive\Commands\TreeCommand()); 46 | $app->command(new \CloudDrive\Commands\DiskUsageCommand()); 47 | $app->command(new \CloudDrive\Commands\RenewCommand()); 48 | $app->command(new \CloudDrive\Commands\TempLinkCommand()); 49 | $app->command(new \CloudDrive\Commands\ListPendingCommand()); 50 | 51 | $app->run(); 52 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "directories": [ 4 | "src" 5 | ], 6 | "files": [ 7 | "LICENSE.md" 8 | ], 9 | "finder": [ 10 | { 11 | "name": "*.php", 12 | "exclude": [ 13 | "Tests" 14 | ], 15 | "in": "vendor" 16 | } 17 | ], 18 | "git-version": "0.1.0", 19 | "main": "bin/clouddrive", 20 | "output": "clouddrive.phar", 21 | "stub": true 22 | } 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alex-phillips/clouddrive", 3 | "description": "PHP SDK and CLI for Amazon's CloudDrive", 4 | "require": { 5 | "php": ">=5.5", 6 | "guzzlehttp/guzzle": "^6.0", 7 | "alex-phillips/utilities": "~0.1", 8 | "cilex/cilex": "^1.1", 9 | "j4mie/idiorm": "^1.5" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Alex Phillips", 14 | "email": "ahp118@gmail.com" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "": "src/" 20 | } 21 | }, 22 | "bin": [ 23 | "bin/clouddrive" 24 | ], 25 | "require-dev": { 26 | "squizlabs/php_codesniffer": "^2.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "443cca422aa5c7d79f8d5db346141c51", 8 | "content-hash": "749b384d152182f59f0c3dc62b05d760", 9 | "packages": [ 10 | { 11 | "name": "alex-phillips/utilities", 12 | "version": "0.1.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/alex-phillips/Utilities.git", 16 | "reference": "78f0f4c57f546ac120a9f39b13bd0c0629d2161e" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/alex-phillips/Utilities/zipball/78f0f4c57f546ac120a9f39b13bd0c0629d2161e", 21 | "reference": "78f0f4c57f546ac120a9f39b13bd0c0629d2161e", 22 | "shasum": "" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^4.6", 26 | "squizlabs/php_codesniffer": "^2.3" 27 | }, 28 | "type": "library", 29 | "autoload": { 30 | "psr-4": { 31 | "": "src/" 32 | } 33 | }, 34 | "notification-url": "https://packagist.org/downloads/", 35 | "license": [ 36 | "MIT" 37 | ], 38 | "authors": [ 39 | { 40 | "name": "Alex Phillips", 41 | "email": "ahp118@gmail.com" 42 | } 43 | ], 44 | "time": "2015-08-15 15:12:09" 45 | }, 46 | { 47 | "name": "cilex/cilex", 48 | "version": "1.1.0", 49 | "source": { 50 | "type": "git", 51 | "url": "https://github.com/Cilex/Cilex.git", 52 | "reference": "7acd965a609a56d0345e8b6071c261fbdb926cb5" 53 | }, 54 | "dist": { 55 | "type": "zip", 56 | "url": "https://api.github.com/repos/Cilex/Cilex/zipball/7acd965a609a56d0345e8b6071c261fbdb926cb5", 57 | "reference": "7acd965a609a56d0345e8b6071c261fbdb926cb5", 58 | "shasum": "" 59 | }, 60 | "require": { 61 | "cilex/console-service-provider": "1.*", 62 | "php": ">=5.3.3", 63 | "pimple/pimple": "~1.0", 64 | "symfony/finder": "~2.1", 65 | "symfony/process": "~2.1" 66 | }, 67 | "require-dev": { 68 | "phpunit/phpunit": "3.7.*", 69 | "symfony/validator": "~2.1" 70 | }, 71 | "suggest": { 72 | "monolog/monolog": ">=1.0.0", 73 | "symfony/validator": ">=1.0.0", 74 | "symfony/yaml": ">=1.0.0" 75 | }, 76 | "type": "library", 77 | "extra": { 78 | "branch-alias": { 79 | "dev-master": "1.0-dev" 80 | } 81 | }, 82 | "autoload": { 83 | "psr-0": { 84 | "Cilex": "src/" 85 | } 86 | }, 87 | "notification-url": "https://packagist.org/downloads/", 88 | "license": [ 89 | "MIT" 90 | ], 91 | "authors": [ 92 | { 93 | "name": "Mike van Riel", 94 | "email": "mike.vanriel@naenius.com" 95 | } 96 | ], 97 | "description": "The PHP micro-framework for Command line tools based on the Symfony2 Components", 98 | "homepage": "http://cilex.github.com", 99 | "keywords": [ 100 | "cli", 101 | "microframework" 102 | ], 103 | "time": "2014-03-29 14:03:13" 104 | }, 105 | { 106 | "name": "cilex/console-service-provider", 107 | "version": "1.0.0", 108 | "source": { 109 | "type": "git", 110 | "url": "https://github.com/Cilex/console-service-provider.git", 111 | "reference": "25ee3d1875243d38e1a3448ff94bdf944f70d24e" 112 | }, 113 | "dist": { 114 | "type": "zip", 115 | "url": "https://api.github.com/repos/Cilex/console-service-provider/zipball/25ee3d1875243d38e1a3448ff94bdf944f70d24e", 116 | "reference": "25ee3d1875243d38e1a3448ff94bdf944f70d24e", 117 | "shasum": "" 118 | }, 119 | "require": { 120 | "php": ">=5.3.3", 121 | "pimple/pimple": "1.*@dev", 122 | "symfony/console": "~2.1" 123 | }, 124 | "require-dev": { 125 | "cilex/cilex": "1.*@dev", 126 | "silex/silex": "1.*@dev" 127 | }, 128 | "type": "library", 129 | "extra": { 130 | "branch-alias": { 131 | "dev-master": "1.0-dev" 132 | } 133 | }, 134 | "autoload": { 135 | "psr-0": { 136 | "Cilex\\Provider\\Console": "src" 137 | } 138 | }, 139 | "notification-url": "https://packagist.org/downloads/", 140 | "license": [ 141 | "MIT" 142 | ], 143 | "authors": [ 144 | { 145 | "name": "Beau Simensen", 146 | "email": "beau@dflydev.com", 147 | "homepage": "http://beausimensen.com" 148 | }, 149 | { 150 | "name": "Mike van Riel", 151 | "email": "mike.vanriel@naenius.com" 152 | } 153 | ], 154 | "description": "Console Service Provider", 155 | "keywords": [ 156 | "cilex", 157 | "console", 158 | "pimple", 159 | "service-provider", 160 | "silex" 161 | ], 162 | "time": "2012-12-19 10:50:58" 163 | }, 164 | { 165 | "name": "guzzlehttp/guzzle", 166 | "version": "6.1.1", 167 | "source": { 168 | "type": "git", 169 | "url": "https://github.com/guzzle/guzzle.git", 170 | "reference": "c6851d6e48f63b69357cbfa55bca116448140e0c" 171 | }, 172 | "dist": { 173 | "type": "zip", 174 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c6851d6e48f63b69357cbfa55bca116448140e0c", 175 | "reference": "c6851d6e48f63b69357cbfa55bca116448140e0c", 176 | "shasum": "" 177 | }, 178 | "require": { 179 | "guzzlehttp/promises": "~1.0", 180 | "guzzlehttp/psr7": "~1.1", 181 | "php": ">=5.5.0" 182 | }, 183 | "require-dev": { 184 | "ext-curl": "*", 185 | "phpunit/phpunit": "~4.0", 186 | "psr/log": "~1.0" 187 | }, 188 | "type": "library", 189 | "extra": { 190 | "branch-alias": { 191 | "dev-master": "6.1-dev" 192 | } 193 | }, 194 | "autoload": { 195 | "files": [ 196 | "src/functions_include.php" 197 | ], 198 | "psr-4": { 199 | "GuzzleHttp\\": "src/" 200 | } 201 | }, 202 | "notification-url": "https://packagist.org/downloads/", 203 | "license": [ 204 | "MIT" 205 | ], 206 | "authors": [ 207 | { 208 | "name": "Michael Dowling", 209 | "email": "mtdowling@gmail.com", 210 | "homepage": "https://github.com/mtdowling" 211 | } 212 | ], 213 | "description": "Guzzle is a PHP HTTP client library", 214 | "homepage": "http://guzzlephp.org/", 215 | "keywords": [ 216 | "client", 217 | "curl", 218 | "framework", 219 | "http", 220 | "http client", 221 | "rest", 222 | "web service" 223 | ], 224 | "time": "2015-11-23 00:47:50" 225 | }, 226 | { 227 | "name": "guzzlehttp/promises", 228 | "version": "1.0.3", 229 | "source": { 230 | "type": "git", 231 | "url": "https://github.com/guzzle/promises.git", 232 | "reference": "b1e1c0d55f8083c71eda2c28c12a228d708294ea" 233 | }, 234 | "dist": { 235 | "type": "zip", 236 | "url": "https://api.github.com/repos/guzzle/promises/zipball/b1e1c0d55f8083c71eda2c28c12a228d708294ea", 237 | "reference": "b1e1c0d55f8083c71eda2c28c12a228d708294ea", 238 | "shasum": "" 239 | }, 240 | "require": { 241 | "php": ">=5.5.0" 242 | }, 243 | "require-dev": { 244 | "phpunit/phpunit": "~4.0" 245 | }, 246 | "type": "library", 247 | "extra": { 248 | "branch-alias": { 249 | "dev-master": "1.0-dev" 250 | } 251 | }, 252 | "autoload": { 253 | "psr-4": { 254 | "GuzzleHttp\\Promise\\": "src/" 255 | }, 256 | "files": [ 257 | "src/functions_include.php" 258 | ] 259 | }, 260 | "notification-url": "https://packagist.org/downloads/", 261 | "license": [ 262 | "MIT" 263 | ], 264 | "authors": [ 265 | { 266 | "name": "Michael Dowling", 267 | "email": "mtdowling@gmail.com", 268 | "homepage": "https://github.com/mtdowling" 269 | } 270 | ], 271 | "description": "Guzzle promises library", 272 | "keywords": [ 273 | "promise" 274 | ], 275 | "time": "2015-10-15 22:28:00" 276 | }, 277 | { 278 | "name": "guzzlehttp/psr7", 279 | "version": "1.2.1", 280 | "source": { 281 | "type": "git", 282 | "url": "https://github.com/guzzle/psr7.git", 283 | "reference": "4d0bdbe1206df7440219ce14c972aa57cc5e4982" 284 | }, 285 | "dist": { 286 | "type": "zip", 287 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/4d0bdbe1206df7440219ce14c972aa57cc5e4982", 288 | "reference": "4d0bdbe1206df7440219ce14c972aa57cc5e4982", 289 | "shasum": "" 290 | }, 291 | "require": { 292 | "php": ">=5.4.0", 293 | "psr/http-message": "~1.0" 294 | }, 295 | "provide": { 296 | "psr/http-message-implementation": "1.0" 297 | }, 298 | "require-dev": { 299 | "phpunit/phpunit": "~4.0" 300 | }, 301 | "type": "library", 302 | "extra": { 303 | "branch-alias": { 304 | "dev-master": "1.0-dev" 305 | } 306 | }, 307 | "autoload": { 308 | "psr-4": { 309 | "GuzzleHttp\\Psr7\\": "src/" 310 | }, 311 | "files": [ 312 | "src/functions_include.php" 313 | ] 314 | }, 315 | "notification-url": "https://packagist.org/downloads/", 316 | "license": [ 317 | "MIT" 318 | ], 319 | "authors": [ 320 | { 321 | "name": "Michael Dowling", 322 | "email": "mtdowling@gmail.com", 323 | "homepage": "https://github.com/mtdowling" 324 | } 325 | ], 326 | "description": "PSR-7 message implementation", 327 | "keywords": [ 328 | "http", 329 | "message", 330 | "stream", 331 | "uri" 332 | ], 333 | "time": "2015-11-03 01:34:55" 334 | }, 335 | { 336 | "name": "j4mie/idiorm", 337 | "version": "v1.5.1", 338 | "source": { 339 | "type": "git", 340 | "url": "https://github.com/j4mie/idiorm.git", 341 | "reference": "b0922d8719a94e3a0e0e4a0ca3876f4f91475dcf" 342 | }, 343 | "dist": { 344 | "type": "zip", 345 | "url": "https://api.github.com/repos/j4mie/idiorm/zipball/b0922d8719a94e3a0e0e4a0ca3876f4f91475dcf", 346 | "reference": "b0922d8719a94e3a0e0e4a0ca3876f4f91475dcf", 347 | "shasum": "" 348 | }, 349 | "require": { 350 | "php": ">=5.2.0" 351 | }, 352 | "type": "library", 353 | "autoload": { 354 | "classmap": [ 355 | "idiorm.php" 356 | ] 357 | }, 358 | "notification-url": "https://packagist.org/downloads/", 359 | "license": [ 360 | "BSD-2-Clause", 361 | "BSD-3-Clause", 362 | "BSD-4-Clause" 363 | ], 364 | "authors": [ 365 | { 366 | "name": "Simon Holywell", 367 | "email": "treffynnon@php.net", 368 | "homepage": "http://simonholywell.com", 369 | "role": "Maintainer" 370 | }, 371 | { 372 | "name": "Jamie Matthews", 373 | "email": "jamie.matthews@gmail.com", 374 | "homepage": "http://j4mie.org", 375 | "role": "Developer" 376 | }, 377 | { 378 | "name": "Durham Hale", 379 | "email": "me@durhamhale.com", 380 | "homepage": "http://durhamhale.com", 381 | "role": "Maintainer" 382 | } 383 | ], 384 | "description": "A lightweight nearly-zero-configuration object-relational mapper and fluent query builder for PHP5", 385 | "homepage": "http://j4mie.github.com/idiormandparis", 386 | "keywords": [ 387 | "idiorm", 388 | "orm", 389 | "query builder" 390 | ], 391 | "time": "2014-06-23 13:08:57" 392 | }, 393 | { 394 | "name": "pimple/pimple", 395 | "version": "v1.1.1", 396 | "source": { 397 | "type": "git", 398 | "url": "https://github.com/silexphp/Pimple.git", 399 | "reference": "2019c145fe393923f3441b23f29bbdfaa5c58c4d" 400 | }, 401 | "dist": { 402 | "type": "zip", 403 | "url": "https://api.github.com/repos/silexphp/Pimple/zipball/2019c145fe393923f3441b23f29bbdfaa5c58c4d", 404 | "reference": "2019c145fe393923f3441b23f29bbdfaa5c58c4d", 405 | "shasum": "" 406 | }, 407 | "require": { 408 | "php": ">=5.3.0" 409 | }, 410 | "type": "library", 411 | "extra": { 412 | "branch-alias": { 413 | "dev-master": "1.1.x-dev" 414 | } 415 | }, 416 | "autoload": { 417 | "psr-0": { 418 | "Pimple": "lib/" 419 | } 420 | }, 421 | "notification-url": "https://packagist.org/downloads/", 422 | "license": [ 423 | "MIT" 424 | ], 425 | "authors": [ 426 | { 427 | "name": "Fabien Potencier", 428 | "email": "fabien@symfony.com" 429 | } 430 | ], 431 | "description": "Pimple is a simple Dependency Injection Container for PHP 5.3", 432 | "homepage": "http://pimple.sensiolabs.org", 433 | "keywords": [ 434 | "container", 435 | "dependency injection" 436 | ], 437 | "time": "2013-11-22 08:30:29" 438 | }, 439 | { 440 | "name": "psr/http-message", 441 | "version": "1.0", 442 | "source": { 443 | "type": "git", 444 | "url": "https://github.com/php-fig/http-message.git", 445 | "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" 446 | }, 447 | "dist": { 448 | "type": "zip", 449 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", 450 | "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", 451 | "shasum": "" 452 | }, 453 | "require": { 454 | "php": ">=5.3.0" 455 | }, 456 | "type": "library", 457 | "extra": { 458 | "branch-alias": { 459 | "dev-master": "1.0.x-dev" 460 | } 461 | }, 462 | "autoload": { 463 | "psr-4": { 464 | "Psr\\Http\\Message\\": "src/" 465 | } 466 | }, 467 | "notification-url": "https://packagist.org/downloads/", 468 | "license": [ 469 | "MIT" 470 | ], 471 | "authors": [ 472 | { 473 | "name": "PHP-FIG", 474 | "homepage": "http://www.php-fig.org/" 475 | } 476 | ], 477 | "description": "Common interface for HTTP messages", 478 | "keywords": [ 479 | "http", 480 | "http-message", 481 | "psr", 482 | "psr-7", 483 | "request", 484 | "response" 485 | ], 486 | "time": "2015-05-04 20:22:00" 487 | }, 488 | { 489 | "name": "symfony/console", 490 | "version": "v2.8.0", 491 | "source": { 492 | "type": "git", 493 | "url": "https://github.com/symfony/console.git", 494 | "reference": "d232bfc100dfd32b18ccbcab4bcc8f28697b7e41" 495 | }, 496 | "dist": { 497 | "type": "zip", 498 | "url": "https://api.github.com/repos/symfony/console/zipball/d232bfc100dfd32b18ccbcab4bcc8f28697b7e41", 499 | "reference": "d232bfc100dfd32b18ccbcab4bcc8f28697b7e41", 500 | "shasum": "" 501 | }, 502 | "require": { 503 | "php": ">=5.3.9", 504 | "symfony/polyfill-mbstring": "~1.0" 505 | }, 506 | "require-dev": { 507 | "psr/log": "~1.0", 508 | "symfony/event-dispatcher": "~2.1|~3.0.0", 509 | "symfony/process": "~2.1|~3.0.0" 510 | }, 511 | "suggest": { 512 | "psr/log": "For using the console logger", 513 | "symfony/event-dispatcher": "", 514 | "symfony/process": "" 515 | }, 516 | "type": "library", 517 | "extra": { 518 | "branch-alias": { 519 | "dev-master": "2.8-dev" 520 | } 521 | }, 522 | "autoload": { 523 | "psr-4": { 524 | "Symfony\\Component\\Console\\": "" 525 | }, 526 | "exclude-from-classmap": [ 527 | "/Tests/" 528 | ] 529 | }, 530 | "notification-url": "https://packagist.org/downloads/", 531 | "license": [ 532 | "MIT" 533 | ], 534 | "authors": [ 535 | { 536 | "name": "Fabien Potencier", 537 | "email": "fabien@symfony.com" 538 | }, 539 | { 540 | "name": "Symfony Community", 541 | "homepage": "https://symfony.com/contributors" 542 | } 543 | ], 544 | "description": "Symfony Console Component", 545 | "homepage": "https://symfony.com", 546 | "time": "2015-11-30 12:35:10" 547 | }, 548 | { 549 | "name": "symfony/finder", 550 | "version": "v2.8.0", 551 | "source": { 552 | "type": "git", 553 | "url": "https://github.com/symfony/finder.git", 554 | "reference": "ead9b07af4ba77b6507bee697396a5c79e633f08" 555 | }, 556 | "dist": { 557 | "type": "zip", 558 | "url": "https://api.github.com/repos/symfony/finder/zipball/ead9b07af4ba77b6507bee697396a5c79e633f08", 559 | "reference": "ead9b07af4ba77b6507bee697396a5c79e633f08", 560 | "shasum": "" 561 | }, 562 | "require": { 563 | "php": ">=5.3.9" 564 | }, 565 | "type": "library", 566 | "extra": { 567 | "branch-alias": { 568 | "dev-master": "2.8-dev" 569 | } 570 | }, 571 | "autoload": { 572 | "psr-4": { 573 | "Symfony\\Component\\Finder\\": "" 574 | }, 575 | "exclude-from-classmap": [ 576 | "/Tests/" 577 | ] 578 | }, 579 | "notification-url": "https://packagist.org/downloads/", 580 | "license": [ 581 | "MIT" 582 | ], 583 | "authors": [ 584 | { 585 | "name": "Fabien Potencier", 586 | "email": "fabien@symfony.com" 587 | }, 588 | { 589 | "name": "Symfony Community", 590 | "homepage": "https://symfony.com/contributors" 591 | } 592 | ], 593 | "description": "Symfony Finder Component", 594 | "homepage": "https://symfony.com", 595 | "time": "2015-10-30 20:15:42" 596 | }, 597 | { 598 | "name": "symfony/polyfill-mbstring", 599 | "version": "v1.0.0", 600 | "source": { 601 | "type": "git", 602 | "url": "https://github.com/symfony/polyfill-mbstring.git", 603 | "reference": "0b6a8940385311a24e060ec1fe35680e17c74497" 604 | }, 605 | "dist": { 606 | "type": "zip", 607 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0b6a8940385311a24e060ec1fe35680e17c74497", 608 | "reference": "0b6a8940385311a24e060ec1fe35680e17c74497", 609 | "shasum": "" 610 | }, 611 | "require": { 612 | "php": ">=5.3.3" 613 | }, 614 | "type": "library", 615 | "extra": { 616 | "branch-alias": { 617 | "dev-master": "1.0-dev" 618 | } 619 | }, 620 | "autoload": { 621 | "psr-4": { 622 | "Symfony\\Polyfill\\Mbstring\\": "" 623 | }, 624 | "files": [ 625 | "bootstrap.php" 626 | ] 627 | }, 628 | "notification-url": "https://packagist.org/downloads/", 629 | "license": [ 630 | "MIT" 631 | ], 632 | "authors": [ 633 | { 634 | "name": "Nicolas Grekas", 635 | "email": "p@tchwork.com" 636 | }, 637 | { 638 | "name": "Symfony Community", 639 | "homepage": "https://symfony.com/contributors" 640 | } 641 | ], 642 | "description": "Symfony polyfill for the Mbstring extension", 643 | "homepage": "https://symfony.com", 644 | "keywords": [ 645 | "compatibility", 646 | "mbstring", 647 | "polyfill", 648 | "portable", 649 | "shim" 650 | ], 651 | "time": "2015-11-04 20:28:58" 652 | }, 653 | { 654 | "name": "symfony/process", 655 | "version": "v2.8.0", 656 | "source": { 657 | "type": "git", 658 | "url": "https://github.com/symfony/process.git", 659 | "reference": "1b988a88e3551102f3c2d9e1d47a18c3a78d6312" 660 | }, 661 | "dist": { 662 | "type": "zip", 663 | "url": "https://api.github.com/repos/symfony/process/zipball/1b988a88e3551102f3c2d9e1d47a18c3a78d6312", 664 | "reference": "1b988a88e3551102f3c2d9e1d47a18c3a78d6312", 665 | "shasum": "" 666 | }, 667 | "require": { 668 | "php": ">=5.3.9" 669 | }, 670 | "type": "library", 671 | "extra": { 672 | "branch-alias": { 673 | "dev-master": "2.8-dev" 674 | } 675 | }, 676 | "autoload": { 677 | "psr-4": { 678 | "Symfony\\Component\\Process\\": "" 679 | }, 680 | "exclude-from-classmap": [ 681 | "/Tests/" 682 | ] 683 | }, 684 | "notification-url": "https://packagist.org/downloads/", 685 | "license": [ 686 | "MIT" 687 | ], 688 | "authors": [ 689 | { 690 | "name": "Fabien Potencier", 691 | "email": "fabien@symfony.com" 692 | }, 693 | { 694 | "name": "Symfony Community", 695 | "homepage": "https://symfony.com/contributors" 696 | } 697 | ], 698 | "description": "Symfony Process Component", 699 | "homepage": "https://symfony.com", 700 | "time": "2015-11-30 12:35:10" 701 | } 702 | ], 703 | "packages-dev": [ 704 | { 705 | "name": "squizlabs/php_codesniffer", 706 | "version": "2.5.0", 707 | "source": { 708 | "type": "git", 709 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 710 | "reference": "e4fb41d5d0387d556e2c25534d630b3cce90ea67" 711 | }, 712 | "dist": { 713 | "type": "zip", 714 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e4fb41d5d0387d556e2c25534d630b3cce90ea67", 715 | "reference": "e4fb41d5d0387d556e2c25534d630b3cce90ea67", 716 | "shasum": "" 717 | }, 718 | "require": { 719 | "ext-tokenizer": "*", 720 | "ext-xmlwriter": "*", 721 | "php": ">=5.1.2" 722 | }, 723 | "require-dev": { 724 | "phpunit/phpunit": "~4.0" 725 | }, 726 | "bin": [ 727 | "scripts/phpcs", 728 | "scripts/phpcbf" 729 | ], 730 | "type": "library", 731 | "extra": { 732 | "branch-alias": { 733 | "dev-master": "2.0.x-dev" 734 | } 735 | }, 736 | "autoload": { 737 | "classmap": [ 738 | "CodeSniffer.php", 739 | "CodeSniffer/CLI.php", 740 | "CodeSniffer/Exception.php", 741 | "CodeSniffer/File.php", 742 | "CodeSniffer/Fixer.php", 743 | "CodeSniffer/Report.php", 744 | "CodeSniffer/Reporting.php", 745 | "CodeSniffer/Sniff.php", 746 | "CodeSniffer/Tokens.php", 747 | "CodeSniffer/Reports/", 748 | "CodeSniffer/Tokenizers/", 749 | "CodeSniffer/DocGenerators/", 750 | "CodeSniffer/Standards/AbstractPatternSniff.php", 751 | "CodeSniffer/Standards/AbstractScopeSniff.php", 752 | "CodeSniffer/Standards/AbstractVariableSniff.php", 753 | "CodeSniffer/Standards/IncorrectPatternException.php", 754 | "CodeSniffer/Standards/Generic/Sniffs/", 755 | "CodeSniffer/Standards/MySource/Sniffs/", 756 | "CodeSniffer/Standards/PEAR/Sniffs/", 757 | "CodeSniffer/Standards/PSR1/Sniffs/", 758 | "CodeSniffer/Standards/PSR2/Sniffs/", 759 | "CodeSniffer/Standards/Squiz/Sniffs/", 760 | "CodeSniffer/Standards/Zend/Sniffs/" 761 | ] 762 | }, 763 | "notification-url": "https://packagist.org/downloads/", 764 | "license": [ 765 | "BSD-3-Clause" 766 | ], 767 | "authors": [ 768 | { 769 | "name": "Greg Sherwood", 770 | "role": "lead" 771 | } 772 | ], 773 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 774 | "homepage": "http://www.squizlabs.com/php-codesniffer", 775 | "keywords": [ 776 | "phpcs", 777 | "standards" 778 | ], 779 | "time": "2015-12-11 00:12:46" 780 | } 781 | ], 782 | "aliases": [], 783 | "minimum-stability": "stable", 784 | "stability-flags": [], 785 | "prefer-stable": false, 786 | "prefer-lowest": false, 787 | "platform": { 788 | "php": ">=5.5" 789 | }, 790 | "platform-dev": [] 791 | } 792 | -------------------------------------------------------------------------------- /src/CloudDrive/Account.php: -------------------------------------------------------------------------------- 1 | email = $email; 108 | $this->clientId = $clientId; 109 | $this->clientSecret = $clientSecret; 110 | $this->cache = $cache; 111 | $this->httpClient = new Client(); 112 | } 113 | 114 | /** 115 | * Authorize the user object. If the initial authorization has not been 116 | * completed, the `auth_url` is returned. If authorization has already 117 | * happened and the `access_token` hasn't passed its expiration time, we 118 | * are already authorized. Otherwise, if the `access_token` has expired, 119 | * request a new token. This method also retrieves the user's API endpoints. 120 | * 121 | * @param null|string $redirectUrl The URL the user is redirected to after 122 | * navigating to the authorization URL. 123 | * 124 | * @return array 125 | */ 126 | public function authorize($redirectUrl = null) 127 | { 128 | $retval = [ 129 | 'success' => true, 130 | 'data' => [], 131 | ]; 132 | 133 | $config = $this->cache->loadAccountConfig($this->email) ?: []; 134 | 135 | $this->token = new ParameterBag($config); 136 | 137 | $scope = rawurlencode(implode(' ', $this->scope)); 138 | 139 | if (!$this->token["access_token"]) { 140 | if (!$redirectUrl) { 141 | $retval['success'] = false; 142 | if (!$this->clientId || !$this->clientSecret) { 143 | $retval['data'] = [ 144 | 'message' => 'Initial authorization is required', 145 | 'auth_url' => 'https://data-mind-687.appspot.com/clouddrive', 146 | ]; 147 | } else { 148 | $retval['data'] = [ 149 | 'message' => 'Initial authorization required.', 150 | 'auth_url' => "https://www.amazon.com/ap/oa?client_id={$this->clientId}&scope={$scope}&response_type=code&redirect_uri=http://localhost", 151 | ]; 152 | } 153 | 154 | return $retval; 155 | } 156 | 157 | $response = $this->requestAuthorization($redirectUrl); 158 | 159 | if (!$response["success"]) { 160 | return $response; 161 | } 162 | } else { 163 | if (time() - $this->token["last_authorized"] > $this->token["expires_in"]) { 164 | $response = $this->renewAuthorization(); 165 | if (!$response["success"]) { 166 | return $response; 167 | } 168 | } 169 | } 170 | 171 | if (isset($response)) { 172 | $this->token->merge($response['data']); 173 | } 174 | 175 | if (!$this->token["metadata_url"] || !$this->token["content_url"]) { 176 | $response = $this->fetchEndpoint(); 177 | if (!$response['success']) { 178 | return $response; 179 | } 180 | 181 | $this->token['metadata_url'] = $response['data']['metadataUrl']; 182 | $this->token['content_url'] = $response['data']['contentUrl']; 183 | } 184 | 185 | $this->checkpoint = $this->token["checkpoint"]; 186 | $this->metadataUrl = $this->token["metadata_url"]; 187 | $this->contentUrl = $this->token["content_url"]; 188 | 189 | $this->save(); 190 | 191 | return $retval; 192 | } 193 | 194 | /** 195 | * Reset the account's sync checkpoint. 196 | */ 197 | public function clearCache() 198 | { 199 | $this->checkpoint = null; 200 | $this->save(); 201 | $this->cache->deleteAllNodes(); 202 | } 203 | 204 | /** 205 | * Fetch the user's API endpoints from the REST API. 206 | * 207 | * @return array 208 | */ 209 | private function fetchEndpoint() 210 | { 211 | $retval = [ 212 | 'success' => false, 213 | 'data' => [], 214 | ]; 215 | 216 | $response = $this->httpClient->get('https://cdws.us-east-1.amazonaws.com/drive/v1/account/endpoint', [ 217 | 'headers' => [ 218 | 'Authorization' => "Bearer {$this->token["access_token"]}", 219 | ], 220 | 'exceptions' => false, 221 | ]); 222 | 223 | $retval['data'] = json_decode((string)$response->getBody(), true); 224 | 225 | if ($response->getStatusCode() === 200) { 226 | $retval['success'] = true; 227 | } 228 | 229 | return $retval; 230 | } 231 | 232 | /** 233 | * Retrieve the last sync checkpoint. 234 | * 235 | * @return null|string 236 | */ 237 | public function getCheckpoint() 238 | { 239 | return $this->checkpoint; 240 | } 241 | 242 | /** 243 | * Retrieve the user's API content URL. 244 | * 245 | * @return string 246 | */ 247 | public function getContentUrl() 248 | { 249 | return $this->contentUrl; 250 | } 251 | 252 | /** 253 | * Retrieve the account's email. 254 | * 255 | * @return string 256 | */ 257 | public function getEmail() 258 | { 259 | return $this->email; 260 | } 261 | 262 | /** 263 | * Retrieve the user's API metadata URL. 264 | * 265 | * @return string 266 | */ 267 | public function getMetadataUrl() 268 | { 269 | return $this->metadataUrl; 270 | } 271 | 272 | /** 273 | * Retrieve the account's quota. 274 | * 275 | * @return array 276 | */ 277 | public function getQuota() 278 | { 279 | $retval = [ 280 | 'success' => false, 281 | 'data' => [], 282 | ]; 283 | 284 | $response = $this->httpClient->get("{$this->getMetadataUrl()}account/quota", [ 285 | 'headers' => [ 286 | 'Authorization' => "Bearer {$this->token["access_token"]}", 287 | ], 288 | 'exceptions' => false, 289 | ]); 290 | 291 | $retval['data'] = json_decode((string)$response->getBody(), true); 292 | 293 | if ($response->getStatusCode() === 200) { 294 | $retval['success'] = true; 295 | } 296 | 297 | return $retval; 298 | } 299 | 300 | /** 301 | * Retrieve access token data. 302 | * 303 | * @return ParameterBag 304 | */ 305 | public function getToken() 306 | { 307 | return $this->token; 308 | } 309 | 310 | /** 311 | * Retrieve the account's current usage. 312 | * 313 | * @return array 314 | */ 315 | public function getUsage() 316 | { 317 | $retval = [ 318 | 'success' => false, 319 | 'data' => [], 320 | ]; 321 | 322 | $response = $this->httpClient->get( 323 | "{$this->getMetadataUrl()}account/usage", 324 | [ 325 | 'headers' => [ 326 | 'Authorization' => "Bearer {$this->token["access_token"]}", 327 | ], 328 | 'exceptions' => false, 329 | ] 330 | ); 331 | 332 | $retval['data'] = json_decode((string)$response->getBody(), true); 333 | 334 | if ($response->getStatusCode() === 200) { 335 | $retval['success'] = true; 336 | } 337 | 338 | return $retval; 339 | } 340 | 341 | /** 342 | * Renew the OAuth2 access token after the current access token has expired. 343 | * 344 | * @return array 345 | */ 346 | public function renewAuthorization() 347 | { 348 | $retval = [ 349 | "success" => false, 350 | "data" => [], 351 | ]; 352 | 353 | if ($this->clientId && $this->clientSecret) { 354 | $response = $this->httpClient->post( 355 | 'https://api.amazon.com/auth/o2/token', 356 | [ 357 | 'form_params' => [ 358 | 'grant_type' => "refresh_token", 359 | 'refresh_token' => $this->token["refresh_token"], 360 | 'client_id' => $this->clientId, 361 | 'client_secret' => $this->clientSecret, 362 | 'redirect_uri' => "http://localhost", 363 | ], 364 | 'exceptions' => false, 365 | ] 366 | ); 367 | } else { 368 | $response = $this->httpClient->get( 369 | 'https://data-mind-687.appspot.com/clouddrive?refresh_token=${this.token.refresh_token}' 370 | ); 371 | } 372 | 373 | $retval["data"] = json_decode((string)$response->getBody(), true); 374 | 375 | if ($response->getStatusCode() === 200) { 376 | $retval["success"] = true; 377 | $retval["data"]["last_authorized"] = time(); 378 | } 379 | 380 | return $retval; 381 | } 382 | 383 | /** 384 | * Use the `code` from the passed in `authUrl` to retrieve the OAuth2 385 | * tokens for API access. 386 | * 387 | * @param string $authUrl The redirect URL from the authorization request 388 | * 389 | * @return array 390 | */ 391 | private function requestAuthorization($authUrl) 392 | { 393 | $retval = [ 394 | 'success' => false, 395 | 'data' => [], 396 | ]; 397 | 398 | if (!$token = json_decode($authUrl, true)) { 399 | $url = parse_url($authUrl); 400 | parse_str($url['query'], $params); 401 | 402 | if (!isset($params['code'])) { 403 | $retval['data']['message'] = "No authorization code found in callback URL: $authUrl"; 404 | 405 | return $retval; 406 | } 407 | 408 | $response = $this->httpClient->post( 409 | 'https://api.amazon.com/auth/o2/token', 410 | [ 411 | 'form_params' => [ 412 | 'grant_type' => 'authorization_code', 413 | 'code' => $params['code'], 414 | 'client_id' => $this->clientId, 415 | 'client_secret' => $this->clientSecret, 416 | 'redirect_uri' => 'http://localhost', 417 | ], 418 | 'exceptions' => false, 419 | ] 420 | ); 421 | 422 | $retval["data"] = json_decode((string)$response->getBody(), true); 423 | 424 | if ($response->getStatusCode() === 200) { 425 | $retval["success"] = true; 426 | $retval["data"]["last_authorized"] = time(); 427 | } 428 | } else { 429 | $retval["success"] = true; 430 | $retval["data"] = $token; 431 | $retval["data"]["last_authorized"] = time(); 432 | } 433 | 434 | return $retval; 435 | } 436 | 437 | /** 438 | * Save the account config into the cache database. This includes authorization 439 | * tokens, endpoint URLs, and the last sync checkpoint. 440 | * 441 | * @return bool 442 | */ 443 | public function save() 444 | { 445 | return $this->cache->saveAccountConfig($this); 446 | } 447 | 448 | /** 449 | * Set the permission scope of the API before requesting authentication. 450 | * 451 | * @param array $scopes The permissions requested 452 | * 453 | * @return $this 454 | */ 455 | public function setScope(array $scopes) 456 | { 457 | $this->scope = $scopes; 458 | 459 | return $this; 460 | } 461 | 462 | /** 463 | * Sync the local cache with the remote changes. If checkpoint is null, this 464 | * will sync all remote node data. 465 | * 466 | * @throws \Exception 467 | */ 468 | public function sync() 469 | { 470 | $params = [ 471 | 'maxNodes' => 5000, 472 | ]; 473 | 474 | if ($this->checkpoint) { 475 | $params['includePurged'] = "true"; 476 | } 477 | 478 | while (true) { 479 | if ($this->checkpoint) { 480 | $params['checkpoint'] = $this->checkpoint; 481 | } 482 | 483 | $loop = true; 484 | 485 | $response = $this->httpClient->post( 486 | "{$this->getMetadataUrl()}changes", 487 | [ 488 | 'headers' => [ 489 | 'Authorization' => "Bearer {$this->token['access_token']}", 490 | ], 491 | 'body' => json_encode($params), 492 | 'exceptions' => false, 493 | ] 494 | ); 495 | 496 | if ($response->getStatusCode() !== 200) { 497 | throw new \Exception((string)$response->getBody()); 498 | } 499 | 500 | $data = explode("\n", (string)$response->getBody()); 501 | foreach ($data as $part) { 502 | $part = json_decode($part, true); 503 | 504 | if (isset($part['end']) && $part['end'] === true) { 505 | break; 506 | } 507 | 508 | if (isset($part['reset']) && $part['reset'] === true) { 509 | $this->cache->deleteAllNodes(); 510 | } 511 | 512 | if (isset($part['nodes'])) { 513 | if (empty($part['nodes'])) { 514 | $loop = false; 515 | } else { 516 | foreach ($part['nodes'] as $node) { 517 | $node = new Node($node); 518 | if ($node['status'] === 'PURGED') { 519 | $node->delete(); 520 | } else { 521 | $node->save(); 522 | } 523 | } 524 | } 525 | } 526 | 527 | if (isset($part['checkpoint'])) { 528 | $this->checkpoint = $part['checkpoint']; 529 | } 530 | 531 | $this->save(); 532 | } 533 | 534 | if (!$loop) { 535 | break; 536 | } 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /src/CloudDrive/Cache.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 11:06 AM 6 | */ 7 | 8 | namespace CloudDrive; 9 | 10 | /** 11 | * Class that handles local storage of remote node information. 12 | * 13 | * @package CloudDrive 14 | */ 15 | interface Cache 16 | { 17 | /** 18 | * Delete all nodes from the cache. 19 | * 20 | * @return bool 21 | */ 22 | public function deleteAllNodes(); 23 | 24 | /** 25 | * Delete node with the given ID from the cache. 26 | * 27 | * @param string $id 28 | * 29 | * @return bool 30 | */ 31 | public function deleteNodeById($id); 32 | 33 | /** 34 | * Find nodes in the local cache based on query filters. 35 | * 36 | * @param array $filters 37 | * 38 | * @return array 39 | */ 40 | public function filterNodes(array $filters); 41 | 42 | /** 43 | * Find the node by the given ID in the cache. 44 | * 45 | * @param string $id 46 | * 47 | * @return \CloudDrive\Node|null 48 | */ 49 | public function findNodeById($id); 50 | 51 | /** 52 | * Find all nodes by the given MD5 in the cache. 53 | * 54 | * @param string $md5 55 | * 56 | * @return array 57 | */ 58 | public function findNodesByMd5($md5); 59 | 60 | /** 61 | * Retrieve all node matching the given name in the cache. 62 | * 63 | * @param string $name 64 | * 65 | * @return array 66 | */ 67 | public function findNodesByName($name); 68 | 69 | /** 70 | * Retrieve all nodes who have the given node as their parent. 71 | * 72 | * @param Node $node 73 | * 74 | * @return array 75 | */ 76 | public function getNodeChildren(Node $node); 77 | 78 | /** 79 | * Retrieve the config for the account matched with the given email. 80 | * 81 | * @param string $email 82 | * 83 | * @return array 84 | */ 85 | public function loadAccountConfig($email); 86 | 87 | /** 88 | * Save the config for the provided account. 89 | * 90 | * @param Account $account 91 | * 92 | * @return bool 93 | */ 94 | public function saveAccountConfig(Account $account); 95 | 96 | /** 97 | * Save the given node into the cache. 98 | * 99 | * @param Node $node 100 | * 101 | * @return bool 102 | */ 103 | public function saveNode(Node $node); 104 | 105 | /** 106 | * Search for nodes in the local cache that contain a string in their name. 107 | * 108 | * @param string $name 109 | * 110 | * @return array 111 | */ 112 | public function searchNodesByName($name); 113 | } 114 | -------------------------------------------------------------------------------- /src/CloudDrive/Cache/MySQL.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/27/15 5 | * Time: 3:56 PM 6 | */ 7 | 8 | namespace CloudDrive\Cache; 9 | 10 | use ORM; 11 | 12 | class MySQL extends SQL 13 | { 14 | public function __construct($host, $database, $username, $password) 15 | { 16 | ORM::configure("mysql:host=$host;dbname=$database"); 17 | ORM::configure('username', $username); 18 | ORM::configure('password', $password); 19 | ORM::get_db()->exec(' 20 | CREATE TABLE IF NOT EXISTS configs ( 21 | id INT(11) NOT NULL auto_increment, 22 | email VARCHAR(32), 23 | token_type VARCHAR(16), 24 | expires_in INT(12), 25 | refresh_token TEXT, 26 | access_token TEXT, 27 | last_authorized INT(12), 28 | content_url MEDIUMTEXT, 29 | metadata_url MEDIUMTEXT, 30 | checkpoint TEXT, 31 | PRIMARY KEY (id), 32 | INDEX (email) 33 | ); 34 | '); 35 | ORM::get_db()->exec(' 36 | CREATE TABLE IF NOT EXISTS nodes ( 37 | id VARCHAR(255) NOT NULL, 38 | name VARCHAR(128), 39 | kind VARCHAR(16), 40 | md5 VARCHAR(128), 41 | status VARCHAR(16), 42 | created DATETIME, 43 | modified DATETIME, 44 | raw_data LONGTEXT, 45 | PRIMARY KEY (id), 46 | INDEX (id, name, md5) 47 | ); 48 | '); 49 | ORM::get_db()->exec(' 50 | CREATE TABLE IF NOT EXISTS nodes_nodes ( 51 | id INT(11) NOT NULL auto_increment, 52 | id_node VARCHAR(255) NOT NULL, 53 | id_parent VARCHAR(255) NOT NULL, 54 | PRIMARY KEY (id), 55 | UNIQUE KEY (id_node, id_parent), 56 | INDEX(id_node, id_parent) 57 | ); 58 | '); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CloudDrive/Cache/SQL.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/23/15 5 | * Time: 5:19 PM 6 | */ 7 | 8 | namespace CloudDrive\Cache; 9 | 10 | use CloudDrive\Account; 11 | use CloudDrive\Cache; 12 | use CloudDrive\Node; 13 | use ORM; 14 | 15 | /** 16 | * The SQL abstract class is what all SQL database cache classes will inherit 17 | * from. 18 | * 19 | * @package CloudDrive\Cache 20 | */ 21 | abstract class SQL implements Cache 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function deleteAllNodes() 27 | { 28 | try { 29 | ORM::get_db()->beginTransaction(); 30 | ORM::for_table('nodes')->delete_many(); 31 | ORM::get_db()->exec('TRUNCATE nodes_nodes'); 32 | ORM::get_db()->commit(); 33 | } catch (\Exception $e) { 34 | return false; 35 | } 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function deleteNodeById($id) 42 | { 43 | $node = ORM::for_table('nodes')->find_one($id); 44 | if ($node) { 45 | try { 46 | ORM::get_db()->beginTransaction(); 47 | 48 | $node->delete(); 49 | ORM::for_table('nodes_nodes') 50 | ->where('id_node', $id) 51 | ->delete_many(); 52 | 53 | return ORM::get_db()->commit(); 54 | } catch (\Exception $e) { 55 | ORM::get_db()->rollBack(); 56 | } 57 | 58 | return false; 59 | } 60 | 61 | return true; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function filterNodes(array $filters) 68 | { 69 | $nodes = ORM::for_table('nodes') 70 | ->select('raw_data') 71 | ->where_any_is($filters) 72 | ->find_many(); 73 | 74 | foreach ($nodes as &$node) { 75 | $node = new Node( 76 | json_decode($node->as_array()['raw_data'], true) 77 | ); 78 | } 79 | 80 | return $nodes; 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function findNodeById($id) 87 | { 88 | $result = ORM::for_table('nodes')->select('raw_data')->where('id', $id)->find_one(); 89 | if ($result) { 90 | return new Node( 91 | json_decode($result['raw_data'], true) 92 | ); 93 | } 94 | 95 | return null; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function findNodesByMd5($md5) 102 | { 103 | $nodes = ORM::for_table('nodes') 104 | ->select('raw_data') 105 | ->where('md5', $md5) 106 | ->find_many(); 107 | 108 | foreach ($nodes as &$node) { 109 | $node = new Node( 110 | json_decode($node->as_array()['raw_data'], true) 111 | ); 112 | } 113 | 114 | return $nodes; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function findNodesByName($name) 121 | { 122 | $nodes = ORM::for_table('nodes') 123 | ->select('raw_data') 124 | ->where('name', $name) 125 | ->find_many(); 126 | 127 | foreach ($nodes as &$node) { 128 | $node = new Node( 129 | json_decode($node->as_array()['raw_data'], true) 130 | ); 131 | } 132 | 133 | return $nodes; 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | public function getNodeChildren(Node $node) 140 | { 141 | $results = ORM::for_table('nodes') 142 | ->select('raw_data') 143 | ->join('nodes_nodes', ['nodes.id', '=', 'nodes_nodes.id_node']) 144 | ->where('nodes_nodes.id_parent', $node['id']) 145 | ->find_many(); 146 | 147 | foreach ($results as &$result) { 148 | $result = new Node( 149 | json_decode($result['raw_data'], true) 150 | ); 151 | } 152 | 153 | return $results; 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function loadAccountConfig($email) 160 | { 161 | $config = ORM::for_table('configs') 162 | ->where('email', $email) 163 | ->find_one(); 164 | 165 | if (!$config) { 166 | return null; 167 | } 168 | 169 | return $config->as_array(); 170 | } 171 | 172 | /** 173 | * {@inheritdoc} 174 | */ 175 | public function saveAccountConfig(Account $account) 176 | { 177 | $config = ORM::for_table('configs')->where('email', $account->getEmail())->find_one(); 178 | if (!$config) { 179 | $config = ORM::for_table('configs')->create(); 180 | } 181 | 182 | $config->set([ 183 | 'email' => $account->getEmail(), 184 | 'token_type' => $account->getToken()['token_type'], 185 | 'expires_in' => $account->getToken()['expires_in'], 186 | 'refresh_token' => $account->getToken()['refresh_token'], 187 | 'access_token' => $account->getToken()['access_token'], 188 | 'last_authorized' => $account->getToken()['last_authorized'], 189 | 'content_url' => $account->getContentUrl(), 190 | 'metadata_url' => $account->getMetadataUrl(), 191 | 'checkpoint' => $account->getCheckpoint(), 192 | ]); 193 | 194 | return $config->save(); 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function saveNode(Node $node) 201 | { 202 | if (!$node['name'] && $node['isRoot'] === true) { 203 | $node['name'] = 'Cloud Drive'; 204 | } 205 | 206 | $n = ORM::for_table('nodes')->find_one($node['id']); 207 | if (!$n) { 208 | $n = ORM::for_table('nodes')->create(); 209 | } 210 | 211 | $n->set([ 212 | 'id' => $node['id'], 213 | 'name' => $node['name'], 214 | 'kind' => $node['kind'], 215 | 'md5' => $node['contentProperties']['md5'], 216 | 'status' => $node['status'], 217 | 'created' => $node['createdDate'], 218 | 'modified' => $node['modifiedDate'], 219 | 'raw_data' => json_encode($node), 220 | ]); 221 | 222 | try { 223 | ORM::get_db()->beginTransaction(); 224 | 225 | $n->save(); 226 | 227 | $parentIds = $node['parents']; 228 | $previousParents = ORM::for_table('nodes_nodes') 229 | ->where('id_node', $node['id']) 230 | ->find_array(); 231 | 232 | foreach ($previousParents as $parent) { 233 | if ($index = array_search($parent['id_parent'], $parentIds)) { 234 | unset($parentIds[$index]); 235 | continue; 236 | } else { 237 | ORM::for_table('nodes_nodes') 238 | ->find_one($parent['id']) 239 | ->delete(); 240 | } 241 | } 242 | 243 | foreach ($parentIds as $parentId) { 244 | $p = ORM::for_table('nodes_nodes')->create(); 245 | $p->set([ 246 | 'id_node' => $node['id'], 247 | 'id_parent' => $parentId, 248 | ]); 249 | $p->save(); 250 | } 251 | 252 | return ORM::get_db()->commit(); 253 | } catch (\Exception $e) { 254 | ORM::get_db()->rollBack(); 255 | 256 | return false; 257 | } 258 | } 259 | 260 | /** 261 | * {@inheritdoc} 262 | */ 263 | public function searchNodesByName($name) 264 | { 265 | $results = ORM::for_table('nodes') 266 | ->where_like('name', "%$name%") 267 | ->find_many(); 268 | 269 | foreach ($results as &$result) { 270 | $result = new Node( 271 | json_decode($result['raw_data'], true) 272 | ); 273 | } 274 | 275 | return $results; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/CloudDrive/Cache/SQLite.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 11:08 AM 6 | */ 7 | 8 | namespace CloudDrive\Cache; 9 | 10 | use CloudDrive\Cache; 11 | use ORM; 12 | use SQLite3; 13 | 14 | class SQLite extends SQL 15 | { 16 | /** 17 | * Construct a new SQLite cache object. The database will be saved in the 18 | * provided `cacheDir` under the name `$email.db`. 19 | * 20 | * @param string $email The email for the account 21 | * @param string $cacheDir The directory to save the database in 22 | */ 23 | public function __construct($email, $cacheDir) 24 | { 25 | if (!file_exists($cacheDir)) { 26 | mkdir($cacheDir, 0777, true); 27 | } 28 | 29 | $cacheDir = rtrim($cacheDir, '/'); 30 | 31 | if (!file_exists("$cacheDir/$email.db")) { 32 | $db = new SQLite3("$cacheDir/$email.db"); 33 | $db->exec( 34 | 'CREATE TABLE nodes( 35 | id VARCHAR PRIMARY KEY NOT NULL, 36 | name VARCHAR NOT NULL, 37 | kind VARCHAR NOT NULL, 38 | md5 VARCHAR, 39 | status VARCHAR, 40 | created DATETIME NOT NULL, 41 | modified DATETIME NOT NULL, 42 | raw_data TEXT NOT NULL 43 | ); 44 | CREATE INDEX node_id on nodes(id); 45 | CREATE INDEX node_name on nodes(name); 46 | CREATE INDEX node_md5 on nodes(md5);' 47 | ); 48 | $db->exec( 49 | 'CREATE TABLE configs 50 | ( 51 | id INTEGER PRIMARY KEY, 52 | email VARCHAR NOT NULL, 53 | token_type VARCHAR, 54 | expires_in INT, 55 | refresh_token TEXT, 56 | access_token TEXT, 57 | last_authorized INT, 58 | content_url VARCHAR, 59 | metadata_url VARCHAR, 60 | checkpoint VARCHAR 61 | ); 62 | CREATE INDEX config_email on configs(email);' 63 | ); 64 | $db->exec( 65 | 'CREATE TABLE nodes_nodes 66 | ( 67 | id INTEGER PRIMARY KEY, 68 | id_node VARCHAR NOT NULL, 69 | id_parent VARCHAR NOT NULL, 70 | UNIQUE (id_node, id_parent) 71 | ); 72 | CREATE INDEX nodes_id_node on nodes_nodes(id_node); 73 | CREATE INDEX nodes_id_parent on nodes_nodes(id_parent);' 74 | ); 75 | } 76 | 77 | ORM::configure("sqlite:$cacheDir/$email.db"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/CloudDrive/CloudDrive.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/10/15 5 | * Time: 3:38 PM 6 | */ 7 | 8 | namespace CloudDrive; 9 | 10 | use GuzzleHttp\Client; 11 | 12 | /** 13 | * Class that handles all communication for accessing and altering nodes, 14 | * retrieving account information, and managing the local cache store. 15 | * 16 | * @package CloudDrive 17 | */ 18 | class CloudDrive 19 | { 20 | /** 21 | * @var \CloudDrive\Account 22 | */ 23 | private $account; 24 | 25 | /** 26 | * @var \GuzzleHttp\Client 27 | */ 28 | private $httpClient; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private $clientId; 34 | 35 | /** 36 | * @var string 37 | */ 38 | private $clientSecret; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private $email; 44 | 45 | /** 46 | * Construct a new instance of `CloudDrive`. This handles all communication 47 | * for accessing and altering nodes, retrieving account information, and 48 | * managing the local cache store. 49 | * 50 | * @param string $email The email for the account to connect to 51 | * @param string $clientId Amazon CloudDrive API client ID credential 52 | * @param string $clientSecret Amazon CloudDrive API client secret credential 53 | * @param Cache $cacheStore Local cache storage object 54 | * @param Account|null $account `Account` object. If not passed in, this will 55 | * be created using the email and credentials used 56 | * here 57 | */ 58 | public function __construct($email, $clientId, $clientSecret, Cache $cacheStore, Account $account = null) 59 | { 60 | $this->email = $email; 61 | $this->clientId = $clientId; 62 | $this->clientSecret = $clientSecret; 63 | $this->httpClient = new Client(); 64 | 65 | if (is_null($account)) { 66 | $account = new Account($this->email, $this->clientId, $this->clientSecret, $cacheStore); 67 | } 68 | 69 | $this->account = $account; 70 | } 71 | 72 | /** 73 | * Recursively create a remote directory path. If parts of the path already 74 | * exist, it will continue until the entire path exists. 75 | * 76 | * @param string $path The directory path to create 77 | * 78 | * @return array 79 | * @throws \Exception 80 | */ 81 | public function createDirectoryPath($path) 82 | { 83 | $retval = [ 84 | 'success' => true, 85 | 'data' => [], 86 | 'resonse_code' => null, 87 | ]; 88 | 89 | $path = $this->getPathArray($path); 90 | $previousNode = Node::loadRoot(); 91 | 92 | $match = null; 93 | foreach ($path as $index => $folder) { 94 | $xary = array_slice($path, 0, $index + 1); 95 | if (!($match = Node::loadByPath(implode('/', $xary)))) { 96 | $response = $this->createFolder($folder, $previousNode['id']); 97 | if (!$response['success']) { 98 | return $response; 99 | } 100 | 101 | $match = $response['data']; 102 | } 103 | 104 | $previousNode = $match; 105 | } 106 | 107 | if (is_null($match)) { 108 | $retval['data'] = $previousNode; 109 | } else { 110 | $retval['data'] = $match; 111 | } 112 | 113 | return $retval; 114 | } 115 | 116 | /** 117 | * Create a new remote node nested under the provided parents (created under 118 | * root node if none given). 119 | * 120 | * @param string $name Name of the new remote folder 121 | * @param null $parents Parent IDs to give the folder 122 | * 123 | * @return array 124 | * @throws \Exception 125 | */ 126 | public function createFolder($name, $parents = null) 127 | { 128 | $retval = [ 129 | 'success' => false, 130 | 'data' => [], 131 | 'response_code' => null, 132 | ]; 133 | 134 | if (is_null($parents)) { 135 | $parents = Node::loadRoot()['id']; 136 | } 137 | 138 | if (!is_array($parents)) { 139 | $parents = [$parents]; 140 | } 141 | 142 | $response = $this->httpClient->post( 143 | "{$this->account->getMetadataUrl()}nodes", 144 | [ 145 | 'headers' => [ 146 | 'Authorization' => "Bearer {$this->account->getToken()["access_token"]}", 147 | ], 148 | 'json' => [ 149 | 'name' => $name, 150 | 'parents' => $parents, 151 | 'kind' => 'FOLDER', 152 | ], 153 | 'exceptions' => false, 154 | ] 155 | ); 156 | 157 | $retval['data'] = json_decode((string)$response->getBody(), true); 158 | 159 | if (($retval['response_code'] = $response->getStatusCode()) === 201) { 160 | $retval['success'] = true; 161 | (new Node($retval['data']))->save(); 162 | } 163 | 164 | return $retval; 165 | } 166 | 167 | /** 168 | * Retrieve the associated `Account` object. 169 | * 170 | * @return \CloudDrive\Account 171 | */ 172 | public function getAccount() 173 | { 174 | return $this->account; 175 | } 176 | 177 | /** 178 | * Convert a given path string into an array of directory names. 179 | * 180 | * @param string|array $path 181 | * 182 | * @return array 183 | */ 184 | public function getPathArray($path) 185 | { 186 | if (is_array($path)) { 187 | return $path; 188 | } 189 | 190 | return array_filter(explode('/', $path)); 191 | } 192 | 193 | /** 194 | * Properly format a string or array of folders into a path string. 195 | * 196 | * @param string|array $path The remote path to format 197 | * 198 | * @return string 199 | */ 200 | public function getPathString($path) 201 | { 202 | if (is_string($path)) { 203 | return trim($path, '/'); 204 | } 205 | 206 | return trim(implode('/', $path)); 207 | } 208 | 209 | /** 210 | * Determine if a node matching the given path exists remotely. If a local 211 | * path is given, the MD5 will be compared as well. 212 | * 213 | * @param string $remotePath The remote path to check 214 | * @param null|string $localPath Local path of file to compare MD5 215 | * 216 | * @return array 217 | * @throws \Exception' 218 | */ 219 | public function nodeExists($remotePath, $localPath = null) 220 | { 221 | if (is_null($file = Node::loadByPath($remotePath))) { 222 | if (!is_null($localPath)) { 223 | if (!empty($nodes = Node::loadByMd5(md5_file($localPath)))) { 224 | $ids = []; 225 | foreach ($nodes as $node) { 226 | $ids[] = $node['id']; 227 | } 228 | 229 | return [ 230 | 'success' => true, 231 | 'data' => [ 232 | 'message' => "File(s) with same MD5: " . implode(', ', $ids), 233 | 'path_match' => false, 234 | 'md5_match' => true, 235 | 'nodes' => $nodes, 236 | ], 237 | ]; 238 | } 239 | } 240 | 241 | return [ 242 | 'success' => false, 243 | 'data' => [ 244 | 'message' => "File $remotePath does not exist.", 245 | 'path_match' => false, 246 | 'md5_match' => false, 247 | ] 248 | ]; 249 | } 250 | 251 | $retval = [ 252 | 'success' => true, 253 | 'data' => [ 254 | 'message' => "File $remotePath exists.", 255 | 'path_match' => true, 256 | 'md5_match' => false, 257 | 'node' => $file, 258 | ], 259 | ]; 260 | 261 | if (!is_null($localPath)) { 262 | if (!is_null($file['contentProperties']['md5'])) { 263 | if (md5_file($localPath) !== $file['contentProperties']['md5']) { 264 | $retval['data']['message'] = "File $remotePath exists but does not match local checksum."; 265 | } else { 266 | $retval['data']['message'] = "File $remotePath exists and is identical to local copy."; 267 | $retval['data']['md5_match'] = true; 268 | } 269 | } else { 270 | $retval['data']['message'] = "File $remotePath exists but no checksum is available."; 271 | } 272 | } 273 | 274 | return $retval; 275 | } 276 | 277 | /** 278 | * Upload a local directory to Amazon Cloud Drive. 279 | * 280 | * @param string $localPath Local path of directory to upload 281 | * @param string $remoteFolder Remote folder to place the directory in 282 | * @param bool $overwrite Flag to overwrite files if they exist remotely 283 | * @param callable|null $callback Callable to perform after each file upload 284 | * 285 | * @return array 286 | * @throws \Exception 287 | */ 288 | public function uploadDirectory($localPath, $remoteFolder, $overwrite = false, $callback = null) 289 | { 290 | $localPath = realpath($localPath); 291 | 292 | $remoteFolder = $this->getPathArray($remoteFolder); 293 | $tmp = $this->getPathArray($localPath); 294 | $remoteFolder[] = array_pop($tmp); 295 | $remoteFolder = $this->getPathString($remoteFolder); 296 | 297 | $retval = []; 298 | 299 | $files = new \RecursiveIteratorIterator( 300 | new \RecursiveDirectoryIterator($localPath), 301 | \RecursiveIteratorIterator::SELF_FIRST 302 | ); 303 | 304 | foreach ($files as $name => $file) { 305 | if (is_dir($file)) { 306 | continue; 307 | } 308 | 309 | $info = pathinfo($file); 310 | $remotePath = str_replace($localPath, $remoteFolder, $info['dirname']); 311 | 312 | $attempts = 0; 313 | while (true) { 314 | if ($attempts > 1) { 315 | throw new \Exception( 316 | "Failed to upload file '{$file->getPathName()}' after reauthentication. " . 317 | "Upload may take longer than the access token is valid for." 318 | ); 319 | } 320 | 321 | $response = $this->uploadFile($file->getPathname(), $remotePath, $overwrite); 322 | 323 | if ($response['success'] === false && $response['response_code'] === 401) { 324 | $auth = $this->account->authorize(); 325 | if ($auth['success'] === false) { 326 | throw new \Exception("Failed to renew account authorization."); 327 | } 328 | 329 | $attempts++; 330 | continue; 331 | } 332 | 333 | break; 334 | } 335 | 336 | if (is_callable($callback)) { 337 | call_user_func($callback, $response, [ 338 | 'file' => $file, 339 | 'local_path' => $localPath, 340 | 'name' => $name, 341 | 'remote_folder' => $remoteFolder, 342 | 'remote_path' => $remotePath, 343 | ]); 344 | } 345 | 346 | $retval[] = $response; 347 | } 348 | 349 | return $retval; 350 | } 351 | 352 | /** 353 | * Upload a single file to Amazon Cloud Drive. 354 | * 355 | * @param string $localPath The local path to the file to upload 356 | * @param string $remotePath The remote folder to upload the file to 357 | * @param bool|false $overwrite Whether to overwrite the file if it already 358 | * exists remotely 359 | * @param bool $suppressDedup Disables checking for duplicates when uploading 360 | * 361 | * @return array 362 | */ 363 | public function uploadFile($localPath, $remotePath, $overwrite = false, $suppressDedup = false) 364 | { 365 | $retval = [ 366 | 'success' => false, 367 | 'data' => [], 368 | 'response_code' => null, 369 | ]; 370 | 371 | $info = pathinfo($localPath); 372 | $remotePath = $this->getPathString($this->getPathArray($remotePath)); 373 | 374 | if (!($remoteFolder = Node::loadByPath($remotePath))) { 375 | $response = $this->createDirectoryPath($remotePath); 376 | if ($response['success'] === false) { 377 | return $response; 378 | } 379 | 380 | $remoteFolder = $response['data']; 381 | } 382 | 383 | $response = $this->nodeExists("$remotePath/{$info['basename']}", $localPath); 384 | if ($response['success'] === true) { 385 | $pathMatch = $response['data']['path_match']; 386 | $md5Match = $response['data']['md5_match']; 387 | 388 | if ($pathMatch === true && $md5Match === true) { 389 | // Skip if path and MD5 match 390 | $retval['data'] = $response['data']; 391 | 392 | return $retval; 393 | } else if ($pathMatch === true && $md5Match === false) { 394 | // If path is the same and checksum differs, only overwrite 395 | if ($overwrite === true) { 396 | return $response['data']['node']->overwrite($localPath); 397 | } 398 | 399 | $retval['data'] = $response['data']; 400 | 401 | return $retval; 402 | } else if ($pathMatch === false && $md5Match === true) { 403 | // If path differs and checksum is the same, check for dedup 404 | if ($suppressDedup === false) { 405 | $retval['data'] = $response['data']; 406 | 407 | return $retval; 408 | } 409 | } 410 | } 411 | 412 | $suppressDedup = $suppressDedup ? '?suppress=deduplication' : ''; 413 | 414 | $response = $this->httpClient->post( 415 | "{$this->account->getContentUrl()}nodes{$suppressDedup}", 416 | [ 417 | 'headers' => [ 418 | 'Authorization' => "Bearer {$this->account->getToken()['access_token']}", 419 | ], 420 | 'multipart' => [ 421 | [ 422 | 'name' => 'metadata', 423 | 'contents' => json_encode( 424 | [ 425 | 'kind' => 'FILE', 426 | 'name' => $info['basename'], 427 | 'parents' => [ 428 | $remoteFolder['id'], 429 | ] 430 | ] 431 | ), 432 | ], 433 | [ 434 | 'name' => 'contents', 435 | 'contents' => fopen($localPath, 'r'), 436 | ], 437 | ], 438 | 'exceptions' => false, 439 | ] 440 | ); 441 | 442 | $retval['data'] = json_decode((string)$response->getBody(), true); 443 | $retval['response_code'] = $response->getStatusCode(); 444 | 445 | if (($retval['response_code'] = $response->getStatusCode()) === 201) { 446 | $retval['success'] = true; 447 | (new Node($retval['data']))->save(); 448 | } 449 | 450 | return $retval; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/CatCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/13/15 5 | * Time: 12:55 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class CatCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('cat') 18 | ->setDescription('Output a file to the standard output stream') 19 | ->addArgument('remote_path', InputArgument::REQUIRED, 'The remote file path to download') 20 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->initOnlineCommand(); 26 | 27 | $remotePath = $this->input->getArgument('remote_path'); 28 | 29 | if ($this->input->getOption('id')) { 30 | if (!($node = Node::loadById($remotePath))) { 31 | throw new \Exception("No node exists with ID '$remotePath'."); 32 | } 33 | } else { 34 | if (!($node = Node::loadByPath($remotePath))) { 35 | throw new \Exception("No node exists at remote path '$remotePath'."); 36 | } 37 | } 38 | 39 | if ($node->isFolder()) { 40 | throw new \Exception("Folder downloads are not currently supported."); 41 | } 42 | 43 | $node->download($this->output->getStream()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/ClearCacheCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 2:18 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | class ClearCacheCommand extends Command 11 | { 12 | protected function configure() 13 | { 14 | $this->setName('clearcache') 15 | ->setAliases([ 16 | 'clear-cache', 17 | ]) 18 | ->setDescription('Clear the local cache'); 19 | } 20 | 21 | protected function main() 22 | { 23 | $this->init(); 24 | $this->clouddrive->getAccount()->clearCache(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/Command.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/10/15 5 | * Time: 5:36 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use Cilex\Command\Command as CilexCommand; 11 | use CloudDrive\Cache\MySQL; 12 | use CloudDrive\Cache\SQLite; 13 | use CloudDrive\CloudDrive; 14 | use CloudDrive\Node; 15 | use Symfony\Component\Console\Formatter\OutputFormatterStyle; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | use Utility\ParameterBag; 19 | 20 | abstract class Command extends CilexCommand 21 | { 22 | /** 23 | * @var \CloudDrive\Cache 24 | */ 25 | protected $cacheStore; 26 | 27 | /** 28 | * @var \CloudDrive\CloudDrive 29 | */ 30 | protected $clouddrive; 31 | 32 | /** 33 | * @var \Utility\ParameterBag 34 | */ 35 | protected $config; 36 | 37 | /** 38 | * @var string 39 | */ 40 | protected $configFile; 41 | 42 | /** 43 | * @var string 44 | */ 45 | protected $configPath; 46 | 47 | /** 48 | * Default and accepted values for the CLI config 49 | * 50 | * @var array 51 | */ 52 | protected $configValues = [ 53 | 'email' => [ 54 | 'type' => 'string', 55 | 'default' => '', 56 | ], 57 | 'client-id' => [ 58 | 'type' => 'string', 59 | 'default' => '', 60 | ], 61 | 'client-secret' => [ 62 | 'type' => 'string', 63 | 'default' => '', 64 | ], 65 | 'json.pretty' => [ 66 | 'type' => 'bool', 67 | 'default' => false, 68 | ], 69 | 'upload.duplicates' => [ 70 | 'type' => 'bool', 71 | 'default' => false, 72 | ], 73 | 'database.driver' => [ 74 | 'type' => 'string', 75 | 'default' => 'sqlite', 76 | ], 77 | 'database.database' => [ 78 | 'type' => 'string', 79 | 'default' => 'clouddrive_php', 80 | ], 81 | 'database.host' => [ 82 | 'type' => 'string', 83 | 'default' => '127.0.0.1', 84 | ], 85 | 'database.username' => [ 86 | 'type' => 'string', 87 | 'default' => 'root', 88 | ], 89 | 'database.password' => [ 90 | 'type' => 'string', 91 | 'default' => '', 92 | ], 93 | 'display.trash' => [ 94 | 'type' => 'bool', 95 | 'default' => false, 96 | ], 97 | ]; 98 | 99 | /** 100 | * @var \Symfony\Component\Console\Input\InputInterface 101 | */ 102 | protected $input; 103 | 104 | /** 105 | * @var bool 106 | */ 107 | protected $onlineCommand = true; 108 | 109 | /** 110 | * @var \Symfony\Component\Console\Output\ConsoleOutput 111 | */ 112 | protected $output; 113 | 114 | const SORT_BY_NAME = 0; 115 | const SORT_BY_TIME = 1; 116 | 117 | protected function convertFilesize($bytes, $decimals = 2) 118 | { 119 | $bytes = $bytes ?: 0; 120 | $size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 121 | $factor = floor((strlen($bytes) - 1) / 3); 122 | 123 | return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$size[$factor]; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | protected function execute(InputInterface $input, OutputInterface $output) 130 | { 131 | $home = getenv('HOME'); 132 | if (!$home) { 133 | throw new \RuntimeException("'HOME' environment variable must be set for Cloud Drive to properly run."); 134 | } 135 | 136 | $this->configPath = rtrim($home, '/') . '/.cache/clouddrive-php/'; 137 | if (!file_exists($this->configPath)) { 138 | mkdir($this->configPath, 0777, true); 139 | } 140 | 141 | $this->configFile = "{$this->configPath}config.json"; 142 | 143 | $this->input = $input; 144 | $this->output = $output; 145 | 146 | // Set up basic styling 147 | $this->output->getFormatter()->setStyle('blue', new OutputFormatterStyle('blue')); 148 | 149 | $this->readConfig(); 150 | $this->main(); 151 | } 152 | 153 | protected function generateCacheStore() 154 | { 155 | switch ($this->config->get('database.driver')) { 156 | case 'sqlite': 157 | return new SQLite($this->config['email'], $this->configPath); 158 | break; 159 | case 'mysql': 160 | return new MySQL( 161 | $this->config['database.host'], 162 | $this->config['database.database'], 163 | $this->config['database.username'], 164 | $this->config['database.password'] 165 | ); 166 | break; 167 | } 168 | } 169 | 170 | protected function init() 171 | { 172 | if ($this->onlineCommand === true) { 173 | $this->initOnlineCommand(); 174 | } else { 175 | $this->initOfflineCommand(); 176 | } 177 | } 178 | 179 | protected function initOfflineCommand() 180 | { 181 | if (count($this->config) === 0) { 182 | throw new \Exception('Account has not been authorized. Please do so using the `init` command.'); 183 | } 184 | 185 | $this->cacheStore = $this->generateCacheStore(); 186 | 187 | if ($this->config['email']) { 188 | $clouddrive = new CloudDrive( 189 | $this->config['email'], 190 | $this->config['client-id'], 191 | $this->config['client-secret'], 192 | $this->cacheStore 193 | ); 194 | 195 | $this->clouddrive = $clouddrive; 196 | Node::init($this->clouddrive->getAccount(), $this->cacheStore); 197 | } 198 | } 199 | 200 | /** 201 | * @throws \Exception 202 | */ 203 | protected function initOnlineCommand() 204 | { 205 | if (count($this->config) === 0) { 206 | throw new \Exception('Account has not been authorized. Please do so using the `init` command.'); 207 | } 208 | 209 | $this->cacheStore = $this->generateCacheStore(); 210 | 211 | if ($this->config['email']) { 212 | $clouddrive = new CloudDrive( 213 | $this->config['email'], 214 | $this->config['client-id'], 215 | $this->config['client-secret'], 216 | $this->cacheStore 217 | ); 218 | 219 | if ($this->output->getVerbosity() === 2) { 220 | $this->output->writeln("Authorizing...", OutputInterface::VERBOSITY_VERBOSE); 221 | } 222 | if ($clouddrive->getAccount()->authorize()['success']) { 223 | if ($this->output->getVerbosity() === 2) { 224 | $this->output->writeln("Done."); 225 | } 226 | $this->clouddrive = $clouddrive; 227 | Node::init($this->clouddrive->getAccount(), $this->cacheStore); 228 | } else { 229 | throw new \Exception('Account has not been authorized. Please do so using the `init` command.'); 230 | } 231 | } 232 | } 233 | 234 | protected function listNodes(array $nodes, $sortBy = self::SORT_BY_NAME) 235 | { 236 | switch ($sortBy) { 237 | case self::SORT_BY_NAME: 238 | usort($nodes, function ($a, $b) { 239 | return strcasecmp($a['name'], $b['name']); 240 | }); 241 | break; 242 | case self::SORT_BY_TIME: 243 | usort($nodes, function ($a, $b) { 244 | return strtotime($a['modifiedDate']) < strtotime($b['modifiedDate']); 245 | }); 246 | break; 247 | } 248 | 249 | foreach ($nodes as $node) { 250 | if ($node->inTrash() && !$this->config['display.trash']) { 251 | continue; 252 | } 253 | 254 | $modified = new \DateTime($node['modifiedDate']); 255 | if ($modified->format('Y') === date('Y')) { 256 | $date = $modified->format('M d H:m'); 257 | } else { 258 | $date = $modified->format('M d Y'); 259 | } 260 | 261 | $name = $node['kind'] === 'FOLDER' ? "{$node['name']}" : $node['name']; 262 | $this->output->writeln( 263 | sprintf( 264 | "%s %s %s %s %s %s", 265 | $node['id'], 266 | $date, 267 | str_pad($node['status'], 10), 268 | str_pad($node['kind'], 7), 269 | str_pad($this->convertFilesize($node['contentProperties']['size'], 0), 6), 270 | $name 271 | ) 272 | ); 273 | } 274 | } 275 | 276 | abstract protected function main(); 277 | 278 | protected function readConfig() 279 | { 280 | $this->config = new ParameterBag(); 281 | if (!file_exists($this->configFile) || !($data = json_decode(file_get_contents($this->configFile), true))) { 282 | $data = []; 283 | } 284 | 285 | $this->setConfig($data); 286 | } 287 | 288 | protected function removeConfigValue($key) 289 | { 290 | $this->config[$key] = $this->configValues[$key]['default']; 291 | } 292 | 293 | protected function setConfig(array $data) 294 | { 295 | $data = (new ParameterBag($data))->flatten(); 296 | foreach ($this->configValues as $option => $config) { 297 | if (isset($data[$option])) { 298 | $this->setConfigValue($option, $data[$option]); 299 | } else { 300 | $this->setConfigValue($option, $config['default']); 301 | } 302 | } 303 | } 304 | 305 | protected function setConfigValue($key, $value = null) 306 | { 307 | if (array_key_exists($key, $this->configValues)) { 308 | switch ($this->configValues[$key]['type']) { 309 | case 'bool': 310 | $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); 311 | break; 312 | } 313 | 314 | settype($value, $this->configValues[$key]['type']); 315 | 316 | $this->config[$key] = $value; 317 | } 318 | } 319 | 320 | protected function saveConfig() 321 | { 322 | file_put_contents("{$this->configPath}config.json", json_encode($this->config)); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/ConfigCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/12/15 5 | * Time: 2:34 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use Symfony\Component\Console\Input\InputArgument; 11 | 12 | class ConfigCommand extends Command 13 | { 14 | protected function configure() 15 | { 16 | $this->setName('config') 17 | ->setDescription('Read, write, and remove config options') 18 | ->addArgument('option', InputArgument::OPTIONAL, 'Config option to read, write, or remove') 19 | ->addArgument('value', InputArgument::OPTIONAL, 'Value to set to config option') 20 | ->addOption('remove', 'r', null, 'Remove config value'); 21 | } 22 | 23 | protected function main() 24 | { 25 | if (!($option = $this->input->getArgument('option'))) { 26 | $maxLength = max( 27 | array_map('strlen', array_keys($this->config->flatten())) 28 | ); 29 | foreach ($this->config->flatten() as $key => $value) { 30 | if ($this->configValues[$key]['type'] === 'bool') { 31 | $value = $value ? 'true' : 'false'; 32 | } 33 | 34 | $key = str_pad($key, $maxLength); 35 | 36 | $this->output->writeln("$key = $value"); 37 | } 38 | } else { 39 | if (!array_key_exists($option, $this->configValues)) { 40 | throw new \Exception("Option '$option' not found."); 41 | } 42 | 43 | if ($value = $this->input->getArgument('value')) { 44 | $this->setConfigValue($option, $value); 45 | $this->output->writeln("$option saved"); 46 | } else { 47 | if ($this->input->getOption('remove')) { 48 | $this->removeConfigValue($option); 49 | } else { 50 | $value = $this->config[$option]; 51 | if ($this->configValues[$option]['type'] === 'bool') { 52 | $value = $value ? 'true' : 'false'; 53 | } 54 | 55 | $this->output->writeln($value); 56 | } 57 | } 58 | } 59 | 60 | $this->saveConfig(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/DiskUsageCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/15/15 5 | * Time: 10:14 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class DiskUsageCommand extends Command 14 | { 15 | protected $onlineCommand = false; 16 | 17 | protected function configure() 18 | { 19 | $this->setName('du') 20 | ->setDescription('Display disk usage (recursively) for the given node') 21 | ->addArgument('path', InputArgument::OPTIONAL, 'The remote path of the node') 22 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path') 23 | ->addOption('assets', 'a', null, 'Include assets in output'); 24 | } 25 | 26 | protected function main() 27 | { 28 | $this->init(); 29 | 30 | $path = $this->input->getArgument('path') ?: ''; 31 | 32 | if ($this->input->getOption('id')) { 33 | if (!($node = Node::loadById($path))) { 34 | throw new \Exception("No node exists with ID '$path'."); 35 | } 36 | } else { 37 | if (!($node = Node::loadByPath($path))) { 38 | throw new \Exception("No node exists at remote path '$path'."); 39 | } 40 | } 41 | 42 | $this->output->writeln($this->convertFilesize($this->calculateTotalSize($node))); 43 | } 44 | 45 | protected function calculateTotalSize(Node $node) 46 | { 47 | $size = $node['contentProperties']['size'] ?: 0; 48 | 49 | if ($node->isFolder() || $this->input->getOption('assets')) { 50 | foreach ($node->getChildren() as $child) { 51 | $size += $this->calculateTotalSize($child); 52 | } 53 | } 54 | 55 | return $size; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/DownloadCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/17/15 5 | * Time: 9:30 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class DownloadCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('download') 18 | ->setDescription('Download remote file or folder to specified local path') 19 | ->addArgument('remote_path', InputArgument::REQUIRED, 'The remote file path to download') 20 | ->addArgument('local_path', InputArgument::OPTIONAL, 'The path to save the file / folder to') 21 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 22 | } 23 | 24 | protected function main() 25 | { 26 | $this->init(); 27 | 28 | $remotePath = $this->input->getArgument('remote_path'); 29 | $savePath = $this->input->getArgument('local_path') ?: getcwd(); 30 | 31 | if ($this->input->getOption('id')) { 32 | if (!($node = Node::loadById($remotePath))) { 33 | throw new \Exception("No node exists with ID '$remotePath'."); 34 | } 35 | } else { 36 | if (!($node = Node::loadByPath($remotePath))) { 37 | throw new \Exception("No node exists at remote path '$remotePath'."); 38 | } 39 | } 40 | 41 | $node->download($savePath, function ($result, $dest) { 42 | if ($result['success']) { 43 | $this->output->writeln("Successfully downloaded file to '$dest'"); 44 | } else { 45 | $this->output->getErrorOutput() 46 | ->writeln("Failed to download node to '$dest'"); 47 | if ($this->output->isVerbose()) { 48 | $this->output->getErrorOutput()->writeln(json_encode($result['data'])); 49 | } 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/FindCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/6/15 5 | * Time: 1:30 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class FindCommand extends Command 14 | { 15 | protected $onlineCommand = false; 16 | 17 | protected function configure() 18 | { 19 | $this->setName('find') 20 | ->setDescription('Find nodes by name or MD5 checksum') 21 | ->addArgument('query', InputArgument::REQUIRED, 'Query string to search for') 22 | ->addOption('md5', 'm', null, 'Search for nodes by MD5 rather than name') 23 | ->addOption('time', 't', null, 'Order output by date modified'); 24 | } 25 | 26 | protected function main() 27 | { 28 | $this->init(); 29 | 30 | $query = $this->input->getArgument('query'); 31 | if ($this->input->getOption('md5')) { 32 | $nodes = Node::loadByMd5($query); 33 | } else { 34 | $nodes = Node::searchNodesByName($query); 35 | } 36 | 37 | $sort = Command::SORT_BY_NAME; 38 | if ($this->input->getOption('time')) { 39 | $sort = Command::SORT_BY_TIME; 40 | } 41 | 42 | $this->listNodes($nodes, $sort); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/InitCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 10:04 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\CloudDrive; 11 | 12 | class InitCommand extends Command 13 | { 14 | protected function configure() 15 | { 16 | $this->setName('init') 17 | ->setDescription('Initialize the command line application for use with an Amazon account'); 18 | } 19 | 20 | protected function main() 21 | { 22 | if (!file_exists($this->configPath)) { 23 | mkdir($this->configPath, 0777, true); 24 | } 25 | 26 | $this->readConfig(); 27 | 28 | if (!$this->config['email']) { 29 | throw new \Exception('Email is required for initialization.'); 30 | } 31 | 32 | $this->saveConfig(); 33 | 34 | $this->cacheStore = $this->generateCacheStore(); 35 | $this->clouddrive = new CloudDrive( 36 | $this->config['email'], 37 | $this->config['client-id'], 38 | $this->config['client-secret'], 39 | $this->cacheStore 40 | ); 41 | 42 | $response = $this->clouddrive->getAccount()->authorize(); 43 | if (!$response['success']) { 44 | $this->output->writeln($response['data']['message']); 45 | if (isset($response['data']['auth_url'])) { 46 | $this->output->writeln('Navigate to the following URL and paste in the redirect URL here.'); 47 | $this->output->writeln($response['data']['auth_url']); 48 | 49 | $redirectUrl = readline(); 50 | 51 | $response = $this->clouddrive->getAccount()->authorize($redirectUrl); 52 | if ($response['success']) { 53 | $this->output->writeln('Successfully authenticated with Amazon Cloud Drive.'); 54 | return; 55 | } 56 | 57 | $this->output->getErrorOutput()->writeln( 58 | 'Failed to authenticate with Amazon Cloud Drive: ' . json_encode($response['data']) . '' 59 | ); 60 | } 61 | } else { 62 | $this->output->writeln('That user is already authenticated with Amazon Cloud Drive.'); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/ListCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 3:22 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class ListCommand extends Command 14 | { 15 | protected $onlineCommand = false; 16 | 17 | protected function configure() 18 | { 19 | $this->setName('ls') 20 | ->setDescription('List all remote nodes inside of a specified directory') 21 | ->addArgument('remote_path', InputArgument::OPTIONAL, 'The remote directory to list') 22 | ->addOption('time', 't', null, 'Order output by date modified') 23 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path') 24 | ->addOption('assets', 'a', null, "List node's assets if requesting a FILE node"); 25 | } 26 | 27 | protected function main() 28 | { 29 | $this->init(); 30 | $this->clouddrive->getAccount()->authorize(); 31 | 32 | $remotePath = $this->input->getArgument('remote_path') ?: ''; 33 | 34 | $sort = Command::SORT_BY_NAME; 35 | if ($this->input->getOption('time')) { 36 | $sort = Command::SORT_BY_TIME; 37 | } 38 | 39 | if ($this->input->getOption('id')) { 40 | if (!($node = Node::loadById($remotePath))) { 41 | throw new \Exception("No node exists with ID '$remotePath'."); 42 | } 43 | } else { 44 | if (!($node = Node::loadByPath($remotePath))) { 45 | throw new \Exception("No node exists at remote path '$remotePath'."); 46 | } 47 | } 48 | 49 | if ($node->isFolder() || $this->input->getOption('assets')) { 50 | $this->listNodes($node->getChildren(), $sort); 51 | } else { 52 | $this->listNodes([$node], $sort); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/ListPendingCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 9/3/15 5 | * Time: 5:28 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | 12 | class ListPendingCommand extends Command 13 | { 14 | protected $onlineCommand = false; 15 | 16 | protected function configure() 17 | { 18 | $this->setName('pending') 19 | ->setDescription("List the nodes that have a status of 'PENDING'") 20 | ->addOption('time', 't', null, 'Order output by date modified'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $sort = Command::SORT_BY_NAME; 28 | if ($this->input->getOption('time')) { 29 | $sort = Command::SORT_BY_TIME; 30 | } 31 | 32 | $this->listNodes( 33 | Node::filter([ 34 | ['status' => 'PENDING'], 35 | ]), 36 | $sort 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/ListTrashCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/5/15 5 | * Time: 3:42 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | 12 | class ListTrashCommand extends Command 13 | { 14 | protected $onlineCommand = false; 15 | 16 | protected function configure() 17 | { 18 | $this->setName('trash') 19 | ->setDescription('List the nodes that are in trash') 20 | ->addOption('time', 't', null, 'Order output by date modified'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $sort = Command::SORT_BY_NAME; 28 | if ($this->input->getOption('time')) { 29 | $sort = Command::SORT_BY_TIME; 30 | } 31 | 32 | $this->listNodes( 33 | Node::filter([ 34 | ['status' => 'TRASH'], 35 | ]), 36 | $sort 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/MetadataCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/10/15 5 | * Time: 5:35 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class MetadataCommand extends Command 14 | { 15 | protected $onlineCommand = false; 16 | 17 | protected function configure() 18 | { 19 | $this->setName('metadata') 20 | ->setDescription('Retrieve the metadata (JSON) of a node') 21 | ->addArgument('path', InputArgument::OPTIONAL, 'The remote path of the node') 22 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 23 | } 24 | 25 | protected function main() 26 | { 27 | $this->init(); 28 | 29 | $path = $this->input->getArgument('path') ?: ''; 30 | 31 | if ($this->input->getOption('id')) { 32 | if (!($node = Node::loadById($path))) { 33 | throw new \Exception("No node exists with ID '$path'."); 34 | } 35 | } else { 36 | if (!($node = Node::loadByPath($path))) { 37 | throw new \Exception("No node exists at remote path '$path'."); 38 | } 39 | } 40 | 41 | if ($this->config['json.pretty']) { 42 | $this->output->writeln(json_encode($node, JSON_PRETTY_PRINT)); 43 | } else { 44 | $this->output->writeln(json_encode($node)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/MkdirCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/3/15 5 | * Time: 10:14 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class MkdirCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('mkdir') 18 | ->setDescription('Create a new remote directory given a path') 19 | ->addArgument('remote_path', InputArgument::REQUIRED, 'The remote path to create'); 20 | } 21 | 22 | protected function main() 23 | { 24 | $this->init(); 25 | 26 | $remotePath = $this->input->getArgument('remote_path'); 27 | 28 | if ($node = Node::loadByPath($remotePath)) { 29 | throw new \Exception("Node already exists at remote path '$remotePath'. Make sure it's not in the trash."); 30 | } 31 | 32 | $result = $this->clouddrive->createDirectoryPath($remotePath); 33 | 34 | if (!$result['success']) { 35 | $this->output->writeln("Failed to create remote path '$remotePath': " . json_encode($result['data'])); 36 | } else { 37 | $this->output->writeln("Successfully created remote path '$remotePath': " . json_encode($result['data'])); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/MoveCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/6/15 5 | * Time: 12:35 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class MoveCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('mv') 18 | ->setDescription('Move a node to a new remote folder') 19 | ->addArgument('node', InputArgument::REQUIRED, 'Remote node path to move') 20 | ->addArgument('new_path', InputArgument::REQUIRED, 'Remote folder to move node into'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $nodePath = $this->input->getArgument('node'); 28 | $newPath = $this->input->getArgument('new_path'); 29 | 30 | if (!($node = Node::loadByPath($nodePath))) { 31 | throw new \Exception("No node exists at remote path '$nodePath'."); 32 | } 33 | 34 | if (!($newParent = Node::loadByPath($newPath))) { 35 | throw new \Exception("No node exists at remote path '$newPath'."); 36 | } 37 | 38 | $result = $node->move($newParent); 39 | if ($result['success']) { 40 | $this->output->writeln( 41 | "Successfully moved node '{$node['name']}' to '$newPath'" 42 | ); 43 | if ($this->output->isVerbose()) { 44 | $this->output->writeln(json_encode($result['data'])); 45 | } 46 | } else { 47 | $this->output->getErrorOutput()->writeln( 48 | "Failed to move node '{$node['name']}' to '$newPath'" 49 | ); 50 | if ($this->output->isVerbose()) { 51 | $this->output->getErrorOutput()->writeln(json_encode($result['data'])); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/QuotaCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/9/15 5 | * Time: 7:59 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | class QuotaCommand extends Command 11 | { 12 | protected function configure() 13 | { 14 | $this->setName('quota') 15 | ->setDescription('Show Cloud Drive quota'); 16 | } 17 | 18 | protected function main() 19 | { 20 | $this->init(); 21 | 22 | $result = $this->clouddrive->getAccount()->getQuota(); 23 | if ($result['success']) { 24 | if ($this->config['json.pretty']) { 25 | $this->output->writeln(json_encode($result['data'], JSON_PRETTY_PRINT)); 26 | } else { 27 | $this->output->writeln(json_encode($result['data'])); 28 | } 29 | } else { 30 | $this->output->writeln("Failed to retrieve accoutn quota: " . json_encode($result['data'])); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/RenameCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/5/15 5 | * Time: 2:47 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class RenameCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('rename') 18 | ->setDescription('Rename remote node') 19 | ->addArgument('remote_path', InputArgument::REQUIRED, 'Path to remote node') 20 | ->addArgument('name', InputArgument::REQUIRED, 'New name for the node') 21 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 22 | } 23 | 24 | protected function main() 25 | { 26 | $this->init(); 27 | 28 | $remotePath = $this->input->getArgument('remote_path'); 29 | $name = $this->input->getArgument('name'); 30 | 31 | if ($this->input->getOption('id')) { 32 | if (!($node = Node::loadById($remotePath))) { 33 | throw new \Exception("No node exists with ID '$remotePath'."); 34 | } 35 | } else { 36 | if (!($node = Node::loadByPath($remotePath))) { 37 | throw new \Exception("No node exists at remote path '$remotePath'."); 38 | } 39 | } 40 | 41 | $result = $node->rename($name); 42 | if ($result['success']) { 43 | $this->output->writeln("Successfully renamed '$remotePath' to '$name': " . json_encode($result['data'])); 44 | } else { 45 | $this->output->writeln("Failed to rename '$remotePath' to '$name': " . json_encode($result['data'])); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/RenewCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/17/15 5 | * Time: 11:39 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | class RenewCommand extends Command 11 | { 12 | protected function configure() 13 | { 14 | $this->setName('renew') 15 | ->setDescription('Renew authorization'); 16 | } 17 | 18 | protected function main() 19 | { 20 | $this->init(); 21 | 22 | $result = $this->clouddrive->getAccount()->renewAuthorization(); 23 | if ($result['success']) { 24 | $this->output->writeln("Successfully renewed authorization."); 25 | } else { 26 | $this->output->getErrorOutput() 27 | ->writeln("Failed to renew authorization."); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/ResolveCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/6/15 5 | * Time: 9:06 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class ResolveCommand extends Command 14 | { 15 | protected $onlineCommand = false; 16 | 17 | protected function configure() 18 | { 19 | $this->setName('resolve') 20 | ->setDescription("Return a node's remote path by its ID") 21 | ->addArgument('id', InputArgument::REQUIRED, 'The ID of the node to resolve'); 22 | } 23 | 24 | protected function main() 25 | { 26 | $this->init(); 27 | 28 | $id = $this->input->getArgument('id'); 29 | if (!($node = Node::loadById($id))) { 30 | throw new \Exception("No node exists with ID '$id'."); 31 | } 32 | 33 | $this->output->writeln($node->getPath()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/RestoreCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/4/15 5 | * Time: 11:42 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class RestoreCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('restore') 18 | ->setDescription('Restore a remote node from the trash') 19 | ->addArgument('remote_path', InputArgument::REQUIRED, 'The remote path of the node') 20 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $remotePath = $this->input->getArgument('remote_path'); 28 | 29 | if ($this->input->getOption('id')) { 30 | if (!($node = Node::loadById($remotePath))) { 31 | throw new \Exception("No node exists with ID '$remotePath'."); 32 | } 33 | } else { 34 | if (!($node = Node::loadByPath($remotePath))) { 35 | throw new \Exception("No node exists at remote path '$remotePath'."); 36 | } 37 | } 38 | 39 | $result = $node->restore(); 40 | if ($result['success']) { 41 | $this->output->writeln("Successfully restored node at '$remotePath': " . json_encode($result['data'])); 42 | } else { 43 | $this->output->writeln("Failed to restore node at '$remotePath': " . json_encode($result['data'])); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/SyncCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 10:25 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | class SyncCommand extends Command 11 | { 12 | protected function configure() 13 | { 14 | $this->setName('sync') 15 | ->setDescription('Sync the local cache with Amazon Cloud Drive'); 16 | } 17 | 18 | protected function main() 19 | { 20 | $this->init(); 21 | 22 | $this->output->writeln("Syncing..."); 23 | $this->clouddrive->getAccount()->sync(); 24 | $this->output->writeln("Done."); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/TempLinkCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/21/15 5 | * Time: 3:49 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class TempLinkCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('link') 18 | ->setDescription('Generate a temporary, pre-authenticated download link') 19 | ->addArgument('remote_path', InputArgument::OPTIONAL, 'The remote directory to list') 20 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $remotePath = $this->input->getArgument('remote_path') ?: ''; 28 | 29 | if ($this->input->getOption('id')) { 30 | if (!($node = Node::loadById($remotePath))) { 31 | throw new \Exception("No node exists with ID '$remotePath'."); 32 | } 33 | } else { 34 | if (!($node = Node::loadByPath($remotePath))) { 35 | throw new \Exception("No node exists at remote path '$remotePath'."); 36 | } 37 | } 38 | 39 | if ($node->isFolder()) { 40 | throw new \Exception("Links can only be created for files."); 41 | } 42 | 43 | $response = $node->getMetadata(true); 44 | if ($response['success']) { 45 | if (isset($response['data']['tempLink'])) { 46 | $this->output->writeln($response['data']['tempLink']); 47 | } else { 48 | $this->output->getErrorOutput() 49 | ->writeln("Failed retrieving temporary link. Make sure you have permission."); 50 | } 51 | } else { 52 | $this->output->getErrorOutput() 53 | ->writeln("Failed retrieving metadata for node '$remotePath'"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/TrashCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/4/15 5 | * Time: 11:42 AM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class TrashCommand extends Command 14 | { 15 | protected function configure() 16 | { 17 | $this->setName('rm') 18 | ->setDescription('Move a remote Node to the trash') 19 | ->addArgument('remote_path', InputArgument::REQUIRED, 'The remote path of the node') 20 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $remotePath = $this->input->getArgument('remote_path'); 28 | 29 | if ($this->input->getOption('id')) { 30 | if (!($node = Node::loadById($remotePath))) { 31 | throw new \Exception("No node exists with ID '$remotePath'."); 32 | } 33 | } else { 34 | if (!($node = Node::loadByPath($remotePath))) { 35 | throw new \Exception("No node exists at remote path '$remotePath'."); 36 | } 37 | } 38 | 39 | $result = $node->trash(); 40 | if ($result['success']) { 41 | $this->output->writeln("Successfully trashed node at '$remotePath'"); 42 | if ($this->output->isVerbose()) { 43 | $this->output->writeln(json_encode($result['data'])); 44 | } 45 | } else { 46 | $this->output->getErrorOutput()->writeln("Failed to trash node at '$remotePath'"); 47 | if ($this->output->isVerbose()) { 48 | $this->output->getErrorOutput() 49 | ->writeln(json_encode($result['data'])); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/TreeCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/14/15 5 | * Time: 2:35 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use CloudDrive\Node; 11 | use Symfony\Component\Console\Input\InputArgument; 12 | 13 | class TreeCommand extends Command 14 | { 15 | protected $onlineCommand = false; 16 | 17 | protected function configure() 18 | { 19 | $this->setName('tree') 20 | ->setDescription('Print directory tree of the given node') 21 | ->addArgument('path', InputArgument::OPTIONAL, 'The remote path of the node') 22 | ->addOption('assets', 'a', null, 'Include assets in output') 23 | ->addOption('id', 'i', null, 'Designate the remote node by its ID instead of its remote path') 24 | ->addOption('markdown', 'm', null, 'Output the tree in Markdown'); 25 | } 26 | 27 | protected function main() 28 | { 29 | $this->init(); 30 | 31 | $path = $this->input->getArgument('path') ?: ''; 32 | 33 | $includeAssets = $this->input->getOption('assets') ? true : false; 34 | 35 | if ($this->input->getOption('id')) { 36 | if (!($node = Node::loadById($path))) { 37 | throw new \Exception("No node exists with ID '$path'."); 38 | } 39 | } else { 40 | if (!($node = Node::loadByPath($path))) { 41 | throw new \Exception("No node exists at remote path '$path'."); 42 | } 43 | } 44 | 45 | if ($this->input->getOption('markdown')) { 46 | $this->buildMarkdownTree($node, $includeAssets); 47 | } else { 48 | $this->buildAsciiTree($node, $includeAssets); 49 | } 50 | } 51 | 52 | protected function buildAsciiTree(Node $node, $includeAssets = false, $prefix = '') 53 | { 54 | static $first; 55 | if (is_null($first)) { 56 | $first = false; 57 | 58 | if ($node->isFolder()) { 59 | $this->output->writeln("{$node['name']}"); 60 | } else { 61 | $this->output->writeln($node['name']); 62 | } 63 | } 64 | 65 | $children = $node->getChildren(); 66 | for ($i = 0, $count = count($children); $i < $count; ++$i) { 67 | $itemPrefix = $prefix; 68 | $next = $children[$i]; 69 | 70 | if ($i === $count - 1) { 71 | if ($next->isFolder()) { 72 | $itemPrefix .= '└─┬ '; 73 | } else { 74 | if ($next->isFile() || $includeAssets === true) { 75 | $itemPrefix .= '└── '; 76 | } 77 | } 78 | } else { 79 | if ($next->isFolder()) { 80 | $itemPrefix .= '├─┬ '; 81 | } else { 82 | if ($next->isFile() || $includeAssets === true) { 83 | $itemPrefix .= '├── '; 84 | } 85 | } 86 | } 87 | 88 | if ($next->isFolder()) { 89 | $this->output->writeln( 90 | $itemPrefix . '' . (string)$next['name'] . '' 91 | ); 92 | } else { 93 | $this->output->writeln( 94 | $itemPrefix . (string)$next['name'] 95 | ); 96 | } 97 | 98 | if ($next->isFolder() || $includeAssets === true) { 99 | $this->buildAsciiTree( 100 | $next, 101 | $includeAssets, 102 | $prefix . ($i == $count - 1 ? ' ' : '| ') 103 | ); 104 | } 105 | } 106 | } 107 | 108 | protected function buildMarkdownTree(Node $node, $includeAssets = false, $prefix = '') 109 | { 110 | static $first; 111 | if (is_null($first)) { 112 | $first = false; 113 | 114 | if ($node->isFolder()) { 115 | $this->output->writeln("{$node['name']}"); 116 | } else { 117 | $this->output->writeln($node['name']); 118 | } 119 | } 120 | 121 | foreach ($node->getChildren() as $node) { 122 | $this->output->writeln("$prefix- {$node['name']}"); 123 | if ($node->isFolder() || $includeAssets === true) { 124 | $this->buildMarkdownTree($node, $includeAssets, "$prefix "); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/UploadCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/11/15 5 | * Time: 1:02 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | use Symfony\Component\Console\Input\InputArgument; 11 | 12 | class UploadCommand extends Command 13 | { 14 | protected function configure() 15 | { 16 | $this->setName('upload') 17 | ->setDescription('Upload local file or folder to remote directory') 18 | ->addArgument('local_path', InputArgument::REQUIRED, 'The location of the local file') 19 | ->addArgument('remote_path', InputArgument::OPTIONAL, 'The remote folder to upload to') 20 | ->addOption('overwrite', 'o', null, 'Overwrite remote file if file exists and does not match local copy'); 21 | } 22 | 23 | protected function main() 24 | { 25 | $this->init(); 26 | 27 | $overwrite = $this->input->getOption('overwrite') ?: false; 28 | 29 | $source = realpath($this->input->getArgument('local_path')); 30 | $remote = $this->input->getArgument('remote_path') ?: ''; 31 | 32 | if (is_dir($source)) { 33 | $this->clouddrive->uploadDirectory($source, $remote, $overwrite, [$this, 'outputResult']); 34 | } else { 35 | $response = $this->clouddrive->uploadFile($source, $remote, $overwrite, $this->config['upload.duplicates']); 36 | $this->outputResult($response, [ 37 | 'name' => $source, 38 | ]); 39 | } 40 | } 41 | 42 | public function outputResult($response, $info = null) 43 | { 44 | if ($response['success']) { 45 | $this->output->writeln("Successfully uploaded file '{$info['name']}'"); 46 | if ($this->output->isVerbose()) { 47 | $this->output->writeln(json_encode($response)); 48 | } 49 | } else { 50 | $this->output->getErrorOutput() 51 | ->writeln("Failed to upload file '{$info['name']}': {$response['data']['message']}"); 52 | if ($this->output->isVerbose()) { 53 | $this->output->getErrorOutput()->writeln(json_encode($response)); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/CloudDrive/Commands/UsageCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 8/9/15 5 | * Time: 8:04 PM 6 | */ 7 | 8 | namespace CloudDrive\Commands; 9 | 10 | class UsageCommand extends Command 11 | { 12 | protected function configure() 13 | { 14 | $this->setName('usage') 15 | ->setDescription('Show Cloud Drive usage'); 16 | } 17 | 18 | protected function main() 19 | { 20 | $this->init(); 21 | 22 | $result = $this->clouddrive->getAccount()->getUsage(); 23 | if ($result['success']) { 24 | if ($this->config['json.pretty']) { 25 | $this->output->writeln(json_encode($result['data'], JSON_PRETTY_PRINT)); 26 | } else { 27 | $this->output->writeln(json_encode($result['data'])); 28 | } 29 | } else { 30 | $this->output->getErrorOutput() 31 | ->writeln("Failed to retrieve account quota"); 32 | if ($this->output->isVerbose()) { 33 | $this->output->getErrorOutput() 34 | ->writeln(json_encode($result['data'])); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CloudDrive/Node.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 7/10/15 5 | * Time: 3:36 PM 6 | */ 7 | 8 | namespace CloudDrive; 9 | 10 | use ArrayAccess; 11 | use Countable; 12 | use GuzzleHttp\Client; 13 | use IteratorAggregate; 14 | use JsonSerializable; 15 | use Utility\Traits\Bag; 16 | 17 | /** 18 | * Class representing a remote `Node` object in Amazon's CloudDrive. 19 | * 20 | * @package CloudDrive 21 | */ 22 | class Node implements ArrayAccess, IteratorAggregate, JsonSerializable, Countable 23 | { 24 | use Bag { 25 | __construct as constructor; 26 | } 27 | 28 | /** 29 | * Cloud Drive `Account` object 30 | * 31 | * @var \CloudDrive\Account 32 | */ 33 | protected static $account; 34 | 35 | /** 36 | * Local `Cache` storage object 37 | * 38 | * @var \CloudDrive\Cache 39 | */ 40 | protected static $cacheStore; 41 | 42 | /** 43 | * HTTP client 44 | * 45 | * @var \GuzzleHttp\Client 46 | */ 47 | protected static $httpClient; 48 | 49 | /** 50 | * Flag set if the `Node` class has already been initialized 51 | * 52 | * @var bool 53 | */ 54 | protected static $initialized = false; 55 | 56 | /** 57 | * Construct a new instance of a remote `Node` object given the metadata 58 | * provided. 59 | * 60 | * @param array $data 61 | * 62 | * @throws \Exception 63 | */ 64 | public function __construct($data = []) 65 | { 66 | if (self::$initialized === false) { 67 | throw new \Exception("`Node` class must first be initialized."); 68 | } 69 | 70 | $this->constructor($data); 71 | } 72 | 73 | /** 74 | * Delete a `Node` and its parent associations. 75 | * 76 | * @return bool 77 | */ 78 | public function delete() 79 | { 80 | return self::$cacheStore->deleteNodeById($this['id']); 81 | } 82 | 83 | /** 84 | * Download contents of `Node` to local save path. If only the local 85 | * directory is given, the file will be saved as its remote name. 86 | * 87 | * @param resource|string $dest 88 | * @param callable $callback 89 | * 90 | * @return array 91 | * @throws \Exception 92 | */ 93 | public function download($dest, $callback = null) 94 | { 95 | if ($this->isFolder()) { 96 | return $this->downloadFolder($dest, $callback); 97 | } 98 | 99 | return $this->downloadFile($dest, $callback); 100 | } 101 | 102 | /** 103 | * Save a FILE node to the specified local destination. 104 | * 105 | * @param resource|string $dest 106 | * @param callable $callback 107 | * 108 | * @return array 109 | * @throws \Exception 110 | */ 111 | protected function downloadFile($dest, $callback = null) 112 | { 113 | $retval = [ 114 | 'success' => false, 115 | 'data' => [], 116 | ]; 117 | 118 | if (is_resource($dest)) { 119 | $handle = $dest; 120 | $metadata = stream_get_meta_data($handle); 121 | $dest = $metadata["uri"]; 122 | } else { 123 | $dest = rtrim($dest, '/'); 124 | 125 | if (file_exists($dest)) { 126 | if (is_dir($dest)) { 127 | $dest = rtrim($dest, '/') . "/{$this['name']}"; 128 | } else { 129 | $retval['data']['message'] = "File already exists at '$dest'."; 130 | if (is_callable($callback)) { 131 | call_user_func($callback, $retval, $dest); 132 | } 133 | 134 | return $retval; 135 | } 136 | } 137 | 138 | $handle = @fopen($dest, 'a'); 139 | 140 | if (!$handle) { 141 | throw new \Exception("Unable to open file at '$dest'. Make sure the directory path exists."); 142 | } 143 | } 144 | 145 | $response = self::$httpClient->get( 146 | self::$account->getContentUrl() . "nodes/{$this['id']}/content", 147 | [ 148 | 'headers' => [ 149 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 150 | ], 151 | 'stream' => true, 152 | 'exceptions' => false, 153 | ] 154 | ); 155 | 156 | $retval['data'] = json_decode((string)$response->getBody(), true); 157 | 158 | if ($response->getStatusCode() !== 200) { 159 | if (is_callable($callback)) { 160 | call_user_func($callback, $retval, $dest); 161 | } 162 | 163 | return $retval; 164 | } 165 | 166 | $retval['success'] = true; 167 | 168 | $body = $response->getBody(); 169 | while (!$body->eof()) { 170 | fwrite($handle, $body->read(1024)); 171 | } 172 | 173 | fclose($handle); 174 | 175 | if (is_callable($callback)) { 176 | call_user_func($callback, $retval, $dest); 177 | } 178 | 179 | return $retval; 180 | } 181 | 182 | /** 183 | * Recursively download all children of a FOLDER node to the specified 184 | * local destination. 185 | * 186 | * @param string $dest 187 | * @param callable $callback 188 | * 189 | * @return array 190 | * @throws \Exception 191 | */ 192 | protected function downloadFolder($dest, $callback = null) 193 | { 194 | $retval = [ 195 | 'success' => false, 196 | 'data' => [], 197 | ]; 198 | 199 | if (!is_string($dest)) { 200 | throw new \Exception("Must pass in local path to download directory to."); 201 | } 202 | 203 | $nodes = $this->getChildren(); 204 | 205 | $dest = rtrim($dest) . "/{$this['name']}"; 206 | if (!file_exists($dest)) { 207 | mkdir($dest); 208 | } 209 | 210 | foreach ($nodes as $node) { 211 | if ($node->isFile()) { 212 | $node->download("{$dest}/{$node['name']}", $callback); 213 | } else if ($node->isFolder()) { 214 | $node->download($dest, $callback); 215 | } 216 | } 217 | 218 | $retval['success'] = true; 219 | 220 | return $retval; 221 | } 222 | 223 | /** 224 | * Search for nodes in the local cache by filters. 225 | * 226 | * @param array $filters 227 | * 228 | * @return array 229 | */ 230 | public static function filter(array $filters) 231 | { 232 | return self::$cacheStore->filterNodes($filters); 233 | } 234 | 235 | /** 236 | * Get all children of the given `Node`. 237 | * 238 | * @return array 239 | */ 240 | public function getChildren() 241 | { 242 | return self::$cacheStore->getNodeChildren($this); 243 | } 244 | 245 | /** 246 | * Get the MD5 checksum of the `Node`. 247 | * 248 | * @return mixed 249 | */ 250 | public function getMd5() 251 | { 252 | return $this['contentProperties']['md5']; 253 | } 254 | 255 | /** 256 | * Retrieve the node's metadata directly from the API. 257 | * 258 | * @param bool|false $tempLink 259 | * 260 | * @return array 261 | */ 262 | public function getMetadata($tempLink = false) 263 | { 264 | $retval = [ 265 | 'success' => false, 266 | 'data' => [], 267 | ]; 268 | 269 | $query = []; 270 | 271 | if ($tempLink) { 272 | $query['tempLink'] = true; 273 | } 274 | 275 | $response = self::$httpClient->get( 276 | self::$account->getMetadataUrl() . "nodes/{$this['id']}", 277 | [ 278 | 'headers' => [ 279 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 280 | ], 281 | 'query' => [ 282 | 'tempLink' => $tempLink ? 'true' : 'false', 283 | ], 284 | 'exceptions' => false, 285 | ] 286 | ); 287 | 288 | $retval['data'] = json_decode((string)$response->getBody(), true); 289 | 290 | if ($response->getStatusCode() === 200) { 291 | $retval['success'] = true; 292 | } 293 | 294 | return $retval; 295 | } 296 | 297 | /** 298 | * Build and return the remote directory path of the given `Node`. 299 | * 300 | * @return string 301 | * @throws \Exception 302 | */ 303 | public function getPath() 304 | { 305 | $node = $this; 306 | $path = []; 307 | 308 | while (true) { 309 | $path[] = $node["name"]; 310 | if ($node->isRoot()) { 311 | break; 312 | } 313 | 314 | $node = self::loadById($node["parents"][0]); 315 | if (is_null($node)) { 316 | throw new \Exception("No parent node found with ID {$node['parents'][0]}."); 317 | } 318 | 319 | if ($node->isRoot()) { 320 | break; 321 | } 322 | } 323 | 324 | $path = array_reverse($path); 325 | 326 | return implode('/', $path); 327 | } 328 | 329 | /** 330 | * Set the local storage cache. 331 | * 332 | * @param \CloudDrive\Account $account 333 | * @param \CloudDrive\Cache $cacheStore 334 | * 335 | * @throws \Exception 336 | */ 337 | public static function init(Account $account, Cache $cacheStore) 338 | { 339 | if (self::$initialized === true) { 340 | throw new \Exception("`Node` class has already been initialized."); 341 | } 342 | 343 | self::$account = $account; 344 | self::$cacheStore = $cacheStore; 345 | self::$httpClient = new Client(); 346 | 347 | self::$initialized = true; 348 | } 349 | 350 | /** 351 | * Return `true` if the node is in the trash. 352 | * 353 | * @return bool 354 | */ 355 | public function inTrash() 356 | { 357 | return $this['status'] === 'TRASH'; 358 | } 359 | 360 | /** 361 | * Returns whether the `Node` is an asset or not. 362 | * 363 | * @return bool 364 | */ 365 | public function isAsset() 366 | { 367 | return $this['kind'] === 'ASSET'; 368 | } 369 | 370 | /** 371 | * Returns whether the `Node` is a file or not. 372 | * 373 | * @return bool 374 | */ 375 | public function isFile() 376 | { 377 | return $this['kind'] === 'FILE'; 378 | } 379 | 380 | /** 381 | * Returns whether the `Node` is a folder or not. 382 | * 383 | * @return bool 384 | */ 385 | public function isFolder() 386 | { 387 | return $this['kind'] === 'FOLDER'; 388 | } 389 | 390 | /** 391 | * Returns whether the `Node` is the `root` node. 392 | * 393 | * @return bool 394 | */ 395 | public function isRoot() 396 | { 397 | return $this['isRoot'] ? true : false; 398 | } 399 | 400 | /** 401 | * Load a `Node` given an ID or remote path. 402 | * 403 | * @param string $param Parameter to find the `Node` by: ID or path 404 | * 405 | * @return \CloudDrive\Node|null 406 | */ 407 | public static function load($param) 408 | { 409 | if (!($node = self::loadById($param))) { 410 | $node = self::loadByPath($param); 411 | } 412 | 413 | return $node; 414 | } 415 | 416 | /** 417 | * Find and return the `Node` matching the given ID. 418 | * 419 | * @param int|string $id ID of the node 420 | * 421 | * @return \CloudDrive\Node|null 422 | */ 423 | public static function loadById($id) 424 | { 425 | return self::$cacheStore->findNodeById($id); 426 | } 427 | 428 | /** 429 | * Find and return `Nodes` that have the given MD5 checksum. 430 | * 431 | * @param string $md5 432 | * 433 | * @return array 434 | */ 435 | public static function loadByMd5($md5) 436 | { 437 | return self::$cacheStore->findNodesByMd5($md5); 438 | } 439 | 440 | /** 441 | * Find all nodes whose name matches the given string. 442 | * 443 | * @param string $name 444 | * 445 | * @return array 446 | */ 447 | public static function loadByName($name) 448 | { 449 | return self::$cacheStore->findNodesByName($name); 450 | } 451 | 452 | /** 453 | * Find and return `Node` that matches the given remote path. 454 | * 455 | * @param string $path Remote path of the `Node` 456 | * 457 | * @return \CloudDrive\Node|null 458 | * @throws \Exception 459 | */ 460 | public static function loadByPath($path) 461 | { 462 | $path = trim($path, '/'); 463 | if (!$path) { 464 | return self::loadRoot(); 465 | } 466 | 467 | $info = pathinfo($path); 468 | $nodes = self::loadByName($info['basename']); 469 | if (empty($nodes)) { 470 | return null; 471 | } 472 | 473 | foreach ($nodes as $node) { 474 | if ($node->getPath() === $path) { 475 | return $node; 476 | } 477 | } 478 | 479 | return null; 480 | } 481 | 482 | /** 483 | * Return the root `Node`. 484 | * 485 | * @return \CloudDrive\Node 486 | * @throws \Exception 487 | */ 488 | public static function loadRoot() 489 | { 490 | $results = self::loadByName('Cloud Drive'); 491 | if (empty($results)) { 492 | throw new \Exception("No node by name 'Cloud Drive' found in the database."); 493 | } 494 | 495 | foreach ($results as $result) { 496 | if ($result->isRoot()) { 497 | return $result; 498 | } 499 | } 500 | 501 | throw new \Exception("Unable to find root node."); 502 | } 503 | 504 | /** 505 | * Move a FILE or FOLDER `Node` to a new remote location. 506 | * 507 | * @param \CloudDrive\Node $newFolder 508 | * 509 | * @return array 510 | * @throws \Exception 511 | */ 512 | public function move(Node $newFolder) 513 | { 514 | if (!$newFolder->isFolder()) { 515 | throw new \Exception("New destination node is not a folder."); 516 | } 517 | 518 | if (!$this->isFile() && !$this->isFolder()) { 519 | throw new \Exception("Moving a node can only be performed on FILE and FOLDER kinds."); 520 | } 521 | 522 | $retval = [ 523 | 'success' => false, 524 | 'data' => [], 525 | ]; 526 | 527 | $response = self::$httpClient->post( 528 | self::$account->getMetadataUrl() . "nodes/{$newFolder['id']}/children", 529 | [ 530 | 'headers' => [ 531 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 532 | ], 533 | 'json' => [ 534 | 'fromParent' => $this['parents'][0], 535 | 'childId' => $this['id'], 536 | ], 537 | 'exceptions' => false, 538 | ] 539 | ); 540 | 541 | $retval['data'] = json_decode((string)$response->getBody(), true); 542 | 543 | if ($response->getStatusCode() === 200) { 544 | $retval['success'] = true; 545 | $this->replace($retval['data']); 546 | $this->save(); 547 | } 548 | 549 | return $retval; 550 | } 551 | 552 | /** 553 | * Replace file contents of the `Node` with the file located at the given 554 | * local path. 555 | * 556 | * @param string $localPath 557 | * 558 | * @return array 559 | */ 560 | public function overwrite($localPath) 561 | { 562 | $retval = [ 563 | 'success' => false, 564 | 'data' => [], 565 | ]; 566 | 567 | $response = self::$httpClient->put( 568 | self::$account->getContentUrl() . "nodes/{$this['id']}/content", 569 | [ 570 | 'headers' => [ 571 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 572 | ], 573 | 'multipart' => [ 574 | [ 575 | 'name' => 'content', 576 | 'contents' => fopen($localPath, 'r'), 577 | ], 578 | ], 579 | 'exceptions' => false, 580 | ] 581 | ); 582 | 583 | $retval['data'] = json_decode((string)$response->getBody(), true); 584 | 585 | if ($response->getStatusCode() === 200) { 586 | $retval['success'] = true; 587 | } 588 | 589 | return $retval; 590 | } 591 | 592 | /** 593 | * Modify the name of a remote `Node`. 594 | * 595 | * @param string $name 596 | * 597 | * @return array 598 | */ 599 | public function rename($name) 600 | { 601 | $retval = [ 602 | 'success' => false, 603 | 'data' => [], 604 | ]; 605 | 606 | $response = self::$httpClient->patch( 607 | self::$account->getMetadataUrl() . "nodes/{$this['id']}", 608 | [ 609 | 'headers' => [ 610 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 611 | ], 612 | 'json' => [ 613 | 'name' => $name, 614 | ], 615 | 'exceptions' => false, 616 | ] 617 | ); 618 | 619 | $retval['data'] = json_decode((string)$response->getBody(), true); 620 | 621 | if ($response->getStatusCode() === 200) { 622 | $retval['success'] = true; 623 | $this->replace($retval['data']); 624 | $this->save(); 625 | } 626 | 627 | return $retval; 628 | } 629 | 630 | /** 631 | * Restore the `Node` from the trash. 632 | * 633 | * @return array 634 | */ 635 | public function restore() 636 | { 637 | $retval = [ 638 | 'success' => false, 639 | 'data' => [], 640 | ]; 641 | 642 | if ($this['status'] === 'AVAILABLE') { 643 | $retval['data']['message'] = 'Node is already available.'; 644 | 645 | return $retval; 646 | } 647 | 648 | $response = self::$httpClient->post( 649 | self::$account->getMetadataUrl() . "trash/{$this['id']}/restore", 650 | [ 651 | 'headers' => [ 652 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 653 | ], 654 | 'exceptions' => false, 655 | ] 656 | ); 657 | 658 | $retval['data'] = json_decode((string)$response->getBody(), true); 659 | 660 | if ($response->getStatusCode() === 200) { 661 | $retval['success'] = true; 662 | $this->replace($retval['data']); 663 | $this->save(); 664 | } 665 | 666 | return $retval; 667 | } 668 | 669 | /** 670 | * Save the `Node` to the local cache. 671 | * 672 | * @return bool 673 | */ 674 | public function save() 675 | { 676 | return self::$cacheStore->saveNode($this); 677 | } 678 | 679 | /** 680 | * Find all nodes that contain a string in the name. 681 | * 682 | * @param string $name 683 | * 684 | * @return array 685 | */ 686 | public static function searchNodesByName($name) 687 | { 688 | return self::$cacheStore->searchNodesByName($name); 689 | } 690 | 691 | /** 692 | * Add the `Node` to trash. 693 | * 694 | * @return array 695 | */ 696 | public function trash() 697 | { 698 | $retval = [ 699 | 'success' => false, 700 | 'data' => [], 701 | ]; 702 | 703 | if ($this['status'] === 'TRASH') { 704 | $retval['data']['message'] = 'Node is already in trash.'; 705 | 706 | return $retval; 707 | } 708 | 709 | $response = self::$httpClient->put( 710 | self::$account->getMetadataUrl() . "trash/{$this['id']}", 711 | [ 712 | 'headers' => [ 713 | 'Authorization' => 'Bearer ' . self::$account->getToken()['access_token'], 714 | ], 715 | 'exceptions' => false, 716 | ] 717 | ); 718 | 719 | $retval['data'] = json_decode((string)$response->getBody(), true); 720 | 721 | if ($response->getStatusCode() === 200) { 722 | $retval['success'] = true; 723 | $this->replace($retval['data']); 724 | $this->save(); 725 | } 726 | 727 | return $retval; 728 | } 729 | } 730 | --------------------------------------------------------------------------------