dev_utils/file.rs
1//! This module provides advanced functions for file and directory operations.
2//!
3//! It offers a simple and efficient way to work with files and directories,
4//! allowing you to create, read, update, delete, list, copy, move, and rename files with ease.
5//!
6//! # Features
7//! - CRUD operations on files
8//! - Listing directory contents
9//! - Copying, moving, and renaming files
10//! - Error handling with custom error types
11//! - All operations use only the Rust standard library
12//!
13//! # Examples
14//! ```
15//! use dev_utils::file::*;
16//!
17//! // Create a new file
18//! let file_path = create("test.txt", "Hello, World!").unwrap();
19//!
20//! // Read the file contents
21//! let content = read(&file_path).unwrap();
22//! assert_eq!(content, "Hello, World!");
23//!
24//! // Update the file contents
25//! update(&file_path, "Updated content").unwrap();
26//! assert_eq!(read(&file_path).unwrap(), "Updated content");
27//!
28//! // Append to the file
29//! append(&file_path, " Appended content").unwrap();
30//! assert_eq!(read(&file_path).unwrap(), "Updated content Appended content");
31//!
32//! // Delete the file
33//! delete(&file_path).unwrap();
34//! assert!(!file_path.exists());
35//! ```
36use std::fmt;
37use std::fs::{self, DirEntry, File, OpenOptions};
38use std::io::{self, Error, Read, Write};
39use std::path::{Path, PathBuf};
40
41/// Custom error type for file operations.
42#[derive(Debug)]
43pub enum FileError {
44 /// Represents an IO error from the standard library.
45 Io(io::Error),
46 /// Represents a path-related error
47 PathError(String),
48}
49
50impl fmt::Display for FileError {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 FileError::Io(err) => write!(f, "IO error: {}", err),
54 FileError::PathError(err) => write!(f, "Path error: {}", err),
55 }
56 }
57}
58
59impl std::error::Error for FileError {}
60
61impl From<io::Error> for FileError {
62 fn from(err: io::Error) -> Self {
63 FileError::Io(err)
64 }
65}
66
67/// Custom Result type for file operations.
68type Result<T> = std::result::Result<T, FileError>;
69
70/// Creates a new file with the given content.
71///
72/// If the file already exists, it will be overwritten.
73///
74/// # Arguments
75///
76/// * `path` - The path where the file should be created.
77/// * `content` - The content to write to the file.
78///
79/// # Returns
80///
81/// Returns a `Result` containing the `PathBuf` of the created file, or a `FileError`.
82///
83/// # Examples
84///
85/// ```
86/// use dev_utils::file::create;
87///
88/// let file_path = create("example.txt", "Hello, World!").unwrap();
89/// assert!(file_path.exists());
90/// ```
91pub fn create<P: AsRef<Path>>(path: P, content: &str) -> Result<PathBuf> {
92 let path = path.as_ref();
93 if let Some(parent) = path.parent() {
94 fs::create_dir_all(parent)?;
95 }
96 let mut file = File::create(path)?;
97 file.write_all(content.as_bytes())?;
98 Ok(path.to_owned())
99}
100
101/// Reads the contents of a file.
102///
103/// # Arguments
104///
105/// * `path` - The path of the file to read.
106///
107/// # Returns
108///
109/// Returns a `Result` containing the file contents as a `String`, or a `FileError`.
110///
111/// # Examples
112///
113/// ```
114/// use dev_utils::file::{create, read};
115///
116/// let file_path = create("example.txt", "Hello, World!").unwrap();
117/// let content = read(&file_path).unwrap();
118/// assert_eq!(content, "Hello, World!");
119/// ```
120pub fn read<P: AsRef<Path>>(path: P) -> Result<String> {
121 let mut file = File::open(path)?;
122 let mut content = String::new();
123 file.read_to_string(&mut content)?;
124 Ok(content)
125}
126
127/// Updates the contents of a file.
128///
129/// If the file doesn't exist, it will be created.
130///
131/// # Arguments
132///
133/// * `path` - The path of the file to update.
134/// * `content` - The new content to write to the file.
135///
136/// # Returns
137///
138/// Returns a `Result` containing `()` if successful, or a `FileError`.
139///
140/// # Examples
141///
142/// ```
143/// use dev_utils::file::{create, update, read};
144///
145/// let file_path = create("example.txt", "Hello").unwrap();
146/// update(&file_path, "Updated content").unwrap();
147/// assert_eq!(read(&file_path).unwrap(), "Updated content");
148/// ```
149pub fn update<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
150 let mut file = OpenOptions::new()
151 .write(true)
152 .truncate(true)
153 .create(true)
154 .open(path)?;
155 file.write_all(content.as_bytes())?;
156 Ok(())
157}
158
159/// Appends content to the end of a file.
160///
161/// If the file doesn't exist, it will be created.
162///
163/// # Arguments
164///
165/// * `path` - The path of the file to append to.
166/// * `content` - The content to append to the file.
167///
168/// # Returns
169///
170/// Returns a `Result` containing `()` if successful, or a `FileError`.
171///
172/// # Examples
173///
174/// ```
175/// use dev_utils::file::{create, append, read};
176///
177/// let file_path = create("example.txt", "Hello").unwrap();
178/// append(&file_path, ", World!").unwrap();
179/// assert_eq!(read(&file_path).unwrap(), "Hello, World!");
180/// ```
181pub fn append<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
182 let mut file = OpenOptions::new().append(true).create(true).open(path)?;
183 file.write_all(content.as_bytes())?;
184 Ok(())
185}
186
187/// Deletes a file.
188///
189/// # Arguments
190///
191/// * `path` - The path of the file to delete.
192///
193/// # Returns
194///
195/// Returns a `Result` containing `()` if successful, or a `FileError`.
196///
197/// # Examples
198///
199/// ```
200/// use dev_utils::file::{create, delete};
201/// use std::path::Path;
202///
203/// let file_path = create("example.txt", "").unwrap();
204/// delete(&file_path).unwrap();
205/// assert!(!Path::new("example.txt").exists());
206/// ```
207pub fn delete<P: AsRef<Path>>(path: P) -> Result<()> {
208 fs::remove_file(path)?;
209 Ok(())
210}
211
212/// Lists the contents of a directory.
213///
214/// # Arguments
215///
216/// * `path` - The path of the directory to list.
217///
218/// # Returns
219///
220/// Returns a `Result` containing a `Vec<PathBuf>` of the directory contents, or a `FileError`.
221///
222/// # Examples
223///
224/// ```
225/// use dev_utils::file::{create, list};
226/// use std::path::Path;
227///
228/// create("dir/file1.txt", "").unwrap();
229/// create("dir/file2.txt", "").unwrap();
230/// let contents = list("dir").unwrap();
231/// assert_eq!(contents.len(), 2);
232/// ```
233pub fn list<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
234 let entries = fs::read_dir(path)?
235 .filter_map(|entry| entry.ok())
236 .map(|entry| entry.path())
237 .collect();
238 Ok(entries)
239}
240
241/// Copies a file from one location to another.
242///
243/// # Arguments
244///
245/// * `from` - The path of the file to copy.
246/// * `to` - The path where the file should be copied to.
247///
248/// # Returns
249///
250/// Returns a `Result` containing `()` if successful, or a `FileError`.
251///
252/// # Examples
253///
254/// ```
255/// use dev_utils::file::{create, copy, read};
256///
257/// let original = create("original.txt", "Hello").unwrap();
258/// copy(&original, "copy.txt").unwrap();
259/// assert_eq!(read("copy.txt").unwrap(), "Hello");
260/// ```
261pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
262 fs::copy(from, to)?;
263 Ok(())
264}
265
266/// Moves a file from one location to another.
267///
268/// # Arguments
269///
270/// * `from` - The current path of the file.
271/// * `to` - The new path for the file.
272///
273/// # Returns
274///
275/// Returns a `Result` containing `()` if successful, or a `FileError`.
276///
277/// # Examples
278///
279/// ```
280/// use dev_utils::file::{create, mv, read};
281/// use std::path::Path;
282///
283/// let original = create("original.txt", "Hello").unwrap();
284/// mv(&original, "moved.txt").unwrap();
285/// assert!(!Path::new("original.txt").exists());
286/// assert_eq!(read("moved.txt").unwrap(), "Hello");
287/// ```
288pub fn mv<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
289 fs::rename(from, to)?;
290 Ok(())
291}
292
293/// Renames a file.
294///
295/// This function is identical to `mv`.
296///
297/// # Arguments
298///
299/// * `from` - The current path of the file.
300/// * `to` - The new path for the file.
301///
302/// # Returns
303///
304/// Returns a `Result` containing `()` if successful, or a `FileError`.
305///
306/// # Examples
307///
308/// ```
309/// use dev_utils::file::{create, rename, read};
310/// use std::path::Path;
311///
312/// let original = create("original.txt", "Hello").unwrap();
313/// rename(&original, "renamed.txt").unwrap();
314/// assert!(!Path::new("original.txt").exists());
315/// assert_eq!(read("renamed.txt").unwrap(), "Hello");
316/// ```
317pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
318 fs::rename(from, to)?;
319 Ok(())
320}
321
322// * Advanced functionality
323/// Recursively copies a directory and its contents.
324///
325/// # Arguments
326///
327/// * `from` - The path of the directory to copy.
328/// * `to` - The path where the directory should be copied to.
329///
330/// # Returns
331///
332/// Returns a `Result` containing `()` if successful, or a `FileError`.
333///
334/// # Examples
335///
336/// ```
337/// use dev_utils::file::{create, recursive_copy, read};
338/// use std::path::Path;
339///
340/// create("dir/file1.txt", "Hello").unwrap();
341/// create("dir/subdir/file2.txt", "World").unwrap();
342/// recursive_copy("dir", "copy_dir").unwrap();
343/// assert!(Path::new("copy_dir/file1.txt").exists());
344/// assert!(Path::new("copy_dir/subdir/file2.txt").exists());
345/// assert_eq!(read("copy_dir/file1.txt").unwrap(), "Hello");
346/// assert_eq!(read("copy_dir/subdir/file2.txt").unwrap(), "World");
347/// ```
348pub fn recursive_copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
349 let from = from.as_ref();
350 let to = to.as_ref();
351
352 if from.is_dir() {
353 if !to.exists() {
354 fs::create_dir_all(to)?;
355 }
356
357 for entry in fs::read_dir(from)? {
358 let entry = entry?;
359 let file_type = entry.file_type()?;
360 let new_from = from.join(entry.file_name());
361 let new_to = to.join(entry.file_name());
362
363 if file_type.is_dir() {
364 recursive_copy(new_from, new_to)?;
365 } else {
366 fs::copy(new_from, new_to)?;
367 }
368 }
369 } else {
370 if let Some(parent) = to.parent() {
371 fs::create_dir_all(parent)?;
372 }
373 fs::copy(from, to)?;
374 }
375
376 Ok(())
377}
378
379/// Finds files in a directory (and its subdirectories) that match a given predicate.
380///
381/// # Arguments
382///
383/// * `path` - The path of the directory to search.
384/// * `filter` - A function that takes a `&DirEntry` and returns a `bool`.
385///
386/// # Returns
387///
388/// Returns a `Result` containing a `Vec<PathBuf>` of matching files, or a `FileError`.
389///
390/// # Examples
391///
392/// ```
393/// use dev_utils::file::{create, find};
394///
395/// create("dir/file1.txt", "").unwrap();
396/// create("dir/file2.dat", "").unwrap();
397/// create("dir/subdir/file3.txt", "").unwrap();
398/// let txt_files = find("dir", |entry| {
399/// entry.path().extension().map_or(false, |ext| ext == "txt")
400/// }).unwrap();
401/// assert_eq!(txt_files.len(), 2);
402/// ```
403pub fn find<P: AsRef<Path>, F>(path: P, filter: F) -> Result<Vec<PathBuf>>
404where
405 F: Fn(&DirEntry) -> bool,
406{
407 let mut results = Vec::new();
408 find_internal(path.as_ref(), &filter, &mut results)?;
409 Ok(results)
410}
411
412// Internal helper function for `find`
413fn find_internal<F>(path: &Path, filter: &F, results: &mut Vec<PathBuf>) -> io::Result<()>
414where
415 F: Fn(&DirEntry) -> bool,
416{
417 if path.is_dir() {
418 for entry in fs::read_dir(path)? {
419 let entry = entry?;
420 let path = entry.path();
421
422 if path.is_dir() {
423 find_internal(&path, filter, results)?;
424 } else if filter(&entry) {
425 results.push(path);
426 }
427 }
428 }
429 Ok(())
430}
431
432// todo: Check why the test module is not working as expected...
433// todo: The individual tests are working fine but when running the module test, it's not working
434// todo: -> (cargo test --lib file)
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use std::fs;
439
440 const TEST_DIR: &str = "test_file_ops";
441 const TEST_FILE: &str = "test_file.txt";
442 const TEST_FILE_COPY: &str = "test_file_copy.txt";
443 const TEST_FILE_MOVE: &str = "test_file_move.txt";
444 const TEST_FILE_RENAME: &str = "test_file_rename.txt";
445
446 fn setup() {
447 fs::create_dir(TEST_DIR);
448 }
449 fn cleanup() {
450 fs::remove_dir_all(TEST_DIR);
451 }
452 fn get_test_path(filename: &str) -> PathBuf {
453 Path::new(TEST_DIR).join(filename)
454 }
455
456 #[test]
457 fn test_crud_operations() {
458 setup();
459
460 // Create
461 let file_path = get_test_path(TEST_FILE);
462 let content = "Hello, World!";
463 let created_path = create(&file_path, content).unwrap();
464 assert_eq!(created_path, file_path);
465 assert!(file_path.exists());
466
467 // Read
468 let read_content = read(&file_path).unwrap();
469 assert_eq!(read_content, content);
470
471 // Update
472 let new_content = "Updated content";
473 update(&file_path, new_content).unwrap();
474 let updated_content = read(&file_path).unwrap();
475 assert_eq!(updated_content, new_content);
476
477 // Append
478 let append_content = " Appended content";
479 append(&file_path, append_content).unwrap();
480 let final_content = read(&file_path).unwrap();
481 assert_eq!(final_content, format!("{}{}", new_content, append_content));
482
483 // Delete
484 delete(&file_path).unwrap();
485 assert!(!file_path.exists());
486
487 cleanup();
488 }
489
490 #[test]
491 fn test_list_and_find() {
492 setup();
493
494 let file_path = get_test_path(TEST_FILE);
495 create(&file_path, "Content").unwrap();
496 create(&get_test_path("file2.txt"), "Content").unwrap();
497 create(&get_test_path("file3.dat"), "Content").unwrap();
498
499 // List
500 let entries = list(TEST_DIR).unwrap();
501 assert_eq!(entries.len(), 3);
502
503 // Find
504 let txt_files = find(TEST_DIR, |entry| {
505 entry.path().extension().map_or(false, |ext| ext == "txt")
506 })
507 .unwrap();
508 assert_eq!(txt_files.len(), 2);
509
510 cleanup();
511 }
512
513 #[test]
514 fn test_copy_move_rename() {
515 setup();
516
517 let original_path = get_test_path(TEST_FILE);
518 let copy_path = get_test_path(TEST_FILE_COPY);
519 let move_path = get_test_path(TEST_FILE_MOVE);
520 let rename_path = get_test_path(TEST_FILE_RENAME);
521
522 // Create original file
523 create(&original_path, "Original content").unwrap();
524
525 // Copy
526 copy(&original_path, ©_path).unwrap();
527 assert!(copy_path.exists());
528 assert_eq!(read(&original_path).unwrap(), read(©_path).unwrap());
529
530 // Move
531 mv(©_path, &move_path).unwrap();
532 assert!(!copy_path.exists());
533 assert!(move_path.exists());
534
535 // Rename
536 rename(&move_path, &rename_path).unwrap();
537 assert!(!move_path.exists());
538 assert!(rename_path.exists());
539
540 cleanup();
541 }
542
543 #[test]
544 fn test_recursive_copy() {
545 setup();
546
547 let sub_dir = Path::new(TEST_DIR).join("sub_dir");
548 fs::create_dir(&sub_dir).unwrap();
549
550 create(&sub_dir.join("file1.txt"), "Content 1").unwrap();
551 create(&sub_dir.join("file2.txt"), "Content 2").unwrap();
552
553 let copy_dir = Path::new(TEST_DIR).join("copy_dir");
554
555 recursive_copy(&sub_dir, ©_dir).unwrap();
556
557 assert!(copy_dir.exists());
558 assert!(copy_dir.join("file1.txt").exists());
559 assert!(copy_dir.join("file2.txt").exists());
560
561 assert_eq!(read(©_dir.join("file1.txt")).unwrap(), "Content 1");
562 assert_eq!(read(©_dir.join("file2.txt")).unwrap(), "Content 2");
563
564 cleanup();
565 }
566
567 #[test]
568 fn test_error_handling() {
569 // Test non-existent file
570 let result = read("non_existent_file.txt");
571 assert!(matches!(result, Err(FileError::Io(_))));
572
573 // Test deleting non-existent file
574 let result = delete("non_existent_file.txt");
575 assert!(matches!(result, Err(FileError::Io(_))));
576 }
577}