Skip to Content
Course content

59: Working with Files and Paths

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

I've spent way too many hours of my career debugging "File Not Found" errors that only happened on a teammate's machine. Usually, it's because someone hardcoded a path using forward slashes on Windows, or assumed the working directory was the project root. Let's dive into how Rust handles this so you don't have to make those same mistakes.

Imagine we're building a simple save-game system. We need to find a folder called saves and read a file named player_stats.txt.

Wait, where is my file?

My first instinct is usually to just pass a string to File::open. It feels fast. Let's try that:

use std::fs::File;
use std::io::Read;

fn main() -> std::io::Result<()> {
    // I'm assuming the file is right here in the project root
    let mut file = File::open("saves/player_stats.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    println!("Stats: {}", contents);
    Ok(())
}

This works fine on my macOS machine. But here's the problem: "saves/player_stats.txt" is just a string. If I ship this to a user on Windows, that forward slash might cause issues depending on the environment, and more importantly, it's completely rigid. What if the saves are actually stored in the user's home directory or a specific app-data folder? We need something more robust than a &str.

The String Path Trap

Rust gives us Path and PathBuf for this. I like to think of Path as a slice (like &str) and PathBuf as an owned, mutable string (like String). If you're building a path dynamically, you want PathBuf.

Let's rewrite this to be more flexible. I'll start with a base directory and append the filename to it. If I try to just use string concatenation, I'm back to the slash problem:

use std::path::PathBuf;

let mut path = PathBuf::from("saves");
path.push("player_stats.txt"); 
// Now 'path' is saves/player_stats.txt (or saves\player_stats.txt on Windows)

The .push() method is the secret sauce here. It doesn't just tack on characters; it understands the operating system's path separators. It handles the "glue" between folders for us. I've found that whenever I'm tempted to use format!("{}/{}", dir, file), I need to stop myself and use PathBuf instead.

Building paths without the headache

Now, let's get realistic. Your app shouldn't just guess where the files are. What if the folder doesn't even exist yet? If I try to open a file in a directory that isn't there, Rust will throw an io::Error. Let's add a check and create the directory if it's missing.

use std::fs;
use std::path::Path;

fn ensure_save_dir(dir: &Path) -> std::io::Result<()> {
    if !dir.exists() {
        println!("Directory missing, creating it...");
        fs::create_dir_all(dir)?;
    }
    Ok(())
}

Notice I used &Path in the argument. This is a pro tip: if your function only needs to read the path, take a reference to Path. This allows the caller to pass in either a &PathBuf or a &str, because Path implements AsRef<Path>. It makes your API much friendlier.

Actually getting the data out

Finally, let's put it all together. I want to read the file, but I don't want the program to crash if the file is empty or missing—I want it to return a default set of stats. This is where fs::read_to_string comes in handy; it's a convenient wrapper that handles opening and reading in one go, so we don't have to manually manage the File handle.

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

fn load_game_stats() -> String {
    let mut path = PathBuf::from("saves");
    path.push("player_stats.txt");

    // We use match here because we want a default value instead of crashing
    match fs::read_to_string(&path) {
        Ok(content) => content,
        Err(_) => {
            println!("No save found, using defaults.");
            "Health: 100, Level: 1".to_string()
        }
    }
}

fn main() {
    let stats = load_game_stats();
    println!("Current Stats: {}", stats);
}

One last thing: if you're dealing with massive files, read_to_string is a trap because it loads the entire file into memory. For a small save file, it's perfect. For a 2GB log file, you'd want to use std::io::BufReader to read it line-by-line. But for our purposes, this is the cleanest way to get the job done.




📋 Practical Task

Exercise: The Project Log Archiver

Your goal is to create a tool that organizes logs. Write a program that does the following:

  • Defines a base directory named logs and a filename named session.log.
  • Uses PathBuf to construct the full path to that file.
  • Checks if the logs directory exists; if not, it creates it.
  • Checks if session.log exists. If it does, it reads the content and prints "Existing log found: [content]".
  • If session.log does not exist, it creates the file and writes "Session started" into it using std::fs::write.

Requirement: Ensure you use &Path for any helper functions to keep the code flexible!

Rating
0 0

There are no comments for now.

to be the first to leave a comment.