rusty-cat × Azure Blob SAS 实战(后端签 URL 版)

4 阅读5分钟

rusty-cat × Azure Blob SAS 实战(后端签 URL 版)

本文是 rusty-cat 分片并发文档 的「云存储实战」系列之一,教你用 SAS(Shared Access Signature) 的方式做 Azure Blob 分片并发上传与下载:账号密钥留在后端,客户端只拿短期有效的 SAS URL。 先读概念请看:《分片并发上传指南》《分片并发下载指南》。 需要的 feature:azure-blob-sas


1. 适用场景与密钥模型

「SAS」的安全模型:你的后端持有账号密钥,签发短期有效的 blob SAS URL,客户端用 rusty-cat 对着 SAS URL 上传 / 下载,不接触账号密钥

🖼️ 【配图占位 1|SAS 密钥模型(三方时序)】 建议配图:三列时序——客户端、后端(🔑 持 Account Key)、Azure。①客户端向后端要 SAS URL;②后端签发短期 blob SAS URL;③客户端拿 SAS URL 并发上传 / 下载。图注:账号密钥只在后端。 临时 ASCII 草图:

客户端 ──①请求 SAS URL──▶ 后端(🔑AccountKey)
客户端 ◀──②短期 blob SAS URL── 后端
客户端 ═══③并发 Put Block / Range GET═══▶ Azure Blob
  • 适合:手机 App、桌面客户端、浏览器等不可信环境(推荐默认方案)。

2. 加依赖

[dependencies]
rusty-cat = { version = "0.2.4", features = ["azure-blob-sas"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

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

和 OSS 预签名不同,Azure SAS 上传必须带一个「完成请求」(Put Block List,SDK 才能在最后把块列表提交。所以除了每片一个 Put Block,还要额外准备一个有序的 block-id 列表和 put_block_list_request

use std::sync::Arc;

use rusty_cat::api::{
    MeowClient, MeowConfig, PresignedMultipartUpload, PresignedMultipartUploadPlan,
    UploadPounceBuilder,
};
use rusty_cat::azure_blob_sas as azure;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // 后端签发的、有时效的 blob SAS URL
    let blob_sas_url = "https://myaccount.blob.core.windows.net/container/blob.bin?sv=...&sig=...";

    let one_mb: u64 = 1024 * 1024;
    let total: u64 = 5 * one_mb;

    // 每个分片一个 Put Block(从 SAS URL 派生),下标从 0 开始
    let parts = (0..5)
        .map(|i| azure::put_block_from_blob_url(blob_sas_url, i, i as u64 * one_mb, one_mb))
        .collect::<Result<Vec<_>, _>>()?;

    // 提交用的、有序的 block-id 列表
    let block_ids = (0..5).map(azure::block_id_by_index).collect::<Vec<_>>();
    let block_id_refs = block_ids.iter().map(String::as_str).collect::<Vec<_>>();
    let complete_request = azure::put_block_list_request(blob_sas_url, block_id_refs)?;

    let plan = PresignedMultipartUploadPlan::new(total, one_mb, parts)
        .with_complete_request(complete_request); // Azure SAS 必需!
    let protocol = PresignedMultipartUpload::new(plan);

    let client = MeowClient::new(MeowConfig::default());
    let task = UploadPounceBuilder::new("azure-sas.bin", "./azure-sas.bin", one_mb)
        .with_url(blob_sas_url)
        .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(())
}

🖼️ 【配图占位 2|Azure SAS 并发上传数据流】 建议配图:客户端并发发 4 个 Put Block(block id = base64(下标));末尾发一次 Put Block List(携带有序 block-id 列表)提交。图注:为什么 SAS 必须带 complete_request——因为 SDK 需要在所有块上传后一次性提交块列表,缺了它 blob 不会真正生成。 临时 ASCII 草图:

客户端 ═══Put Block #0═══▶ ┐
       ═══Put Block #1═══▶ ├─ 4 块并行
       ═══Put Block #2═══▶ │
       ═══Put Block #3═══▶ ┘
       ──Put Block List(有序 block-id)──▶ 提交 ✅  ← with_complete_request 必需

4. 下载:串行 vs 并发

方式一(串行):SAS range 下载协议

用通用的 PresignedRangeDownload + PresignedRangeDownloadPlan这里的 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 sas_get_url = "https://myaccount.blob.core.windows.net/container/blob.bin?sv=...&sig=...";
    let total_size: u64 = 5 * 1024 * 1024;

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

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

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

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

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

同一个 SAS GET URL,不挂 provider 协议、直接喂默认下载器 + with_total_size + with_max_parts_in_flight(4)

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let sas_get_url = "https://myaccount.blob.core.windows.net/container/blob.bin?sv=...&sig=...";
    let total_size: u64 = 5 * 1024 * 1024;

    let client = MeowClient::new(MeowConfig::default());
    let task = DownloadPounceBuilder::new("blob.bin", "./downloads/blob.bin", 1024 * 1024, sas_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 别搞混:任务构建器的会开启并发PresignedRangeDownloadPlan 的是给串行协议用的。

⚠️ 稳定 URL 规则:并发下载复用一个 URL 且无刷新,SAS GET URL 有效期要覆盖整个下载(含续传)。若网关需要 x-ms-version 等头,用 with_headers(...) 加,别改 URL。详见 《分片并发下载指南》


5. 断点续传与清理

  • 续传:Azure 无 upload id,续传靠恢复服务端「未提交 block 列表」,重启后重建任务再 try_enqueue
  • 未提交 block 会计费到 Azure GC(约 7 天),配生命周期规则清理。

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?;  // 取消(Azure 会停止累积未提交的 block)

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 编译验证。