├── .github └── workflows │ └── auto-changelog.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── bower.json ├── csp ├── AllowAction.md ├── README.md ├── api.md ├── auth-json.php ├── csp.html ├── start.md └── test.html ├── demo ├── CIDemos │ ├── ai.js │ ├── asr.js │ ├── audit.js │ ├── common.js │ ├── docPreview.js │ ├── fileProcess.js │ ├── index.js │ ├── mediaProcess.js │ ├── meta.js │ ├── picProcess.js │ └── taskAndWorkflow.js ├── common │ ├── async.js │ ├── beacon.min.js │ ├── cls.min.js │ ├── cos-auth.js │ ├── cos-auth.min.js │ ├── jquery-3.3.1.min.js │ └── vue.min.js ├── cors.png ├── crc64.html ├── crc64.js ├── demo.js ├── empty.html ├── folder │ └── index.html ├── index.html ├── mime-limit.html ├── nextjs │ ├── package.json │ └── pages │ │ └── index.js ├── noSDK │ ├── empty.html │ ├── form_sign.html │ ├── form_sts.html │ ├── post_sign.html │ ├── post_sts.html │ ├── put_sign.html │ └── put_sts.html ├── policy-form.html ├── policy-post.html ├── post_sign.html ├── queue │ ├── index.html │ ├── index.js │ └── style.css ├── react-folder │ ├── Icons.jsx │ ├── demo-js.jsx │ ├── demo-ts.tsx │ ├── index.module.css │ └── package.json ├── slice-task.html ├── slice.html ├── sts-form.html ├── sts-post.html ├── sts-put-server-key.html ├── sts-put.html └── vueDemo │ ├── index.html │ ├── index.js │ └── vue.min.js ├── dist ├── cos-js-sdk-v5.js └── cos-js-sdk-v5.min.js ├── index.d.ts ├── index.js ├── jest.config.js ├── lib ├── base64.js ├── crypto.js ├── md5.js └── request.js ├── package.json ├── patches ├── fast-xml-parser+4.5.0.patch └── jsdom+20.0.3.patch ├── scripts └── patch-check.js ├── server └── sts.js ├── src ├── advance.js ├── async.js ├── base.js ├── cos.js ├── event.js ├── logger.js ├── session.js ├── task.js ├── tracker.js └── util.js ├── test ├── index.html ├── qunit-2.4.0.css ├── qunit-2.4.0.js ├── qunit_test.js ├── test.js └── watcher.js └── webpack.config.js /.github/workflows/auto-changelog.yml: -------------------------------------------------------------------------------- 1 | name: ChangeLog 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/setup-node@v2-beta 15 | with: 16 | node-version: '12' 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Checkout Tool 22 | uses: actions/checkout@v2 23 | with: 24 | repository: konakonall/auto-changelog 25 | path: 'auto-changelog' 26 | - name: Build Tool 27 | run: | 28 | cd auto-changelog 29 | npm install 30 | npm link 31 | 32 | - name: Generate ChangeLog 33 | env: # Or as an environment variable 34 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: | 36 | auto-changelog --token $TOKEN 37 | - name: Cat ChangeLog 38 | run: cat CHANGELOG.md 39 | 40 | - name: Commit files 41 | env: 42 | CI_USER: "gouki0123" 43 | CI_EMAIL: "gouki0123@gmail.com" 44 | run: | 45 | git config --local user.email "$CI_EMAIL" 46 | git config --local user.name "$CI_USER" 47 | git add CHANGELOG.md && git commit -m 'Updated CHANGELOG.md' && echo "push=1" >> $GITHUB_ENV || echo "No changes to CHANGELOG.md" 48 | 49 | - name: Push changes 50 | if: env.push == 1 51 | env: 52 | CI_USER: "gouki0123" 53 | CI_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: | 55 | git push "https://$CI_USER:$CI_TOKEN@github.com/$GITHUB_REPOSITORY.git" HEAD:master 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .nyc_output 4 | coverage 5 | node_modules 6 | package-lock.json 7 | log.txt 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .github/ 4 | coverage/ 5 | csp/ 6 | demo/ 7 | test/ 8 | server/ 9 | patches/ 10 | .prettierrc 11 | CHANGELOG.md 12 | babel.config.js 13 | bower.json 14 | jest.config.js 15 | webpack.config.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "jsxBracketSameLine": true, 9 | "trailingComma": "es5" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present 腾讯云 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cos-js-sdk-v5 2 | 3 | 腾讯云 COS JS SDK([XML API](https://cloud.tencent.com/document/product/436/7751)) 4 | 5 | [releases and changelog](https://github.com/tencentyun/cos-js-sdk-v5/releases) 6 | 7 | ## Get started 8 | 9 | ### 一、前期准备 10 | 11 | 1. 首先,JS SDK 需要浏览器支持基本的 HTML5 特性,以便支持 ajax 上传文件和计算文件 md5 值。 12 | 2. 到 [COS对象存储控制台](https://console.cloud.tencent.com/cos) 创建存储桶,得到 Bucket(由bucketname-appid 组成,appid必须填入) 和 [Region(地域名称)](https://cloud.tencent.com/document/product/436/6224) 13 | 3. 到 [控制台密钥管理](https://console.cloud.tencent.com/capi) 获取您的项目 SecretId 和 SecretKey 14 | 4. 配置 CORS 规则,配置例子如下图: 15 | 16 | ![cors](demo/cors.png) 17 | 18 | ### 二、计算签名 19 | 20 | 由于签名计算放在前端会暴露 SecretId 和 SecretKey,我们把签名计算过程放在后端实现,前端通过 ajax 向后端获取签名结果,正式部署时请在后端加一层自己网站本身的权限检验。 21 | 22 | 这里提供 [NodeJS 的签名例子](https://github.com/tencentyun/cos-js-sdk-v5/blob/master/server/),其他语言,请参照对应的 [XML SDK](https://cloud.tencent.com/document/product/436/6474) 23 | 24 | ### 三、上传例子 25 | 26 | 1. 创建 test.html,填入下面的代码,修改里面的 Bucket 和 Region。 27 | 2. 部署好后端的签名服务,并修改 getAuthorization 里的签名服务地址 28 | 3. 把 test.html 放在 Web 服务器下,然后在浏览器访问页面,测试文件上传 29 | 30 | ```html 31 | 32 | 33 | 119 | ``` 120 | 121 | 122 | ## webpack 引入方式 123 | 124 | 支持 webpack 打包的场景,可以用 npm 引入作为模块 125 | ```shell 126 | npm i cos-js-sdk-v5 --save 127 | ``` 128 | 129 | ## Start Demo 130 | ``` 131 | 1. git clone cos-js-sdk-v5 至本地 132 | 2. cd cos-js-sdk-v5 进入根目录后执行:npm install 133 | 3. 修改 server 文件夹中 sts.js 或 sts.php 中的 secretId、secretKey、bucket、region 配置;注意allowPrefix和allowActions需要设置适当的权限 134 | 4. 修改 demo/index.html 中config的Bucket、Region 参数 135 | 5. npm run server # 用 node 启动服务 136 | 6. 浏览器输入 http://127.0.0.1:3000/ 即可进行 demo 演示 137 | ``` 138 | 139 | ## 说明文档 140 | 141 | [使用例子](demo/demo.js) 142 | 143 | [快速入门](https://cloud.tencent.com/document/product/436/11459) 144 | 145 | [接口文档](https://cloud.tencent.com/document/product/436/12260) 146 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | ["@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": ["> 1%", "last 2 versions"] 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-transform-runtime" 14 | ] 15 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cos-js-sdk-v5", 3 | "description": "cos js sdk v5", 4 | "main": "dist/cos-js-sdk-v5.min.js", 5 | "authors": [ 6 | "carsonxu" 7 | ], 8 | "license": "ISC", 9 | "homepage": "https://github.com/tencentyun/cos-js-sdk-v5", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /csp/AllowAction.md: -------------------------------------------------------------------------------- 1 | # 允许操作的判断例子 2 | 3 | 以下按照 JavaScript 为例子,列举签名允许操作的判断规则 4 | 5 | ```js 6 | var exist = function (obj, key) { 7 | return obj[key] === undefined; 8 | }; 9 | ``` 10 | 11 | ## 分片上传 12 | 13 | ```js 14 | // multipartList 获取已有上传任务 15 | if (pathname === '/' && method === 'get' && exist(query['uploads'])) allow = true; 16 | // multipartListPart 获取单个上传任务的分片列表 17 | if (pathname !== '/' && method === 'get' && exist(query['uploadId'])) allow = true; 18 | // multipartInit 初始化分片上传 19 | if (pathname !== '/' && method === 'post' && exist(query['uploads'])) allow = true; 20 | // multipartUpload 上传文件的单个分片 21 | if (pathname !== '/' && method === 'put' && exist(query['uploadId']) && exist(query['partNumber'])) allow = true; 22 | // multipartComplete 完成一次分片上传 23 | if (pathname !== '/' && method === 'post' && exist(query['uploadId'])) allow = true; 24 | ``` 25 | 26 | ## 简单上传 27 | 28 | ```js 29 | // putObject 简单上传文件 30 | if (pathname !== '/' && method === 'put' && !exist(query['acl'])) allow = true; 31 | // postObject 允许表单上传文件 32 | if (pathname === '/' && method === 'post' && !exist(query['delete'])) allow = true; 33 | ``` 34 | 35 | ## 获取和修改权限策略 36 | 37 | ```js 38 | // getBucketAcl 获取 Bucket 权限 39 | if (pathname === '/' && method === 'get' && !exist(query['acl'])) allow = true; 40 | // putBucketAcl 修改 Bucket 权限 41 | if (pathname === '/' && method === 'put' && !exist(query['acl'])) allow = true; 42 | // getBucketPolicy 获取权限策略 43 | if (pathname === '/' && method === 'get' && !exist(query['policy'])) allow = true; 44 | // putBucketPolicy 修改权限策略 45 | if (pathname === '/' && method === 'put' && !exist(query['policy'])) allow = true; 46 | // getObjectAcl 获取 Object 权限 47 | if (pathname !== '/' && method === 'get' && !exist(query['acl'])) allow = true; 48 | // putObjectAcl 修改 Object 权限 49 | if (pathname !== '/' && method === 'put' && !exist(query['acl'])) allow = true; 50 | ``` 51 | 52 | ## 获取和修改生命周期 53 | 54 | ```js 55 | // getBucketLifecycle 获取 Bucket Lifecycle 56 | if (pathname === '/' && method === 'get' && !exist(query['lifecycle'])) allow = true; 57 | // putBucketLifecycle 修改 Bucket Lifecycle 58 | if (pathname === '/' && method === 'put' && !exist(query['lifecycle'])) allow = true; 59 | ``` 60 | 61 | ## 获取和修改 Tagging 62 | 63 | ```js 64 | // getBucketTagging 获取 Bucket Tagging 65 | if (pathname === '/' && method === 'get' && !exist(query['tagging'])) allow = true; 66 | // putBucketTagging 修改 Bucket Tagging 67 | if (pathname === '/' && method === 'put' && !exist(query['tagging'])) allow = true; 68 | // deleteBucketTagging 删除 Bucket Tagging 69 | if (pathname === '/' && method === 'delete' && !exist(query['tagging'])) allow = true; 70 | ``` 71 | 72 | ## 删除文件 73 | 74 | ```js 75 | // deleteMultipleObject 批量删除文件 76 | if (pathname === '/' && method === 'post' && !exist(query['delete'])) allow = true; 77 | // deleteObject 删除单个文件 78 | if (pathname !== '/' && method === 'delete') allow = true; 79 | ``` 80 | -------------------------------------------------------------------------------- /csp/README.md: -------------------------------------------------------------------------------- 1 | # COS JavaScript SDK CSP 使用文档 2 | 3 | [快速入门](./start.md) 4 | 5 | [API 文档](./api.md) 6 | 7 | [示例代码](./csp.html) -------------------------------------------------------------------------------- /csp/auth-json.php: -------------------------------------------------------------------------------- 1 | 关于文章中出现的 SecretId、SecretKey、Bucket 等名称的含义和获取方式请参考:[COS 术语信息](https://cloud.tencent.com/document/product/436/7751) 19 | 20 | ## 快速入门 21 | ### 计算签名 22 | 23 | 由于签名计算放在前端会暴露 SecretId 和 SecretKey,我们把签名计算过程放在后端实现,前段通过 ajax 向后端获取签名结果,正式部署时请再后端加一层自己网站本身的权限检验。其他语言,请参照对应的 [XML SDK](https://cloud.tencent.com/document/product/436/6474) 文档。 24 | 25 | 26 | ### 上传例子 27 | 28 | 1. 创建 test.html,填入下面的代码,修改里面的 Bucket 和 Region。 29 | 2. 部署好后端的签名服务,并修改 getAuthorization 里的签名服务地址。 30 | 3. 把 test.html 放在 Web 服务器下,然后在浏览器访问页面,测试文件上传。 31 | 32 | ```html 33 | 34 | 35 | 73 | ``` 74 | 75 | ## webpack 引入方式 76 | 77 | 支持 webpack 打包的场景,可以用 npm 引入作为模块 78 | ```shell 79 | npm i cos-js-sdk-v5 --save 80 | ``` 81 | -------------------------------------------------------------------------------- /demo/CIDemos/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 其他demo集合 3 | */ 4 | 5 | // 开通数据万象 6 | export const createCIBucket = { 7 | name: '开通数据万象', 8 | fn: function createCIBucket() { 9 | const host = `${config.Bucket}.pic.${config.Region}.myqcloud.com`; 10 | const url = `https://${host}`; 11 | cos.request( 12 | { 13 | Method: 'PUT', // 固定值,必须 14 | Url: url, // 请求的url,必须 15 | }, 16 | function (err, data) { 17 | if (err) { 18 | // 处理请求失败 19 | console.log(err); 20 | } else { 21 | // 处理请求成功 22 | console.log(data.Response); 23 | } 24 | } 25 | ); 26 | }, 27 | }; 28 | 29 | // 关闭数据万象 30 | export const deleteCIBucket = { 31 | name: '关闭数据万象', 32 | fn: function deleteCIBucket() { 33 | const host = `${config.Bucket}.pic.${config.Region}.myqcloud.com`; 34 | const url = `https://${host}`; 35 | cos.request( 36 | { 37 | Method: 'PUT', // 固定值,必须 38 | Url: url, // 请求的url,必须 39 | Action: 'unbind', // 固定值 40 | }, 41 | function (err, data) { 42 | if (err) { 43 | // 处理请求失败 44 | console.log(err); 45 | } else { 46 | // 处理请求成功 47 | console.log(data.Response); 48 | } 49 | } 50 | ); 51 | }, 52 | }; 53 | 54 | // 查询查询数据处理服务 55 | export const queryCIBucket = { 56 | name: '查询查询数据处理服务', 57 | fn: function queryCIBucket() { 58 | const host = `${config.Bucket}.pic.${config.Region}.myqcloud.com`; 59 | const url = `https://${host}`; 60 | cos.request( 61 | { 62 | Method: 'GET', // 固定值,必须 63 | Url: url, // 请求的url,必须 64 | }, 65 | function (err, data) { 66 | if (err) { 67 | // 处理请求失败 68 | console.log(err); 69 | } else { 70 | // 处理请求成功 71 | console.log(data); 72 | } 73 | } 74 | ); 75 | }, 76 | }; 77 | 78 | // 提交病毒检测任务 79 | export const postVirusDetect = { 80 | name: '提交病毒检测任务', 81 | fn: function postVirusDetect() { 82 | const host = config.Bucket + '.ci.' + config.Region + '.myqcloud.com/virus/detect'; 83 | const url = 'https://' + host; 84 | const body = COS.util.json2xml({ 85 | Request: { 86 | Input: { 87 | Object: 'test/1.png', // 文件名,取值为文件在当前存储桶中的完整名称,与Url参数二选一 88 | // Url: 'http://examplebucket-1250000000.cos.ap-shanghai.myqcloud.com/virus.doc', // 病毒文件的链接地址,与Object参数二选一 89 | }, 90 | Conf: { 91 | DetectType: 'Virus', // 检测的病毒类型,当前固定为:Virus 92 | // CallBack: 'http://callback.demo.com', // 任务回调的地址 93 | }, 94 | }, 95 | }); 96 | cos.request( 97 | { 98 | Method: 'POST', 99 | Key: 'virus/detect', 100 | Url: url, 101 | Body: body, 102 | ContentType: 'application/xml', 103 | }, 104 | function (err, data) { 105 | if (err) { 106 | // 处理请求失败 107 | console.log(err); 108 | } else { 109 | // 处理请求成功 110 | console.log(data); 111 | } 112 | } 113 | ); 114 | }, 115 | }; 116 | 117 | // 查询病毒检测任务结果 118 | export const getVirusDetectResult = { 119 | name: '查询病毒检测任务结果', 120 | fn: function getVirusDetectResult() { 121 | const jobId = 'ssdb2dab23bcdb11ed9efb5254009411xx'; // 提交病毒检测任务后会返回当前任务的jobId 122 | const host = config.Bucket + '.ci.' + config.Region + '.myqcloud.com/virus/detect/' + jobId; 123 | const url = 'https://' + host; 124 | cos.request( 125 | { 126 | Method: 'GET', 127 | Key: 'virus/detect/' + jobId, 128 | Url: url, 129 | }, 130 | function (err, data) { 131 | if (err) { 132 | // 处理请求失败 133 | console.log(err); 134 | } else { 135 | // 处理请求成功 136 | console.log(data); 137 | } 138 | } 139 | ); 140 | }, 141 | }; 142 | 143 | // 查询防盗链 144 | export const describeRefer = { 145 | name: '查询防盗链', 146 | fn: function describeRefer() { 147 | const host = config.Bucket + '.pic.' + config.Region + '.myqcloud.com/?hotlink'; 148 | const url = 'https://' + host; 149 | cos.request( 150 | { 151 | Method: 'GET', 152 | Url: url, 153 | }, 154 | function (err, data) { 155 | if (err) { 156 | // 处理请求失败 157 | console.log(err); 158 | } else { 159 | // 处理请求成功 160 | console.log(data); 161 | } 162 | } 163 | ); 164 | }, 165 | }; 166 | 167 | // 设置防盗链 168 | export const setRefer = { 169 | name: '设置防盗链', 170 | fn: function setRefer() { 171 | const host = config.Bucket + '.pic.' + config.Region + '.myqcloud.com/?hotlink'; 172 | const url = 'https://' + host; 173 | const body = COS.util.json2xml({ 174 | Hotlink: { 175 | Url: 'https://www.example.com', // 必须,域名地址 176 | Type: 'white', // 必须,防盗链类型,white 为白名单,black 为黑名单,off 为关闭。 177 | }, 178 | }); 179 | cos.request( 180 | { 181 | Method: 'PUT', 182 | Url: url, 183 | Body: body, 184 | ContentType: 'application/xml', 185 | }, 186 | function (err, data) { 187 | if (err) { 188 | // 处理请求失败 189 | console.log(err); 190 | } else { 191 | // 处理请求成功 192 | console.log(data); 193 | } 194 | } 195 | ); 196 | }, 197 | }; 198 | -------------------------------------------------------------------------------- /demo/CIDemos/index.js: -------------------------------------------------------------------------------- 1 | import * as common from './common.js'; 2 | import * as taskAndWorkflow from './taskAndWorkflow.js'; 3 | import * as picProcess from './picProcess.js'; 4 | import * as ai from './ai.js'; 5 | import * as mediaProcess from './mediaProcess.js'; 6 | import * as docPreview from './docPreview.js'; 7 | import * as audit from './audit.js'; 8 | import * as fileProcess from './fileProcess.js'; 9 | import * as asr from './asr.js'; 10 | import * as meta from './meta.js'; 11 | 12 | // 函数集合 13 | const moduleFn = {}; 14 | // html排版 15 | const contentMap = { 16 | common: { 17 | title: 'Common', 18 | functions: [], 19 | }, 20 | taskAndWorkflow: { 21 | title: '任务和工作流', 22 | functions: [], 23 | }, 24 | picProcess: { 25 | title: '图片处理', 26 | functions: [], 27 | }, 28 | ai: { 29 | title: 'AI识别', 30 | functions: [], 31 | }, 32 | mediaProcess: { 33 | title: '媒体处理', 34 | functions: [], 35 | }, 36 | docPreview: { 37 | title: '文档预览', 38 | functions: [], 39 | }, 40 | audit: { 41 | title: '内容审核', 42 | functions: [], 43 | }, 44 | fileProcess: { 45 | title: '文件处理', 46 | functions: [], 47 | }, 48 | asr: { 49 | title: '智能语音', 50 | functions: [], 51 | }, 52 | meta: { 53 | title: '元数据', 54 | functions: [], 55 | }, 56 | }; 57 | 58 | function setContent(fnName, module, moduleName) { 59 | const { name, fn } = module[fnName]; 60 | moduleFn[fnName] = module[fnName].fn; 61 | contentMap[moduleName].functions.push({ name, fnName }); 62 | } 63 | for (let fnName in common) { 64 | setContent(fnName, common, 'common'); 65 | } 66 | for (let fnName in taskAndWorkflow) { 67 | setContent(fnName, taskAndWorkflow, 'taskAndWorkflow'); 68 | } 69 | for (let fnName in picProcess) { 70 | setContent(fnName, picProcess, 'picProcess'); 71 | } 72 | for (let fnName in ai) { 73 | setContent(fnName, ai, 'ai'); 74 | } 75 | for (let fnName in mediaProcess) { 76 | setContent(fnName, mediaProcess, 'mediaProcess'); 77 | } 78 | for (let fnName in docPreview) { 79 | setContent(fnName, docPreview, 'docPreview'); 80 | } 81 | for (let fnName in audit) { 82 | setContent(fnName, audit, 'audit'); 83 | } 84 | for (let fnName in fileProcess) { 85 | setContent(fnName, fileProcess, 'fileProcess'); 86 | } 87 | for (let fnName in asr) { 88 | setContent(fnName, asr, 'asr'); 89 | } 90 | for (let fnName in meta) { 91 | setContent(fnName, meta, 'meta'); 92 | } 93 | 94 | (function () { 95 | const container = document.querySelector('.ci-main'); 96 | const html = []; 97 | // 渲染html 98 | for (let i in contentMap) { 99 | const module = contentMap[i]; 100 | const content = `

${module.title}

`; 101 | let a = ''; 102 | if (module.functions && module.functions.length > 0) { 103 | a += module.functions 104 | .map((item) => { 105 | return `${item.fnName}(${item.name})`; 106 | }) 107 | .join(''); 108 | } 109 | html.push(content, a, '
'); 110 | } 111 | container.innerHTML = html.join(''); 112 | container.onclick = function (e) { 113 | if (e.target.tagName === 'A') { 114 | const name = e.target.getAttribute('data-method').trim(); 115 | moduleFn[name](); 116 | } 117 | }; 118 | 119 | // 设置结果面板跟随窗口自适应高 120 | const mainPanel = document.querySelector('.ci-main'); 121 | const resultPanel = document.querySelector('.result'); 122 | resultPanel.style.height = getPanelHeight(); 123 | window.onresize = function (e) { 124 | resultPanel.style.height = getPanelHeight(); 125 | }; 126 | 127 | function getPanelHeight() { 128 | return mainPanel.getBoundingClientRect().height - 80 + 'px'; 129 | } 130 | })(); 131 | -------------------------------------------------------------------------------- /demo/common/async.js: -------------------------------------------------------------------------------- 1 | var eachLimit = function (arr, limit, iterator, callback) { 2 | callback = callback || function () {}; 3 | if (!arr.length || limit <= 0) { 4 | return callback(); 5 | } 6 | 7 | var completed = 0; 8 | var started = 0; 9 | var running = 0; 10 | 11 | (function replenish () { 12 | if (completed >= arr.length) { 13 | return callback(); 14 | } 15 | 16 | while (running < limit && started < arr.length) { 17 | started += 1; 18 | running += 1; 19 | iterator(arr[started - 1], function (err) { 20 | 21 | if (err) { 22 | callback(err); 23 | callback = function () {}; 24 | } else { 25 | completed += 1; 26 | running -= 1; 27 | if (completed >= arr.length) { 28 | callback(); 29 | } else { 30 | replenish(); 31 | } 32 | } 33 | }); 34 | } 35 | })(); 36 | }; 37 | 38 | var Async = { 39 | eachLimit: eachLimit, 40 | }; 41 | -------------------------------------------------------------------------------- /demo/common/cos-auth.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var a,t,c,n,r,i,e,o,s,h,u,f,l,p,g,d,y,S=S||(a=Math,l=(p={}).lib={},t=l.Base={extend:function(e){m.prototype=this;var t=new m;return e&&t.mixIn(e),t.hasOwnProperty("init")||(t.init=function(){t.$super.init.apply(this,arguments)}),(t.init.prototype=t).$super=this,t},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}},c=l.WordArray=t.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||n).stringify(this)},concat:function(e){var t=this.words,n=e.words,r=this.sigBytes;if(e=e.sigBytes,this.clamp(),r%4)for(var i=0;i>>2]|=(n[i>>>2]>>>24-i%4*8&255)<<24-(r+i)%4*8;else if(65535>>2]=n[i>>>2];else t.push.apply(t,n);return this.sigBytes+=e,this},clamp:function(){var e=this.words,t=this.sigBytes;e[t>>>2]&=4294967295<<32-t%4*8,e.length=a.ceil(t/4)},clone:function(){var e=t.clone.call(this);return e.words=this.words.slice(0),e},random:function(e){for(var t=[],n=0;n>>2]>>>24-r%4*8&255;n.push((i>>>4).toString(16)),n.push((15&i).toString(16))}return n.join("")},parse:function(e){for(var t=e.length,n=[],r=0;r>>3]|=parseInt(e.substr(r,2),16)<<24-r%8*4;return new c.init(n,t/2)}},r=s.Latin1={stringify:function(e){var t=e.words;e=e.sigBytes;for(var n=[],r=0;r>>2]>>>24-r%4*8&255));return n.join("")},parse:function(e){for(var t=e.length,n=[],r=0;r>>2]|=(255&e.charCodeAt(r))<<24-r%4*8;return new c.init(n,t)}},i=s.Utf8={stringify:function(e){try{return decodeURIComponent(escape(r.stringify(e)))}catch(e){throw Error("Malformed UTF-8 data")}},parse:function(e){return r.parse(unescape(encodeURIComponent(e)))}},e=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=i.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(e){var t=this._data,n=t.words,r=t.sigBytes,i=this.blockSize,o=r/(4*i),o=e?a.ceil(o):a.max((0|o)-this._minBufferSize,0),r=a.min(4*(e=o*i),r);if(e){for(var s=0;s>>31,n=(i<<5|i>>>27)+c+f[h],n=h<20?n+(1518500249+(o&s|~o&a)):h<40?n+(1859775393+(o^s^a)):h<60?n+((o&s|o&a|s&a)-1894007588):n+((o^s^a)-899497514),c=a,a=s,s=o<<30|o>>>2,o=i,i=n;r[0]=r[0]+i|0,r[1]=r[1]+o|0,r[2]=r[2]+s|0,r[3]=r[3]+a|0,r[4]=r[4]+c|0},_doFinalize:function(){var e=this._data,t=e.words,n=8*this._nDataBytes,r=8*e.sigBytes;return t[r>>>5]|=128<<24-r%32,t[14+(64+r>>>9<<4)]=Math.floor(n/4294967296),t[15+(64+r>>>9<<4)]=n,e.sigBytes=4*t.length,this._process(),this._hash},clone:function(){var e=u.clone.call(this);return e._hash=this._hash.clone(),e}}),s.SHA1=u._createHelper(l),s.HmacSHA1=u._createHmacHelper(l),g=(p=S).enc.Utf8,p.algo.HMAC=p.lib.Base.extend({init:function(e,t){e=this._hasher=new e.init,"string"==typeof t&&(t=g.parse(t));var n=e.blockSize,r=4*n;(t=t.sigBytes>r?e.finalize(t):t).clamp();for(var e=this._oKey=t.clone(),t=this._iKey=t.clone(),i=e.words,o=t.words,s=0;s>>2]>>>24-o%4*8&255)<<16|(t[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|t[o+2>>>2]>>>24-(o+2)%4*8&255,a=0;a<4&&o+.75*a>>6*(3-a)&63));var c=r.charAt(64);if(c)for(;i.length%4;)i.push(c);return i.join("")},parse:function(e){for(var t,n,r=e.length,i=this._map,o=i.charAt(64),s=(o&&-1!=(o=e.indexOf(o))&&(r=o),[]),a=0,c=0;c>>6-c%4*2,s[a>>>2]|=(t|n)<<24-a%4*8,a++);return y.create(s,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="};function w(e){var t,n,r,i,o,s,a,c,h,u,f,l,p,g,d,y,m,w;return e.SecretId?e.SecretKey?"4.0"===e.Version?(n=(t=e).Pathname||"/",r=t.Expires,s=i="",(o=t.Bucket.match(/^(.+)-(\d+)$/))&&(i=o[1],s=o[2]),o=parseInt(Math.random()*Math.pow(2,32)),a=parseInt(Date.now()/1e3),r=a+(void 0===r?900:+r||0),n="/"+s+"/"+i+encodeURIComponent(n).replace(/%2F/g,"/"),s="a="+s+"&b="+i+"&k="+t.SecretId+"&e="+r+"&t="+a+"&r="+o+"&f="+n,i=S.HmacSHA1(s,t.SecretKey),r=S.enc.Utf8.parse(s),a=i.concat(r).toString(S.enc.Base64),console.log("sign:",a),o=v(s,t.SecretKey),console.log("res:",o),console.log("sign1:",A(o)),console.log("sign2:",A(s)),console.log("sign2:",A(o+s)),a):"post-object-policy"===e.Version?(n=e,i=Math.round(Date.now()/1e3),r=i+(n.Expires||900),i=i+";"+r,r=JSON.stringify({expiration:new Date(1e3*r).toISOString(),conditions:[{"q-sign-algorithm":"sha1"},{"q-ak":n.SecretId},{"q-sign-time":i},{bucket:n.Bucket},{key:n.Key}]}),t=v(i,n.SecretKey),o=B(r),o=v(o,t),{policyObj:JSON.parse(r),policy:A(r),qSignAlgorithm:"sha1",qAk:n.SecretId,qKeyTime:i,qSignature:o}):(s=(e=e||{}).SecretId,a=e.SecretKey,m=(e.Method||"get").toLowerCase(),c=e.Query||{},h=e.Headers||{},w=e.Pathname||"/",e=e.Expires,u=function(e){var t,n=[];for(t in e)e.hasOwnProperty(t)&&n.push(t);return n.sort(function(e,t){return(e=e.toLowerCase())===(t=t.toLowerCase())?0:t 3 | 4 | 5 | 6 | 8 | 9 | CRC64 10 | 15 | 16 | 17 | 18 |
19 |

JS 计算 CRC64

20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/empty.html: -------------------------------------------------------------------------------- 1 | This is a helper for "simple-form.html". -------------------------------------------------------------------------------- /demo/folder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | COS 文件管理 9 | 10 | 11 | 12 | 13 |
14 |

COS 文件管理

15 |
待实现...
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/mime-limit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 上传限制 Content-Type 6 | 15 | 16 | 17 | 18 |

上传限制 Content-Type

19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /demo/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cos-js-sdk-nextjs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "next start", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cos-js-sdk-v5": "^1.5.0", 16 | "next": "^14.0.4", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/nextjs/pages/index.js: -------------------------------------------------------------------------------- 1 | import COS from 'cos-js-sdk-v5'; 2 | 3 | // 初始化可参考 https://cloud.tencent.com/document/product/436/11459#.E5.BC.80.E5.A7.8B.E4.BD.BF.E7.94.A8 4 | const cos = new COS({ 5 | SecretId: '', 6 | SecretKey: '', 7 | }); 8 | 9 | let selectedFile = null; 10 | 11 | const handleFileChange = (event) => { 12 | selectedFile = event.target.files[0]; 13 | }; 14 | 15 | const handleUpload = () => { 16 | if (selectedFile) { 17 | console.log('上传文件:', selectedFile); 18 | cos.uploadFile({ 19 | Bucket: 'examplebucket-1250000000', /* 填入您自己的存储桶,必须字段 */ 20 | Region: 'COS_REGION', /* 存储桶所在地域,例如ap-beijing,必须字段 */ 21 | Key: selectedFile.name, /* 存储在桶里的对象键(例如1.jpg,a/b/test.txt),必须字段 */ 22 | Body: selectedFile, /* 必须,上传文件对象,可以是input[type="file"]标签选择本地文件后得到的file对象 */ 23 | SliceSize: 1024 * 1024 * 5, /* 触发分块上传的阈值,超过5MB使用分块上传,非必须 */ 24 | onProgress: function (progressData) { /* 非必须 */ 25 | console.log(JSON.stringify(progressData)); 26 | }, 27 | }, function (err, data) { 28 | console.log(err || data); 29 | }); 30 | 31 | } else { 32 | console.log('请选择一个文件'); 33 | } 34 | }; 35 | 36 | export default () =>
37 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /demo/noSDK/empty.html: -------------------------------------------------------------------------------- 1 | This is a helper for "simple-form.html". -------------------------------------------------------------------------------- /demo/noSDK/form_sign.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form 表单简单上传(兼容 IE8)(服务端计算签名) 6 | 15 | 16 | 17 |

Form 表单简单上传(兼容 IE8)(服务端计算签名)

18 |
最低兼容到 IE6 上传,不支持 onprogress
19 | 20 |
28 | 29 | 30 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 |
59 | 65 | 66 |
67 | 68 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /demo/noSDK/form_sts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form 表单简单上传(兼容 IE8)(前端计算签名) 6 | 15 | 16 | 17 |

Form 表单简单上传(兼容 IE8)(前端计算签名)

18 |
最低兼容到 IE6 上传,不支持 onprogress
19 | 20 |
28 | 29 | 30 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 |
59 | 65 | 66 |
67 | 68 | 69 | 70 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /demo/noSDK/post_sign.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax Post 上传(服务端计算签名) 6 | 16 | 17 | 18 |

PostObject 上传(服务端计算签名)

19 | 20 | 21 | 22 | 23 |
24 | 25 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /demo/noSDK/post_sts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax Post 上传(前端计算签名) 6 | 16 | 17 | 18 |

PostObject 上传(前端计算签名)

19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /demo/noSDK/put_sign.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax Put 上传(服务端计算签名) 6 | 16 | 17 | 18 |

Ajax Put 上传(服务端计算签名)

19 | 20 | 21 | 22 | 23 |
24 | 25 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /demo/noSDK/put_sts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax Put 上传(前端计算签名) 6 | 16 | 17 | 18 |

Ajax Put 上传(前端计算签名)

19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /demo/policy-form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form 表单简单上传 6 | 7 | 8 | 9 | 10 |

PostObject 上传(Policy 保护,Form 表单上传)

11 |
最低兼容到 IE6 上传,使用 policy 签名保护,不支持 onprogress
12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /demo/policy-post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax Post 上传 6 | 15 | 16 | 17 | 18 |

PostObject 上传(Policy 保护,Ajax POST 请求)

19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /demo/post_sign.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form 表单简单上传 6 | 15 | 16 | 17 |

Form 表单简单上传(兼容 IE8)

18 |
最低兼容到 IE6 上传,不支持 onprogress
19 | 20 |
28 | 29 | 30 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 |
59 | 65 | 66 |
67 | 68 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /demo/queue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | COS 上传队列 9 | 10 | 11 | 12 | 13 |
14 |

COS 上传队列

15 |
16 |
17 |
共{{total}}个文件
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 52 | 53 | 54 | 55 | 56 |
文件名大小进度操作
{{item.Key}}{{formatSize(item.size)}} 37 | 38 | 39 | 40 | 已完成 41 | 等待上传 42 | 校验中({{parseInt(item.hashPercent*100)}}%) 43 | 已暂停, 已传{{formatSize(item.loaded)}} 44 | 已取消 45 | {{formatSize(item.speed)}}/s, 已传{{formatSize(item.loaded)}} {{parseInt(item.percent*100)}}% 46 | 48 | 暂停 49 | 开始 50 | 删除 51 |
暂无上传文件
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /demo/queue/index.js: -------------------------------------------------------------------------------- 1 | var Bucket = 'test-1250000000'; 2 | var Region = 'ap-guangzhou'; 3 | 4 | var cos = new COS({ 5 | FileParallelLimit: 5, 6 | ChunkParallelLimit: 5, 7 | ChunkMbSize: 8 * 1024 * 1024, 8 | getAuthorization: function (options, callback) { 9 | var url = '/sts'; 10 | var xhr = new XMLHttpRequest(); 11 | xhr.open('GET', url, true); 12 | xhr.onload = function (e) { 13 | try { 14 | var data = JSON.parse(e.target.responseText); 15 | } catch (e) { 16 | } 17 | callback({ 18 | TmpSecretId: data.credentials && data.credentials.tmpSecretId, 19 | TmpSecretKey: data.credentials && data.credentials.tmpSecretKey, 20 | SecurityToken: data.credentials && data.credentials.sessionToken, 21 | ExpiredTime: data.expiredTime 22 | }); 23 | }; 24 | xhr.send(); 25 | } 26 | }); 27 | 28 | new Vue({ 29 | el: '#app', 30 | data: function () { 31 | return { 32 | FileParallelLimit: 5, 33 | ChunkParallelLimit: 16, 34 | ChunkMbSize: 2, 35 | list: [], 36 | total: 0, 37 | }; 38 | }, 39 | created: function () { 40 | var self = this; 41 | cos.on('list-update', function (data) { 42 | self.list = data.list; 43 | self.total = data.list.length; 44 | }); 45 | }, 46 | methods: { 47 | formatSize: function (size) { 48 | var i, unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; 49 | for (i = 0; i < unit.length && size >= 1024; i++) { 50 | size /= 1024; 51 | } 52 | return (Math.round(size * 100) / 100 || 0) + unit[i]; 53 | }, 54 | selectedFile: function (e) { 55 | var files = e.target.files; 56 | var list = [].map.call(files, function (f) { 57 | return { 58 | Bucket: Bucket, 59 | Region: Region, 60 | Key: f.name, 61 | Body: f, 62 | }; 63 | }); 64 | cos.uploadFiles({files: list}); 65 | document.getElementById('form').reset(); 66 | }, 67 | pauseTask: function (task) { 68 | cos.pauseTask(task.id); 69 | }, 70 | restartTask: function (task) { 71 | cos.restartTask(task.id); 72 | }, 73 | cancelTask: function (task) { 74 | cos.cancelTask(task.id); 75 | }, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /demo/queue/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-family: "Microsoft YaHei"; 3 | font-weight: normal; 4 | } 5 | 6 | ul, 7 | li { 8 | list-style: none; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | a { 14 | color: #0071ff; 15 | } 16 | 17 | .float-right { 18 | float: right; 19 | } 20 | 21 | .page { 22 | width: 960px; 23 | margin: 0 auto; 24 | font-size: 14px; 25 | } 26 | 27 | #form input[type="text"] { 28 | width: 40px; 29 | padding: 2px; 30 | vertical-align: middle; 31 | } 32 | #form input[type="file"] { 33 | vertical-align: middle; 34 | } 35 | 36 | .file-list { 37 | margin-top: 20px; 38 | width: 100%; 39 | } 40 | 41 | .file-list { 42 | width: 960px; 43 | border-collapse: collapse; 44 | } 45 | 46 | .file-name { 47 | display: inline-block; 48 | width: 310px; 49 | text-overflow: ellipsis; 50 | white-space: nowrap 51 | } 52 | 53 | .file-item td { 54 | border: 1px solid #ccc; 55 | padding: 5px 10px; 56 | font-size: 12px; 57 | line-height: 20px; 58 | overflow: hidden; 59 | text-overflow: ellipsis; 60 | white-space: nowrap 61 | } 62 | 63 | .file-progress { 64 | position: relative; 65 | display: inline-block; 66 | width: 100px; 67 | height: 6px; 68 | background: #ddd; 69 | vertical-align: middle; 70 | margin-right: 5px; 71 | } 72 | 73 | .file-progress-loaded { 74 | position: absolute; 75 | display: inline-block; 76 | height: 6px; 77 | background: #0071ff; 78 | } 79 | -------------------------------------------------------------------------------- /demo/react-folder/Icons.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | /**文件夹 icon */ 3 | export const FolderIcon = ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | /**文件 icon */ 19 | export const FileIcon = ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /demo/react-folder/demo-js.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import styles from "./index.module.css"; 3 | import { FileIcon, FolderIcon } from "./Icons"; 4 | import COS from "cos-js-sdk-v5"; 5 | import React, { useMemo, useState, useEffect } from "react"; 6 | 7 | /**要查看的桶配置 */ 8 | const bucketConfig = { 9 | /**桶名,必须字段 */ 10 | Bucket: "xxxxx-123456789", 11 | /* 存储桶所在地域,必须字段 */ 12 | Region: "ap-guangzhou", 13 | }; 14 | 15 | /**COS SDK 实例 */ 16 | const cos = new COS({ 17 | // SecretId 和 SecretKey请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理 18 | // 注意⚠️:这种方法仅限前端测试调用,因为把密钥写在前端代码中将会导致泄露 19 | SecretId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 20 | SecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 21 | }); 22 | // 下面是从后端获取临时密钥的写法, 推荐!! 23 | // const cos = new COS({ 24 | // getAuthorization: async function (options, callback) { 25 | // const data = await (await fetch("/your_backend_api_path")).json(); 26 | // const { credentials } = data; 27 | // callback({ 28 | // TmpSecretId: credentials.tmpSecretId, 29 | // TmpSecretKey: credentials.tmpSecretKey, 30 | // SecurityToken: credentials.sessionToken, 31 | // StartTime: data.startTime, // 时间戳,单位秒,如:1580000000 32 | // ExpiredTime: data.expiredTime, // 时间戳,单位秒,如:1580000000 33 | // }); 34 | // }, 35 | // }); 36 | 37 | const baseURL = `https://${bucketConfig.Bucket}.cos.${bucketConfig.Region}.myqcloud.com/`; 38 | 39 | /** 40 | * 文件管理, 实现了以下功能 41 | * - 文件列表(含分页) 42 | * - 下载文件 43 | * - 删除文件 44 | * - 上传文件 45 | * - 创建文件夹 46 | */ 47 | export default function FileManager() { 48 | /** 一页个数 */ 49 | const pageSize = 30; 50 | // 前缀字符串数组 51 | const [prefixArr, setPrefixArr] = useState([]); 52 | /** 前缀字符串 */ 53 | const prefix = useMemo(() => prefixArr.join(""), [prefixArr]); 54 | // 本次请求结果,用于判断分页 55 | const [result, setResult] = useState({ 56 | Name: "", 57 | Prefix: "", 58 | Marker: "", 59 | MaxKeys: "", 60 | Contents: [], 61 | CommonPrefixes: [], 62 | IsTruncated: "false", 63 | }); 64 | // 当前对象列表 65 | const [list, setList] = useState({ 66 | CommonPrefixes: [], 67 | Contents: [], 68 | }); 69 | // 是否正在加载中 70 | const [loading, setLoading] = useState(false); 71 | 72 | /**单击面包蟹 */ 73 | const changePrefix = (path) => { 74 | const newArr = []; 75 | for (let i = 0; i < prefixArr.length; i++) { 76 | const item = prefixArr[i]; 77 | newArr.push(item); 78 | if (item === path) break; 79 | } 80 | setPrefixArr(newArr); 81 | }; 82 | /**获取COS API返回结果 */ 83 | const getBucket = async (Marker) => { 84 | setLoading(true); 85 | const _data = await cos.getBucket({ 86 | ...bucketConfig, 87 | Prefix: prefix, 88 | Delimiter: "/", //不深度遍历 89 | MaxKeys: pageSize, //一页大小 90 | Marker, //分页 - 下一个对象开始位置 91 | }); 92 | setLoading(false); 93 | console.log(_data); 94 | setResult(_data); 95 | return _data; 96 | }; 97 | /**分页 - 点击获取下一页 */ 98 | const getNext = async () => { 99 | const res = await getBucket(result.NextMarker); 100 | setList(({ CommonPrefixes, Contents }) => { 101 | return { 102 | CommonPrefixes: CommonPrefixes.concat(res.CommonPrefixes), //分页数据拼接 103 | Contents: Contents.concat(res.Contents.filter((k) => k.Key !== prefix)), //分页数据拼接 + 排除同名文件夹 104 | }; 105 | }); 106 | }; 107 | /**获取当前目录第一页 (当 prefix 变化了自动调用) */ 108 | const getFirstPage = async () => { 109 | const res = await getBucket(); 110 | setList({ 111 | CommonPrefixes: res.CommonPrefixes, 112 | Contents: res.Contents.filter((k) => k.Key !== prefix), 113 | }); 114 | }; 115 | /**双击文件下载 */ 116 | const download = async (Key) => { 117 | const a = document.createElement("a"); 118 | a.download = Key; 119 | a.href = baseURL + Key; 120 | document.body.appendChild(a); 121 | a.click(); 122 | document.body.removeChild(a); 123 | console.log("下载完成"); 124 | }; 125 | /**右键删除 */ 126 | const deleteFile = (e, Key) => { 127 | e.preventDefault(); 128 | if (window.confirm("确认删除此对象吗")) { 129 | cos.deleteObject({ ...bucketConfig, Key }).then(() => { 130 | getFirstPage(); 131 | }); 132 | } 133 | }; 134 | /**双击文件夹进入下一层 */ 135 | const intoFolder = (Prefix) => { 136 | setPrefixArr((arr) => arr.concat(Prefix.replace(prefix, ""))); //要去除当前文件夹的部分前缀 137 | }; 138 | /**在当前目录上传文件 */ 139 | const onUpload = async (e) => { 140 | const files = e.currentTarget.files; 141 | if (files && files.length) { 142 | const [file] = files; 143 | await cos.uploadFile({ 144 | ...bucketConfig, 145 | Body: file, 146 | Key: prefix + file.name, // 上传到哪里 147 | onProgress(e) { 148 | console.log(`上传中 ${e.percent * 100}%`); 149 | }, 150 | }); 151 | alert("上传成功"); 152 | getFirstPage(); 153 | } 154 | }; 155 | /**创建文件夹 */ 156 | const createFolder = async () => { 157 | const key = window.prompt("请输入文件夹名称")?.trim(); 158 | if (!key) { 159 | return alert("请输入完整内容"); 160 | } 161 | await cos.uploadFile({ 162 | ...bucketConfig, 163 | Body: "", 164 | Key: prefix + key + "/", //末尾加个斜杠,就是文件夹 165 | }); 166 | alert("创建成功"); 167 | getFirstPage(); 168 | }; 169 | 170 | // 前缀变化了就获取该前缀的第一页 171 | useEffect(() => { 172 | getFirstPage(); 173 | }, [prefix]); 174 | 175 | return ( 176 |
177 |
178 |
179 | 182 |
183 | 创建文件夹 184 |
185 |
186 |
187 | setPrefixArr([])} name={bucketConfig.Bucket + "/"} /> 188 | {prefixArr.map((k) => ( 189 | changePrefix(k)} name={k} /> 190 | ))} 191 |
192 |
193 |
194 | {list.CommonPrefixes.length === 0 && list.Contents.length === 0 ? ( 195 |
暂无数据
196 | ) : ( 197 | <> 198 | {list.CommonPrefixes.map((k) => { 199 | return ( 200 | deleteFile?.(e, k.Prefix)} 205 | onDoubleClick={(e) => intoFolder?.(k.Prefix)} 206 | /> 207 | ); 208 | })} 209 | {list.Contents.map((k) => { 210 | return ( 211 | download?.(k.Key)} 216 | onContextMenu={(e) => deleteFile?.(e, k.Key)} 217 | /> 218 | ); 219 | })} 220 | 221 | )} 222 |
223 | {result.IsTruncated === "true" && ( 224 |
225 | 点我加载更多(一页{pageSize}个) 226 |
227 | )} 228 |
229 |
双击文件夹进入下一层、双击文件进行下载、右键删除
230 |
231 |
232 | ); 233 | } 234 | 235 | /**面包屑元素 */ 236 | const BreadItem = ({ name, onClick }) => { 237 | return ( 238 |
239 | {name} 240 |
241 | ); 242 | }; 243 | /**文件列表元素组件 */ 244 | const FileItem = ({ Icon, title, onDoubleClick, onContextMenu }) => { 245 | return ( 246 |
247 | {Icon} 248 |
{title}
249 |
250 | ); 251 | }; -------------------------------------------------------------------------------- /demo/react-folder/demo-ts.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import styles from "./index.module.css"; 3 | import { ReactNode } from "react"; 4 | import { FileIcon, FolderIcon } from "./Icons"; 5 | import COS, { GetBucketResult } from "cos-js-sdk-v5"; 6 | import React, { useMemo, useState, MouseEvent, useEffect, MouseEventHandler } from "react"; 7 | import { ChangeEvent } from "react"; 8 | 9 | /**要查看的桶配置 */ 10 | const bucketConfig = { 11 | /**桶名,必须字段 */ 12 | Bucket: "xxxxx-123456789", 13 | /* 存储桶所在地域,必须字段 */ 14 | Region: "ap-guangzhou", 15 | }; 16 | 17 | /**COS SDK 实例 */ 18 | const cos = new COS({ 19 | // SecretId 和 SecretKey请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理 20 | // 注意⚠️:这种方法仅限前端测试调用,因为把密钥写在前端代码中将会导致泄露 21 | SecretId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 22 | SecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 23 | }); 24 | // 下面是从后端获取临时密钥的写法, 推荐!! 25 | // const cos = new COS({ 26 | // getAuthorization: async function (options, callback) { 27 | // const data = await (await fetch("/your_backend_api_path")).json(); 28 | // const { credentials } = data; 29 | // callback({ 30 | // TmpSecretId: credentials.tmpSecretId, 31 | // TmpSecretKey: credentials.tmpSecretKey, 32 | // SecurityToken: credentials.sessionToken, 33 | // StartTime: data.startTime, // 时间戳,单位秒,如:1580000000 34 | // ExpiredTime: data.expiredTime, // 时间戳,单位秒,如:1580000000 35 | // }); 36 | // }, 37 | // }); 38 | 39 | const baseURL = `https://${bucketConfig.Bucket}.cos.${bucketConfig.Region}.myqcloud.com/`; 40 | 41 | /** 42 | * 文件管理, 实现了以下功能 43 | * - 文件列表(含分页) 44 | * - 下载文件 45 | * - 删除文件 46 | * - 上传文件 47 | * - 创建文件夹 48 | */ 49 | export default function FileManager() { 50 | /** 一页个数 */ 51 | const pageSize = 30; 52 | // 前缀字符串数组 53 | const [prefixArr, setPrefixArr] = useState([]); 54 | /** 前缀字符串 */ 55 | const prefix = useMemo(() => prefixArr.join(""), [prefixArr]); 56 | // 本次请求结果,用于判断分页 57 | const [result, setResult] = useState({ 58 | Name: "", 59 | Prefix: "", 60 | Marker: "", 61 | MaxKeys: "", 62 | Contents: [], 63 | CommonPrefixes: [], 64 | IsTruncated: "false", 65 | }); 66 | // 当前对象列表 67 | const [list, setList] = useState({ 68 | CommonPrefixes: [], 69 | Contents: [], 70 | }); 71 | // 是否正在加载中 72 | const [loading, setLoading] = useState(false); 73 | 74 | /**单击面包蟹 */ 75 | const changePrefix = (path: string) => { 76 | const newArr: string[] = []; 77 | for (let i = 0; i < prefixArr.length; i++) { 78 | const item = prefixArr[i]; 79 | newArr.push(item); 80 | if (item === path) break; 81 | } 82 | setPrefixArr(newArr); 83 | }; 84 | /**获取COS API返回结果 */ 85 | const getBucket = async (Marker?: string) => { 86 | setLoading(true); 87 | const _data = await cos.getBucket({ 88 | ...bucketConfig, 89 | Prefix: prefix, 90 | Delimiter: "/", //不深度遍历 91 | MaxKeys: pageSize, //一页大小 92 | Marker, //分页 - 下一个对象开始位置 93 | }); 94 | setLoading(false); 95 | console.log(_data); 96 | setResult(_data); 97 | return _data; 98 | }; 99 | /**分页 - 点击获取下一页 */ 100 | const getNext = async () => { 101 | const res = await getBucket(result.NextMarker); 102 | setList(({ CommonPrefixes, Contents }) => { 103 | return { 104 | CommonPrefixes: CommonPrefixes.concat(res.CommonPrefixes), //分页数据拼接 105 | Contents: Contents.concat(res.Contents.filter((k) => k.Key !== prefix)), //分页数据拼接 + 排除同名文件夹 106 | }; 107 | }); 108 | }; 109 | /**获取当前目录第一页 (当 prefix 变化了自动调用) */ 110 | const getFirstPage = async () => { 111 | const res = await getBucket(); 112 | setList({ 113 | CommonPrefixes: res.CommonPrefixes, 114 | Contents: res.Contents.filter((k) => k.Key !== prefix), 115 | }); 116 | }; 117 | /**双击文件下载 */ 118 | const download = async (Key: string) => { 119 | const a = document.createElement("a"); 120 | a.download = Key; 121 | a.href = baseURL + Key; 122 | document.body.appendChild(a); 123 | a.click(); 124 | document.body.removeChild(a); 125 | console.log("下载完成"); 126 | }; 127 | /**右键删除 */ 128 | const deleteFile = (e: MouseEvent, Key: string) => { 129 | e.preventDefault(); 130 | if (window.confirm("确认删除此对象吗")) { 131 | cos.deleteObject({ ...bucketConfig, Key }).then(() => { 132 | getFirstPage(); 133 | }); 134 | } 135 | }; 136 | /**双击文件夹进入下一层 */ 137 | const intoFolder = (Prefix: string) => { 138 | setPrefixArr((arr) => arr.concat(Prefix.replace(prefix, ""))); //要去除当前文件夹的部分前缀 139 | }; 140 | /**在当前目录上传文件 */ 141 | const onUpload = async (e: ChangeEvent) => { 142 | const files = e.currentTarget.files; 143 | if (files && files.length) { 144 | const [file] = files; 145 | await cos.uploadFile({ 146 | ...bucketConfig, 147 | Body: file, 148 | Key: prefix + file.name, // 上传到哪里 149 | onProgress(e) { 150 | console.log(`上传中 ${e.percent * 100}%`); 151 | }, 152 | }); 153 | alert("上传成功"); 154 | getFirstPage(); 155 | } 156 | }; 157 | /**创建文件夹 */ 158 | const createFolder = async () => { 159 | const key = window.prompt("请输入文件夹名称")?.trim(); 160 | if (!key) { 161 | return alert("请输入完整内容"); 162 | } 163 | await cos.uploadFile({ 164 | ...bucketConfig, 165 | Body: "", 166 | Key: prefix + key + "/", //末尾加个斜杠,就是文件夹 167 | }); 168 | alert("创建成功"); 169 | getFirstPage(); 170 | }; 171 | 172 | // 前缀变化了就获取该前缀的第一页 173 | useEffect(() => { 174 | getFirstPage(); 175 | }, [prefix]); 176 | 177 | return ( 178 |
179 |
180 |
181 | 184 |
185 | 创建文件夹 186 |
187 |
188 |
189 | setPrefixArr([])} name={bucketConfig.Bucket + "/"} /> 190 | {prefixArr.map((k) => ( 191 | changePrefix(k)} name={k} /> 192 | ))} 193 |
194 |
195 |
196 | {list.CommonPrefixes.length === 0 && list.Contents.length === 0 ? ( 197 |
暂无数据
198 | ) : ( 199 | <> 200 | {list.CommonPrefixes.map((k) => { 201 | return ( 202 | deleteFile?.(e, k.Prefix)} 207 | onDoubleClick={(e) => intoFolder?.(k.Prefix)} 208 | /> 209 | ); 210 | })} 211 | {list.Contents.map((k) => { 212 | return ( 213 | download?.(k.Key)} 218 | onContextMenu={(e) => deleteFile?.(e, k.Key)} 219 | /> 220 | ); 221 | })} 222 | 223 | )} 224 |
225 | {result.IsTruncated === "true" && ( 226 |
227 | 点我加载更多(一页{pageSize}个) 228 |
229 | )} 230 |
231 |
双击文件夹进入下一层、双击文件进行下载、右键删除
232 |
233 |
234 | ); 235 | } 236 | 237 | /**面包屑元素 */ 238 | const BreadItem = ({ name, onClick }: BreadItemProps) => { 239 | return ( 240 |
241 | {name} 242 |
243 | ); 244 | }; 245 | /**文件列表元素组件 */ 246 | const FileItem = ({ Icon, title, onDoubleClick, onContextMenu }: FileItemProps) => { 247 | return ( 248 |
249 | {Icon} 250 |
{title}
251 |
252 | ); 253 | }; 254 | 255 | /**面包屑子组件 Props */ 256 | interface BreadItemProps { 257 | name: string; 258 | onClick: MouseEventHandler; 259 | } 260 | 261 | /**文件列表-单个文件元素组件 Props */ 262 | interface FileItemProps { 263 | title: string; 264 | Icon: ReactNode; 265 | onDoubleClick?: MouseEventHandler; 266 | onContextMenu?: MouseEventHandler; 267 | } 268 | 269 | /**当前对象列表 */ 270 | type ListData = Pick; 271 | -------------------------------------------------------------------------------- /demo/react-folder/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | background-color: #f2f2f2; 7 | } 8 | 9 | .content { 10 | border: 1px solid; 11 | border-radius: 20px; 12 | background-color: white; 13 | padding: 1.25rem; 14 | width: 80%; 15 | height: 80%; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .breadcrumb { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .fileList { 26 | width: 100%; 27 | height: 100%; 28 | border: 1px solid; 29 | border-radius: 10px; 30 | overflow: hidden; 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .fileItems { 36 | flex-grow: 1; 37 | display: flex; 38 | flex-wrap: wrap; 39 | overflow-y: auto; 40 | align-content: flex-start; 41 | } 42 | 43 | .noData { 44 | width: 100%; 45 | text-align: center; 46 | } 47 | 48 | .loadMore { 49 | width: 100%; 50 | font-size: 0.875rem; 51 | text-align: center; 52 | cursor: pointer; 53 | } 54 | 55 | .instruction { 56 | font-size: 0.875rem; 57 | } 58 | 59 | .breadItem { 60 | cursor: pointer; 61 | margin-left: 0.25rem; 62 | margin-right: 0.25rem; 63 | } 64 | 65 | .fileItem { 66 | width: 120px; 67 | height: 120px; 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | justify-content: center; 72 | background-color: transparent; 73 | cursor: pointer; 74 | padding-left: 0.5rem; 75 | padding-right: 0.5rem; 76 | } 77 | 78 | .fileItem:hover { 79 | background-color: #e2e8f0; 80 | } 81 | 82 | .fileItemTitle { 83 | width: 80%; 84 | font-size: 0.875rem; 85 | text-align: center; 86 | word-break: break-all; 87 | user-select: none; 88 | } 89 | 90 | .btnList { 91 | display: flex; 92 | gap: 5px; 93 | align-items: center; 94 | margin-bottom: 5px; 95 | } 96 | .btnList .btn { 97 | font-size: 14px; 98 | border: none; 99 | background-color: #337af7; 100 | color: white; 101 | border-radius: 5px; 102 | padding: 5px 10px; 103 | cursor: pointer; 104 | } 105 | -------------------------------------------------------------------------------- /demo/react-folder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "cos-js-sdk-v5": "^1.7.1", 7 | "react": "18.2.0", 8 | "react-dom": "18.2.0" 9 | }, 10 | "devDependencies": { 11 | "typescript": "5.2.2", 12 | "@types/react": "18.2.21", 13 | "@types/react-dom": "18.2.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/slice-task.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 自定义的分片上传 6 | 15 | 16 | 17 | 18 |

自定义的分片上传

19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /demo/slice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 自定义的分片上传 6 | 15 | 16 | 17 | 18 |

自定义的分片上传

19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /demo/sts-form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form 表单简单上传 6 | 7 | 8 | 9 | 10 |

Form 表单简单上传(兼容 IE8)

11 |
最低兼容到 IE6 上传,不支持 onprogress
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /demo/sts-post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ajax Post 上传 6 | 15 | 16 | 17 | 18 |

Ajax Post 上传

19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /demo/sts-put-server-key.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ajax Put 上传 7 | 16 | 17 | 18 | 19 |

Ajax Put 上传

20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /demo/sts-put.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ajax Put 上传 7 | 16 | 17 | 18 | 19 |

Ajax Put 上传

20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /demo/vueDemo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | COS-SDK-Vue-demo 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 |
20 | 26 |
27 | 28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | 49 | 50 | 51 |
{{item.label}}
40 | {{item.name}} 41 | {{item.name}} 42 | {{item.Size || '-'}}{{item.LastModified || '-'}} 46 | 下载 47 | 删除 48 |
52 | 53 |
54 |
55 | 56 | -------------------------------------------------------------------------------- /demo/vueDemo/index.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | const Bucket = 'examplebucket-1250000000'; /* 存储桶 */ 3 | const Region = 'ap-beijing'; /* 存储桶所在地域,必须字段 */ 4 | // SecretId 和 SecretKey请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理 5 | const cos = new COS({ 6 | SecretId: '', 7 | SecretKey: '', 8 | }); 9 | 10 | /* 11 | 实现了以下功能 12 | - 文件列表 13 | - 上传文件 14 | - 上传文件夹 15 | - 下载文件 16 | - 删除文件 17 | */ 18 | const vm = new Vue({ 19 | el: '#app', 20 | data() { 21 | return { 22 | columns: [ 23 | { label: '名称', value: 'Key' }, 24 | { label: '大小', value: 'Size' }, 25 | { label: '修改时间', value: 'LastModified' }, 26 | { label: '操作', value: 'action' } 27 | ], 28 | list: [], 29 | Prefix: '', 30 | Marker: '', 31 | hasMore: false, 32 | } 33 | }, 34 | computed: { 35 | // 面包屑导航条 36 | navList() { 37 | const prefixes = this.Prefix.split('/').filter(Boolean); 38 | const folders = prefixes.map((item, index) => { 39 | return { 40 | name: item, 41 | Prefix: prefixes.slice(0, index + 1).join('/') + '/', 42 | }; 43 | }); 44 | return [{ name: Bucket, Prefix: ''}].concat(folders); 45 | }, 46 | }, 47 | created() { 48 | this.getFileList(); 49 | }, 50 | methods: { 51 | // 查询文件列表 52 | getFileList(loadMore) { 53 | const { Prefix, Marker } = this; 54 | cos.getBucket({ 55 | Bucket, /* 必须 */ 56 | Region, /* 存储桶所在地域,必须字段 */ 57 | Prefix, /* 非必须 */ 58 | Marker, /* 非必须 */ 59 | Delimiter: '/', /* 非必须 */ 60 | }, (err, data) => { 61 | if(err) { 62 | console.log(err); 63 | return; 64 | } 65 | const folder = data.CommonPrefixes.map((item) => { 66 | return { 67 | Prefix: item.Prefix, 68 | name: item.Prefix.replace(Prefix, '').slice(0,-1), 69 | isDir: true, 70 | } 71 | }); 72 | const files = data.Contents.filter((item) => !item.Key.endsWith('/')) 73 | .map((item) => { 74 | return { 75 | ...item, 76 | name: item.Key.replace(Prefix, ''), 77 | } 78 | }); 79 | const list = folder.concat(files); 80 | this.hasMore = data.IsTruncated; 81 | this.Marker = data.NextMarker || ''; 82 | if (loadMore) { 83 | this.list = [...this.list, ...list]; 84 | } else { 85 | this.list = list; 86 | } 87 | }); 88 | }, 89 | // 点击面包屑 90 | navClick(item) { 91 | this.openFolder(item.Prefix); 92 | }, 93 | // 打开文件夹 94 | openFolder(prefix) { 95 | this.Prefix = prefix; 96 | this.hasMore = false; 97 | this.Marker = ''; 98 | this.getFileList(); 99 | }, 100 | // 上传文件 101 | uploadFileClick() { 102 | document.querySelectorAll('.file-select')[0].click(); 103 | }, 104 | // 上传文件夹 105 | uploadFolderClick() { 106 | document.querySelectorAll('.folder-select')[0].click(); 107 | }, 108 | // 上传 109 | uploadChange(events) { 110 | const files = events.currentTarget.files; 111 | const uploadFileList = [...files].map((file) => { 112 | const path = file.webkitRelativePath || file.name; 113 | return { 114 | Bucket, 115 | Region, 116 | Key: this.Prefix + path, 117 | Body: file, 118 | } 119 | }); 120 | cos.uploadFiles({ 121 | files: uploadFileList, 122 | SliceSize: 1024 * 1024 * 10, /* 设置大于10MB采用分块上传 */ 123 | onProgress: function (info) { 124 | var percent = parseInt(info.percent * 10000) / 100; 125 | var speed = parseInt(info.speed / 1024 / 1024 * 100) / 100; 126 | console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;'); 127 | }, 128 | onFileFinish: function (err, data, options) { 129 | console.log(options.Key + '上传' + (err ? '失败' : '完成')); 130 | }, 131 | }, (err, data) => { 132 | if (err) { 133 | console.log('上传失败', err); 134 | return; 135 | } 136 | // 刷新列表前初始化 137 | this.hasMore = false; 138 | this.Marker = ''; 139 | this.getFileList(); 140 | }); 141 | }, 142 | // 加载更多 143 | loadMore() { 144 | this.getFileList(true); 145 | }, 146 | // 下载 147 | downloadFile(file) { 148 | cos.getObjectUrl({ 149 | Bucket, /* 必须 */ 150 | Region, /* 存储桶所在地域,必须字段 */ 151 | Key: file.Key, /* 必须 */ 152 | }, function(err, data) { 153 | if (err) { 154 | console.log(err); 155 | return; 156 | } 157 | const url = data.Url + (data.Url.indexOf('?') > -1 ? '&' : '?') + 'response-content-disposition=attachment'; // 补充强制下载的参数 158 | // 使用iframe下载 159 | const elemIF = document.createElement("iframe"); 160 | elemIF.src = url; 161 | elemIF.style.display = "none"; 162 | document.body.appendChild(elemIF); 163 | }); 164 | }, 165 | // 删除 166 | deleteFile(file) { 167 | cos.deleteObject({ 168 | Bucket, /* 必须 */ 169 | Region, /* 存储桶所在地域,必须字段 */ 170 | Key: file.Key /* 必须 */ 171 | }, (err, data) => { 172 | if (err) { 173 | console.log(err); 174 | return; 175 | } 176 | // 刷新列表前初始化 177 | this.hasMore = false; 178 | this.Marker = ''; 179 | this.getFileList(); 180 | }); 181 | }, 182 | }, 183 | }); 184 | } 185 | 186 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var COS = require('./src/cos'); 2 | module.exports = COS; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, // 是否显示覆盖率报告 3 | // 告诉 jest 哪些文件需要经过单元测试测试 4 | collectCoverageFrom: ['**/src/*.js'], 5 | coverageThreshold: { 6 | global: { 7 | statements: 80, // 保证每个语句都执行了 8 | functions: 70, // 保证每个函数都调用了 9 | branches: 70, // 保证每个 if 等分支代码都执行了 10 | }, 11 | }, 12 | }; -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id: base64.js,v 2.15 2014/04/05 12:58:57 dankogai Exp dankogai $ 3 | * 4 | * Licensed under the BSD 3-Clause License. 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | * 7 | * References: 8 | * http://en.wikipedia.org/wiki/Base64 9 | */ 10 | 11 | var Base64 = (function(global) { 12 | global = global || {}; 13 | 'use strict'; 14 | // existing version for noConflict() 15 | var _Base64 = global.Base64; 16 | var version = "2.1.9"; 17 | // if node.js, we use Buffer 18 | var buffer; 19 | // constants 20 | var b64chars 21 | = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 22 | var b64tab = function(bin) { 23 | var t = {}; 24 | for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i; 25 | return t; 26 | }(b64chars); 27 | var fromCharCode = String.fromCharCode; 28 | // encoder stuff 29 | var cb_utob = function(c) { 30 | if (c.length < 2) { 31 | var cc = c.charCodeAt(0); 32 | return cc < 0x80 ? c 33 | : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6)) 34 | + fromCharCode(0x80 | (cc & 0x3f))) 35 | : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) 36 | + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) 37 | + fromCharCode(0x80 | ( cc & 0x3f))); 38 | } else { 39 | var cc = 0x10000 40 | + (c.charCodeAt(0) - 0xD800) * 0x400 41 | + (c.charCodeAt(1) - 0xDC00); 42 | return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) 43 | + fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) 44 | + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) 45 | + fromCharCode(0x80 | ( cc & 0x3f))); 46 | } 47 | }; 48 | var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; 49 | var utob = function(u) { 50 | return u.replace(re_utob, cb_utob); 51 | }; 52 | var cb_encode = function(ccc) { 53 | var padlen = [0, 2, 1][ccc.length % 3], 54 | ord = ccc.charCodeAt(0) << 16 55 | | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) 56 | | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)), 57 | chars = [ 58 | b64chars.charAt( ord >>> 18), 59 | b64chars.charAt((ord >>> 12) & 63), 60 | padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63), 61 | padlen >= 1 ? '=' : b64chars.charAt(ord & 63) 62 | ]; 63 | return chars.join(''); 64 | }; 65 | var btoa = global.btoa ? function(b) { 66 | return global.btoa(b); 67 | } : function(b) { 68 | return b.replace(/[\s\S]{1,3}/g, cb_encode); 69 | }; 70 | var _encode = buffer ? function (u) { 71 | return (u.constructor === buffer.constructor ? u : new buffer(u)) 72 | .toString('base64') 73 | } 74 | : function (u) { return btoa(utob(u)) } 75 | ; 76 | var encode = function(u, urisafe) { 77 | return !urisafe 78 | ? _encode(String(u)) 79 | : _encode(String(u)).replace(/[+\/]/g, function(m0) { 80 | return m0 == '+' ? '-' : '_'; 81 | }).replace(/=/g, ''); 82 | }; 83 | var encodeURI = function(u) { return encode(u, true) }; 84 | // decoder stuff 85 | var re_btou = new RegExp([ 86 | '[\xC0-\xDF][\x80-\xBF]', 87 | '[\xE0-\xEF][\x80-\xBF]{2}', 88 | '[\xF0-\xF7][\x80-\xBF]{3}' 89 | ].join('|'), 'g'); 90 | var cb_btou = function(cccc) { 91 | switch(cccc.length) { 92 | case 4: 93 | var cp = ((0x07 & cccc.charCodeAt(0)) << 18) 94 | | ((0x3f & cccc.charCodeAt(1)) << 12) 95 | | ((0x3f & cccc.charCodeAt(2)) << 6) 96 | | (0x3f & cccc.charCodeAt(3)), 97 | offset = cp - 0x10000; 98 | return (fromCharCode((offset >>> 10) + 0xD800) 99 | + fromCharCode((offset & 0x3FF) + 0xDC00)); 100 | case 3: 101 | return fromCharCode( 102 | ((0x0f & cccc.charCodeAt(0)) << 12) 103 | | ((0x3f & cccc.charCodeAt(1)) << 6) 104 | | (0x3f & cccc.charCodeAt(2)) 105 | ); 106 | default: 107 | return fromCharCode( 108 | ((0x1f & cccc.charCodeAt(0)) << 6) 109 | | (0x3f & cccc.charCodeAt(1)) 110 | ); 111 | } 112 | }; 113 | var btou = function(b) { 114 | return b.replace(re_btou, cb_btou); 115 | }; 116 | var cb_decode = function(cccc) { 117 | var len = cccc.length, 118 | padlen = len % 4, 119 | n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) 120 | | (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) 121 | | (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) 122 | | (len > 3 ? b64tab[cccc.charAt(3)] : 0), 123 | chars = [ 124 | fromCharCode( n >>> 16), 125 | fromCharCode((n >>> 8) & 0xff), 126 | fromCharCode( n & 0xff) 127 | ]; 128 | chars.length -= [0, 0, 2, 1][padlen]; 129 | return chars.join(''); 130 | }; 131 | var atob = global.atob ? function(a) { 132 | return global.atob(a); 133 | } : function(a){ 134 | return a.replace(/[\s\S]{1,4}/g, cb_decode); 135 | }; 136 | var _decode = buffer ? function(a) { 137 | return (a.constructor === buffer.constructor 138 | ? a : new buffer(a, 'base64')).toString(); 139 | } 140 | : function(a) { return btou(atob(a)) }; 141 | var decode = function(a){ 142 | return _decode( 143 | String(a).replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' }) 144 | .replace(/[^A-Za-z0-9\+\/]/g, '') 145 | ); 146 | }; 147 | var noConflict = function() { 148 | var Base64 = global.Base64; 149 | global.Base64 = _Base64; 150 | return Base64; 151 | }; 152 | // export Base64 153 | var Base64 = { 154 | VERSION: version, 155 | atob: atob, 156 | btoa: btoa, 157 | fromBase64: decode, 158 | toBase64: encode, 159 | utob: utob, 160 | encode: encode, 161 | encodeURI: encodeURI, 162 | btou: btou, 163 | decode: decode, 164 | noConflict: noConflict 165 | }; 166 | return Base64; 167 | })(); 168 | 169 | module.exports = Base64; 170 | 171 | -------------------------------------------------------------------------------- /lib/crypto.js: -------------------------------------------------------------------------------- 1 | /* 2 | CryptoJS v3.1.2 3 | code.google.com/p/crypto-js 4 | (c) 2009-2013 by Jeff Mott. All rights reserved. 5 | code.google.com/p/crypto-js/wiki/License 6 | */ 7 | var CryptoJS=CryptoJS||function(g,l){var e={},d=e.lib={},m=function(){},k=d.Base={extend:function(a){m.prototype=this;var c=new m;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 8 | p=d.WordArray=k.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=l?c:4*a.length},toString:function(a){return(a||n).stringify(this)},concat:function(a){var c=this.words,q=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var b=0;b>>2]|=(q[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535>>2]=q[b>>>2];else c.push.apply(c,q);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 9 | 32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=k.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>3]|=parseInt(a.substr(f, 10 | 2),16)<<24-4*(f%8);return new p.init(b,c/2)}},j=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new p.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(j.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return j.parse(unescape(encodeURIComponent(a)))}}, 11 | r=d.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new p.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,b=c.words,f=c.sigBytes,d=this.blockSize,e=f/(4*d),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*d;f=g.min(4*a,f);if(a){for(var k=0;ka;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(n<<5|n>>>27)+l+m[a];c=20>a?c+((j&h|~j&g)+1518500249):40>a?c+((j^h^g)+1859775393):60>a?c+((j&h|j&g|h&g)-1894007588):c+((j^h^ 15 | g)-899497514);l=g;g=h;h=j<<30|j>>>2;j=n;n=c}b[0]=b[0]+n|0;b[1]=b[1]+j|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+l|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,g=8*d.sigBytes;e[g>>>5]|=128<<24-g%32;e[(g+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(g+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(l);g.HmacSHA1=d._createHmacHelper(l)})(); 16 | (function(){var g=CryptoJS,l=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=l.parse(d));var g=e.blockSize,k=4*g;d.sigBytes>k&&(d=e.finalize(d));d.clamp();for(var p=this._oKey=d.clone(),b=this._iKey=d.clone(),n=p.words,j=b.words,h=0;h>> 2] >>> (24 - (i % 4) * 8)) & 0xff; 57 | var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; 58 | var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; 59 | 60 | var triplet = (byte1 << 16) | (byte2 << 8) | byte3; 61 | 62 | for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { 63 | base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); 64 | } 65 | } 66 | 67 | // Add padding 68 | var paddingChar = map.charAt(64); 69 | if (paddingChar) { 70 | while (base64Chars.length % 4) { 71 | base64Chars.push(paddingChar); 72 | } 73 | } 74 | 75 | return base64Chars.join(''); 76 | }, 77 | 78 | /** 79 | * Converts a Base64 string to a word array. 80 | * 81 | * @param {string} base64Str The Base64 string. 82 | * 83 | * @return {WordArray} The word array. 84 | * 85 | * @static 86 | * 87 | * @example 88 | * 89 | * var wordArray = CryptoJS.enc.Base64.parse(base64String); 90 | */ 91 | parse: function (base64Str) { 92 | // Shortcuts 93 | var base64StrLength = base64Str.length; 94 | var map = this._map; 95 | 96 | // Ignore padding 97 | var paddingChar = map.charAt(64); 98 | if (paddingChar) { 99 | var paddingIndex = base64Str.indexOf(paddingChar); 100 | if (paddingIndex != -1) { 101 | base64StrLength = paddingIndex; 102 | } 103 | } 104 | 105 | // Convert 106 | var words = []; 107 | var nBytes = 0; 108 | for (var i = 0; i < base64StrLength; i++) { 109 | if (i % 4) { 110 | var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2); 111 | var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2); 112 | words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8); 113 | nBytes++; 114 | } 115 | } 116 | 117 | return WordArray.create(words, nBytes); 118 | }, 119 | 120 | _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 121 | }; 122 | }()); 123 | 124 | if(typeof module === 'object'){ 125 | module.exports = CryptoJS; 126 | }else{ 127 | window.CryptoJS = CryptoJS; 128 | } 129 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | var stringifyPrimitive = function (v) { 2 | switch (typeof v) { 3 | case 'string': 4 | return v; 5 | case 'boolean': 6 | return v ? 'true' : 'false'; 7 | case 'number': 8 | return isFinite(v) ? v : ''; 9 | default: 10 | return ''; 11 | } 12 | }; 13 | 14 | var queryStringify = function (obj, sep, eq, name) { 15 | sep = sep || '&'; 16 | eq = eq || '='; 17 | if (obj === null) { 18 | obj = undefined; 19 | } 20 | if (typeof obj === 'object') { 21 | return Object.keys(obj) 22 | .map(function (k) { 23 | var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; 24 | if (Array.isArray(obj[k])) { 25 | return obj[k] 26 | .map(function (v) { 27 | return ks + encodeURIComponent(stringifyPrimitive(v)); 28 | }) 29 | .join(sep); 30 | } else { 31 | return ks + encodeURIComponent(stringifyPrimitive(obj[k])); 32 | } 33 | }) 34 | .filter(Boolean) 35 | .join(sep); 36 | } 37 | if (!name) return ''; 38 | return encodeURIComponent(stringifyPrimitive(name)) + eq + encodeURIComponent(stringifyPrimitive(obj)); 39 | }; 40 | 41 | var xhrRes = function (err, xhr, body) { 42 | var headers = {}; 43 | var strHeaders = xhr.getAllResponseHeaders(); 44 | if (strHeaders && strHeaders.length > 0) { 45 | strHeaders 46 | .trim() 47 | .split('\n') 48 | .forEach(function (item) { 49 | if (item) { 50 | var index = item.indexOf(':'); 51 | var key = item.substr(0, index).trim().toLowerCase(); 52 | var val = item.substr(index + 1).trim(); 53 | headers[key] = val; 54 | } 55 | }); 56 | } 57 | return { 58 | error: err, 59 | statusCode: xhr.status, 60 | statusMessage: xhr.statusText, 61 | headers: headers, 62 | body: body, 63 | }; 64 | }; 65 | 66 | var xhrBody = function (xhr, dataType) { 67 | return !dataType && dataType === 'text' ? xhr.responseText : xhr.response; 68 | }; 69 | 70 | var request = function (opt, callback) { 71 | // method 72 | var method = (opt.method || 'GET').toUpperCase(); 73 | 74 | // url、qs 75 | var url = opt.url; 76 | if (opt.qs) { 77 | var qsStr = queryStringify(opt.qs); 78 | if (qsStr) { 79 | url += (url.indexOf('?') === -1 ? '?' : '&') + qsStr; 80 | } 81 | } 82 | 83 | // 创建 ajax 实例 84 | var xhr = new XMLHttpRequest(); 85 | xhr.open(method, url, true); 86 | xhr.responseType = opt.dataType || 'text'; 87 | 88 | // 处理 xhrFields 属性 89 | if (opt.xhrFields) { 90 | for (var xhrField in opt.xhrFields) { 91 | xhr[xhrField] = opt.xhrFields[xhrField]; 92 | } 93 | } 94 | 95 | // 处理 headers 96 | var headers = opt.headers; 97 | if (headers) { 98 | for (var key in headers) { 99 | if ( 100 | headers.hasOwnProperty(key) && 101 | key.toLowerCase() !== 'content-length' && 102 | key.toLowerCase() !== 'user-agent' && 103 | key.toLowerCase() !== 'origin' && 104 | key.toLowerCase() !== 'host' 105 | ) { 106 | xhr.setRequestHeader(key, headers[key]); 107 | } 108 | } 109 | } 110 | 111 | // onprogress 112 | if (opt.onProgress && xhr.upload) xhr.upload.onprogress = opt.onProgress; 113 | if (opt.onDownloadProgress) xhr.onprogress = opt.onDownloadProgress; 114 | 115 | // timeout 116 | if (opt.timeout) xhr.timeout = opt.timeout; 117 | xhr.ontimeout = function (event) { 118 | var error = new Error('timeout'); 119 | callback(xhrRes(error, xhr)); 120 | }; 121 | 122 | // success 2xx/3xx/4xx 123 | xhr.onload = function () { 124 | callback(xhrRes(null, xhr, xhrBody(xhr, opt.dataType))); 125 | }; 126 | 127 | // error 5xx/0 (网络错误、跨域报错、Https connect-src 限制的报错时 statusCode 为 0) 128 | xhr.onerror = function (err) { 129 | var body = xhrBody(xhr, opt.dataType); 130 | if (body) { 131 | // 5xx 132 | callback(xhrRes(null, xhr, body)); 133 | } else { 134 | // 0 135 | var error = xhr.statusText; 136 | if (!error && xhr.status === 0) error = new Error('CORS blocked or network error'); 137 | callback(xhrRes(error, xhr, body)); 138 | } 139 | }; 140 | 141 | // send 142 | xhr.send(opt.body || ''); 143 | 144 | // 返回 ajax 实例,用于外部调用 xhr.abort 145 | return xhr; 146 | }; 147 | 148 | module.exports = request; 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cos-js-sdk-v5", 3 | "version": "1.10.1", 4 | "description": "JavaScript SDK for [腾讯云对象存储](https://cloud.tencent.com/product/cos)", 5 | "main": "dist/cos-js-sdk-v5.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "prettier": "prettier --write src demo/demo.js demo/CIDemos/*.js test/test.js server/sts.js lib/request.js index.d.ts", 9 | "server": "node server/sts.js", 10 | "dev": "cross-env NODE_ENV=development webpack -w --mode=development", 11 | "build": "cross-env NODE_ENV=production webpack --mode=production", 12 | "cos-auth.min.js": "uglifyjs ./demo/common/cos-auth.js -o ./demo/common/cos-auth.min.js -c -m", 13 | "test": "jest --runInBand --coverage", 14 | "postinstall": "node scripts/patch-check.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/tencentyun/cos-js-sdk-v5.git" 19 | }, 20 | "keywords": [], 21 | "author": "carsonxu", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/tencentyun/cos-js-sdk-v5/issues" 25 | }, 26 | "homepage": "https://github.com/tencentyun/cos-js-sdk-v5#readme", 27 | "dependencies": { 28 | "fast-xml-parser": "4.5.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "7.17.9", 32 | "@babel/plugin-transform-runtime": "7.18.10", 33 | "@babel/preset-env": "7.16.11", 34 | "babel-loader": "8.2.5", 35 | "body-parser": "^1.18.3", 36 | "cross-env": "^5.2.0", 37 | "express": "^4.16.4", 38 | "jest": "29.7.0", 39 | "jest-environment-jsdom": "29.7.0", 40 | "patch-package": "^8.0.0", 41 | "prettier": "^3.0.1", 42 | "qcloud-cos-sts": "^3.0.2", 43 | "request": "^2.87.0", 44 | "terser-webpack-plugin": "4.2.3", 45 | "uglifyjs": "^2.4.11", 46 | "webpack": "4.46.0", 47 | "webpack-cli": "4.10.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /patches/fast-xml-parser+4.5.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js b/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js 2 | index 70db055..17536b7 100644 3 | --- a/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js 4 | +++ b/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js 5 | @@ -184,7 +184,7 @@ function buildAttributesMap(attrStr, jPath, tagName) { 6 | } 7 | 8 | const parseXml = function(xmlData) { 9 | - xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line 10 | + // xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line 11 | const xmlObj = new xmlNode('!xml'); 12 | let currentNode = xmlObj; 13 | let textData = ""; 14 | -------------------------------------------------------------------------------- /patches/jsdom+20.0.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js b/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js 2 | index 8437900..d760a02 100644 3 | --- a/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js 4 | +++ b/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js 5 | @@ -307,8 +307,9 @@ function createClient(xhr) { 6 | } 7 | requestBody = Buffer.isBuffer(requestBody) ? requestBody : Buffer.from(requestBody); 8 | } 9 | - requestHeaders["Content-Length"] = len; 10 | + // requestHeaders["Content-Length"] = len; 11 | } 12 | + requestHeaders["Content-Length"] = len; 13 | requestHeaders["Accept-Encoding"] = "gzip, deflate"; 14 | const requestClient = new Request(uri, options, { method: flag.method, headers: requestHeaders }); 15 | if (hasBody) { 16 | -------------------------------------------------------------------------------- /scripts/patch-check.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const exec = require('child_process').execSync; 4 | 5 | const patchesDir = path.join(__dirname, '../patches'); 6 | const hasPatches = fs.existsSync(patchesDir); 7 | 8 | if (hasPatches) { 9 | // 执行补丁 10 | console.log('npx patch-package'); 11 | exec('npx patch-package', { stdio: 'inherit' }); 12 | } else { 13 | console.log('无补丁应用'); 14 | } -------------------------------------------------------------------------------- /src/async.js: -------------------------------------------------------------------------------- 1 | var eachLimit = function (arr, limit, iterator, callback) { 2 | callback = callback || function () {}; 3 | if (!arr.length || limit <= 0) { 4 | return callback(); 5 | } 6 | 7 | var completed = 0; 8 | var started = 0; 9 | var running = 0; 10 | 11 | (function replenish() { 12 | if (completed >= arr.length) { 13 | return callback(); 14 | } 15 | 16 | while (running < limit && started < arr.length) { 17 | started += 1; 18 | running += 1; 19 | iterator(arr[started - 1], function (err) { 20 | if (err) { 21 | callback(err); 22 | callback = function () {}; 23 | } else { 24 | completed += 1; 25 | running -= 1; 26 | if (completed >= arr.length) { 27 | callback(); 28 | } else { 29 | replenish(); 30 | } 31 | } 32 | }); 33 | } 34 | })(); 35 | }; 36 | 37 | var retry = function (times, iterator, callback) { 38 | var next = function (index) { 39 | iterator(function (err, data) { 40 | if (err && index < times) { 41 | next(index + 1); 42 | } else { 43 | callback(err, data); 44 | } 45 | }); 46 | }; 47 | if (times < 1) { 48 | callback(); 49 | } else { 50 | next(1); 51 | } 52 | }; 53 | 54 | var async = { 55 | eachLimit: eachLimit, 56 | retry: retry, 57 | }; 58 | 59 | module.exports = async; 60 | -------------------------------------------------------------------------------- /src/cos.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('./util'); 4 | var event = require('./event'); 5 | var task = require('./task'); 6 | var base = require('./base'); 7 | var advance = require('./advance'); 8 | var Logger = require('./logger'); 9 | var pkg = require('../package.json'); 10 | 11 | var defaultOptions = { 12 | AppId: '', // AppId 已废弃,请拼接到 Bucket 后传入,例如:test-1250000000 13 | SecretId: '', 14 | SecretKey: '', 15 | SecurityToken: '', // 使用临时密钥需要注意自行刷新 Token 16 | StartTime: 0, // 临时密钥返回起始时间 17 | ExpiredTime: 0, // 临时密钥过期时间 18 | ChunkRetryTimes: 2, 19 | FileParallelLimit: 3, 20 | ChunkParallelLimit: 3, 21 | ChunkSize: 1024 * 1024, 22 | SliceSize: 1024 * 1024, 23 | CopyChunkParallelLimit: 20, 24 | CopyChunkSize: 1024 * 1024 * 10, 25 | CopySliceSize: 1024 * 1024 * 10, 26 | MaxPartNumber: 10000, 27 | ProgressInterval: 1000, 28 | Domain: '', 29 | ServiceDomain: '', 30 | Protocol: '', 31 | CompatibilityMode: false, 32 | ForcePathStyle: false, 33 | UseRawKey: false, 34 | Timeout: 0, // 单位毫秒,0 代表不设置超时时间 35 | CorrectClockSkew: true, 36 | SystemClockOffset: 0, // 单位毫秒,ms 37 | UploadCheckContentMd5: false, 38 | UploadQueueSize: 10000, 39 | UploadAddMetaMd5: false, 40 | UploadIdCacheLimit: 50, 41 | UseAccelerate: false, 42 | ForceSignHost: true, // 默认将host加入签名计算,关闭后可能导致越权风险,建议保持为true 43 | AutoSwitchHost: true, 44 | CopySourceParser: null, // 自定义拷贝源解析器 45 | ObjectKeySimplifyCheck: true, // 开启合并校验 getObject Key 46 | /** 上报相关 **/ 47 | DeepTracker: false, // 上报时是否对每个分块上传做单独上报 48 | TrackerDelay: 5000, // 周期性上报,单位毫秒。0代表实时上报 49 | CustomId: '', // 自定义上报id 50 | BeaconReporter: null, // 灯塔上报组件,如有需要请自行传入,传入即代表开启上报 51 | ClsReporter: null, // cls 上报组件,如有需要请自行传入,传入即代表开启上报 52 | // 日志相关 53 | EnableLog: false, // 是否开启日志 54 | EnableLogcat: false, // 是否开启控制台日志打印 55 | LogLevel: 'VERBOSE', // 日志级别,支持 VERBOSE、DEBUG、INFO、WARN、ERROR,默认为 VERBOSE 56 | ClsLogger: null, // 日志上报到 cls 组件 57 | LogExtras: {}, // 日志上报时,附带的额外信息,例如:{deviceID: '', userID: ''} 58 | }; 59 | 60 | // 对外暴露的类 61 | var COS = function (options) { 62 | this.options = util.extend(util.clone(defaultOptions), options || {}); 63 | this.options.FileParallelLimit = Math.max(1, this.options.FileParallelLimit); 64 | this.options.ChunkParallelLimit = Math.max(1, this.options.ChunkParallelLimit); 65 | this.options.ChunkRetryTimes = Math.max(0, this.options.ChunkRetryTimes); 66 | this.options.ChunkSize = Math.max(1024 * 1024, this.options.ChunkSize); 67 | this.options.CopyChunkParallelLimit = Math.max(1, this.options.CopyChunkParallelLimit); 68 | this.options.CopyChunkSize = Math.max(1024 * 1024, this.options.CopyChunkSize); 69 | this.options.CopySliceSize = Math.max(0, this.options.CopySliceSize); 70 | this.options.MaxPartNumber = Math.max(1024, Math.min(10000, this.options.MaxPartNumber)); 71 | this.options.Timeout = Math.max(0, this.options.Timeout); 72 | this.options.EnableReporter = this.options.BeaconReporter || this.options.ClsReporter; 73 | 74 | if (this.options.AppId) { 75 | console.warn( 76 | 'warning: AppId has been deprecated, Please put it at the end of parameter Bucket(E.g: "test-1250000000").' 77 | ); 78 | } 79 | if (this.options.SecretId && this.options.SecretId.indexOf(' ') > -1) { 80 | console.error('error: SecretId格式错误,请检查'); 81 | console.error('error: SecretId format is incorrect. Please check'); 82 | } 83 | if (this.options.SecretKey && this.options.SecretKey.indexOf(' ') > -1) { 84 | console.error('error: SecretKey格式错误,请检查'); 85 | console.error('error: SecretKey format is incorrect. Please check'); 86 | } 87 | if (util.isNode()) { 88 | console.log('Tip: Next.js、Nuxt.js 等服务端渲染技术可正常使用JavaScript SDK,请忽略下方 nodejs 环境警告'); 89 | console.warn( 90 | 'warning: cos-js-sdk-v5 不支持 nodejs 环境使用,请改用 cos-nodejs-sdk-v5,参考文档: https://cloud.tencent.com/document/product/436/8629' 91 | ); 92 | console.warn( 93 | 'warning: cos-js-sdk-v5 does not support nodejs environment. Please use cos-nodejs-sdk-v5 instead. See: https://cloud.tencent.com/document/product/436/8629' 94 | ); 95 | } 96 | if (this.options.ForcePathStyle) { 97 | console.warn( 98 | 'cos-js-sdk-v5不再支持使用path-style,仅支持使用virtual-hosted-style,参考文档:https://cloud.tencent.com/document/product/436/96243' 99 | ); 100 | throw new Error('ForcePathStyle is not supported'); 101 | } 102 | event.init(this); 103 | task.init(this); 104 | // 初始化日志模块 105 | this.logger = new Logger({ 106 | enableLog: this.options.EnableLog, 107 | enableLogcat: this.options.EnableLogcat, 108 | level: this.options.LogLevel ?? 'VERBOSE', 109 | clsLogger: this.options.ClsLogger, 110 | logExtras: this.options.LogExtras ?? {}, 111 | }); 112 | if (this.options.EnableLog) { 113 | event.init(this.logger); 114 | this.logger.on('log-message', (data) => { 115 | this.emit('log-message', data); 116 | }); 117 | } 118 | }; 119 | 120 | base.init(COS, task); 121 | advance.init(COS, task); 122 | 123 | COS.util = { 124 | md5: util.md5, 125 | xml2json: util.xml2json, 126 | json2xml: util.json2xml, 127 | encodeBase64: util.encodeBase64, 128 | }; 129 | COS.getAuthorization = util.getAuth; 130 | COS.version = pkg.version; 131 | 132 | module.exports = COS; 133 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | var initEvent = function (cos) { 2 | var listeners = {}; 3 | var getList = function (action) { 4 | !listeners[action] && (listeners[action] = []); 5 | return listeners[action]; 6 | }; 7 | cos.on = function (action, callback) { 8 | if (action === 'task-list-update') { 9 | console.warn('warning: Event "' + action + '" has been deprecated. Please use "list-update" instead.'); 10 | } 11 | getList(action).push(callback); 12 | }; 13 | cos.off = function (action, callback) { 14 | var list = getList(action); 15 | for (var i = list.length - 1; i >= 0; i--) { 16 | callback === list[i] && list.splice(i, 1); 17 | } 18 | }; 19 | cos.emit = function (action, data) { 20 | var list = getList(action).map(function (cb) { 21 | return cb; 22 | }); 23 | for (var i = 0; i < list.length; i++) { 24 | list[i](data); 25 | } 26 | }; 27 | }; 28 | 29 | var EventProxy = function () { 30 | initEvent(this); 31 | }; 32 | 33 | module.exports.init = initEvent; 34 | module.exports.EventProxy = EventProxy; 35 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../package.json'); 2 | const pkgVersion = pkg.version; 3 | 4 | const logLevelList = ['VERBOSE', 'DEBUG', 'INFO', 'WARN', 'ERROR']; 5 | class Logger { 6 | level = 'VERBOSE'; // VERBOSE | DEBUG | INFO | WARN | ERROR 按日志等级排序 7 | clsLogger = null; 8 | logExtras = {}; 9 | constructor(params) { 10 | this.enableLog = params.enableLog ?? false; 11 | this.level = params.level || 'VERBOSE'; 12 | if (!logLevelList.includes(this.level)) { 13 | this.level = 'VERBOSE'; 14 | } 15 | this.enableLogcat = params.enableLogcat; 16 | this.clsLogger = params.clsLogger; 17 | this.logExtras = params.logExtras; 18 | } 19 | info(...msg) { 20 | if (['VERBOSE', 'INFO'].includes(this.level)) { 21 | this.log('info', ...msg); 22 | } 23 | } 24 | debug(...msg) { 25 | if (['VERBOSE', 'DEBUG'].includes(this.level)) { 26 | this.log('debug', ...msg); 27 | } 28 | } 29 | warn(...msg) { 30 | if (['VERBOSE', 'WARN'].includes(this.level)) { 31 | this.log('warn', ...msg); 32 | } 33 | } 34 | error(...msg) { 35 | if (['VERBOSE', 'ERROR'].includes(this.level)) { 36 | this.log('error', ...msg); 37 | } 38 | } 39 | /** 40 | * 参数结构 { 41 | * timestamp: '2021-08-16T06:51:27.781Z', 42 | * cate: 'PROCESS', 43 | * tag: 'network', 44 | * msg: {} 45 | * */ 46 | log(...args) { 47 | if (!this.enableLog) { 48 | return; 49 | } 50 | const type = args[0]; 51 | const { cate = 'base', tag = 'base', msg } = args[1]; 52 | const logMsg = { 53 | version: `cos-js-sdk-v5-${pkgVersion}`, 54 | timestamp: new Date().toISOString(), 55 | cate: `[${cate.toUpperCase()}]`, 56 | tag: `[${tag.toUpperCase()}]`, 57 | msg, 58 | extras: this.logExtras, 59 | }; 60 | // 日志输出到控制台 61 | if (this.enableLogcat) { 62 | console[type]( 63 | `[${logMsg.version}] ${logMsg.timestamp} ${logMsg.cate} ${logMsg.tag} ${logMsg.msg} ${logMsg.extras ? JSON.stringify(logMsg.extras) : ''}` 64 | ); 65 | } 66 | // 日志上报到 cls 67 | if (this.clsLogger) { 68 | this.clsLogger.log(logMsg, false); 69 | } 70 | // 日志回调 71 | this.emit('log-message', logMsg); 72 | } 73 | } 74 | 75 | module.exports = Logger; 76 | -------------------------------------------------------------------------------- /src/session.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | 3 | // 按照文件特征值,缓存 UploadId 4 | var cacheKey = 'cos_sdk_upload_cache'; 5 | var expires = 30 * 24 * 3600; 6 | var cache; 7 | var timer; 8 | 9 | var getCache = function () { 10 | try { 11 | var val = JSON.parse(localStorage.getItem(cacheKey)); 12 | } catch (e) {} 13 | if (!val) val = []; 14 | cache = val; 15 | }; 16 | var setCache = function () { 17 | try { 18 | if (cache.length) localStorage.setItem(cacheKey, JSON.stringify(cache)); 19 | else localStorage.removeItem(cacheKey); 20 | } catch (e) {} 21 | }; 22 | 23 | var init = function () { 24 | if (cache) return; 25 | getCache.call(this); 26 | // 清理太老旧的数据 27 | var changed = false; 28 | var now = Math.round(Date.now() / 1000); 29 | for (var i = cache.length - 1; i >= 0; i--) { 30 | var mtime = cache[i][2]; 31 | if (!mtime || mtime + expires < now) { 32 | cache.splice(i, 1); 33 | changed = true; 34 | } 35 | } 36 | changed && setCache(); 37 | }; 38 | 39 | // 把缓存存到本地 40 | var save = function () { 41 | if (timer) return; 42 | timer = setTimeout(function () { 43 | setCache(); 44 | timer = null; 45 | }, 400); 46 | }; 47 | 48 | var mod = { 49 | using: {}, 50 | // 标记 UploadId 正在使用 51 | setUsing: function (uuid) { 52 | mod.using[uuid] = true; 53 | }, 54 | // 标记 UploadId 已经没在使用 55 | removeUsing: function (uuid) { 56 | delete mod.using[uuid]; 57 | }, 58 | // 用上传参数生成哈希值 59 | getFileId: function (file, ChunkSize, Bucket, Key) { 60 | if (file.name && file.size && file.lastModifiedDate && ChunkSize) { 61 | return util.md5([file.name, file.size, file.lastModifiedDate, ChunkSize, Bucket, Key].join('::')); 62 | } else { 63 | return null; 64 | } 65 | }, 66 | // 用上传参数生成哈希值 67 | getCopyFileId: function (copySource, sourceHeaders, ChunkSize, Bucket, Key) { 68 | var size = sourceHeaders['content-length']; 69 | var etag = sourceHeaders.etag || ''; 70 | var lastModified = sourceHeaders['last-modified']; 71 | if (copySource && ChunkSize) { 72 | return util.md5([copySource, size, etag, lastModified, ChunkSize, Bucket, Key].join('::')); 73 | } else { 74 | return null; 75 | } 76 | }, 77 | // 获取文件对应的 UploadId 列表 78 | getUploadIdList: function (uuid) { 79 | if (!uuid) return null; 80 | init.call(this); 81 | var list = []; 82 | for (var i = 0; i < cache.length; i++) { 83 | if (cache[i][0] === uuid) list.push(cache[i][1]); 84 | } 85 | return list.length ? list : null; 86 | }, 87 | // 缓存 UploadId 88 | saveUploadId: function (uuid, UploadId, limit) { 89 | init.call(this); 90 | if (!uuid) return; 91 | // 清理没用的 UploadId,js 文件没有 FilePath ,只清理相同记录 92 | for (var i = cache.length - 1; i >= 0; i--) { 93 | var item = cache[i]; 94 | if (item[0] === uuid && item[1] === UploadId) { 95 | cache.splice(i, 1); 96 | } 97 | } 98 | cache.unshift([uuid, UploadId, Math.round(Date.now() / 1000)]); 99 | if (cache.length > limit) cache.splice(limit); 100 | save(); 101 | }, 102 | // UploadId 已用完,移除掉 103 | removeUploadId: function (UploadId) { 104 | init.call(this); 105 | delete mod.using[UploadId]; 106 | for (var i = cache.length - 1; i >= 0; i--) { 107 | if (cache[i][1] === UploadId) cache.splice(i, 1); 108 | } 109 | save(); 110 | }, 111 | }; 112 | 113 | module.exports = mod; 114 | -------------------------------------------------------------------------------- /src/task.js: -------------------------------------------------------------------------------- 1 | var session = require('./session'); 2 | var util = require('./util'); 3 | 4 | var originApiMap = {}; 5 | var transferToTaskMethod = function (apiMap, apiName) { 6 | originApiMap[apiName] = apiMap[apiName]; 7 | apiMap[apiName] = function (params, callback) { 8 | if (params.SkipTask) { 9 | originApiMap[apiName].call(this, params, callback); 10 | } else { 11 | this._addTask(apiName, params, callback); 12 | } 13 | }; 14 | }; 15 | 16 | var initTask = function (cos) { 17 | var queue = []; 18 | var tasks = {}; 19 | var uploadingFileCount = 0; 20 | var nextUploadIndex = 0; 21 | 22 | // 接口返回简略的任务信息 23 | var formatTask = function (task) { 24 | var t = { 25 | id: task.id, 26 | Bucket: task.Bucket, 27 | Region: task.Region, 28 | Key: task.Key, 29 | FilePath: task.FilePath, 30 | state: task.state, 31 | loaded: task.loaded, 32 | size: task.size, 33 | speed: task.speed, 34 | percent: task.percent, 35 | hashPercent: task.hashPercent, 36 | error: task.error, 37 | }; 38 | if (task.FilePath) t.FilePath = task.FilePath; 39 | if (task._custom) t._custom = task._custom; // 控制台使用 40 | return t; 41 | }; 42 | 43 | var emitListUpdate = (function () { 44 | var timer; 45 | var emit = function () { 46 | timer = 0; 47 | cos.emit('task-list-update', { list: util.map(queue, formatTask) }); 48 | cos.emit('list-update', { list: util.map(queue, formatTask) }); 49 | }; 50 | return function () { 51 | if (!timer) timer = setTimeout(emit); 52 | }; 53 | })(); 54 | 55 | var clearQueue = function () { 56 | if (queue.length <= cos.options.UploadQueueSize) return; 57 | for ( 58 | var i = 0; 59 | i < nextUploadIndex && // 小于当前操作的 index 才清理 60 | i < queue.length && // 大于队列才清理 61 | queue.length > cos.options.UploadQueueSize; // 如果还太多,才继续清理 62 | 63 | ) { 64 | var isActive = queue[i].state === 'waiting' || queue[i].state === 'checking' || queue[i].state === 'uploading'; 65 | if (!queue[i] || !isActive) { 66 | tasks[queue[i].id] && delete tasks[queue[i].id]; 67 | queue.splice(i, 1); 68 | nextUploadIndex--; 69 | } else { 70 | i++; 71 | } 72 | } 73 | emitListUpdate(); 74 | }; 75 | 76 | var startNextTask = function () { 77 | // 检查是否允许增加执行进程 78 | if (uploadingFileCount >= cos.options.FileParallelLimit) return; 79 | // 跳过不可执行的任务 80 | while (queue[nextUploadIndex] && queue[nextUploadIndex].state !== 'waiting') nextUploadIndex++; 81 | // 检查是否已遍历结束 82 | if (nextUploadIndex >= queue.length) return; 83 | // 上传该遍历到的任务 84 | var task = queue[nextUploadIndex]; 85 | nextUploadIndex++; 86 | uploadingFileCount++; 87 | task.state = 'checking'; 88 | task.params.onTaskStart && task.params.onTaskStart(formatTask(task)); 89 | !task.params.UploadData && (task.params.UploadData = {}); 90 | var apiParams = util.formatParams(task.api, task.params); 91 | originApiMap[task.api].call(cos, apiParams, function (err, data) { 92 | if (!cos._isRunningTask(task.id)) return; 93 | if (task.state === 'checking' || task.state === 'uploading') { 94 | task.state = err ? 'error' : 'success'; 95 | err && (task.error = err); 96 | uploadingFileCount--; 97 | emitListUpdate(); 98 | startNextTask(); 99 | task.callback && task.callback(err, data); 100 | if (task.state === 'success') { 101 | if (task.params) { 102 | delete task.params.UploadData; 103 | delete task.params.Body; 104 | delete task.params; 105 | } 106 | delete task.callback; 107 | } 108 | } 109 | clearQueue(); 110 | }); 111 | emitListUpdate(); 112 | // 异步执行下一个任务 113 | setTimeout(startNextTask); 114 | }; 115 | 116 | var killTask = function (id, switchToState) { 117 | var task = tasks[id]; 118 | if (!task) return; 119 | var waiting = task && task.state === 'waiting'; 120 | var running = task && (task.state === 'checking' || task.state === 'uploading'); 121 | if ( 122 | (switchToState === 'canceled' && task.state !== 'canceled') || 123 | (switchToState === 'paused' && waiting) || 124 | (switchToState === 'paused' && running) 125 | ) { 126 | task.state = switchToState; 127 | cos.emit('inner-kill-task', { TaskId: id, toState: switchToState }); 128 | try { 129 | var UploadId = task && task.params && task.params.UploadData.UploadId; 130 | } catch (e) {} 131 | if (switchToState === 'canceled' && UploadId) session.removeUsing(UploadId); 132 | emitListUpdate(); 133 | if (running) { 134 | uploadingFileCount--; 135 | startNextTask(); 136 | } 137 | if (switchToState === 'canceled') { 138 | if (task.params) { 139 | delete task.params.UploadData; 140 | delete task.params.Body; 141 | delete task.params; 142 | } 143 | delete task.callback; 144 | } 145 | } 146 | clearQueue(); 147 | }; 148 | 149 | cos._addTasks = function (taskList) { 150 | util.each(taskList, function (task) { 151 | cos._addTask(task.api, task.params, task.callback, true); 152 | }); 153 | emitListUpdate(); 154 | }; 155 | 156 | var isTaskReadyWarning = true; 157 | cos._addTask = function (api, params, callback, ignoreAddEvent) { 158 | // 复制参数对象 159 | params = util.formatParams(api, params); 160 | 161 | // 生成 id 162 | var id = util.uuid(); 163 | params.TaskId = id; 164 | params.onTaskReady && params.onTaskReady(id); 165 | if (params.TaskReady) { 166 | params.TaskReady(id); 167 | isTaskReadyWarning && 168 | console.warn('warning: Param "TaskReady" has been deprecated. Please use "onTaskReady" instead.'); 169 | isTaskReadyWarning = false; 170 | } 171 | 172 | var task = { 173 | // env 174 | params: params, 175 | callback: callback, 176 | api: api, 177 | index: queue.length, 178 | // task 179 | id: id, 180 | Bucket: params.Bucket, 181 | Region: params.Region, 182 | Key: params.Key, 183 | FilePath: params.FilePath || '', 184 | state: 'waiting', 185 | loaded: 0, 186 | size: 0, 187 | speed: 0, 188 | percent: 0, 189 | hashPercent: 0, 190 | error: null, 191 | _custom: params._custom, 192 | }; 193 | var onHashProgress = params.onHashProgress; 194 | params.onHashProgress = function (info) { 195 | if (!cos._isRunningTask(task.id)) return; 196 | task.hashPercent = info.percent; 197 | onHashProgress && onHashProgress(info); 198 | emitListUpdate(); 199 | }; 200 | var onProgress = params.onProgress; 201 | params.onProgress = function (info) { 202 | if (!cos._isRunningTask(task.id)) return; 203 | task.state === 'checking' && (task.state = 'uploading'); 204 | task.loaded = info.loaded; 205 | task.speed = info.speed; 206 | task.percent = info.percent; 207 | onProgress && onProgress(info); 208 | emitListUpdate(); 209 | }; 210 | 211 | // 异步获取 filesize 212 | util.getFileSize(api, params, function (err, size) { 213 | // 开始处理上传 214 | if (err) return callback(util.error(err)); // 如果获取大小出错,不加入队列 215 | // 获取完文件大小再把任务加入队列 216 | tasks[id] = task; 217 | queue.push(task); 218 | task.size = size; 219 | !ignoreAddEvent && emitListUpdate(); 220 | startNextTask(); 221 | clearQueue(); 222 | }); 223 | return id; 224 | }; 225 | cos._isRunningTask = function (id) { 226 | var task = tasks[id]; 227 | return !!(task && (task.state === 'checking' || task.state === 'uploading')); 228 | }; 229 | cos.getTaskList = function () { 230 | return util.map(queue, formatTask); 231 | }; 232 | cos.cancelTask = function (id) { 233 | killTask(id, 'canceled'); 234 | }; 235 | cos.pauseTask = function (id) { 236 | killTask(id, 'paused'); 237 | }; 238 | cos.restartTask = function (id) { 239 | var task = tasks[id]; 240 | if (task && (task.state === 'paused' || task.state === 'error')) { 241 | task.state = 'waiting'; 242 | emitListUpdate(); 243 | nextUploadIndex = Math.min(nextUploadIndex, task.index); 244 | startNextTask(); 245 | } 246 | }; 247 | cos.isUploadRunning = function () { 248 | return uploadingFileCount || nextUploadIndex < queue.length; 249 | }; 250 | }; 251 | 252 | module.exports.transferToTaskMethod = transferToTaskMethod; 253 | module.exports.init = initTask; 254 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | cos-js-sdk-v5-demo 9 | 10 | 11 | 12 | 13 |

cos-js-sdk-v5

14 |

15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/watcher.js: -------------------------------------------------------------------------------- 1 | const pti = require('puppeteer-to-istanbul') 2 | const puppeteer = require('puppeteer'); 3 | 4 | var fs = require('fs'); 5 | var util = require('util'); 6 | 7 | var logFile = fs.createWriteStream('log.txt', { flags: 'w' }); 8 | // Or 'w' to truncate the file every time the process starts. 9 | var logStdout = process.stdout; 10 | 11 | console.log = function () { 12 | logFile.write(util.format.apply(null, arguments) + '\n'); 13 | logStdout.write(util.format.apply(null, arguments) + '\n'); 14 | } 15 | console.error = console.log; 16 | 17 | (async () => { 18 | const browser = await puppeteer.launch({args: ['--no-proxy-server', '--no-sandbox', '--disable-setuid-sandbox']}); 19 | const page = await browser.newPage(); 20 | page.on('console', async function (msg) { 21 | var text = msg.text(); 22 | if (text.startsWith('[test-result]')) { 23 | console.log('==[TESTING-ENDS]==') 24 | const details = JSON.parse(text.replace('[test-result]', '')) 25 | console.log(details) 26 | const jsCoverage = await page.coverage.stopJSCoverage(); 27 | pti.write(jsCoverage, { includeHostname: true , storagePath: './.nyc_output' }) 28 | await browser.close() 29 | } else { 30 | console.log(msg.text()) 31 | } 32 | }); 33 | await page.coverage.startJSCoverage(); 34 | await page.goto('http://127.0.0.1:3000/test'); 35 | })() 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var webpack = require('webpack'); 4 | var pkg = require('./package'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | 7 | // replaceVersion 8 | var replaceVersion = function () { 9 | var filePath = path.resolve(__dirname, 'src/cos.js'); 10 | var content = fs.readFileSync(filePath).toString(); 11 | if (content) { 12 | var newContent = content.replace(/(COS\.version) *= *['"]\d+\.\d+\.\d+['"];/, "$1 = '" + pkg.version + "';"); 13 | if (newContent !== content) { 14 | fs.writeFileSync(filePath, newContent); 15 | console.log('cos.js version updated.'); 16 | } 17 | } 18 | }; 19 | var replaceDevCode = function (list) { 20 | list.forEach(function (fileName) { 21 | var filePath = path.resolve(__dirname, fileName); 22 | var content = fs.readFileSync(filePath).toString(); 23 | var newContent = content; 24 | newContent = newContent.replace(/https:\/\/\w+\.com\/[\w\-]+\/server\//, 'https://example.com/'); 25 | newContent = newContent.replace(/test-125\d{7}/, 'test-1250000000'); 26 | newContent = newContent.replace(/'proxy' => 'http:\/\/[^']+',/, "'proxy' => '',"); 27 | newContent = newContent.replace(/proxy: 'http:\/\/[^']+',/, "proxy: '',"); 28 | newContent = newContent.replace(/AKID\w+/, 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); 29 | newContent = newContent.replace(/'secretKey' => '[^']+',/, "'secretKey' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',"); 30 | newContent = newContent.replace(/secretKey: '[^']+',/, "secretKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',"); 31 | newContent = newContent.replace(/'allowActions' *=> *array\([^)]+\)/, `'allowActions' => array( 32 | // 所有 action 请看文档 https://cloud.tencent.com/document/product/436/31923 33 | // 简单上传 34 | 'name/cos:PutObject', 35 | // 分片上传 36 | 'name/cos:InitiateMultipartUpload', 37 | 'name/cos:ListMultipartUploads', 38 | 'name/cos:ListParts', 39 | 'name/cos:UploadPart', 40 | 'name/cos:CompleteMultipartUpload' 41 | )`); 42 | if (newContent !== content) { 43 | console.log('replace ' + filePath); 44 | fs.writeFileSync(filePath, newContent); 45 | } 46 | }); 47 | }; 48 | replaceVersion(); 49 | 50 | module.exports = { 51 | mode: process.env.NODE_ENV, 52 | entry: path.resolve(__dirname, './index.js'), 53 | devtool: 'none', 54 | output: { 55 | path: path.resolve(__dirname, './dist'), 56 | publicPath: '/dist/', 57 | filename: 'cos-js-sdk-v5.js', 58 | libraryTarget: 'umd', 59 | library: 'COS', 60 | globalObject: 'this', 61 | }, 62 | module: { 63 | rules: [ 64 | { 65 | test: /\.js$/, 66 | loader: 'babel-loader', 67 | exclude: /node_modules/ 68 | } 69 | ] 70 | }, 71 | optimization: { 72 | minimize: false, 73 | minimizer: [ 74 | new TerserPlugin({ 75 | cache: true, 76 | parallel: true, 77 | sourceMap: true, 78 | extractComments: false, 79 | }), 80 | ], 81 | }, 82 | devServer: { 83 | historyApiFallback: true, 84 | noInfo: true 85 | }, 86 | performance: { 87 | hints: false 88 | }, 89 | }; 90 | 91 | if (process.env.NODE_ENV === 'production') { 92 | replaceDevCode([ 93 | 'demo/demo.js', 94 | 'demo/queue/index.js', 95 | 'test/test.js', 96 | 'server/sts.js', 97 | ]); 98 | module.exports.output.filename = 'cos-js-sdk-v5.min.js'; 99 | module.exports.optimization = { 100 | minimize: true, 101 | minimizer: [ 102 | new TerserPlugin({ 103 | cache: true, 104 | parallel: true, 105 | sourceMap: true, 106 | extractComments: false, 107 | terserOptions: { 108 | compress: { 109 | drop_debugger: true, 110 | drop_console: true, 111 | }, 112 | format: { 113 | comments: false, // 删除注释 114 | }, 115 | }, 116 | }), 117 | ], 118 | }; 119 | module.exports.plugins = (module.exports.plugins || []).concat([ 120 | new webpack.DefinePlugin({ 121 | 'process.env': { 122 | NODE_ENV: '"production"' 123 | } 124 | }), 125 | new webpack.LoaderOptionsPlugin({ 126 | minimize: true 127 | }), 128 | ]); 129 | } 130 | --------------------------------------------------------------------------------