From: Roy7Kim Date: Mon, 13 Mar 2023 09:09:26 +0000 (+0900) Subject: Import fs-err 2.9.0 X-Git-Tag: upstream/2.9.0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=b4d868724f81c291fa772c9dbc01dd5e63ce1ddb;p=platform%2Fupstream%2Frust-fs-err.git Import fs-err 2.9.0 --- b4d868724f81c291fa772c9dbc01dd5e63ce1ddb diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..1114130 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "3f59ce2d8a8fc1bbd267eac6848fb9d683542769" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aa4079e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# fs-err Changelog + +## 2.9.0 + +* Add wrappers for [`tokio::fs`](https://docs.rs/tokio/latest/tokio/fs/index.html) ([#40](https://github.com/andrewhickman/fs-err/pull/40)). + +## 2.8.1 + +* Fixed docs.rs build + +## 2.8.0 + +* Implement I/O safety traits (`AsFd`/`AsHandle`, `Into`/`Into`) for file. This feature requires Rust 1.63 or later and is gated behind the `io_safety` feature flag. ([#39](https://github.com/andrewhickman/fs-err/pull/39)) + +## 2.7.0 + +* Implement `From for std::fs::File` ([#38](https://github.com/andrewhickman/fs-err/pull/38)) + +## 2.6.0 + +* Added [`File::into_parts`](https://docs.rs/fs-err/2.6.0/fs_err/struct.File.html#method.into_parts) and [`File::file_mut`](https://docs.rs/fs-err/2.6.0/fs_err/struct.File.html#method.file_mut) to provide more access to the underlying `std::fs::File`. +* Fixed some typos in documention ([#33](https://github.com/andrewhickman/fs-err/pull/33)) + +## 2.5.0 +* Added `symlink` for unix platforms +* Added `symlink_file` and `symlink_dir` for windows +* Implemented os-specific extension traits for `File` + - `std::os::unix::io::{AsRawFd, IntoRawFd}` + - `std::os::windows::io::{AsRawHandle, IntoRawHandle}` + - Added trait wrappers for `std::os::{unix, windows}::fs::FileExt` and implemented them for `fs_err::File` +* Implemented os-specific extension traits for `OpenOptions` + - Added trait wrappers for `std::os::{unix, windows}::fs::OpenOptionsExt` and implemented them for `fs_err::OpenOptions` +* Improved compile times by converting arguments early and forwarding only a small number of types internally. There will be a slight performance hit only in the error case. +* Reduced trait bounds on generics from `AsRef + Into` to either `AsRef` or `Into`, making the functions more general. + +## 2.4.0 +* Added `canonicalize`, `hard link`, `read_link`, `rename`, `symlink_metadata` and `soft_link`. ([#25](https://github.com/andrewhickman/fs-err/pull/25)) +* Added aliases to `std::path::Path` via extension trait ([#26](https://github.com/andrewhickman/fs-err/pull/26)) +* Added `OpenOptions` ([#27](https://github.com/andrewhickman/fs-err/pull/27)) +* Added `set_permissions` ([#28](https://github.com/andrewhickman/fs-err/pull/28)) + +## 2.3.0 +* Added `create_dir` and `create_dir_all`. ([#19](https://github.com/andrewhickman/fs-err/pull/19)) +* Added `remove_file`, `remove_dir`, and `remove_dir_all`. ([#16](https://github.com/andrewhickman/fs-err/pull/16)) + +## 2.2.0 +* Added `metadata`. ([#15](https://github.com/andrewhickman/fs-err/pull/15)) + +## 2.1.0 +* Updated crate-level documentation. ([#8](https://github.com/andrewhickman/fs-err/pull/8)) +* Added `read_dir`, `ReadDir`, and `DirEntry`. ([#9](https://github.com/andrewhickman/fs-err/pull/9)) + +## 2.0.1 (2020-02-22) +* Added `copy`. ([#7](https://github.com/andrewhickman/fs-err/pull/7)) + +## 2.0.0 (2020-02-19) +* Removed custom error type in favor of `std::io::Error`. ([#2](https://github.com/andrewhickman/fs-err/pull/2)) + +## 1.0.1 (2020-02-15) +* Fixed bad documentation link in `Cargo.toml`. + +## 1.0.0 (2020-02-15) +* No changes from 0.1.2. + +## 0.1.2 (2020-02-10) +* Added `Error::cause` implementation for `fs_err::Error`. + +## 0.1.1 (2020-02-05) +* Added wrappers for `std::fs::*` functions. + +## 0.1.0 (2020-02-02) +* Initial release, containing a wrapper around `std::fs::File`. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7a7194 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,59 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "fs-err" +version = "2.9.0" +authors = ["Andrew Hickman "] +exclude = [ + ".github", + ".gitignore", + "README.tpl", +] +description = "A drop-in replacement for std::fs with more helpful error messages." +documentation = "https://docs.rs/fs-err" +readme = "README.md" +categories = [ + "command-line-interface", + "filesystem", +] +license = "MIT/Apache-2.0" +repository = "https://github.com/andrewhickman/fs-err" + +[package.metadata.release] +tag-name = "{{version}}" +sign-tag = true + +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = 'html_root_url = "https://docs\.rs/fs-err/.*?"' +replace = "html_root_url = \"https://docs.rs/fs-err/{{version}}\"" +exactly = 1 + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[dependencies.tokio] +version = "1.21" +features = ["fs"] +optional = true +default_features = false + +[dev-dependencies.serde_json] +version = "1.0.64" + +[features] +io_safety = [] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..b0627e4 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,36 @@ +[package] +name = "fs-err" +description = "A drop-in replacement for std::fs with more helpful error messages." +version = "2.9.0" +authors = ["Andrew Hickman "] +edition = "2018" +repository = "https://github.com/andrewhickman/fs-err" +documentation = "https://docs.rs/fs-err" +categories = ["command-line-interface", "filesystem"] +license = "MIT/Apache-2.0" +readme = "README.md" +exclude = [".github", ".gitignore", "README.tpl"] + +[dependencies] +tokio = { version = "1.21", optional = true, default_features = false, features = ["fs"] } + +[dev-dependencies] +serde_json = "1.0.64" + +[features] +# Adds I/O safety traits introduced in Rust 1.63 +io_safety = [] + +[package.metadata.release] +tag-name = "{{version}}" +sign-tag = true + +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = "html_root_url = \"https://docs\\.rs/fs-err/.*?\"" +replace = "html_root_url = \"https://docs.rs/fs-err/{{version}}\"" +exactly = 1 + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f47c941 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..458723b --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dc5fc1 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ + + +# fs-err + +[![Crates.io](https://img.shields.io/crates/v/fs-err.svg)](https://crates.io/crates/fs-err) +[![GitHub Actions](https://github.com/andrewhickman/fs-err/workflows/CI/badge.svg)](https://github.com/andrewhickman/fs-err/actions?query=workflow%3ACI) + +fs-err is a drop-in replacement for [`std::fs`][std::fs] that provides more +helpful messages on errors. Extra information includes which operations was +attempted and any involved paths. + +## Error Messages + +Using [`std::fs`][std::fs], if this code fails: + +```rust +let file = File::open("does not exist.txt")?; +``` + +The error message that Rust gives you isn't very useful: + +```txt +The system cannot find the file specified. (os error 2) +``` + +...but if we use fs-err instead, our error contains more actionable information: + +```txt +failed to open file `does not exist.txt` + caused by: The system cannot find the file specified. (os error 2) +``` + +## Usage + +fs-err's API is the same as [`std::fs`][std::fs], so migrating code to use it is easy. + +```rust +// use std::fs; +use fs_err as fs; + +let contents = fs::read_to_string("foo.txt")?; + +println!("Read foo.txt: {}", contents); + +``` + +fs-err uses [`std::io::Error`][std::io::Error] for all errors. This helps fs-err +compose well with traits from the standard library like +[`std::io::Read`][std::io::Read] and crates that use them like +[`serde_json`][serde_json]: + +```rust +use fs_err::File; + +let file = File::open("my-config.json")?; + +// If an I/O error occurs inside serde_json, the error will include a file path +// as well as what operation was being performed. +let decoded: Vec = serde_json::from_reader(file)?; + +println!("Program config: {:?}", decoded); + +``` + +[std::fs]: https://doc.rust-lang.org/stable/std/fs/ +[std::io::Error]: https://doc.rust-lang.org/stable/std/io/struct.Error.html +[std::io::Read]: https://doc.rust-lang.org/stable/std/io/trait.Read.html +[serde_json]: https://crates.io/crates/serde_json + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/src/dir.rs b/src/dir.rs new file mode 100644 index 0000000..6efa58d --- /dev/null +++ b/src/dir.rs @@ -0,0 +1,90 @@ +use std::ffi::OsString; +use std::fs; +use std::io; +use std::path::PathBuf; + +use crate::errors::{Error, ErrorKind}; + +/// Wrapper for [`fs::read_dir`](https://doc.rust-lang.org/stable/std/fs/fn.read_dir.html). +pub fn read_dir>(path: P) -> io::Result { + let path = path.into(); + + match fs::read_dir(&path) { + Ok(inner) => Ok(ReadDir { inner, path }), + Err(source) => Err(Error::build(source, ErrorKind::ReadDir, path)), + } +} + +/// Wrapper around [`std::fs::ReadDir`][std::fs::ReadDir] which adds more +/// helpful information to all errors. +/// +/// This struct is created via [`fs_err::read_dir`][fs_err::read_dir]. +/// +/// [std::fs::ReadDir]: https://doc.rust-lang.org/stable/std/fs/struct.ReadDir.html +/// [fs_err::read_dir]: fn.read_dir.html +#[derive(Debug)] +pub struct ReadDir { + inner: fs::ReadDir, + path: PathBuf, +} + +impl Iterator for ReadDir { + type Item = io::Result; + + fn next(&mut self) -> Option { + Some( + self.inner + .next()? + .map_err(|source| Error::build(source, ErrorKind::ReadDir, &self.path)) + .map(|inner| DirEntry { inner }), + ) + } +} + +/// Wrapper around [`std::fs::DirEntry`][std::fs::DirEntry] which adds more +/// helpful information to all errors. +/// +/// [std::fs::DirEntry]: https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html +#[derive(Debug)] +pub struct DirEntry { + inner: fs::DirEntry, +} + +impl DirEntry { + /// Wrapper for [`DirEntry::path`](https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html#method.path). + pub fn path(&self) -> PathBuf { + self.inner.path() + } + + /// Wrapper for [`DirEntry::metadata`](https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html#method.metadata). + pub fn metadata(&self) -> io::Result { + self.inner + .metadata() + .map_err(|source| Error::build(source, ErrorKind::Metadata, self.path())) + } + + /// Wrapper for [`DirEntry::file_type`](https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html#method.file_type). + pub fn file_type(&self) -> io::Result { + self.inner + .file_type() + .map_err(|source| Error::build(source, ErrorKind::Metadata, self.path())) + } + + /// Wrapper for [`DirEntry::file_name`](https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html#method.file_name). + pub fn file_name(&self) -> OsString { + self.inner.file_name() + } +} + +#[cfg(unix)] +mod unix { + use std::os::unix::fs::DirEntryExt; + + use super::*; + + impl DirEntryExt for DirEntry { + fn ino(&self) -> u64 { + self.inner.ino() + } + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..43bc4ba --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,198 @@ +use std::error::Error as StdError; +use std::fmt; +use std::io; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum ErrorKind { + OpenFile, + CreateFile, + CreateDir, + SyncFile, + SetLen, + Metadata, + Clone, + SetPermissions, + Read, + Seek, + Write, + Flush, + ReadDir, + RemoveFile, + RemoveDir, + Canonicalize, + ReadLink, + SymlinkMetadata, + + #[cfg(windows)] + SeekRead, + #[cfg(windows)] + SeekWrite, + + #[cfg(unix)] + ReadAt, + #[cfg(unix)] + WriteAt, +} + +/// Contains an IO error that has a file path attached. +/// +/// This type is never returned directly, but is instead wrapped inside yet +/// another IO error. +#[derive(Debug)] +pub(crate) struct Error { + kind: ErrorKind, + source: io::Error, + path: PathBuf, +} + +impl Error { + pub fn build(source: io::Error, kind: ErrorKind, path: impl Into) -> io::Error { + io::Error::new( + source.kind(), + Self { + kind, + source, + path: path.into(), + }, + ) + } +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + use ErrorKind::*; + + let path = self.path.display(); + + match self.kind { + OpenFile => write!(formatter, "failed to open file `{}`", path), + CreateFile => write!(formatter, "failed to create file `{}`", path), + CreateDir => write!(formatter, "failed to create directory `{}`", path), + SyncFile => write!(formatter, "failed to sync file `{}`", path), + SetLen => write!(formatter, "failed to set length of file `{}`", path), + Metadata => write!(formatter, "failed to query metadata of file `{}`", path), + Clone => write!(formatter, "failed to clone handle for file `{}`", path), + SetPermissions => write!(formatter, "failed to set permissions for file `{}`", path), + Read => write!(formatter, "failed to read from file `{}`", path), + Seek => write!(formatter, "failed to seek in file `{}`", path), + Write => write!(formatter, "failed to write to file `{}`", path), + Flush => write!(formatter, "failed to flush file `{}`", path), + ReadDir => write!(formatter, "failed to read directory `{}`", path), + RemoveFile => write!(formatter, "failed to remove file `{}`", path), + RemoveDir => write!(formatter, "failed to remove directory `{}`", path), + Canonicalize => write!(formatter, "failed to canonicalize path `{}`", path), + ReadLink => write!(formatter, "failed to read symbolic link `{}`", path), + SymlinkMetadata => write!(formatter, "failed to query metadata of symlink `{}`", path), + + #[cfg(windows)] + SeekRead => write!(formatter, "failed to seek and read from `{}`", path), + #[cfg(windows)] + SeekWrite => write!(formatter, "failed to seek and write to `{}`", path), + + #[cfg(unix)] + ReadAt => write!(formatter, "failed to read with offset from `{}`", path), + #[cfg(unix)] + WriteAt => write!(formatter, "failed to write with offset to `{}`", path), + } + } +} + +impl StdError for Error { + fn cause(&self) -> Option<&dyn StdError> { + self.source() + } + + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&self.source) + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum SourceDestErrorKind { + Copy, + HardLink, + Rename, + SoftLink, + + #[cfg(unix)] + Symlink, + + #[cfg(windows)] + SymlinkDir, + #[cfg(windows)] + SymlinkFile, +} + +/// Error type used by functions like `fs::copy` that holds two paths. +#[derive(Debug)] +pub(crate) struct SourceDestError { + kind: SourceDestErrorKind, + source: io::Error, + from_path: PathBuf, + to_path: PathBuf, +} + +impl SourceDestError { + pub fn build( + source: io::Error, + kind: SourceDestErrorKind, + from_path: impl Into, + to_path: impl Into, + ) -> io::Error { + io::Error::new( + source.kind(), + Self { + kind, + source, + from_path: from_path.into(), + to_path: to_path.into(), + }, + ) + } +} + +impl fmt::Display for SourceDestError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let from = self.from_path.display(); + let to = self.to_path.display(); + match self.kind { + SourceDestErrorKind::Copy => { + write!(formatter, "failed to copy file from {} to {}", from, to) + } + SourceDestErrorKind::HardLink => { + write!(formatter, "failed to hardlink file from {} to {}", from, to) + } + SourceDestErrorKind::Rename => { + write!(formatter, "failed to rename file from {} to {}", from, to) + } + SourceDestErrorKind::SoftLink => { + write!(formatter, "failed to softlink file from {} to {}", from, to) + } + + #[cfg(unix)] + SourceDestErrorKind::Symlink => { + write!(formatter, "failed to symlink file from {} to {}", from, to) + } + + #[cfg(windows)] + SourceDestErrorKind::SymlinkFile => { + write!(formatter, "failed to symlink file from {} to {}", from, to) + } + #[cfg(windows)] + SourceDestErrorKind::SymlinkDir => { + write!(formatter, "failed to symlink dir from {} to {}", from, to) + } + } + } +} + +impl StdError for SourceDestError { + fn cause(&self) -> Option<&dyn StdError> { + self.source() + } + + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&self.source) + } +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..fcf54fe --- /dev/null +++ b/src/file.rs @@ -0,0 +1,366 @@ +use std::fs; +use std::io::{self, Read, Seek, Write}; +use std::path::{Path, PathBuf}; + +use crate::errors::{Error, ErrorKind}; + +/// Wrapper around [`std::fs::File`][std::fs::File] which adds more helpful +/// information to all errors. +/// +/// [std::fs::File]: https://doc.rust-lang.org/stable/std/fs/struct.File.html +#[derive(Debug)] +pub struct File { + file: fs::File, + path: PathBuf, +} + +// Opens a std File and returns it or an error generator which only needs the path to produce the error. +// Exists for the `crate::read*` functions so they don't unconditionally build a PathBuf. +pub(crate) fn open(path: &Path) -> Result io::Error> { + fs::File::open(&path).map_err(|err| |path| Error::build(err, ErrorKind::OpenFile, path)) +} + +// like `open()` but for `crate::write` +pub(crate) fn create(path: &Path) -> Result io::Error> { + fs::File::create(&path).map_err(|err| |path| Error::build(err, ErrorKind::CreateFile, path)) +} + +/// Wrappers for methods from [`std::fs::File`][std::fs::File]. +/// +/// [std::fs::File]: https://doc.rust-lang.org/stable/std/fs/struct.File.html +impl File { + /// Wrapper for [`File::open`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.open). + pub fn open

(path: P) -> Result + where + P: Into, + { + let path = path.into(); + match open(&path) { + Ok(file) => Ok(File::from_parts(file, path)), + Err(err_gen) => Err(err_gen(path)), + } + } + + /// Wrapper for [`File::create`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.create). + pub fn create

(path: P) -> Result + where + P: Into, + { + let path = path.into(); + match create(&path) { + Ok(file) => Ok(File::from_parts(file, path)), + Err(err_gen) => Err(err_gen(path)), + } + } + + /// Wrapper for [`OpenOptions::open`](https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html#method.open). + /// + /// This takes [`&std::fs::OpenOptions`](https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html), + /// not [`crate::OpenOptions`]. + #[deprecated = "use fs_err::OpenOptions::open instead"] + pub fn from_options

(path: P, options: &fs::OpenOptions) -> Result + where + P: Into, + { + let path = path.into(); + match options.open(&path) { + Ok(file) => Ok(File::from_parts(file, path)), + Err(source) => Err(Error::build(source, ErrorKind::OpenFile, path)), + } + } + + /// Wrapper for [`File::sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all). + pub fn sync_all(&self) -> Result<(), io::Error> { + self.file + .sync_all() + .map_err(|source| self.error(source, ErrorKind::SyncFile)) + } + + /// Wrapper for [`File::sync_data`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_data). + pub fn sync_data(&self) -> Result<(), io::Error> { + self.file + .sync_data() + .map_err(|source| self.error(source, ErrorKind::SyncFile)) + } + + /// Wrapper for [`File::set_len`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.set_len). + pub fn set_len(&self, size: u64) -> Result<(), io::Error> { + self.file + .set_len(size) + .map_err(|source| self.error(source, ErrorKind::SetLen)) + } + + /// Wrapper for [`File::metadata`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.metadata). + pub fn metadata(&self) -> Result { + self.file + .metadata() + .map_err(|source| self.error(source, ErrorKind::Metadata)) + } + + /// Wrapper for [`File::try_clone`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.try_clone). + pub fn try_clone(&self) -> Result { + self.file + .try_clone() + .map(|file| File { + file, + path: self.path.clone(), + }) + .map_err(|source| self.error(source, ErrorKind::Clone)) + } + + /// Wrapper for [`File::set_permissions`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.set_permissions). + pub fn set_permissions(&self, perm: fs::Permissions) -> Result<(), io::Error> { + self.file + .set_permissions(perm) + .map_err(|source| self.error(source, ErrorKind::SetPermissions)) + } +} + +/// Methods added by fs-err that are not available on +/// [`std::fs::File`][std::fs::File]. +/// +/// [std::fs::File]: https://doc.rust-lang.org/stable/std/fs/struct.File.html +impl File { + /// Creates a [`File`](struct.File.html) from a raw file and its path. + pub fn from_parts

(file: fs::File, path: P) -> Self + where + P: Into, + { + File { + file, + path: path.into(), + } + } + + /// Extract the raw file and its path from this [`File`](struct.File.html) + pub fn into_parts(self) -> (fs::File, PathBuf) { + (self.file, self.path) + } + + /// Returns a reference to the underlying [`std::fs::File`][std::fs::File]. + /// + /// [std::fs::File]: https://doc.rust-lang.org/stable/std/fs/struct.File.html + pub fn file(&self) -> &fs::File { + &self.file + } + + /// Returns a mutable reference to the underlying [`std::fs::File`][std::fs::File]. + /// + /// [std::fs::File]: https://doc.rust-lang.org/stable/std/fs/struct.File.html + pub fn file_mut(&mut self) -> &mut fs::File { + &mut self.file + } + + /// Returns a reference to the path that this file was created with. + pub fn path(&self) -> &Path { + &self.path + } + + /// Wrap the error in information specific to this `File` object. + fn error(&self, source: io::Error, kind: ErrorKind) -> io::Error { + Error::build(source, kind, &self.path) + } +} + +impl Read for File { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.file + .read(buf) + .map_err(|source| self.error(source, ErrorKind::Read)) + } + + fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + self.file + .read_vectored(bufs) + .map_err(|source| self.error(source, ErrorKind::Read)) + } +} + +impl<'a> Read for &'a File { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + (&(**self).file) + .read(buf) + .map_err(|source| self.error(source, ErrorKind::Read)) + } + + fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + (&(**self).file) + .read_vectored(bufs) + .map_err(|source| self.error(source, ErrorKind::Read)) + } +} + +impl From for fs::File { + fn from(file: File) -> Self { + file.into_parts().0 + } +} + +impl Seek for File { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.file + .seek(pos) + .map_err(|source| self.error(source, ErrorKind::Seek)) + } +} + +impl<'a> Seek for &'a File { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + (&(**self).file) + .seek(pos) + .map_err(|source| self.error(source, ErrorKind::Seek)) + } +} + +impl Write for File { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.file + .write(buf) + .map_err(|source| self.error(source, ErrorKind::Write)) + } + + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + self.file + .write_vectored(bufs) + .map_err(|source| self.error(source, ErrorKind::Write)) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.file + .flush() + .map_err(|source| self.error(source, ErrorKind::Flush)) + } +} + +impl<'a> Write for &'a File { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + (&(**self).file) + .write(buf) + .map_err(|source| self.error(source, ErrorKind::Write)) + } + + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + (&(**self).file) + .write_vectored(bufs) + .map_err(|source| self.error(source, ErrorKind::Write)) + } + + fn flush(&mut self) -> std::io::Result<()> { + (&(**self).file) + .flush() + .map_err(|source| self.error(source, ErrorKind::Flush)) + } +} + +#[cfg(unix)] +mod unix { + use crate::os::unix::fs::FileExt; + use crate::ErrorKind; + use std::io; + use std::os::unix::fs::FileExt as _; + use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; + + impl AsRawFd for crate::File { + fn as_raw_fd(&self) -> RawFd { + self.file().as_raw_fd() + } + } + + impl IntoRawFd for crate::File { + fn into_raw_fd(self) -> RawFd { + self.file.into_raw_fd() + } + } + + impl FileExt for crate::File { + fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { + self.file() + .read_at(buf, offset) + .map_err(|err| self.error(err, ErrorKind::ReadAt)) + } + fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { + self.file() + .write_at(buf, offset) + .map_err(|err| self.error(err, ErrorKind::WriteAt)) + } + } + + #[cfg(feature = "io_safety")] + mod io_safety { + use std::os::unix::io::{AsFd, BorrowedFd, OwnedFd}; + + #[cfg_attr(docsrs, doc(cfg(feature = "io_safety")))] + impl AsFd for crate::File { + fn as_fd(&self) -> BorrowedFd<'_> { + self.file().as_fd() + } + } + + #[cfg_attr(docsrs, doc(cfg(feature = "io_safety")))] + impl From for OwnedFd { + fn from(file: crate::File) -> Self { + file.into_parts().0.into() + } + } + } +} + +#[cfg(windows)] +mod windows { + use crate::os::windows::fs::FileExt; + use crate::ErrorKind; + use std::io; + use std::os::windows::{ + fs::FileExt as _, + io::{AsRawHandle, IntoRawHandle, RawHandle}, + }; + + impl FileExt for crate::File { + fn seek_read(&self, buf: &mut [u8], offset: u64) -> io::Result { + self.file() + .seek_read(buf, offset) + .map_err(|err| self.error(err, ErrorKind::SeekRead)) + } + + fn seek_write(&self, buf: &[u8], offset: u64) -> io::Result { + self.file() + .seek_write(buf, offset) + .map_err(|err| self.error(err, ErrorKind::SeekWrite)) + } + } + + impl AsRawHandle for crate::File { + fn as_raw_handle(&self) -> RawHandle { + self.file().as_raw_handle() + } + } + + // can't be implemented, because the trait doesn't give us a Path + // impl std::os::windows::io::FromRawHandle for crate::File { + // } + + impl IntoRawHandle for crate::File { + fn into_raw_handle(self) -> RawHandle { + self.file.into_raw_handle() + } + } + + #[cfg(feature = "io_safety")] + mod io_safety { + use std::os::windows::io::{AsHandle, BorrowedHandle, OwnedHandle}; + + #[cfg_attr(docsrs, doc(cfg(feature = "io_safety")))] + impl AsHandle for crate::File { + fn as_handle(&self) -> BorrowedHandle<'_> { + self.file().as_handle() + } + } + + #[cfg_attr(docsrs, doc(cfg(feature = "io_safety")))] + impl From for OwnedHandle { + fn from(file: crate::File) -> Self { + file.into_parts().0.into() + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b1ceae5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,250 @@ +/*! +fs-err is a drop-in replacement for [`std::fs`][std::fs] that provides more +helpful messages on errors. Extra information includes which operations was +attempted and any involved paths. + +# Error Messages + +Using [`std::fs`][std::fs], if this code fails: + +```no_run +# use std::fs::File; +let file = File::open("does not exist.txt")?; +# Ok::<(), std::io::Error>(()) +``` + +The error message that Rust gives you isn't very useful: + +```txt +The system cannot find the file specified. (os error 2) +``` + +...but if we use fs-err instead, our error contains more actionable information: + +```txt +failed to open file `does not exist.txt` + caused by: The system cannot find the file specified. (os error 2) +``` + +# Usage + +fs-err's API is the same as [`std::fs`][std::fs], so migrating code to use it is easy. + +```no_run +// use std::fs; +use fs_err as fs; + +let contents = fs::read_to_string("foo.txt")?; + +println!("Read foo.txt: {}", contents); + +# Ok::<(), std::io::Error>(()) +``` + +fs-err uses [`std::io::Error`][std::io::Error] for all errors. This helps fs-err +compose well with traits from the standard library like +[`std::io::Read`][std::io::Read] and crates that use them like +[`serde_json`][serde_json]: + +```no_run +use fs_err::File; + +let file = File::open("my-config.json")?; + +// If an I/O error occurs inside serde_json, the error will include a file path +// as well as what operation was being performed. +let decoded: Vec = serde_json::from_reader(file)?; + +println!("Program config: {:?}", decoded); + +# Ok::<(), Box>(()) +``` + +[std::fs]: https://doc.rust-lang.org/stable/std/fs/ +[std::io::Error]: https://doc.rust-lang.org/stable/std/io/struct.Error.html +[std::io::Read]: https://doc.rust-lang.org/stable/std/io/trait.Read.html +[serde_json]: https://crates.io/crates/serde_json +*/ + +#![doc(html_root_url = "https://docs.rs/fs-err/2.9.0")] +#![deny(missing_debug_implementations, missing_docs)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod dir; +mod errors; +mod file; +mod open_options; +pub mod os; +mod path; +#[cfg(feature = "tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub mod tokio; + +use std::fs; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; + +use errors::{Error, ErrorKind, SourceDestError, SourceDestErrorKind}; + +pub use dir::*; +pub use file::*; +pub use open_options::OpenOptions; +pub use path::PathExt; + +/// Wrapper for [`fs::read`](https://doc.rust-lang.org/stable/std/fs/fn.read.html). +pub fn read>(path: P) -> io::Result> { + let path = path.as_ref(); + let mut file = file::open(path).map_err(|err_gen| err_gen(path.to_path_buf()))?; + let mut bytes = Vec::with_capacity(initial_buffer_size(&file)); + file.read_to_end(&mut bytes) + .map_err(|err| Error::build(err, ErrorKind::Read, path))?; + Ok(bytes) +} + +/// Wrapper for [`fs::read_to_string`](https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html). +pub fn read_to_string>(path: P) -> io::Result { + let path = path.as_ref(); + let mut file = file::open(path).map_err(|err_gen| err_gen(path.to_path_buf()))?; + let mut string = String::with_capacity(initial_buffer_size(&file)); + file.read_to_string(&mut string) + .map_err(|err| Error::build(err, ErrorKind::Read, path))?; + Ok(string) +} + +/// Wrapper for [`fs::write`](https://doc.rust-lang.org/stable/std/fs/fn.write.html). +pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> { + let path = path.as_ref(); + file::create(path) + .map_err(|err_gen| err_gen(path.to_path_buf()))? + .write_all(contents.as_ref()) + .map_err(|err| Error::build(err, ErrorKind::Write, path)) +} + +/// Wrapper for [`fs::copy`](https://doc.rust-lang.org/stable/std/fs/fn.copy.html). +pub fn copy(from: P, to: Q) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + let from = from.as_ref(); + let to = to.as_ref(); + fs::copy(from, to) + .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::Copy, from, to)) +} + +/// Wrapper for [`fs::create_dir`](https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html). +pub fn create_dir

(path: P) -> io::Result<()> +where + P: AsRef, +{ + let path = path.as_ref(); + fs::create_dir(path).map_err(|source| Error::build(source, ErrorKind::CreateDir, path)) +} + +/// Wrapper for [`fs::create_dir_all`](https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html). +pub fn create_dir_all

(path: P) -> io::Result<()> +where + P: AsRef, +{ + let path = path.as_ref(); + fs::create_dir_all(path).map_err(|source| Error::build(source, ErrorKind::CreateDir, path)) +} + +/// Wrapper for [`fs::remove_dir`](https://doc.rust-lang.org/stable/std/fs/fn.remove_dir.html). +pub fn remove_dir

(path: P) -> io::Result<()> +where + P: AsRef, +{ + let path = path.as_ref(); + fs::remove_dir(path).map_err(|source| Error::build(source, ErrorKind::RemoveDir, path)) +} + +/// Wrapper for [`fs::remove_dir_all`](https://doc.rust-lang.org/stable/std/fs/fn.remove_dir_all.html). +pub fn remove_dir_all

(path: P) -> io::Result<()> +where + P: AsRef, +{ + let path = path.as_ref(); + fs::remove_dir_all(path).map_err(|source| Error::build(source, ErrorKind::RemoveDir, path)) +} + +/// Wrapper for [`fs::remove_file`](https://doc.rust-lang.org/stable/std/fs/fn.remove_file.html). +pub fn remove_file

(path: P) -> io::Result<()> +where + P: AsRef, +{ + let path = path.as_ref(); + fs::remove_file(path).map_err(|source| Error::build(source, ErrorKind::RemoveFile, path)) +} + +/// Wrapper for [`fs::metadata`](https://doc.rust-lang.org/stable/std/fs/fn.metadata.html). +pub fn metadata>(path: P) -> io::Result { + let path = path.as_ref(); + fs::metadata(path).map_err(|source| Error::build(source, ErrorKind::Metadata, path)) +} + +/// Wrapper for [`fs::canonicalize`](https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html). +pub fn canonicalize>(path: P) -> io::Result { + let path = path.as_ref(); + fs::canonicalize(path).map_err(|source| Error::build(source, ErrorKind::Canonicalize, path)) +} + +/// Wrapper for [`fs::hard_link`](https://doc.rust-lang.org/stable/std/fs/fn.hard_link.html). +pub fn hard_link, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + fs::hard_link(src, dst) + .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::HardLink, src, dst)) +} + +/// Wrapper for [`fs::read_link`](https://doc.rust-lang.org/stable/std/fs/fn.read_link.html). +pub fn read_link>(path: P) -> io::Result { + let path = path.as_ref(); + fs::read_link(path).map_err(|source| Error::build(source, ErrorKind::ReadLink, path)) +} + +/// Wrapper for [`fs::rename`](https://doc.rust-lang.org/stable/std/fs/fn.rename.html). +pub fn rename, Q: AsRef>(from: P, to: Q) -> io::Result<()> { + let from = from.as_ref(); + let to = to.as_ref(); + fs::rename(from, to) + .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::Rename, from, to)) +} + +/// Wrapper for [`fs::soft_link`](https://doc.rust-lang.org/stable/std/fs/fn.soft_link.html). +#[deprecated = "replaced with std::os::unix::fs::symlink and \ +std::os::windows::fs::{symlink_file, symlink_dir}"] +pub fn soft_link, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + #[allow(deprecated)] + fs::soft_link(src, dst) + .map_err(|source| SourceDestError::build(source, SourceDestErrorKind::SoftLink, src, dst)) +} + +/// Wrapper for [`fs::symlink_metadata`](https://doc.rust-lang.org/stable/std/fs/fn.symlink_metadata.html). +pub fn symlink_metadata>(path: P) -> io::Result { + let path = path.as_ref(); + fs::symlink_metadata(path) + .map_err(|source| Error::build(source, ErrorKind::SymlinkMetadata, path)) +} + +/// Wrapper for [`fs::set_permissions`](https://doc.rust-lang.org/stable/std/fs/fn.set_permissions.html). +pub fn set_permissions>(path: P, perm: fs::Permissions) -> io::Result<()> { + let path = path.as_ref(); + fs::set_permissions(path, perm) + .map_err(|source| Error::build(source, ErrorKind::SetPermissions, path)) +} + +fn initial_buffer_size(file: &std::fs::File) -> usize { + file.metadata().map(|m| m.len() as usize + 1).unwrap_or(0) +} + +pub(crate) use private::Sealed; +mod private { + pub trait Sealed {} + + impl Sealed for crate::File {} + impl Sealed for std::path::Path {} + impl Sealed for crate::OpenOptions {} +} diff --git a/src/open_options.rs b/src/open_options.rs new file mode 100644 index 0000000..557fa7a --- /dev/null +++ b/src/open_options.rs @@ -0,0 +1,134 @@ +use std::{fs, io, path::PathBuf}; +#[derive(Clone, Debug)] +/// Wrapper around [`std::fs::OptionOptions`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html) +pub struct OpenOptions(fs::OpenOptions); + +impl OpenOptions { + /// Wrapper for [`std::fs::OpenOptions::new`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.new) + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + OpenOptions(fs::OpenOptions::new()) + } + + /// Wrapper for [`std::fs::OpenOptions::read`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.read) + pub fn read(&mut self, read: bool) -> &mut Self { + self.0.read(read); + self + } + + /// Wrapper for [`std::fs::OpenOptions::write`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.write) + pub fn write(&mut self, write: bool) -> &mut Self { + self.0.write(write); + self + } + + /// Wrapper for [`std::fs::OpenOptions::append`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.append) + pub fn append(&mut self, append: bool) -> &mut Self { + self.0.append(append); + self + } + + /// Wrapper for [`std::fs::OpenOptions::truncate`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.truncate) + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.0.truncate(truncate); + self + } + + /// Wrapper for [`std::fs::OpenOptions::create`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create) + pub fn create(&mut self, create: bool) -> &mut Self { + self.0.create(create); + self + } + + /// Wrapper for [`std::fs::OpenOptions::create_new`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new) + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.0.create_new(create_new); + self + } + + /// Wrapper for [`std::fs::OpenOptions::open`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.open) + pub fn open

(&self, path: P) -> io::Result + where + P: Into, + { + // We have to either duplicate the logic or call the deprecated method here. + // We can't let the deprecated function call this method, because we can't construct + // `&fs_err::OpenOptions` from `&fs::OpenOptions` without cloning + // (although cloning would probably be cheap). + #[allow(deprecated)] + crate::File::from_options(path.into(), self.options()) + } +} + +/// Methods added by fs-err that are not available on +/// [`std::fs::OpenOptions`](https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html). +impl OpenOptions { + /// Constructs `Self` from [`std::fs::OpenOptions`](https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html) + pub fn from_options(options: fs::OpenOptions) -> Self { + Self(options) + } + + /// Returns a reference to the underlying [`std::fs::OpenOptions`](https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html). + /// + /// Note that calling `open()` on this reference will NOT give you the improved errors from fs-err. + pub fn options(&self) -> &fs::OpenOptions { + &self.0 + } + + /// Returns a mutable reference to the underlying [`std::fs::OpenOptions`](https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html). + /// + /// This allows you to change settings that don't yet have wrappers in fs-err. + /// Note that calling `open()` on this reference will NOT give you the improved errors from fs-err. + pub fn options_mut(&mut self) -> &mut fs::OpenOptions { + &mut self.0 + } +} + +#[cfg(unix)] +mod unix { + use crate::os::unix::fs::OpenOptionsExt; + use std::os::unix::fs::OpenOptionsExt as _; + impl OpenOptionsExt for crate::OpenOptions { + fn mode(&mut self, mode: u32) -> &mut Self { + self.options_mut().mode(mode); + self + } + + fn custom_flags(&mut self, flags: i32) -> &mut Self { + self.options_mut().custom_flags(flags); + self + } + } +} + +#[cfg(windows)] +mod windows { + use crate::os::windows::fs::OpenOptionsExt; + use std::os::windows::fs::OpenOptionsExt as _; + + impl OpenOptionsExt for crate::OpenOptions { + fn access_mode(&mut self, access: u32) -> &mut Self { + self.options_mut().access_mode(access); + self + } + + fn share_mode(&mut self, val: u32) -> &mut Self { + self.options_mut().share_mode(val); + self + } + fn custom_flags(&mut self, flags: u32) -> &mut Self { + self.options_mut().custom_flags(flags); + self + } + + fn attributes(&mut self, val: u32) -> &mut Self { + self.options_mut().attributes(val); + self + } + + fn security_qos_flags(&mut self, flags: u32) -> &mut Self { + self.options_mut().security_qos_flags(flags); + self + } + } +} diff --git a/src/os.rs b/src/os.rs new file mode 100644 index 0000000..b801e60 --- /dev/null +++ b/src/os.rs @@ -0,0 +1,11 @@ +//! OS-specific functionality. + +// The std-library has a couple more platforms than just `unix` for which these apis +// are defined, but we're using just `unix` here. We can always expand later. +#[cfg(unix)] +/// Platform-specific extensions for Unix platforms. +pub mod unix; + +#[cfg(windows)] +/// Platform-specific extensions for Windows. +pub mod windows; diff --git a/src/os/unix.rs b/src/os/unix.rs new file mode 100644 index 0000000..ad7c488 --- /dev/null +++ b/src/os/unix.rs @@ -0,0 +1,38 @@ +/// Unix-specific extensions to wrappers in `fs_err` for `std::fs` types. +pub mod fs { + use std::io; + use std::path::Path; + + use crate::SourceDestError; + use crate::SourceDestErrorKind; + + /// Wrapper for [`std::os::unix::fs::symlink`](https://doc.rust-lang.org/std/os/unix/fs/fn.symlink.html) + pub fn symlink, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + std::os::unix::fs::symlink(src, dst) + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::Symlink, src, dst)) + } + + /// Wrapper for [`std::os::unix::fs::FileExt`](https://doc.rust-lang.org/std/os/unix/fs/trait.FileExt.html). + /// + /// The std traits might be extended in the future (See issue [#49961](https://github.com/rust-lang/rust/issues/49961#issuecomment-382751777)). + /// This trait is sealed and can not be implemented by other crates. + pub trait FileExt: crate::Sealed { + /// Wrapper for [`FileExt::read_at`](https://doc.rust-lang.org/std/os/unix/fs/trait.FileExt.html#tymethod.read_at) + fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result; + /// Wrapper for [`FileExt::write_at`](https://doc.rust-lang.org/std/os/unix/fs/trait.FileExt.html#tymethod.write_at) + fn write_at(&self, buf: &[u8], offset: u64) -> io::Result; + } + + /// Wrapper for [`std::os::unix::fs::OpenOptionsExt`](https://doc.rust-lang.org/std/os/unix/fs/trait.OpenOptionsExt.html) + /// + /// The std traits might be extended in the future (See issue [#49961](https://github.com/rust-lang/rust/issues/49961#issuecomment-382751777)). + /// This trait is sealed and can not be implemented by other crates. + pub trait OpenOptionsExt: crate::Sealed { + /// Wrapper for [`OpenOptionsExt::mode`](https://doc.rust-lang.org/std/os/unix/fs/trait.OpenOptionsExt.html#tymethod.mode) + fn mode(&mut self, mode: u32) -> &mut Self; + /// Wrapper for [`OpenOptionsExt::custom_flags`](https://doc.rust-lang.org/std/os/unix/fs/trait.OpenOptionsExt.html#tymethod.custom_flags) + fn custom_flags(&mut self, flags: i32) -> &mut Self; + } +} diff --git a/src/os/windows.rs b/src/os/windows.rs new file mode 100644 index 0000000..f559856 --- /dev/null +++ b/src/os/windows.rs @@ -0,0 +1,49 @@ +/// Windows-specific extensions to wrappers in `fs_err` for `std::fs` types. +pub mod fs { + use crate::{SourceDestError, SourceDestErrorKind}; + use std::io; + use std::path::Path; + /// Wrapper for [std::os::windows::fs::symlink_dir](https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_dir.html) + pub fn symlink_dir, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + std::os::windows::fs::symlink_dir(src, dst) + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::SymlinkDir, src, dst)) + } + + /// Wrapper for [std::os::windows::fs::symlink_file](https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_file.html) + pub fn symlink_file, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + std::os::windows::fs::symlink_file(src, dst) + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::SymlinkFile, src, dst)) + } + + /// Wrapper for [`std::os::windows::fs::FileExt`](https://doc.rust-lang.org/std/os/windows/fs/trait.FileExt.html). + /// + /// The std traits might be extended in the future (See issue [#49961](https://github.com/rust-lang/rust/issues/49961#issuecomment-382751777)). + /// This trait is sealed and can not be implemented by other crates. + pub trait FileExt: crate::Sealed { + /// Wrapper for [`FileExt::seek_read`](https://doc.rust-lang.org/std/os/windows/fs/trait.FileExt.html#tymethod.seek_read) + fn seek_read(&self, buf: &mut [u8], offset: u64) -> io::Result; + /// Wrapper for [`FileExt::seek_wriite`](https://doc.rust-lang.org/std/os/windows/fs/trait.FileExt.html#tymethod.seek_write) + fn seek_write(&self, buf: &[u8], offset: u64) -> io::Result; + } + + /// Wrapper for [`std::os::windows::fs::OpenOptionsExt`](https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html) + /// + /// The std traits might be extended in the future (See issue [#49961](https://github.com/rust-lang/rust/issues/49961#issuecomment-382751777)). + /// This trait is sealed and can not be implemented by other crates. + pub trait OpenOptionsExt: crate::Sealed { + /// Wrapper for [`OpenOptionsExt::access_mode`](https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html#tymethod.access_mode) + fn access_mode(&mut self, access: u32) -> &mut Self; + /// Wrapper for [`OpenOptionsExt::share_mode`](https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html#tymethod.share_mode) + fn share_mode(&mut self, val: u32) -> &mut Self; + /// Wrapper for [`OpenOptionsExt::custom_flags`](https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html#tymethod.custom_flags) + fn custom_flags(&mut self, flags: u32) -> &mut Self; + /// Wrapper for [`OpenOptionsExt::attributes`](https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html#tymethod.attributes) + fn attributes(&mut self, val: u32) -> &mut Self; + /// Wrapper for [`OpenOptionsExt::security_qos_flags`](https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html#tymethod.security_qos_flags) + fn security_qos_flags(&mut self, flags: u32) -> &mut Self; + } +} diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..28549a3 --- /dev/null +++ b/src/path.rs @@ -0,0 +1,43 @@ +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +/// Defines aliases on [`Path`](https://doc.rust-lang.org/std/path/struct.Path.html) for `fs_err` functions. +/// +/// This trait is sealed and can not be implemented by other crates. +// +// Because no one else can implement it, we can add methods backwards-compatibly. +pub trait PathExt: crate::Sealed { + /// Wrapper for [`crate::metadata`]. + fn fs_err_metadata(&self) -> io::Result; + /// Wrapper for [`crate::symlink_metadata`]. + fn fs_err_symlink_metadata(&self) -> io::Result; + /// Wrapper for [`crate::canonicalize`]. + fn fs_err_canonicalize(&self) -> io::Result; + /// Wrapper for [`crate::read_link`]. + fn fs_err_read_link(&self) -> io::Result; + /// Wrapper for [`crate::read_dir`]. + fn fs_err_read_dir(&self) -> io::Result; +} + +impl PathExt for Path { + fn fs_err_metadata(&self) -> io::Result { + crate::metadata(self) + } + + fn fs_err_symlink_metadata(&self) -> io::Result { + crate::symlink_metadata(self) + } + + fn fs_err_canonicalize(&self) -> io::Result { + crate::canonicalize(self) + } + + fn fs_err_read_link(&self) -> io::Result { + crate::read_link(self) + } + + fn fs_err_read_dir(&self) -> io::Result { + crate::read_dir(self) + } +} diff --git a/src/tokio/dir_builder.rs b/src/tokio/dir_builder.rs new file mode 100644 index 0000000..0615999 --- /dev/null +++ b/src/tokio/dir_builder.rs @@ -0,0 +1,54 @@ +use crate::errors::{Error, ErrorKind}; +use std::io; +use std::path::Path; + +/// A builder for creating directories in various manners. +/// +/// This is a wrapper around [`tokio::fs::DirBuilder`]. +#[derive(Debug, Default)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub struct DirBuilder { + inner: tokio::fs::DirBuilder, +} + +impl DirBuilder { + /// Creates a new set of options with default mode/security settings for all + /// platforms and also non-recursive. + /// + /// This is a wrapper version of [`tokio::fs::DirBuilder::new`] + /// + /// # Examples + /// + /// ```no_run + /// use fs_err::tokio::DirBuilder; + /// + /// let builder = DirBuilder::new(); + /// ``` + pub fn new() -> Self { + Default::default() + } + + /// Wrapper around [`tokio::fs::DirBuilder::recursive`]. + pub fn recursive(&mut self, recursive: bool) -> &mut Self { + self.inner.recursive(recursive); + self + } + + /// Wrapper around [`tokio::fs::DirBuilder::create`]. + pub async fn create(&self, path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + self.inner + .create(path) + .await + .map_err(|err| Error::build(err, ErrorKind::CreateDir, path)) + } +} + +#[cfg(unix)] +impl DirBuilder { + /// Wrapper around [`tokio::fs::DirBuilder::mode`]. + pub fn mode(&mut self, mode: u32) -> &mut Self { + self.inner.mode(mode); + self + } +} diff --git a/src/tokio/file.rs b/src/tokio/file.rs new file mode 100644 index 0000000..c0dac4c --- /dev/null +++ b/src/tokio/file.rs @@ -0,0 +1,243 @@ +use crate::errors::{Error, ErrorKind}; +use std::fs::{Metadata, Permissions}; +use std::io; +use std::io::{IoSlice, SeekFrom}; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; +use tokio::fs; +use tokio::fs::File as TokioFile; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; + +/// Wrapper around [`tokio::fs::File`] which adds more helpful +/// information to all errors. +#[derive(Debug)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub struct File { + tokio: fs::File, + path: PathBuf, +} + +impl File { + /// Wrapper for [`tokio::fs::File::open`]. + pub async fn open(path: impl Into) -> io::Result { + let path = path.into(); + let f = TokioFile::open(&path) + .await + .map_err(|err| Error::build(err, ErrorKind::OpenFile, &path))?; + Ok(File::from_parts(f, path)) + } + + /// Wrapper for [`tokio::fs::File::create`]. + pub async fn create(path: impl Into) -> io::Result { + let path = path.into(); + match TokioFile::create(&path).await { + Ok(f) => Ok(File::from_parts(f, path)), + Err(err) => Err(Error::build(err, ErrorKind::CreateFile, &path)), + } + } + + /// Wrapper for [`tokio::fs::File::from_std`]. + pub fn from_std(std: crate::File) -> File { + let (std, path) = std.into_parts(); + File::from_parts(TokioFile::from_std(std), path) + } + + /// Wrapper for [`tokio::fs::File::sync_all`]. + pub async fn sync_all(&self) -> io::Result<()> { + self.tokio + .sync_all() + .await + .map_err(|err| self.error(err, ErrorKind::SyncFile)) + } + + /// Wrapper for [`tokio::fs::File::sync_data`]. + pub async fn sync_data(&self) -> io::Result<()> { + self.tokio + .sync_data() + .await + .map_err(|err| self.error(err, ErrorKind::SyncFile)) + } + + /// Wrapper for [`tokio::fs::File::set_len`]. + pub async fn set_len(&self, size: u64) -> io::Result<()> { + self.tokio + .set_len(size) + .await + .map_err(|err| self.error(err, ErrorKind::SetLen)) + } + + /// Wrapper for [`tokio::fs::File::metadata`]. + pub async fn metadata(&self) -> io::Result { + self.tokio + .metadata() + .await + .map_err(|err| self.error(err, ErrorKind::Metadata)) + } + + /// Wrapper for [`tokio::fs::File::try_clone`]. + pub async fn try_clone(&self) -> io::Result { + match self.tokio.try_clone().await { + Ok(file) => Ok(File::from_parts(file, self.path.clone())), + Err(err) => Err(self.error(err, ErrorKind::Clone)), + } + } + + /// Wrapper for [`tokio::fs::File::into_std`]. + pub async fn into_std(self) -> crate::File { + crate::File::from_parts(self.tokio.into_std().await, self.path) + } + + /// Wrapper for [`tokio::fs::File::try_into_std`]. + pub fn try_into_std(self) -> Result { + match self.tokio.try_into_std() { + Ok(f) => Ok(crate::File::from_parts(f, self.path)), + Err(f) => Err(File::from_parts(f, self.path)), + } + } + + /// Wrapper for [`tokio::fs::File::set_permissions`]. + pub async fn set_permissions(&self, perm: Permissions) -> io::Result<()> { + self.tokio + .set_permissions(perm) + .await + .map_err(|err| self.error(err, ErrorKind::SetPermissions)) + } +} + +/// Methods added by fs-err that are not available on +/// [`tokio::fs::File`]. +impl File { + /// Creates a [`File`](struct.File.html) from a raw file and its path. + pub fn from_parts

(file: TokioFile, path: P) -> Self + where + P: Into, + { + File { + tokio: file, + path: path.into(), + } + } + + /// Extract the raw file and its path from this [`File`](struct.File.html). + pub fn into_parts(self) -> (TokioFile, PathBuf) { + (self.tokio, self.path) + } + + /// Returns a reference to the underlying [`tokio::fs::File`]. + pub fn file(&self) -> &TokioFile { + &self.tokio + } + + /// Returns a mutable reference to the underlying [`tokio::fs::File`]. + pub fn file_mut(&mut self) -> &mut TokioFile { + &mut self.tokio + } + + /// Returns a reference to the path that this file was created with. + pub fn path(&self) -> &Path { + &self.path + } + + /// Wrap the error in information specific to this `File` object. + fn error(&self, source: io::Error, kind: ErrorKind) -> io::Error { + Error::build(source, kind, &self.path) + } +} + +impl From for File { + fn from(f: crate::File) -> Self { + let (f, path) = f.into_parts(); + File::from_parts(f.into(), path) + } +} + +impl From for TokioFile { + fn from(f: File) -> Self { + f.into_parts().0 + } +} + +#[cfg(unix)] +impl std::os::unix::io::AsRawFd for File { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + self.tokio.as_raw_fd() + } +} + +#[cfg(windows)] +impl std::os::windows::io::AsRawHandle for File { + fn as_raw_handle(&self) -> std::os::windows::io::RawHandle { + self.tokio.as_raw_handle() + } +} + +impl AsyncRead for File { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready( + ready!(Pin::new(&mut self.tokio).poll_read(cx, buf)) + .map_err(|err| self.error(err, ErrorKind::Read)), + ) + } +} + +impl AsyncSeek for File { + fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { + Pin::new(&mut self.tokio) + .start_seek(position) + .map_err(|err| self.error(err, ErrorKind::Seek)) + } + + fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Poll::Ready( + ready!(Pin::new(&mut self.tokio).poll_complete(cx)) + .map_err(|err| self.error(err, ErrorKind::Seek)), + ) + } +} + +impl AsyncWrite for File { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready( + ready!(Pin::new(&mut self.tokio).poll_write(cx, buf)) + .map_err(|err| self.error(err, ErrorKind::Write)), + ) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Poll::Ready( + ready!(Pin::new(&mut self.tokio).poll_flush(cx)) + .map_err(|err| self.error(err, ErrorKind::Flush)), + ) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Poll::Ready( + ready!(Pin::new(&mut self.tokio).poll_shutdown(cx)) + .map_err(|err| self.error(err, ErrorKind::Flush)), + ) + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + Poll::Ready( + ready!(Pin::new(&mut self.tokio).poll_write_vectored(cx, bufs)) + .map_err(|err| self.error(err, ErrorKind::Write)), + ) + } + + fn is_write_vectored(&self) -> bool { + self.tokio.is_write_vectored() + } +} diff --git a/src/tokio/mod.rs b/src/tokio/mod.rs new file mode 100644 index 0000000..bf4ac94 --- /dev/null +++ b/src/tokio/mod.rs @@ -0,0 +1,189 @@ +//! Tokio-specific wrappers that use `fs_err` error messages. + +use crate::errors::{Error, ErrorKind, SourceDestError, SourceDestErrorKind}; +use std::fs::{Metadata, Permissions}; +use std::path::{Path, PathBuf}; +use tokio::io; +mod dir_builder; +mod file; +mod open_options; +mod read_dir; + +pub use self::open_options::OpenOptions; +pub use self::read_dir::{read_dir, DirEntry, ReadDir}; +pub use dir_builder::DirBuilder; +pub use file::File; + +/// Wrapper for [`tokio::fs::canonicalize`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn canonicalize(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + tokio::fs::canonicalize(path) + .await + .map_err(|err| Error::build(err, ErrorKind::Canonicalize, path)) +} + +/// Wrapper for [`tokio::fs::copy`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn copy(from: impl AsRef, to: impl AsRef) -> Result { + let (from, to) = (from.as_ref(), to.as_ref()); + tokio::fs::copy(from, to) + .await + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::Copy, from, to)) +} + +/// Wrapper for [`tokio::fs::create_dir`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn create_dir(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + tokio::fs::create_dir(path) + .await + .map_err(|err| Error::build(err, ErrorKind::CreateDir, path)) +} + +/// Wrapper for [`tokio::fs::create_dir_all`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn create_dir_all(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + tokio::fs::create_dir_all(path) + .await + .map_err(|err| Error::build(err, ErrorKind::CreateDir, path)) +} + +/// Wrapper for [`tokio::fs::hard_link`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn hard_link(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + let (src, dst) = (src.as_ref(), dst.as_ref()); + tokio::fs::hard_link(src, dst) + .await + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::HardLink, src, dst)) +} + +/// Wrapper for [`tokio::fs::metadata`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn metadata(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + tokio::fs::metadata(path) + .await + .map_err(|err| Error::build(err, ErrorKind::Metadata, path)) +} + +/// Wrapper for [`tokio::fs::read`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn read(path: impl AsRef) -> io::Result> { + let path = path.as_ref(); + tokio::fs::read(path) + .await + .map_err(|err| Error::build(err, ErrorKind::Read, path)) +} + +/// Wrapper for [`tokio::fs::read_link`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn read_link(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + tokio::fs::read_link(path) + .await + .map_err(|err| Error::build(err, ErrorKind::ReadLink, path)) +} + +/// Wrapper for [`tokio::fs::read_to_string`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn read_to_string(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + tokio::fs::read_to_string(path) + .await + .map_err(|err| Error::build(err, ErrorKind::Read, path)) +} + +/// Wrapper for [`tokio::fs::remove_dir`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn remove_dir(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + tokio::fs::remove_dir(path) + .await + .map_err(|err| Error::build(err, ErrorKind::RemoveDir, path)) +} + +/// Wrapper for [`tokio::fs::remove_dir_all`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn remove_dir_all(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + tokio::fs::remove_dir_all(path) + .await + .map_err(|err| Error::build(err, ErrorKind::RemoveDir, path)) +} + +/// Wrapper for [`tokio::fs::remove_file`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn remove_file(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + tokio::fs::remove_file(path) + .await + .map_err(|err| Error::build(err, ErrorKind::RemoveFile, path)) +} + +/// Wrapper for [`tokio::fs::rename`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn rename(from: impl AsRef, to: impl AsRef) -> io::Result<()> { + let (from, to) = (from.as_ref(), to.as_ref()); + tokio::fs::rename(from, to) + .await + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::Rename, from, to)) +} + +/// Wrapper for [`tokio::fs::set_permissions`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn set_permissions(path: impl AsRef, perm: Permissions) -> io::Result<()> { + let path = path.as_ref(); + tokio::fs::set_permissions(path, perm) + .await + .map_err(|err| Error::build(err, ErrorKind::SetPermissions, path)) +} + +/// Wrapper for [`tokio::fs::symlink_metadata`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn symlink_metadata(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + tokio::fs::symlink_metadata(path) + .await + .map_err(|err| Error::build(err, ErrorKind::SymlinkMetadata, path)) +} + +/// Wrapper for [`tokio::fs::symlink`]. +#[cfg(unix)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + let (src, dst) = (src.as_ref(), dst.as_ref()); + tokio::fs::symlink(src, dst) + .await + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::Symlink, src, dst)) +} + +/// Wrapper for [`tokio::fs::symlink_dir`]. +#[cfg(windows)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + let (src, dst) = (src.as_ref(), dst.as_ref()); + tokio::fs::symlink_dir(src, dst) + .await + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::SymlinkDir, src, dst)) +} + +/// Wrapper for [`tokio::fs::symlink_file`]. +#[cfg(windows)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn symlink_file(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + let (src, dst) = (src.as_ref(), dst.as_ref()); + tokio::fs::symlink_file(src, dst) + .await + .map_err(|err| SourceDestError::build(err, SourceDestErrorKind::SymlinkFile, src, dst)) +} + +/// Wrapper for [`tokio::fs::write`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> io::Result<()> { + let (path, contents) = (path.as_ref(), contents.as_ref()); + tokio::fs::write(path, contents) + .await + .map_err(|err| Error::build(err, ErrorKind::Write, path)) +} diff --git a/src/tokio/open_options.rs b/src/tokio/open_options.rs new file mode 100644 index 0000000..6a2b5bf --- /dev/null +++ b/src/tokio/open_options.rs @@ -0,0 +1,109 @@ +use crate::errors::{Error, ErrorKind}; +use crate::tokio::File; +use std::io; +use std::path::Path; +use tokio::fs::OpenOptions as TokioOpenOptions; + +/// Options and flags which can be used to configure how a file is opened. +/// +/// This is a wrapper around [`tokio::fs::OpenOptions`]. +#[derive(Clone, Debug, Default)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub struct OpenOptions { + tokio: TokioOpenOptions, +} + +impl OpenOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// All options are initially set to `false`. + /// + /// This is a wrapped version of [`tokio::fs::OpenOptions::new`] + /// + /// # Examples + /// + /// ```no_run + /// use fs_err::tokio::OpenOptions; + /// + /// let mut options = OpenOptions::new(); + /// let future = options.read(true).open("foo.txt"); + /// ``` + pub fn new() -> OpenOptions { + OpenOptions { + tokio: TokioOpenOptions::new(), + } + } + + /// Wrapper for [`tokio::fs::OpenOptions::read`]. + pub fn read(&mut self, read: bool) -> &mut OpenOptions { + self.tokio.read(read); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::write`]. + pub fn write(&mut self, write: bool) -> &mut OpenOptions { + self.tokio.write(write); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::append`]. + pub fn append(&mut self, append: bool) -> &mut OpenOptions { + self.tokio.append(append); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::truncate`]. + pub fn truncate(&mut self, truncate: bool) -> &mut OpenOptions { + self.tokio.truncate(truncate); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::create`]. + pub fn create(&mut self, create: bool) -> &mut OpenOptions { + self.tokio.create(create); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::create_new`]. + pub fn create_new(&mut self, create_new: bool) -> &mut OpenOptions { + self.tokio.create_new(create_new); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::open`]. + pub async fn open(&self, path: impl AsRef) -> io::Result { + let path = path.as_ref(); + self.tokio + .open(path) + .await + .map(|f| File::from_parts(f, path)) + .map_err(|err| Error::build(err, ErrorKind::OpenFile, path)) + } +} + +#[cfg(unix)] +impl OpenOptions { + /// Wrapper for [`tokio::fs::OpenOptions::mode`]. + pub fn mode(&mut self, mode: u32) -> &mut OpenOptions { + self.tokio.mode(mode); + self + } + + /// Wrapper for [`tokio::fs::OpenOptions::custom_flags`]. + pub fn custom_flags(&mut self, flags: i32) -> &mut OpenOptions { + self.tokio.custom_flags(flags); + self + } +} + +impl From for OpenOptions { + fn from(std: std::fs::OpenOptions) -> Self { + OpenOptions { tokio: std.into() } + } +} + +impl From for OpenOptions { + fn from(tokio: TokioOpenOptions) -> Self { + OpenOptions { tokio } + } +} diff --git a/src/tokio/read_dir.rs b/src/tokio/read_dir.rs new file mode 100644 index 0000000..2594edf --- /dev/null +++ b/src/tokio/read_dir.rs @@ -0,0 +1,94 @@ +use crate::errors::{Error, ErrorKind}; +use std::ffi::OsString; +use std::fs::{FileType, Metadata}; +use std::io; +use std::path::{Path, PathBuf}; +use std::task::{ready, Context, Poll}; +use tokio::fs; + +/// Wrapper for [`tokio::fs::read_dir`]. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn read_dir(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + let tokio = fs::read_dir(path) + .await + .map_err(|err| Error::build(err, ErrorKind::ReadDir, path))?; + Ok(ReadDir { + tokio, + path: path.to_owned(), + }) +} + +/// Reads the entries in a directory. +/// +/// This is a wrapper around [`tokio::fs::ReadDir`]. +#[derive(Debug)] +#[must_use = "streams do nothing unless polled"] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub struct ReadDir { + tokio: fs::ReadDir, + path: PathBuf, +} + +impl ReadDir { + /// Wrapper around [`tokio::fs::ReadDir::next_entry`]. + pub async fn next_entry(&mut self) -> io::Result> { + match self.tokio.next_entry().await { + Ok(entry) => Ok(entry.map(|e| DirEntry { tokio: e })), + Err(err) => Err(Error::build(err, ErrorKind::ReadDir, &self.path)), + } + } + + /// Wrapper around [`tokio::fs::ReadDir::poll_next_entry`]. + pub fn poll_next_entry(&mut self, cx: &mut Context<'_>) -> Poll>> { + Poll::Ready(match ready!(self.tokio.poll_next_entry(cx)) { + Ok(entry) => Ok(entry.map(|e| DirEntry { tokio: e })), + Err(err) => Err(Error::build(err, ErrorKind::ReadDir, &self.path)), + }) + } +} + +/// Entries returned by the [`ReadDir`] stream. +/// +/// This is a wrapper around [`tokio::fs::DirEntry`]. +#[derive(Debug)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub struct DirEntry { + tokio: fs::DirEntry, +} + +impl DirEntry { + /// Wrapper around [`tokio::fs::DirEntry::path`]. + pub fn path(&self) -> PathBuf { + self.tokio.path() + } + + /// Wrapper around [`tokio::fs::DirEntry::file_name`]. + pub fn file_name(&self) -> OsString { + self.tokio.file_name() + } + + /// Wrapper around [`tokio::fs::DirEntry::metadata`]. + pub async fn metadata(&self) -> io::Result { + self.tokio + .metadata() + .await + .map_err(|err| Error::build(err, ErrorKind::Metadata, self.path())) + } + + /// Wrapper around [`tokio::fs::DirEntry::file_type`]. + pub async fn file_type(&self) -> io::Result { + self.tokio + .file_type() + .await + .map_err(|err| Error::build(err, ErrorKind::Metadata, self.path())) + } +} + +#[cfg(unix)] +impl DirEntry { + /// Wrapper around [`tokio::fs::DirEntry::ino`]. + pub fn ino(&self) -> u64 { + self.tokio.ino() + } +}