How to copy or move a file
Short answer: fs::copy("from", "to") copies a file and leaves the original where it was. fs::rename("from", "to") moves it — the original name is gone afterwards. Both take a from-path and a to-path and return io::Result. The one trap: fs::rename refuses to cross a filesystem boundary, so a move onto another drive is copy-then-remove.
From a fresh project — cargo new copy-move-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. Each block creates the files it works on, so it runs on its own and in any order. Paths are relative to the directory you run from, which under cargo run is the project root.
Copying with fs::copy
Section titled “Copying with fs::copy”use std::fs;
fn main() -> std::io::Result<()> { fs::write("notes.txt", "meeting at ten\n")?; fs::write("notes-backup.txt", "an old backup\n")?; let bytes = fs::copy("notes.txt", "notes-backup.txt")?; println!("{bytes} bytes copied"); println!("original untouched: {:?}", fs::read_to_string("notes.txt")?); println!("target replaced: {:?}", fs::read_to_string("notes-backup.txt")?); Ok(())}15 bytes copiedoriginal untouched: "meeting at ten\n"target replaced: "meeting at ten\n"The return value is the number of bytes copied, which you rarely need except for a progress line. Three behaviours are worth knowing:
- An existing target is overwritten, not merged or rejected — the old backup contents above are gone after the copy.
- The source’s permission bits come along: a file with mode 755 produces a copy with mode 755, even when it replaces an existing target.
- Both paths must be files.
fs::copyon a directory returns an error rather than recursing.
Moving with fs::rename
Section titled “Moving with fs::rename”use std::fs;
fn main() -> std::io::Result<()> { fs::write("draft.txt", "first draft\n")?; fs::rename("draft.txt", "chapter-one.txt")?; println!("old name still there: {}", fs::metadata("draft.txt").is_ok()); println!("new name has it: {:?}", fs::read_to_string("chapter-one.txt")?); Ok(())}old name still there: falsenew name has it: "first draft\n"Nothing is read or rewritten: the rename is one directory-entry update, which is why it is instant on a 10 GB file and why the atomic-write page uses it as its commit point. An existing target is replaced in the same operation. Directories rename the same way as files, with one condition the next section is about.
Crossing a filesystem
Section titled “Crossing a filesystem”A rename does not move bytes — it re-points a directory entry, and a directory entry cannot reach into another filesystem. The standard library documents that fs::rename fails when the two paths are on different filesystems. On this machine, renaming a file from the startup volume to an external USB drive returned exactly:
Cross-device link (os error 18)Its io::ErrorKind is CrossesDevices, and the error kind is the thing to match on — the message wording varies by platform, the kind does not. When a move has to cross that line, fall back to copy and remove:
use std::fs;use std::io::{self, ErrorKind};
fn main() -> io::Result<()> { fs::write("report.txt", "final numbers\n")?; move_file("report.txt", "report-archived.txt")?; println!("original removed: {}", fs::metadata("report.txt").is_err()); println!("arrived: {:?}", fs::read_to_string("report-archived.txt")?); Ok(())}
fn move_file(from: &str, to: &str) -> io::Result<()> { match fs::rename(from, to) { Ok(()) => Ok(()), Err(e) if e.kind() == ErrorKind::CrossesDevices => { fs::copy(from, to)?; fs::remove_file(from) } Err(e) => Err(e), }}original removed: truearrived: "final numbers\n"Within one filesystem the rename path runs and the fallback never fires, so the move stays atomic. Across the boundary it degrades to copy-then-remove, which is not atomic — a crash between the two steps leaves two copies. That is the price of crossing volumes; fs::copy itself has no such restriction, since it reads and writes bytes rather than re-pointing entries.
Related
Section titled “Related”- How to write a file atomically — the temp-file-plus-rename pattern that relies on rename being atomic
- How to write to a file — the one-call truncating write
- Reading and writing files — the rest of the chapter