const fs = require('fs');
							 | 
						|
								const is = require('is-type-of');
							 | 
						|
								const util = require('util');
							 | 
						|
								const path = require('path');
							 | 
						|
								const mime = require('mime');
							 | 
						|
								const { isFile } = require('./common/utils/isFile');
							 | 
						|
								const { isArray } = require('./common/utils/isArray');
							 | 
						|
								const { isBuffer } = require('./common/utils/isBuffer');
							 | 
						|
								const { retry } = require('./common/utils/retry');
							 | 
						|
								
							 | 
						|
								const proto = exports;
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Multipart operations
							 | 
						|
								 */
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Upload a file to OSS using multipart uploads
							 | 
						|
								 * @param {String} name
							 | 
						|
								 * @param {String|File|Buffer} file
							 | 
						|
								 * @param {Object} options
							 | 
						|
								 *        {Object} [options.callback] The callback parameter is composed of a JSON string encoded in Base64
							 | 
						|
								 *        {String} options.callback.url the OSS sends a callback request to this URL
							 | 
						|
								 *        {String} [options.callback.host] The host header value for initiating callback requests
							 | 
						|
								 *        {String} options.callback.body The value of the request body when a callback is initiated
							 | 
						|
								 *        {String} [options.callback.contentType] The Content-Type of the callback requests initiated
							 | 
						|
								 *        {Boolean} [options.callback.callbackSNI] Whether OSS sends SNI to the origin address specified by callbackUrl when a callback request is initiated from the client
							 | 
						|
								 *        {Object} [options.callback.customValue] Custom parameters are a map of key-values, e.g:
							 | 
						|
								 *                  customValue = {
							 | 
						|
								 *                    key1: 'value1',
							 | 
						|
								 *                    key2: 'value2'
							 | 
						|
								 *                  }
							 | 
						|
								 */
							 | 
						|
								proto.multipartUpload = async function multipartUpload(name, file, options) {
							 | 
						|
								  this.resetCancelFlag();
							 | 
						|
								  options = options || {};
							 | 
						|
								  if (options.checkpoint && options.checkpoint.uploadId) {
							 | 
						|
								    return await this._resumeMultipart(options.checkpoint, options);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const minPartSize = 100 * 1024;
							 | 
						|
								  if (!options.mime) {
							 | 
						|
								    if (isFile(file)) {
							 | 
						|
								      options.mime = mime.getType(path.extname(file.name));
							 | 
						|
								    } else if (isBuffer(file)) {
							 | 
						|
								      options.mime = '';
							 | 
						|
								    } else {
							 | 
						|
								      options.mime = mime.getType(path.extname(file));
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  options.headers = options.headers || {};
							 | 
						|
								  this._convertMetaToHeaders(options.meta, options.headers);
							 | 
						|
								
							 | 
						|
								  const fileSize = await this._getFileSize(file);
							 | 
						|
								  if (fileSize < minPartSize) {
							 | 
						|
								    options.contentLength = fileSize;
							 | 
						|
								    const result = await this.put(name, file, options);
							 | 
						|
								    if (options && options.progress) {
							 | 
						|
								      await options.progress(1);
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    const ret = {
							 | 
						|
								      res: result.res,
							 | 
						|
								      bucket: this.options.bucket,
							 | 
						|
								      name,
							 | 
						|
								      etag: result.res.headers.etag
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    if ((options.headers && options.headers['x-oss-callback']) || options.callback) {
							 | 
						|
								      ret.data = result.data;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    return ret;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (options.partSize && !(parseInt(options.partSize, 10) === options.partSize)) {
							 | 
						|
								    throw new Error('partSize must be int number');
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (options.partSize && options.partSize < minPartSize) {
							 | 
						|
								    throw new Error(`partSize must not be smaller than ${minPartSize}`);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const initResult = await this.initMultipartUpload(name, options);
							 | 
						|
								  const { uploadId } = initResult;
							 | 
						|
								  const partSize = this._getPartSize(fileSize, options.partSize);
							 | 
						|
								
							 | 
						|
								  const checkpoint = {
							 | 
						|
								    file,
							 | 
						|
								    name,
							 | 
						|
								    fileSize,
							 | 
						|
								    partSize,
							 | 
						|
								    uploadId,
							 | 
						|
								    doneParts: []
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  if (options && options.progress) {
							 | 
						|
								    await options.progress(0, checkpoint, initResult.res);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return await this._resumeMultipart(checkpoint, options);
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/*
							 | 
						|
								 * Resume multipart upload from checkpoint. The checkpoint will be
							 | 
						|
								 * updated after each successful part upload.
							 | 
						|
								 * @param {Object} checkpoint the checkpoint
							 | 
						|
								 * @param {Object} options
							 | 
						|
								 */
							 | 
						|
								proto._resumeMultipart = async function _resumeMultipart(checkpoint, options) {
							 | 
						|
								  const that = this;
							 | 
						|
								  if (this.isCancel()) {
							 | 
						|
								    throw this._makeCancelEvent();
							 | 
						|
								  }
							 | 
						|
								  const { file, fileSize, partSize, uploadId, doneParts, name } = checkpoint;
							 | 
						|
								
							 | 
						|
								  const partOffs = this._divideParts(fileSize, partSize);
							 | 
						|
								  const numParts = partOffs.length;
							 | 
						|
								  let uploadPartJob = retry(
							 | 
						|
								    (self, partNo) => {
							 | 
						|
								      // eslint-disable-next-line no-async-promise-executor
							 | 
						|
								      return new Promise(async (resolve, reject) => {
							 | 
						|
								        try {
							 | 
						|
								          if (!self.isCancel()) {
							 | 
						|
								            const pi = partOffs[partNo - 1];
							 | 
						|
								            const stream = await self._createStream(file, pi.start, pi.end);
							 | 
						|
								            const data = {
							 | 
						|
								              stream,
							 | 
						|
								              size: pi.end - pi.start
							 | 
						|
								            };
							 | 
						|
								
							 | 
						|
								            if (isArray(self.multipartUploadStreams)) {
							 | 
						|
								              self.multipartUploadStreams.push(data.stream);
							 | 
						|
								            } else {
							 | 
						|
								              self.multipartUploadStreams = [data.stream];
							 | 
						|
								            }
							 | 
						|
								
							 | 
						|
								            const removeStreamFromMultipartUploadStreams = function () {
							 | 
						|
								              if (!stream.destroyed) {
							 | 
						|
								                stream.destroy();
							 | 
						|
								              }
							 | 
						|
								              const index = self.multipartUploadStreams.indexOf(stream);
							 | 
						|
								              if (index !== -1) {
							 | 
						|
								                self.multipartUploadStreams.splice(index, 1);
							 | 
						|
								              }
							 | 
						|
								            };
							 | 
						|
								
							 | 
						|
								            stream.on('close', removeStreamFromMultipartUploadStreams);
							 | 
						|
								            stream.on('error', removeStreamFromMultipartUploadStreams);
							 | 
						|
								
							 | 
						|
								            let result;
							 | 
						|
								            try {
							 | 
						|
								              result = await self._uploadPart(name, uploadId, partNo, data, options);
							 | 
						|
								            } catch (error) {
							 | 
						|
								              removeStreamFromMultipartUploadStreams();
							 | 
						|
								              if (error.status === 404) {
							 | 
						|
								                throw self._makeAbortEvent();
							 | 
						|
								              }
							 | 
						|
								              throw error;
							 | 
						|
								            }
							 | 
						|
								            if (!self.isCancel()) {
							 | 
						|
								              doneParts.push({
							 | 
						|
								                number: partNo,
							 | 
						|
								                etag: result.res.headers.etag
							 | 
						|
								              });
							 | 
						|
								              checkpoint.doneParts = doneParts;
							 | 
						|
								
							 | 
						|
								              if (options.progress) {
							 | 
						|
								                await options.progress(doneParts.length / (numParts + 1), checkpoint, result.res);
							 | 
						|
								              }
							 | 
						|
								            }
							 | 
						|
								          }
							 | 
						|
								          resolve();
							 | 
						|
								        } catch (err) {
							 | 
						|
								          err.partNum = partNo;
							 | 
						|
								          reject(err);
							 | 
						|
								        }
							 | 
						|
								      });
							 | 
						|
								    },
							 | 
						|
								    this.options.retryMax,
							 | 
						|
								    {
							 | 
						|
								      errorHandler: err => {
							 | 
						|
								        const _errHandle = _err => {
							 | 
						|
								          const statusErr = [-1, -2].includes(_err.status);
							 | 
						|
								          const requestErrorRetryHandle = this.options.requestErrorRetryHandle || (() => true);
							 | 
						|
								          return statusErr && requestErrorRetryHandle(_err);
							 | 
						|
								        };
							 | 
						|
								        return !!_errHandle(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]);
							 | 
						|
								    }
							 | 
						|
								  } else {
							 | 
						|
								    // upload in parallel
							 | 
						|
								    const jobErr = await this._parallel(todo, parallel, value => {
							 | 
						|
								      return new Promise((resolve, reject) => {
							 | 
						|
								        uploadPartJob(that, value)
							 | 
						|
								          .then(() => {
							 | 
						|
								            resolve();
							 | 
						|
								          })
							 | 
						|
								          .catch(reject);
							 | 
						|
								      });
							 | 
						|
								    });
							 | 
						|
								
							 | 
						|
								    const abortEvent = jobErr.find(err => err.name === 'abort');
							 | 
						|
								    if (abortEvent) throw abortEvent;
							 | 
						|
								
							 | 
						|
								    if (this.isCancel()) {
							 | 
						|
								      uploadPartJob = null;
							 | 
						|
								      throw this._makeCancelEvent();
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    if (jobErr && jobErr.length > 0) {
							 | 
						|
								      jobErr[0].message = `Failed to upload some parts with error: ${jobErr[0].toString()} part_num: ${
							 | 
						|
								        jobErr[0].partNum
							 | 
						|
								      }`;
							 | 
						|
								      throw jobErr[0];
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return await this.completeMultipartUpload(name, uploadId, doneParts, options);
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Get file size
							 | 
						|
								 */
							 | 
						|
								proto._getFileSize = async function _getFileSize(file) {
							 | 
						|
								  if (isBuffer(file)) {
							 | 
						|
								    return file.length;
							 | 
						|
								  } else if (isFile(file)) {
							 | 
						|
								    return file.size;
							 | 
						|
								  } else if (is.string(file)) {
							 | 
						|
								    const stat = await this._statFile(file);
							 | 
						|
								    return stat.size;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  throw new Error('_getFileSize requires Buffer/File/String.');
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/*
							 | 
						|
								 * Readable stream for Web File
							 | 
						|
								 */
							 | 
						|
								const { Readable } = require('stream');
							 | 
						|
								
							 | 
						|
								function WebFileReadStream(file, options) {
							 | 
						|
								  if (!(this instanceof WebFileReadStream)) {
							 | 
						|
								    return new WebFileReadStream(file, options);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  Readable.call(this, options);
							 | 
						|
								
							 | 
						|
								  this.file = file;
							 | 
						|
								  this.reader = new FileReader();
							 | 
						|
								  this.start = 0;
							 | 
						|
								  this.finish = false;
							 | 
						|
								  this.fileBuffer = null;
							 | 
						|
								}
							 | 
						|
								util.inherits(WebFileReadStream, Readable);
							 | 
						|
								
							 | 
						|
								WebFileReadStream.prototype.readFileAndPush = function readFileAndPush(size) {
							 | 
						|
								  if (this.fileBuffer) {
							 | 
						|
								    let pushRet = true;
							 | 
						|
								    while (pushRet && this.fileBuffer && this.start < this.fileBuffer.length) {
							 | 
						|
								      const { start } = this;
							 | 
						|
								      let end = start + size;
							 | 
						|
								      end = end > this.fileBuffer.length ? this.fileBuffer.length : end;
							 | 
						|
								      this.start = end;
							 | 
						|
								      pushRet = this.push(this.fileBuffer.slice(start, end));
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								WebFileReadStream.prototype._read = function _read(size) {
							 | 
						|
								  if (
							 | 
						|
								    (this.file && this.start >= this.file.size) ||
							 | 
						|
								    (this.fileBuffer && this.start >= this.fileBuffer.length) ||
							 | 
						|
								    this.finish ||
							 | 
						|
								    (this.start === 0 && !this.file)
							 | 
						|
								  ) {
							 | 
						|
								    if (!this.finish) {
							 | 
						|
								      this.fileBuffer = null;
							 | 
						|
								      this.finish = true;
							 | 
						|
								    }
							 | 
						|
								    this.push(null);
							 | 
						|
								    return;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const defaultReadSize = 16 * 1024;
							 | 
						|
								  size = size || defaultReadSize;
							 | 
						|
								
							 | 
						|
								  const that = this;
							 | 
						|
								  this.reader.onload = function (e) {
							 | 
						|
								    that.fileBuffer = Buffer.from(new Uint8Array(e.target.result));
							 | 
						|
								    that.file = null;
							 | 
						|
								    that.readFileAndPush(size);
							 | 
						|
								  };
							 | 
						|
								  this.reader.onerror = function onload(e) {
							 | 
						|
								    const error = e.srcElement && e.srcElement.error;
							 | 
						|
								    if (error) {
							 | 
						|
								      throw error;
							 | 
						|
								    }
							 | 
						|
								    throw e;
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  if (this.start === 0) {
							 | 
						|
								    this.reader.readAsArrayBuffer(this.file);
							 | 
						|
								  } else {
							 | 
						|
								    this.readFileAndPush(size);
							 | 
						|
								  }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								proto._createStream = function _createStream(file, start, end) {
							 | 
						|
								  if (is.readableStream(file)) {
							 | 
						|
								    return file;
							 | 
						|
								  } else if (isFile(file)) {
							 | 
						|
								    return new WebFileReadStream(file.slice(start, end));
							 | 
						|
								  } else if (isBuffer(file)) {
							 | 
						|
								    const iterable = file.subarray(start, end);
							 | 
						|
								    // we can't use Readable.from() since it is only support in Node v10
							 | 
						|
								    return new Readable({
							 | 
						|
								      read() {
							 | 
						|
								        this.push(iterable);
							 | 
						|
								        this.push(null);
							 | 
						|
								      }
							 | 
						|
								    });
							 | 
						|
								  } else if (is.string(file)) {
							 | 
						|
								    return fs.createReadStream(file, {
							 | 
						|
								      start,
							 | 
						|
								      end: end - 1
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								  throw new Error('_createStream requires Buffer/File/String.');
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								proto._getPartSize = function _getPartSize(fileSize, partSize) {
							 | 
						|
								  const maxNumParts = 10 * 1000;
							 | 
						|
								  const defaultPartSize = 1 * 1024 * 1024;
							 | 
						|
								
							 | 
						|
								  if (!partSize) partSize = defaultPartSize;
							 | 
						|
								  const safeSize = Math.ceil(fileSize / maxNumParts);
							 | 
						|
								
							 | 
						|
								  if (partSize < safeSize) {
							 | 
						|
								    partSize = safeSize;
							 | 
						|
								    console.warn(
							 | 
						|
								      `partSize has been set to ${partSize}, because the partSize you provided causes partNumber to be greater than 10,000`
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								  return partSize;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								proto._divideParts = function _divideParts(fileSize, partSize) {
							 | 
						|
								  const numParts = Math.ceil(fileSize / partSize);
							 | 
						|
								
							 | 
						|
								  const partOffs = [];
							 | 
						|
								  for (let i = 0; i < numParts; i++) {
							 | 
						|
								    const start = partSize * i;
							 | 
						|
								    const end = Math.min(start + partSize, fileSize);
							 | 
						|
								
							 | 
						|
								    partOffs.push({
							 | 
						|
								      start,
							 | 
						|
								      end
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return partOffs;
							 | 
						|
								};
							 |