Skip to main content

Crate rp_supabase_storage

Crate rp_supabase_storage 

Source
Expand description

§rp-supabase-storage

crates.io docs.rs

Async Rust client for the Supabase Storage API. It manages buckets, uploads and downloads objects, and creates signed and public URLs.

§Install

[dependencies]
rp-supabase-storage = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

§Quickstart

use rp_supabase_storage::url::Url;
use rp_supabase_storage::{BucketOptions, FileOptions, ListOptions, StorageClient};

let project = Url::parse("https://abc.supabase.co/")?;
let storage = StorageClient::new(&project, "service-role-key")?;

// Create a private bucket that accepts PNG images up to 1 MiB.
let options = BucketOptions {
    public: false,
    file_size_limit: Some(1024 * 1024),
    allowed_mime_types: Some(vec!["image/png".to_owned()]),
};
storage.create_bucket("avatars", &options).await?;

let avatars = storage.from("avatars");

// Upload an object.
let file = FileOptions {
    content_type: Some("image/png".to_owned()),
    cache_control: 3600,
    upsert: true,
};
avatars.upload("users/1.png", vec![0_u8; 16], &file).await?;

// Share it for 60 seconds.
let link = avatars.create_signed_url("users/1.png", 60).await?;
println!("{link}");

// Download, list, and remove.
let bytes = avatars.download("users/1.png").await?;
let entries = avatars.list("users", &ListOptions::default()).await?;
avatars.remove(&["users/1.png"]).await?;

Use StorageClient::new_with_client to reuse an existing reqwest::Client and its connection pool. The default client from StorageClient::new follows redirects (up to 10) only to the same origin, so the API key never goes to another host; a client you pass in is used unchanged.

§Auth model

Every request sends the apikey header. The Authorization header depends on the key and token:

  • StorageClient::new with a legacy JWT key (anon or service role) also sends Authorization: Bearer <api_key>. With the service role key, the client bypasses row level security (RLS). Use it only on a server.
  • StorageClient::new with a new-format key (sb_publishable_... or sb_secret_...) sends no Authorization header. These keys are not JWTs; the gateway checks the apikey header.
  • with_access_token(user_jwt) returns a client that sends the user’s JWT as the bearer token. Storage then applies the RLS policies on storage.objects and storage.buckets for that user. Use the anon key plus a user token in code that acts for a user.
let storage = StorageClient::new(&Url::parse("https://abc.supabase.co/")?, "anon-key")?;
let as_user = storage.with_access_token(user_jwt)?;

public_url sends no request. The URL works only when the bucket is public. For private buckets, call create_signed_url or create_signed_urls. Both use the batch signing endpoint, because the single-object endpoint returns an invalid signature for keys with ?. The API returns signed URLs relative to /storage/v1 with the object key unescaped; this crate rebuilds each URL from the bucket and the encoded object path, then appends the token. Keys with ? and spaces work. The Storage server itself rejects some characters in keys, for example #.

BucketOptions fields are always sent. On update_bucket, None removes an existing file size limit or MIME type list. FileOptions::default() sends Cache-Control: max-age=3600.

§Object paths

Separate path segments with /, for example users/1.png. The client percent-encodes each segment once. It rejects empty paths, empty segments (a//b, leading or trailing /), . and .., tabs, carriage returns, and line feeds with StorageError::InvalidPath before it sends a request.

§Errors

All operations return StorageError:

  • Api { status, body }: the server answered with a non-success status. body is ApiErrorBody::Storage with optional status_code, code, error, and message when the server sent the standard Storage error JSON, else ApiErrorBody::Raw with the body text.
  • InvalidPath: a bucket id or object path is not valid.
  • MissingSignedToken: a signed URL from the server has no ?token= query.
  • SignFailed { path, message }: create_signed_url got no URL for the object, for example because it does not exist or the caller cannot read it.
  • Http, Json, Url, InvalidHeader, InvalidBaseUrl: transport, decoding, and input errors.
match storage.from("blobs").upload("a.bin", vec![1_u8], &FileOptions::default()).await {
    Ok(_) => {}
    Err(error) => match error.code() {
        Some(StorageErrorCode::KeyAlreadyExists | StorageErrorCode::ResourceAlreadyExists) => {
            println!("already uploaded");
        }
        _ => return Err(error),
    },
}

StorageError::code() returns the body’s machine-readable code as a StorageErrorCode. A code this crate does not know is StorageErrorCode::Other.

Most object errors arrive as HTTP 400 with the real status in the body’s statusCode. StorageError::api_status() returns that status, or the HTTP status when the body has none.

Bucket::remove is the bulk endpoint. It skips missing paths and paths that row level security hides, without an error. Bucket::remove_object deletes one object and fails when nothing is deleted: api_status() is 404 and code() is NoSuchKey for a missing object, and api_status() is 403 and code() is AccessDenied for a denied delete.

Bucket::download reads the whole body into memory. Bucket::download_stream returns after the headers arrive. Read the body with chunk() or into_stream(), and stop when the byte count passes your limit:

let mut download = bucket.download_stream("blobs/1.bin").await?;
let mut body = Vec::new();
while let Some(chunk) = download.chunk().await? {
    if body.len() + chunk.len() > limit {
        return Ok(None);
    }
    body.extend_from_slice(&chunk);
}

§Limits

  • No resumable (TUS) uploads. Each upload sends the whole body in one request.
  • No image transformations and no S3 protocol support.
  • Bucket and object operations only; no analytics or vector buckets.

Re-exports§

pub use bytes;
pub use reqwest;
pub use url;

Structs§

Bucket
Object operations in one bucket. Create it with StorageClient::from.
BucketInfo
A bucket as returned by the Storage API.
BucketOptions
Settings for StorageClient::create_bucket and StorageClient::update_bucket.
FileObject
An object or folder entry. Folder entries have no id, timestamps, or metadata.
FileOptions
Settings for Bucket::upload and Bucket::update.
ListOptions
Settings for Bucket::list. None fields use the server defaults (limit 100, offset 0, sorted by name ascending).
ObjectDownload
Body of an object download that is read in chunks. Create it with Bucket::download_stream. The status is already checked, so only transport errors remain.
ObjectKey
Key of a stored object.
SignedUrl
One entry of Bucket::create_signed_urls.
SortBy
Sort order for ListOptions.
StorageClient
Client for the Supabase Storage API at <project>/storage/v1.
StorageErrorBody
Standard Storage error JSON, for example {"statusCode": "409", "code": "KeyAlreadyExists", "error": "Duplicate", "message": "..."}. Every field is optional because some responses omit them.

Enums§

ApiErrorBody
Body of a failed Storage API response.
PathError
Reason for StorageError::InvalidPath.
SortColumn
Sortable column.
SortOrder
Sort direction.
StorageError
Errors returned by StorageClient and Bucket.
StorageErrorCode
Machine-readable code of a Storage API error, from the ErrorCode enum of supabase/storage. A code this crate does not know is kept in Other.