object.js 14 KB

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