Skip to main content

backbone_bucket/
bucket_module.rs

1//! Custom BucketModule extensions — Phase 6: Child Entity Collapse
2//!
3//! This file is NEVER touched by backbone-schema generators.
4//! It adds http_routes() to BucketModule, which returns only the 10 active
5//! CRUD stacks (Thumbnail, FileVersion, and AccessLog are intentionally excluded
6//! as they are child entities with no independent lifecycle).
7//!
8//! The generated routes() in lib.rs is always regenerated with all 13 entities.
9//! The app MUST call http_routes() instead.
10
11use std::sync::Arc;
12use axum::{response::IntoResponse, Router};
13
14use crate::auth::{ArcAuthzPolicy, AuthExtractor, HasOwnerId};
15use crate::error::{BucketError, BucketResult};
16use crate::presentation::http::{
17    create_bucket_routes,
18    create_content_hash_routes,
19    create_conversion_job_routes,
20    create_file_comment_routes,
21    create_file_lock_routes,
22    create_file_share_routes,
23    create_processing_job_routes,
24    create_stored_file_routes,
25    create_upload_session_routes,
26    create_user_quota_routes,
27    serving_router as build_serving_router,
28    upload_router as build_upload_router,
29    ServingContext,
30    UploadConfig,
31    UploadContext,
32};
33use crate::BucketModule;
34
35impl BucketModule {
36    /// HTTP routes for the 10 active CRUD stacks.
37    ///
38    /// Excludes Thumbnail, FileVersion, AccessLog — child entities managed through
39    /// their parent (StoredFile/Bucket) and not exposed as independent endpoints.
40    pub fn http_routes(&self) -> Router {
41        Router::new()
42            .merge(create_bucket_routes(self.bucket_service.clone()))
43            .merge(create_content_hash_routes(self.content_hash_service.clone()))
44            .merge(create_conversion_job_routes(self.conversion_job_service.clone()))
45            .merge(create_file_comment_routes(self.file_comment_service.clone()))
46            .merge(create_file_lock_routes(self.file_lock_service.clone()))
47            .merge(create_file_share_routes(self.file_share_service.clone()))
48            .merge(create_processing_job_routes(self.processing_job_service.clone()))
49            .merge(create_stored_file_routes(self.stored_file_service.clone()))
50            .merge(create_upload_session_routes(self.upload_session_service.clone()))
51            .merge(create_user_quota_routes(self.user_quota_service.clone()))
52    }
53
54    /// The 10 generated CRUD routers, composed.
55    ///
56    /// Alias for [`Self::http_routes`]. Returns the existing
57    /// `/api/v1/bucket/*` router — unchanged from before the serving
58    /// handler was added. Named to pair symmetrically with
59    /// [`Self::serving_router`].
60    pub fn crud_router(&self) -> Router {
61        self.http_routes()
62    }
63
64    /// Build the mode-B serving router.
65    ///
66    /// `A` is the consumer's [`AuthExtractor`] — and, since Axum
67    /// extractors yield themselves, also the identity type the
68    /// [`crate::auth::AuthzPolicy`] decides on.
69    ///
70    /// Returns `Err` when `.with_storage()` / `.with_config()` were not
71    /// called on the builder — the serving handler can't operate without
72    /// both.
73    pub fn serving_router<A>(&self, authz: ArcAuthzPolicy<A>) -> BucketResult<Router>
74    where
75        A: AuthExtractor<()> + HasOwnerId + Clone + 'static,
76        A::Rejection: IntoResponse,
77    {
78        let storage = self
79            .storage
80            .clone()
81            .ok_or_else(|| BucketError::Config("storage backend not configured".into()))?;
82        let config = self
83            .bucket_config
84            .clone()
85            .ok_or_else(|| BucketError::Config("bucket config not provided".into()))?;
86        let ctx = ServingContext::<A> {
87            storage,
88            file_repo: self.stored_file_repository.clone(),
89            authz,
90            config,
91        };
92        Ok(build_serving_router::<A>(ctx))
93    }
94
95    /// Build the multipart upload router.
96    ///
97    /// Mounts five routes (see [`crate::presentation::http::upload`]):
98    /// single-shot `POST /uploads`, and the resumable session lifecycle
99    /// under `/uploads/sessions`. The consumer's [`AuthExtractor`] `A`
100    /// must also implement [`HasOwnerId`] so `owner_id` is derived from
101    /// the authenticated identity instead of being trusted from input.
102    ///
103    /// Returns `Err` when `.with_storage()` / `.with_config()` were not
104    /// configured — uploads cannot run without an [`ObjectStorage`]
105    /// backend and the wired-up [`crate::FileService`].
106    pub fn upload_router<A>(&self, config: UploadConfig) -> BucketResult<Router>
107    where
108        A: AuthExtractor<()> + HasOwnerId + Clone + 'static,
109        A::Rejection: IntoResponse,
110    {
111        let storage = self
112            .storage
113            .clone()
114            .ok_or_else(|| BucketError::Config("storage backend not configured".into()))?;
115        let file_service = self
116            .file_service
117            .clone()
118            .ok_or_else(|| BucketError::Config("file service not configured (call .with_storage() and .with_config())".into()))?;
119        let ctx = UploadContext {
120            file_service,
121            multipart_service: self.multipart_upload_service.clone(),
122            bucket_service: self.bucket_service.clone(),
123            storage,
124        };
125        Ok(build_upload_router::<A>(ctx, config))
126    }
127
128    /// One-call composition: CRUD + upload + serving routers merged.
129    ///
130    /// Pick this when the consumer just wants "everything the module
131    /// exposes" under a single nest point. Each sub-router is still
132    /// reachable individually via [`Self::crud_router`] /
133    /// [`Self::upload_router`] / [`Self::serving_router`] for advanced
134    /// composition.
135    ///
136    /// The merged router pairs CRUD endpoints with the upload surface
137    /// at the same prefix the consumer chooses (e.g. `/api/v1/bucket`),
138    /// and nests the serving handler under `serving_prefix` (default
139    /// `/cdn`). Passing `RouterOptions::default()` plus a configured
140    /// `BucketModule` is the smallest possible wiring:
141    ///
142    /// ```ignore
143    /// let app = Router::new()
144    ///     .nest("/api/v1/bucket", bucket.router::<MyUser>(opts)?);
145    /// ```
146    ///
147    /// Returns `Err` for the same reasons [`Self::serving_router`] and
148    /// [`Self::upload_router`] do — `.with_storage()` and
149    /// `.with_config()` are required on the builder.
150    pub fn router<A>(&self, opts: RouterOptions<A>) -> BucketResult<Router>
151    where
152        A: AuthExtractor<()> + HasOwnerId + Clone + 'static,
153        A::Rejection: IntoResponse,
154    {
155        let RouterOptions {
156            upload_config,
157            authz,
158            serving_prefix,
159        } = opts;
160        let serving = self.serving_router::<A>(authz)?;
161        let uploads = self.upload_router::<A>(upload_config)?;
162        Ok(self
163            .crud_router()
164            .merge(uploads)
165            .nest(&serving_prefix, serving))
166    }
167}
168
169/// Options for [`BucketModule::router`].
170///
171/// `authz` is the only required field — the policy decides whether an
172/// authenticated caller may read a given file. Defaults: stock
173/// [`UploadConfig`] (256 MiB single-shot / 16 MiB per chunk) and
174/// `/cdn` as the serving prefix.
175pub struct RouterOptions<A> {
176    pub upload_config: UploadConfig,
177    pub authz: ArcAuthzPolicy<A>,
178    pub serving_prefix: String,
179}
180
181impl<A> RouterOptions<A> {
182    /// Construct with the policy and module-default values.
183    pub fn new(authz: ArcAuthzPolicy<A>) -> Self {
184        Self {
185            upload_config: UploadConfig::default(),
186            authz,
187            serving_prefix: "/cdn".to_string(),
188        }
189    }
190
191    /// Override the single-shot / chunk body limits.
192    pub fn with_upload_config(mut self, cfg: UploadConfig) -> Self {
193        self.upload_config = cfg;
194        self
195    }
196
197    /// Override the serving-router mount path (default `/cdn`).
198    pub fn with_serving_prefix(mut self, prefix: impl Into<String>) -> Self {
199        self.serving_prefix = prefix.into();
200        self
201    }
202}