use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use axum::extract::FromRequestParts;
use axum::http::{request::Parts, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Router;
use url::Url;
use uuid::Uuid;
use backbone_bucket::auth::{AuthzDecision, AuthzPolicy, HasOwnerId};
use backbone_bucket::config::{ServingConfig, ServingMode};
use backbone_bucket::storage::{LocalStorage, ObjectStorage};
use backbone_bucket::{
BucketConfig, BucketError, BucketModule, RouterOptions, StorageConfig, StoredFile,
UploadConfig,
};
#[derive(Debug, Clone)]
struct ExampleUser {
user_id: Uuid,
}
impl HasOwnerId for ExampleUser {
fn owner_id(&self) -> Uuid {
self.user_id
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for ExampleUser {
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let header = parts
.headers
.get("x-user-id")
.ok_or_else(|| (StatusCode::UNAUTHORIZED, "missing x-user-id").into_response())?
.to_str()
.map_err(|_| (StatusCode::BAD_REQUEST, "bad x-user-id").into_response())?;
let user_id = Uuid::parse_str(header)
.map_err(|_| (StatusCode::BAD_REQUEST, "x-user-id not a uuid").into_response())?;
Ok(Self { user_id })
}
}
struct ExamplePolicy;
#[async_trait]
impl AuthzPolicy<ExampleUser> for ExamplePolicy {
async fn decide(
&self,
identity: &ExampleUser,
file: &StoredFile,
) -> Result<AuthzDecision, BucketError> {
if file.storage_key.starts_with("public/") {
return Ok(AuthzDecision::Allow);
}
if identity.user_id == file.owner_id {
Ok(AuthzDecision::Allow)
} else {
Ok(AuthzDecision::Deny)
}
}
}
#[allow(dead_code)]
fn build_app(pool: sqlx::PgPool) -> anyhow::Result<Router> {
let storage: Arc<dyn ObjectStorage> = Arc::new(LocalStorage::new(
"/tmp/bucket-example",
Url::parse("http://localhost:8080/cdn/")?,
b"dev-signing-secret".to_vec(),
));
let config = BucketConfig {
enabled: true,
storage: StorageConfig::Local {
root: "/tmp/bucket-example".into(),
base_url: Url::parse("http://localhost:8080/cdn/")?,
signing_secret_env: "BUCKET_LOCAL_SIGNING_SECRET".into(),
},
serving: ServingConfig {
default_mode: ServingMode::Redirect,
public_prefix: "public/".into(),
presigned_ttl: Duration::from_secs(300),
},
};
let module = BucketModule::builder()
.with_database(pool)
.with_config(config)
.with_storage(storage)
.build()?;
let serving = module.serving_router::<ExampleUser>(Arc::new(ExamplePolicy))?;
let uploads = module.upload_router::<ExampleUser>(UploadConfig::default())?;
Ok(Router::new()
.merge(module.crud_router())
.merge(uploads)
.nest("/cdn", serving))
}
#[allow(dead_code)]
fn build_app_merged(pool: sqlx::PgPool) -> anyhow::Result<Router> {
let storage: Arc<dyn ObjectStorage> = Arc::new(LocalStorage::new(
"/tmp/bucket-example",
Url::parse("http://localhost:8080/cdn/")?,
b"dev-signing-secret".to_vec(),
));
let module = BucketModule::builder()
.with_database(pool)
.with_config(BucketConfig::default())
.with_storage(storage)
.build()?;
let opts = RouterOptions::new(Arc::new(ExamplePolicy));
Ok(module.router::<ExampleUser>(opts)?)
}
fn main() {
eprintln!("backbone-bucket serving example — see `build_app` for wiring.");
}