分段上传
开发过程中,您有任何问题可以在github上提交issue,或者在华为云对象存储服务论坛中发帖求助。接口参考文档详细介绍了每个接口的参数和使用方法。
对于较大文件上传,可以切分成段上传。用户可以在如下的应用场景内(但不仅限于此),使用分段上传的模式:
- 上传超过100MB大小的文件。
- 网络条件较差,和OBS服务端之间的链接经常断开。
- 上传前无法确定将要上传文件的大小。
分段上传分为如下3个步骤:
- 初始化分段上传任务(ObsClient.initiateMultipartUpload)。
- 逐个或并行上传段(ObsClient.uploadPart)。
- 合并段(ObsClient.completeMultipartUpload)或取消分段上传任务(ObsClient.abortMultipartUpload)。
初始化分段上传任务
使用分段上传方式传输数据前,必须先通知OBS初始化一个分段上传任务。该操作会返回一个OBS服务端创建的全局唯一标识(Upload ID),用于标识本次分段上传任务。您可以根据这个唯一标识来发起相关的操作,如取消分段上传任务、列举分段上传任务、列举已上传的段等。
您可以通过ObsClient.initiateMultipartUpload初始化一个分段上传任务:
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); obsClient.initiateMultipartUpload({ Bucket : 'bucketname', Key : 'objectname', ContentType : 'text/plain', Metadata : {'property' : 'property-value'} }, (err, result) => { if(err){ console.error('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); if(result.CommonMsg.Status < 300 && result.InterfaceResult){ console.log('UploadId-->' + result.InterfaceResult.UploadId); } } });
- 初始化分段上传任务时,除了指定上传对象的名称和所属桶外,您还可以使用ContentType参数和Metadata参数分别指定对象MIME类型和对象自定义元数据。
- 调用初始化分段上传任务接口成功后,会返回分段上传任务的全局唯一标识(Upload ID),在后面的操作中将用到它。
上传段
初始化一个分段上传任务之后,可以根据指定的对象名和Upload ID来分段上传数据。每一个上传的段都有一个标识它的号码——分段号(Part Number,范围是1~10000)。对于同一个Upload ID,该分段号不但唯一标识这一段数据,也标识了这段数据在整个对象内的相对位置。如果您用同一个分段号上传了新的数据,那么OBS上已有的这个段号的数据将被覆盖。除了最后一段以外,其他段的大小范围是100KB~5GB;最后段大小范围是0~5GB。每个段不需要按顺序上传,甚至可以在不同进程、不同机器上上传,OBS会按照分段号排序组成最终对象。
您可以通过ObsClient.uploadPart上传段:
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); obsClient.uploadPart({ Bucket:'bucketname', Key:'objectname', // 设置分段号,范围是1~10000 PartNumber:1, // 设置Upload ID UploadId:'upload id from initiateMultipartUpload', // 设置将要上传的大文件,其中localfile为待上传的本地文件路径,需要指定到具体的文件名 SourceFile: 'localfile', // 设置分段大小 PartSize: 5 * 1024 * 1024, // 设置分段的起始偏移大小 Offset: 0 }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); if(result.CommonMsg.Status < 300 && result.InterfaceResult){ console.log('ETag-->' + result.InterfaceResult.ETag); } } });
- 使用PartNumber参数指定分段号;使用UploadId参数指定分段上传任务的全局唯一标识;使用SourceFile参数指定待上传的文件;使用PartSize参数指定分段大小;使用Offset参数指定待上传文件的起始偏移大小。
- 上传段接口要求除最后一段以外,其他的段大小都要大于100KB。但是上传段接口并不会立即校验上传段的大小(因为不知道是否为最后一块);只有调用合并段接口时才会校验。
- OBS会将服务端收到段数据的ETag值(段数据的MD5值)返回给用户。
- 可以通过ContentMD5参数设置上传数据的MD5值,提供给OBS服务端用于校验数据完整性。
- 分段号的范围是1~10000。如果超出这个范围,OBS将返回400 Bad Request错误。
- OBS 3.0的桶支持最小段的大小为100KB,OBS 2.0的桶支持最小段的大小为5MB。请在OBS 3.0的桶上执行分段上传操作。
合并段
所有分段上传完成后,需要调用合并段接口来在OBS服务端生成最终对象。在执行该操作时,需要提供所有有效的分段列表(包括分段号和分段ETag值);OBS收到提交的分段列表后,会逐一验证每个段的有效性。当所有段验证通过后,OBS将把这些分段组合成最终的对象。
您可以通过ObsClient.completeMultipartUpload合并段:
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); obsClient.completeMultipartUpload({ Bucket:'bucketname', Key:'objectname', // 设置Upload ID UploadId:'upload id from initiateMultipartUpload', Parts: [{'PartNumber':1,'ETag':'etag value from uploadPart'}] }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); } });
- 如果最后一个段之外的其它段尺寸过小(小于100KB),OBS返回400 Bad Request。
- 使用UploadId参数指定分段上传任务的全局唯一标识;使用Parts参数指定分段号与分段ETag值的列表,该列表必须按分段号升序排列。
- 分段可以是不连续的。
并发分段上传
// 引入obs库 // 使用npm安装 const ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); const fs = require('fs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); const Bucket = 'bucketname' const Key = 'objectname' // 指定分段大小 const DEFAULT_PART_SIZE = 9 * 1024 * 1024; // 指定并发数为20 const CONCURRENCY = 20 // 准备分段上传的参数 const preparePartParams = (Bucket, Key, UploadId) => (sampleFile, partSize = DEFAULT_PART_SIZE) => { try { const fileSize = fs.lstatSync(sampleFile).size; const partCount = fileSize % partSize === 0 ? Math.floor(fileSize / partSize) : Math.floor(fileSize / partSize) + 1; const uploadPartParams = []; // 指定并发上传 for (let i = 0; i < partCount; i++) { // 分段在文件中的起始位置 let Offset = i * partSize; // 分段大小 let currPartSize = (i + 1 === partCount) ? fileSize - Offset : partSize; // 分段号 let PartNumber = i + 1; uploadPartParams.push({ Bucket, Key, PartNumber, UploadId, Offset, SourceFile: sampleFile, PartSize: currPartSize, }); } return ({ uploadPartParams, fileSize }); } catch (error) { console.log(error) } } /** * uploadSuccessSize:已经上传成功的大小 * uploadSuccessCount:已经上传成功的段数量 * concurrency:当前并发数 */ let uploadSuccessSize = 0; let uploadSuccessCount = 0; let concurrency = 0 const parts = []; const uploadPart = (uploadPartParam, otherUploadPartInfo) => { const partCount = otherUploadPartInfo.partCount; const fileSize = otherUploadPartInfo.fileSize; concurrency++; return obsClient .uploadPart(uploadPartParam) .then(result => { const { PartNumber, PartSize } = uploadPartParam; if (result.CommonMsg.Status < 300) { uploadSuccessCount++; uploadSuccessSize += PartSize; console.log(`the current concurrent count is ${concurrency} | uploaded segment: ${uploadSuccessCount}/${partCount}. the progress is ${((uploadSuccessSize / fileSize) * 100).toFixed(2)}% | the partNumber ${PartNumber} upload successed.`) parts.push({ PartNumber, ETag: result.InterfaceResult.ETag }); } else { console.log(result.CommonMsg.Code, parts) } concurrency--; }).catch(function (err) { console.log(err); throw err; }) } const uploadFile = (sourceFile) => { obsClient.initiateMultipartUpload({ Bucket, Key }).then(res => { const Status = res.CommonMsg.Status; const UploadId = res.InterfaceResult.UploadId; if (typeof Status === 'number' && Status > 300) { console.log(`initiateMultipartUpload failed! Status:${Status}`); return; } const partParams = preparePartParams(Bucket, Key, UploadId)(sourceFile) const uploadPartParams = partParams.uploadPartParams; const fileSize = partParams.fileSize; const partCount = uploadPartParams.length; const otherUploadPartInfo = { fileSize, partCount } // 调用并行上传函数 parallelFunc(uploadPartParams, (param) => uploadPart(param, otherUploadPartInfo), CONCURRENCY) .then(() => { obsClient.completeMultipartUpload({ Bucket, Key, UploadId, Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber) }, (err, result) => { if (err) { console.log('Error-->' + err); } else { console.log('Status-->' + result.CommonMsg.Status); } }); }) }).catch(function (err) { console.log(err) }) } /** * 实现函数并行执行 * @param {Array} params 回调函数的参数数组 * @param {Promise} promiseFn 回调函数 * @param {number} limit 并行数 */ const parallelFunc = (params, promiseFn, limit) => { return new Promise((resolve) => { let concurrency = 0; let finished = 0; const count = params.length; const run = (param) => { concurrency++; promiseFn(param) .then(() => { concurrency--; drainQueue(); finished++ if (finished === count) { resolve() } }); } const drainQueue = () => { while (params.length > 0 && concurrency < limit) { var param = params.shift(); run(param); } } drainQueue(); }) } uploadFile('localfile');
大文件分段上传时,使用Offset参数和PartSize参数配合指定每段数据在文件中的起始结束位置。
并发数过大,可能会因为网络不稳定等原因,产生Timeout错误,需要限制并发数。
取消分段上传任务
分段上传任务可以被取消,当一个分段上传任务被取消后,就不能再使用其Upload ID做任何操作,已经上传段也会被OBS删除。
采用分段上传方式上传对象过程中或上传对象失败后会在桶内产生段,这些段会占用您的存储空间,您可以通过取消该分段上传任务来清理掉不需要的段,节约存储空间。
您可以通过ObsClient.abortMultipartUpload取消分段上传任务:
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); obsClient.abortMultipartUpload({ Bucket:'bucketname', Key:'objectname', // 设置Upload ID UploadId:'upload id from initiateMultipartUpload', }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); } });
列举已上传的段
您可使用ObsClient.listParts列举出某一分段上传任务所有已经上传成功的段。
该接口可设置的参数如下:
参数 |
作用 |
---|---|
UploadId |
分段上传任务全局唯一标识,从ObsClient.initiateMultipartUpload返回的结果获取。 |
MaxParts |
表示列举已上传段的返回结果最大段数目,即分页时每一页中段数目。 |
PartNumberMarker |
表示列举已上传段的起始位置,只有Part Number大于该参数的段会被列出。 |
- 简单列举
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); // 列举已上传的段,其中uploadId来自于initiateMultipartUpload obsClient.listParts({ Bucket : 'bucketname', Key: 'objectname', UploadId : 'upload id from initiateMultipartUpload' }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); if(result.CommonMsg.Status < 300 && result.InterfaceResult){ for(let i=0;i<result.InterfaceResult.Parts.length;i++){ console.log('Part['+ i +']:'); // 分段号,上传时候指定 console.log('PartNumber-->' + result.InterfaceResult.Parts[i]['PartNumber']); // 段的最后上传时间 console.log('LastModified-->' + result.InterfaceResult.Parts[i]['LastModified']); // 分段的ETag值 console.log('ETag-->' + result.InterfaceResult.Parts[i]['ETag']); // 段数据大小 console.log('Size-->' + result.InterfaceResult.Parts[i]['Size']); } } } });
- 列举段至多返回1000个段信息,如果指定的Upload ID包含的段数量大于1000,则返回结果中InterfaceResult.IsTruncated为true表明本次没有返回全部段,并可通过InterfaceResult.NextPartNumberMarker获取下次列举的起始位置。
- 如果想获取指定Upload ID包含的所有分段,可以采用分页列举的方式。
- 列举所有段
由于ObsClient.listParts只能列举至多1000个段,如果段数量大于1000,列举所有分段请参考如下示例:
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); var listAll = (partNumberMarker) => { // 列举已上传的段,其中uploadId来自于initiateMultipartUpload obsClient.listParts({ Bucket : 'bucketname', Key: 'objectname', UploadId : 'upload id from initiateMultipartUpload', PartNumberMarker : partNumberMarker }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); if(result.CommonMsg.Status < 300 && result.InterfaceResult){ for(let i=0;i<result.InterfaceResult.Parts.length;i++){ console.log('Part['+ i +']:'); // 分段号,上传时候指定 console.log('PartNumber-->' + result.InterfaceResult.Parts[i]['PartNumber']); // 段的最后上传时间 console.log('LastModified-->' + result.InterfaceResult.Parts[i]['LastModified']); // 分段的ETag值 console.log('ETag-->' + result.InterfaceResult.Parts[i]['ETag']); // 段数据大小 console.log('Size-->' + result.InterfaceResult.Parts[i]['Size']); } if(result.InterfaceResult.IsTruncated === 'true'){ listAll(result.InterfaceResult.NextPartNumberMarker); } } } }); }; listAll();
列举分段上传任务
您可以通过ObsClient.listMultipartUploads列举分段上传任务。列举分段上传任务可设置的参数如下:
参数 |
作用 |
---|---|
Prefix |
限定返回的分段上传任务中的对象名必须带有Prefix前缀。 |
Delimiter |
用于对分段上传任务中的对象名进行分组的字符。对于对象名中包含Delimiter的任务,其对象名(如果请求中指定了Prefix,则此处的对象名需要去掉Prefix)中从首字符至第一个Delimiter之间的字符串将作为一个分组并作为CommonPrefix返回。 |
MaxUploads |
列举分段上传任务的最大数目,取值范围为1~1000,当超出范围时,按照默认的1000进行处理。 |
KeyMarker |
表示列举时返回指定的KeyMarker之后的分段上传任务。 |
UploadIdMarker |
只有与KeyMarker参数一起使用时才有意义,用于指定返回结果的起始位置,即列举时返回指定KeyMarker的UploadIdMarker之后的分段上传任务。 |
- 简单列举分段上传任务
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); obsClient.listMultipartUploads({ Bucket : 'bucketname' }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); if(result.CommonMsg.Status < 300 && result.InterfaceResult){ for(let i=0;i<result.InterfaceResult.Uploads.length;i++){ console.log('Uploads[' + i + ']'); console.log('UploadId-->' + result.InterfaceResult.Uploads[i]['UploadId']); console.log('Key-->' + result.InterfaceResult.Uploads[i]['Key']); console.log('Initiated-->' + result.InterfaceResult.Uploads[i]['Initiated']); } } } });
- 列举分段上传任务至多返回1000个任务信息,如果指定的桶包含的分段上传任务数量大于1000,则返回结果中InterfaceResult.IsTruncated为true表明本次没有返回全部结果,并可通过InterfaceResult.NextKeyMarker和InterfaceResult.NextUploadIdMarker获取下次列举的起点。
- 如果想获取指定桶包含的所有分段上传任务,可以采用分页列举的方式。
- 列举全部分段上传任务
// 引入obs库 // 使用npm安装 var ObsClient = require('esdk-obs-nodejs'); // 使用源码安装 // var ObsClient = require('./lib/obs'); // 创建ObsClient实例 var obsClient = new ObsClient({ //推荐通过环境变量获取AKSK,这里也可以使用其他外部引入方式传入,如果使用硬编码可能会存在泄露风险。 //您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html access_key_id: process.env.ACCESS_KEY_ID, secret_access_key: process.env.SECRET_ACCESS_KEY, server : 'https://your-endpoint' }); var listAll = (keyMarker, uploadIdMarker) => { obsClient.listMultipartUploads({ Bucket : 'bucketname', KeyMarker : keyMarker, UploadIdMarker : uploadIdMarker }, (err, result) => { if(err){ console.log('Error-->' + err); }else{ console.log('Status-->' + result.CommonMsg.Status); if(result.CommonMsg.Status < 300 && result.InterfaceResult){ for(let i=0;i<result.InterfaceResult.Uploads.length;i++){ console.log('Uploads[' + i + ']'); console.log('UploadId-->' + result.InterfaceResult.Uploads[i]['UploadId']); console.log('Key-->' + result.InterfaceResult.Uploads[i]['Key']); console.log('Initiated-->' + result.InterfaceResult.Uploads[i]['Initiated']); } if(result.InterfaceResult.IsTruncated === 'true'){ listAll(result.InterfaceResult.NextKeyMarker, result.InterfaceResult.NextUploadIdMarker); } } } }); } listAll();