From 0148400020d02d1b3d52a9d5e0b8a1006f67e665 Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Tue, 9 Apr 2024 20:05:45 +0200 Subject: Import 'fs-err' crate Request Document: go/android-rust-importing-crates For CL Reviewers: go/android3p#cl-review For Build Team: go/ab-third-party-imports Bug: http://b/330683694 Test: m libfs_err Change-Id: I66991131e3b6d6233515d293b2a13dcbb7c5937f --- .cargo_vcs_info.json | 6 + Android.bp | 20 +++ CHANGELOG.md | 81 ++++++++++ Cargo.lock | 196 +++++++++++++++++++++++ Cargo.toml | 62 ++++++++ LICENSE | 1 + LICENSE-APACHE | 201 ++++++++++++++++++++++++ LICENSE-MIT | 23 +++ METADATA | 21 +++ MODULE_LICENSE_APACHE2 | 0 OWNERS | 2 + README.md | 96 ++++++++++++ build.rs | 7 + cargo_embargo.json | 1 + src/dir.rs | 100 ++++++++++++ src/errors.rs | 201 ++++++++++++++++++++++++ src/file.rs | 384 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 284 ++++++++++++++++++++++++++++++++++ src/open_options.rs | 150 ++++++++++++++++++ src/os.rs | 11 ++ src/os/unix.rs | 40 +++++ src/os/windows.rs | 54 +++++++ src/path.rs | 67 ++++++++ src/tokio/dir_builder.rs | 62 ++++++++ src/tokio/file.rs | 269 ++++++++++++++++++++++++++++++++ src/tokio/mod.rs | 245 +++++++++++++++++++++++++++++ src/tokio/open_options.rs | 127 +++++++++++++++ src/tokio/read_dir.rs | 111 ++++++++++++++ 28 files changed, 2822 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Android.bp create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 120000 LICENSE create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 METADATA create mode 100644 MODULE_LICENSE_APACHE2 create mode 100644 OWNERS create mode 100644 README.md create mode 100644 build.rs create mode 100644 cargo_embargo.json create mode 100644 src/dir.rs create mode 100644 src/errors.rs create mode 100644 src/file.rs create mode 100644 src/lib.rs create mode 100644 src/open_options.rs create mode 100644 src/os.rs create mode 100644 src/os/unix.rs create mode 100644 src/os/windows.rs create mode 100644 src/path.rs create mode 100644 src/tokio/dir_builder.rs create mode 100644 src/tokio/file.rs create mode 100644 src/tokio/mod.rs create mode 100644 src/tokio/open_options.rs create mode 100644 src/tokio/read_dir.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..4ac0dcf --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "ba9888793c517a82499c8df1ad013529d8578777" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..d4bf844 --- /dev/null +++ b/Android.bp @@ -0,0 +1,20 @@ +// This file is generated by cargo_embargo. +// Do not modify this file as changes will be overridden on upgrade. + +// TODO: Add license. +rust_library { + name: "libfs_err", + host_supported: true, + crate_name: "fs_err", + cargo_env_compat: true, + cargo_pkg_version: "2.11.0", + srcs: ["src/lib.rs"], + edition: "2018", + cfgs: ["rustc_1_63"], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c2a5d0e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,81 @@ +# fs-err Changelog + +## 2.11.0 + +* Added the first line of the standard library documentation to each function's rustdocs, to make them more useful in IDEs ([#50](https://github.com/andrewhickman/fs-err/issues/45)) +* Fixed the wrapper for `tokio::fs::symlink_dir()` on Windows being incorrectly named `symlink`. The old function is now deprecated and will be removed in the next breaking release. + +## 2.10.0 + +* Add `fs_err_try_exists` to `std::path::Path` via extension trait. This feature requires Rust 1.63 or later. ([#48](https://github.com/andrewhickman/fs-err/pull/48)) + +## 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.lock b/Cargo.lock new file mode 100644 index 0000000..c9399ca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,196 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fs-err" +version = "2.11.0" +dependencies = [ + "autocfg", + "serde_json", + "tokio", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "pin-project-lite", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f2ea497 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,62 @@ +# 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.11.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.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[package.metadata.release] +sign-tag = true +tag-name = "{{version}}" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "src/lib.rs" +replace = "html_root_url = \"https://docs.rs/fs-err/{{version}}\"" +search = 'html_root_url = "https://docs\.rs/fs-err/.*?"' + +[dependencies.tokio] +version = "1.21" +features = ["fs"] +optional = true +default_features = false + +[dev-dependencies.serde_json] +version = "1.0.64" + +[build-dependencies.autocfg] +version = "1" + +[features] +io_safety = [] diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..6b579aa --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +LICENSE-APACHE \ No newline at end of file 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/METADATA b/METADATA new file mode 100644 index 0000000..d7cfd93 --- /dev/null +++ b/METADATA @@ -0,0 +1,21 @@ +name: "fs-err" +description: "A drop-in replacement for std::fs with more helpful error messages." +third_party { + identifier { + type: "crates.io" + value: "fs-err" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/fs-err/fs-err-2.11.0.crate" + primary_source: true + } + version: "2.11.0" + # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same. + license_type: NOTICE + last_upgrade_date { + year: 2024 + month: 3 + day: 21 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..48bea6e --- /dev/null +++ b/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 688011 +include platform/prebuilts/rust:main:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0f63ea --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ + + +# 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 + +## Minimum Supported Rust Version + +The oldest rust version this crate is tested on is **1.40**. + +This 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. + +If the `tokio` feature is enabled, this crate will inherit the MSRV of the selected [`tokio`](https://crates.io/crates/tokio) version. + +## 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/build.rs b/build.rs new file mode 100644 index 0000000..954dd27 --- /dev/null +++ b/build.rs @@ -0,0 +1,7 @@ +extern crate autocfg; + +fn main() { + let ac = autocfg::new(); + // Allows `#[cfg(rustc_1_63)]` to be used in code + ac.emit_rustc_version(1, 63); +} diff --git a/cargo_embargo.json b/cargo_embargo.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/cargo_embargo.json @@ -0,0 +1 @@ +{} diff --git a/src/dir.rs b/src/dir.rs new file mode 100644 index 0000000..90d8fa4 --- /dev/null +++ b/src/dir.rs @@ -0,0 +1,100 @@ +use std::ffi::OsString; +use std::fs; +use std::io; +use std::path::PathBuf; + +use crate::errors::{Error, ErrorKind}; + +/// Returns an iterator over the entries within a directory. +/// +/// 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 { + /// Returns the full path to the file that this entry represents. + /// + /// 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() + } + + /// Returns the metadata for the file that this entry points at. + /// + /// 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())) + } + + /// Returns the file type for the file that this entry points at. + /// + /// 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())) + } + + /// Returns the file name of this directory entry without any leading path component(s). + /// + /// 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..d732c0e --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,201 @@ +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, + #[allow(dead_code)] + FileExists, + + #[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), + FileExists => write!(formatter, "failed to check file existance `{}`", 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..acdb3c2 --- /dev/null +++ b/src/file.rs @@ -0,0 +1,384 @@ +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 { + /// Attempts to open a file in read-only mode. + /// + /// 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)), + } + } + + /// Opens a file in write-only mode. + /// + /// 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)), + } + } + + /// Attempts to sync all OS-internal metadata to disk. + /// + /// 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)) + } + + /// This function is similar to [`sync_all`], except that it might not synchronize file metadata to the filesystem. + /// + /// 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)) + } + + /// Truncates or extends the underlying file, updating the size of this file to become `size`. + /// + /// 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)) + } + + /// Queries metadata about the underlying file. + /// + /// 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)) + } + + /// Creates a new `File` instance that shares the same underlying file handle as the + /// existing `File` instance. Reads, writes, and seeks will affect both `File` + /// instances simultaneously. + /// + /// 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)) + } + + /// Changes the permissions on the underlying file. + /// + /// 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..b1ef0d5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,284 @@ +/*! +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.11.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; + +/// Read the entire contents of a file into a bytes vector. +/// +/// 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) +} + +/// Read the entire contents of a file into a string. +/// +/// 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) +} + +/// Write a slice as the entire contents of a file. +/// +/// 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)) +} + +/// Copies the contents of one file to another. This function will also copy the +/// permission bits of the original file to the destination file. +/// +/// 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)) +} + +/// Creates a new, empty directory at the provided path. +/// +/// 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)) +} + +/// Recursively create a directory and all of its parent components if they are missing. +/// +/// 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)) +} + +/// Removes an empty directory. +/// +/// 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)) +} + +/// Removes a directory at this path, after removing all its contents. Use carefully! +/// +/// 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)) +} + +/// Removes a file from the filesystem. +/// +/// 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)) +} + +/// Given a path, query the file system to get information about a file, directory, etc. +/// +/// 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)) +} + +/// Returns the canonical, absolute form of a path with all intermediate components +/// normalized and symbolic links resolved. +/// +/// 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)) +} + +/// Creates a new hard link on the filesystem. +/// +/// 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)) +} + +/// Reads a symbolic link, returning the file that the link points to. +/// +/// 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)) +} + +/// Rename a file or directory to a new name, replacing the original file if to already exists. +/// +/// 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)) +} + +/// Query the metadata about a file without following symlinks. +/// +/// 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)) +} + +/// Changes the permissions found on a file or a directory. +/// +/// 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..08c3562 --- /dev/null +++ b/src/open_options.rs @@ -0,0 +1,150 @@ +use std::{fs, io, path::PathBuf}; +#[derive(Clone, Debug)] +/// Wrapper around [`std::fs::OpenOptions`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html) +pub struct OpenOptions(fs::OpenOptions); + +impl OpenOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// 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()) + } + + /// Sets the option for read access. + /// + /// 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 + } + + /// Sets the option for write access. + /// + /// 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 + } + + /// Sets the option for the append mode. + /// + /// 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 + } + + /// Sets the option for truncating a previous file. + /// + /// 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 + } + + /// Sets the option to create a new file, or open it if it already exists. + /// + /// 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 + } + + /// Sets the option to create a new file, failing if it already exists. + /// + /// 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 + } + + /// Opens a file at `path` with the options specified by `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..a0f6b15 --- /dev/null +++ b/src/os/unix.rs @@ -0,0 +1,40 @@ +/// 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; + + /// Creates a new symbolic link on the filesystem. + /// + /// 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..b78c20e --- /dev/null +++ b/src/os/windows.rs @@ -0,0 +1,54 @@ +/// 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; + + /// Creates a new symlink to a directory on the filesystem. + /// + /// 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)) + } + + /// Creates a new symlink to a non-directory file on the filesystem. + /// + /// 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..a232a78 --- /dev/null +++ b/src/path.rs @@ -0,0 +1,67 @@ +#[allow(unused_imports)] +use crate::errors::{Error, ErrorKind}; +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 { + /// Returns Ok(true) if the path points at an existing entity. + /// + /// Wrapper for [`Path::try_exists`](https://doc.rust-lang.org/std/path/struct.Path.html#method.try_exists). + #[cfg(rustc_1_63)] + fn fs_err_try_exists(&self) -> io::Result; + /// Given a path, query the file system to get information about a file, directory, etc. + /// + /// Wrapper for [`crate::metadata`]. + fn fs_err_metadata(&self) -> io::Result; + /// Query the metadata about a file without following symlinks. + /// + /// Wrapper for [`crate::symlink_metadata`]. + fn fs_err_symlink_metadata(&self) -> io::Result; + /// Returns the canonical, absolute form of a path with all intermediate components + /// normalized and symbolic links resolved. + /// + /// Wrapper for [`crate::canonicalize`]. + fn fs_err_canonicalize(&self) -> io::Result; + /// Reads a symbolic link, returning the file that the link points to. + /// + /// Wrapper for [`crate::read_link`]. + fn fs_err_read_link(&self) -> io::Result; + /// Returns an iterator over the entries within a directory. + /// + /// Wrapper for [`crate::read_dir`]. + fn fs_err_read_dir(&self) -> io::Result; +} + +impl PathExt for Path { + #[cfg(rustc_1_63)] + fn fs_err_try_exists(&self) -> io::Result { + self.try_exists() + .map_err(|source| Error::build(source, ErrorKind::FileExists, self)) + } + + 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..df3ffd2 --- /dev/null +++ b/src/tokio/dir_builder.rs @@ -0,0 +1,62 @@ +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() + } + + /// Indicates whether to create directories recursively (including all parent + /// directories). Parents that do not exist are created with the same security and + /// permissions settings. + /// + /// Wrapper around [`tokio::fs::DirBuilder::recursive`]. + pub fn recursive(&mut self, recursive: bool) -> &mut Self { + self.inner.recursive(recursive); + self + } + + /// Creates the specified directory with the configured options. + /// + /// 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 { + /// Sets the mode to create new directories with. + /// + /// 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..2a005ba --- /dev/null +++ b/src/tokio/file.rs @@ -0,0 +1,269 @@ +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 { + /// Attempts to open a file in read-only mode. + /// + /// 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)) + } + + /// Opens a file in write-only mode. + /// + /// 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)), + } + } + + /// Converts a [`crate::File`] to a [`tokio::fs::File`]. + /// + /// 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) + } + + /// Attempts to sync all OS-internal metadata to disk. + /// + /// 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)) + } + + /// This function is similar to `sync_all`, except that it may not + /// synchronize file metadata to the filesystem. + /// + /// 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)) + } + + /// Truncates or extends the underlying file, updating the size of this file to become size. + /// + /// 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)) + } + + /// Queries metadata about the underlying file. + /// + /// 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)) + } + + /// Creates a new `File` instance that shares the same underlying file handle + /// as the existing `File` instance. Reads, writes, and seeks will affect both + /// `File` instances simultaneously. + /// + /// 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)), + } + } + + /// Destructures `File` into a [`crate::File`]. This function is async to allow any + /// in-flight operations to complete. + /// + /// 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) + } + + /// Tries to immediately destructure `File` into a [`crate::File`]. + /// + /// 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)), + } + } + + /// Changes the permissions on the underlying file. + /// + /// 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..1a56532 --- /dev/null +++ b/src/tokio/mod.rs @@ -0,0 +1,245 @@ +//! 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; + +/// Returns the canonical, absolute form of a path with all intermediate +/// components normalized and symbolic links resolved. +/// +/// 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)) +} + +/// Copies the contents of one file to another. This function will also copy the permission bits +/// of the original file to the destination file. +/// This function will overwrite the contents of to. +/// +/// 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)) +} + +/// Creates a new, empty directory at the provided path. +/// +/// 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)) +} + +/// Recursively creates a directory and all of its parent components if they +/// are missing. +/// +/// 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)) +} + +/// Creates a new hard link on the filesystem. +/// +/// 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)) +} + +/// Given a path, queries the file system to get information about a file, +/// directory, etc. +/// +/// 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)) +} + +/// Reads the entire contents of a file into a bytes vector. +/// +/// 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)) +} + +/// Reads a symbolic link, returning the file that the link points to. +/// +/// 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)) +} + +/// Creates a future which will open a file for reading and read the entire +/// contents into a string and return said string. +/// +/// 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)) +} + +/// Removes an existing, empty directory. +/// +/// 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)) +} + +/// Removes a directory at this path, after removing all its contents. Use carefully! +/// +/// 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)) +} + +/// Removes a file from the filesystem. +/// +/// 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)) +} + +/// Renames a file or directory to a new name, replacing the original file if +/// `to` already exists. +/// +/// 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)) +} + +/// Changes the permissions found on a file or a directory. +/// +/// 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)) +} + +/// Queries the file system metadata for a 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)) +} + +/// Creates a new symbolic link on the filesystem. +/// +/// 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)) +} + +/// Creates a new directory symlink on the filesystem. +/// +/// Wrapper for [`tokio::fs::symlink_dir`]. +#[cfg(windows)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +#[deprecated = "use fs_err::tokio::symlink_dir instead"] +pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + symlink_dir(src, dst).await +} + +/// Creates a new directory symlink on the filesystem. +/// +/// Wrapper for [`tokio::fs::symlink_dir`]. +#[cfg(windows)] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] +pub async fn symlink_dir(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)) +} + +/// Creates a new file symbolic link on the filesystem. +/// +/// 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)) +} + +/// Creates a future that will open a file for writing and write the entire +/// contents of `contents` to it. +/// +/// 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..1fb14c4 --- /dev/null +++ b/src/tokio/open_options.rs @@ -0,0 +1,127 @@ +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(), + } + } + + /// Sets the option for read access. + /// + /// Wrapper for [`tokio::fs::OpenOptions::read`]. + pub fn read(&mut self, read: bool) -> &mut OpenOptions { + self.tokio.read(read); + self + } + + /// Sets the option for write access. + /// + /// Wrapper for [`tokio::fs::OpenOptions::write`]. + pub fn write(&mut self, write: bool) -> &mut OpenOptions { + self.tokio.write(write); + self + } + + /// Sets the option for the append mode. + /// + /// Wrapper for [`tokio::fs::OpenOptions::append`]. + pub fn append(&mut self, append: bool) -> &mut OpenOptions { + self.tokio.append(append); + self + } + + /// Sets the option for truncating a previous file. + /// + /// Wrapper for [`tokio::fs::OpenOptions::truncate`]. + pub fn truncate(&mut self, truncate: bool) -> &mut OpenOptions { + self.tokio.truncate(truncate); + self + } + + /// Sets the option for creating a new file. + /// + /// Wrapper for [`tokio::fs::OpenOptions::create`]. + pub fn create(&mut self, create: bool) -> &mut OpenOptions { + self.tokio.create(create); + self + } + + /// Sets the option to always create a new file. + /// + /// 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 + } + + /// Opens a file at `path` with the options specified by `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 { + /// Sets the mode bits that a new file will be created with. + /// + /// Wrapper for [`tokio::fs::OpenOptions::mode`]. + pub fn mode(&mut self, mode: u32) -> &mut OpenOptions { + self.tokio.mode(mode); + self + } + + /// Passes custom flags to the `flags` argument of `open`. + /// + /// 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..c98b5cf --- /dev/null +++ b/src/tokio/read_dir.rs @@ -0,0 +1,111 @@ +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; + +/// Returns a stream over the entries within a directory. +/// +/// 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 { + /// Returns the next entry in the directory stream. + /// + /// 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)), + } + } + + /// Polls for the next directory entry in the stream. + /// + /// 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 { + /// Returns the full path to the file that this entry represents. + /// + /// Wrapper around [`tokio::fs::DirEntry::path`]. + pub fn path(&self) -> PathBuf { + self.tokio.path() + } + + /// Returns the bare file name of this directory entry without any other + /// leading path component. + /// + /// Wrapper around [`tokio::fs::DirEntry::file_name`]. + pub fn file_name(&self) -> OsString { + self.tokio.file_name() + } + + /// Returns the metadata for the file that this entry points at. + /// + /// 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())) + } + + /// Returns the file type for the file that this entry points at. + /// + /// 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 { + /// Returns the underlying `d_ino` field in the contained `dirent` structure. + /// + /// Wrapper around [`tokio::fs::DirEntry::ino`]. + pub fn ino(&self) -> u64 { + self.tokio.ino() + } +} -- cgit v1.2.3