Skip to main content

fileloft_core/handler/
mod.rs

1mod delete;
2mod head;
3mod options;
4mod patch;
5mod post;
6
7use std::sync::Arc;
8
9use bytes::Bytes;
10use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri};
11use tokio::sync::broadcast;
12
13use crate::{
14    config::Config,
15    error::TusError,
16    hooks::{HookEvent, HookSender},
17    lock::SendLocker,
18    proto::{HDR_CACHE_CONTROL, HDR_TUS_RESUMABLE, TUS_VERSION},
19    store::SendDataStore,
20    util::static_header,
21};
22
23/// Incoming request as seen by the tus handler.
24/// Framework integrations construct this from their native request type.
25pub struct TusRequest {
26    pub method: Method,
27    pub uri: Uri,
28    /// Upload ID extracted from the URL path by the framework router.
29    /// Present for HEAD / PATCH / DELETE; absent for OPTIONS and POST.
30    pub upload_id: Option<String>,
31    pub headers: HeaderMap,
32    /// Streaming body. `None` for HEAD / DELETE / OPTIONS.
33    pub body: Option<Box<dyn tokio::io::AsyncRead + Send + Sync + Unpin>>,
34}
35
36/// Outgoing response produced by the tus handler.
37/// Framework integrations convert this into their native response type.
38pub struct TusResponse {
39    pub status: StatusCode,
40    pub headers: HeaderMap,
41    /// Body bytes — always small for tus (empty or short error text).
42    pub body: Bytes,
43}
44
45/// The central tus protocol handler.
46///
47/// Wrap in `Arc<TusHandler<S, L>>` and share across request-handling tasks.
48///
49/// # Type Parameters
50/// - `S`: Storage backend implementing [`SendDataStore`].
51/// - `L`: Optional locker implementing [`SendLocker`]. Use `NoLocker` if concurrency
52///   control is handled by the store itself or is not needed.
53pub struct TusHandler<S, L = NoLocker> {
54    pub(crate) store: S,
55    pub(crate) locker: Option<L>,
56    pub(crate) config: Arc<Config>,
57    pub(crate) hook_tx: Option<HookSender>,
58}
59
60impl<S, L> TusHandler<S, L>
61where
62    S: SendDataStore + Send + Sync + 'static,
63    L: SendLocker + Send + Sync + 'static,
64{
65    pub fn new(store: S, locker: Option<L>, config: Config) -> Self {
66        let hook_tx = if config.hooks.channel_capacity > 0 {
67            let (tx, _) = broadcast::channel(config.hooks.channel_capacity);
68            Some(tx)
69        } else {
70            None
71        };
72        Self {
73            store,
74            locker,
75            config: Arc::new(config),
76            hook_tx,
77        }
78    }
79
80    /// Subscribe to lifecycle events. Returns `None` if hooks are not configured.
81    pub fn hook_receiver(&self) -> Option<broadcast::Receiver<HookEvent>> {
82        self.hook_tx.as_ref().map(|tx| tx.subscribe())
83    }
84
85    /// Main dispatch — routes to the appropriate sub-handler.
86    pub async fn handle(&self, req: TusRequest) -> TusResponse {
87        let result = match req.method {
88            Method::OPTIONS => options::handle(self, &req).await,
89            Method::HEAD => head::handle(self, &req).await,
90            Method::POST => post::handle(self, req).await,
91            Method::PATCH => patch::handle(self, req).await,
92            Method::DELETE => delete::handle(self, &req).await,
93            _ => Err(TusError::MethodNotAllowed),
94        };
95        match result {
96            Ok(resp) => resp,
97            Err(err) => self.error_response(err),
98        }
99    }
100
101    /// Build a response with common tus headers added.
102    pub(crate) fn response(
103        &self,
104        status: StatusCode,
105        extra_headers: HeaderMap,
106        body: Bytes,
107    ) -> TusResponse {
108        let mut headers = self.base_headers();
109        headers.extend(extra_headers);
110        TusResponse { status, headers, body }
111    }
112
113    /// Build an error response from a `TusError`.
114    pub(crate) fn error_response(&self, err: TusError) -> TusResponse {
115        let status = err.status_code();
116        let body = Bytes::from(err.to_string());
117        let mut headers = self.base_headers();
118        headers.insert(
119            http::header::CONTENT_TYPE,
120            HeaderValue::from_static("text/plain; charset=utf-8"),
121        );
122        TusResponse { status, headers, body }
123    }
124
125    /// Headers added to every response.
126    fn base_headers(&self) -> HeaderMap {
127        let mut h = HeaderMap::new();
128        h.insert(HDR_TUS_RESUMABLE, static_header(TUS_VERSION));
129        h.insert(HDR_CACHE_CONTROL, static_header("no-store"));
130        if self.config.enable_cors {
131            h.insert(
132                crate::proto::HDR_ACCESS_CONTROL_ALLOW_ORIGIN,
133                static_header("*"),
134            );
135            h.insert(
136                crate::proto::HDR_ACCESS_CONTROL_EXPOSE_HEADERS,
137                static_header(
138                    "Upload-Offset,Upload-Length,Upload-Metadata,Upload-Expires,\
139                     Upload-Defer-Length,Location,Tus-Resumable,Tus-Version,Tus-Extension,\
140                     Tus-Max-Size,Tus-Checksum-Algorithm",
141                ),
142            );
143        }
144        h
145    }
146
147    /// Emit a hook event (non-blocking; missed if no subscriber).
148    pub(crate) fn emit(&self, event: HookEvent) {
149        if let Some(tx) = &self.hook_tx {
150            let _ = tx.send(event);
151        }
152    }
153}
154
155/// A no-op locker used when the caller passes `None` for the locker type.
156pub struct NoLocker;
157
158impl crate::lock::SendLock for NoLocker {
159    async fn release(self) -> Result<(), TusError> {
160        Ok(())
161    }
162}
163
164impl crate::lock::SendLocker for NoLocker {
165    type LockType = NoLocker;
166    async fn acquire(&self, _id: &crate::info::UploadId) -> Result<NoLocker, TusError> {
167        Ok(NoLocker)
168    }
169}