multipart.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. 'use strict';
  2. // var debug = require('debug')('ali-oss:multipart');
  3. // var fs = require('fs');
  4. var is = require('is-type-of');
  5. // var destroy = require('destroy');
  6. // var eoe = require('end-or-error');
  7. var util = require('util');
  8. var path = require('path');
  9. var mime = require('mime');
  10. var gather = require('co-gather');
  11. var proto = exports;
  12. /**
  13. * Multipart operations
  14. */
  15. /**
  16. * Upload a file to OSS using multipart uploads
  17. * @param {String} name
  18. * @param {String|File} file
  19. * @param {Object} options
  20. */
  21. proto.multipartUpload = function* multipartUpload(name, file, options) {
  22. options = options || {};
  23. if (options.checkpoint && options.checkpoint.uploadId) {
  24. return yield this._resumeMultipart(options.checkpoint, options);
  25. }
  26. var minPartSize = 100 * 1024;
  27. var filename = is.file(file) ? file.name : file;
  28. options.mime = options.mime || mime.getType(path.extname(filename));
  29. options.headers = options.headers || {};
  30. this._convertMetaToHeaders(options.meta, options.headers);
  31. var fileSize = yield this._getFileSize(file);
  32. if (fileSize < minPartSize) {
  33. var stream = this._createStream(file, 0, fileSize);
  34. options.contentLength = fileSize;
  35. var result = yield this.putStream(name, stream, options);
  36. if (options && options.progress) {
  37. yield options.progress(1);
  38. }
  39. var ret = {
  40. res: result.res,
  41. bucket: this.options.bucket,
  42. name: name,
  43. etag: result.res.headers.etag
  44. };
  45. if (options.headers && options.headers['x-oss-callback']) {
  46. ret.data = JSON.parse(result.data.toString());
  47. }
  48. return ret;
  49. }
  50. if (options.partSize && options.partSize < minPartSize) {
  51. throw new Error('partSize must not be smaller than ' + minPartSize);
  52. }
  53. var result = yield this._initMultipartUpload(name, options);
  54. var uploadId = result.uploadId;
  55. var partSize = this._getPartSize(fileSize, options.partSize);
  56. var checkpoint = {
  57. file: file,
  58. name: name,
  59. fileSize: fileSize,
  60. partSize: partSize,
  61. uploadId: uploadId,
  62. doneParts: []
  63. };
  64. if (options && options.progress) {
  65. yield options.progress(0, checkpoint, result.res);
  66. }
  67. return yield this._resumeMultipart(checkpoint, options);
  68. };
  69. /*
  70. * Resume multipart upload from checkpoint. The checkpoint will be
  71. * updated after each successful part upload.
  72. * @param {Object} checkpoint the checkpoint
  73. * @param {Object} options
  74. */
  75. proto._resumeMultipart = function* _resumeMultipart(checkpoint, options) {
  76. var file = checkpoint.file;
  77. var fileSize = checkpoint.fileSize;
  78. var partSize = checkpoint.partSize;
  79. var uploadId = checkpoint.uploadId;
  80. var doneParts = checkpoint.doneParts;
  81. var name = checkpoint.name;
  82. var partOffs = this._divideParts(fileSize, partSize);
  83. var numParts = partOffs.length;
  84. var uploadPartJob = function* (self, partNo) {
  85. var pi = partOffs[partNo - 1];
  86. var data = {
  87. stream: self._createStream(file, pi.start, pi.end),
  88. size: pi.end - pi.start
  89. };
  90. var result = yield self._uploadPart(name, uploadId, partNo, data);
  91. doneParts.push({
  92. number: partNo,
  93. etag: result.res.headers.etag
  94. });
  95. checkpoint.doneParts = doneParts;
  96. if (options && options.progress) {
  97. yield options.progress(doneParts.length / numParts, checkpoint, result.res);
  98. }
  99. };
  100. var all = Array.from(new Array(numParts), (x, i) => i + 1);
  101. var done = doneParts.map(p => p.number);
  102. var todo = all.filter(p => done.indexOf(p) < 0);
  103. if (this.checkBrowserAndVersion('Internet Explorer', '10')) {
  104. for (var i = 0; i < todo.length; i++) {
  105. yield uploadPartJob(this, todo[i]);
  106. }
  107. } else {
  108. // upload in parallel
  109. var jobs = [];
  110. for (var i = 0; i < todo.length; i++) {
  111. jobs.push(uploadPartJob(this, todo[i]));
  112. }
  113. const defaultParallel = 5;
  114. var parallel = options.parallel || defaultParallel;
  115. var results = yield gather(jobs, parallel);
  116. // check errors after all jobs are completed
  117. for (var i = 0; i < results.length; i++) {
  118. if (results[i].isError) {
  119. var error = results[i].error;
  120. error.partNum = i;
  121. error.message = 'Failed to upload some parts with error: ' + results[i].error.toString() + " part_num: "+ i;
  122. throw error;
  123. }
  124. }
  125. }
  126. return yield this._completeMultipartUpload(name, uploadId, doneParts, options);
  127. };
  128. /**
  129. * List the on-going multipart uploads
  130. * https://help.aliyun.com/document_detail/31997.html
  131. * @param {Object} options
  132. * @return {Array} the multipart uploads
  133. */
  134. proto.listUploads = function* listUploads(query, options) {
  135. options = options || {};
  136. options.subres = 'uploads';
  137. var params = this._objectRequestParams('GET', '', options)
  138. params.query = query;
  139. params.xmlResponse = true;
  140. params.successStatuses = [200];
  141. var result = yield this.request(params);
  142. var uploads = result.data.Upload || [];
  143. if (!Array.isArray(uploads)) {
  144. uploads = [uploads];
  145. }
  146. uploads = uploads.map(function (up) {
  147. return {
  148. name: up.Key,
  149. uploadId: up.UploadId,
  150. initiated: up.Initiated
  151. };
  152. });
  153. return {
  154. res: result.res,
  155. uploads: uploads,
  156. bucket: result.data.Bucket,
  157. nextKeyMarker: result.data.NextKeyMarker,
  158. nextUploadIdMarker: result.data.NextUploadIdMarker,
  159. isTruncated: result.data.IsTruncated === 'true'
  160. };
  161. };
  162. /**
  163. * Abort a multipart upload transaction
  164. * @param {String} name the object name
  165. * @param {String} uploadId the upload id
  166. * @param {Object} options
  167. */
  168. proto.abortMultipartUpload = function* abortMultipartUpload(name, uploadId, options) {
  169. options = options || {};
  170. options.subres = {uploadId: uploadId};
  171. var params = this._objectRequestParams('DELETE', name, options);
  172. params.successStatuses = [204];
  173. var result = yield this.request(params);
  174. return {
  175. res: result.res
  176. };
  177. };
  178. /**
  179. * Initiate a multipart upload transaction
  180. * @param {String} name the object name
  181. * @param {Object} options
  182. * @return {String} upload id
  183. */
  184. proto._initMultipartUpload = function* _initMultipartUpload(name, options) {
  185. options = options || {};
  186. options.headers = options.headers || {};
  187. this._convertMetaToHeaders(options.meta, options.headers);
  188. options.subres = 'uploads';
  189. var params = this._objectRequestParams('POST', name, options);
  190. params.mime = options.mime;
  191. params.xmlResponse = true;
  192. params.successStatuses = [200];
  193. var result = yield this.request(params);
  194. return {
  195. res: result.res,
  196. bucket: result.data.Bucket,
  197. name: result.data.Key,
  198. uploadId: result.data.UploadId
  199. };
  200. };
  201. /**
  202. * Upload a part in a multipart upload transaction
  203. * @param {String} name the object name
  204. * @param {String} uploadId the upload id
  205. * @param {Integer} partNo the part number
  206. * @param {Object} data the body data
  207. * @param {Object} options
  208. */
  209. proto._uploadPart = function* _uploadPart(name, uploadId, partNo, data, options) {
  210. options = options || {};
  211. options.headers = {
  212. 'Content-Length': data.size
  213. };
  214. options.subres = {
  215. partNumber: partNo,
  216. uploadId: uploadId
  217. };
  218. var params = this._objectRequestParams('PUT', name, options);
  219. params.mime = options.mime;
  220. params.stream = data.stream;
  221. params.successStatuses = [200];
  222. var result = yield this.request(params);
  223. data.stream = null;
  224. params.stream = null;
  225. return {
  226. name: name,
  227. etag: result.res.headers.etag,
  228. res: result.res
  229. };
  230. };
  231. /**
  232. * Complete a multipart upload transaction
  233. * @param {String} name the object name
  234. * @param {String} uploadId the upload id
  235. * @param {Array} parts the uploaded parts
  236. * @param {Object} options
  237. */
  238. proto._completeMultipartUpload = function* _completeMultipartUpload(name, uploadId, parts, options) {
  239. parts.sort((a, b) => a.number - b.number);
  240. var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<CompleteMultipartUpload>\n';
  241. for (var i = 0; i < parts.length; i++) {
  242. var p = parts[i];
  243. xml += '<Part>\n';
  244. xml += '<PartNumber>' + p.number + '</PartNumber>\n';
  245. xml += '<ETag>' + p.etag + '</ETag>\n';
  246. xml += '</Part>\n';
  247. }
  248. xml += '</CompleteMultipartUpload>';
  249. options = options || {};
  250. options.subres = {uploadId: uploadId};
  251. var params = this._objectRequestParams('POST', name, options);
  252. params.mime = 'xml';
  253. params.content = xml;
  254. if (!(options.headers && options.headers['x-oss-callback'])) {
  255. params.xmlResponse = true;
  256. }
  257. params.successStatuses = [200];
  258. var result = yield this.request(params);
  259. var ret = {
  260. res: result.res,
  261. bucket: params.bucket,
  262. name: name,
  263. etag: result.res.headers['etag']
  264. };
  265. if (options.headers && options.headers['x-oss-callback']) {
  266. ret.data = JSON.parse(result.data.toString());
  267. }
  268. return ret;
  269. };
  270. is.file = function (file) {
  271. return typeof(File) !== 'undefined' && file instanceof File;
  272. };
  273. /**
  274. * Get file size
  275. */
  276. proto._getFileSize = function* _getFileSize(file) {
  277. if (is.buffer(file)) {
  278. return file.length;
  279. } else if (is.file(file)) {
  280. return file.size;
  281. } if (is.string(file)) {
  282. var stat = yield this._statFile(file);
  283. return stat.size;
  284. }
  285. throw new Error('_getFileSize requires Buffer/File/String.');
  286. };
  287. /*
  288. * Readable stream for Web File
  289. */
  290. var Readable = require('stream').Readable;
  291. function WebFileReadStream(file, options) {
  292. if (!(this instanceof WebFileReadStream)) {
  293. return new WebFileReadStream(file, options);
  294. }
  295. Readable.call(this, options);
  296. this.file = file;
  297. this.reader = new FileReader();
  298. this.start = 0;
  299. this.finish = false;
  300. this.fileBuffer;
  301. }
  302. util.inherits(WebFileReadStream, Readable);
  303. WebFileReadStream.prototype.readFileAndPush = function readFileAndPush(size) {
  304. if (this.fileBuffer){
  305. var pushRet = true;
  306. while (pushRet && this.fileBuffer && this.start < this.fileBuffer.length) {
  307. var start = this.start;
  308. var end = start + size;
  309. end = end > this.fileBuffer.length ? this.fileBuffer.length : end;
  310. this.start = end;
  311. pushRet = this.push(this.fileBuffer.slice(start, end));
  312. }
  313. }
  314. }
  315. WebFileReadStream.prototype._read = function _read(size) {
  316. if ((this.file && this.start >= this.file.size) ||
  317. (this.fileBuffer && this.start >= this.fileBuffer.length) ||
  318. (this.finish) || (0 == this.start && !this.file)) {
  319. if (!this.finish) {
  320. this.fileBuffer = null;
  321. this.finish = true;
  322. }
  323. this.push(null);
  324. return;
  325. }
  326. var defaultReadSize = 16 * 1024;
  327. size = size ? size : defaultReadSize;
  328. var that = this;
  329. this.reader.onload = function (e) {
  330. that.fileBuffer = new Buffer(new Uint8Array(e.target.result));
  331. that.file = null;
  332. that.readFileAndPush(size);
  333. };
  334. if (0 == this.start) {
  335. this.reader.readAsArrayBuffer(this.file);
  336. } else {
  337. this.readFileAndPush(size);
  338. }
  339. };
  340. proto._createStream = function _createStream(file, start, end) {
  341. if (is.file(file)) {
  342. return new WebFileReadStream(file.slice(start, end));
  343. }
  344. // else if (is.string(file)) {
  345. // return fs.createReadStream(file, {
  346. // start: start,
  347. // end: end - 1
  348. // });
  349. // }
  350. else {
  351. throw new Error('_createStream requires File/String.');
  352. }
  353. };
  354. proto._getPartSize = function _getPartSize(fileSize, partSize) {
  355. var maxNumParts = 10 * 1000;
  356. var defaultPartSize = 1 * 1024 * 1024;
  357. if (!partSize) {
  358. return defaultPartSize;
  359. }
  360. return Math.max(
  361. Math.ceil(fileSize / maxNumParts),
  362. partSize
  363. );
  364. };
  365. proto._divideParts = function _divideParts(fileSize, partSize) {
  366. var numParts = Math.ceil(fileSize / partSize);
  367. var partOffs = [];
  368. for (var i = 0; i < numParts; i++) {
  369. var start = partSize * i;
  370. var end = Math.min(start + partSize, fileSize);
  371. partOffs.push({
  372. start: start,
  373. end: end
  374. });
  375. }
  376. return partOffs;
  377. };