Cloud Storage オブジェクトを書き換える

Cloud Storage でオブジェクトを書き換えると、既存のオブジェクトがコピーまたは変換されます。たとえば、ロケーション間の移動、暗号鍵のローテーション、ストレージ クラスの変更などが行われます。オブジェクトのサイズと書き換えパラメータによっては、Cloud Storage が 1 回の呼び出しでオペレーションを完了するか、rewrite_token で中間進行状況を返す場合があります。呼び出しが rewrite_token を返したら、書き換えが完了するまで、そのトークンを後続のリクエストに渡します。

このドキュメントでは、Cloud Client Libraries for Rust で rewrite_until_done ヘルパーまたは手動ループを使用して、書き換えリクエストを構成し、書き換えループを駆動する方法について説明します。クライアント ライブラリを初めて使用する場合は、まず Rust で Cloud Storage を使用するをご覧ください。

始める前に

このドキュメントのコードを実行する前に、次の前提条件を満たしてください。

依存関係

Cloud Storage クライアント ライブラリとサポート クレートを 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 リクエスト ビルダーを作成し、クライアントが一時的なエラーを再試行できるようにリクエストをべき等としてマークし、ソースと宛先のバケット名とオブジェクト名を設定します。

    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. 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 ストレージ クラスに書き換えます。書き換えが完了すると、この例ではテスト オブジェクトと空のテストバケットが削除されます。

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;
}

出力は次のようになります。3 回の反復処理の進行状況と、宛先オブジェクトのメタデータが表示されます。

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", ... }

クリーンアップ

サンプルが早期に終了した場合、またはクリーンアップ ヘルパーなしで個々のスニペットを実行した場合は、 Google Cloud アカウントに継続的な料金が発生しないように、残りのテスト オブジェクトを削除し、完了したらテストバケットを削除します。