├── .gitignore ├── .jshintrc ├── .npmignore ├── CHANGELOG.md ├── Gulpfile.js ├── README.md ├── bin └── clouddrive ├── index.js ├── lib ├── Account.js ├── Cache │ ├── Cache.js │ ├── Mongo.js │ ├── MySQL.js │ ├── SQL.js │ └── SQLite3.js ├── Commands │ ├── AboutCommand.js │ ├── CatCommand.js │ ├── ClearCacheCommand.js │ ├── Command.js │ ├── ConfigCommand.js │ ├── DecryptCommand.js │ ├── DeleteEverythingCommand.js │ ├── DiskUsageCommand.js │ ├── DownloadCommand.js │ ├── EncryptCommand.js │ ├── ExistsCommand.js │ ├── FindCommand.js │ ├── InfoCommand.js │ ├── InitCommand.js │ ├── LinkCommand.js │ ├── ListCommand.js │ ├── ListPendingCommand.js │ ├── ListTrashCommand.js │ ├── MetadataCommand.js │ ├── MkdirCommand.js │ ├── MoveCommand.js │ ├── QuotaCommand.js │ ├── RenameCommand.js │ ├── ResolveCommand.js │ ├── RestoreCommand.js │ ├── ShareCommand.js │ ├── SyncCommand.js │ ├── TrashCommand.js │ ├── TreeCommand.js │ ├── UnlinkCommand.js │ ├── UpdateCommand.js │ ├── UploadCommand.js │ └── UsageCommand.js ├── Config.js ├── Logger.js ├── Node.js ├── ParameterBag.js ├── ProgressBar.js ├── Utils.js └── cli.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.idea/ 3 | /dist/ 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "evil" : true, 3 | "validthis": true, 4 | "node" : true, 5 | "debug" : true, 6 | "boss" : true, 7 | "expr" : true, 8 | "eqnull" : true, 9 | "quotmark" : "single", 10 | "sub" : true, 11 | "trailing" : true, 12 | "undef" : true, 13 | "laxbreak" : true, 14 | "esnext" : true, 15 | "eqeqeq" : true 16 | } 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `clouddrive-node` will be documented in this file 4 | 5 | ## 0.6.4 6 | 7 | ### Added 8 | - `Utils` now contains method for rounding numbers 9 | - `Utils` now contains method for checking if a value exists in an array 10 | - New `config` option `--config` for passing in a custom config file 11 | - Added global option to force or disable ANSI output 12 | 13 | ### Fixed 14 | - Fixed two memory leaks involving open file streams 15 | 16 | ## 0.6.3 17 | 18 | ### Fixed 19 | - Removed redundant 'node' string in app name after upgrading `env-paths` 20 | 21 | ## 0.6.2 22 | 23 | ### Fixed 24 | - Upgrade version of `env-paths` to fix some thrown Exceptions on CentOS 25 | 26 | ## 0.6.1 27 | 28 | ### Fixed 29 | - `encrypt` and `decrypt` commands now require a source file path 30 | - The software was not properly checking if encryption was active before attempting to remove 'encrypted' file 31 | 32 | ## 0.6.0 33 | 34 | ### Added 35 | - `upload` command now supports `--labels` 36 | - New `update` command lets you update a node's labels and description 37 | - Added support for encrypting / decrypting file names and contents on upload/download 38 | - Added `encrypt` and `decrypt` commands for encrypting/decrypting files locally using the same method as uploading/downloading 39 | - Now supports concurrent upload/download connections 40 | 41 | ## 0.5.0 42 | 43 | ### Added 44 | - Logging: we now not only have better streamlined CLI output with actually working verbosity levels, but these messages are also logged to a file. 45 | - Upload can check and detect existing files based on their paths and a file's size instead of MD5 checks (which can a long time). This is toggled in the config. 46 | - Now using events to trigger events instead of passing in callbacks everywhere (download, upload, overwrite, etc.) 47 | - Now using custom progress bar since others were either broken in some way or no longer maintained 48 | - `UploadCommand` now supports the `--checksum` option in addition to the default config value 49 | - `delete-everything` command now used to delete all CLI files and folders 50 | - `upload` and `download` now supports encryption 51 | 52 | ### Breaking Changes 53 | - `cache` and `config` directories are now stored using the [env-paths](https://github.com/sindresorhus/env-paths) package. NOTE: you will need to either manually move your existing files or re-run `init` and `sync` with this new version. 54 | - `Account` object now accepts `cache` as its second parameter since `clientId` and `clientSecret` are optional. 55 | 56 | ### Fixed 57 | - `request` library has been swapped out for `got` 58 | - `colors` has been swapped out for `chalk` 59 | - `unlink` command now requires child ID since constructing the path of a node with multiple parents is currently only returning one path 60 | - `exists` command can now check for individual files, not just directories 61 | - Changed `appConfig` to `cliConfig` and `defaultConfig` to `appConfig` in `cli.js` (makes more sense) 62 | 63 | ## 0.4.0 64 | 65 | ### Added 66 | - `sync` commands `chunkSize` and `maxNodes` options are not configurable via the `config` command 67 | - `exists` command recursively checks local directory to check if the files exist remotely without uploading any new files 68 | - `link` and `unlink` commands can be used to add a node or remove it from a parent (the old `link` command is now `share`) 69 | - Some functionality is now optionally `remote` (such as `getChildren` and `getTrash`) that can go to the API rather than relying on the local cache 70 | 71 | ### Fixed 72 | - Default config for the `Config` object has been moved outside of the class file 73 | - `rm` and `restore` commands now have a recursive option to delete all children nodes 74 | 75 | ## 0.3.2 76 | 77 | ### Fixed 78 | - Failed file upload where response body is invalid now uses retry attempts to upload again 79 | - We now "force" reauthentication to Amazon in some cases even if our token hasn't expired 80 | 81 | ## 0.3.1 82 | 83 | ### Added 84 | - Updated `yargs` package to version 4 85 | 86 | ### Fixed 87 | - Now properly building `dist` code for `npm` distribution and including it (added `.npmignore`) when publishing 88 | 89 | ## 0.3.0 90 | 91 | ### Added 92 | - `cat` command outputs contents of remote file to STDOUT 93 | - Added "searching" spinner when running `find` command 94 | - Refactored the entire codebase to use several ES6 features including classes, template strings, arrow function notations, etc 95 | - Failed uploads due to expired tokens now retry `x` number of times (set in the config) 96 | - Converted base CLI framework from `commander` to `yargs`. 97 | - `sync` function now accepts parameters (i.e., `chunkSize`, `maxNodes`) 98 | - Incomplete downloads now have file prefix (`.__incomplete`) 99 | - Can now specify dimensions when downloading images 100 | - More color information on listings output (red = in trash, yellow = pending) 101 | - Added `config` option to toggle ANSI colors 102 | - Added `config` option to toggle display of progress bars 103 | - `Config` is now it's own object and separated from the `Command` class 104 | - Added a `force` flag on file upload to overwrite remote node's contents even if the MD5 matches the local file 105 | - Added `config` option to bypass MD5 check when downloading files 106 | 107 | ### Fixed 108 | - `download` command no longer outputs multiple "failure" messages when it fails to download remote file 109 | - Fixed authorization renewal issue where we weren't properly checking of the API key OR secret were both invalid 110 | - Fixed exception when attempting to upload to `root` without any notation (empty path). 111 | - Fixed bug where we were not properly reading boolean values from the saved config. 112 | - All `async` operations now properly pass up their errors 113 | - A file `Node` will automatically be overwritten on upload if its status is `PENDING` 114 | - Node version check is now the first thing and with as few dependencies as possible 115 | 116 | ## 0.2.2 117 | 118 | ### Added 119 | - `downloadFile` options now accepts an optional stream to write to 120 | - `info` command retrieves and displays account information from Amazon 121 | - Added a "catch all" for invalid commands instead of `clouddrive` not outputting anything 122 | - Now using ES6 template strings 123 | 124 | ### Fixed 125 | - Incorrect prototypal inheritance with some objects 126 | - `authorize` call on `Account` ALWAYS checks to make sure we have a `metadataUrl` and `contentUrl`, not just on initial authorization 127 | - Fixed output size of year string (and padding) for nodes not created in the current year 128 | - `init` now outputs the URL for initial authentication for systems without a UI 129 | - Added `repository` field in `package.json` 130 | - Removed `promise` npm package dependency (using native promises) 131 | 132 | ## 0.2.1 133 | 134 | ### Added 135 | - This app can now be used without needing API credentials! If you attempt to `init` your account without specifying a `client-id` or `client-secret`, the app will request authorization to your account via credentials owned by me (the creator of this repo). NOTE: No personal information is saved 136 | - Added various options to pass into `download` function. This provides ability to do things like output progress bars, run code before download, and run code on completion. The CLI now utilizes this for displaying progress bars for each file downloaded/uploaded 137 | 138 | ### Deprecated 139 | 140 | ### Fixed 141 | - Was not exposing the code as an NPM module properly 142 | - Added `try/catch` support for `Errors` in command functions 143 | - `pretty.json` config value actually controls JSON output formatting now 144 | 145 | ## 0.2.0 146 | 147 | ### Added 148 | - "Initial" release 149 | 150 | ## 0.1.0 151 | 152 | ### Added 153 | - Mistake `npm` publish. Can no longer reference this version number, so `0.2.0` is the "initial" release 154 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var async = require('async'); 3 | var jshint = require('gulp-jshint'); 4 | var babel = require('gulp-babel'); 5 | 6 | gulp.task('lint', function () { 7 | gulp.src([ 8 | './*.js', 9 | './lib/*.js', 10 | './lib/**/*.js' 11 | ]) 12 | .pipe(jshint()) 13 | .pipe(jshint.reporter('default')); 14 | }); 15 | 16 | gulp.task('build', (finished) => { 17 | gulp.src([ 18 | 'lib/*.js', 19 | 'lib/**/*.js' 20 | ]) 21 | .pipe(babel({ 22 | presets: ['es2015'] 23 | })) 24 | .pipe(gulp.dest('dist')); 25 | }); 26 | 27 | gulp.task('default', ['lint', 'build']); 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Cloud Drive CLI and SDK 2 | 3 | Amazon's Cloud Drive offers unlimited cloud storage but no good way to interact with your data (upload, download, find, etc). The web app is lacking and the desktop app is sub-par. So here's a command-line interface and SDK for interacting with Cloud Drive as if it were a filesystem. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install -g clouddrive 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### Initial Authorization 14 | 15 | Before using the CLI, the config values for the application will need to be set. Use the `config` command to view and set the available options. 16 | 17 | ``` 18 | $ clouddrive config 19 | auth.email = 20 | auth.id = 21 | auth.secret = 22 | cli.colors = true 23 | cli.ignoreFiles = ^(\.DS_Store|[Tt]humbs.db)$ 24 | cli.progressBars = true 25 | cli.progressInterval = 250 26 | cli.timestamp = false 27 | database.driver = sqlite 28 | database.host = 127.0.0.1 29 | database.database = clouddrive 30 | database.username = root 31 | database.password = 32 | display.date = modified 33 | display.showPending = true 34 | display.showTrash = true 35 | download.checkMd5 = true 36 | json.pretty = false 37 | log.file = 38 | log.level = info 39 | sync.chunkSize = 40 | sync.maxNodes = 41 | upload.duplicates = false 42 | upload.checkMd5 = false 43 | upload.numRetries = 1 44 | 45 | $ clouddrive config auth.email me@example.com 46 | email saved 47 | ``` 48 | 49 | You will need to set the `email` for the Amazon account you wish to use with the CLI. The first run of the application will require you to run `clouddrive init` to authorize the CLI with your Amazon account. This will open a browser and take you to Amazon for authorization. After authorization, your access token will be printed in the browser. Simply copy and paste this back into the terminal. 50 | 51 | Optionally, if you'd like to use your own Amazon Cloud Drive credentials, set the `auth.client-id` and `auth.client-secret` options using the `config` command. 52 | 53 | ``` 54 | $ clouddrive init 55 | Initializing... 56 | Initial authorization is required 57 | https://www.amazon.com/ap/oa?client_id=... 58 | ? url: 59 | ``` 60 | 61 | Naviage to the URL displayed to to authorize the app with your Cloud Drive account using your credentials. This will redirect your browser to a new URL: paste that URL back into the prompt. 62 | 63 | ### Syncing 64 | 65 | The first time you run the CLI (after initialization), you will need to (and periodically after the initial sync) run the `sync` command to pull down any Cloud Drive changes to the local cache. This local cache is required for the CLI to work and speeds up reading information when 'browsing' Cloud Drive using the CLI. This also makes many commands available for offline use. 66 | 67 | ``` 68 | $ clouddrive sync 69 | ``` 70 | 71 | ### Commands 72 | 73 | The CLI makes interacting with Cloud Drive feel like using a remote filesystem with commands such as `ls`, `du`, `mkdir`, etc. 74 | 75 | ``` 76 | Usage: 77 | clouddrive command [flags] [options] [arguments] 78 | 79 | Commands: 80 | about Print app-specific information 81 | cat Print files to STDOUT 82 | clearcache Clear the local cache 83 | config Read, write, and reset config values 84 | delete-everything Remove all files and folders related to the CLI 85 | download Download remote file or folder to specified local path 86 | du Display the disk usage (recursively) for the specified node 87 | exists Check if a file or folder exists remotely 88 | find Search for nodes by name 89 | info Show Cloud Drive account info 90 | init Initialize and authorize with Amazon Cloud Drive 91 | link Link a file to exist under another directory 92 | ls List all remote nodes belonging to a specified node 93 | metadata Retrieve metadata of a node by its path 94 | mkdir Create a remote directory path (recursively) 95 | mv Move a remote node to a new directory 96 | pending List the nodes that have a status of "PENDING" 97 | quota Show Cloud Drive account quota 98 | rename Rename a remote node 99 | resolve Return the remote path of a node by its ID 100 | restore Restore a remote node from the trash 101 | rm Move a remote Node to the trash 102 | share Generate a temporary, pre-authenticated download link 103 | sync Sync the local cache with Amazon Cloud Drive 104 | trash List the nodes that have a status of "TRASH" 105 | tree Print directory tree of the given node 106 | update Update a node's metadata 107 | unlink Unlink a node from a parent node 108 | upload Upload local file(s) or folder(s) to remote directory 109 | usage Show Cloud Drive account usage 110 | 111 | Global Flags: 112 | -h, --help Show help [boolean] 113 | -v, --verbose Output verbosity: 1 for normal (-v), 2 for more verbose (-vv), 114 | and 3 for debug (-vvv) [count] 115 | -q, --quiet Suppress all output [boolean] 116 | -V, --version Show version number [boolean] 117 | ``` 118 | 119 | ### config 120 | 121 | The `config` command is used for reading, writing, and resetting config values for the CLI. The following options are available: 122 | - `auth.email`: The email to use with the CLI 123 | - `auth.id`: Custom Amazon API credentials if you would like to use your own 124 | - `auth.secret`: Custom Amazon API credentials if you would like to use your own 125 | - `cli.colors`: ANSI color output 126 | - `cli.progressBars`: Display or suppress progress bars 127 | - `database.driver`: Database type to use for the local cache (`sqlite`, `mysql`, or `mongo`) 128 | - `database.host`: Host/IP the database exists on (if not SQLite) 129 | - `database.database`: Database to use (MySQL) 130 | - `database.username`: Database username for authentication 131 | - `database.password`: Database password for authentication 132 | - `display.date`: Display either `modified` or `created` date when listing nodes 133 | - `display.showPending`: Toggle displaying of `PENDING` nodes with `ls` command 134 | - `display.showTrash`: Toggle display of `TRASH` nodes with the `ls` command 135 | - `download.checkMd5`: Perform or suppress MD5 check when downloading files 136 | - `json.pretty`: Whether to format JSON output or not 137 | - `upload.duplicates`: Allow duplicate files to be uploaded to Cloud Drive 138 | - `upload.retryAttempt`: Number of attempts to upload a file 139 | 140 | ### ls 141 | 142 | The `ls` command allows you to view the contents of a folder. If you don't provide a remote path argument, it will display the contents at the root directory. The output provides detailed information for each item including the remote ID, its modified date (or created if you change the config), its status (`AVAILABLE`, `TRASH`), its type (`FILE` or `FOLDER`), its size, and its name. 143 | 144 | You can also provide a node's ID instead of path for the `ls` argument by using the `-i` flag. 145 | 146 | ``` 147 | $ clouddrive ls 148 | 1234564789 Nov 8 15:20 AVAILABLE FOLDER 0B Documents 149 | 0123456829 Mar 30 8:35 AVAILABLE FOLDER 0B Pictures 150 | 8723457923 Aug 23 15:39 TRASH FILE 0B test.txt 151 | ``` 152 | 153 | ### du 154 | 155 | The `du` command will output the total size used by the given file or folder, recursively. Again, if no argument is given, it will calculate the entire used space of your entire Cloud Drive. Passing the `-i` flag will calculate the size of the node by its ID instead of its path. It will also output the total files and folders contained in the path. 156 | 157 | ``` 158 | $ clouddrive du 159 | 174.77MB 160 | 3 files, 1 folders 161 | ``` 162 | 163 | ### upload 164 | 165 | The `upload` command lets you upload files and folders (recursively) to Amazon. Simply pass an arbitrary number of local paths (globbing is supported) and the last argument must be the remote folder to upload the files to. If you want to upload to the top-level directory, simply pass in `/` as the last parameter. 166 | 167 | ``` 168 | $ clouddrive upload ./test/* / 169 | ``` 170 | 171 | ### download 172 | 173 | In addition to uploading files, the `download` command allows you to retrieve files you've uploaded to Amazon. The first parameter is the file or folder (recursively) you want to download. The second (optional) parameter is the location and/or filename to save the file as. If no path is given, the remote node is downloaded to the current working directory with the same name as it exists remotely. 174 | 175 | ``` 176 | $ clouddrive download /test/ . 177 | ``` 178 | 179 | ## Encryption 180 | 181 | You can optionally choose to encrypt your files upon upload to Amazon Drive. Passing the `--encrypt` flag with the `upload` command will encrypt both the file's name and contents before uploading to Amazon using the `crypto` options in the config. If no password is specified in the config, or you manually pass the `--password` flag, you will be prompted for the encryption password. 182 | 183 | The encrypted file contents, by default, are a binary format generated by the `crypto` library in NodeJS. You can choose to run the file's contents through a base64 encoding after encryption to armor the contents by passing the `--armor` flag on upload. This setting is also sticky based on changing the `crypto.armor` config option. 184 | 185 | Once files are encrypted, their nodes are tagged with the 'enc' label in Amazon Drive. This makes the app friendly across other commands such as `ls`, `tree`, etc. Using any of these commands with the `--decrypt` flag will decrypt the file's names to easily be read back out while listing and navigating through directories. 186 | 187 | Passing the `--decrypt` flag with the `download` command will also decrypt the files on download so they exist locally, unencrypted. Note: you will need to use the same amoring flag or config setting when downloading files as you did uploading. 188 | -------------------------------------------------------------------------------- /bin/clouddrive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | try { 5 | require('../dist/cli'); 6 | } catch (e) { 7 | require('../lib/cli'); 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Account: require('./dist/Account'), 3 | Node: require('./dist/Node'), 4 | Config: require('./dist/Config'), 5 | Utils: require('./dist/Utils'), 6 | Cache: { 7 | Cache: require('./dist/Cache/Cache'), 8 | SQLite: require('./dist/Cache/SQLite3'), 9 | MySQL: require('./dist/Cache/MySQL'), 10 | Mongo: require('./dist/Cache/Mongo') 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/Account.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let got = require('got'), 4 | Url = require('url'), 5 | Node = require('./Node'), 6 | Logger = require('./Logger'), 7 | async = require('async'), 8 | fs = require('fs'); 9 | 10 | class Account { 11 | constructor(email, cache, clientId = null, clientSecret = null) { 12 | this.email = email; 13 | this.clientId = clientId; 14 | this.clientSecret = clientSecret; 15 | this.cache = cache; 16 | this.token = {}; 17 | this.checkpoint = null; 18 | this.metadataUrl = null; 19 | this.contentUrl = null; 20 | this.scope = [ 21 | Account.SCOPE_READ_ALL, 22 | Account.SCOPE_WRITE, 23 | ]; 24 | } 25 | 26 | authorize(authCredentials, options, callback) { 27 | let retval = { 28 | success: false, 29 | data: {}, 30 | }; 31 | 32 | if (!options) { 33 | options = {}; 34 | } 35 | 36 | if (authCredentials === undefined) { 37 | authCredentials = null; 38 | } 39 | 40 | let scope = encodeURIComponent(this.scope.join(' ')); 41 | 42 | if (this.token.access_token === undefined || !this.token.access_token) { 43 | if (authCredentials === null) { 44 | retval.data.message = 'Initial authorization is required'; 45 | if (!this.clientId || !this.clientSecret) { 46 | retval.data.auth_url = 'https://data-mind-687.appspot.com/clouddrive'; 47 | } else { 48 | retval.data.auth_url = `https://www.amazon.com/ap/oa?client_id=${this.clientId}&scope=${scope}&response_type=code&redirect_uri=http://localhost`; 49 | } 50 | 51 | return callback(null, retval); 52 | } 53 | 54 | switch (typeof authCredentials) { 55 | case 'string': 56 | return this.requestAuthorization(authCredentials, (err, data) => { 57 | if (err) { 58 | return callback(err); 59 | } 60 | 61 | if (data.success === false) { 62 | return callback(null, data); 63 | } 64 | 65 | retval.success = true; 66 | 67 | return async.forEachOf(data.data, (value, key, callback) => { 68 | this.token[key] = value; 69 | callback(); 70 | }, err => { 71 | if (err) { 72 | return callback(err); 73 | } 74 | 75 | return this.getEndpoints((err, data) => { 76 | if (err) { 77 | return callback(err); 78 | } 79 | 80 | if (data.success === true) { 81 | this.metadataUrl = data.data.metadataUrl; 82 | this.contentUrl = data.data.contentUrl; 83 | 84 | return this.save(() => { 85 | return callback(null, retval); 86 | }); 87 | } 88 | 89 | return this.save(() => { 90 | return callback(null, data); 91 | }); 92 | }); 93 | }); 94 | }); 95 | case 'object': 96 | retval.success = true; 97 | 98 | return async.forEachOf(authCredentials, (value, key, callback) => { 99 | this.token[key] = value; 100 | callback(); 101 | }, err => { 102 | if (err) { 103 | return callback(err); 104 | } 105 | 106 | return this.getEndpoints((err, data) => { 107 | if (err) { 108 | return callback(err); 109 | } 110 | 111 | if (data.success === true) { 112 | this.metadataUrl = data.data.metadataUrl; 113 | this.contentUrl = data.data.contentUrl; 114 | 115 | return this.save(() => { 116 | return callback(null, retval); 117 | }); 118 | } 119 | 120 | return this.save(() => { 121 | return callback(null, data); 122 | }); 123 | }); 124 | }); 125 | default: 126 | return callback(Error('Auth credentials must either be a token object or a redirect URL')); 127 | } 128 | } 129 | 130 | retval.success = true; 131 | 132 | if ((Date.now() - (this.token.expires_in * 1000) > (this.token.last_authorized)) || options.force === true) { 133 | return this.renewAuthorization((err, data) => { 134 | if (err) { 135 | return callback(err); 136 | } 137 | 138 | if (data.success === true) { 139 | return async.forEachOf(data.data, (value, key, callback) => { 140 | this.token[key] = value; 141 | callback(); 142 | }, err => { 143 | if (err) { 144 | return callback(err); 145 | } 146 | 147 | return this.getEndpoints((err, data) => { 148 | if (err) { 149 | return callback(err); 150 | } 151 | 152 | if (data.success === true) { 153 | this.metadataUrl = data.data.metadataUrl; 154 | this.contentUrl = data.data.contentUrl; 155 | 156 | return this.save(() => { 157 | return callback(null, retval); 158 | }); 159 | } 160 | 161 | return this.save(() => { 162 | return callback(null, data); 163 | }); 164 | }); 165 | }); 166 | } 167 | 168 | return callback(null, data); 169 | }); 170 | } 171 | 172 | return this.getEndpoints((err, data) => { 173 | if (err) { 174 | return callback(err); 175 | } 176 | 177 | if (data.success === true) { 178 | this.metadataUrl = data.data.metadataUrl; 179 | this.contentUrl = data.data.contentUrl; 180 | 181 | return this.save(() => { 182 | return callback(null, retval); 183 | }); 184 | } 185 | 186 | return callback(null, data); 187 | }); 188 | } 189 | 190 | getEndpoints(callback) { 191 | let retval = { 192 | success: false, 193 | data: { 194 | metadataUrl: this.metadataUrl, 195 | contentUrl: this.contentUrl, 196 | }, 197 | }; 198 | 199 | Logger.verbose('Requesting account:endpoints endpoint'); 200 | Logger.debug(`HTTP Request: GET 'https://cdws.us-east-1.amazonaws.com/drive/v1/account/endpoint'`); 201 | got.get('https://cdws.us-east-1.amazonaws.com/drive/v1/account/endpoint', { 202 | headers: { 203 | Authorization: `Bearer ${this.token.access_token}`, 204 | }, 205 | }) 206 | .then(response => { 207 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 208 | Logger.silly(`Response body: ${response.body}`); 209 | retval.data = JSON.parse(response.body); 210 | if (response.statusCode === 200) { 211 | retval.success = true; 212 | } 213 | 214 | return callback(null, retval); 215 | }) 216 | .catch(err => { 217 | return callback(err); 218 | }); 219 | } 220 | 221 | getInfo(callback) { 222 | let retval = { 223 | success: false, 224 | data: {}, 225 | }; 226 | 227 | Logger.verbose('Requesting account:info endpoint'); 228 | Logger.debug(`HTTP Request: GET '${this.metadataUrl}account/info'`); 229 | got.get(`${this.metadataUrl}account/info`, { 230 | headers: { 231 | Authorization: `Bearer ${this.token.access_token}`, 232 | }, 233 | }) 234 | .then(response => { 235 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 236 | Logger.silly(`Response body: ${response.body}`); 237 | retval.data = JSON.parse(response.body); 238 | if (response.statusCode === 200) { 239 | retval.success = true; 240 | } 241 | 242 | return callback(null, retval); 243 | }) 244 | .catch(err => { 245 | return callback(err); 246 | }); 247 | } 248 | 249 | getQuota(callback) { 250 | let retval = { 251 | success: false, 252 | data: {}, 253 | }; 254 | 255 | Logger.verbose('Requesting account:quota endpoint'); 256 | Logger.debug(`HTTP Request: GET '${this.metadataUrl}account/quota'`); 257 | got.get(`${this.metadataUrl}account/quota`, { 258 | headers: { 259 | Authorization: `Bearer ${this.token.access_token}`, 260 | }, 261 | }) 262 | .then(response => { 263 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 264 | Logger.silly(`Response body: ${response.body}`); 265 | retval.data = JSON.parse(response.body); 266 | if (retval.statusCode === 200) { 267 | retval.success = true; 268 | } 269 | 270 | return callback(null, retval); 271 | }) 272 | .catch(err => { 273 | return callback(err); 274 | }); 275 | } 276 | 277 | getUsage(callback) { 278 | let retval = { 279 | success: false, 280 | data: {}, 281 | }; 282 | 283 | Logger.verbose('Requesting account:usage endpoint'); 284 | Logger.debug(`HTTP Request: GET '${this.metadataUrl}account/usage'`); 285 | got.get(`${this.metadataUrl}account/usage`, { 286 | headers: { 287 | Authorization: `Bearer ${this.token.access_token}`, 288 | }, 289 | }) 290 | .then(response => { 291 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 292 | Logger.debug(`Response body: ${response.body}`); 293 | retval.data = JSON.parse(response.body); 294 | if (response.statusCode === 200) { 295 | retval.success = true; 296 | } 297 | 298 | return callback(null, retval); 299 | }) 300 | .catch(err => { 301 | return callback(err); 302 | }); 303 | } 304 | 305 | load(callback) { 306 | this.cache.loadConfigByEmail(this.email, (err, data) => { 307 | if (err) { 308 | return callback(err); 309 | } 310 | 311 | let retval = { 312 | success: true, 313 | data: {}, 314 | }; 315 | 316 | let config = data.length === 0 ? {} : data[0]; 317 | 318 | return async.forEachOf(config, (value, key, callback) => { 319 | this.token[key] = value; 320 | callback(); 321 | }, err => { 322 | if (err) { 323 | return callback(err); 324 | } 325 | 326 | if (config.checkpoint !== undefined) { 327 | this.checkpoint = config.checkpoint; 328 | } 329 | 330 | this.metadataUrl = this.token.metadata_url; 331 | this.contentUrl = this.token.content_url; 332 | 333 | return callback(null, retval); 334 | }); 335 | }); 336 | } 337 | 338 | renewAuthorization(callback) { 339 | let retval = { 340 | success: false, 341 | data: {}, 342 | }; 343 | 344 | if (!this.clientId || !this.clientSecret) { 345 | Logger.verbose('Requesting auth:renew endpoint (no API credentials)'); 346 | Logger.debug(`HTTP Request: GET 'https://data-mind-687.appspot.com/clouddrive?refresh_token=${this.token.refresh_token}'`); 347 | got.get(`https://data-mind-687.appspot.com/clouddrive?refresh_token=${this.token.refresh_token}`) 348 | .then(response => { 349 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 350 | Logger.silly(`Response body: ${response.body}`); 351 | retval.success = true; 352 | retval.data = JSON.parse(response.body); 353 | retval.data.last_authorized = Date.now(); 354 | 355 | return callback(null, retval); 356 | }) 357 | .catch(err => { 358 | return callback(err); 359 | }); 360 | } else { 361 | Logger.verbose('Requesting auth:renew endpoint (with API credentials)'); 362 | Logger.debug(`HTTP Request: POST 'https://api.amazon.com/auth/o2/token'`); 363 | got.post('https://api.amazon.com/auth/o2/token', { 364 | body: { 365 | grant_type: 'refresh_token', 366 | refresh_token: this.token.refresh_token, 367 | client_id: this.clientId, 368 | client_secret: this.clientSecret, 369 | redirect_uri: 'http://localhost', 370 | }, 371 | }) 372 | .then(response => { 373 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 374 | Logger.silly(`Response body: ${response.body}`); 375 | retval.data = JSON.parse(response.body); 376 | if (response.statusCode === 200) { 377 | retval.success = true; 378 | retval.data.last_authorized = Date.now(); 379 | } 380 | 381 | return callback(null, retval); 382 | }) 383 | .catch(err => { 384 | return callback(err); 385 | }); 386 | } 387 | } 388 | 389 | requestAuthorization(redirectUrl, callback) { 390 | let retval = { 391 | success: false, 392 | data: {}, 393 | }; 394 | 395 | let url = Url.parse(redirectUrl, true); 396 | if (url.query.code === undefined) { 397 | return callback(Error(`No authorization code found in callback URL: ${redirectUrl}`)); 398 | } 399 | 400 | Logger.verbose('Requesting auth:grant endpoint'); 401 | Logger.debug(`HTTP Request: POST 'https://api.amazon.com/auth/o2/token'`); 402 | got.post('https://api.amazon.com/auth/o2/token', { 403 | body: { 404 | grant_type: 'authorization_code', 405 | code: url.query.code, 406 | client_id: this.clientId, 407 | client_secret: this.clientSecret, 408 | redirect_uri: 'http://localhost', 409 | }, 410 | }) 411 | .then(response => { 412 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 413 | Logger.silly(`Response body: ${response.body}`); 414 | retval.data = JSON.parse(response.body); 415 | if (response.statusCode === 200) { 416 | retval.success = true; 417 | retval.data.last_authorized = Date.now(); 418 | } 419 | 420 | return callback(null, retval); 421 | }) 422 | .catch(err => { 423 | return callback(err); 424 | }); 425 | } 426 | 427 | save(callback) { 428 | return this.cache.saveAccount(this, callback); 429 | } 430 | 431 | setScope(scopes) { 432 | this.scope = scopes; 433 | } 434 | 435 | sync(params, callback) { 436 | params = params || {}; 437 | 438 | if (this.checkpoint) { 439 | params.includePurged = 'true'; 440 | } 441 | 442 | return this.getChanges(params, callback); 443 | } 444 | 445 | getChanges(params, callback) { 446 | let loop = true; 447 | 448 | if (this.checkpoint !== null) { 449 | params.checkpoint = this.checkpoint; 450 | } 451 | 452 | let self = this; 453 | function processData(part, callback) { 454 | if (part.nodes !== undefined) { 455 | if (part.nodes.length === 0) { 456 | loop = false; 457 | 458 | return callback(null, 0, 0); 459 | } else { 460 | let inserted = 0, 461 | purged = 0; 462 | self.cache.transaction(() => { 463 | async.forEach(part.nodes, (node, callback) => { 464 | node = new Node(node); 465 | if (node.getStatus() === Node.STATUS_PURGED) { 466 | purged++; 467 | 468 | return node.del(callback); 469 | } 470 | 471 | inserted++; 472 | 473 | return node.save(callback); 474 | }, err => { 475 | if (err) { 476 | return callback(err); 477 | } 478 | 479 | if (part.checkpoint !== undefined) { 480 | self.checkpoint = part.checkpoint; 481 | } 482 | 483 | return self.save(err => { 484 | if (err) { 485 | self.cache.rollback(); 486 | 487 | return callback(err); 488 | } 489 | 490 | self.cache.commit(); 491 | 492 | return callback(null, inserted, purged); 493 | }); 494 | }); 495 | }); 496 | } 497 | } 498 | } 499 | 500 | Logger.verbose('Requesting changes:get endpoint'); 501 | Logger.debug(`HTTP Request: POST '${this.metadataUrl}changes'`); 502 | got.post(`${this.metadataUrl}changes`, { 503 | headers: { 504 | Authorization: `Bearer ${this.token.access_token}`, 505 | }, 506 | body: JSON.stringify(params), 507 | gzip: true, 508 | }) 509 | .then(response => { 510 | Logger.debug(`Response returned with status code ${response.statusCode}.`); 511 | Logger.silly(`Response body: ${response.body}`); 512 | if (response.statusCode === 401) { 513 | return this.authorize(null, {force: true}, (err, data) => { 514 | if (err) { 515 | return callback(err); 516 | } 517 | 518 | return this.getChanges(params, callback); 519 | }); 520 | } 521 | 522 | if (!response.body) { 523 | return callback(Error(`Invalid data received: ${response.body}`)); 524 | } 525 | 526 | let data = response.body.split('\n'); 527 | Logger.debug(`Received ${data.length} parts.`); 528 | async.forEachSeries(data, (part, callback) => { 529 | part = JSON.parse(part); 530 | 531 | if (part.end !== undefined && part.end === true) { 532 | return callback(); 533 | } 534 | 535 | if (part.reset !== undefined && part.reset === true) { 536 | return this.cache.deleteAllNodes(() => { 537 | return processData(part, (err, inserted, purged) => { 538 | if (err) { 539 | return callback(err); 540 | } 541 | 542 | Logger.verbose(`Created/updated ${inserted} nodes.`); 543 | Logger.verbose(`Purged ${purged} nodes.`); 544 | callback(); 545 | }); 546 | }); 547 | } 548 | 549 | return processData(part, (err, inserted, purged) => { 550 | if (err) { 551 | return callback(err); 552 | } 553 | 554 | Logger.verbose(`Created/updated ${inserted} nodes.`); 555 | Logger.verbose(`Purged ${purged} nodes.`); 556 | callback(); 557 | }); 558 | }, err => { 559 | if (err) { 560 | return callback(err); 561 | } 562 | 563 | if (loop === true) { 564 | return this.getChanges(params, callback); 565 | } 566 | 567 | return callback(); 568 | }); 569 | }) 570 | .catch(err => { 571 | return callback(err); 572 | }); 573 | } 574 | } 575 | 576 | Account.SCOPE_READ_IMAGE = 'clouddrive:read_image'; 577 | Account.SCOPE_READ_VIDEO = 'clouddrive:read_video'; 578 | Account.SCOPE_READ_DOCUMENT = 'clouddrive:read_document'; 579 | Account.SCOPE_READ_OTHER = 'clouddrive:read_other'; 580 | Account.SCOPE_READ_ALL = 'clouddrive:read_all'; 581 | Account.SCOPE_WRITE = 'clouddrive:write'; 582 | 583 | module.exports = Account; 584 | -------------------------------------------------------------------------------- /lib/Cache/Cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Cache { 4 | close() { 5 | } 6 | 7 | commit() { 8 | } 9 | 10 | deleteAllNodes(callback) { 11 | } 12 | 13 | deleteNodeById(id, callback) { 14 | } 15 | 16 | filter(filters, callback) { 17 | } 18 | 19 | findNodeById(id, callback) { 20 | } 21 | 22 | findNodesByName(name, callback) { 23 | } 24 | 25 | getNodeChildren(node, callback) { 26 | } 27 | 28 | loadConfigByEmail(email, callback) { 29 | } 30 | 31 | rollback() { 32 | } 33 | 34 | saveAccount(account, callback) { 35 | } 36 | 37 | saveNode(node, callback) { 38 | } 39 | 40 | saveNodeParents(node, callback) { 41 | } 42 | 43 | searchBy(field, value, callback) { 44 | } 45 | 46 | transaction(callback) { 47 | } 48 | } 49 | 50 | module.exports = Cache; 51 | -------------------------------------------------------------------------------- /lib/Cache/Mongo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let mongodb = require('mongodb'), 4 | Cache = require('./Cache'), 5 | Node = require('../Node'), 6 | async = require('async'); 7 | 8 | class Mongo extends Cache { 9 | constructor(config, callback) { 10 | super(config); 11 | let defaultConfig = { 12 | host: 'localhost', 13 | port: 27017, 14 | database: 'clouddrive' 15 | }; 16 | 17 | async.forEachOf(config, (value, key, callback) => { 18 | if (defaultConfig[key] !== undefined) { 19 | defaultConfig[key] = value; 20 | } 21 | 22 | callback(); 23 | }, err => { 24 | if (err) { 25 | return callback(err); 26 | } 27 | 28 | return mongodb.MongoClient.connect( 29 | `mongodb://${defaultConfig.host}:${defaultConfig.port}/${defaultConfig.database}`, 30 | (err, database) => { 31 | if (err) { 32 | return callback(Error(`Unable to connect to database: ${err.message}`)); 33 | } 34 | 35 | this.db = database; 36 | 37 | return callback(null, this); 38 | } 39 | ); 40 | }); 41 | } 42 | 43 | close() { 44 | mongodb.MongoClient.close(); 45 | } 46 | 47 | deleteAllNodes(callback) { 48 | this.db.collection('nodes').removeMany({}, {}, err => { 49 | callback(null); 50 | }); 51 | } 52 | 53 | deleteNodeById(id, callback) { 54 | this.db.collection('nodes').removeOne({ 55 | id: id 56 | }, err => { 57 | callback(null); 58 | }); 59 | } 60 | 61 | filter(filters, callback) { 62 | this.db.collection('nodes').find(filters) 63 | .toArray((err, data) => { 64 | if (err) { 65 | return callback(err); 66 | } 67 | 68 | return async.forEachOf(data, (row, index, callback) => { 69 | data[index] = new Node(row); 70 | callback(); 71 | }, err => { 72 | return callback(err, data); 73 | }); 74 | }); 75 | } 76 | 77 | findNodeById(id, callback) { 78 | this.db.collection('nodes').findOne({ 79 | id: id 80 | }, (err, data) => { 81 | return callback(null, new Node(data)); 82 | }); 83 | } 84 | 85 | findNodesByName(name, callback) { 86 | this.db.collection('nodes').find({name: name}) 87 | .toArray((err, data) => { 88 | return async.forEachOf(data, (row, index, callback) => { 89 | data[index] = new Node(row); 90 | callback(); 91 | }, err => { 92 | return callback(err, data); 93 | }); 94 | }); 95 | } 96 | 97 | getNodeChildren(node, callback) { 98 | this.db.collection('nodes').find({ 99 | parents: node.getId() 100 | }) 101 | .toArray((err, data) => { 102 | return async.forEachOf(data, (row, index, callback) => { 103 | data[index] = new Node(row); 104 | callback(); 105 | }, err => { 106 | return callback(err, data); 107 | }); 108 | }); 109 | } 110 | 111 | loadConfigByEmail(email, callback) { 112 | let collection = this.db.collection('configs'); 113 | collection.find({email: email}).toArray((err, data) => { 114 | if (err) { 115 | return callback(err); 116 | } 117 | 118 | callback(null, data); 119 | }); 120 | } 121 | 122 | saveAccount(account, callback) { 123 | let collection = this.db.collection('configs'); 124 | collection.updateOne({email: account.email}, { 125 | email: account.email, 126 | token_type: account.token.token_type, 127 | expires_in: account.token.expires_in, 128 | refresh_token: account.token.refresh_token, 129 | access_token: account.token.access_token, 130 | last_authorized: account.token.last_authorized, 131 | content_url: account.contentUrl, 132 | metadata_url: account.metadataUrl, 133 | checkpoint: account.checkpoint 134 | }, { 135 | upsert: true 136 | }, (err, numUpdated) => { 137 | if (err) { 138 | return callback(err); 139 | } 140 | 141 | return callback(null); 142 | }); 143 | } 144 | 145 | saveNode(node, callback) { 146 | if (!node.getName() && node.isRoot() === true) { 147 | node.set('name', 'Cloud Drive'); 148 | } 149 | 150 | this.db.collection('nodes').updateOne({ 151 | id: node.getId() 152 | }, node.getData(), { 153 | upsert: true 154 | }, (err, data) => { 155 | return this.saveNodeParents(node, callback); 156 | }); 157 | } 158 | 159 | saveNodeParents(node, callback) { 160 | let collection = this.db.collection('nodes_nodes'); 161 | 162 | let parents = node.getParentIds(); 163 | collection.find({ 164 | id_node: node.getId() 165 | }).toArray((err, data) => { 166 | async.forEach(data, (row, callback) => { 167 | let index = parents.indexOf(row['id_parent']); 168 | if (index === -1) { 169 | return collection.removeMany({ 170 | id_parent: row['id_parent'], 171 | id_node: node.getId() 172 | }, callback); 173 | } 174 | 175 | delete(parents[index]); 176 | 177 | callback(); 178 | }, err => { 179 | if (err) { 180 | return callback(err); 181 | } 182 | 183 | async.forEach(parents, (parent, callback) => { 184 | if (!parent) { 185 | return callback(); 186 | } 187 | 188 | collection.insertOne({ 189 | id_node: node.getId(), 190 | id_parent: parent 191 | }, callback); 192 | }, err => { 193 | callback(err); 194 | }); 195 | }); 196 | }); 197 | } 198 | 199 | searchBy(field, value, callback) { 200 | let params = {}; 201 | params[field] = { 202 | '$regex': value, 203 | '$options': 'i' 204 | }; 205 | 206 | this.db.collection('nodes').find(params) 207 | .toArray((err, data) => { 208 | return async.forEachOf(data, (row, index, callback) => { 209 | data[index] = new Node(row); 210 | callback(); 211 | }, err => { 212 | return callback(err, data); 213 | }); 214 | }); 215 | } 216 | } 217 | 218 | module.exports = Mongo; 219 | -------------------------------------------------------------------------------- /lib/Cache/MySQL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let SQL = require('./SQL'); 4 | 5 | class MySQL extends SQL { 6 | constructor(config, callback) { 7 | super(config); 8 | 9 | this.db.raw( 10 | `CREATE TABLE IF NOT EXISTS configs ( 11 | id INT(11) NOT NULL auto_increment, 12 | email VARCHAR(32), 13 | token_type VARCHAR(16), 14 | expires_in INT(12), 15 | refresh_token TEXT, 16 | access_token TEXT, 17 | last_authorized TIMESTAMP, 18 | content_url MEDIUMTEXT, 19 | metadata_url MEDIUMTEXT, 20 | checkpoint TEXT, 21 | PRIMARY KEY (id), 22 | INDEX (email) 23 | );` 24 | ) 25 | .then(() => { 26 | return this.db.raw( 27 | `CREATE TABLE IF NOT EXISTS nodes ( 28 | id VARCHAR(22) NOT NULL, 29 | name VARCHAR(255), 30 | kind VARCHAR(16), 31 | md5 VARCHAR(128), 32 | status VARCHAR(16), 33 | created DATETIME, 34 | modified DATETIME, 35 | raw_data LONGTEXT, 36 | PRIMARY KEY (id), 37 | INDEX (id, name, md5) 38 | );` 39 | ); 40 | }) 41 | .then(() => { 42 | return this.db.raw( 43 | `CREATE TABLE IF NOT EXISTS nodes_nodes ( 44 | id INT(11) NOT NULL auto_increment, 45 | id_node VARCHAR(22) NOT NULL, 46 | id_parent VARCHAR(22) NOT NULL, 47 | PRIMARY KEY (id), 48 | UNIQUE KEY (id_node, id_parent), 49 | INDEX(id_node, id_parent) 50 | );` 51 | ); 52 | }) 53 | .then(() => { 54 | return callback(null, this); 55 | }); 56 | } 57 | } 58 | 59 | module.exports = MySQL; 60 | -------------------------------------------------------------------------------- /lib/Cache/SQL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Cache = require('./Cache'), 4 | Node = require('../Node'), 5 | async = require('async'), 6 | moment = require('moment'); 7 | 8 | class SQL extends Cache { 9 | constructor(config) { 10 | super(config); 11 | this.db = require('knex')(config); 12 | this.trx = null; 13 | } 14 | 15 | commit() { 16 | if (this.trx) { 17 | this.trx.commit(); 18 | } 19 | 20 | this.trx = null; 21 | } 22 | 23 | deleteAllNodes(callback) { 24 | this.db('nodes').del() 25 | .transacting(this.trx) 26 | .then(() => { 27 | return this.db('nodes_nodes').del() 28 | .transacting(this.trx) 29 | .then(function() { 30 | callback(); 31 | }, function(err) { 32 | return callback(err); 33 | }); 34 | }) 35 | .catch(function(err) { 36 | return callback(err); 37 | }); 38 | } 39 | 40 | deleteNodeById(id, callback) { 41 | this.db('nodes').where({ 42 | id: id 43 | }) 44 | .del() 45 | .transacting(this.trx) 46 | .then(function() { 47 | callback(); 48 | }, function(err) { 49 | return callback(err); 50 | }); 51 | } 52 | 53 | filter(filters, callback) { 54 | this.db.select('raw_data') 55 | .from('nodes') 56 | .where(filters) 57 | .transacting(this.trx) 58 | .then((data) => { 59 | return async.forEachOf(data, (row, index, callback) => { 60 | data[index] = new Node(JSON.parse(row.raw_data)); 61 | callback(); 62 | }, err => { 63 | return callback(err, data); 64 | }); 65 | }); 66 | } 67 | 68 | findNodeById(id, callback) { 69 | this.db.select('raw_data') 70 | .from('nodes') 71 | .where({ 72 | id: id 73 | }) 74 | .transacting(this.trx) 75 | .then((data) => { 76 | if (data.length === 0) { 77 | return callback(null, null); 78 | } 79 | 80 | return callback(null, new Node(JSON.parse(data[0].raw_data))); 81 | }); 82 | } 83 | 84 | findNodesByMd5(md5, callback) { 85 | this.db.select('raw_data') 86 | .from('nodes') 87 | .where({ 88 | md5: md5 89 | }) 90 | .transacting(this.trx) 91 | .then((data) => { 92 | return async.forEachOf(data, (row, index, callback) => { 93 | data[index] = new Node(JSON.parse(row.raw_data)); 94 | callback(); 95 | }, err => { 96 | return callback(err, data); 97 | }); 98 | }); 99 | } 100 | 101 | findNodesByName(name, callback) { 102 | this.db.select('raw_data') 103 | .from('nodes') 104 | .where({ 105 | name: name 106 | }) 107 | .transacting(this.trx) 108 | .then((data) => { 109 | return async.forEachOf(data, (row, index, callback) => { 110 | data[index] = new Node(JSON.parse(row.raw_data)); 111 | callback(); 112 | }, err => { 113 | return callback(err, data); 114 | }); 115 | }); 116 | } 117 | 118 | getNodeChildren(node, callback) { 119 | this.db.select('raw_data') 120 | .from('nodes') 121 | .innerJoin('nodes_nodes', 'nodes.id', 'nodes_nodes.id_node') 122 | .where({ 123 | 'nodes_nodes.id_parent': node.getId() 124 | }) 125 | .transacting(this.trx) 126 | .then((data) => { 127 | return async.forEachOf(data, (row, index, callback) => { 128 | data[index] = new Node(JSON.parse(row.raw_data)); 129 | callback(); 130 | }, err => { 131 | return callback(err, data); 132 | }); 133 | }); 134 | } 135 | 136 | loadConfigByEmail(email, callback) { 137 | this.db.select('*') 138 | .from('configs') 139 | .where({ 140 | email: email 141 | }) 142 | .transacting(this.trx) 143 | .then((data) => { 144 | callback(null, data); 145 | }); 146 | } 147 | 148 | rollback() { 149 | if (this.trx) { 150 | this.trx.rollback(); 151 | } 152 | 153 | this.trx = null; 154 | } 155 | 156 | saveAccount(account, callback) { 157 | this.db.select('*') 158 | .from('configs') 159 | .where({ 160 | email: account.email 161 | }) 162 | .transacting(this.trx) 163 | .then((data) => { 164 | if (data.length === 0) { 165 | this.db('configs') 166 | .insert({ 167 | email: account.email, 168 | token_type: account.token.token_type, 169 | expires_in: account.token.expires_in, 170 | refresh_token: account.token.refresh_token, 171 | access_token: account.token.access_token, 172 | last_authorized: account.token.last_authorized, 173 | content_url: account.contentUrl, 174 | metadata_url: account.metadataUrl, 175 | checkpoint: account.checkpoint 176 | }) 177 | .transacting(this.trx) 178 | .then(function() { 179 | callback(); 180 | }, function(err) { 181 | return callback(err); 182 | }); 183 | } else { 184 | this.db('configs') 185 | .where('email', '=', account.email) 186 | .update({ 187 | token_type: account.token.token_type, 188 | expires_in: account.token.expires_in, 189 | refresh_token: account.token.refresh_token, 190 | access_token: account.token.access_token, 191 | last_authorized: account.token.last_authorized, 192 | content_url: account.contentUrl, 193 | metadata_url: account.metadataUrl, 194 | checkpoint: account.checkpoint 195 | }) 196 | .transacting(this.trx) 197 | .then(function() { 198 | callback(); 199 | }, function(err) { 200 | return callback(err); 201 | }); 202 | } 203 | }); 204 | } 205 | 206 | saveNode(node, callback) { 207 | if (!node.getName()) { 208 | if (node.isRoot() === true) { 209 | node.set('name', 'Cloud Drive'); 210 | } else { 211 | return callback(Error(`Node cannot be saved with no name (node ID: ${node.getId()})`)); 212 | } 213 | } 214 | 215 | this.db.select('*') 216 | .from('nodes') 217 | .where({ 218 | id: node.getId() 219 | }) 220 | .transacting(this.trx) 221 | .then((data) => { 222 | if (data.length === 0) { 223 | this.db('nodes') 224 | .insert({ 225 | id: node.getId(), 226 | name: node.getName(), 227 | kind: node.getKind(), 228 | md5: node.getMd5(), 229 | status: node.getStatus(), 230 | created: moment(new Date(node.getCreatedDate())).format('YYYY-MM-DD HH:mm:ss'), 231 | modified: moment(new Date(node.getModifiedDate())).format('YYYY-MM-DD HH:mm:ss'), 232 | raw_data: JSON.stringify(node.getData()) 233 | }) 234 | .transacting(this.trx) 235 | .then(() => { 236 | return this.saveNodeParents(node, callback); 237 | }) 238 | .catch(function(err) { 239 | return callback(err); 240 | }); 241 | } else { 242 | this.db('nodes') 243 | .where('id', '=', node.getId()) 244 | .update({ 245 | name: node.getName(), 246 | kind: node.getKind(), 247 | md5: node.getMd5(), 248 | status: node.getStatus(), 249 | created: moment(new Date(node.getCreatedDate())).format('YYYY-MM-DD HH:mm:ss'), 250 | modified: moment(new Date(node.getModifiedDate())).format('YYYY-MM-DD HH:mm:ss'), 251 | raw_data: JSON.stringify(node.getData()) 252 | }) 253 | .transacting(this.trx) 254 | .then(() => { 255 | return this.saveNodeParents(node, callback); 256 | }) 257 | .catch(function(err) { 258 | return callback(err); 259 | }); 260 | } 261 | }) 262 | .catch(function(err) { 263 | return callback(err); 264 | }); 265 | } 266 | 267 | saveNodeParents(node, callback) { 268 | let parents = node.getParentIds(); 269 | this.db.select('*') 270 | .from('nodes_nodes') 271 | .where({ 272 | id_node: node.getId() 273 | }) 274 | .transacting(this.trx) 275 | .then((data) => { 276 | async.forEach(data, (row, callback) => { 277 | let index = parents.indexOf(row['id_parent']); 278 | if (index === -1) { 279 | return this.db('nodes_nodes') 280 | .where({ 281 | id_parent: row['id_parent'], 282 | id_node: node.getId() 283 | }) 284 | .del() 285 | .transacting(this.trx) 286 | .then(function() { 287 | callback(); 288 | }, function(err) { 289 | return callback(err); 290 | }); 291 | } 292 | 293 | delete(parents[index]); 294 | 295 | callback(); 296 | }, err => { 297 | if (err) { 298 | return callback(err); 299 | } 300 | 301 | async.forEach(parents, (parent, callback) => { 302 | if (!parent) { 303 | return callback(); 304 | } 305 | 306 | this.db('nodes_nodes') 307 | .insert({ 308 | id_node: node.getId(), 309 | id_parent: parent 310 | }) 311 | .transacting(this.trx) 312 | .then(function() { 313 | callback(); 314 | }, function(err) { 315 | return callback(err); 316 | }); 317 | }, err => { 318 | callback(err); 319 | }); 320 | }); 321 | }) 322 | .catch(function(err) { 323 | return callback(err); 324 | }); 325 | } 326 | 327 | searchBy(field, value, callback) { 328 | this.db.select('raw_data') 329 | .from('nodes') 330 | .where(field, 'like', `%${value}%`) 331 | .transacting(this.trx) 332 | .then((data) => { 333 | return async.forEachOf(data, (row, index, callback) => { 334 | data[index] = new Node(JSON.parse(row.raw_data)); 335 | callback(); 336 | }, err => { 337 | return callback(err, data); 338 | }); 339 | }); 340 | } 341 | 342 | transaction(callback) { 343 | this.db.transaction((trx) => { 344 | this.trx = trx; 345 | callback(); 346 | }); 347 | } 348 | } 349 | 350 | module.exports = SQL; 351 | -------------------------------------------------------------------------------- /lib/Cache/SQLite3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let SQL = require('./SQL'); 4 | 5 | class SQLite3 extends SQL { 6 | constructor(config, callback) { 7 | super(config); 8 | 9 | this.db.raw(` 10 | CREATE TABLE IF NOT EXISTS nodes( 11 | id VARCHAR PRIMARY KEY NOT NULL, 12 | name VARCHAR NOT NULL, 13 | kind VARCHAR NOT NULL, 14 | md5 VARCHAR, 15 | status VARCHAR, 16 | created DATETIME NOT NULL, 17 | modified DATETIME NOT NULL, 18 | raw_data TEXT NOT NULL 19 | ); 20 | `) 21 | .then(() => { 22 | return this.db.raw('CREATE INDEX IF NOT EXISTS node_id on nodes(id);'); 23 | }) 24 | .then(() => { 25 | return this.db.raw('CREATE INDEX IF NOT EXISTS node_name on nodes(name);'); 26 | }) 27 | .then(() => { 28 | return this.db.raw('CREATE INDEX IF NOT EXISTS node_md5 on nodes(md5);'); 29 | }) 30 | .then(() => { 31 | return this.db.raw(` 32 | CREATE TABLE IF NOT EXISTS configs( 33 | id INTEGER PRIMARY KEY, 34 | email VARCHAR NOT NULL, 35 | token_type VARCHAR, 36 | expires_in INT, 37 | refresh_token TEXT, 38 | access_token TEXT, 39 | last_authorized INT, 40 | content_url VARCHAR, 41 | metadata_url VARCHAR, 42 | checkpoint VARCHAR 43 | ); 44 | `); 45 | }) 46 | .then(() => { 47 | return this.db.raw('CREATE INDEX IF NOT EXISTS config_email on configs(email);'); 48 | }) 49 | .then(() => { 50 | return this.db.raw(` 51 | CREATE TABLE IF NOT EXISTS nodes_nodes( 52 | id INTEGER PRIMARY KEY, 53 | id_node VARCHAR NOT NULL, 54 | id_parent VARCHAR NOT NULL, 55 | UNIQUE (id_node, id_parent) 56 | ); 57 | `); 58 | }) 59 | .then(() => { 60 | return this.db.raw('CREATE INDEX IF NOT EXISTS nodes_id_node on nodes_nodes(id_node);'); 61 | }) 62 | .then(() => { 63 | return this.db.raw('CREATE INDEX IF NOT EXISTS nodes_id_parent on nodes_nodes(id_parent);'); 64 | }) 65 | .then(() => { 66 | return callback(null, this); 67 | }); 68 | } 69 | } 70 | 71 | module.exports = SQLite3; 72 | -------------------------------------------------------------------------------- /lib/Commands/AboutCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Logger = require('../Logger'); 5 | 6 | class AboutCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | this.initialize((err, data) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | 14 | Logger.info(`Config directory: ${Command.getConfigDirectory()}`); 15 | Logger.info(`Cache directory: ${Command.getCacheDirectory()}`); 16 | Logger.info(`Log directory: ${Command.getLogDirectory()}`); 17 | 18 | return resolve(); 19 | }); 20 | }); 21 | } 22 | } 23 | 24 | module.exports = AboutCommand; 25 | -------------------------------------------------------------------------------- /lib/Commands/CatCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | async = require('async'), 6 | inquirer = require('inquirer'); 7 | 8 | class CatCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | let remotePath = args[0], 12 | searchFunction = Node.loadByPath, 13 | notFound = `No node exists at path '${remotePath}'`; 14 | if (options.id) { 15 | searchFunction = Node.loadById; 16 | notFound = `No node exists with ID '${remotePath}'`; 17 | } 18 | 19 | if (remotePath) { 20 | remotePath = remotePath.trim(); 21 | } 22 | 23 | this.initialize((err, data) => { 24 | if (err) { 25 | return reject(err); 26 | } 27 | 28 | searchFunction(remotePath, (err, node) => { 29 | if (err) { 30 | return reject(err); 31 | } 32 | 33 | if (!node) { 34 | return reject(Error(notFound)); 35 | } 36 | 37 | let opts = { 38 | stream: process.stdout, 39 | decrypt: options.decrypt, 40 | password: this.config.get('crypto.password'), 41 | algorithm: this.config.get('crypto.algorithm'), 42 | armor: this.config.get('crypto.armor'), 43 | }; 44 | 45 | if (!node.isFile()) { 46 | return reject(Error('Node must be a file')); 47 | } 48 | 49 | async.waterfall([ 50 | callback => { 51 | if (!options.decrypt || !options.password) { 52 | return callback(); 53 | } 54 | 55 | inquirer.prompt([ 56 | { 57 | type: 'password', 58 | name: 'password', 59 | message: 'password: ' 60 | } 61 | ], answers => { 62 | opts.password = answers.password; 63 | callback(); 64 | }); 65 | }, 66 | callback => { 67 | node.download('', opts, (err, data) => { 68 | if (err) { 69 | return reject(err); 70 | } 71 | 72 | if (data.success) { 73 | return resolve(); 74 | } 75 | 76 | return reject(Error(data.data.message)); 77 | }); 78 | }, 79 | ], err => { 80 | if (err) { 81 | return reject(err); 82 | } 83 | 84 | return resolve(); 85 | }); 86 | }); 87 | }); 88 | }); 89 | } 90 | } 91 | 92 | module.exports = CatCommand; 93 | -------------------------------------------------------------------------------- /lib/Commands/ClearCacheCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'); 4 | 5 | class ClearCacheCommand extends Command { 6 | run(args, options) { 7 | return new Promise((resolve, reject) => { 8 | this.initialize((err, data) => { 9 | if (err) { 10 | return reject(err); 11 | } 12 | 13 | this.account.checkpoint = null; 14 | this.account.save((err, data) => { 15 | if (err) { 16 | return reject(err); 17 | } 18 | 19 | this.account.cache.deleteAllNodes((err, data) => { 20 | if (err) { 21 | return reject(err); 22 | } 23 | 24 | return resolve(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | } 30 | } 31 | 32 | module.exports = ClearCacheCommand; 33 | -------------------------------------------------------------------------------- /lib/Commands/Command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let path = require('path'), 4 | fs = require('fs-extra'), 5 | ParameterBag = require('../ParameterBag'), 6 | Account = require('../Account'), 7 | Node = require('../Node'), 8 | Utils = require('../Utils'), 9 | Logger = require('../Logger'), 10 | chalk = require('chalk'), 11 | moment = require('moment'), 12 | elegantSpinner = require('elegant-spinner'), 13 | envPaths = require('env-paths'), 14 | logUpdate = require('log-update'), 15 | paths = null, 16 | spinner = null, 17 | spinnerText = ''; 18 | 19 | class Command { 20 | constructor(config = {}, appConfig = {}) { 21 | if (Command.APP_NAME === null) { 22 | throw Error('No app name set'); 23 | } 24 | 25 | this.params = { 26 | offline: false 27 | }; 28 | 29 | for (let key in config) { 30 | if (this.params[key] === undefined) { 31 | continue; 32 | } 33 | 34 | this.params[key] = config[key]; 35 | } 36 | 37 | if (appConfig.constructor.name !== 'Config') { 38 | throw Error(`Second parameter must be an instance of 'Config'.`); 39 | } 40 | 41 | this.config = appConfig; 42 | } 43 | 44 | execute(args, options) { 45 | if (options.remote) { 46 | this.params.offline = false; 47 | } 48 | 49 | if (options.debug) { 50 | Logger.warn('Running in debug mode'); 51 | let memwatch = require('memwatch-next'); 52 | memwatch.on('leak', info => { 53 | Logger.warn(info); 54 | }); 55 | } 56 | 57 | try { 58 | Logger.debug(`Running ${this.constructor.name}`); 59 | this.run.apply(this, arguments) 60 | .then(() => { 61 | Command.shutdown(0); 62 | }, err => { 63 | if (err) { 64 | switch (err.code) { 65 | case 'ENOTFOUND': 66 | Logger.error(`Unable to connect. Please make sure you have network access: ${err}`); 67 | break; 68 | default: 69 | Logger.error(`${err}`); 70 | } 71 | } 72 | Command.shutdown(1); 73 | }); 74 | } catch (err) { 75 | if (err) { 76 | Logger.error(`Uncaught error: ${err}`); 77 | Logger.debug(`Stack: ${err.stack}`); 78 | } 79 | Command.shutdown(1); 80 | } 81 | } 82 | 83 | initialize(callback) { 84 | this.initializeDatabase(err => { 85 | if (err) { 86 | return callback(err); 87 | } 88 | 89 | Logger.debug('Successfully conncted to databse.'); 90 | 91 | this.account.load(err => { 92 | if (err) { 93 | return callback(err); 94 | } 95 | 96 | Logger.debug('Successfully loaded account.'); 97 | 98 | if (this.params.offline === false) { 99 | return this.account.authorize(null, {}, (err, data) => { 100 | if (err) { 101 | return callback(err); 102 | } 103 | 104 | Logger.debug('Successfully authenticated with Amazon account.'); 105 | 106 | return callback(null, data); 107 | }); 108 | } 109 | 110 | return callback(); 111 | }); 112 | }); 113 | } 114 | 115 | initializeDatabase(callback) { 116 | Logger.debug('Connecting to databsae'); 117 | Logger.silly(`Database config: `, this.config.get('database')); 118 | switch (this.config.get('database.driver')) { 119 | case 'sqlite': 120 | let SQLite = require('../Cache/SQLite3'); 121 | new SQLite({ 122 | client: 'sqlite3', 123 | connection: { 124 | filename: `${Command.getCacheDirectory()}/${this.config.get('auth.email')}.db` 125 | } 126 | }, (err, cache) => { 127 | this.account = new Account(this.config.get('auth.email'), cache, this.config.get('auth.id'), this.config.get('auth.secret')); 128 | Node.init(this.account, cache); 129 | callback(null); 130 | }); 131 | break; 132 | case 'mysql': 133 | let MySQL = require('../Cache/MySQL'); 134 | new MySQL({ 135 | client: 'mysql', 136 | connection: { 137 | host: this.config.get('database.host'), 138 | user: this.config.get('database.username'), 139 | password: this.config.get('database.password'), 140 | database: this.config.get('database.database') 141 | } 142 | }, (err, cache) => { 143 | this.account = new Account(this.config.get('auth.email'), cache, this.config.get('auth.id'), this.config.get('auth.secret')); 144 | Node.init(this.account, cache); 145 | callback(null); 146 | }); 147 | break; 148 | case 'mongo': 149 | let Mongo = require('../Cache/Mongo'); 150 | new Mongo({}, (err, cache) => { 151 | this.account = new Account(this.config.get('auth.email'), cache.config.get('auth.id'), this.config.get('auth.secret'), cache); 152 | Node.init(this.account, cache); 153 | callback(null); 154 | }); 155 | break; 156 | } 157 | } 158 | 159 | run(cmd, options) { 160 | 161 | } 162 | 163 | static getAppName() { 164 | return Command.APP_NAME; 165 | } 166 | 167 | static getCacheDirectory() { 168 | if (Command.APP_NAME === null) { 169 | throw Error('No app name set'); 170 | } 171 | 172 | try { 173 | fs.statSync(paths.cache); 174 | } catch (e) { 175 | fs.mkdirsSync(paths.cache); 176 | } 177 | 178 | return paths.cache; 179 | } 180 | 181 | static getConfigDirectory() { 182 | if (Command.APP_NAME === null) { 183 | throw Error('No app name set'); 184 | } 185 | 186 | try { 187 | fs.statSync(paths.config); 188 | } catch (e) { 189 | fs.mkdirsSync(paths.config); 190 | } 191 | 192 | return paths.config; 193 | } 194 | 195 | static getLogDirectory() { 196 | if (Command.APP_NAME === null) { 197 | throw Error('No app name set'); 198 | } 199 | 200 | try { 201 | fs.statSync(paths.log); 202 | } catch (e) { 203 | fs.mkdirsSync(paths.log); 204 | } 205 | 206 | return paths.log; 207 | } 208 | 209 | static getTempDirectory() { 210 | if (Command.APP_NAME === null) { 211 | throw Error('No app name set'); 212 | } 213 | 214 | try { 215 | fs.statSync(paths.temp); 216 | } catch (e) { 217 | fs.mkdirsSync(paths.temp); 218 | } 219 | 220 | return paths.temp; 221 | } 222 | 223 | static list(nodes, options) { 224 | if (!options) { 225 | options = {}; 226 | } 227 | 228 | switch (options.sortOrder) { 229 | case Command.SORT_BY_DATE: 230 | nodes.sort((a, b) => { 231 | if (a.getModifiedDate() < b.getModifiedDate()) { 232 | return -1; 233 | } 234 | if (a.getModifiedDate() > b.getModifiedDate()) { 235 | return 1; 236 | } 237 | 238 | return 0; 239 | }); 240 | break; 241 | default: 242 | nodes.sort((a, b) => { 243 | if (a.getName().toLowerCase() < b.getName().toLowerCase()) { 244 | return -1; 245 | } 246 | if (a.getName().toLowerCase() > b.getName().toLowerCase()) { 247 | return 1; 248 | } 249 | 250 | return 0; 251 | }); 252 | break; 253 | } 254 | 255 | for (let i = 0; i < nodes.length; i++) { 256 | let node = nodes[i]; 257 | 258 | if (node.inTrash() && options.showTrash === false) { 259 | continue; 260 | } 261 | 262 | if (node.isPending() && options.showPending === false) { 263 | continue; 264 | } 265 | 266 | let now = new Date(), 267 | date = null; 268 | 269 | switch (options.displayDate) { 270 | case 'created': 271 | date = new Date(node.getCreatedDate()); 272 | break; 273 | default: 274 | date = new Date(node.getModifiedDate()); 275 | break; 276 | } 277 | 278 | let month = Utils.pad(moment(date).format('MMM'), 4), 279 | day = Utils.pad(moment(date).format('D'), 2, 'left'), 280 | dateString = `${month} ${day} ${moment(date).format('YYYY')}`; 281 | 282 | if (now.getYear() === date.getYear()) { 283 | dateString = `${month} ${day} ${Utils.pad(moment(date).format('H:mm'), 5, 'left')}`; 284 | } 285 | 286 | let name = node.getName(); 287 | if (options.decrypt && node.getLabels().indexOf('enc') !== -1) { 288 | name = Utils.decryptString(node.getName(), options.password, options.algorithm); 289 | } 290 | 291 | switch (node.getStatus()) { 292 | case Node.STATUS_TRASH: 293 | name = chalk.red(name); 294 | break; 295 | case Node.STATUS_PENDING: 296 | name = chalk.yellow(name); 297 | break; 298 | default: 299 | if (node.isFolder()) { 300 | name = chalk.blue(name); 301 | } 302 | break; 303 | } 304 | 305 | let size = Utils.convertFileSize(node.getSize(), 0); 306 | 307 | Logger.info(`${node.getId()} ${dateString} ${Utils.pad(node.getStatus(), 10)} ${Utils.pad(node.getKind(), 7)} ${Utils.pad(size, 6)} ${name}`); 308 | } 309 | } 310 | 311 | static setAppName(name) { 312 | Command.APP_NAME = name; 313 | paths = envPaths(name); 314 | } 315 | 316 | static shutdown(code) { 317 | if (code === undefined) { 318 | code = 0; 319 | } 320 | 321 | Logger.debug(`Exiting with code ${code}`); 322 | Logger.flushAndExit(code); 323 | } 324 | 325 | static startSpinner(message, verbosity) { 326 | if (Logger.getOutputLevel() !== 'info') { 327 | return; 328 | } 329 | 330 | spinnerText = message || ''; 331 | 332 | let frame = elegantSpinner(); 333 | logUpdate.done(); 334 | spinner = setInterval(() => { 335 | process.stdout.clearLine(); 336 | process.stdout.cursorTo(0); 337 | process.stdout.write(spinnerText + chalk.bold.cyan(frame())); 338 | }, 50); 339 | } 340 | 341 | static updateSpinnerText(message) { 342 | spinnerText = message || ''; 343 | } 344 | 345 | static stopSpinner(message) { 346 | if (spinner) { 347 | clearInterval(spinner); 348 | process.stdout.clearLine(); 349 | process.stdout.cursorTo(0); 350 | if (message) { 351 | console.log(message); 352 | } 353 | } 354 | } 355 | } 356 | 357 | Command.APP_NAME = null; 358 | Command.SORT_BY_NAME = 'name'; 359 | Command.SORT_BY_DATE = 'date'; 360 | Command.VERBOSE_LEVEL = 0; 361 | 362 | module.exports = Command; 363 | -------------------------------------------------------------------------------- /lib/Commands/ConfigCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Utils = require('../Utils'), 5 | Logger = require('../Logger'), 6 | chalk = require('chalk'); 7 | 8 | class ConfigCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | let option = args[0], 12 | value = args[1], 13 | data = this.config.flatten(); 14 | 15 | if (!option) { 16 | let keys = Object.keys(data), 17 | maxSize = keys.sort((a, b) => { 18 | return b.length - a.length; 19 | })[0].length; 20 | 21 | for (let key in data) { 22 | Logger.info(`${Utils.pad(key, maxSize)} = ${chalk.cyan(data[key])}`); 23 | } 24 | 25 | this.config.save(); 26 | 27 | return resolve(); 28 | } else if (data[option] === undefined) { 29 | return reject(Error(`Option '${option}' not found`)); 30 | } 31 | 32 | if (!value) { 33 | if (options.reset) { 34 | this.config.reset(option); 35 | Logger.info(`Reset ${chalk.cyan(option)} to ${this.config.get(option)}`); 36 | } else { 37 | Logger.info(data[option]); 38 | } 39 | } else { 40 | this.config.set(option, value); 41 | Logger.info(`${chalk.cyan(option)} saved`); 42 | } 43 | 44 | this.config.save(); 45 | 46 | return resolve(); 47 | }); 48 | } 49 | } 50 | 51 | module.exports = ConfigCommand; 52 | -------------------------------------------------------------------------------- /lib/Commands/DecryptCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'), 6 | crypto = require('crypto'), 7 | fs = require('fs-extra'), 8 | base64 = require('base64-stream'), 9 | zlib = require('zlib'), 10 | async = require('async'), 11 | inquirer = require('inquirer'); 12 | 13 | class DecryptCommand extends Command { 14 | run(args, options) { 15 | return new Promise((resolve, reject) => { 16 | let filePath = args[0], 17 | savePath = args[1] || null, 18 | password = this.config.get('crypto.password'); 19 | 20 | this.initialize((err, data) => { 21 | if (err) { 22 | return reject(err); 23 | } 24 | 25 | async.waterfall([ 26 | callback => { 27 | if (!options.password) { 28 | return callback(); 29 | } 30 | 31 | inquirer.prompt([ 32 | { 33 | type: 'password', 34 | name: 'password', 35 | message: 'password: ' 36 | } 37 | ], answers => { 38 | password = answers.password; 39 | callback(); 40 | }); 41 | }, 42 | callback => { 43 | let readStream = fs.createReadStream(filePath), 44 | decipher = crypto.createDecipher(this.config.get('crypto.algorithm'), password), 45 | writeStream = process.stdout; 46 | 47 | if (savePath) { 48 | writeStream = fs.createWriteStream(savePath); 49 | writeStream.on('finish', callback); 50 | } else { 51 | readStream.on('end', callback); 52 | } 53 | 54 | if (this.config.get('crypto.armor') || options.armor) { 55 | Logger.debug(`De-armoring encrypted contents`); 56 | readStream.pipe(base64.decode()).pipe(decipher).pipe(writeStream); 57 | } else { 58 | readStream.pipe(decipher).pipe(writeStream); 59 | } 60 | }, 61 | ], err => { 62 | if (err) { 63 | return reject(err); 64 | } 65 | 66 | return resolve(); 67 | }); 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | module.exports = DecryptCommand; 74 | -------------------------------------------------------------------------------- /lib/Commands/DeleteEverythingCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'), 6 | async = require('async'), 7 | fs = require('fs-extra'), 8 | inquirer = require('inquirer'); 9 | 10 | class DeleteEverythingCommand extends Command { 11 | run(args, options) { 12 | return new Promise((resolve, reject) => { 13 | inquirer.prompt([ 14 | { 15 | type: 'confirm', 16 | name: 'confirm', 17 | message: 'really delete everything? ', 18 | default: false, 19 | } 20 | ], answers => { 21 | if (!answers.confirm) { 22 | return resolve(); 23 | } 24 | 25 | Command.startSpinner('Removing all files and folders'); 26 | async.waterfall([ 27 | callback => { 28 | Logger.verbose(`Removing cache directory ${Command.getCacheDirectory()}`); 29 | fs.remove(Command.getCacheDirectory(), callback); 30 | }, 31 | callback => { 32 | Logger.verbose(`Removing config directory ${Command.getConfigDirectory()}`); 33 | fs.remove(Command.getConfigDirectory(), callback); 34 | }, 35 | callback => { 36 | Logger.verbose(`Removing log directory ${Command.getLogDirectory()}`); 37 | fs.remove(Command.getLogDirectory(), callback); 38 | }, 39 | ], err => { 40 | if (err) { 41 | return reject(err); 42 | } 43 | 44 | Command.stopSpinner('Done.'); 45 | 46 | return resolve(); 47 | }); 48 | }); 49 | }); 50 | } 51 | } 52 | 53 | module.exports = DeleteEverythingCommand; 54 | -------------------------------------------------------------------------------- /lib/Commands/DiskUsageCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | async = require('async'), 6 | Utils = require('../Utils'), 7 | Logger = require('../Logger'); 8 | 9 | class DiskUsageCommand extends Command { 10 | run(args, options) { 11 | return new Promise((resolve, reject) => { 12 | let remotePath = args[0], 13 | searchFunction = Node.loadByPath, 14 | notFound = `No node exists at path '${remotePath}'`; 15 | if (options.id) { 16 | searchFunction = Node.loadById; 17 | notFound = `No node exists with ID '${remotePath}'`; 18 | } 19 | 20 | if (remotePath) { 21 | remotePath = remotePath.trim(); 22 | } 23 | 24 | this.initialize((err, data) => { 25 | if (err) { 26 | return reject(err); 27 | } 28 | 29 | searchFunction(remotePath, (err, node) => { 30 | if (err) { 31 | return reject(err); 32 | } 33 | 34 | if (!node) { 35 | return reject(Error(notFound)); 36 | } 37 | 38 | let size = 0, 39 | files = 0, 40 | folders = 0; 41 | 42 | Command.startSpinner('Calculating '); 43 | 44 | calculateSize(node, () => { 45 | Command.stopSpinner(); 46 | Logger.info(Utils.convertFileSize(size)); 47 | Logger.info(`${files} files, ${folders} folders`, 1); 48 | 49 | resolve(); 50 | }); 51 | 52 | function calculateSize(node, callback) { 53 | node.getChildren({ 54 | remote: options.remote, 55 | }, (err, children) => { 56 | async.forEach(children, (child, callback) => { 57 | let nodeSize = child.getSize(); 58 | if (nodeSize) { 59 | size += nodeSize; 60 | } 61 | 62 | if (child.isFolder()) { 63 | folders++; 64 | 65 | return calculateSize(child, callback); 66 | } 67 | 68 | files++; 69 | callback(); 70 | }, err => { 71 | callback(err); 72 | }); 73 | }); 74 | } 75 | }); 76 | }); 77 | }); 78 | } 79 | } 80 | 81 | module.exports = DiskUsageCommand; 82 | -------------------------------------------------------------------------------- /lib/Commands/DownloadCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Utils = require('../Utils'), 6 | Logger = require('../Logger'), 7 | ProgressBar = require('../ProgressBar'), 8 | async = require('async'), 9 | inquirer = require('inquirer'); 10 | 11 | class DownloadCommand extends Command { 12 | run(args, options) { 13 | return new Promise((resolve, reject) => { 14 | let remotePath = args[0], 15 | localPath = args[1], 16 | searchFunction = Node.loadByPath, 17 | notFound = `No node exists at path '${remotePath}'`; 18 | if (options.id) { 19 | searchFunction = Node.loadById; 20 | notFound = `No node exists with ID '${remotePath}'`; 21 | } 22 | 23 | if (remotePath) { 24 | remotePath = remotePath.trim(); 25 | } 26 | 27 | this.initialize((err, data) => { 28 | if (err) { 29 | return reject(err); 30 | } 31 | 32 | searchFunction(remotePath, (err, node) => { 33 | if (err) { 34 | return reject(err); 35 | } 36 | 37 | if (!node) { 38 | return reject(Error(notFound)); 39 | } 40 | 41 | let queryParams = {}; 42 | if (options.dimensions) { 43 | queryParams.viewBox = options.dimensions; 44 | } 45 | 46 | let bar = null, 47 | downloadingNode = null, 48 | bytesDownloaded = 0, 49 | bytesTransfered = 0, 50 | lastRun = null, 51 | startTime = null, 52 | opts = { 53 | remote: options.remote, 54 | queryParams: queryParams, 55 | maxConnections: this.config.get('download.maxConnections') || 1, 56 | retryAttempt: 0, 57 | numRetries: this.config.get('upload.numRetries'), 58 | checkMd5: this.config.get('download.checkMd5'), 59 | decrypt: options.decrypt || false, 60 | password: this.config.get('crypto.password'), 61 | algorithm: this.config.get('crypto.algorithm'), 62 | armor: options.armor || this.config.get('crypto.armor'), 63 | }; 64 | 65 | async.waterfall([ 66 | callback => { 67 | if (!options.decrypt || !options.password) { 68 | return callback(); 69 | } 70 | 71 | inquirer.prompt([ 72 | { 73 | type: 'password', 74 | name: 'password', 75 | message: 'password: ' 76 | } 77 | ], answers => { 78 | opts.password = answers.password; 79 | callback(); 80 | }); 81 | }, 82 | ], err => { 83 | if (err) { 84 | return reject(err); 85 | } 86 | 87 | node.on('fileDownload', node => { 88 | if (this.config.get('cli.progressBars') && opts.maxConnections === 1) { 89 | startTime = Date.now(); 90 | bytesDownloaded = 0; 91 | downloadingNode = node; 92 | lastRun = Date.now(); 93 | bar = new ProgressBar(`Downloading ${downloadingNode.getName()}\n:percent[:bar] :speed eta :etas (:downloaded / :filesize)`, { 94 | total: node.getSize(), 95 | incomplete: ' ', 96 | width: 40, 97 | clear: false, 98 | renderThrottle: this.config.get('cli.progressInterval') 99 | }); 100 | } 101 | }); 102 | 103 | node.on('downloadProgress', data => { 104 | if (bar) { 105 | bytesDownloaded += data.length; 106 | bytesTransfered += data.length; 107 | 108 | let timeDiff = Date.now() - lastRun; 109 | 110 | if (timeDiff >= this.config.get('cli.progressInterval') || bytesDownloaded >= downloadingNode.getSize()) { 111 | lastRun = Date.now(); 112 | bar.tick(bytesTransfered, { 113 | speed: `${Utils.convertFileSize(Math.round(bytesTransfered / (timeDiff / 1000)), 2)}/s`, 114 | downloaded: Utils.convertFileSize(bytesDownloaded), 115 | filesize: Utils.convertFileSize(downloadingNode.getSize()), 116 | }); 117 | bytesTransfered = 0; 118 | } 119 | } 120 | }); 121 | 122 | node.on('downloadComplete', (response, body, retval, data) => { 123 | // Clear out progress bar 124 | if (bar !== null) { 125 | bar.clear(); 126 | bar = null; 127 | } 128 | 129 | if (response) { 130 | Logger.debug(`Response returned with status code ${response.statusCode}`); 131 | } 132 | 133 | if (retval.success) { 134 | return Logger.info(`Successfully downloaded '${data.localPath}'`); 135 | } 136 | 137 | let message = `Failed to download '${data.localPath}'`; 138 | if (retval.data.message) { 139 | message += ': ' + retval.data.message; 140 | } 141 | 142 | if (retval.data.exists) { 143 | Logger.warn(message); 144 | } else { 145 | Logger.error(message); 146 | } 147 | }); 148 | 149 | node.download(localPath, opts, (err, data) => { 150 | if (err) { 151 | return reject(err); 152 | } 153 | 154 | if (data.success) { 155 | return resolve(); 156 | } 157 | 158 | return reject(); 159 | }); 160 | }); 161 | }); 162 | }); 163 | }); 164 | } 165 | } 166 | 167 | module.exports = DownloadCommand; 168 | -------------------------------------------------------------------------------- /lib/Commands/EncryptCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | crypto = require('crypto'), 6 | fs = require('fs-extra'), 7 | base64 = require('base64-stream'), 8 | zlib = require('zlib'), 9 | async = require('async'), 10 | inquirer = require('inquirer'); 11 | 12 | class EncryptCommand extends Command { 13 | run(args, options) { 14 | return new Promise((resolve, reject) => { 15 | let filePath = args[0], 16 | savePath = args[1] || null, 17 | password = this.config.get('crypto.password'); 18 | 19 | this.initialize((err, data) => { 20 | if (err) { 21 | return reject(err); 22 | } 23 | 24 | async.waterfall([ 25 | callback => { 26 | if (!options.password) { 27 | return callback(); 28 | } 29 | 30 | inquirer.prompt([ 31 | { 32 | type: 'password', 33 | name: 'password', 34 | message: 'password: ' 35 | } 36 | ], answers => { 37 | password = answers.password; 38 | callback(); 39 | }); 40 | }, 41 | callback => { 42 | let readStream = fs.createReadStream(filePath), 43 | cipher = crypto.createCipher(this.config.get('crypto.algorithm'), password), 44 | writeStream = process.stdout; 45 | 46 | if (savePath) { 47 | writeStream = fs.createWriteStream(savePath); 48 | writeStream.on('finish', callback); 49 | } else { 50 | readStream.on('end', callback); 51 | } 52 | 53 | readStream.pipe(cipher); 54 | 55 | if (this.config.get('crypto.armor') || options.armor) { 56 | readStream.pipe(base64.encode()); 57 | } 58 | 59 | readStream.pipe(writeStream); 60 | }, 61 | ], err => { 62 | if (err) { 63 | return reject(err); 64 | } 65 | 66 | return resolve(); 67 | }); 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | module.exports = EncryptCommand; 74 | -------------------------------------------------------------------------------- /lib/Commands/ExistsCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let fs = require('fs'), 4 | Command = require('./Command'), 5 | Node = require('../Node'), 6 | chalk = require('chalk'), 7 | async = require('async'), 8 | path = require('path'), 9 | Utils = require('../Utils'), 10 | Logger = require('../Logger'); 11 | 12 | class ExistsCommand extends Command { 13 | run(args, options) { 14 | return new Promise((resolve, reject) => { 15 | let remoteFolder = args.pop(); 16 | 17 | this.initialize((err, data) => { 18 | if (err) { 19 | return reject(err); 20 | } 21 | 22 | if (args.length === 0) { 23 | return reject(Error('Destination path must be specified')); 24 | } 25 | 26 | async.forEachSeries(args, (localPath, callback) => { 27 | localPath = path.resolve(localPath); 28 | 29 | remoteFolder = Utils.getPathArray(remoteFolder); 30 | remoteFolder.push(Utils.getPathArray(localPath).pop()); 31 | remoteFolder = remoteFolder.join('/'); 32 | 33 | fs.stat(localPath, (err, stat) => { 34 | if (err) { 35 | return callback(err); 36 | } 37 | 38 | if (stat.isDirectory()) { 39 | this.iterateDirectory(localPath, remoteFolder, callback); 40 | } else { 41 | this.checkFile(localPath, `${remoteFolder}/${path.basename(localPath)}`, callback); 42 | } 43 | }); 44 | }, err => { 45 | if (err) { 46 | return reject(err); 47 | } 48 | 49 | return resolve(); 50 | }); 51 | }); 52 | }); 53 | } 54 | 55 | iterateDirectory(directory, remoteFolder, callback) { 56 | fs.readdir(directory, (err, list) => { 57 | if (err) { 58 | return callback(err); 59 | } 60 | 61 | if (list.length === 0) { 62 | return callback(); 63 | } 64 | 65 | async.forEachSeries(list, (item, callback) => { 66 | let itemPath = `${directory}/${item}`, 67 | remoteFile = itemPath.replace(directory, remoteFolder); 68 | this.checkFile(itemPath, remoteFile, callback); 69 | }, err => { 70 | callback(err); 71 | }); 72 | }); 73 | } 74 | 75 | checkFile(localPath, remoteFile, callback) { 76 | fs.stat(localPath, (err, stat) => { 77 | if (err) { 78 | return callback(err); 79 | } 80 | 81 | if (stat.isDirectory()) { 82 | return this.iterateDirectory(localPath, callback); 83 | } 84 | 85 | Node.exists(remoteFile, localPath, { 86 | checkMd5: this.config.get('upload.checkMd5'), 87 | }, (err, result) => { 88 | if (!result.success) { 89 | Logger.error(`File ${remoteFile} does not exist`); 90 | 91 | return callback(); 92 | } 93 | 94 | if (result.data.pathMatch && (result.data.md5Match || result.data.sizeMatch)) { 95 | Logger.info(`File ${remoteFile} exists and is identical to local copy`); 96 | } else if (result.data.pathMatch) { 97 | Logger.warn(`File ${remoteFile} exists but does not match local copy`); 98 | } else { 99 | Logger.warn(`File ${remoteFile} exists at the following location: ${result.data.nodes.join(', ')}`); 100 | } 101 | 102 | return callback(); 103 | }); 104 | }); 105 | } 106 | } 107 | 108 | module.exports = ExistsCommand; 109 | -------------------------------------------------------------------------------- /lib/Commands/FindCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'); 5 | 6 | class FindCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | let query = args[0], 10 | sort = Command.SORT_BY_NAME; 11 | 12 | if (options.time) { 13 | sort = Command.SORT_BY_DATE; 14 | } 15 | 16 | this.initialize((err, data) => { 17 | if (err) { 18 | return reject(err); 19 | } 20 | 21 | Command.startSpinner('Searching '); 22 | 23 | Node.searchBy('name', query, (err, nodes) => { 24 | Command.stopSpinner(); 25 | 26 | if (err) { 27 | return reject(err); 28 | } 29 | 30 | Command.list(nodes, { 31 | sortOrder: sort, 32 | showTrash: this.config.get('display.showTrash'), 33 | showPending: this.config.get('display.showPending') 34 | }); 35 | 36 | return resolve(); 37 | }); 38 | }); 39 | }); 40 | } 41 | } 42 | 43 | module.exports = FindCommand; 44 | -------------------------------------------------------------------------------- /lib/Commands/InfoCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Logger = require('../Logger'); 5 | 6 | class InfoCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | this.initialize((err, data) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | 14 | if (!data.success) { 15 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 16 | } 17 | 18 | this.account.getInfo((err, data) => { 19 | if (err) { 20 | return reject(err); 21 | } 22 | 23 | if (this.config.get('json.pretty') === true) { 24 | let output = JSON.stringify(data.data, null, 2); 25 | output.split('\n').forEach(line => { 26 | Logger.info(line); 27 | }); 28 | } else { 29 | Logger.info(JSON.stringify(data.data)); 30 | } 31 | 32 | return resolve(); 33 | }); 34 | }); 35 | }); 36 | } 37 | } 38 | 39 | module.exports = InfoCommand; 40 | -------------------------------------------------------------------------------- /lib/Commands/InitCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Logger = require('../Logger'), 5 | inquirer = require('inquirer'), 6 | open = require('open'); 7 | 8 | class InitCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | Command.startSpinner('Initializing... '); 12 | 13 | if (!this.config.get('auth.email')) { 14 | Command.stopSpinner(); 15 | 16 | return reject(Error('Account email must be set via the `config` command')); 17 | } 18 | 19 | this.initialize((err, data) => { 20 | Command.stopSpinner(); 21 | 22 | if (err) { 23 | return reject(err); 24 | } 25 | 26 | if (data.success === true) { 27 | Logger.info(`Successfully authenticated with Amazon Cloud Drive`); 28 | 29 | return resolve(); 30 | } 31 | 32 | if (data.data.auth_url) { 33 | if (this.config.get('auth.id') && this.config.get('auth.secret')) { 34 | Logger.info(data.data.message); 35 | Logger.info(data.data.auth_url); 36 | inquirer.prompt([ 37 | { 38 | type: 'input', 39 | name: 'callbackUrl', 40 | message: 'url: ' 41 | } 42 | ], answers => { 43 | this.account.authorize(answers.callbackUrl, {}, (err, data) => { 44 | if (err) { 45 | return reject(err); 46 | } 47 | 48 | if (data.success === false) { 49 | return reject(Error(`Failed to authenticate with Amazon Cloud Drive: ${JSON.stringify(data.data)}`)); 50 | } 51 | 52 | Logger.info('Successfully authenticated with Amazon Cloud Drive'); 53 | 54 | return resolve(); 55 | }); 56 | }); 57 | } else { 58 | Logger.info(`For the one-time initial authorization, a browser tab will be opened to 'https://data-mind-687.appspot.com/clouddrive'. 59 | Accept the authorization and paste in the response into this application.`); 60 | open('https://data-mind-687.appspot.com/clouddrive'); 61 | inquirer.prompt([ 62 | { 63 | type: 'input', 64 | name: 'token', 65 | message: 'token: ' 66 | } 67 | ], answers => { 68 | this.account.authorize(JSON.parse(answers.token.trim()), {}, (err, data) => { 69 | if (err) { 70 | return reject(err); 71 | } 72 | 73 | if (data.success === false) { 74 | return reject(Error(`Failed to authenticate with Amazon Cloud Drive: ${JSON.stringify(data.data)}`)); 75 | } 76 | 77 | Logger.info(`Successfully authenticated with Amazon Cloud Drive`); 78 | 79 | return resolve(); 80 | }); 81 | }); 82 | } 83 | } else { 84 | return reject(Error(`Failed to authorize account with Amazon Cloud Drive. Unknown error occurred: ${JSON.stringify(data.data)}`)); 85 | } 86 | }); 87 | }); 88 | } 89 | } 90 | 91 | module.exports = InitCommand; 92 | -------------------------------------------------------------------------------- /lib/Commands/LinkCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class LinkCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let remotePath = args[0], 11 | newPath = args[1], 12 | searchFunction = Node.loadByPath, 13 | notFound = `No node exists at path '${remotePath}'`; 14 | if (options.id) { 15 | searchFunction = Node.loadById; 16 | notFound = `No node exists with ID '${remotePath}'`; 17 | } 18 | 19 | if (remotePath) { 20 | remotePath = remotePath.trim(); 21 | } 22 | 23 | if (!newPath) { 24 | newPath = ''; 25 | } 26 | 27 | this.initialize((err, data) => { 28 | if (err) { 29 | return reject(err); 30 | } 31 | 32 | searchFunction(remotePath, (err, node) => { 33 | if (err) { 34 | return reject(err); 35 | } 36 | 37 | if (!node) { 38 | return reject(Error(notFound)); 39 | } 40 | 41 | Node.loadByPath(newPath, (err, newParent) => { 42 | if (err) { 43 | return reject(err); 44 | } 45 | 46 | if (!newParent || !newParent.isFolder()) { 47 | return reject(Error(`No directory exists at path '${newPath}'`)); 48 | } 49 | 50 | node.link(newParent.getId(), (err, data) => { 51 | if (err) { 52 | return reject(err); 53 | } 54 | 55 | if (!data.success) { 56 | return reject(Error(`Failed to link node to '${newPath ? newPath : '/'}': ${JSON.stringify(data.data)}`)); 57 | } 58 | 59 | Logger.info(`Successfully linked node to '${newPath ? newPath : '/'}'`); 60 | 61 | return resolve(); 62 | }); 63 | }); 64 | }); 65 | }); 66 | }); 67 | } 68 | } 69 | 70 | module.exports = LinkCommand; 71 | -------------------------------------------------------------------------------- /lib/Commands/ListCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | async = require('async'), 6 | inquirer = require('inquirer'); 7 | 8 | class ListCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | let remotePath = args[0], 12 | searchFunction = Node.loadByPath, 13 | notFound = `No node exists at path '${remotePath}'`, 14 | sort = Command.SORT_BY_NAME; 15 | 16 | if (options.id) { 17 | searchFunction = Node.loadById; 18 | notFound = `No node exists with ID '${remotePath}'`; 19 | } 20 | 21 | if (options.time) { 22 | sort = Command.SORT_BY_DATE; 23 | } 24 | 25 | if (remotePath) { 26 | remotePath = remotePath.trim(); 27 | } 28 | 29 | this.initialize((err, data) => { 30 | if (err) { 31 | return reject(err); 32 | } 33 | 34 | searchFunction(remotePath, (err, node) => { 35 | if (err) { 36 | return reject(err); 37 | } 38 | 39 | if (!node) { 40 | return reject(Error(notFound)); 41 | } 42 | 43 | node.getChildren({ 44 | remote: options.remote, 45 | }, (err, children) => { 46 | if (err) { 47 | return reject(err); 48 | } 49 | 50 | let opts = { 51 | sortOrder: sort, 52 | showTrash: this.config.get('display.showTrash'), 53 | showPending: this.config.get('display.showPending'), 54 | displayDate: this.config.get('display.date'), 55 | decrypt: options.decrypt, 56 | password: this.config.get('crypto.password'), 57 | algorithm: this.config.get('crypto.algorithm'), 58 | }; 59 | 60 | async.waterfall([ 61 | callback => { 62 | if (!options.decrypt || !options.password) { 63 | return callback(); 64 | } 65 | 66 | inquirer.prompt([ 67 | { 68 | type: 'password', 69 | name: 'password', 70 | message: 'password: ' 71 | } 72 | ], answers => { 73 | opts.password = answers.password; 74 | callback(); 75 | }); 76 | }, 77 | callback => { 78 | Command.list(children, opts); 79 | 80 | return callback(); 81 | }, 82 | ], err => { 83 | if (err) { 84 | return reject(err); 85 | } 86 | 87 | return resolve(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | }); 93 | } 94 | } 95 | 96 | module.exports = ListCommand; 97 | -------------------------------------------------------------------------------- /lib/Commands/ListPendingCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'); 5 | 6 | class ListPendingCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | let sort = Command.SORT_BY_NAME; 10 | if (options.time) { 11 | sort = Command.SORT_BY_DATE; 12 | } 13 | 14 | this.initialize((err, data) => { 15 | if (err) { 16 | return reject(err); 17 | } 18 | 19 | Node.filter({ 20 | status: 'PENDING' 21 | }, (err, nodes) => { 22 | Command.list(nodes, { 23 | sortOrder: sort, 24 | showTrash: false, 25 | showPending: true 26 | }); 27 | 28 | return resolve(); 29 | }); 30 | }); 31 | }); 32 | } 33 | } 34 | 35 | module.exports = ListPendingCommand; 36 | -------------------------------------------------------------------------------- /lib/Commands/ListTrashCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class ListTrashCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let sort = Command.SORT_BY_NAME; 11 | if (options.time) { 12 | sort = Command.SORT_BY_DATE; 13 | } 14 | 15 | this.initialize((err, data) => { 16 | if (err) { 17 | return reject(err); 18 | } 19 | 20 | if (options.remote) { 21 | Node.getTrash((err, result) => { 22 | if (err) { 23 | return reject(err); 24 | } 25 | 26 | Logger.info(JSON.stringify(result)); 27 | }); 28 | } else { 29 | Node.filter({ 30 | status: 'TRASH' 31 | }, (err, nodes) => { 32 | Command.list(nodes, { 33 | sortOrder: sort, 34 | showTrash: true, 35 | showPending: false 36 | }); 37 | 38 | return resolve(); 39 | }); 40 | } 41 | }); 42 | }); 43 | } 44 | } 45 | 46 | module.exports = ListTrashCommand; 47 | -------------------------------------------------------------------------------- /lib/Commands/MetadataCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class MetadataCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let remotePath = args[0], 11 | searchFunction = Node.loadByPath, 12 | notFound = `No node exists at path '${remotePath}'`; 13 | if (options.id) { 14 | searchFunction = Node.loadById; 15 | notFound = `No node exists with ID '${remotePath}'`; 16 | } 17 | 18 | if (remotePath) { 19 | remotePath = remotePath.trim(); 20 | } 21 | 22 | this.initialize((err, data) => { 23 | if (err) { 24 | return reject(err); 25 | } 26 | 27 | searchFunction(remotePath, (err, node) => { 28 | if (err) { 29 | return reject(err); 30 | } 31 | 32 | if (!node) { 33 | Logger.error(notFound); 34 | } else { 35 | if (this.config.get('json.pretty') === true) { 36 | let output = JSON.stringify(node.getData(), null, 2); 37 | output.split('\n').forEach(line => { 38 | Logger.info(line); 39 | }); 40 | } else { 41 | Logger.info(JSON.stringify(node.getData())); 42 | } 43 | } 44 | 45 | resolve(); 46 | }); 47 | }); 48 | }); 49 | } 50 | } 51 | 52 | module.exports = MetadataCommand; 53 | -------------------------------------------------------------------------------- /lib/Commands/MkdirCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class MkdirCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let path = args[0]; 11 | 12 | this.initialize((err, data) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | 17 | if (!data.success) { 18 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 19 | } 20 | 21 | Node.createDirectoryPath(path, {}, (err, data) => { 22 | if (err) { 23 | return reject(err); 24 | } 25 | 26 | if (!data.success) { 27 | return reject(Error(`Failed creating remote directory '${path}'`)); 28 | } 29 | 30 | Logger.info(`Successfully created remote directory '${path}'`); 31 | 32 | return resolve(); 33 | }); 34 | }); 35 | }); 36 | } 37 | } 38 | 39 | module.exports = MkdirCommand; 40 | -------------------------------------------------------------------------------- /lib/Commands/MoveCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class MoveCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let remotePath = args[0], 11 | newPath = args[1], 12 | searchFunction = Node.loadByPath, 13 | notFound = `No node exists at path '${remotePath}'`; 14 | if (options.id) { 15 | searchFunction = Node.loadById; 16 | notFound = `No node exists with ID '${remotePath}'`; 17 | } 18 | 19 | if (remotePath) { 20 | remotePath = remotePath.trim(); 21 | } 22 | 23 | if (!newPath) { 24 | newPath = ''; 25 | } 26 | 27 | this.initialize((err, data) => { 28 | if (err) { 29 | return reject(err); 30 | } 31 | 32 | searchFunction(remotePath, (err, node) => { 33 | if (err) { 34 | return reject(err); 35 | } 36 | 37 | if (!node) { 38 | return reject(Error(notFound)); 39 | } 40 | 41 | Node.loadByPath(newPath, (err, newParent) => { 42 | if (err) { 43 | return reject(err); 44 | } 45 | 46 | if (!newParent || !newParent.isFolder()) { 47 | return reject(Error(`No directory exists at path '${newPath}'`)); 48 | } 49 | 50 | node.move(newParent, (err, data) => { 51 | if (err) { 52 | return reject(err); 53 | } 54 | 55 | if (!data.success) { 56 | return reject(Error(`Failed to move node to '${newPath}': ${JSON.stringify(data.data)}`)); 57 | } 58 | 59 | Logger.info(`Successfully moved node to '${newPath}'`); 60 | 61 | return resolve(); 62 | }); 63 | }); 64 | }); 65 | }); 66 | }); 67 | } 68 | } 69 | 70 | module.exports = MoveCommand; 71 | -------------------------------------------------------------------------------- /lib/Commands/QuotaCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Logger = require('../Logger'); 5 | 6 | class QuotaCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | this.initialize((err, data) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | 14 | if (!data.success) { 15 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 16 | } 17 | 18 | this.account.getQuota((err, data) => { 19 | if (err) { 20 | return reject(err); 21 | } 22 | 23 | if (this.config.get('json.pretty') === true) { 24 | let output = JSON.stringify(data.data, null, 2); 25 | output.split('\n').forEach(line => { 26 | Logger.info(line); 27 | }); 28 | } else { 29 | Logger.info(JSON.stringify(data.data)); 30 | } 31 | 32 | return resolve(); 33 | }); 34 | }); 35 | }); 36 | } 37 | } 38 | 39 | module.exports = QuotaCommand; 40 | -------------------------------------------------------------------------------- /lib/Commands/RenameCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class RenameCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let remotePath = args[0], 11 | name = args[1]; 12 | 13 | this.initialize((err, data) => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | 18 | if (!data.success) { 19 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 20 | } 21 | 22 | let searchFunction = Node.loadByPath, 23 | notFound = `No node exists at path '${remotePath}'`; 24 | if (options.id) { 25 | searchFunction = Node.loadById; 26 | notFound = `No node exists with ID '${remotePath}'`; 27 | } 28 | 29 | if (remotePath) { 30 | remotePath = remotePath.trim(); 31 | } 32 | 33 | searchFunction(remotePath, (err, node) => { 34 | if (err) { 35 | return reject(err); 36 | } 37 | 38 | if (!node) { 39 | return reject(Error(notFound)); 40 | } 41 | 42 | node.rename(name, (err, data) => { 43 | if (err) { 44 | return reject(err); 45 | } 46 | 47 | if (!data.success) { 48 | return reject(Error(`Failed to rename node to '${name}'`)); 49 | } 50 | 51 | Logger.info(`Successfully renamed node to '${name}'`); 52 | 53 | return resolve(); 54 | }); 55 | }); 56 | }); 57 | }); 58 | } 59 | } 60 | 61 | module.exports = RenameCommand; 62 | -------------------------------------------------------------------------------- /lib/Commands/ResolveCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class ResolveCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let id = args[0]; 11 | if (!id) { 12 | return reject(Error('ID is required to resolve')); 13 | } 14 | 15 | id = id.trim(); 16 | 17 | this.initialize((err, data) => { 18 | if (err) { 19 | return reject(err); 20 | } 21 | 22 | Node.loadById(id, (err, node) => { 23 | if (err) { 24 | return reject(err); 25 | } 26 | 27 | if (!node) { 28 | return reject(Error(`No node found with ID '${id}'`)); 29 | } 30 | 31 | node.getPath((err, path) => { 32 | Logger.info(path); 33 | 34 | return resolve(); 35 | }); 36 | }); 37 | }); 38 | }); 39 | } 40 | } 41 | 42 | module.exports = ResolveCommand; 43 | -------------------------------------------------------------------------------- /lib/Commands/RestoreCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'), 6 | async = require('async'); 7 | 8 | class RestoreCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | let remotePath = args[0], 12 | searchFunction = Node.loadByPath, 13 | notFound = `No node exists at path '${remotePath}'`; 14 | this.options = options; 15 | if (options.id) { 16 | searchFunction = Node.loadById; 17 | notFound = `No node exists with ID '${remotePath}'`; 18 | } 19 | 20 | if (remotePath) { 21 | remotePath = remotePath.trim(); 22 | } 23 | 24 | this.initialize((err, data) => { 25 | if (err) { 26 | return reject(err); 27 | } 28 | 29 | if (!data.success) { 30 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 31 | } 32 | 33 | searchFunction(remotePath, (err, node) => { 34 | if (err) { 35 | return reject(err); 36 | } 37 | 38 | if (!node) { 39 | return reject(Error(notFound)); 40 | } 41 | 42 | if (!options.recursive || !node.isFolder()) { 43 | return node.restore((err, result) => { 44 | if (err) { 45 | return reject(err); 46 | } 47 | 48 | if (result.success) { 49 | Logger.info(`Successfully restored node ${node.getName()} (${node.getId()})`); 50 | } else { 51 | Logger.error(`Failed to restore node ${node.getName()} (${node.getId()}): ${JSON.stringify(result)}`); 52 | } 53 | 54 | return resolve(); 55 | }); 56 | } 57 | 58 | Logger.info('Recursively restoring nodes...'); 59 | 60 | return this.restore(node, err => { 61 | if (err) { 62 | return reject(err); 63 | } 64 | 65 | return resolve(); 66 | }); 67 | }); 68 | }); 69 | }); 70 | } 71 | 72 | restore(node, callback) { 73 | this.restoreNode(node, (err, result) => { 74 | if (err) { 75 | return callback(err); 76 | } 77 | 78 | if (result.success && node.isFolder()) { 79 | return node.getChildren({ 80 | remote: this.options.remote, 81 | }, (err, children) => { 82 | async.forEachSeries(children, (child, callback) => { 83 | this.restore(child, (err, result) => { 84 | return callback(err, result); 85 | }); 86 | }, err => { 87 | return callback(err); 88 | }); 89 | }); 90 | } 91 | 92 | return callback(null, result); 93 | }); 94 | } 95 | 96 | restoreNode(node, callback) { 97 | let retval = { 98 | success: true, 99 | data: {}, 100 | }; 101 | 102 | node.getPath((err, path) => { 103 | Logger.info(`Attempting to restore ${node.getKind()} node "${path}" (${node.getId()})...`); 104 | if (!node.inTrash()) { 105 | Logger.warn(`Node ${node.getName()} (${node.getId()}) is not in the trash`, 2); 106 | 107 | return callback(null, retval); 108 | } 109 | 110 | node.restore((err, result) => { 111 | if (err) { 112 | return callback(err); 113 | } 114 | 115 | if (result.success) { 116 | Logger.info(`Restored node "${path}" (${node.getId()})`); 117 | } else { 118 | Logger.error(`Failed to restore node "${path}" (${node.getId()}): ${JSON.stringify(result)}`); 119 | } 120 | 121 | return callback(null, result); 122 | }); 123 | }); 124 | } 125 | } 126 | 127 | module.exports = RestoreCommand; 128 | -------------------------------------------------------------------------------- /lib/Commands/ShareCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class ShareCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let remotePath = args[0]; 11 | 12 | this.initialize((err, data) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | 17 | if (!data.success) { 18 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 19 | } 20 | 21 | let searchFunction = Node.loadByPath, 22 | notFound = `No node exists at path '${remotePath}'`; 23 | if (options.id) { 24 | searchFunction = Node.loadById; 25 | notFound = `No node exists with ID '${remotePath}'`; 26 | } 27 | 28 | if (remotePath) { 29 | remotePath = remotePath.trim(); 30 | } 31 | 32 | searchFunction(remotePath, (err, node) => { 33 | if (err) { 34 | return reject(err); 35 | } 36 | 37 | if (!node) { 38 | return reject(Error(notFound)); 39 | } 40 | 41 | if (!node.isFile()) { 42 | return reject(Error('Links can only be created for files.')); 43 | } 44 | 45 | node.getMetadata(true, (err, data) => { 46 | if (err) { 47 | return reject(err); 48 | } 49 | 50 | if (data.success === false) { 51 | return reject(Error(`Failed to retrieve metadata for node '${remotePath}': ${JSON.stringify(data.data)}`)); 52 | } 53 | 54 | if (data.data.tempLink === undefined) { 55 | return reject(Error('Failed retrieving temporary link. Make sure you have permission.')); 56 | } 57 | 58 | Logger.info(`Share link: ${data.data.tempLink}`); 59 | 60 | return resolve(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | } 66 | } 67 | 68 | module.exports = ShareCommand; 69 | -------------------------------------------------------------------------------- /lib/Commands/SyncCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Logger = require('../Logger'); 5 | 6 | class SyncCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | this.initialize((err, data) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | 14 | if (!data.success) { 15 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 16 | } 17 | 18 | Command.startSpinner('Syncing... '); 19 | 20 | let params = {}; 21 | if (this.config.get('sync.chunkSize')) { 22 | params.chunkSize = parseInt(this.config.get('sync.chunkSize')); 23 | } 24 | if (this.config.get('sync.maxNodes')) { 25 | params.maxNodes = parseInt(this.config.get('sync.maxNodes')); 26 | } 27 | 28 | this.account.sync(params, (err, data) => { 29 | Command.stopSpinner('Done.'); 30 | 31 | if (err) { 32 | return reject(err); 33 | } 34 | 35 | return resolve(); 36 | } 37 | ); 38 | }); 39 | }); 40 | } 41 | } 42 | 43 | module.exports = SyncCommand; 44 | -------------------------------------------------------------------------------- /lib/Commands/TrashCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'), 6 | async = require('async'); 7 | 8 | class TrashCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | let remotePath = args[0], 12 | searchFunction = Node.loadByPath, 13 | notFound = `No node exists at path '${remotePath}'`; 14 | this.options = options; 15 | if (options.id) { 16 | searchFunction = Node.loadById; 17 | notFound = `No node exists with ID '${remotePath}'`; 18 | } 19 | 20 | if (remotePath) { 21 | remotePath = remotePath.trim(); 22 | } 23 | 24 | this.initialize((err, data) => { 25 | if (err) { 26 | return reject(err); 27 | } 28 | 29 | if (!data.success) { 30 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 31 | } 32 | 33 | searchFunction(remotePath, (err, node) => { 34 | if (err) { 35 | return reject(err); 36 | } 37 | 38 | if (!node) { 39 | return reject(Error(notFound)); 40 | } 41 | 42 | if (!options.recursive || !node.isFolder()) { 43 | return node.trash((err, result) => { 44 | if (err) { 45 | return reject(err); 46 | } 47 | 48 | if (!data.success) { 49 | return reject(Error('Failed to trash node')); 50 | } 51 | 52 | Logger.info(`Node at '${remotePath}' successfully moved to trash`); 53 | 54 | return resolve(); 55 | }); 56 | } 57 | 58 | Logger.verbose('Recursively removing nodes...', 3); 59 | return this.trash(node, (err, result) => { 60 | if (err) { 61 | return reject(err); 62 | } 63 | 64 | return resolve(); 65 | }); 66 | }); 67 | }); 68 | }); 69 | } 70 | 71 | trash(node, callback) { 72 | if (node.isFile()) { 73 | return this.trashNode(node, callback); 74 | } 75 | 76 | node.getChildren({ 77 | remote: this.options.remote, 78 | }, (err, children) => { 79 | async.forEachSeries(children, (child, callback) => { 80 | if (child.isFolder()) { 81 | return this.trash(child, callback); 82 | } 83 | 84 | this.trashNode(child, (err, result) => { 85 | if (err) { 86 | return callback(err); 87 | } 88 | 89 | if (result.success) { 90 | return callback(); 91 | } 92 | 93 | return callback(Error(JSON.stringify(result))); 94 | }); 95 | }, err => { 96 | if (err) { 97 | return callback(err); 98 | } 99 | 100 | this.trashNode(node, callback); 101 | }); 102 | }); 103 | } 104 | 105 | trashNode(node, callback) { 106 | let retval = { 107 | success: true, 108 | data: {}, 109 | }; 110 | 111 | node.getPath((err, path) => { 112 | Logger.verbose(`Attempting to remove ${node.getKind()} node "${node.getName()}" (${node.getId()})`); 113 | if (node.inTrash()) { 114 | Logger.warn(`Node ${node.getName()} (${node.getId()}) is already in the trash`); 115 | 116 | return callback(null, retval); 117 | } 118 | 119 | return node.trash((err, result) => { 120 | if (err) { 121 | return callback(err); 122 | } 123 | 124 | if (result.success) { 125 | Logger.info(`Trashed node "${path}" (${node.getId()})`); 126 | } else { 127 | Logger.error(`Failed to trash node "${path}" (${node.getId()}): ${JSON.stringify(result)}`); 128 | } 129 | 130 | return callback(null, result); 131 | }); 132 | }); 133 | } 134 | } 135 | 136 | module.exports = TrashCommand; 137 | -------------------------------------------------------------------------------- /lib/Commands/TreeCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'), 6 | Utils = require('../Utils'), 7 | async = require('async'), 8 | chalk = require('chalk'), 9 | inquirer = require('inquirer'); 10 | 11 | class TreeCommand extends Command { 12 | run(args, options) { 13 | return new Promise((resolve, reject) => { 14 | let remotePath = args[0], 15 | searchFunction = Node.loadByPath, 16 | notFound = `No node exists at path '${remotePath}'`; 17 | if (options.id) { 18 | searchFunction = Node.loadById; 19 | notFound = `No node exists with ID '${remotePath}'`; 20 | } 21 | 22 | if (remotePath) { 23 | remotePath = remotePath.trim(); 24 | } 25 | 26 | this.initialize((err, data) => { 27 | if (err) { 28 | return reject(err); 29 | } 30 | 31 | let password = this.config.get('crypto.password'); 32 | async.waterfall([ 33 | callback => { 34 | if (!options.decrypt || !options.password) { 35 | return callback(); 36 | } 37 | 38 | inquirer.prompt([ 39 | { 40 | type: 'password', 41 | name: 'password', 42 | message: 'password: ' 43 | } 44 | ], answers => { 45 | password = answers.password; 46 | callback(); 47 | }); 48 | }, 49 | callback => { 50 | searchFunction(remotePath, (err, node) => { 51 | if (err) { 52 | return callback(err); 53 | } 54 | 55 | if (!node) { 56 | return callback(Error(notFound)); 57 | } 58 | 59 | let opts = { 60 | remote: options.remote, 61 | decrypt: options.decrypt, 62 | password: password, 63 | algorithm: this.config.get('crypto.algorithm'), 64 | 'show-ids': options['show-ids'], 65 | }; 66 | if (options.assets) { 67 | opts.showAssets = true; 68 | } 69 | 70 | let name = node.getName(); 71 | if (node.getLabels().indexOf('enc') !== -1 && options.decrypt) { 72 | name = Utils.decryptString(node.getName(), opts.password, opts.algorithm); 73 | } 74 | 75 | if (options.markdown) { 76 | Logger.info('- ' + name); 77 | 78 | return TreeCommand.buildMarkdownTree(node, ' ', opts, callback); 79 | } 80 | 81 | Logger.info(name); 82 | 83 | return TreeCommand.buildAsciiTree(node, '', opts, callback); 84 | }); 85 | }, 86 | ], err => { 87 | if (err) { 88 | return reject(err); 89 | } 90 | 91 | return resolve(); 92 | }); 93 | }); 94 | }); 95 | } 96 | 97 | static buildAsciiTree(node, prefix, options, callback) { 98 | node.getChildren({ 99 | remote: options.remote, 100 | }, (err, nodes) => { 101 | let counter = 0; 102 | async.forEachSeries(nodes, (node, callback) => { 103 | let itemPrefix = prefix; 104 | 105 | if (counter === nodes.length - 1) { 106 | if (node.isFolder()) { 107 | itemPrefix += '└─┬ '; 108 | } else { 109 | itemPrefix += '└── '; 110 | } 111 | } else { 112 | if (node.isFolder()) { 113 | itemPrefix += '├─┬ '; 114 | } else { 115 | itemPrefix += '├── '; 116 | } 117 | } 118 | 119 | let name = node.getName(); 120 | if (node.getLabels().indexOf('enc') !== -1 && options.decrypt) { 121 | name = Utils.decryptString(node.getName(), options.password, options.algorithm); 122 | } 123 | 124 | if (options['show-ids']) { 125 | name += ` (${node.getId()})`; 126 | } 127 | 128 | if (node.inTrash()) { 129 | Logger.info(`${itemPrefix}${chalk.red(name)}`); 130 | } else if (node.isFolder()) { 131 | Logger.info(`${itemPrefix}${chalk.blue(name)}`); 132 | } else { 133 | Logger.info(`${itemPrefix}${name}`); 134 | } 135 | 136 | counter++; 137 | if (node.isFolder() || options.showAssets) { 138 | return TreeCommand.buildAsciiTree( 139 | node, 140 | prefix + (counter === nodes.length ? ' ' : '| '), 141 | options, 142 | callback 143 | ); 144 | } 145 | 146 | return callback(); 147 | }, err => { 148 | callback(err); 149 | }); 150 | }); 151 | } 152 | 153 | static buildMarkdownTree(node, prefix, options, callback) { 154 | node.getChildren({ 155 | remote: options.remote, 156 | }, (err, nodes) => { 157 | async.forEachSeries(nodes, (node, callback) => { 158 | let name = node.getName(); 159 | if (node.getLabels().indexOf('enc') !== -1 && options.decrypt) { 160 | name = Utils.decryptString(node.getName(), options.password, options.algorithm); 161 | } 162 | 163 | if (options['show-ids']) { 164 | name += ` (${node.getId()})`; 165 | } 166 | 167 | Logger.info(`${prefix}- ${name}`); 168 | if (node.isFolder() || options.showAssets) { 169 | return TreeCommand.buildMarkdownTree(node, `${prefix} `, options, callback); 170 | } 171 | 172 | callback(); 173 | }, err => { 174 | callback(err); 175 | }); 176 | }); 177 | } 178 | } 179 | 180 | module.exports = TreeCommand; 181 | -------------------------------------------------------------------------------- /lib/Commands/UnlinkCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Utils = require('../Utils'), 6 | Logger = require('../Logger'); 7 | 8 | class LinkCommand extends Command { 9 | run(args, options) { 10 | return new Promise((resolve, reject) => { 11 | let id = args[0], 12 | parentPath = args[1], 13 | searchFunction = Node.loadByPath, 14 | notFound = `No directory exists at path '${parentPath}'`; 15 | if (options.id) { 16 | searchFunction = Node.loadById; 17 | notFound = `No directory exists with ID '${parentPath}'`; 18 | } 19 | 20 | if (id) { 21 | id = id.trim(); 22 | } 23 | 24 | if (!parentPath) { 25 | parentPath = ''; 26 | } 27 | 28 | this.initialize((err, data) => { 29 | if (err) { 30 | return reject(err); 31 | } 32 | 33 | Node.loadById(id, (err, node) => { 34 | if (err) { 35 | return reject(err); 36 | } 37 | 38 | if (!node) { 39 | return reject(Error(`No node exists with ID '${id}'`)); 40 | } 41 | 42 | searchFunction(parentPath, (err, parentNode) => { 43 | if (err) { 44 | return reject(err); 45 | } 46 | 47 | if (!parentNode || !parentNode.isFolder()) { 48 | return reject(Error()); 49 | } 50 | 51 | if (!node.getParentIds().includes(parentNode.getId())) { 52 | return reject(Error(`That node does not exist under that folder`)); 53 | } 54 | 55 | if (node.getParentIds().length <= 1) { 56 | return reject(Error(`Cannot unlink Node. Must have 1 remaining parent`)); 57 | } 58 | 59 | node.unlink(parentNode.getId(), (err, data) => { 60 | if (err) { 61 | return reject(err); 62 | } 63 | 64 | if (!data.success) { 65 | return reject(Error(`Failed to unlink node from '${parentPath ? parentPath : '/'}': ${JSON.stringify(data.data)}`)); 66 | } 67 | 68 | Logger.info(`Successfully unlinked node from '${parentPath ? parentPath : '/'}'`); 69 | 70 | return resolve(); 71 | }); 72 | }); 73 | }); 74 | }); 75 | }); 76 | } 77 | } 78 | 79 | module.exports = LinkCommand; 80 | -------------------------------------------------------------------------------- /lib/Commands/UpdateCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Node = require('../Node'), 5 | Logger = require('../Logger'); 6 | 7 | class UpdateCommand extends Command { 8 | run(args, options) { 9 | return new Promise((resolve, reject) => { 10 | let remotePath = args[0]; 11 | 12 | this.initialize((err, data) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | 17 | if (!data.success) { 18 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 19 | } 20 | 21 | let searchFunction = Node.loadByPath, 22 | notFound = `No node exists at path '${remotePath}'`; 23 | if (options.id) { 24 | searchFunction = Node.loadById; 25 | notFound = `No node exists with ID '${remotePath}'`; 26 | } 27 | 28 | if (remotePath) { 29 | remotePath = remotePath.trim(); 30 | } 31 | 32 | searchFunction(remotePath, (err, node) => { 33 | if (err) { 34 | return reject(err); 35 | } 36 | 37 | if (!node) { 38 | return reject(Error(notFound)); 39 | } 40 | 41 | node.update({ 42 | description: options.description, 43 | labels: options.labels, 44 | }, (err, data) => { 45 | if (err) { 46 | return reject(err); 47 | } 48 | 49 | if (!data.success) { 50 | return reject(Error(`Failed to update node metadata`)); 51 | } 52 | 53 | Logger.info(`Successfully updated node metadata`); 54 | 55 | return resolve(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | } 61 | } 62 | 63 | module.exports = UpdateCommand; 64 | -------------------------------------------------------------------------------- /lib/Commands/UploadCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let fs = require('fs'), 4 | Command = require('./Command'), 5 | Node = require('../Node'), 6 | chalk = require('chalk'), 7 | ProgressBar = require('../ProgressBar'), 8 | async = require('async'), 9 | inquirer = require('inquirer'), 10 | Utils = require('../Utils'), 11 | Logger = require('../Logger'); 12 | 13 | class UploadCommand extends Command { 14 | run(args, options) { 15 | return new Promise((resolve, reject) => { 16 | let remotePath = args.pop(); 17 | 18 | this.initialize((err, data) => { 19 | if (err) { 20 | return reject(err); 21 | } 22 | 23 | if (!data.success) { 24 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 25 | } 26 | 27 | if (args.length === 0) { 28 | return reject(Error('Destination path must be specified')); 29 | } 30 | 31 | let bar = null, 32 | localFilesize = null, 33 | bytesUploaded = 0, 34 | bytesTransfered = 0, 35 | lastRun = null, 36 | startTime = null, 37 | opts = { 38 | force: !!options.force, 39 | ignoreFiles: this.config.get('cli.ignoreFiles'), 40 | maxConnections: this.config.get('upload.maxConnections') || 1, 41 | retryAttempt: 0, 42 | numRetries: this.config.get('upload.numRetries'), 43 | suppressDedupe: options.duplicates === true ? true : this.config.get('upload.duplicates'), 44 | checkMd5: options.checksum || this.config.get('upload.checkMd5'), 45 | encrypt: options.encrypt || false, 46 | password: this.config.get('crypto.password'), 47 | algorithm: this.config.get('crypto.algorithm'), 48 | armor: options.armor || this.config.get('crypto.armor'), 49 | labels: options.labels || [], 50 | }; 51 | 52 | if (options.overwrite) { 53 | opts.overwrite = true; 54 | } 55 | 56 | if (options.encrypt) { 57 | if (opts.labels.indexOf('enc') === -1) { 58 | opts.labels.push('enc'); 59 | } 60 | 61 | if (opts.armor && opts.labels.indexOf('armored') === -1) { 62 | opts.labels.push('armored'); 63 | } 64 | } 65 | 66 | async.waterfall([ 67 | callback => { 68 | if (!options.encrypt || !options.password) { 69 | return callback(); 70 | } 71 | 72 | inquirer.prompt([ 73 | { 74 | type: 'password', 75 | name: 'password', 76 | message: 'password: ' 77 | } 78 | ], answers => { 79 | opts.password = answers.password; 80 | callback(); 81 | }); 82 | }, 83 | ], err => { 84 | if (err) { 85 | return reject(err); 86 | } 87 | 88 | Node.on('fileUpload', localPath => { 89 | if (this.config.get('cli.progressBars') && opts.maxConnections === 1) { 90 | startTime = Date.now(); 91 | bytesUploaded = 0; 92 | localFilesize = fs.statSync(localPath).size; 93 | lastRun = Date.now(); 94 | bar = new ProgressBar(`Uploading '${localPath}'\n:percent [:bar] :speed eta :etas (:uploaded / :filesize)`, { 95 | total: localFilesize, 96 | incomplete: ' ', 97 | width: 40, 98 | clear: false, 99 | renderThrottle: this.config.get('cli.progressInterval') 100 | }); 101 | } 102 | }); 103 | 104 | Node.on('uploadProgress', (localPath, chunk) => { 105 | if (bar) { 106 | bytesUploaded += chunk.length; 107 | bytesTransfered += chunk.length; 108 | 109 | let timeDiff = Date.now() - lastRun; 110 | 111 | if (timeDiff >= this.config.get('cli.progressInterval') || bytesUploaded >= localFilesize) { 112 | lastRun = Date.now(); 113 | bar.tick(bytesTransfered, { 114 | speed: `${Utils.convertFileSize(Math.round(bytesTransfered / (timeDiff / 1000)), 2)}/s`, 115 | uploaded: Utils.convertFileSize(bytesUploaded), 116 | filesize: Utils.convertFileSize(localFilesize), 117 | }); 118 | bytesTransfered = 0; 119 | } 120 | } 121 | }); 122 | 123 | Node.on('uploadComplete', (response, body, retval, data) => { 124 | // Clear out progress bar 125 | if (bar !== null) { 126 | bar.clear(); 127 | bar = null; 128 | localFilesize = null; 129 | } 130 | 131 | if (response) { 132 | if (!body) { 133 | return Logger.error(`Failed to upload file '${data.localPath}'. Invalid body returned: ${body}`); 134 | } 135 | } 136 | 137 | if (retval.success) { 138 | Logger.info(`Successfully uploaded file '${data.localPath}' to '${data.remotePath}'`); 139 | if (options['remove-source-files']) { 140 | Logger.verbose(`Attempting to remove local file '${data.localPath}'`); 141 | try { 142 | fs.unlinkSync(data.localPath); 143 | Logger.verbose(`Successfully removed local file '${data.localPath}'`); 144 | } catch (e) { 145 | Logger.error(`Failed to remove source file '${data.localPath}': ${e}`); 146 | } 147 | } 148 | } else { 149 | let message = `Failed to upload file '${data.localPath}'`; 150 | 151 | if (retval.data.message) { 152 | message += `: ${retval.data.message}`; 153 | } else { 154 | message += `: ${JSON.stringify(retval.data)}`; 155 | } 156 | 157 | if (retval.data.exists === true) { 158 | if ((retval.data.md5Match === true || retval.data.sizeMatch === true) && retval.data.pathMatch === true) { 159 | Logger.warn(message); 160 | if (options['remove-source-files']) { 161 | Logger.verbose(`Attempting to remove local file '${data.localPath}'`); 162 | try { 163 | fs.unlinkSync(data.localPath); 164 | Logger.verbose(`Successfully removed local file '${data.localPath}'`); 165 | } catch (e) { 166 | Logger.error(`Failed to remove source file '${data.localPath}': ${e}`); 167 | } 168 | } 169 | } else { 170 | Logger.error(message); 171 | } 172 | } else { 173 | if (retval.data.retry !== undefined && retval.data.retry === true) { 174 | Logger.warn(message); 175 | } else { 176 | Logger.error(message); 177 | } 178 | } 179 | } 180 | }); 181 | 182 | Logger.debug(`Beginning upload...`); 183 | async.forEachSeries(args, (localPath, callback) => { 184 | try { 185 | fs.statSync(localPath); 186 | } catch (e) { 187 | return reject(Error(`No file exists at '${localPath}'`)); 188 | } 189 | 190 | if (fs.lstatSync(localPath).isDirectory()) { 191 | Logger.debug(`Local path '${localPath}' is a directory. Uploading recursively...`); 192 | return Node.uploadDirectory(localPath, remotePath, opts, (err, data) => { 193 | if (err) { 194 | return reject(err); 195 | } 196 | 197 | callback(); 198 | }); 199 | } 200 | 201 | Logger.debug(`Preparing to upload file '${localPath}'...`); 202 | Node.uploadFile(localPath, remotePath, opts, (err, data) => { 203 | if (err) { 204 | return reject(err); 205 | } 206 | 207 | callback(); 208 | }); 209 | }, err => { 210 | if (err) { 211 | return reject(err); 212 | } 213 | 214 | return resolve(); 215 | }); 216 | }); 217 | }); 218 | }); 219 | } 220 | } 221 | 222 | module.exports = UploadCommand; 223 | -------------------------------------------------------------------------------- /lib/Commands/UsageCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Command = require('./Command'), 4 | Logger = require('../Logger'); 5 | 6 | class UsageCommand extends Command { 7 | run(args, options) { 8 | return new Promise((resolve, reject) => { 9 | this.initialize((err, data) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | 14 | if (!data.success) { 15 | return reject(Error('Account not authorized with Amazon Cloud Drive. Run `init` command first.')); 16 | } 17 | 18 | this.account.getUsage((err, data) => { 19 | if (err) { 20 | return reject(err); 21 | } 22 | 23 | if (this.config.get('json.pretty') === true) { 24 | let output = JSON.stringify(data.data, null, 2); 25 | output.split('\n').forEach(line => { 26 | Logger.info(line); 27 | }); 28 | } else { 29 | Logger.info(JSON.stringify(data.data)); 30 | } 31 | 32 | return resolve(); 33 | }); 34 | }); 35 | }); 36 | } 37 | } 38 | 39 | module.exports = UsageCommand; 40 | -------------------------------------------------------------------------------- /lib/Config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let ParameterBag = require('./ParameterBag'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | defaultConfig = {}; 7 | 8 | class Config extends ParameterBag { 9 | constructor(filePath, baseConfig = {}) { 10 | defaultConfig = baseConfig; 11 | 12 | let config = {}; 13 | try { 14 | fs.statSync(filePath); 15 | config = JSON.parse(fs.readFileSync(filePath)); 16 | } catch (e) {} 17 | 18 | config = new ParameterBag(config); 19 | 20 | for (let key in defaultConfig) { 21 | let val = defaultConfig[key].default, 22 | savedValue = config.get(key); 23 | if (savedValue !== null) { 24 | val = savedValue; 25 | } 26 | 27 | config[key] = val; 28 | } 29 | 30 | super(config); 31 | this.filePath = filePath; 32 | } 33 | 34 | reset(key) { 35 | if (defaultConfig[key] !== undefined) { 36 | this.set(key, defaultConfig[key].default); 37 | } 38 | } 39 | 40 | save() { 41 | fs.writeFileSync(this.filePath, JSON.stringify(this.getData())); 42 | } 43 | 44 | set(key, value) { 45 | if (!defaultConfig[key]) { 46 | return; 47 | } 48 | 49 | switch (defaultConfig[key].type) { 50 | case 'bool': 51 | if (typeof value === 'boolean') { 52 | break; 53 | } 54 | 55 | if (value === true || value === 'true' || value === 1 || value === '1') { 56 | value = true; 57 | break; 58 | } 59 | value = false; 60 | break; 61 | case 'choice': 62 | if (defaultConfig[key].choices.indexOf(value) === -1) { 63 | throw Error(`Invalid value '${value}'. Must be one of the following: ${defaultConfig[key].choices.join(', ')}`); 64 | } 65 | break; 66 | default: 67 | break; 68 | } 69 | 70 | return super.set(key, value); 71 | } 72 | } 73 | 74 | module.exports = Config; 75 | -------------------------------------------------------------------------------- /lib/Logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let chalk = require('chalk'), 4 | winston = require('winston'), 5 | moment = require('moment'), 6 | logUpdate = require('log-update'), 7 | Utils = require('./Utils'), 8 | instance = null, 9 | levels = { 10 | error: 0, 11 | warn: 1, 12 | info: 2, 13 | verbose: 3, 14 | debug: 4, 15 | silly: 5, 16 | }; 17 | 18 | class Logger { 19 | static getInstance(config = {verbosity: 'error'}) { 20 | if (instance === null) { 21 | if (config.cliTimestamp === true) { 22 | config.cliTimestamp = Logger.timestamp; 23 | } 24 | let showLevels = levels[config.verbosity] <= levels.info ? false : true, 25 | transports = [ 26 | new (winston.transports.Console)({ 27 | level: config.verbosity, 28 | colorize: config.colorize, 29 | align: showLevels, 30 | handleExceptions: true, 31 | timestamp: config.cliTimestamp, 32 | showLevel: showLevels, 33 | }), 34 | ]; 35 | 36 | if (config.file) { 37 | transports.push(new (winston.transports.File)({ 38 | filename: config.file, 39 | level: config.logLevel, 40 | align: true, 41 | timestamp: Logger.timestamp, 42 | json: false, 43 | handleExceptions: true, 44 | colorize: false, 45 | })); 46 | } 47 | 48 | instance = new (winston.Logger)({ 49 | transports: transports, 50 | }); 51 | } 52 | 53 | return instance; 54 | } 55 | 56 | static flushAndExit(code) { 57 | return process.exit(code); 58 | // if (!Logger.getInstance().transports.file) { 59 | // return process.exit(code); 60 | // } 61 | // 62 | // return Logger.getInstance().transports.file.on('flush', () => { 63 | // process.exit(code); 64 | // }); 65 | } 66 | 67 | static getLogLevel() { 68 | return Logger.getInstance().transports.file.level; 69 | } 70 | 71 | static getOutputLevel() { 72 | return Logger.getInstance().transports.console.level; 73 | } 74 | 75 | static info(message, data = null, callback = null) { 76 | logUpdate.clear(); 77 | Logger.getInstance().info(message, data, callback); 78 | logUpdate.done(); 79 | } 80 | 81 | static error(message, data = null, callback = null) { 82 | logUpdate.clear(); 83 | Logger.getInstance().error(message, data, callback); 84 | logUpdate.done(); 85 | } 86 | 87 | static warn(message, data = null, callback = null) { 88 | logUpdate.clear(); 89 | Logger.getInstance().warn(message, data, callback); 90 | logUpdate.done(); 91 | } 92 | 93 | static verbose(message, data = null, callback = null) { 94 | logUpdate.clear(); 95 | Logger.getInstance().verbose(message, data, callback); 96 | logUpdate.done(); 97 | } 98 | 99 | static debug(message, data = null, callback = null) { 100 | logUpdate.clear(); 101 | Logger.getInstance().debug(message, data, callback); 102 | logUpdate.done(); 103 | } 104 | 105 | static silly(message, data = null, callback = null) { 106 | logUpdate.clear(); 107 | Logger.getInstance().silly(message, data, callback); 108 | logUpdate.done(); 109 | } 110 | 111 | static setConsoleLevel(level) { 112 | if (Logger.getInstance().transports.console) { 113 | let showLevel = levels[level] <= levels.info ? false : true; 114 | Logger.getInstance().transports.console.level = level; 115 | Logger.getInstance().transports.console.showLevel = showLevel; 116 | Logger.getInstance().transports.console.align = showLevel; 117 | } 118 | } 119 | 120 | static setFileLevel(level) { 121 | if (Logger.getInstance().transports.file) { 122 | Logger.getInstance().transports.file.level = level; 123 | } 124 | } 125 | 126 | static timestamp() { 127 | return moment().format('YYYY-MM-DD HH:mm:ss'); 128 | } 129 | } 130 | 131 | module.exports = Logger; 132 | -------------------------------------------------------------------------------- /lib/ParameterBag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let events = require('events'), 4 | staticEvents = new events.EventEmitter(); 5 | 6 | class ParameterBag extends events.EventEmitter { 7 | constructor(data) { 8 | super(); 9 | this.replace(data); 10 | } 11 | 12 | flatten() { 13 | let flatten = (data, prefix = '') => { 14 | let retval = {}; 15 | for (let i in data) { 16 | if (typeof data[i] === 'object') { 17 | let nestedObj = flatten(data[i], `${prefix}${i}.`); 18 | for (let j in nestedObj) { 19 | retval[j] = nestedObj[j]; 20 | } 21 | } else { 22 | retval['' + prefix + i] = data[i]; 23 | } 24 | } 25 | 26 | return retval; 27 | }; 28 | 29 | return flatten(this.data); 30 | } 31 | 32 | get(path, defaultValue = null) { 33 | let retval = this.data; 34 | 35 | path = path.split('.'); 36 | for (let i = 0; i < path.length; i++) { 37 | if (retval[path[i]] === undefined) { 38 | return defaultValue; 39 | } 40 | 41 | retval = retval[path[i]]; 42 | } 43 | 44 | return retval; 45 | } 46 | 47 | getData() { 48 | return this.data; 49 | } 50 | 51 | replace(data) { 52 | this.data = {}; 53 | for (let i in data) { 54 | this.set(i, data[i]); 55 | } 56 | } 57 | 58 | set(key, value) { 59 | let keys = key.split('.'), 60 | xary = this.data; 61 | while (key = keys.shift()) { 62 | if (keys.length === 0) { 63 | xary[key] = value; 64 | break; 65 | } 66 | 67 | if (xary[key] === undefined) { 68 | xary[key] = {}; 69 | } 70 | 71 | xary = xary[key]; 72 | } 73 | } 74 | 75 | static on(...args) { 76 | staticEvents.on(...args); 77 | } 78 | 79 | static emit(...args) { 80 | staticEvents.emit(...args); 81 | } 82 | } 83 | 84 | module.exports = ParameterBag; 85 | -------------------------------------------------------------------------------- /lib/ProgressBar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let logUpdate = require('log-update'); 4 | 5 | /** 6 | * Initialize a `ProgressBar` with the given `format` string and `options` or 7 | * `total`. 8 | * 9 | * Options: 10 | * 11 | * - `total` total number of ticks to complete 12 | * - `width` the displayed width of the progress bar defaulting to total 13 | * - `stream` the output stream defaulting to stderr 14 | * - `complete` completion character defaulting to "=" 15 | * - `incomplete` incomplete character defaulting to "-" 16 | * - `renderThrottle` minimum time between updates in milliseconds defaulting to 16 17 | * - `callback` optional function to call when the progress bar completes 18 | * - `clear` will clear the progress bar upon termination 19 | * 20 | * Tokens: 21 | * 22 | * - `:bar` the progress bar itself 23 | * - `:current` current tick number 24 | * - `:total` total ticks 25 | * - `:elapsed` time elapsed in seconds 26 | * - `:percent` completion percentage 27 | * - `:eta` eta in seconds 28 | * 29 | * @param {string} fmt 30 | * @param {object|number} options or total 31 | * @api public 32 | */ 33 | class ProgressBar { 34 | constructor(format, options = {}) { 35 | let config = { 36 | format: format, 37 | total: options.total, 38 | width: options.width || options.total, 39 | chars: { 40 | complete: options.complete || '=', 41 | incomplete: options.incomplete || '-', 42 | }, 43 | renderThrottle: options.renderThrottle !== 0 ? (options.renderThrottle || 16) : 0, 44 | current: 0, 45 | output: options.output !== undefined ? options.output : true, 46 | clear: options.clear !== undefined ? options.clear : true, 47 | stream: options.stream || process.stderr, 48 | callback: options.callback, 49 | tokens: {}, 50 | lastDraw: '', 51 | complete: false, 52 | }; 53 | 54 | this.options = {}; 55 | for (let option in config) { 56 | this.options[option] = config[option]; 57 | } 58 | } 59 | 60 | getOutput() { 61 | return this.options.lastDraw; 62 | } 63 | 64 | tick(length = 1, tokens = {}) { 65 | if (typeof length === 'object') { 66 | tokens = length; 67 | length = 1; 68 | } 69 | 70 | this.options.tokens = tokens; 71 | if (!this.options.current) { 72 | logUpdate.done(); 73 | this.options.start = new Date(); 74 | } 75 | 76 | this.options.current += length; 77 | 78 | // schedule render 79 | if (!this.options.renderThrottleTimeout) { 80 | this.options.renderThrottleTimeout = setTimeout(this.render.bind(this), this.options.renderThrottle); 81 | } 82 | 83 | // progress complete 84 | if (this.options.current >= this.options.total) { 85 | if (this.options.renderThrottleTimeout) { 86 | this.render(); 87 | } 88 | this.options.complete = true; 89 | if (this.options.clear) { 90 | this.clear(); 91 | } 92 | if (this.options.callback) { 93 | this.options.callback(this); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Method to render the progress bar with optional `tokens` to place in the 100 | * progress bar's `fmt` field. 101 | * 102 | * @param {object} tokens 103 | * @api public 104 | */ 105 | render(tokens = null) { 106 | clearTimeout(this.options.renderThrottleTimeout); 107 | this.options.renderThrottleTimeout = null; 108 | 109 | this.options.tokens = tokens || this.options.tokens; 110 | 111 | let ratio = this.options.current / this.options.total; 112 | ratio = Math.min(Math.max(ratio, 0), 1); 113 | 114 | let percent = ratio * 100, 115 | incomplete, complete, completeLength, 116 | elapsed = new Date() - this.options.start, 117 | eta = percent === 100 ? 0 : elapsed * (this.options.total / this.options.current - 1); 118 | 119 | let str = this.options.format 120 | .replace(':current', this.options.current) 121 | .replace(':total', this.options.total) 122 | .replace(':elapsed', isNaN(elapsed) ? '0.0' : (elapsed / 1000).toFixed(1)) 123 | .replace(':eta', (isNaN(eta) || !isFinite(eta)) ? '0.0' : (eta / 1000).toFixed(1)) 124 | .replace(':percent', percent.toFixed(0) + '%'); 125 | 126 | /* compute the available space (non-zero) for the bar */ 127 | let availableSpace = Math.max(0, this.options.stream.columns - str.replace(':bar', '').length), 128 | width = Math.min(this.options.width, availableSpace); 129 | 130 | /* TODO: the following assumes the user has one ':bar' token */ 131 | completeLength = Math.round(width * ratio); 132 | complete = Array(completeLength + 1).join(this.options.chars.complete); 133 | incomplete = Array(width - completeLength + 1).join(this.options.chars.incomplete); 134 | 135 | /* fill in the actual progress bar */ 136 | str = str.replace(':bar', complete + incomplete); 137 | 138 | /* replace the extra tokens */ 139 | if (this.options.tokens) for (var key in this.options.tokens) str = str.replace(':' + key, this.options.tokens[key]); 140 | 141 | if (this.options.lastDraw !== str) { 142 | if (this.options.output) { 143 | logUpdate(str); 144 | } 145 | this.options.lastDraw = str; 146 | } 147 | } 148 | 149 | /** 150 | * "update" the progress bar to represent an exact percentage. 151 | * The ratio (between 0 and 1) specified will be multiplied by `total` and 152 | * floored, representing the closest available "tick." For example, if a 153 | * progress bar has a length of 3 and `update(0.5)` is called, the progress 154 | * will be set to 1. 155 | * 156 | * A ratio of 0.5 will attempt to set the progress to halfway. 157 | * 158 | * @param {number} ratio The ratio (between 0 and 1 inclusive) to set the 159 | * overall completion to. 160 | * @api public 161 | */ 162 | update(ratio, tokens) { 163 | let goal = Math.floor(ratio * this.options.total), 164 | delta = goal - this.options.current; 165 | 166 | this.tick(delta, tokens); 167 | } 168 | 169 | clear() { 170 | logUpdate.clear(); 171 | } 172 | } 173 | 174 | module.exports = ProgressBar; 175 | -------------------------------------------------------------------------------- /lib/Utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let fs = require('fs'), 4 | crypto = require('crypto'); 5 | 6 | class Utils { 7 | static arrayContains(xary, value) { 8 | return xary.indexOf(value) !== -1; 9 | } 10 | 11 | static arrayUnique(xary) { 12 | xary = xary.sort(function(a, b) { 13 | return a * 1 - b * 1; 14 | }); 15 | 16 | let retval = [xary[0]]; 17 | // Start loop at 1 as element 0 can never be a duplicate 18 | for (var i = 1; i < xary.length; i++) { 19 | if (xary[i - 1] !== xary[i]) { 20 | retval.push(xary[i]); 21 | } 22 | } 23 | 24 | return retval; 25 | } 26 | 27 | static convertFileSize(bytes, decimals = 2) { 28 | let size = bytes ? bytes : 0, 29 | sizes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 30 | factor = Math.floor((size.toString().length - 1) / 3); 31 | 32 | size = size / Math.pow(1024, factor); 33 | 34 | return parseFloat(size.toFixed(decimals)) + sizes[factor]; 35 | } 36 | 37 | static decryptString(string, password, algorithm) { 38 | let decipher = crypto.createDecipher(algorithm, password), 39 | retval = decipher.update(string, 'hex', 'utf8'); 40 | retval += decipher.final('utf8'); 41 | 42 | return retval; 43 | } 44 | 45 | static encryptString(string, password, algorithm) { 46 | let cipher = crypto.createCipher(algorithm, password), 47 | retval = cipher.update(string, 'utf8', 'hex'); 48 | retval += cipher.final('hex'); 49 | 50 | return retval; 51 | } 52 | 53 | static getFileMd5(filepath, callback) { 54 | let sum = crypto.createHash('md5'); 55 | 56 | if (callback && typeof callback === 'function') { 57 | let fileStream = fs.createReadStream(filepath); 58 | 59 | fileStream.on('error', err => { 60 | return callback(err, null); 61 | }); 62 | 63 | fileStream.on('data', chunk => { 64 | try { 65 | sum.update(chunk); 66 | } catch (ex) { 67 | return callback(ex, null); 68 | } 69 | }); 70 | 71 | fileStream.on('end', () => { 72 | return callback(null, sum.digest('hex')); 73 | }); 74 | } else { 75 | sum.update(fs.readFileSync(filepath)); 76 | 77 | return sum.digest('hex'); 78 | } 79 | } 80 | 81 | static getPathArray(path) { 82 | let retval = []; 83 | path = path.split('/'); 84 | for (let i = 0; i < path.length; i++) { 85 | if (path[i]) { 86 | retval.push(path[i]); 87 | } 88 | } 89 | 90 | return retval; 91 | } 92 | 93 | static pad(string, length, side) { 94 | if (!side) { 95 | side = 'right'; 96 | } 97 | 98 | switch (side) { 99 | case 'left': 100 | return (string.toString().length < length) ? Utils.pad(` ${string}`, length, side) : string; 101 | default: 102 | return (string.toString().length < length) ? Utils.pad(`${string} `, length, side) : string; 103 | } 104 | } 105 | 106 | static roundNumber(val, precision) { 107 | let factor = Math.pow(10, precision), 108 | tempNumber = val * factor, 109 | roundedTempNumber = Math.round(tempNumber); 110 | 111 | return roundedTempNumber / factor; 112 | } 113 | 114 | static trimPath(string) { 115 | while (string.charAt(0) === '/') { 116 | string = string.substring(1); 117 | } 118 | 119 | while (string.charAt(string.length - 1) === '/') { 120 | string = string.substring(0, string.length - 1); 121 | } 122 | 123 | return string; 124 | } 125 | } 126 | 127 | module.exports = Utils; 128 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let semver = require('semver'), 4 | pkgJson = require('../package.json'), 5 | chalk = require('chalk'), 6 | yargs = require('yargs'), 7 | Command = require('./Commands/Command'), 8 | Config = require('./Config'), 9 | Logger = require('./Logger'), 10 | async = require('async'), 11 | crypto = require('crypto'), 12 | banner = ` ________ __ ____ _ 13 | / ____/ /___ __ ______/ / / __ \\_____(_) _____ 14 | / / / / __ \\/ / / / __ / / / / / ___/ / | / / _ \\ 15 | / /___/ / /_/ / /_/ / /_/ / / /_/ / / / /| |/ / __/ 16 | \\____/_/\\____/\\__,_/\\__,_/ /_____/_/ /_/ |___/\\___/ 17 | `; 18 | 19 | // check that we're using Node.js 0.12 or newer 20 | try { 21 | if (semver.lt(process.version, '0.12.0')) { 22 | console.error(`${chalk.cyan.bold(pkgJson.name)}, CLI version ${pkgJson.version} 23 | ${chalk.white.bgRed(`ERROR: ${pkgJson.name} requires Node.js 0.12 or newer`)} 24 | Visit ${chalk.cyan('http://nodejs.org/')} to download a newer version.`); 25 | 26 | process.exit(1); 27 | } 28 | } catch (e) {} 29 | 30 | Command.setAppName('clouddrive'); 31 | 32 | let cliConfig = { 33 | commands: { 34 | 'about': { 35 | offline: true, 36 | usage: '', 37 | desc: 'Print app-specific information', 38 | options: {}, 39 | file: './Commands/AboutCommand', 40 | }, 41 | 'cat': { 42 | offline: false, 43 | usage: '[flags] ', 44 | demand: 1, 45 | desc: 'Print files to STDOUT', 46 | options: { 47 | i: { 48 | group: 'Flags:', 49 | alias: 'id', 50 | demand: false, 51 | desc: 'Specify the remote node by its ID instead of path', 52 | type: 'boolean', 53 | }, 54 | decrypt: { 55 | group: 'Flags:', 56 | demand: false, 57 | desc: 'Decrypt files when downloading', 58 | type: 'boolean', 59 | }, 60 | armor: { 61 | group: 'Flags:', 62 | demand: false, 63 | desc: 'Decrypt ASCII data - default is binary data', 64 | type: 'boolean', 65 | }, 66 | password: { 67 | group: 'Flags:', 68 | alias: 'p', 69 | demand: false, 70 | desc: 'Prompt for decryption password', 71 | type: 'boolean', 72 | }, 73 | }, 74 | file: './Commands/CatCommand', 75 | }, 76 | 'clearcache': { 77 | offline: true, 78 | usage: '', 79 | desc: 'Clear the local cache', 80 | options: {}, 81 | file: './Commands/ClearCacheCommand', 82 | }, 83 | 'config': { 84 | offline: true, 85 | usage: '[flags] [key] [value]', 86 | desc: 'Read, write, and reset config values', 87 | options: { 88 | r: { 89 | group: 'Flags:', 90 | alias: 'reset', 91 | demand: false, 92 | desc: 'Reset the config option to its default value', 93 | type: 'boolean', 94 | }, 95 | }, 96 | file: './Commands/ConfigCommand', 97 | }, 98 | 'encrypt': { 99 | offline: true, 100 | usage: '', 101 | demand: 1, 102 | desc: 'Encrypt a file', 103 | options: { 104 | password: { 105 | group: 'Flags:', 106 | alias: 'p', 107 | demand: false, 108 | desc: 'Prompt for decryption password', 109 | type: 'boolean', 110 | }, 111 | armor: { 112 | group: 'Flags:', 113 | demand: false, 114 | desc: 'Create ASCII output - default is a binary format', 115 | type: 'boolean', 116 | }, 117 | }, 118 | file: './Commands/EncryptCommand', 119 | }, 120 | 'decrypt': { 121 | offline: true, 122 | usage: '', 123 | demand: 1, 124 | desc: 'Decrypt a file', 125 | options: { 126 | password: { 127 | group: 'Flags:', 128 | alias: 'p', 129 | demand: false, 130 | desc: 'Prompt for decryption password', 131 | type: 'boolean', 132 | }, 133 | armor: { 134 | group: 'Flags:', 135 | demand: false, 136 | desc: 'Decrypt ASCII data - default is binary data', 137 | type: 'boolean', 138 | }, 139 | }, 140 | file: './Commands/DecryptCommand', 141 | }, 142 | 'delete-everything': { 143 | offline: true, 144 | usage: '', 145 | desc: 'Remove all files and folders related to the CLI', 146 | options: {}, 147 | file: './Commands/DeleteEverythingCommand', 148 | }, 149 | 'download': { 150 | offline: false, 151 | usage: '[flags] [options] [dest]', 152 | demand: 1, 153 | desc: 'Download remote file or folder to specified local path', 154 | options: { 155 | i: { 156 | group: 'Flags:', 157 | alias: 'id', 158 | demand: false, 159 | desc: 'Specify the remote node by its ID instead of path', 160 | type: 'boolean', 161 | }, 162 | decrypt: { 163 | group: 'Flags:', 164 | demand: false, 165 | desc: 'Decrypt files when downloading', 166 | type: 'boolean', 167 | }, 168 | armor: { 169 | group: 'Flags:', 170 | demand: false, 171 | desc: 'Decrypt ASCII data - default is binary data', 172 | type: 'boolean', 173 | }, 174 | password: { 175 | group: 'Flags:', 176 | alias: 'p', 177 | demand: false, 178 | desc: 'Prompt for decryption password', 179 | type: 'boolean', 180 | }, 181 | remote: { 182 | group: 'Flags:', 183 | demand: false, 184 | desc: 'Force the command to fetch from the API', 185 | type: 'boolean', 186 | }, 187 | s: { 188 | group: 'Options:', 189 | alias: 'size', 190 | demand: false, 191 | desc: 'Maximum width or height (if image)', 192 | type: 'string', 193 | }, 194 | }, 195 | file: './Commands/DownloadCommand', 196 | }, 197 | 'du': { 198 | offline: true, 199 | usage: '[flags] [path]', 200 | desc: 'Display the disk usage (recursively) for the specified node', 201 | options: { 202 | i: { 203 | group: 'Flags:', 204 | alias: 'id', 205 | demand: false, 206 | desc: 'Specify the remote node by its ID instead of path', 207 | type: 'boolean', 208 | }, 209 | remote: { 210 | group: 'Flags:', 211 | demand: false, 212 | desc: 'Force the command to fetch from the API', 213 | type: 'boolean', 214 | }, 215 | }, 216 | file: './Commands/DiskUsageCommand', 217 | }, 218 | 'exists': { 219 | offline: true, 220 | usage: '[flags] ', 221 | demand: 2, 222 | desc: 'Check if a file or folder exists remotely', 223 | options: {}, 224 | file: './Commands/ExistsCommand', 225 | }, 226 | 'find': { 227 | offline: true, 228 | usage: '[flags] ', 229 | demand: 1, 230 | desc: 'Search for nodes by name', 231 | options: { 232 | t: { 233 | group: 'Flags:', 234 | alias: 'time', 235 | demand: false, 236 | desc: 'Sort nodes by modified time', 237 | type: 'boolean', 238 | }, 239 | }, 240 | file: './Commands/FindCommand', 241 | }, 242 | 'info': { 243 | offline: false, 244 | usage: '', 245 | desc: 'Show Cloud Drive account info', 246 | options: {}, 247 | file: './Commands/InfoCommand', 248 | }, 249 | 'init': { 250 | offline: false, 251 | usage: '', 252 | desc: 'Initialize and authorize with Amazon Cloud Drive', 253 | options: {}, 254 | file: './Commands/InitCommand', 255 | }, 256 | 'link': { 257 | offline: false, 258 | usage: '[flags] ', 259 | demand: 1, 260 | desc: 'Link a file to exist under another directory', 261 | options: { 262 | i: { 263 | group: 'Flags:', 264 | alias: 'id', 265 | demand: false, 266 | desc: 'Specify the remote node by its ID instead of path', 267 | type: 'boolean', 268 | }, 269 | }, 270 | file: './Commands/LinkCommand', 271 | }, 272 | 'ls': { 273 | offline: true, 274 | usage: '[flags] [path]', 275 | desc: 'List all remote nodes belonging to a specified node', 276 | options: { 277 | i: { 278 | group: 'Flags:', 279 | alias: 'id', 280 | demand: false, 281 | desc: 'Specify the remote node by its ID instead of path', 282 | type: 'boolean', 283 | }, 284 | t: { 285 | group: 'Flags:', 286 | alias: 'time', 287 | demand: false, 288 | desc: 'Sort nodes by modified time', 289 | type: 'boolean', 290 | }, 291 | decrypt: { 292 | group: 'Flags:', 293 | demand: false, 294 | desc: 'Decrypt files when downloading', 295 | type: 'boolean', 296 | }, 297 | password: { 298 | group: 'Flags:', 299 | alias: 'p', 300 | demand: false, 301 | desc: 'Prompt for decryption password', 302 | type: 'boolean', 303 | }, 304 | remote: { 305 | group: 'Flags:', 306 | demand: false, 307 | desc: 'Force the command to fetch from the API', 308 | type: 'boolean', 309 | }, 310 | }, 311 | file: './Commands/ListCommand', 312 | }, 313 | 'metadata': { 314 | offline: true, 315 | usage: '[flags] [path]', 316 | desc: 'Retrieve metadata of a node by its path', 317 | options: { 318 | i: { 319 | group: 'Flags:', 320 | alias: 'id', 321 | demand: false, 322 | desc: 'Specify the remote node by its ID instead of path', 323 | type: 'boolean', 324 | }, 325 | }, 326 | file: './Commands/MetadataCommand', 327 | }, 328 | 'mkdir': { 329 | offline: false, 330 | usage: '', 331 | demand: 1, 332 | desc: 'Create a remote directory path (recursively)', 333 | options: {}, 334 | file: './Commands/MkdirCommand', 335 | }, 336 | 'mv': { 337 | offline: false, 338 | usage: '[flags] ', 339 | demand: 1, 340 | desc: 'Move a remote node to a new directory', 341 | options: { 342 | i: { 343 | group: 'Flags:', 344 | alias: 'id', 345 | demand: false, 346 | desc: 'Specify the remote node by its ID instead of path', 347 | type: 'boolean', 348 | }, 349 | }, 350 | file: './Commands/MoveCommand', 351 | }, 352 | 'pending': { 353 | offline: true, 354 | usage: '[flags]', 355 | desc: 'List the nodes that have a status of "PENDING"', 356 | options: { 357 | t: { 358 | group: 'Flags:', 359 | alias: 'time', 360 | demand: false, 361 | desc: 'Sort nodes by modified time', 362 | type: 'boolean', 363 | }, 364 | }, 365 | file: './Commands/ListPendingCommand', 366 | }, 367 | 'quota': { 368 | offline: false, 369 | usage: '', 370 | desc: 'Show Cloud Drive account quota', 371 | options: {}, 372 | file: './Commands/QuotaCommand', 373 | }, 374 | 'rename': { 375 | offline: false, 376 | usage: '[flags] ', 377 | demand: 2, 378 | desc: 'Rename a remote node', 379 | options: { 380 | i: { 381 | group: 'Flags:', 382 | alias: 'id', 383 | demand: false, 384 | desc: 'Specify the remote node by its ID instead of path', 385 | type: 'boolean', 386 | }, 387 | }, 388 | file: './Commands/RenameCommand', 389 | }, 390 | 'resolve': { 391 | offline: true, 392 | usage: '', 393 | demand: 1, 394 | desc: 'Return the remote path of a node by its ID', 395 | options: {}, 396 | file: './Commands/ResolveCommand', 397 | }, 398 | 'restore': { 399 | offline: false, 400 | usage: '[flags] ', 401 | demand: 1, 402 | desc: 'Restore a remote node from the trash', 403 | options: { 404 | i: { 405 | group: 'Flags:', 406 | alias: 'id', 407 | demand: false, 408 | desc: 'Specify the remote node by its ID instead of path', 409 | type: 'boolean', 410 | }, 411 | r: { 412 | group: 'Flags:', 413 | alias: 'recursive', 414 | demand: false, 415 | desc: 'Recursively restore nodes', 416 | type: 'boolean', 417 | }, 418 | remote: { 419 | group: 'Flags:', 420 | demand: false, 421 | desc: 'Force a remote to be restored by its ID', 422 | type: 'booelan', 423 | }, 424 | }, 425 | file: './Commands/RestoreCommand', 426 | }, 427 | 'rm': { 428 | offline: false, 429 | usage: '[flags] ', 430 | demand: 1, 431 | desc: 'Move a remote Node to the trash', 432 | options: { 433 | i: { 434 | group: 'Flags:', 435 | alias: 'id', 436 | demand: false, 437 | desc: 'Specify the remote node by its ID instead of path', 438 | type: 'boolean', 439 | }, 440 | r: { 441 | group: 'Flags:', 442 | alias: 'recursive', 443 | demand: false, 444 | desc: 'Recursively delete nodes', 445 | type: 'boolean', 446 | }, 447 | remote: { 448 | group: 'Flags:', 449 | demand: false, 450 | desc: 'Gather the child nodes via API call (instead of local cache) to deleting', 451 | type: 'boolean', 452 | }, 453 | }, 454 | file: './Commands/TrashCommand', 455 | }, 456 | 'share': { 457 | offline: false, 458 | usage: '[flags] ', 459 | demand: 1, 460 | desc: 'Generate a temporary, pre-authenticated download link', 461 | options: { 462 | i: { 463 | group: 'Flags:', 464 | alias: 'id', 465 | demand: false, 466 | desc: 'Specify the remote node by its ID instead of path', 467 | type: 'boolean', 468 | }, 469 | }, 470 | file: './Commands/ShareCommand', 471 | }, 472 | 'sync': { 473 | offline: false, 474 | usage: '', 475 | desc: 'Sync the local cache with Amazon Cloud Drive', 476 | options: {}, 477 | file: './Commands/SyncCommand', 478 | }, 479 | 'trash': { 480 | offline: true, 481 | usage: '[flags]', 482 | desc: 'List the nodes that have a status of "TRASH"', 483 | options: { 484 | t: { 485 | group: 'Flags:', 486 | alias: 'time', 487 | demand: false, 488 | desc: 'Sort nodes by modified time', 489 | type: 'boolean', 490 | }, 491 | remote: { 492 | group: 'Flags:', 493 | demand: false, 494 | desc: 'View the trash listed by the API call', 495 | type: 'boolean', 496 | }, 497 | }, 498 | file: './Commands/ListTrashCommand', 499 | }, 500 | 'tree': { 501 | offline: true, 502 | usage: '[flags] [path]', 503 | desc: 'Print directory tree of the given node', 504 | options: { 505 | i: { 506 | group: 'Flags:', 507 | alias: 'id', 508 | demand: false, 509 | desc: 'Specify the remote node by its ID instead of path', 510 | type: 'boolean', 511 | }, 512 | a: { 513 | group: 'Flags:', 514 | alias: 'assets', 515 | demand: false, 516 | desc: 'Include ASSET nodes', 517 | type: 'boolean', 518 | }, 519 | m: { 520 | group: 'Flags:', 521 | alias: 'markdown', 522 | demand: false, 523 | desc: 'Output tree in markdown', 524 | type: 'boolean', 525 | }, 526 | decrypt: { 527 | group: 'Flags:', 528 | demand: false, 529 | desc: 'Decrypt files when downloading', 530 | type: 'boolean', 531 | }, 532 | password: { 533 | group: 'Flags:', 534 | alias: 'p', 535 | demand: false, 536 | desc: 'Prompt for decryption password', 537 | type: 'boolean', 538 | }, 539 | remote: { 540 | group: 'Flags:', 541 | demand: false, 542 | desc: 'Force the command to fetch from the API', 543 | type: 'boolean', 544 | }, 545 | 'show-ids': { 546 | group: 'Flags:', 547 | demand: false, 548 | desc: 'Include each node\'s ID on output', 549 | type: 'boolean', 550 | }, 551 | }, 552 | file: './Commands/TreeCommand', 553 | }, 554 | 'update': { 555 | offline: false, 556 | usage: '[flags] [options]', 557 | demand: 1, 558 | desc: `Update a node's metadata`, 559 | options: { 560 | i: { 561 | group: 'Flags:', 562 | alias: 'id', 563 | demand: false, 564 | desc: 'Specify the remote node by its ID instead of path', 565 | type: 'boolean', 566 | }, 567 | description: { 568 | group: 'Options:', 569 | demand: false, 570 | desc: 'Update the description', 571 | type: 'string', 572 | }, 573 | labels: { 574 | group: 'Options:', 575 | demand: false, 576 | desc: 'Update the labels', 577 | type: 'array', 578 | }, 579 | }, 580 | file: './Commands/UpdateCommand', 581 | }, 582 | 'unlink': { 583 | offline: false, 584 | usage: '[flags] ', 585 | demand: 2, 586 | desc: 'Unlink a node from a parent node', 587 | options: { 588 | i: { 589 | group: 'Flags:', 590 | alias: 'id', 591 | demand: false, 592 | desc: 'Specify the parent node by its ID instead of path', 593 | type: 'boolean', 594 | }, 595 | }, 596 | file: './Commands/UnlinkCommand', 597 | }, 598 | 'upload': { 599 | offline: false, 600 | usage: '[flags] ', 601 | demand: 2, 602 | desc: 'Upload local file(s) or folder(s) to remote directory', 603 | options: { 604 | o: { 605 | group: 'Flags:', 606 | alias: 'overwrite', 607 | demand: false, 608 | desc: 'Overwrite the remote file if it already exists', 609 | type: 'boolean', 610 | }, 611 | f: { 612 | group: 'Flags:', 613 | alias: 'force', 614 | demand: false, 615 | desc: 'Force a re-upload of the file even if the path and MD5 both match', 616 | type: 'boolean', 617 | }, 618 | d: { 619 | group: 'Flags:', 620 | alias: 'duplicates', 621 | demand: false, 622 | desc: 'Allow duplicate uploads', 623 | type: 'boolean', 624 | }, 625 | checksum: { 626 | group: 'Flags:', 627 | demand: false, 628 | desc: 'Compare remote MD5 checksum instead of filesize', 629 | type: 'boolean', 630 | }, 631 | encrypt: { 632 | group: 'Flags:', 633 | demand: false, 634 | desc: 'Encrypt files before uploading', 635 | type: 'boolean', 636 | }, 637 | armor: { 638 | group: 'Flags:', 639 | demand: false, 640 | desc: 'Create ASCII output - default is a binary format', 641 | type: 'boolean', 642 | }, 643 | password: { 644 | group: 'Flags:', 645 | alias: 'p', 646 | demand: false, 647 | desc: 'Prompt for decryption password', 648 | type: 'boolean', 649 | }, 650 | 'remove-source-files': { 651 | group: 'Flags:', 652 | demand: false, 653 | desc: 'Delete local files upon successful upload', 654 | type: 'boolean', 655 | }, 656 | labels: { 657 | group: 'Options:', 658 | demand: false, 659 | desc: 'Update the labels', 660 | type: 'array', 661 | }, 662 | }, 663 | file: './Commands/UploadCommand', 664 | }, 665 | 'usage': { 666 | offline: false, 667 | usage: '', 668 | desc: 'Show Cloud Drive account usage', 669 | options: {}, 670 | file: './Commands/UsageCommand', 671 | }, 672 | 'version': { 673 | offline: true, 674 | usage: '', 675 | desc: false, 676 | options: {}, 677 | callback: callback => { 678 | Logger.info(`v${pkgJson.version}`); 679 | callback(0); 680 | }, 681 | }, 682 | }, 683 | global: { 684 | options: { 685 | h: { 686 | group: 'Global Flags:', 687 | }, 688 | v: { 689 | group: 'Global Flags:', 690 | alias: 'verbose', 691 | demand: false, 692 | desc: 'Output verbosity: 1 for normal (-v), 2 for more verbose (-vv), and 3 for debug (-vvv)', 693 | type: 'count', 694 | }, 695 | q: { 696 | group: 'Global Flags:', 697 | alias: 'quiet', 698 | demand: false, 699 | desc: 'Suppress all output', 700 | type: 'boolean', 701 | }, 702 | 'ansi': { 703 | group: 'Global Flags:', 704 | demand: false, 705 | desc: 'Force ANSI output', 706 | type: 'boolean', 707 | }, 708 | 'no-ansi': { 709 | group: 'Global Flags:', 710 | demand: false, 711 | desc: 'Disable ANSI output', 712 | type: 'boolean', 713 | }, 714 | c: { 715 | group: 'Global Flags:', 716 | alias: 'config', 717 | demand: false, 718 | desc: 'Specify location of config file', 719 | type: 'string', 720 | }, 721 | V: { 722 | group: 'Global Flags:', 723 | }, 724 | d: { 725 | group: 'Global Flags:', 726 | alias: 'debug', 727 | demand: false, 728 | desc: 'Run debugging code', 729 | type: 'boolean', 730 | }, 731 | }, 732 | }, 733 | }, 734 | appConfig = { 735 | 'auth.email': { 736 | type: 'string', 737 | default: '', 738 | }, 739 | 'auth.id': { 740 | type: 'string', 741 | default: '', 742 | }, 743 | 'auth.secret': { 744 | type: 'string', 745 | default: '', 746 | }, 747 | 'cli.colors': { 748 | type: 'bool', 749 | default: true, 750 | }, 751 | 'cli.ignoreFiles': { 752 | type: 'string', 753 | default: '^(\\.DS_Store|[Tt]humbs.db)$', 754 | }, 755 | 'cli.progressBars': { 756 | type: 'bool', 757 | default: true, 758 | }, 759 | 'cli.progressInterval': { 760 | type: 'string', 761 | default: 250, 762 | }, 763 | 'cli.timestamp': { 764 | type: 'bool', 765 | default: false, 766 | }, 767 | 'crypto.algorithm': { 768 | type: 'choice', 769 | default: 'aes-256-ctr', 770 | choices: crypto.getCiphers(), 771 | }, 772 | 'crypto.armor': { 773 | type: 'bool', 774 | default: false, 775 | }, 776 | 'crypto.password': { 777 | type: 'string', 778 | default: '', 779 | }, 780 | 'database.driver': { 781 | type: 'string', 782 | default: 'sqlite', 783 | }, 784 | 'database.host': { 785 | type: 'string', 786 | default: '127.0.0.1', 787 | }, 788 | 'database.database': { 789 | type: 'string', 790 | default: 'clouddrive', 791 | }, 792 | 'database.username': { 793 | type: 'string', 794 | default: 'root', 795 | }, 796 | 'database.password': { 797 | type: 'string', 798 | default: '', 799 | }, 800 | 'display.date': { 801 | type: 'choice', 802 | default: 'modified', 803 | choices: [ 804 | 'modified', 805 | 'created', 806 | ], 807 | }, 808 | 'display.showPending': { 809 | type: 'bool', 810 | default: true, 811 | }, 812 | 'display.showTrash': { 813 | type: 'bool', 814 | default: true, 815 | }, 816 | 'download.checkMd5': { 817 | type: 'bool', 818 | default: true, 819 | }, 820 | 'download.maxConnections': { 821 | type: 'string', 822 | default: 1, 823 | }, 824 | 'json.pretty': { 825 | type: 'bool', 826 | default: false, 827 | }, 828 | 'log.file': { 829 | type: 'string', 830 | default: `${Command.getLogDirectory()}/main.log`, 831 | }, 832 | 'log.level': { 833 | type: 'choice', 834 | default: 'info', 835 | choices: [ 836 | 'info', 837 | 'verbose', 838 | 'debug', 839 | 'silly', 840 | ], 841 | }, 842 | 'sync.chunkSize': { 843 | type: 'string', 844 | default: '', 845 | }, 846 | 'sync.maxNodes': { 847 | type: 'string', 848 | default: '', 849 | }, 850 | 'upload.duplicates': { 851 | type: 'bool', 852 | default: false, 853 | }, 854 | 'upload.checkMd5': { 855 | type: 'bool', 856 | default: false, 857 | }, 858 | 'upload.maxConnections': { 859 | type: 'string', 860 | default: 1, 861 | }, 862 | 'upload.numRetries': { 863 | type: 'string', 864 | default: 1, 865 | }, 866 | }; 867 | 868 | let configFile = `${Command.getConfigDirectory()}/config.json`; 869 | if (yargs.argv['c'] || yargs.argv['config']) { 870 | configFile = yargs.argv['c'] || yargs.argv['config']; 871 | } 872 | 873 | let config = new Config(configFile, appConfig); 874 | if (yargs.argv['ansi'] !== undefined) { 875 | chalk.enabled = yargs.argv['ansi']; 876 | } else if (!config.get('cli.colors')) { 877 | chalk.enabled = false; 878 | } 879 | 880 | Logger.getInstance({ 881 | file: config.get('log.file'), 882 | logLevel: config.get('log.level'), 883 | verbosity: 'warn', 884 | cliTimestamp: config.get('cli.timestamp'), 885 | colorize: config.get('cli.colors'), 886 | }); 887 | 888 | async.forEachOfSeries(cliConfig.commands, (command, name, callback) => { 889 | yargs.command(name, command.desc, yargs => { 890 | return yargs.usage(`${command.desc}\n\n${chalk.magenta('Usage:')}\n ${name} ${command.usage}`) 891 | .options(command.options) 892 | .options(cliConfig.global.options) 893 | .demand(command.demand || 0) 894 | .strict() 895 | .fail(message => { 896 | yargs.showHelp(); 897 | Logger.error(message); 898 | Command.shutdown(1); 899 | }); 900 | }, argv => { 901 | let cliVerbosity = 'info'; 902 | switch (parseInt(argv.verbose)) { 903 | case 1: 904 | cliVerbosity = 'verbose'; 905 | break; 906 | case 2: 907 | cliVerbosity = 'debug'; 908 | break; 909 | case 3: 910 | cliVerbosity = 'silly'; 911 | break; 912 | default: 913 | break; 914 | } 915 | 916 | if (argv.quiet) { 917 | cliVerbosity = 'error'; 918 | } 919 | 920 | Logger.setConsoleLevel(cliVerbosity); 921 | 922 | // Load in the command file and run 923 | if (command.file) { 924 | let Cmd = require(command.file); 925 | new Cmd({offline: command.offline}, config).execute(argv._.slice(1), argv); 926 | } else if (command.callback) { 927 | // Otherwise, 928 | command.callback(code => { 929 | Command.shutdown(code); 930 | }); 931 | } else { 932 | Logger.error(`Command '${name}' does not have a valid config action`); 933 | Command.shutdown(1); 934 | } 935 | }); 936 | 937 | callback(); 938 | }, err => { 939 | if (err) { 940 | Logger.error(err); 941 | Command.shutdown(1); 942 | } 943 | 944 | let argv = yargs 945 | .usage(`${chalk.cyan(banner)} 946 | ${chalk.cyan('Cloud Drive')} version ${chalk.magenta(pkgJson.version)} 947 | 948 | ${chalk.magenta('Usage:')} 949 | clouddrive command [flags] [options] [arguments]`) 950 | .version(function() { 951 | return `v${pkgJson.version}`; 952 | }) 953 | .help('h') 954 | .alias('h', 'help') 955 | .alias('V', 'version') 956 | .updateStrings({ 957 | 'Commands:': chalk.magenta('Commands:'), 958 | 'Flags:': chalk.magenta('Flags:'), 959 | 'Options:': chalk.magenta('Options:'), 960 | 'Global Flags:': chalk.magenta('Global Flags:'), 961 | }) 962 | .options(cliConfig.global.options) 963 | .epilog(`Copyright ${new Date().getFullYear()}`) 964 | .strict() 965 | .fail((message) => { 966 | yargs.showHelp(); 967 | Logger.error(message); 968 | Command.shutdown(1); 969 | }) 970 | .recommendCommands() 971 | .argv; 972 | 973 | if (!argv._[0]) { 974 | yargs.showHelp(); 975 | } else { 976 | if (!cliConfig.commands[argv._[0]]) { 977 | yargs.showHelp(); 978 | Command.shutdown(1); 979 | } 980 | } 981 | }); 982 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clouddrive", 3 | "version": "0.6.4", 4 | "description": "Amazon Cloud Drive CLI and SDK", 5 | "repository": "alex-phillips/node-clouddrive", 6 | "bin": { 7 | "clouddrive": "bin/clouddrive" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "prepublish": "gulp" 12 | }, 13 | "keywords": [ 14 | "amazon", 15 | "clouddrive", 16 | "cloud", 17 | "drive", 18 | "acd_cli", 19 | "sdk", 20 | "cli", 21 | "acd" 22 | ], 23 | "author": "Alex Phillips ", 24 | "license": "ISC", 25 | "dependencies": { 26 | "async": "^1.5.2", 27 | "base64-stream": "^0.1.3", 28 | "chalk": "^1.1.3", 29 | "elegant-spinner": "^1.0.1", 30 | "env-paths": "^0.3.1", 31 | "form-data": "^1.0.1", 32 | "fs-extra": "^1.0.0", 33 | "got": "^6.6.3", 34 | "inquirer": "^0.10.1", 35 | "knex": "^0.8.6", 36 | "log-update": "^1.0.2", 37 | "moment": "^2.15.1", 38 | "mysql": "^2.11.1", 39 | "open": "0.0.5", 40 | "semver": "^5.3.0", 41 | "sqlite3": "^3.1.4", 42 | "winston": "^2.2.0", 43 | "yargs": "^6.2.0" 44 | }, 45 | "main": "index.js", 46 | "devDependencies": { 47 | "babel-preset-es2015": "^6.5.0", 48 | "gulp": "^3.9.0", 49 | "gulp-babel": "^6.1.2", 50 | "gulp-jshint": "^1.11.2" 51 | } 52 | } 53 | --------------------------------------------------------------------------------