ENbext.dev
DocumentationFonctionnalités

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Client de stockage

La fonctionnalité StorageClient est l’interface côté exécution pour tout magasin d’objets blob/objets. Un StorageClientPlugin récupère les octets par clé, place les octets sur une clé, supprime une clé, répertorie les clés sous un préfixe et crée des URL présignées. C'est la forme entière - tout ce qui ressemble à "un blob nommé dans un espace de clé plat" s'insère derrière, et le code d'application appelant cette fonctionnalité ne sait jamais si le backend est S3, R2, MinIO, un système de fichiers local ou un double de test en mémoire.

La forme est volontairement étroite. La gestion des versions au-delà d'un seul etag, les niveaux de cohérence, le téléchargement en plusieurs parties et la réplication sont tous des problèmes spécifiques au backend qui restent dans le plugin, et non dans le trait partagé.

Le trait#

rust
pub trait StorageClientPlugin: Send + Sync {
    fn name(&self) -> &str;

    fn get(&self, key: &str) -> Result<Vec<u8>, StorageError>;

    fn put(&self, key: &str, body: Vec<u8>, opts: PutOpts) -> Result<(), StorageError>;

    fn delete(&self, key: &str) -> Result<(), StorageError>;

    fn list_page(
        &self,
        prefix: &str,
        continuation_token: Option<&str>,
    ) -> Result<(Vec<Object>, Option<String>), StorageError>;

    fn presigned_url(
        &self,
        key: &str,
        op: PresignOp,
        ttl_secs: u64,
    ) -> Result<String, StorageError>;

    fn is_healthy(&self) -> bool { true }
}
  • name() : un court identifiant affiché dans les journaux, les métriques et la TUI.
  • get / put / delete — opérations au niveau clé. Les charges utiles sont délibérément Vec<u8>. Voir la note sur les charges utiles ci-dessous.
  • list_page — liste paginée. Voir la note sur la pagination des listes ci-dessous.
  • presigned_url — renvoie une URL signée sous la forme String. L'appelant choisit comment l'utiliser.
  • is_healthy — true par défaut ; les backends avec un client de longue durée doivent remplacer le service par ping.

Les charges utiles sont Vec<u8>#

Les premières ébauches utilisaient bytes::Bytes pour get et put. Nous sommes revenus à Vec<u8> pour trois raisons :

  1. Hygiène des caisses à feuilles. bext-plugin-api dépend uniquement de serde et serde_json. Mettre Bytes dans le trait entraînerait bytes dans chaque caisse invité WASM liée à l'API, pour une victoire ergonomique qui ne dépasse pas le coût de la dépendance.
  2. Plat WASM ABI. La limite invité/hôte transmet de toute façon des tampons d'octets simples, il n'y a donc aucun avantage de clone bon marché sur le fil.
  3. Les appelants peuvent terminer. Quiconque souhaite Bytes à l'extérieur peut créer Bytes::from(vec) en une seule ligne et payer le coût de conversion uniquement là où cela l'intéresse.

Les backends natifs qui contiennent un client aws-sdk / object_store / local-fs en interne peuvent bien sûr utiliser Bytes à l'intérieur ; la surface du trait leur demande simplement de distribuer des vecteurs simples à la frontière.

La liste est paginée et non diffusée#

Chaque magasin d'objets expose la liste en tant que point de terminaison paginé avec un jeton de continuation : S3, R2, GCS, Azure Blob, ils se ressemblent tous de l'extérieur. Nous exposons cette forme directement :

rust
fn list_page(
    &self,
    prefix: &str,
    continuation_token: Option<&str>,
) -> Result<(Vec<Object>, Option<String>), StorageError>;

Le tuple renvoyé est (objects_on_this_page, next_token). Si next_token est Some, retransmettez-le lors du prochain appel. None signifie « plus de pages ». Une première page vide est une réponse valide (sans erreur) — NotFound est réservé aux clés qui n'existent pas, pas aux préfixes qui n'ont pas encore de correspondance.

Nous avons choisi cette forme plutôt qu'un BoxStream<'_, Object> car :

  • Il conserve le trait synchrone, correspondant à toutes les autres capacités E1/E2 (Session, Auth, Mailer, Tracer, Scheduled, Webhook, FeatureFlag, I18n, Locking).
  • Cela évite de glisser futures dans une caisse à feuilles. Sinon, bext-plugin-api aurait besoin de futures-core juste pour nommer Stream.
  • Cela correspond au fonctionnement des vrais backends. L'API de liste native de chaque magasin d'objets est paginée. Un wrapper de flux était l’exception et non la règle.
  • Les appelants qui souhaitent un flux peuvent en créer un sur deux lignes. Un std::iter::from_fn (sync) ou un async_stream::stream! (async) sur list_page est trivial.

Les backends qui contiennent une forme de flux native peuvent toujours l'utiliser en interne et la découper en pages à la limite du trait.

Classement des erreurs#

Les erreurs sont une énumération plate avec quatre variantes porteuses :

Variante HTTP (lorsqu'il est apparu en amont) Quand le backend le renvoie
NotFound 404 La clé n'existe pas. Renvoyé par get et presigned_url pour les clés manquantes.
AccessDenied 403 Informations d'identification manquantes, erronées ou autorisées mais pas pour cette clé.
QuotaExceeded 413 La taille de l'objet, le plafond par compartiment ou le quota de taux de requêtes ont été dépassés.
Backend(String) 500 Tout le reste : réseau, disque, fournisseur 5xx, réponse mal formée, erreur SDK. Le message concerne les journaux.

Les appelants se dirigent vers la variante pour décider quoi faire. Un gestionnaire de téléchargement typique ressemble à :

rust
match storage.put(key, bytes, opts) {
    Ok(()) => Ok(204),
    Err(StorageError::QuotaExceeded) => Err((413, "too large")),
    Err(StorageError::AccessDenied) => Err((403, "forbidden")),
    Err(StorageError::NotFound) => Err((404, "gone")),
    Err(StorageError::Backend(msg)) => {
        tracing::error!("storage backend error: {msg}");
        Err((500, "internal"))
    }
}

Les plugins sont censés classer – et non paraphraser – les erreurs de leur backend. @bext/storage-s3 mappe NoSuchKey → NotFound, HTTP 403 → AccessDenied et EntityTooLarge → QuotaExceeded, tout le reste tombant sur Backend(msg).

URL présignées#

presigned_url(key, op, ttl_secs) crée une URL signée de courte durée pour la clé donnée. PresignOp est une énumération étroite :

Variante Signification
Get URL de téléchargement signée. Le titulaire peut récupérer l'objet pour ttl_secs.
Put URL de téléchargement signée. Le titulaire peut télécharger des octets sur la clé pour ttl_secs.

Seulement ces deux-là, délibérément. Delete et Head peuvent être ajoutés ultérieurement si un appelant concret en a besoin ; les expédier plus tôt verrouillerait une forme que nous n’avons pas testée. Les backends qui ne peuvent pas créer d'URL présignées (un plugin de système de fichiers pur, par exemple) devraient renvoyer Backend("presigned URLs not supported") plutôt que de paniquer.

Le type de retour est String, et non url::Url, pour la même raison de caisse de feuilles : la caisse de traits n'extrait pas url. Les appelants s'analysent s'ils le souhaitent.

Backends compatibles S3 via @bext/storage-s3#

La première implémentation de référence, @bext/storage-s3, est un wrapper léger autour du client AWS SDK v1 aws-sdk-s3. Il fonctionne avec le vrai AWS S3 et avec tout fournisseur parlant l'API S3 : le backend est sélectionné uniquement en remplaçant l'URL du point de terminaison au moment de la construction :

  • AWS S3 — pas de remplacement, région par défaut résolue à partir de l'environnement.
  • Cloudflare R2 — https://<account>.r2.cloudflarestorage.com, région auto.
  • MinIO — http://minio.local:9000 (ou partout où se trouve votre MinIO).
  • Backblaze B2 — https://s3.<region>.backblazeb2.com.

Le plugin active automatiquement l'adressage de type chemin lorsqu'un remplacement de point de terminaison est défini, car chaque fournisseur non AWS de cette liste l'exige et AWS le tolère. Les opérateurs n'ont pas besoin d'apprendre un autre drapeau.

À l’intérieur du plugin, chaque méthode de trait est synchrone ; le client asynchrone AWS SDK est piloté soit par un handle tokio injecté (lorsque l'hôte exécute déjà tokio), soit par un environnement d'exécution à thread unique détenu. Même modèle que @bext/mailer-ses.

Exemple : bext.config.toml#

toml
# dev: local MinIO, single endpoint override
[storage]
provider    = "s3"
bucket      = "dev-uploads"
endpoint    = "http://minio.local:9000"
region      = "us-east-1"

# prod: Cloudflare R2, production bucket
[storage]
provider    = "s3"
bucket      = "prod-uploads"
endpoint    = "https://abc123.r2.cloudflarestorage.com"
region      = "auto"
# credentials come from env (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)

Changer de backend est un changement de configuration. Le code d'application appelant storage.put(...) reste exactement le même ; le plugin derrière la capacité storage est celui configuré par l'opérateur.

Conseil

Si vous devez servir un fichier volumineux directement au client sans le mettre en mémoire tampon dans votre gestionnaire, utilisez une URL Get présignée ( PresignOp::Get ) et redirigez-y le client — get matérialise l'objet entier dans un Vec<u8> dans le processus de plug-in, ce qui ne convient pas aux objets de plusieurs gigaoctets.

Lorsque StorageClient n'a pas la bonne forme#

StorageClient est destiné au travail** en forme de blob **: octets opaques derrière une clé, indexés par préfixe. Ce n'est pas pour :

  • Requêtes structurées. Si vous avez besoin de WHERE tag = 'foo' ORDER BY created_at, il s'agit d'une base de données ou d'un index de recherche, pas d'un magasin d'objets blob.
  • Mutations transactionnelles entre clés. Les magasins d'objets sont des gagnants en dernière écriture ; Les écritures atomiques multi-clés n'existent pas sous cette forme. Utilisez une vraie base de données.
  • Flux de téléchargement/téléchargement multipart avec reprise. Ceux-ci auront éventuellement une extension dédiée sur le trait (probablement un sous-module multipart avec ses propres types), car la forme est trop différente d'un put à un seul coup pour être intégrée dans une seule méthode sans compromis.
  • Lectures en continu d'objets très volumineux. get matérialise le corps entier en un Vec<u8>. Les appelants qui ont besoin de diffuser directement une exportation de 10 Go vers le client doivent utiliser l'API du plugin spécifique au backend jusqu'à ce qu'une extension de streaming arrive.

Si l'élément que vous stockez est « un fichier de taille raisonnable téléchargé par l'utilisateur » ou « un instantané sérialisé pour un travail par lots », StorageClient est la bonne forme. S'il s'agit d'un « pipeline d'objets volumineux avec des lectures partielles et des écritures pouvant être reprises », attendez l'extension en plusieurs parties.

Où aller ensuite#

  • Aperçu des capacités couvre l’échelle de promotion et les autres capacités fondamentales E1/E2.
  • Le plugin de référence @bext/storage-s3 se trouve dans crates/bext-impls/bext-storage-s3 et constitue le point de départ recommandé pour toute personne portant une base de code S3/R2/MinIO/B2 existante sur la forme de capacité.
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.