Skip to main content

mailsis_utils/
file.rs

1//! Async file I/O helpers for large payloads.
2//!
3//! Email attachments can be arbitrarily large, so file reads and writes
4//! go through async, chunked helpers here to avoid blocking the Tokio
5//! runtime. Also includes random-file generation for test fixtures.
6
7use std::{io::Error, path::Path};
8
9use rand::Rng;
10use tokio::{
11    fs::File,
12    io::{AsyncReadExt, AsyncWriteExt},
13};
14
15/// Reads a large file from the given path.
16///
17/// The file is read in an async manner.
18pub async fn read_large_file(path: impl AsRef<Path>) -> Result<Vec<u8>, Error> {
19    let mut file = File::open(path).await?;
20    let mut buffer = Vec::new();
21    file.read_to_end(&mut buffer).await?;
22    Ok(buffer)
23}
24
25/// Generates a random file with the given size in MB.
26///
27/// The file is created in the path provided and in an
28/// async manner.
29pub async fn generate_random_file(path: impl AsRef<Path>, size_mb: usize) -> Result<(), Error> {
30    let mut file = File::create(path).await?;
31    let mut rng = rand::thread_rng();
32    let chunk_size = 1024 * 1024;
33    let total_chunks = size_mb;
34
35    for _ in 0..total_chunks {
36        let mut chunk = vec![0u8; chunk_size];
37        rng.fill(&mut chunk[..]);
38        AsyncWriteExt::write_all(&mut file, &chunk).await?;
39    }
40
41    file.flush().await?;
42    Ok(())
43}
44
45/// Generates random bytes that can be used to test the file system.
46///
47/// The generated bytes are not guaranteed to be unique, but it
48/// uses a good random number generator.
49pub async fn generate_random_bytes(size: usize) -> Result<Vec<u8>, Error> {
50    let mut rng = rand::thread_rng();
51    let mut bytes = vec![0u8; size];
52    rng.fill(&mut bytes[..]);
53    Ok(bytes)
54}
55
56#[cfg(test)]
57mod tests {
58    use tempfile::TempDir;
59
60    use super::*;
61
62    #[tokio::test]
63    async fn test_read_large_file_success() {
64        let temp_dir = TempDir::new().unwrap();
65        let file_path = temp_dir.path().join("test.txt");
66        tokio::fs::write(&file_path, b"Hello, World!")
67            .await
68            .unwrap();
69
70        let content = read_large_file(&file_path).await.unwrap();
71        assert_eq!(content, b"Hello, World!");
72    }
73
74    #[tokio::test]
75    async fn test_read_large_file_empty() {
76        let temp_dir = TempDir::new().unwrap();
77        let file_path = temp_dir.path().join("empty.txt");
78        tokio::fs::write(&file_path, b"").await.unwrap();
79
80        let content = read_large_file(&file_path).await.unwrap();
81        assert!(content.is_empty());
82    }
83
84    #[tokio::test]
85    async fn test_read_large_file_not_found() {
86        let result = read_large_file("/nonexistent/path/file.txt").await;
87        assert!(result.is_err());
88    }
89
90    #[tokio::test]
91    async fn test_generate_random_file_creates_file() {
92        let temp_dir = TempDir::new().unwrap();
93        let file_path = temp_dir.path().join("random.bin");
94
95        generate_random_file(&file_path, 1).await.unwrap();
96
97        let content = tokio::fs::read(&file_path).await.unwrap();
98        assert_eq!(content.len(), 1024 * 1024);
99    }
100
101    #[tokio::test]
102    async fn test_generate_random_file_zero_size() {
103        let temp_dir = TempDir::new().unwrap();
104        let file_path = temp_dir.path().join("empty_random.bin");
105
106        generate_random_file(&file_path, 0).await.unwrap();
107
108        let content = tokio::fs::read(&file_path).await.unwrap();
109        assert!(content.is_empty());
110    }
111
112    #[tokio::test]
113    async fn test_generate_random_bytes_size() {
114        let bytes = generate_random_bytes(256).await.unwrap();
115        assert_eq!(bytes.len(), 256);
116    }
117
118    #[tokio::test]
119    async fn test_generate_random_bytes_zero_size() {
120        let bytes = generate_random_bytes(0).await.unwrap();
121        assert!(bytes.is_empty());
122    }
123
124    #[tokio::test]
125    async fn test_generate_random_bytes_not_all_zeros() {
126        let bytes = generate_random_bytes(1024).await.unwrap();
127        assert!(bytes.iter().any(|&byte| byte != 0));
128    }
129
130    #[tokio::test]
131    async fn test_read_large_file_roundtrip() {
132        let temp_dir = TempDir::new().unwrap();
133        let file_path = temp_dir.path().join("roundtrip.bin");
134
135        generate_random_file(&file_path, 1).await.unwrap();
136        let content = read_large_file(&file_path).await.unwrap();
137        assert_eq!(content.len(), 1024 * 1024);
138    }
139}