object.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. 'use strict';
  2. var debug = require('debug')('ali-oss:object');
  3. var utility = require('utility');
  4. // var crypto = require('crypto');
  5. var fs = require('fs');
  6. var is = require('is-type-of');
  7. // var eoe = require('end-or-error');
  8. var urlutil = require('url');
  9. var copy = require('copy-to');
  10. var path = require('path');
  11. var mime = require('mime');
  12. // var assert = require('assert');
  13. var proto = exports;
  14. /**
  15. * Object operations
  16. */
  17. /**
  18. * append an object from String(file path)/Buffer/ReadableStream
  19. * @param {String} name the object key
  20. * @param {Mixed} file String(file path)/Buffer/ReadableStream
  21. * @param {Object} options
  22. * @return {Object}
  23. */
  24. proto.append = function* (name, file, options) {
  25. options = options || {};
  26. if (options.position === undefined) options.position = '0';
  27. options.subres = {
  28. append: '',
  29. position: options.position,
  30. };
  31. options.method = 'POST';
  32. var result = yield this.put(name, file, options);
  33. result.nextAppendPosition = result.res.headers['x-oss-next-append-position'];
  34. return result;
  35. }
  36. /**
  37. * put an object from String(file path)/Buffer/ReadableStream
  38. * @param {String} name the object key
  39. * @param {Mixed} file String(file path)/Buffer/ReadableStream
  40. * @param {Object} options
  41. * @return {Object}
  42. */
  43. proto.put = function* put(name, file, options) {
  44. var content;
  45. options = options || {};
  46. if (is.buffer(file)) {
  47. content = file;
  48. } else if (is.string(file)) {
  49. options.mime = options.mime || mime.getType(path.extname(file));
  50. var stream = fs.createReadStream(file);
  51. options.contentLength = yield this._getFileSize(file);
  52. return yield this.putStream(name, stream, options);
  53. } else if (is.readableStream(file)) {
  54. return yield this.putStream(name, file, options);
  55. } else {
  56. throw new TypeError('Must provide String/Buffer/ReadableStream for put.');
  57. }
  58. options.headers = options.headers || {};
  59. this._convertMetaToHeaders(options.meta, options.headers);
  60. var method = options.method || 'PUT';
  61. var params = this._objectRequestParams(method, name, options);
  62. params.mime = options.mime;
  63. params.content = content;
  64. params.successStatuses = [200];
  65. var result = yield this.request(params);
  66. var ret = {
  67. name: name,
  68. url: this._objectUrl(name),
  69. res: result.res,
  70. };
  71. if (options.headers && options.headers['x-oss-callback']) {
  72. ret.data = JSON.parse(result.data.toString());
  73. }
  74. return ret;
  75. };
  76. /**
  77. * put an object from ReadableStream. If `options.contentLength` is
  78. * not provided, chunked encoding is used.
  79. * @param {String} name the object key
  80. * @param {Readable} stream the ReadableStream
  81. * @param {Object} options
  82. * @return {Object}
  83. */
  84. proto.putStream = function* putStream(name, stream, options) {
  85. options = options || {};
  86. options.headers = options.headers || {};
  87. if (options.contentLength) {
  88. options.headers['Content-Length'] = options.contentLength;
  89. } else {
  90. options.headers['Transfer-Encoding'] = 'chunked';
  91. }
  92. this._convertMetaToHeaders(options.meta, options.headers);
  93. var method = options.method || 'PUT';
  94. var params = this._objectRequestParams(method, name, options);
  95. params.mime = options.mime;
  96. params.stream = stream;
  97. params.successStatuses = [200];
  98. var result = yield this.request(params);
  99. var ret = {
  100. name: name,
  101. url: this._objectUrl(name),
  102. res: result.res,
  103. };
  104. if (options.headers && options.headers['x-oss-callback']) {
  105. ret.data = JSON.parse(result.data.toString());
  106. }
  107. return ret;
  108. };
  109. proto.head = function* head(name, options) {
  110. var params = this._objectRequestParams('HEAD', name, options);
  111. params.successStatuses = [200, 304];
  112. var result = yield this.request(params);
  113. var data = {
  114. meta: null,
  115. res: result.res,
  116. status: result.status
  117. };
  118. if (result.status === 200) {
  119. for (var k in result.headers) {
  120. if (k.indexOf('x-oss-meta-') === 0) {
  121. if (!data.meta) {
  122. data.meta = {};
  123. }
  124. data.meta[k.substring(11)] = result.headers[k];
  125. }
  126. }
  127. }
  128. return data;
  129. };
  130. proto.get = function* get(name, file, options) {
  131. var writeStream = null;
  132. var needDestroy = false;
  133. if (is.writableStream(file)) {
  134. writeStream = file;
  135. } else if (is.string(file)) {
  136. writeStream = fs.createWriteStream(file);
  137. needDestroy = true;
  138. } else {
  139. // get(name, options)
  140. options = file;
  141. }
  142. options = options || {};
  143. if (options.process) {
  144. options.subres = options.subres || {};
  145. options.subres['x-oss-process'] = options.process;
  146. }
  147. var result;
  148. try {
  149. var params = this._objectRequestParams('GET', name, options);
  150. params.writeStream = writeStream;
  151. params.successStatuses = [200, 206, 304];
  152. result = yield this.request(params);
  153. if (needDestroy) {
  154. writeStream.destroy();
  155. }
  156. } catch (err) {
  157. if (needDestroy) {
  158. writeStream.destroy();
  159. // should delete the exists file before throw error
  160. debug('get error: %s, delete the exists file %s', err, file);
  161. yield this._deleteFileSafe(file);
  162. }
  163. throw err;
  164. }
  165. return {
  166. res: result.res,
  167. content: result.data
  168. };
  169. };
  170. proto.getStream = function* getStream(name, options) {
  171. options = options || {};
  172. var params = this._objectRequestParams('GET', name, options);
  173. params.customResponse = true;
  174. params.successStatuses = [200, 206, 304];
  175. var result = yield this.request(params);
  176. return {
  177. stream: result.res,
  178. res: {
  179. status: result.status,
  180. headers: result.headers
  181. }
  182. };
  183. };
  184. proto.delete = function* _delete(name, options) {
  185. var params = this._objectRequestParams('DELETE', name, options);
  186. params.successStatuses = [204];
  187. var result = yield this.request(params);
  188. return {
  189. res: result.res
  190. };
  191. };
  192. proto.deleteMulti = function* deleteMulti(names, options) {
  193. options = options || {};
  194. var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<Delete>\n';
  195. if (options.quiet) {
  196. xml += ' <Quiet>true</Quiet>\n';
  197. } else {
  198. xml += ' <Quiet>false</Quiet>\n';
  199. }
  200. for (var i = 0; i < names.length; i++) {
  201. xml += ' <Object><Key>' +
  202. utility.escape(this._objectName(names[i])) + '</Key></Object>\n';
  203. }
  204. xml += '</Delete>';
  205. debug('delete multi objects: %s', xml);
  206. options.subres = 'delete';
  207. var params = this._objectRequestParams('POST', '', options);
  208. params.mime = 'xml';
  209. params.content = xml;
  210. params.xmlResponse = true;
  211. params.successStatuses = [200];
  212. var result = yield this.request(params);
  213. var r = result.data;
  214. var deleted = r && r.Deleted || null;
  215. if (deleted) {
  216. if (!Array.isArray(deleted)) {
  217. deleted = [deleted];
  218. }
  219. deleted = deleted.map(function (item) {
  220. return item.Key;
  221. });
  222. }
  223. return {
  224. res: result.res,
  225. deleted: deleted
  226. };
  227. };
  228. proto.copy = function* copy(name, sourceName, options) {
  229. options = options || {};
  230. options.headers = options.headers || {};
  231. for (var k in options.headers) {
  232. options.headers['x-oss-copy-source-' + k.toLowerCase()] = options.headers[k];
  233. }
  234. if (options.meta) {
  235. options.headers['x-oss-metadata-directive'] = 'REPLACE';
  236. }
  237. this._convertMetaToHeaders(options.meta, options.headers);
  238. if (sourceName[0] !== '/') {
  239. // no specify bucket name
  240. sourceName = '/' + this.options.bucket + '/' + encodeURIComponent(sourceName);
  241. } else {
  242. sourceName = '/' + encodeURIComponent(sourceName.slice(1));
  243. }
  244. options.headers['x-oss-copy-source'] = sourceName;
  245. var params = this._objectRequestParams('PUT', name, options);
  246. params.xmlResponse = true;
  247. params.successStatuses = [200, 304];
  248. var result = yield this.request(params);
  249. var data = result.data;
  250. if (data) {
  251. data = {
  252. etag: data.ETag,
  253. lastModified: data.LastModified,
  254. };
  255. }
  256. return {
  257. data: data,
  258. res: result.res
  259. };
  260. };
  261. proto.putMeta = function* putMeta(name, meta, options) {
  262. return yield this.copy(name, name, {
  263. meta: meta || {},
  264. timeout: options && options.timeout,
  265. ctx: options && options.ctx,
  266. });
  267. };
  268. proto.list = function* list(query, options) {
  269. // prefix, marker, max-keys, delimiter
  270. var params = this._objectRequestParams('GET', '', options);
  271. params.query = query;
  272. params.xmlResponse = true;
  273. params.successStatuses = [200];
  274. var result = yield this.request(params);
  275. var objects = result.data.Contents;
  276. var that = this;
  277. if (objects) {
  278. if (!Array.isArray(objects)) {
  279. objects = [objects];
  280. }
  281. objects = objects.map(function (obj) {
  282. return {
  283. name: obj.Key,
  284. url: that._objectUrl(obj.Key),
  285. lastModified: obj.LastModified,
  286. etag: obj.ETag,
  287. type: obj.Type,
  288. size: Number(obj.Size),
  289. storageClass: obj.StorageClass,
  290. owner: {
  291. id: obj.Owner.ID,
  292. displayName: obj.Owner.DisplayName,
  293. }
  294. };
  295. });
  296. }
  297. var prefixes = result.data.CommonPrefixes || null;
  298. if (prefixes) {
  299. if (!Array.isArray(prefixes)) {
  300. prefixes = [prefixes];
  301. }
  302. prefixes = prefixes.map(function (item) {
  303. return item.Prefix;
  304. });
  305. }
  306. return {
  307. res: result.res,
  308. objects: objects,
  309. prefixes: prefixes,
  310. nextMarker: result.data.NextMarker || null,
  311. isTruncated: result.data.IsTruncated === 'true'
  312. };
  313. };
  314. /*
  315. * Set object's ACL
  316. * @param {String} name the object key
  317. * @param {String} acl the object ACL
  318. * @param {Object} options
  319. */
  320. proto.putACL = function* putACL(name, acl, options) {
  321. options = options || {};
  322. options.subres = 'acl';
  323. options.headers = options.headers || {};
  324. options.headers['x-oss-object-acl'] = acl;
  325. name = this._objectName(name);
  326. var params = this._objectRequestParams('PUT', name, options);
  327. params.successStatuses = [200];
  328. var result = yield this.request(params);
  329. return {
  330. res: result.res
  331. };
  332. };
  333. /*
  334. * Get object's ACL
  335. * @param {String} name the object key
  336. * @param {Object} options
  337. * @return {Object}
  338. */
  339. proto.getACL = function* getACL(name, options) {
  340. options = options || {};
  341. options.subres = 'acl';
  342. name = this._objectName(name);
  343. var params = this._objectRequestParams('GET', name, options);
  344. params.successStatuses = [200];
  345. params.xmlResponse = true;
  346. var result = yield this.request(params);
  347. return {
  348. acl: result.data.AccessControlList.Grant,
  349. owner: {
  350. id: result.data.Owner.ID,
  351. displayName: result.data.Owner.DisplayName,
  352. },
  353. res: result.res
  354. };
  355. };
  356. proto.signatureUrl = function (name, options) {
  357. name = this._objectName(name);
  358. var params = {
  359. bucket: this.options.bucket,
  360. object: name
  361. };
  362. options = options || {};
  363. var expires = utility.timestamp() + (options.expires || 1800);
  364. var resource = this._getResource(params);
  365. var query = {};
  366. var signList = [];
  367. for (var k in options.response) {
  368. var key = 'response-' + k.toLowerCase();
  369. query[key] = options.response[k];
  370. signList.push(key + '=' + options.response[k]);
  371. }
  372. if (this.options.stsToken) {
  373. query['security-token'] = this.options.stsToken;
  374. signList.push('security-token=' + this.options.stsToken);
  375. }
  376. if (options.process){
  377. var processKeyword = 'x-oss-process';
  378. query[processKeyword] = options.process;
  379. var item = processKeyword + '=' + options.process;
  380. signList.push(item);
  381. }
  382. if (signList.length > 0) {
  383. signList.sort();
  384. resource += '?' + signList.join('&');
  385. }
  386. var stringToSign = [
  387. options.method || 'GET',
  388. options['content-md5'] || '', // Content-MD5
  389. options['content-type'] || '', // Content-Type
  390. expires,
  391. resource
  392. ].join('\n');
  393. var signature = this.signature(stringToSign);
  394. var url = urlutil.parse(this._getReqUrl(params));
  395. url.query = {
  396. OSSAccessKeyId: this.options.accessKeyId,
  397. Expires: expires,
  398. Signature: signature
  399. };
  400. copy(query).to(url.query);
  401. return url.format();
  402. };
  403. /**
  404. * Get Object url by name
  405. * @param {String} name - object name
  406. * @param {String} [baseUrl] - If provide `baseUrl`, will use `baseUrl` instead the default `endpoint`.
  407. * @return {String} object url
  408. */
  409. proto.getObjectUrl = function (name, baseUrl) {
  410. if (!baseUrl) {
  411. baseUrl = this.options.endpoint.format();
  412. } else if (baseUrl[baseUrl.length - 1] !== '/') {
  413. baseUrl += '/';
  414. }
  415. return baseUrl + this._escape(this._objectName(name));
  416. };
  417. proto._objectUrl = function (name) {
  418. return this._getReqUrl({bucket: this.options.bucket, object: name});
  419. };
  420. /**
  421. * generator request params
  422. * @return {Object} params
  423. *
  424. * @api private
  425. */
  426. proto._objectRequestParams = function (method, name, options) {
  427. if (!this.options.bucket) {
  428. throw new Error('Please create a bucket first');
  429. }
  430. options = options || {};
  431. name = this._objectName(name);
  432. var params = {
  433. object: name,
  434. bucket: this.options.bucket,
  435. method: method,
  436. subres: options && options.subres,
  437. timeout: options && options.timeout,
  438. ctx: options && options.ctx,
  439. };
  440. if (options.headers) {
  441. params.headers = options.headers;
  442. }
  443. return params;
  444. };
  445. proto._objectName = function (name) {
  446. return name.replace(/^\/+/, '');
  447. };
  448. proto._statFile = function (filepath) {
  449. return function (callback) {
  450. fs.stat(filepath, callback);
  451. };
  452. };
  453. proto._convertMetaToHeaders = function (meta, headers) {
  454. if (!meta) {
  455. return;
  456. }
  457. for (var k in meta) {
  458. headers['x-oss-meta-' + k] = meta[k];
  459. }
  460. };
  461. proto._deleteFileSafe = function (filepath) {
  462. return function (callback) {
  463. fs.exists(filepath, function (exists) {
  464. if (!exists) {
  465. return callback();
  466. }
  467. fs.unlink(filepath, function (err) {
  468. if (err) {
  469. debug('unlink %j error: %s', filepath, err);
  470. }
  471. callback();
  472. });
  473. });
  474. };
  475. };