1use std::collections::BTreeSet;
4use std::sync::{Arc, Mutex, MutexGuard};
5
6pub const MIN_CHUNK_SIZE: u64 = 5 * 1024 * 1024;
8
9pub const MAX_CHUNK_SIZE: u64 = 5 * 1024 * 1024 * 1024;
11
12pub const DEFAULT_CHUNK_SIZE: u64 = 10 * 1024 * 1024;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct ChunkBoundary {
18 pub index: u32,
20 pub start: u64,
22 pub end: u64,
24}
25
26impl ChunkBoundary {
27 pub const fn new(index: u32, start: u64, end: u64) -> Self {
29 Self { index, start, end }
30 }
31
32 pub const fn len(&self) -> u64 {
34 self.end.saturating_sub(self.start)
35 }
36
37 pub const fn is_empty(&self) -> bool {
39 self.start >= self.end
40 }
41}
42
43#[derive(Clone, Debug)]
48pub struct UploadSession {
49 user_id: String,
50 upload_id: String,
51 destination_path: String,
52 total_size: u64,
53 uploaded_chunks: Arc<Mutex<BTreeSet<u32>>>,
54}
55
56impl UploadSession {
57 pub const MIN_CHUNK_SIZE: u64 = MIN_CHUNK_SIZE;
59 pub const MAX_CHUNK_SIZE: u64 = MAX_CHUNK_SIZE;
61 pub const DEFAULT_CHUNK_SIZE: u64 = DEFAULT_CHUNK_SIZE;
63
64 pub fn new(
66 user_id: impl Into<String>,
67 upload_id: impl Into<String>,
68 destination_path: impl Into<String>,
69 total_size: u64,
70 ) -> Self {
71 Self {
72 user_id: user_id.into(),
73 upload_id: upload_id.into(),
74 destination_path: destination_path.into(),
75 total_size,
76 uploaded_chunks: Arc::new(Mutex::new(BTreeSet::new())),
77 }
78 }
79
80 pub fn user_id(&self) -> &str {
82 &self.user_id
83 }
84
85 pub fn upload_id(&self) -> &str {
87 &self.upload_id
88 }
89
90 pub fn destination_path(&self) -> &str {
92 &self.destination_path
93 }
94
95 pub const fn total_size(&self) -> u64 {
97 self.total_size
98 }
99
100 pub fn mark_chunk_uploaded(&self, chunk_index: u32) {
102 self.lock_chunks().insert(chunk_index);
103 }
104
105 pub fn is_chunk_uploaded(&self, chunk_index: u32) -> bool {
107 self.lock_chunks().contains(&chunk_index)
108 }
109
110 pub fn uploaded_chunks(&self) -> BTreeSet<u32> {
112 self.lock_chunks().clone()
113 }
114
115 pub fn uploaded_chunk_count(&self) -> usize {
117 self.lock_chunks().len()
118 }
119
120 pub fn expected_chunk_count(&self, chunk_size: u64) -> Option<u32> {
124 if chunk_size == 0 {
125 return None;
126 }
127 if self.total_size == 0 {
128 return Some(1);
129 }
130 let count = self.total_size.div_ceil(chunk_size);
131 u32::try_from(count).ok()
132 }
133
134 pub fn is_complete(&self, chunk_size: u64) -> bool {
136 let Some(expected) = self.expected_chunk_count(chunk_size) else {
137 return false;
138 };
139 let uploaded = self.lock_chunks();
140 if uploaded.len() != expected as usize {
141 return false;
142 }
143 (1..=expected).all(|i| uploaded.contains(&i))
144 }
145
146 pub fn chunk_boundary(&self, chunk_index: u32, chunk_size: u64) -> Option<ChunkBoundary> {
150 if chunk_size == 0 || chunk_index == 0 {
151 return None;
152 }
153 let total_chunks = self.expected_chunk_count(chunk_size)?;
154 if chunk_index > total_chunks {
155 return None;
156 }
157 let offset = u64::from(chunk_index - 1) * chunk_size;
158 let end = (offset + chunk_size).min(self.total_size);
159 Some(ChunkBoundary::new(chunk_index, offset, end))
160 }
161
162 pub fn chunk_boundaries(&self, chunk_size: u64) -> Option<Vec<ChunkBoundary>> {
166 let total_chunks = self.expected_chunk_count(chunk_size)?;
167 let mut boundaries = Vec::with_capacity(total_chunks as usize);
168 for index in 1..=total_chunks {
169 if let Some(boundary) = self.chunk_boundary(index, chunk_size) {
170 boundaries.push(boundary);
171 }
172 }
173 Some(boundaries)
174 }
175
176 fn lock_chunks(&self) -> MutexGuard<'_, BTreeSet<u32>> {
177 match self.uploaded_chunks.lock() {
178 Ok(guard) => guard,
179 Err(poisoned) => poisoned.into_inner(),
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn boundary_calculations_exact_multiple() {
190 let session = UploadSession::new("alice", "up-1", "/path", 30 * 1024 * 1024);
191 let chunk_size = 10 * 1024 * 1024;
192 assert_eq!(session.expected_chunk_count(chunk_size), Some(3));
193
194 let boundaries = session.chunk_boundaries(chunk_size).unwrap();
195 assert_eq!(boundaries.len(), 3);
196
197 assert_eq!(boundaries[0].index, 1);
198 assert_eq!(boundaries[0].start, 0);
199 assert_eq!(boundaries[0].end, 10 * 1024 * 1024);
200 assert_eq!(boundaries[0].len(), 10 * 1024 * 1024);
201 assert!(!boundaries[0].is_empty());
202
203 assert_eq!(boundaries[1].index, 2);
204 assert_eq!(boundaries[1].start, 10 * 1024 * 1024);
205 assert_eq!(boundaries[1].end, 20 * 1024 * 1024);
206
207 assert_eq!(boundaries[2].index, 3);
208 assert_eq!(boundaries[2].start, 20 * 1024 * 1024);
209 assert_eq!(boundaries[2].end, 30 * 1024 * 1024);
210 }
211
212 #[test]
213 fn boundary_calculations_with_remainder() {
214 let session = UploadSession::new("alice", "up-1", "/path", 25 * 1024 * 1024);
215 let chunk_size = 10 * 1024 * 1024;
216 assert_eq!(session.expected_chunk_count(chunk_size), Some(3));
217
218 let boundaries = session.chunk_boundaries(chunk_size).unwrap();
219 assert_eq!(boundaries.len(), 3);
220
221 assert_eq!(boundaries[2].index, 3);
222 assert_eq!(boundaries[2].start, 20 * 1024 * 1024);
223 assert_eq!(boundaries[2].end, 25 * 1024 * 1024);
224 assert_eq!(boundaries[2].len(), 5 * 1024 * 1024);
225 }
226
227 #[test]
228 fn zero_size_and_invalid_chunk_size() {
229 let session = UploadSession::new("alice", "up-1", "/path", 0);
230 assert_eq!(session.expected_chunk_count(1024), Some(1));
231 assert_eq!(
232 session.chunk_boundary(1, 1024),
233 Some(ChunkBoundary::new(1, 0, 0))
234 );
235 assert!(session.chunk_boundary(1, 1024).unwrap().is_empty());
236
237 assert_eq!(session.expected_chunk_count(0), None);
238 assert_eq!(session.chunk_boundary(1, 0), None);
239 assert_eq!(session.chunk_boundaries(0), None);
240
241 assert_eq!(session.chunk_boundary(0, 1024), None);
242 assert_eq!(session.chunk_boundary(2, 1024), None);
243 }
244
245 #[test]
246 fn chunk_tracking_and_completion() {
247 let session = UploadSession::new("alice", "up-1", "/path", 20 * 1024 * 1024);
248 let chunk_size = 10 * 1024 * 1024;
249
250 assert_eq!(session.uploaded_chunk_count(), 0);
251 assert!(!session.is_chunk_uploaded(1));
252 assert!(!session.is_complete(chunk_size));
253
254 session.mark_chunk_uploaded(1);
255 assert_eq!(session.uploaded_chunk_count(), 1);
256 assert!(session.is_chunk_uploaded(1));
257 assert!(!session.is_complete(chunk_size));
258
259 session.mark_chunk_uploaded(2);
260 assert_eq!(session.uploaded_chunk_count(), 2);
261 assert!(session.is_chunk_uploaded(2));
262 assert!(session.is_complete(chunk_size));
263
264 let uploaded = session.uploaded_chunks();
265 assert_eq!(uploaded.into_iter().collect::<Vec<_>>(), vec![1, 2]);
266 }
267
268 #[test]
269 fn cloned_session_shares_upload_progress() {
270 let session = UploadSession::new("alice", "up-1", "/path", 30 * 1024 * 1024);
271 let clone = session.clone();
272
273 session.mark_chunk_uploaded(1);
274 assert!(clone.is_chunk_uploaded(1));
275 assert_eq!(clone.uploaded_chunk_count(), 1);
276
277 clone.mark_chunk_uploaded(2);
278 assert!(session.is_chunk_uploaded(2));
279 assert_eq!(session.uploaded_chunk_count(), 2);
280 }
281}