Skip to Content
Course content

101: Directory Traversal with read_dir

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start poking around the std::fs module, read_dir seems straightforward enough. You give it a path, it gives you an iterator. But there's a specific friction point in Rust's API here that trips up almost everyone: the double Result. You aren't just dealing with the possibility that the directory doesn't exist; you're dealing with the possibility that individual entries inside that directory might be unreadable due to permission changes or disk glitches while you're iterating.

The instinct to unwrap everything

If we're building a simple tool—say, a utility that scans a folder for .log files to aggregate them—your first instinct might be to just loop through and unwrap. It feels cleaner in the code, and you might think, "I know these files are there, why bother with the overhead?"

use std::fs;

fn get_logs_naive(path: &str) {
    let entries = fs::read_dir(path).unwrap();

    for entry in entries {
        // This is where the danger hides
        let entry = entry.unwrap(); 
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) == Some("log") {
            println!("Found log: {:?}", path);
        }
    }
}

This works fine in a controlled environment. But in production, this is a landmine. If the OS encounters a single file it can't access—maybe a system file that got dropped in there or a locked file—the whole program panics and dies. I've seen entire data pipelines crash because one corrupted inode in a directory of ten thousand files triggered an unwrap(). You've essentially tied the stability of your entire application to the perfect health of the filesystem.

Handling the noise gracefully

The professional way to handle this is to acknowledge that directory traversal is inherently "noisy." You don't want one bad entry to kill the process; you just want to skip it or log a warning. I usually lean toward filter_map here. It allows us to flatten that nested Result and discard the errors in one fluid motion.

use std::fs;
use std::path::PathBuf;

fn get_logs_robust(path: &str) -> Vec {
    fs::read_dir(path)
        .expect("Failed to open directory") // The directory itself MUST exist
        .filter_map(|entry| entry.ok())     // Silently skip entries we can't read
        .map(|entry| entry.path())
        .filter(|path| {
            path.extension()
                .and_then(|s| s.to_str()) == Some("log")
        })
        .collect()
}

Notice the trade-off here. We are still using expect on the initial read_dir call because if the root directory is missing, the function can't do its job. However, inside the loop, we use entry.ok(). This converts the Result<DirEntry, Error> into an Option<DirEntry>. When filter_map sees a None, it simply ignores it and moves to the next file.

Why this matters for performance

You might wonder if this functional approach is slower than a for loop. In Rust, generally, no. The compiler is incredibly good at optimizing these iterator chains into the same machine code as a loop. The real win is in the mental model: you're explicitly defining what constitutes a "valid" entry. You've separated the critical failure (directory missing) from the expected noise (unreadable files). I've found that this pattern prevents a huge category of "it worked on my machine" bugs that only appear when the code hits a real-world server with restrictive permissions.




📋 Practical Task

Build a Config File Finder

Write a function called find_config_files that takes a directory path as a string slice. The function should scan the directory and return a Vec<PathBuf> containing only the files that end with the .toml or .conf extensions.

Your implementation must meet these criteria:

  • It must not panic if it encounters a file it doesn't have permission to read (it should simply skip it).
  • It should handle the initial directory opening error by returning an empty Vec rather than panicking.
  • It must correctly filter for both extensions using a case-sensitive check.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.