| Crates.io | garage-sdk |
| lib.rs | garage-sdk |
| version | 0.1.1 |
| created_at | 2026-01-09 13:27:13.669594+00 |
| updated_at | 2026-01-09 14:15:59.200462+00 |
| description | Async Rust SDK for Garage S3-compatible storage with uploads and public URL generation |
| homepage | https://github.com/boniface/garage-sdk |
| repository | https://github.com/boniface/garage-sdk |
| max_upload_size | |
| id | 2032062 |
| size | 174,840 |
An async Rust SDK for Garage (S3-compatible) that uploads files from paths, URLs, or bytes and returns a stable public URL for CDN or proxy-fronted access. Designed for production deployments where Garage sits behind a signing proxy (e.g., Envoy) or a public CDN base URL.
Need a production-ready Garage + Envoy setup? See docs/background.md for the full deployment guide.
tracing cratetokio for async operationsAdd to your Cargo.toml:
[dependencies]
garage-sdk = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use garage_sdk::{GarageUploader, UploaderConfig};
#[tokio::main]
async fn main() -> Result<(), garage_sdk::Error> {
// Configure the uploader
let config = UploaderConfig::builder()
.endpoint("https://s3.example.com")
.bucket("my-bucket")
.public_base_url("https://cdn.example.com")
.credentials("access_key_id", "secret_access_key")
.build()?;
let uploader = GarageUploader::new(config)?;
// Upload a local file
let result = uploader.upload_from_path("./image.png").await?;
println!("Uploaded to: {}", result.public_url);
Ok(())
}
You can load configuration in three primary ways:
UploaderConfig::from_env() (recommended for K8s env injection)UploaderConfig::from_secret_dir(...) (recommended for mounted secrets)UploaderConfig::from_env_or_secret_dir(...)use garage_sdk::UploaderConfig;
use std::time::Duration;
let config = UploaderConfig::builder()
.endpoint("https://s3.example.com") // Required: S3 endpoint
.region("garage") // Optional: defaults to "garage"
.bucket("my-bucket") // Required: target bucket
.public_base_url("https://cdn.example.com") // Required: public CDN URL
.key_prefix("uploads") // Optional: prefix for all keys
.credentials("access_key", "secret_key") // Required: AWS credentials
.download_timeout(Duration::from_secs(60)) // Optional: defaults to 30s
.max_file_size(50 * 1024 * 1024) // Optional: defaults to 100MB
.max_buffered_bytes(8 * 1024 * 1024) // Optional: defaults to 8MB
.build()?;
use garage_sdk::UploaderConfig;
// Reads from environment variables:
// - GARAGE_ENDPOINT or S3_ENDPOINT
// - GARAGE_REGION or S3_REGION (optional)
// - GARAGE_BUCKET or S3_BUCKET
// - GARAGE_PUBLIC_URL or S3_PUBLIC_URL
// - GARAGE_KEY_PREFIX or S3_KEY_PREFIX (optional)
// - AWS_ACCESS_KEY_ID
// - AWS_SECRET_ACCESS_KEY
let config = UploaderConfig::from_env()?;
Kubernetes example:
env:
- name: GARAGE_ENDPOINT
value: "https://s3.example.com"
- name: GARAGE_BUCKET
valueFrom:
secretKeyRef:
name: garage-sdk
key: bucket
- name: GARAGE_PUBLIC_URL
valueFrom:
secretKeyRef:
name: garage-sdk
key: public_url
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: garage-sdk
key: access_key_id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: garage-sdk
key: secret_access_key
Mount your secret as a volume (each key becomes a file), then load from the directory:
use garage_sdk::UploaderConfig;
let config = UploaderConfig::from_secret_dir("/var/run/secrets/garage")?;
Kubernetes example:
volumes:
- name: garage-secrets
secret:
secretName: garage-sdk
containers:
- name: app
volumeMounts:
- name: garage-secrets
mountPath: /var/run/secrets/garage
readOnly: true
Expected filenames:
endpointregion (optional, defaults to garage)bucketpublic_urlkey_prefix (optional)access_key_idsecret_access_keyuse garage_sdk::{SecretFileNames, UploaderConfig};
let names = SecretFileNames {
endpoint: "s3_endpoint".into(),
region: None,
bucket: "s3_bucket".into(),
public_url: "s3_public_url".into(),
key_prefix: None,
access_key_id: "s3_access_key_id".into(),
secret_access_key: "s3_secret_access_key".into(),
};
let config = UploaderConfig::from_secret_dir_with_names("/var/run/secrets/garage", &names)?;
Or with a common prefix:
use garage_sdk::{SecretFileNames, UploaderConfig};
let names = SecretFileNames::with_prefix("s3_");
let config = UploaderConfig::from_secret_dir_with_names("/var/run/secrets/garage", &names)?;
Or with a common suffix:
use garage_sdk::{SecretFileNames, UploaderConfig};
let names = SecretFileNames::with_suffix("_secret");
let config = UploaderConfig::from_secret_dir_with_names("/var/run/secrets/garage", &names)?;
Or via the builder:
use garage_sdk::{SecretFileNamesBuilder, UploaderConfig};
let names = SecretFileNamesBuilder::new()
.with_prefix("s3_")
.endpoint("s3_endpoint")
.bucket("s3_bucket")
.public_url("s3_public_url")
.access_key_id("s3_access_key_id")
.secret_access_key("s3_secret_access_key")
.region(None::<String>)
.key_prefix(None::<String>)
.build()?;
let config = UploaderConfig::from_secret_dir_with_names("/var/run/secrets/garage", &names)?;
Merge defaults without overriding explicit fields:
use garage_sdk::{SecretFileNames, SecretFileNamesBuilder, UploaderConfig};
let names = SecretFileNamesBuilder::new()
.endpoint("custom_endpoint")
.merge_defaults(SecretFileNames::with_prefix("s3_"))
.build()?;
let config = UploaderConfig::from_secret_dir_with_names("/var/run/secrets/garage", &names)?;
use garage_sdk::UploaderConfig;
let config = UploaderConfig::from_env_or_secret_dir("/var/run/secrets/garage")?;
let result = uploader.upload_from_path("./photo.jpg").await?;
println!("Public URL: {}", result.public_url);
println!("Key: {}", result.key);
println!("Size: {} bytes", result.size);
println!("Content-Type: {}", result.content_type);
Downloads the content from a URL and uploads it to storage:
let result = uploader
.upload_from_url("https://example.com/image.png")
.await?;
Small downloads are buffered in memory by default (8 MB), while larger or unknown-size responses are streamed with the size cap enforced.
upload_from_url buffers small downloads in memory and streams larger or unknown-size
responses to avoid unbounded memory usage.
8 MB (max_buffered_bytes)100 MB (max_file_size)If Content-Length is present and below the threshold, the response is buffered.
Otherwise, the response is streamed and the size cap is enforced during the read.
let json_data = r#"{"message": "Hello!"}"#;
let result = uploader
.upload_bytes(
json_data.as_bytes().to_vec(),
"application/json",
Some("json"),
)
.await?;
All upload methods return an UploadResult:
pub struct UploadResult {
pub bucket: String, // The bucket name
pub key: String, // The object key
pub public_url: String, // The public CDN URL
pub etag: Option<String>, // MD5 hash from S3
pub content_type: String, // MIME type
pub size: u64, // File size in bytes
}
You can extend the SDK without changing core logic by plugging in your own implementations of the provided traits:
Downloader: controls how remote URLs are fetchedStorageClient: controls how objects are uploadedKeyGenerator: controls how object keys are generatedUse GarageUploader::with_components to supply custom implementations while
keeping the public API unchanged.
src/
config/
mod.rs
data.rs
download/
mod.rs
impls.rs
error/
mod.rs
types.rs
keygen/
mod.rs
generator.rs
storage/
mod.rs
client.rs
types/
mod.rs
model.rs
uploader/
mod.rs
client.rs
lib.rs
The SDK uses custom error types for proper error handling:
use garage_sdk::Error;
match uploader.upload_from_path("./file.txt").await {
Ok(result) => println!("Success: {}", result.public_url),
Err(Error::FileRead { path, source }) => {
eprintln!("Could not read file {}: {}", path, source);
}
Err(Error::S3Operation { operation, reason }) => {
eprintln!("S3 {} failed: {}", operation, reason);
}
Err(Error::Config { message }) => {
eprintln!("Configuration error: {}", message);
}
Err(e) => eprintln!("Error: {}", e),
}
| Error | Description |
|---|---|
Config |
Invalid configuration |
InvalidUrl |
Failed to parse URL |
FileRead |
Cannot read local file |
Download |
Failed to download from URL |
Http |
HTTP request error |
S3Operation |
S3 API call failed |
InvalidPath |
Invalid file path |
# In your application's Cargo.toml
[dependencies]
garage-sdk = { path = "../garage-sdk" }
# Or from a git repository:
# garage-sdk = { git = "https://github.com/boniface/garage-sdk" }
use garage_sdk::{GarageUploader, UploaderConfig, Error};
pub struct ImageService {
uploader: GarageUploader,
}
impl ImageService {
pub fn new() -> Result<Self, Error> {
let config = UploaderConfig::from_env()?;
let uploader = GarageUploader::new(config)?;
Ok(Self { uploader })
}
pub async fn upload_user_avatar(&self, path: &str) -> Result<String, Error> {
let result = self.uploader.upload_from_path(path).await?;
Ok(result.public_url)
}
}
# Set configuration and credentials
export GARAGE_ENDPOINT="https://s3.example.com"
export GARAGE_BUCKET="my-bucket"
export GARAGE_PUBLIC_URL="https://cdn.example.com"
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
# Optional inputs for extra examples
export GARAGE_EXAMPLE_FILE="/path/to/local/file.jpg"
export GARAGE_EXAMPLE_URL="https://example.com/image.png"
# Run the example
cargo run --features example
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Licensed under either of:
at your option.