Checking for file existence with PathBuf to ensure unique filenames

Chris Biscardi
InstructorChris Biscardi
Share this video with your friends

Social Share Links

Send Tweet

PathBuf lets us check to see if a file exists before writing to it, so we can take advantage of that to make sure that we don't overwrite a file that already exists, even if the user has indicated a title that would overwrite another file.

let mut i: usize = 0;
loop {
    let dest_filename = format!(
        "{}{}",
        filename,
        if i == 0 {
            "".to_string()
        } else {
            i.to_string()
        }
    );
    let mut dest = garden_path.join(dest_filename);
    dest.set_extension("md");
    if dest.exists() {
        i = i + 1;
    } else {
        fs::rename(garden_tmpfile, &dest)?;
        break;
    }
}

Chris Biscardi: [0:01] The next thing we want to do is take the filename, which we can see rust-analyzer is treating as a result string report, which isn't what we want. If there's an error here, we want that error to propagate back through the function, so we use a question mark to do that.

[0:19] Then we're going to write a loop. We used a loop already, so I won't explain too much about that. We're using this loop to format a new file name that is unique. We need to do this because, if there's a file with the same name as the user wants, we don't want to overwrite that file.

[0:38] The approach we're going to take is, if we have a file named hello.md that already exists and the user wants to write out hello.md again, we're going to give them hello1.md, which preserves both files. We can start out with the number . This is going to be a mutable variable because it will increment until we hit a number that gives us a unique file name.

[1:04] We'll use format again with two display formatters. One will be the file name that we got from the user. The other one will be a number. In the case of , we won't add anything to the file name. We'll make this an empty string.

[1:18] In the case of it being greater than , we want to add the number, so we'll stringify the number then put it into the format. This gives us a destination file name of hello1. We're going to finish off the rest of the file name by joining the garden path to the destination file name. This gives us a mutable path buffer on which we can set the extension of this file.

[1:43] At this point in the code, what we have is a path to the garden path and a file of hello1.md. Path buffers include a function called exists that allows us to detect if this file already exists.

[1:59] If we have hello.md here, test that exists will return true and we'll increment i and loop back through again trying with hello1. If it doesn't exist, that means we can take the temp file that we had before and rename it into the official location in the garden path.

[2:20] Note that we have to import fs which is from std::fs. Rust-analyzer helps us out a little bit, but we're getting used here after move, when we try to use the filepath on line 65. This is because all the way back up here, when we got the filepath from the builder, we passed it edit_file.

[2:41] These are the same semantics that we came across earlier in this course which are called move semantics. Move semantics are the default in Rust. In this case, we're moving filepath into this edit_file function.

[2:54] To use copy semantics instead, we can pass a reference into edit_file and if we save, we can see that later on, we can use the filepath in our rename function. Our code checks and the test passed, so let's give it a whirl.

[3:12] Also note that we're returning OK(unit) if everything that we've written succeeds. We'll run our write command with VS Code. We'll give it some title and some content. We'll save and exit the file, which returns control to our program. The current title is "Some Title," and that's going to be OK for me this time, so I'll hit Enter.

[3:35] If we look in our garden, we should see a file with some title with the content, a heading of "Some Title" and on line 3, some content. This is, in fact, what we see. Our program has succeeded. Let's review what we've done so far.

[3:52] In main.rs, we've installed color_eyre to give better error messages. We get the options using structopt from_args, and then we debug that out. We no longer need this debug, so we'll remove it. If the user passes in a garden path, we'll use that. If not, we'll get a default garden path. If anything fails, we'll wrap the error with additional context for the user.

[4:18] Finally, we'll match an Opt command and use destructuring to pass the garden path and the title into our write function. Our write function lives in a file called write.rs which we're re-exporting from lib.rs. We use a builder to build up the file with the appropriate extension and a couple of other configurations.

[4:43] We write out a constant to the file as a template so when the user opens a file, they have something to work with. We pass the control to the user using a filepath we just created. When they're done editing the file in their editor of choice, we make sure to [inaudible] back to the beginning of the file and read it into a string.

[5:04] Given the fact that our user can choose to pass in a title or not, we combine multiple options together and try to find a title that we can use. If we do find one using either our heuristic or from the user, we ask the user to confirm at this point because they've been writing for a while, and maybe they change their mind about what they're writing about. If we can't find one and they didn't pass one in, we ask them for one.

[5:29] Finally, once we have a file name and the contents of the file, we find the unique file name that we can write out to disk without conflicting with other files, finally renaming our temporary file in the garden path to an actual file in the garden path.