Skip to content

Commit ef3d9f9

Browse files
authored
Merge pull request #112 from dt3310321/s3
S3
2 parents f36c08b + 61fb5d8 commit ef3d9f9

File tree

8 files changed

+295
-35
lines changed

8 files changed

+295
-35
lines changed

qcloud_cos/cos_client.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import base64
77
import os
88
import sys
9+
import time
910
import copy
1011
import json
1112
import xml.dom.minidom
@@ -23,15 +24,15 @@
2324
from .cos_exception import CosClientError
2425
from .cos_exception import CosServiceError
2526
from .version import __version__
26-
27+
from .select_event_stream import EventStream
2728
logger = logging.getLogger(__name__)
2829

2930

3031
class CosConfig(object):
3132
"""config类,保存用户相关信息"""
3233
def __init__(self, Appid=None, Region=None, SecretId=None, SecretKey=None, Token=None, Scheme=None, Timeout=None,
3334
Access_id=None, Access_key=None, Secret_id=None, Secret_key=None, Endpoint=None, IP=None, Port=None,
34-
Anonymous=None, UA=None, Proxies=None, Domain=None, ServiceDomain=None):
35+
Anonymous=None, UA=None, Proxies=None, Domain=None, ServiceDomain=None, PoolConnections=10, PoolMaxSize=10):
3536
"""初始化,保存用户的信息
3637
3738
:param Appid(string): 用户APPID.
@@ -53,6 +54,8 @@ def __init__(self, Appid=None, Region=None, SecretId=None, SecretKey=None, Token
5354
:param Proxies(dict): 使用代理来访问COS
5455
:param Domain(string): 使用自定义的域名来访问COS
5556
:param ServiceDomain(string): 使用自定义的域名来访问cos service
57+
:param PoolConnections(int): 连接池个数
58+
:param PoolMaxSize(int): 连接池中最大连接数
5659
"""
5760
self._appid = to_unicode(Appid)
5861
self._token = to_unicode(Token)
@@ -66,6 +69,8 @@ def __init__(self, Appid=None, Region=None, SecretId=None, SecretKey=None, Token
6669
self._proxies = Proxies
6770
self._domain = Domain
6871
self._service_domain = ServiceDomain
72+
self._pool_connections = PoolConnections
73+
self._pool_maxsize = PoolMaxSize
6974

7075
if self._domain is None:
7176
self._endpoint = format_endpoint(Endpoint, Region)
@@ -175,6 +180,8 @@ def __init__(self, conf, retry=1, session=None):
175180
self._retry = retry # 重试的次数,分片上传时可适当增大
176181
if session is None:
177182
self._session = requests.session()
183+
self._session.mount('http://', requests.adapters.HTTPAdapter(pool_connections=self._conf._pool_connections, pool_maxsize=self._conf._pool_maxsize))
184+
self._session.mount('https://', requests.adapters.HTTPAdapter(pool_connections=self._conf._pool_connections, pool_maxsize=self._conf._pool_maxsize))
178185
else:
179186
self._session = session
180187

@@ -235,6 +242,8 @@ def send_request(self, method, url, bucket, timeout=30, **kwargs):
235242
kwargs['verify'] = False
236243
for j in range(self._retry + 1):
237244
try:
245+
if j != 0:
246+
time.sleep(j)
238247
if method == 'POST':
239248
res = self._session.post(url, timeout=timeout, proxies=self._conf._proxies, **kwargs)
240249
elif method == 'GET':
@@ -948,7 +957,7 @@ def restore_object(self, Bucket, Key, RestoreRequest={}, **kwargs):
948957
949958
:param Bucket(string): 存储桶名称.
950959
:param Key(string): COS路径.
951-
:param RestoreRequest: 取回object的属性设置
960+
:param RestoreRequest(dict): 取回object的属性设置
952961
:param kwargs(dict): 设置请求headers.
953962
:return: None.
954963
"""
@@ -972,6 +981,46 @@ def restore_object(self, Bucket, Key, RestoreRequest={}, **kwargs):
972981
params=params)
973982
return None
974983

984+
def select_object_content(self, Bucket, Key, Expression, ExpressionType, InputSerialization, OutputSerialization, RequestProgress=None, **kwargs):
985+
"""从指定文对象中检索内容
986+
987+
:param Bucket(string): 存储桶名称.
988+
:param Key(string): 检索的路径.
989+
:param Expression(string): 查询语句
990+
:param ExpressionType(string): 查询语句的类型
991+
:param RequestProgress(dict): 查询进度设置
992+
:param InputSerialization(dict): 输入格式设置
993+
:param OutputSerialization(dict): 输出格式设置
994+
:param kwargs(dict): 设置请求headers.
995+
:return(dict): 检索内容.
996+
"""
997+
params = {'select': '', 'select-type': 2}
998+
headers = mapped(kwargs)
999+
url = self._conf.uri(bucket=Bucket, path=Key)
1000+
logger.info("select object content, url=:{url} ,headers=:{headers}".format(
1001+
url=url,
1002+
headers=headers))
1003+
SelectRequest = {
1004+
'Expression': Expression,
1005+
'ExpressionType': ExpressionType,
1006+
'InputSerialization': InputSerialization,
1007+
'OutputSerialization': OutputSerialization
1008+
}
1009+
if RequestProgress is not None:
1010+
SelectRequest['RequestProgress'] = RequestProgress
1011+
xml_config = format_xml(data=SelectRequest, root='SelectRequest')
1012+
rt = self.send_request(
1013+
method='POST',
1014+
url=url,
1015+
stream=True,
1016+
bucket=Bucket,
1017+
data=xml_config,
1018+
auth=CosS3Auth(self._conf, Key, params=params),
1019+
headers=headers,
1020+
params=params)
1021+
data = {'Payload': EventStream(rt)}
1022+
return data
1023+
9751024
# s3 bucket interface begin
9761025
def create_bucket(self, Bucket, **kwargs):
9771026
"""创建一个bucket
@@ -2687,9 +2736,9 @@ def list_buckets(self, **kwargs):
26872736
)
26882737
"""
26892738
headers = mapped(kwargs)
2690-
url = 'https://service.cos.myqcloud.com/'
2739+
url = '{scheme}://service.cos.myqcloud.com/'.format(scheme=self._conf._scheme)
26912740
if self._conf._service_domain is not None:
2692-
url = 'https://{domain}/'.format(domain=self._conf._service_domain)
2741+
url = '{scheme}://{domain}/'.format(scheme=self._conf._scheme, domain=self._conf._service_domain)
26932742
rt = self.send_request(
26942743
method='GET',
26952744
url=url,

qcloud_cos/cos_comm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
'SSECustomerKeyMD5': 'x-cos-server-side-encryption-customer-key-MD5',
5757
'SSEKMSKeyId': 'x-cos-server-side-encryption-cos-kms-key-id',
5858
'Referer': 'Referer',
59-
'PicOperations': 'Pic-Operations'
59+
'PicOperations': 'Pic-Operations',
60+
'TrafficLimit': 'x-cos-traffic-limit',
6061
}
6162

6263

qcloud_cos/cos_exception.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
class CosException(Exception):
77
def __init__(self, message):
8-
Exception.__init__(self, message)
8+
self._message = message
9+
10+
def __str__(self):
11+
return str(self._message)
912

1013

1114
def digest_xml(data):
@@ -46,14 +49,17 @@ class CosServiceError(CosException):
4649
"""COS Server端错误,可以获取特定的错误信息"""
4750
def __init__(self, method, message, status_code):
4851
CosException.__init__(self, message)
49-
if method == 'HEAD': # 对HEAD进行特殊处理
52+
if isinstance(message, dict):
5053
self._origin_msg = ''
5154
self._digest_msg = message
5255
else:
5356
self._origin_msg = message
5457
self._digest_msg = digest_xml(message)
5558
self._status_code = status_code
5659

60+
def __str__(self):
61+
return str(self._digest_msg)
62+
5763
def get_origin_msg(self):
5864
"""获取原始的XML格式错误信息"""
5965
return self._origin_msg

qcloud_cos/select_event_stream.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding=utf-8
2+
import os
3+
import uuid
4+
import struct
5+
import logging
6+
from .cos_comm import xml_to_dict
7+
from .cos_comm import to_unicode
8+
from .cos_exception import CosServiceError
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class EventStream():
14+
def __init__(self, rt):
15+
self._rt = rt
16+
self._raw = self._rt.raw
17+
self._finish = False
18+
19+
def __iter__(self):
20+
return self
21+
22+
def __next__(self):
23+
return self.next_event()
24+
25+
next = __next__
26+
27+
def next_event(self):
28+
"""获取下一个事件"""
29+
if self._finish:
30+
"""要把剩下的内容读完丢弃或者自己关连接,否则不会自动关连接"""
31+
self._raw.read()
32+
raise StopIteration
33+
total_byte_length = struct.unpack('>I', bytes(self._raw.read(4)))[0] # message总长度
34+
header_byte_length = struct.unpack('>I', bytes(self._raw.read(4)))[0] # header总长度
35+
prelude_crc = struct.unpack('>I', bytes(self._raw.read(4)))[0]
36+
# 处理headers
37+
offset = 0
38+
msg_headers = {}
39+
while offset < header_byte_length:
40+
header_name_length = struct.unpack('>B', bytes(self._raw.read(1)))[0]
41+
header_name = to_unicode(self._raw.read(header_name_length))
42+
header_value_type = struct.unpack('>B', bytes(self._raw.read(1)))[0]
43+
header_value_length = struct.unpack('>H', bytes(self._raw.read(2)))[0]
44+
header_value = to_unicode(self._raw.read(header_value_length))
45+
msg_headers[header_name] = header_value
46+
offset += 4 + header_name_length + header_value_length
47+
# 处理payload(输出给用户的dict中也为bytes)
48+
payload_byte_length = total_byte_length - header_byte_length - 16 # payload总长度
49+
payload = self._raw.read(payload_byte_length)
50+
message_crc = struct.unpack('>I', bytes(self._raw.read(4)))[0]
51+
if ':message-type' in msg_headers and msg_headers[':message-type'] == 'event':
52+
if ':event-type' in msg_headers and msg_headers[':event-type'] == "Records":
53+
return {'Records': {'Payload': payload}}
54+
elif ':event-type' in msg_headers and msg_headers[':event-type'] == "Stats":
55+
return {'Stats': {'Details': xml_to_dict(payload)}}
56+
elif ':event-type' in msg_headers and msg_headers[':event-type'] == "Progress":
57+
return {'Progress': {'Details': xml_to_dict(payload)}}
58+
elif ':event-type' in msg_headers and msg_headers[':event-type'] == "Cont":
59+
return {'Cont': {}}
60+
elif ':event-type' in msg_headers and msg_headers[':event-type'] == "End":
61+
self._finish = True
62+
return {'End': {}}
63+
# 处理Error Message(抛出异常)
64+
if ':message-type' in msg_headers and msg_headers[':message-type'] == 'error':
65+
error_info = dict()
66+
error_info['code'] = msg_headers[':error-code']
67+
error_info['message'] = msg_headers[':error-message']
68+
error_info['resource'] = self._rt.request.url
69+
error_info['requestid'] = ''
70+
error_info['traceid'] = ''
71+
if 'x-cos-request-id' in self._rt.headers:
72+
error_info['requestid'] = self._rt.headers['x-cos-request-id']
73+
if 'x-cos-trace-id' in self._rt.headers:
74+
error_info['traceid'] = self._rt.headers['x-cos-trace-id']
75+
logger.error(error_info)
76+
e = CosServiceError('POST', error_info, self._rt.status_code)
77+
raise e
78+
79+
def get_select_result(self):
80+
"""获取查询结果"""
81+
data = b""
82+
for event in self:
83+
if 'Records' in event:
84+
data += event['Records']['Payload']
85+
return data
86+
87+
def get_select_result_to_file(self, file_name):
88+
"""保存查询结果到文件"""
89+
tmp_file_name = "{file_name}_{uuid}".format(file_name=file_name, uuid=uuid.uuid4().hex)
90+
with open(tmp_file_name, 'wb') as fp:
91+
for event in self:
92+
if 'Records' in event:
93+
data = event['Records']['Payload']
94+
fp.write(data)
95+
if os.path.exists(file_name):
96+
os.remove(file_name)
97+
os.rename(tmp_file_name, file_name)

qcloud_cos/streambody.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@ class StreamBody():
77
def __init__(self, rt):
88
self._rt = rt
99

10+
def __iter__(self):
11+
"""提供一个默认的迭代器"""
12+
return self._rt.iter_content(1024)
13+
1014
def get_raw_stream(self):
15+
"""提供原始流"""
1116
return self._rt.raw
1217

1318
def get_stream(self, chunk_size=1024):
19+
"""提供一个chunk可变的迭代器"""
1420
return self._rt.iter_content(chunk_size=chunk_size)
1521

1622
def get_stream_to_file(self, file_name, auto_decompress=False):
23+
"""保存流到本地文件"""
1724
use_chunked = False
1825
if 'Content-Length' in self._rt.headers:
1926
content_len = int(self._rt.headers['Content-Length'])

qcloud_cos/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11

2-
__version__ = '5.1.7.6'
2+
__version__ = '5.1.7.8'

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def long_description():
1616

1717
setup(
1818
name='cos-python-sdk-v5',
19-
version='1.7.7',
19+
version='1.7.8',
2020
url='https://www.qcloud.com/',
2121
license='MIT',
2222
author='tiedu, lewzylu, channingliu',

0 commit comments

Comments
 (0)