backup_suite/core/
copy_engine.rs1use std::fs::File;
2use std::io::{BufReader, BufWriter, Read, Write};
3use std::path::Path;
4
5use crate::error::Result;
6
7#[derive(Debug, Clone)]
31pub struct CopyEngine {
32 buffer_size: usize,
34 parallel_threshold: u64,
36}
37
38impl CopyEngine {
39 #[must_use]
56 pub fn new() -> Self {
57 Self {
58 buffer_size: 64 * 1024, parallel_threshold: 10 * 1024 * 1024, }
61 }
62
63 #[must_use]
82 pub fn with_config(buffer_size: usize, parallel_threshold: u64) -> Self {
83 Self {
84 buffer_size,
85 parallel_threshold,
86 }
87 }
88
89 pub fn copy_file(&self, source: &Path, dest: &Path) -> Result<u64> {
125 let metadata = std::fs::metadata(source)?;
127 let size = metadata.len();
128
129 if size < self.parallel_threshold {
131 return std::fs::copy(source, dest).map_err(Into::into);
132 }
133
134 self.buffered_copy(source, dest)
136 }
137
138 #[allow(clippy::indexing_slicing)] fn buffered_copy(&self, source: &Path, dest: &Path) -> Result<u64> {
152 let mut reader = BufReader::with_capacity(self.buffer_size, File::open(source)?);
153 let mut writer = BufWriter::with_capacity(self.buffer_size, File::create(dest)?);
154
155 let mut buffer = vec![0u8; self.buffer_size];
156 let mut total_bytes = 0u64;
157
158 loop {
159 let bytes_read = reader.read(&mut buffer)?;
160 if bytes_read == 0 {
161 break;
162 }
163
164 writer.write_all(&buffer[..bytes_read])?;
165 total_bytes += bytes_read as u64;
166 }
167
168 writer.flush()?;
169
170 #[cfg(unix)]
172 {
173 let perms = std::fs::metadata(source)?.permissions();
174 std::fs::set_permissions(dest, perms)?;
175 }
176
177 Ok(total_bytes)
178 }
179
180 #[must_use]
186 pub fn buffer_size(&self) -> usize {
187 self.buffer_size
188 }
189
190 #[must_use]
196 pub fn parallel_threshold(&self) -> u64 {
197 self.parallel_threshold
198 }
199}
200
201impl Default for CopyEngine {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use std::io::Write;
211 use tempfile::TempDir;
212
213 #[test]
214 fn test_new_engine() {
215 let engine = CopyEngine::new();
216 assert_eq!(engine.buffer_size(), 64 * 1024);
217 assert_eq!(engine.parallel_threshold(), 10 * 1024 * 1024);
218 }
219
220 #[test]
221 fn test_with_config() {
222 let engine = CopyEngine::with_config(128 * 1024, 20 * 1024 * 1024);
223 assert_eq!(engine.buffer_size(), 128 * 1024);
224 assert_eq!(engine.parallel_threshold(), 20 * 1024 * 1024);
225 }
226
227 #[test]
228 fn test_copy_small_file() {
229 let temp_dir = TempDir::new().unwrap();
230 let source = temp_dir.path().join("source.txt");
231 let dest = temp_dir.path().join("dest.txt");
232
233 let mut file = File::create(&source).unwrap();
235 file.write_all(b"test content").unwrap();
236
237 let engine = CopyEngine::new();
238 let bytes = engine.copy_file(&source, &dest).unwrap();
239
240 assert_eq!(bytes, 12);
241 assert!(dest.exists());
242
243 let content = std::fs::read_to_string(&dest).unwrap();
244 assert_eq!(content, "test content");
245 }
246
247 #[test]
248 fn test_copy_large_file() {
249 let temp_dir = TempDir::new().unwrap();
250 let source = temp_dir.path().join("large_source.bin");
251 let dest = temp_dir.path().join("large_dest.bin");
252
253 let large_content = vec![0u8; 15 * 1024 * 1024];
255 std::fs::write(&source, &large_content).unwrap();
256
257 let engine = CopyEngine::new();
258 let bytes = engine.copy_file(&source, &dest).unwrap();
259
260 assert_eq!(bytes, 15 * 1024 * 1024);
261 assert!(dest.exists());
262
263 let metadata = std::fs::metadata(&dest).unwrap();
264 assert_eq!(metadata.len(), 15 * 1024 * 1024);
265 }
266
267 #[test]
268 fn test_buffered_copy() {
269 let temp_dir = TempDir::new().unwrap();
270 let source = temp_dir.path().join("buffered_source.txt");
271 let dest = temp_dir.path().join("buffered_dest.txt");
272
273 let content = "buffered copy test content";
275 std::fs::write(&source, content).unwrap();
276
277 let engine = CopyEngine::new();
278 let bytes = engine.buffered_copy(&source, &dest).unwrap();
279
280 assert_eq!(bytes, content.len() as u64);
281 assert!(dest.exists());
282
283 let copied_content = std::fs::read_to_string(&dest).unwrap();
284 assert_eq!(copied_content, content);
285 }
286
287 #[test]
288 fn test_default_engine() {
289 let engine = CopyEngine::default();
290 assert_eq!(engine.buffer_size(), 64 * 1024);
291 }
292
293 #[cfg(unix)]
294 #[test]
295 fn test_preserve_permissions() {
296 use std::os::unix::fs::PermissionsExt;
297
298 let temp_dir = TempDir::new().unwrap();
299 let source = temp_dir.path().join("perm_source.txt");
300 let dest = temp_dir.path().join("perm_dest.txt");
301
302 let mut file = File::create(&source).unwrap();
304 file.write_all(b"permission test").unwrap();
305 drop(file);
306
307 let mut perms = std::fs::metadata(&source).unwrap().permissions();
308 perms.set_mode(0o644);
309 std::fs::set_permissions(&source, perms).unwrap();
310
311 let engine = CopyEngine::new();
313 engine.copy_file(&source, &dest).unwrap();
314
315 let dest_perms = std::fs::metadata(&dest).unwrap().permissions();
317 assert_eq!(dest_perms.mode() & 0o777, 0o644);
318 }
319}