分段上传
对于较大文件上传,可以切分成段上传。用户可以在如下的应用场景内(但不仅限于此),使用分段上传的模式:
- 上传超过100MB大小的文件。
- 网络条件较差,和OBS服务端之间的链接经常断开。
- 上传前无法确定将要上传文件的大小。
分段上传分为如下3个步骤:
- 初始化分段上传任务(ObsClient.initiateMultipartUpload)。
- 逐个或并行上传段(ObsClient.uploadPart)。
- 合并段(ObsClient.completeMultipartUpload)或取消分段上传任务(ObsClient.abortMultipartUpload)。
初始化分段上传任务
使用分段上传方式传输数据前,必须先通知OBS初始化一个分段上传任务。该操作会返回一个OBS服务端创建的全局唯一标识(Upload ID),用于标识本次分段上传任务。您可以根据这个唯一标识来发起相关的操作,如取消分段上传任务、列举分段上传任务、列举已上传的段等。
您可以通过ObsClient.initiateMultipartUpload初始化一个分段上传任务:
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest("bucketname", "objectname"); ObjectMetadata metadata = new ObjectMetadata(); metadata.addUserMetadata("property", "property-value"); metadata.setContentType("text/plain"); request.setMetadata(metadata); InitiateMultipartUploadResult result = obsClient.initiateMultipartUpload(request); String uploadId = result.getUploadId(); Log.i("InitiateMultipartUpload", "\t" + uploadId);
- 用InitiateMultipartUploadRequest指定上传对象的名称和所属桶。
- 在InitiateMultipartUploadRequest中,您可以设置对象MIME类型、对象存储类型、对象自定义元数据等对象属性。
- InitiateMultipartUploadResult.getUploadId返回分段上传任务的全局唯一标识(Upload ID),在后面的操作中将用到它。
- 由于 HTTP 编码规范限制,无法发送非 ASCII 码字符,SDK 会在发送请求时对您头域中的中文汉字进行 url 编码,发送编码后数据。如您设置的值 content-disposition 为 ”attachment; filename="中文.txt"”,则对象元数据中存储的信息为”attachment; filename="%E4%B8%AD%E6%96%87.txt"”。使用浏览器访问时浏览器将会自动解码。
- 如果不需要 SDK 帮您编码,可以调用 InitiateMultipartUploadRequest.setIsEncodeHeaders(false) 关闭自动编码。
上传段
初始化一个分段上传任务之后,可以根据指定的对象名和Upload ID来分段上传数据。每一个上传的段都有一个标识它的号码——分段号(Part Number,范围是1~10000)。对于同一个Upload ID,该分段号不但唯一标识这一段数据,也标识了这段数据在整个对象内的相对位置。如果您用同一个分段号上传了新的数据,那么OBS上已有的这个段号的数据将被覆盖。除了最后一段以外,其他段的大小范围是100KB~5GB;最后段大小范围是0~5GB。每个段不需要按顺序上传,甚至可以在不同进程、不同机器上上传,OBS会按照分段号排序组成最终对象。
您可以通过ObsClient.uploadPart上传段:
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); List<PartEtag> partEtags = new ArrayList<PartEtag>(); // 上传第一段 UploadPartRequest request = new UploadPartRequest("bucketname", "objectname"); // 设置Upload ID request.setUploadId(uploadId); // 设置分段号,范围是1~10000, request.setPartNumber(1); // 设置将要上传的大文件,其中localfile为待上传的本地文件路径,需要指定到具体的文件名 request.setFile(new File("localfile")); // 设置分段大小 request.setPartSize(5 * 1024 * 1024L); UploadPartResult result = obsClient.uploadPart(request); partEtags.add(new PartEtag(result.getEtag(), result.getPartNumber())); // 上传第二段 request = new UploadPartRequest("bucketname", "objectname"); // 设置Upload ID request.setUploadId(uploadId); // 设置分段号 request.setPartNumber(2); // 设置将要上传的大文件 request.setFile(new File("localfile")); // 设置第二段的段偏移量 request.setOffset(5 * 1024 * 1024L); // 设置分段大小 request.setPartSize(5 * 1024 * 1024L); result = obsClient.uploadPart(request); partEtags.add(new PartEtag(result.getEtag(), result.getPartNumber()));
- 上传段接口要求除最后一段以外,其他的段大小都要大于100KB。但是上传段接口并不会立即校验上传段的大小(因为不知道是否为最后一块);只有调用合并段接口时才会校验。
- OBS会将服务端收到段数据的ETag值(段数据的MD5值)返回给用户。
- 为了保证数据在网络传输过程中不出现错误,可以通过设置UploadPartRequest.setAttachMd5为true来让SDK自动计算每段数据的MD5值(仅在数据源为本地文件时有效),并放到Content-MD5请求头中;OBS服务端会计算上传数据的MD5值与SDK计算的MD5值比较,保证数据完整性。
- 可以通过UploadPartRequest.setContentMd5直接设置上传数据的MD5值,提供给OBS服务端用于校验数据完整性。如果设置了该值,UploadPartRequest.setAttachMd5参数会被忽略。
- 分段号的范围是1~10000。如果超出这个范围,OBS将返回400 Bad Request错误。
合并段
所有分段上传完成后,需要调用合并段接口来在OBS服务端生成最终对象。在执行该操作时,需要提供所有有效的分段列表(包括分段号和分段ETag值);OBS收到提交的分段列表后,会逐一验证每个段的有效性。当所有段验证通过后,OBS将把这些分段组合成最终的对象。
您可以通过ObsClient.completeMultipartUpload合并段:
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); List<PartEtag> partEtags = new ArrayList<PartEtag>(); // 第一段 PartEtag part1 = new PartEtag(); part1.setPartNumber(1); part1.seteTag("etag1"); partEtags.add(part1); // 第二段 PartEtag part2 = new PartEtag(); part2.setPartNumber(2); part2.seteTag("etag2"); partEtags.add(part2); CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest("bucketname", "objectname", uploadId, partEtags); obsClient.completeMultipartUpload(request);
- 上面代码中的 partEtags是进行上传段后保存的分段号和分段ETag值的列表,它必须是按分段号升序排列。
- 分段可以是不连续的。
并发分段上传
分段上传的主要目的是解决大文件上传或网络条件较差的情况。下面的示例代码展示了如何使用分段上传并发上传大文件:
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; final String bucketName = "bucketname"; final String objectKey = "objectname"; // 创建ObsClient实例 final ObsClient obsClient = new ObsClient(ak, sk, endPoint); // 初始化线程池 ExecutorService executorService = Executors.newFixedThreadPool(20); final File largeFile = new File("localfile"); // 初始化分段上传任务 InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectKey); InitiateMultipartUploadResult result = obsClient.initiateMultipartUpload(request); final String uploadId = result.getUploadId(); Log.i("UploadPart", "\t"+ uploadId + "\n"); // 每段上传100MB long partSize = 100 * 1024 * 1024L; long fileSize = largeFile.length(); // 计算需要上传的段数 long partCount = fileSize % partSize == 0 ? fileSize / partSize : fileSize / partSize + 1; final List<PartEtag> partEtags = Collections.synchronizedList(new ArrayList<PartEtag>()); // 执行并发上传段 for (int i = 0; i < partCount; i++) { // 分段在文件中的起始位置 final long offset = i * partSize; // 分段大小 final long currPartSize = (i + 1 == partCount) ? fileSize - offset : partSize; // 分段号 final int partNumber = i + 1; executorService.execute(new Runnable() { @Override public void run() { UploadPartRequest uploadPartRequest = new UploadPartRequest(); uploadPartRequest.setBucketName(bucketName); uploadPartRequest.setObjectKey(objectKey); uploadPartRequest.setUploadId(uploadId); uploadPartRequest.setFile(largeFile); uploadPartRequest.setPartSize(currPartSize); uploadPartRequest.setOffset(offset); uploadPartRequest.setPartNumber(partNumber); UploadPartResult uploadPartResult; try { uploadPartResult = obsClient.uploadPart(uploadPartRequest); Log.i("UploadPart", "Part#" + partNumber + " done\n"); partEtags.add(new PartEtag(uploadPartResult.getEtag(), uploadPartResult.getPartNumber())); } catch (ObsException e) { Log.e("UploadPart", e.getMessage(), e); } } }); } // 等待上传完成 executorService.shutdown(); while (!executorService.isTerminated()) { try { executorService.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e("UploadPart", e.getMessage(), e); } } // 合并段 CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(bucketName, objectKey, uploadId, partEtags); obsClient.completeMultipartUpload(completeMultipartUploadRequest);
大文件分段上传时,使用UploadPartRequest.setOffset和UploadPartRequest.setPartSize来设置每段的起始结束位置。
取消分段上传任务
分段上传任务可以被取消,当一个分段上传任务被取消后,就不能再使用其Upload ID做任何操作,已经上传段也会被OBS删除。
采用分段上传方式上传对象过程中或上传对象失败后会在桶内产生段,这些段会占用您的存储空间,您可以通过取消该分段上传任务来清理掉不需要的段,节约存储空间。
您可以通过ObsClient.abortMultipartUpload取消分段上传任务:
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); AbortMultipartUploadRequest request = new AbortMultipartUploadRequest("bucketname", "objectname", uploadId); obsClient.abortMultipartUpload(request);
列举已上传的段
您可使用ObsClient.listParts列举出某一分段上传任务所有已经上传成功的段。
该接口可设置的参数如下:
参数 |
作用 |
OBS Android SDK对应方法 |
---|---|---|
bucketName |
分段上传任务所属的桶名。 |
ListPartsRequest.setBucketName |
key |
分段上传任务所属的对象名。 |
ListPartsRequest.setKey |
uploadId |
分段上传任务全局唯一标识,从ObsClient.initiateMultipartUpload返回的结果获取。 |
ListPartsRequest.setUploadId |
maxParts |
表示列举已上传的段返回结果最大段数目,即分页时每一页中段数目。 |
ListPartsRequest.setMaxParts |
partNumberMarker |
表示待列出段的起始位置,只有Part Number大于该参数的段会被列出。 |
ListPartsRequest.setPartNumberMarker |
- 简单列举
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); //列举已上传的段,其中uploadId来自于initiateMultipartUpload ListPartsRequest request = new ListPartsRequest("bucketname", "objectname"); request.setUploadId(uploadId); ListPartsResult result = obsClient.listParts(request); for(Multipart part : result.getMultipartList()){ // 分段号,上传时候指定 Log.i("ListParts", "\t"+part.getPartNumber()); // 段数据大小 Log.i("ListParts","\t"+part.getSize()); // 分段的ETag值 Log.i("ListParts","\t"+part.getEtag()); // 段的最后上传时间 Log.i("ListParts","\t"+part.getLastModified()); }
- 列举段至多返回1000个段信息,如果指定的Upload ID包含的段数量大于1000,则返回结果中ListPartsResult.isTruncated为true表明本次没有返回全部段,并可通过ListPartsResult.getNextPartNumberMarker获取下次列举的起始位置。
- 如果想获取指定Upload ID包含的所有分段,可以采用分页列举的方式。
- 列举所有段
由于ObsClient.listParts只能列举至多1000个段,如果段数量大于1000,列举所有分段请参考如下示例:
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); // 列举所有已上传的段,其中uploadId来自于initiateMultipartUpload ListPartsRequest request = new ListPartsRequest("bucketname", "objectname"); request.setUploadId(uploadId); ListPartsResult result; do{ result = obsClient.listParts(request); for(Multipart part : result.getMultipartList()){ // 分段号,上传时候指定 Log.i("ListParts","\t"+part.getPartNumber()); // 段数据大小 Log.i("ListParts","\t"+part.getSize()); // 分段的ETag值 Log.i("ListParts","\t"+part.getEtag()); // 段的最后上传时间 Log.i("ListParts","\t"+part.getLastModified()); } request.setPartNumberMarker(Integer.parseInt(result.getNextPartNumberMarker())); }while(result.isTruncated());
列举分段上传任务
您可以通过ObsClient.listMultipartUploads列举分段上传任务。列举分段上传任务可设置的参数如下:
参数 |
作用 |
OBS Android SDK对应方法 |
---|---|---|
bucketName |
桶名。 |
ListMultipartUploadsRequest.setBucketName |
prefix |
限定返回的分段上传任务中的对象名必须带有prefix前缀。 |
ListMultipartUploadsRequest.setPrefix |
delimiter |
用于对分段上传任务中的对象名进行分组的字符。对于对象名中包含delimiter的任务,其对象名(如果请求中指定了prefix,则此处的对象名需要去掉prefix)中从首字符至第一个delimiter之间的字符串将作为一个分组并作为commonPrefix返回。 |
ListMultipartUploadsRequest.setDelimiter |
maxUploads |
列举分段上传任务的最大数目,取值范围为1~1000,当超出范围时,按照默认的1000进行处理。 |
ListMultipartUploadsRequest.setMaxUploads |
keyMarker |
表示列举时返回指定的keyMarker之后的分段上传任务。 |
ListMultipartUploadsRequest.setKeyMarker |
uploadIdMarker |
只有与keyMarker参数一起使用时才有意义,用于指定返回结果的起始位置,即列举时返回指定keyMarker的uploadIdMarker之后的分段上传任务。 |
ListMultipartUploadsRequest.setUploadIdMarker |
- 简单列举分段上传任务
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); ListMultipartUploadsRequest request = new ListMultipartUploadsRequest("bucketname"); MultipartUploadListing result = obsClient.listMultipartUploads(request); for(MultipartUpload upload : result.getMultipartTaskList()){ Log.i("ListMultipartUploads","\t" + upload.getUploadId()); Log.i("ListMultipartUploads","\t" + upload.getObjectKey()); Log.i("ListMultipartUploads","\t" + upload.getInitiatedDate()); }
- 列举分段上传任务至多返回1000个任务信息,如果指定的桶包含的分段上传任务数量大于1000,则MultipartUploadListing.isTruncated为true表明本次没有返回全部结果,并可通过MultipartUploadListing.getNextKeyMarker和MultipartUploadListing.getNextUploadIdMarker获取下次列举的起点。
- 如果想获取指定桶包含的所有分段上传任务,可以采用分页列举的方式。
- 分页列举全部分段上传任务
// 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量ACCESS_KEY_ID和SECRET_ACCESS_KEY_ID。 // 您可以登录访问管理控制台获取访问密钥AK/SK,获取方式请参见https://support.huaweicloud.com/intl/zh-cn/usermanual-ca/ca_01_0003.html String ak = System.getenv("ACCESS_KEY_ID"); String sk = System.getenv("SECRET_ACCESS_KEY_ID"); String endPoint = "https://your-endpoint"; String uploadId = "upload id from initiateMultipartUpload"; // 创建ObsClient实例 ObsClient obsClient = new ObsClient(ak, sk, endPoint); ListMultipartUploadsRequest request = new ListMultipartUploadsRequest("bucketname"); MultipartUploadListing result; do{ result = obsClient.listMultipartUploads(request); for(MultipartUpload upload : result.getMultipartTaskList()){ Log.i("ListMultipartUploads","\t" + upload.getUploadId()); Log.i("ListMultipartUploads","\t" + upload.getObjectKey()); Log.i("ListMultipartUploads","\t" + upload.getInitiatedDate()); } request.setKeyMarker(result.getNextKeyMarker()); request.setUploadIdMarker(result.getNextUploadIdMarker()); }while(result.isTruncated());