diff --git a/demo-album/lib/cos-wx-sdk-v5.js b/demo-album/lib/cos-wx-sdk-v5.js index f6be7ef..410667b 100644 --- a/demo-album/lib/cos-wx-sdk-v5.js +++ b/demo-album/lib/cos-wx-sdk-v5.js @@ -254,7 +254,7 @@ var getFileMd5 = function (body, callback) { function clone(obj) { return map(obj, function (v) { - return typeof v === 'object' ? clone(v) : v; + return typeof v === 'object' && v !== null ? clone(v) : v; }); } @@ -629,6 +629,13 @@ var canFileSlice = (function () { }; })(); +var isCIHost = function(url) { + if (url && url.split('?')[0].match(/(.ci.|ci.|.ci)/g)) { + return true; + } + return false; +} + var util = { noop: noop, formatParams: formatParams, @@ -659,6 +666,7 @@ var util = { getAuth: getAuth, compareVersion: compareVersion, canFileSlice: canFileSlice, + isCIHost: isCIHost, }; module.exports = util; @@ -2339,7 +2347,7 @@ base.init(COS, task); advance.init(COS, task); COS.getAuthorization = util.getAuth; -COS.version = '1.0.13'; +COS.version = '1.1.0'; module.exports = COS; @@ -8098,6 +8106,53 @@ function multipartAbort(params, callback) { }); } +/** + * 追加上传 + * @param {Object} params 参数对象,必须 + * @param {String} params.Bucket Bucket名称,必须 + * @param {String} params.Region 地域名称,必须 + * @param {String} params.Key object名称,必须 + * @param {String} params.Body 上传文件的内容,只支持字符串 + * @param {Number} params.Position 追加操作的起始点,单位为字节,必须 + * @param {String} params.CacheControl RFC 2616 中定义的缓存策略,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentDisposition RFC 2616 中定义的文件名称,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentEncoding RFC 2616 中定义的编码格式,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentLength RFC 2616 中定义的 HTTP 请求内容长度(字节),必须 + * @param {String} params.ContentType RFC 2616 中定义的内容类型(MIME),将作为 Object 元数据保存,非必须 + * @param {String} params.Expect 当使用 Expect: 100-continue 时,在收到服务端确认后,才会发送请求内容,非必须 + * @param {String} params.Expires RFC 2616 中定义的过期时间,将作为 Object 元数据保存,非必须 + * @param {String} params.ACL 允许用户自定义文件权限,有效值:private | public-read,非必须 + * @param {String} params.GrantRead 赋予被授权者读取对象的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantReadAcp 赋予被授权者读取对象的访问控制列表(ACL)的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantWriteAcp 赋予被授权者写入对象的访问控制列表(ACL)的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantFullControl 赋予被授权者操作对象的所有权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.StorageClass 设置对象的存储级别,枚举值:STANDARD、STANDARD_IA、ARCHIVE,默认值:STANDARD,非必须 + * @param {String} params.x-cos-meta-* 允许用户自定义的头部信息,将作为对象的元数据保存。大小限制2KB,非必须 + * @param {String} params.ContentSha1 RFC 3174 中定义的 160-bit 内容 SHA-1 算法校验,非必须 + * @param {String} params.ServerSideEncryption 支持按照指定的加密算法进行服务端数据加密,格式 x-cos-server-side-encryption: "AES256",非必须 + * @param {Function} callback 回调函数,必须 + * @return {Object} err 请求失败的错误,如果请求成功,则为空。https://cloud.tencent.com/document/product/436/7730 + * @return {Object} data 返回的数据 + */ + function appendObject(params, callback) { + submitRequest.call(this, { + Action: 'name/cos:AppendObject', + method: 'POST', + Bucket: params.Bucket, + Region: params.Region, + action: 'append', + Key: params.Key, + body: params.Body, + qs: { + position: params.Position + }, + headers: params.Headers, + }, function (err, data) { + if (err) return callback(err); + callback(null, data); + }); +} + /** * cos 内置请求 @@ -8119,6 +8174,8 @@ function multipartAbort(params, callback) { headers: params.Headers, qs: params.Query, body: params.Body, + Url: params.Url, + rawBody: params.RawBody, }, function (err, data) { if (err) return callback(err); if (data && data.body) { @@ -8616,7 +8673,7 @@ function _submitRequest(params, callback) { var region = params.Region; var object = params.Key; var method = params.method || 'GET'; - var url = params.url; + var url = params.url || params.Url; var body = params.body; var json = params.json; var rawBody = params.rawBody; @@ -8654,12 +8711,18 @@ function _submitRequest(params, callback) { json: json, }; + // 兼容ci接口 + var token = 'x-cos-security-token'; + if (util.isCIHost(url)) { + token = 'x-ci-security-token'; + } + // 获取签名 opt.headers.Authorization = params.AuthData.Authorization; params.AuthData.Token && (opt.headers['token'] = params.AuthData.Token); params.AuthData.ClientIP && (opt.headers['clientIP'] = params.AuthData.ClientIP); params.AuthData.ClientUA && (opt.headers['clientUA'] = params.AuthData.ClientUA); - params.AuthData.XCosSecurityToken && (opt.headers['x-cos-security-token'] = params.AuthData.XCosSecurityToken); + params.AuthData.XCosSecurityToken && (opt.headers[token] = params.AuthData.XCosSecurityToken); // 清理 undefined 和 null 字段 opt.headers && (opt.headers = util.clearKey(opt.headers)); @@ -8811,6 +8874,7 @@ var API_MAP = { putObjectTagging: putObjectTagging, getObjectTagging: getObjectTagging, deleteObjectTagging: deleteObjectTagging, + appendObject: appendObject, // 分块上传相关方法 uploadPartCopy: uploadPartCopy, @@ -8840,22 +8904,47 @@ module.exports.init = function (COS, task) { /* 18 */ /***/ (function(module, exports) { +function camSafeUrlEncode(str) { + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/\*/g, '%2A'); +} + +function getObjectKeys(obj, forKey) { + var list = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + list.push(forKey ? camSafeUrlEncode(key).toLowerCase() : key); + } + } + return list.sort(function (a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + return a === b ? 0 : (a > b ? 1 : -1); + }); +}; + var obj2str = function (obj) { - var i, key, val; - var list = []; - var keyList = Object.keys(obj); - for (i = 0; i < keyList.length; i++) { - key = keyList[i]; - val = obj[key] || ''; - list.push(key + '=' + encodeURIComponent(val)); - } - return list.join('&'); + var i, key, val; + var list = []; + var keyList = getObjectKeys(obj); + for (i = 0; i < keyList.length; i++) { + key = keyList[i]; + val = (obj[key] === undefined || obj[key] === null) ? '' : ('' + obj[key]); + key = camSafeUrlEncode(key).toLowerCase(); + val = camSafeUrlEncode(val) || ''; + list.push(key + '=' + val) + } + return list.join('&'); }; var request = function (params, callback) { var filePath = params.filePath; var headers = params.headers || {}; - var url = params.url; + var url = params.url || params.Url; var method = params.method; var onProgress = params.onProgress; var requestTask; @@ -8894,6 +8983,7 @@ var request = function (params, callback) { 'Expires', 'x-cos-storage-class', 'x-cos-security-token', + 'x-ci-security-token', ]; for (var i in params.headers) { if (params.headers.hasOwnProperty(i) && (i.indexOf('x-cos-meta-') > -1 || headerKeys.indexOf(i) > -1)) { diff --git a/demo/demo-sdk.js b/demo/demo-sdk.js index 51ba3b4..4155157 100644 --- a/demo/demo-sdk.js +++ b/demo/demo-sdk.js @@ -213,7 +213,7 @@ var toolsDao = { } }); }, - '分片上传': function() { + 'sliceUploadFile 分片上传': function() { var sliceUploadFile = function (file) { var key = file.name; cos.sliceUploadFile({ @@ -341,7 +341,7 @@ var toolsDao = { Body: fileBuf, }, requestCallback); }, - getObjectUrl: function() { + 'getObjectUrl 获取对象访问url': function() { var url = cos.getObjectUrl({ Bucket: config.Bucket, // Bucket 格式:test-1250000000 Region: config.Region, @@ -361,44 +361,44 @@ var bucketDao = { cos.getService(requestCallback); }, // 简单 Bucket 操作 - putBucket: function() { + 'putBucket 创建存储桶': function() { cos.putBucket({ Bucket: config.Bucket, Region: config.Region, }, requestCallback); }, - headBucket: function() { + 'headBucket 检索存储桶及其权限': function() { cos.headBucket({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - deleteBucket: function() { + 'deleteBucket 删除存储桶': function() { cos.deleteBucket({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - getBucketACL: function() { + 'getBucketACL 查询存储桶 ACL': function() { cos.getBucketAcl({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - putBucketACL: function() { + 'putBucketACL 设置存储桶 ACL': function() { cos.putBucketAcl({ Bucket: config.Bucket, Region: config.Region, ACL: 'public-read' }, requestCallback); }, - getBucketCors: function() { + 'getBucketCors 查询跨域配置': function() { cos.getBucketCors({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - putBucketCors: function() { + 'putBucketCors 设置跨域配置': function() { cos.putBucketCors({ Bucket: config.Bucket, Region: config.Region, @@ -411,13 +411,13 @@ var bucketDao = { }] }, requestCallback); }, - deleteBucketCors: function() { + 'deleteBucketCors 删除跨域配置': function() { cos.deleteBucketCors({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - putBucketPolicy: function() { + 'putBucketPolicy 设置存储桶策略': function() { var AppId = config.Bucket.substr(config.Bucket.lastIndexOf('-') + 1); cos.putBucketPolicy({ Bucket: config.Bucket, @@ -447,31 +447,31 @@ var bucketDao = { }, }, requestCallback); }, - getBucketPolicy: function() { + 'getBucketPolicy 查询存储桶策略': function() { cos.getBucketPolicy({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - deleteBucketPolicy: function() { + 'deleteBucketPolicy 删除存储桶策略': function() { cos.deleteBucketPolicy({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - getBucketLocation: function() { + 'getBucketLocation 获取Bucket的地域信息': function() { cos.getBucketLocation({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - getBucketTagging: function() { + 'getBucketTagging 获取Bucket标签': function() { cos.getBucketTagging({ Bucket: config.Bucket, Region: config.Region }, requestCallback); }, - putBucketTagging: function() { + 'putBucketTagging 设置Bucket标签': function() { cos.putBucketTagging({ Bucket: config.Bucket, Region: config.Region, @@ -489,7 +489,7 @@ var bucketDao = { } }, requestCallback); }, - deleteBucketTagging: function() { + 'deleteBucketTagging 删除存储桶标签': function() { cos.deleteBucketTagging({ Bucket: config.Bucket, Region: config.Region @@ -581,7 +581,7 @@ var objectDao = { Body: fileBuf, }, requestCallback); }, - getObject: function() { + 'getObject 下载对象': function() { cos.getObject({ Bucket: config.Bucket, Region: config.Region, @@ -590,28 +590,28 @@ var objectDao = { // QueryString: `imageMogr2/thumbnail/200x/`, }, requestCallback); }, - headObject: function() { + 'headObject 检索对象': function() { cos.headObject({ Bucket: config.Bucket, Region: config.Region, Key: '1.txt' }, requestCallback); }, - deleteObject: function() { + 'deleteObject 删除对象': function() { cos.deleteObject({ Bucket: config.Bucket, Region: config.Region, Key: '1.txt' }, requestCallback); }, - getObjectACL: function() { + 'getObjectACL 获取对象ACL': function() { cos.getObjectAcl({ Bucket: config.Bucket, Region: config.Region, Key: '1.txt' }, requestCallback); }, - putObjectACL: function() { + 'putObjectACL 设置对象ACL': function() { cos.putObjectAcl({ Bucket: config.Bucket, // Bucket 格式:test-1250000000 Region: config.Region, @@ -636,7 +636,7 @@ var objectDao = { // } }, requestCallback); }, - deleteMultipleObject: function() { + 'deleteMultipleObject 批量删除对象': function() { cos.deleteMultipleObject({ Bucket: config.Bucket, // Bucket 格式:test-1250000000 Region: config.Region, @@ -649,7 +649,7 @@ var objectDao = { ] }, requestCallback); }, - putObjectCopy: function() { + 'putObjectCopy 复制对象': function() { cos.putObjectCopy({ Bucket: config.Bucket, // Bucket 格式:test-1250000000 Region: config.Region, @@ -657,7 +657,7 @@ var objectDao = { CopySource: config.Bucket + '.cos.' + config.Region + '.myqcloud.com/1.txt', }, requestCallback); }, - restoreObject: function() { + 'restoreObject 恢复归档对象': function() { cos.restoreObject({ Bucket: config.Bucket, Region: config.Region, @@ -670,7 +670,44 @@ var objectDao = { } }, requestCallback); }, - abortUploadTask: function() { + 'appendObject 追加上传': function() { + // 初始化一个可追加上传的对象append.txt + cos.appendObject({ + Bucket: config.Bucket, // Bucket 格式:test-1250000000 + Region: config.Region, + Key: 'append.txt', /* 必须 */ + Body: '12345', + Position: 0, + }, + function(err, data) { + logger.log('putObject:', err || data); + }) + }, + 'appendObject_continue 继续追加上传': function() { + // append.txt必须是一个可追加上传的对象,参考文档:https://cloud.tencent.com/document/product/436/7741 + cos.headObject({ + Bucket: config.Bucket, // Bucket 格式:test-1250000000 + Region: config.Region, + Key: 'append.txt', /* 必须 */ + }, function(err, data) { + if (err) return console.log(err); + // 首先取到要追加的文件当前长度,即需要上送的Position + var position = data.headers['content-length']; + cos.appendObject({ + Bucket: config.Bucket, // Bucket 格式:test-1250000000 + Region: config.Region, + Key: 'append.txt', /* 必须 */ + Body: '66666', + Position: position, + }, + function(err, data) { + // 也可以取到下一次上传的position继续追加上传 + // var nextPosition = data.headers['x-cos-next-append-position']; + logger.log('putObject:', err || data); + }) + }); + }, + 'abortUploadTask 抛弃分块上传任务': function() { cos.abortUploadTask({ Bucket: config.Bucket, /* 必须 */ // Bucket 格式:test-1250000000 @@ -690,7 +727,7 @@ var objectDao = { } var advanceObjectDao = { - 'sliceUploadFile 分片上传': function() { + 'sliceUploadFile 分块上传': function() { var sliceUploadFile = function (file) { var key = file.name; cos.sliceUploadFile({ @@ -741,7 +778,7 @@ var advanceObjectDao = { // } // }) }, - 'sliceCopyFile 分片复制': function() { + 'sliceCopyFile 分块复制对象': function() { // 创建测试文件 var sourceName = '1.txt'; var Key = '1.slicecopy.exe'; @@ -852,7 +889,62 @@ var ciObjectDao = { (err, data) => { console.log('不带签名', err || data); }); - + }, + 'describeMediaBuckets 查询已经开通数据万象功能的存储桶': function() { + var host = 'ci.' + config.Region + '.myqcloud.com'; + var url = 'https://' + host + '/mediabucket'; + cos.request({ + Bucket: config.Bucket, + Region: config.Region, + Method: 'GET', + Key: 'mediabucket', /** 固定值,必须 */ + Url: url, + Query: { + pageNumber: '1', /** 第几页,非必须 */ + pageSize: '10', /** 每页个数,非必须 */ + // regions: 'ap-chengdu', /** 地域信息,例如'ap-beijing',支持多个值用逗号分隔如'ap-shanghai,ap-beijing',非必须 */ + // bucketNames: 'test-1250000000', /** 存储桶名称,精确搜索,例如'test-1250000000',支持多个值用逗号分隔如'test1-1250000000,test2-1250000000',非必须 */ + // bucketName: 'test', /** 存储桶名称前缀,前缀搜索,例如'test',支持多个值用逗号分隔如'test1,test2',非必须 */ + } + }, + function(err, data){ + console.log(err || data); + }); + }, + 'getMediaInfo 获取媒体文件信息': function() { + cos.request({ + Bucket: config.Bucket, + Region: config.Region, + Method: 'GET', + Key: 'test.mp4', + Query: { + 'ci-process': 'videoinfo' /** 固定值,必须 */ + } + }, + function(err, data){ + console.log(err || data); + }); + }, + 'getSnapshot 获取媒体文件某个时间的截图': function() { + cos.request({ + Bucket: config.Bucket, + Region: config.Region, + Method: 'GET', + Key: 'test.mp4', + Query: { + 'ci-process': 'snapshot', /** 固定值,必须 */ + time: 1, /** 截图的时间点,单位为秒,必须 */ + // width: 0, /** 截图的宽,非必须 */ + // height: 0, /** 截图的高,非必须 */ + // format: 'jpg', /** 截图的格式,支持 jpg 和 png,默认 jpg,非必须 */ + // rotate: 'auto', /** 图片旋转方式,默认为'auto',非必须 */ + // mode: 'exactframe', /** 截帧方式,默认为'exactframe',非必须 */ + }, + RawBody: true, + }, + function(err, data){ + console.log(err || data); + }); }, } diff --git a/demo/lib/cos-wx-sdk-v5.js b/demo/lib/cos-wx-sdk-v5.js index f6be7ef..410667b 100644 --- a/demo/lib/cos-wx-sdk-v5.js +++ b/demo/lib/cos-wx-sdk-v5.js @@ -254,7 +254,7 @@ var getFileMd5 = function (body, callback) { function clone(obj) { return map(obj, function (v) { - return typeof v === 'object' ? clone(v) : v; + return typeof v === 'object' && v !== null ? clone(v) : v; }); } @@ -629,6 +629,13 @@ var canFileSlice = (function () { }; })(); +var isCIHost = function(url) { + if (url && url.split('?')[0].match(/(.ci.|ci.|.ci)/g)) { + return true; + } + return false; +} + var util = { noop: noop, formatParams: formatParams, @@ -659,6 +666,7 @@ var util = { getAuth: getAuth, compareVersion: compareVersion, canFileSlice: canFileSlice, + isCIHost: isCIHost, }; module.exports = util; @@ -2339,7 +2347,7 @@ base.init(COS, task); advance.init(COS, task); COS.getAuthorization = util.getAuth; -COS.version = '1.0.13'; +COS.version = '1.1.0'; module.exports = COS; @@ -8098,6 +8106,53 @@ function multipartAbort(params, callback) { }); } +/** + * 追加上传 + * @param {Object} params 参数对象,必须 + * @param {String} params.Bucket Bucket名称,必须 + * @param {String} params.Region 地域名称,必须 + * @param {String} params.Key object名称,必须 + * @param {String} params.Body 上传文件的内容,只支持字符串 + * @param {Number} params.Position 追加操作的起始点,单位为字节,必须 + * @param {String} params.CacheControl RFC 2616 中定义的缓存策略,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentDisposition RFC 2616 中定义的文件名称,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentEncoding RFC 2616 中定义的编码格式,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentLength RFC 2616 中定义的 HTTP 请求内容长度(字节),必须 + * @param {String} params.ContentType RFC 2616 中定义的内容类型(MIME),将作为 Object 元数据保存,非必须 + * @param {String} params.Expect 当使用 Expect: 100-continue 时,在收到服务端确认后,才会发送请求内容,非必须 + * @param {String} params.Expires RFC 2616 中定义的过期时间,将作为 Object 元数据保存,非必须 + * @param {String} params.ACL 允许用户自定义文件权限,有效值:private | public-read,非必须 + * @param {String} params.GrantRead 赋予被授权者读取对象的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantReadAcp 赋予被授权者读取对象的访问控制列表(ACL)的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantWriteAcp 赋予被授权者写入对象的访问控制列表(ACL)的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantFullControl 赋予被授权者操作对象的所有权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.StorageClass 设置对象的存储级别,枚举值:STANDARD、STANDARD_IA、ARCHIVE,默认值:STANDARD,非必须 + * @param {String} params.x-cos-meta-* 允许用户自定义的头部信息,将作为对象的元数据保存。大小限制2KB,非必须 + * @param {String} params.ContentSha1 RFC 3174 中定义的 160-bit 内容 SHA-1 算法校验,非必须 + * @param {String} params.ServerSideEncryption 支持按照指定的加密算法进行服务端数据加密,格式 x-cos-server-side-encryption: "AES256",非必须 + * @param {Function} callback 回调函数,必须 + * @return {Object} err 请求失败的错误,如果请求成功,则为空。https://cloud.tencent.com/document/product/436/7730 + * @return {Object} data 返回的数据 + */ + function appendObject(params, callback) { + submitRequest.call(this, { + Action: 'name/cos:AppendObject', + method: 'POST', + Bucket: params.Bucket, + Region: params.Region, + action: 'append', + Key: params.Key, + body: params.Body, + qs: { + position: params.Position + }, + headers: params.Headers, + }, function (err, data) { + if (err) return callback(err); + callback(null, data); + }); +} + /** * cos 内置请求 @@ -8119,6 +8174,8 @@ function multipartAbort(params, callback) { headers: params.Headers, qs: params.Query, body: params.Body, + Url: params.Url, + rawBody: params.RawBody, }, function (err, data) { if (err) return callback(err); if (data && data.body) { @@ -8616,7 +8673,7 @@ function _submitRequest(params, callback) { var region = params.Region; var object = params.Key; var method = params.method || 'GET'; - var url = params.url; + var url = params.url || params.Url; var body = params.body; var json = params.json; var rawBody = params.rawBody; @@ -8654,12 +8711,18 @@ function _submitRequest(params, callback) { json: json, }; + // 兼容ci接口 + var token = 'x-cos-security-token'; + if (util.isCIHost(url)) { + token = 'x-ci-security-token'; + } + // 获取签名 opt.headers.Authorization = params.AuthData.Authorization; params.AuthData.Token && (opt.headers['token'] = params.AuthData.Token); params.AuthData.ClientIP && (opt.headers['clientIP'] = params.AuthData.ClientIP); params.AuthData.ClientUA && (opt.headers['clientUA'] = params.AuthData.ClientUA); - params.AuthData.XCosSecurityToken && (opt.headers['x-cos-security-token'] = params.AuthData.XCosSecurityToken); + params.AuthData.XCosSecurityToken && (opt.headers[token] = params.AuthData.XCosSecurityToken); // 清理 undefined 和 null 字段 opt.headers && (opt.headers = util.clearKey(opt.headers)); @@ -8811,6 +8874,7 @@ var API_MAP = { putObjectTagging: putObjectTagging, getObjectTagging: getObjectTagging, deleteObjectTagging: deleteObjectTagging, + appendObject: appendObject, // 分块上传相关方法 uploadPartCopy: uploadPartCopy, @@ -8840,22 +8904,47 @@ module.exports.init = function (COS, task) { /* 18 */ /***/ (function(module, exports) { +function camSafeUrlEncode(str) { + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/\*/g, '%2A'); +} + +function getObjectKeys(obj, forKey) { + var list = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + list.push(forKey ? camSafeUrlEncode(key).toLowerCase() : key); + } + } + return list.sort(function (a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + return a === b ? 0 : (a > b ? 1 : -1); + }); +}; + var obj2str = function (obj) { - var i, key, val; - var list = []; - var keyList = Object.keys(obj); - for (i = 0; i < keyList.length; i++) { - key = keyList[i]; - val = obj[key] || ''; - list.push(key + '=' + encodeURIComponent(val)); - } - return list.join('&'); + var i, key, val; + var list = []; + var keyList = getObjectKeys(obj); + for (i = 0; i < keyList.length; i++) { + key = keyList[i]; + val = (obj[key] === undefined || obj[key] === null) ? '' : ('' + obj[key]); + key = camSafeUrlEncode(key).toLowerCase(); + val = camSafeUrlEncode(val) || ''; + list.push(key + '=' + val) + } + return list.join('&'); }; var request = function (params, callback) { var filePath = params.filePath; var headers = params.headers || {}; - var url = params.url; + var url = params.url || params.Url; var method = params.method; var onProgress = params.onProgress; var requestTask; @@ -8894,6 +8983,7 @@ var request = function (params, callback) { 'Expires', 'x-cos-storage-class', 'x-cos-security-token', + 'x-ci-security-token', ]; for (var i in params.headers) { if (params.headers.hasOwnProperty(i) && (i.indexOf('x-cos-meta-') > -1 || headerKeys.indexOf(i) > -1)) { diff --git a/demo/project.config.json b/demo/project.config.json index 42494af..674c38c 100644 --- a/demo/project.config.json +++ b/demo/project.config.json @@ -18,18 +18,22 @@ "checkSiteMap": true, "uploadWithSourceMap": true, "compileHotReLoad": false, - "useMultiFrameRuntime": false, + "useMultiFrameRuntime": true, "useApiHook": true, + "useApiHostProcess": true, "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, + "enableEngineNative": false, "useIsolateContext": true, "useCompilerModule": true, "userConfirmedUseCompilerModuleSwitch": false, + "userConfirmedBundleSwitch": false, "packNpmManually": false, - "packNpmRelationList": [] + "packNpmRelationList": [], + "minifyWXSS": true }, "compileType": "miniprogram", "libVersion": "1.6.0", diff --git a/demo/test.js b/demo/test.js index 2fcc7ca..a2f4721 100644 --- a/demo/test.js +++ b/demo/test.js @@ -1,8 +1,14 @@ var COS = require('./lib/cos-wx-sdk-v5'); var wxfs = wx.getFileSystemManager(); var config = require('./config'); +// 这里替换成自己的Uin (账号ID查询:https://console.cloud.tencent.com/developer) config.Uin = '10001'; +/** + * 测试须知 + * 需要本地准备一个名为 5m.zip,大小为5mb的文件进行初始化上传 +**/ + var util = { createFile: function (options, filePath) { var buffer = new ArrayBuffer(options.size || 0); @@ -364,7 +370,7 @@ group('putObject(),cancelTask()', function () { Bucket: config.Bucket, Region: config.Region, Key: filename, - Body: util.createFile({size: 1024 * 1024 * 100}), + Body: util.createFile({size: 1024 * 1024 * 10}), onTaskReady: function (taskId) { TaskId = taskId; }, @@ -391,8 +397,8 @@ group('putObject(),cancelTask()', function () { group('sliceUploadFile() 完整上传文件', function () { test('sliceUploadFile() 完整上传文件', function (done, assert) { var lastPercent; - var filename = '3m.zip'; - var fileSize = 1024 * 1024 * 3; + var filename = '5m.zip'; + var fileSize = 1024 * 1024 * 5; cos.abortUploadTask({ Bucket: config.Bucket, Region: config.Region, @@ -473,8 +479,8 @@ group('sliceUploadFile(),pauseTask(),restartTask()', function () { group('sliceUploadFile(),cancelTask()', function () { test('sliceUploadFile(),cancelTask()', function (done, assert) { - var filename = '3m.zip'; - var blob = util.createFile({size: 1024 * 1024 * 3}); + var filename = '5m.zip'; + var blob = util.createFile({size: 1024 * 1024 * 5}); var alive = false; var canceled = false; cos.sliceUploadFile({ @@ -2840,3 +2846,91 @@ group('Query 的键值带有特殊字符', function () { }); }); }); + +group('appendObject', function () { + test('appendObject()', function (done, assert) { + cos.headObject({ + Bucket: config.Bucket, // Bucket 格式:test-1250000000 + Region: config.Region, + Key: 'append.txt', /* 必须 */ + }, function(err, data) { + assert.ok(!err); + if (err) return console.log(err); + // 首先取到要追加的文件当前长度,即需要上送的Position + var position = data.headers['content-length']; + cos.appendObject({ + Bucket: config.Bucket, // Bucket 格式:test-1250000000 + Region: config.Region, + Key: 'append.txt', /* 必须 */ + Body: '66666', + Position: position, + }, + function(err, data) { + assert.ok(!err); + done(); + }) + }); + }); +}); + +group('数据万象', function () { + test('describeMediaBuckets()', function (done, assert) { + var host = 'ci.' + config.Region + '.myqcloud.com'; + var url = 'https://' + host + '/mediabucket'; + cos.request({ + Bucket: config.Bucket, + Region: config.Region, + Method: 'GET', + Key: 'mediabucket', /** 固定值,必须 */ + Url: url, + Query: { + pageNumber: '1', /** 第几页,非必须 */ + pageSize: '10', /** 每页个数,非必须 */ + // regions: 'ap-chengdu', /** 地域信息,例如'ap-beijing',支持多个值用逗号分隔如'ap-shanghai,ap-beijing',非必须 */ + // bucketNames: 'test-1250000000', /** 存储桶名称,精确搜索,例如'test-1250000000',支持多个值用逗号分隔如'test1-1250000000,test2-1250000000',非必须 */ + // bucketName: 'test', /** 存储桶名称前缀,前缀搜索,例如'test',支持多个值用逗号分隔如'test1,test2',非必须 */ + } + }, + function(err, data){ + assert.ok(!err); + done(); + }); + }); + test('getMediaInfo()', function (done, assert) { + cos.request({ + Bucket: config.Bucket, + Region: config.Region, + Method: 'GET', + Key: 'test.mp4', + Query: { + 'ci-process': 'videoinfo' /** 固定值,必须 */ + } + }, + function(err, data){ + assert.ok(!err); + done(); + }); + }); + test('getSnapshot()', function (done, assert) { + cos.request({ + Bucket: config.Bucket, + Region: config.Region, + Method: 'GET', + Key: 'test.mp4', + Query: { + 'ci-process': 'snapshot', /** 固定值,必须 */ + time: 1, /** 截图的时间点,单位为秒,必须 */ + // width: 0, /** 截图的宽,非必须 */ + // height: 0, /** 截图的高,非必须 */ + // format: 'jpg', /** 截图的格式,支持 jpg 和 png,默认 jpg,非必须 */ + // rotate: 'auto', /** 图片旋转方式,默认为'auto',非必须 */ + // mode: 'exactframe', /** 截帧方式,默认为'exactframe',非必须 */ + }, + RawBody: true, + }, + function(err, data){ + assert.ok(!err); + done(); + }); + }); + }); diff --git a/lib/request.js b/lib/request.js index d5fdbb4..60b2d9f 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,19 +1,44 @@ +function camSafeUrlEncode(str) { + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/\*/g, '%2A'); +} + +function getObjectKeys(obj, forKey) { + var list = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + list.push(forKey ? camSafeUrlEncode(key).toLowerCase() : key); + } + } + return list.sort(function (a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + return a === b ? 0 : (a > b ? 1 : -1); + }); +}; + var obj2str = function (obj) { - var i, key, val; - var list = []; - var keyList = Object.keys(obj); - for (i = 0; i < keyList.length; i++) { - key = keyList[i]; - val = obj[key] || ''; - list.push(key + '=' + encodeURIComponent(val)); - } - return list.join('&'); + var i, key, val; + var list = []; + var keyList = getObjectKeys(obj); + for (i = 0; i < keyList.length; i++) { + key = keyList[i]; + val = (obj[key] === undefined || obj[key] === null) ? '' : ('' + obj[key]); + key = camSafeUrlEncode(key).toLowerCase(); + val = camSafeUrlEncode(val) || ''; + list.push(key + '=' + val) + } + return list.join('&'); }; var request = function (params, callback) { var filePath = params.filePath; var headers = params.headers || {}; - var url = params.url; + var url = params.url || params.Url; var method = params.method; var onProgress = params.onProgress; var requestTask; @@ -52,6 +77,7 @@ var request = function (params, callback) { 'Expires', 'x-cos-storage-class', 'x-cos-security-token', + 'x-ci-security-token', ]; for (var i in params.headers) { if (params.headers.hasOwnProperty(i) && (i.indexOf('x-cos-meta-') > -1 || headerKeys.indexOf(i) > -1)) { diff --git a/package.json b/package.json index 593001e..006da04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cos-wx-sdk-v5", - "version": "1.0.13", + "version": "1.1.0", "description": "小程序 SDK for [腾讯云对象存储服务](https://cloud.tencent.com/product/cos)", "main": "demo/lib/cos-wx-sdk-v5.js", "scripts": { diff --git a/src/base.js b/src/base.js index ee2b6bf..b7cee82 100644 --- a/src/base.js +++ b/src/base.js @@ -2802,6 +2802,53 @@ function multipartAbort(params, callback) { }); } +/** + * 追加上传 + * @param {Object} params 参数对象,必须 + * @param {String} params.Bucket Bucket名称,必须 + * @param {String} params.Region 地域名称,必须 + * @param {String} params.Key object名称,必须 + * @param {String} params.Body 上传文件的内容,只支持字符串 + * @param {Number} params.Position 追加操作的起始点,单位为字节,必须 + * @param {String} params.CacheControl RFC 2616 中定义的缓存策略,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentDisposition RFC 2616 中定义的文件名称,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentEncoding RFC 2616 中定义的编码格式,将作为 Object 元数据保存,非必须 + * @param {String} params.ContentLength RFC 2616 中定义的 HTTP 请求内容长度(字节),必须 + * @param {String} params.ContentType RFC 2616 中定义的内容类型(MIME),将作为 Object 元数据保存,非必须 + * @param {String} params.Expect 当使用 Expect: 100-continue 时,在收到服务端确认后,才会发送请求内容,非必须 + * @param {String} params.Expires RFC 2616 中定义的过期时间,将作为 Object 元数据保存,非必须 + * @param {String} params.ACL 允许用户自定义文件权限,有效值:private | public-read,非必须 + * @param {String} params.GrantRead 赋予被授权者读取对象的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantReadAcp 赋予被授权者读取对象的访问控制列表(ACL)的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantWriteAcp 赋予被授权者写入对象的访问控制列表(ACL)的权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.GrantFullControl 赋予被授权者操作对象的所有权限,格式:id="[OwnerUin]",可使用半角逗号(,)分隔多组被授权者,非必须 + * @param {String} params.StorageClass 设置对象的存储级别,枚举值:STANDARD、STANDARD_IA、ARCHIVE,默认值:STANDARD,非必须 + * @param {String} params.x-cos-meta-* 允许用户自定义的头部信息,将作为对象的元数据保存。大小限制2KB,非必须 + * @param {String} params.ContentSha1 RFC 3174 中定义的 160-bit 内容 SHA-1 算法校验,非必须 + * @param {String} params.ServerSideEncryption 支持按照指定的加密算法进行服务端数据加密,格式 x-cos-server-side-encryption: "AES256",非必须 + * @param {Function} callback 回调函数,必须 + * @return {Object} err 请求失败的错误,如果请求成功,则为空。https://cloud.tencent.com/document/product/436/7730 + * @return {Object} data 返回的数据 + */ + function appendObject(params, callback) { + submitRequest.call(this, { + Action: 'name/cos:AppendObject', + method: 'POST', + Bucket: params.Bucket, + Region: params.Region, + action: 'append', + Key: params.Key, + body: params.Body, + qs: { + position: params.Position + }, + headers: params.Headers, + }, function (err, data) { + if (err) return callback(err); + callback(null, data); + }); +} + /** * cos 内置请求 @@ -2823,6 +2870,8 @@ function multipartAbort(params, callback) { headers: params.Headers, qs: params.Query, body: params.Body, + Url: params.Url, + rawBody: params.RawBody, }, function (err, data) { if (err) return callback(err); if (data && data.body) { @@ -3320,7 +3369,7 @@ function _submitRequest(params, callback) { var region = params.Region; var object = params.Key; var method = params.method || 'GET'; - var url = params.url; + var url = params.url || params.Url; var body = params.body; var json = params.json; var rawBody = params.rawBody; @@ -3358,12 +3407,18 @@ function _submitRequest(params, callback) { json: json, }; + // 兼容ci接口 + var token = 'x-cos-security-token'; + if (util.isCIHost(url)) { + token = 'x-ci-security-token'; + } + // 获取签名 opt.headers.Authorization = params.AuthData.Authorization; params.AuthData.Token && (opt.headers['token'] = params.AuthData.Token); params.AuthData.ClientIP && (opt.headers['clientIP'] = params.AuthData.ClientIP); params.AuthData.ClientUA && (opt.headers['clientUA'] = params.AuthData.ClientUA); - params.AuthData.XCosSecurityToken && (opt.headers['x-cos-security-token'] = params.AuthData.XCosSecurityToken); + params.AuthData.XCosSecurityToken && (opt.headers[token] = params.AuthData.XCosSecurityToken); // 清理 undefined 和 null 字段 opt.headers && (opt.headers = util.clearKey(opt.headers)); @@ -3515,6 +3570,7 @@ var API_MAP = { putObjectTagging: putObjectTagging, getObjectTagging: getObjectTagging, deleteObjectTagging: deleteObjectTagging, + appendObject: appendObject, // 分块上传相关方法 uploadPartCopy: uploadPartCopy, diff --git a/src/cos.js b/src/cos.js index 7c7de0b..adc6a3f 100644 --- a/src/cos.js +++ b/src/cos.js @@ -57,6 +57,6 @@ base.init(COS, task); advance.init(COS, task); COS.getAuthorization = util.getAuth; -COS.version = '1.0.13'; +COS.version = '1.1.0'; module.exports = COS; diff --git a/src/util.js b/src/util.js index e874f85..b8d20da 100644 --- a/src/util.js +++ b/src/util.js @@ -174,7 +174,7 @@ var getFileMd5 = function (body, callback) { function clone(obj) { return map(obj, function (v) { - return typeof v === 'object' ? clone(v) : v; + return typeof v === 'object' && v !== null ? clone(v) : v; }); } @@ -549,6 +549,13 @@ var canFileSlice = (function () { }; })(); +var isCIHost = function(url) { + if (url && url.split('?')[0].match(/(.ci.|ci.|.ci)/g)) { + return true; + } + return false; +} + var util = { noop: noop, formatParams: formatParams, @@ -579,6 +586,7 @@ var util = { getAuth: getAuth, compareVersion: compareVersion, canFileSlice: canFileSlice, + isCIHost: isCIHost, }; module.exports = util;