文件上传

本文档描述了上传文件到 PDS 的最佳实践,您可以参考该文档实现上传文件功能。

PDS 文件类型

  • 文件夹:目录类型的文件,自身不承载任何物理数据,所以直接调用 PDS 创建文件接口即可。

  • 文件:非目录类型的文件,自身包含真实的文件数据,创建此类型文件涉及到文件上传步骤,下面文档主要针对此类型的文件进行上传流程介绍。

文件上传流程

流程简述

PDS 的文件上传分为以下三步:

  1. 调用 PDS CreateFile - 创建文件或文件夹接口初始化文件,PDS 会返回文件的元信息以及 HTTP 上传地址。

  2. 上传文件到第一步返回的 HTTP 上传地址中。

  3. 调用 PDS CompleteFile - 完成文件上传接口,完成上传流程。

image

详细流程

下面以上传本地文件做一个简单的示例。

1.创建文件

简述:调用创建文件接口,初始化文件上传。

核心参数设置:

  1. 设置上传到哪个空间下:drive_id = "xxxx";

  2. 设置上传到哪个目录下:parent_file_id = "root",root 表示上传到根目录下;

  3. 设置文件名:name = "test.jpg";

  4. 设置文件类型:type = "file",file 表示文件,folder 表示目录;

  5. 设置文件大小(本地文件真实大小,单位为字节):size = 13381200;

  6. 设置分片列表:part_info_list = [{"part_number":1},{"part_number":2},{"part_number":3}],这里要根据文件大小和客户端定义的分片大小来计算。设置多个分片可以提高上传成功率,还可以进行断点续传。示例里的文件大小约 13MB,客户端可以将分片大小设置为 5MB,因此计算得出需要上传三个分片。

请求 body 示例:

{
    "drive_id":"xxxx",
    "name":"test.jpg",
    "parent_file_id":"root",
    "part_info_list":[
        {
            "part_number":1
        },
        {
            "part_number":2
        },
        {
            "part_number":3
        }
    ],
    "size":13381200,
    "type":"file"
}

返回 body 示例:

{
    "parent_file_id":"root",
    "part_info_list":[
        {
            "part_number":1,
            "upload_url":"https://xxxx1"
        },
        {
            "part_number":2,
            "upload_url":"https://xxxx2"
        },
        {
            "part_number":3,
            "upload_url":"https://xxxx3"
        }
    ],
    "upload_id":"xxxxxx",
    "rapid_upload":false,
    "type":"file",
    "file_id":"xxxxx",
    "revision_id":"xxxxx",
    "domain_id":"xxxx",
    "drive_id":"xxxx",
    "file_name":"test.jpg"
}

核心返回参数解析:

  1. file_id: 云端给文件分配的唯一 id,在同一 drive 空间内,每个文件都有唯一的 file_id。

  2. upload_id: 云端给本次上传过程分配的 id,后续 complete 等流程都需要和该 upload_id 关联。

  3. part_info_list:云端给本次上传过程分配的分片上传地址列表,对应上行参数中的 part_info_list,云端会给每个分片分配一个上传地址。

2.上传文件

简述:根据第一步返回的分片上传地址,遍历上传每个分片,HTTP Method 为 PUT。

下面以 Java 代码示例:

// 遍历所有分片
for (PartInfo uploadPartInfo : partInfoList) {
   // 计算分片在本地文件中的位置
   int number = uploadPartInfo.getPartNumber();
   long pos = (number - 1) * partSize;
   long size = Math.min(length - pos, partSize);
   byte[] partContent = new byte[(int) size];

   // 从本地文件中读取分片内容到内存中
   RandomAccessFile randomAccessFile = new RandomAccessFile(localFile, "r");
   randomAccessFile.seek(pos);
   randomAccessFile.readFully(partContent, 0, (int) size);
   randomAccessFile.close();

   // 上传分片
   RequestBody body = RequestBody.create(null, partContent);
   Request request = new Request.Builder()
   .url(uploadPartInfo.getUploadUrl())
   .header("Content-Length", String.valueOf(size))
   .put(body)
   .build();

   OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
   Response response = okHttpClient.newCall(request).execute();

   // 判断分片是否上传成功
   if (!response.isSuccessful()) {
   System.out.println(response.body().string() + "\n");
   Assert.fail("upload part failed, partNumber:" + number);
   return "";
   }
   System.out.println("upload part success, partNumber:" + number);
}

3.完成文件上传

简述:当第二步中所有的分片都上传完成后,调用完成文件上传接口,完成上传流程。

核心参数设置:

  1. 设置文件所在的空间 id:drive_id = "xxxx";

  2. 设置文件 id:file_id = "xxxx",file_id 填写为第一步创建文件返回的 file_id;

  3. 设置上传流程 id:upload_id = "xxxx",upload_id 填写为第一步创建文件接口返回的 upload_id

请求 body 示例:

{
    "drive_id":"xxxx",
    "file_id":"xxx",
    "upload_id":"xxxx"
}

接口返回 HTTP 状态码 200 后,表示文件已经上传完成。

返回 body 示例:

{
    "domain_id":"xxxx",
    "drive_id":"xxxx",
    "file_id":"xxxx",
    "parent_file_id":"root",
    "type":"file",
    "file_extension":"jpg",
    "name":"test.jpg",
    "size":13381200,
    "status":"available",
    "content_hash":"xxxxx",
    "created_at":"2023-01-16T11:55:12.166Z",
    "updated_at":"2023-01-16T11:55:13.368Z"
}

断点续传

应用场景

用户在上传文件过程中,可以临时暂停上传文件,比如用户只希望在 WIFI 场景下上传文件,则可以在切换到非 WIFI 场景时暂停上传,等切换回 WIFI 后可以从中断点继续上传文件。

实现方式

前述提到客户端可以在上传文件时,对文件进行分片上传。

以下面上传为例:上传 50MB 的文件,假设客户端按照 5 MB 进行分片,则一共可以分成 10 个分片。

image

当客户端已经上传成功前 6 个分片,第 7 片已经上传了一段数据时,用户点击了暂停上传。此时客户端需要将上传的中间信息(文件信息、上传进度)持久化下来,比如可以存储到数据库中。

image

当用户再次点击上传时,第 7 片虽然已经上传过一段数据,但是该分片并没有上传完整,会被作废,所以从第 7 片开始需要重新上传完整的分片。此时后面分片的上传 URL 可能已经过期,上传接口会返回 403,此时可以调用ListUploadedParts - 列举已上传分片,重新获取已过期分片的上传地址。

因为文件存在上传时限,如果文件暂停上传超过 10 天后,则无法再继续断点续传该文件,此时客户端只能重新调用创建文件接口,创建新的云端文件并进行上传。

文件秒传能力

秒传介绍

PDS 提供了 Domain 级文件去重的能力,用户上传文件时,如果文件在云端的该 Domain 下已经存在,无需再走完整的上传流程,只需要本地计算出文件的 SHA1,即可进行秒传。

应用场景

用户 A 上传了一部影片,用户 B 后续再次上传相同影片时,只需走秒传,即可在用户 B 的空间内生成文件,B 不需要再完整上传该影片,既提高了上传效率,又节省了上传流量。

用户 B 秒传上去的文件和用户 A 空间下的文件在 File 级别没有任何关联,无需担心数据安全问题。

使用秒传

秒传需要提前计算出文件完整的 SHA1,在调用创建文件接口时,设置到参数 content_hash 中。

核心参数设置:

  1. 设置文件秒传计算算法,目前只支持 SHA1,content_hash_name = "sha1";

  2. 设置文件 sha1:content_hash = "xxxx";

  3. 设置文件 size:size = 13381200;

其他参数和前述文件上传流程章节的创建文件参数一致。

请求 body 示例:

{
    "drive_id":"xxxx",
    "name":"test.jpg",
    "parent_file_id":"root",
    "content_hash":"xxxxx",
    "content_hash_name":"sha1",
    "part_info_list":[
        {
            "part_number":1
        },
        {
            "part_number":2
        },
        {
            "part_number":3
        }
    ],
    "size":13381200,
    "type":"file"
}

sha1 计算示例代码(Java):

public static String getFileHash(File file) throws IOException {
 	 return Hex.encodeHexString((getFileHashBytes(file)));
}

public static byte[] getFileHashBytes(File file) throws IOException {
   byte[] sha1;
   try {
     MessageDigest digest = MessageDigest.getInstance("SHA1");
     byte[] buffer = new byte[10 * 1024];
     FileInputStream is = new FileInputStream(file);
     int len;
     while ((len = is.read(buffer)) != -1) {
        digest.update(buffer, 0, len);
     }
     is.close();
     sha1 = digest.digest();
   } catch (NoSuchAlgorithmException e) {
   	 throw new RuntimeException("SHA1 algorithm not found.");
   }
   return sha1;
}

返回 body 示例:

{
    "domain_id":"xxxx",
    "drive_id":"xxxx",
    "parent_file_id":"root",
    "upload_id":"rapid-xxxx",
    "rapid_upload":true,
    "type":"file",
    "file_id":"xxxxx",
    "revision_id":"xxxxx",
    "file_name":"test.jpg"
}

核心返回参数解析:

  1. rapid_upload:表示是否命中秒传:

a. rapid_upload = true,表示命中秒传,即云端该 domain 下存在 data 数据一样的文件(根据 SHA1 匹配),直接秒传成功,返回 body 中也不再返回分片上传地址,客户端无需再走后续的【上传文件】和【完成文件上传】这两步。

b. rapid_upload = false,表示没有命中秒传, 此时返回 body 里会包含分片的上传地址,需要再继续后续的【上传文件】和【完成文件上传】这两步。

使用预秒传提升准确率

秒传需要计算文件完整的 SHA1,一般客户端算力有限,上传大文件时计算完整 SHA1 比较耗时,而且云端如果没有 SHA1 匹配的 data 数据,也不能秒传成功,白白浪费了客户端的算力,增加了上传耗时。

为了解决这个问题,提升秒传准确率,PDS 提供了预秒传能力。客户端只需要先计算出文件前 1k 数据的 SHA1,调用 PDS 创建文件接口,设置到参数 pre_hash 中,服务端会校验该 data 是否可能在云端已经存在。

核心参数设置:

  1. 设置文件前 1k 数据的 SHA1:pre_hash = "xxxxx";

其他参数和前述文件上传流程章节的创建文件参数一致。

请求 body 示例:

{
    "drive_id":"10530",
    "name":"test.jpg",
    "parent_file_id":"root",
    "part_info_list":[
        {
            "part_number":1
        },
        {
            "part_number":2
        },
        {
            "part_number":3
        }
    ],
    "pre_hash":"xxxx",
    "size":13381200,
    "type":"file"
}

接口返回解析:

  • 如果云端返回 HTTP 状态码为 409, 则表明预秒传匹配成功,也就是云端可能存在相同数据(因为前 1k 碰撞几率大,所以不能保证一定存在),此时客户端可以继续计算文件完整 SHA1,再次调用创建文件接口,尝试秒传。

  • 如果云端返回 HTTP 状态码为 201, 则表明预秒传没有匹配成功,也就是云端此刻肯定不存在相同数据,此时接口会同步返回分片上传地址,客户端继续走完整上传流程即可。

注意点:预秒传是后台异步计算的,可能会出现分钟级延迟,所以刚上传到云端的文件,再次上传相同文件时,不一定能命中预秒传。

整体流程图

image

覆盖上传

PDS 支持覆盖上传某个文件,只需要在创建文件接口中,设置 file_id 为要覆盖的文件 id 即可,其他流程和普通上传一致。

请求 body 示例:

{
    "drive_id":"xxxx",
    "file_id":"xxxxx",
    "name":"test.jpg",
    "parent_file_id":"root",
    "part_info_list":[
        {
            "part_number":1
        },
        {
            "part_number":2
        },
        {
            "part_number":3
        }
    ],
    "size":13373603,
    "type":"file"
}

关键问题:

  1. 文件覆盖写后,文件的 file_id 不会发生变化,还可以使用之前的 file_id 进行文件操作。

  2. 多个端并发覆盖写同一个文件时,会使用最后一个成功调用完成文件上传接口的版本作为文件最新版本。

  3. 如果想保留文件覆盖写之前的版本,需要先开启多版本功能,多版本接口可以参考ListRevision - 列举版本

常见问题

文件分片的上传地址过期了

文件分片的上传地址是有时效的,目前有效期为 1 个小时,如果 1 个小时后再进行上传(比如用户暂停后,断点续传场景),则上传接口会返回 403。此时可以调用ListUploadedParts - 列举已上传分片,重新获取已过期分片的上传地址。

文件上传限制

  • 单个文件分片最大限制 5GB

  • 因服务端使用流式计算SHA1值,单个文件的分片需要串行上传,不支持多个分片并行上传

  • 分片不允许覆盖

文件上传时限

文件从开始上传到最后完成上传,需要在 10 天内完成,超过10 天后本次上传流程会被作废,此时客户端只能重新调用创建文件接口,创建新的云端文件并进行上传。