rusty-cat × Azure Blob 直传直下实战(Shared Key 版)

1 阅读5分钟

rusty-cat × Azure Blob 直传直下实战(Shared Key 版)

本文是 rusty-cat 分片并发文档 的「云存储实战」系列之一,教你用 账号 Shared Key 直连 的方式,把大文件分片并发上传到 Azure Blob Storage、并从 Blob 下载。 先读概念请看:《分片并发上传指南》《分片并发下载指南》。 需要的 feature:azure-blob-direct


1. 适用场景与密钥模型

「直传(direct)」= 客户端进程直接持有 Azure 存储账号的账号密钥(Account Key,Base64),自己在本地做 Shared Key 签名。

🖼️ 【配图占位 1|direct 密钥模型】 建议配图:可信服务器持一把钥匙(Account Key),直接连 Azure Blob;旁注不可信环境(手机/浏览器)打叉、请改用 SAS。 临时 ASCII 草图:

[可信服务器 🔑AccountKey] ──Shared Key 签名──▶ [Azure Blob]
[用户手机/浏览器 ❌]      请改用「SAS」方案
  • 适合:跑在你完全掌控的可信服务器 / 后端。
  • 不适合:不可信环境请改用 《Azure Blob SAS 实战》

2. 加依赖

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

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

use std::sync::Arc;

use rusty_cat::api::{FileTransferRecord, MeowClient, MeowConfig, UploadPounceBuilder};
use rusty_cat::azure_blob_direct::AzureBlobDirectUpload;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = MeowClient::new(MeowConfig::default());

    // 账号名、Base64 的账号密钥
    let protocol = AzureBlobDirectUpload::new("myaccount", "BASE64_ACCOUNT_KEY");

    let task = UploadPounceBuilder::new(
        "blob.bin",
        "./blob.bin",
        1024 * 1024, // 1 MiB 分片
    )
    .with_url("https://myaccount.blob.core.windows.net/mycontainer/blob.bin")
    .with_breakpoint_upload(Arc::new(protocol))
    .with_max_parts_in_flight(4) // 单文件 4 片并发
    .build()?;

    let outcome = client
        .enqueue_and_wait(task, |r: FileTransferRecord| {
            println!("上传进度 {:.1}%  状态 {:?}", r.progress() * 100.0, r.status());
        })
        .await?;

    println!("上传完成!payload={:?}", outcome.payload);
    client.close().await?;
    Ok(())
}

内部发生了什么

Azure Block Blob 的模型是「先分块上传、再一次性提交块列表」:

🖼️ 【配图占位 2|Azure Block Blob 并发上传数据流】 建议配图:客户端并发发送 4 个 Put Block(每个 block id 是分片下标的 base64);全部完成后发一次 Put Block List 按下标严格排序提交。图注:提交顺序来自 block-id 列表,不是到达顺序;Put Block 不返回 ETag。 临时 ASCII 草图:

客户端 ═══Put Block #0═══▶ ┐
       ═══Put Block #1═══▶ ├─ 最多 4 块并行(block id = base64(下标))
       ═══Put Block #2═══▶ │
       ═══Put Block #3═══▶ ┘
       ──Put Block List(按下标排序)──▶ Azure 提交合并 ✅
  • 每个 block id 是分片下标的纯函数,Put Block List 固定提交顺序,所以并发安全;
  • Azure 最多 50000 个 block、单块最大 4000 MiB
  • 峰值内存 ≈ n × 分片大小

4. 下载:两种方式

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

use std::sync::Arc;

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = MeowClient::new(MeowConfig::default());

    let protocol = AzureBlobDirectDownload::new("myaccount", "BASE64_ACCOUNT_KEY");

    let task = DownloadPounceBuilder::new(
        "blob.bin",
        "./downloads/blob.bin",
        1024 * 1024,
        "https://myaccount.blob.core.windows.net/mycontainer/blob.bin",
    )
    .with_breakpoint_download(Arc::new(protocol)) // 挂 provider 协议 = 串行
    .build();

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

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

并发下载 Blob,就让后端签一个 SAS GET URL,不挂 provider 协议、直接喂默认下载器:

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/mycontainer/blob.bin?sv=...&sig=...";
    let total_size: u64 = 20 * 1024 * 1024;

    let client = MeowClient::new(MeowConfig::default());
    let task = DownloadPounceBuilder::new("blob.bin", "./downloads/blob.bin", 2 * 1024 * 1024, sas_get_url)
        .with_total_size(total_size)
        .with_max_parts_in_flight(4)
        .build();

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

如果某个严格的 Azure 网关要求显式 x-ms-version 等头,用任务构建器的 with_headers(...) 加进去(会同时作用于 HEAD 和 range GET),绝不要去改签名 URL。并发下载完整规则见 《分片并发下载指南》。签 SAS 的细节见 《Azure Blob SAS 实战》


5. 断点续传与清理

  • 续传:Azure 没有 upload id,续传靠恢复服务端的「未提交 block 列表」。重启后重建任务再 try_enqueue 即可。
  • 未提交 block 会计费,直到 Azure 垃圾回收(约 7 天)。给容器配一条生命周期规则清理废弃 block。

🖼️ 【配图占位 3|未提交 block 的续传与回收】 建议配图:中断后服务端留有若干「未提交 block」;续传时接管这些 block、只补缺失的;未被提交的 block 约 7 天后被 GC。


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