rusty-cat × 阿里云 OSS 预签名实战

2 阅读6分钟

rusty-cat × 阿里云 OSS 预签名实战(后端签 URL 版)

本文是 rusty-cat 分片并发文档 的「云存储实战」系列之一,教你用 预签名(presigned) 的方式做 OSS 分片并发上传与下载:密钥永远留在后端,客户端只拿短期有效的临时 URL。 先读概念请看:《分片并发上传指南》《分片并发下载指南》。 需要的 feature:aliyun-oss-presigned


1. 适用场景与密钥模型

「预签名」的安全模型是:你的应用服务器持有 AccessKey,签发短期有效的 UploadPart / GET URL,客户端用 rusty-cat 只负责对着这些 URL 执行上传 / 下载,从不接触密钥

🖼️ 【配图占位 1|预签名密钥模型(三方时序)】 建议配图:三列时序图——客户端、你的后端(🔑 持 AccessKey)、OSS。①客户端向后端「要 N 个分片的上传 URL」;②后端用密钥签好 N 个短期 UploadPart URL 返回;③客户端拿着这些 URL 直接并发 PUT 到 OSS。图注:密钥只在后端,客户端只有临时 URL。 临时 ASCII 草图:

客户端 ──①请求上传URL──▶ 后端(🔑AccessKey)
客户端 ◀──②返回 N 个短期 UploadPart URL── 后端
客户端 ═══③并发 PUT 分片═══▶ OSS
  • 适合:手机 App、桌面客户端、浏览器等不可信环境(这是推荐的默认方案)。
  • 客户端不需要地域 / Bucket / 密钥,只需要后端给的 URL。

2. 加依赖

[dependencies]
rusty-cat = { version = "0.2.4", features = ["aliyun-oss-presigned"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

3. 上传:完整可运行程序

后端为每个分片偏移签好一个预签名 PUT URL(一片一个),客户端用 aliyun::upload_part 构建分片列表:

use std::sync::Arc;

use rusty_cat::aliyun_oss_presigned as aliyun;
use rusty_cat::api::{
    MeowClient, MeowConfig, PresignedMultipartUpload, PresignedMultipartUploadPlan,
    UploadPounceBuilder,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let one_mb: u64 = 1024 * 1024;
    let total: u64 = 5 * one_mb; // 文件总大小

    // 这些预签名 PUT URL 由你的后端签发,每个分片一个
    let part_urls: [&str; 5] = [
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?partNumber=1&uploadId=...&Signature=...",
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?partNumber=2&uploadId=...&Signature=...",
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?partNumber=3&uploadId=...&Signature=...",
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?partNumber=4&uploadId=...&Signature=...",
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?partNumber=5&uploadId=...&Signature=...",
    ];

    // 为每个分片偏移构建一个 PresignedUploadPart:
    // upload_part(分片号从1开始, 偏移量, 该片大小, 预签名URL)
    let parts = part_urls
        .iter()
        .enumerate()
        .map(|(i, url)| aliyun::upload_part((i + 1) as u64, i as u64 * one_mb, one_mb, *url))
        .collect::<Vec<_>>();

    // Plan 的 total / chunk 必须和任务一致
    let plan = PresignedMultipartUploadPlan::new(total, one_mb, parts);
    let protocol = PresignedMultipartUpload::new(plan);

    let client = MeowClient::new(MeowConfig::default());
    let task = UploadPounceBuilder::new("presigned.bin", "./presigned.bin", one_mb)
        .with_url(part_urls[0])                      // 用任意一个分片 URL 占位,plan 里已带全部
        .with_breakpoint_upload(Arc::new(protocol))
        .with_max_parts_in_flight(4)                 // 4 片并发
        .build()?;

    client
        .enqueue_and_wait(task, |r| println!("{:.1}%", r.progress() * 100.0))
        .await?;
    client.close().await?;
    Ok(())
}

几个关键约束

  • plantotal / chunk 必须等于任务的总大小 / 分片大小,协议会在 prepare()(发送任何字节前)检查;
  • 最后一片允许比 chunk 短(文件不是分片整数倍时);
  • PresignedMultipartUploadPlan::validate() 会拒绝零长度分片、重复偏移、超出总大小的分片;
  • 合并通常由你的后端在带外完成(拿到 protocol.uploaded_parts() 后调 CompleteMultipartUpload)。

🖼️ 【配图占位 2|预签名并发上传数据流】 建议配图:客户端拿着后端签好的 N 个分片 URL,4 条并行箭头直接 PUT 到 OSS;末尾用虚线画「后端带外发起 Complete 合并」。图注:客户端只负责传字节,合并由后端完成。


4. 下载:串行 vs 并发

方式一(串行):预签名 range 下载协议

PresignedRangeDownload + PresignedRangeDownloadPlan,走 provider 串行路径。注意这里 with_total_size 是 Plan 上的方法(不是任务构建器的那个):

use std::sync::Arc;

use rusty_cat::api::{
    DownloadPounceBuilder, MeowClient, MeowConfig, PresignedRangeDownload,
    PresignedRangeDownloadPlan,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let download_url = "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?Expires=...&Signature=...";
    let total_size: u64 = 5 * 1024 * 1024;

    let client = MeowClient::new(MeowConfig::default());

    let plan = PresignedRangeDownloadPlan::new(download_url).with_total_size(total_size);
    let protocol = PresignedRangeDownload::new(plan);

    let task = DownloadPounceBuilder::new("obj.bin", "./downloads/obj.bin", 1024 * 1024, download_url)
        .with_breakpoint_download(Arc::new(protocol))
        .build();

    client.enqueue_and_wait(task, |_r| {}).await?;
    client.close().await?;
    Ok(())
}

方式二(推荐,可并发):预签名 GET URL + 默认下载器

同一个预签名 GET URL,不挂 provider 协议、直接喂默认下载器,就能并发。这是并发下载 OSS 对象的推荐姿势:

use rusty_cat::api::{DownloadPounceBuilder, MeowClient, MeowConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let presigned_get_url = "https://my-bucket.oss-cn-beijing.aliyuncs.com/obj?Expires=...&Signature=...";
    let total_size: u64 = 5 * 1024 * 1024;

    let client = MeowClient::new(MeowConfig::default());
    let task = DownloadPounceBuilder::new("obj.bin", "./downloads/obj.bin", 1024 * 1024, presigned_get_url)
        .with_total_size(total_size)   // 任务构建器上的 with_total_size:开启并发的关键
        .with_max_parts_in_flight(4)   // 4 个 Range GET 并发
        .build();

    client.enqueue_and_wait(task, |r| println!("{:.1}%", r.progress() * 100.0)).await?;
    client.close().await?;
    Ok(())
}

⚠️ 两个 with_total_size 别搞混:任务构建器 DownloadPounceBuilder::with_total_size开启并发;而 PresignedRangeDownloadPlan::with_total_size 是给串行协议用的、不开并发。

⚠️ 稳定 URL 规则:并发下载对每个分片复用同一个 URL、且没有刷新机制,所以预签名 GET URL 的有效期要覆盖整个下载(含续传)。完整规则见 《分片并发下载指南》


5. 断点续传

进程重启后重建任务再 try_enqueue。预签名上传续传用 PresignedMultipartUpload::with_resumed_parts(...) 把上次持久化的分片重新注入,prepare 会跳过已验证的最长连续前缀、只重传其余部分。

🖼️ 【配图占位 3|预签名续传注入已传分片】 建议配图:重启后把已持久化的分片记录(offset+size)注入协议,协议校验连续前缀后只重传空洞之后的分片。


6. 进度回调与任务控制(暂停 / 恢复 / 取消)

前面用的 enqueue_and_wait 会一直等到任务结束,适合脚本或一次性任务。做 UI、或要在后台并发跑并随时控制任务时,改用底层的 try_enqueue:它立刻返回 TaskId,让你分别处理进度回调和完成回调。把上文构建好的 task 传进去即可:

use rusty_cat::api::{
    FileTransferRecord, MeowClient, MeowError, PounceTask, TaskId, TransferStatus,
};

// task 按上文「上传」或「下载」小节构建好后传进来
async fn enqueue_with_control(
    client: &MeowClient,
    task: PounceTask,
) -> Result<TaskId, MeowError> {
    let task_id = client
        .try_enqueue(
            task,
            // 进度回调:每次进度更新都会调用
            move |record: FileTransferRecord| match record.status() {
                TransferStatus::Failed(err) => eprintln!("失败:{err}"),
                TransferStatus::Canceled => println!("已取消"),
                _ => println!("进度 {:.1}%", record.progress() * 100.0),
            },
            // 完成回调:成功到达 Complete 时调用一次
            move |task_id, payload| println!("任务 {task_id} 完成 {payload:?}"),
        )
        .await?;
    Ok(task_id)
}

拿到 TaskId 后就能随时控制任务:

client.pause(task_id).await?;   // 暂停
client.resume(task_id).await?;  // 从断点继续(TaskId 不变)
client.cancel(task_id).await?;  // 取消(取消上传会触发 OSS 的 AbortMultipartUpload 清理未提交分片)

record.status() 返回 &TransferStatus,取值:Pending / Transmission / Paused / Complete / Failed(err) / Canceled;进度是 0.0~1.0 的比例(f32)。完整语义见《分片并发上传指南》第 8 节《分片并发下载指南》第 9 节


7. 相关阅读

本文基于 rusty-cat 0.2.4,代码片段均经过 cargo check --features all 编译验证。