diff --git a/sdk/advanced_api.js b/sdk/advanced_api.js new file mode 100644 index 0000000..ba2473b --- /dev/null +++ b/sdk/advanced_api.js @@ -0,0 +1,837 @@ +var fs = require('fs'); +var EventProxy = require('eventproxy'); +var Async = require('async'); +var cos = require('./cos'); +var util = require('./util'); + +// 分片大小 +var SLICE_SIZE = 1 * 1024 * 1024; + +var MultipartInit = cos.MultipartInit; +var MultipartUpload = cos.MultipartUpload; +var MultipartComplete = cos.MultipartComplete; +var MultipartList = cos.MultipartList; +var MultipartListPart = cos.MultipartListPart; +var MultipartAbort = cos.MultipartAbort; + + + +// 获取文件大小 +function getFileSize(params, callback) { + var FilePath = params.FilePath; + fs.stat(FilePath, function(err, stats) { + if (err) { + return callback(err); + } + + callback(null, { + FileSize : stats.size + }); + }); +} + + +// 文件分块上传全过程,暴露的分块上传接口 +function sliceUploadFile(params, callback) { + var proxy = new EventProxy(); + var Bucket = params.Bucket; + var Region = params.Region; + var Key = params.Key; + var FilePath = params.FilePath; + var SliceSize = params.SliceSize || SLICE_SIZE; + var AsyncLimit = params.AsyncLimit || 1; + var StorageClass = params.StorageClass || 'Standard'; + + var onProgress = params.onProgress; + var onHashProgress = params.onHashProgress; + + + // 上传过程中出现错误,返回错误 + proxy.all('error', function(errData) { + return callback(errData); + }); + + // 获取文件大小和 UploadId 成功之后,开始获取上传成功的分片信息 + proxy.all('get_file_size', 'get_upload_id', function(FileSizeData, UploadIdData) { + var FileSize = FileSizeData.FileSize; + var UploadId = UploadIdData.UploadId; + + params.FileSize = FileSize; + params.UploadId = UploadId; + + getUploadedParts({ + Bucket : Bucket, + Region : Region, + Key : Key, + UploadId : UploadId + }, function(err, data) { + if (err) { + return proxy.emit('error', err); + } + + proxy.emit('get_uploaded_parts', data); + }); + }); + + // 获取文件大小之后,开始计算分块 ETag 值(也就是 sha1值,需要前后加双引号 " ),HashProgressCallback 是计算每个分片 ETag 值之后的进度回调 + proxy.all('get_file_size', function(FileSizeData) { + var FileSize = FileSizeData.FileSize; + getSliceETag({ + FilePath : FilePath, + FileSize : FileSize, + SliceSize : SliceSize, + HashProgressCallback : onHashProgress + }, function(err, data) { + if (err) { + return proxy.emit('error', err); + } + + proxy.emit('get_slice_etag', data); + }); + + }); + + // 计算完分块的 ETag 值,以及获取到上传成功的文件分块的 ETag ,然后合并两者,更新需要上传的分块 + proxy.all('get_slice_etag', 'get_uploaded_parts', function(SliceETagData ,UploadedPartsData) { + var Parts = UploadedPartsData.Parts || []; + var SliceETag = SliceETagData.SliceETag || []; + + var SliceList = fixSliceList({ + SliceETag : SliceETag, + Parts : Parts + }); + + uploadSliceList({ + Bucket : Bucket, + Region : Region, + Key : Key, + FilePath : FilePath, + SliceSize : SliceSize, + AsyncLimit : AsyncLimit, + SliceList : SliceList, + UploadId : params.UploadId, + FileSize : params.FileSize, + ProgressCallback : onProgress + }, function(err, data) { + if (err) { + return proxy.emit('error', err); + } + + proxy.emit('upload_slice_list', data); + + }); + }); + + // 上传分块完成,开始 uploadSliceComplete 操作 + proxy.all('upload_slice_list', function(SliceListData) { + var SliceList = SliceListData.SliceList; + + uploadSliceComplete({ + Bucket : Bucket, + Region : Region, + Key : Key, + UploadId : params.UploadId, + SliceList : SliceList + }, function(err, data) { + if (err) { + return proxy.emit('error', err); + } + + proxy.emit('upload_slice_complete', data); + }); + }); + + // uploadSliceComplete 完成,成功回调 + proxy.all('upload_slice_complete', function(UploadCompleteData) { + callback(UploadCompleteData); + }); + + + + // 获取上传文件大小 + getFileSize({ + FilePath : FilePath + }, function(err, data) { + if (err) { + return proxy.emit('error', err); + } + proxy.emit('get_file_size', data); + }); + + // 获取文件 UploadId + getUploadId({ + Bucket : Bucket, + Region : Region, + Key : Key, + StorageClass : StorageClass + }, function(err, data) { + if (err) { + return proxy.emit('error', err); + } + + proxy.emit('get_upload_id', data); + }); +} + +// 获取上传的 UploadId +function getUploadIds(params, callback) { + var Bucket = params.Bucket; + var Region = params.Region; + var Key = params.Key; + var StorageClass = params.StorageClass; + + + getAllListParts({ + Bucket : Bucket, + Region : Region, + Prefix : Key + }, function(err, data) { + if (err) { + return callback(err); + } + + var Upload = data || []; + + var UploadIds = []; + + for (var i=0,len=Upload.length;i FileSize) { + end = FileSize; + } + + end --; + + var Body = fs.createReadStream(FilePath, { + start : start, + end : end + }); + + util.getFileSHA(Body, function(err, data) { + if (err) { + return callback(err); + } + + callback(null, data); + }); + +} + +function fixSliceList(params) { + var SliceETag = params.SliceETag; + var Parts = params.Parts; + + var SliceCount = SliceETag.length; + + for (var i=0,len = Parts.length; i SliceCount) { + continue; + } + + if (SliceETag[PartNumber - 1].ETag == ETag) { + SliceETag[PartNumber - 1].Uploaded = true; + } + } + + return SliceETag; +} + +// 上传文件分块,包括 +/* + UploadId (上传任务编号) + AsyncLimit (并发量), + SliceList (上传的分块数组), + FilePath (本地文件的位置), + SliceSize (文件分块大小) + FileSize (文件大小) + ProgressCallback (上传成功之后的回调函数) + +*/ +function uploadSliceList(params, cb) { + console.log('---------------- upload file -----------------'); + var Bucket = params.Bucket; + var Region = params.Region; + var Key = params.Key; + var UploadId = params.UploadId; + var FileSize = params.FileSize; + var SliceSize = params.SliceSize; + var AsyncLimit = params.AsyncLimit; + var SliceList = params.SliceList; + var FilePath = params.FilePath; + var ProgressCallback = params.ProgressCallback; + + console.log('file name : ' + Key); + + Async.mapLimit(SliceList, AsyncLimit, function(SliceItem, callback) { + var PartNumber = SliceItem['PartNumber']; + + var ETag = SliceItem['ETag']; + + var Uploaded = SliceItem['Uploaded']; + + if (Uploaded) { + process.nextTick(function() { + + if (ProgressCallback && (typeof ProgressCallback == 'function')) { + // 分块上传成功,触发进度回调 + ProgressCallback({ + PartNumber : PartNumber, + SliceSize : SliceSize, + FileSize : FileSize + }); + } + + callback(null, { + ETag : ETag, + PartNumber : PartNumber + }); + }); + + return; + } + + console.log('Async uploading...----- ' + PartNumber); + + + uploadSliceItem({ + Bucket : Bucket, + Region : Region, + Key : Key, + SliceSize : SliceSize, + FileSize : FileSize, + PartNumber : PartNumber, + UploadId : UploadId, + FilePath : FilePath, + SliceList : SliceList + }, function(err, data) { + if (err) { + return callback(err); + } + + callback(null, data); + + if (ProgressCallback && (typeof ProgressCallback == 'function')) { + // 分块上传成功,触发进度回调 + ProgressCallback({ + PartNumber : PartNumber, + SliceSize : SliceSize, + FileSize : FileSize + }); + } + + return; + + }); + + }, function(err, datas) { + if (err) { + return cb(err); + } + + var data = { + datas : datas, + UploadId : UploadId, + SliceList : SliceList + }; + + cb(null, data); + }); +} + +// 上传指定分片 +function uploadSliceItem(params, callback) { + var Bucket = params.Bucket; + var Region = params.Region; + var Key = params.Key; + var UploadId = params.UploadId; + var FileSize = params.FileSize; + var FilePath = params.FilePath; + var PartNumber = params.PartNumber * 1; + var SliceSize = params.SliceSize; + var SliceList = params.SliceList; + + var start = SliceSize * (PartNumber - 1); + + var ContentLength = SliceSize; + + var end = start + SliceSize; + + if (end > FileSize) { + end = FileSize; + ContentLength = end - start; + } + + end -- ; + + + var Body = fs.createReadStream(FilePath, { + start : start, + end : end + }); + + var ContentSha1 = SliceList[PartNumber * 1 - 1].ETag; + + MultipartUpload({ + Bucket : Bucket, + Region : Region, + Key : Key, + ContentLength : ContentLength, + ContentSha1 : ContentSha1, + PartNumber : PartNumber, + UploadId : UploadId, + Body : Body + }, function(err, data) { + if (err) { + return callback(err); + } + + return callback(null, data); + }); +} + +// 完成分块上传 +function uploadSliceComplete(params, callback) { + console.log('---------------- upload complete -----------------'); + var Bucket = params.Bucket; + var Region = params.Region; + var Key = params.Key; + var UploadId = params.UploadId; + var SliceList = params.SliceList; + + var Parts = []; + + for (var i=0,len=SliceList.length; i -1) { + headers[key] = params[key]; + } + } + + var body = params['Body']; + + var readStream = body; + + if (body && (typeof body == 'string')) { + readStream = fs.createReadStream(body); + } + + return submitRequest({ + method : 'PUT', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + headers : headers, + body : readStream, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + if (data && data.headers && data.headers['etag']) { + return callback(null, { + 'ETag' : data.headers['etag'] + }); + } + + return callback(null, data); + }); +} + +/** + * 删除 object + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {function} callback 回调函数,必须 + * @param {object} err 请求失败的错误,如果请求成功,则为空。 + * @param {object} data 删除操作成功之后返回的数据,如果删除操作成功,则返回 success 为 true, 并且附带原先 object 的 url + * @param {Boolean} data.Success 删除操作是否成功,成功则为 true,否则为 false + * @param {Boolean} data.BucketNotFound 请求的 object 所在的 bucket 是否不存在,如果为 true,则说明该 bucket 不存在 + */ +function deleteObject(params, callback) { + submitRequest({ + method : 'DELETE', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + var statusCode = err.statusCode; + if (statusCode && statusCode == 204) { + return callback(null, { + DeleteObjectSuccess : true + }); + } else if (statusCode && statusCode == 404) { + return callback(null, { + BucketNotFound : true + }); + } else { + return callback(err); + } + } + + return callback(null, { + DeleteObjectSuccess : true + }); + + }); +} + +/** + * 获取 object 的 权限列表 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + * @return {object} data.AccessControlPolicy 权限列表 + */ +function getObjectACL (params, callback) { + + return submitRequest({ + method : 'GET', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + action : '?acl', + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + var Grant = data.AccessControlPolicy.AccessControlList.Grant || []; + + if (!(Grant instanceof Array)) { + Grant = [Grant]; + } + + data.AccessControlPolicy.AccessControlList.Grant = Grant; + + return callback(null, data.AccessControlPolicy || {}); + }); +} + +/** + * 设置 object 的 权限列表 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + */ +function putObjectACL (params, callback) { + var headers = {}; + + headers['x-cos-acl'] = params['ACL']; + headers['x-cos-grant-read'] = params['GrantRead']; + headers['x-cos-grant-write'] = params['GrantWrite']; + headers['x-cos-grant-full-control'] = params['GrantFullControl']; + + return submitRequest({ + method : 'PUT', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + action : '?acl', + headers : headers, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + return callback(null, { + PutObjectACLSuccess : true + }); + }); +} + +/** + * Options Object请求实现跨域访问的预请求。即发出一个 OPTIONS 请求给服务器以确认是否可以进行跨域操作。 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + */ +function optionsObject (params, callback) { + var headers = {}; + + headers['Origin'] = params['Origin']; + headers['Access-Control-Request-Method'] = params['AccessControlRequestMethod']; + headers['Access-Control-Request-Headers'] = params['AccessControlRequestHeaders']; + + return submitRequest({ + method : 'OPTIONS', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + headers : headers, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + if (err.statusCode && err.statusCode == 403) { + return callback(null, { + OptionsForbidden : true + }); + } + return callback(err); + } + + data = data || {}; + + var resHeaders = data.headers || {}; + + var retData = {}; + + retData['AccessControlAllowOrigin'] = resHeaders['access-control-allow-origin']; + retData['AccessControlAllowMethods'] = resHeaders['access-control-allow-methods']; + retData['AccessControlAllowHeaders'] = resHeaders['access-control-allow-headers']; + retData['AccessControlExposeHeaders'] = resHeaders['access-control-expose-headers']; + retData['AccessControlMaxAge'] = resHeaders['access-control-max-age']; + + return callback(null, retData); + }); +} + +/** + * @params** (Object) : 参数列表 + * Bucket —— (String) : Bucket 名称 + * Region —— (String) : 地域名称 + * Key —— (String) : 文件名称 + * CopySource —— (String) : 源文件URL绝对路径,可以通过versionid子资源指定历史版本 + * ACL —— (String) : 允许用户自定义文件权限。有效值:private,public-read默认值:private。 + * GrantRead —— (String) : 赋予被授权者读的权限,格式 x-cos-grant-read: uin=" ",uin=" ",当需要给子账户授权时,uin="RootAcountID/SubAccountID",当需要给根账户授权时,uin="RootAcountID"。 + * GrantWrite —— (String) : 赋予被授权者写的权限,格式 x-cos-grant-write: uin=" ",uin=" ",当需要给子账户授权时,uin="RootAcountID/SubAccountID",当需要给根账户授权时,uin="RootAcountID"。 + * GrantFullControl —— (String) : 赋予被授权者读写权限,格式 x-cos-grant-full-control: uin=" ",uin=" ",当需要给子账户授权时,uin="RootAcountID/SubAccountID",当需要给根账户授权时,uin="RootAcountID"。 + * MetadataDirective —— (String) : 是否拷贝元数据,枚举值:Copy, Replaced,默认值Copy。假如标记为Copy,忽略Header中的用户元数据信息直接复制;假如标记为Replaced,按Header信息修改元数据。当目标路径和原路径一致,即用户试图修改元数据时,必须为Replaced + * CopySourceIfModifiedSince —— (String) : 当Object在指定时间后被修改,则执行操作,否则返回412。可与x-cos-copy-source-If-None-Match一起使用,与其他条件联合使用返回冲突。 + * CopySourceIfUnmodifiedSince —— (String) : 当Object在指定时间后未被修改,则执行操作,否则返回412。可与x-cos-copy-source-If-Match一起使用,与其他条件联合使用返回冲突。 + * CopySourceIfMatch —— (String) : 当Object的Etag和给定一致时,则执行操作,否则返回412。可与x-cos-copy-source-If-Unmodified-Since一起使用,与其他条件联合使用返回冲突。 + * CopySourceIfNoneMatch —— (String) : 当Object的Etag和给定不一致时,则执行操作,否则返回412。可与x-cos-copy-source-If-Modified-Since一起使用,与其他条件联合使用返回冲突。 + * StorageClass —— (String) : 存储级别,枚举值:存储级别,枚举值:Standard, Standard_IA,Nearline;默认值:Standard + * CacheControl —— (String) : 指定所有缓存机制在整个请求/响应链中必须服从的指令。 + * ContentDisposition —— (String) : MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件 + * ContentEncoding —— (String) : HTTP 中用来对「采用何种编码格式传输正文」进行协定的一对头部字段 + * ContentLength —— (String) : 设置响应消息的实体内容的大小,单位为字节 + * ContentType —— (String) : RFC 2616 中定义的 HTTP 请求内容类型(MIME),例如text/plain + * Expect —— (String) : 请求的特定的服务器行为 + * Expires —— (String) : 响应过期的日期和时间 + * ContentLanguage —— (String) : 指定内容语言 + * x-cos-meta-* —— (String) : 允许用户自定义的头部信息,将作为 Object 元数据返回。大小限制2K。 +*/ +function putObjectCopy (params, callback) { + var headers = {}; + + headers['x-cos-copy-source'] = params['CopySource']; + headers['x-cos-metadata-directive'] = params['MetadataDirective']; + headers['x-cos-copy-source-If-Modified-Since'] = params['CopySourceIfModifiedSince']; + headers['x-cos-copy-source-If-Unmodified-Since'] = params['CopySourceIfUnmodifiedSince']; + headers['x-cos-copy-source-If-Match'] = params['CopySourceIfMatch']; + headers['x-cos-copy-source-If-None-Match'] = params['CopySourceIfNoneMatch']; + headers['x-cos-storage-class'] = params['StorageClass']; + headers['x-cos-acl'] = params['ACL']; + headers['x-cos-grant-read'] = params['GrantRead']; + headers['x-cos-grant-write'] = params['GrantWrite']; + headers['x-cos-grant-full-control'] = params['GrantFullControl']; + headers['Cache-Control'] = params['CacheControl']; + headers['Content-Disposition'] = params['ContentDisposition']; + headers['Content-Encoding'] = params['ContentEncoding']; + headers['Content-Length'] = params['ContentLength']; + headers['Content-Type'] = params['ContentType']; + headers['Expect'] = params['Expect']; + headers['Expires'] = params['Expires']; + headers['x-cos-content-sha1'] = params['ContentSha1']; + + for (var key in params) { + if (key.indexOf('x-cos-meta-') > -1) { + headers[key] = params[key]; + } + } + + return submitRequest({ + method : 'PUT', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + headers : headers, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + return callback(null, data.CopyObjectResult || {}); + }); +} + + + +function deleteMultipleObject(params, callback) { + var headers = {}; + + headers['Content-Type'] = 'application/xml'; + + var Objects = params.Objects || {}; + var Quiet = params.Quiet; + + var DeleteConfiguration = { + Delete : { + Object : Objects, + Quiet : Quiet || false + } + }; + + var xml = util.json2xml(DeleteConfiguration); + + headers['Content-MD5'] = util.md5(xml); + headers['Content-Length'] = Buffer.byteLength(xml, 'utf8'); + + return submitRequest({ + method : 'POST', + Bucket : params.Bucket, + Region : params.Region, + body : xml, + action : '/?delete', + headers : headers, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + var Deleted = data.DeleteResult.Deleted || []; + var Errors = data.DeleteResult.Error || []; + + if (!(Deleted instanceof Array)) { + Deleted = [Deleted]; + } + + if (!(Errors instanceof Array)) { + Errors = [Errors]; + } + + data.DeleteResult.Error = Errors; + data.DeleteResult.Deleted = Deleted; + + return callback(null, data.DeleteResult || {}); + }); +} + + +// ----------------------------------------- 分块上传相关部分 ---------------------------------- + + +/** + * 初始化分块上传 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {string} params.CacheControl RFC 2616 中定义的缓存策略,将作为 Object 元数据保存,非必须 + * @param {string} params.ContentDisposition RFC 2616 中定义的文件名称,将作为 Object 元数据保存 ,非必须 + * @param {string} params.ContentEncoding RFC 2616 中定义的编码格式,将作为 Object 元数据保存,非必须 + * @param {string} params.ContentType RFC 2616 中定义的内容类型(MIME),将作为 Object 元数据保存,非必须 + * @param {string} params.Expires RFC 2616 中定义的过期时间,将作为 Object 元数据保存,非必须 + * @param {string} params.ACL 允许用户自定义文件权限,非必须 + * @param {string} params.GrantRead 赋予被授权者读的权限 ,非必须 + * @param {string} params.GrantWrite 赋予被授权者写的权限 ,非必须 + * @param {string} params.GrantFullControl 赋予被授权者读写权限 ,非必须 + * @param {string} params.StorageClass 设置Object的存储级别,枚举值:Standard,Standard_IA,Nearline,非必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + * @return {object} data.InitiateMultipartUploadResult 初始化上传信息,包括 Bucket(Bucket名称), Key(文件名称) 和 UploadId (上传任务ID) + */ +function MultipartInit(params, callback) { + var headers = {}; + + headers['Cache-Control'] = params['CacheControl']; + headers['Content-Disposition'] = params['ContentDisposition']; + headers['Content-Encoding'] = params['ContentEncoding']; + headers['Content-Type'] = params['ContentType']; + headers['Expires'] = params['Expires']; + + headers['x-cos-acl'] = params['ACL']; + headers['x-cos-grant-read'] = params['GrantRead']; + headers['x-cos-grant-write'] = params['GrantWrite']; + headers['x-cos-grant-full-control'] = params['GrantFullControl']; + headers['x-cos-storage-class'] = params['StorageClass']; + + for (var key in params) { + if (key.indexOf('x-cos-meta-') > -1) { + headers[key] = params[key]; + } + } + + return submitRequest({ + method : 'POST', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + action : '?uploads', + headers : headers, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + if (data && data.InitiateMultipartUploadResult) { + return callback(null, data.InitiateMultipartUploadResult); + } + + return callback(null, data); + + + }); +} + +/** + * 分块上传 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {string} params.ContentLength RFC 2616 中定义的 HTTP 请求内容长度(字节),非必须 + * @param {string} params.Expect 当使用 Expect: 100-continue 时,在收到服务端确认后,才会发送请求内容,非必须 + * @param {string} params.ContentSha1 RFC 3174 中定义的 160-bit 内容 SHA-1 算法校验值,非必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + * @return {object} data.ETag 返回的文件分块 sha1 值 + */ +function MultipartUpload(params, callback) { + var headers = {}; + + headers['Content-Length'] = params['ContentLength']; + headers['Expect'] = params['Expect']; + headers['x-cos-content-sha1'] = params['ContentSha1']; + + var PartNumber = params['PartNumber']; + var UploadId = params['UploadId']; + + var action = '?partNumber=' + PartNumber + '&uploadId=' + UploadId; + + var body = params.Body; + + var req = submitRequest({ + method : 'PUT', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + action : action, + headers : headers, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + data['headers'] = data['headers'] || {}; + + return callback(null, { + ETag : data['headers']['etag'] || '' + }); + + }); + + return body.pipe(req); +} + +/** + * 完成分块上传 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {array} params.Parts 分块信息列表,必须 + * @param {string} params.Parts[i].PartNumber 块编号,必须 + * @param {string} params.Parts[i].ETag 分块的 sha1 校验值 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + * @return {object} data.CompleteMultipartUpload 完成分块上传后的文件信息,包括Location, Bucket, Key 和 ETag + */ +function MultipartComplete(params, callback) { + var headers = {}; + + headers['Content-Type'] = 'application/xml'; + + var UploadId = params.UploadId; + + var action = '?uploadId=' + UploadId; + + var Parts = params['Parts']; + + for (var i=0,len=Parts.length;i当upload-id-marker未被指定时,ObjectName字母顺序大于key-marker的条目将被列出
当upload-id-marker被指定时,ObjectName字母顺序大于key-marker的条目被列出,ObjectName字母顺序等于key-marker同时UploadID大于upload-id-marker的条目将被列出,非必须 + * @param {string} params.UploadIdMarker 与key-marker一起使用
当key-marker未被指定时,upload-id-marker将被忽略
当key-marker被指定时,ObjectName字母顺序大于key-marker的条目被列出,ObjectName字母顺序等于key-marker同时UploadID大于upload-id-marker的条目将被列出,非必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + * @return {object} data.ListMultipartUploadsResult 分块上传任务信息 + */ +function MultipartList(params, callback) { + var reqParams = {}; + + reqParams['delimiter'] = params['Delimiter']; + reqParams['encoding-type'] = params['EncodingType']; + reqParams['prefix'] = params['Prefix']; + + reqParams['max-uploads'] = params['MaxUploads']; + + reqParams['key-marker'] = params['KeyMarker']; + reqParams['upload-id-marker'] = params['UploadIdMarker']; + + reqParams = util.clearKey(reqParams); + + + return submitRequest({ + method : 'GET', + Bucket : params.Bucket, + Region : params.Region, + action : '/?uploads&' + querystring.stringify(reqParams), + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + if (data && data.ListMultipartUploadsResult) { + var Upload = data.ListMultipartUploadsResult.Upload || []; + + var CommonPrefixes = data.ListMultipartUploadsResult.CommonPrefixes || []; + + + if (!(CommonPrefixes instanceof Array)) { + CommonPrefixes = [CommonPrefixes]; + } + + if (!(Upload instanceof Array)) { + Upload = [Upload]; + } + + data.ListMultipartUploadsResult.Upload = Upload; + data.ListMultipartUploadsResult.CommonPrefixes = CommonPrefixes; + } + + return callback(null, data.ListMultipartUploadsResult || {}); + }); +} + +/** + * 上传的分块列表查询 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {string} params.UploadId 标示本次分块上传的ID,必须 + * @param {string} params.EncodingType 规定返回值的编码方式,非必须 + * @param {string} params.MaxParts 单次返回最大的条目数量,默认1000,非必须 + * @param {string} params.PartNumberMarker 默认以UTF-8二进制顺序列出条目,所有列出条目从marker开始,非必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + * @return {object} data.ListMultipartUploadsResult 分块信息 + */ +function MultipartListPart(params, callback) { + var reqParams = {}; + + reqParams['uploadId'] = params['UploadId']; + reqParams['encoding-type'] = params['EncodingType']; + reqParams['max-parts'] = params['MaxParts']; + reqParams['part-number-marker'] = params['PartNumberMarker']; + + + return submitRequest({ + method : 'GET', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + qs : reqParams, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + data = data || {}; + + var Part = data.ListPartsResult.Part || []; + + if (!(Part instanceof Array)) { + Part = [Part]; + } + + data.ListPartsResult.Part = Part; + + return callback(null, data.ListPartsResult || {}); + }); +} + +/** + * 抛弃分块上传 + * @param {object} params 参数对象,必须 + * @param {string} params.Bucket Bucket名称,必须 + * @param {string} params.Region 地域名称,必须 + * @param {string} params.Key object名称,必须 + * @param {string} params.UploadId 标示本次分块上传的ID,必须 + * @param {function} callback 回调函数,必须 + * @return {object} err 请求失败的错误,如果请求成功,则为空。 + * @return {object} data 返回的数据 + */ +function MultipartAbort(params, callback) { + var reqParams = {}; + + reqParams['uploadId'] = params['UploadId']; + + return submitRequest({ + method : 'DELETE', + Bucket : params.Bucket, + Region : params.Region, + Key : params.Key, + qs : reqParams, + needHeaders : true, + Appid : params.Appid, + SecretId : params.SecretId, + SecretKey : params.SecretKey + }, function(err, data) { + if (err) { + return callback(err); + } + + return callback(null, { + MultipartAbortSuccess : true + }); + }); +} + + +/** +* String 方法添加 +*/ + +String.prototype.strip = function() { + return this.replace(/(^\/*)|(\/*$)/g, ''); +}; + +String.prototype.lstrip = function() { + return this.replace(/(^\/*)/g, ''); +}; + +String.prototype.rstrip = function() { + return this.replace(/(\/*$)/g, ''); +}; + +/** +* 私有方法 +*/ + +// 生成操作 url +function getUrl(params) { + var bucket = params.bucket; + var region = params.region; + var object = params.object; + var action = params.action; + var appid = params.appid; + + var url = 'http://' + bucket + '-' + appid + '.' + region + '.myqcloud.com'; + + if (object) { + url += '/' + encodeURIComponent(object); + } + + if (action) { + url += action; + } + + return url; +} + +// 检测参数是否填写完全 +function checkParamsRequire(callerName, params) { + var bucket = params.Bucket; + var region = params.Region; + var object = params.Key; + + if(callerName.indexOf('Bucket') > -1 || callerName == 'deleteMultipleObject' || callerName == 'MultipartList') { + if(!bucket || !region) { + return false; + } + + return true; + } + + if(callerName.indexOf('Object') > -1) { + if(!bucket || !region || !object) { + return false; + } + + return true; + } + + if(callerName.indexOf('Multipart') > -1) { + if(!bucket || !region || !object) { + return false; + } + + return true; + } + +} + +// 发起请求 +function submitRequest(params, callback) { + + // 获取默认秘钥信息 + var defaultAuth = { + Appid : config.APPID, + SecretId : config.SECRET_ID, + SecretKey : config.SECRET_KEY + }; + + var bucket = params.Bucket; + var region = params.Region; + var object = params.Key; + var action = params.action; + var method = params.method || 'GET'; + var headers = params.headers || {}; + var url = params.url; + var body = params.body; + var json = params.json; + + // 通过调用的函数名确定需要的参数 + var callerName = arguments.callee.caller.name; + + + if(!checkParamsRequire(callerName, params) && callerName!=='getService') { + return callback({ + error : 'lack of required params' + }); + } + + var needHeaders = params.needHeaders; + var rawBody = params.rawBody; + + var qs = params.qs; + + var appid = params.Appid || defaultAuth.Appid; + var secretId = params.SecretId || defaultAuth.SecretId; + var secretKey = params.SecretKey || defaultAuth.SecretKey; + + var opt = { + url: url || getUrl({ + bucket : bucket, + region : region, + object : object, + action : action, + appid : appid, + secretId : secretId, + secretKey : secretKey + }), + method: method, + headers: headers, + qs: qs, + body : body, + json : json, + // 这里的 proxy 用于处理内网网关限制,代理转发华南园区的请求,华北园区无需代理 + //'proxy':'http://dev-proxy.oa.com:8080' + }; + + + if (object) { + object = '/' + object; + } + + // 获取签名 + opt.headers.Authorization = util.getAuth({ + method: opt.method, + pathname : object || '/', + appid : appid, + secretId : secretId, + secretKey : secretKey + }); + + // 预先处理 undefine 的属性 + if (opt.headers) { + opt.headers = util.clearKey(opt.headers); + } + + if (opt.qs) { + opt.qs = util.clearKey(opt.qs); + } + + return REQUEST(opt, function (err, response, body) { + + // 请求错误,发生网络错误 + if (err) { + return callback({ + error : err + }); + } + + var statusCode = response.statusCode; + var jsonRes; + + try { + jsonRes = util.xml2json(body) || {}; + } catch (e) { + jsonRes = body || {}; + } + + // 请求返回码不为 200 + if (statusCode != 200) { + return callback({ + statusCode : statusCode, + error : jsonRes.Error || jsonRes + }); + } + + // 不对 body 进行转换,body 直接挂载返回 + if (rawBody) { + jsonRes = {}; + jsonRes.body = body; + } + + // 如果需要头部信息,则 headers 挂载返回 + if (needHeaders) { + jsonRes.headers = response.headers || {}; + } + + if (jsonRes.Error) { + return callback({ + statusCode : statusCode, + error : jsonRes.Error + }); + } + + return callback(null, jsonRes); + }); +} + + +// bucket 相关 +exports.getService = getService; +exports.getBucket = getBucket; +exports.headBucket = headBucket; +exports.putBucket = putBucket; +exports.deleteBucket = deleteBucket; +exports.getBucketACL = getBucketACL; +exports.putBucketACL = putBucketACL; +exports.getBucketCORS = getBucketCORS; +exports.putBucketCORS = putBucketCORS; +exports.deleteBucketCORS = deleteBucketCORS; +exports.getBucketLocation = getBucketLocation; +//exports.getBucketPolicy = getBucketPolicy; +//exports.putBucketPolicy = putBucketPolicy; +exports.getBucketTagging = getBucketTagging; +exports.putBucketTagging = putBucketTagging; +exports.deleteBucketTagging = deleteBucketTagging; +/* +exports.getBucketLifecycle = getBucketLifecycle; +exports.putBucketLifecycle = putBucketLifecycle; +exports.deleteBucketLifecycle = deleteBucketLifecycle; +*/ + +// object 相关 +exports.getObject = getObject; +exports.headObject = headObject; +exports.putObject = putObject; +exports.deleteObject = deleteObject; +exports.getObjectACL = getObjectACL; +exports.putObjectACL = putObjectACL; +exports.optionsObject = optionsObject; +//exports.putObjectCopy = putObjectCopy; +exports.deleteMultipleObject = deleteMultipleObject; + +// 分块上传相关 +exports.MultipartInit = MultipartInit; +exports.MultipartUpload = MultipartUpload; +exports.MultipartComplete = MultipartComplete; +exports.MultipartList = MultipartList; +exports.MultipartListPart = MultipartListPart; +exports.MultipartAbort = MultipartAbort; diff --git a/sdk/util.js b/sdk/util.js new file mode 100644 index 0000000..29eb700 --- /dev/null +++ b/sdk/util.js @@ -0,0 +1,167 @@ +var crypto = require('crypto'); +var xml2js = require('xml2js'); +var xmlParser = new xml2js.Parser({explicitArray : false, ignoreAttrs : true}); +var xmlBuilder = new xml2js.Builder(); + + +//测试用的key后面可以去掉 +var getAuth = function (opt) { + + opt = opt || {}; + + var secretId = opt.secretId; + var secretKey = opt.secretKey; + + var method = opt.method || 'get'; + method = method.toLowerCase(); + var pathname = opt.pathname || '/'; + var queryParams = opt.params || ''; + var headers = opt.headers || ''; + + var getObjectKeys = function (obj) { + var list = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + list.push(key); + } + } + return list.sort(); + }; + + 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] || ''; + key = key.toLowerCase(); + key = encodeURIComponent(key); + list.push(key + '=' + encodeURIComponent(val)); + } + return list.join('&'); + }; + + // 签名有效起止时间 + var now = parseInt(new Date().getTime() / 1000) - 1; + var expired = now; // now + ';' + (now + 60) + ''; // 签名过期时间为当前 + 3600s + + if (opt.expires) { + expired += (opt.expires * 1); + } else { + expired += 3600; + } + + // 要用到的 Authorization 参数列表 + var qSignAlgorithm = 'sha1'; + var qAk = secretId; + var qSignTime = now + ';' + expired; + var qKeyTime = now + ';' + expired; + var qHeaderList = getObjectKeys(headers).join(';').toLowerCase(); + var qUrlParamList = getObjectKeys(queryParams).join(';').toLowerCase(); + + // 签名算法说明文档:https://www.qcloud.com/document/product/436/7778 + // 步骤一:计算 SignKey + var signKey = crypto.createHmac('sha1', secretKey).update(qKeyTime).digest('hex');//CryptoJS.HmacSHA1(qKeyTime, secretKey).toString(); + + // 新增修改,formatString 添加 encodeURIComponent + + //pathname = encodeURIComponent(pathname); + + + // 步骤二:构成 FormatString + var formatString = [method, pathname, obj2str(queryParams), obj2str(headers), ''].join('\n'); + + formatString = new Buffer(formatString,'utf8'); + + // 步骤三:计算 StringToSign + var sha1Algo = crypto.createHash('sha1'); + sha1Algo.update(formatString); + var res = sha1Algo.digest('hex'); + var stringToSign = ['sha1', qSignTime, res, ''].join('\n'); + + // 步骤四:计算 Signature + var qSignature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex');//CryptoJS.HmacSHA1(stringToSign, signKey).toString(); + + // 步骤五:构造 Authorization + var authorization = [ + 'q-sign-algorithm=' + qSignAlgorithm, + 'q-ak=' + qAk, + 'q-sign-time=' + qSignTime, + 'q-key-time=' + qKeyTime, + 'q-header-list=' + qHeaderList, + 'q-url-param-list=' + qUrlParamList, + 'q-signature=' + qSignature + ].join('&'); + + return authorization; + +}; + + +// XML格式转json +var xml2json = function(bodyStr) { + var d = {}; + xmlParser.parseString(bodyStr, function(err, result) { + d = result; + }); + + return d; +}; + +var json2xml = function(json) { + var xml = xmlBuilder.buildObject(json); + return xml; +}; + +var md5 = function(str, encoding) { + var md5 = crypto.createHash('md5'); + md5.update(str); + encoding = encoding || 'hex'; + return md5.digest(encoding); +}; + + + +// 用于清除值为 undefine 或者 null 的属性 +var clearKey = function(obj) { + var retObj = {}; + for (var key in obj) { + if (obj[key]) { + retObj[key] = obj[key]; + } + } + + return retObj; +}; + + +var getFileSHA = function(readStream, callback) { + var SHA = crypto.createHash('sha1'); + + readStream.on('data', function(chunk) { + SHA.update(chunk); + }); + + readStream.on('error', function(err) { + callback(err); + }); + + readStream.on('end', function() { + var hash = SHA.digest('hex'); + + callback(null, hash); + }); +}; + +var util = { + getAuth: getAuth, + xml2json: xml2json, + json2xml: json2xml, + md5: md5, + clearKey: clearKey, + getFileSHA : getFileSHA +}; + + +module.exports = util; \ No newline at end of file