boatramp_server/
limits.rs1use std::sync::Arc;
10use std::time::Duration;
11
12use boatramp_core::{ByteStream, StorageError};
13use futures::StreamExt;
14use tokio::sync::{OwnedSemaphorePermit, Semaphore};
15
16#[derive(Debug, Clone, Default)]
20pub struct ServerLimits {
21 pub max_upload_bytes: Option<u64>,
25 pub upload_idle_timeout: Option<Duration>,
28 pub max_concurrent_uploads: Option<usize>,
31}
32
33impl ServerLimits {
34 pub fn is_unlimited(&self) -> bool {
36 self.max_upload_bytes.is_none()
37 && self.upload_idle_timeout.is_none()
38 && self.max_concurrent_uploads.is_none()
39 }
40}
41
42#[derive(Clone)]
45pub struct UploadGuard {
46 max_upload_bytes: Option<u64>,
47 upload_idle_timeout: Option<Duration>,
48 uploads: Option<Arc<Semaphore>>,
49}
50
51impl UploadGuard {
52 pub fn new(limits: ServerLimits) -> Self {
54 Self {
55 max_upload_bytes: limits.max_upload_bytes,
56 upload_idle_timeout: limits.upload_idle_timeout,
57 uploads: limits
58 .max_concurrent_uploads
59 .map(|n| Arc::new(Semaphore::new(n.max(1)))),
60 }
61 }
62
63 pub fn try_acquire(&self) -> Option<UploadPermit> {
67 match &self.uploads {
68 None => Some(UploadPermit(None)),
69 Some(sem) => sem
70 .clone()
71 .try_acquire_owned()
72 .ok()
73 .map(|permit| UploadPermit(Some(permit))),
74 }
75 }
76
77 pub fn content_length_rejected(&self, content_length: Option<u64>) -> bool {
79 matches!((self.max_upload_bytes, content_length), (Some(max), Some(len)) if len > max)
80 }
81
82 pub fn limit_body(&self, stream: ByteStream) -> ByteStream {
86 limited_stream(stream, self.max_upload_bytes, self.upload_idle_timeout)
87 }
88}
89
90pub struct UploadPermit(#[allow(dead_code)] Option<OwnedSemaphorePermit>);
93
94fn limited_stream(inner: ByteStream, max: Option<u64>, idle: Option<Duration>) -> ByteStream {
97 if max.is_none() && idle.is_none() {
98 return inner;
99 }
100 futures::stream::unfold(
101 (inner, 0u64, false),
102 move |(mut inner, sent, done)| async move {
103 if done {
104 return None;
105 }
106 let next = match idle {
107 Some(timeout) => match tokio::time::timeout(timeout, inner.next()).await {
108 Ok(item) => item,
109 Err(_) => {
110 return Some((
111 Err(StorageError::backend("upload idle timeout")),
112 (inner, sent, true),
113 ))
114 }
115 },
116 None => inner.next().await,
117 };
118 match next {
119 None => None,
120 Some(Err(err)) => Some((Err(err), (inner, sent, true))),
121 Some(Ok(chunk)) => {
122 let sent = sent + chunk.len() as u64;
123 if let Some(max) = max {
124 if sent > max {
125 return Some((
126 Err(StorageError::backend(format!(
127 "upload exceeds the {max}-byte limit"
128 ))),
129 (inner, sent, true),
130 ));
131 }
132 }
133 Some((Ok(chunk), (inner, sent, false)))
134 }
135 }
136 },
137 )
138 .boxed()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use bytes::Bytes;
145
146 fn stream_of(chunks: Vec<&'static [u8]>) -> ByteStream {
147 futures::stream::iter(chunks.into_iter().map(|c| Ok(Bytes::from_static(c)))).boxed()
148 }
149
150 async fn drain(mut s: ByteStream) -> Result<u64, StorageError> {
151 let mut total = 0u64;
152 while let Some(item) = s.next().await {
153 total += item?.len() as u64;
154 }
155 Ok(total)
156 }
157
158 #[tokio::test]
159 async fn under_cap_passes_through() {
160 let s = limited_stream(stream_of(vec![b"abc", b"de"]), Some(10), None);
161 assert_eq!(drain(s).await.unwrap(), 5);
162 }
163
164 #[tokio::test]
165 async fn over_cap_aborts() {
166 let s = limited_stream(stream_of(vec![b"abc", b"defgh", b"more"]), Some(6), None);
167 let err = drain(s).await.unwrap_err();
168 assert!(err.to_string().contains("6-byte limit"), "{err}");
169 }
170
171 #[test]
172 fn content_length_early_reject() {
173 let guard = UploadGuard::new(ServerLimits {
174 max_upload_bytes: Some(100),
175 ..Default::default()
176 });
177 assert!(guard.content_length_rejected(Some(101)));
178 assert!(!guard.content_length_rejected(Some(100)));
179 assert!(!guard.content_length_rejected(None));
180 }
181
182 #[test]
183 fn concurrency_cap_admits_then_saturates() {
184 let guard = UploadGuard::new(ServerLimits {
185 max_concurrent_uploads: Some(1),
186 ..Default::default()
187 });
188 let permit = guard.try_acquire().expect("first admitted");
189 assert!(guard.try_acquire().is_none(), "second rejected while held");
190 drop(permit);
191 assert!(guard.try_acquire().is_some(), "slot freed after drop");
192 }
193
194 #[test]
195 fn unlimited_always_admits() {
196 let guard = UploadGuard::new(ServerLimits::default());
197 assert!(guard.try_acquire().is_some());
198 assert!(guard.try_acquire().is_some());
199 }
200}