const Base = require('sdk-base');
							 | 
						|
								const util = require('util');
							 | 
						|
								const ready = require('get-ready');
							 | 
						|
								const copy = require('copy-to');
							 | 
						|
								const currentIP = require('address').ip();
							 | 
						|
								
							 | 
						|
								const RR = 'roundRobin';
							 | 
						|
								const MS = 'masterSlave';
							 | 
						|
								
							 | 
						|
								module.exports = function (OssClient) {
							 | 
						|
								  function Client(options) {
							 | 
						|
								    if (!(this instanceof Client)) {
							 | 
						|
								      return new Client(options);
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    if (!options || !Array.isArray(options.cluster)) {
							 | 
						|
								      throw new Error('require options.cluster to be an array');
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    Base.call(this);
							 | 
						|
								
							 | 
						|
								    this.clients = [];
							 | 
						|
								    this.availables = {};
							 | 
						|
								
							 | 
						|
								    for (let i = 0; i < options.cluster.length; i++) {
							 | 
						|
								      const opt = options.cluster[i];
							 | 
						|
								      copy(options).pick('timeout', 'agent', 'urllib').to(opt);
							 | 
						|
								      this.clients.push(new OssClient(opt));
							 | 
						|
								      this.availables[i] = true;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    this.schedule = options.schedule || RR;
							 | 
						|
								    // only read from master, default is false
							 | 
						|
								    this.masterOnly = !!options.masterOnly;
							 | 
						|
								    this.index = 0;
							 | 
						|
								
							 | 
						|
								    const heartbeatInterval = options.heartbeatInterval || 10000;
							 | 
						|
								    this._checkAvailableLock = false;
							 | 
						|
								    this._timerId = this._deferInterval(this._checkAvailable.bind(this, true), heartbeatInterval);
							 | 
						|
								    this._ignoreStatusFile = options.ignoreStatusFile || false;
							 | 
						|
								    this._init();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  util.inherits(Client, Base);
							 | 
						|
								  const proto = Client.prototype;
							 | 
						|
								  ready.mixin(proto);
							 | 
						|
								
							 | 
						|
								  const GET_METHODS = ['head', 'get', 'getStream', 'list', 'getACL'];
							 | 
						|
								
							 | 
						|
								  const PUT_METHODS = ['put', 'putStream', 'delete', 'deleteMulti', 'copy', 'putMeta', 'putACL'];
							 | 
						|
								
							 | 
						|
								  GET_METHODS.forEach(method => {
							 | 
						|
								    proto[method] = async function (...args) {
							 | 
						|
								      const client = this.chooseAvailable();
							 | 
						|
								      let lastError;
							 | 
						|
								      try {
							 | 
						|
								        return await client[method](...args);
							 | 
						|
								      } catch (err) {
							 | 
						|
								        if (err.status && err.status >= 200 && err.status < 500) {
							 | 
						|
								          // 200 ~ 499 belong to normal response, don't try again
							 | 
						|
								          throw err;
							 | 
						|
								        }
							 | 
						|
								        // < 200 || >= 500 need to retry from other cluser node
							 | 
						|
								        lastError = err;
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      for (let i = 0; i < this.clients.length; i++) {
							 | 
						|
								        const c = this.clients[i];
							 | 
						|
								        if (c !== client) {
							 | 
						|
								          try {
							 | 
						|
								            return await c[method].apply(client, args);
							 | 
						|
								          } catch (err) {
							 | 
						|
								            if (err.status && err.status >= 200 && err.status < 500) {
							 | 
						|
								              // 200 ~ 499 belong to normal response, don't try again
							 | 
						|
								              throw err;
							 | 
						|
								            }
							 | 
						|
								            // < 200 || >= 500 need to retry from other cluser node
							 | 
						|
								            lastError = err;
							 | 
						|
								          }
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      lastError.message += ' (all clients are down)';
							 | 
						|
								      throw lastError;
							 | 
						|
								    };
							 | 
						|
								  });
							 | 
						|
								
							 | 
						|
								  // must cluster node write success
							 | 
						|
								  PUT_METHODS.forEach(method => {
							 | 
						|
								    proto[method] = async function (...args) {
							 | 
						|
								      const res = await Promise.all(this.clients.map(client => client[method](...args)));
							 | 
						|
								      return res[0];
							 | 
						|
								    };
							 | 
						|
								  });
							 | 
						|
								
							 | 
						|
								  proto.signatureUrl = function signatureUrl(/* name */ ...args) {
							 | 
						|
								    const client = this.chooseAvailable();
							 | 
						|
								    return client.signatureUrl(...args);
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto.getObjectUrl = function getObjectUrl(/* name, baseUrl */ ...args) {
							 | 
						|
								    const client = this.chooseAvailable();
							 | 
						|
								    return client.getObjectUrl(...args);
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto._init = function _init() {
							 | 
						|
								    const that = this;
							 | 
						|
								    (async () => {
							 | 
						|
								      await that._checkAvailable(that._ignoreStatusFile);
							 | 
						|
								      that.ready(true);
							 | 
						|
								    })().catch(err => {
							 | 
						|
								      that.emit('error', err);
							 | 
						|
								    });
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto._checkAvailable = async function _checkAvailable(ignoreStatusFile) {
							 | 
						|
								    const name = `._ali-oss/check.status.${currentIP}.txt`;
							 | 
						|
								    if (!ignoreStatusFile) {
							 | 
						|
								      // only start will try to write the file
							 | 
						|
								      await this.put(name, Buffer.from(`check available started at ${Date()}`));
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    if (this._checkAvailableLock) {
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								    this._checkAvailableLock = true;
							 | 
						|
								    const downStatusFiles = [];
							 | 
						|
								    for (let i = 0; i < this.clients.length; i++) {
							 | 
						|
								      const client = this.clients[i];
							 | 
						|
								      // check 3 times
							 | 
						|
								      let available = await this._checkStatus(client, name);
							 | 
						|
								      if (!available) {
							 | 
						|
								        // check again
							 | 
						|
								        available = await this._checkStatus(client, name);
							 | 
						|
								      }
							 | 
						|
								      if (!available) {
							 | 
						|
								        // check again
							 | 
						|
								        /* eslint no-await-in-loop: [0] */
							 | 
						|
								        available = await this._checkStatus(client, name);
							 | 
						|
								        if (!available) {
							 | 
						|
								          downStatusFiles.push(client._objectUrl(name));
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								      this.availables[i] = available;
							 | 
						|
								    }
							 | 
						|
								    this._checkAvailableLock = false;
							 | 
						|
								
							 | 
						|
								    if (downStatusFiles.length > 0) {
							 | 
						|
								      const err = new Error(
							 | 
						|
								        `${downStatusFiles.length} data node down, please check status file: ${downStatusFiles.join(', ')}`
							 | 
						|
								      );
							 | 
						|
								      err.name = 'CheckAvailableError';
							 | 
						|
								      this.emit('error', err);
							 | 
						|
								    }
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto._checkStatus = async function _checkStatus(client, name) {
							 | 
						|
								    let available = true;
							 | 
						|
								    try {
							 | 
						|
								      await client.head(name);
							 | 
						|
								    } catch (err) {
							 | 
						|
								      // 404 will be available too
							 | 
						|
								      if (!err.status || err.status >= 500 || err.status < 200) {
							 | 
						|
								        available = false;
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								    return available;
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto.chooseAvailable = function chooseAvailable() {
							 | 
						|
								    if (this.schedule === MS) {
							 | 
						|
								      // only read from master
							 | 
						|
								      if (this.masterOnly) {
							 | 
						|
								        return this.clients[0];
							 | 
						|
								      }
							 | 
						|
								      for (let i = 0; i < this.clients.length; i++) {
							 | 
						|
								        if (this.availables[i]) {
							 | 
						|
								          return this.clients[i];
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								      // all down, try to use this first one
							 | 
						|
								      return this.clients[0];
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // RR
							 | 
						|
								    let n = this.clients.length;
							 | 
						|
								    while (n > 0) {
							 | 
						|
								      const i = this._nextRRIndex();
							 | 
						|
								      if (this.availables[i]) {
							 | 
						|
								        return this.clients[i];
							 | 
						|
								      }
							 | 
						|
								      n--;
							 | 
						|
								    }
							 | 
						|
								    // all down, try to use this first one
							 | 
						|
								    return this.clients[0];
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto._nextRRIndex = function _nextRRIndex() {
							 | 
						|
								    const index = this.index++;
							 | 
						|
								    if (this.index >= this.clients.length) {
							 | 
						|
								      this.index = 0;
							 | 
						|
								    }
							 | 
						|
								    return index;
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto._error = function error(err) {
							 | 
						|
								    if (err) throw err;
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto._createCallback = function _createCallback(ctx, gen, cb) {
							 | 
						|
								    return () => {
							 | 
						|
								      cb = cb || this._error;
							 | 
						|
								      gen.call(ctx).then(() => {
							 | 
						|
								        cb();
							 | 
						|
								      }, cb);
							 | 
						|
								    };
							 | 
						|
								  };
							 | 
						|
								  proto._deferInterval = function _deferInterval(gen, timeout, cb) {
							 | 
						|
								    return setInterval(this._createCallback(this, gen, cb), timeout);
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  proto.close = function close() {
							 | 
						|
								    clearInterval(this._timerId);
							 | 
						|
								    this._timerId = null;
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  return Client;
							 | 
						|
								};
							 |