fs_err/lib.rs
1/*!
2fs-err is a drop-in replacement for [`std::fs`][std::fs] that provides more
3helpful messages on errors. Extra information includes which operations was
4attempted and any involved paths.
5
6# Error Messages
7
8Using [`std::fs`][std::fs], if this code fails:
9
10```no_run
11# use std::fs::File;
12let file = File::open("does not exist.txt")?;
13# Ok::<(), std::io::Error>(())
14```
15
16The error message that Rust gives you isn't very useful:
17
18```txt
19The system cannot find the file specified. (os error 2)
20```
21
22...but if we use fs-err instead, our error contains more actionable information:
23
24```txt
25failed to open file `does not exist.txt`: The system cannot find the file specified. (os error 2)
26```
27
28# Usage
29
30fs-err's API is the same as [`std::fs`][std::fs], so migrating code to use it is easy.
31
32```no_run
33// use std::fs;
34use fs_err as fs;
35
36let contents = fs::read_to_string("foo.txt")?;
37
38println!("Read foo.txt: {}", contents);
39
40# Ok::<(), std::io::Error>(())
41```
42
43fs-err uses [`std::io::Error`][std::io::Error] for all errors. This helps fs-err
44compose well with traits from the standard library like
45[`std::io::Read`][std::io::Read] and crates that use them like
46[`serde_json`][serde_json]:
47
48```no_run
49use fs_err::File;
50
51let file = File::open("my-config.json")?;
52
53// If an I/O error occurs inside serde_json, the error will include a file path
54// as well as what operation was being performed.
55let decoded: Vec<String> = serde_json::from_reader(file)?;
56
57println!("Program config: {:?}", decoded);
58
59# Ok::<(), Box<dyn std::error::Error>>(())
60```
61
62# Feature flags
63
64* `expose_original_error`: when enabled, the [`Error::source()`](https://doc.rust-lang.org/stable/std/error/trait.Error.html#method.source) method of errors returned by this crate return the original `io::Error`. To avoid duplication in error messages,
65 this also suppresses printing its message in their `Display` implementation, so make sure that you are printing the full error chain.
66* `debug`: Debug filesystem errors faster by exposing more information. When a filesystem command
67 fails, the error message might say "file does not exist." But it won't say **why** it doesn't exist.
68 Perhaps the programmer misspelled the filename, perhaps that directory doesn't exist, or if it does,
69 but the current user doesn't have permissions to see the contents. This feature analyzes the filesystem
70 to output various "facts" that will help a developer debug the root of the current error.
71 * Warning: Exposes filesystem metadata. This feature exposes additional metadata about your filesystem
72 such as directory contents and permissions, which may be sensitive. Only enable `debug` when
73 error messages won't be displayed to the end user, or they have access to filesystem metadata some
74 other way.
75 * Warning: This may slow down your program. This feature will trigger additional filesystem calls when
76 errors occur, which may cause performance issues. Do not use if filesystem errors are common on a
77 performance-sensitive "hotpath." Use in scenarios where developer hours are more expensive than
78 compute time.
79 * To mitigate performance and security concerns, consider only enabling this feature in `dev-dependencies`:
80 * Requires Rust 1.79 or later
81
82```toml
83[dev-dependencies]
84fs-err = { features = ["debug"] }
85```
86
87To use with the `tokio` feature, use `debug_tokio`:
88
89```toml
90[dependencies]
91fs-err = { features = ["debug_tokio", "tokio"] }
92```
93
94# Minimum Supported Rust Version
95
96The oldest rust version this crate is tested on is **1.40**.
97
98This crate will generally be conservative with rust version updates. It uses the [`autocfg`](https://crates.io/crates/autocfg) crate to allow wrapping new APIs without incrementing the MSRV.
99
100If the `tokio` feature is enabled, this crate will inherit the MSRV of the selected [`tokio`](https://crates.io/crates/tokio) version.
101
102[std::fs]: https://doc.rust-lang.org/stable/std/fs/
103[std::io::Error]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
104[std::io::Read]: https://doc.rust-lang.org/stable/std/io/trait.Read.html
105[serde_json]: https://crates.io/crates/serde_json
106*/
107
108#![doc(html_root_url = "https://docs.rs/fs-err/3.2.0")]
109#![deny(missing_debug_implementations, missing_docs)]
110#![cfg_attr(docsrs, feature(doc_cfg))]
111
112mod dir;
113mod errors;
114mod file;
115mod open_options;
116pub mod os;
117mod path;
118#[cfg(feature = "tokio")]
119#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
120pub mod tokio;
121
122use std::fs;
123use std::io::{self, Read, Write};
124use std::path::{Path, PathBuf};
125
126use errors::{Error, ErrorKind, SourceDestError, SourceDestErrorKind};
127
128pub use dir::*;
129pub use file::*;
130pub use open_options::OpenOptions;
131pub use path::PathExt;
132
133/// Read the entire contents of a file into a bytes vector.
134///
135/// Wrapper for [`fs::read`](https://doc.rust-lang.org/stable/std/fs/fn.read.html).
136pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
137 let path = path.as_ref();
138 let mut file = file::open(path).map_err(|err_gen| err_gen(path.to_path_buf()))?;
139 let mut bytes = Vec::with_capacity(initial_buffer_size(&file));
140 file.read_to_end(&mut bytes)
141 .map_err(|err| Error::build(err, ErrorKind::Read, path))?;
142 Ok(bytes)
143}
144
145/// Read the entire contents of a file into a string.
146///
147/// Wrapper for [`fs::read_to_string`](https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html).
148pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
149 let path = path.as_ref();
150 let mut file = file::open(path).map_err(|err_gen| err_gen(path.to_path_buf()))?;
151 let mut string = String::with_capacity(initial_buffer_size(&file));
152 file.read_to_string(&mut string)
153 .map_err(|err| Error::build(err, ErrorKind::Read, path))?;
154 Ok(string)
155}
156
157/// Write a slice as the entire contents of a file.
158///
159/// Wrapper for [`fs::write`](https://doc.rust-lang.org/stable/std/fs/fn.write.html).
160pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
161 let path = path.as_ref();
162 file::create(path)
163 .map_err(|err_gen| err_gen(path.to_path_buf()))?
164 .write_all(contents.as_ref())
165 .map_err(|err| Error::build(err, ErrorKind::Write, path))
166}
167
168/// Copies the contents of one file to another. This function will also copy the
169/// permission bits of the original file to the destination file.
170///
171/// Wrapper for [`fs::copy`](https://doc.rust-lang.org/stable/std/fs/fn.copy.html).
172pub fn copy<P, Q>(from: P, to: Q) -> io::Result<u64>
173where
174 P: AsRef<Path>,
175 Q: AsRef<Path>,
176{
177 let from = from.as_ref();
178 let to = to.as_ref();
179 fs::copy(from, to)
180 .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::Copy, from, to))
181}
182
183/// Creates a new, empty directory at the provided path.
184///
185/// Wrapper for [`fs::create_dir`](https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html).
186pub fn create_dir<P>(path: P) -> io::Result<()>
187where
188 P: AsRef<Path>,
189{
190 let path = path.as_ref();
191 fs::create_dir(path).map_err(|source| Error::build(source, ErrorKind::CreateDir, path))
192}
193
194/// Recursively create a directory and all of its parent components if they are missing.
195///
196/// Wrapper for [`fs::create_dir_all`](https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html).
197pub fn create_dir_all<P>(path: P) -> io::Result<()>
198where
199 P: AsRef<Path>,
200{
201 let path = path.as_ref();
202 fs::create_dir_all(path).map_err(|source| Error::build(source, ErrorKind::CreateDir, path))
203}
204
205/// Removes an empty directory.
206///
207/// Wrapper for [`fs::remove_dir`](https://doc.rust-lang.org/stable/std/fs/fn.remove_dir.html).
208pub fn remove_dir<P>(path: P) -> io::Result<()>
209where
210 P: AsRef<Path>,
211{
212 let path = path.as_ref();
213 fs::remove_dir(path).map_err(|source| Error::build(source, ErrorKind::RemoveDir, path))
214}
215
216/// Removes a directory at this path, after removing all its contents. Use carefully!
217///
218/// Wrapper for [`fs::remove_dir_all`](https://doc.rust-lang.org/stable/std/fs/fn.remove_dir_all.html).
219pub fn remove_dir_all<P>(path: P) -> io::Result<()>
220where
221 P: AsRef<Path>,
222{
223 let path = path.as_ref();
224 fs::remove_dir_all(path).map_err(|source| Error::build(source, ErrorKind::RemoveDir, path))
225}
226
227/// Removes a file from the filesystem.
228///
229/// Wrapper for [`fs::remove_file`](https://doc.rust-lang.org/stable/std/fs/fn.remove_file.html).
230pub fn remove_file<P>(path: P) -> io::Result<()>
231where
232 P: AsRef<Path>,
233{
234 let path = path.as_ref();
235 fs::remove_file(path).map_err(|source| Error::build(source, ErrorKind::RemoveFile, path))
236}
237
238/// Given a path, query the file system to get information about a file, directory, etc.
239///
240/// Wrapper for [`fs::metadata`](https://doc.rust-lang.org/stable/std/fs/fn.metadata.html).
241pub fn metadata<P: AsRef<Path>>(path: P) -> io::Result<fs::Metadata> {
242 let path = path.as_ref();
243 fs::metadata(path).map_err(|source| Error::build(source, ErrorKind::Metadata, path))
244}
245
246/// Returns `Ok(true)` if the path points at an existing entity.
247///
248/// Wrapper for [`fs::exists`](https://doc.rust-lang.org/stable/std/fs/fn.exists.html).
249#[cfg(rustc_1_81)]
250pub fn exists<P: AsRef<Path>>(path: P) -> io::Result<bool> {
251 let path = path.as_ref();
252 fs::exists(path).map_err(|source| Error::build(source, ErrorKind::FileExists, path))
253}
254
255/// Returns the canonical, absolute form of a path with all intermediate components
256/// normalized and symbolic links resolved.
257///
258/// Wrapper for [`fs::canonicalize`](https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html).
259pub fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
260 let path = path.as_ref();
261 fs::canonicalize(path).map_err(|source| Error::build(source, ErrorKind::Canonicalize, path))
262}
263
264/// Creates a new hard link on the filesystem.
265///
266/// Wrapper for [`fs::hard_link`](https://doc.rust-lang.org/stable/std/fs/fn.hard_link.html).
267pub fn hard_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {
268 let src = src.as_ref();
269 let dst = dst.as_ref();
270 fs::hard_link(src, dst)
271 .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::HardLink, src, dst))
272}
273
274/// Reads a symbolic link, returning the file that the link points to.
275///
276/// Wrapper for [`fs::read_link`](https://doc.rust-lang.org/stable/std/fs/fn.read_link.html).
277pub fn read_link<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
278 let path = path.as_ref();
279 fs::read_link(path).map_err(|source| Error::build(source, ErrorKind::ReadLink, path))
280}
281
282/// Rename a file or directory to a new name, replacing the original file if to already exists.
283///
284/// Wrapper for [`fs::rename`](https://doc.rust-lang.org/stable/std/fs/fn.rename.html).
285pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
286 let from = from.as_ref();
287 let to = to.as_ref();
288 fs::rename(from, to)
289 .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::Rename, from, to))
290}
291
292/// Wrapper for [`fs::soft_link`](https://doc.rust-lang.org/stable/std/fs/fn.soft_link.html).
293#[deprecated = "replaced with std::os::unix::fs::symlink and \
294std::os::windows::fs::{symlink_file, symlink_dir}"]
295pub fn soft_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {
296 let src = src.as_ref();
297 let dst = dst.as_ref();
298 #[allow(deprecated)]
299 fs::soft_link(src, dst)
300 .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::SoftLink, src, dst))
301}
302
303/// Query the metadata about a file without following symlinks.
304///
305/// Wrapper for [`fs::symlink_metadata`](https://doc.rust-lang.org/stable/std/fs/fn.symlink_metadata.html).
306pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> io::Result<fs::Metadata> {
307 let path = path.as_ref();
308 fs::symlink_metadata(path)
309 .map_err(|source| Error::build(source, ErrorKind::SymlinkMetadata, path))
310}
311
312/// Changes the permissions found on a file or a directory.
313///
314/// Wrapper for [`fs::set_permissions`](https://doc.rust-lang.org/stable/std/fs/fn.set_permissions.html).
315pub fn set_permissions<P: AsRef<Path>>(path: P, perm: fs::Permissions) -> io::Result<()> {
316 let path = path.as_ref();
317 fs::set_permissions(path, perm)
318 .map_err(|source| Error::build(source, ErrorKind::SetPermissions, path))
319}
320
321fn initial_buffer_size(file: &std::fs::File) -> usize {
322 file.metadata().map(|m| m.len() as usize + 1).unwrap_or(0)
323}
324
325pub(crate) use private::Sealed;
326mod private {
327 pub trait Sealed {}
328
329 impl Sealed for crate::File {}
330 impl Sealed for std::path::Path {}
331 impl Sealed for crate::OpenOptions {}
332}