Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
100: std::fs Functions: read_to_string, write, metadata
I see this happen all the time when people first start interacting with the filesystem in Rust. They treat std::fs::write like a logging function—something that just keeps adding data to the end of a file. But that's not how it works.
use std::fs;
fn main() {
let logs = vec!["User logged in", "Database connection established", "Payment processed"];
for log in logs {
// I'm trying to save each log entry to a file
fs::write("app.log", format!("{}\n", log)).expect("Unable to write file");
}
println!("Logs saved successfully!");
}
The Case of the Disappearing Logs
If you run the code above and then open app.log, you'll notice something frustrating: only the very last entry ("Payment processed") is there. The first two entries have vanished into the void.
The problem is that fs::write is a "convenience" function. It's great for quick tasks, but it has a very specific behavior: it opens the file in write-only mode and truncates it. Truncating is a fancy way of saying "wipe everything currently in this file to zero bytes before writing the new data." Every time the loop iterated, Rust effectively deleted the previous log entry to make room for the new one.
Using the Right Tool for the Job
Now, if your goal is to replace the entire contents of a file (like updating a config.json), fs::write is exactly what you want. It's clean and handles the file opening and closing in one go. But if you need to preserve data, you'd move toward std::fs::OpenOptions. For this lesson, though, let's focus on the three heavy lifters: write, read_to_string, and metadata.
Once you've written a file, you usually need to get that data back. fs::read_to_string is the counterpart to fs::write. It reads the entire file into a String. It's incredibly handy, but it's also dangerous if you don't know how big the file is. If you accidentally try to read_to_string a 10GB log file, your program will likely crash with an "out of memory" error because it's trying to shove the entire file into RAM.
Preventing Crashes with Metadata
This is where std::fs::metadata comes in. Before you commit to reading a file into memory, you should check its properties. metadata returns a Metadata struct that tells you the file size, permissions, and whether the path is actually a file or a directory.
Here is how I would actually handle a "read" operation safely:
use std::fs;
fn main() -> std::io::Result<()> {
let path = "app.log";
// First, check the metadata
let meta = fs::metadata(path)?;
let file_size = meta.len();
println!("File size is {} bytes", file_size);
// Only read the file if it's under 1MB to avoid memory issues
if file_size < 1_000_000 {
let content = fs::read_to_string(path)?;
println!("File content: {}", content);
} else {
println!("File is too large to read safely into a string.");
}
Ok(())
}
I like this approach because it's defensive. In a real production environment, you can't trust that the files on disk are the size you expect. A user might have accidentally piped a massive binary blob into your config file, and checking metadata first saves your app from a catastrophic crash.
📋 Practical Task
Build a File-Based Backup Validator
Your task is to create a small utility that manages a "configuration" file. The program should perform the following logic:
- Create a file named
config.txtand write the string"version=1.0\nstatus=active"to it usingstd::fs::write. - Use
std::fs::metadatato verify that the file exists and has a size greater than 0 bytes. - If the validation passes, use
std::fs::read_to_stringto read the content and write that same content into a new file calledconfig.bak. - If the metadata check fails (e.g., the file is empty), print an error message and do not create the backup.
Ensure you handle the std::io::Result for all filesystem operations using ? or expect().
There are no comments for now.