Skip to content
大纲

上传文件

在FOS中,用户操作的基本数据单元是Object。Object包含Key、Meta和Data。其中,Key是Object的名字;Meta是用户对该Object的描述,由一系列Name-Value对组成;Data是Object的数据。 FOS Java SDK提供了丰富的文件上传接口,可以通过以下方式上传文件:

  • 简单上传
  • 追加上传
  • 分片上传
  • 断点续传上传

简单上传

FOS在简单上传的场景中,支持以指定文件形式、以数据流方式、以二进制串方式、以字符串方式执行Object上传。

java
public void PutObject(FosClient client, String bucketName, String objectKey, byte[] byte1, String string1){
    // 获取指定文件
    File file = new File("/path/to/file.zip");
    // 获取数据流
    InputStream inputStream = new FileInputStream("/path/to/test.zip");

    // 以文件形式上传
    PutObjectResponse putObjectFromFileResponse = client.putObject(bucketName, objectKey, file);
    // 以数据流形式上传
    PutObjectResponse putObjectResponseFromInputStream = client.putObject(bucketName, objectKey, inputStream);
    // 以二进制串上传
    PutObjectResponse putObjectResponseFromByte = client.putObject(bucketName, objectKey, byte1);
    // 以字符串上传
    PutObjectResponse putObjectResponseFromString = client.putObject(bucketName, objectKey, string1);
    // 创建空目录,objectKey需要以正斜线结尾,例如test/
    // FOS控制台默认会将以正斜线(/)结尾的对象,作为文件目录的形式展示
    PutObjectResponse putObjectResponseFromString = client.putObject(bucketName, objectKey, "");
    // 打印ETag
    System.out.println(putObjectFromFileResponse.getETag());
}

Object以文件的形式上传到FOS中,PutObject函数支持不超过5GB的Object上传。在PutObject请求处理成功后,FOS会在Header中返回Object的ETag作为文件标识。 设置文件元信息 文件元信息(Object Meta),是对用户在向FOS上传文件时,同时对文件进行的属性描述,主要分为分为两种:设置HTTP标准属性(HTTP Headers)和用户自定义的元信息。

  • 设定Object的Http Header

FOS Java SDK本质上是调用后台的HTTP接口,因此用户可以在上传文件时自定义Object的Http Header。常用的http header说明如下:

名称描述默认值
Content-MD5文件数据校验,设置后FOS会启用文件内容MD5校验,把您提供的MD5与文件的MD5比较,不一致会抛出错误
Content-Type文件的MIME,定义文件的类型及网页编码,决定浏览器将以什么形式、什么编码读取文件。如没有指,FOS则根据文件的扩展名自动生成,如文件没有扩展名则填默认值application/octet-stream
Content-Disposition指示MIME用户代理如何显示附加的文件,打开或下载,及文件名称
Content-Length上传的文件的长度,超过流/文件的长度会截断,不足为实际值流/文件时间长度
Expires缓存过期时间
Cache-Control指定该Object被下载时的网页的缓存行为
x-fos-content-crc32上传object的CRC值(循环冗余校验码)

参考代码如下:

java
// 初始化上传输入流
ObjectMetadata meta = new ObjectMetadata();

// 设置ContentLength大小
meta.setContentLength(1000);

// 设置ContentType
meta.setContentType("application/json");

// 设置cache-control
meta.setCacheControl("no-cache");

// 设置x-fos-content-crc32
meta.setxIcCrc("crc");

client.putObject(bucketName, objectKey, content, meta);
  • 用户自定义元信息

FOS支持用户自定义元数据来对Object进行描述。如下代码所示:

java
// 设置自定义元数据name的值为my-data
meta.addUserMetadata("name", "my-data");

// 上传Object
client.putObject(bucketName, objectKey, content, meta);

提示:

  • 在上面代码中,用户自定义了一个名字为”name”,值为”my-data”的元数据
  • 当用户下载此Object的时候,此元数据也可以一并得到
  • 一个Object可以有多个类似的参数,但所有的User Meta总大小不能超过2KB

追加上传

上文介绍的简单上传方式,创建的Object都是Normal类型,用户不可再进行追加写,这在日志、视频监控、视频直播等数据复写较频繁的场景中使用不方便。 正因如此,Flyme云FOS特别支持了AppendObject,即以追加写的方式上传文件。通过AppendObject操作创建的Object类型为Appendable Object,可以对该Object追加数据。AppendObject大小限制为0~5G。 通过AppendObject方式上传示例代码如下

java
public void AppendObject(FosClient client, String bucketName, String objectKey, byte[] byte1, String string1) {
    // 获取指定文件
    File file = new File("/path/to/file.zip");
    // 获取数据流
    InputStream inputStream = new FileInputStream("/path/to/test.zip");

    // 以文件形式上传
    ObjectAppendObjectResponse appendObjectFromFileResponse = client.appendObject(bucketName, objectKey, file);
    // 以数据流形式上传
    ObjectAppendObjectResponse appendObjectResponseFromInputStream = client.appendObject(bucketName, objectKey, inputStream);
    // 以二进制串上传
    ObjectAppendObjectResponse appendObjectResponseFromByte = client.appendObject(bucketName, objectKey, byte1);
    // 以字符串上传
    ObjectAppendObjectResponse appendObjectResponseFromString = client.appendObject(bucketName, objectKey, string1);
    // 打印ETag
    System.out.println(appendObjectFromFileResponse.getETag());
    // 打印NextAppendOffset
    System.out.println(appendObjectFromFileResponse.getNextAppendOffset());
    // 打印ContentMd5
    System.out.println(appendObjectFromFileResponse.getContentMd5());
    // 追加上传的示例,需要在请求中加上下次追加写的位置
    Long nextAppendOffset = appendObjectFromFileResponse.getNextAppendOffset();
    AppendObjectRequest appendObjectFromFileRequest = new AppendObjectRequest(bucketName,objectKey,file);
    appendObjectFromFileRequest.setOffset(nextAppendOffset);
    AppendObjectResponse appendObjectFromFileResponse = client.appendObject(appendObjectFromFileRequest);
}

注意:

  1. 追加上传需要指定追加写的位置,否则已写数据会被覆盖
  2. 不支持并发,如果确实有多客户端写入的需求,需要自行维护追加写位置,避免冲突

分块上传

除了通过简单上传及追加上传方式将文上传到FOS以外,FOS还提供了另外一种上传模式 —— Multipart Upload。用户可以在如下的应用场景内(但不仅限于此),使用Multipart Upload上传模式,如:

  • 需要支持断点上传。
  • 上传超过5GB大小的文件。
  • 网络条件较差,和FOS的服务器之间的连接经常断开。
  • 需要流式地上传文件。
  • 上传文件之前,无法确定上传文件的大小。

下面将一步步介绍Multipart Upload的实现。假设有一个文件,本地路径为 /path/to/file.zip ,由于文件比较大,将其分块传输到FOS中。

初始化Multipart Upload

使用 initiateMultipartUpload 方法来初始化一个分块上传事件:

java
// 开始Multipart Upload
InitiateMultipartUploadRequest initiateMultipartUploadRequest = new InitiateMultipartUploadRequest(bucketName, objectKey);
InitiateMultipartUploadResponse initiateMultipartUploadResponse = client.initiateMultipartUpload(initiateMultipartUploadRequest);
// 打印UploadId
System.out.println("UploadId: " + initiateMultipartUploadResponse.getUploadId());

initiateMultipartUpload 的返回结果中含有 UploadId ,它是区分分块上传事件的唯一标识,在后面的操作中,我们将用到它。

上传分块

接着,把文件分块上传。

java
// 设置每块为 5MB
final long partSize = 1024 * 1024 * 5L;
File partFile = new File("/path/to/file.zip");

// 计算分块数目
int partCount = (int) (partFile.length() / partSize);
if (partFile.length() % partSize != 0){
    partCount++;
}

// 新建一个List保存每个分块上传后的ETag和PartNumber
List<PartETag> partETags = new ArrayList<PartETag>();

for(int i = 0; i < partCount; i++){
    // 获取文件流
    FileInputStream fis = new FileInputStream(partFile);
    // 跳到每个分块的开头
    long skipBytes = partSize * i;
    fis.skip(skipBytes);

    // 计算每个分块的大小
    long size = partSize < partFile.length() - skipBytes ?
        partSize : partFile.length() - skipBytes;

    // 创建UploadPartRequest,上传分块
    UploadPartRequest uploadPartRequest = new UploadPartRequest();
    uploadPartRequest.setBucketName(bucketName);
    uploadPartRequest.setKey(objectKey);
    uploadPartRequest.setUploadId(initiateMultipartUploadResponse.getUploadId());
    uploadPartRequest.setInputStream(fis);
    uploadPartRequest.setPartSize(size);
    uploadPartRequest.setPartNumber(i + 1);
    UploadPartResponse uploadPartResponse = client.uploadPart(uploadPartRequest);

    // 将返回的PartETag保存到List中。
    partETags.add(uploadPartResponse.getPartETag());
    // 关闭文件
    fis.close();
}

上面代码的核心是调用 UploadPart 方法来上传每一个分块,但是要注意以下几点:

  • UploadPart 方法要求除最后一个Part以外,其他的Part大小都要大于等于100KB。但是Upload Part接口并不会立即校验上传Part的大小;只有当Complete Multipart Upload的时候才会校验。
  • 为了保证数据在网络传输过程中不出现错误,建议您在UploadPart后,使用每个分块FOS返回的Content-MD5值分别验证已上传分块数据的正确性。当所有分块数据合成一个Object后,不再含MD5值。
  • Part号码的范围是1~10000。如果超出这个范围,FOS将返回InvalidArgument的错误码。
  • 每次上传Part时都要把流定位到此次上传块开头所对应的位置。
  • 每次上传Part之后,FOS的返回结果会包含一个 PartETag 对象,它是上传块的ETag与块编号(PartNumber)的组合,在后续完成分块上传的步骤中会用到它,因此需要将其保存起来。一般来讲这些 PartETag 对象将被保存到List中。

完成分块上传

如下代码所示,完成分块上传:

java
CompleteMultipartUploadRequest completeMultipartUploadRequest =new CompleteMultipartUploadRequest(bucketName, objectKey, initiateMultipartUploadResponse.getUploadId(), partETags);

// 完成分块上传
CompleteMultipartUploadResponse completeMultipartUploadResponse =
    client.completeMultipartUpload(completeMultipartUploadRequest);

// 打印Object的ETag
System.out.println(completeMultipartUploadResponse.getETag());

上面代码中的 partETags 是第二部中保存的partETag的列表,FOS收到用户提交的Part列表后,会逐一验证每个数据Part的有效性。当所有的数据Part验证通过后,FOS将把这些数据part组合成一个完整的Object。

取消分块上传事件

用户可以使用abortMultipartUpload方法取消分块上传。

java
AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(bucketName, objectKey, uploadId);
// 取消分块上传
client.abortMultipartUpload(abortMultipartUploadRequest);

获取未完成的分块上传事件

用户可以使用 listMultipartUploads 方法获取Bucket内未完成的分块上传事件。

java
ListMultipartUploadsRequest listMultipartUploadsRequest =
    new ListMultipartUploadsRequest(bucketName);

// 获取Bucket内所有上传事件
ListMultipartUploadsResponse listing = client.listMultipartUploads(listMultipartUploadsRequest);
// 遍历所有上传事件
for (MultipartUploadSummary multipartUpload : listing.getMultipartUploads()) {
    System.out.println("Key: " + multipartUpload.getKey() + " UploadId: " + multipartUpload.getUploadId());
}

注意:

  1. 默认情况下,如果Bucket中的分块上传事件的数目大于1000,则只会返回1000个Object,并且返回结果中IsTruncated的值为True,同时返回NextKeyMarker作为下次读取的起点。
  2. 若想返回更多分块上传事件的数目,可以使用KeyMarker参数分次读取。

获取所有已上传的块信息

用户可以使用 listParts 方法获取某个上传事件中所有已上传的块。

java
ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, objectKey, uploadId);

// 获取上传的所有Part信息
ListPartsResponse partListing = client.listParts(listPartsRequest);

// 遍历所有Part
for (PartSummary part : partListing.getParts()) {
    System.out.println("PartNumber: " + part.getPartNumber() + " ETag: " + part.getETag());
}

如果需要查看Object的存储类型storage class使用以下代码:

java
public void listPartsStorageClass(){
    ListResponse listPartsResponse = client.listParts(bucketName, key, uploadId);
    String storageClass = listPartsResponse.getStorageClass();
}

注意:

  1. 默认情况下,如果Bucket中的分块上传事件的数目大于1000,则只会返回1000个Object,并且返回结果中IsTruncated的值为True,同时返回NextPartNumberMarker作为下次读取的起点。
  2. 若想返回更多分块上传事件的数目,可以使用PartNumberMarker参数分次读取。

封装分块上传

在Java SDK中,Fos为用户提供了putSuperObjectFromFile接口,它对分块上传涉及到的initiateMultipartUpload、UploadPart、completeMultipartUpload三个方法进行封装,用户只需调用该接口即可完成分块上传,

java
File file = new File("/path/to/file.zip");
PutSuperObjectRequest request = new PutSuperObjectRequest(bucketName, objectKey, file);
FosClient.putSuperObjectFromFile(request);

其中PutSuperObjectRequest的参数有:

参数说明
chunkSize分块大小,默认为5MB,不能小于100KB
nThreads分块上传中线程池中线程的数量,默认等于CPU的核数
isSuperObjectUploadCanced是否取消分块上传
File上传文件

若一个大文件耗时很长,用户想结束分块上传,可调用PutSuperObjectRequest中的cancel()方法设置isSuperObjectUploadCanced为true实现取消分块上传操作。

断点续传上传

当用户向FOS上传大文件时,如果网络不稳定或者遇到程序崩等情况,则整个上传就失败了,失败前已经上传的部分也作废,用户不得不重头再来。这样做不仅浪费资源,在网络不稳定的情况下,往往重试多次还是无法完成上传。 基于上述场景,FOS提供了断点续传上传的能力:

  • 当网络情况一般的情况下,建议使用三步上传方式,将object分为5Mb的块,参考分块上传。
  • 当您的网络情况非常差,推荐使用appendObject的方式进行断点续传,每次append 较小数据256kb,参考追加上传。

    提示

    • 断点续传是分片上传的封装和加强,是用分片上传实现的;
    • 文件较大或网络环境较差时,推荐使用分片上传;