multipart.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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.lookup(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. * @param {Object} options
  131. * @return {Array} the multipart uploads
  132. */
  133. proto.listUploads = function* listUploads(query, options) {
  134. options = options || {};
  135. options.subres = 'uploads';
  136. var params = this._objectRequestParams('GET', '', options)
  137. params.query = query;
  138. params.xmlResponse = true;
  139. params.successStatuses = [200];
  140. var result = yield this.request(params);
  141. var uploads = result.data.Upload || [];
  142. if (!Array.isArray(uploads)) {
  143. uploads = [uploads];
  144. }
  145. uploads = uploads.map(function (up) {
  146. return {
  147. name: up.Key,
  148. uploadId: up.UploadId,
  149. initiated: up.Initiated
  150. };
  151. });
  152. return {
  153. res: result.res,
  154. uploads: uploads,
  155. bucket: result.data.Bucket,
  156. nextKeyMarker: result.data.NextKeyMarker,
  157. nextUploadIdMarker: result.data.NextUploadIdMarker,
  158. isTruncated: result.data.IsTruncated === 'true'
  159. };
  160. };
  161. /**
  162. * Abort a multipart upload transaction
  163. * @param {String} name the object name
  164. * @param {String} uploadId the upload id
  165. * @param {Object} options
  166. */
  167. proto.abortMultipartUpload = function* abortMultipartUpload(name, uploadId, options) {
  168. options = options || {};
  169. options.subres = {uploadId: uploadId};
  170. var params = this._objectRequestParams('DELETE', name, options);
  171. params.successStatuses = [204];
  172. var result = yield this.request(params);
  173. return {
  174. res: result.res
  175. };
  176. };
  177. /**
  178. * Initiate a multipart upload transaction
  179. * @param {String} name the object name
  180. * @param {Object} options
  181. * @return {String} upload id
  182. */
  183. proto._initMultipartUpload = function* _initMultipartUpload(name, options) {
  184. options = options || {};
  185. options.headers = options.headers || {};
  186. this._convertMetaToHeaders(options.meta, options.headers);
  187. options.subres = 'uploads';
  188. var params = this._objectRequestParams('POST', name, options);
  189. params.mime = options.mime;
  190. params.xmlResponse = true;
  191. params.successStatuses = [200];
  192. var result = yield this.request(params);
  193. return {
  194. res: result.res,
  195. bucket: result.data.Bucket,
  196. name: result.data.Key,
  197. uploadId: result.data.UploadId
  198. };
  199. };
  200. /**
  201. * Upload a part in a multipart upload transaction
  202. * @param {String} name the object name
  203. * @param {String} uploadId the upload id
  204. * @param {Integer} partNo the part number
  205. * @param {Object} data the body data
  206. * @param {Object} options
  207. */
  208. proto._uploadPart = function* _uploadPart(name, uploadId, partNo, data, options) {
  209. options = options || {};
  210. options.headers = {
  211. 'Content-Length': data.size
  212. };
  213. options.subres = {
  214. partNumber: partNo,
  215. uploadId: uploadId
  216. };
  217. var params = this._objectRequestParams('PUT', name, options);
  218. params.mime = options.mime;
  219. params.stream = data.stream;
  220. params.successStatuses = [200];
  221. var result = yield this.request(params);
  222. data.stream = null;
  223. params.stream = null;
  224. return {
  225. name: name,
  226. etag: result.res.headers.etag,
  227. res: result.res
  228. };
  229. };
  230. /**
  231. * Complete a multipart upload transaction
  232. * @param {String} name the object name
  233. * @param {String} uploadId the upload id
  234. * @param {Array} parts the uploaded parts
  235. * @param {Object} options
  236. */
  237. proto._completeMultipartUpload = function* _completeMultipartUpload(name, uploadId, parts, options) {
  238. parts.sort((a, b) => a.number - b.number);
  239. var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<CompleteMultipartUpload>\n';
  240. for (var i = 0; i < parts.length; i++) {
  241. var p = parts[i];
  242. xml += '<Part>\n';
  243. xml += '<PartNumber>' + p.number + '</PartNumber>\n';
  244. xml += '<ETag>' + p.etag + '</ETag>\n';
  245. xml += '</Part>\n';
  246. }
  247. xml += '</CompleteMultipartUpload>';
  248. options = options || {};
  249. options.subres = {uploadId: uploadId};
  250. var params = this._objectRequestParams('POST', name, options);
  251. params.mime = 'xml';
  252. params.content = xml;
  253. if (!(options.headers && options.headers['x-oss-callback'])) {
  254. params.xmlResponse = true;
  255. }
  256. params.successStatuses = [200];
  257. var result = yield this.request(params);
  258. var ret = {
  259. res: result.res,
  260. bucket: params.bucket,
  261. name: name,
  262. etag: result.res.headers['etag']
  263. };
  264. if (options.headers && options.headers['x-oss-callback']) {
  265. ret.data = JSON.parse(result.data.toString());
  266. }
  267. return ret;
  268. };
  269. is.file = function (file) {
  270. return typeof(File) !== 'undefined' && file instanceof File;
  271. };
  272. /**
  273. * Get file size
  274. */
  275. proto._getFileSize = function* _getFileSize(file) {
  276. if (is.buffer(file)) {
  277. return file.length;
  278. } else if (is.file(file)) {
  279. return file.size;
  280. } if (is.string(file)) {
  281. var stat = yield this._statFile(file);
  282. return stat.size;
  283. }
  284. throw new Error('_getFileSize requires Buffer/File/String.');
  285. };
  286. /*
  287. * Readable stream for Web File
  288. */
  289. var Readable = require('stream').Readable;
  290. function WebFileReadStream(file, options) {
  291. if (!(this instanceof WebFileReadStream)) {
  292. return new WebFileReadStream(file, options);
  293. }
  294. Readable.call(this, options);
  295. this.file = file;
  296. this.reader = new FileReader();
  297. this.start = 0;
  298. this.finish = false;
  299. this.fileBuffer;
  300. };
  301. util.inherits(WebFileReadStream, Readable);
  302. WebFileReadStream.prototype.readFileAndPush = function readFileAndPush(size) {
  303. if (this.fileBuffer){
  304. var pushRet = true;
  305. while (pushRet && this.fileBuffer && this.start < this.fileBuffer.length) {
  306. var start = this.start;
  307. var end = start + size;
  308. end = end > this.fileBuffer.length ? this.fileBuffer.length : end;
  309. this.start = end;
  310. pushRet = this.push(this.fileBuffer.slice(start, end));
  311. }
  312. }
  313. }
  314. WebFileReadStream.prototype._read = function _read(size) {
  315. if ((this.file && this.start >= this.file.size) ||
  316. (this.fileBuffer && this.start >= this.fileBuffer.length) ||
  317. (this.finish) || (0 == this.start && !this.file)) {
  318. if (!this.finish) {
  319. this.fileBuffer = null;
  320. this.finish = true;
  321. }
  322. this.push(null);
  323. return;
  324. }
  325. var defaultReadSize = 16 * 1024;
  326. size = size ? size : defaultReadSize;
  327. var that = this;
  328. this.reader.onload = function (e) {
  329. that.fileBuffer = new Buffer(new Uint8Array(e.target.result));
  330. that.file = null;
  331. that.readFileAndPush(size);
  332. };
  333. if (0 == this.start) {
  334. this.reader.readAsArrayBuffer(this.file);
  335. } else {
  336. this.readFileAndPush(size);
  337. }
  338. };
  339. proto._createStream = function _createStream(file, start, end) {
  340. if (is.file(file)) {
  341. return new WebFileReadStream(file.slice(start, end));
  342. } else if (is.string(file)) {
  343. return fs.createReadStream(file, {
  344. start: start,
  345. end: end - 1
  346. });
  347. } else {
  348. throw new Error('_createStream requires File/String.');
  349. }
  350. };
  351. proto._getPartSize = function _getPartSize(fileSize, partSize) {
  352. var maxNumParts = 10 * 1000;
  353. var defaultPartSize = 1 * 1024 * 1024;
  354. if (!partSize) {
  355. return defaultPartSize;
  356. }
  357. return Math.max(
  358. Math.ceil(fileSize / maxNumParts),
  359. partSize
  360. );
  361. };
  362. proto._divideParts = function _divideParts(fileSize, partSize) {
  363. var numParts = Math.ceil(fileSize / partSize);
  364. var partOffs = [];
  365. for (var i = 0; i < numParts; i++) {
  366. var start = partSize * i;
  367. var end = Math.min(start + partSize, fileSize);
  368. partOffs.push({
  369. start: start,
  370. end: end
  371. });
  372. }
  373. return partOffs;
  374. };