main.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. /**
  2. * Created by ETiV on 4/27/14.
  3. */
  4. 'use strict';
  5. (function () {
  6. /**
  7. * CONST of UPYUN API HOSTS
  8. * @type {string[]}
  9. */
  10. var API_HOSTS = [
  11. 'v0.api.upyun.com',
  12. 'v1.api.upyun.com',
  13. 'v2.api.upyun.com',
  14. 'v3.api.upyun.com'
  15. ];
  16. //noinspection JSUnusedLocalSymbols
  17. /**
  18. * CONST of Response Headers
  19. * @type {Object} Dictionary of response header and body
  20. */
  21. var GENERAL_STATUS_HEADER = {
  22. '200:ok': '操作成功',
  23. // <h1>400 Bad Request</h1>Need a bucket name
  24. '400:badrequest': '错误请求(如 URL 缺少空间名)',
  25. // ??
  26. '401:unauthorized': '访问未授权',
  27. // <h1>401 Unauthorized</h1>Sign error (sign = md5(METHOD&URI&DATE&CONTENT_LENGTH&MD5(PASSWORD)))
  28. '401:signerror': '签名错误(操作员和密码,或签名格式错误)',
  29. // <h1>401 Unauthorized</h1>Sign error (sign = md5(METHOD&URI&DATE&CONTENT_LENGTH&MD5(PASSWORD))) , Need Date Header!
  30. '401:needdateheader': '发起的请求缺少 Date 头信息',
  31. // <h1>401 Unauthorized</h1>Sign error (sign = md5(METHOD&URI&DATE&CONTENT_LENGTH&MD5(PASSWORD))) , Date offset error!
  32. '401:dateoffseterror': '发起请求的服务器时间错误,请检查服务器时间是否与世界时间一致',
  33. // ?? 403 Not Access
  34. '403:notaccess': '权限错误(如非图片文件上传到图片空间)',
  35. '403:filesizetoomax': '单个文件超出大小(100MB 以内)',
  36. '403:notapicturefile': '图片类空间错误码,非图片文件或图片文件格式错误。针对图片空间只允许上传 jpg/png/gif/bmp/tif 格式。',
  37. '403:picturesizetoomax': '图片类空间错误码,图片尺寸太大。针对图片空间,图片总像素在 200000000 以内。',
  38. '403:bucketfull': '空间已用满',
  39. '403:bucketblocked': '空间被禁用,请联系管理员',
  40. '403:userblocked': '操作员被禁用',
  41. '403:imagerotateinvalidparameters': '图片旋转参数错误',
  42. '403:imagecropinvalidparameters': '图片裁剪参数错误',
  43. // 404 Not Found
  44. '404:notfound': '获取文件或目录不存在;上传文件或目录时上级目录不存在',
  45. // 406 Not Acceptable(path)
  46. '406:notacceptable(path)': '目录错误(创建目录时已存在同名文件;或上传文件时存在同名目录)',
  47. '503:systemerror': '系统错误'
  48. };
  49. /**
  50. * Libraries and helper functions
  51. */
  52. var request = require('request');
  53. var async = require('async');
  54. var lib_path = require('path');
  55. var lib_crypto = require('crypto');
  56. var lib_util = require('util');
  57. var str_gmt = function () {
  58. return (new Date()).toUTCString();
  59. };
  60. var str_md5 = function (buffer) {
  61. var md5 = lib_crypto.createHash('md5');
  62. md5.update(buffer);
  63. return md5.digest('hex');
  64. };
  65. var str_signature = function (method, uri, date_gmt, buffer_length, password) {
  66. return str_md5(
  67. new Buffer(
  68. method + '&' + uri + '&' + date_gmt + '&' + String(buffer_length) + '&' + str_md5(new Buffer(String(password)))
  69. )
  70. );
  71. };
  72. var str_signature_purge = function (url_list, bucket, date_gmt, password) {
  73. var urls = url_list.join('\n');
  74. var token = str_md5(new Buffer(password));
  75. return str_md5(new Buffer(urls + '&' + bucket + '&' + date_gmt + '&' + token));
  76. };
  77. /**
  78. * do access to UPYUN REST API
  79. * @param {String} method
  80. * @param {String} uri
  81. * @param {Buffer|Function} [buffer]
  82. * @param {Object|Function} [headers]
  83. * @param {Function} cb
  84. */
  85. var do_access = function (method, uri, buffer, headers, cb) {
  86. if (typeof cb !== 'function') {
  87. cb = function () {
  88. };
  89. }
  90. if (arguments.length >= 4 && !(buffer instanceof Buffer)) {
  91. cb(new Error('Local Status: 0x80007F11\tBody: Argument 3 Must Be An Instance Of Buffer.'));
  92. return;
  93. }
  94. if (arguments.length < 3) {
  95. cb(new Error('Local Status: 0x80007F12\tBody: No Enough Arguments To Access UPYun API.'));
  96. return;
  97. }
  98. if (arguments.length === 3) {
  99. if (typeof buffer === 'function') {
  100. cb = buffer;
  101. buffer = null;
  102. headers = null;
  103. }
  104. }
  105. if (arguments.length === 4) {
  106. if (typeof headers === 'function') {
  107. cb = headers;
  108. headers = null;
  109. }
  110. }
  111. method = method.toUpperCase();
  112. headers = headers || {};
  113. var date_gmt = str_gmt();
  114. buffer = buffer || [];
  115. var signature = str_signature(method, uri, date_gmt, buffer.length, this.pass);
  116. // console.log(':: DEBUG :: do_access -> signature', signature);
  117. //noinspection JSUndefinedPropertyAssignment
  118. headers.Date = date_gmt;
  119. //noinspection JSUndefinedPropertyAssignment
  120. headers.Authorization = 'UpYun ' + this.user + ':' + signature;
  121. request({
  122. method: method,
  123. uri: 'http://' + this.host + uri,
  124. body: buffer.length > 0 ? buffer : undefined,
  125. headers: headers
  126. }, function (req, resp, body) {
  127. // callback
  128. // console.log(':: DEBUG :: do_access -> request.callback -> body =', body);
  129. if (!resp) {
  130. cb(new Error('Local Status: 0x80007FFF\tBody: Network Error.'));
  131. return;
  132. }
  133. var status = resp.statusCode;
  134. if (status === 200) {
  135. cb(null, resp.headers, body);
  136. } else {
  137. body = 'HTTP Status: ' + status + '\tBody: ' + body;
  138. cb(new Error(body));
  139. }
  140. // console.log(resp.headers);
  141. });
  142. };
  143. var UPYUN = function (bucket, user, pass) {
  144. this.bucket = bucket;
  145. this.user = user;
  146. this.pass = pass;
  147. this.host = API_HOSTS[0];
  148. this.validate = false;
  149. this.iDOReallyWantToDestroyDirectories = false;
  150. };
  151. //noinspection JSUnusedGlobalSymbols
  152. /**
  153. * Write(Upload) some contents to a certain path
  154. * @param {String} path
  155. * @param {String|Buffer} buffer
  156. * @param {Function} cb
  157. */
  158. UPYUN.prototype.writeFile = function (path, buffer, cb) {
  159. var headers = {};
  160. if (typeof buffer === 'string') {
  161. buffer = new Buffer(buffer);
  162. }
  163. if (this.validate) {
  164. headers['content-md5'] = str_md5(buffer);
  165. }
  166. var dirs = path.split('/');
  167. var depth = 0;
  168. for (var i = 0; i < dirs.length; i++) {
  169. if (dirs[i] !== '') {
  170. depth++;
  171. }
  172. }
  173. if (depth - 1 > 10) {
  174. cb('Too Many Directories. The Dirs Depth Should Not Larger Than 10.');
  175. return;
  176. }
  177. headers.mkdir = 'true';
  178. if (typeof cb !== 'function') {
  179. cb = function () {
  180. };
  181. }
  182. if (path[0] !== '/') {
  183. path = lib_path.join('/', path);
  184. }
  185. do_access.apply(this, [
  186. 'PUT', '/' + this.bucket + path,
  187. buffer, headers, function (err) {
  188. cb(err);
  189. // console.log(':: DEBUG :: writeFile -> callback -> err =', err);
  190. }
  191. ]
  192. );
  193. };
  194. //noinspection JSUnusedGlobalSymbols
  195. /**
  196. * Fetch(Download) a certain file at the path
  197. * @param {String} path
  198. * @param {Function} cb
  199. */
  200. UPYUN.prototype.fetchFile = function (path, cb) {
  201. if (typeof cb !== 'function') {
  202. cb = function () {
  203. };
  204. }
  205. if (path[0] !== '/') {
  206. path = lib_path.join('/', path);
  207. }
  208. do_access.apply(this, [
  209. 'GET', '/' + this.bucket + path,
  210. function (err, headers /* useless */, body) {
  211. cb(err, body || '');
  212. // console.log(':: DEBUG :: fetchFile -> callback -> err =', err);
  213. }
  214. ]);
  215. };
  216. //noinspection JSUnusedGlobalSymbols
  217. /**
  218. * Inspect a certain file or directory by given path
  219. * @param {String} path can be a path of file or directory
  220. * @param {Function} cb
  221. */
  222. UPYUN.prototype.inspect = function (path, cb) {
  223. if (typeof cb !== 'function') {
  224. cb = function () {
  225. };
  226. }
  227. if (path[0] !== '/') {
  228. path = lib_path.join('/', path);
  229. }
  230. do_access.apply(this, [
  231. 'HEAD', '/' + this.bucket + path,
  232. function (err, headers/*, body /* useless */) {
  233. if (err) {
  234. cb(err);
  235. return;
  236. }
  237. /**
  238. * headers when the target is a Folder
  239. * 'x-upyun-file-type': 'folder',
  240. * 'x-upyun-file-date': '1396411022'
  241. */
  242. /**
  243. * headers when the target is a File
  244. * 'x-upyun-file-type': 'file',
  245. * 'x-upyun-file-size': '5',
  246. * 'x-upyun-file-date': '1398563107'
  247. */
  248. var entity = {};
  249. entity.path = path;
  250. entity.name = lib_path.basename(path);
  251. entity.type = String(headers['x-upyun-file-type']).toLowerCase();
  252. entity.time = headers['x-upyun-file-date'];
  253. if (!!headers['x-upyun-file-size']) {
  254. entity.size = headers['x-upyun-file-size'];
  255. } else {
  256. entity.size = 0;
  257. }
  258. cb(err, entity);
  259. // console.log(':: DEBUG :: inspect -> callback -> err =', err);
  260. // console.log(':: DEBUG :: inspect -> callback -> headers =', headers);
  261. }
  262. ]);
  263. };
  264. //noinspection JSUnusedGlobalSymbols
  265. /**
  266. * Remove a certain file at the path
  267. * @param {String} path
  268. * @param {Function} cb
  269. */
  270. UPYUN.prototype.removeFile = function (path, cb) {
  271. if (typeof cb !== 'function') {
  272. cb = function () {
  273. };
  274. }
  275. if (path[0] !== '/') {
  276. path = lib_path.join('/', path);
  277. }
  278. do_access.apply(this, [
  279. 'DELETE', '/' + this.bucket + path,
  280. function (err/*, headers /* useless *//*, body /* useless */) {
  281. cb(err);
  282. // console.log(':: DEBUG :: removeFile -> callback -> err =', err);
  283. }
  284. ]);
  285. };
  286. //noinspection JSUnusedGlobalSymbols
  287. /**
  288. * Create directories by a full path, with 10 levels of max depth
  289. * @param {String} path
  290. * @param {Function} cb
  291. */
  292. UPYUN.prototype.createDirs = function (path, cb) {
  293. var headers = {};
  294. if (typeof cb !== 'function') {
  295. cb = function () {
  296. };
  297. }
  298. if (path[0] !== '/') {
  299. path = lib_path.join('/', path);
  300. }
  301. if (path[path.length - 1] !== '/') {
  302. path = lib_path.join(path, '/');
  303. }
  304. headers.folder = 'true';
  305. headers.mkdir = 'true';
  306. var dirs = path.split('/');
  307. var depth = 0;
  308. for (var i = 0; i < dirs.length; i++) {
  309. if (dirs[i] !== '') {
  310. depth++;
  311. }
  312. }
  313. if (depth > 10) {
  314. // should trigger an Error ?
  315. cb(new Error('Local Status: 0x80007F03\tBody: Too Many Directories. The Dirs Depth Should Not Larger Than 10.'));
  316. return;
  317. }
  318. do_access.apply(this, [
  319. 'POST', '/' + this.bucket + path,
  320. new Buffer(0), headers,
  321. function (err/*, headers /* useless *//*, body /* useless */) {
  322. cb(err);
  323. // console.log(':: DEBUG :: createDirs -> callback -> err =', err);
  324. }
  325. ]);
  326. };
  327. //noinspection JSUnusedGlobalSymbols
  328. /**
  329. * Remove a certain directory at the path, non-recursive
  330. * @param {String} path
  331. * @param {Function} cb
  332. */
  333. UPYUN.prototype.removeDir = function (path, cb) {
  334. if (typeof cb !== 'function') {
  335. cb = function () {
  336. };
  337. }
  338. if (path[0] !== '/') {
  339. path = lib_path.join('/', path);
  340. }
  341. if (path[path.length - 1] !== '/') {
  342. path = lib_path.join(path, '/');
  343. }
  344. do_access.apply(this, [
  345. 'DELETE', '/' + this.bucket + path,
  346. function (err/*, headers /* useless *//*, body /* useless */) {
  347. cb(err);
  348. // console.log(':: DEBUG :: removeDir -> callback -> err =', err);
  349. }
  350. ]);
  351. };
  352. //noinspection JSUnusedGlobalSymbols
  353. /**
  354. * Get all the directory contents, including files and sub-dirs
  355. * @param {String} path
  356. * @param {Function} cb
  357. */
  358. UPYUN.prototype.listDir = function (path, cb) {
  359. if (typeof cb !== 'function') {
  360. cb = function () {
  361. };
  362. }
  363. if (path[0] !== '/') {
  364. path = lib_path.join('/', path);
  365. }
  366. if (path[path.length - 1] !== '/') {
  367. path = lib_path.join(path, '/');
  368. }
  369. do_access.apply(this, [
  370. 'GET', '/' + this.bucket + path,
  371. function (err, headers /* useless */, body) {
  372. if (err) {
  373. cb(err);
  374. return;
  375. }
  376. /**
  377. * {FILE_NAME}\t{'N'(文件)|'F'(目录)}\t{FILE_SIZE in byte}\t{LAST_MODIFIED_TIME in timestamp}\n
  378. */
  379. var contents = body.split('\n');
  380. var list = [];
  381. for (var i = 0; i < contents.length; i++) {
  382. var entity = {};
  383. var pieces = contents[i].split('\t');
  384. entity.name = pieces[0];
  385. if (entity.name === '') {
  386. continue;
  387. }
  388. entity.path = lib_path.join(path, entity.name);
  389. if (pieces[1] === 'N') {
  390. entity.type = UPYUN.TYPES.FILE;
  391. } else if (pieces[1] === 'F') {
  392. entity.type = UPYUN.TYPES.FOLDER;
  393. }
  394. entity.size = pieces[2];
  395. entity.time = pieces[3];
  396. list.push(entity);
  397. }
  398. cb(err, list);
  399. // console.log(':: DEBUG :: listDir -> callback -> err =', err);
  400. // console.log(':: DEBUG :: listDir -> callback -> body =', body);
  401. }
  402. ]);
  403. };
  404. //noinspection JSUnusedGlobalSymbols
  405. /**
  406. * Destroy the directory from the given path
  407. * @param {String} path
  408. * @param {Function} cb
  409. * @param {Number} [_depth]
  410. */
  411. UPYUN.prototype.destroyDir = function (path, cb, _depth) {
  412. if (typeof cb !== 'function') {
  413. cb = function () {
  414. };
  415. }
  416. if (path[0] !== '/') {
  417. path = lib_path.join('/', path);
  418. }
  419. if (path[path.length - 1] !== '/') {
  420. path = lib_path.join(path, '/');
  421. }
  422. if (typeof _depth === 'number') {
  423. _depth++;
  424. } else {
  425. _depth = 0;
  426. }
  427. // to protect the root directory
  428. if (path === '/') {
  429. cb(new Error('Local Status: 0x80007F01\tBody: Destroy Root Directory "/" Is NOT ALLOWED!!!'));
  430. return;
  431. }
  432. if (this.iDOReallyWantToDestroyDirectories === false) {
  433. cb(new Error('Local Status: 0x80007F0F\tBody: DO YOU REALLY WANT TO DESTROY DIRECTORIES? PLEASE SET upyunObj.iDOReallyWantToDestroyDirectories = true.'));
  434. return;
  435. }
  436. var self = this;
  437. this.listDir(path, function (err, list) {
  438. if (err) {
  439. // reset iDOReallyWantToDestroyDirectories = false
  440. // to force the user set this protect lock again
  441. if (_depth === 0) {
  442. self.iDOReallyWantToDestroyDirectories = false;
  443. }
  444. cb(err);
  445. // console.log(':: DEBUG :: destroyDir -> listDir -> err =', err);
  446. } else {
  447. async.eachSeries(
  448. list,
  449. function (entity, _cb) {
  450. // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> entity =', entity);
  451. if (entity.type === UPYUN.TYPES.FILE) {
  452. self.removeFile(entity.path, function (err) {
  453. _cb(err);
  454. // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> removeFile -> err =', err);
  455. });
  456. } else {
  457. self.destroyDir(entity.path, function (err) {
  458. _cb(err);
  459. // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> destroyDir -> err =', err);
  460. }, _depth);
  461. }
  462. },
  463. function (err) {
  464. // reset iDOReallyWantToDestroyDirectories = false
  465. // to force the user set this protect lock again
  466. if (_depth === 0) {
  467. self.iDOReallyWantToDestroyDirectories = false;
  468. }
  469. if (err) {
  470. cb(err);
  471. // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> callback -> err =', err);
  472. return;
  473. }
  474. self.removeDir(path, function (err) {
  475. cb(err);
  476. // console.log(':: DEBUG :: destroyDir -> async.eachSeries -> callback -> removeDir -> err =', err);
  477. });
  478. }
  479. );
  480. }
  481. });
  482. };
  483. //noinspection JSUnusedGlobalSymbols
  484. /**
  485. * Get the bytes used by current bucket
  486. * @param {Function} cb ({Error}err, {Integer}bytes)
  487. */
  488. UPYUN.prototype.bucketUsage = function (cb) {
  489. if (typeof cb !== 'function') {
  490. cb = function () {
  491. };
  492. }
  493. do_access.apply(this, [
  494. 'GET', '/' + this.bucket + '/?usage',
  495. function (err, headers /* useless */, body) {
  496. if (typeof body !== 'number') {
  497. body = 0;
  498. } else {
  499. body = parseInt(body);
  500. }
  501. cb(err, body);
  502. // console.log(':: DEBUG :: bucketUsage -> callback -> err =', err);
  503. /**
  504. * bucket usage in byte
  505. */
  506. // console.log(':: DEBUG :: bucketUsage -> callback -> body =', body);
  507. }
  508. ]);
  509. };
  510. //noinspection JSUnusedGlobalSymbols
  511. /**
  512. * UPYUN Cache purge API
  513. * @param {Array} url_list
  514. * @param {Function} cb
  515. */
  516. UPYUN.prototype.purge = function (url_list, cb) {
  517. if (typeof cb !== 'function') {
  518. cb = function () {
  519. };
  520. }
  521. if (!lib_util.isArray(url_list)) {
  522. cb(new Error('Local Status: 0x80007F31 The URL List Must Be An Array.'));
  523. return;
  524. }
  525. var date_gmt = str_gmt();
  526. var signature = str_signature_purge(url_list, this.bucket, date_gmt, this.pass);
  527. request.post('http://purge.upyun.com/purge/', {
  528. headers: {
  529. Authorization: 'UpYun ' + this.bucket + ':' + this.user + ':' + signature,
  530. Date: date_gmt
  531. },
  532. form: {
  533. 'purge': url_list.join('\n')
  534. }
  535. }, function (req, resp, body) {
  536. if (!resp) {
  537. cb(new Error('Local Status: 0x80007FFF\tBody: Network Error.'));
  538. return;
  539. }
  540. var status = resp.statusCode;
  541. if (status === 200) {
  542. var json = {};
  543. try {
  544. json = JSON.parse(body);
  545. } catch (e) {
  546. cb(new Error('Local Status: 0x80007F32 Purge Response Body Known'));
  547. return;
  548. }
  549. //noinspection JSUnresolvedVariable
  550. cb(null, json.invalid_domain_of_url || []);
  551. } else {
  552. body = 'HTTP Status: ' + status + '\tBody: ' + body;
  553. cb(new Error(body));
  554. }
  555. });
  556. };
  557. //noinspection JSUnusedGlobalSymbols
  558. /**
  559. * Set whether to use message-digest-5 to validate uploaded contents
  560. * Turn this on will take more time to calculate the MD5 of the buffer
  561. *
  562. * @param {Boolean} boolean
  563. */
  564. UPYUN.prototype.setValidate = function (boolean) {
  565. this.validate = !!boolean;
  566. };
  567. //noinspection JSUnusedGlobalSymbols
  568. /**
  569. * Set which UPYUN API host to be used
  570. * @param {Integer} hostIndex Value Range: [0, 1, 2, 3]
  571. */
  572. UPYUN.prototype.setHost = function (hostIndex) {
  573. if (Math.floor(hostIndex) === hostIndex) {
  574. hostIndex = hostIndex % API_HOSTS.length;
  575. } else {
  576. hostIndex = 0;
  577. }
  578. this.host = API_HOSTS[ hostIndex ];
  579. };
  580. // export type constant
  581. UPYUN.TYPES = {
  582. FILE: 'file',
  583. FOLDER: 'folder'
  584. };
  585. module.exports = UPYUN;
  586. })();