multipart-copy.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. /* eslint-disable no-async-promise-executor */
  2. const debug = require('debug')('ali-oss:multipart-copy');
  3. const copy = require('copy-to');
  4. const proto = exports;
  5. /**
  6. * Upload a part copy in a multipart from the source bucket/object
  7. * used with initMultipartUpload and completeMultipartUpload.
  8. * @param {String} name copy object name
  9. * @param {String} uploadId the upload id
  10. * @param {Number} partNo the part number
  11. * @param {String} range like 0-102400 part size need to copy
  12. * @param {Object} sourceData
  13. * {String} sourceData.sourceKey the source object name
  14. * {String} sourceData.sourceBucketName the source bucket name
  15. * @param {Object} options
  16. */
  17. /* eslint max-len: [0] */
  18. proto.uploadPartCopy = async function uploadPartCopy(name, uploadId, partNo, range, sourceData, options = {}) {
  19. options.headers = options.headers || {};
  20. const versionId = options.versionId || (options.subres && options.subres.versionId) || null;
  21. let copySource;
  22. if (versionId) {
  23. copySource = `/${sourceData.sourceBucketName}/${encodeURIComponent(sourceData.sourceKey)}?versionId=${versionId}`;
  24. } else {
  25. copySource = `/${sourceData.sourceBucketName}/${encodeURIComponent(sourceData.sourceKey)}`;
  26. }
  27. options.headers['x-oss-copy-source'] = copySource;
  28. if (range) {
  29. options.headers['x-oss-copy-source-range'] = `bytes=${range}`;
  30. }
  31. options.subres = {
  32. partNumber: partNo,
  33. uploadId
  34. };
  35. const params = this._objectRequestParams('PUT', name, options);
  36. params.mime = options.mime;
  37. params.successStatuses = [200];
  38. const result = await this.request(params);
  39. return {
  40. name,
  41. etag: result.res.headers.etag,
  42. res: result.res
  43. };
  44. };
  45. /**
  46. * @param {String} name copy object name
  47. * @param {Object} sourceData
  48. * {String} sourceData.sourceKey the source object name
  49. * {String} sourceData.sourceBucketName the source bucket name
  50. * {Number} sourceData.startOffset data copy start byte offset, e.g: 0
  51. * {Number} sourceData.endOffset data copy end byte offset, e.g: 102400
  52. * @param {Object} options
  53. * {Number} options.partSize
  54. */
  55. proto.multipartUploadCopy = async function multipartUploadCopy(name, sourceData, options = {}) {
  56. this.resetCancelFlag();
  57. const { versionId = null } = options;
  58. const metaOpt = {
  59. versionId
  60. };
  61. const objectMeta = await this._getObjectMeta(sourceData.sourceBucketName, sourceData.sourceKey, metaOpt);
  62. const fileSize = objectMeta.res.headers['content-length'];
  63. sourceData.startOffset = sourceData.startOffset || 0;
  64. sourceData.endOffset = sourceData.endOffset || fileSize;
  65. if (options.checkpoint && options.checkpoint.uploadId) {
  66. return await this._resumeMultipartCopy(options.checkpoint, sourceData, options);
  67. }
  68. const minPartSize = 100 * 1024;
  69. const copySize = sourceData.endOffset - sourceData.startOffset;
  70. if (copySize < minPartSize) {
  71. throw new Error(`copySize must not be smaller than ${minPartSize}`);
  72. }
  73. if (options.partSize && options.partSize < minPartSize) {
  74. throw new Error(`partSize must not be smaller than ${minPartSize}`);
  75. }
  76. const init = await this.initMultipartUpload(name, options);
  77. const { uploadId } = init;
  78. const partSize = this._getPartSize(copySize, options.partSize);
  79. const checkpoint = {
  80. name,
  81. copySize,
  82. partSize,
  83. uploadId,
  84. doneParts: []
  85. };
  86. if (options && options.progress) {
  87. await options.progress(0, checkpoint, init.res);
  88. }
  89. return await this._resumeMultipartCopy(checkpoint, sourceData, options);
  90. };
  91. /*
  92. * Resume multipart copy from checkpoint. The checkpoint will be
  93. * updated after each successful part copy.
  94. * @param {Object} checkpoint the checkpoint
  95. * @param {Object} options
  96. */
  97. proto._resumeMultipartCopy = async function _resumeMultipartCopy(checkpoint, sourceData, options) {
  98. if (this.isCancel()) {
  99. throw this._makeCancelEvent();
  100. }
  101. const { versionId = null } = options;
  102. const metaOpt = {
  103. versionId
  104. };
  105. const {
  106. copySize, partSize, uploadId, doneParts, name
  107. } = checkpoint;
  108. const partOffs = this._divideMultipartCopyParts(copySize, partSize, sourceData.startOffset);
  109. const numParts = partOffs.length;
  110. const uploadPartCopyOptions = {
  111. headers: {}
  112. };
  113. if (options.copyheaders) {
  114. copy(options.copyheaders).to(uploadPartCopyOptions.headers);
  115. }
  116. if (versionId) {
  117. copy(metaOpt).to(uploadPartCopyOptions);
  118. }
  119. const uploadPartJob = function uploadPartJob(self, partNo, source) {
  120. return new Promise(async (resolve, reject) => {
  121. try {
  122. if (!self.isCancel()) {
  123. const pi = partOffs[partNo - 1];
  124. const range = `${pi.start}-${pi.end - 1}`;
  125. let result;
  126. try {
  127. result = await self.uploadPartCopy(name, uploadId, partNo, range, source, uploadPartCopyOptions);
  128. } catch (error) {
  129. if (error.status === 404) {
  130. throw self._makeAbortEvent();
  131. }
  132. throw error;
  133. }
  134. if (!self.isCancel()) {
  135. debug(`content-range ${result.res.headers['content-range']}`);
  136. doneParts.push({
  137. number: partNo,
  138. etag: result.res.headers.etag
  139. });
  140. checkpoint.doneParts = doneParts;
  141. if (options && options.progress) {
  142. await options.progress(doneParts.length / numParts, checkpoint, result.res);
  143. }
  144. }
  145. }
  146. resolve();
  147. } catch (err) {
  148. err.partNum = partNo;
  149. reject(err);
  150. }
  151. });
  152. };
  153. const all = Array.from(new Array(numParts), (x, i) => i + 1);
  154. const done = doneParts.map(p => p.number);
  155. const todo = all.filter(p => done.indexOf(p) < 0);
  156. const defaultParallel = 5;
  157. const parallel = options.parallel || defaultParallel;
  158. if (this.checkBrowserAndVersion('Internet Explorer', '10') || parallel === 1) {
  159. for (let i = 0; i < todo.length; i++) {
  160. if (this.isCancel()) {
  161. throw this._makeCancelEvent();
  162. }
  163. /* eslint no-await-in-loop: [0] */
  164. await uploadPartJob(this, todo[i], sourceData);
  165. }
  166. } else {
  167. // upload in parallel
  168. const errors = await this._parallelNode(todo, parallel, uploadPartJob, sourceData);
  169. const abortEvent = errors.find(err => err.name === 'abort');
  170. if (abortEvent) throw abortEvent;
  171. if (this.isCancel()) {
  172. throw this._makeCancelEvent();
  173. }
  174. // check errors after all jobs are completed
  175. if (errors && errors.length > 0) {
  176. const err = errors[0];
  177. err.message = `Failed to copy some parts with error: ${err.toString()} part_num: ${err.partNum}`;
  178. throw err;
  179. }
  180. }
  181. return await this.completeMultipartUpload(name, uploadId, doneParts, options);
  182. };
  183. proto._divideMultipartCopyParts = function _divideMultipartCopyParts(fileSize, partSize, startOffset) {
  184. const numParts = Math.ceil(fileSize / partSize);
  185. const partOffs = [];
  186. for (let i = 0; i < numParts; i++) {
  187. const start = (partSize * i) + startOffset;
  188. const end = Math.min(start + partSize, fileSize + startOffset);
  189. partOffs.push({
  190. start,
  191. end
  192. });
  193. }
  194. return partOffs;
  195. };
  196. /**
  197. * Get Object Meta
  198. * @param {String} bucket bucket name
  199. * @param {String} name object name
  200. * @param {Object} options
  201. */
  202. proto._getObjectMeta = async function _getObjectMeta(bucket, name, options) {
  203. const currentBucket = this.getBucket();
  204. this.setBucket(bucket);
  205. const data = await this.head(name, options);
  206. this.setBucket(currentBucket);
  207. return data;
  208. };