重寫 Cloud Storage 物件

在 Cloud Storage 中重寫物件,會複製或轉換現有物件,例如跨位置移動、輪替加密金鑰,或變更儲存空間級別。視物件大小和重寫參數而定,Cloud Storage 可能會在單一呼叫中完成作業,或透過 rewrite_token 傳回中間進度。當呼叫傳回 rewrite_token 時,請將該權杖傳遞至後續要求,直到重寫作業完成。

本文說明如何使用 rewrite_until_done 輔助程式,或 Rust 適用的 Cloud 用戶端程式庫中的手動迴圈,設定重寫要求並驅動重寫迴圈。如果您是第一次使用用戶端程式庫,請先參閱「搭配使用 Cloud Storage 與 Rust」。

事前準備

執行本文件中的程式碼前,請先完成下列必要條件:

依附元件

將 Cloud Storage 用戶端程式庫和支援的 Crate 新增至 Cargo.toml 檔案:

cargo add google-cloud-storage google-cloud-gax anyhow bytes
cargo add tokio --features macros,rt-multi-thread

準備用戶端和重寫要求

自動輔助程式和手動迴圈都會從設定的 StorageControl 用戶端和 RewriteObject 要求建構工具開始。在接受 projects/_/buckets/BUCKET_ID 格式 bucket_name 的範圍內,完成下列步驟:

  1. 使用重試政策初始化 StorageControl 用戶端。建議每項要求至少預留 30 秒。本範例使用 RetryableErrors,限制重試 5 次,且每項作業沒有整體時間限制:

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

  2. 建立 rewrite_object 要求建構工具,將要求標示為冪等,讓用戶端可以重試暫時性錯誤,並設定來源和目的地 bucket 及物件名稱:

    let mut builder = control
        .rewrite_object()
        .with_idempotency(true) // retry transient errors
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

  3. 選用:限制每次呼叫重寫的位元組數上限,讓個別要求能更快傳回進度,而不必延長要求逾時時間。這個範例設定了 1 MiB 的小限制,以強制執行多次疊代;在實際工作環境中,請使用較大的值:

    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

  4. 選用:指定目的地物件中繼資料,例如將儲存空間級別變更為 Nearline (NEARLINE):

    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

自動執行重寫迴圈

在大多數情況下,請將 RewriteObjectExt 特徵帶入範圍,並在要求建構工具上呼叫 rewrite_until_done。這個輔助程式會管理各個疊代的 rewrite_token,並在重寫完成後解析為目的地 Object 中繼資料:

use google_cloud_storage::builder_ext::RewriteObjectExt;
let dest_object = builder.rewrite_until_done().await?;
println!("dest_object={dest_object:?}");

手動執行重寫迴圈

如需記錄中繼位元組數、檢查點進度,或從其他程序繼續執行重寫作業,請手動執行重寫迴圈:

  1. 定義輔助函式,用於傳送單一 RewriteObject 要求、記錄進度,並在 resp.done 為 true 時傳回已完成的 Object 中繼資料,或傳回下一個 rewrite_token:

    enum RewriteProgress {
        // This holds the rewrite token
        Incomplete(String),
        Done(Box<Object>),
    }
    
    async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
        let resp = builder.send().await?;
        if resp.done {
            println!(
                "DONE:     total_bytes_rewritten={}; object_size={}",
                resp.total_bytes_rewritten, resp.object_size
            );
            return Ok(RewriteProgress::Done(Box::new(
                resp.resource
                    .expect("A `done` response must have an object."),
            )));
        }
        println!(
            "PROGRESS: total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        Ok(RewriteProgress::Incomplete(resp.rewrite_token))
    }

  2. 在迴圈中呼叫輔助程式,在每次疊代時複製要求建構工具,並在 make_one_request 傳回 RewriteProgress::Incomplete 時,將傳回的權杖傳遞至 set_rewrite_token:

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

完整程式碼

以下範例會上傳 3 MiB 的來源物件,並使用手動迴圈,以 1 MiB 為單位將其重寫至 Nearline Storage 儲存空間級別。重寫完成後,範例會刪除測試物件和空白測試 bucket:

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .with_idempotency(true) // retry transient errors
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

輸出內容大致如下,顯示三次疊代的進度,以及目的地物件中繼資料:

PROGRESS: total_bytes_rewritten=1048576; object_size=3145728
PROGRESS: total_bytes_rewritten=2097152; object_size=3145728
DONE:     total_bytes_rewritten=3145728; object_size=3145728
dest_object=Object { name: "rewrite-object-clone", ... }

清除所用資源

如果範例提早結束,或您在沒有清理輔助程式的情況下執行個別程式碼片段,請刪除所有剩餘的測試物件,並在完成後刪除測試 bucket,以免 Google Cloud 帳戶持續產生費用。