├── .gitignore ├── History.md ├── LICENSE ├── Makefile ├── Readme.md ├── bin ├── _up-config ├── up ├── up-config ├── up-copy ├── up-open └── up-streams ├── index.js ├── lib ├── console.js └── plain.js ├── package.json └── test └── enoent.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /?.js 3 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.7.0 / 2015-06-02 3 | ================== 4 | 5 | * index: remove leading space from "User-Agent" header 6 | * index: `chmod` the config file to `rw` for user only (#45, @mmalecki) 7 | * config: add file piping support to `up config` 8 | * config: refactor to use "co-prompt" 9 | * package: add MIT license 10 | * package: require node >= v0.8.0 11 | * package: update all the deps 12 | * package: organize deps array 13 | * package: add "repository" field 14 | 15 | 0.6.2 / 2014-01-28 16 | ================== 17 | 18 | * package: update "cloudup-client" to v0.3.1 19 | * package: add "preferGlobal: true" flag 20 | 21 | 0.6.1 / 2014-01-21 22 | ================== 23 | 24 | * package: update "cloudup-client" to v0.3.0 25 | 26 | 0.6.0 / 2013-11-10 27 | ================== 28 | 29 | * console: move the cursor relative to the origin 30 | * index: fix saveConfig() filename saving with a newline 31 | 32 | 0.5.2 / 2013-10-28 33 | ================== 34 | 35 | * fixed node v0.10.x exit bug (process exits cleanly now after upload) 36 | * switch to TooTallNate/ansi.js for ANSI escape codes 37 | * ensure that nobody using node < v0.6.0 can install (not supported) 38 | * fix uploading URL type items from the command line 39 | * use osenv.home() for the HOME dir (beginnings of Windows compat) 40 | * various other minor lint and whitespace fixes 41 | 42 | 0.5.1 / 2013-10-09 43 | ================== 44 | 45 | * resume stdin before calling .pipe() 46 | * display "help" when no arguments are passed and not piping 47 | 48 | 0.5.0 / 2013-09-05 49 | ================== 50 | 51 | * add up-open(1). Closes #32 52 | * add index support 53 | * add up-copy(1) 54 | * add copying of stream.url as soon as the stream is created 55 | * change: be less critical with errors 56 | * change ua to cloudup-cli 57 | * fix aggregate progress: use rows not cols 58 | * fix code thumbs: dont generate them :) 59 | 60 | 0.4.0 / 2013-08-28 61 | ================== 62 | 63 | * add clipboard copy support 64 | * refactor interactive output, remove urls here 65 | * make sure streams fit within the # of rows 66 | 67 | 0.3.0 / 2013-08-02 68 | ================== 69 | 70 | * add interactive stream listing support 71 | * add interactive item listing support 72 | * add oauth support 73 | * add -s, --stream for adding to a stream 74 | * add up-config(1) 75 | * add aggregate progress for large #s of items. Closes #23 76 | * add up-streams(1) 77 | * remove --json and --json-stream 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Automattic, Inc and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha \ 4 | --require should \ 5 | --reporter spec \ 6 | --timeout 10s \ 7 | --bail 8 | 9 | .PHONY: test 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Cloudup CLI 3 | 4 | The cloudup cli `up(1)` allows you to upload files to the cloud with ease. 5 | 6 | ![Cloudup cli](https://i.cloudup.com/tpBkHd8URl.gif) 7 | ![Cloudup interactive mode](https://i.cloudup.com/m8K8vVohPm.gif) 8 | 9 | ## Installation 10 | 11 | Install with npm: 12 | 13 | ``` 14 | $ npm install -g Automattic/cloudup-cli 15 | ``` 16 | 17 | Authenticate: 18 | 19 | ``` 20 | $ up config 21 | 22 | Cloudup up(1) one-time configuration requires your 23 | password, however it is transfered via https 24 | and is not stored locally. Subsequent operations 25 | use the auth token generated from this process. 26 | 27 | Username: tobi 28 | Password: ****** 29 | 30 | ``` 31 | 32 | ## Usage 33 | 34 | ``` 35 | 36 | Usage: up [options] [file ...] 37 | 38 | Commands: 39 | 40 | open open matching stream 41 | copy copy matching stream's url 42 | streams list streams 43 | config configure up(1) 44 | help [cmd] display help for [cmd] 45 | 46 | Options: 47 | 48 | -h, --help output usage information 49 | -V, --version output the version number 50 | -t, --title stream title name 51 | -s, --stream upload to the given stream 52 | -d, --direct output direct links 53 | -f, --filename assign filename to stdin 54 | -T, --thumb-size thumbnail size in pixels [600] 55 | 56 | ``` 57 | 58 | ## Examples 59 | 60 | Examples illustrating how to use the cloudup command-line tool 61 | to upload files and access your account. 62 | 63 | ### Uploading 64 | 65 | Upload a single file, the stream url is copied to your clipboard _immediately_ 66 | for sharing, even before the upload has completed. 67 | 68 | ``` 69 | $ up reflection.png 70 | 71 | reflection.png : 92% 72 | stream : https://cloudup.com/cHFtYYeB8fJ 73 | ``` 74 | 75 | #### Multiple Files 76 | 77 | Upload several files at once by passing multiple filenames: 78 | 79 | ``` 80 | $ up simon-*.png 81 | 82 | simon-1.png : https://cloudup.com/iqd4NLa13ZV 83 | simon-2.png : https://cloudup.com/iCxBKJZAm36 84 | simon-3.png : https://cloudup.com/iEzTZXvVRYP 85 | simon-4.png : https://cloudup.com/iRYA6bLp70E 86 | simon-5.png : https://cloudup.com/ilMqsXxtTsV 87 | simon-6.png : https://cloudup.com/ilVngVMMeSd 88 | simon-7.png : https://cloudup.com/i1Tx8vkIbCC 89 | simon-8.png : https://cloudup.com/ifUKcaz5I3A 90 | simon-ball-ocean.png… : https://cloudup.com/iCA5N2PCJJS 91 | simon-ocean-stick-2.… : 71% 92 | simon-ocean-stick.pn… : 55% 93 | simon-ocean.png : 74% 94 | stream : https://cloudup.com/c7WwhIwSl6Y 95 | ``` 96 | 97 | #### Thumbnails 98 | 99 | `up(1)` delivers thumbnails when possible before the files are uploaded, so viewers can 100 | see what they're getting before-hand, and progress is updated in real-time. 101 | 102 | ![cloudup cli simon photos](https://i.cloudup.com/jy3GcK9VpO-900x900.jpeg) 103 | 104 | #### STDIN 105 | 106 | When no filenames are given `up(1)` reads from __stdin__: 107 | 108 | ``` 109 | $ echo 'hello world' | up 110 | ``` 111 | 112 | A filename can be passed to help cloudup interpret the content: 113 | 114 | ``` 115 | $ echo 'hello __world__' | up --filename hello.md 116 | ``` 117 | 118 | #### Upload Options 119 | 120 | You may optionally provide a stream `--title` upon upload, otherwise Cloudup 121 | will generate one for you based on the content: 122 | 123 | ``` 124 | $ up ferrets/*.png --title Ferrets 125 | ``` 126 | 127 | You may also upload to an existing stream by passing `--stream`: 128 | 129 | ``` 130 | $ up simon.png --stream c7WwhIwSl6Y 131 | ``` 132 | 133 | If you prefer direct links you may use `--direct`: 134 | 135 | ``` 136 | $ up example.jpeg --direct 137 | 138 | example.jpeg : http://i.cloudup.com/uBuZVUk80lK/SXSc1V.jpeg 139 | stream : https://cloudup.com/c1rAycLAdHo 140 | ``` 141 | 142 | ### Streams 143 | 144 | List your cloudup streams: 145 | 146 | ``` 147 | $ up streams 148 | 149 | Art (19) https://cloudup.com/cQD5fdgPrU1 150 | C (2) https://cloudup.com/c4f5h12Ti1T 151 | Cats (3) https://cloudup.com/cVeLe7dWdEH 152 | Cloudup - light (5) https://cloudup.com/ce4R6fdsQo 153 | Cluster (3) https://cloudup.com/cQJg8sdf7qO 154 | Design (35) https://cloudup.com/c7nHCsd30hhF 155 | Dolphins intelligence (8) https://cloudup.com/c5Hy71w2fWe 156 | EXIF (6) https://cloudup.com/coRcOdfXXiom 157 | Es6 yield (2) https://cloudup.com/cJWXLX1af2t 158 | 159 | ``` 160 | 161 | Search for streams: 162 | 163 | ``` 164 | $ up streams australia 165 | 166 | Australia 2013 (63) https://cloudup.com/c_nzIQcjCWo 167 | 168 | ``` 169 | 170 | Copy the first matching stream to the clipboard: 171 | 172 | ``` 173 | $ up copy australia 174 | ``` 175 | 176 | Open the first matching stream in your default browser: 177 | 178 | ``` 179 | $ up open australia 180 | ``` 181 | 182 | ### Interactive Mode 183 | 184 | The `-i` or `--interactive` flag may be used to list streams in an 185 | interactive list using the arrow keys to traverse the list. Pressing 186 | _return_ will open the stream or item in your default browser. 187 | 188 | The `up` / `down` arrows for navigating the list, and `left` / `right` 189 | to view the items or go back to the stream list. 190 | 191 | ![interactive mode](https://i.cloudup.com/m8K8vVohPm.gif) 192 | 193 | ## Tips 194 | 195 | Stream and item links that output to stdout may be opened 196 | in the browser by holding down __command__ and double-clicking 197 | the url. 198 | 199 | ## License 200 | 201 | (The MIT License) 202 | 203 | Copyright (c) 2014 Automattic, Inc and contributors <dev@automattic.com> 204 | 205 | Permission is hereby granted, free of charge, to any person obtaining 206 | a copy of this software and associated documentation files (the 207 | 'Software'), to deal in the Software without restriction, including 208 | without limitation the rights to use, copy, modify, merge, publish, 209 | distribute, sublicense, and/or sell copies of the Software, and to 210 | permit persons to whom the Software is furnished to do so, subject to 211 | the following conditions: 212 | 213 | The above copyright notice and this permission notice shall be 214 | included in all copies or substantial portions of the Software. 215 | 216 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 217 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 218 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 219 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 220 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 221 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 222 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 223 | -------------------------------------------------------------------------------- /bin/_up-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --harmony-generators 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var up = require('..'); 8 | var co = require('co'); 9 | var prompt = require('co-prompt'); 10 | var readline = require('readline'); 11 | var thunkify = require('thunkify'); 12 | 13 | // app id 14 | 15 | var appid = process.env.UP_APP_ID || 'arKRYoze02T'; 16 | 17 | // setup 18 | 19 | console.log(''); 20 | console.log(' Cloudup up(1) one-time configuration requires your'); 21 | console.log(' password, however it is transfered via https'); 22 | console.log(' and is not stored locally. Subsequent operations'); 23 | console.log(' use the auth token generated from this process.'); 24 | console.log(''); 25 | 26 | var rli; 27 | co(function* (){ 28 | var user, pass; 29 | 30 | if (process.stdin.isTTY) { 31 | user = yield prompt(' Username: '); 32 | pass = yield prompt.password(' Password: '); 33 | } else { 34 | // user is piping a file of credentials 35 | rli = readline.createInterface({ input: process.stdin }); 36 | var readLine = function (fn) { 37 | rli.once('line', function(line){ 38 | fn(null, line); 39 | }); 40 | }; 41 | user = yield readLine; 42 | pass = yield readLine; 43 | } 44 | 45 | var client = up.client({ 46 | user: user, 47 | pass: pass 48 | }); 49 | 50 | client.requestToken = thunkify(client.requestToken); 51 | 52 | var tok = yield client.requestToken(appid); 53 | 54 | up.saveConfig({ 55 | token: tok, 56 | user: user 57 | }); 58 | 59 | console.log('\n Configuration saved to ' + up.configPath + '\n'); 60 | })(function (err) { 61 | if (err) throw err; 62 | }); 63 | -------------------------------------------------------------------------------- /bin/up: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var program = require('commander'); 8 | var isCode = require('is-code'); 9 | var pkg = require('../package'); 10 | var thumb = require('osthumb'); 11 | var copy = require('cliparoo'); 12 | var path = require('path'); 13 | var head = require('head'); 14 | var basename = path.basename; 15 | var uid = require('uid2'); 16 | var up = require('..'); 17 | var fs = require('fs'); 18 | var index = 0; 19 | 20 | // options 21 | 22 | program 23 | .version(pkg.version) 24 | .usage('[options] [file ...]') 25 | .option('-t, --title ', 'stream title name', '') 26 | .option('-s, --stream ', 'upload to the given stream') 27 | .option('-d, --direct', 'output direct links') 28 | .option('-f, --filename ', 'assign filename to stdin') 29 | .option('-T, --thumb-size ', 'thumbnail size in pixels [600]', 600) 30 | .command('open', 'open matching stream') 31 | .command('copy', 'copy matching stream\'s url') 32 | .command('streams', 'list streams') 33 | .command('config', 'configure up(1)') 34 | .parse(process.argv); 35 | 36 | // title 37 | 38 | process.title = 'up'; 39 | 40 | // sub-command 41 | 42 | if (program.runningCommand) return; 43 | 44 | // size 45 | 46 | var size = program.thumbSize; 47 | 48 | // reporter 49 | 50 | var reporter = 'console'; 51 | if (program.json) reporter = 'json'; 52 | if (program.jsonStream) reporter = 'json-stream'; 53 | 54 | // config 55 | 56 | var conf = up.readConfig(); 57 | var client = up.client(conf); 58 | 59 | // stream 60 | 61 | var title = '' == program.title ? null : program.title; 62 | var files = program.args; 63 | 64 | var stream = client.stream({ 65 | id: program.stream, 66 | title: title 67 | }); 68 | 69 | // copy 70 | 71 | stream.on('save', function(){ 72 | copy(stream.url); 73 | }); 74 | 75 | // stdin reporter 76 | 77 | var isatty = process.stdin.isTTY 78 | if (!files.length && isatty) reporter = 'plain'; 79 | 80 | // reporter 81 | 82 | var Reporter = require('../lib/' + reporter); 83 | var reporter = new Reporter(stream, { 84 | progressOnly: files.length > process.stdout.rows - 5, 85 | direct: program.direct 86 | }); 87 | 88 | // stdin 89 | 90 | if (!files.length) { 91 | if (process.stdin.isTTY) { 92 | // user simply typed `up`... display "help" page 93 | program.help(); 94 | } else { 95 | // upload whatever comes through stdin as a stream 96 | var out = fs.createWriteStream('/tmp/up-' + uid(10)); 97 | process.stdin.resume(); 98 | process.stdin.pipe(out); 99 | process.stdin.on('end', function(){ 100 | var filename = program.filename; 101 | var title = '' == program.title ? null : program.title; 102 | 103 | var opts = { title: title }; 104 | 105 | if (textual(out.path)) { 106 | filename = filename || 'untitled.txt'; 107 | opts.mime = 'text/plain'; 108 | } 109 | 110 | filename = filename || out.path; 111 | opts.filename = filename; 112 | 113 | var item = stream.item(opts); 114 | 115 | item.file(out.path); 116 | stream.save(function(err){ 117 | if (err) throw err; 118 | }); 119 | }); 120 | } 121 | return; 122 | } 123 | 124 | // files 125 | 126 | files.forEach(function(file){ 127 | var ind = Date.now() + ++index; 128 | var item = stream.item({ index: ind }); 129 | if (isUrl(file)) return item.link(file); 130 | item.file(file); 131 | if (isCode(file)) return; 132 | thumb(file, { width: size, height: size }, function(err, path){ 133 | if (err || !path) return; 134 | item.thumb(path); 135 | }); 136 | }); 137 | 138 | // save 139 | 140 | stream.on('error', function(err){ 141 | console.error(err.message); 142 | }); 143 | 144 | stream.save(function(err){ 145 | if (err) throw err; 146 | }); 147 | 148 | /** 149 | * Check if `file` looks textual. 150 | */ 151 | 152 | function textual(file) { 153 | var buf = head(file, 24 * 1024); 154 | 155 | for (var i = 0; i < buf.length; i++) { 156 | if (0 == buf[i]) return false; 157 | } 158 | 159 | return true; 160 | } 161 | 162 | /** 163 | * Check if `str` is a url. 164 | */ 165 | 166 | function isUrl(str) { 167 | return ~str.indexOf('://'); 168 | } 169 | -------------------------------------------------------------------------------- /bin/up-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('gnode'); 4 | require('./_up-config'); 5 | -------------------------------------------------------------------------------- /bin/up-copy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var Cloudup = require('cloudup-client'); 8 | var program = require('commander'); 9 | var copy = require('cliparoo'); 10 | var open = require('open'); 11 | var up = require('..'); 12 | 13 | // options 14 | 15 | program 16 | .usage('[query...]') 17 | .parse(process.argv); 18 | 19 | // title 20 | 21 | process.title = 'up-copy'; 22 | 23 | // query 24 | 25 | var query = program.args.join(' ').trim(); 26 | 27 | // config 28 | 29 | var conf = up.readConfig(); 30 | var client = up.client(conf); 31 | 32 | // fetch streams 33 | 34 | client.streams({ title: query, only: 'title,url' }, function(err, streams){ 35 | if (err) throw err; 36 | 37 | var first = streams.pop(); 38 | 39 | // none 40 | if (!first) { 41 | console.error('no matching streams found'); 42 | process.exit(1); 43 | } 44 | 45 | console.log('\n \033[36m%s\033[0m copied to the clipboard\n', first.title); 46 | copy(first.url); 47 | }); 48 | -------------------------------------------------------------------------------- /bin/up-open: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var Cloudup = require('cloudup-client'); 8 | var program = require('commander'); 9 | var open = require('open'); 10 | var up = require('..'); 11 | 12 | // options 13 | 14 | program 15 | .usage('[query...]') 16 | .parse(process.argv); 17 | 18 | // title 19 | 20 | process.title = 'up-open'; 21 | 22 | // query 23 | 24 | var query = program.args.join(' ').trim(); 25 | 26 | // config 27 | 28 | var conf = up.readConfig(); 29 | var client = up.client(conf); 30 | 31 | // fetch streams 32 | 33 | client.streams({ title: query, only: 'title,url' }, function(err, streams){ 34 | if (err) throw err; 35 | 36 | var first = streams.pop(); 37 | 38 | // none 39 | if (!first) { 40 | console.error('no matching streams found'); 41 | process.exit(1); 42 | } 43 | 44 | console.log('\n opening %s\n', first.title); 45 | open(first.url); 46 | }); 47 | -------------------------------------------------------------------------------- /bin/up-streams: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var Cloudup = require('cloudup-client'); 8 | var program = require('commander'); 9 | var max = require('max-component'); 10 | var List = require('term-list'); 11 | var bytes = require('bytes'); 12 | var open = require('open'); 13 | var path = require('path'); 14 | var extname = path.extname; 15 | var up = require('..'); 16 | var s = require('printf'); 17 | 18 | // options 19 | 20 | program 21 | .usage('[options] [query...]') 22 | .option('-i, --interactive', 'display interactive list') 23 | .parse(process.argv); 24 | 25 | // title 26 | 27 | process.title = 'up-streams'; 28 | 29 | // query 30 | 31 | var query = program.args.join(' ').trim(); 32 | 33 | // max width 34 | 35 | var maxWidth = 40; 36 | 37 | // term sizing 38 | 39 | var stdout = process.stdout; 40 | var cols = stdout.cols; 41 | var rows = stdout.rows; 42 | 43 | // config 44 | 45 | var conf = up.readConfig(); 46 | var client = up.client(conf); 47 | 48 | // fetch streams 49 | 50 | client.streams({ title: query }, function(err, streams){ 51 | if (err) throw err; 52 | if (program.interactive) interactiveStreams(streams); 53 | else output(streams); 54 | }); 55 | 56 | /** 57 | * Output `streams`. 58 | */ 59 | 60 | function output(streams) { 61 | console.log(); 62 | var w = Math.min(maxWidth, maxTitleLength(streams) + 3); 63 | streams.sort(byTitle).forEach(function(stream){ 64 | console.log(formatStream(stream, w)); 65 | }); 66 | console.log(); 67 | } 68 | 69 | /** 70 | * Truncate `str` to `len`. 71 | */ 72 | 73 | function truncate(str, len) { 74 | str = String(str); 75 | if (str.length < len) return str; 76 | return str.slice(0, len) + '…'; 77 | } 78 | 79 | /** 80 | * Format `stream` to `width`. 81 | */ 82 | 83 | function formatStream(stream, width, removing) { 84 | if (removing) { 85 | return s(' \033[31;1m%*s\033[m \033[31m%s\033[m', 86 | truncate(stream.title || 'Untitled', maxWidth - 2), 87 | width, 88 | 'press backspace again to remove') 89 | } 90 | 91 | return s(' \033[36m%*s\033[m \033[90m(%s)\033[m %s', 92 | truncate(stream.title || 'Untitled', maxWidth - 2), 93 | width, 94 | stream.item_ids.length, 95 | stream.url); 96 | } 97 | 98 | /** 99 | * Format interactive `stream` to `width`. 100 | */ 101 | 102 | function formatInteractiveStream(stream, width, removing) { 103 | if (removing) { 104 | return s(' \033[31;1m%*s\033[m \033[31m%s\033[m', 105 | truncate(stream.title || 'Untitled', maxWidth - 2), 106 | width, 107 | 'press backspace again to remove') 108 | } 109 | 110 | return s(' \033[36m%*s\033[m \033[90m(%s)\033[m', 111 | truncate(stream.title || 'Untitled', maxWidth - 2), 112 | width, 113 | stream.item_ids.length); 114 | } 115 | 116 | /** 117 | * Display interactive streams list. 118 | */ 119 | 120 | function interactiveStreams(streams) { 121 | var w = Math.min(40, maxTitleLength(streams) + 3); 122 | var list = new List; 123 | var removing; 124 | 125 | function find(id) { 126 | return streams.filter(function(s){ 127 | return s.id == id; 128 | }).pop(); 129 | } 130 | 131 | streams.sort(byTitle).slice(0, rows - 4).forEach(function(stream){ 132 | list.add(stream.id, formatInteractiveStream(stream, w, false)); 133 | }); 134 | 135 | list.start(); 136 | 137 | list.on('keypress', function(key, id){ 138 | switch (key.name) { 139 | case 'return': 140 | list.stop(); 141 | open(find(id).url); 142 | break; 143 | case 'backspace': 144 | if (removing == id) { 145 | removing = null; 146 | list.remove(list.selected); 147 | client.stream(id).remove(function(err){ 148 | if (err) throw err; 149 | }); 150 | } else { 151 | removing = id; 152 | list.get(id).label = formatInteractiveStream(find(id), w, true); 153 | list.draw(); 154 | } 155 | break; 156 | case 'right': 157 | removing = null; 158 | find(id).items(function(err, items){ 159 | if (err) throw err; 160 | list.stop(); 161 | interactiveItems(items); 162 | }); 163 | break; 164 | default: 165 | if (removing) list.get(removing).label = formatInteractiveStream(find(removing), w); 166 | removing = null; 167 | } 168 | }); 169 | 170 | list.on('empty', function(){ 171 | list.stop(); 172 | }); 173 | 174 | return list; 175 | } 176 | 177 | /** 178 | * Format `item` to `width`. 179 | */ 180 | 181 | function formatItem(item, width, removing) { 182 | if (removing) { 183 | return s(' \033[31;1m%*s\033[m \033[31m%s\033[m', 184 | truncate(item.title || 'Untitled', maxWidth - 2), 185 | width, 186 | 'press backspace again to remove') 187 | } 188 | 189 | return s(' \033[36m%*s\033[m %s\033[90m - %s %s\033[0m', 190 | truncate(item.title || 'Untitled', maxWidth - 2), 191 | width, 192 | item.url, 193 | bytes(item.size || 0), 194 | item.filename || ''); 195 | } 196 | 197 | /** 198 | * Format `item` to `width`. 199 | */ 200 | 201 | function formatInteractiveItem(item, width, removing) { 202 | if (removing) { 203 | return s(' \033[31;1m%*s\033[m \033[31m%s\033[m', 204 | truncate(item.title || 'Untitled', maxWidth - 2), 205 | width, 206 | 'press backspace again to remove') 207 | } 208 | 209 | var size = item.size ? bytes(item.size) : ''; 210 | var ext = extname(item.filename || '').toLowerCase(); 211 | var title = truncate(item.title || 'Untitled', maxWidth - 2).trim(); 212 | 213 | return s(' \033[36m%*s\033[m %s\033[90m %s\033[0m', title, width, ext, size); 214 | } 215 | 216 | /** 217 | * Display interactive items list. 218 | */ 219 | 220 | function interactiveItems(items) { 221 | var w = Math.min(40, maxTitleLength(items) + 3); 222 | var list = new List; 223 | var removing; 224 | 225 | function find(id) { 226 | return items.filter(function(i){ 227 | return i.id == id; 228 | }).pop(); 229 | } 230 | 231 | items.sort(byTitle).forEach(function(item){ 232 | list.add(item.id, formatInteractiveItem(item, w)); 233 | }); 234 | 235 | list.start(); 236 | 237 | function showStreams(id) { 238 | removing = null; 239 | list.stop(); 240 | client.streams({ title: query }, function(err, streams){ 241 | if (err) throw err; 242 | var list = interactiveStreams(streams); 243 | if (id) list.select(id); 244 | }); 245 | } 246 | 247 | list.on('keypress', function(key, id){ 248 | switch (key.name) { 249 | case 'return': 250 | list.stop(); 251 | open(find(id).url); 252 | break; 253 | case 'backspace': 254 | if (removing == id) { 255 | removing = null; 256 | list.remove(list.selected); 257 | var item = find(id); 258 | item.remove(function(err){ 259 | if (err) throw err; 260 | if (!list.items.length) showStreams(item.stream.id); 261 | }); 262 | } else { 263 | removing = id; 264 | list.get(id).label = formatInteractiveItem(find(id), w, true); 265 | list.draw(); 266 | } 267 | break; 268 | case 'left': 269 | showStreams(find(id).stream.id); 270 | break; 271 | default: 272 | if (removing) list.get(removing).label = formatInteractiveItem(find(removing), w); 273 | removing = null; 274 | } 275 | }); 276 | 277 | list.on('empty', function(){ 278 | list.stop(); 279 | }); 280 | } 281 | 282 | /** 283 | * Return max title length in `streams`. 284 | */ 285 | 286 | function maxTitleLength(streams) { 287 | return max(streams, function(s){ 288 | return (s.title || '').length; 289 | }); 290 | } 291 | 292 | /** 293 | * Sort by title. 294 | */ 295 | 296 | function byTitle(a, b) { 297 | if (a.title > b.title) return 1; 298 | if (a.title < b.title) return -1; 299 | return 0; 300 | } 301 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var osenv = require('osenv'); 10 | var Cloudup = require('cloudup-client'); 11 | var pkg = require('./package'); 12 | 13 | /** 14 | * Configuration path. 15 | */ 16 | 17 | exports.configPath = path.resolve(osenv.home(), '.cloudup.json'); 18 | 19 | /** 20 | * Create a client with `opts`. 21 | * 22 | * @param {Object} opts 23 | * @return {Cloudup} 24 | * @api private 25 | */ 26 | 27 | exports.client = function(opts){ 28 | opts = opts || {}; 29 | opts.useragent = 'cloudup-cli/' + pkg.version; 30 | if (process.env.UP_API_URL) opts.url = process.env.UP_API_URL; 31 | if (process.env.UP_CLOUDUP_URL) opts.cloudupUrl = process.env.UP_CLOUDUP_URL; 32 | return new Cloudup(opts); 33 | }; 34 | 35 | /** 36 | * Read config. 37 | * 38 | * @return {Object} 39 | * @api public 40 | */ 41 | 42 | exports.readConfig = function(){ 43 | var json, obj; 44 | 45 | // read 46 | try { 47 | json = fs.readFileSync(exports.configPath, 'utf8'); 48 | } catch (err) { 49 | console.error('\n Failed to load configuration.'); 50 | console.error(' Execute: `up config` to get started!\n'); 51 | process.exit(1); 52 | } 53 | 54 | // parse 55 | try { 56 | obj = JSON.parse(json); 57 | } catch (err) { 58 | console.error('\n Failed to parse ' + exports.configPath + '\n'); 59 | process.exit(1); 60 | } 61 | 62 | // validate 63 | if (!(obj.user && obj.token)) { 64 | console.error('\n Auth token missing.'); 65 | console.error(' Execute: `up config` to get a token!\n'); 66 | process.exit(1); 67 | } 68 | 69 | return obj; 70 | }; 71 | 72 | /** 73 | * Save config `obj`. 74 | * 75 | * @param {Object} obj 76 | * @api public 77 | */ 78 | 79 | exports.saveConfig = function(obj){ 80 | var json = JSON.stringify(obj, null, 2) + '\n'; 81 | fs.writeFileSync(exports.configPath, json); 82 | // chmod the config file to rw for owner only to prevent other users from 83 | // stealing the token 84 | fs.chmodSync(exports.configPath, parseInt('0600', 8)); 85 | }; 86 | -------------------------------------------------------------------------------- /lib/console.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var ansi = require('ansi'); 7 | var path = require('path'); 8 | var sprintf = require('printf'); 9 | var sum = require('sum-component'); 10 | var basename = path.basename; 11 | 12 | /** 13 | * Expose `Console`. 14 | */ 15 | 16 | module.exports = Console; 17 | 18 | /** 19 | * Initialize a new `Console` reporter. 20 | * 21 | * - `direct` show direct links 22 | * - `progressOnly` show aggregate progress only 23 | * 24 | * @param {Stream} stream 25 | * @param {Object} options 26 | * @api public 27 | */ 28 | 29 | function Console(stream, options) { 30 | this.y = 0; 31 | this.newlines = 0; 32 | this.items = []; 33 | this.cursor = ansi(process.stdout); 34 | this.stream = stream; 35 | this.direct = options.direct; 36 | this.progressOnly = options.progressOnly; 37 | 38 | // bind event listeners to `this` 39 | this.onitem = this.onitem.bind(this); 40 | this.onsave = this.onsave.bind(this); 41 | this.onend = this.onend.bind(this); 42 | 43 | // attach event listeners to `stream` 44 | stream.on('item', this.onitem); 45 | stream.on('save', this.onsave); 46 | stream.on('end', this.onend); 47 | 48 | // setup the cursor and global event listeners 49 | this.cursor.hide(); 50 | var self = this; 51 | function showCursor(){ 52 | self.cursor.show(); 53 | } 54 | process.on('uncaughtException', showCursor); 55 | process.on('uncaughtException', fatal); 56 | process.on('exit', showCursor); 57 | process.on('SIGINT', showCursor); 58 | process.on('SIGINT', process.exit.bind(null, 1)); 59 | } 60 | 61 | /** 62 | * Stream "item" event listener. 63 | * 64 | * @api private 65 | */ 66 | 67 | Console.prototype.onitem = function(item){ 68 | this.items.push(item); 69 | if (this.progressOnly) { 70 | this.aggregate(item); 71 | } else { 72 | this.progress(item); 73 | } 74 | }; 75 | 76 | /** 77 | * Item aggregate progress. 78 | * 79 | * @api private 80 | */ 81 | 82 | Console.prototype.aggregate = function(item){ 83 | var self = this; 84 | item.progress = 0; 85 | item.on('progress', function(e){ 86 | item.progress = e.percent; 87 | self.update(); 88 | }); 89 | }; 90 | 91 | /** 92 | * Update aggregate progress. 93 | * 94 | * @api private 95 | */ 96 | 97 | Console.prototype.update = function(){ 98 | var len = this.items.length; 99 | var percent = sum(this.items, 'progress') / len | 0; 100 | this.log(0, len + ' items', percent + '%'); 101 | }; 102 | 103 | /** 104 | * Item progress reporting. 105 | * 106 | * @api private 107 | */ 108 | 109 | Console.prototype.progress = function(item){ 110 | var y = this.y++; 111 | var self = this; 112 | var direct = this.direct; 113 | var cursor = this.cursor; 114 | 115 | function onprogress(e){ 116 | var n = e.percent | 0; 117 | self.log(y, ctx(item), n + '%'); 118 | } 119 | 120 | function onend(){ 121 | var url = direct ? item.direct_url : item.url; 122 | self.log(y, ctx(item), url); 123 | } 124 | 125 | if (cursor.enabled) { 126 | process.nextTick(function(){ 127 | // fire off a fake 0% event so that the line gets rendered 128 | item.emit('progress', 0); 129 | }); 130 | 131 | item.on('progress', onprogress); 132 | } 133 | 134 | item.on('end', onend); 135 | }; 136 | 137 | /** 138 | * Output collection link on save. 139 | * 140 | * @api private 141 | */ 142 | 143 | Console.prototype.onsave = function(){ 144 | var y = this.progressOnly ? 1 : this.items.length; 145 | this.log(y, 'stream', this.stream.url); 146 | }; 147 | 148 | /** 149 | * Stream "end" event listener. 150 | * Doesn't actually need to do anything... 151 | * 152 | * @api private 153 | */ 154 | 155 | Console.prototype.onend = function(){ 156 | // no-op 157 | //var y = this.progressOnly ? 2 : this.items.length + 1; 158 | //this.log(y, 'that\'s all', 'folks!'); 159 | }; 160 | 161 | /** 162 | * Log `key` / `str` on the relative line index `y` (starts at 0). 163 | * 164 | * @param {Number} y 165 | * @param {String} key 166 | * @param {String} str 167 | * @api private 168 | */ 169 | 170 | Console.prototype.log = function(y, key, str){ 171 | 172 | var up = 0; 173 | var moved = false; 174 | if (this.cursor.enabled) { 175 | // first ensure that we've at least written `y` newlines by now 176 | while (this.newlines < y) { 177 | this.cursor.write('\n'); 178 | this.newlines++; 179 | } 180 | 181 | // at this point, we may need to move the cursor up one or more 182 | // rows in order to be on the correct `y` line before writing 183 | up = this.newlines - y; 184 | moved = false; 185 | if (up > 0) { 186 | moved = true; 187 | this.cursor.up(up); 188 | } 189 | } 190 | 191 | // now that we know we're on the correct `y` line, output the text 192 | this.cursor 193 | .fg.cyan() 194 | .write(sprintf('%25s', key)) 195 | .fg.reset() 196 | .write(' : ') 197 | .fg.brightBlack() 198 | .write(str) 199 | .fg.reset() 200 | .write('\n'); 201 | 202 | if (this.cursor.enabled) { 203 | up--; // subtract from `up` since we just output a \n 204 | if (up > 0) { 205 | this.cursor.down(up); 206 | } 207 | if (!moved) { 208 | // if we didn't call `cursor.up()` before, then we can increment the \n count 209 | this.newlines++; 210 | } 211 | } 212 | }; 213 | 214 | /** 215 | * Fatal error. 216 | */ 217 | 218 | function fatal(err) { 219 | console.error(err.stack.replace(/^/gm, ' ')); 220 | process.exit(1); 221 | } 222 | 223 | /** 224 | * Truncate `str`. 225 | */ 226 | 227 | function truncate(str, width) { 228 | if (null == width) width = 20; 229 | if (str.length < width) return str; 230 | return str.slice(0, width) + '…'; 231 | } 232 | 233 | /** 234 | * Context string for `item`. 235 | */ 236 | 237 | function ctx(item) { 238 | return truncate(item._file ? 239 | basename(item.filename || item._file) : 240 | item._url); 241 | } 242 | -------------------------------------------------------------------------------- /lib/plain.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose `PlainReporter`. 4 | */ 5 | 6 | module.exports = PlainReporter; 7 | 8 | /** 9 | * Initialize a new `PlainReporter` reporter. 10 | * 11 | * @param {Stream} stream 12 | * @param {Object} options 13 | * @api public 14 | */ 15 | 16 | function PlainReporter(stream, options) { 17 | stream.on('end', function(){ 18 | console.log(stream.url); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "up", 3 | "version": "0.7.0", 4 | "description": "cloudup command-line executable", 5 | "keywords": [ 6 | "cloudup", 7 | "upload", 8 | "file", 9 | "files", 10 | "cli" 11 | ], 12 | "dependencies": { 13 | "ansi": "~0.3.0", 14 | "bytes": "~2.1.0", 15 | "cliparoo": "1.0.0", 16 | "cloudup-client": "0.3.2", 17 | "co": "3.1.0", 18 | "co-prompt": "1.0.0", 19 | "commander": "1.3.2", 20 | "gnode": "0.1.1", 21 | "head": "~1.0.0", 22 | "is-code": "~1.1.0", 23 | "max-component": "~1.0.0", 24 | "ms": "0.7.1", 25 | "open": "0.0.5", 26 | "osenv": "~0.1.1", 27 | "osthumb": "0.0.1", 28 | "printf": "0.2.2", 29 | "sum-component": "~0.1.1", 30 | "term-list": "0.2.1", 31 | "thunkify": "2.1.2", 32 | "uid2": "0.0.3" 33 | }, 34 | "devDependencies": { 35 | "mocha": "*", 36 | "should": "*", 37 | "better-assert": "*" 38 | }, 39 | "engines": { 40 | "node": ">=0.8.0" 41 | }, 42 | "preferGlobal": true, 43 | "repository": { 44 | "type": "git", 45 | "url": "git://github.com/Automattic/cloudup-cli.git" 46 | }, 47 | "license": "MIT", 48 | "bin": { 49 | "up": "bin/up", 50 | "up-streams": "bin/up-streams", 51 | "up-config": "bin/up-config", 52 | "up-copy": "bin/up-copy", 53 | "up-open": "bin/up-open" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/enoent.js: -------------------------------------------------------------------------------- 1 | 2 | var exec = require('child_process').exec; 3 | var assert = require('assert'); 4 | 5 | describe('up', function(){ 6 | describe('with a missing file', function(){ 7 | it('should output an error', function(done){ 8 | exec('bin/up maru', function(err, stdout, stderr){ 9 | assert(err); 10 | done(); 11 | }); 12 | }) 13 | }) 14 | }) 15 | --------------------------------------------------------------------------------