Skip to content

Commit cccbdd8

Browse files
authored
Merge branch 'master' into cos-live
2 parents fbb946f + c37b7b2 commit cccbdd8

17 files changed

+2071
-60
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: ChangeLog
2+
3+
on:
4+
workflow_dispatch:
5+
release:
6+
types: [published]
7+
8+
jobs:
9+
build:
10+
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/setup-node@v2-beta
15+
with:
16+
node-version: '12'
17+
- uses: actions/checkout@v2
18+
with:
19+
fetch-depth: 0
20+
21+
- name: Checkout Tool
22+
uses: actions/checkout@v2
23+
with:
24+
repository: konakonall/auto-changelog
25+
path: 'auto-changelog'
26+
- name: Build Tool
27+
run: |
28+
cd auto-changelog
29+
npm install
30+
npm link
31+
32+
- name: Generate ChangeLog
33+
env: # Or as an environment variable
34+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
run: |
36+
auto-changelog --token $TOKEN
37+
- name: Cat ChangeLog
38+
run: cat CHANGELOG.md
39+
40+
- name: Commit files
41+
env:
42+
CI_USER: "gouki0123"
43+
CI_EMAIL: "[email protected]"
44+
run: |
45+
git config --local user.email "$CI_EMAIL"
46+
git config --local user.name "$CI_USER"
47+
git add CHANGELOG.md && git commit -m 'Updated CHANGELOG.md' && echo "push=1" >> $GITHUB_ENV || echo "No changes to CHANGELOG.md"
48+
49+
- name: Push changes
50+
if: env.push == 1
51+
env:
52+
CI_USER: "gouki0123"
53+
CI_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54+
run: |
55+
git push "https://$CI_USER:[email protected]/$GITHUB_REPOSITORY.git" HEAD:master

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ python:
44
- '2.7'
55
- '3.5'
66
- '3.6'
7+
- '3.7'
78
install:
89
- pip install requests
910
- pip install six
1011
- pip install nose
1112
- pip install pycodestyle
1213
- pip install dicttoxml
14+
- pip install crcmod
15+
- pip install pycryptodome
1316
notifications:
1417
email:
1518
recipients:

CHANGELOG.md

Lines changed: 689 additions & 0 deletions
Large diffs are not rendered by default.

qcloud_cos/.travis.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
sudo: false
2+
language: python
3+
python:
4+
- '2.7'
5+
- '3.5'
6+
- '3.6'
7+
install:
8+
- pip install requests
9+
- pip install six
10+
- pip install nose
11+
- pip install pycodestyle
12+
- pip install dicttoxml
13+
- pip install crcmod
14+
notifications:
15+
email:
16+
recipients:
17+
18+
19+
script:
20+
- pycodestyle --max-line-length=200 qcloud_cos/.
21+
- nosetests -s -v ut/
22+
deploy:
23+
provider: pypi
24+
distributions: sdist bdist_wheel
25+
user: dt3310321
26+
password:
27+
secure: JCBE84C22lHElRm7HmIf//UI123EuiFznEFPoVMPlRGb/XBrtYb+x1SRaO7Dn165CfVDpXtdNbJYfD9s2p3FUKzxSqkwl7FkkSl2g1jwKO97gKBPGxozBN+9pOJLTQUXBwON+erJSpMCHxrUjKKZBi56mUYXPP+A1X8sIHFMF4rLdPSuobjx0VGm2qFWhFeuLFPNOfF5ZKQDCnieptBLhrMXRcxyhZja/HsQh/JOjnMKZAmgJep2w2hI7ScYeTF0Ljk3RQbSN88HjZ7XP+U3bhiy5IE2u0WhJr6Q1OwxIuw8EIP+5mBNELT8Q5AMDnR85ehOVf67nl8j0nCiLzS55t1wuFHWExwW4kKF0dLpeV/fj3huFwQuAYItgZzCA/h3Fne6D3omjknd1uvWcUQzzXU1ixdeuq8XoDYxF8eox3GWQ/jbZY8lLXQ1BhaMK5E/MY8DJs1S+i6I1mJ34rCcnRYS1R3zZAJryaFxI6UsEAniXu4ESI+da7KD4y4TC0hY4RlcFyqQ0OVeeXoclQytRfgIT+EPZHt1mAr8qinmy5K2GoVcWwEj54AXp4LwiOosve6vqdeXjR/EeGy3zWjEfhn5B4z8UMLyGS/S1k3rSpV85KB4nLuzKGlyUkC2sjGr/xiG7CBC1UCbqx1CGLlCZ/HvEmKvuSrbJNbsgBNU/og=
28+
on:
29+
tags: true
30+
branch: master
31+

qcloud_cos/cos_auth.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@ def filter_headers(data):
1616
:param data(dict): 所有的头部信息.
1717
:return(dict): 计算进签名的头部.
1818
"""
19+
valid_headers = [
20+
"cache-control",
21+
"content-disposition",
22+
"content-encoding",
23+
"content-type",
24+
"expires",
25+
"content-md5",
26+
"host"
27+
]
1928
headers = {}
2029
for i in data:
21-
if i == 'Content-Type' or i == 'Host' or i[0] == 'x' or i[0] == 'X':
30+
if str.lower(i) in valid_headers or str.lower(i[0]) == "x":
2231
headers[i] = data[i]
2332
return headers
2433

qcloud_cos/cos_client.py

Lines changed: 157 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import json
1212
import xml.dom.minidom
1313
import xml.etree.ElementTree
14-
from requests import Request, Session
14+
from requests import Request, Session, ConnectionError, Timeout
1515
from datetime import datetime
1616
from six.moves.urllib.parse import quote, unquote, urlencode
17+
from six import text_type, binary_type
1718
from hashlib import md5
1819
from dicttoxml import dicttoxml
1920
from .streambody import StreamBody
@@ -26,6 +27,7 @@
2627
from .cos_exception import CosServiceError
2728
from .version import __version__
2829
from .select_event_stream import EventStream
30+
from .resumable_downloader import ResumableDownLoader
2931
logger = logging.getLogger(__name__)
3032

3133

@@ -186,7 +188,7 @@ def __init__(self, conf, retry=1, session=None):
186188
else:
187189
self._session = session
188190

189-
def get_conf():
191+
def get_conf(self):
190192
"""获取配置"""
191193
return self._conf
192194

@@ -237,7 +239,12 @@ def send_request(self, method, url, bucket, timeout=30, cos_request=True, **kwar
237239
elif bucket is not None:
238240
kwargs['headers']['Host'] = self._conf.get_host(bucket)
239241
kwargs['headers'] = format_values(kwargs['headers'])
242+
243+
file_position = None
240244
if 'data' in kwargs:
245+
body = kwargs['data']
246+
if hasattr(body, 'tell') and hasattr(body, 'seek') and hasattr(body, 'read'):
247+
file_position = body.tell() # 记录文件当前位置
241248
kwargs['data'] = to_bytes(kwargs['data'])
242249
if self._conf._ip is not None and self._conf._scheme == 'https':
243250
kwargs['verify'] = False
@@ -259,10 +266,16 @@ def send_request(self, method, url, bucket, timeout=30, cos_request=True, **kwar
259266
return res
260267
elif res.status_code < 500: # 4xx 不重试
261268
break
269+
else:
270+
if j < self._retry and client_can_retry(file_position, **kwargs):
271+
continue
272+
else:
273+
break
262274
except Exception as e: # 捕获requests抛出的如timeout等客户端错误,转化为客户端错误
263275
logger.exception('url:%s, retry_time:%d exception:%s' % (url, j, str(e)))
264-
if j < self._retry:
265-
continue
276+
if j < self._retry and (isinstance(e, ConnectionError) or isinstance(e, Timeout)): # 只重试网络错误
277+
if client_can_retry(file_position, **kwargs):
278+
continue
266279
raise CosClientError(str(e))
267280

268281
if not cos_request:
@@ -277,7 +290,7 @@ def send_request(self, method, url, bucket, timeout=30, cos_request=True, **kwar
277290
info['requestid'] = res.headers['x-cos-request-id']
278291
if 'x-cos-trace-id' in res.headers:
279292
info['traceid'] = res.headers['x-cos-trace-id']
280-
logger.error(info)
293+
logger.warn(info)
281294
raise CosServiceError(method, info, res.status_code)
282295
else:
283296
msg = res.text
@@ -1028,6 +1041,7 @@ def get_object_acl(self, Bucket, Key, **kwargs):
10281041
lst = []
10291042
lst.append(data['AccessControlList']['Grant'])
10301043
data['AccessControlList']['Grant'] = lst
1044+
data['CannedACL'] = parse_object_canned_acl(data, rt.headers)
10311045
return data
10321046

10331047
def restore_object(self, Bucket, Key, RestoreRequest={}, **kwargs):
@@ -1478,6 +1492,7 @@ def get_bucket_acl(self, Bucket, **kwargs):
14781492
lst = []
14791493
lst.append(data['AccessControlList']['Grant'])
14801494
data['AccessControlList']['Grant'] = lst
1495+
data['CannedACL'] = parse_bucket_canned_acl(data)
14811496
return data
14821497

14831498
def put_bucket_cors(self, Bucket, CORSConfiguration={}, **kwargs):
@@ -2798,6 +2813,38 @@ def get_bucket_referer(self, Bucket, **kwargs):
27982813
format_dict(data['DomainList'], ['Domain'])
27992814
return data
28002815

2816+
def delete_bucket_referer(self, Bucket, **kwargs):
2817+
"""删除bucket防盗链规则
2818+
2819+
:param Bucket(string): 存储桶名称.
2820+
:param kwargs(dict): 设置请求headers.
2821+
:return(dict): None.
2822+
2823+
.. code-block:: python
2824+
2825+
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token) # 获取配置对象
2826+
client = CosS3Client(config)
2827+
# 获取bucket标签
2828+
response = client.delete_bucket_referer(
2829+
Bucket='bucket'
2830+
)
2831+
"""
2832+
xml_config = ''
2833+
headers = mapped(kwargs)
2834+
headers['Content-MD5'] = get_md5(xml_config)
2835+
headers['Content-Type'] = 'application/xml'
2836+
params = {'referer': ''}
2837+
url = self._conf.uri(bucket=Bucket)
2838+
rt = self.send_request(
2839+
method='PUT',
2840+
url=url,
2841+
bucket=Bucket,
2842+
data=xml_config,
2843+
auth=CosS3Auth(self._conf, params=params),
2844+
headers=headers,
2845+
params=params)
2846+
return None
2847+
28012848
# service interface begin
28022849
def list_buckets(self, **kwargs):
28032850
"""列出所有bucket
@@ -2809,9 +2856,7 @@ def list_buckets(self, **kwargs):
28092856
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token) # 获取配置对象
28102857
client = CosS3Client(config)
28112858
# 获取账户下所有存储桶信息
2812-
response = logging_client.list_buckets(
2813-
Bucket='bucket'
2814-
)
2859+
response = client.list_buckets()
28152860
"""
28162861
headers = mapped(kwargs)
28172862
url = '{scheme}://service.cos.myqcloud.com/'.format(scheme=self._conf._scheme)
@@ -2945,6 +2990,30 @@ def _check_all_upload_parts(self, bucket, key, uploadid, local_path, parts_num,
29452990
already_exist_parts[part_num] = part['ETag']
29462991
return True
29472992

2993+
def download_file(self, Bucket, Key, DestFilePath, PartSize=20, MAXThread=5, EnableCRC=False, **Kwargs):
2994+
"""小于等于20MB的文件简单下载,大于20MB的文件使用续传下载
2995+
2996+
:param Bucket(string): 存储桶名称.
2997+
:param key(string): COS文件的路径名.
2998+
:param DestFilePath(string): 下载文件的目的路径.
2999+
:param PartSize(int): 分块下载的大小设置,单位为MB.
3000+
:param MAXThread(int): 并发下载的最大线程数.
3001+
:param EnableCRC(bool): 校验下载文件与源文件是否一致
3002+
:param kwargs(dict): 设置请求headers.
3003+
"""
3004+
logger.debug("Start to download file, bucket: {0}, key: {1}, dest_filename: {2}, part_size: {3}MB,\
3005+
max_thread: {4}".format(Bucket, Key, DestFilePath, PartSize, MAXThread))
3006+
3007+
object_info = self.head_object(Bucket, Key)
3008+
file_size = int(object_info['Content-Length'])
3009+
if file_size <= 1024*1024*20:
3010+
response = self.get_object(Bucket, Key, **Kwargs)
3011+
response['Body'].get_stream_to_file(DestFilePath)
3012+
return
3013+
3014+
downloader = ResumableDownLoader(self, Bucket, Key, DestFilePath, object_info, PartSize, MAXThread, EnableCRC, **Kwargs)
3015+
downloader.start()
3016+
29483017
def upload_file(self, Bucket, Key, LocalFilePath, PartSize=1, MAXThread=5, EnableMD5=False, **kwargs):
29493018
"""小于等于20MB的文件简单上传,大于20MB的文件使用分块上传
29503019
@@ -3039,7 +3108,7 @@ def _inner_head_object(self, CopySource):
30393108
params = {}
30403109
if versionid != '':
30413110
params['versionId'] = versionid
3042-
url = u"{scheme}://{bucket}.{endpoint}/{path}".format(scheme=self._conf._scheme, bucket=bucket, endpoint=endpoint, path=path)
3111+
url = u"{scheme}://{bucket}.{endpoint}/{path}".format(scheme=self._conf._scheme, bucket=bucket, endpoint=endpoint, path=quote(to_bytes(path), '/-_.~'))
30433112
rt = self.send_request(
30443113
method='HEAD',
30453114
url=url,
@@ -3403,6 +3472,85 @@ def update_object_meta(self, Bucket, Key, **kwargs):
34033472
)
34043473
return response
34053474

3475+
def put_bucket_encryption(self, Bucket, ServerSideEncryptionConfiguration={}, **kwargs):
3476+
"""设置执行存储桶下的默认加密配置
3477+
3478+
:param Bucket(string): 存储桶名称.
3479+
:param ServerSideEncryptionConfiguration(dict): 设置Bucket的加密规则
3480+
:param kwargs(dict): 设置请求的headers.
3481+
:return: None.
3482+
"""
3483+
# 类型为list的标签
3484+
lst = [
3485+
'<Rule>',
3486+
'</Rule>'
3487+
]
3488+
xml_config = format_xml(data=ServerSideEncryptionConfiguration, root='ServerSideEncryptionConfiguration', lst=lst)
3489+
headers = mapped(kwargs)
3490+
params = {'encryption': ''}
3491+
url = self._conf.uri(bucket=Bucket)
3492+
logger.info("put bucket encryption, url=:{url} ,headers=:{headers}".format(
3493+
url=url,
3494+
headers=headers))
3495+
rt = self.send_request(
3496+
method='PUT',
3497+
url=url,
3498+
bucket=Bucket,
3499+
auth=CosS3Auth(self._conf, params=params),
3500+
data=xml_config,
3501+
headers=headers,
3502+
params=params)
3503+
3504+
return None
3505+
3506+
def get_bucket_encryption(self, Bucket, **kwargs):
3507+
"""获取存储桶下的默认加密配置
3508+
3509+
:param Bucket(string): 存储桶名称.
3510+
:param kwargs(dict): 设置请求的headers.
3511+
:return(dict): 返回bucket的加密规则.
3512+
"""
3513+
headers = mapped(kwargs)
3514+
params = {'encryption': ''}
3515+
url = self._conf.uri(bucket=Bucket)
3516+
logger.info("get bucket encryption, url=:{url} ,headers=:{headers}".format(
3517+
url=url,
3518+
headers=headers))
3519+
rt = self.send_request(
3520+
method='GET',
3521+
url=url,
3522+
bucket=Bucket,
3523+
auth=CosS3Auth(self._conf, params=params),
3524+
headers=headers,
3525+
params=params)
3526+
3527+
data = xml_to_dict(rt.content)
3528+
format_dict(data, ['Rule'])
3529+
return data
3530+
3531+
def delete_bucket_encryption(self, Bucket, **kwargs):
3532+
"""用于删除指定存储桶下的默认加密配置
3533+
3534+
:param Bucket(string): 存储桶名称.
3535+
:param kwargs(dict): 设置请求的headers.
3536+
:return: None.
3537+
"""
3538+
headers = mapped(kwargs)
3539+
params = {'encryption': ''}
3540+
url = self._conf.uri(bucket=Bucket)
3541+
logger.info("delete bucket encryption, url=:{url} ,headers=:{headers}".format(
3542+
url=url,
3543+
headers=headers))
3544+
rt = self.send_request(
3545+
method='DELETE',
3546+
url=url,
3547+
bucket=Bucket,
3548+
auth=CosS3Auth(self._conf, params=params),
3549+
headers=headers,
3550+
params=params)
3551+
3552+
return None
3553+
34063554
def put_async_fetch_task(self, Bucket, FetchTaskConfiguration={}, **kwargs):
34073555
"""发起异步拉取对象到COS的任务
34083556

0 commit comments

Comments
 (0)