Skip to main content

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, &copy_path).unwrap();
527        assert!(copy_path.exists());
528        assert_eq!(read(&original_path).unwrap(), read(&copy_path).unwrap());
529
530        // Move
531        mv(&copy_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, &copy_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(&copy_dir.join("file1.txt")).unwrap(), "Content 1");
562        assert_eq!(read(&copy_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}