├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── CONTRIBUTING ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── qcloud_cos ├── __init__.py ├── cos_auth.py ├── cos_client.py ├── cos_common.py ├── cos_config.py ├── cos_cred.py ├── cos_err.py ├── cos_op.py ├── cos_params_check.py ├── cos_request.py └── threadpool.py ├── sample.py ├── setup.py └── test ├── __init__.py └── test_simple.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .idea 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.6' 4 | - '2.7' 5 | install: 6 | - python setup.py install 7 | - pip freeze 8 | - pip install pep8 9 | script: 10 | - pep8 qcloud_cos --max-line-length=140 11 | - nosetests 12 | deploy: 13 | provider: pypi 14 | distributions: "sdist bdist bdist_wheel" 15 | user: liuchang0812 16 | password: 17 | secure: B+yhh1dUtQzNz5Yozp9NVORoUybyq0v0wrpkOURIgbv1H9lG+kLmkTD+Gtc3cfhAIzq9VXJZtcSuh2uKuIIP+6Lo/UKMA91eCJR345WGW7HZsz/erI/y7Zh0ART6DL2ZAC1k+XstrmRTgjdD7kq7JwD2CtJRCJl74WjS5YhzDXgRWNLmw22HL4eK0kPubN+2VdoJNCQY2Rd262TZ/YkO3K+Vd3l6EttWr7uKCafl68AkqUYs0nujTrCdUVezIxIq5qxI5FJYhB6yBl36oI1yCaKMeHlmZ8jw8rt15wqMV3es4ggrQKhvVBkoatAY2HnZcHgutitp3PbVmP/5uRabBA/9kTASPYW0SEi1G8itfk7mNqLAC4lFuw2bq03tbOPnA/j6q1B46HXAk5adgyG+k46y7xRJIcXTo29h1F01hRekT+J431SPR3/qRPiONQ3NpkD5vfBSVvuz0q1Kt++HgE4q/6ej3am0/1a7kVL87WmmH+qzmDPPNOo0BiTxJZ/X2bgmid5yF4EqNuoAde6StKnkqYmvZBsd1noE6EwcNhU1S1Wr/ecTtscfHgN0b/eaLIep9OL6ZzffuUjvZwIpzf7xIhuYluNg2XtaunfVEeVRqU+SKWQ9AD+3ZZqpr04Cbbtu+j7SNQw8y9hkJQxdib+K/xK/3AHpZ1YWL+WTwtA= 18 | on: 19 | tags: true 20 | branch: master 21 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES 2 | +++++++++ 3 | * 0.0.20 fix slice_size 4 | * 0.0.14 avoid incomplete downloading 5 | * 0.0.13 add more region information 6 | * 0.0.12 fix bug: close context in download 7 | * 0.0.11 fix bug: check http status code in download 8 | * 0.0.10 fix bug: download signature expired 9 | * 0.0.9 add new region: spg 10 | * 0.0.8 fix bug 11 | * 0.0.7 update documents 12 | * 0.0.6 add tj region, enable_https changes to False 13 | * 0.0.5 Optimization for uploadding file 14 | * 0.0.4 Add traceback information, Add default logging handler 15 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | Thanks for your contribution, please make sure of following: 4 | 5 | 1. Your code should follows PEP8 style. 6 | 1. Your code should passes travis CI. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Qcloud COSv4 SDK 2 | ####################### 3 | 4 | 5 | .. image:: https://travis-ci.org/tencentyun/cos-python-sdk-v4.svg?branch=master 6 | :target: https://travis-ci.org/tencentyun/cos-python-sdk-v4 7 | 8 | .. image:: https://badge.fury.io/py/qcloud_cos_v4.png 9 | :target: https://badge.fury.io/py/qcloud_cos_v4 10 | 11 | .. image:: https://readthedocs.org/projects/cossdkv4/badge/?version=latest 12 | :target: http://cossdkv4.readthedocs.io/en/latest/?badge=latest 13 | 14 | 已弃用 - 请升级到 cos-python-sdk-v5 15 | _______ 16 | 17 | SDK 依赖的 JSON API 已弃用,请直接使用基于 XML API 的 `cos-python-sdk-v5 `_,或者参照 `升级指引 `_ 升级到新版SDK。 18 | 19 | 介绍 20 | _______ 21 | 22 | `腾讯云COSv4 `_ 的Python SDK, 目前可以支持Python2.6与Python2.7。 23 | 24 | 安装指南 25 | __________ 26 | 27 | 使用pip安装 :: 28 | 29 | pip install -U qcloud_cos_v4 30 | 31 | 32 | 手动安装:: 33 | 34 | python setup.py install 35 | 36 | 使用方法 37 | __________ 38 | 39 | 使用python sdk,参照sample.py 40 | 41 | .. code:: python 42 | 43 | # 设置用户属性, 包括appid, secret_id和secret_key 44 | # 这些属性可以在cos控制台获取(https://console.qcloud.com/cos) 45 | appid = 100000 # 替换为用户的appid 46 | secret_id = u'xxxxxxxx' # 替换为用户的secret_id 47 | secret_key = u'xxxxxxx' # 替换为用户的secret_key 48 |   region_info = "sh" #           # 替换为用户的region,目前可以为 sh/gz/tj/sgp,分别对应于上海,广州,天津,新加坡园区 49 | cos_client = CosClient(appid, secret_id, secret_key, region=region_info) 50 | 51 | # 设置要操作的bucket 52 | bucket = u'mybucket' 53 | 54 | ############################################################################ 55 | # 文件操作 # 56 | ############################################################################ 57 | # 1. 上传文件(默认不覆盖) 58 | # 将本地的local_file_1.txt上传到bucket的根分区下,并命名为sample_file.txt 59 | # 默认不覆盖, 如果cos上文件存在,则会返回错误 60 | request = UploadFileRequest(bucket, u'/sample_file.txt', u'local_file_1.txt') 61 | upload_file_ret = cos_client.upload_file(request) 62 | print 'upload file ret:', repr(upload_file_ret) 63 | 64 | # 2. 上传文件(覆盖文件) 65 | # 2.1上传本地文件,将本地的local_file_2.txt上传到bucket的根分区下,覆盖已上传的sample_file.txt 66 | request = UploadFileRequest(bucket, u'/sample_file.txt', u'local_file_2.txt') 67 | request.set_insert_only(0) # 设置允许覆盖 68 | upload_file_ret = cos_client.upload_file(request) 69 | print 'overwrite file ret:', repr(upload_file_ret) 70 | # 2.2从内存上传文件 71 | request = UploadFileFromBufferRequest(bucket, u'/sample_file.txt', data) 72 | request.set_insert_only(0) # 设置允许覆盖 73 | upload_file_ret = cos_client.upload_file_from_buffer(request) 74 | print 'overwrite file ret:', repr(upload_file_ret) 75 | 76 | # 3. 获取文件属性 77 | request = StatFileRequest(bucket, u'/sample_file.txt') 78 | stat_file_ret = cos_client.stat_file(request) 79 | print 'stat file ret:', repr(stat_file_ret) 80 | 81 | # 4. 更新文件属性 82 | request = UpdateFileRequest(bucket, u'/sample_file.txt') 83 | 84 | request.set_biz_attr(u'this is demo file') # 设置文件biz_attr属性,不支持中文 85 | request.set_authority(u'eWRPrivate') # 设置文件的权限 86 | request.set_cache_control(u'cache_xxx') # 设置Cache-Control 87 | request.set_content_type(u'application/text') # 设置Content-Type 88 | request.set_content_disposition(u'ccccxxx.txt') # 设置Content-Disposition 89 | request.set_content_language(u'english') # 设置Content-Language 90 | request.set_x_cos_meta(u'x-cos-meta-xxx', u'xxx') # 设置自定义的x-cos-meta-属性 91 | request.set_x_cos_meta(u'x-cos-meta-yyy', u'yyy') # 设置自定义的x-cos-meta-属性 92 | 93 | update_file_ret = cos_client.update_file(request) 94 | print 'update file ret:', repr(update_file_ret) 95 | 96 | # 5. 更新后再次获取文件属性 97 | request = StatFileRequest(bucket, u'/sample_file.txt') 98 | stat_file_ret = cos_client.stat_file(request) 99 | print 'stat file ret:', repr(stat_file_ret) 100 | 101 | # 6. 移动文件, 将sample_file.txt移动位sample_file_move.txt 102 | request = MoveFileRequest(bucket, u'/sample_file.txt', u'/sample_file_move.txt') 103 | stat_file_ret = cos_client.move_file(request) 104 | print 'move file ret:', repr(stat_file_ret) 105 | 106 | # 7. 删除文件 107 | request = DelFileRequest(bucket, u'/sample_file_move.txt') 108 | del_ret = cos_client.del_file(request) 109 | print 'del file ret:', repr(del_ret) 110 | 111 | # 8. 下载文件 112 | request = DownloadFileRequest(bucket, u'/sample_file_move.txt') 113 | del_ret = cos_client.download_file(request) 114 | print 'del file ret:', repr(del_ret) 115 | 116 | # 9. 下载文件到内存 117 | request = DownloadObjectRequest(bucket, u'/sample_file_move.txt') 118 | fp = cos_client.download_object(request) 119 | fp.read() 120 | 121 | ############################################################################ 122 | # 目录操作 # 123 | ############################################################################ 124 | # 1. 生成目录, 目录名为sample_folder 125 | request = CreateFolderRequest(bucket, u'/sample_folder/') 126 | create_folder_ret = cos_client.create_folder(request) 127 | print 'create folder ret:', create_folder_ret 128 | 129 | # 2. 更新目录的biz_attr属性 130 | request = UpdateFolderRequest(bucket, u'/sample_folder/', u'this is a test folder') # biz_attr不支持中文 131 | update_folder_ret = cos_client.update_folder(request) 132 | print 'update folder ret:', repr(update_folder_ret) 133 | 134 | # 3. 获取目录属性 135 | request = StatFolderRequest(bucket, u'/sample_folder/') 136 | stat_folder_ret = cos_client.stat_folder(request) 137 | print 'stat folder ret:', repr(stat_folder_ret) 138 | 139 | # 4. list目录, 获取目录下的成员 140 | request = ListFolderRequest(bucket, u'/sample_folder/') 141 | list_folder_ret = cos_client.list_folder(request) 142 | print 'list folder ret:', repr(list_folder_ret) 143 | 144 | # 5. 删除目录 145 | request = DelFolderRequest(bucket, u'/sample_folder/') 146 | delete_folder_ret = cos_client.del_folder(request) 147 | print 'delete folder ret:', repr(delete_folder_ret) 148 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/CosPythonSDKV4.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/CosPythonSDKV4.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/CosPythonSDKV4" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/CosPythonSDKV4" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Cos Python SDK V4 documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Sep 23 19:05:20 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | # 47 | # source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'Cos Python SDK V4' 54 | copyright = u'2016, liuchang' 55 | author = u'liuchang' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = u'0.0.1' 63 | # The full version, including alpha/beta/rc tags. 64 | release = u'0.0.1' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | # 76 | # today = '' 77 | # 78 | # Else, today_fmt is used as the format for a strftime call. 79 | # 80 | # today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | # This patterns also effect to html_static_path and html_extra_path 85 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | # 90 | # default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | # 94 | # add_function_parentheses = True 95 | 96 | # If true, the current module name will be prepended to all description 97 | # unit titles (such as .. function::). 98 | # 99 | # add_module_names = True 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | # 104 | # show_authors = False 105 | 106 | # The name of the Pygments (syntax highlighting) style to use. 107 | pygments_style = 'sphinx' 108 | 109 | # A list of ignored prefixes for module index sorting. 110 | # modindex_common_prefix = [] 111 | 112 | # If true, keep warnings as "system message" paragraphs in the built documents. 113 | # keep_warnings = False 114 | 115 | # If true, `todo` and `todoList` produce output, else they produce nothing. 116 | todo_include_todos = False 117 | 118 | 119 | # -- Options for HTML output ---------------------------------------------- 120 | 121 | # The theme to use for HTML and HTML Help pages. See the documentation for 122 | # a list of builtin themes. 123 | # 124 | html_theme = 'alabaster' 125 | 126 | # Theme options are theme-specific and customize the look and feel of a theme 127 | # further. For a list of options available for each theme, see the 128 | # documentation. 129 | # 130 | # html_theme_options = {} 131 | 132 | # Add any paths that contain custom themes here, relative to this directory. 133 | # html_theme_path = [] 134 | 135 | # The name for this set of Sphinx documents. 136 | # " v documentation" by default. 137 | # 138 | # html_title = u'Cos Python SDK V4 v0.0.1' 139 | 140 | # A shorter title for the navigation bar. Default is the same as html_title. 141 | # 142 | # html_short_title = None 143 | 144 | # The name of an image file (relative to this directory) to place at the top 145 | # of the sidebar. 146 | # 147 | # html_logo = None 148 | 149 | # The name of an image file (relative to this directory) to use as a favicon of 150 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 151 | # pixels large. 152 | # 153 | # html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = ['_static'] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | # 164 | # html_extra_path = [] 165 | 166 | # If not None, a 'Last updated on:' timestamp is inserted at every page 167 | # bottom, using the given strftime format. 168 | # The empty string is equivalent to '%b %d, %Y'. 169 | # 170 | # html_last_updated_fmt = None 171 | 172 | # If true, SmartyPants will be used to convert quotes and dashes to 173 | # typographically correct entities. 174 | # 175 | # html_use_smartypants = True 176 | 177 | # Custom sidebar templates, maps document names to template names. 178 | # 179 | # html_sidebars = {} 180 | 181 | # Additional templates that should be rendered to pages, maps page names to 182 | # template names. 183 | # 184 | # html_additional_pages = {} 185 | 186 | # If false, no module index is generated. 187 | # 188 | # html_domain_indices = True 189 | 190 | # If false, no index is generated. 191 | # 192 | # html_use_index = True 193 | 194 | # If true, the index is split into individual pages for each letter. 195 | # 196 | # html_split_index = False 197 | 198 | # If true, links to the reST sources are added to the pages. 199 | # 200 | # html_show_sourcelink = True 201 | 202 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 203 | # 204 | # html_show_sphinx = True 205 | 206 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 207 | # 208 | # html_show_copyright = True 209 | 210 | # If true, an OpenSearch description file will be output, and all pages will 211 | # contain a tag referring to it. The value of this option must be the 212 | # base URL from which the finished HTML is served. 213 | # 214 | # html_use_opensearch = '' 215 | 216 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 217 | # html_file_suffix = None 218 | 219 | # Language to be used for generating the HTML full-text search index. 220 | # Sphinx supports the following languages: 221 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 222 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 223 | # 224 | # html_search_language = 'en' 225 | 226 | # A dictionary with options for the search language support, empty by default. 227 | # 'ja' uses this config value. 228 | # 'zh' user can custom change `jieba` dictionary path. 229 | # 230 | # html_search_options = {'type': 'default'} 231 | 232 | # The name of a javascript file (relative to the configuration directory) that 233 | # implements a search results scorer. If empty, the default will be used. 234 | # 235 | # html_search_scorer = 'scorer.js' 236 | 237 | # Output file base name for HTML help builder. 238 | htmlhelp_basename = 'CosPythonSDKV4doc' 239 | 240 | # -- Options for LaTeX output --------------------------------------------- 241 | 242 | latex_elements = { 243 | # The paper size ('letterpaper' or 'a4paper'). 244 | # 245 | # 'papersize': 'letterpaper', 246 | 247 | # The font size ('10pt', '11pt' or '12pt'). 248 | # 249 | # 'pointsize': '10pt', 250 | 251 | # Additional stuff for the LaTeX preamble. 252 | # 253 | # 'preamble': '', 254 | 255 | # Latex figure (float) alignment 256 | # 257 | # 'figure_align': 'htbp', 258 | } 259 | 260 | # Grouping the document tree into LaTeX files. List of tuples 261 | # (source start file, target name, title, 262 | # author, documentclass [howto, manual, or own class]). 263 | latex_documents = [ 264 | (master_doc, 'CosPythonSDKV4.tex', u'Cos Python SDK V4 Documentation', 265 | u'liuchang', 'manual'), 266 | ] 267 | 268 | # The name of an image file (relative to this directory) to place at the top of 269 | # the title page. 270 | # 271 | # latex_logo = None 272 | 273 | # For "manual" documents, if this is true, then toplevel headings are parts, 274 | # not chapters. 275 | # 276 | # latex_use_parts = False 277 | 278 | # If true, show page references after internal links. 279 | # 280 | # latex_show_pagerefs = False 281 | 282 | # If true, show URL addresses after external links. 283 | # 284 | # latex_show_urls = False 285 | 286 | # Documents to append as an appendix to all manuals. 287 | # 288 | # latex_appendices = [] 289 | 290 | # It false, will not define \strong, \code, itleref, \crossref ... but only 291 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 292 | # packages. 293 | # 294 | # latex_keep_old_macro_names = True 295 | 296 | # If false, no module index is generated. 297 | # 298 | # latex_domain_indices = True 299 | 300 | 301 | # -- Options for manual page output --------------------------------------- 302 | 303 | # One entry per manual page. List of tuples 304 | # (source start file, name, description, authors, manual section). 305 | man_pages = [ 306 | (master_doc, 'cospythonsdkv4', u'Cos Python SDK V4 Documentation', 307 | [author], 1) 308 | ] 309 | 310 | # If true, show URL addresses after external links. 311 | # 312 | # man_show_urls = False 313 | 314 | 315 | # -- Options for Texinfo output ------------------------------------------- 316 | 317 | # Grouping the document tree into Texinfo files. List of tuples 318 | # (source start file, target name, title, author, 319 | # dir menu entry, description, category) 320 | texinfo_documents = [ 321 | (master_doc, 'CosPythonSDKV4', u'Cos Python SDK V4 Documentation', 322 | author, 'CosPythonSDKV4', 'One line description of project.', 323 | 'Miscellaneous'), 324 | ] 325 | 326 | # Documents to append as an appendix to all manuals. 327 | # 328 | # texinfo_appendices = [] 329 | 330 | # If false, no module index is generated. 331 | # 332 | # texinfo_domain_indices = True 333 | 334 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 335 | # 336 | # texinfo_show_urls = 'footnote' 337 | 338 | # If true, do not generate a @detailmenu in the "Top" node's menu. 339 | # 340 | # texinfo_no_detailmenu = False 341 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Cos Python SDK V4 documentation master file, created by 2 | sphinx-quickstart on Fri Sep 23 19:05:20 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Cos Python SDK V4's documentation! 7 | ============================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\CosPythonSDKV4.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\CosPythonSDKV4.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /qcloud_cos/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from .cos_client import CosClient 4 | from .cos_client import CosConfig 5 | from .cos_client import CredInfo 6 | from .cos_request import UploadFileRequest 7 | from .cos_request import UploadSliceFileRequest 8 | from .cos_request import UploadFileFromBufferRequest 9 | from .cos_request import UploadSliceFileFromBufferRequest 10 | from .cos_request import UpdateFileRequest 11 | from .cos_request import UpdateFolderRequest 12 | from .cos_request import DelFolderRequest 13 | from .cos_request import DelFileRequest 14 | from .cos_request import CreateFolderRequest 15 | from .cos_request import StatFileRequest 16 | from .cos_request import StatFolderRequest 17 | from .cos_request import ListFolderRequest 18 | from .cos_request import DownloadFileRequest 19 | from .cos_request import DownloadObjectRequest 20 | from .cos_request import MoveFileRequest 21 | from .cos_auth import Auth 22 | from .cos_cred import CredInfo 23 | 24 | 25 | import logging 26 | 27 | try: 28 | from logging import NullHandler 29 | except ImportError: 30 | class NullHandler(logging.Handler): 31 | def emit(self, record): 32 | pass 33 | 34 | logging.getLogger(__name__).addHandler(NullHandler()) 35 | -------------------------------------------------------------------------------- /qcloud_cos/cos_auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import random 5 | import time 6 | import urllib 7 | import hmac 8 | import hashlib 9 | import binascii 10 | import base64 11 | 12 | 13 | class Auth(object): 14 | def __init__(self, cred): 15 | self.cred = cred 16 | 17 | def app_sign(self, bucket, cos_path, expired, upload_sign=True): 18 | appid = self.cred.get_appid() 19 | bucket = bucket.encode('utf8') 20 | secret_id = self.cred.get_secret_id().encode('utf8') 21 | now = int(time.time()) 22 | rdm = random.randint(0, 999999999) 23 | cos_path = urllib.quote(cos_path.encode('utf8'), '~/') 24 | if upload_sign: 25 | fileid = '/%s/%s%s' % (appid, bucket, cos_path) 26 | else: 27 | fileid = cos_path 28 | 29 | if expired != 0 and expired < now: 30 | expired = now + expired 31 | 32 | sign_tuple = (appid, secret_id, expired, now, rdm, fileid, bucket) 33 | 34 | plain_text = 'a=%s&k=%s&e=%d&t=%d&r=%d&f=%s&b=%s' % sign_tuple 35 | secret_key = self.cred.get_secret_key().encode('utf8') 36 | sha1_hmac = hmac.new(secret_key, plain_text, hashlib.sha1) 37 | hmac_digest = sha1_hmac.hexdigest() 38 | hmac_digest = binascii.unhexlify(hmac_digest) 39 | sign_hex = hmac_digest + plain_text 40 | sign_base64 = base64.b64encode(sign_hex) 41 | return sign_base64 42 | 43 | def sign_once(self, bucket, cos_path): 44 | """单次签名(针对删除和更新操作) 45 | 46 | :param bucket: bucket名称 47 | :param cos_path: 要操作的cos路径, 以'/'开始 48 | :return: 签名字符串 49 | """ 50 | return self.app_sign(bucket, cos_path, 0) 51 | 52 | def sign_more(self, bucket, cos_path, expired): 53 | """多次签名(针对上传文件,创建目录, 获取文件目录属性, 拉取目录列表) 54 | 55 | :param bucket: bucket名称 56 | :param cos_path: 要操作的cos路径, 以'/'开始 57 | :param expired: 签名过期时间, UNIX时间戳, 如想让签名在30秒后过期, 即可将expired设成当前时间加上30秒 58 | :return: 签名字符串 59 | """ 60 | return self.app_sign(bucket, cos_path, expired) 61 | 62 | def sign_download(self, bucket, cos_path, expired): 63 | """下载签名(用于获取后拼接成下载链接,下载私有bucket的文件) 64 | 65 | :param bucket: bucket名称 66 | :param cos_path: 要下载的cos文件路径, 以'/'开始 67 | :param expired: 签名过期时间, UNIX时间戳, 如想让签名在30秒后过期, 即可将expired设成当前时间加上30秒 68 | :return: 签名字符串 69 | """ 70 | return self.app_sign(bucket, cos_path, expired, False) 71 | -------------------------------------------------------------------------------- /qcloud_cos/cos_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import requests 5 | from cos_cred import CredInfo 6 | from cos_config import CosConfig 7 | from cos_op import FileOp 8 | from cos_op import FolderOp 9 | from cos_request import UploadFileRequest 10 | from cos_request import UploadSliceFileRequest 11 | from cos_request import UploadFileFromBufferRequest 12 | from cos_request import UploadSliceFileFromBufferRequest 13 | from cos_request import UpdateFileRequest 14 | from cos_request import UpdateFolderRequest 15 | from cos_request import DelFileRequest 16 | from cos_request import DelFolderRequest 17 | from cos_request import CreateFolderRequest 18 | from cos_request import StatFolderRequest 19 | from cos_request import StatFileRequest 20 | from cos_request import ListFolderRequest 21 | from cos_request import DownloadFileRequest 22 | from cos_request import DownloadObjectRequest 23 | try: 24 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 25 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 26 | except ImportError: 27 | pass 28 | 29 | 30 | class CosClient(object): 31 | """Cos客户端类""" 32 | 33 | def __init__(self, appid, secret_id, secret_key, region="shanghai"): 34 | """ 设置用户的相关信息 35 | 36 | :param appid: appid 37 | :param secret_id: secret_id 38 | :param secret_key: secret_key 39 | """ 40 | self._cred = CredInfo(appid, secret_id, secret_key) 41 | self._config = CosConfig(region=region) 42 | self._http_session = requests.session() 43 | self._file_op = FileOp(self._cred, self._config, self._http_session) 44 | self._folder_op = FolderOp(self._cred, self._config, self._http_session) 45 | 46 | def set_config(self, config): 47 | """设置config""" 48 | assert isinstance(config, CosConfig) 49 | self._config = config 50 | self._file_op.set_config(config) 51 | self._folder_op.set_config(config) 52 | 53 | def get_config(self): 54 | """获取config""" 55 | return self._config 56 | 57 | def set_cred(self, cred): 58 | """设置用户的身份信息 59 | 60 | :param cred: 61 | :return: 62 | """ 63 | assert isinstance(cred, CredInfo) 64 | self._cred = cred 65 | self._file_op.set_cred(cred) 66 | self._folder_op.set_cred(cred) 67 | 68 | def get_cred(self): 69 | """获取用户的相关信息 70 | 71 | :return: 72 | """ 73 | return self._cred 74 | 75 | def upload_file(self, request): 76 | """ 上传文件(自动根据文件大小,选择上传策略, 强烈推荐使用),上传策略: 8MB以下适用单文件上传, 8MB(含)适用分片上传 77 | 78 | :param request: 79 | :return: 80 | """ 81 | assert isinstance(request, UploadFileRequest) 82 | return self._file_op.upload_file(request) 83 | 84 | def upload_single_file(self, request): 85 | """单文件上传接口, 适用用小文件8MB以下, 最大不得超过20MB, 否则会返回参数错误 86 | 87 | :param request: 88 | :return: 89 | """ 90 | assert isinstance(request, UploadFileRequest) 91 | return self._file_op.upload_single_file(request) 92 | 93 | def upload_slice_file(self, request): 94 | """ 分片上传接口, 适用于大文件8MB及以上 95 | 96 | :param request: 97 | :return: 98 | """ 99 | assert isinstance(request, UploadSliceFileRequest) 100 | return self._file_op.upload_slice_file(request) 101 | 102 | def upload_file_from_buffer(self, request): 103 | """ 从内存上传文件(自动根据文件大小,选择上传策略, 强烈推荐使用),上传策略: 8MB以下适用单文件上传, 8MB(含)适用分片上传 104 | 105 | :param request: 106 | :return: 107 | """ 108 | assert isinstance(request, UploadFileFromBufferRequest) 109 | return self._file_op.upload_file_from_buffer(request) 110 | 111 | def upload_single_file_from_buffer(self, request): 112 | """从内存单文件上传接口, 适用用小文件8MB以下, 最大不得超过20MB, 否则会返回参数错误 113 | 114 | :param request: 115 | :return: 116 | """ 117 | assert isinstance(request, UploadFileFromBufferRequest) 118 | return self._file_op.upload_single_file_from_buffer(request) 119 | 120 | def upload_slice_file_from_buffer(self, request): 121 | """ 从内存分片上传接口, 适用于大文件8MB及以上 122 | 123 | :param request: 124 | :return: 125 | """ 126 | assert isinstance(request, UploadSliceFileFromBufferRequest) 127 | return self._file_op.upload_slice_file_from_buffer(request) 128 | 129 | def del_file(self, request): 130 | """ 删除文件 131 | 132 | :param request: 133 | :return: 134 | """ 135 | assert isinstance(request, DelFileRequest) 136 | return self._file_op.del_file(request) 137 | 138 | def move_file(self, request): 139 | return self._file_op.move_file(request) 140 | 141 | def stat_file(self, request): 142 | """获取文件属性 143 | 144 | :param request: 145 | :return: 146 | """ 147 | assert isinstance(request, StatFileRequest) 148 | return self._file_op.stat_file(request) 149 | 150 | def update_file(self, request): 151 | """更新文件属性 152 | 153 | :param request: 154 | :return: 155 | """ 156 | assert isinstance(request, UpdateFileRequest) 157 | return self._file_op.update_file(request) 158 | 159 | def download_file(self, request): 160 | assert isinstance(request, DownloadFileRequest) 161 | return self._file_op.download_file(request) 162 | 163 | def download_object(self, request): 164 | assert isinstance(request, DownloadObjectRequest) 165 | return self._file_op.download_object(request) 166 | 167 | def create_folder(self, request): 168 | """创建目录 169 | 170 | :param request: 171 | :return: 172 | """ 173 | assert isinstance(request, CreateFolderRequest) 174 | return self._folder_op.create_folder(request) 175 | 176 | def del_folder(self, request): 177 | """删除目录 178 | 179 | :param request: 180 | :return: 181 | """ 182 | assert isinstance(request, DelFolderRequest) 183 | return self._folder_op.del_folder(request) 184 | 185 | def stat_folder(self, request): 186 | """获取folder属性请求 187 | 188 | :param request: 189 | :return: 190 | """ 191 | assert isinstance(request, StatFolderRequest) 192 | return self._folder_op.stat_folder(request) 193 | 194 | def update_folder(self, request): 195 | """更新目录属性 196 | 197 | :param request: 198 | :return: 199 | """ 200 | assert isinstance(request, UpdateFolderRequest) 201 | return self._folder_op.update_folder(request) 202 | 203 | def list_folder(self, request): 204 | """获取目录下的文件和目录列表 205 | 206 | :param request: 207 | :return: 208 | """ 209 | assert isinstance(request, ListFolderRequest) 210 | return self._folder_op.list_folder(request) 211 | -------------------------------------------------------------------------------- /qcloud_cos/cos_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | import struct 5 | import io 6 | 7 | try: 8 | range = xrange 9 | except NameError: 10 | pass 11 | 12 | 13 | def _left_rotate(n, b): 14 | """Left rotate a 32-bit integer n by b bits.""" 15 | return ((n << b) | (n >> (32 - b))) & 0xffffffff 16 | 17 | 18 | def _process_chunk(chunk, h0, h1, h2, h3, h4): 19 | """Process a chunk of data and return the new digest variables.""" 20 | assert len(chunk) == 64 21 | 22 | w = [0] * 80 23 | 24 | # Break chunk into sixteen 4-byte big-endian words w[i] 25 | for i in range(16): 26 | w[i] = struct.unpack(b'>I', chunk[i * 4:i * 4 + 4])[0] 27 | 28 | # Extend the sixteen 4-byte words into eighty 4-byte words 29 | for i in range(16, 80): 30 | w[i] = _left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) 31 | 32 | # Initialize hash value for this chunk 33 | a = h0 34 | b = h1 35 | c = h2 36 | d = h3 37 | e = h4 38 | 39 | for i in range(80): 40 | if 0 <= i <= 19: 41 | # Use alternative 1 for f from FIPS PB 180-1 to avoid bitwise not 42 | f = d ^ (b & (c ^ d)) 43 | k = 0x5A827999 44 | elif 20 <= i <= 39: 45 | f = b ^ c ^ d 46 | k = 0x6ED9EBA1 47 | elif 40 <= i <= 59: 48 | f = (b & c) | (b & d) | (c & d) 49 | k = 0x8F1BBCDC 50 | elif 60 <= i <= 79: 51 | f = b ^ c ^ d 52 | k = 0xCA62C1D6 53 | 54 | a, b, c, d, e = ((_left_rotate(a, 5) + f + e + k + w[i]) & 0xffffffff, 55 | a, _left_rotate(b, 30), c, d) 56 | 57 | # Add this chunk's hash to result so far 58 | h0 = (h0 + a) & 0xffffffff 59 | h1 = (h1 + b) & 0xffffffff 60 | h2 = (h2 + c) & 0xffffffff 61 | h3 = (h3 + d) & 0xffffffff 62 | h4 = (h4 + e) & 0xffffffff 63 | 64 | return h0, h1, h2, h3, h4 65 | 66 | 67 | class Sha1Hash(object): 68 | """A class that mimics that hashlib api and implements the SHA-1 algorithm.""" 69 | 70 | name = 'python-sha1' 71 | digest_size = 20 72 | block_size = 64 73 | 74 | def __init__(self): 75 | # Initial digest variables 76 | self._h = ( 77 | 0x67452301, 78 | 0xEFCDAB89, 79 | 0x98BADCFE, 80 | 0x10325476, 81 | 0xC3D2E1F0, 82 | ) 83 | 84 | # bytes object with 0 <= len < 64 used to store the end of the message 85 | # if the message length is not congruent to 64 86 | self._unprocessed = b'' 87 | # Length in bytes of all data that has been processed so far 88 | self._message_byte_length = 0 89 | 90 | def update(self, arg): 91 | """Update the current digest. 92 | This may be called repeatedly, even after calling digest or hexdigest. 93 | 94 | Arguments: 95 | arg: bytes, bytearray, or BytesIO object to read from. 96 | """ 97 | if isinstance(arg, (bytes, bytearray)): 98 | arg = io.BytesIO(arg) 99 | 100 | # Try to build a chunk out of the unprocessed data, if any 101 | chunk = self._unprocessed + arg.read(64 - len(self._unprocessed)) 102 | 103 | # Read the rest of the data, 64 bytes at a time 104 | while len(chunk) == 64: 105 | self._h = _process_chunk(chunk, *self._h) 106 | self._message_byte_length += 64 107 | chunk = arg.read(64) 108 | 109 | self._unprocessed = chunk 110 | return self 111 | 112 | def digest(self): 113 | """Produce the final hash value (big-endian) as a bytes object""" 114 | return b''.join(struct.pack(b'>I', h) for h in self._produce_digest()) 115 | 116 | def hexdigest(self): 117 | """Produce the final hash value (big-endian) as a hex string""" 118 | return '%08x%08x%08x%08x%08x' % self._produce_digest() 119 | 120 | def inner_digest(self): 121 | 122 | tmp = struct.unpack(">5I", struct.pack("<5I", *self._h)) 123 | return '%08x%08x%08x%08x%08x' % tmp 124 | 125 | def _produce_digest(self): 126 | """Return finalized digest variables for the data processed so far.""" 127 | # Pre-processing: 128 | message = self._unprocessed 129 | message_byte_length = self._message_byte_length + len(message) 130 | 131 | # append the bit '1' to the message 132 | message += b'\x80' 133 | 134 | # append 0 <= k < 512 bits '0', so that the resulting message length (in bytes) 135 | # is congruent to 56 (mod 64) 136 | message += b'\x00' * ((56 - (message_byte_length + 1) % 64) % 64) 137 | 138 | # append length of message (before pre-processing), in bits, as 64-bit big-endian integer 139 | message_bit_length = message_byte_length * 8 140 | message += struct.pack(b'>Q', message_bit_length) 141 | 142 | # Process the final chunk 143 | # At this point, the length of the message is either 64 or 128 bytes. 144 | h = _process_chunk(message[:64], *self._h) 145 | if len(message) == 64: 146 | return h 147 | return _process_chunk(message[64:], *h) 148 | 149 | 150 | def sha1(data): 151 | """SHA-1 Hashing Function 152 | A custom SHA-1 hashing function implemented entirely in Python. 153 | Arguments: 154 | data: A bytes or BytesIO object containing the input message to hash. 155 | Returns: 156 | A hex SHA-1 digest of the input message. 157 | """ 158 | return Sha1Hash().update(data).hexdigest() 159 | 160 | 161 | class Sha1Util(object): 162 | 163 | @staticmethod 164 | def get_sha1_by_slice(file_name, slice_size): 165 | """ Get SHA array based on Qcloud Slice Upload Interface 166 | 167 | :param file_name: local file path 168 | :param slice_size: slice size in bit 169 | :return: sha array like [{“offset”:0, “datalen”:1024,”datasha”:”aaa”}, {}, {}] 170 | """ 171 | from os import path 172 | 173 | with open(file_name, 'rb') as f: 174 | 175 | result = [] 176 | file_size = path.getsize(file_name) 177 | sha1_obj = Sha1Hash() 178 | for current_offset in range(0, file_size, slice_size): 179 | 180 | data_length = min(slice_size, file_size - current_offset) 181 | sha1_obj.update(f.read(data_length)) 182 | sha1_val = sha1_obj.inner_digest() 183 | result.append({"offset": current_offset, "datalen": data_length, "datasha": sha1_val}) 184 | 185 | result[-1]['datasha'] = sha1_obj.hexdigest() 186 | return result 187 | 188 | 189 | if __name__ == '__main__': 190 | # Imports required for command line parsing. No need for these elsewhere 191 | import argparse 192 | import sys 193 | import os 194 | 195 | # Parse the incoming arguments 196 | parser = argparse.ArgumentParser() 197 | parser.add_argument('input', nargs='?', 198 | help='input file or message to hash') 199 | args = parser.parse_args() 200 | 201 | data = None 202 | 203 | if args.input is None: 204 | # No argument given, assume message comes from standard input 205 | try: 206 | # sys.stdin is opened in text mode, which can change line endings, 207 | # leading to incorrect results. Detach fixes this issue, but it's 208 | # new in Python 3.1 209 | data = sys.stdin.detach() 210 | except AttributeError: 211 | # Linux ans OSX both use \n line endings, so only windows is a 212 | # problem. 213 | if sys.platform == "win32": 214 | import msvcrt 215 | 216 | msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) 217 | data = sys.stdin 218 | elif os.path.isfile(args.input): 219 | # An argument is given and it's a valid file. Read it 220 | data = open(args.input, 'rb') 221 | else: 222 | data = args.input 223 | 224 | # Show the final digest 225 | print('sha1-digest:', sha1(data)) 226 | -------------------------------------------------------------------------------- /qcloud_cos/cos_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class CosRegionInfo(object): 6 | 7 | def __init__(self, region=None, hostname=None, download_hostname=None, *args, **kwargs): 8 | self._hostname = None 9 | self._download_hostname = None 10 | 11 | if region in ['sh', 'shanghai']: 12 | self._hostname = 'sh.file.myqcloud.com' 13 | self._download_hostname = 'cossh.myqcloud.com' 14 | 15 | elif region in ['gz', 'guangzhou']: 16 | self._hostname = 'gz.file.myqcloud.com' 17 | self._download_hostname = 'cosgz.myqcloud.com' 18 | 19 | elif region in ['tj', 'tianjin', 'tianjing']: # bug: for compact previous release 20 | self._hostname = 'tj.file.myqcloud.com' 21 | self._download_hostname = 'costj.myqcloud.com' 22 | 23 | elif region in ['sgp', 'singapore']: 24 | self._hostname = 'sgp.file.myqcloud.com' 25 | self._download_hostname = 'cossgp.myqcloud.com' 26 | 27 | elif region is not None: 28 | self._hostname = '{region}.file.myqcloud.com'.format(region=region) 29 | self._download_hostname = 'cos{region}.myqcloud.com'.format(region=region) 30 | else: 31 | if hostname and download_hostname: 32 | self._hostname = hostname 33 | self._download_hostname = download_hostname 34 | else: 35 | raise ValueError("region or [hostname, download_hostname] must be set, and region should be sh/gz/tj/sgp") 36 | 37 | @property 38 | def hostname(self): 39 | assert self._hostname is not None 40 | return self._hostname 41 | 42 | @property 43 | def download_hostname(self): 44 | assert self._download_hostname is not None 45 | return self._download_hostname 46 | 47 | 48 | class CosConfig(object): 49 | """CosConfig 有关cos的配置""" 50 | 51 | def __init__(self, timeout=300, sign_expired=300, enable_https=False, *args, **kwargs): 52 | self._region = CosRegionInfo(*args, **kwargs) 53 | self._user_agent = 'cos-python-sdk-v4' 54 | self._timeout = timeout 55 | self._sign_expired = sign_expired 56 | self._enable_https = enable_https 57 | if self._enable_https: 58 | self._protocol = "https" 59 | else: 60 | self._protocol = "http" 61 | 62 | def get_endpoint(self): 63 | """获取域名地址 64 | 65 | :return: 66 | """ 67 | # tmpl = "%s://%s/files/v2" 68 | return self._protocol + "://" + self._region.hostname + "/files/v2" 69 | 70 | def get_download_hostname(self): 71 | return self._region.download_hostname 72 | 73 | def get_user_agent(self): 74 | """获取HTTP头中的user_agent 75 | 76 | :return: 77 | """ 78 | return self._user_agent 79 | 80 | def set_timeout(self, time_out): 81 | """设置连接超时, 单位秒 82 | 83 | :param time_out: 84 | :return: 85 | """ 86 | assert isinstance(time_out, int) 87 | self._timeout = time_out 88 | 89 | def get_timeout(self): 90 | """获取连接超时,单位秒 91 | 92 | :return: 93 | """ 94 | return self._timeout 95 | 96 | def set_sign_expired(self, expired): 97 | """设置签名过期时间, 单位秒 98 | 99 | :param expired: 100 | :return: 101 | """ 102 | assert isinstance(expired, int) 103 | self._sign_expired = expired 104 | 105 | def get_sign_expired(self): 106 | """获取签名过期时间, 单位秒 107 | 108 | :return: 109 | """ 110 | return self._sign_expired 111 | 112 | @property 113 | def enable_https(self): 114 | assert self._enable_https is not None 115 | return self._enable_https 116 | 117 | @enable_https.setter 118 | def enable_https(self, val): 119 | if val != self._enable_https: 120 | if val: 121 | self._enable_https = val 122 | self._protocol = "https" 123 | else: 124 | self._enable_https = val 125 | self._protocol = "http" 126 | -------------------------------------------------------------------------------- /qcloud_cos/cos_cred.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | from cos_params_check import ParamCheck 5 | 6 | 7 | class CredInfo(object): 8 | """CredInfo用户的身份信息""" 9 | def __init__(self, appid, secret_id, secret_key): 10 | self._appid = appid 11 | self._secret_id = secret_id 12 | self._secret_key = secret_key 13 | self._param_check = ParamCheck() 14 | 15 | def get_appid(self): 16 | return self._appid 17 | 18 | def get_secret_id(self): 19 | return self._secret_id 20 | 21 | def get_secret_key(self): 22 | return self._secret_key 23 | 24 | def check_params_valid(self): 25 | if not self._param_check.check_param_int('appid', self._appid): 26 | return False 27 | if not self._param_check.check_param_unicode('secret_id', self._secret_id): 28 | return False 29 | return self._param_check.check_param_unicode('secret_key', self._secret_key) 30 | 31 | def get_err_tips(self): 32 | """获取错误信息 33 | 34 | :return: 35 | """ 36 | return self._param_check.get_err_tips() 37 | -------------------------------------------------------------------------------- /qcloud_cos/cos_err.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class CosErr(object): 6 | """sdk错误码""" 7 | PARAMS_ERROR = -1 # 参数错误 8 | NETWORK_ERROR = -2 # 网络错误 9 | SERVER_ERROR = -3 # server端返回错误 10 | UNKNOWN_ERROR = -4 # 未知错误 11 | 12 | @staticmethod 13 | def get_err_msg(errcode, err_info): 14 | return {u'code': errcode, u'message': err_info} 15 | -------------------------------------------------------------------------------- /qcloud_cos/cos_op.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import time 6 | import json 7 | import hashlib 8 | import urllib 9 | from contextlib import closing 10 | import cos_auth 11 | from cos_err import CosErr 12 | from cos_request import UploadFileRequest 13 | from cos_request import UploadSliceFileRequest 14 | from cos_request import UploadFileFromBufferRequest 15 | from cos_request import UploadSliceFileFromBufferRequest 16 | from cos_request import UpdateFileRequest 17 | from cos_request import DelFileRequest 18 | from cos_request import StatFileRequest 19 | from cos_request import CreateFolderRequest 20 | from cos_request import UpdateFolderRequest 21 | from cos_request import StatFolderRequest 22 | from cos_request import DelFolderRequest 23 | from cos_request import ListFolderRequest, DownloadFileRequest, DownloadObjectRequest, MoveFileRequest 24 | from cos_common import Sha1Util 25 | 26 | from logging import getLogger 27 | from traceback import format_exc 28 | 29 | logger = getLogger(__name__) 30 | 31 | 32 | class BaseOp(object): 33 | """ 34 | BaseOp基本操作类型 35 | """ 36 | 37 | def __init__(self, cred, config, http_session): 38 | """ 初始化类 39 | 40 | :param cred: 用户的身份信息 41 | :param config: cos_config配置类 42 | :param http_session: http 会话 43 | """ 44 | self._cred = cred 45 | self._config = config 46 | self._http_session = http_session 47 | self._expired_period = self._config.get_sign_expired() 48 | 49 | def set_cred(self, cred): 50 | """设置用户的身份信息 51 | 52 | :param cred: 53 | :return: 54 | """ 55 | self._cred = cred 56 | 57 | def set_config(self, config): 58 | """ 设置config 59 | 60 | :param config: 61 | :return: 62 | """ 63 | self._config = config 64 | self._expired_period = self._config.get_sign_expired() 65 | 66 | def _build_url(self, bucket, cos_path): 67 | """生成url 68 | 69 | :param bucket: 70 | :param cos_path: 71 | :return: 72 | """ 73 | bucket = bucket.encode('utf8') 74 | end_point = self._config.get_endpoint().rstrip('/').encode('utf8') 75 | appid = self._cred.get_appid() 76 | cos_path = urllib.quote(cos_path.encode('utf8'), '~/') 77 | url = '%s/%s/%s%s' % (end_point, appid, bucket, cos_path) 78 | return url 79 | 80 | def build_download_url(self, bucket, cos_path, sign): 81 | # Only support http now 82 | appid = self._cred.get_appid() 83 | hostname = self._config.get_download_hostname() 84 | cos_path = urllib.quote(cos_path.encode('utf8'), '~/') 85 | url_tmpl = 'http://{bucket}-{appid}.{hostname}{cos_path}?sign={sign}' 86 | 87 | return url_tmpl.format(bucket=bucket, appid=appid, hostname=hostname, cos_path=cos_path, sign=sign) 88 | 89 | def send_request(self, method, bucket, cos_path, **kwargs): 90 | """ 发送http请求 91 | 92 | :param method: 93 | :param bucket: 94 | :param cos_path: 95 | :param args: 96 | :return: 97 | """ 98 | url = self._build_url(bucket, cos_path) 99 | logger.debug("sending request, method: %s, bucket: %s, cos_path: %s" % (method, bucket, cos_path)) 100 | 101 | try: 102 | if method == 'POST': 103 | http_resp = self._http_session.post(url, verify=False, **kwargs) 104 | else: 105 | http_resp = self._http_session.get(url, verify=False, **kwargs) 106 | 107 | status_code = http_resp.status_code 108 | if status_code < 500: 109 | return http_resp.json() 110 | else: 111 | logger.warning("request failed, response message: %s" % http_resp.text) 112 | err_detail = 'url:%s, status_code:%d' % (url, status_code) 113 | return CosErr.get_err_msg(CosErr.NETWORK_ERROR, err_detail) 114 | except Exception as e: 115 | logger.exception("request failed, return SERVER_ERROR") 116 | err_detail = 'url:%s, exception:%s traceback:%s' % (url, str(e), format_exc()) 117 | return CosErr.get_err_msg(CosErr.SERVER_ERROR, err_detail) 118 | 119 | def _check_params(self, request): 120 | """检查用户输入参数, 检查通过返回None, 否则返回一个代表错误原因的dict 121 | 122 | :param request: 123 | :return: 124 | """ 125 | if not self._cred.check_params_valid(): 126 | return CosErr.get_err_msg(CosErr.PARAMS_ERROR, self._cred.get_err_tips()) 127 | if not request.check_params_valid(): 128 | return CosErr.get_err_msg(CosErr.PARAMS_ERROR, request.get_err_tips()) 129 | return None 130 | 131 | def del_base(self, request): 132 | """删除文件或者目录, is_file_op为True表示是文件操作 133 | 134 | :param request: 135 | :return: 136 | """ 137 | check_params_ret = self._check_params(request) 138 | if check_params_ret is not None: 139 | return check_params_ret 140 | 141 | auth = cos_auth.Auth(self._cred) 142 | bucket = request.get_bucket_name() 143 | cos_path = request.get_cos_path() 144 | sign = auth.sign_once(bucket, cos_path) 145 | 146 | http_header = dict() 147 | http_header['Authorization'] = sign 148 | http_header['Content-Type'] = 'application/json' 149 | http_header['User-Agent'] = self._config.get_user_agent() 150 | 151 | http_body = {'op': 'delete'} 152 | 153 | timeout = self._config.get_timeout() 154 | return self.send_request('POST', bucket, cos_path, headers=http_header, data=json.dumps(http_body), timeout=timeout) 155 | 156 | def stat_base(self, request): 157 | """获取文件和目录的属性 158 | 159 | :param request: 160 | :return: 161 | """ 162 | check_params_ret = self._check_params(request) 163 | if check_params_ret is not None: 164 | return check_params_ret 165 | 166 | auth = cos_auth.Auth(self._cred) 167 | bucket = request.get_bucket_name() 168 | cos_path = request.get_cos_path() 169 | expired = int(time.time()) + self._expired_period 170 | sign = auth.sign_more(bucket, cos_path, expired) 171 | 172 | http_header = dict() 173 | http_header['Authorization'] = sign 174 | http_header['User-Agent'] = self._config.get_user_agent() 175 | 176 | http_body = dict() 177 | http_body['op'] = 'stat' 178 | 179 | timeout = self._config.get_timeout() 180 | return self.send_request('GET', bucket, cos_path, headers=http_header, params=http_body, timeout=timeout) 181 | 182 | 183 | class FileOp(BaseOp): 184 | """FileOp 文件相关操作""" 185 | 186 | def __init__(self, cred, config, http_session): 187 | """ 初始化类 188 | 189 | :param cred: 用户的身份信息 190 | :param config: cos_config配置类 191 | :param http_session: http 会话 192 | """ 193 | BaseOp.__init__(self, cred, config, http_session) 194 | # 单文件上传的最大上限是20MB 195 | self.max_single_file = 20 * 1024 * 1024 196 | 197 | @staticmethod 198 | def _sha1_content(content): 199 | """获取content的sha1 200 | 201 | :param content: 202 | :return: 203 | """ 204 | sha1_obj = hashlib.sha1() 205 | sha1_obj.update(content) 206 | return sha1_obj.hexdigest() 207 | 208 | def update_file(self, request): 209 | """更新文件 210 | 211 | :param request: 212 | :return: 213 | """ 214 | assert isinstance(request, UpdateFileRequest) 215 | logger.debug("request: " + str(request.get_custom_headers())) 216 | check_params_ret = self._check_params(request) 217 | if check_params_ret is not None: 218 | return check_params_ret 219 | 220 | logger.debug("params verify successfully") 221 | auth = cos_auth.Auth(self._cred) 222 | bucket = request.get_bucket_name() 223 | cos_path = request.get_cos_path() 224 | sign = auth.sign_once(bucket, cos_path) 225 | 226 | http_header = dict() 227 | http_header['Authorization'] = sign 228 | http_header['Content-Type'] = 'application/json' 229 | http_header['User-Agent'] = self._config.get_user_agent() 230 | 231 | http_body = dict() 232 | http_body['op'] = 'update' 233 | 234 | if request.get_biz_attr() is not None: 235 | http_body['biz_attr'] = request.get_biz_attr() 236 | 237 | if request.get_authority() is not None: 238 | http_body['authority'] = request.get_authority() 239 | 240 | if request.get_custom_headers() is not None and len(request.get_custom_headers()) is not 0: 241 | http_body['custom_headers'] = request.get_custom_headers() 242 | logger.debug("Update Request Header: " + json.dumps(http_body)) 243 | timeout = self._config.get_timeout() 244 | return self.send_request('POST', bucket, cos_path, headers=http_header, data=json.dumps(http_body), timeout=timeout) 245 | 246 | def del_file(self, request): 247 | """删除文件 248 | 249 | :param request: 250 | :return: 251 | """ 252 | assert isinstance(request, DelFileRequest) 253 | return self.del_base(request) 254 | 255 | def stat_file(self, request): 256 | """获取文件的属性 257 | 258 | :param request: 259 | :return: 260 | """ 261 | assert isinstance(request, StatFileRequest) 262 | return self.stat_base(request) 263 | 264 | def upload_file(self, request): 265 | """上传文件, 根据用户的文件大小,选择单文件上传和分片上传策略 266 | 267 | :param request: 268 | :return: 269 | """ 270 | assert isinstance(request, UploadFileRequest) 271 | check_params_ret = self._check_params(request) 272 | if check_params_ret is not None: 273 | return check_params_ret 274 | 275 | local_path = request.get_local_path() 276 | file_size = os.path.getsize(local_path) 277 | 278 | suit_single_file_zie = 8 * 1024 * 1024 279 | if file_size < suit_single_file_zie: 280 | return self.upload_single_file(request) 281 | else: 282 | bucket = request.get_bucket_name() 283 | cos_path = request.get_cos_path() 284 | local_path = request.get_local_path() 285 | slice_size = 1024 * 1024 286 | biz_attr = request.get_biz_attr() 287 | enable_sha1 = request.get_verify_sha1() 288 | upload_slice_request = UploadSliceFileRequest(bucket, cos_path, local_path, slice_size, biz_attr, enable_sha1) 289 | upload_slice_request.set_insert_only(request.get_insert_only()) 290 | return self.upload_slice_file(upload_slice_request) 291 | 292 | def upload_single_file(self, request): 293 | """ 单文件上传 294 | 295 | :param request: 296 | :return: 297 | """ 298 | assert isinstance(request, UploadFileRequest) 299 | check_params_ret = self._check_params(request) 300 | if check_params_ret is not None: 301 | return check_params_ret 302 | 303 | local_path = request.get_local_path() 304 | file_size = os.path.getsize(local_path) 305 | # 判断文件是否超过单文件最大上限, 如果超过则返回错误 306 | # 并提示用户使用别的接口 307 | if file_size > self.max_single_file: 308 | return CosErr.get_err_msg(CosErr.NETWORK_ERROR, 'file is too big, please use upload_file interface') 309 | 310 | auth = cos_auth.Auth(self._cred) 311 | bucket = request.get_bucket_name() 312 | cos_path = request.get_cos_path() 313 | expired = int(time.time()) + self._expired_period 314 | sign = auth.sign_more(bucket, cos_path, expired) 315 | 316 | http_header = dict() 317 | http_header['Authorization'] = sign 318 | http_header['User-Agent'] = self._config.get_user_agent() 319 | 320 | with open(local_path, 'rb') as f: 321 | file_content = f.read() 322 | 323 | http_body = dict() 324 | http_body['op'] = 'upload' 325 | http_body['filecontent'] = file_content 326 | http_body['sha'] = FileOp._sha1_content(file_content) 327 | http_body['biz_attr'] = request.get_biz_attr() 328 | http_body['insertOnly'] = str(request.get_insert_only()) 329 | 330 | timeout = self._config.get_timeout() 331 | ret = self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 332 | return ret 333 | 334 | def _upload_slice_file(self, request): 335 | assert isinstance(request, UploadSliceFileRequest) 336 | check_params_ret = self._check_params(request) 337 | if check_params_ret is not None: 338 | return check_params_ret 339 | 340 | local_path = request.get_local_path() 341 | slice_size = request.get_slice_size() 342 | enable_sha1 = request.enable_sha1 343 | 344 | if enable_sha1 is True: 345 | sha1_by_slice_list = Sha1Util.get_sha1_by_slice(local_path, slice_size) 346 | request.sha1_list = sha1_by_slice_list 347 | request.sha1_content = sha1_by_slice_list[-1]["datasha"] 348 | else: 349 | request.sha1_list = None 350 | request.sha1_content = None 351 | 352 | control_ret = self._upload_slice_control(request) 353 | 354 | # 表示控制分片已经产生错误信息 355 | if control_ret[u'code'] != 0: 356 | return control_ret 357 | 358 | # 命中秒传 359 | if u'access_url' in control_ret[u'data']: 360 | return control_ret 361 | 362 | local_path = request.get_local_path() 363 | file_size = os.path.getsize(local_path) 364 | if u'slice_size' in control_ret[u'data']: 365 | slice_size = control_ret[u'data'][u'slice_size'] 366 | offset = 0 367 | session = control_ret[u'data'][u'session'] 368 | # ?concurrency 369 | if request._max_con <= 1 or ( 370 | u'serial_upload' in control_ret[u'data'] and control_ret[u'data'][u'serial_upload'] == 1): 371 | 372 | logger.info("upload file serially") 373 | slice_idx = 0 374 | with open(local_path, 'rb') as local_file: 375 | 376 | while offset < file_size: 377 | file_content = local_file.read(slice_size) 378 | 379 | data_ret = self._upload_slice_data(request, file_content, session, offset) 380 | 381 | if data_ret[u'code'] == 0: 382 | if u'access_url' in data_ret[u'data']: 383 | return data_ret 384 | else: 385 | return data_ret 386 | 387 | offset += slice_size 388 | slice_idx += 1 389 | else: 390 | logger.info('upload file concurrently') 391 | from threadpool import SimpleThreadPool 392 | pool = SimpleThreadPool(request._max_con) 393 | 394 | slice_idx = 0 395 | with open(local_path, 'rb') as local_file: 396 | 397 | while offset < file_size: 398 | file_content = local_file.read(slice_size) 399 | 400 | pool.add_task(self._upload_slice_data, request, file_content, session, offset) 401 | 402 | offset += slice_size 403 | slice_idx += 1 404 | 405 | pool.wait_completion() 406 | result = pool.get_result() 407 | if not result['success_all']: 408 | return {u'code': 1, u'message': str(result)} 409 | 410 | data_ret = self._upload_slice_finish(request, session, file_size) 411 | return data_ret 412 | 413 | def upload_slice_file(self, request): 414 | """分片文件上传(串行) 415 | 416 | :param request: 417 | :return: 418 | """ 419 | ret = self._upload_slice_file(request) 420 | return ret 421 | 422 | def _upload_slice_finish(self, request, session, filesize): 423 | auth = cos_auth.Auth(self._cred) 424 | bucket = request.get_bucket_name() 425 | cos_path = request.get_cos_path() 426 | expired = int(time.time()) + self._expired_period 427 | sign = auth.sign_more(bucket, cos_path, expired) 428 | 429 | http_header = dict() 430 | http_header['Authorization'] = sign 431 | http_header['User-Agent'] = self._config.get_user_agent() 432 | 433 | http_body = dict() 434 | http_body['op'] = "upload_slice_finish" 435 | http_body['session'] = session 436 | http_body['filesize'] = str(filesize) 437 | if request.sha1_list is not None: 438 | http_body['sha'] = request.sha1_list[-1]["datasha"] 439 | timeout = self._config.get_timeout() 440 | 441 | return self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 442 | 443 | def _upload_slice_control(self, request): 444 | """串行分片第一步, 上传控制分片 445 | 446 | :param request: 447 | :return: 448 | """ 449 | auth = cos_auth.Auth(self._cred) 450 | bucket = request.get_bucket_name() 451 | cos_path = request.get_cos_path() 452 | expired = int(time.time()) + self._expired_period 453 | sign = auth.sign_more(bucket, cos_path, expired) 454 | 455 | http_header = dict() 456 | http_header['Authorization'] = sign 457 | http_header['User-Agent'] = self._config.get_user_agent() 458 | 459 | local_path = request.get_local_path() 460 | file_size = os.path.getsize(local_path) 461 | slice_size = request.get_slice_size() 462 | biz_atrr = request.get_biz_attr() 463 | 464 | http_body = dict() 465 | http_body['op'] = 'upload_slice_init' 466 | if request.enable_sha1: 467 | http_body['sha'] = request.sha1_list[-1]["datasha"] 468 | http_body['uploadparts'] = json.dumps(request.sha1_list) 469 | http_body['filesize'] = str(file_size) 470 | http_body['slice_size'] = str(slice_size) 471 | http_body['biz_attr'] = biz_atrr 472 | http_body['insertOnly'] = str(request.get_insert_only()) 473 | 474 | timeout = self._config.get_timeout() 475 | return self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 476 | 477 | def _upload_slice_data(self, request, file_content, session, offset, retry=3): 478 | """串行分片第二步, 上传数据分片 479 | 480 | :param request: 481 | :param file_content: 482 | :param session: 483 | :param offset: 484 | :return: 485 | """ 486 | bucket = request.get_bucket_name() 487 | cos_path = request.get_cos_path() 488 | auth = cos_auth.Auth(self._cred) 489 | expired = int(time.time()) + self._expired_period 490 | sign = auth.sign_more(bucket, cos_path, expired) 491 | 492 | http_header = dict() 493 | http_header['Authorization'] = sign 494 | http_header['User-Agent'] = self._config.get_user_agent() 495 | 496 | http_body = dict() 497 | http_body['op'] = 'upload_slice_data' 498 | http_body['filecontent'] = file_content 499 | http_body['session'] = session 500 | http_body['offset'] = str(offset) 501 | if request.sha1_content is not None: 502 | http_body['sha'] = request.sha1_content 503 | 504 | timeout = self._config.get_timeout() 505 | 506 | for _ in range(retry): 507 | ret = self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 508 | if ret['code'] == 0: 509 | return ret 510 | else: 511 | return ret 512 | 513 | def upload_file_from_buffer(self, request): 514 | """上传文件, 根据用户的文件大小,选择单文件上传和分片上传策略 515 | 516 | :param request: 517 | :return: 518 | """ 519 | assert isinstance(request, UploadFileFromBufferRequest) 520 | check_params_ret = self._check_params(request) 521 | if check_params_ret is not None: 522 | return check_params_ret 523 | 524 | data = request.get_data() 525 | file_size = len(data) 526 | 527 | suit_single_file_zie = 8 * 1024 * 1024 528 | if file_size < suit_single_file_zie: 529 | return self.upload_single_file_from_buffer(request) 530 | else: 531 | bucket = request.get_bucket_name() 532 | cos_path = request.get_cos_path() 533 | data = request.get_data() 534 | slice_size = 1024 * 1024 535 | biz_attr = request.get_biz_attr() 536 | enable_sha1 = request.get_verify_sha1() 537 | upload_slice_request = UploadSliceFileFromBufferRequest(bucket, cos_path, data, slice_size, biz_attr, enable_sha1) 538 | upload_slice_request.set_insert_only(request.get_insert_only()) 539 | return self.upload_slice_file_from_buffer(upload_slice_request) 540 | 541 | def upload_single_file_from_buffer(self, request): 542 | """ 单文件上传 543 | 544 | :param request: 545 | :return: 546 | """ 547 | assert isinstance(request, UploadFileFromBufferRequest) 548 | check_params_ret = self._check_params(request) 549 | if check_params_ret is not None: 550 | return check_params_ret 551 | 552 | data = request.get_data() 553 | file_size = len(data) 554 | # 判断文件是否超过单文件最大上限, 如果超过则返回错误 555 | # 并提示用户使用别的接口 556 | if file_size > self.max_single_file: 557 | return CosErr.get_err_msg(CosErr.NETWORK_ERROR, 'file is too big, please use upload_file interface') 558 | 559 | auth = cos_auth.Auth(self._cred) 560 | bucket = request.get_bucket_name() 561 | cos_path = request.get_cos_path() 562 | expired = int(time.time()) + self._expired_period 563 | sign = auth.sign_more(bucket, cos_path, expired) 564 | 565 | http_header = dict() 566 | http_header['Authorization'] = sign 567 | http_header['User-Agent'] = self._config.get_user_agent() 568 | 569 | file_content = data 570 | 571 | http_body = dict() 572 | http_body['op'] = 'upload' 573 | http_body['filecontent'] = file_content 574 | http_body['sha'] = FileOp._sha1_content(file_content) 575 | http_body['biz_attr'] = request.get_biz_attr() 576 | http_body['insertOnly'] = str(request.get_insert_only()) 577 | 578 | timeout = self._config.get_timeout() 579 | ret = self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 580 | return ret 581 | 582 | def _upload_slice_file_from_buffer(self, request): 583 | assert isinstance(request, UploadSliceFileFromBufferRequest) 584 | check_params_ret = self._check_params(request) 585 | if check_params_ret is not None: 586 | return check_params_ret 587 | 588 | data = request.get_data() 589 | slice_size = request.get_slice_size() 590 | enable_sha1 = request.enable_sha1 591 | 592 | request.sha1_list = None 593 | request.sha1_content = None 594 | 595 | control_ret = self._upload_slice_control_from_buffer(request) 596 | 597 | # 表示控制分片已经产生错误信息 598 | if control_ret[u'code'] != 0: 599 | return control_ret 600 | 601 | # 命中秒传 602 | if u'access_url' in control_ret[u'data']: 603 | return control_ret 604 | 605 | data = request.get_data() 606 | file_size = len(data) 607 | if u'slice_size' in control_ret[u'data']: 608 | slice_size = control_ret[u'data'][u'slice_size'] 609 | offset = 0 610 | session = control_ret[u'data'][u'session'] 611 | # ?concurrency 612 | if request._max_con <= 1 or ( 613 | u'serial_upload' in control_ret[u'data'] and control_ret[u'data'][u'serial_upload'] == 1): 614 | 615 | logger.info("upload file serially") 616 | slice_idx = 0 617 | 618 | while offset < file_size: 619 | file_content = data[offset:offset + slice_size] 620 | 621 | data_ret = self._upload_slice_data(request, file_content, session, offset) 622 | 623 | if data_ret[u'code'] == 0: 624 | if u'access_url' in data_ret[u'data']: 625 | return data_ret 626 | else: 627 | return data_ret 628 | 629 | offset += slice_size 630 | slice_idx += 1 631 | else: 632 | logger.info('upload file concurrently') 633 | from threadpool import SimpleThreadPool 634 | pool = SimpleThreadPool(request._max_con) 635 | 636 | slice_idx = 0 637 | 638 | while offset < file_size: 639 | file_content = data[offset:offset + slice_size] 640 | pool.add_task(self._upload_slice_data, request, file_content, session, offset) 641 | 642 | offset += slice_size 643 | slice_idx += 1 644 | 645 | pool.wait_completion() 646 | result = pool.get_result() 647 | if not result['success_all']: 648 | return {u'code': 1, u'message': str(result)} 649 | 650 | data_ret = self._upload_slice_finish_from_buffer(request, session, file_size) 651 | return data_ret 652 | 653 | def upload_slice_file_from_buffer(self, request): 654 | """分片文件上传(串行) 655 | 656 | :param request: 657 | :return: 658 | """ 659 | ret = self._upload_slice_file_from_buffer(request) 660 | return ret 661 | 662 | def _upload_slice_finish_from_buffer(self, request, session, filesize): 663 | auth = cos_auth.Auth(self._cred) 664 | bucket = request.get_bucket_name() 665 | cos_path = request.get_cos_path() 666 | expired = int(time.time()) + self._expired_period 667 | sign = auth.sign_more(bucket, cos_path, expired) 668 | 669 | http_header = dict() 670 | http_header['Authorization'] = sign 671 | http_header['User-Agent'] = self._config.get_user_agent() 672 | 673 | http_body = dict() 674 | http_body['op'] = "upload_slice_finish" 675 | http_body['session'] = session 676 | http_body['filesize'] = str(filesize) 677 | if request.sha1_list is not None: 678 | http_body['sha'] = request.sha1_list[-1]["datasha"] 679 | timeout = self._config.get_timeout() 680 | 681 | return self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 682 | 683 | def _upload_slice_control_from_buffer(self, request): 684 | """串行分片第一步, 上传控制分片 685 | 686 | :param request: 687 | :return: 688 | """ 689 | auth = cos_auth.Auth(self._cred) 690 | bucket = request.get_bucket_name() 691 | cos_path = request.get_cos_path() 692 | expired = int(time.time()) + self._expired_period 693 | sign = auth.sign_more(bucket, cos_path, expired) 694 | 695 | http_header = dict() 696 | http_header['Authorization'] = sign 697 | http_header['User-Agent'] = self._config.get_user_agent() 698 | 699 | data = request.get_data() 700 | file_size = len(data) 701 | slice_size = request.get_slice_size() 702 | biz_atrr = request.get_biz_attr() 703 | 704 | http_body = dict() 705 | http_body['op'] = 'upload_slice_init' 706 | if request.enable_sha1: 707 | http_body['sha'] = request.sha1_list[-1]["datasha"] 708 | http_body['uploadparts'] = json.dumps(request.sha1_list) 709 | http_body['filesize'] = str(file_size) 710 | http_body['slice_size'] = str(slice_size) 711 | http_body['biz_attr'] = biz_atrr 712 | http_body['insertOnly'] = str(request.get_insert_only()) 713 | 714 | timeout = self._config.get_timeout() 715 | return self.send_request('POST', bucket, cos_path, headers=http_header, files=http_body, timeout=timeout) 716 | 717 | def __download_url(self, uri, filename, headers): 718 | session = self._http_session 719 | 720 | with closing(session.get(uri, stream=True, timeout=30, headers=headers)) as ret: 721 | if ret.status_code in [200, 206]: 722 | 723 | if 'Content-Length' in ret.headers: 724 | content_len = int(ret.headers['Content-Length']) 725 | else: 726 | raise IOError("download failed without Content-Length header") 727 | 728 | file_len = 0 729 | with open(filename, 'wb') as f: 730 | for chunk in ret.iter_content(chunk_size=1024): 731 | if chunk: 732 | file_len += len(chunk) 733 | f.write(chunk) 734 | f.flush() 735 | if file_len != content_len: 736 | raise IOError("download failed with incomplete file") 737 | else: 738 | raise IOError("download failed with status code:" + str(ret.status_code)) 739 | 740 | def download_file(self, request): 741 | assert isinstance(request, DownloadFileRequest) 742 | 743 | auth = cos_auth.Auth(self._cred) 744 | sign = auth.sign_download(request.get_bucket_name(), request.get_cos_path(), self._config.get_sign_expired()) 745 | url = self.build_download_url(request.get_bucket_name(), request.get_cos_path(), sign) 746 | logger.info("Uri is %s" % url) 747 | try: 748 | self.__download_url(url, request._local_filename, request._custom_headers) 749 | return {u'code': 0, u'message': "download successfully"} 750 | except Exception as e: 751 | return {u'code': 1, u'message': "download failed, exception: " + str(e)} 752 | 753 | def __download_object_url(self, uri, headers): 754 | session = self._http_session 755 | 756 | ret = session.get(uri, stream=True, timeout=30, headers=headers) 757 | if ret.status_code in [200, 206]: 758 | return ret.raw 759 | else: 760 | raise IOError("get object failed with status code:" + str(ret.status_code)) 761 | 762 | def download_object(self, request): 763 | assert isinstance(request, DownloadObjectRequest) 764 | 765 | auth = cos_auth.Auth(self._cred) 766 | sign = auth.sign_download(request.get_bucket_name(), request.get_cos_path(), self._config.get_sign_expired()) 767 | url = self.build_download_url(request.get_bucket_name(), request.get_cos_path(), sign) 768 | logger.info("Uri is %s" % url) 769 | 770 | ret = self.__download_object_url(url, request._custom_headers) 771 | return ret 772 | 773 | def __move_file(self, request): 774 | 775 | auth = cos_auth.Auth(self._cred) 776 | bucket = request.get_bucket_name() 777 | cos_path = request.get_cos_path() 778 | sign = auth.sign_once(bucket, cos_path) 779 | 780 | http_header = dict() 781 | http_header['Authorization'] = sign 782 | http_header['User-Agent'] = self._config.get_user_agent() 783 | 784 | http_body = dict() 785 | http_body['op'] = 'move' 786 | http_body['dest_fileid'] = request.dest_path 787 | http_body['to_over_write'] = str(1 if request.overwrite else 0) 788 | 789 | timeout = self._config.get_timeout() 790 | return self.send_request('POST', bucket, cos_path, headers=http_header, params=http_body, timeout=timeout) 791 | 792 | def move_file(self, request): 793 | 794 | assert isinstance(request, MoveFileRequest) 795 | return self.__move_file(request) 796 | 797 | 798 | class FolderOp(BaseOp): 799 | """FolderOp 目录相关操作""" 800 | def __init__(self, cred, config, http_session): 801 | BaseOp.__init__(self, cred, config, http_session) 802 | 803 | def update_folder(self, request): 804 | """更新目录 805 | 806 | :param request: 807 | :return: 808 | """ 809 | assert isinstance(request, UpdateFolderRequest) 810 | check_params_ret = self._check_params(request) 811 | if check_params_ret is not None: 812 | return check_params_ret 813 | 814 | auth = cos_auth.Auth(self._cred) 815 | bucket = request.get_bucket_name() 816 | cos_path = request.get_cos_path() 817 | sign = auth.sign_once(bucket, cos_path) 818 | 819 | http_header = dict() 820 | http_header['Authorization'] = sign 821 | http_header['Content-Type'] = 'application/json' 822 | http_header['User-Agent'] = self._config.get_user_agent() 823 | 824 | http_body = dict() 825 | http_body['op'] = 'update' 826 | http_body['biz_attr'] = request.get_biz_attr() 827 | 828 | timeout = self._config.get_timeout() 829 | return self.send_request('POST', bucket, cos_path, headers=http_header, data=json.dumps(http_body), timeout=timeout) 830 | 831 | def del_folder(self, request): 832 | """删除目录 833 | 834 | :param request: 835 | :return: 836 | """ 837 | assert isinstance(request, DelFolderRequest) 838 | return self.del_base(request) 839 | 840 | def stat_folder(self, request): 841 | """获取目录属性 842 | 843 | :param request: 844 | :return: 845 | """ 846 | assert isinstance(request, StatFolderRequest) 847 | return self.stat_base(request) 848 | 849 | def create_folder(self, request): 850 | """创建目录 851 | 852 | :param request: 853 | :return: 854 | """ 855 | assert isinstance(request, CreateFolderRequest) 856 | check_params_ret = self._check_params(request) 857 | if check_params_ret is not None: 858 | return check_params_ret 859 | 860 | auth = cos_auth.Auth(self._cred) 861 | bucket = request.get_bucket_name() 862 | cos_path = request.get_cos_path() 863 | expired = int(time.time()) + self._expired_period 864 | sign = auth.sign_more(bucket, cos_path, expired) 865 | 866 | http_header = dict() 867 | http_header['Authorization'] = sign 868 | http_header['Content-Type'] = 'application/json' 869 | http_header['User-Agent'] = self._config.get_user_agent() 870 | 871 | http_body = dict() 872 | http_body['op'] = 'create' 873 | http_body['biz_attr'] = request.get_biz_attr() 874 | 875 | timeout = self._config.get_timeout() 876 | return self.send_request('POST', bucket, cos_path, headers=http_header, data=json.dumps(http_body), timeout=timeout) 877 | 878 | def list_folder(self, request): 879 | """list目录 880 | 881 | :param request: 882 | :return: 883 | """ 884 | assert isinstance(request, ListFolderRequest) 885 | check_params_ret = self._check_params(request) 886 | if check_params_ret is not None: 887 | return check_params_ret 888 | 889 | http_body = dict() 890 | http_body['op'] = 'list' 891 | http_body['num'] = request.get_num() 892 | 893 | http_body['context'] = request.get_context() 894 | 895 | if request.get_delimiter(): 896 | http_body['delimiter'] = request.get_delimiter() 897 | 898 | auth = cos_auth.Auth(self._cred) 899 | bucket = request.get_bucket_name() 900 | list_path = request.get_cos_path() + request.get_prefix() 901 | expired = int(time.time()) + self._expired_period 902 | sign = auth.sign_more(bucket, list_path, expired) 903 | 904 | http_header = dict() 905 | http_header['Authorization'] = sign 906 | http_header['User-Agent'] = self._config.get_user_agent() 907 | 908 | timeout = self._config.get_timeout() 909 | return self.send_request('GET', bucket, list_path, headers=http_header, params=http_body, timeout=timeout) 910 | -------------------------------------------------------------------------------- /qcloud_cos/cos_params_check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | 6 | 7 | class ParamCheck(object): 8 | """BaseRequest基本类型的请求""" 9 | def __init__(self): 10 | self._err_tips = u'' 11 | 12 | def get_err_tips(self): 13 | """获取错误信息 14 | 15 | :return: 16 | """ 17 | return self._err_tips 18 | 19 | def check_param_unicode(self, param_name, param_value): 20 | """检查参数是否是unicode 21 | 22 | :param param_name: param_name 参数名 23 | :param param_value: param_value 参数值 24 | :return: 25 | """ 26 | if param_value is None: 27 | self._err_tips = param_name + ' is None!' 28 | return False 29 | if not isinstance(param_value, unicode): 30 | self._err_tips = param_name + ' is not unicode!' 31 | return False 32 | return True 33 | 34 | def check_param_int(self, param_name, param_value): 35 | """检查参数是否是int 36 | 37 | :param param_name: param_name 参数名 38 | :param param_value: param_value 参数值 39 | :return: 40 | """ 41 | if param_value is None: 42 | self._err_tips = param_name + ' is None!' 43 | return False 44 | if not isinstance(param_value, int): 45 | self._err_tips = param_name + ' is not int!' 46 | return False 47 | return True 48 | 49 | def check_cos_path_valid(self, cos_path, is_file_path): 50 | """检查cos_path是否合法 51 | 52 | 路径必须以/开始,文件路径则不能以/结束, 目录路径必须以/结束 53 | 54 | :param cos_path: 55 | :param is_file_path: 56 | :return: True for valid path, other False 57 | """ 58 | if cos_path[0] != u'/': 59 | self._err_tips = 'cos path must start with /' 60 | return False 61 | 62 | last_letter = cos_path[len(cos_path) - 1] 63 | if is_file_path and last_letter == u'/': 64 | self._err_tips = 'for file operation, cos_path must not end with /' 65 | return False 66 | elif not is_file_path and last_letter != u'/': 67 | self._err_tips = 'for folder operation, cos_path must end with /' 68 | return False 69 | else: 70 | pass 71 | 72 | illegal_letters = ['?', '*', ':', '|', '\\', '<', '>', '"'] 73 | for illegal_letter in illegal_letters: 74 | if cos_path.find(illegal_letter) != -1: 75 | self._err_tips = 'cos path contain illegal letter %s' % illegal_letter 76 | return False 77 | 78 | pattern = re.compile(r'/(\s*)/') 79 | if pattern.search(cos_path): 80 | self._err_tips = 'cos path contain illegal letter / /' 81 | return False 82 | return True 83 | 84 | def check_not_cos_root(self, cos_path): 85 | """检查不是cos的根路径 86 | 87 | 不能对根路径操作的有 1 update 2 create 3 delete 88 | :param cos_path: 89 | :return: 90 | """ 91 | if cos_path == u'/': 92 | self._err_tips = 'bucket operation is not supported by sdk,' 93 | ' please use cos console: https://console.qcloud.com/cos' 94 | return False 95 | else: 96 | return True 97 | 98 | def check_local_file_valid(self, local_path): 99 | """检查本地文件有效(存在并且可读) 100 | 101 | :param local_path: 102 | :return: 103 | """ 104 | if not os.path.exists(local_path): 105 | self._err_tips = 'local_file %s not exist!' % local_path 106 | return False 107 | if not os.path.isfile(local_path): 108 | self._err_tips = 'local_file %s is not regular file!' % local_path 109 | return False 110 | if not os.access(local_path, os.R_OK): 111 | self._err_tips = 'local_file %s is not readable!' % local_path 112 | return False 113 | return True 114 | 115 | def check_slice_size(self, slice_size): 116 | """检查分片大小有效 117 | 118 | :param slice_size: 119 | :return: 120 | """ 121 | min_size = 64 * 1024 # 512KB 122 | max_size = 3 * 1024 * 1024 # 20MB 123 | 124 | if max_size >= slice_size >= min_size: 125 | return True 126 | else: 127 | self._err_tips = 'slice_size is invalid, only accept [%d, %d]' \ 128 | % (min_size, max_size) 129 | return False 130 | 131 | def check_insert_only(self, insert_only): 132 | """检查文件上传的insert_only参数 133 | 134 | :param insert_only: 135 | :return: 136 | """ 137 | if insert_only != 1 and insert_only != 0: 138 | self._err_tips = 'insert_only only support 0 and 1' 139 | return False 140 | else: 141 | return True 142 | 143 | def check_move_over_write(self, to_over_write): 144 | """检查move的over write标志 145 | 146 | :param to_over_write: 147 | :return: 148 | """ 149 | if to_over_write != 1 and to_over_write != 0: 150 | self._err_tips = 'to_over_write only support 0 and 1' 151 | return False 152 | else: 153 | return True 154 | 155 | def check_file_authority(self, authority): 156 | """检查文件的authority属性 157 | 158 | 合法的取值只有eInvalid, eWRPrivate, eWPrivateRPublic和空值 159 | :param authority: 160 | :return: 161 | """ 162 | if authority != u''and authority != u'eInvalid' and authority != u'eWRPrivate' and authority != u'eWPrivateRPublic': 163 | self._err_tips = 'file authority valid value is: eInvalid, eWRPrivate, eWPrivateRPublic' 164 | return False 165 | else: 166 | return True 167 | 168 | def check_x_cos_meta_dict(self, x_cos_meta_dict): 169 | """检查x_cos_meta_dict, key和value都必须是UTF8编码 170 | 171 | :param x_cos_meta_dict: 172 | :return: 173 | """ 174 | prefix_len = len('x-cos-meta-') 175 | for key in x_cos_meta_dict.keys(): 176 | if not self.check_param_unicode('x-cos-meta-key', key): 177 | return False 178 | if not self.check_param_unicode('x-cos-meta-value', x_cos_meta_dict[key]): 179 | return False 180 | if key[0:prefix_len] != u'x-cos-meta-': 181 | self._err_tips = 'x-cos-meta key must start with x-cos-meta-' 182 | return False 183 | if len(key) == prefix_len: 184 | self._err_tips = 'x-cos-meta key must not just be x-cos-meta-' 185 | return False 186 | if len(x_cos_meta_dict[key]) == 0: 187 | self._err_tips = 'x-cos-meta value must not be empty' 188 | return False 189 | return True 190 | 191 | def check_update_flag(self, flag): 192 | """检查更新文件的flag 193 | 194 | :param flag: 195 | :return: 196 | """ 197 | if flag == 0: 198 | self._err_tips = 'no any attribute to be updated!' 199 | return False 200 | else: 201 | return True 202 | 203 | def check_list_order(self, list_order): 204 | """ 检查list folder的order 205 | 206 | :param list_order: 合法取值0(正序), 1(逆序) 207 | :return: 208 | """ 209 | if list_order != 0 and list_order != 1: 210 | self._err_tips = 'list order is invalid, please use 0(positive) or 1(reverse)!' 211 | return False 212 | else: 213 | return True 214 | 215 | def check_list_pattern(self, list_pattern): 216 | """检查list folder的pattern 217 | 218 | :param list_pattern: 合法取值eListBoth, eListDirOnly, eListFileOnly 219 | :return: 220 | """ 221 | if list_pattern != u'eListBoth' and list_pattern != u'eListDirOnly' and list_pattern != u'eListFileOnly': 222 | self._err_tips = 'list pattern is invalid, please use eListBoth or eListDirOnly or eListFileOnly' 223 | return False 224 | else: 225 | return True 226 | -------------------------------------------------------------------------------- /qcloud_cos/cos_request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """the request type in tencent qcloud cos""" 5 | 6 | from cos_params_check import ParamCheck 7 | import collections 8 | 9 | 10 | class BaseRequest(object): 11 | """BaseRequest基本类型的请求""" 12 | 13 | def __init__(self, bucket_name, cos_path): 14 | """ 类初始化 15 | 16 | :param bucket_name: bucket的名称 17 | :param cos_path: cos的绝对路径, 即从bucket下的根/开始 18 | """ 19 | self._bucket_name = bucket_name.strip() 20 | self._cos_path = cos_path.strip() 21 | self._param_check = ParamCheck() 22 | 23 | def set_bucket_name(self, bucket_name=u''): 24 | """设置bucket_name 25 | 26 | :param bucket_name: 27 | :return: 28 | """ 29 | self._bucket_name = bucket_name.strip() 30 | 31 | def get_bucket_name(self): 32 | """获取bucket_name 33 | 34 | :return: 35 | """ 36 | return self._bucket_name 37 | 38 | def set_cos_path(self, cos_path=u''): 39 | """设置cos_path 40 | 41 | :param cos_path: 42 | :return: 43 | """ 44 | self._cos_path = cos_path.strip() 45 | 46 | def get_cos_path(self): 47 | """获取cos_path 48 | 49 | :return: 50 | """ 51 | return self._cos_path 52 | 53 | def get_err_tips(self): 54 | """获取错误信息 55 | 56 | :return: 57 | """ 58 | return self._param_check.get_err_tips() 59 | 60 | def check_params_valid(self): 61 | """检查参数是否合法 62 | 63 | :return: 64 | """ 65 | if not self._param_check.check_param_unicode('bucket', self._bucket_name): 66 | return False 67 | return self._param_check.check_param_unicode('cos_path', self._cos_path) 68 | 69 | 70 | class CreateFolderRequest(BaseRequest): 71 | """CreateFolderRequest 创建目录类型的请求""" 72 | 73 | def __init__(self, bucket_name, cos_path, biz_attr=u''): 74 | """ 75 | 76 | :param bucket_name: bucket的名称 77 | :param cos_path: cos的绝对路径, 从bucket根/开始 78 | :param biz_attr: 目录的属性 79 | """ 80 | super(CreateFolderRequest, self).__init__(bucket_name, cos_path) 81 | self._biz_attr = biz_attr 82 | 83 | def set_biz_attr(self, biz_attr): 84 | """设置biz_attr 85 | 86 | :param biz_attr: 87 | :return: 88 | """ 89 | self._biz_attr = biz_attr 90 | 91 | def get_biz_attr(self): 92 | """ 获取biz_attr 93 | 94 | :return: 95 | """ 96 | return self._biz_attr 97 | 98 | def check_params_valid(self): 99 | """检查参数是否合法 100 | 101 | :return: 102 | """ 103 | if not super(CreateFolderRequest, self).check_params_valid(): 104 | return False 105 | if not self._param_check.check_param_unicode('biz_attr', self._biz_attr): 106 | return False 107 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=False): 108 | return False 109 | return self._param_check.check_not_cos_root(self._cos_path) 110 | 111 | 112 | class UploadFileRequest(BaseRequest): 113 | """ 114 | UploadFileRequest 单文件上传请求 115 | """ 116 | 117 | def __init__(self, bucket_name, cos_path, local_path, biz_attr=u'', insert_only=1, verify_sha1=False): 118 | """ 119 | 120 | :param bucket_name: bucket的名称 121 | :param cos_path: cos的绝对路径(目的路径), 从bucket根/开始 122 | :param local_path: 上传的本地文件路径(源路径) 123 | :param biz_attr: 文件的属性 124 | :param insert_only: 是否覆盖写, 0覆盖, 1不覆盖,返回错误 125 | :param verify_sha1: 分片上传是否带sha1上传,默认为False 126 | """ 127 | super(UploadFileRequest, self).__init__(bucket_name, cos_path) 128 | self._local_path = local_path.strip() 129 | self._biz_attr = biz_attr 130 | self._insert_only = insert_only 131 | self._verify_sha1 = verify_sha1 132 | 133 | def set_local_path(self, local_path): 134 | """设置local_path 135 | 136 | :param local_path: 137 | :return: 138 | """ 139 | self._local_path = local_path.strip() 140 | 141 | def get_local_path(self): 142 | """获取local_path 143 | 144 | :return: 145 | """ 146 | return self._local_path 147 | 148 | def set_biz_attr(self, biz_attr): 149 | """设置biz_attr 150 | 151 | :param biz_attr: 152 | :return: 153 | """ 154 | self._biz_attr = biz_attr 155 | 156 | def get_biz_attr(self): 157 | """获取biz_attr 158 | 159 | :return: 160 | """ 161 | return self._biz_attr 162 | 163 | def set_insert_only(self, insert_only): 164 | """设置insert_only,0表示如果文件存在, 则覆盖 165 | 166 | :param insert_only: 167 | :return: 168 | """ 169 | self._insert_only = insert_only 170 | 171 | def get_insert_only(self): 172 | """获取insert_only 173 | 174 | :return: 175 | """ 176 | return self._insert_only 177 | 178 | def set_verify_sha1(self, verify_sha1): 179 | """设置enable_sha1 180 | 181 | :param verify_sha1: 182 | :return: 183 | """ 184 | self._verify_sha1 = verify_sha1 185 | 186 | def get_verify_sha1(self): 187 | """获取verify_sha1 188 | 189 | :return: 190 | """ 191 | return self._verify_sha1 192 | 193 | def check_params_valid(self): 194 | """检查参数是否有效 195 | 196 | :return: 197 | """ 198 | if not super(UploadFileRequest, self).check_params_valid(): 199 | return False 200 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=True): 201 | return False 202 | if not self._param_check.check_param_unicode('biz_attr', self._biz_attr): 203 | return False 204 | if not self._param_check.check_param_unicode('local_path', self._local_path): 205 | return False 206 | if not self._param_check.check_local_file_valid(self._local_path): 207 | return False 208 | if not self._param_check.check_param_int('insert_only', self._insert_only): 209 | return False 210 | return self._param_check.check_insert_only(self._insert_only) 211 | 212 | 213 | class UploadSliceFileRequest(UploadFileRequest): 214 | """ 215 | UploadSliceFileRequest 分片文件上传请求 216 | """ 217 | 218 | def __init__(self, bucket_name, cos_path, local_path, slice_size=1024*1024, biz_attr=u'', enable_sha1=False, max_con=1, insert_only=1): 219 | """ 220 | 221 | :param bucket_name: bucket的名称 222 | :param cos_path: cos的绝对路径(目的路径), 从bucket根/开始 223 | :param local_path: 上传的本地文件路径(源路径) 224 | :param slice_size: 文件的属性 225 | :param biz_attr: 分片大小(字节, 默认1MB) 226 | :param enable_sha1: 是否启用sha1校验 227 | :param insert_only: 是否覆盖,默认为1不覆盖 228 | """ 229 | super(UploadSliceFileRequest, self).__init__(bucket_name, cos_path, local_path, biz_attr, insert_only) 230 | self._slice_size = slice_size 231 | self._enable_sha1 = enable_sha1 232 | self._max_con = max_con 233 | 234 | @property 235 | def enable_sha1(self): 236 | return self._enable_sha1 237 | 238 | @enable_sha1.setter 239 | def enable_sha1(self, val): 240 | if val in (True, False): 241 | self._enable_sha1 = val 242 | else: 243 | raise ValueError("enable_sha1 should be True/False") 244 | 245 | def set_slice_size(self, slice_size): 246 | """设置分片大小 247 | 248 | :param slice_size: 249 | :return: 250 | """ 251 | self._slice_size = slice_size 252 | 253 | def get_slice_size(self): 254 | """获取分片大小 255 | 256 | :return: 257 | """ 258 | return self._slice_size 259 | 260 | def check_params_valid(self): 261 | """检查参数是否有效 262 | 263 | :return: 264 | """ 265 | if not super(UploadSliceFileRequest, self).check_params_valid(): 266 | return False 267 | 268 | if self._enable_sha1 and self._slice_size != 1024*1024: 269 | self._param_check._err_tips = 'slice_size is invalid, slice must be 1MB with enable_sha1' 270 | return False 271 | 272 | return self._param_check.check_slice_size(self._slice_size) 273 | 274 | 275 | class UploadFileFromBufferRequest(BaseRequest): 276 | """ 277 | UploadFileFromBufferRequest 内存单文件上传请求 278 | """ 279 | 280 | def __init__(self, bucket_name, cos_path, data, biz_attr=u'', insert_only=1, verify_sha1=False): 281 | """ 282 | 283 | :param bucket_name: bucket的名称 284 | :param cos_path: cos的绝对路径(目的路径), 从bucket根/开始 285 | :param data: 文件内容 286 | :param biz_attr: 文件的属性 287 | :param insert_only: 是否覆盖写, 0覆盖, 1不覆盖,返回错误 288 | """ 289 | super(UploadFileFromBufferRequest, self).__init__(bucket_name, cos_path) 290 | self._data = data 291 | self._biz_attr = biz_attr 292 | self._insert_only = insert_only 293 | self._verify_sha1 = verify_sha1 294 | 295 | def set_data(self, data): 296 | """设置local_path 297 | 298 | :param local_path: 299 | :return: 300 | """ 301 | self._data = data 302 | 303 | def get_data(self): 304 | """获取local_path 305 | 306 | :return: 307 | """ 308 | return self._data 309 | 310 | def set_biz_attr(self, biz_attr): 311 | """设置biz_attr 312 | 313 | :param biz_attr: 314 | :return: 315 | """ 316 | self._biz_attr = biz_attr 317 | 318 | def get_biz_attr(self): 319 | """获取biz_attr 320 | 321 | :return: 322 | """ 323 | return self._biz_attr 324 | 325 | def set_insert_only(self, insert_only): 326 | """设置insert_only,0表示如果文件存在, 则覆盖 327 | 328 | :param insert_only: 329 | :return: 330 | """ 331 | self._insert_only = insert_only 332 | 333 | def get_insert_only(self): 334 | """获取insert_only 335 | 336 | :return: 337 | """ 338 | return self._insert_only 339 | 340 | def set_verify_sha1(self, verify_sha1): 341 | """设置enable_sha1 342 | 343 | :param verify_sha1: 344 | :return: 345 | """ 346 | self._verify_sha1 = verify_sha1 347 | 348 | def get_verify_sha1(self): 349 | """获取verify_sha1 350 | 351 | :return: 352 | """ 353 | return self._verify_sha1 354 | 355 | def check_params_valid(self): 356 | """检查参数是否有效 357 | 358 | :return: 359 | """ 360 | if not super(UploadFileFromBufferRequest, self).check_params_valid(): 361 | return False 362 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=True): 363 | return False 364 | if not self._param_check.check_param_unicode('biz_attr', self._biz_attr): 365 | return False 366 | if not self._param_check.check_param_int('insert_only', self._insert_only): 367 | return False 368 | return self._param_check.check_insert_only(self._insert_only) 369 | 370 | 371 | class UploadSliceFileFromBufferRequest(UploadFileFromBufferRequest): 372 | """ 373 | UploadSliceFileFromBufferRequest 内存分片文件上传请求 374 | """ 375 | 376 | def __init__(self, bucket_name, cos_path, data, slice_size=1024*1024, biz_attr=u'', enable_sha1=False, max_con=1, insert_only=1): 377 | """ 378 | 379 | :param bucket_name: bucket的名称 380 | :param cos_path: cos的绝对路径(目的路径), 从bucket根/开始 381 | :param data: 文件内容 382 | :param slice_size: 文件的属性 383 | :param biz_attr: 分片大小(字节, 默认1MB) 384 | :param enable_sha1: 是否启用sha1校验 385 | :param insert_only: 是否覆盖,默认为1不覆盖 386 | """ 387 | super(UploadSliceFileFromBufferRequest, self).__init__(bucket_name, cos_path, data, biz_attr, insert_only) 388 | self._slice_size = slice_size 389 | self._enable_sha1 = enable_sha1 390 | self._max_con = max_con 391 | 392 | @property 393 | def enable_sha1(self): 394 | return self._enable_sha1 395 | 396 | @enable_sha1.setter 397 | def enable_sha1(self, val): 398 | if val in (True, False): 399 | self._enable_sha1 = val 400 | else: 401 | raise ValueError("enable_sha1 should be True/False") 402 | 403 | def set_slice_size(self, slice_size): 404 | """设置分片大小 405 | 406 | :param slice_size: 407 | :return: 408 | """ 409 | self._slice_size = slice_size 410 | 411 | def get_slice_size(self): 412 | """获取分片大小 413 | 414 | :return: 415 | """ 416 | return self._slice_size 417 | 418 | def check_params_valid(self): 419 | """检查参数是否有效 420 | 421 | :return: 422 | """ 423 | if not super(UploadSliceFileFromBufferRequest, self).check_params_valid(): 424 | return False 425 | 426 | if self._enable_sha1 and self._slice_size != 1024*1024: 427 | self._param_check._err_tips = 'slice_size is invalid, slice must be 1MB with enable_sha1' 428 | return False 429 | 430 | return self._param_check.check_slice_size(self._slice_size) 431 | 432 | 433 | class UpdateFolderRequest(BaseRequest): 434 | """UpdateFolderRequest 更新目录请求""" 435 | 436 | def __init__(self, bucket_name, cos_path, biz_attr=u''): 437 | """ 438 | 439 | :param bucket_name: bucket name 440 | :param cos_path: the path on cos 441 | :param biz_attr: biz attributes 442 | """ 443 | super(UpdateFolderRequest, self).__init__(bucket_name, cos_path) 444 | self._biz_attr = biz_attr 445 | 446 | def set_biz_attr(self, biz_attr): 447 | """设置biz_attr 448 | 449 | :param biz_attr: 450 | :return: 451 | """ 452 | self._biz_attr = biz_attr 453 | 454 | def get_biz_attr(self): 455 | """获取biz_attr 456 | 457 | :return: 458 | """ 459 | return self._biz_attr 460 | 461 | def check_params_valid(self): 462 | """检查参数是否有效 463 | 464 | :return: 465 | """ 466 | if not super(UpdateFolderRequest, self).check_params_valid(): 467 | return False 468 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=False): 469 | return False 470 | if not self._param_check.check_not_cos_root(self._cos_path): 471 | return False 472 | return self._param_check.check_param_unicode('biz_attr', self._biz_attr) 473 | 474 | 475 | class UpdateFileRequest(BaseRequest): 476 | """UpdateFileRequest 更新文件请求 """ 477 | 478 | def __init__(self, bucket_name, cos_path): 479 | """ 初始化类 480 | 481 | biz_attr: 要更新的文件的属性 482 | authority: 文件权限: 483 | eInvalid(继承bucket), 484 | eWRPrivate(私有读写), 485 | eWPrivateRPublic(私有写, 公有读) 486 | customer_header: 用户自定义的HTTP请求头,包括以下成员 487 | cache_control: 文件的缓存机制,参见HTTP的Cache-Control 488 | content_type: 文件的MIME信息,参见HTTP的Content-Type 489 | content_disposition: MIME协议的扩展,参见HTTP的Content-Disposition 490 | content_language: 文件的语言, 参见HTTP的Content-Language 491 | content_encoding: body的编码, 参见HTTP的Content-Encoding 492 | _x_cos_meta_dict: 用户自定义的属性, key是以x-cos-meta-开头,value为属性值 493 | 494 | :param bucket_name: bucket的名称 495 | :param cos_path: cos的绝对路径, 从bucket根/开始 496 | """ 497 | super(UpdateFileRequest, self).__init__(bucket_name, cos_path) 498 | self._biz_attr = None 499 | self._custom_headers = {} 500 | self._authority = None 501 | self._cache_control = None 502 | self._content_type = None 503 | self._content_disposition = None 504 | self._content_language = None 505 | self._content_encoding = None 506 | self._x_cos_meta_dict = dict() 507 | 508 | def set_biz_attr(self, biz_attr): 509 | """设置biz_attr""" 510 | self._biz_attr = biz_attr 511 | 512 | def get_biz_attr(self): 513 | """获取biz_attr""" 514 | return self._biz_attr 515 | 516 | # 设置authority, 合法取值如下所示 517 | # eInvalid(继承bucket), 518 | # eWRPrivate(私有读写), 519 | # eWPrivateRPublic(私有写, 公有读) 520 | def set_authority(self, authority): 521 | """设置authority, 522 | 523 | 合法取值:eInvalid(继承bucket),eWRPrivate(私有读写),eWPrivateRPublic(私有写, 公有读) 524 | :param authority: 525 | :return: 526 | """ 527 | self._authority = authority 528 | 529 | def get_authority(self): 530 | """获取authority""" 531 | return self._authority 532 | 533 | def set_cache_control(self, cache_control): 534 | """设置缓存机制Cache-Control""" 535 | self._cache_control = cache_control 536 | self._custom_headers[u'Cache-Control'] = cache_control 537 | 538 | def set_content_type(self, content_type): 539 | """设置Content-Type""" 540 | self._content_type = content_type 541 | self._custom_headers['Content-Type'] = content_type 542 | 543 | def set_content_disposition(self, content_disposition): 544 | """设置Content-Disposition""" 545 | self._content_disposition = content_disposition 546 | self._custom_headers['Content-Disposition'] = content_disposition 547 | 548 | def set_content_language(self, content_language): 549 | """设置Content-Language""" 550 | self._content_language = content_language 551 | self._custom_headers['Content-Language'] = content_language 552 | 553 | def set_content_encoding(self, content_encoding): 554 | """设置Content-Encoding""" 555 | self._content_encoding = content_encoding 556 | self._custom_headers['Content-Encoding'] = content_encoding 557 | 558 | def set_x_cos_meta(self, key, value): 559 | """设置自定义的x-cos-meta 560 | 561 | key以x-cos-meta-开头,例如自定义key为u'x-cos-meta-len', value为u'1024' 562 | :param key: 563 | :param value: 564 | :return: 565 | """ 566 | self._x_cos_meta_dict[key] = value 567 | self._custom_headers[key] = value 568 | 569 | def _convert_dict(self, data): 570 | """convert a dict's keys & values from `unicode` to `str` 571 | 572 | :param data: 573 | :return: 574 | """ 575 | if isinstance(data, basestring): 576 | return str(data) 577 | elif isinstance(data, collections.Mapping): 578 | return dict(map(self._convert_dict, data.iteritems())) 579 | elif isinstance(data, collections.Iterable): 580 | return type(data)(map(self._convert_dict, data)) 581 | else: 582 | return data 583 | 584 | def get_custom_headers(self): 585 | """ 获取自定义的HTTP头""" 586 | return self._convert_dict(self._custom_headers) 587 | 588 | def check_params_valid(self): 589 | """ 检查参数是否合法""" 590 | if not super(UpdateFileRequest, self).check_params_valid(): 591 | return False 592 | 593 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=True): 594 | return False 595 | 596 | if self._biz_attr is not None: 597 | if not self._param_check.check_param_unicode('biz_attr', self._biz_attr): 598 | return False 599 | 600 | if self._authority is not None: 601 | if not self._param_check.check_param_unicode('authority', self._authority): 602 | return False 603 | 604 | if self._authority is not None: 605 | if not self._param_check.check_file_authority(self._authority): 606 | return False 607 | 608 | if self._cache_control is not None: 609 | if not self._param_check.check_param_unicode('cache_control', self._cache_control): 610 | return False 611 | 612 | if self._content_type is not None: 613 | if not self._param_check.check_param_unicode('content_type', self._content_type): 614 | return False 615 | 616 | if self._content_disposition is not None: 617 | if not self._param_check.check_param_unicode('content_disposition', self._content_disposition): 618 | return False 619 | 620 | if self._content_language is not None: 621 | if not self._param_check.check_param_unicode('content_language', self._content_language): 622 | return False 623 | 624 | if self._content_encoding is not None: 625 | if not self._param_check.check_param_unicode('content_encoding', self._content_encoding): 626 | return False 627 | 628 | return self._param_check.check_x_cos_meta_dict(self._x_cos_meta_dict) 629 | 630 | 631 | class StatFileRequest(BaseRequest): 632 | """StatRequest 获取文件属性请求""" 633 | 634 | def __init__(self, bucket_name, cos_path): 635 | """ 636 | :param bucket_name: bucket的名称 637 | :param cos_path: cos的文件路径, 从bucket根/开始, 不以/结束 638 | """ 639 | super(StatFileRequest, self).__init__(bucket_name, cos_path) 640 | 641 | def check_params_valid(self): 642 | """检查参数是否合法""" 643 | if not super(StatFileRequest, self).check_params_valid(): 644 | return False 645 | return self._param_check.check_cos_path_valid(self._cos_path, is_file_path=True) 646 | 647 | 648 | class StatFolderRequest(BaseRequest): 649 | """StatRequest 获取目录属性请求 """ 650 | 651 | def __init__(self, bucket_name, cos_path): 652 | """ 653 | 654 | :param bucket_name: bucket的名称 655 | :param cos_path: cos的目录路径, 从bucket根/开始, 以/结束 656 | """ 657 | super(StatFolderRequest, self).__init__(bucket_name, cos_path) 658 | 659 | def check_params_valid(self): 660 | """检查参数是否合法""" 661 | if not super(StatFolderRequest, self).check_params_valid(): 662 | return False 663 | return self._param_check.check_cos_path_valid(self._cos_path, is_file_path=False) 664 | 665 | 666 | class DelFileRequest(BaseRequest): 667 | """ DelFileRequest 删除文件请求 """ 668 | 669 | def __init__(self, bucket_name, cos_path): 670 | """ 671 | 672 | :param bucket_name: bucket的名称 673 | :param cos_path: cos的文件路径, 从bucket根/开始, 不以/结束 674 | """ 675 | super(DelFileRequest, self).__init__(bucket_name, cos_path) 676 | 677 | def check_params_valid(self): 678 | """检查参数是否合法""" 679 | if not super(DelFileRequest, self).check_params_valid(): 680 | return False 681 | return self._param_check.check_cos_path_valid(self._cos_path, is_file_path=True) 682 | 683 | 684 | class DelFolderRequest(BaseRequest): 685 | """DelFolderRequest 删除目录请求""" 686 | 687 | def __init__(self, bucket_name, cos_path): 688 | """ 689 | 690 | :param bucket_name: bucket的名称 691 | :param cos_path: cos的目录路径, 从bucket根/开始, 以/结束 692 | """ 693 | super(DelFolderRequest, self).__init__(bucket_name, cos_path) 694 | 695 | def check_params_valid(self): 696 | """ 检查参数合法""" 697 | if not super(DelFolderRequest, self).check_params_valid(): 698 | return False 699 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=False): 700 | return False 701 | return self._param_check.check_not_cos_root(self._cos_path) 702 | 703 | 704 | class ListFolderRequest(BaseRequest): 705 | """ListFolderRequest 获取目录列表的请求""" 706 | 707 | def __init__(self, bucket_name, cos_path, num=199, prefix=u'', context=u'', delimiter=u''): 708 | """ 709 | :param bucket_name: bucket的名称 710 | :param cos_path: cos的绝对路径, 从bucket根/开始 711 | :param num: 搜索数量 712 | :param prefix: 搜索前缀 713 | :param context: 搜索上下文 714 | :param delimiter: 定义分隔符 715 | """ 716 | super(ListFolderRequest, self).__init__(bucket_name, cos_path) 717 | self._num = num 718 | self._prefix = prefix 719 | self._context = context 720 | self._delimiter = delimiter 721 | 722 | def set_num(self, num): 723 | """设置List数量 724 | 725 | :param num: 726 | :return: 727 | """ 728 | self._num = num 729 | 730 | def get_num(self): 731 | """获取List数量 732 | 733 | :return: 734 | """ 735 | """ 736 | 737 | :return: 738 | """ 739 | return self._num 740 | 741 | def set_prefix(self, prefix): 742 | """设置前缀""" 743 | self._prefix = prefix 744 | 745 | def get_prefix(self): 746 | """获取前缀""" 747 | return self._prefix 748 | 749 | def set_context(self, context): 750 | """设置搜索上下文""" 751 | self._context = context 752 | 753 | def get_context(self): 754 | """获取搜索上下文""" 755 | return self._context 756 | 757 | def set_delimiter(self, delimiter): 758 | """设置分割符""" 759 | self._delimiter = delimiter 760 | 761 | def get_delimiter(self): 762 | """获取分隔符""" 763 | return self._delimiter 764 | 765 | def check_params_valid(self): 766 | """检查参数是否有效""" 767 | if not super(ListFolderRequest, self).check_params_valid(): 768 | return False 769 | if not self._param_check.check_cos_path_valid(self._cos_path, is_file_path=False): 770 | return False 771 | if not self._param_check.check_param_unicode('prefix', self._prefix): 772 | return False 773 | if not self._param_check.check_param_unicode('delimeter', self._delimiter): 774 | return False 775 | return self._param_check.check_param_unicode('context', self._context) 776 | 777 | 778 | class DownloadFileRequest(BaseRequest): 779 | def __init__(self, bucket_name, cos_path, local_filename, range_start=None, range_end=None, *args, **kwargs): 780 | super(DownloadFileRequest, self).__init__(bucket_name, cos_path) 781 | 782 | self._local_filename = local_filename 783 | self._range_start = range_start 784 | self._range_end = range_end 785 | 786 | self._custom_headers = None 787 | if 'headers' in kwargs: 788 | self._custom_headers = kwargs['headers'] 789 | 790 | def check_params_valid(self): 791 | if not super(DownloadFileRequest, self).check_params_valid(): 792 | return False 793 | 794 | from os import path 795 | if path.exists(self._local_filename): 796 | return False 797 | 798 | 799 | class DownloadObjectRequest(BaseRequest): 800 | def __init__(self, bucket_name, cos_path, range_start=None, range_end=None, *args, **kwargs): 801 | super(DownloadObjectRequest, self).__init__(bucket_name, cos_path) 802 | 803 | self._range_start = range_start 804 | self._range_end = range_end 805 | 806 | self._custom_headers = None 807 | if 'headers' in kwargs: 808 | self._custom_headers = kwargs['headers'] 809 | 810 | def check_params_valid(self): 811 | if not super(DownloadObjectRequest, self).check_params_valid(): 812 | return False 813 | 814 | 815 | class MoveFileRequest(BaseRequest): 816 | 817 | def __init__(self, bucket_name, cos_path, dest_path, overwrite=False): 818 | super(MoveFileRequest, self).__init__(bucket_name, cos_path) 819 | self._dest_path = dest_path 820 | if isinstance(overwrite, bool): 821 | if overwrite: 822 | self._overwrite = 1 823 | else: 824 | self._overwrite = 0 825 | else: 826 | raise ValueError("overwrite must be an instance of Boolean") 827 | 828 | @property 829 | def dest_path(self): 830 | return self._dest_path 831 | 832 | @property 833 | def overwrite(self): 834 | return self._overwrite 835 | -------------------------------------------------------------------------------- /qcloud_cos/threadpool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from threading import Thread 4 | from logging import getLogger 5 | from Queue import Queue 6 | from threading import Lock 7 | 8 | logger = getLogger(__name__) 9 | 10 | 11 | class WorkerThread(Thread): 12 | def __init__(self, task_queue, *args, **kwargs): 13 | super(WorkerThread, self).__init__(*args, **kwargs) 14 | 15 | self._task_queue = task_queue 16 | self._succ_task_num = 0 17 | self._fail_task_num = 0 18 | self._ret = list() 19 | 20 | def run(self): 21 | 22 | while True: 23 | func, args, kwargs = self._task_queue.get() 24 | 25 | # 判断进程是否需要退出 26 | if func is None: 27 | return 28 | 29 | try: 30 | ret = func(*args, **kwargs) 31 | self._succ_task_num += 1 32 | self._ret.append(ret) 33 | 34 | except Exception as e: 35 | logger.warn(str(e)) 36 | self._fail_task_num += 1 37 | self._ret.append(e) 38 | finally: 39 | self._task_queue.task_done() 40 | 41 | def get_result(self): 42 | return self._succ_task_num, self._fail_task_num, self._ret 43 | 44 | 45 | class SimpleThreadPool: 46 | 47 | def __init__(self, num_threads=5): 48 | self._num_threads = num_threads 49 | self._queue = Queue(num_threads) 50 | self._lock = Lock() 51 | self._active = False 52 | self._workers = list() 53 | self._finished = False 54 | 55 | def add_task(self, func, *args, **kwargs): 56 | if not self._active: 57 | with self._lock: 58 | if not self._active: 59 | self._active = True 60 | 61 | # 由于是重新创建新的进程队列, 需要确认队列为全新的 62 | self._workers = [] 63 | 64 | for i in range(self._num_threads): 65 | w = WorkerThread(self._queue) 66 | self._workers.append(w) 67 | w.start() 68 | 69 | self._queue.put((func, args, kwargs)) 70 | 71 | def wait_completion(self): 72 | self._queue.join() 73 | self._finished = True 74 | 75 | # 已经结束的任务, 需要将进程都退出, 防止卡死 76 | for i in range(self._num_threads): 77 | self._queue.put((None, None, None)) 78 | 79 | self._active = False 80 | 81 | def get_result(self): 82 | assert self._finished 83 | detail = [worker.get_result() for worker in self._workers] 84 | succ_all = all([tp[1] == 0 for tp in detail]) 85 | return {'success_all': succ_all, 'detail': detail} 86 | 87 | 88 | if __name__ == '__main__': 89 | 90 | pool = SimpleThreadPool(2) 91 | 92 | def task_sleep(x): 93 | from time import sleep 94 | sleep(x) 95 | return 'hello, sleep %d seconds' % x 96 | 97 | def raise_exception(): 98 | raise ValueError("Pa! Exception!") 99 | 100 | pool.add_task(task_sleep, 5) 101 | pool.add_task(task_sleep, 2) 102 | pool.add_task(task_sleep, 3) 103 | pool.add_task(raise_exception) 104 | pool.add_task(raise_exception) 105 | 106 | pool.wait_completion() 107 | print pool.get_result() 108 | # [(1, 0, ['hello, sleep 5 seconds']), (2, 1, ['hello, sleep 2 seconds', 'hello, sleep 3 seconds', ValueError('Pa! Exception!',)])] 109 | -------------------------------------------------------------------------------- /sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from qcloud_cos import CosClient 5 | from qcloud_cos import UploadFileRequest 6 | from qcloud_cos import UploadSliceFileRequest 7 | from qcloud_cos import UploadFileFromBufferRequest 8 | from qcloud_cos import UploadSliceFileFromBufferRequest 9 | from qcloud_cos import UpdateFileRequest 10 | from qcloud_cos import UpdateFolderRequest 11 | from qcloud_cos import DelFileRequest 12 | from qcloud_cos import DelFolderRequest 13 | from qcloud_cos import CreateFolderRequest 14 | from qcloud_cos import StatFileRequest 15 | from qcloud_cos import StatFolderRequest 16 | from qcloud_cos import ListFolderRequest 17 | 18 | import logging 19 | import sys 20 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | def cos_demo(): 25 | # 设置用户属性, 包括appid, secret_id和secret_key 26 | # 这些属性可以在cos控制台获取(https://console.qcloud.com/cos) 27 | appid = 111 # 替换为用户的appid 28 | secret_id = u'' # 替换为用户的secret_id 29 | secret_key = u'' # 替换为用户的secret_key 30 | region = "shanghai" # # 替换为用户的region,目前可以为 shanghai/guangzhou 31 | cos_client = CosClient(appid, secret_id, secret_key, region) 32 | 33 | # 设置要操作的bucket 34 | bucket = u'' 35 | 36 | ############################################################################ 37 | # 文件操作 # 38 | ############################################################################ 39 | # 1. 上传文件(默认不覆盖) 40 | # 将本地的local_file_1.txt上传到bucket的根分区下,并命名为sample_file.txt 41 | # 默认不覆盖, 如果cos上文件存在,则会返回错误 42 | 43 | request = UploadFileRequest(bucket, u'/sample_file.txt', u'local_file_1.txt') 44 | upload_file_ret = cos_client.upload_file(request) 45 | 46 | logger.info("upload file, return message: " + str(upload_file_ret)) 47 | 48 | # 分片上传大文件 49 | 50 | request = UploadSliceFileRequest(bucket, u'/sample_bigfile.txt', u'local_bigfile.txt') 51 | ret = cos_client.upload_slice_file(request) 52 | logger.info("slice upload, return message: " + str(ret)) 53 | 54 | # 2. 上传文件(覆盖文件) 55 | # 将本地的local_file_2.txt上传到bucket的根分区下,覆盖已上传的sample_file.txt 56 | with open('local_file_2.txt', 'w') as f: 57 | f.write("hello world2") 58 | request = UploadFileRequest(bucket, u'/sample_file.txt', u'local_file_2.txt') 59 | request.set_insert_only(0) # 设置允许覆盖 60 | upload_file_ret = cos_client.upload_file(request) 61 | logger.info('overwrite file, return message:' + str(upload_file_ret)) 62 | 63 | # 3. 获取文件属性 64 | request = StatFileRequest(bucket, u'/sample_file.txt') 65 | stat_file_ret = cos_client.stat_file(request) 66 | logger.info('stat file, return message: ' + str(stat_file_ret)) 67 | 68 | # 4. 更新文件属性 69 | request = UpdateFileRequest(bucket, u'/sample_file.txt') 70 | 71 | request.set_biz_attr(u'这是个demo文件') # 设置文件biz_attr属性 72 | request.set_authority(u'eWRPrivate') # 设置文件的权限 73 | request.set_cache_control(u'cache_xxx') # 设置Cache-Control 74 | request.set_content_type(u'application/text') # 设置Content-Type 75 | request.set_content_disposition(u'ccccxxx.txt') # 设置Content-Disposition 76 | request.set_content_language(u'english') # 设置Content-Language 77 | request.set_x_cos_meta(u'x-cos-meta-xxx', u'xxx') # 设置自定义的x-cos-meta-属性 78 | request.set_x_cos_meta(u'x-cos-meta-yyy', u'yyy') # 设置自定义的x-cos-meta-属性 79 | 80 | update_file_ret = cos_client.update_file(request) 81 | logger.info('update file, return message: ' + str(update_file_ret)) 82 | 83 | # 5. 更新后再次获取文件属性 84 | request = StatFileRequest(bucket, u'/sample_file.txt') 85 | stat_file_ret = cos_client.stat_file(request) 86 | logger.info('stat file, return message: ' + str(stat_file_ret)) 87 | 88 | # 6. 从内存上传文件 89 | with open("111.txt", 'rb') as File: 90 | data = File.read() 91 | request = UploadFileFromBufferRequest(bucket, u'/119.txt', data) 92 | request.set_insert_only(0) # 设置允许覆盖 93 | upload_file_ret = cos_client.upload_file_from_buffer(request) 94 | logger.info('overwrite file, return message:' + str(upload_file_ret)) 95 | 96 | ############################################################################ 97 | # 目录操作 # 98 | ############################################################################ 99 | # 1. 生成目录, 目录名为sample_folder 100 | request = CreateFolderRequest(bucket, u'/sample_folder/') 101 | create_folder_ret = cos_client.create_folder(request) 102 | logger.info('create folder ret:' + str(create_folder_ret)) 103 | 104 | # 2. 更新目录的biz_attr属性 105 | request = UpdateFolderRequest(bucket, u'/sample_folder/', u'这是一个测试目录') 106 | update_folder_ret = cos_client.update_folder(request) 107 | logger.info('update folder ret:' + str(update_folder_ret)) 108 | 109 | # 3. 获取目录属性 110 | request = StatFolderRequest(bucket, u'/sample_folder/') 111 | stat_folder_ret = cos_client.stat_folder(request) 112 | logger.info("stat folder, return message: " + str(stat_folder_ret)) 113 | 114 | # 4. list目录, 获取目录下的成员 115 | request = ListFolderRequest(bucket, u'/sample_folder/') 116 | list_folder_ret = cos_client.list_folder(request) 117 | logger.info("list folder, return message: " + str(list_folder_ret)) 118 | # 5. 删除目录 119 | request = DelFolderRequest(bucket, u'/sample_folder/') 120 | delete_folder_ret = cos_client.del_folder(request) 121 | logger.info("delete folder, return message: " + str(delete_folder_ret)) 122 | 123 | if __name__ == '__main__': 124 | cos_demo() 125 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | import sys 5 | from setuptools import setup, find_packages 6 | 7 | if sys.version_info[0] != 2 or sys.version_info[1] < 6: 8 | sys.exit('Sorry, only python 2.6 or 2.7 is supported') 9 | 10 | setup(name='qcloud_cos_v4', 11 | version='0.0.27', 12 | description='python sdk for tencent qcloud cos v4.0', 13 | long_description=open('README.rst', 'r').read(), 14 | license='MIT License', 15 | install_requires=['requests'], 16 | author='liuchang0812', 17 | author_email='liuchang0812@gmail.com', 18 | url='https://www.qcloud.com', 19 | packages=find_packages()) 20 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tencentyun/cos-python-sdk-v4/e500a37314df5903d5b0372da473abda186da101/test/__init__.py -------------------------------------------------------------------------------- /test/test_simple.py: -------------------------------------------------------------------------------- 1 | from qcloud_cos.cos_request import UploadFileRequest 2 | 3 | import unittest 4 | 5 | class TestRequestCase(unittest.TestCase): 6 | 7 | def test_upload_file_request(self): 8 | req = UploadFileRequest('bucketname', '/a/b/c.jpg', '/tmp/a.jpg') 9 | self.assertEqual(req.get_local_path(), '/tmp/a.jpg') 10 | self.assertEqual(req.get_insert_only(), 1) 11 | 12 | --------------------------------------------------------------------------------