Réécrire des objets Cloud Storage

La réécriture d'un objet dans Cloud Storage copie ou transforme un objet existant, par exemple en le déplaçant d'un emplacement à un autre, en effectuant une rotation de sa clé de chiffrement ou en modifiant sa classe de stockage. En fonction de la taille de l'objet et des paramètres de réécriture, Cloud Storage peut effectuer l'opération en un seul appel ou renvoyer une progression intermédiaire avec un rewrite_token. Lorsqu'un appel renvoie un rewrite_token, transmettez ce jeton aux requêtes suivantes jusqu'à ce que la réécriture soit terminée.

Ce document vous explique comment configurer une requête de réécriture et piloter la boucle de réécriture à l'aide de l'assistant rewrite_until_done ou d'une boucle manuelle dans les bibliothèques clientes Cloud pour Rust. Si vous débutez avec la bibliothèque cliente, consultez d'abord Utiliser Cloud Storage avec Rust.

Avant de commencer

Avant d'exécuter le code de ce document, assurez-vous de remplir les conditions préalables suivantes :

Dépendances

Ajoutez la bibliothèque cliente Cloud Storage et les crates compatibles à votre fichier Cargo.toml :

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

Préparer le client et la requête de réécriture

L'assistant automatique et la boucle manuelle démarrent à partir d'un client StorageControl et d'un compilateur de requêtes RewriteObject configurés. Dans un champ d'application qui accepte bucket_name au format projects/_/buckets/BUCKET_ID, procédez comme suit :

  1. Initialisez un client StorageControl avec une règle de nouvelle tentative. Nous vous recommandons de prévoir au moins 30 secondes par requête. Cet exemple utilise RetryableErrors avec une limite de cinq tentatives et aucune limite de temps globale par opération :

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

  2. Créez le compilateur de requêtes rewrite_object, marquez la requête comme idempotente afin que le client puisse réessayer en cas d'erreurs temporaires, puis définissez les noms des buckets et des objets source et de destination :

    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. Facultatif : Limitez le nombre maximal d'octets réécrits par appel afin que les requêtes individuelles renvoient la progression plus rapidement au lieu de nécessiter un délai avant expiration plus long. Cet exemple définit une petite limite de 1 Mio pour forcer plusieurs itérations. Utilisez une valeur plus élevée en production :

    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

  4. Facultatif : spécifiez les métadonnées de l'objet de destination, par exemple en remplaçant la classe de stockage par Nearline (NEARLINE) :

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

Exécuter automatiquement la boucle de réécriture

Dans la plupart des cas d'utilisation, intégrez la caractéristique RewriteObjectExt dans le champ d'application et appelez rewrite_until_done sur le générateur de requêtes. Cet outil d'aide gère le rewrite_token au fil des itérations et résout les métadonnées Object de destination une fois la réécriture terminée :

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

Exécuter manuellement la boucle de réécriture

Si vous devez consigner le nombre d'octets intermédiaires, vérifier la progression ou reprendre une réécriture à partir d'un autre processus, pilotez la boucle de réécriture manuellement :

  1. Définissez une fonction d'assistance qui envoie une seule requête RewriteObject, consigne la progression et renvoie les métadonnées Object complètes lorsque resp.done est true ou le rewrite_token suivant :

    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. Appelez l'assistant dans une boucle, en clonant le compilateur de requêtes à chaque itération et en transmettant le jeton renvoyé à set_rewrite_token chaque fois que make_one_request renvoie RewriteProgress::Incomplete :

    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:?}");

Compléter le code

L'exemple suivant importe un objet source de 3 Mio et le réécrit dans la classe de stockage Nearline par incréments de 1 Mio à l'aide de la boucle manuelle. Une fois la réécriture terminée, l'exemple supprime les objets de test et le bucket de test vide :

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

Le résultat ressemble à ce qui suit, montrant la progression sur trois itérations et les métadonnées de l'objet de destination :

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

Effectuer un nettoyage

Si l'exemple se termine prématurément ou si vous exécutez des extraits individuels sans l'assistant de nettoyage, supprimez tous les objets de test restants et supprimez votre bucket de test lorsque vous avez terminé, afin d'éviter que des frais ne soient facturés sur votre compte Google Cloud .