/** * Created by ETiV on 4/27/14. */ 'use strict'; (function () { /** * CONST of UPYUN API HOSTS * @type {string[]} */ var API_HOSTS = [ 'v0.api.upyun.com', 'v1.api.upyun.com', 'v2.api.upyun.com', 'v3.api.upyun.com' ]; //noinspection JSUnusedLocalSymbols /** * CONST of Response Headers * @type {Object} Dictionary of response header and body */ var GENERAL_STATUS_HEADER = { '200:ok': '操作成功', //

400 Bad Request

Need a bucket name '400:badrequest': '错误请求(如 URL 缺少空间名)', // ?? '401:unauthorized': '访问未授权', //

401 Unauthorized

Sign error (sign = md5(METHOD&URI&DATE&CONTENT_LENGTH&MD5(PASSWORD))) '401:signerror': '签名错误(操作员和密码,或签名格式错误)', //

401 Unauthorized

Sign error (sign = md5(METHOD&URI&DATE&CONTENT_LENGTH&MD5(PASSWORD))) , Need Date Header! '401:needdateheader': '发起的请求缺少 Date 头信息', //

401 Unauthorized

Sign error (sign = md5(METHOD&URI&DATE&CONTENT_LENGTH&MD5(PASSWORD))) , Date offset error! '401:dateoffseterror': '发起请求的服务器时间错误,请检查服务器时间是否与世界时间一致', // ?? 403 Not Access '403:notaccess': '权限错误(如非图片文件上传到图片空间)', '403:filesizetoomax': '单个文件超出大小(100MB 以内)', '403:notapicturefile': '图片类空间错误码,非图片文件或图片文件格式错误。针对图片空间只允许上传 jpg/png/gif/bmp/tif 格式。', '403:picturesizetoomax': '图片类空间错误码,图片尺寸太大。针对图片空间,图片总像素在 200000000 以内。', '403:bucketfull': '空间已用满', '403:bucketblocked': '空间被禁用,请联系管理员', '403:userblocked': '操作员被禁用', '403:imagerotateinvalidparameters': '图片旋转参数错误', '403:imagecropinvalidparameters': '图片裁剪参数错误', // 404 Not Found '404:notfound': '获取文件或目录不存在;上传文件或目录时上级目录不存在', // 406 Not Acceptable(path) '406:notacceptable(path)': '目录错误(创建目录时已存在同名文件;或上传文件时存在同名目录)', '503:systemerror': '系统错误' }; /** * Libraries and helper functions */ var request = require('request'); var async = require('async'); var lib_path = require('path'); var lib_crypto = require('crypto'); var lib_util = require('util'); var str_gmt = function () { return (new Date()).toUTCString(); }; var str_md5 = function (buffer) { var md5 = lib_crypto.createHash('md5'); md5.update(buffer); return md5.digest('hex'); }; var str_signature = function (method, uri, date_gmt, buffer_length, password) { return str_md5( new Buffer( method + '&' + uri + '&' + date_gmt + '&' + String(buffer_length) + '&' + str_md5(new Buffer(String(password))) ) ); }; var str_signature_purge = function (url_list, bucket, date_gmt, password) { var urls = url_list.join('\n'); var token = str_md5(new Buffer(password)); return str_md5(new Buffer(urls + '&' + bucket + '&' + date_gmt + '&' + token)); }; /** * do access to UPYUN REST API * @param {String} method * @param {String} uri * @param {Buffer|Function} [buffer] * @param {Object|Function} [headers] * @param {Function} cb */ var do_access = function (method, uri, buffer, headers, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (arguments.length >= 4 && !(buffer instanceof Buffer)) { cb(new Error('Local Status: 0x80007F11\tBody: Argument 3 Must Be An Instance Of Buffer.')); return; } if (arguments.length < 3) { cb(new Error('Local Status: 0x80007F12\tBody: No Enough Arguments To Access UPYun API.')); return; } if (arguments.length === 3) { if (typeof buffer === 'function') { cb = buffer; buffer = null; headers = null; } } if (arguments.length === 4) { if (typeof headers === 'function') { cb = headers; headers = null; } } method = method.toUpperCase(); headers = headers || {}; var date_gmt = str_gmt(); buffer = buffer || []; var signature = str_signature(method, uri, date_gmt, buffer.length, this.pass); // console.log(':: DEBUG :: do_access -> signature', signature); //noinspection JSUndefinedPropertyAssignment headers.Date = date_gmt; //noinspection JSUndefinedPropertyAssignment headers.Authorization = 'UpYun ' + this.user + ':' + signature; request({ method: method, uri: 'http://' + this.host + uri, body: buffer.length > 0 ? buffer : undefined, headers: headers }, function (req, resp, body) { // callback // console.log(':: DEBUG :: do_access -> request.callback -> body =', body); if (!resp) { cb(new Error('Local Status: 0x80007FFF\tBody: Network Error.')); return; } var status = resp.statusCode; if (status === 200) { cb(null, resp.headers, body); } else { body = 'HTTP Status: ' + status + '\tBody: ' + body; cb(new Error(body)); } // console.log(resp.headers); }); }; var UPYUN = function (bucket, user, pass) { this.bucket = bucket; this.user = user; this.pass = pass; this.host = API_HOSTS[0]; this.validate = false; this.iDOReallyWantToDestroyDirectories = false; }; //noinspection JSUnusedGlobalSymbols /** * Write(Upload) some contents to a certain path * @param {String} path * @param {String|Buffer} buffer * @param {Function} cb */ UPYUN.prototype.writeFile = function (path, buffer, cb) { var headers = {}; if (typeof buffer === 'string') { buffer = new Buffer(buffer); } if (this.validate) { headers['content-md5'] = str_md5(buffer); } var dirs = path.split('/'); var depth = 0; for (var i = 0; i < dirs.length; i++) { if (dirs[i] !== '') { depth++; } } if (depth - 1 > 10) { cb('Too Many Directories. The Dirs Depth Should Not Larger Than 10.'); return; } headers.mkdir = 'true'; if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } do_access.apply(this, [ 'PUT', '/' + this.bucket + path, buffer, headers, function (err) { cb(err); // console.log(':: DEBUG :: writeFile -> callback -> err =', err); } ] ); }; //noinspection JSUnusedGlobalSymbols /** * Fetch(Download) a certain file at the path * @param {String} path * @param {Function} cb */ UPYUN.prototype.fetchFile = function (path, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } do_access.apply(this, [ 'GET', '/' + this.bucket + path, function (err, headers /* useless */, body) { cb(err, body || ''); // console.log(':: DEBUG :: fetchFile -> callback -> err =', err); } ]); }; //noinspection JSUnusedGlobalSymbols /** * Inspect a certain file or directory by given path * @param {String} path can be a path of file or directory * @param {Function} cb */ UPYUN.prototype.inspect = function (path, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } do_access.apply(this, [ 'HEAD', '/' + this.bucket + path, function (err, headers/*, body /* useless */) { if (err) { cb(err); return; } /** * headers when the target is a Folder * 'x-upyun-file-type': 'folder', * 'x-upyun-file-date': '1396411022' */ /** * headers when the target is a File * 'x-upyun-file-type': 'file', * 'x-upyun-file-size': '5', * 'x-upyun-file-date': '1398563107' */ var entity = {}; entity.path = path; entity.name = lib_path.basename(path); entity.type = String(headers['x-upyun-file-type']).toLowerCase(); entity.time = headers['x-upyun-file-date']; if (!!headers['x-upyun-file-size']) { entity.size = headers['x-upyun-file-size']; } else { entity.size = 0; } cb(err, entity); // console.log(':: DEBUG :: inspect -> callback -> err =', err); // console.log(':: DEBUG :: inspect -> callback -> headers =', headers); } ]); }; //noinspection JSUnusedGlobalSymbols /** * Remove a certain file at the path * @param {String} path * @param {Function} cb */ UPYUN.prototype.removeFile = function (path, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } do_access.apply(this, [ 'DELETE', '/' + this.bucket + path, function (err/*, headers /* useless *//*, body /* useless */) { cb(err); // console.log(':: DEBUG :: removeFile -> callback -> err =', err); } ]); }; //noinspection JSUnusedGlobalSymbols /** * Create directories by a full path, with 10 levels of max depth * @param {String} path * @param {Function} cb */ UPYUN.prototype.createDirs = function (path, cb) { var headers = {}; if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } if (path[path.length - 1] !== '/') { path = lib_path.join(path, '/'); } headers.folder = 'true'; headers.mkdir = 'true'; var dirs = path.split('/'); var depth = 0; for (var i = 0; i < dirs.length; i++) { if (dirs[i] !== '') { depth++; } } if (depth > 10) { // should trigger an Error ? cb(new Error('Local Status: 0x80007F03\tBody: Too Many Directories. The Dirs Depth Should Not Larger Than 10.')); return; } do_access.apply(this, [ 'POST', '/' + this.bucket + path, new Buffer(0), headers, function (err/*, headers /* useless *//*, body /* useless */) { cb(err); // console.log(':: DEBUG :: createDirs -> callback -> err =', err); } ]); }; //noinspection JSUnusedGlobalSymbols /** * Remove a certain directory at the path, non-recursive * @param {String} path * @param {Function} cb */ UPYUN.prototype.removeDir = function (path, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } if (path[path.length - 1] !== '/') { path = lib_path.join(path, '/'); } do_access.apply(this, [ 'DELETE', '/' + this.bucket + path, function (err/*, headers /* useless *//*, body /* useless */) { cb(err); // console.log(':: DEBUG :: removeDir -> callback -> err =', err); } ]); }; //noinspection JSUnusedGlobalSymbols /** * Get all the directory contents, including files and sub-dirs * @param {String} path * @param {Function} cb */ UPYUN.prototype.listDir = function (path, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } if (path[path.length - 1] !== '/') { path = lib_path.join(path, '/'); } do_access.apply(this, [ 'GET', '/' + this.bucket + path, function (err, headers /* useless */, body) { if (err) { cb(err); return; } /** * {FILE_NAME}\t{'N'(文件)|'F'(目录)}\t{FILE_SIZE in byte}\t{LAST_MODIFIED_TIME in timestamp}\n */ var contents = body.split('\n'); var list = []; for (var i = 0; i < contents.length; i++) { var entity = {}; var pieces = contents[i].split('\t'); entity.name = pieces[0]; if (entity.name === '') { continue; } entity.path = lib_path.join(path, entity.name); if (pieces[1] === 'N') { entity.type = UPYUN.TYPES.FILE; } else if (pieces[1] === 'F') { entity.type = UPYUN.TYPES.FOLDER; } entity.size = pieces[2]; entity.time = pieces[3]; list.push(entity); } cb(err, list); // console.log(':: DEBUG :: listDir -> callback -> err =', err); // console.log(':: DEBUG :: listDir -> callback -> body =', body); } ]); }; //noinspection JSUnusedGlobalSymbols /** * Destroy the directory from the given path * @param {String} path * @param {Function} cb * @param {Number} [_depth] */ UPYUN.prototype.destroyDir = function (path, cb, _depth) { if (typeof cb !== 'function') { cb = function () { }; } if (path[0] !== '/') { path = lib_path.join('/', path); } if (path[path.length - 1] !== '/') { path = lib_path.join(path, '/'); } if (typeof _depth === 'number') { _depth++; } else { _depth = 0; } // to protect the root directory if (path === '/') { cb(new Error('Local Status: 0x80007F01\tBody: Destroy Root Directory "/" Is NOT ALLOWED!!!')); return; } if (this.iDOReallyWantToDestroyDirectories === false) { cb(new Error('Local Status: 0x80007F0F\tBody: DO YOU REALLY WANT TO DESTROY DIRECTORIES? PLEASE SET upyunObj.iDOReallyWantToDestroyDirectories = true.')); return; } var self = this; this.listDir(path, function (err, list) { if (err) { // reset iDOReallyWantToDestroyDirectories = false // to force the user set this protect lock again if (_depth === 0) { self.iDOReallyWantToDestroyDirectories = false; } cb(err); // console.log(':: DEBUG :: destroyDir -> listDir -> err =', err); } else { async.eachSeries( list, function (entity, _cb) { // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> entity =', entity); if (entity.type === UPYUN.TYPES.FILE) { self.removeFile(entity.path, function (err) { _cb(err); // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> removeFile -> err =', err); }); } else { self.destroyDir(entity.path, function (err) { _cb(err); // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> destroyDir -> err =', err); }, _depth); } }, function (err) { // reset iDOReallyWantToDestroyDirectories = false // to force the user set this protect lock again if (_depth === 0) { self.iDOReallyWantToDestroyDirectories = false; } if (err) { cb(err); // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> callback -> err =', err); return; } self.removeDir(path, function (err) { cb(err); // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> callback -> removeDir -> err =', err); }); } ); } }); }; //noinspection JSUnusedGlobalSymbols /** * Get the bytes used by current bucket * @param {Function} cb ({Error}err, {Integer}bytes) */ UPYUN.prototype.bucketUsage = function (cb) { if (typeof cb !== 'function') { cb = function () { }; } do_access.apply(this, [ 'GET', '/' + this.bucket + '/?usage', function (err, headers /* useless */, body) { if (typeof body !== 'number') { body = 0; } else { body = parseInt(body); } cb(err, body); // console.log(':: DEBUG :: bucketUsage -> callback -> err =', err); /** * bucket usage in byte */ // console.log(':: DEBUG :: bucketUsage -> callback -> body =', body); } ]); }; //noinspection JSUnusedGlobalSymbols /** * UPYUN Cache purge API * @param {Array} url_list * @param {Function} cb */ UPYUN.prototype.purge = function (url_list, cb) { if (typeof cb !== 'function') { cb = function () { }; } if (!lib_util.isArray(url_list)) { cb(new Error('Local Status: 0x80007F31 The URL List Must Be An Array.')); return; } var date_gmt = str_gmt(); var signature = str_signature_purge(url_list, this.bucket, date_gmt, this.pass); request.post('http://purge.upyun.com/purge/', { headers: { Authorization: 'UpYun ' + this.bucket + ':' + this.user + ':' + signature, Date: date_gmt }, form: { 'purge': url_list.join('\n') } }, function (req, resp, body) { if (!resp) { cb(new Error('Local Status: 0x80007FFF\tBody: Network Error.')); return; } var status = resp.statusCode; if (status === 200) { var json = {}; try { json = JSON.parse(body); } catch (e) { cb(new Error('Local Status: 0x80007F32 Purge Response Body Known')); return; } //noinspection JSUnresolvedVariable cb(null, json.invalid_domain_of_url || []); } else { body = 'HTTP Status: ' + status + '\tBody: ' + body; cb(new Error(body)); } }); }; //noinspection JSUnusedGlobalSymbols /** * Set whether to use message-digest-5 to validate uploaded contents * Turn this on will take more time to calculate the MD5 of the buffer * * @param {Boolean} boolean */ UPYUN.prototype.setValidate = function (boolean) { this.validate = !!boolean; }; //noinspection JSUnusedGlobalSymbols /** * Set which UPYUN API host to be used * @param {Integer} hostIndex Value Range: [0, 1, 2, 3] */ UPYUN.prototype.setHost = function (hostIndex) { if (Math.floor(hostIndex) === hostIndex) { hostIndex = hostIndex % API_HOSTS.length; } else { hostIndex = 0; } this.host = API_HOSTS[ hostIndex ]; }; // export type constant UPYUN.TYPES = { FILE: 'file', FOLDER: 'folder' }; module.exports = UPYUN; })();