Skip to main content

scirs2_io/network/
streaming.rs

1//! Streaming I/O operations for efficient network data transfer
2//!
3//! This module provides streaming capabilities for handling large files over network
4//! connections with minimal memory usage. It supports chunked reading/writing,
5//! progress monitoring, and efficient buffering strategies.
6
7use crate::error::{IoError, Result};
8use std::io::{Read, Seek, SeekFrom, Write};
9use std::path::Path;
10
11/// Streaming configuration
12#[derive(Debug, Clone)]
13pub struct StreamConfig {
14    /// Buffer size for streaming operations
15    pub buffer_size: usize,
16    /// Maximum memory usage for buffering
17    pub max_memory: usize,
18    /// Enable compression during streaming
19    pub compression: bool,
20    /// Progress reporting interval (in bytes)
21    pub progress_interval: u64,
22}
23
24impl Default for StreamConfig {
25    fn default() -> Self {
26        Self {
27            buffer_size: 64 * 1024,       // 64KB chunks
28            max_memory: 16 * 1024 * 1024, // 16MB max buffer
29            compression: false,
30            progress_interval: 1024 * 1024, // Report every 1MB
31        }
32    }
33}
34
35/// Progress information for streaming operations
36#[derive(Debug, Clone)]
37pub struct StreamProgress {
38    /// Bytes transferred so far
39    pub bytes_transferred: u64,
40    /// Total bytes to transfer (if known)
41    pub total_bytes: Option<u64>,
42    /// Transfer rate in bytes per second
43    pub rate: f64,
44    /// Estimated time remaining (if total known)
45    pub eta_seconds: Option<f64>,
46}
47
48impl StreamProgress {
49    /// Calculate progress percentage (0-100)
50    pub fn percentage(&self) -> Option<f64> {
51        self.total_bytes.map(|total| {
52            if total > 0 {
53                (self.bytes_transferred as f64 / total as f64) * 100.0
54            } else {
55                0.0
56            }
57        })
58    }
59
60    /// Check if transfer is complete
61    pub fn is_complete(&self) -> bool {
62        if let Some(total) = self.total_bytes {
63            self.bytes_transferred >= total
64        } else {
65            false
66        }
67    }
68}
69
70/// Progress callback type
71pub type ProgressCallback = Box<dyn Fn(StreamProgress) + Send + Sync>;
72
73/// Stream reader with progress tracking
74pub struct ProgressReader<R: Read> {
75    inner: R,
76    bytes_read: u64,
77    total_bytes: Option<u64>,
78    progresscallback: Option<ProgressCallback>,
79    progress_interval: u64,
80    last_progress_report: u64,
81    start_time: std::time::Instant,
82}
83
84impl<R: Read> ProgressReader<R> {
85    /// Create a new progress reader
86    pub fn new(inner: R) -> Self {
87        Self {
88            inner,
89            bytes_read: 0,
90            total_bytes: None,
91            progresscallback: None,
92            progress_interval: 1024 * 1024, // 1MB
93            last_progress_report: 0,
94            start_time: std::time::Instant::now(),
95        }
96    }
97
98    /// Set total bytes for progress calculation
99    pub fn with_total_bytes(mut self, total: u64) -> Self {
100        self.total_bytes = Some(total);
101        self
102    }
103
104    /// Set progress callback
105    pub fn with_progresscallback(mut self, callback: ProgressCallback) -> Self {
106        self.progresscallback = Some(callback);
107        self
108    }
109
110    /// Set progress reporting interval
111    pub fn with_progress_interval(mut self, interval: u64) -> Self {
112        self.progress_interval = interval;
113        self
114    }
115
116    /// Get current progress
117    pub fn progress(&self) -> StreamProgress {
118        let elapsed = self.start_time.elapsed().as_secs_f64();
119        let rate = if elapsed > 0.0 {
120            self.bytes_read as f64 / elapsed
121        } else {
122            0.0
123        };
124
125        let eta_seconds = if let Some(total) = self.total_bytes {
126            if rate > 0.0 && self.bytes_read < total {
127                Some((total - self.bytes_read) as f64 / rate)
128            } else {
129                None
130            }
131        } else {
132            None
133        };
134
135        StreamProgress {
136            bytes_transferred: self.bytes_read,
137            total_bytes: self.total_bytes,
138            rate,
139            eta_seconds,
140        }
141    }
142
143    fn report_progress(&mut self) {
144        if let Some(ref callback) = self.progresscallback {
145            let progress = self.progress();
146            callback(progress);
147            self.last_progress_report = self.bytes_read;
148        }
149    }
150}
151
152impl<R: Read> Read for ProgressReader<R> {
153    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
154        let bytes_read = self.inner.read(buf)?;
155        self.bytes_read += bytes_read as u64;
156
157        // Report progress if interval reached
158        if self.bytes_read - self.last_progress_report >= self.progress_interval {
159            self.report_progress();
160        }
161
162        Ok(bytes_read)
163    }
164}
165
166/// Stream writer with progress tracking
167pub struct ProgressWriter<W: Write> {
168    inner: W,
169    bytes_written: u64,
170    total_bytes: Option<u64>,
171    progresscallback: Option<ProgressCallback>,
172    progress_interval: u64,
173    last_progress_report: u64,
174    start_time: std::time::Instant,
175}
176
177impl<W: Write> ProgressWriter<W> {
178    /// Create a new progress writer
179    pub fn new(inner: W) -> Self {
180        Self {
181            inner,
182            bytes_written: 0,
183            total_bytes: None,
184            progresscallback: None,
185            progress_interval: 1024 * 1024, // 1MB
186            last_progress_report: 0,
187            start_time: std::time::Instant::now(),
188        }
189    }
190
191    /// Set total bytes for progress calculation
192    pub fn with_total_bytes(mut self, total: u64) -> Self {
193        self.total_bytes = Some(total);
194        self
195    }
196
197    /// Set progress callback
198    pub fn with_progresscallback(mut self, callback: ProgressCallback) -> Self {
199        self.progresscallback = Some(callback);
200        self
201    }
202
203    /// Set progress reporting interval
204    pub fn with_progress_interval(mut self, interval: u64) -> Self {
205        self.progress_interval = interval;
206        self
207    }
208
209    /// Get current progress
210    pub fn progress(&self) -> StreamProgress {
211        let elapsed = self.start_time.elapsed().as_secs_f64();
212        let rate = if elapsed > 0.0 {
213            self.bytes_written as f64 / elapsed
214        } else {
215            0.0
216        };
217
218        let eta_seconds = if let Some(total) = self.total_bytes {
219            if rate > 0.0 && self.bytes_written < total {
220                Some((total - self.bytes_written) as f64 / rate)
221            } else {
222                None
223            }
224        } else {
225            None
226        };
227
228        StreamProgress {
229            bytes_transferred: self.bytes_written,
230            total_bytes: self.total_bytes,
231            rate,
232            eta_seconds,
233        }
234    }
235
236    fn report_progress(&mut self) {
237        if let Some(ref callback) = self.progresscallback {
238            let progress = self.progress();
239            callback(progress);
240            self.last_progress_report = self.bytes_written;
241        }
242    }
243}
244
245impl<W: Write> Write for ProgressWriter<W> {
246    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
247        let bytes_written = self.inner.write(buf)?;
248        self.bytes_written += bytes_written as u64;
249
250        // Report progress if interval reached
251        if self.bytes_written - self.last_progress_report >= self.progress_interval {
252            self.report_progress();
253        }
254
255        Ok(bytes_written)
256    }
257
258    fn flush(&mut self) -> std::io::Result<()> {
259        self.inner.flush()
260    }
261}
262
263/// Chunked file reader for streaming large files
264pub struct ChunkedReader {
265    file: std::fs::File,
266    chunk_size: usize,
267    current_position: u64,
268    file_size: u64,
269}
270
271impl ChunkedReader {
272    /// Create a new chunked reader
273    pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
274        let file = std::fs::File::open(path.as_ref())
275            .map_err(|e| IoError::FileError(format!("Failed to open file: {}", e)))?;
276
277        let file_size = file
278            .metadata()
279            .map_err(|e| IoError::FileError(format!("Failed to get file metadata: {}", e)))?
280            .len();
281
282        Ok(Self {
283            file,
284            chunk_size,
285            current_position: 0,
286            file_size,
287        })
288    }
289
290    /// Read the next chunk
291    pub fn read_chunk(&mut self) -> Result<Option<Vec<u8>>> {
292        if self.current_position >= self.file_size {
293            return Ok(None);
294        }
295
296        let mut buffer = vec![0u8; self.chunk_size];
297        let bytes_read = self
298            .file
299            .read(&mut buffer)
300            .map_err(|e| IoError::FileError(format!("Failed to read chunk: {}", e)))?;
301
302        if bytes_read == 0 {
303            return Ok(None);
304        }
305
306        buffer.truncate(bytes_read);
307        self.current_position += bytes_read as u64;
308
309        Ok(Some(buffer))
310    }
311
312    /// Seek to a specific position
313    pub fn seek(&mut self, position: u64) -> Result<()> {
314        self.file
315            .seek(SeekFrom::Start(position))
316            .map_err(|e| IoError::FileError(format!("Failed to seek: {}", e)))?;
317        self.current_position = position;
318        Ok(())
319    }
320
321    /// Get current position
322    pub fn position(&self) -> u64 {
323        self.current_position
324    }
325
326    /// Get file size
327    pub fn size(&self) -> u64 {
328        self.file_size
329    }
330
331    /// Check if at end of file
332    pub fn is_eof(&self) -> bool {
333        self.current_position >= self.file_size
334    }
335
336    /// Get progress percentage
337    pub fn progress_percentage(&self) -> f64 {
338        if self.file_size > 0 {
339            (self.current_position as f64 / self.file_size as f64) * 100.0
340        } else {
341            0.0
342        }
343    }
344}
345
346/// Chunked file writer for streaming large files
347pub struct ChunkedWriter {
348    file: std::fs::File,
349    bytes_written: u64,
350    buffer: Vec<u8>,
351    buffer_size: usize,
352}
353
354impl ChunkedWriter {
355    /// Create a new chunked writer
356    pub fn new<P: AsRef<Path>>(path: P, buffersize: usize) -> Result<Self> {
357        let file = std::fs::File::create(path.as_ref())
358            .map_err(|e| IoError::FileError(format!("Failed to create file: {}", e)))?;
359
360        Ok(Self {
361            file,
362            bytes_written: 0,
363            buffer: Vec::with_capacity(buffersize),
364            buffer_size: buffersize,
365        })
366    }
367
368    /// Write a chunk of data
369    pub fn write_chunk(&mut self, data: &[u8]) -> Result<()> {
370        self.buffer.extend_from_slice(data);
371
372        // Flush buffer if it's full
373        if self.buffer.len() >= self.buffer_size {
374            self.flush_buffer()?;
375        }
376
377        Ok(())
378    }
379
380    /// Flush the internal buffer
381    pub fn flush_buffer(&mut self) -> Result<()> {
382        if !self.buffer.is_empty() {
383            self.file
384                .write_all(&self.buffer)
385                .map_err(|e| IoError::FileError(format!("Failed to write buffer: {}", e)))?;
386
387            self.bytes_written += self.buffer.len() as u64;
388            self.buffer.clear();
389        }
390        Ok(())
391    }
392
393    /// Finish writing and close the file
394    pub fn finish(mut self) -> Result<u64> {
395        self.flush_buffer()?;
396        self.file
397            .flush()
398            .map_err(|e| IoError::FileError(format!("Failed to flush file: {}", e)))?;
399        Ok(self.bytes_written)
400    }
401
402    /// Get bytes written so far
403    pub fn bytes_written(&self) -> u64 {
404        self.bytes_written + self.buffer.len() as u64
405    }
406}
407
408/// Stream copy with progress tracking
409#[allow(dead_code)]
410pub fn copy_with_progress<R: Read, W: Write>(
411    mut reader: R,
412    mut writer: W,
413    total_size: Option<u64>,
414    progresscallback: Option<ProgressCallback>,
415) -> Result<u64> {
416    let mut buffer = vec![0u8; 64 * 1024]; // 64KB buffer
417    let mut total_copied = 0u64;
418    let start_time = std::time::Instant::now();
419    let mut last_progress_report = 0u64;
420    let progress_interval = 1024 * 1024; // Report every 1MB
421
422    loop {
423        let bytes_read = reader
424            .read(&mut buffer)
425            .map_err(|e| IoError::FileError(format!("Read error: {}", e)))?;
426
427        if bytes_read == 0 {
428            break;
429        }
430
431        writer
432            .write_all(&buffer[..bytes_read])
433            .map_err(|e| IoError::FileError(format!("Write error: {}", e)))?;
434
435        total_copied += bytes_read as u64;
436
437        // Report progress if needed
438        if let Some(ref callback) = progresscallback {
439            if total_copied - last_progress_report >= progress_interval {
440                let elapsed = start_time.elapsed().as_secs_f64();
441                let rate = if elapsed > 0.0 {
442                    total_copied as f64 / elapsed
443                } else {
444                    0.0
445                };
446
447                let eta_seconds = if let Some(total) = total_size {
448                    if rate > 0.0 && total_copied < total {
449                        Some((total - total_copied) as f64 / rate)
450                    } else {
451                        None
452                    }
453                } else {
454                    None
455                };
456
457                let progress = StreamProgress {
458                    bytes_transferred: total_copied,
459                    total_bytes: total_size,
460                    rate,
461                    eta_seconds,
462                };
463
464                callback(progress);
465                last_progress_report = total_copied;
466            }
467        }
468    }
469
470    // Final progress report
471    if let Some(ref callback) = progresscallback {
472        let elapsed = start_time.elapsed().as_secs_f64();
473        let rate = if elapsed > 0.0 {
474            total_copied as f64 / elapsed
475        } else {
476            0.0
477        };
478
479        let progress = StreamProgress {
480            bytes_transferred: total_copied,
481            total_bytes: total_size,
482            rate,
483            eta_seconds: Some(0.0),
484        };
485
486        callback(progress);
487    }
488
489    Ok(total_copied)
490}
491
492/// Async stream copy with progress tracking
493#[cfg(feature = "async")]
494pub async fn async_copy_with_progress<R, W>(
495    mut reader: R,
496    mut writer: W,
497    total_size: Option<u64>,
498    progresscallback: Option<ProgressCallback>,
499) -> Result<u64>
500where
501    R: tokio::io::AsyncRead + Unpin,
502    W: tokio::io::AsyncWrite + Unpin,
503{
504    use tokio::io::{AsyncReadExt, AsyncWriteExt};
505
506    let mut buffer = vec![0u8; 64 * 1024]; // 64KB buffer
507    let mut total_copied = 0u64;
508    let start_time = std::time::Instant::now();
509    let mut last_progress_report = 0u64;
510    let progress_interval = 1024 * 1024; // Report every 1MB
511
512    loop {
513        let bytes_read = reader
514            .read(&mut buffer)
515            .await
516            .map_err(|e| IoError::FileError(format!("Async read error: {}", e)))?;
517
518        if bytes_read == 0 {
519            break;
520        }
521
522        writer
523            .write_all(&buffer[..bytes_read])
524            .await
525            .map_err(|e| IoError::FileError(format!("Async write error: {}", e)))?;
526
527        total_copied += bytes_read as u64;
528
529        // Report progress if needed
530        if let Some(ref callback) = progresscallback {
531            if total_copied - last_progress_report >= progress_interval {
532                let elapsed = start_time.elapsed().as_secs_f64();
533                let rate = if elapsed > 0.0 {
534                    total_copied as f64 / elapsed
535                } else {
536                    0.0
537                };
538
539                let eta_seconds = if let Some(total) = total_size {
540                    if rate > 0.0 && total_copied < total {
541                        Some((total - total_copied) as f64 / rate)
542                    } else {
543                        None
544                    }
545                } else {
546                    None
547                };
548
549                let progress = StreamProgress {
550                    bytes_transferred: total_copied,
551                    total_bytes: total_size,
552                    rate,
553                    eta_seconds,
554                };
555
556                callback(progress);
557                last_progress_report = total_copied;
558            }
559        }
560    }
561
562    writer
563        .flush()
564        .await
565        .map_err(|e| IoError::FileError(format!("Async flush error: {}", e)))?;
566
567    Ok(total_copied)
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use std::io::Cursor;
574    use tempfile::tempdir;
575
576    #[test]
577    fn test_stream_config_default() {
578        let config = StreamConfig::default();
579        assert_eq!(config.buffer_size, 64 * 1024);
580        assert_eq!(config.max_memory, 16 * 1024 * 1024);
581        assert!(!config.compression);
582        assert_eq!(config.progress_interval, 1024 * 1024);
583    }
584
585    #[test]
586    fn test_stream_progress() {
587        let progress = StreamProgress {
588            bytes_transferred: 512,
589            total_bytes: Some(1024),
590            rate: 256.0,
591            eta_seconds: Some(2.0),
592        };
593
594        assert_eq!(progress.percentage(), Some(50.0));
595        assert!(!progress.is_complete());
596
597        let complete_progress = StreamProgress {
598            bytes_transferred: 1024,
599            total_bytes: Some(1024),
600            rate: 512.0,
601            eta_seconds: Some(0.0),
602        };
603
604        assert_eq!(complete_progress.percentage(), Some(100.0));
605        assert!(complete_progress.is_complete());
606    }
607
608    #[test]
609    fn test_progress_reader() {
610        let data = b"Hello, world! This is test data for streaming.";
611        let cursor = Cursor::new(data);
612
613        let mut reader = ProgressReader::new(cursor)
614            .with_total_bytes(data.len() as u64)
615            .with_progress_interval(10);
616
617        let mut buffer = [0u8; 20];
618        let bytes_read = reader.read(&mut buffer).expect("Operation failed");
619
620        assert_eq!(bytes_read, 20);
621        assert_eq!(reader.progress().bytes_transferred, 20);
622
623        let progress = reader.progress();
624        assert_eq!(progress.bytes_transferred, 20);
625        assert_eq!(progress.total_bytes, Some(data.len() as u64));
626        assert!(progress.rate >= 0.0);
627    }
628
629    #[test]
630    fn test_progress_writer() {
631        let mut output = Vec::new();
632        let mut writer = ProgressWriter::new(&mut output)
633            .with_total_bytes(100)
634            .with_progress_interval(25);
635
636        let data = b"Test data for progress writer functionality.";
637        let bytes_written = writer.write(data).expect("Operation failed");
638
639        assert_eq!(bytes_written, data.len());
640        assert_eq!(writer.progress().bytes_transferred, data.len() as u64);
641
642        writer.flush().expect("Operation failed");
643        assert_eq!(output, data);
644    }
645
646    #[test]
647    fn test_chunked_reader() {
648        let temp_dir = tempdir().expect("Operation failed");
649        let file_path = temp_dir.path().join("test_chunked.txt");
650
651        // Create test file
652        let test_data = b"This is test data for chunked reading. It should be read in chunks.";
653        std::fs::write(&file_path, test_data).expect("Operation failed");
654
655        let mut reader = ChunkedReader::new(&file_path, 10).expect("Operation failed");
656        assert_eq!(reader.size(), test_data.len() as u64);
657        assert!(!reader.is_eof());
658
659        let mut all_data = Vec::new();
660        while let Some(chunk) = reader.read_chunk().expect("Operation failed") {
661            all_data.extend_from_slice(&chunk);
662        }
663
664        assert_eq!(all_data, test_data);
665        assert!(reader.is_eof());
666        assert_eq!(reader.progress_percentage(), 100.0);
667    }
668
669    #[test]
670    fn test_chunked_writer() {
671        let temp_dir = tempdir().expect("Operation failed");
672        let file_path = temp_dir.path().join("test_chunked_write.txt");
673
674        let mut writer = ChunkedWriter::new(&file_path, 20).expect("Operation failed");
675
676        let data1 = b"First chunk of data.";
677        let data2 = b"Second chunk of data.";
678
679        writer.write_chunk(data1).expect("Operation failed");
680        writer.write_chunk(data2).expect("Operation failed");
681
682        let total_bytes = writer.finish().expect("Operation failed");
683        assert_eq!(total_bytes, (data1.len() + data2.len()) as u64);
684
685        // Verify file contents
686        let file_contents = std::fs::read(&file_path).expect("Operation failed");
687        let expected = [&data1[..], &data2[..]].concat();
688        assert_eq!(file_contents, expected);
689    }
690
691    #[test]
692    fn test_copy_with_progress() {
693        let input_data = b"This is test data for copy with progress functionality. It demonstrates streaming copy operations.";
694        let input = Cursor::new(input_data);
695        let mut output = Vec::new();
696
697        let progress_reports = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
698        let progress_reports_clone = progress_reports.clone();
699        let callback = Box::new(move |progress: StreamProgress| {
700            progress_reports_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
701            assert!(progress.bytes_transferred <= input_data.len() as u64);
702            assert!(progress.rate >= 0.0);
703        }) as ProgressCallback;
704
705        // Use a small progress interval to ensure we get reports
706        let copied = copy_with_progress(
707            input,
708            &mut output,
709            Some(input_data.len() as u64),
710            Some(callback),
711        )
712        .expect("Operation failed");
713
714        assert_eq!(copied, input_data.len() as u64);
715        assert_eq!(output, input_data);
716    }
717
718    #[tokio::test]
719    #[cfg(feature = "async")]
720    async fn test_async_copy_with_progress() {
721        let input_data = b"Async test data for copy with progress functionality.";
722        let input = Cursor::new(input_data);
723        let mut output = Vec::new();
724
725        let copied =
726            async_copy_with_progress(input, &mut output, Some(input_data.len() as u64), None)
727                .await
728                .expect("Operation failed");
729
730        assert_eq!(copied, input_data.len() as u64);
731        assert_eq!(output, input_data);
732    }
733}