| /* eslint-disable no-async-promise-executor */ | |
| 
 | |
| const debug = require('debug')('ali-oss:multipart-copy'); | |
| const copy = require('copy-to'); | |
| 
 | |
| const proto = exports; | |
| 
 | |
| /** | |
|  * Upload a part copy in a multipart from the source bucket/object | |
|  * used with initMultipartUpload and completeMultipartUpload. | |
|  * @param {String} name copy object name | |
|  * @param {String} uploadId the upload id | |
|  * @param {Number} partNo the part number | |
|  * @param {String} range  like 0-102400  part size need to copy | |
|  * @param {Object} sourceData | |
|  *        {String} sourceData.sourceKey  the source object name | |
|  *        {String} sourceData.sourceBucketName  the source bucket name | |
|  * @param {Object} options | |
|  */ | |
| /* eslint max-len: [0] */ | |
| proto.uploadPartCopy = async function uploadPartCopy(name, uploadId, partNo, range, sourceData, options = {}) { | |
|   options.headers = options.headers || {}; | |
|   const versionId = options.versionId || (options.subres && options.subres.versionId) || null; | |
|   let copySource; | |
|   if (versionId) { | |
|     copySource = `/${sourceData.sourceBucketName}/${encodeURIComponent(sourceData.sourceKey)}?versionId=${versionId}`; | |
|   } else { | |
|     copySource = `/${sourceData.sourceBucketName}/${encodeURIComponent(sourceData.sourceKey)}`; | |
|   } | |
| 
 | |
|   options.headers['x-oss-copy-source'] = copySource; | |
|   if (range) { | |
|     options.headers['x-oss-copy-source-range'] = `bytes=${range}`; | |
|   } | |
| 
 | |
|   options.subres = { | |
|     partNumber: partNo, | |
|     uploadId | |
|   }; | |
|   const params = this._objectRequestParams('PUT', name, options); | |
|   params.mime = options.mime; | |
|   params.successStatuses = [200]; | |
| 
 | |
|   const result = await this.request(params); | |
| 
 | |
|   return { | |
|     name, | |
|     etag: result.res.headers.etag, | |
|     res: result.res | |
|   }; | |
| }; | |
| 
 | |
| /** | |
|  * @param {String} name copy object name | |
|  * @param {Object} sourceData | |
|  *        {String} sourceData.sourceKey  the source object name | |
|  *        {String} sourceData.sourceBucketName  the source bucket name | |
|  *        {Number} sourceData.startOffset  data copy start byte offset, e.g: 0 | |
|  *        {Number} sourceData.endOffset  data copy end byte offset, e.g: 102400 | |
|  * @param {Object} options | |
|  *        {Number} options.partSize | |
|  */ | |
| proto.multipartUploadCopy = async function multipartUploadCopy(name, sourceData, options = {}) { | |
|   this.resetCancelFlag(); | |
|   const { versionId = null } = options; | |
|   const metaOpt = { | |
|     versionId | |
|   }; | |
|   const objectMeta = await this._getObjectMeta(sourceData.sourceBucketName, sourceData.sourceKey, metaOpt); | |
|   const fileSize = objectMeta.res.headers['content-length']; | |
|   sourceData.startOffset = sourceData.startOffset || 0; | |
|   sourceData.endOffset = sourceData.endOffset || fileSize; | |
| 
 | |
|   if (options.checkpoint && options.checkpoint.uploadId) { | |
|     return await this._resumeMultipartCopy(options.checkpoint, sourceData, options); | |
|   } | |
| 
 | |
|   const minPartSize = 100 * 1024; | |
| 
 | |
|   const copySize = sourceData.endOffset - sourceData.startOffset; | |
|   if (copySize < minPartSize) { | |
|     throw new Error(`copySize must not be smaller than ${minPartSize}`); | |
|   } | |
| 
 | |
|   if (options.partSize && options.partSize < minPartSize) { | |
|     throw new Error(`partSize must not be smaller than ${minPartSize}`); | |
|   } | |
| 
 | |
|   const init = await this.initMultipartUpload(name, options); | |
|   const { uploadId } = init; | |
|   const partSize = this._getPartSize(copySize, options.partSize); | |
| 
 | |
|   const checkpoint = { | |
|     name, | |
|     copySize, | |
|     partSize, | |
|     uploadId, | |
|     doneParts: [] | |
|   }; | |
| 
 | |
|   if (options && options.progress) { | |
|     await options.progress(0, checkpoint, init.res); | |
|   } | |
| 
 | |
|   return await this._resumeMultipartCopy(checkpoint, sourceData, options); | |
| }; | |
| 
 | |
| /* | |
|  * Resume multipart copy from checkpoint. The checkpoint will be | |
|  * updated after each successful part copy. | |
|  * @param {Object} checkpoint the checkpoint | |
|  * @param {Object} options | |
|  */ | |
| proto._resumeMultipartCopy = async function _resumeMultipartCopy(checkpoint, sourceData, options) { | |
|   if (this.isCancel()) { | |
|     throw this._makeCancelEvent(); | |
|   } | |
|   const { versionId = null } = options; | |
|   const metaOpt = { | |
|     versionId | |
|   }; | |
|   const { copySize, partSize, uploadId, doneParts, name } = checkpoint; | |
| 
 | |
|   const partOffs = this._divideMultipartCopyParts(copySize, partSize, sourceData.startOffset); | |
|   const numParts = partOffs.length; | |
| 
 | |
|   const uploadPartCopyOptions = { | |
|     headers: {} | |
|   }; | |
| 
 | |
|   if (options.copyheaders) { | |
|     copy(options.copyheaders).to(uploadPartCopyOptions.headers); | |
|   } | |
|   if (versionId) { | |
|     copy(metaOpt).to(uploadPartCopyOptions); | |
|   } | |
| 
 | |
|   const uploadPartJob = function uploadPartJob(self, partNo, source) { | |
|     return new Promise(async (resolve, reject) => { | |
|       try { | |
|         if (!self.isCancel()) { | |
|           const pi = partOffs[partNo - 1]; | |
|           const range = `${pi.start}-${pi.end - 1}`; | |
| 
 | |
|           let result; | |
|           try { | |
|             result = await self.uploadPartCopy(name, uploadId, partNo, range, source, uploadPartCopyOptions); | |
|           } catch (error) { | |
|             if (error.status === 404) { | |
|               throw self._makeAbortEvent(); | |
|             } | |
|             throw error; | |
|           } | |
|           if (!self.isCancel()) { | |
|             debug(`content-range ${result.res.headers['content-range']}`); | |
|             doneParts.push({ | |
|               number: partNo, | |
|               etag: result.res.headers.etag | |
|             }); | |
|             checkpoint.doneParts = doneParts; | |
| 
 | |
|             if (options && options.progress) { | |
|               await options.progress(doneParts.length / numParts, checkpoint, result.res); | |
|             } | |
|           } | |
|         } | |
|         resolve(); | |
|       } catch (err) { | |
|         err.partNum = partNo; | |
|         reject(err); | |
|       } | |
|     }); | |
|   }; | |
| 
 | |
|   const all = Array.from(new Array(numParts), (x, i) => i + 1); | |
|   const done = doneParts.map(p => p.number); | |
|   const todo = all.filter(p => done.indexOf(p) < 0); | |
|   const defaultParallel = 5; | |
|   const parallel = options.parallel || defaultParallel; | |
| 
 | |
|   if (this.checkBrowserAndVersion('Internet Explorer', '10') || parallel === 1) { | |
|     for (let i = 0; i < todo.length; i++) { | |
|       if (this.isCancel()) { | |
|         throw this._makeCancelEvent(); | |
|       } | |
|       /* eslint no-await-in-loop: [0] */ | |
|       await uploadPartJob(this, todo[i], sourceData); | |
|     } | |
|   } else { | |
|     // upload in parallel | |
|     const errors = await this._parallelNode(todo, parallel, uploadPartJob, sourceData); | |
| 
 | |
|     const abortEvent = errors.find(err => err.name === 'abort'); | |
|     if (abortEvent) throw abortEvent; | |
| 
 | |
|     if (this.isCancel()) { | |
|       throw this._makeCancelEvent(); | |
|     } | |
| 
 | |
|     // check errors after all jobs are completed | |
|     if (errors && errors.length > 0) { | |
|       const err = errors[0]; | |
|       err.message = `Failed to copy some parts with error: ${err.toString()} part_num: ${err.partNum}`; | |
|       throw err; | |
|     } | |
|   } | |
| 
 | |
|   return await this.completeMultipartUpload(name, uploadId, doneParts, options); | |
| }; | |
| 
 | |
| proto._divideMultipartCopyParts = function _divideMultipartCopyParts(fileSize, partSize, startOffset) { | |
|   const numParts = Math.ceil(fileSize / partSize); | |
| 
 | |
|   const partOffs = []; | |
|   for (let i = 0; i < numParts; i++) { | |
|     const start = partSize * i + startOffset; | |
|     const end = Math.min(start + partSize, fileSize + startOffset); | |
| 
 | |
|     partOffs.push({ | |
|       start, | |
|       end | |
|     }); | |
|   } | |
| 
 | |
|   return partOffs; | |
| }; | |
| 
 | |
| /** | |
|  * Get Object Meta | |
|  * @param {String} bucket  bucket name | |
|  * @param {String} name   object name | |
|  * @param {Object} options | |
|  */ | |
| proto._getObjectMeta = async function _getObjectMeta(bucket, name, options) { | |
|   const currentBucket = this.getBucket(); | |
|   this.setBucket(bucket); | |
|   const data = await this.head(name, options); | |
|   this.setBucket(currentBucket); | |
|   return data; | |
| };
 |