├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── AUTHORS ├── History.md ├── LICENSE.txt ├── README.md ├── appveyor.yml ├── lib ├── client.js ├── dl.js ├── doc.js ├── image.js ├── other.js ├── rs.js ├── up.js └── utils.js ├── logo.png ├── package.json └── test ├── client.test.js ├── config.json ├── dl.test.js ├── doc.test.js ├── fixtures ├── big.txt ├── foo.txt ├── github.css ├── gogopher.jpg ├── logo.png └── readme.md ├── image.test.js ├── other.test.js ├── qn.js ├── rs.test.js └── up.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.html 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | .DS_Store 10 | 11 | pids 12 | logs 13 | results 14 | 15 | node_modules 16 | npm-debug.log 17 | coverage/ 18 | test/tmp_* 19 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .tmp/ 4 | .git/ 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : false, // {int} Number of spaces to use for indentation 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : false, // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : false, // true: Require all defined variables be used 28 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 29 | "trailing" : false, // true: Prohibit trailing whitespaces 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : false, // {int} Max number of characters per line 35 | 36 | // Relaxing 37 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 38 | "boss" : true, // true: Tolerate assignments where comparisons would be expected 39 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 40 | "eqnull" : false, // true: Tolerate use of `== null` 41 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 42 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 43 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 44 | // (ex: `for each`, multiple try/catch, function expression…) 45 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 46 | "expr" : true, // true: Tolerate `ExpressionStatement` as Programs 47 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 48 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 49 | "iterator" : false, // true: Tolerate using the `__iterator__` property 50 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 51 | "laxbreak" : true, // true: Tolerate possibly unsafe line breakings 52 | "laxcomma" : false, // true: Tolerate comma-first style coding 53 | "loopfunc" : false, // true: Tolerate functions being defined in loops 54 | "multistr" : true, // true: Tolerate multi-line strings 55 | "proto" : false, // true: Tolerate using the `__proto__` property 56 | "scripturl" : false, // true: Tolerate script-targeted URLs 57 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 58 | "shadow" : true, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 59 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 60 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 61 | "validthis" : true, // true: Tolerate using this in a non-constructor function 62 | 63 | // Environments 64 | "browser" : true, // Web Browser (window, document, etc) 65 | "couch" : false, // CouchDB 66 | "devel" : true, // Development/debugging (alert, confirm, etc) 67 | "dojo" : false, // Dojo Toolkit 68 | "jquery" : false, // jQuery 69 | "mootools" : false, // MooTools 70 | "node" : true, // Node.js 71 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 72 | "prototypejs" : false, // Prototype and Scriptaculous 73 | "rhino" : false, // Rhino 74 | "worker" : false, // Web Workers 75 | "wsh" : false, // Windows Scripting Host 76 | "yui" : false, // Yahoo User Interface 77 | "noyield" : true, // allow generators without a yield 78 | 79 | // Legacy 80 | "nomen" : false, // true: Prohibit dangling `_` in variables 81 | "onevar" : false, // true: Allow only one `var` statement per function 82 | "passfail" : false, // true: Stop on first error 83 | "white" : false, // true: Check against strict whitespace and indentation rules 84 | 85 | // Custom Globals 86 | "globals" : { // additional predefined global variables 87 | // mocha 88 | "describe": true, 89 | "it": true, 90 | "before": true, 91 | "afterEach": true, 92 | "beforeEach": true, 93 | "after": true 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - '6' 6 | install: 7 | - npm i npminstall && npminstall 8 | script: 9 | - npm run ci 10 | after_script: 11 | - npminstall codecov && codecov 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Ordered by date of first contribution. 2 | 3 | fengmk2 (https://github.com/fengmk2) 4 | alsotang (https://github.com/alsotang) 5 | Yiyu He (https://github.com/dead-horse) 6 | Chen Wei 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.3.0 / 2016-06-26 3 | ================== 4 | 5 | * deps: use ^ instead of ~ (#90) 6 | 7 | 1.2.0 / 2016-04-06 8 | ================== 9 | 10 | * deps: upgrade formstream to 1.0.0 11 | * feat: add rs /fetch action 12 | * doc: add uploadToken usage 13 | * deps: agentkeepalive@2.1.1 14 | * chore(package): update urllib to version 2.8.0 15 | * chore(package): update utility to version 1.6.0 16 | 17 | 1.1.1 / 2015-11-22 18 | ================== 19 | 20 | * fix: Update client.js 21 | * chore(package): update mocha to version 2.3.4 22 | 23 | 1.1.0 / 2015-10-15 24 | ================== 25 | 26 | * feat: use http keepalive 27 | * add 0.10 and iojs-1.2 test matrix 28 | * fix tests on windows 29 | * fix Duplicate key 30 | * add appveyor 31 | * chore: breaking change notification and fallback 32 | * chore: change domain to origin 33 | * fix example 34 | 35 | 1.0.1 / 2014-10-11 36 | ================== 37 | 38 | * upload url can be config 39 | 40 | 1.0.0 / 2014-09-19 41 | ================== 42 | 43 | * update deps 44 | * fix empty string test 45 | * Fix `client.upload()` example 46 | 47 | 0.2.2 / 2014-05-15 48 | ================== 49 | 50 | * update urllib to 0.5.16 51 | 52 | 0.2.1 / 2014-03-10 53 | ================== 54 | 55 | * update deps 56 | 57 | 0.2.0 / 2013-12-06 58 | ================== 59 | 60 | * set content-length on rs op 61 | * add npm image 62 | * update test cases 63 | * download work with options.writeStream 64 | * move signData() to Qiniu 65 | 66 | 0.1.3 / 2013-09-09 67 | ================== 68 | 69 | * use global default timeout if options.timeout not set. 70 | 71 | 0.1.2 / 2013-09-08 72 | ================== 73 | 74 | * add download() and fix delete() key 75 | 76 | 0.1.1 / 2013-09-06 77 | ================== 78 | 79 | * fixed key start with "/" bug 80 | 81 | 0.1.0 / 2013-09-06 82 | ================== 83 | 84 | * add md2html() 85 | * fix tests 86 | 87 | 0.0.5 / 2013-09-05 88 | ================== 89 | 90 | * add image operations 91 | * add qrcode(key, mode, level) 92 | * Refactor test dir 93 | * Refactor 94 | * fix tests for ci 95 | 96 | 0.0.4 / 2013-09-05 97 | ================== 98 | 99 | * add batch() operations and list() 100 | 101 | 0.0.3 / 2013-09-05 102 | ================== 103 | 104 | * add stat(), move(), copy() and delete() 105 | 106 | 0.0.2 / 2013-09-04 107 | ================== 108 | 109 | * upload Stream without size work now. 110 | 111 | 0.0.1 / 2013-09-03 112 | ================== 113 | 114 | * uploadFile() 115 | * add more test cases 116 | * add upload() with string and buffer 117 | * add logo 118 | * first commit 119 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright(c) 2013 - 2014 fengmk2 and other contributors. 4 | Copyright(c) node-modules and other contributors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | qn 2 | ======= 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![build status][travis-image]][travis-url] 6 | [![Test coverage][codecov-image]][codecov-url] 7 | [![David deps][david-image]][david-url] 8 | [![Known Vulnerabilities][snyk-image]][snyk-url] 9 | [![npm download][download-image]][download-url] 10 | 11 | [npm-image]: https://img.shields.io/npm/v/qn.svg?style=flat 12 | [npm-url]: https://npmjs.org/package/qn 13 | [travis-image]: https://img.shields.io/travis/node-modules/qn.svg?style=flat 14 | [travis-url]: https://travis-ci.org/node-modules/qn 15 | [codecov-image]: https://codecov.io/gh/node-modules/qn/branch/master/graph/badge.svg 16 | [codecov-url]: https://codecov.io/gh/node-modules/qn 17 | [david-image]: https://img.shields.io/david/node-modules/qn.svg?style=flat 18 | [david-url]: https://david-dm.org/node-modules/qn 19 | [snyk-image]: https://snyk.io/test/npm/qn/badge.svg?style=flat-square 20 | [snyk-url]: https://snyk.io/test/npm/qn 21 | [download-image]: https://img.shields.io/npm/dm/qn.svg?style=flat-square 22 | [download-url]: https://npmjs.org/package/qn 23 | 24 | 25 | Another [qiniu](http://docs.qiniu.com/api/) API client for Node.js. 26 | 27 | ## Install 28 | 29 | ```bash 30 | $ npm install qn --save 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### Upload 36 | 37 | ```js 38 | var qn = require('qn'); 39 | 40 | var client = qn.create({ 41 | accessKey: 'your access key', 42 | secretKey: 'your secret key', 43 | bucket: 'your bucket name', 44 | origin: 'http://{bucket}.u.qiniudn.com', 45 | // timeout: 3600000, // default rpc timeout: one hour, optional 46 | // if your app outside of China, please set `uploadURL` to `http://up.qiniug.com/` 47 | // uploadURL: 'http://up.qiniu.com/', 48 | }); 49 | 50 | // upload a file with custom key 51 | client.uploadFile(filepath, {key: 'qn/lib/client.js'}, function (err, result) { 52 | console.log(result); 53 | // { 54 | // hash: 'FhGbwBlFASLrZp2d16Am2bP5A9Ut', 55 | // key: 'qn/lib/client.js', 56 | // url: 'http://qtestbucket.qiniudn.com/qn/lib/client.js' 57 | // "x:ctime": "1378150371", 58 | // "x:filename": "client.js", 59 | // "x:mtime": "1378150359", 60 | // "x:size": "21944", 61 | // } 62 | }); 63 | 64 | // upload a stream 65 | client.upload(fs.createReadStream(filepath), function (err, result) { 66 | console.log(result); 67 | // { 68 | // hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 69 | // key: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 70 | // url: 'http://qtestbucket.qiniudn.com/FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 71 | // "x:filename": "foo.txt", 72 | // } 73 | }); 74 | 75 | // you also can upload a string or Buffer directly 76 | client.upload('哈哈', {key: 'haha.txt'}, function (err, result) { 77 | console.log(result); 78 | // hash: 'FptOdeKmWhcYHUXa5YmNZxJC934B', 79 | // key: 'haha.txt', 80 | // url: 'http://qtestbucket.qiniudn.com/haha.txt', 81 | }); 82 | 83 | // xVariables 84 | client.upload(filepath, { 'x:foo': 'bar' }, function (err, result) { 85 | console.log(result); 86 | // hash: 'FptOdeKmWhcYHUXa5YmNZxJC934B', 87 | // key: 'foobar.txt', 88 | // url: 'http://qtestbucket.qiniudn.com/foobar.txt', 89 | // x:foo: 'bar' 90 | }); 91 | ``` 92 | 93 | ### uploadToken 94 | 95 | ``` 96 | var token = client.uploadToken(); 97 | ``` 98 | 99 | or with options 100 | 101 | - scope 102 | - deadline 103 | 104 | ```` 105 | var token = client.uploadToken({ 106 | deadline: utility.timestamp() + 10 107 | }); 108 | ``` 109 | 110 | ### Download 111 | 112 | ```js 113 | // download to Buffer 114 | client.download('foo.txt', function (err, content, res) { 115 | // content is a Buffer instance. 116 | console.log('content size: %d', content.length); 117 | }); 118 | 119 | // save as url 120 | var url = client.saveAsURL('qn/test/dl/foo.txt', '哈哈foo.txt'); 121 | // http://qtestbucket.qiniudn.com/qn/test/dl/foo.txt?download/%E5%93%88%E5%93%88foo.txt 122 | ``` 123 | 124 | ### RS Operations 125 | 126 | ```js 127 | // stat 128 | client.stat('foo.txt', function (err, stat) { 129 | console.log(stat); 130 | // fsize: 8, 131 | // hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 132 | // mimeType: 'text/plain', 133 | // putTime: 13783134309588504 134 | }); 135 | 136 | // move 137 | client.move('foo.txt', 'qn/bar.txt', function (err) { 138 | 139 | }); 140 | 141 | // copy 142 | client.copy('foo.txt', 'qn/bar.txt', function (err) { 143 | 144 | }); 145 | 146 | // delete 147 | client.delete('foo.txt', function (err) { 148 | 149 | }); 150 | 151 | // list 152 | client.list('/', function (err, result) { 153 | console.log(result); 154 | // marker: 'eyJjIjowLCJrIjoicW4vYmlnLnR4dCJ9' 155 | // items: [ 156 | // { 157 | // fsize: 21944, 158 | // putTime: 13783144546186030, 159 | // key: 'qn/logo.png', 160 | // hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 161 | // mimeType: 'image/png' 162 | // }, ... 163 | // ] 164 | }); 165 | ``` 166 | 167 | ### Image operations 168 | 169 | ```js 170 | // imageInfo 171 | client.imageInfo('qn/logo.png', function (err, info) { 172 | console.log(info); 173 | // { format: 'png', width: 190, height: 150, colorModel: 'nrgba' } 174 | }); 175 | 176 | // exif 177 | client.exif('qn/logo.png', function (err, exif) { 178 | 179 | }); 180 | 181 | // imageView 182 | var url = client.imageView('qn/logo.png', {mode: 1, width: 100, height: 100, q: 50, format: 'png'}); 183 | // http://qtestbucket.qiniudn.com/qn/logo.png?imageView/1/w/100/h/100/q/50/format/png 184 | 185 | // imageMogr 186 | var url = client.imageMogr('qn/fixtures/gogopher.jpg', { 187 | thumbnail: '!50p', 188 | gravity: 'NorthWest', 189 | quality: 50, 190 | rotate: -50, 191 | format: 'gif' 192 | }); 193 | // http://qtestbucket.qiniudn.com/qn/fixtures/gogopher.jpg?imageMogr/v2/auto-orient/thumbnail/!50p/gravity/NorthWest/quality/50/rotate/-50/format/gif 194 | 195 | // watermark 196 | var url = client.watermark('qn/logo.png', { 197 | mode: 2, 198 | text: 'Node.js 哈哈', 199 | font: '宋体', 200 | fontsize: 500, 201 | fill: 'red', 202 | dissolve: 100, 203 | gravity: 'SouthEast', 204 | dx: 100, 205 | dy: 90 206 | }); 207 | // http://qtestbucket.qiniudn.com/qn/fixtures/gogopher.jpg?watermark/2/text/Tm9kZS5qcyDlk4jlk4g=/font/5a6L5L2T/fontsize/500/fill/cmVk/dissolve/100/gravity/SouthEast/dx/100/dy/90 208 | ``` 209 | 210 | ### Document Operations 211 | 212 | ```js 213 | // markdown to html 214 | var url = client.md2html('qn/test/fixtures/readme.md', { 215 | css: 'http://qtestbucket.qiniudn.com/qn/test/fixtures/github.css' 216 | }); 217 | // http://qtestbucket.qiniudn.com/qn/test/fixtures/readme.md?md2html/0/css/aHR0cDovL3F0ZXN0YnVja2V0LnFpbml1ZG4uY29tL3FuL3Rlc3QvZml4dHVyZXMvZ2l0aHViLmNzcw== 218 | ``` 219 | 220 | ## TODO 221 | 222 | * [x] RS Operations 223 | * [ ] HTTP Keep-alive 224 | * [x] Image Operations 225 | * [ ] Media Operations 226 | * [x] Doc Operations 227 | * [ ] Pipeline Operations 228 | * [x] QR code Operations 229 | 230 | ## License 231 | 232 | [MIT](LICENSE.txt) 233 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '4' 4 | - nodejs_version: '6' 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm i npminstall && node_modules\.bin\npminstall 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm run ci 14 | 15 | build: off 16 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/client.js 3 | * 4 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 5 | * MIT Licensed 6 | */ 7 | 8 | "use strict"; 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | var urllib = require('urllib'); 15 | var utility = require('utility'); 16 | var Agent = require('agentkeepalive'); 17 | var utils = require('./utils'); 18 | 19 | var keepaliveAgent = new Agent({ 20 | maxSockets: 100, 21 | maxFreeSockets: 10, 22 | keepAliveTimeout: 30000 // free socket keepalive for 30 seconds 23 | }); 24 | 25 | var DEFAULT_TIMEOUT = 36000000; 26 | 27 | function Qiniu(options) { 28 | if (!options || !options.accessKey || !options.secretKey || !options.bucket) { 29 | throw new TypeError('required accessKey, secretKey and bucket'); 30 | } 31 | 32 | if (options.domain) { 33 | console.error('`qn` package: options.domain deprecated, use options.origin instead'); 34 | options.origin = options.domain; 35 | } 36 | 37 | options.origin = options.origin || null; 38 | options.timeout = options.timeout || DEFAULT_TIMEOUT; 39 | options.downloadTimeout = options.downloadTimeout || DEFAULT_TIMEOUT; 40 | this.options = options; 41 | this._uploadURL = options.uploadURL || 'http://up.qiniu.com/'; 42 | 43 | this._baseURL = options.origin || 'http://' + options.bucket + '.qiniudn.com'; 44 | if (this._baseURL[this._baseURL.length - 1] !== '/') { 45 | this._baseURL += '/'; 46 | } 47 | this._rsURL = 'http://rs.qbox.me'; 48 | } 49 | 50 | Qiniu.create = function create(options) { 51 | return new Qiniu(options); 52 | }; 53 | 54 | Qiniu.prototype.resourceKey = function (key) { 55 | if (key && key[0] === '/') { 56 | key = key.replace(/^\/+/, ''); 57 | } 58 | return key; 59 | }; 60 | 61 | Qiniu.prototype.resourceURL = function (key) { 62 | if (!key) { 63 | return; 64 | } 65 | 66 | return this._baseURL + this.resourceKey(key); 67 | }; 68 | 69 | Qiniu.prototype.signData = function (data) { 70 | var signature = utility.hmac('sha1', this.options.secretKey, data, 'base64'); 71 | return utils.urlsafe(signature); 72 | }; 73 | 74 | Qiniu.prototype._request = function (url, options, callback) { 75 | if (typeof options === 'function') { 76 | callback = options; 77 | options = { 78 | dataType: 'json' 79 | }; 80 | } 81 | // use global default timeout if options.timeout not set. 82 | options.timeout = options.timeout || this.options.timeout; 83 | options.agent = keepaliveAgent; 84 | urllib.request(url, options, function (err, data, res) { 85 | err = utils.handleResponse(err, data, res); 86 | if (err) { 87 | return callback(err, data, res); 88 | } 89 | callback(null, data, res); 90 | }); 91 | }; 92 | 93 | ['./up', './rs', './image', './doc', './dl', './other'].forEach(function (name) { 94 | var proto = require(name); 95 | for (var k in proto) { 96 | Qiniu.prototype[k] = proto[k]; 97 | } 98 | }); 99 | 100 | module.exports = Qiniu; 101 | -------------------------------------------------------------------------------- /lib/dl.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/dl.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var debug = require('debug')('qn:dl'); 14 | var utility = require('utility'); 15 | 16 | function deadline() { 17 | return utility.timestamp() + 3600; 18 | } 19 | 20 | exports.downloadToken = function (url) { 21 | return this.options.accessKey + ':' + this.signData(url); 22 | }; 23 | 24 | /** 25 | * Download a file 26 | * 27 | * @param {String} key 28 | * @param {Object} [options] 29 | * - {Number} [timeout] default is `global.options.downloadTimeout` one hour. 30 | * - {WriteStream} [writeStream] writable stream to save response data. 31 | * If you use this, callback's data should be null. 32 | * We will just `pipe(ws, {end: true})`. 33 | * @param {Function(err, content, res)} callback 34 | */ 35 | exports.download = function (key, options, callback) { 36 | if (typeof options === 'function') { 37 | callback = options; 38 | options = null; 39 | } 40 | options = options || {}; 41 | options.timeout = options.timeout || this.downloadTimeout; 42 | 43 | var url = this.resourceURL(key) + '?e=' + deadline(); 44 | var token = this.downloadToken(url); 45 | url += '&token=' + token; 46 | debug('download %s, timeout %d', url, options.timeout); 47 | 48 | this._request(url, options, callback); 49 | }; 50 | 51 | exports.saveAsURL = function (key, name) { 52 | return this.resourceURL(key) + '?download/' + encodeURIComponent(name); 53 | }; 54 | -------------------------------------------------------------------------------- /lib/doc.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/doc.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utility = require('utility'); 14 | 15 | /** 16 | * Markdown 转 HTML 17 | * 18 | * @param {String} key 19 | * @param {Object} [options] 20 | * - {Number} mode 0 表示转为完整的 HTML(head+body) 输出; 1 表示只转为HTML Body,缺省值:0 21 | * - {String} css CSS 样式的URL 22 | * @return {String} 23 | */ 24 | exports.md2html = function (key, options) { 25 | var url = this.resourceURL(key) + '?md2html'; 26 | options = options || {}; 27 | if (options.mode) { 28 | url += '/' + options.mode; 29 | } 30 | if (options.css) { 31 | if (!options.mode) { 32 | url += '/0'; 33 | } 34 | url += '/css/' + utility.base64encode(options.css, true); 35 | } 36 | return url; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/image.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/image.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utility = require('utility'); 14 | 15 | exports._getInfo = function (name, key, callback) { 16 | var url = this.resourceURL(key) + '?' + name; 17 | this._request(url, callback); 18 | }; 19 | 20 | /** 21 | * Get Image base infomation. 22 | * @param {String} key 23 | * @param {Function(err, info)} callback 24 | * - {Object} info 25 | * - {String} format "png", "jpeg", "gif", "bmp", etc. 26 | * - {String} colorModel "palette16", "ycbcr", etc. 27 | * - {Number} width 28 | * - {Number} height 29 | */ 30 | exports.imageInfo = function (key, callback) { 31 | this._getInfo('imageInfo', key, callback); 32 | }; 33 | 34 | exports.exif = function (key, callback) { 35 | this._getInfo('exif', key, callback); 36 | }; 37 | 38 | /** 39 | * 生成指定规格的缩略图 40 | * 41 | * @param {String} key 42 | * @param {Object} options thumbnail options. 43 | * - {Number} mode 44 | * =1 表示限定目标缩略图的宽度和高度,放大并从缩略图中央处裁剪为指定 x 大小的图片。 45 | * =2 指定 ,表示限定目标缩略图的长和宽,将缩略图的大小限定在指定的宽高矩形内。 46 | * =2 指定 但不指定 ,表示限定目标缩略图的宽度,高度等比缩略自适应。 47 | * =2 指定 但不指定 ,表示限定目标缩略图的高度,宽度等比缩略自适应。 48 | * - {Number} width 49 | * - {Number} height 50 | * - {Number} [quality] 51 | * - {String} [format] 指定目标缩略图的输出格式,取值范围:jpg, gif, png, webp 等图片格式 52 | * @return {String} thumbnail url 53 | */ 54 | exports.imageView = function (key, options) { 55 | var url = this.resourceURL(key) + '?imageView/' + options.mode; 56 | if (options.width) { 57 | url += '/w/' + options.width; 58 | } 59 | if (options.height) { 60 | url += '/h/' + options.height; 61 | } 62 | if (options.quality) { 63 | url += '/q/' + options.quality; 64 | } 65 | if (options.format) { 66 | url += '/format/' + options.format; 67 | } 68 | return url; 69 | }; 70 | 71 | /** 72 | * 高级图像处理接口(第二版)(缩略、裁剪、旋转、转化) 73 | * 除了能够方便的生成图像缩略图之外,七牛云存储提供了其它高级图像处理接口,包含缩略、裁剪、旋转等一系列的功能. 74 | * 75 | * @param {String} key 76 | * @param {Object} options 77 | * - {String} [thumbnail] 缩略图大小,详解见下。 78 | * - {String} [gravity] 位置偏移,只会使其后的裁剪偏移({offset})受到影响。默认值为 NorthWest(左上角)。 79 | * 可选值:NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast 。 80 | * - {String} [crop] 裁剪大小和偏移,详解见下。 81 | * - {Number} [quality] 图片质量,取值范围是[1, 100]。 82 | * - {Number} [rotate] 旋转角度。 83 | * - {String} [format] 输出格式,可选为jpg, gif, png, bmp, tiff, webp等。 84 | * @return {String} 85 | */ 86 | exports.imageMogr = function (key, options) { 87 | var url = this.resourceURL(key) + '?imageMogr/v2/auto-orient'; 88 | for (var k in options) { 89 | url += '/' + k + '/' + options[k]; 90 | } 91 | return url; 92 | }; 93 | 94 | /** 95 | * 图像水印接口支持两种加水印的方式:图片水印和文字水印。 96 | * 97 | * @param {String} key 98 | * @param {Object} options 99 | * - {Number} mode 100 | * = 1 时,表示图片水印: 101 | * - {String} image 水印图片,使用图片水印时需指定用于水印的远程图片URL。EncodedImageURL = urlsafe_base64_encode(ImageURL) 102 | * = 2 时,表示文字水印: 103 | * - {String} text 水印文本,文字水印时必须。EncodedText = urlsafe_base64_encode(Text) 104 | * - {String} [font] 字体名,若水印文本为非英文字符(比如中文)构成,则必须。EncodedFontName = urlsafe_base64_encode(FontName) 105 | * - {Number} [fontsize] 字体大小,0 表示默认,单位: 缇,等于 1⁄20 磅。 106 | * - {String} [fill] 字体颜色。EncodedTextColor = urlsafe_base64_encode(TextColor)。 107 | * RGB格式,可以是颜色名称(比如 red)或十六进制(比如 #FF0000), 108 | * 参考 [RGB颜色编码表](http://www.rapidtables.com/web/color/RGB_Color.htm) 109 | * - {Number} [dissolve] 透明度,取值范围 1-100,默认值 100,即表示 100%(不透明)。 110 | * - {String} [gravity] 位置,默认值为 SouthEast(右下角)。 111 | * 可选值:NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast 。 112 | * - {Number} [dx] 横向边距,单位:像素(px),默认值为 10。 113 | * - {Number} [dy] 纵向边距,单位:像素(px),默认值为 10。 114 | * @return {String} 115 | */ 116 | exports.watermark = function (key, options) { 117 | var mode = Number(options.mode) || 1; 118 | var url = this.resourceURL(key) + '?watermark/' + mode; 119 | if (mode === 1) { 120 | url += '/image/' + utility.base64encode(options.image, true); 121 | } else { 122 | url += '/text/' + utility.base64encode(options.text, true); 123 | if (options.font) { 124 | url += '/font/' + utility.base64encode(options.font, true); 125 | } 126 | if (options.fontsize) { 127 | url += '/fontsize/' + options.fontsize; 128 | } 129 | if (options.fill) { 130 | url += '/fill/' + utility.base64encode(options.fill, true); 131 | } 132 | } 133 | 134 | var names = ['dissolve', 'gravity', 'dx', 'dy']; 135 | for (var i = 0; i < names.length; i++) { 136 | var name = names[i]; 137 | if (options[name] !== undefined) { 138 | url += '/' + name + '/' + options[name]; 139 | } 140 | } 141 | 142 | return url; 143 | }; 144 | -------------------------------------------------------------------------------- /lib/other.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * QR Code 5 | * @param {String} key 6 | * @param {Number} [mode] 可选值0或1,缺省为0。0表示以当前url生成二维码,1表示以当前URL中的数据生成二维码。 7 | * @param {Number} [level] 冗余度,可选值 L、M、Q,或 H,缺省为 L 8 | * @return {String} qr code url 9 | */ 10 | exports.qrcode = function (key, mode, level) { 11 | var url = this.resourceURL(key) + '?qrcode'; 12 | if (mode !== null && mode !== undefined) { 13 | mode = Number(mode) || 0; 14 | if (mode === 0 || mode === 1) { 15 | url += '/' + mode; 16 | if (level) { 17 | url += '/level/' + level; 18 | } 19 | } 20 | } 21 | return url; 22 | }; 23 | 24 | // TODO: fop 25 | -------------------------------------------------------------------------------- /lib/rs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/rs.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var debug = require('debug')('qn:rs'); 14 | var querystring = require('querystring'); 15 | var utility = require('utility'); 16 | var utils = require('./utils'); 17 | 18 | exports.accessToken = function (pathname, body) { 19 | // http://docs.qiniu.com/api/v6/rs.html#digest-auth 20 | var data = pathname + '\n'; 21 | if (body) { 22 | data += body; 23 | } 24 | var encodedSign = this.signData(data); 25 | return this.options.accessKey + ':' + encodedSign; 26 | }; 27 | 28 | exports.encodeEntryURI = function (key) { 29 | key = this.resourceKey(key); 30 | return utility.base64encode(this.options.bucket + ':' + key, true); 31 | }; 32 | 33 | exports._rsAction = function (data, callback, rsURL) { 34 | var pathname = null; 35 | var body = null; 36 | if (typeof data === 'string') { 37 | pathname = data; 38 | } else { 39 | pathname = data.pathname; 40 | body = data.body; 41 | } 42 | var url = (rsURL || this._rsURL) + pathname; 43 | var options = { 44 | method: 'POST', 45 | headers: { 46 | Authorization: 'QBox ' + this.accessToken(pathname, body), 47 | 'Content-Type': 'application/x-www-form-urlencoded', 48 | 'Content-Length': body ? body.length : 0 49 | }, 50 | dataType: 'json' 51 | }; 52 | if (body) { 53 | options.content = body; 54 | } 55 | debug('rs action %s %j', url, options); 56 | this._request(url, options, callback); 57 | }; 58 | 59 | exports.batch = function (ops, callback) { 60 | var pathname = '/batch'; 61 | var body = querystring.stringify({op: ops}); 62 | this._rsAction({pathname: pathname, body: body}, callback); 63 | }; 64 | 65 | exports._batchAction = function (name, items, callback) { 66 | var ops = []; 67 | for (var i = 0; i < items.length; i++) { 68 | var item = items[i]; 69 | var pathname; 70 | if (typeof item === 'string') { 71 | pathname = '/' + name + '/' + this.encodeEntryURI(item); 72 | } else { 73 | pathname = '/' + name + '/' + this.encodeEntryURI(item[0]) + '/' + this.encodeEntryURI(item[1]); 74 | } 75 | ops.push(pathname); 76 | } 77 | this.batch(ops, callback); 78 | }; 79 | 80 | /** 81 | * Get file stat 82 | * 83 | * @param {String} key 84 | * @param {Function(err, stat)} callback 85 | * - {Object} stat 86 | * - {Number} fsize 87 | * - {String} hash 88 | * - {String} mimeType 89 | * - {Number} putTime 90 | */ 91 | exports.stat = function (key, callback) { 92 | var pathname = '/stat/' + this.encodeEntryURI(key); 93 | this._rsAction(pathname, callback); 94 | }; 95 | 96 | exports.delete = function (key, callback) { 97 | var pathname = '/delete/' + this.encodeEntryURI(key); 98 | debug('delete key:%s, entryURI:%s', key, pathname); 99 | this._rsAction(pathname, callback); 100 | }; 101 | 102 | exports.move = function (src, dest, callback) { 103 | var pathname = '/move/' + this.encodeEntryURI(src) + '/' + this.encodeEntryURI(dest); 104 | this._rsAction(pathname, callback); 105 | }; 106 | 107 | exports.copy = function (src, dest, callback) { 108 | var pathname = '/copy/' + this.encodeEntryURI(src) + '/' + this.encodeEntryURI(dest); 109 | this._rsAction(pathname, callback); 110 | }; 111 | 112 | exports.fetch = function (url, key, callback) { 113 | var encodedURL = utility.base64encode(url); 114 | var pathname = '/fetch/' + encodedURL + '/to/' + this.encodeEntryURI(key); 115 | this._rsAction(pathname, callback, 'http://iovip.qbox.me'); 116 | }; 117 | 118 | exports.batchStat = function (keys, callback) { 119 | this._batchAction('stat', keys, callback); 120 | }; 121 | 122 | exports.batchMove = function (items, callback) { 123 | this._batchAction('move', items, callback); 124 | }; 125 | 126 | exports.batchCopy = function (items, callback) { 127 | this._batchAction('copy', items, callback); 128 | }; 129 | 130 | exports.batchDelete = function (keys, callback) { 131 | this._batchAction('delete', keys, callback); 132 | }; 133 | 134 | /** 135 | * List current bucket files with options. 136 | * 137 | * 请求某个存储空间(bucket)下的文件列表, 138 | * 如果有前缀,可以按前缀(prefix)进行过滤; 139 | * 如果前一次返回marker就表示还有资源,下一步请求需要将marker参数填上。 140 | * 141 | * @param {String|Object} [prefix|options] 142 | * - {String} prefix 指定要过滤出来的前缀, 默认 '/' 143 | * - {String} marker 为服务器上次导出时返回的标记,没有可以不填 144 | * - {Number} limit 单次查询返回的最大条目数,最大不超过1000 145 | * @param {Function(err, result)} callback 146 | * - {Object} result 147 | * - {String} marker 148 | * - {Array} items Stat items list 149 | * - {Number} putTime 150 | * - {String} hash 151 | * - {Number} fsize 152 | * - {String} mimeType 153 | */ 154 | exports.list = function (options, callback) { 155 | var t = typeof options; 156 | if (t === 'function') { 157 | callback = options; 158 | options = null; 159 | } else if (t === 'string') { 160 | options = {prefix: options}; 161 | } 162 | options = options || {}; 163 | if (options.prefix && options.prefix[0] === '/') { 164 | options.prefix = options.prefix.substring(1); 165 | } 166 | 167 | options.bucket = this.options.bucket; 168 | var pathname = '/list?' + querystring.stringify(options); 169 | this._rsAction(pathname, callback, 'http://rsf.qbox.me'); 170 | }; 171 | -------------------------------------------------------------------------------- /lib/up.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/up.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var utility = require('utility'); 14 | var debug = require('debug')('qn:up'); 15 | var fs = require('fs'); 16 | var path = require('path'); 17 | var FormStream = require('formstream'); 18 | var bt = require('buffer-type'); 19 | var utils = require('./utils'); 20 | var version = require('../package.json').version; 21 | 22 | /** 23 | * Create uploadToken 24 | * @see http://docs.qiniu.com/api/v6/put.html#uploadToken 25 | * 26 | * @param {Object} options 27 | * - {String} scope 一般指文件要上传到的目标存储空间(Bucket)。 28 | * 若为”Bucket”,表示限定只能传到该Bucket(仅限于新增文件);若为”Bucket:Key”,表示限定特定的文件,可修改该文件。 29 | * - {Number} [deadline] 定义 uploadToken 的失效时间,Unix时间戳,精确到秒,缺省为 3600 秒 30 | * - {String} [endUser] 给上传的文件添加唯一属主标识,特殊场景下非常有用,比如根据终端用户标识给图片或视频打水印 31 | * - {String} [returnUrl] 设置用于浏览器端文件上传成功后,浏览器执行301跳转的URL,一般为 HTML Form 上传时使用。 32 | * 文件上传成功后会跳转到 returnUrl?query_string, query_string 会包含 returnBody 内容。 33 | * returnUrl 不可与 callbackUrl 同时使用。 34 | * - {String} [returnBody] 文件上传成功后,自定义从 Qiniu-Cloud-Server 最终返回給终端 App-Client 的数据。 35 | * 支持 魔法变量,不可与 callbackBody 同时使用。 36 | * - {String} [callbackBody] 文件上传成功后,Qiniu-Cloud-Server 向 App-Server 发送POST请求的数据。 37 | * 支持 魔法变量 和 自定义变量,不可与 returnBody 同时使用。 38 | * - {String} [callbackUrl] 文件上传成功后,Qiniu-Cloud-Server 向 App-Server 发送POST请求的URL, 39 | * 必须是公网上可以正常进行POST请求并能响应 HTTP Status 200 OK 的有效 URL 40 | * - {String} [asyncOps] 指定文件(图片/音频/视频)上传成功后异步地执行指定的预转操作。 41 | * 每个预转指令是一个API规格字符串,多个预转指令可以使用分号“;”隔开 42 | * @return {String} upload token string 43 | */ 44 | exports.uploadToken = function uploadToken(options) { 45 | options = options || {}; 46 | options.scope = options.scope || this.options.bucket; 47 | options.deadline = options.deadline || (utility.timestamp() + 3600); 48 | var flags = options; 49 | // 步骤2:将 Flags 进行安全编码 50 | var encodedFlags = utility.base64encode(JSON.stringify(flags), true); 51 | 52 | // 步骤3:将编码后的元数据混入私钥进行签名 53 | // 步骤4:将签名摘要值进行安全编码 54 | var encodedSign = this.signData(encodedFlags); 55 | 56 | // 步骤5:连接各字符串,生成上传授权凭证 57 | return this.options.accessKey + ':' + encodedSign + ':' + encodedFlags; 58 | }; 59 | 60 | /** 61 | * Upload file content 62 | * 63 | * @param {String|Buffer|Stream} file content string or buffer, or a Stream instance. 64 | * @param {Object} [options] 65 | * - {String} [key] 标识文件的索引,所在的存储空间内唯一。key可包含斜杠,但不以斜杠开头,比如 a/b/c.jpg 是一个合法的key。 66 | * 若不指定 key,缺省使用文件的 etag(即上传成功后返回的hash值)作为key; 67 | * 此时若 UploadToken 有指定 returnUrl 选项,则文件上传成功后跳转到 returnUrl?query_string, 68 | * query_string 包含key={FileID} 69 | * - {String} [x:custom_field_name] 自定义变量,必须以 x: 开头命名,不限个数。 70 | * 可以在 uploadToken 的 callbackBody 选项中使用 $(x:custom_field_name) 求值。 71 | * - {String} [filename] 72 | * - {String} [contentType] 73 | * - {Number} [size] 74 | * @param {Function(err, result)} callback 75 | * - {Object} result 76 | * - {String} hash 77 | * - {String} key 78 | * - {String} url 79 | */ 80 | exports.upload = function upload(content, options, callback) { 81 | if (typeof options === 'function') { 82 | callback = options; 83 | options = null; 84 | } 85 | if (typeof content === 'string') { 86 | content = new Buffer(content); 87 | } 88 | 89 | options = options || {}; 90 | options.filename = options.filename || options.key; 91 | if (Buffer.isBuffer(content) && !options.filename && !options.contentType) { 92 | // try to guess contentType from buffer 93 | var info = bt(content); 94 | if (info) { 95 | options.contentType = info.type; 96 | options.filename = 'file' + info.extension; 97 | } 98 | } 99 | 100 | // if (!options.filename) { 101 | // options.filename = 'unknowfile'; 102 | // } 103 | 104 | var form = content; 105 | if (!(content instanceof FormStream)) { 106 | form = new FormStream(); 107 | if (Buffer.isBuffer(content)) { 108 | form.buffer('file', content, options.filename, options.contentType); 109 | } else { 110 | // stream 111 | if (content.path) { 112 | // try to get filename in `stream.path` 113 | options['x:filename'] = options['x:filename'] || path.basename(content.path); 114 | if (!options.filename) { 115 | options.filename = options['x:filename']; 116 | } 117 | } 118 | form.stream('file', content, options.filename, options.contentType, options.size); 119 | } 120 | } 121 | 122 | var that = this; 123 | form.field('token', that.uploadToken()); 124 | 125 | if (options.key) { 126 | form.field('key', this.resourceKey(String(options.key))); 127 | } 128 | 129 | for (var k in options) { 130 | if (k.indexOf('x:') === 0) { 131 | form.field(k, String(options[k])); 132 | } 133 | } 134 | var headers = form.headers(); 135 | headers['User-Agent'] = 'qn/' + version; 136 | var reqOptions = { 137 | method: 'POST', 138 | dataType: 'json', 139 | headers: headers, 140 | timeout: options.timeout || that.options.timeout, 141 | stream: form, 142 | }; 143 | 144 | that._request(that._uploadURL, reqOptions, function (err, data, res) { 145 | if (err) { 146 | return callback(err, data, res); 147 | } 148 | 149 | if (data) { 150 | data.url = that.resourceURL(data.key); 151 | } 152 | callback(null, data, res); 153 | }); 154 | }; 155 | 156 | /** 157 | * Upload a file 158 | * 159 | * @param {String} filepath file full path 160 | * @param {Object} [options] options use on `upload()` 161 | * @param {Function(err, result)} callback 162 | */ 163 | exports.uploadFile = function (filepath, options, callback) { 164 | if (typeof options === 'function') { 165 | callback = options; 166 | options = null; 167 | } 168 | 169 | var that = this; 170 | fs.stat(filepath, function (err, stat) { 171 | if (err) { 172 | return callback(err); 173 | } 174 | 175 | var form = new FormStream(); 176 | form.file('file', filepath, stat.size); 177 | options = options || {}; 178 | options['x:filename'] = options['x:filename'] || path.basename(filepath); 179 | options['x:size'] = options['x:size'] || stat.size; 180 | if (stat.mtime) { 181 | options['x:mtime'] = options['x:mtime'] || stat.mtime.getTime() / 1000; 182 | } 183 | if (stat.ctime) { 184 | options['x:ctime'] = options['x:ctime'] || stat.ctime.getTime() / 1000; 185 | } 186 | that.upload(form, options, callback); 187 | }); 188 | }; 189 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - lib/utils.js 3 | * 4 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 5 | * MIT Licensed 6 | */ 7 | 8 | "use strict"; 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | var utility = require('utility'); 15 | 16 | exports.createError = function (statusCode, data) { 17 | // http://docs.qiniu.com/api/v6/put.html#error-code 18 | var msg = data && data.error || ('status ' + statusCode); 19 | var err = new Error(msg); 20 | err.code = statusCode; 21 | if (err.code === 599 && msg === 'file exists') { 22 | err.code = 614; 23 | } 24 | switch (err.code) { 25 | case 400: 26 | err.name = 'QiniuRequestParameterError'; // 请求参数错误 27 | break; 28 | case 401: 29 | err.name = 'QiniuAuthFailError'; // 认证授权失败,可能是 AccessKey/SecretKey 错误或 AccessToken 无效 30 | break; 31 | case 404: 32 | err.name = 'QiniuNotFoundError'; 33 | break; 34 | case 405: 35 | err.name = 'QiniuRequestMethodWrongError'; // 请求方式错误,非预期的请求方式 36 | break; 37 | case 579: 38 | err.name = 'QiniuCallbackAppServerError'; // 文件上传成功,但是回调(callback app-server)失败 39 | break; 40 | case 599: 41 | err.name = 'QiniuServerError'; // 服务端操作失败 42 | break; 43 | case 608: 44 | err.name = 'QiniuFileContentChangeError'; // 文件内容被修改 45 | break; 46 | case 612: 47 | err.name = 'QiniuFileNotExistsError'; // 指定的文件不存在或已经被删除 48 | break; 49 | case 614: 50 | err.name = 'QiniuFileExistsError'; // 文件已存在 51 | break; 52 | case 631: 53 | err.name = 'QiniuBucketNotExistsError'; // 指定的存储空间(Bucket)不存在 54 | break; 55 | case 701: 56 | err.name = 'QiniuDataChunkChecksumError'; // 上传数据块校验出错 57 | break; 58 | default: 59 | err.name = 'QiniuUnknowError'; 60 | break; 61 | } 62 | return err; 63 | }; 64 | 65 | exports.handleResponse = function (err, data, res) { 66 | if (err) { 67 | return err; 68 | } 69 | var statusCode = res.statusCode; 70 | if (statusCode >= 400) { 71 | err = exports.createError(statusCode, data); 72 | return err; 73 | } 74 | }; 75 | 76 | exports.urlsafe = function (s) { 77 | return s.replace(/\//g, '_').replace(/\+/g, '-'); 78 | }; 79 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-modules/qn/ce34145903dbdf1dddf40f6d7ae6bf28eb2d9395/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qn", 3 | "version": "1.3.0", 4 | "description": "Another qiniu API client for Node.js.", 5 | "main": "lib/client.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "test": "mocha -R spec -t 30000 -r should -r should-http test/*.test.js", 11 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- -t 30000 -r should -r should-http test/*.test.js", 12 | "ci": "npm run lint && npm run test-cov", 13 | "lint": "jshint ." 14 | }, 15 | "dependencies": { 16 | "agentkeepalive": "^2.2.0", 17 | "buffer-type": "^0.0.2", 18 | "debug": "^2.2.0", 19 | "formstream": "^1.0.0", 20 | "urllib": "^2.11.0", 21 | "utility": "^1.8.0" 22 | }, 23 | "devDependencies": { 24 | "egg-ci": "^1.0.2", 25 | "istanbul": "*", 26 | "jshint": "^2.9.2", 27 | "mocha": "^3.1.2", 28 | "pedding": "1", 29 | "should": "^11.1.1", 30 | "should-http": "*" 31 | }, 32 | "homepage": "https://github.com/node-modules/qn", 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/node-modules/qn.git", 36 | "web": "https://github.com/node-modules/qn" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/node-modules/qn/issues", 40 | "email": "fengmk2@gmail.com" 41 | }, 42 | "keywords": [ 43 | "qn", 44 | "qiniu", 45 | "qbox", 46 | "node-qiniu" 47 | ], 48 | "engines": { 49 | "node": ">= 0.10.0" 50 | }, 51 | "ci": { 52 | "version": "4, 6" 53 | }, 54 | "author": "fengmk2 (http://fengmk2.com)", 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * qn - test/client.test.js 3 | * 4 | * Copyright(c) fengmk2 and other contributors. 5 | * MIT Licensed 6 | * 7 | * Authors: 8 | * fengmk2 (http://fengmk2.github.com) 9 | */ 10 | 11 | 'use strict'; 12 | 13 | /** 14 | * Module dependencies. 15 | */ 16 | 17 | var should = require('should'); 18 | var fs = require('fs'); 19 | var path = require('path'); 20 | var qn = require('../'); 21 | 22 | describe('client.test.js', function () { 23 | it('should throw TypeError', function () { 24 | (function () { 25 | qn.create(); 26 | }).should.throw('required accessKey, secretKey and bucket'); 27 | (function () { 28 | qn.create({}); 29 | }).should.throw('required accessKey, secretKey and bucket'); 30 | (function () { 31 | qn.create({accessKey: 'accessKey', bucket: 'bucket'}); 32 | }).should.throw('required accessKey, secretKey and bucket'); 33 | }); 34 | 35 | describe('options.uploadURL', function () { 36 | it('should upload from up.qiniug.com work', function (done) { 37 | var options = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'))); 38 | options.uploadURL = 'http://up.qiniug.com/'; 39 | var client = qn.create(options); 40 | client.uploadFile(__filename, function (err, result) { 41 | should.not.exist(err); 42 | should.exist(result); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessKey": "QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm", 3 | "secretKey": "Bxckh6FA-Fbs9Yt3i3cbKVK22UPBmAOHJcL95pGz", 4 | "bucket": "nodesdk", 5 | "domain": "http://nodesdk.qiniudn.com", 6 | "uploadURL": "http://upload.qiniu.com/" 7 | } 8 | -------------------------------------------------------------------------------- /test/dl.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - test/dl.test.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var path = require('path'); 14 | var fs = require('fs'); 15 | var should = require('should'); 16 | var urllib = require('urllib'); 17 | 18 | var CI_ENV = (process.env.TRAVIS ? 'TRAVIS' : process.env.CI_ENV) + '-' + process.version; 19 | 20 | describe('dl.test.js', function () { 21 | var filepath = path.join(__dirname, 'fixtures', 'foo.txt'); 22 | var fooData = fs.readFileSync(filepath); 23 | before(function () { 24 | this.client = require('./qn'); 25 | }); 26 | 27 | before(function (done) { 28 | this.client.uploadFile(filepath, {key: 'qn/test/dl/foo.txt'}, done); 29 | }); 30 | 31 | describe('download()', function () { 32 | it('should download a file', function (done) { 33 | this.client.download('qn/test/dl/foo.txt', function (err, data) { 34 | should.not.exist(err); 35 | data.should.length(fooData.length); 36 | data.toString().should.equal(fooData.toString()); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should download /qn/test/dl/foo.txt file', function (done) { 42 | this.client.download('/qn/test/dl/foo.txt', function (err, data) { 43 | should.not.exist(err); 44 | data.should.length(fooData.length); 45 | data.toString().should.equal(fooData.toString()); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should download a not exists file', function (done) { 51 | this.client.download('qn/test/dl/foo_not_exists.txt', function (err, data) { 52 | should.exist(err); 53 | err.name.should.equal('QiniuNotFoundError'); 54 | err.message.should.equal('status 404'); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should download content to a stream', function (done) { 60 | var savePath = path.join(__dirname, 'tmp_qn_logo.png_' + CI_ENV); 61 | var stream = fs.createWriteStream(savePath); 62 | this.client.download('/qn/test/logo.png', {writeStream: stream}, function (err, data, res) { 63 | should.not.exist(err); 64 | should.not.exist(data); 65 | res.should.status(200); 66 | fs.statSync(savePath).size.should.equal(21944); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('saveAsURL()', function () { 73 | it('should return a saveAs URL', function (done) { 74 | var url = this.client.saveAsURL('qn/test/dl/foo.txt', '哈哈foo.txt'); 75 | url.should.equal(this.client.saveAsURL('/qn/test/dl/foo.txt', '哈哈foo.txt')); 76 | url.should.containEql('.qiniudn.com/qn/test/dl/foo.txt?download/%E5%93%88%E5%93%88foo.txt'); 77 | urllib.request(url, function (err, data, res) { 78 | should.not.exist(err); 79 | data.toString().should.equal(fooData.toString()); 80 | res.should.have.header('content-disposition'); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/doc.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - test/doc.test.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var should = require('should'); 14 | var urllib = require('urllib'); 15 | var path = require('path'); 16 | var config = require('./config.json'); 17 | 18 | describe('doc.test.js', function () { 19 | before(function () { 20 | this.client = require('./qn'); 21 | }); 22 | 23 | before(function (done) { 24 | var that = this; 25 | that.client.uploadFile(path.join(__dirname, 'fixtures', 'readme.md'), {key: 'qn/test/fixtures/readme.md'}, function () { 26 | that.client.uploadFile(path.join(__dirname, 'fixtures', 'github.css'), {key: 'qn/test/fixtures/github.css'}, done); 27 | }); 28 | }); 29 | 30 | describe('md2html()', function () { 31 | it('should convert markdown to html', function (done) { 32 | var url = this.client.md2html('qn/test/fixtures/readme.md'); 33 | url.should.containEql('.qiniudn.com/qn/test/fixtures/readme.md?md2html'); 34 | urllib.request(url, function (err, data, res) { 35 | should.not.exist(err); 36 | data.length.should.above(0); 37 | res.should.status(200); 38 | res.should.have.header('Content-Type', 'text/html'); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('should convert markdown to html with css', function (done) { 44 | var url = this.client.md2html('qn/test/fixtures/readme.md', { 45 | css: config.domain + '/qn/test/fixtures/github.css' 46 | }); 47 | url.should.containEql('.qiniudn.com/qn/test/fixtures/readme.md?md2html/0/css/aHR0cDovL25vZGVzZGsucWluaXVkbi5jb20vcW4vdGVzdC9maXh0dXJlcy9naXRodWIuY3Nz'); 48 | urllib.request(url, function (err, data, res) { 49 | should.not.exist(err); 50 | data.length.should.above(0); 51 | res.should.status(200); 52 | res.should.have.header('Content-Type', 'text/html'); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should convert markdown to only body html', function (done) { 58 | var url = this.client.md2html('qn/test/fixtures/readme.md', {mode: 1}); 59 | url.should.containEql('.qiniudn.com/qn/test/fixtures/readme.md?md2html/1'); 60 | urllib.request(url, function (err, data, res) { 61 | should.not.exist(err); 62 | data.length.should.above(0); 63 | res.should.status(200); 64 | res.should.have.header('Content-Type', 'text/html'); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/fixtures/big.txt: -------------------------------------------------------------------------------- 1 | qn [![Build Status](https://secure.travis-ci.org/fengmk2/qn.png)](http://travis-ci.org/fengmk2/qn) [![Coverage Status](https://coveralls.io/repos/fengmk2/qn/badge.png)](https://coveralls.io/r/fengmk2/qn) [![Build Status](https://drone.io/github.com/fengmk2/qn/status.png)](https://drone.io/github.com/fengmk2/qn/latest) 2 | ======= 3 | 4 | ![logo](https://raw.github.com/fengmk2/qn/master/logo.png) 5 | 6 | Another [qiniu](http://docs.qiniu.com/api/) API client for Node.js. 7 | 8 | ## Install 9 | 10 | ```bash 11 | $ npm install qn 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```js 17 | var qn = require('qn'); 18 | 19 | var client = qn.create({ 20 | accessKey: 'your access key', 21 | secretKey: 'your secret key', 22 | bucket: 'your bucket name', 23 | // domain: 'your domain', // optional 24 | // timeout: 3600000, // default rpc timeout: one hour, optional 25 | }); 26 | 27 | client.upload(filepath, options, function (err, result) { 28 | 29 | }); 30 | ``` 31 | 32 | ## License 33 | 34 | (The MIT License) 35 | 36 | Copyright (c) 2013 fengmk2 <fengmk2@gmail.com> 37 | 38 | Permission is hereby granted, free of charge, to any person obtaining 39 | a copy of this software and associated documentation files (the 40 | 'Software'), to deal in the Software without restriction, including 41 | without limitation the rights to use, copy, modify, merge, publish, 42 | distribute, sublicense, and/or sell copies of the Software, and to 43 | permit persons to whom the Software is furnished to do so, subject to 44 | the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be 47 | included in all copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 50 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 51 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 52 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 53 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 54 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 55 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 56 | -------------------------------------------------------------------------------- /test/fixtures/foo.txt: -------------------------------------------------------------------------------- 1 | foo body -------------------------------------------------------------------------------- /test/fixtures/gogopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-modules/qn/ce34145903dbdf1dddf40f6d7ae6bf28eb2d9395/test/fixtures/gogopher.jpg -------------------------------------------------------------------------------- /test/fixtures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-modules/qn/ce34145903dbdf1dddf40f6d7ae6bf28eb2d9395/test/fixtures/logo.png -------------------------------------------------------------------------------- /test/fixtures/readme.md: -------------------------------------------------------------------------------- 1 | qn [![Build Status](https://secure.travis-ci.org/fengmk2/qn.png)](http://travis-ci.org/fengmk2/qn) [![Coverage Status](https://coveralls.io/repos/fengmk2/qn/badge.png)](https://coveralls.io/r/fengmk2/qn) [![Build Status](https://drone.io/github.com/fengmk2/qn/status.png)](https://drone.io/github.com/fengmk2/qn/latest) 2 | ======= 3 | 4 | ![logo](https://raw.github.com/fengmk2/qn/master/logo.png) 5 | 6 | Another [qiniu](http://docs.qiniu.com/api/) API client for Node.js. 7 | 8 | ## Install 9 | 10 | ```bash 11 | $ npm install qn 12 | ``` 13 | 14 | ## Usage 15 | 16 | ### Upload 17 | 18 | ```js 19 | var qn = require('qn'); 20 | 21 | var client = qn.create({ 22 | accessKey: 'your access key', 23 | secretKey: 'your secret key', 24 | bucket: 'your bucket name', 25 | // domain: 'your domain', // optional 26 | // timeout: 3600000, // default rpc timeout: one hour, optional 27 | }); 28 | 29 | // upload a file with custom key 30 | client.uploadFile(filepath, {key: 'qn/lib/client.js'}, function (err, result) { 31 | console.log(result); 32 | // { 33 | // hash: 'FhGbwBlFASLrZp2d16Am2bP5A9Ut', 34 | // key: 'qn/lib/client.js', 35 | // url: 'http://qtestbucket.qiniudn.com/qn/lib/client.js' 36 | // "x:ctime": "1378150371", 37 | // "x:filename": "client.js", 38 | // "x:mtime": "1378150359", 39 | // "x:size": "21944", 40 | // } 41 | }); 42 | 43 | // upload a stream 44 | client.upload(fs.createReadStream(filepath), function (err, result) { 45 | console.log(result); 46 | // { 47 | // hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 48 | // key: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 49 | // url: 'http://qtestbucket.qiniudn.com/FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 50 | // "x:filename": "foo.txt", 51 | // } 52 | }); 53 | 54 | // you also can upload a string or Buffer directly 55 | client.upload('哈哈', {filename: 'haha.txt'}, function (err, result) { 56 | console.log(result); 57 | // hash: 'FptOdeKmWhcYHUXa5YmNZxJC934B', 58 | // key: 'foobar.txt', 59 | // url: 'http://qtestbucket.qiniudn.com/foobar.txt', 60 | }); 61 | 62 | // xVariables 63 | client.upload(filepath, { 'x:foo': 'bar' }, function (err, result) { 64 | console.log(result); 65 | // hash: 'FptOdeKmWhcYHUXa5YmNZxJC934B', 66 | // key: 'foobar.txt', 67 | // url: 'http://qtestbucket.qiniudn.com/foobar.txt', 68 | // x:foo: 'bar' 69 | }); 70 | ``` 71 | 72 | ### RS Operations 73 | 74 | ```js 75 | // stat 76 | client.stat('foo.txt', function (err, stat) { 77 | console.log(stat); 78 | // fsize: 8, 79 | // hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 80 | // mimeType: 'text/plain', 81 | // putTime: 13783134309588504 82 | }); 83 | 84 | // move 85 | client.move('foo.txt', 'qn/bar.txt', function (err) { 86 | 87 | }); 88 | 89 | // copy 90 | client.copy('foo.txt', 'qn/bar.txt', function (err) { 91 | 92 | }); 93 | 94 | // delete 95 | client.delete('foo.txt', function (err) { 96 | 97 | }); 98 | 99 | // list 100 | client.list('/', function (err, result) { 101 | console.log(result); 102 | // marker: 'eyJjIjowLCJrIjoicW4vYmlnLnR4dCJ9' 103 | // items: [ 104 | // { 105 | // fsize: 21944, 106 | // putTime: 13783144546186030, 107 | // key: 'qn/logo.png', 108 | // hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 109 | // mimeType: 'image/png' 110 | // }, ... 111 | // ] 112 | }); 113 | ``` 114 | 115 | ### Image operations 116 | 117 | ```js 118 | // imageInfo 119 | client.imageInfo('qn/logo.png', function (err, info) { 120 | console.log(info); 121 | // { format: 'png', width: 190, height: 150, colorModel: 'nrgba' } 122 | }); 123 | 124 | // exif 125 | client.exif('qn/logo.png', function (err, exif) { 126 | 127 | }); 128 | 129 | // imageView 130 | var url = client.imageView('qn/logo.png', {mode: 1, width: 100, height: 100, q: 50, format: 'png'}); 131 | // http://qtestbucket.qiniudn.com/qn/logo.png?imageView/1/w/100/h/100/q/50/format/png 132 | 133 | // imageMogr 134 | var url = this.client.imageMogr('qn/fixtures/gogopher.jpg', { 135 | thumbnail: '!50p', 136 | gravity: 'NorthWest', 137 | quality: 50, 138 | rotate: -50, 139 | format: 'gif' 140 | }); 141 | // http://qtestbucket.qiniudn.com/qn/fixtures/gogopher.jpg?imageMogr/v2/auto-orient/thumbnail/!50p/gravity/NorthWest/quality/50/rotate/-50/format/gif 142 | 143 | // watermark 144 | 145 | ``` 146 | 147 | ## TODO 148 | 149 | * [√] RS Operations 150 | * [ ] HTTP Keep-alive 151 | * [√] Image Operations 152 | * [ ] Media Operations 153 | * [ ] Doc Operations 154 | * [ ] Pipeline Operations 155 | * [√] QR code Operations 156 | 157 | ## Authors 158 | 159 | ```bash 160 | $ git summary 161 | 162 | project : qn 163 | repo age : 3 days 164 | active : 3 days 165 | commits : 19 166 | files : 26 167 | authors : 168 | 19 fengmk2 100.0% 169 | ``` 170 | 171 | ## License 172 | 173 | (The MIT License) 174 | 175 | Copyright (c) 2013 fengmk2 <fengmk2@gmail.com> 176 | 177 | Permission is hereby granted, free of charge, to any person obtaining 178 | a copy of this software and associated documentation files (the 179 | 'Software'), to deal in the Software without restriction, including 180 | without limitation the rights to use, copy, modify, merge, publish, 181 | distribute, sublicense, and/or sell copies of the Software, and to 182 | permit persons to whom the Software is furnished to do so, subject to 183 | the following conditions: 184 | 185 | The above copyright notice and this permission notice shall be 186 | included in all copies or substantial portions of the Software. 187 | 188 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 189 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 190 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 191 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 192 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 193 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 194 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 195 | -------------------------------------------------------------------------------- /test/image.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - test/image.test.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var path = require('path'); 14 | var should = require('should'); 15 | var pedding = require('pedding'); 16 | var urllib = require('urllib'); 17 | urllib.TIMEOUT = 10000; 18 | 19 | describe('image.test.js', function () { 20 | before(function () { 21 | this.client = require('./qn'); 22 | }); 23 | 24 | before(function (done) { 25 | done = pedding(3, done); 26 | this.client.uploadFile(path.join(__dirname, 'fixtures', 'logo.png'), {key: 'qn/fixtures/logo.png'}, done); 27 | this.client.uploadFile(path.join(__dirname, 'fixtures', 'big.txt'), {key: 'qn/fixtures/big.txt'}, done); 28 | this.client.uploadFile(path.join(__dirname, 'fixtures', 'gogopher.jpg'), {key: 'qn/fixtures/gogopher.jpg'}, done); 29 | }); 30 | 31 | describe('imageInfo()', function () { 32 | it('should return image info', function (done) { 33 | this.client.imageInfo('qn/fixtures/logo.png', function (err, info) { 34 | should.not.exist(err); 35 | info.should.have.keys('format', 'width', 'height', 'colorModel'); 36 | info.should.eql({ format: 'png', width: 190, height: 150, colorModel: 'nrgba' }); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should return QiniuNotFoundError when get not exists image info', function (done) { 42 | this.client.imageInfo('qn/logo_not_exists.png', function (err, info) { 43 | should.exist(err); 44 | err.name.should.equal('QiniuNotFoundError'); 45 | // err.message.should.equal('Not Found'); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('exif()', function () { 52 | it('should return image exif', function (done) { 53 | this.client.exif('qn/fixtures/gogopher.jpg', function (err, info) { 54 | should.not.exist(err); 55 | should.exist(info); 56 | info.should.have.property('ApertureValue'); 57 | info.ApertureValue.should.eql({ val: '5.00 EV (f/5.7)', type: 5 }); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should return QiniuNotFoundError when get not exists exif info', function (done) { 63 | this.client.exif('qn/fixtures/gogopher_no_exists.jpg', function (err, info) { 64 | should.exist(err); 65 | err.name.should.equal('QiniuNotFoundError'); 66 | // err.message.should.equal('E404'); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('imageView()', function () { 73 | it('should return thumbnail url', function (done) { 74 | var url = this.client.imageView('qn/fixtures/gogopher.jpg', {mode: 1, width: 50, height: 50}); 75 | url.should.match(/\?imageView\/1\/w\/50\/h\/50$/); 76 | urllib.request(url, function (err, data, res) { 77 | should.not.exist(err); 78 | data.length.should.above(0); 79 | res.should.status(200); 80 | res.should.have.header('Content-Type', 'image/jpeg'); 81 | done(); 82 | }); 83 | }); 84 | 85 | it('should return error when file is not image', function (done) { 86 | var url = this.client.imageView('qn/fixtures/big.txt', {mode: 1, width: 50, height: 50}); 87 | url.should.match(/\?imageView\/1\/w\/50\/h\/50$/); 88 | urllib.request(url, function (err, data, res) { 89 | should.not.exist(err); 90 | data.length.should.above(0); 91 | data.toString().should.equal('{"error":"unsupported format:image: unknown format"}'); 92 | res.should.status(400); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should return png thumbnail url', function (done) { 98 | var url = this.client.imageView('qn/fixtures/gogopher.jpg', {mode: 1, width: 50, height: 50, format: 'png'}); 99 | url.should.match(/\?imageView\/1\/w\/50\/h\/50\/format\/png$/); 100 | urllib.request(url, function (err, data, res) { 101 | should.not.exist(err); 102 | data.length.should.above(0); 103 | res.should.status(200); 104 | res.should.have.header('Content-Type', 'image/png'); 105 | done(); 106 | }); 107 | }); 108 | 109 | it('should return gif quality:50 thumbnail url', function (done) { 110 | var url = this.client.imageView('qn/fixtures/gogopher.jpg', {mode: 1, width: 50, height: 50, quality: 50, format: 'gif'}); 111 | url.should.match(/\?imageView\/1\/w\/50\/h\/50\/q\/50\/format\/gif$/); 112 | urllib.request(url, function (err, data, res) { 113 | should.not.exist(err); 114 | data.length.should.above(0); 115 | res.should.status(200); 116 | res.should.have.header('Content-Type', 'image/gif'); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('imageMogr()', function () { 123 | it('should rotate a image', function (done) { 124 | var url = this.client.imageMogr('qn/fixtures/logo.png', { 125 | thumbnail: '!50p', 126 | gravity: 'NorthWest', 127 | quality: 50, 128 | rotate: -50, 129 | format: 'gif' 130 | }); 131 | url.should.match(/\?imageMogr\/v2\/auto\-orient\/thumbnail\/\!50p\/gravity\/NorthWest\/quality\/50\/rotate\/\-50\/format\/gif$/); 132 | urllib.request(url, function (err, data, res) { 133 | should.not.exist(err); 134 | data.length.should.above(0); 135 | res.should.status(200); 136 | res.should.have.header('Content-Type', 'image/gif'); 137 | done(); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('watermark()', function () { 143 | it('should return text watermark image', function (done) { 144 | var url = this.client.watermark('qn/fixtures/logo.png', { 145 | mode: 2, 146 | text: 'Node.js 哈哈', 147 | font: '宋体', 148 | fontsize: 500, 149 | fill: 'red', 150 | dissolve: 100, 151 | gravity: 'SouthEast', 152 | dx: 100, 153 | dy: 90 154 | }); 155 | url.should.containEql('?watermark/2/text/Tm9kZS5qcyDlk4jlk4g=/font/5a6L5L2T/fontsize/500/fill/cmVk/dissolve/100/gravity/SouthEast/dx/100/dy/90'); 156 | urllib.request(url, function (err, data, res) { 157 | should.not.exist(err); 158 | data.length.should.above(0); 159 | res.should.status(200); 160 | res.should.have.header('Content-Type', 'image/png'); 161 | done(); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/other.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - test/other.test.js 3 | * Copyright(c) fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var should = require('should'); 14 | var urllib = require('urllib'); 15 | var pedding = require('pedding'); 16 | 17 | describe('other.test.js', function () { 18 | before(function () { 19 | this.client = require('./qn'); 20 | }); 21 | 22 | describe('qrcode()', function () { 23 | it('should return qrcode url with default params', function (done) { 24 | var url = this.client.qrcode('foo'); 25 | url.should.match(/\?qrcode$/); 26 | urllib.request(url, function (err, data, res) { 27 | should.not.exist(err); 28 | data.length.should.above(0); 29 | res.should.status(200); 30 | res.should.header('Content-Type', 'image/png'); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should return qrcode url with mode 0', function (done) { 36 | done = pedding(3, done); 37 | var url = this.client.qrcode('foo', 0); 38 | url.should.match(/\?qrcode\/0$/); 39 | urllib.request(url, function (err, data, res) { 40 | should.not.exist(err); 41 | data.length.should.above(0); 42 | res.should.status(200); 43 | res.should.header('Content-Type', 'image/png'); 44 | done(); 45 | }); 46 | 47 | var url = this.client.qrcode('foo', 2); 48 | url.should.match(/\?qrcode$/); 49 | urllib.request(url, function (err, data, res) { 50 | should.not.exist(err); 51 | data.length.should.above(0); 52 | res.should.status(200); 53 | res.should.header('Content-Type', 'image/png'); 54 | done(); 55 | }); 56 | 57 | var url = this.client.qrcode('foo', '0'); 58 | url.should.match(/\?qrcode\/0$/); 59 | urllib.request(url, function (err, data, res) { 60 | should.not.exist(err); 61 | data.length.should.above(0); 62 | res.should.status(200); 63 | res.should.header('Content-Type', 'image/png'); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should return qrcode url with mode 1', function (done) { 69 | done = pedding(2, done); 70 | var url = this.client.qrcode('foo', 1); 71 | url.should.match(/\?qrcode\/1$/); 72 | urllib.request(url, function (err, data, res) { 73 | should.not.exist(err); 74 | data.length.should.above(0); 75 | res.should.status(200); 76 | res.should.header('Content-Type', 'image/png'); 77 | done(); 78 | }); 79 | 80 | var url = this.client.qrcode('foo', '1'); 81 | url.should.match(/\?qrcode\/1$/); 82 | urllib.request(url, function (err, data, res) { 83 | should.not.exist(err); 84 | data.length.should.above(0); 85 | res.should.status(200); 86 | res.should.header('Content-Type', 'image/png'); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/qn.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - test/qn.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var qn = require('../'); 14 | var config = require('./config.json'); 15 | 16 | module.exports = qn.create(config); 17 | -------------------------------------------------------------------------------- /test/rs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pedding = require('pedding'); 4 | var should = require('should'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var utility = require('utility'); 8 | var qn = require('../'); 9 | 10 | var fixtures = path.join(__dirname, 'fixtures'); 11 | var CI_ENV = (process.env.TRAVIS ? 'TRAVIS' : process.env.CI_ENV) + '-' + process.version; 12 | var rsOpFile = 'qn/rs_op.' + CI_ENV + '.txt'; 13 | var rsOpFileMove = rsOpFile + '.move.txt'; 14 | var rsOpFileCopy = rsOpFile + '.copy.txt'; 15 | 16 | describe('rs.test.js', function () { 17 | before(function () { 18 | this.client = require('./qn'); 19 | }); 20 | 21 | beforeEach(function (done) { 22 | done = pedding(3, done); 23 | this.client.uploadFile(path.join(fixtures, 'foo.txt'), {key: rsOpFile}, done); 24 | this.client.delete(rsOpFileMove, function () { 25 | done(); 26 | }); 27 | this.client.delete(rsOpFileCopy, function () { 28 | done(); 29 | }); 30 | }); 31 | 32 | describe('stat()', function () { 33 | it('should return stat of ' + rsOpFile, function (done) { 34 | this.client.stat(rsOpFile, function (err, info) { 35 | should.not.exist(err); 36 | should.exist(info); 37 | info.should.have.keys('fsize', 'hash', 'mimeType', 'putTime'); 38 | info.fsize.should.equal(8); 39 | info.hash.should.equal('FvnDEnGu6pjzxxxc5d6IlNMrbDnH'); 40 | info.mimeType.should.equal('text/plain'); 41 | info.putTime.should.match(/^\d+$/); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should return QiniuFileNotExistsError', function (done) { 47 | this.client.stat('qn/rs_op_not_exists.txt', function (err) { 48 | should.exist(err); 49 | err.name.should.equal('QiniuFileNotExistsError'); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('move()', function () { 56 | it('should move ' + rsOpFile + ' to ' + rsOpFileMove, function (done) { 57 | this.client.move(rsOpFile, rsOpFileMove, function (err, result) { 58 | should.not.exist(err); 59 | should.not.exist(result); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should return QiniuFileNotExistsError when move not exist file', function (done) { 65 | this.client.move('qn/rs_op_not_exists.txt', rsOpFileMove, function (err, result) { 66 | should.exist(err); 67 | err.name.should.equal('QiniuFileNotExistsError'); 68 | err.message.should.equal('no such file or directory'); 69 | err.code.should.equal(612); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should return QiniuFileExistsError when src and dest are same', function (done) { 75 | var that = this; 76 | this.client.move(rsOpFile, rsOpFileMove, function (err, result) { 77 | that.client.move(rsOpFileMove, rsOpFileMove, function (err, result) { 78 | should.exist(err); 79 | err.name.should.equal('QiniuFileExistsError'); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | 85 | it('should return QiniuFileExistsError', function (done) { 86 | var that = this; 87 | that.client.move(rsOpFile, rsOpFileMove, function (err, result) { 88 | should.not.exist(err); 89 | should.not.exist(result); 90 | that.client.uploadFile(path.join(fixtures, 'foo.txt'), {key: rsOpFile}, function (err) { 91 | should.not.exist(err); 92 | that.client.move(rsOpFile, rsOpFileMove, function (err, result) { 93 | should.exist(err); 94 | err.name.should.equal('QiniuFileExistsError'); 95 | err.message.should.equal('file exists'); 96 | err.code.should.equal(614); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('copy', function () { 105 | it('should copy ' + rsOpFile + ' to ' + rsOpFileCopy, function (done) { 106 | this.client.copy(rsOpFile, rsOpFileCopy, function (err, result) { 107 | should.not.exist(err); 108 | should.not.exist(result); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('should return QiniuFileNotExistsError when copy not exist file', function (done) { 114 | this.client.copy('qn/rs_op_not_exists.txt', rsOpFileCopy, function (err, result) { 115 | should.exist(err); 116 | err.name.should.equal('QiniuFileNotExistsError'); 117 | err.message.should.equal('no such file or directory'); 118 | err.code.should.equal(612); 119 | done(); 120 | }); 121 | }); 122 | 123 | it('should return QiniuFileExistsError when src and dest are same', function (done) { 124 | var that = this; 125 | this.client.copy(rsOpFile, rsOpFileCopy, function (err, result) { 126 | that.client.copy(rsOpFileCopy, rsOpFileCopy, function (err, result) { 127 | should.exist(err); 128 | err.name.should.equal('QiniuFileExistsError'); 129 | err.message.should.equal('file exists'); 130 | done(); 131 | }); 132 | }); 133 | }); 134 | 135 | it('should return QiniuFileExistsError', function (done) { 136 | var that = this; 137 | that.client.copy(rsOpFile, rsOpFileCopy, function (err, result) { 138 | should.not.exist(err); 139 | should.not.exist(result); 140 | that.client.uploadFile(path.join(fixtures, 'foo.txt'), {key: rsOpFile}, function (err) { 141 | should.not.exist(err); 142 | that.client.copy(rsOpFile, rsOpFileCopy, function (err, result) { 143 | should.exist(err); 144 | err.name.should.equal('QiniuFileExistsError'); 145 | err.message.should.equal('file exists'); 146 | err.code.should.equal(614); 147 | done(); 148 | }); 149 | }); 150 | }); 151 | }); 152 | }); 153 | 154 | describe('delete', function () { 155 | it('should delete "' + rsOpFile + '" file', function (done) { 156 | this.client.delete(rsOpFile, function (err, result) { 157 | should.not.exist(err); 158 | should.not.exist(result); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('should delete "' + '/' + rsOpFile + '" file', function (done) { 164 | this.client.delete('/' + rsOpFile, function (err, result) { 165 | should.not.exist(err); 166 | should.not.exist(result); 167 | done(); 168 | }); 169 | }); 170 | 171 | it('should delete not exists file return QiniuFileNotExistsError', function (done) { 172 | this.client.delete('qn/rs_op_not_exists.txt', function (err, result) { 173 | should.exist(err); 174 | err.name.should.equal('QiniuFileNotExistsError'); 175 | err.message.should.equal('no such file or directory'); 176 | err.code.should.equal(612); 177 | done(); 178 | }); 179 | }); 180 | }); 181 | 182 | describe('list()', function () { 183 | it('should list / files', function (done) { 184 | done = pedding(3, done); 185 | this.client.list('/', function (err, result) { 186 | should.not.exist(err); 187 | should.exist(result); 188 | result.should.have.property('items').with.be.an.instanceof(Array); 189 | result.items.length.should.above(0); 190 | result.items.forEach(function (item) { 191 | // item.should.have.keys('fsize', 'putTime', 'key', 'hash', 'mimeType'); 192 | item.fsize.should.be.a.Number; 193 | item.putTime.should.be.a.Number; 194 | item.key.should.be.a.String; 195 | item.hash.should.be.a.String; 196 | item.mimeType.should.be.a.String; 197 | }); 198 | done(); 199 | }); 200 | 201 | this.client.list('', function (err, result) { 202 | should.not.exist(err); 203 | should.exist(result); 204 | result.should.have.property('items').with.be.an.instanceof(Array); 205 | result.items.length.should.above(0); 206 | result.items.forEach(function (item) { 207 | // item.should.have.keys('fsize', 'putTime', 'key', 'hash', 'mimeType'); 208 | item.fsize.should.be.a.Number; 209 | item.putTime.should.be.a.Number; 210 | item.key.should.be.a.String; 211 | item.hash.should.be.a.String; 212 | item.mimeType.should.be.a.String; 213 | }); 214 | done(); 215 | }); 216 | 217 | this.client.list(function (err, result) { 218 | should.not.exist(err); 219 | should.exist(result); 220 | result.should.have.property('items').with.be.an.instanceof(Array); 221 | result.items.length.should.above(0); 222 | result.items.forEach(function (item) { 223 | // item.should.have.keys('fsize', 'putTime', 'key', 'hash', 'mimeType'); 224 | item.fsize.should.be.a.Number; 225 | item.putTime.should.be.a.Number; 226 | item.key.should.be.a.String; 227 | item.hash.should.be.a.String; 228 | item.mimeType.should.be.a.String; 229 | }); 230 | done(); 231 | }); 232 | }); 233 | 234 | it('should list /qn limit 5, and next page marker work', function (done) { 235 | var that = this; 236 | this.client.list({prefix: '/qn', limit: 5}, function (err, result) { 237 | should.not.exist(err); 238 | result.items.should.length(5); 239 | result.marker.should.be.a.String; 240 | // next page 241 | that.client.list({prefix: '/qn', limit: 5, marker: result.marker}, function (err, result2) { 242 | should.not.exist(err); 243 | result2.items.should.length(5); 244 | result2.marker.should.be.a.String; 245 | done(); 246 | }); 247 | }); 248 | }); 249 | 250 | it.skip('should limit 0 equal not limit', function (done) { 251 | this.client.list({prefix: 'qn/', limit: 0}, function (err, result) { 252 | should.not.exist(err); 253 | should.exist(result); 254 | result.should.have.property('items').with.be.an.Array; 255 | result.items.length.should.above(0); 256 | result.items.forEach(function (item) { 257 | item.should.have.keys('fsize', 'putTime', 'key', 'hash', 'mimeType'); 258 | item.fsize.should.be.a.Number; 259 | item.putTime.should.be.a.Number; 260 | item.key.should.be.a.String; 261 | item.hash.should.be.a.String; 262 | item.mimeType.should.be.a.String; 263 | }); 264 | done(); 265 | }); 266 | }); 267 | 268 | }); 269 | 270 | describe('batchStat()', function () { 271 | it('should show 2 files stats', function (done) { 272 | this.client.batchStat(['qn/logo.png', 'qn/big.txt', 'not-exists-file'], function (err, results) { 273 | should.not.exist(err); 274 | should.exist(results); 275 | results.should.length(3); 276 | results[0].code.should.equal(200); 277 | results[0].data.mimeType.should.equal('image/png'); 278 | results[1].code.should.equal(200); 279 | results[1].data.mimeType.should.equal('text/plain'); 280 | results[2].should.eql({ code: 612, data: { error: 'no such file or directory' } }); 281 | done(); 282 | }); 283 | }); 284 | }); 285 | 286 | describe('batchMove()', function () { 287 | it('should move 2 files', function (done) { 288 | this.client.batchMove([ 289 | [rsOpFile, rsOpFileMove], 290 | ['qn/rs_op_batch_notexists.txt', 'qn/rs_op_batch_move_notexists.txt'], 291 | ], function (err, results) { 292 | should.not.exist(err); 293 | should.exist(results); 294 | results.should.length(2); 295 | results[0].code.should.equal(200); 296 | should.not.exist(results[0].data); 297 | results[1].should.eql({ code: 612, data: { error: 'no such file or directory' } }); 298 | done(); 299 | }); 300 | }); 301 | }); 302 | 303 | describe('batchCopy()', function () { 304 | it('should move 2 files', function (done) { 305 | this.client.batchCopy([ 306 | [rsOpFile, rsOpFileCopy], 307 | ['qn/rs_op_batch_notexists.txt', 'qn/rs_op_batch_copy_notexists.txt'], 308 | ], function (err, results) { 309 | should.not.exist(err); 310 | should.exist(results); 311 | results.should.length(2); 312 | results[0].code.should.equal(200); 313 | should.not.exist(results[0].data); 314 | results[1].code.should.equal(612); 315 | results[1].data.error.should.equal('no such file or directory'); 316 | done(); 317 | }); 318 | }); 319 | }); 320 | 321 | describe('batchDelete()', function () { 322 | it('should move 2 files', function (done) { 323 | this.client.batchDelete([ 324 | rsOpFile, 325 | 'qn/rs_op_batch_notexists.txt', 326 | ], function (err, results) { 327 | should.not.exist(err); 328 | should.exist(results); 329 | results.should.length(2); 330 | results[0].code.should.equal(200); 331 | should.not.exist(results[0].data); 332 | results[1].should.eql({ code: 612, data: { error: 'no such file or directory' } }); 333 | done(); 334 | }); 335 | }); 336 | }); 337 | }); 338 | -------------------------------------------------------------------------------- /test/up.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * qn - test/up.test.js 3 | * Copyright(c) 2013 fengmk2 (http://fengmk2.github.com) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var pedding = require('pedding'); 14 | var should = require('should'); 15 | var fs = require('fs'); 16 | var path = require('path'); 17 | var utility = require('utility'); 18 | var qn = require('../'); 19 | var config = require('./config.json'); 20 | 21 | var root = path.dirname(__dirname); 22 | var fixtures = path.join(__dirname, 'fixtures'); 23 | var imagepath = path.join(path.dirname(__dirname), 'logo.png'); 24 | var imageContent = fs.readFileSync(imagepath); 25 | 26 | describe('up.test.js', function () { 27 | before(function () { 28 | this.client = require('./qn'); 29 | }); 30 | 31 | describe('TypeError', function () { 32 | (function () { 33 | qn.create(); 34 | }).should.throw('required accessKey, secretKey and bucket'); 35 | (function () { 36 | qn.create({}); 37 | }).should.throw('required accessKey, secretKey and bucket'); 38 | (function () { 39 | qn.create({accessKey: 'accessKey', bucket: 'bucket'}); 40 | }).should.throw('required accessKey, secretKey and bucket'); 41 | }); 42 | 43 | describe('uploadToken()', function () { 44 | it('should return a upload token with default empty options', function () { 45 | var token = this.client.uploadToken(); 46 | should.exist(token); 47 | }); 48 | 49 | it('should return a upload token with a deadline', function () { 50 | var token = this.client.uploadToken({ 51 | deadline: utility.timestamp() + 10 52 | }); 53 | should.exist(token); 54 | }); 55 | }); 56 | 57 | describe('uploadFile()', function () { 58 | it('should upload the logo file', function (done) { 59 | this.client.uploadFile(imagepath, function (err, result) { 60 | should.not.exist(err); 61 | result.should.have.keys('hash', 'key', 'url', 'x:ctime', 'x:filename', 'x:mtime', 'x:size'); 62 | result.hash.should.equal('FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki'); 63 | result.key.should.equal('FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki'); 64 | result.url.should.containEql('.qiniudn.com/FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki'); 65 | result["x:filename"].should.equal('logo.png'); 66 | result["x:ctime"].should.be.match(/^[\d\.]+$/); 67 | result["x:mtime"].should.be.match(/^[\d\.]+$/); 68 | result["x:size"].should.equal('21944'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should upload the logo file with key', function (done) { 74 | this.client.uploadFile(imagepath, {key: 'qn-logo.png'}, function (err, result) { 75 | should.not.exist(err); 76 | result.should.have.keys('hash', 'key', 'url', 'x:ctime', 'x:filename', 'x:mtime', 'x:size'); 77 | result.hash.should.equal('FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki'); 78 | result.key.should.equal('qn-logo.png'); 79 | result.url.should.containEql('.qiniudn.com/qn-logo.png'); 80 | result["x:filename"].should.equal('logo.png'); 81 | result["x:ctime"].should.be.match(/^[\d\.]+$/); 82 | result["x:mtime"].should.be.match(/^[\d\.]+$/); 83 | result["x:size"].should.equal('21944'); 84 | done(); 85 | }); 86 | }); 87 | 88 | it('should upload the logo file with key: /qn/test/logo.png', function (done) { 89 | var that = this; 90 | this.client.uploadFile(imagepath, {key: '/qn/test/logo.png'}, function (err, result) { 91 | should.not.exist(err); 92 | result.should.have.keys('hash', 'key', 'url', 'x:ctime', 'x:filename', 'x:mtime', 'x:size'); 93 | result.hash.should.equal('FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki'); 94 | result.key.should.equal('qn/test/logo.png'); 95 | result.url.should.equal(config.domain + '/qn/test/logo.png'); 96 | result["x:filename"].should.equal('logo.png'); 97 | result["x:ctime"].should.be.match(/^[\d\.]+$/); 98 | result["x:mtime"].should.be.match(/^[\d\.]+$/); 99 | result["x:size"].should.equal('21944'); 100 | 101 | that.client.list('/qn/test/', function (err, result) { 102 | should.not.exist(err); 103 | result.items.length.should.above(0); 104 | done(); 105 | }); 106 | }); 107 | }); 108 | 109 | it('should return err when file not exist', function (done) { 110 | this.client.uploadFile(imagepath + 'notexists', function (err) { 111 | should.exist(err); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('upload()', function () { 118 | it('should upload a foo string with filename', function (done) { 119 | this.client.upload('foo bar 哈哈', {filename: 'foo'}, function (err, result) { 120 | should.not.exist(err); 121 | should.exist(result); 122 | result.hash.should.equal('FiuFB_kYboxBnU6VCMirPzLtIpIq'); 123 | result.key.should.equal('FiuFB_kYboxBnU6VCMirPzLtIpIq'); 124 | result.url.should.containEql('.qiniudn.com/FiuFB_kYboxBnU6VCMirPzLtIpIq'); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('should success when upload same key and same content', function (done) { 130 | this.client.upload('foo bar 哈哈', {filename: 'foo', key: 'foo'}, function (err, result) { 131 | should.not.exist(err); 132 | should.exist(result); 133 | result.hash.should.equal('FiuFB_kYboxBnU6VCMirPzLtIpIq'); 134 | result.key.should.equal('foo'); 135 | result.url.should.containEql('.qiniudn.com/foo'); 136 | done(); 137 | }); 138 | }); 139 | 140 | it('should upload a foobar string with key and x:headers', function (done) { 141 | this.client.upload('foo foo bar 哈哈', { 142 | key: 'foobar.txt', 143 | 'x:foo': 'bar哈哈', 144 | "x:filepath":"/Users/mk2/git/qn/logo.png", 145 | "x:filename":"logo.png", 146 | "x:size": 21944, 147 | "x:mtime": Date.parse("2013-09-02T19:32:39.000Z"), 148 | "x:ctime": "2013-09-02T19:32:51.000Z" 149 | }, function (err, result) { 150 | should.not.exist(err); 151 | should.exist(result); 152 | result.should.eql({ 153 | hash: 'FptOdeKmWhcYHUXa5YmNZxJC934B', 154 | key: 'foobar.txt', 155 | url: config.domain + '/foobar.txt', 156 | 'x:foo': 'bar哈哈', 157 | "x:ctime": "2013-09-02T19:32:51.000Z", 158 | "x:filename": "logo.png", 159 | "x:filepath": "/Users/mk2/git/qn/logo.png", 160 | "x:mtime": "1378150359000", 161 | "x:size": "21944", 162 | }); 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should upload empty string', function (done) { 168 | this.client.upload('', {filename: 'empty'}, function (err, result) { 169 | should.not.exist(err); 170 | should.exist(result); 171 | result.should.eql({ 172 | hash: 'Fto5o-5ea0sNMlW_75VgGJCv2AcJ', 173 | key: 'Fto5o-5ea0sNMlW_75VgGJCv2AcJ', 174 | url: config.domain + '/Fto5o-5ea0sNMlW_75VgGJCv2AcJ' 175 | }); 176 | done(); 177 | }); 178 | }); 179 | 180 | it.skip('should upload a text content with no filename and no contentType return error', function (done) { 181 | var txtpath = path.join(fixtures, 'big.txt'); 182 | this.client.upload(fs.readFileSync(txtpath), function (err, result) { 183 | console.log(result); 184 | should.exist(err); 185 | err.name.should.equal('QiniuRequestParameterError'); 186 | err.message.should.equal('file is not specified in multipart'); 187 | done(); 188 | }); 189 | }); 190 | 191 | it.skip('should upload a text content with no filename and has contentType return error', function (done) { 192 | var txtpath = path.join(fixtures, 'big.txt'); 193 | this.client.upload(fs.readFileSync(txtpath), {contentType: 'text/plain'}, function (err, result) { 194 | should.exist(err); 195 | err.name.should.equal('QiniuRequestParameterError'); 196 | err.message.should.equal('file is not specified in multipart'); 197 | done(); 198 | }); 199 | }); 200 | 201 | it('should upload a text content with filename', function (done) { 202 | var txtpath = path.join(fixtures, 'big.txt'); 203 | this.client.upload(fs.readFileSync(txtpath), {filename: 'big.txt'}, function (err, result) { 204 | should.not.exist(err); 205 | should.exist(result); 206 | result.should.eql({ 207 | hash: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 208 | key: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 209 | url: config.domain + '/FhRP7GIsuzMrSOp0AQnVVymMNsXJ' 210 | }); 211 | done(); 212 | }); 213 | }); 214 | 215 | it('should upload a text content with key', function (done) { 216 | var txtpath = path.join(fixtures, 'big.txt'); 217 | this.client.upload(fs.readFileSync(txtpath), {key: 'qn/big.txt'}, function (err, result) { 218 | should.not.exist(err); 219 | should.exist(result); 220 | result.should.eql({ 221 | hash: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 222 | key: 'qn/big.txt', 223 | url: config.domain + '/qn/big.txt' 224 | }); 225 | done(); 226 | }); 227 | }); 228 | 229 | it('should upload a image content with no filename and no contentType', function (done) { 230 | this.client.upload(imageContent, function (err, result) { 231 | should.not.exist(err); 232 | should.exist(result); 233 | result.should.eql({ 234 | hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 235 | key: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 236 | url: config.domain + '/FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki' 237 | }); 238 | done(); 239 | }); 240 | }); 241 | 242 | it('should upload a image content with filename', function (done) { 243 | this.client.upload(imageContent, {filename: 'logo.png'}, function (err, result) { 244 | should.not.exist(err); 245 | should.exist(result); 246 | result.should.eql({ 247 | hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 248 | key: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 249 | url: config.domain + '/FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki' 250 | }); 251 | done(); 252 | }); 253 | }); 254 | 255 | it('should upload a image content with qn/logo.png key', function (done) { 256 | this.client.upload(imageContent, {key: 'qn/logo.png'}, function (err, result) { 257 | should.not.exist(err); 258 | should.exist(result); 259 | result.should.eql({ 260 | hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 261 | key: 'qn/logo.png', 262 | url: config.domain + '/qn/logo.png' 263 | }); 264 | done(); 265 | }); 266 | }); 267 | 268 | it('should upload a big text stream with size', function (done) { 269 | var txtpath = path.join(fixtures, 'big.txt'); 270 | this.client.upload(fs.createReadStream(txtpath), 271 | {filename: 'big.txt', size: fs.statSync(txtpath).size}, 272 | function (err, result) { 273 | should.not.exist(err); 274 | should.exist(result); 275 | result.should.eql({ 276 | hash: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 277 | key: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 278 | url: config.domain + '/FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 279 | "x:filename": "big.txt", 280 | }); 281 | done(); 282 | }); 283 | }); 284 | 285 | it('should upload a small text stream with no size', function (done) { 286 | var txtpath = path.join(fixtures, 'foo.txt'); 287 | this.client.upload(fs.createReadStream(txtpath), {filename: 'foo', key: 'foo_chunked.txt'}, 288 | function (err, result) { 289 | should.not.exist(err); 290 | should.exist(result); 291 | result.should.eql({ 292 | hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 293 | key: 'foo_chunked.txt', 294 | url: config.domain + '/foo_chunked.txt', 295 | "x:filename": "foo.txt", 296 | }); 297 | done(); 298 | }); 299 | }); 300 | 301 | it('should upload a small text stream with no size and no options', function (done) { 302 | var txtpath = path.join(fixtures, 'foo.txt'); 303 | this.client.upload(fs.createReadStream(txtpath), function (err, result) { 304 | should.not.exist(err); 305 | should.exist(result); 306 | result.should.eql({ 307 | hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 308 | key: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 309 | url: config.domain + '/FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 310 | "x:filename": "foo.txt", 311 | }); 312 | done(); 313 | }); 314 | }); 315 | 316 | it('should upload a small text stream with size', function (done) { 317 | var txtpath = path.join(fixtures, 'foo.txt'); 318 | var size = fs.statSync(txtpath).size; 319 | this.client.upload(fs.createReadStream(txtpath), {filename: 'foo.txt', size: size}, 320 | function (err, result) { 321 | should.not.exist(err); 322 | should.exist(result); 323 | result.should.eql({ 324 | hash: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 325 | key: 'FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 326 | url: config.domain + '/FvnDEnGu6pjzxxxc5d6IlNMrbDnH', 327 | "x:filename": "foo.txt", 328 | }); 329 | done(); 330 | }); 331 | }); 332 | 333 | it('should upload a big text stream with no size', function (done) { 334 | var txtpath = path.join(fixtures, 'big.txt'); 335 | this.client.upload(fs.createReadStream(txtpath), {filename: 'big.txt'}, 336 | function (err, result) { 337 | should.not.exist(err); 338 | should.exist(result); 339 | result.should.eql({ 340 | hash: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 341 | key: 'FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 342 | url: config.domain + '/FhRP7GIsuzMrSOp0AQnVVymMNsXJ', 343 | "x:filename": "big.txt", 344 | }); 345 | done(); 346 | }); 347 | }); 348 | 349 | it('should upload a image stream with no key', function (done) { 350 | this.client.upload(fs.createReadStream(imagepath), {filename: 'logo.png'}, function (err, result) { 351 | should.not.exist(err); 352 | should.exist(result); 353 | result.should.eql({ 354 | hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 355 | key: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 356 | url: config.domain + '/FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 357 | "x:filename": "logo.png", 358 | }); 359 | done(); 360 | }); 361 | }); 362 | 363 | it('should upload a file stream', function (done) { 364 | var filepath = path.join(root, 'lib', 'client.js'); 365 | var that = this; 366 | that.client.delete('qn/lib/client.js', function () { 367 | that.client.upload(fs.createReadStream(filepath), {filename: filepath, key: 'qn/lib/client.js'}, function (err, result) { 368 | should.not.exist(err); 369 | should.exist(result); 370 | result.key.should.equal('qn/lib/client.js'); 371 | // result.should.eql({ 372 | // hash: 'FhGbwBlFASLrZp2d16Am2bP5A9Ut', 373 | // key: 'qn/lib/client.js', 374 | // url: 'http://qtestbucket.qiniudn.com/qn/lib/client.js' 375 | // }) 376 | done(); 377 | }); 378 | }); 379 | }); 380 | 381 | it('should upload a image stream with qn/logo.png key', function (done) { 382 | this.client.upload(fs.createReadStream(imagepath), {key: 'qn/logo_chunked.png'}, function (err, result) { 383 | should.not.exist(err); 384 | should.exist(result); 385 | result.should.eql({ 386 | hash: 'FvzqAF1oWlYgQ9t62k_xn_mzZ1Ki', 387 | key: 'qn/logo_chunked.png', 388 | url: config.domain + '/qn/logo_chunked.png', 389 | "x:filename": "logo.png", 390 | }); 391 | done(); 392 | }); 393 | }); 394 | }); 395 | }); 396 | --------------------------------------------------------------------------------