Skip to main content

elif_storage/
upload.rs

1//! File upload utilities
2
3use crate::{StorageResult, StorageError};
4use bytes::Bytes;
5use futures::Stream;
6use std::pin::Pin;
7use std::task::{Context, Poll};
8
9/// File upload stream wrapper
10pub struct UploadStream<S> {
11    inner: S,
12    bytes_read: u64,
13    max_size: Option<u64>,
14}
15
16impl<S> UploadStream<S>
17where
18    S: Stream<Item = Result<Bytes, std::io::Error>>,
19{
20    /// Create a new upload stream with optional size limit
21    pub fn new(stream: S, max_size: Option<u64>) -> Self {
22        Self {
23            inner: stream,
24            bytes_read: 0,
25            max_size,
26        }
27    }
28    
29    /// Get the total bytes read so far
30    pub fn bytes_read(&self) -> u64 {
31        self.bytes_read
32    }
33}
34
35impl<S> Stream for UploadStream<S>
36where
37    S: Stream<Item = Result<Bytes, std::io::Error>> + Unpin,
38{
39    type Item = Result<Bytes, std::io::Error>;
40    
41    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
42        match Pin::new(&mut self.inner).poll_next(cx) {
43            Poll::Ready(Some(Ok(chunk))) => {
44                self.bytes_read += chunk.len() as u64;
45                
46                // Check size limit
47                if let Some(max_size) = self.max_size {
48                    if self.bytes_read > max_size {
49                        return Poll::Ready(Some(Err(std::io::Error::new(
50                            std::io::ErrorKind::InvalidData,
51                            format!("Upload size {} exceeds maximum {}", self.bytes_read, max_size),
52                        ))));
53                    }
54                }
55                
56                Poll::Ready(Some(Ok(chunk)))
57            }
58            other => other,
59        }
60    }
61}
62
63/// Multipart upload manager for large files
64pub struct MultipartUpload {
65    upload_id: String,
66    parts: Vec<UploadPart>,
67    current_part: u32,
68}
69
70/// Upload part information
71#[derive(Debug, Clone)]
72pub struct UploadPart {
73    pub part_number: u32,
74    pub etag: String,
75    pub size: u64,
76}
77
78impl MultipartUpload {
79    /// Create a new multipart upload
80    pub fn new(upload_id: String) -> Self {
81        Self {
82            upload_id,
83            parts: Vec::new(),
84            current_part: 1,
85        }
86    }
87    
88    /// Get the upload ID
89    pub fn upload_id(&self) -> &str {
90        &self.upload_id
91    }
92    
93    /// Add a completed part
94    pub fn add_part(&mut self, etag: String, size: u64) {
95        self.parts.push(UploadPart {
96            part_number: self.current_part,
97            etag,
98            size,
99        });
100        self.current_part += 1;
101    }
102    
103    /// Get all parts
104    pub fn parts(&self) -> &[UploadPart] {
105        &self.parts
106    }
107    
108    /// Get total size of uploaded parts
109    pub fn total_size(&self) -> u64 {
110        self.parts.iter().map(|p| p.size).sum()
111    }
112    
113    /// Check if upload is complete (has at least one part)
114    pub fn is_complete(&self) -> bool {
115        !self.parts.is_empty()
116    }
117}
118
119/// Upload progress callback
120pub type ProgressCallback = Box<dyn Fn(u64, u64) + Send + Sync>;
121
122/// Upload configuration
123pub struct UploadConfig {
124    /// Chunk size for streaming uploads (default: 8MB)
125    pub chunk_size: usize,
126    
127    /// Enable progress reporting
128    pub report_progress: bool,
129    
130    /// Progress callback
131    pub progress_callback: Option<Box<dyn Fn(u64, u64) + Send + Sync>>,
132    
133    /// Maximum concurrent uploads for multipart
134    pub max_concurrent_parts: usize,
135    
136    /// Minimum part size for multipart uploads (default: 5MB)
137    pub min_part_size: u64,
138}
139
140impl std::fmt::Debug for UploadConfig {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("UploadConfig")
143            .field("chunk_size", &self.chunk_size)
144            .field("report_progress", &self.report_progress)
145            .field("progress_callback", &self.progress_callback.as_ref().map(|_| "Some(callback)"))
146            .field("max_concurrent_parts", &self.max_concurrent_parts)
147            .field("min_part_size", &self.min_part_size)
148            .finish()
149    }
150}
151
152impl Default for UploadConfig {
153    fn default() -> Self {
154        Self {
155            chunk_size: 8 * 1024 * 1024, // 8MB
156            report_progress: false,
157            progress_callback: None,
158            max_concurrent_parts: 4,
159            min_part_size: 5 * 1024 * 1024, // 5MB
160        }
161    }
162}
163
164impl UploadConfig {
165    /// Create new upload configuration
166    pub fn new() -> Self {
167        Self::default()
168    }
169    
170    /// Set chunk size
171    pub fn with_chunk_size(mut self, size: usize) -> Self {
172        self.chunk_size = size;
173        self
174    }
175    
176    /// Enable progress reporting
177    pub fn with_progress<F>(mut self, callback: F) -> Self
178    where
179        F: Fn(u64, u64) + Send + Sync + 'static,
180    {
181        self.report_progress = true;
182        self.progress_callback = Some(Box::new(callback));
183        self
184    }
185    
186    /// Set maximum concurrent parts for multipart uploads
187    pub fn with_max_concurrent_parts(mut self, max: usize) -> Self {
188        self.max_concurrent_parts = max;
189        self
190    }
191    
192    /// Set minimum part size for multipart uploads
193    pub fn with_min_part_size(mut self, size: u64) -> Self {
194        self.min_part_size = size;
195        self
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use futures::{stream, StreamExt};
203    use std::io;
204    
205    #[tokio::test]
206    async fn test_upload_stream() {
207        let data = vec![
208            Ok(Bytes::from("hello")),
209            Ok(Bytes::from(" ")),
210            Ok(Bytes::from("world")),
211        ];
212        
213        let stream = stream::iter(data);
214        let mut upload_stream = UploadStream::new(stream, Some(20));
215        
216        let mut result = Vec::new();
217        while let Some(chunk) = upload_stream.next().await {
218            result.push(chunk.unwrap());
219        }
220        
221        let combined: Bytes = result.into_iter().collect::<Vec<_>>().concat().into();
222        assert_eq!(combined, Bytes::from("hello world"));
223        assert_eq!(upload_stream.bytes_read(), 11);
224    }
225    
226    #[tokio::test]
227    async fn test_upload_stream_size_limit() {
228        let data = vec![
229            Ok(Bytes::from("hello")),
230            Ok(Bytes::from(" world")),
231        ];
232        
233        let stream = stream::iter(data);
234        let mut upload_stream = UploadStream::new(stream, Some(5)); // Limit to 5 bytes
235        
236        // First chunk should succeed
237        let chunk1 = upload_stream.next().await.unwrap().unwrap();
238        assert_eq!(chunk1, Bytes::from("hello"));
239        
240        // Second chunk should fail due to size limit
241        let chunk2 = upload_stream.next().await.unwrap();
242        assert!(chunk2.is_err());
243        assert_eq!(chunk2.unwrap_err().kind(), io::ErrorKind::InvalidData);
244    }
245    
246    #[test]
247    fn test_multipart_upload() {
248        let mut upload = MultipartUpload::new("test-upload-123".to_string());
249        
250        assert_eq!(upload.upload_id(), "test-upload-123");
251        assert!(!upload.is_complete());
252        assert_eq!(upload.total_size(), 0);
253        
254        upload.add_part("etag1".to_string(), 1000);
255        upload.add_part("etag2".to_string(), 2000);
256        
257        assert!(upload.is_complete());
258        assert_eq!(upload.total_size(), 3000);
259        assert_eq!(upload.parts().len(), 2);
260        
261        let part1 = &upload.parts()[0];
262        assert_eq!(part1.part_number, 1);
263        assert_eq!(part1.etag, "etag1");
264        assert_eq!(part1.size, 1000);
265    }
266    
267    #[test]
268    fn test_upload_config() {
269        let config = UploadConfig::new()
270            .with_chunk_size(16 * 1024 * 1024)
271            .with_max_concurrent_parts(8)
272            .with_min_part_size(10 * 1024 * 1024);
273        
274        assert_eq!(config.chunk_size, 16 * 1024 * 1024);
275        assert_eq!(config.max_concurrent_parts, 8);
276        assert_eq!(config.min_part_size, 10 * 1024 * 1024);
277        assert!(!config.report_progress);
278    }
279}