# Copyright 2014 Baidu, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distributed under the # License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # either express or implied. See the License for the specific language governing permissions # and limitations under the License. """ This module provides a client class for BOS. """ import io import copy import http.client import os import json import logging import shutil import struct from builtins import str from builtins import bytes from future.utils import iteritems, iterkeys, itervalues from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, FIRST_COMPLETED import threading import functools import multiprocessing import baidubce from baidubce import bce_client_configuration from baidubce import utils from baidubce.auth import bce_v1_signer from baidubce.bce_base_client import BceBaseClient from baidubce.exception import BceClientError from baidubce.exception import BceServerError from baidubce.exception import BceHttpClientError from baidubce.http import bce_http_client from baidubce.http import handler from baidubce.http import http_content_types from baidubce.http import http_headers from baidubce.http import http_methods from baidubce.services import bos from baidubce.services.bos import bos_handler from baidubce.services.bos import storage_class from baidubce.utils import required from baidubce import compat _logger = logging.getLogger(__name__) FETCH_MODE_SYNC = b"sync" FETCH_MODE_ASYNC = b"async" ENCRYPTION_ALGORITHM= "AES256" HTTP_PROTOCOL_HEAD = b'http' class UploadTaskHandle: """ handle to control multi upload file with multi-thread """ def __init__(self): self.cancel_flag= False self.cancel_lock = threading.Lock() def cancel(self): """ cancel putting super object from file with multi-thread """ self.cancel_lock.acquire() self.cancel_flag= True self.cancel_lock.release() def is_cancel(self): """ get cancel flag """ self.cancel_lock.acquire() result = self.cancel_flag self.cancel_lock.release() return result class BosClient(BceBaseClient): """ sdk client """ def __init__(self, config=None): BceBaseClient.__init__(self, config) def list_buckets(self, config=None): """ List buckets of user :param config: None :type config: BceClientConfiguration :returns: all buckets owned by the user. :rtype: baidubce.bce_response.BceResponse """ return self._send_request(http_methods.GET, config=config) @required(bucket_name=(bytes, str)) def get_bucket_location(self, bucket_name, config=None): """ Get the region which the bucket located in. :param bucket_name: the name of bucket :type bucket_name: string or unicode :param config: None :type config: BceClientConfiguration :return: region of the bucket :rtype: str """ params = {b'location': b''} response = self._send_request(http_methods.GET, bucket_name, params=params, config=config) return response.location_constraint @required(bucket_name=(bytes, str)) def create_bucket(self, bucket_name, config=None): """ Create bucket with specific name :param bucket_name: the name of bucket :type bucket_name: string or unicode :param config: None :type config: BceClientConfiguration :returns: :rtype: baidubce.bce_response.BceResponse """ return self._send_request(http_methods.PUT, bucket_name, config=config) @required(bucket_name=(bytes, str)) def does_bucket_exist(self, bucket_name, config=None): """ Check whether there is a bucket with specific name :param bucket_name: None :type bucket_name: str :return:True or False :rtype: bool """ try: self._send_request(http_methods.HEAD, bucket_name, config=config) return True except BceHttpClientError as e: if isinstance(e.last_error, BceServerError): if e.last_error.status_code == http.client.FORBIDDEN: return True if e.last_error.status_code == http.client.NOT_FOUND: return False raise e @required(bucket_name=(bytes, str)) def get_bucket_acl(self, bucket_name, config=None): """ Get Access Control Level of bucket :type bucket: string :param bucket: None :return: **json text of acl** """ return self._send_request( http_methods.GET, bucket_name, params={b'acl': b''}, config=config) @staticmethod def _dump_acl_object(acl): result = {} for k, v in iteritems(acl.__dict__): if not k.startswith('_'): result[k] = v return result @required(bucket_name=(bytes, str), acl=(list, dict)) def set_bucket_acl(self, bucket_name, acl, config=None): """ Set Access Control Level of bucket :type bucket: string :param bucket: None :type grant_list: list of grant :param grant_list: None :return: **HttpResponse Class** """ self._send_request(http_methods.PUT, bucket_name, body=json.dumps({'accessControlList': acl}, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'acl': b''}, config=config) @required(bucket_name=(bytes, str), canned_acl=bytes) def set_bucket_canned_acl(self, bucket_name, canned_acl, config=None): """ :param bucket_name: :param canned_acl: :param config: :return: """ self._send_request(http_methods.PUT, bucket_name, headers={http_headers.BCE_ACL: canned_acl}, params={b'acl': b''}, config=config) @required(bucket_name=(bytes, str)) def set_bucket_storage_class(self, bucket_name, storage_class, config=None): """ :param bucket_name: :param config: :return: """ storage_class = compat.convert_to_string(storage_class) return self._send_request(http_methods.PUT, bucket_name, body=json.dumps({'storageClass': storage_class}, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'storageClass': b''}, config=config) @required(bucket_name=(bytes, str)) def get_bucket_storage_class(self, bucket_name, config=None): """ :param bucket_name: :param config: :return: """ return self._send_request(http_methods.GET, bucket_name, params={b'storageClass': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket(self, bucket_name, config=None): """ Delete a Bucket(Must Delete all the Object in Bucket before) :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, config=config) # bucket static website @required(bucket_name=(bytes, str)) def put_bucket_static_website(self, bucket_name, index=None, not_found=None, config=None): """ Set index page and not_found 404 page for static website trusteeship :type bucket_name: string :param bucket_name: None :type index:string :param index:object name of index page for static website trusteeship :type not_found:string :param not_found:object name of not_found 404 page for static website trusteeship :return: **HttpResponse Class** """ body = {} if index is not None: body['index'] = index if not_found is not None: body['notFound'] = not_found return self._send_request(http_methods.PUT, bucket_name, body=json.dumps(body, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'website': b''}, config=config) @required(bucket_name=(bytes, str)) def get_bucket_static_website(self, bucket_name, config=None): """ Get Information of static website trusteeship :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'website': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_static_website(self, bucket_name, config=None): """ Delete Information of static website trusteeship to be closed :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'website': b''}, config=config) # bucket encryption @required(bucket_name=(bytes, str)) def put_bucket_encryption(self, bucket_name, encryption_algorithm=ENCRYPTION_ALGORITHM, config=None): """ Set server encryption for bucket :type bucket: string :param bucket: None :type encryption_algorithm: string :param grant_list: server encryption algorithm for bucekt.Now the value of encryption_algorithm only is 'AES256' :return: **HttpResponse Class** """ encryption_algorithm = compat.convert_to_string(encryption_algorithm) return self._send_request(http_methods.PUT, bucket_name, body=json.dumps({"encryptionAlgorithm":encryption_algorithm}, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'encryption': b''}, config=config) @required(bucket_name=(bytes, str)) def get_bucket_encryption(self, bucket_name, config=None): """ Get status of server encryption :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'encryption': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_encryption(self, bucket_name, config=None): """ Close server encryption :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'encryption': b''}, config=config) # Bucket Copyright Protection @required(bucket_name=(bytes, str), resource=(list)) def put_bucket_copyright_protection(self, bucket_name, resource, config=None): """ Open image copyright protection and set resource :type bucket: string :param bucket: None :type resource: list of string :param grant_list: resource range to be protected :return: **HttpResponse Class** """ return self._send_request(http_methods.PUT, bucket_name, body=json.dumps({"resource": resource}, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'copyrightProtection': b''}, config=config) @required(bucket_name=(bytes, str)) def get_bucket_copyright_protection(self, bucket_name, config=None): """ Get configuration of image copyright protection :type bucket: string :param grant_list: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'copyrightProtection': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_copyright_protection(self, bucket_name, config=None): """ Close image copyright protection :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'copyrightProtection': b''}, config=config) # bucket replication @required(bucket_name=(bytes, str), replication=(dict)) def put_bucket_replication(self, bucket_name, replication, config=None): """ Open cross-region replication :type bucket: string :param bucket: None :type replication: dict :type replication: configuration for cross-region replication :return: **HttpResponse Class** """ params={b'replication': b''} if "id" in replication: params[b"id"] = compat.convert_to_bytes(replication["id"]) return self._send_request(http_methods.PUT, bucket_name, body=json.dumps(replication, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params=params, config=config) @required(bucket_name=(bytes, str)) def get_bucket_replication(self, bucket_name, id=None, config=None): """ Get configuration of cross-region replication :type bucket: string :param bucket: None :type id: string :param id: replication rule id :return: **HttpResponse Class** """ params={b'replication': b''} if id is not None: params[b"id"] = compat.convert_to_bytes(id) return self._send_request(http_methods.GET, bucket_name, params=params, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_replication(self, bucket_name, id=None, config=None): """ Delete configuration of cross-region replication and close it :type bucket: string :param bucket: None :type id: string :param id: replication rule id :return: **HttpResponse Class** """ params={b'replication': b''} if id is not None: params[b"id"] = compat.convert_to_bytes(id) return self._send_request(http_methods.DELETE, bucket_name, params=params, config=config) @required(bucket_name=(bytes, str)) def get_bucket_replication_progress(self, bucket_name, id=None, config=None): """ Get status of cross-region replication,for exapmle 'historyReplicationPercent', 'latestReplicationTime' :type bucket: string :param bucket: None :type id: string :param id: replication rule id :return: **HttpResponse Class** """ params={b'replicationProgress': b''} if id is not None: params[b"id"] = compat.convert_to_bytes(id) return self._send_request(http_methods.GET, bucket_name, params=params, config=config) @required(bucket_name=(bytes, str)) def list_bucket_replication(self, bucket_name, config=None): """ list configuration of cross-region replication rule :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'replication': b'', b'list': b''}, config=config) @required(bucket_name=(bytes, str), inventory=(dict)) def put_bucket_inventory(self, bucket_name, inventory, config=None): """ set bucket inventoru :type bucket: string :param bucket: None :type inventory: dict :param inventory: configuration for bucket inventory :return: **HttpResponse Class** """ conf_id = compat.convert_to_bytes(inventory["id"]) return self._send_request(http_methods.PUT, bucket_name, body=json.dumps(inventory, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'inventory': b'', b'id': conf_id}, config=config) @required(bucket_name=(bytes, str), inventory_conf_id=(bytes, str)) def get_bucket_inventory(self, bucket_name, inventory_conf_id, config=None): """ Get configuration of bucket inventory :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'inventory': b'', b'id': compat.convert_to_bytes(inventory_conf_id)}, config=config) @required(bucket_name=(bytes, str), inventory_conf_id=(bytes, str)) def delete_bucket_inventory(self, bucket_name, inventory_conf_id, config=None): """ Delete configuration of bucket inventory :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'inventory': b'', b'id': compat.convert_to_bytes(inventory_conf_id)}, config=config) @required(bucket_name=(bytes, str)) def list_bucket_inventory(self, bucket_name, config=None): """ list configuration of bucket inventory :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'inventory': b''}, config=config) @required(bucket_name=(bytes, str)) def put_bucket_trash(self, bucket_name, trash_dir=None, config=None): """ Open bucket trash function :type bucket: string :param bucket: None :type trash_dir: string :param trash_dir: directory of trash,optional :return: **HttpResponse Class** """ if trash_dir is not None: trash_dir = compat.convert_to_string(trash_dir) return self._send_request(http_methods.PUT, bucket_name, body=json.dumps({"trashDir": trash_dir}, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'trash': b''}, config=config) @required(bucket_name=(bytes, str)) def get_bucket_trash(self, bucket_name, config=None): """ Get status of bucket trash :type bucket: string :param grant_list: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'trash': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_trash(self, bucket_name, config=None): """ Close bucket trash :type bucket: string :param bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'trash': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str)) def generate_pre_signed_url(self, bucket_name, key, timestamp=0, expiration_in_seconds=1800, headers=None, params=None, headers_to_sign=None, protocol=None, config=None, httpmethod=http_methods.GET): """ Get an authorization url with expire time. specified protocol in endpoint > protocal > default protocol in config. :type timestamp: int :param timestamp: None :type expiration_in_seconds: int :param expiration_in_seconds: None :type options: dict :param options: None :param is_official_domain: default use not official domain,example: bucket.bj.bcebos.com :return: **URL string** """ key = compat.convert_to_bytes(key) config = self._merge_config(config, bucket_name) headers = headers or {} params = params or {} # specified protocol in endpoint > protocal > default protocol in config if protocol is not None: config.protocol = protocol endpoint_protocol, endpoint_host, endpoint_port = \ utils.parse_host_port(config.endpoint, config.protocol) full_host = endpoint_host if endpoint_port != endpoint_protocol.default_port: full_host += b':' + compat.convert_to_bytes(endpoint_port) headers[http_headers.HOST] = full_host path = self._get_path(config, bucket_name, key) if httpmethod != http_methods.GET and httpmethod != http_methods.HEAD: headers_to_sign = set([b'host']) # Compatible with STS request acquisition if config.security_token is not None: params[http_headers.STS_SECURITY_TOKEN.lower()] = config.security_token params[http_headers.AUTHORIZATION.lower()] = bce_v1_signer.sign( config.credentials, httpmethod, path, headers, params, timestamp, expiration_in_seconds, headers_to_sign) return b"%s://%s%s?%s" % (compat.convert_to_bytes(endpoint_protocol.name), full_host, path, utils.get_canonical_querystring(params, False)) @required(bucket_name=(bytes, str), rules=(list, dict)) def put_bucket_lifecycle(self, bucket_name, rules, config=None): """ Put Bucket Lifecycle :type bucket: string :param bucket: None :type rules: list :param rules: None :return:**Http Response** """ return self._send_request(http_methods.PUT, bucket_name, params={b'lifecycle': b''}, body=json.dumps({'rule': rules}), config=config) @required(bucket_name=(bytes, str)) def get_bucket_lifecycle(self, bucket_name, config=None): """ Get Bucket Lifecycle :type bucket: string :param bucket: None :return:**Http Response** """ return self._send_request(http_methods.GET, bucket_name, params={b'lifecycle': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_lifecycle(self, bucket_name, config=None): """ Delete Bucket Lifecycle :type bucket: string :param bucket: None :return:**Http Response** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'lifecycle': b''}, config=config) @required(bucket_name=(bytes, str), cors_configuration=list) def put_bucket_cors(self, bucket_name, cors_configuration, config=None): """ Put Bucket Cors :type bucket: string :param bucket: None :type cors_configuration: list :param cors_configuration: None :return:**Http Response** """ return self._send_request(http_methods.PUT, bucket_name, params={b'cors': b''}, body=json.dumps({'corsConfiguration': cors_configuration}), config=config) @required(bucket_name=(bytes, str)) def get_bucket_cors(self, bucket_name, config=None): """ Get Bucket Cors :type bucket: string :param bucket: None :return:**Http Response** """ return self._send_request(http_methods.GET, bucket_name, params={b'cors': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_cors(self, bucket_name, config=None): """ Delete Bucket Cors :type bucket: string :param bucket: None :return:**Http Response** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'cors': b''}, config=config) @required(bucket_name=(bytes, str)) def list_objects(self, bucket_name, max_keys=1000, prefix=None, marker=None, delimiter=None, config=None): """ Get Object Information of bucket :type bucket: string :param bucket: None :type delimiter: string :param delimiter: None :type marker: string :param marker: None :type max_keys: int :param max_keys: value <= 1000 :type prefix: string :param prefix: None :return: **_ListObjectsResponse Class** """ params = {} if max_keys is not None: params[b'maxKeys'] = max_keys if prefix is not None: params[b'prefix'] = prefix if marker is not None: params[b'marker'] = marker if delimiter is not None: params[b'delimiter'] = delimiter return self._send_request(http_methods.GET, bucket_name, params=params, config=config) @required(bucket_name=(bytes, str)) def list_all_objects(self, bucket_name, prefix=None, delimiter=None, config=None): """ :param bucket_name: :param prefix: :param delimiter: :param config: :return: """ marker = None while True: response = self.list_objects( bucket_name, marker=marker, prefix=prefix, delimiter=delimiter, config=config) for item in response.contents: yield item if response.is_truncated: marker = response.next_marker else: break @staticmethod def _get_range_header_dict(range): if range is None: return None if not isinstance(range, (list, tuple)): raise TypeError('range should be a list or a tuple') if len(range) != 2: raise ValueError('range should have length of 2') return {http_headers.RANGE: b'bytes=%d-%d' % tuple(range)} @staticmethod def _parse_bos_object(http_response, response): """Sets response.body to http_response and response.user_metadata to a dict consists of all http headers starts with 'x-bce-meta-'. :param http_response: the http_response object returned by HTTPConnection.getresponse() :type http_response: httplib.HTTPResponse :param response: general response object which will be returned to the caller :type response: baidubce.BceResponse :return: always true :rtype bool """ user_metadata = {} headers_list = http_response.getheaders() if compat.PY3: temp_heads = [] for k, v in headers_list: k = k.lower() temp_heads.append((k, v)) headers_list = temp_heads prefix = compat.convert_to_string( http_headers.BCE_USER_METADATA_PREFIX ) for k, v in headers_list: if k.startswith(prefix): k = k[len(prefix):] user_metadata[compat.convert_to_unicode(k)] = \ compat.convert_to_unicode(v) response.metadata.user_metadata = user_metadata response.data = http_response return True @required(bucket_name=(bytes, str), key=(bytes, str)) def get_object(self, bucket_name, key, range=None, traffic_limit=None, version_id=None, config=None): """ :param bucket_name: :param key: :param range: :param config: :return: """ query_params = {} if version_id is not None: version_id = compat.convert_to_bytes(version_id) query_params={b'versionId': version_id} key = compat.convert_to_bytes(key) if len(key) == 0 or key.startswith(b"/"): raise BceClientError("Key can not be empty or start with '/' .") range_header = BosClient._get_range_header_dict(range) if traffic_limit is not None: if range_header is None: range_header = {} range_header[http_headers.BOS_TRAFFIC_LIMIT] = traffic_limit return self._send_request( http_methods.GET, bucket_name, key, headers=range_header, params=query_params, config=config, body_parser=BosClient._parse_bos_object) # restore object @required(bucket_name=(bytes, str), key=(bytes, str)) def restore_object(self, bucket_name, key, days=None, tier="Standard", config=None): """ :param bucket_name: :param key: :param config: :return: """ key = compat.convert_to_bytes(key) headers = {} if days is not None: headers[http_headers.BOS_RESTORE_DAYS] = days if compat.convert_to_string(tier) not in ("Standard", "Expedited", "LowCost"): raise ValueError('invalid tier:{} for restore_object.The valid value is \"Standard\" or \"Expedited\" or '\ '\"LowCost\"'.format(tier) ) headers[http_headers.BOS_RESTORE_TIER] = compat.convert_to_bytes(tier) return self._send_request( http_methods.POST, bucket_name, key, headers=headers, params={b'restore': b''}, config=config, body_parser=BosClient._parse_bos_object) @staticmethod def _save_body_to_file(http_response, response, file_name, buf_size=16 * 1024, progress_callback=None): f = open(file_name, 'wb') try: # Added progress bar monitoring if progress_callback: file_size = int(response.metadata.content_length) stream = utils.make_progress_adapter(http_response, progress_callback, file_size) else: stream = http_response shutil.copyfileobj(stream, f, buf_size) http_response.close() finally: f.close() return True @staticmethod def _parse_select_message(http_response, response, select_response): select_response.init_from_http_response(http_response, response) return True @required(bucket_name=(bytes, str), key=(bytes, str)) def get_object_as_string(self, bucket_name, key, range=None, version_id=None, config=None): """ :param bucket_name: :param key: :param range: :param config: :return: """ key = compat.convert_to_bytes(key) response = self.get_object(bucket_name, key, range=range, version_id = version_id, config=config) s = response.data.read() response.data.close() return s @required(bucket_name=(bytes, str), key=(bytes, str), file_name=(bytes, str)) def get_object_to_file(self, bucket_name, key, file_name, range=None, config=None, progress_callback=None, traffic_limit=None, version_id=None): """ Get Content of Object and Put Content to File :type bucket: string :param bucket: None :type key: string :param key: None :type file_name: string :param file_name: None :type range: tuple :param range: (0,9) represent get object contents of 0-9 in bytes. 10 bytes date in total. :return: **HTTP Response** """ query_params = {} if version_id is not None: version_id = compat.convert_to_bytes(version_id) query_params={b'versionId': version_id} key = compat.convert_to_bytes(key) if len(key) == 0 or key.startswith(b"/"): raise BceClientError("Key can not be empty or start with '/' .") file_name = compat.convert_to_bytes(file_name) range_header = BosClient._get_range_header_dict(range) if traffic_limit is not None: if range_header is None: range_header = {} range_header[http_headers.BOS_TRAFFIC_LIMIT] = traffic_limit return self._send_request( http_methods.GET, bucket_name, key, headers=range_header, params=query_params, config=config, body_parser=lambda http_response, response: BosClient._save_body_to_file( http_response, response, file_name, self._get_config_parameter(config, 'recv_buf_size'), progress_callback=progress_callback)) @required(bucket_name=(bytes, str), key=(bytes, str)) def get_object_meta_data(self, bucket_name, key, version_id=None, config=None): """ Get head of object :type bucket: string :param bucket: None :type key: string :param key: None :return: **_GetObjectMetaDataResponse Class** """ query_params = {} if version_id is not None: version_id = compat.convert_to_bytes(version_id) query_params={b'versionId': version_id} key = compat.convert_to_bytes(key) return self._send_request(http_methods.HEAD, bucket_name, key, params=query_params, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), data=object, content_length=compat.integer_types, content_md5=(bytes, str)) def append_object(self, bucket_name, key, data, content_md5, content_length, offset=None, content_type=None, user_metadata=None, content_sha256=None, storage_class=None, user_headers=None, progress_callback=None, traffic_limit=None, object_tagging=None, config=None): """ Put an appendable object to BOS or add content to an appendable object :type bucket: string :param bucket: None :type key: string :param key: None :type content_length: long :type offset: long :return: **HTTP Response** """ key = compat.convert_to_bytes(key) content_md5 = compat.convert_to_bytes(content_md5) headers = self._prepare_object_headers( content_length=content_length, content_md5=content_md5, content_type=content_type, content_sha256=content_sha256, user_metadata=user_metadata, storage_class=storage_class, user_headers=user_headers, traffic_limit=traffic_limit, object_tagging=object_tagging) if content_length > bos.MAX_APPEND_OBJECT_LENGTH: raise ValueError('Object length should be less than %d. ' 'Use multi-part upload instead.' % bos.MAX_APPEND_OBJECT_LENGTH) params = {b'append': b''} if offset is not None: params[b'offset'] = offset if progress_callback: data = utils.make_progress_adapter(data, progress_callback) return self._send_request( http_methods.POST, bucket_name, key, body=data, headers=headers, params=params, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), data=(bytes, str)) def append_object_from_string(self, bucket_name, key, data, content_md5=None, offset=None, content_type=None, user_metadata=None, content_sha256=None, storage_class=None, user_headers=None, progress_callback=None, traffic_limit=None, config=None): """ Create an appendable object and put content of string to the object or add content of string to an appendable object """ key = compat.convert_to_bytes(key) if isinstance(data, str): data = data.encode(baidubce.DEFAULT_ENCODING) fp = None try: fp = io.BytesIO(data) if content_md5 is None: content_md5 = utils.get_md5_from_fp( fp, buf_size=self._get_config_parameter(config, 'recv_buf_size')) return self.append_object(bucket_name=bucket_name, key=key, data=fp, content_md5=content_md5, content_length=len(data), offset=offset, content_type=content_type, user_metadata=user_metadata, content_sha256=content_sha256, storage_class=storage_class, user_headers=user_headers, progress_callback=progress_callback, traffic_limit=traffic_limit, config=config) finally: if fp is not None: fp.close() @required(bucket_name=(bytes, str), key=(bytes, str), data=object, content_length=compat.integer_types, content_md5=(bytes, str)) def put_object(self, bucket_name, key, data, content_length, content_md5, content_type=None, content_sha256=None, user_metadata=None, storage_class=None, user_headers=None, encryption=None, customer_key=None, customer_key_md5=None, progress_callback=None, traffic_limit=None, object_tagging=None, config=None): """ Put object and put content of file to the object :type bucket: string :param bucket: None :type key: string :param key: None :type fp: FILE :param fp: None :type file_size: long :type offset: long :type content_length: long :return: **HTTP Response** """ key = compat.convert_to_bytes(key) content_md5 = compat.convert_to_bytes(content_md5) headers = self._prepare_object_headers( content_length=content_length, content_md5=content_md5, content_type=content_type, content_sha256=content_sha256, user_metadata=user_metadata, storage_class=storage_class, user_headers=user_headers, traffic_limit=traffic_limit, object_tagging=object_tagging,) buf_size = self._get_config_parameter(config, 'recv_buf_size') if content_length > bos.MAX_PUT_OBJECT_LENGTH: raise ValueError('Object length should be less than %d. ' 'Use multi-part upload instead.' % bos.MAX_PUT_OBJECT_LENGTH) if progress_callback: data = utils.make_progress_adapter(data, progress_callback) return self._send_request( http_methods.PUT, bucket_name, key, body=data, headers=headers, config=config) @required(bucket=(bytes, str), key=(bytes, str), data=(bytes, str)) def put_object_from_string(self, bucket, key, data, content_md5=None, content_type=None, content_sha256=None, user_metadata=None, storage_class=None, user_headers=None, encryption=None, customer_key=None, customer_key_md5=None, progress_callback=None, traffic_limit=None, object_tagging=None, config=None): """ Create object and put content of string to the object :type bucket: string :param bucket: None :type key: string :param key: None :type input_content: string :param input_content: None :type options: dict :param options: None :return: **HTTP Response** """ key = compat.convert_to_bytes(key) if isinstance(data, str): data = data.encode(baidubce.DEFAULT_ENCODING) fp = None try: fp = io.BytesIO(data) if content_md5 is None: content_md5 = utils.get_md5_from_fp( fp, buf_size=self._get_config_parameter(config, 'recv_buf_size')) return self.put_object(bucket, key, fp, content_length=len(data), content_md5=content_md5, content_type=content_type, content_sha256=content_sha256, user_metadata=user_metadata, storage_class=storage_class, user_headers=user_headers, encryption=encryption, customer_key=customer_key, customer_key_md5=customer_key_md5, progress_callback = progress_callback, traffic_limit=traffic_limit, object_tagging=object_tagging, config=config) finally: if fp is not None: fp.close() @required(bucket=(bytes, str), key=(bytes, str), file_name=(bytes, str)) def put_object_from_file(self, bucket, key, file_name, content_length=None, content_md5=None, content_type=None, content_sha256=None, user_metadata=None, storage_class=None, user_headers=None, encryption=None, customer_key=None, customer_key_md5=None, progress_callback=None, traffic_limit=None, object_tagging=None, config=None, ): """ Put object and put content of file to the object :type bucket: string :param bucket: None :type key: string :param key: None :type file_name: string :param file_name: None :type options: dict :param options: None :return: **HttpResponse Class** """ key = compat.convert_to_bytes(key) fp = open(file_name, 'rb') try: if content_length is None: fp.seek(0, os.SEEK_END) content_length = fp.tell() fp.seek(0) if content_md5 is None: recv_buf_size = self._get_config_parameter(config, 'recv_buf_size') content_md5 = utils.get_md5_from_fp(fp, length=content_length, buf_size=recv_buf_size) if content_type is None: content_type = utils.guess_content_type_by_file_name(file_name) return self.put_object(bucket, key, fp, content_length=content_length, content_md5=content_md5, content_type=content_type, content_sha256=content_sha256, user_metadata=user_metadata, storage_class=storage_class, user_headers=user_headers, encryption=encryption, customer_key=customer_key, customer_key_md5=customer_key_md5, progress_callback=progress_callback, traffic_limit=traffic_limit, object_tagging=object_tagging, config=config) finally: fp.close() @required(source_bucket_name=(bytes, str), source_key=(bytes, str), target_bucket_name=(bytes, str), target_key=(bytes, str)) def copy_object(self, source_bucket_name, source_key, target_bucket_name, target_key, etag=None, content_type=None, user_metadata=None, storage_class=None, user_headers=None, copy_object_user_headers=None, traffic_limit=None, object_tagging=None, config=None): """ Copy one object to another object :type source_bucket: string :param source_bucket: None :type source_key: string :param source_key: None :type target_bucket: string :param target_bucket: None :type target_key: string :param target_key: None :return: **HttpResponse Class** """ source_key = compat.convert_to_bytes(source_key) target_key = compat.convert_to_bytes(target_key) headers = self._prepare_object_headers( content_type=content_type, user_metadata=user_metadata, storage_class=storage_class, user_headers=user_headers, traffic_limit=traffic_limit, object_tagging=object_tagging) headers[http_headers.BCE_COPY_SOURCE] = utils.normalize_string( b'/%s/%s' % ( compat.convert_to_bytes(source_bucket_name), source_key), False) if etag is not None: headers[http_headers.BCE_COPY_SOURCE_IF_MATCH] = etag if user_metadata is not None or content_type is not None: headers[http_headers.BCE_COPY_METADATA_DIRECTIVE] = b'replace' else: headers[http_headers.BCE_COPY_METADATA_DIRECTIVE] = b'copy' if copy_object_user_headers is not None: try: headers = BosClient._get_user_header(headers, copy_object_user_headers, True) except Exception as e: raise e return self._send_request( http_methods.PUT, target_bucket_name, target_key, headers=headers, config=config, body_parser=bos_handler.parse_copy_object_response) @required(bucket_name=(bytes, str), key=(bytes, str)) def delete_object(self, bucket_name, key, version_id=None, config=None): """ Delete Object :type bucket: string :param bucket: None :type key: string :param key: None :return: **HttpResponse Class** """ query_params = {} if version_id is not None: version_id = compat.convert_to_bytes(version_id) query_params={b'versionId': version_id} key = compat.convert_to_bytes(key) return self._send_request(http_methods.DELETE, bucket_name, key, params=query_params, config=config) @required(bucket_name=(bytes, str), key_list=list) def delete_multiple_objects(self, bucket_name, key_list, config=None): """ Delete Multiple Objects :type bucket: string :param bucket: None :type key_list: string list :param key_list: None :return: **HttpResponse Class** """ key_list_json = [{'key': compat.convert_to_string(k)} for k in key_list] return self._send_request(http_methods.POST, bucket_name, body=json.dumps({'objects': key_list_json}), params={b'delete': b''}, config=config) @required(source_bucket=(bytes, str), target_bucket=(bytes, str), target_prefix=(bytes, str)) def put_bucket_logging(self, source_bucket, target_bucket, target_prefix=None, config=None): """ Put Bucket Logging :type source_bucket: string :param source_bucket: None :type target_bucket: string :param target_bucket: None :return: **HttpResponse Class** """ return self._send_request(http_methods.PUT, source_bucket, params={b'logging': b''}, body=json.dumps({'targetBucket': target_bucket, 'targetPrefix': target_prefix}), config=config) @required(bucket_name=(bytes, str)) def get_bucket_logging(self, bucket_name, config=None): """ Get Bucket Logging :type bucket_name: string :param bucket_name: None :return: **HttpResponse Class** """ return self._send_request(http_methods.GET, bucket_name, params={b'logging': b''}, config=config) @required(bucket_name=(bytes, str)) def delete_bucket_logging(self, bucket_name, config=None): """ Delete Bucket Logging :type bucket_name: string :param bucket_name: None :return: **HttpResponse Class** """ return self._send_request(http_methods.DELETE, bucket_name, params={b'logging': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str)) def initiate_multipart_upload(self, bucket_name, key, content_type=None, storage_class=None, user_headers=None, config=None): """ Initialize multi_upload_file. :type bucket: string :param bucket: None :type key: string :param key: None :return: **HttpResponse** """ key = compat.convert_to_bytes(key) headers = {} if storage_class is not None: headers[http_headers.BOS_STORAGE_CLASS] = storage_class if content_type is not None: headers[http_headers.CONTENT_TYPE] = utils.convert_to_standard_string(content_type) else: headers[http_headers.CONTENT_TYPE] = http_content_types.OCTET_STREAM if user_headers is not None: try: headers = BosClient._get_user_header(headers, user_headers, False) except Exception as e: raise e return self._send_request( http_methods.POST, bucket_name, key, headers=headers, params={b'uploads': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), upload_id=(bytes, str), part_number=int, part_size=compat.integer_types, part_fp=object) def upload_part(self, bucket_name, key, upload_id, part_number, part_size, part_fp, part_md5=None, progress_callback=None, traffic_limit=None, config=None): """ Upload a part. :type bucket: string :param bucket: None :type key: string :param key: None :type upload_id: string :param upload_id: None :type part_number: int :param part_number: None :type part_size: int or long :param part_size: None :type part_fp: file pointer :param part_fp: not None :type part_md5: str :param part_md5: None :type config: dict :param config: None :return: **HttpResponse** """ key = compat.convert_to_bytes(key) if part_number < bos.MIN_PART_NUMBER or part_number > bos.MAX_PART_NUMBER: raise ValueError('Invalid part_number %d. The valid range is from %d to %d.' % ( part_number, bos.MIN_PART_NUMBER, bos.MAX_PART_NUMBER)) if part_size > bos.MAX_PUT_OBJECT_LENGTH: raise ValueError('Single part length should be less than %d. ' % bos.MAX_PUT_OBJECT_LENGTH) headers = {http_headers.CONTENT_LENGTH: part_size, http_headers.CONTENT_TYPE: http_content_types.OCTET_STREAM} if part_md5 is not None: headers[http_headers.CONTENT_MD5] = part_md5 if progress_callback: part_fp = utils.make_progress_adapter(part_fp, progress_callback, part_size) if traffic_limit is not None: headers[http_headers.BOS_TRAFFIC_LIMIT] = traffic_limit return self._send_request( http_methods.PUT, bucket_name, key, body=part_fp, headers=headers, params={b'partNumber': part_number, b'uploadId': upload_id}, config=config) @required(source_bucket_name=(bytes, str), source_key=(bytes, str), target_bucket_name=(bytes, str), target_key=(bytes, str), upload_id=(bytes, str), part_number=int, part_size=compat.integer_types, offset=compat.integer_types) def upload_part_copy(self, source_bucket_name, source_key, target_bucket_name, target_key, upload_id, part_number, part_size, offset, etag=None, content_type=None, user_metadata=None, traffic_limit=None, config=None): """ Copy part. :type source_bucket_name: string :param source_bucket_name: None :type source_key: string :param source_key: None :type target_bucket_name: string :param target_bucket_name: None :type target_key: string :param target_key: None :type upload_id: string :param upload_id: None :return: **HttpResponse** """ source_key = compat.convert_to_bytes(source_key) target_key = compat.convert_to_bytes(target_key) headers = self._prepare_object_headers( content_type=content_type, user_metadata=user_metadata, traffic_limit=traffic_limit) headers[http_headers.BCE_COPY_SOURCE] = utils.normalize_string( b"/%s/%s" % (compat.convert_to_bytes(source_bucket_name), source_key), False) range = b"""bytes=%d-%d""" % (offset, offset + part_size - 1) headers[http_headers.BCE_COPY_SOURCE_RANGE] = range if etag is not None: headers[http_headers.BCE_COPY_SOURCE_IF_MATCH] = etag return self._send_request( http_methods.PUT, target_bucket_name, target_key, headers=headers, params={b'partNumber': part_number, b'uploadId': upload_id}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), upload_id=(bytes, str), part_number=int, part_size=compat.integer_types, file_name=(bytes, str), offset=compat.integer_types) def upload_part_from_file(self, bucket_name, key, upload_id, part_number, part_size, file_name, offset, part_md5=None, progress_callback=None, traffic_limit=None, config=None): """ :param bucket_name: :param key: :param upload_id: :param part_number: :param part_size: :param file_name: :param offset: :param part_md5: :param config: :return: """ key = compat.convert_to_bytes(key) f = open(file_name, 'rb') try: f.seek(offset) return self.upload_part(bucket_name, key, upload_id, part_number, part_size, f, part_md5=part_md5, progress_callback=progress_callback, traffic_limit=traffic_limit, config=config) finally: f.close() @required(bucket_name=(bytes, str), key=(bytes, str), upload_id=(bytes, str), part_list=list) def complete_multipart_upload(self, bucket_name, key, upload_id, part_list, user_metadata=None, config=None): """ After finish all the task, complete multi_upload_file. :type bucket: string :param bucket: None :type key: string :param key: None :type upload_id: string :param upload_id: None :type part_list: list :param part_list: None :return: **HttpResponse** """ key = compat.convert_to_bytes(key) headers = self._prepare_object_headers( content_type=http_content_types.JSON, user_metadata=user_metadata) return self._send_request( http_methods.POST, bucket_name, key, body=json.dumps({'parts': part_list}), headers=headers, params={b'uploadId': upload_id}) @required(bucket_name=(bytes, str), key=(bytes, str), upload_id=(bytes, str)) def abort_multipart_upload(self, bucket_name, key, upload_id, config=None): """ Abort upload a part which is being uploading. :type bucket: string :param bucket: None :type key: string :param key: None :type upload_id: string :param upload_id: None :return: **HttpResponse** """ key = compat.convert_to_bytes(key) return self._send_request(http_methods.DELETE, bucket_name, key, params={b'uploadId': upload_id}) @required(bucket_name=(bytes, str), key=(bytes, str), upload_id=(bytes, str)) def list_parts(self, bucket_name, key, upload_id, max_parts=None, part_number_marker=None, config=None): """ List all the parts that have been upload success. :type bucket: string :param bucket: None :type key: string :param key: None :type upload_id: string :param upload_id: None :type max_parts: int :param max_parts: None :type part_number_marker: string :param part_number_marker: None :return: **_ListPartsResponse Class** """ key = compat.convert_to_bytes(key) params = {b'uploadId': upload_id} if max_parts is not None: params[b'maxParts'] = max_parts if part_number_marker is not None: params[b'partNumberMarker'] = part_number_marker return self._send_request(http_methods.GET, bucket_name, key, params=params, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), upload_id=(bytes, str)) def list_all_parts(self, bucket_name, key, upload_id, config=None): """ :param bucket_name: :param key: :param upload_id: :param config: :return: """ key = compat.convert_to_bytes(key) part_number_marker = None while True: response = self.list_parts(bucket_name, key, upload_id, part_number_marker=part_number_marker, config=config) for item in response.parts: yield item if not response.is_truncated: break part_number_marker = response.next_part_number_marker @required(bucket_name=(bytes, str)) def list_multipart_uploads(self, bucket_name, max_uploads=None, key_marker=None, prefix=None, delimiter=None, config=None): """ List all Multipart upload task which haven't been ended.(Completed Init_MultiPartUpload but not completed Complete_MultiPartUpload or Abort_MultiPartUpload) :type bucket: string :param bucket: None :type delimiter: string :param delimiter: None :type max_uploads: int :param max_uploads: <=1000 :type key_marker: string :param key_marker: None :type prefix: string :param prefix: None :type upload_id_marker: string :param upload_id_marker: :return: **_ListMultipartUploadResponse Class** """ params = {b'uploads': b''} if delimiter is not None: params[b'delimiter'] = delimiter if max_uploads is not None: params[b'maxUploads'] = max_uploads if key_marker is not None: params[b'keyMarker'] = key_marker if prefix is not None: params[b'prefix'] = prefix return self._send_request(http_methods.GET, bucket_name, params=params, config=config) @required(bucket_name=(bytes, str)) def list_all_multipart_uploads(self, bucket_name, prefix=None, delimiter=None, config=None): """ :param bucket_name: :param prefix: :param delimiter: :param config: :return: """ key_marker = None while True: response = self.list_multipart_uploads(bucket_name, key_marker=key_marker, prefix=prefix, delimiter=delimiter, config=config) for item in response.uploads: yield item if not response.is_truncated: break if response.next_key_marker is not None: key_marker = response.next_key_marker elif len(response.uploads) != 0: key_marker = response.uploads[-1].key else: break def _upload_task(self, bucket_name, object_key, upload_id, part_number, part_size, file_name, offset, part_list, uploadTaskHandle, progress_callback=None, traffic_limit=None): if uploadTaskHandle.is_cancel(): _logger.debug("upload task canceled with partNumber={}!".format(part_number)) return try: response = self.upload_part_from_file(bucket_name, object_key, upload_id, part_number, part_size, file_name, offset, progress_callback=progress_callback , traffic_limit=traffic_limit) part_list.append({ "partNumber": part_number, "eTag": response.metadata.etag }) _logger.debug("upload task success with partNumber={}!".format(part_number)) except Exception as e: _logger.debug("upload task failed with partNumber={}!".format(part_number)) raise e #_logger.debug(e) @required(bucket_name=(bytes, str), key=(bytes, str), file_name=(bytes, str)) def put_super_obejct_from_file(self, bucket_name, key, file_name, chunk_size=5, thread_num=None, uploadTaskHandle=None, content_type=None, storage_class=None, user_headers=None, progress_callback=None, traffic_limit=None, config=None): """ Multipart Upload file to bos param chunk_size: part size , default part size is 5MB """ # check params if chunk_size > 5 * 1024 or chunk_size <= 0: raise BceClientError("chunk size is valid, it should be more than 0 and not nore than 5120!") left_size = os.path.getsize(file_name) # if file size more than 48.8TB, reject if left_size > 50000 * 1024 * 1024 * 1024: raise BceClientError("File size must not be more than 48.8TB!") if thread_num is None or thread_num <= 1: thread_num = multiprocessing.cpu_count() part_size = chunk_size * 1024 * 1024 total_part = left_size // part_size if left_size % part_size != 0: total_part += 1 if uploadTaskHandle is None: uploadTaskHandle = UploadTaskHandle() # initial upload_id = self.initiate_multipart_upload(bucket_name, key, content_type=content_type, storage_class=storage_class, user_headers=user_headers).upload_id executor = ThreadPoolExecutor(thread_num) all_tasks = [] offset = 0 part_number = 1 part_list = [] while left_size > 0: if left_size < part_size: part_size = left_size temp_task= executor.submit(self._upload_task, bucket_name, key, upload_id, part_number, part_size, file_name, offset, part_list, uploadTaskHandle, progress_callback, traffic_limit) all_tasks.append(temp_task) left_size -= part_size offset += part_size part_number += 1 # wait all upload task to exit wait(all_tasks, return_when=ALL_COMPLETED) if uploadTaskHandle.is_cancel(): _logger.debug("putting super object is canceled!") self.abort_multipart_upload(bucket_name, key, upload_id = upload_id) return False elif len(part_list) != total_part: _logger.debug("putting super object failed!") self.abort_multipart_upload(bucket_name, key, upload_id = upload_id) return False # sort part_list.sort(key=lambda x: x["partNumber"]) # complete_multipart_upload self.complete_multipart_upload(bucket_name, key, upload_id, part_list) return True @required(bucket_name=(bytes, str), key=(bytes, str), acl=(list, dict)) def set_object_acl(self, bucket_name, key, acl, config=None): """ Set Access Control Level of object :type bucket: string :param bucket: None :type acl: list of grant :param acl: None :return: **HttpResponse Class** """ key = compat.convert_to_bytes(key) self._send_request(http_methods.PUT, bucket_name, key, body=json.dumps({'accessControlList': acl}, default=BosClient._dump_acl_object), headers={http_headers.CONTENT_TYPE: http_content_types.JSON}, params={b'acl': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str)) def set_object_canned_acl(self, bucket_name, key, canned_acl=None, grant_read=None, grant_full_control=None, config=None): """ :type bucket_name: string :param bucket_name: None :type key: string :param key: None :type canned_acl: string :param canned_acl: for header 'x-bce-acl', it's value only is canned_acl.PRIVATE or canned_acl.PRIVATE_READ :type grant_read: string :param grant_read: Object id of getting READ right permission. for exapmle,grant_read = 'id="6c47...4c94",id="8c42...4c94"' :type grant_full_control: string :param grant_full_control: Object id of getting READ right permission. for exapmle,grant_full_control = 'id="6c47...4c94",id="8c42...4c94"' :param config: :return: **HttpResponse Class** """ key = compat.convert_to_bytes(key) headers = None num_args = 0 if canned_acl is not None: headers = {http_headers.BCE_ACL: compat.convert_to_bytes(canned_acl)} num_args += 1 if grant_read is not None: headers = {http_headers.BOS_GRANT_READ: compat.convert_to_bytes(grant_read)} num_args += 1 if grant_full_control is not None: headers = {http_headers.BOS_GRANT_FULL_CONTROL: compat.convert_to_bytes(grant_full_control)} num_args += 1 if num_args == 0: raise ValueError("donn't give any object canned acl arguments!") elif num_args >= 2: raise ValueError("cann't get more than one object canned acl arguments!") self._send_request(http_methods.PUT, bucket_name, key, headers=headers, params={b'acl': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str)) def get_object_acl(self, bucket_name, key, config=None): """ Get Access Control Level of object :type bucket: string :param bucket: None :type key: string :param key: None :return: **HttpResponse Class** """ key = compat.convert_to_bytes(key) return self._send_request( http_methods.GET, bucket_name, key, params={b'acl': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str)) def delete_object_acl(self, bucket_name, key, config=None): """ Get Access Control Level of object :type bucket: string :param bucket: None :type key: string :param key: None :return: **HttpResponse Class** """ key = compat.convert_to_bytes(key) return self._send_request( http_methods.DELETE, bucket_name, key, params={b'acl': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), url=(bytes, str)) def fetch_object(self, bucket_name, key, url, fetch_mode=None, storage_class=None, config=None): """ fetch object with given url and save to Baidu object storage :type bucket: string :param bucket: None :type key: string :param key: object name to be saved :type url:string :param url: url of resource to be fetched :type fetch_mode:string :param fetch_mode: fetch mode for get resource, valid value only is 'sync' and 'async' :return: **HttpResponse Class** """ key = compat.convert_to_bytes(key) headers = {} headers[http_headers.BOS_FETCH_SOURCE] = compat.convert_to_bytes(url) if fetch_mode is not None: headers[http_headers.BOS_FETCH_MODE] = fetch_mode if storage_class is not None: headers[http_headers.BOS_STORAGE_CLASS] = storage_class return self._send_request( http_methods.POST, bucket_name, key, headers=headers, params={b'fetch': b''}, config=config) @required(bucket_name=(bytes, str), target_key=(bytes, str), symlink=(bytes, str), forbid_overwrite=(bool)) def put_object_symlink(self, bucket_name, target_key, symlink, forbid_overwrite=None, user_metadata=None, storage_class=None, target_bucket=None, content_type=None, config=None): """ put object symlink :type bucket: string :param bucket: None :type key: string :type key: object name :type symlink: string :type symlink_key: symlink name :return: **HttpResponse Class** """ target_key = compat.convert_to_bytes(target_key) symlink = compat.convert_to_bytes(symlink) if content_type is None: content_type = utils.guess_content_type_by_file_name(symlink) headers = self._prepare_object_headers(user_metadata=user_metadata, content_type=content_type, storage_class=storage_class) headers[http_headers.BOS_SYMLINK_TARGET] = target_key if forbid_overwrite is not None: if forbid_overwrite: headers[http_headers.BOS_FORBID_OVERWRITE] = b'true' else: headers[http_headers.BOS_FORBID_OVERWRITE] = b'false' if target_bucket is not None: headers[http_headers.BOS_SYMLINK_BUCKET] = compat.convert_to_bytes(target_bucket) return self._send_request(http_methods.PUT, bucket_name, symlink, headers=headers, params={b'symlink': b''}, config=config) @required(bucket_name=(bytes, str), symlink=(bytes, str)) def get_object_symlink(self, bucket_name, symlink, config=None): """ Get symlink info :type bucket: string :param bucket: None :type symlink: string :param symlink: symlink :return: **HttpResponse Class** """ key = compat.convert_to_bytes(symlink) return self._send_request( http_methods.GET, bucket_name, key, params={b'symlink': b''}, config=config) @required(bucket_name=(bytes, str), key=(bytes, str), select_object_args=(dict, )) def select_object(self, bucket_name, key, select_object_args, headers=None, config=None): """ :type bucket_name: string :param bucket_name: bucket name :type key: string :param key: object name :type select_object_args: dict :param select_object_args: requesta parameters for select object api :param config: :return: """ key = compat.convert_to_bytes(key) headers = headers or {} if "inputSerialization" in select_object_args and "json" in select_object_args["inputSerialization"]: select_type = b"json" elif "inputSerialization" in select_object_args and "csv" in select_object_args["inputSerialization"]: select_type = b"csv" else: select_type = b"parquet" select_response = SelectResponse() self._send_request( http_methods.POST, bucket_name, key, body=json.dumps({'selectRequest': select_object_args}, default=BosClient._dump_acl_object), headers=headers, params={b'select': b'', b'type': select_type}, config=config, body_parser=lambda http_response, response: BosClient._parse_select_message( http_response, response, select_response) ) return select_response def get_user_quota(self, config=None): """ get user quota :param config: :return: """ return self._send_request( http_methods.GET, params={b'userQuota': b''}, config=config,) @required(max_bucket_count=(int), max_capacity_mega_bytes=(int)) def put_user_quota(self, max_bucket_count, max_capacity_mega_bytes, config=None): """ put user quota :type max_bucket_count: int :param max_bucket_count: max bucket count :type max_capacity_mega_bytes: long :param max_capacity_mega_bytes: max capacity mega bytes :param config: :return: """ return self._send_request( http_methods.PUT, body=json.dumps({'maxBucketCount': max_bucket_count, 'maxCapacityMegaBytes': max_capacity_mega_bytes}), params={b'userQuota': b''}, config=config) def delete_user_quota(self, config=None): """ delete user quota :param config: :return: """ return self._send_request( http_methods.DELETE, params={b'userQuota': b''}, config=config) @required(bucket_name=(bytes, str)) def get_notification(self, bucket_name, config=None): """ get notification :type bucket_name: string :param bucket_name: bucket name :param config: :return: """ return self._send_request( http_methods.GET, bucket_name=bucket_name, params={b'notification': b''}, config=config,) @required(bucket_name=(bytes, str), notifications=(list, )) def put_notification(self, bucket_name, notifications, config=None): """ put user quota :type bucket_name: string :param bucket_name: bucket :type notifications: list of dict :param notifications: notifacation param :param config: :return: """ return self._send_request( http_methods.PUT, bucket_name=bucket_name, body=json.dumps({'notifications': notifications}), params={b'notification': b''}, config=config,) @required(bucket_name=(bytes, str)) def delete_notification(self, bucket_name, config=None): """ delete notification :type bucket_name: string :param bucket_name: bucket name :param config: :return: """ return self._send_request( http_methods.DELETE, bucket_name=bucket_name, params={b'notification': b''}, config=config,) @required(bucket_name=(bytes, str), mirror_args=(list, )) def put_bucket_mirroring(self, bucket_name, mirror_args, config=None): """ put bucket mirroring :type bucket_name: string :param bucket_name: bucket name :param mirror_args: mirror conf :return: """ return self._send_request( http_methods.PUT, bucket_name=bucket_name, body=json.dumps({'bucketMirroringConfiguration': mirror_args}, default=BosClient._dump_acl_object), params={b'mirroring': b''}, config=config, ) @required(bucket_name=(bytes, str)) def get_bucket_mirroring(self, bucket_name, config=None): """ get bucket mirroring :type bucket_name: string :param bucket_name: bucket name :return: """ return self._send_request( http_methods.GET, bucket_name=bucket_name, params={b'mirroring': b''}, config=config, ) @required(bucket_name=(bytes, str)) def delete_bucket_mirroring(self, bucket_name, config=None): """ delete bucket mirroring :type bucket_name: string :param bucket_name: bucket name :return: """ return self._send_request( http_methods.DELETE, bucket_name=bucket_name, params={b'mirroring': b''}, config=config, ) def put_object_tagging(self, bucket_name, key, obj_tag_args, config=None): """ put object tagging :type bucket_name: string :param bucket_name: bucket name :type key: string :param key: object name :type obj_tag_args: dict :param obj_tag_args: object tagging args :return: """ return self._send_request( http_methods.PUT, bucket_name=bucket_name, key=key, body=json.dumps(obj_tag_args, default=BosClient._dump_acl_object), params={b'tagging': b''}, config=config,) def put_object_tagging_canned(self, bucket_name, key, tag_header, config=None): """ put object tagging :type bucket_name: string :param bucket_name: bucket name :type key: string :param key: object name :type obj_tag_args: dict :param obj_tag_args: object tagging args :return: """ headers = {} headers[http_headers.BOS_TAGGING] = compat.convert_to_bytes(tag_header) return self._send_request( http_methods.PUT, bucket_name=bucket_name, key=key, headers=headers, params={b'tagging': b''}, config=config,) def get_object_tagging(self, bucket_name, key, config=None): """ put object tagging :type bucket_name: string :param bucket_name: bucket name :type key: string :param key: object name :return: """ return self._send_request( http_methods.GET, bucket_name=bucket_name, key=key, params={b'tagging': b''}, config=config,) def put_bucket_versioning(self, bucket_name, status, config=None): """ put bucket versioning :type bucket_name: string :param bucket_name: bucket name :type status: string :param key: version status:disable/enabled/suspended :return: """ return self._send_request( http_methods.PUT, bucket_name=bucket_name, body=json.dumps({'status': status}, default=BosClient._dump_acl_object), params={b'versioning': b''}, config=config,) def get_bucket_versioning(self, bucket_name, config=None): """ get bucket versioning :type bucket_name: string :param bucket_name: bucket name :return: """ return self._send_request( http_methods.GET, bucket_name=bucket_name, params={b'versioning': b''}, config=config,) @required(bucket_name=(bytes, str)) def list_objects_versions(self, bucket_name, max_keys=1000, prefix=None, marker=None, version_marker=None, delimiter=None, config=None): """ Get Object Information of bucket :type bucket: string :param bucket: None :type delimiter: string :param delimiter: None :type marker: string :param marker: None :type max_keys: int :param max_keys: value <= 1000 :type prefix: string :param prefix: None :return: **_ListObjectsResponse Class** """ params = {b'versions': b''} if max_keys is not None: params[b'maxKeys'] = max_keys if prefix is not None: params[b'prefix'] = prefix if marker is not None: params[b'marker'] = marker if delimiter is not None: params[b'delimiter'] = delimiter if version_marker is not None: params[b'versionIdMarker'] = version_marker return self._send_request(http_methods.GET, bucket_name, params=params, config=config) @staticmethod def _prepare_object_headers( content_length=None, content_md5=None, content_type=None, content_sha256=None, etag=None, user_metadata=None, storage_class=None, user_headers=None, encryption=None, customer_key=None, customer_key_md5=None, traffic_limit=None, object_tagging=None,): headers = {} if content_length is not None: if content_length and content_length < 0: raise ValueError('content_length should not be negative.') headers[http_headers.CONTENT_LENGTH] = compat.convert_to_bytes(content_length) if content_md5 is not None: headers[http_headers.CONTENT_MD5] = utils.convert_to_standard_string(content_md5) if content_type is not None: headers[http_headers.CONTENT_TYPE] = utils.convert_to_standard_string(content_type) else: headers[http_headers.CONTENT_TYPE] = http_content_types.OCTET_STREAM if content_sha256 is not None: headers[http_headers.BCE_CONTENT_SHA256] = content_sha256 if etag is not None: headers[http_headers.ETAG] = b'"%s"' % utils.convert_to_standard_string(etag) if user_metadata is not None: meta_size = 0 if not isinstance(user_metadata, dict): raise TypeError('user_metadata should be of type dict.') for k, v in iteritems(user_metadata): k = utils.convert_to_standard_string(k) v = utils.convert_to_standard_string(v) normalized_key = http_headers.BCE_USER_METADATA_PREFIX + k headers[normalized_key] = v meta_size += len(normalized_key) meta_size += len(v) if meta_size > bos.MAX_USER_METADATA_SIZE: raise ValueError( 'Metadata size should not be greater than %d.' % bos.MAX_USER_METADATA_SIZE) if storage_class is not None: headers[http_headers.BOS_STORAGE_CLASS] = storage_class if encryption is not None: headers[http_headers.BOS_SERVER_SIDE_ENCRYPTION] = utils.convert_to_standard_string(encryption) if customer_key is not None: headers[http_headers.BOS_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY] = \ utils.convert_to_standard_string(customer_key) if customer_key_md5 is not None: headers[http_headers.BOS_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5] = \ utils.convert_to_standard_string(customer_key_md5) if user_headers is not None: try: headers = BosClient._get_user_header(headers, user_headers, False) except Exception as e: raise e if traffic_limit is not None: headers[http_headers.BOS_TRAFFIC_LIMIT] = traffic_limit if object_tagging is not None: headers[http_headers.BOS_TAGGING] = compat.convert_to_bytes(object_tagging) return headers @staticmethod def _get_user_header(headers, user_headers, is_copy=False): if not isinstance(user_headers, dict): raise TypeError('user_headers should be of type dict.') if not is_copy: user_headers_set = set([http_headers.CACHE_CONTROL, http_headers.CONTENT_ENCODING, http_headers.CONTENT_DISPOSITION, http_headers.EXPIRES, http_headers.BOS_PROCESS]) else: user_headers_set = set([http_headers.BCE_COPY_SOURCE_IF_NONE_MATCH, http_headers.BCE_COPY_SOURCE_IF_UNMODIFIED_SINCE, http_headers.BCE_COPY_SOURCE_IF_MODIFIED_SINCE]) for k, v in iteritems(user_headers): k = utils.convert_to_standard_string(k) v = utils.convert_to_standard_string(v) if k in user_headers_set: headers[k] = v return headers def _get_config_parameter(self, config, attr): result = None if config is not None: result = getattr(config, attr) if result is not None: return result return getattr(self.config, attr) @staticmethod def _get_path(config, bucket_name=None, key=None, use_backup_endpoint=False): host = config.endpoint if use_backup_endpoint: host = config.backup_endpoint endpoint_protocol, host_name, endpoint_port = \ utils.parse_host_port(config.endpoint, config.protocol) if config.cname_enabled or utils.is_cname_like_host(host_name) or utils.is_custom_host(host_name, bucket_name): return utils.append_uri(bos.URL_PREFIX, key) return utils.append_uri(bos.URL_PREFIX, bucket_name, key) def _merge_config(self, config, bucket_name): new_config = copy.copy(self.config) if config is not None: new_config.merge_non_none_values(config) endpoint = self._change_user_endpoint(new_config, bucket_name) new_config.endpoint = endpoint return new_config def _change_user_endpoint(self, config, bucket_name): endpoint_protocol, user_host_name, endpoint_port = \ utils.parse_host_port(config.endpoint, config.protocol) user_endpoint_split = compat.convert_to_bytes(user_host_name).split(b'.') user_endpoint = config.endpoint is_bos_path_style_host = utils.is_bos_suffixed_host(user_host_name) and len(user_endpoint_split) == 3 # 1. check ipv4 or path style if utils.check_ipv4(user_host_name): return config.endpoint if config.path_style_enable: return config.endpoint # 2. check cname domain if config.cname_enabled or utils.is_cname_like_host(user_host_name): # cname domain if is_bos_path_style_host: raise ValueError( 'endpoint is not cname domain, please set cname_enabled=False') else: return config.endpoint # default use virtual-hosted endpoint if bucket_name is not None: if is_bos_path_style_host: # split http head if user_endpoint.startswith(HTTP_PROTOCOL_HEAD): http_head_split = user_endpoint.split(b'//') if len(http_head_split) < 2: return config.endpoint bucket_endpoint = http_head_split[0] + b'//' + compat.convert_to_bytes(bucket_name) +\ b'.' + http_head_split[1] return compat.convert_to_bytes(bucket_endpoint) else: return compat.convert_to_bytes(bucket_name)+b'.'+\ compat.convert_to_bytes(user_endpoint) # check virtual-hosted endpoint's bucket_name is not query bucket_name if len(user_endpoint_split) == 4 and bucket_name is not None: if user_endpoint_split[0] != compat.convert_to_bytes(bucket_name): raise ValueError('your endpoint\'s bucket_name is not equal your query bucket_name!') return config.endpoint @staticmethod def _need_retry_backup_endpoint(error): # always retry on IOError if isinstance(error, IOError): return True # Only retry on a subset of service exceptions if isinstance(error, BceServerError): if error.status_code == http.client.INTERNAL_SERVER_ERROR: return True if error.status_code == http.client.SERVICE_UNAVAILABLE: return True if error.code == BceServerError.REQUEST_EXPIRED: return True return False def _send_request( self, http_method, bucket_name=None, key=None, body=None, headers=None, params=None, config=None, body_parser=None): config = self._merge_config(config, bucket_name) path = BosClient._get_path(config, bucket_name, key) if body_parser is None: body_parser = handler.parse_json if config.security_token is not None: headers = headers or {} headers[http_headers.STS_SECURITY_TOKEN] = config.security_token try: return bce_http_client.send_request( config, bce_v1_signer.sign, [handler.parse_error, body_parser], http_method, path, body, headers, params) except BceHttpClientError as e: # retry backup endpoint if config.backup_endpoint is None: raise e if BosClient._need_retry_backup_endpoint(e.last_error): _logger.debug(b'Retry for backup endpoint.') path = BosClient._get_path(config, bucket_name, key, True) return bce_http_client.send_request( config, bce_v1_signer.sign, [handler.parse_error, body_parser], http_method, path, body, headers, params, True) else: raise e class SelectMessage(object): """ returned message from select object api """ def set_record_message(self, headers, payload, crc): """ Initialize for record message """ self.type = "Records" self.headers = headers self.payload = payload self.crc = crc def set_cont_message(self, headers, bytes_scanned, bytes_returned, crc): """ Initialize for continue message """ self.type = "Cont" self.headers = headers self.bytes_scanned = bytes_scanned self.bytes_returned = bytes_returned self.crc = crc def set_end_message(self, headers, crc): """ Initialize for end message """ self.type = "End" self.headers = headers self.crc = crc def __str__(self): if self.type == "Records": return '{}\n{}'.format(self.headers, self.payload) elif self.type == "Cont": return '{}\nbytes_scanned/bytes_returned={}/{}'.format(self.headers, self.bytes_scanned, self.bytes_returned) else: return '{}'.format(self.headers) class SelectResponse(object): """ deal with message of select object api """ def __init__(self): self.finish = False def init_from_http_response(self, http_response, response): """ get HttpResponse and BceResponse """ self.http_response = http_response self.response = response def result(self): """ generator for SelectMessage """ f = self.http_response try: while not self.finish: prelude = f.read(8) if not prelude: raise StopIteration return total_len = struct.unpack('>I', prelude[0:4])[0] headers_len = struct.unpack('>I', prelude[4:8])[0] headers = f.read(headers_len) headers_map = self._parse_select_headers(headers) msg = SelectMessage() if headers_map['message-type'] == 'Records': payload_len = total_len - headers_len - 12 payload = f.read(payload_len) crc = struct.unpack('>I', f.read(4))[0] msg.set_record_message(headers_map, compat.convert_to_string(payload), crc) yield msg elif headers_map['message-type'] == 'Cont': bytes_scanned = f.read(8) bytes_returned = f.read(8) crc = struct.unpack('>I', f.read(4))[0] bytes_scanned = struct.unpack('>Q', bytes_scanned)[0] bytes_returned = struct.unpack('>Q', bytes_returned)[0] msg.set_cont_message(headers_map, bytes_scanned, bytes_returned, crc) yield msg elif headers_map['message-type'] == 'End': crc = struct.unpack('>I', f.read(4))[0] if headers_map["error-code"] != "success": raise BceServerError(headers_map['error-message'], code=headers_map['error-code'], request_id=self.response.metadata.bce_request_id) return msg.set_end_message(headers_map, crc) self.finish = True yield msg raise StopIteration finally: self.http_response.close() @staticmethod def _parse_select_headers(headers): """ parse SELECT headers :param headers: :return: """ hm = {} index = 0 while index < len(headers): # headers key length key_len = struct.unpack('B', headers[index: index + 1])[0] index += 1 # headers key key = headers[index: index + key_len] index += key_len # headers value length value_len = struct.unpack('>H', headers[index: index + 2])[0] index += 2 # headers value value = headers[index: index + value_len] index += value_len hm[compat.convert_to_string(key)] = compat.convert_to_string(value) return hm