class FDSClientConfiguration(object):
  URI_HTTP = 'http://'

  URI_HTTPS = 'https://'

  URI_CDN = 'cdn'

  URI_SUFFIX = 'fds.api.xiaomi.com'

  URI_NET_SUFFIX = 'fds.api.xiaomi.net'

  URI_CDN_SUFFIX = 'fds.api.mi-img.com'

  def __init__(self, region_name='cnbj0',
               enable_cdn_for_download=True,
               enable_cdn_for_upload=False,
               enable_https=True,
               threshold_size=100 * 1024 * 1024,
               part_size=50 * 1024 * 1024,
               timeout=30,
               max_retries=3,
               endpoint=None):
    """
    :param region_name:
    :param enable_cdn_for_download:
    :param enable_cdn_for_upload:
    :param enable_https:
    :param timeout: connection and read timeout (seconds)
    :param max_retries:
    """
    if region_name is None:
      self._region_name = 'cnbj0'
    else:
      self._region_name = region_name
    self._enable_cdn_for_download = enable_cdn_for_download
    self._enable_cdn_for_upload = enable_cdn_for_upload
    self._enable_https = enable_https
    self._enable_md5_calculate = False
    self._timeout = timeout
    self._max_retries = max_retries
    self._debug = False
    if endpoint is None:
      endpoint = self._region_name + '.' + self.URI_SUFFIX

    self.set_endpoint(endpoint)

    self._threshold_size = threshold_size
    self._part_size = part_size

  def get_threshold_size(self):
    return self._threshold_size

  def set_threshold_size(self, threshold_size):
    self._threshold_size = threshold_size

  def get_part_size(self):
    return self._part_size

  def set_part_size(self, part):
    self._part_size = part

  @property
  def debug(self):
    return self._debug

  @debug.setter
  def debug(self, debug):
    self._debug = debug

  @property
  def enable_md5_calculate(self):
    return self._enable_md5_calculate

  @enable_md5_calculate.setter
  def enable_md5_calculate(self, enable):
    self._enable_md5_calculate = enable

  @property
  def timeout(self):
    return self._timeout

  @timeout.setter
  def timeout(self, timeout):
    self._timeout = timeout

  @property
  def max_retries(self):
    return self._max_retries

  @max_retries.setter
  def max_retries(self, max_retries):
    self._max_retries = max_retries

  def set_endpoint(self, endpoint):
    assert endpoint.endswith(self.URI_SUFFIX) or endpoint.endswith(self.URI_NET_SUFFIX)

    self._endpoint = endpoint
    suffix = self._endpoint[-19:]
    i = self._endpoint.index(suffix)
    self._region_name = self._endpoint[0:i]
    self._cdn_endpoint = self.URI_CDN + '.' + self._region_name + '.' + self.URI_CDN_SUFFIX

  def get_download_base_uri(self):
    return self._build_base_uri(self._enable_cdn_for_download)

  def get_upload_base_uri(self):
    return self._build_base_uri(self._enable_cdn_for_upload)

  def get_base_uri(self):
    return self._build_base_uri(False)

  def get_cdn_base_uri(self):
    return self._build_base_uri(True)

  def _build_base_uri(self, enable_cdn):
    base_uri = str()
    if self._enable_https:
      base_uri += self.URI_HTTPS
    else:
      base_uri += self.URI_HTTP

    if enable_cdn:
      base_uri += self._cdn_endpoint
    else:
      base_uri += self._endpoint

    base_uri += '/'
    return base_uri class GalaxyFDSClient(object):
  '''
  Client for Galaxy FDS Service.
  '''

  def __init__(self, access_key=None, access_secret=None, config=None):
    '''
    :param access_key:    The app access key
    :param access_secret: The app access secret
    :param config:        The FDS service's config
    '''
    self._delimiter = "/"

    if access_key == None or access_secret == None:
      self._access_key = self.load_access_key()
      self._secret_key = self.load_secret_key()
    else:
      self._access_key = access_key
      self._secret_key = access_secret

    self._auth = Signer(self._access_key, self._secret_key)
    if config == None:
      config = FDSClientConfiguration()
      config.set_endpoint(self.load_endpoint())

    self._config = config
    self._request = FDSRequest(config.timeout, config.max_retries)

  def load_endpoint(self):
    endpoint = None
    if endpoint is None and "XIAOMI_FDS_ENDPOINT" in os.environ:
      endpoint = os.environ["XIAOMI_FDS_ENDPOINT"]
    if endpoint is None and "FDS_ENDPOINT" in os.environ:
      endpoint = os.environ["FDS_ENDPOINT"]
    if endpoint is None:
      endpoint = self.load_config("xiaomi_fds_endpoint")
    if endpoint is not None and len(endpoint.strip()) == 0:
      logging.warn(
        "endpoint is set to empty, please check ${XIAOMI_FDS_ENDPOINT} or ${FDS_ENDPOINT} in environ variables, or \"xiaomi_fds_endpoint\" in ~/.config/xiaomi/config")
    return endpoint

  def load_access_key(self):
    access_key = None
    if access_key is None and "XIAOMI_ACCESS_KEY_ID" in os.environ:
      access_key = os.environ["XIAOMI_ACCESS_KEY_ID"]
    if access_key is None and "XIAOMI_ACCESS_KEY" in os.environ:
      access_key = os.environ["XIAOMI_ACCESS_KEY"]
    if access_key is None:
      access_key = self.load_config("xiaomi_access_key_id")
    if access_key is not None and len(access_key.strip()) == 0:
      logging.warn(
        "access_key is set to empty, please check ${XIAOMI_ACCESS_KEY_ID} or ${XIAOMI_ACCESS_KEY} in environ variables, or \"xiaomi_access_key_id\" in ~/.config/xiaomi/config")
    return access_key

  def load_secret_key(self):
    secret_key = None
    if secret_key is None and "XIAOMI_SECRET_ACCESS_KEY" in os.environ:
      secret_key = os.environ["XIAOMI_SECRET_ACCESS_KEY"]
    if secret_key is None and "XIAOMI_SECRET_KEY" in os.environ:
      secret_key = os.environ["XIAOMI_SECRET_KEY"]
    if secret_key is None:
      secret_key = self.load_config("xiaomi_secret_access_key")
    if secret_key is not None and len(secret_key.strip()) == 0:
      logging.warn(
        "secret_key is set to empty, please check ${XIAOMI_SECRET_ACCESS_KEY} or ${XIAOMI_SECRET_KEY} in environ variables, or \"xiaomi_secret_access_key\" in ~/.config/xiaomi/config")
    return secret_key

  def load_config(self, config_key):
    try:
      config_filename = os.path.join(os.path.expanduser('~'), ".config/xiaomi/config")
      if os.path.exists(config_filename):
        with open(config_filename) as f:
          data = to_json_object(f.read())
          return data[config_key]
    except:
      pass

  @property
  def delimiter(self):
    return self._delimiter

  @delimiter.setter
  def delimiter(self, delimiter):
    self._delimiter = delimiter

  def does_bucket_exist(self, bucket_name):
    '''
    Check the existence of a specified bucket.
    :param bucket_name: The bucket name of the bucket to check
    :return: True if the bucket exists, otherwise False
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.head(uri, auth=self._auth)
    if response.status_code == requests.codes.ok:
      return True
    elif response.status_code == requests.codes.not_found:
      return False
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Check bucket existence failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def list_buckets(self):
    '''
    List all the buckets of the current developer.
    :return: A list of FDSBucket which contains name and owner of the bucket.
    '''
    uri = self._config.get_base_uri()
    response = self._request.get(uri, auth=self._auth)
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'List buckets failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)
    elif response.content:
      buckets_list = []
      json_response = to_json_object(response.content)
      buckets = json_response['buckets']
      owner = Owner().from_json(json_response['owner'])
      for bucket in buckets:
        buckets_list.append(FDSBucket(bucket['name'], owner))
      return buckets_list
    else:
      return list()

  def list_authorized_buckets(self):
    '''
    List all the authorized buckets of the current developer.
    :return: A list of FDSBucket which contains name and owner of the bucket.
    '''
    uri = self._config.get_base_uri()
    response = self._request.get(uri, auth=self._auth, params={"authorizedBuckets": "true"})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'List buckets failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)
    elif response.content:
      buckets_list = []
      json_response = to_json_object(response.content)
      buckets = json_response['buckets']
      for bucket in buckets:
        buckets_list.append(FDSBucket(bucket['name'], ''))
      return buckets_list
    else:
      return list()

  def create_bucket(self, bucket_name):
    '''
    Create a bucket with the specified name.
    :param bucket_name: The name of the bucket to create
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth)
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Create bucket failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def delete_bucket(self, bucket_name):
    '''
    Delete a bucket of a specified name.
    :param bucket_name: The name of the bucket to delete
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.delete(uri, auth=self._auth)
    if (response.status_code != requests.codes.ok and
        response.status_code != requests.codes.not_found):
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Delete bucket failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def list_objects(self, bucket_name, prefix='', delimiter=None, max_keys=None, is_archive=False):
    '''
    List all objects in a specified bucket with prefix. If the number of objects
    in the bucket is larger than a threshold, you would get a FDSObjectListing
    contains no FDSObjects. In this scenario, you should call
    list_next_batch_of_objects with the returned value
    :param bucket_name: The name of the bucket to whom the object is put
    :param prefix:      The prefix of the object to list
    :param delimiter:   The delimiter used in listing, using '/' if 'None' given
    :param is_archive:  List archives if true
    :return:  FDSObjectListing contains FDSObject list and other metadata
    '''
    if delimiter is None:
      delimiter = self._delimiter
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    params = {
      'prefix': prefix,
      'delimiter': delimiter
    }
    if max_keys is not None:
      params["maxKeys"] = str(max_keys)
    if is_archive:
      params["archive"] = "true"
    response = self._request.get(uri, auth=self._auth, params=params)
    if response.status_code == requests.codes.ok:
      objects_list = FDSObjectListing(to_json_object(response.content))
      return objects_list
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'List objects under bucket %s with prefix %s failed, ' \
                'status=%s, reason=%s%s' % \
                (bucket_name, prefix, response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def list_trash_objects(self, prefix='', delimiter=None, max_keys=None):
    '''
    Compared with list_objects, it returns a list of objects in the trash.
    :param prefix: The prefix of bucket_name/object_name.
    :param delimiter: The delimiter used in listing, using '/' if 'None' given.
    :return: FDSObjectListing contains a list of objects in the trash.
    '''
    return self.list_objects("trash", prefix, delimiter, max_keys=max_keys);

  def list_next_batch_of_objects(self, previous, is_archive=False):
    '''
    List objects in a iterative manner
    :param previous: The FDSObjectListing returned by previous call or list_objects
    :return:  FDSObjectListing contains FDSObject list and other metadata, 'None'
              if all objects returned by previous calls
    '''
    if not previous.is_truncated:
      return None
    bucket_name = previous.bucket_name
    prefix = previous.prefix
    delimiter = previous.delimiter
    marker = previous.next_marker
    uri = "%s%s" % (self._config.get_base_uri(), quote(bucket_name))

    params = {
      'prefix': previous.prefix,
      'delimiter': previous.delimiter,
      'marker': previous.next_marker,
      'maxKeys': previous.max_keys
    }

    if is_archive:
      params['archive'] = 'true'
    response = self._request.get(uri, auth=self._auth, params=params)
    if response.status_code == requests.codes.ok:
      objects_list = FDSObjectListing(to_json_object(response.content))
      return objects_list
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'List next batch of objects under bucket %s with prefix %s ' \
                'and marker %s failed, status=%s, reason=%s%s' % \
                (bucket_name, prefix, marker, response.status_code, response.content,
                 headers)
      raise GalaxyFDSClientException(message)

  def put_object_with_uri(self, uri, data, metadata=None):
    '''
    Put the object with the uri.
    :param uri:         The uri of th bucket and object
    :param data:        The data to put, bytes or a file like object
    :param metadata:    The metadata of the object
    :return: The result of putting action server returns
    '''
    bucket_name, object_name = uri_to_bucket_and_object(uri)
    self.put_object(bucket_name, object_name, data, metadata)

  def put_object(self, bucket_name, object_name, data, metadata=None, is_archive=False):
    '''
    Put the object to a specified bucket. If a object with the same name already
    existed, it will be overwritten.
    :param bucket_name: The name of the bucket to whom the object is put
    :param object_name: The name of the object to put
    :param data:        The data to put, bytes or a file like object
    :param metadata:    The metadata of the object
    :param is_archive:  Put as archived object if true
    :return: The result of putting action server returns
    '''
    part_size = self._config.get_part_size()
    threshold_size = self._config.get_threshold_size()
    inputstream = None

    if isinstance(data, bytes):
      inputstream = BytesIO(data)
    elif isinstance(data, str):
      inputstream = StringIO(data)
    elif not IS_PY3 and isinstance(data, file):
      inputstream = data
    elif (isinstance(data, IOBase)):
      if data.seekable():
        inputstream = data
      else:
        buf = data.readlines()
        if len(buf) > 0:
          if isinstance(buf[0], str):
            inputstream = StringIO(''.join(buf))
          else:
            inputstream = BytesIO(b''.join(buf))
        else:
          inputstream = StringIO('')
    else:
      raise GalaxyFDSClientException("Cannot identify data type")

    pos = inputstream.tell()
    inputstream.seek(0, 2)
    filen = inputstream.tell()
    inputstream.seek(0, pos)
    if filen < threshold_size:
      return self.put_object_directly(bucket_name, object_name, inputstream, metadata, is_archive=is_archive)
    else:
      content = inputstream.read(part_size)
      upload_token = self.init_multipart_upload(bucket_name, object_name, metadata, is_archive=is_archive)
      upload_list = []
      part_number = 1
      while (content):

        upload_result = None
        upload_success_flag = False
        last_exception = None
        for i in range(3):
          try:
            upload_result = self.upload_part(bucket_name, object_name, upload_token.upload_id,
                                             part_number, content)
            upload_list.append(upload_result)
            upload_success_flag = True
            break
          except Exception as e:
            last_exception = e
            logging.warning("upload part %d failed, retry after %d seconds" % (part_number, 3))
            sleep(3)
        if not upload_success_flag:
          logging.error("upload failed, bucket: %s, object: %s, upload part: %d" % (bucket_name, object_name, part_number))
          raise last_exception

        part_number = part_number + 1
        content = inputstream.read(part_size)
      upload_part_result = UploadPartResultList({"uploadPartResultList": upload_list})
      return self.complete_multipart_upload(bucket_name=bucket_name, object_name=object_name,
                                            upload_id=upload_token.upload_id,
                                            metadata=metadata,
                                            upload_part_result_list=json.dumps(upload_part_result))

  def put_object_directly(self, bucket_name, object_name, data, metadata=None, is_archive=False):
    '''
    Put the object to a specified bucket. If a object with the same name already
    existed, it will be overwritten.
    :param bucket_name: The name of the bucket to whom the object is put
    :param object_name: The name of the object to put
    :param data:        The data to put, bytes or a file like object
    :param metadata:    The metadata of the object
    :param is_archive:  Put as archived object if true
    :return: The result of putting action server returns
    '''
    uri = '%s%s/%s' % (self._config.get_upload_base_uri(), quote(bucket_name), quote(object_name))
    if metadata is None:
      metadata = FDSObjectMetadata()
    if is_archive:
      metadata.add_header(Common.STORAGE_CLASS, FDSStorageClass.Archive.value)
    inputstream = None
    if isinstance(data, bytes):
      inputstream = BytesIO(data)
    elif isinstance(data, str):
      inputstream = StringIO(data)
    elif not IS_PY3 and isinstance(data, file):
      inputstream = data
    elif (isinstance(data, IOBase)):
      if data.seekable():
        inputstream = data
      else:
        buf = data.readlines()
        if len(buf) > 0:
          if isinstance(buf[0], str):
            inputstream = StringIO(''.join(buf))
          else:
            inputstream = BytesIO(b''.join(buf))
        else:
          inputstream = StringIO('')
    else:
      raise GalaxyFDSClientException("Cannot identify data type")

    if self._config.enable_md5_calculate:
      digest = hashlib.md5()
      pos = inputstream.tell()
      content = inputstream.read()
      if IS_PY3 and isinstance(content, str):
        content = content.encode(encoding="UTF-8")
      digest.update(content)
      inputstream.seek(0, pos)

      metadata.add_header(Common.CONTENT_MD5, digest.hexdigest())
    response = None
    if IS_PY3 and isinstance(inputstream, StringIO):
      response = self._request.put(uri, data=inputstream.read(), auth=self._auth,
                                   headers=metadata.metadata)
    else:
      response = self._request.put(uri, data=inputstream, auth=self._auth,
                                   headers=metadata.metadata)
    if response.status_code == requests.codes.ok:
      return PutObjectResult(to_json_object(response.content))
    headers = ""
    if self._config.debug:
      headers = ' header=%s' % response.headers
    message = 'Put object failed, status=%s, reason=%s%s' % (
      response.status_code, response.content, headers)
    raise GalaxyFDSClientException(message)

  def post_object(self, bucket_name, data, metadata=None):
    '''
    Post the object to a specified bucket. The object name will be generated
    by the server uniquely.
    :param bucket_name: The name of the bucket to whom the object is put
    :param data:        The data to put, bytes or a file like object
    :param metadata:    The metadata of the object
    :return: The result of posting action server returns
    '''
    uri = '%s%s/' % (self._config.get_upload_base_uri(), quote(bucket_name))
    if metadata is None:
      metadata = FDSObjectMetadata()
    if self._config.enable_md5_calculate:
      digest = hashlib.md5()
      digest.update(data)
      metadata.add_header(Common.CONTENT_MD5, digest.hexdigest())

    response = self._request.post(uri, data=data, auth=self._auth,
                                  headers=metadata.metadata)
    if response.status_code == requests.codes.ok:
      return PutObjectResult(to_json_object(response.content))
    headers = ""
    if self._config.debug:
      headers = ' header=%s' % response.headers
    message = 'Post object failed, status=%s, reason=%s%s' % (
      response.status_code, response.content, headers)
    raise GalaxyFDSClientException(message)

  def get_object_with_uri(self, uri, position=0, size=4096):
    '''
    Get a specified object from fds uri.
    :param uri:         The uri of th bucket and object
    :param position:    The start index of object to get
    :param size:        The maximum size of each piece when return streaming is on
    :return:            The FDS object
    '''
    bucket_name, object_name = uri_to_bucket_and_object(uri)
    return self.get_object(bucket_name, object_name, position, size)

  def get_object(self, bucket_name, object_name, position=0, size=4096, stream=None,
                 version_id=None, is_archive=False):
    '''
    Get a specified object from a bucket.
    :param bucket_name: The name of the bucket from whom to get the object
    :param object_name: The name of the object to get
    :param position: The start index of object to get
    :param size:        The maximum size of each piece when return streaming is on
    :param stream:      Set True to enable streaming, otherwise, whole object content is read to memory
    :param is_archive:  Get archive object if true
    :return: The FDS object
    '''
    if position < 0:
      raise GalaxyFDSClientException("Seek position should be no less than 0")
    uri = '%s%s/%s' % (self._config.get_download_base_uri(), quote(bucket_name), quote(object_name))
    req_params = dict()
    if version_id:
      req_params["versionId"] = version_id

    if is_archive:
      req_params["archive"] = "true"

    if position > 0:
      header = {Common.RANGE: 'bytes=%d-' % position}
      response = self._request.get(uri, auth=self._auth, headers=header, stream=stream,
                                   params=req_params)
    else:
      response = self._request.get(uri, auth=self._auth, stream=stream, params=req_params)
    if response.status_code == requests.codes.ok or \
        response.status_code == requests.codes.partial:
      obj = FDSObject()
      obj.stream = response.iter_content(chunk_size=size)
      summary = FDSObjectSummary()
      summary.bucket_name = bucket_name
      summary.object_name = object_name
      summary.size = int(response.headers['content-length'])
      obj.summary = summary
      obj.metadata = self._parse_object_metadata_from_headers(response.headers)
      return obj
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get object failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def download_object_with_uri(self, uri, data_file, offset=0, length=-1):
    bucket_name, object_name = uri_to_bucket_and_object(uri)
    self.download_object(bucket_name, object_name, data_file, offset, length)

  def download_object(self, bucket_name, object_name, data_file, offset=0, length=-1):
    fds_object = self.get_object(bucket_name=bucket_name,
                                 object_name=object_name,
                                 position=offset)
    length_left = length
    if length_left == -1:
      length_left = IS_PY3 and sys.maxsize or sys.maxint
    try:
      if data_file:
        with open(data_file, "wb") as f:
          for chunk in fds_object.stream:
            l = min(length_left, len(chunk));
            f.write(chunk[0:l])
            length_left -= l
            if length_left <= 0:
              break
      else:
        for chunk in fds_object.stream:
          l = min(length_left, len(chunk))
          if IS_PY3:
            sys.stdout.buffer.write(chunk[0:l])
          else:
            sys.stdout.write(chunk[0:l])
          length_left -= l
          if length_left <= 0:
            break
        sys.stdout.flush()
    finally:
      fds_object.stream.close()

  def does_object_exists(self, bucket_name, object_name):
    '''
    Check the existence of a specified object.
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object to check
    :return: True if the object exists, otherwise, False
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.head(uri, auth=self._auth)
    if response.status_code == requests.codes.ok:
      return True
    elif response.status_code == requests.codes.not_found:
      return False
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Check object existence failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def delete_object(self, bucket_name, object_name, **kwargs):
    '''
    Delete specified object.
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    params = {}
    if "is_archive" in kwargs and kwargs["is_archive"] is True:
      params["archive"] = "true"
    elif "enable_trash" in kwargs and kwargs["enable_trash"] is False:
      params["enableTrash"] = "false"

    response = self._request.delete(uri, auth=self._auth, params=params)
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Delete object failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

    if "object-version-id" in response.headers:
      return response.headers["object-version-id"]
    return None

  def delete_objects(self, bucket_name, object_names):
    '''
    Delete specified objects in the bucket
    :param bucket_name:
    :param object_names:
    :return:
    '''
    uri = "%s%s" % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, data=json.dumps(object_names), params={"deleteObjects": "true"})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Delete objects failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)
    else:
      failed_list = to_json_object(response.content)
      return failed_list

  def restore_object(self, bucket_name, object_name):
    '''
    Restore a specified object from trash.
    :param bucket_name:     The name of the bucket
    :param object_name: The name of the object
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, params={"restore": 'true'})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Restore object failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def rename_object(self, bucket_name, src_object_name, dst_object_name):
    '''
    Rename a specified object to a new name.
    :param bucket_name:     The name of the bucket
    :param src_object_name: The original name of the object
    :param dst_object_name: The target name of the object to rename to
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(src_object_name))
    response = self._request.put(uri, auth=self._auth, params={"renameTo": dst_object_name})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Rename object failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def put_domain_mapping(self, bucket_name, domain_name, index_name='index.html'):
    '''
    Put bucket domain mapping
    :param bucket_name: The name of the bucket
    :param domain_name: The name of domain to put
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, params={'domain': domain_name, 'index': index_name})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Put bucket domain mapping failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def delete_domain_mapping(self, bucket_name, domain_name):
    '''
    Delete bucket domain mapping
    :param bucket_name: The name of bucket
    :param domain_name: The name of domain to delete
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.delete(uri, auth=self._auth, params={'domain': domain_name})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      messgae = 'Delete bucket domain mapping failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(messgae)

  def list_domain_mappins(self, bucket_name):
    '''
    List bucket domain mappings
    :param bucket_name: The name of bucket
    :return:            The list domain mappings
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth, params={'domain': 'true'})
    if response.status_code == requests.codes.ok:
      list_domain_mappings_result = ListDomainMappingsResult(to_json_object(response.content))
      return list_domain_mappings_result.domain_mappings
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'List bucket domain mappings failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def crop_image(self, bucket_name, object_name, x, y, w, h):
    '''
    Crop image
    :param bucket_name:The name of bucket
    :param object_name: The name of object
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, params={'cropImage': 'true', 'x': x, 'y': y, 'w': w, 'h': h})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'crop image failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def get_access_log_config(self, bucket_name):
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth, params={'accessLog': 'true'})
    if response.status_code == requests.codes.ok:
      access_log_config = AccessLogConfig(to_json_object(response.content))
      return access_log_config
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'get access log config failed, status=%s, reason=%s%s' % (
      response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def update_access_log_config(self, bucket_name, access_log_config):
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth,
                                 data=json.dumps(access_log_config, default=lambda x: x.to_string()),
                                 params={'accessLog': 'true'})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'crop image failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def set_bucket_acl(self, bucket_name, acl):
    '''
    Add grant(ACL) for specified bucket.
    :param bucket_name: The name of the bucket to add grant
    :param acl:         The grant(ACL) to add
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    acp = self._acl_to_acp(acl)
    response = self._request.put(uri, auth=self._auth,
                                 params={SubResource.ACL: 'true'},
                                 data=json.dumps(acp, default=lambda x: x.to_string()))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Set bucket acl failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def get_bucket_acl(self, bucket_name):
    '''
    Get the ACL of a specified bucket.
    :param bucket_name: The name of the bucket to get ACL
    :return: The got access control list
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth, params={SubResource.ACL: 'true'})
    if response.status_code == requests.codes.ok:
      acp = AccessControlPolicy(to_json_object(response.content))
      acl = self._acp_to_acl(acp)
      return acl
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get bucket acl failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def delete_bucket_acl(self, bucket_name, acl):
    '''
    Delete grant(ACL) for specified bucket.
    :param bucket_name: The name of the bucket to delete grant
    :param acl:         The grant(ACL) to delete
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    acp = self._acl_to_acp(acl)
    response = self._request.put(uri, auth=self._auth, data=json.dumps(acp, default=lambda x: x.to_string()), params={
      "acl": "true",
      "action": "delete"})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Delete bucket acl failed,status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def delete_object_acl(self, bucket_name, object_name, acl, is_archive=False):
    '''
    Delete grant(ACL)for a specified object.
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :param acl:         The grant(ACL) to delete
    :param is_archive:  If true, put ACL of archive object
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    acp = self._acl_to_acp(acl)
    params = {'acl': 'true', 'action': "delete"}
    if is_archive:
      params["archive"] = "true"
    response = self._request.put(uri, auth=self._auth, params=params,
                                 data=json.dumps(acp, default=lambda x: x.to_string()))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = 'header=%s' % response.headers
      message = 'Delete object acl failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def set_object_acl(self, bucket_name, object_name, acl, is_archive=False):
    '''
    Add grant(ACL) for a specified object.
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :param acl:         The grant(ACL) to add
    :param is_archive:  If true, put ACL of archive object
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    acp = self._acl_to_acp(acl)
    params = {SubResource.ACL: 'true'}
    if is_archive:
      params['archive'] = "true"
    response = self._request.put(uri, auth=self._auth, params=params,
                                 data=json.dumps(acp, default=lambda x: x.to_string()))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Set object acl failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def get_object_acl(self, bucket_name, object_name, is_archive=False):
    '''
    Get the ACL of a specified object.
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :param is_archive:  If true, put ACL of archive object
    :return: The got access control list
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))

    params = {SubResource.ACL: 'true'}
    if is_archive:
      params["archive"] = "true"
    response = self._request.get(uri, auth=self._auth, params=params)
    if response.status_code == requests.codes.ok:
      acp = AccessControlPolicy(to_json_object(response.content))
      acl = self._acp_to_acl(acp)
      return acl
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get object acl failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def get_object_metadata(self, bucket_name, object_name):
    '''
    Get the metadata of a specified object.
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :return: The got object metadata
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.get(uri, auth=self._auth, params={SubResource.METADATA: 'true'})
    if response.status_code == requests.codes.ok:
      metadata = self._parse_object_metadata_from_headers(response.headers)
      return metadata
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get object metadata failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def prefetch_object(self, bucket_name, object_name):
    '''
    Prefetch the object to CDN
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :return: void
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, data="", params={"prefetch": 'true'})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Prefetch object failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def refresh_object(self, bucket_name, object_name):
    '''
    Refresh the cache of the object in CDN
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :return: void
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, data="", params={"refresh": 'true'})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Refresh object failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def set_public(self, bucket_name, object_name=None, is_archive=False):
    """
    Make resource public, if object_name is not being set, this method will set the bucket_name to public resource,
    if object_name is set, this method will set object_name to public resource
    :param bucket_name:
    :param object_name:
    :return:
    """
    if object_name is None:
      self._set_bucket_public(bucket_name)
    else:
      self._set_object_public(bucket_name, object_name, is_archive=is_archive)

  def _set_object_public(self, bucket_name, object_name, is_archive=False):
    acl = AccessControlList()
    grant = Grant(Grantee(UserGroups.ALL_USERS), Permission.READ)
    grant.type = GrantType.GROUP
    acl.add_grant(grant)
    self.set_object_acl(bucket_name, object_name, acl, is_archive=is_archive)

  def _set_bucket_public(self, bucket_name):
    acl = AccessControlList()
    grant = Grant(Grantee(UserGroups.ALL_USERS), Permission.READ_OBJECTS)
    grant.type = GrantType.GROUP
    acl.add_grant(grant)
    self.set_bucket_acl(bucket_name, acl)

  def set_private(self, bucket_name, object_name=None, is_archive=False):
    if object_name is None:
      self._set_bucket_private(bucket_name)
    else:
      self._set_object_private(bucket_name, object_name, is_archive)

  def _set_bucket_private(self, bucket_name):
    acl = AccessControlList()
    grant = Grant(Grantee(UserGroups.ALL_USERS), Permission.READ_OBJECTS)
    grant.type = GrantType.GROUP
    acl.add_grant(grant)
    self.delete_bucket_acl(bucket_name, acl)

  def _set_object_private(self, bucket_name, object_name, is_archive=False):
    acl = AccessControlList()
    grant = Grant(Grantee(UserGroups.ALL_USERS), Permission.READ)
    grant.type = GrantType.GROUP
    acl.add_grant(grant)
    self.delete_object_acl(bucket_name, object_name, acl, is_archive)

  def init_multipart_upload(self, bucket_name, object_name, metadata=None, is_archive=False):
    '''
    Init a multipart upload session
    :param bucket_name:
    :param object_name:
    :return:
    '''
    if not metadata:
      metadata = FDSObjectMetadata()
    if is_archive:
      metadata.add_header(Common.STORAGE_CLASS, FDSStorageClass.Archive.value)
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, data="", params={"uploads": 'true'}, headers=metadata.metadata)
    if response.status_code == requests.codes.ok:
      result = InitMultipartUploadResult(to_json_object(response.content))
      return result
    else:
      headers = ""
      if self._config.debug:
        headers = ' headers=%s' % response.headers
      message = 'Init multipart upload failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def upload_part(self, bucket_name, object_name, upload_id, part_number, data):
    '''
    Upload a multipart upload part
    :param bucket_name:
    :param object_name:
    :param upload_id:
    :param part_number:
    :param data:
    :return:
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, data=data, params={
      "uploadId": upload_id,
      "partNumber": str(part_number)
    })
    if response.status_code == requests.codes.ok:
      result = UploadPartResult(to_json_object(response.content))
      return result
    else:
      headers = ""
      if self._config.debug:
        headers = ' headers=%s' % response.headers
      message = 'Upload part failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def complete_multipart_upload(self, bucket_name, object_name, upload_id,
                                metadata, upload_part_result_list):
    '''
    Complete a multipart upload
    :param bucket_name:
    :param object_name:
    :param upload_id:
    :param metadata:
    :param upload_part_result_list:
    :return:
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    if metadata is None:
      metadata = FDSObjectMetadata()
    response = self._request.put(uri, auth=self._auth,
                                 data=upload_part_result_list, headers=metadata.metadata,
                                 params={"uploadId": upload_id})
    if response.status_code == requests.codes.ok:
      result = PutObjectResult(to_json_object(response.content))
      return result
    else:
      headers = ""
      if self._config.debug:
        headers = ' headers=%s' % response.headers
      message = 'Complete multipart upload failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def abort_multipart_upload(self, bucket_name, object_name, upload_id):
    '''
    Abort a multipart upload
    :param bucket_name:
    :param object_name:
    :param upload_id:
    :return:
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.delete(uri, auth=self._auth, data='', params={
      "uploadId": upload_id
    })
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' headers=%s' % response.headers
      message = 'Abort multipart upload failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def generate_presigned_uri(self, base_uri, bucket_name, object_name,
                             expiration, http_method="GET", content_type=None, sub_resources=None):
    '''
    Generate a pre-signed uri to share object with the public
    :param base_uri: The base uri of rest server. Use client's default if 'None' pass
    :param bucket_name: The name of the bucket
    :param object_name: The name of the object
    :param expiration: The expiration time of the uri: milliseconds from the Epoch
    :param http_method: The http method used in uri
    :return: The pre-signed uri string
    '''
    if not base_uri or base_uri == '':
      if http_method == 'PUT' or http_method == 'POST':
        base_uri = self._config.get_upload_base_uri()
      elif http_method == 'DELETE':
        base_uri = self._config.get_base_uri()
      else:
        base_uri = self._config.get_download_base_uri()
    if not IS_PY3 and isinstance(base_uri, unicode):
      base_uri = base_uri.encode('utf8')
    try:
      if sub_resources == None:
        uri = '%s%s/%s?%s=%s&%s=%s&' % \
              (base_uri, bucket_name, object_name, \
               Common.GALAXY_ACCESS_KEY_ID, self._auth._app_key, \
               Common.EXPIRES, str(int(expiration)))
      else:
        uri = '%s%s/%s?%s&%s=%s&%s=%s&' % \
              (base_uri, bucket_name, object_name, '&'.join(sub_resources), \
               Common.GALAXY_ACCESS_KEY_ID, self._auth._app_key, \
               Common.EXPIRES, str(int(expiration)))
      headers = None
      if content_type != None and isinstance(content_type, IS_PY3 and str or basestring):
        headers = {Common.CONTENT_TYPE: content_type}
      signature = str(self._auth._sign_to_base64(http_method, headers, uri, \
                                                 self._auth._app_secret))
      if sub_resources == None:
        return '%s%s/%s?%s=%s&%s=%s&%s=%s' % \
               (base_uri, quote(bucket_name), quote(object_name), \
                Common.GALAXY_ACCESS_KEY_ID, self._auth._app_key, \
                Common.EXPIRES, str(int(expiration)), Common.SIGNATURE, signature)
      else:
        return '%s%s/%s?%s&%s=%s&%s=%s&%s=%s' % \
               (base_uri, quote(bucket_name), quote(object_name), '&'.join(sub_resources), \
                Common.GALAXY_ACCESS_KEY_ID, self._auth._app_key, \
                Common.EXPIRES, str(int(expiration)), Common.SIGNATURE, signature)

    except Exception as e:
      import traceback
      traceback.print_exc()
      message = 'Wrong expiration given. ' \
                'Milliseconds since January 1, 1970 should be used. ' + str(e)
      raise GalaxyFDSClientException(message)

  def generate_download_object_uri(self, bucket_name, object_name):
    '''
    Generate a URI for downloading object
    '''
    return '%s%s/%s' % (self._config.get_download_base_uri(), bucket_name,
                        object_name)

  def copy_object(self, src_bucket_name, src_object_name, dst_bucket_name, dst_object_name):
    '''
    Copy src_object_name from src_bucket_name to dst_bucket_name, and rename it to dst_object_name
    :param src_bucket_name: Source bucket name
    :param src_object_name: Source object name
    :param dst_bucket_name: Target bucket name
    :param dst_object_name: Target object name
    :return:
    '''
    uri = '%s%s/%s' % (self._config.get_upload_base_uri(), quote(dst_bucket_name),\
      quote(dst_object_name))

    data = {"srcBucketName": src_bucket_name, "srcObjectName": src_object_name}

    response = self._request.put(uri, data=json.dumps(data, default=lambda x: x.to_string()),
                                 auth=self._auth, params={"cp": "cpparam"})
    if response.status_code == requests.codes.ok:
      return CopyObjectResult(to_json_object(response.content))
    headers = ""
    if self._config.debug:
      headers = ' header=%s' % response.headers
    message = 'Copy object failed, status=%s, reason=%s%s' % (
      response.status_code, response.content, headers)
    raise GalaxyFDSClientException(message)

  def _acp_to_acl(self, acp):
    '''
    Translate AccessControlPolicy to AccessControlList.
    '''
    if acp is not None:
      acl = AccessControlList()
      for item in acp['accessControlList']:
        grantee = item['grantee']
        grant_id = grantee['id']
        permission = item['permission']
        g = Grant(Grantee(grant_id), permission)
        acl.add_grant(g)
      return acl
    return str()

  # def copy_object(self, srcBucketName, srcObjectName, dstBucketName, dstObjectName):
  #   cp_request = FDSCopyObjectRequest(srcBucketName, srcObjectName, dstBucketName, dstObjectName)
  #   return self.copy_object(cp_request)

  def _acl_to_acp(self, acl):
    '''
    Translate AccessControlList to AccessControlPolicy.
    '''
    if acl is not None:
      acp = AccessControlPolicy(dict())
      owner = Owner()
      owner.id = self._access_key
      acp.owner = owner
      acp.access_control_list = acl.get_grant_list()
      return acp
    return ''

  def _parse_object_metadata_from_headers(self, response_headers):
    '''
    Parse object metadata from the response headers.
    '''
    metadata = FDSObjectMetadata()
    header_keys = [c.lower() for c in response_headers.keys()]
    for key in FDSObjectMetadata.PRE_DEFINED_METADATA:
      if key.lower() in header_keys:
        metadata.add_header(key, response_headers[key])
    for key in response_headers:
      if key.lower().startswith(FDSObjectMetadata.USER_DEFINED_METADATA_PREFIX):
        metadata.add_user_metadata(key, response_headers[key])
    return metadata

  def list_all_objects(self, bucket_name, prefix='', delimiter=None, is_archive=False):
    '''
    traverse all objects in the bucket
    :param bucket_name:
    :param prefix:
    :param delimiter:
    :return:
    '''
    result = self.list_objects(bucket_name, prefix, delimiter, is_archive=is_archive)
    while True:
      for object_summary in result.objects:
        yield object_summary
      if result.is_truncated:
        result = self.list_next_batch_of_objects(result, is_archive=is_archive)
      else:
        break

  def _update_bucket_versioning_(self, bucket_name, versioning):
    '''
    Update bucket versioning
    :param bucket_name:
    :param versioning:
    :return:
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, params={"versioning": str(versioning)})
    if response.status_code == requests.codes.ok:
      return to_json_object(response.content)
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Update bucket versioning failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _get_bucket_versioning_(self, bucket_name):
    '''
    Get bucket versioning
    :param bucket_name:
    :param versioning:
    :return:
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth, params={"versioning": "true"})
    if response.status_code == requests.codes.ok:
      content = response.content
      if isinstance(content, bytes):
        content = content.decode(encoding='utf-8')
      return int(content)
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get bucket versioning failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _list_version_ids_(self, bucket_name, object_name):
    '''
    List all version ids of specified object order by timestamp desc
    :param bucket_name:
    :param object_name:
    :return: a list of version ids of specified object
    '''
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.get(uri, auth=self._auth, params={"versionIds": "true"})
    if response.status_code == requests.codes.ok:
      return to_json_object(response.content)
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get object version ids failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def update_lifecycle_config(self, bucket_name, lifecycle_config):
    '''
    Update lifecycle config of a bucket which determine by the config
    :param lifecycle_config:
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri,
                                 auth=self._auth,
                                 params={"lifecycle": "true"},
                                 data=json.dumps(lifecycle_config))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Update bucket lifecycle config failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def update_lifecycle_rule(self, bucket_name, rule):
    '''
    Update a TTL rule of specified bucket
    :param rule: If rule.id is None or not exists in bucket.lifecycle_config, add rule with a generated ID. Otherwise bucket.lifecycle_config[rule.id] will be updated
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri,
                                 auth=self._auth,
                                 params={"lifecycle": "rule"},
                                 data=json.dumps(rule))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Update bucket lifecycle rule failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def get_lifecycle_config(self, bucket_name, rule_id=None):
    '''
    Get lifecycle config of specified bucket
    :param bucket_name:
    :param rule_id: if rule_id is None, return lifecycle_config of the bucket; otherwise return bucket.lifecycle_config[rule_id], if the rule exists
    :return:
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth,
                                 params={"lifecycle": rule_id and rule_id or ""})
    if response.status_code == requests.codes.ok:
      js = to_json_object(response.content)
      if rule_id:
        return FDSLifecycleRule(js)
      else:
        return FDSLifecycleConfig(js)
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get bucket lifecycle config failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def update_cors_config(self, bucket_name, cors_config):
    '''
    Update cors config of a bucket which determine by the config
    :param bucket_name:
    :param cors_config:
    :return:
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, params={"cors": "true"},
                                 data=json.dumps(cors_config))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Update bucket cors config failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def update_cors_rule(self, bucket_name, rule):
    '''
    Update a cors rule of specified bucket
    :param rule: If rule.id is None or not exists in bucket.cors_config, add rule with a generated ID. Otherwise bucket.cors_config[rule.id] will be updated
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri,
                                 auth=self._auth,
                                 params={"cors": "rule"},
                                 data=json.dumps(rule))
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Update bucket cors rule failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def get_cors_config(self, bucket_name, rule_id=None):
    '''
    Get cors config of specified bucket
    :param bucket_name:
    :param rule_id: if rule_id is None, return cors_config of the bucket; otherwise return bucket.cors_config[rule_id], if the rule exists
    :return:
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth,
                                 params={"cors": rule_id and rule_id or ""})
    if response.status_code == requests.codes.ok:
      print(response.content)
      js = to_json_object(response.content)
      if rule_id:
        return FDSCORSRule(js)
      else:
        return FDSCORSConfig(js)
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get bucket cors config failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _set_bucket_default_webp_quality_(self, bucket_name, quality):
    '''
    Enable bucket auto convert webp.
    :param bucket_name:     The name of the bucket
    :param quality:         Default webp quality, -1 means disable auto convert webp
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, params={
      "webpQuality": str(quality)
    })
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Enable bucket auto convert webp failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _get_bucket_default_webp_quality_(self, bucket_name):
    '''
    Check the existence of a specified object.
    :param bucket_name: The name of the bucket
    :return: True if the object exists, otherwise, False
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth, params={
      "webpQuality": "true"
    })
    if response.status_code == requests.codes.ok:
      content = response.content
      if IS_PY3:
        content = content.decode("UTF-8")
      return int(content)
    elif response.status_code == requests.codes.not_found:
      return False
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get bucket default webp quality failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _get_webp_(self, bucket_name, object_name, stream=None, quality=None):
    '''
    Get a specified object from a bucket.
    :param bucket_name: The name of the bucket from whom to get the object
    :param object_name: The name of the object to get
    :param stream:      Set True to enable streaming, otherwise, whole object content is read to memory
    :return: The FDS object
    '''
    uri = '%s%s/%s' % (self._config.get_download_base_uri(), quote(bucket_name), quote(object_name))
    params = {
      "f": "webp"
    }
    if quality:
      params["q"] = quality
    response = self._request.get(uri, auth=self._auth, stream=stream, params=params)

    if response.status_code == requests.codes.ok or \
        response.status_code == requests.codes.partial:
      obj = FDSObject()
      obj.stream = response.iter_content()
      summary = FDSObjectSummary()
      summary.bucket_name = bucket_name
      summary.object_name = object_name
      summary.size = int(response.headers['content-length'])
      obj.summary = summary
      obj.metadata = self._parse_object_metadata_from_headers(response.headers)
      return obj
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get webp failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _set_bucket_default_gif_extract_type_(self, bucket_name, mime_type):
    '''
    Enable bucket auto gif extract.
    :param bucket_name:     The name of the bucket
    :param mime_type:         Default gif extract type, unknown means disable auto gif extract
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, params={"gifType": mime_type})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Enable bucket auto gif extract failed,status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _get_extracted_gif_(self, bucket_name, object_name, type, stream=None):
    '''
    Get gif extracted frame of a specified
    :param bucket_name: The name of the bucket from whom to get the object
    :param object_name: The name of the object to get
    :param stream:      Set True to enable streaming, otherwise, whole object content is read to memory
    :param type: The FDS object
    :return:
    '''
    uri = '%s%s/%s' % (self._config.get_download_base_uri(), quote(bucket_name), quote(object_name))
    params = {"gf": type}
    response = self._request.get(uri, auth=self._auth, stream=stream, params=params)

    if response.status_code == requests.codes.ok or response.status_code == requests.codes.partial:
      obj = FDSObject()
      obj.stream = response.iter_content()
      summary = FDSObjectSummary()
      summary.bucket_name = bucket_name
      summary.object_name = object_name
      summary.size = int(response.headers['content-length'])
      obj.summary = summary
      obj.metadata = self._parse_object_metadata_from_headers(response.headers)
      return obj
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get extracted gif failed, status=%s, reason=%s%s' % (
      response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def _get_bucket_default_gif_extract_type_(self, bucket_name):
    '''
    Check the existence of a specified bucket.
    :param bucket_name: The name of the bucket
    :return: Type if the bucket can auto gif extract, otherwise, unknown
    '''
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.get(uri, auth=self._auth, params={"gifType": "true"})
    if response.status_code == requests.codes.ok:
      content = response.content
      if IS_PY3:
        content = content.decode("UTF-8")
      return content
    elif response.status_code == requests.codes.not_found:
      return False
    else:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Get bucket default gif extract type failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def generate_anti_stealing_uri(self, bucket_name, object_name, expiration, key):
    '''
    Generate anti stealing URI
    :param bucket_name: The name of the bucket from whom to get the object
    :param object_name: The name of the object to get
    :param expiration : The timestamp of milliseconds from 1970-01-01-00:00:00
    :param key        : The key you set up for the bucket
    :return: The anti stealing URI
    '''
    return self._generate_anti_stealing_uri(self._config.get_base_uri(), bucket_name, object_name,
                                            expiration, key)

  def generate_anti_stealing_cdn_uri(self, bucket_name, object_name, expiration, key):
    '''
    Generate anti stealing CDN URI
    :param bucket_name: The name of the bucket from whom to get the object
    :param object_name: The name of the object to get
    :param expiration : The timestamp of milliseconds from 1970-01-01-00:00:00
    :param key        : The key you set up for the bucket
    :return: The anti stealing CDN URI
    '''
    return self._generate_anti_stealing_uri(self._config.get_cdn_base_uri(), bucket_name,
                                            object_name, expiration,
                                            key)

  def _generate_anti_stealing_uri(self, base_uri, bucket_name, object_name, expiration, key):
    timestamp = int(expiration) / 1000
    t = hex(timestamp).lstrip('0x')
    string_to_cal = key + '/' + bucket_name + '/' + object_name + t
    m = hashlib.md5()
    m.update(string_to_cal)
    sign = m.hexdigest()

    return "%s%s/%s?sign=%s&t=%s" % (base_uri, bucket_name, object_name, sign, t)

  def update_timestamp_anti_stealing_link_config(self, bucket_name, config):
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    resp = self._request.put(uri, auth=self._auth, params={'antiStealingLink': 'true'},
                             data=json.dumps(config, default=lambda x: x.to_string()))

    if resp.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % resp.headers
      message = 'Update timestamp anti stealing link config failed, status=%s, reason=%s%s' % (
        resp.status_code, resp.content, headers)
      raise GalaxyFDSClientException(message)

  def get_timestamp_anti_stealing_link_config(self, bucket_name):
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    resp = self._request.get(uri, params={'antiStealingLink': 'true'}, auth=self._auth)

    if resp.status_code == requests.codes.ok:
      config = TimestampAntiStealingLinkConfig(to_json_object(resp.content))
      return config

    headers = ""
    if self._config.debug:
      headers = ' header=%s' % resp.headers
    message = 'Get timestamp anti stealing link config failed, status=%s, reason=%s%s' % (
      resp.status_code, resp.content, headers)
    raise GalaxyFDSClientException(message)

  def delete_timestamp_anti_stealing_link_config(self, bucket_name):
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    resp = self._request.delete(uri, params={'antiStealingLink': 'true'}, auth=self._auth)

    if resp.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % resp.headers
      message = 'Delete timestamp anti stealing link config failed, status=%s, reason=%s%s' % (
        resp.status_code, resp.content, headers)
      raise GalaxyFDSClientException(message)

  def _restore_archived_object_(self, src_bucket_name, src_object_name):
      '''
      Restore archived object
      If object is restored, extend expiration time of the copy
      :param bucket_name:     The name of the bucket
      :param object_name:     The name of the object
      '''
      uri = '%s%s/%s' % (self._config.get_base_uri(), quote(src_bucket_name), quote(src_object_name))
      response = self._request.put(uri, auth=self._auth,
        params={"restoreArchive": "true"})
      if response.status_code != requests.codes.ok:
          headers = ""
          if self._config.debug:
              headers = ' header=%s' % response.headers
          message = 'Restore archived object failed, status=%s, reason=%s%s' % (
              response.status_code, response.content, headers)
          raise GalaxyFDSClientException(message)

  def set_object_outside_access(self, bucket_name, object_name, value):
    if not isinstance(value, bool):
      raise GalaxyFDSClientException("value should be bool")
    uri = '%s%s/%s' % (self._config.get_base_uri(), quote(bucket_name), quote(object_name))
    response = self._request.put(uri, auth=self._auth, params={'setOutsideAccess': 'true' if value else 'false'})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Set object outsideAccess failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message)

  def set_bucket_outside_access(self, bucket_name, value):
    if not isinstance(value, bool):
      raise GalaxyFDSClientException("value should be bool")
    uri = '%s%s' % (self._config.get_base_uri(), quote(bucket_name))
    response = self._request.put(uri, auth=self._auth, params={'setOutsideAccess': 'true' if value else 'false'})
    if response.status_code != requests.codes.ok:
      headers = ""
      if self._config.debug:
        headers = ' header=%s' % response.headers
      message = 'Set bucket outsideAccess failed, status=%s, reason=%s%s' % (
        response.status_code, response.content, headers)
      raise GalaxyFDSClientException(message) class GalaxyFDSClientException(Error):
  def __init__(self, message):
    self.message = message

  def __str__(self):
    if self.message:
      return super.__str__(self) + ": " + str(self.message)
    return super.__str__(self) class FDSLifecycleConfig(dict):
  '''
  lifecycle config like this:

  {
   "rules": [
     {
       "id": 0,
       "enabled": true,
       "prefix": "log",
       "actions": {
         "nonCurrentVersionExpiration": {
           "days": 7
         },
         "expiration": {
           "days":30
         }
       }
     },
     {
       "enabled": true,
       "prefix": "images",
       "actions": {
         "nonCurrentVersionExpiration": {
           "days": 7
         },
         "expiration": {
           "days":30
         },
         "abortIncompleteMultipartUpload": {
           "days":7
         }
       }
     }
   ]
 }
  '''

  def __init__(self, json={}):
    dict.__init__(self, json)
    self._rules = []
    for rule in self.get('rules', []):
      self._rules.append(FDSLifecycleRule(rule))
    self['rules'] = self._rules

  @property
  def rules(self):
    return self._rules

  def get_rule_by_prefix(self, prefix):
    for rule in self.rules:
      if rule.prefix == prefix:
        return rule
    return None

  def get_rule_by_object_name(self, object_name, enabled_rule_only=False):
    '''
    Get rule by object name
    :param object_name:
    :param enabled_rule_only:
    :return: Only one rule will return if more than one rules matched.
    '''
    for rule in self.rules:
      if object_name.startswith(rule.prefix) and (rule.enabled or not enabled_rule_only):
        return rule
    return None class FDSLifecycleRule(dict):
  def __init__(self, json={}):
    dict.__init__(self, json)
    self._actions = {}
    for name, action in self.get('actions', {}).items():
      if name == 'abortIncompleteMultipartUpload':
        self._actions[name] = FDSAbortIncompleteMultipartUpload(action)
      elif name == 'expiration':
        self._actions[name] = FDSExpiration(action)
      elif name == 'nonCurrentVersionExpiration':
        self._actions[name] = FDSNonCurrentVersionExpiration(action)
      elif name == 'lifeCycleStorageClass':
        self._actions[name] = FDSLifecycleStorageClass(action)
      else:
        raise GalaxyFDSClientException("invalid action type: " + name)
    self['actions'] = self._actions

  @property
  def id(self):
    return self.get('id', None)

  @property
  def enabled(self):
    return self.get('enabled', False)

  @enabled.setter
  def enabled(self, enabled):
    self['enabled'] = enabled

  @property
  def prefix(self):
    return self.get('prefix', None)

  @prefix.setter
  def prefix(self, prefix):
    self['prefix'] = prefix

  @property
  def actions(self):
    return self._actions

  def update_action(self, action):
    self.actions[action.name] = action class FDSCORSConfig(dict):
  '''
  cors config like this:

  {
   "rules": [
     {
       "id": "0",
       "allowOrigin":"*.example.com"
     },
     {
       "id": "1",
       "allowOrigin":"*"
     }
   ]
  }
  '''

  def __init__(self, json={}):
    dict.__init__(self, json)
    self._rules = []
    for rule in self.get('rules', []):
      self._rules.append(FDSCORSRule(rule))
    self['rules'] = self._rules

  @property
  def rules(self):
    return self._rules

  def get_rule_by_id(self, id):
    for rule in self.rules:
      if rule.id == id:
        return rule
    return None class FDSCORSRule(dict):
  def __init__(self, json = {}):
    dict.__init__(self, json)

  @property
  def id(self):
    return self.get('id', None)

  @id.setter
  def id(self, id):
    self['id'] = id

  @property
  def allowOrigin(self):
    return self.get('allowOrigin', None)

  @allowOrigin.setter
  def allowOrigin(self, allowOrigin):
    self['allowOrigin'] = allowOrigin class FDSObjectMetadata(object):
  """
  The FDS object metadata class.
  """
  USER_DEFINED_METADATA_PREFIX = "x-xiaomi-meta-"

  PRE_DEFINED_METADATA = [
    Common.CACHE_CONTROL,
    Common.CONTENT_ENCODING,
    Common.CONTENT_LENGTH,
    Common.CONTENT_RANGE,
    Common.LAST_MODIFIED,
    Common.CONTENT_MD5,
    Common.CONTENT_TYPE,
    Common.LAST_CHECKED,
    Common.UPLOAD_TIME,
    Common.EXPIRATION_TIME,
    Common.OBJECT_OWNER_ID,
    Common.FILE_MODE,
    Common.MULITPART_UPLOAD_MODE,
    Common.PREVIOUS_VERSION_ID,
    Common.SERVER_SIDE_ENCRYPTION,
    Common.STORAGE_CLASS,
    Common.RESTORE_EXPIRY_DATE,
    Common.ONGOING_RESTORE
  ]

  def __init__(self):
    self.metadata = {}

  def add_header(self, key, value):
    self._check_metadata(key)
    self.metadata.update({key: value})

  def add_user_metadata(self, key, value):
    self._check_metadata(key)
    self.metadata.update({key: value})

  def _check_metadata(self, key):
    is_valid = key.startswith(self.USER_DEFINED_METADATA_PREFIX)

    if key in self.PRE_DEFINED_METADATA:
      is_valid = True

    if not is_valid:
      raise GalaxyFDSClientException("Invalid metadata: " + key) class AccessControlList(object):
  '''
  The access control list class.
  '''

  def __init__(self):
    self.acl = {}

  def add_grant(self, grant):
    self.acl['%s:%s' % (grant.grantee.id, grant.type)] = grant

  def get_grant_list(self):
    grants = []
    for k in self.acl:
      grants.append(self.acl[k])
    return grants

  def is_subset(self, other):
    for k in self.acl:
      if self.acl[k] != other.acl[k]:
        return False
    return True class Grant(dict):
  '''
  The grant class.
  '''

  def __init__(self, grantee, permission):
    self.grantee = grantee
    self.type = GrantType.USER
    self.permission = permission

  @property
  def permission(self):
    return self['permission']

  @permission.setter
  def permission(self, permission):
    self['permission'] = Permission(permission)

  @property
  def grantee(self):
    return self['grantee']

  @grantee.setter
  def grantee(self, grantee):
    self['grantee'] = grantee

  @property
  def type(self):
    return self['type']

  @type.setter
  def type(self, type):
    self['type'] = type class GrantType(object):
  '''
  The grant type class.
  '''
  USER = 'USER'
  GROUP = 'GROUP' class Grantee(dict):
  '''
  The grantee definition class.
  '''

  def __init__(self, id):
    self.id = id

  @property
  def display_name(self):
    return self['displayName']

  @display_name.setter
  def display_name(self, display_name):
    self['displayName'] = display_name

  @property
  def id(self):
    return self['id']

  @id.setter
  def id(self, id):
    self['id'] = id class Permission(object):
  """
  The Permission definition class.
  """

  '''
  The READ permission: when it applies to buckets it means allow the grantee to
  list the objects in the bucket; when it applies to objects it means allow the
  grantee to read the object data and metadata.
  '''
  READ = 0x01

  '''
  The WRITE permission: when it applies to buckets it means allow the grantee
  to create, overwrite and delete any object in the bucket; it is not applicable
  for objects.
  '''
  WRITE = 0x02

  '''
  The READ_OBJECT permission: when it applies to buckets it means
  allow the grantee to read any object in the bucket;
  it is not applicable to object.
  '''
  READ_OBJECTS = 0x04

  '''
  The SSO_WRITE permission: when applied to bucket, it means
  users can put objects to the bucket with SSO auth
  it is not applicable to object.
  '''
  SSO_WRITE = 0x08

  '''
  The FULL_CONTROL permission: allows the grantee the READ and WRITE permission
  on the bucket/object.
  '''
  FULL_CONTROL = 0xff

  def __init__(self, value):
    types = IS_PY3 and (str) or (str, unicode)

    if isinstance(value, types):
      value = value.strip().upper()
      if value == 'READ':
        self._value = Permission.READ
      elif value == 'WRITE':
        self._value = Permission.WRITE
      elif value == 'READ_OBJECTS':
        self._value = Permission.READ_OBJECTS
      elif value == 'SSO_WRITE':
        self._value = Permission.SSO_WRITE
      elif value == 'FULL_CONTROL':
        self._value = Permission.FULL_CONTROL
      else:
        raise RuntimeError('Fatal error')
    else:
      self._value = value

  def __eq__(self, other):
    return self._value == other._value

  def to_string(self):
    value = self.get_value()
    if value == Permission.READ:
      return 'READ'
    elif value == Permission.WRITE:
      return 'WRITE'
    elif value == Permission.READ_OBJECTS:
      return 'READ_OBJECTS'
    elif value == Permission.SSO_WRITE:
      return 'SSO_WRITE'
    elif value == Permission.FULL_CONTROL:
      return 'FULL_CONTROL'
    else:
      raise RuntimeError('Fatal error')

  def get_value(self):
    return self._value class UserGroups(object):
  '''
  The user groups class.
  '''
  ALL_USERS = 'ALL_USERS'
  AUTHENTICATED_USERS = 'AUTHENTICATED_USERS' class UploadPartResultList(dict):
  """
  The Upload Part Result List class
  """

  def __init__(self, json):
    if json:
      self.uploadPartResultList = json['uploadPartResultList']

  @property
  def uploadPartResultList(self):
    return self.uploadPartResultList

  @uploadPartResultList.setter
  def uploadPartResultList(self, upload_part_result_list):
    self['uploadPartResultList'] = upload_part_result_list
import json
import logging
import os
import sys
import traceback
import argcomplete
import argparse
from os.path import expanduser
from sys import version_info
from argcomplete.completers import ChoicesCompleter
from datetime import datetime
from time import sleep
from fds import FDSClientConfiguration, GalaxyFDSClient
from fds.galaxy_fds_client_exception import GalaxyFDSClientException
from fds.model.fds_lifecycle import FDSLifecycleConfig, FDSLifecycleRule
from fds.model.fds_cors import FDSCORSConfig, FDSCORSRule
from fds.model.fds_object_metadata import FDSObjectMetadata
from fds.model.permission import AccessControlList
from fds.model.permission import Grant
from fds.model.permission import GrantType
from fds.model.permission import Grantee
from fds.model.permission import Permission
from fds.model.permission import UserGroups
from fds.model.upload_part_result_list import UploadPartResultList
                                         quality=webp_quality)
    length = -1
    offset = 0
  elif gif_extract_type:
    fds_object = fds_client._get_extracted_gif_(bucket_name=bucket_name,
                                                object_name=object_name,
                                                type=gif_extract_type)
    length = -1
    offset = 0
  else:
    fds_object = fds_client.get_object(bucket_name=bucket_name,
                                       object_name=object_name,
                                       position=offset,
                                       stream=True,
                                       is_archive=is_archive)

  length_left = length
  if length_left == -1:
    length_left = IS_PY3 and sys.maxsize or sys.maxint
  try:
    if data_file:
      with open(data_file, "wb") as f:
        for chunk in fds_object.stream:
          # if isinstance(chunk, bytes):
          #   chunk = chunk.decode(encoding='UTF-8')
          l = min(length_left, len(chunk));
          f.write(chunk[0:l])
          length_left -= l
          if length_left <= 0:
            break
    else:
      for chunk in fds_object.stream:
        # if isinstance(chunk, bytes):
        #   chunk = chunk.decode(encoding='UTF-8')
        l = min(length_left, len(chunk))
        if IS_PY3:
          sys.stdout.buffer.write(chunk[0:l])
        else:
          sys.stdout.write(chunk[0:l])
        length_left -= l
        if length_left <= 0:
          break
      sys.stdout.flush()
  finally:
    fds_object.stream.close()


def post_object(data_file, bucket_name, metadata):
  with open(data_file, 'r') as f:
    fds_object = fds_client.post_object(bucket_name=bucket_name, data=f, metadata=metadata)
    logger.debug('Post object [' + fds_object.object_name + ']')
    sys.stdout.write(fds_object.object_name)


def download_directory(bucket_name, object_prefix, data_dir, recursive):
  def mkdirs(path):
    if not os.path.isdir(path):
      os.makedirs(path)

  delimiter = not recursive and '/' or ''

  if not object_prefix.endswith('/'):
    sys.stderr.write('Object prefix must endswith /')
    sys.stderr.flush()
    return

  if not data_dir:
    data_dir = './'

  if not data_dir.endswith('/'):
    data_dir += '/'

  if object_prefix == '/':  # download all objects under bucket
    objects_list = fds_client.list_objects(bucket_name, prefix='', delimiter=delimiter)
  else:
    objects_list = fds_client.list_objects(bucket_name, prefix=object_prefix, delimiter=delimiter)
  # print(objects_list.objects)
  while True:
    for obj in objects_list.objects:
      if obj.object_name.endswith('/'):
        mkdirs(data_dir + obj.object_name)
      else:
        if '/' in obj.object_name:
          mkdirs(data_dir + obj.object_name[:obj.object_name.rfind('/')])
        obj_path = data_dir + obj.object_name
        if os.path.exists(obj_path) and obj.size == os.path.getsize(obj_path):
          logger.debug("[%s/%s] is already downloaded" % (bucket_name, obj.object_name))
        else:
          get_object(data_dir + obj.object_name, bucket_name, obj.object_name, None, 0, -1)
          logger.debug("Download [%s/%s] success" % (bucket_name, obj.object_name))
    for prefix in objects_list.common_prefixes:
      mkdirs(data_dir + prefix)
    if objects_list.is_truncated:
      objects_list = fds_client.list_next_batch_of_objects(objects_list)
    else:
      break

  sys.stdout.write("Downdoad directory[%s] success" % object_prefix)
  sys.stdout.flush()


def put_bucket(bucket_name):
  check_bucket_name(bucket_name=bucket_name)
  fds_client.create_bucket(bucket_name)
  sys.stdout.write("Create bucket[%s] success" % bucket_name)


def get_bucket_acl(bucket_name):
  acl = fds_client.get_bucket_acl(bucket_name=bucket_name)
  sys.stdout.write('ACL:\n')
  sys.stdout.write('gratee_id\tgrant_type\tpermission\n')
  for i in acl.get_grant_list():
    sys.stdout.write(
      str(i.grantee['id']) + '\t' + str(i.type) + '\t' + str(i.permission.to_string()) + '\n')


def put_bucket_acl(bucket_name, gratee_list, permission_list):
  check_bucket_name(bucket_name=bucket_name)
  bucketAcl = AccessControlList()
  for role in gratee_list:

    grant = Grant(Grantee(role), Permission(permission_list).get_value())