├── .github └── workflows │ └── cargo-test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── RELEASENOTES.md ├── clippy.toml ├── example ├── Cargo.toml ├── README.md └── src │ ├── libc_extras.rs │ ├── libc_wrappers.rs │ ├── main.rs │ └── passthrough.rs ├── smoke_test.sh └── src ├── directory_cache.rs ├── fusemt.rs ├── inode_table.rs ├── lib.rs └── types.rs /.github/workflows/cargo-test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | checks: 7 | name: Cargo checks 8 | runs-on: ${{matrix.os}} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | rust_toolchain: [stable, nightly] 13 | include: 14 | - os: ubuntu-latest 15 | fuse_install: sudo apt-get install fuse libfuse-dev 16 | - rust_toolchain: stable 17 | deny_warnings: --deny warnings 18 | - rust_toolchain: nightly 19 | deny_warnings: 20 | steps: 21 | - name: Set up FUSE 22 | run: ${{matrix.fuse_install}} 23 | 24 | - name: Install Rust toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | components: clippy 29 | toolchain: ${{matrix.rust_toolchain}} 30 | override: true 31 | 32 | - name: Checkout sources 33 | uses: actions/checkout@v2 34 | 35 | - name: Run clippy 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: clippy 39 | args: --all-targets -- ${{matrix.deny_warnings}} 40 | 41 | - name: Run cargo test 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | 46 | - name: Run clippy on example 47 | uses: actions-rs/cargo@v1 48 | with: 49 | command: clippy 50 | args: --manifest-path example/Cargo.toml --all-targets -- ${{matrix.deny_warnings}} 51 | 52 | - name: Run smoke test 53 | run: ./smoke_test.sh 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuse_mt" 3 | version = "0.6.1" 4 | authors = ["William R. Fraser "] 5 | repository = "https://github.com/wfraser/fuse-mt" 6 | description = "A higher-level FUSE filesystem library with multi-threading and inode->path translation." 7 | categories = ["filesystem"] 8 | keywords = ["fuse", "filesystem"] 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | fuser = "0.13" 15 | libc = "0.2" 16 | log = "0.4" 17 | threadpool = "1.8" 18 | 19 | [workspace] 20 | members = [".", "example"] 21 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 William R. Fraser 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FUSE-MT 2 | 3 | ![Build Status](https://github.com/wfraser/fuse-mt/workflows/Cargo%20Checks/badge.svg) 4 | [![Crates.io](https://img.shields.io/crates/v/fuse_mt.svg)](https://crates.io/crates/fuse_mt) 5 | 6 | [Documentation](https://docs.rs/fuse_mt) 7 | 8 | This code is a wrapper on top of the Rust FUSE crate with the following additions: 9 | * Dispatch system calls on multiple threads, so that e.g. I/O doesn't block directory listing. 10 | * Translate inodes into paths, to simplify filesystem implementation. 11 | 12 | The `fuser` crate provides a minimal, low-level access to the FUSE kernel API, whereas this crate is more high-level, like the FUSE C API. 13 | 14 | It includes a sample filesystem that uses the crate to pass all system calls through to another filesystem at any arbitrary path. 15 | 16 | This is a work-in-progress. Bug reports, pull requests, and other feedback are welcome! 17 | 18 | Some random notes on the implementation: 19 | * The trait that filesystems will implement is called `FilesystemMT`, and instead of the FUSE crate's convention of having methods return void and including a "reply" parameter, the methods return their values. This feels more idiomatic to me. They also take `&Path` arguments instead of inode numbers. 20 | * Currently, only the following calls are dispatched to other threads: 21 | * read 22 | * write 23 | * flush 24 | * fsync 25 | * Other calls run synchronously on the main thread because either it is expected that they will complete quickly and/or they require mutating internal state of the InodeTranslator and I want to avoid needing locking in there. 26 | * The inode/path translation is always done on the main thread. 27 | * It might be a good idea to limit the number of concurrent read and write operations in flight. I'm not sure yet how many outstanding read/write requests FUSE will issue though, so it might be a non-issue. 28 | -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | v0.6.1: 2023-09-19 2 | * Updated `fuser` dependency to v0.13. 3 | 4 | v0.6.0: 2022-07-12 5 | * Changed underlying FUSE crate to [`fuser`](https://github.com/cberner/fuser) v0.11 6 | * Note that fuser has some additional capabilities which are not yet being exposed in this 7 | crate's API. These may be added in future releases. 8 | * breaking change: replaced `time` crate's `Timespec` with 9 | `std::time::SystemTime` and `std::time::Duration`. 10 | * breaking change: destroy() no longer takes an argument. 11 | 12 | v0.5.1: 2020-08-16 13 | * Changed FilesystemMT::init default impl to succeed instead of error. 14 | 15 | v0.5.0: 2019-05-11 16 | * Changed read() to use a callback instead of having implementations return data by value. 17 | * This allows implementations to avoid allocating memory in some cases. 18 | 19 | v0.4.4: 2018-02-18 20 | * Implemented `getxtimes` and `setvolname` for macOS 21 | 22 | v0.4.3: 2017-11-08 23 | * Implemented socket file type support from rust-fuse. 24 | * u64 -> i64 offset type changed in rust-fuse; fuse-mt's type is unchanged. 25 | 26 | v0.4.2: 2017-10-30 27 | * Fixed a bug that caused 'forget' log messages on stdout. 28 | 29 | v0.4.1: 2017-06-06 30 | * Added basic derives (Clone, Copy, Debug) for types as appropriate. 31 | 32 | v0.4.0: 2017-05-29 33 | * Removed `FilesystemMT::lookup`. See #10. 34 | * Removed the `ino` field of `FileAttr`. See #12. 35 | 36 | v0.3.0: 2017-02-01 37 | * Merged the `generation-managed` branch. 38 | * The inode table now keeps track of when it re-uses an inode. 39 | * This is a breaking change because the type signature of `ResultEntry` was changed to not 40 | have a `generation` member. This affects the `lookup`, `mknod`, `mkdir`, `symlink`, 41 | `link`, and `create` calls. 42 | * Added some tests for the inode table. 43 | 44 | v0.2.2: 2017-01-13 45 | * fixed a build error on 32-bit Linux. 46 | * added a `VERSION` public const string with the fuse_mt package version. 47 | 48 | v0.2.1: 2017-01-09 49 | * Added lots of documentation. 50 | * Implemented `access`, `setxattr` 51 | * Delay threadpool creation until it is actually used. 52 | * Added `setxattr`, `removexattr` in passthrufs. 53 | * Build fixes for MacOS. 54 | 55 | v0.2.0: 2017-01-06 56 | * Merged the `lookup-refcount` branch. 57 | * The inode table no longer grows without bound. :) 58 | * Fixed readdir() so that filesystems don't need to handle the `offset` parameter at all. 59 | 60 | v0.1.2: 2017-01-06 61 | * Fixed a bug in mknod(): the inode was not set in the response, nor was it added to the inode 62 | table. 63 | * Updated to rust-fuse v0.3.0 64 | * First release on crates.io. 65 | 66 | v0.1.1: 2017-01-06 67 | * (accidental release of experimental branch; yanked) 68 | 69 | v0.1.0: 2017-01-04 70 | * initial release, not yet on crates.io 71 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # Additional words to be allowed without backticks by the `doc_markdown` lint. 2 | doc-valid-idents = ["FuseMT"] 3 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passthrufs" 3 | version = "0.1.0" 4 | authors = ["William R. Fraser "] 5 | edition = "2018" 6 | workspace = ".." 7 | 8 | [dependencies] 9 | libc = "0.2" 10 | log = "0.4" 11 | fuse_mt = { path = ".." } 12 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a sample program that uses fuse_mt. 2 | 3 | It implements a filesystem that forwards all requests to another filesystem at any arbitrary location. 4 | 5 | To use it and test fuse_mt, run: 6 | 7 | cargo run 8 | 9 | Unmount it with `fusermount -u ` or just CTRL-C the running program. 10 | -------------------------------------------------------------------------------- /example/src/libc_extras.rs: -------------------------------------------------------------------------------- 1 | // libc_extras :: Functions missing from the libc crate and wrappers for better cross-platform 2 | // compatibility. 3 | // 4 | // Copyright (c) 2016-2017 by William R. Fraser 5 | // 6 | 7 | pub mod libc { 8 | #![allow(non_camel_case_types)] 9 | 10 | pub use ::libc::*; 11 | 12 | // stuff missing from the libc crate. 13 | extern "system" { 14 | // Specified by POSIX.1-2008; not sure why this is missing. 15 | pub fn fchown(fd: c_int, uid: uid_t, gid: gid_t) -> c_int; 16 | 17 | // On Mac OS X, off_t is always 64 bits. 18 | // https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/64bitPorting/transition/transition.html 19 | #[cfg(target_os = "macos")] 20 | pub fn truncate(path: *const c_char, size: off_t) -> c_int; 21 | 22 | // On Linux, off_t is architecture-dependent, and this is provided for 32-bit systems: 23 | #[cfg(target_os = "linux")] 24 | pub fn truncate64(path: *const c_char, size: off64_t) -> c_int; 25 | 26 | // These XATTR functions are missing from the libc crate on Darwin for some reason. 27 | #[cfg(target_os = "macos")] 28 | pub fn listxattr(path: *const c_char, list: *mut c_char, size: size_t, options: c_int) -> ssize_t; 29 | 30 | #[cfg(target_os = "macos")] 31 | pub fn getxattr(path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t, position: u32, options: c_int) -> ssize_t; 32 | 33 | #[cfg(target_os = "macos")] 34 | pub fn setxattr(path: *const c_char, name: *const c_char, value: *const c_void, size: size_t, flags: c_int, position: u32) -> c_int; 35 | 36 | #[cfg(target_os = "macos")] 37 | pub fn removexattr(path: *const c_char, name: *const c_char, flags: c_int) -> c_int; 38 | } 39 | 40 | // 41 | // Mac-Linux 64-bit compat 42 | // 43 | 44 | #[cfg(target_os = "macos")] 45 | pub type stat64 = stat; 46 | 47 | #[cfg(target_os = "macos")] 48 | pub unsafe fn lstat64(path: *const c_char, stat: *mut stat64) -> c_int { 49 | lstat(path, stat) 50 | } 51 | 52 | #[cfg(target_os = "macos")] 53 | pub unsafe fn fstat64(fd: c_int, stat: *mut stat64) -> c_int { 54 | fstat(fd, stat) 55 | } 56 | 57 | #[cfg(target_os = "macos")] 58 | pub unsafe fn ftruncate64(fd: c_int, length: i64) -> c_int { 59 | ftruncate(fd, length as off_t) 60 | } 61 | 62 | #[cfg(target_os = "macos")] 63 | pub unsafe fn truncate64(path: *const c_char, size: off_t) -> c_int { 64 | truncate(path, size) 65 | } 66 | 67 | #[cfg(target_os = "macos")] 68 | fn timespec_to_timeval(timespec: ×pec) -> timeval { 69 | timeval { 70 | tv_sec: timespec.tv_sec, 71 | tv_usec: timespec.tv_nsec as suseconds_t * 1000, 72 | } 73 | } 74 | 75 | pub const UTIME_OMIT: time_t = (11 << 30) - 21; 76 | 77 | // Mac OS X does not support futimens; map it to futimes with lower precision. 78 | #[cfg(target_os = "macos")] 79 | pub unsafe fn futimens(fd: c_int, times: *const timespec) -> c_int { 80 | use super::super::libc_wrappers; 81 | let mut times_osx = [timespec_to_timeval(&*times), 82 | timespec_to_timeval(&*times)]; 83 | 84 | let mut stat: Option = None; 85 | 86 | if (*times).tv_nsec == UTIME_OMIT { 87 | // atime is unspecified 88 | 89 | stat = match libc_wrappers::fstat(fd as u64) { 90 | Ok(s) => Some(s), 91 | Err(e) => return e, 92 | }; 93 | 94 | times_osx[0].tv_sec = stat.unwrap().st_atime; 95 | times_osx[0].tv_usec = stat.unwrap().st_atime_nsec as suseconds_t * 1000; 96 | } 97 | 98 | if (*times.offset(1)).tv_nsec == UTIME_OMIT { 99 | // mtime is unspecified 100 | 101 | if stat.is_none() { 102 | stat = match libc_wrappers::fstat(fd as u64) { 103 | Ok(s) => Some(s), 104 | Err(e) => return e, 105 | }; 106 | } 107 | 108 | times_osx[1].tv_sec = stat.unwrap().st_mtime; 109 | times_osx[1].tv_usec = stat.unwrap().st_mtime_nsec as suseconds_t * 1000; 110 | } 111 | 112 | futimes(fd, ×_osx as *const timeval) 113 | } 114 | 115 | // Mac OS X does not support utimensat; map it to lutimes with lower precision. 116 | // The relative path feature of utimensat is not supported by this workaround. 117 | #[cfg(target_os = "macos")] 118 | pub fn utimensat(dirfd: c_int, path: *const c_char, times: *const timespec, 119 | _flag_ignored: c_int) -> c_int { 120 | use super::super::libc_wrappers; 121 | unsafe { 122 | if dirfd != AT_FDCWD { 123 | assert_eq!(*path, b'/' as c_char, "relative paths are not supported here!"); 124 | } 125 | let mut times_osx = [timespec_to_timeval(&*times), 126 | timespec_to_timeval(&*times)]; 127 | 128 | let mut stat: Option = None; 129 | fn stat_if_needed(path: *const c_char, stat: &mut Option) -> Result<(), c_int> { 130 | use std::ffi::{CStr, OsString}; 131 | use std::os::unix::ffi::OsStringExt; 132 | if stat.is_none() { 133 | let path_c = unsafe { CStr::from_ptr(path) } .to_owned(); 134 | let path_os = OsString::from_vec(path_c.into_bytes()); 135 | *stat = Some(libc_wrappers::lstat(path_os)?); 136 | } 137 | Ok(()) 138 | } 139 | 140 | if (*times).tv_nsec == UTIME_OMIT { 141 | // atime is unspecified 142 | 143 | if let Err(e) = stat_if_needed(path, &mut stat) { 144 | return e; 145 | } 146 | 147 | times_osx[0].tv_sec = stat.unwrap().st_atime; 148 | times_osx[0].tv_usec = stat.unwrap().st_atime_nsec as suseconds_t * 1000; 149 | } 150 | 151 | if (*times.offset(1)).tv_nsec == UTIME_OMIT { 152 | // mtime is unspecified 153 | 154 | if stat.is_none() { 155 | if let Err(e) = stat_if_needed(path, &mut stat) { 156 | return e; 157 | } 158 | } 159 | times_osx[1].tv_sec = stat.unwrap().st_mtime; 160 | times_osx[1].tv_usec = stat.unwrap().st_mtime_nsec as suseconds_t * 1000; 161 | } 162 | 163 | lutimes(path, ×_osx as *const timeval) 164 | } 165 | } 166 | 167 | // the value is ignored; this is for OS X compat 168 | #[cfg(target_os = "macos")] 169 | pub const AT_FDCWD: c_int = -100; 170 | 171 | // the value is ignored; this is for OS X compat 172 | #[cfg(target_os = "macos")] 173 | pub const AT_SYMLINK_NOFOLLOW: c_int = 0x400; 174 | 175 | #[cfg(target_os = "macos")] 176 | pub const XATTR_NOFOLLOW: c_int = 1; 177 | 178 | #[cfg(target_os = "macos")] 179 | pub unsafe fn llistxattr(path: *const c_char, namebuf: *mut c_char, size: size_t) -> ssize_t { 180 | listxattr(path, namebuf, size, XATTR_NOFOLLOW) 181 | } 182 | 183 | #[cfg(target_os = "macos")] 184 | pub unsafe fn lgetxattr(path: *const c_char, name: *const c_char, value: *mut c_void, size: size_t) -> ssize_t { 185 | getxattr(path, name, value, size, 0, XATTR_NOFOLLOW) 186 | } 187 | 188 | #[cfg(target_os = "macos")] 189 | pub unsafe fn lsetxattr(path: *const c_char, name: *const c_char, value: *const c_void, size: size_t, flags: c_int, position: u32) -> c_int { 190 | setxattr(path, name, value, size, flags | XATTR_NOFOLLOW, position) 191 | } 192 | 193 | #[cfg(target_os = "macos")] 194 | pub unsafe fn lremovexattr(path: *const c_char, name: *const c_char) -> c_int { 195 | removexattr(path, name, XATTR_NOFOLLOW) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /example/src/libc_wrappers.rs: -------------------------------------------------------------------------------- 1 | // Libc Wrappers :: Safe wrappers around system calls. 2 | // 3 | // Copyright (c) 2016-2019 by William R. Fraser 4 | // 5 | 6 | use std::ffi::{CString, OsString}; 7 | use std::io; 8 | use std::mem; 9 | use std::ptr; 10 | use std::os::unix::ffi::OsStringExt; 11 | use crate::libc_extras::libc; 12 | 13 | macro_rules! into_cstring { 14 | ($path:expr, $syscall:expr) => { 15 | match CString::new($path.into_vec()) { 16 | Ok(s) => s, 17 | Err(e) => { 18 | error!(concat!($syscall, ": path {:?} contains interior NUL byte"), 19 | OsString::from_vec(e.into_vec())); 20 | return Err(libc::EINVAL); 21 | } 22 | } 23 | } 24 | } 25 | 26 | pub fn opendir(path: OsString) -> Result { 27 | let path_c = into_cstring!(path, "opendir"); 28 | 29 | let dir: *mut libc::DIR = unsafe { libc::opendir(path_c.as_ptr()) }; 30 | if dir.is_null() { 31 | return Err(io::Error::last_os_error().raw_os_error().unwrap()); 32 | } 33 | 34 | Ok(dir as u64) 35 | } 36 | 37 | pub fn readdir(fh: u64) -> Result, libc::c_int> { 38 | let dir = fh as usize as *mut libc::DIR; 39 | let mut entry: libc::dirent = unsafe { mem::zeroed() }; 40 | let mut result: *mut libc::dirent = ptr::null_mut(); 41 | 42 | let error: i32 = unsafe { libc::readdir_r(dir, &mut entry, &mut result) }; 43 | if error != 0 { 44 | return Err(error); 45 | } 46 | 47 | if result.is_null() { 48 | return Ok(None); 49 | } 50 | 51 | Ok(Some(entry)) 52 | } 53 | 54 | pub fn closedir(fh: u64) -> Result<(), libc::c_int> { 55 | let dir = fh as usize as *mut libc::DIR; 56 | if -1 == unsafe { libc::closedir(dir) } { 57 | Err(io::Error::last_os_error().raw_os_error().unwrap()) 58 | } else { 59 | Ok(()) 60 | } 61 | } 62 | 63 | pub fn open(path: OsString, flags: libc::c_int) -> Result { 64 | let path_c = into_cstring!(path, "open"); 65 | 66 | let fd: libc::c_int = unsafe { libc::open(path_c.as_ptr(), flags) }; 67 | if fd == -1 { 68 | return Err(io::Error::last_os_error().raw_os_error().unwrap()); 69 | } 70 | 71 | Ok(fd as u64) 72 | } 73 | 74 | pub fn close(fh: u64) -> Result<(), libc::c_int> { 75 | let fd = fh as libc::c_int; 76 | if -1 == unsafe { libc::close(fd) } { 77 | Err(io::Error::last_os_error().raw_os_error().unwrap()) 78 | } else { 79 | Ok(()) 80 | } 81 | } 82 | 83 | pub fn lstat(path: OsString) -> Result { 84 | let path_c = into_cstring!(path, "lstat"); 85 | 86 | let mut buf: libc::stat64 = unsafe { mem::zeroed() }; 87 | if -1 == unsafe { libc::lstat64(path_c.as_ptr(), &mut buf) } { 88 | return Err(io::Error::last_os_error().raw_os_error().unwrap()); 89 | } 90 | 91 | Ok(buf) 92 | } 93 | 94 | pub fn fstat(fd: u64) -> Result { 95 | let mut buf: libc::stat64 = unsafe { mem::zeroed() }; 96 | if -1 == unsafe { libc::fstat64(fd as libc::c_int, &mut buf) } { 97 | return Err(io::Error::last_os_error().raw_os_error().unwrap()); 98 | } 99 | 100 | Ok(buf) 101 | } 102 | 103 | pub fn llistxattr(path: OsString, buf: &mut [u8]) -> Result { 104 | let path_c = into_cstring!(path, "llistxattr"); 105 | 106 | let result = unsafe { 107 | libc::llistxattr(path_c.as_ptr(), buf.as_mut_ptr() as *mut libc::c_char, buf.len()) 108 | }; 109 | match result { 110 | -1 => Err(io::Error::last_os_error().raw_os_error().unwrap()), 111 | nbytes => Ok(nbytes as usize), 112 | } 113 | } 114 | 115 | pub fn lgetxattr(path: OsString, name: OsString, buf: &mut [u8]) -> Result { 116 | let path_c = into_cstring!(path, "lgetxattr"); 117 | let name_c = into_cstring!(name, "lgetxattr"); 118 | 119 | let result = unsafe { 120 | libc::lgetxattr(path_c.as_ptr(), name_c.as_ptr(), buf.as_mut_ptr() as *mut libc::c_void, 121 | buf.len()) 122 | }; 123 | match result { 124 | -1 => Err(io::Error::last_os_error().raw_os_error().unwrap()), 125 | nbytes => Ok(nbytes as usize), 126 | } 127 | } 128 | 129 | pub fn lsetxattr(path: OsString, name: OsString, value: &[u8], flags: u32, position: u32) -> Result<(), libc::c_int> { 130 | let path_c = into_cstring!(path, "lsetxattr"); 131 | let name_c = into_cstring!(name, "lsetxattr"); 132 | 133 | // MacOS obnoxiously has an non-standard parameter at the end of their lsetxattr... 134 | #[cfg(target_os = "macos")] 135 | unsafe fn real(path: *const libc::c_char, name: *const libc::c_char, 136 | value: *const libc::c_void, size: libc::size_t, flags: libc::c_int, 137 | position: u32) -> libc::c_int { 138 | libc::lsetxattr(path, name, value, size, flags, position) 139 | } 140 | 141 | #[cfg(not(target_os = "macos"))] 142 | unsafe fn real(path: *const libc::c_char, name: *const libc::c_char, 143 | value: *const libc::c_void, size: libc::size_t, flags: libc::c_int, 144 | _position: u32) -> libc::c_int { 145 | libc::lsetxattr(path, name, value, size, flags) 146 | } 147 | 148 | if cfg!(not(target_os = "macos")) && position != 0 { 149 | error!("lsetxattr: position != 0 is only supported on MacOS"); 150 | return Err(libc::EINVAL); 151 | } 152 | 153 | let result = unsafe { 154 | real(path_c.as_ptr(), name_c.as_ptr(), value.as_ptr() as *const libc::c_void, 155 | value.len(), flags as libc::c_int, position) 156 | }; 157 | 158 | if result == -1 { 159 | Err(io::Error::last_os_error().raw_os_error().unwrap()) 160 | } else { 161 | Ok(()) 162 | } 163 | } 164 | 165 | pub fn lremovexattr(path: OsString, name: OsString) -> Result<(), libc::c_int> { 166 | let path_c = into_cstring!(path, "lremovexattr"); 167 | let name_c = into_cstring!(name, "lremovexattr"); 168 | 169 | if -1 == unsafe { libc::lremovexattr(path_c.as_ptr(), name_c.as_ptr()) } { 170 | Err(io::Error::last_os_error().raw_os_error().unwrap()) 171 | } else { 172 | Ok(()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | // Main Entry Point :: A fuse_mt test program. 2 | // 3 | // Copyright (c) 2016-2022 by William R. Fraser 4 | // 5 | 6 | #![deny(rust_2018_idioms)] 7 | 8 | use std::env; 9 | use std::ffi::{OsStr, OsString}; 10 | 11 | #[macro_use] 12 | extern crate log; 13 | 14 | mod libc_extras; 15 | mod libc_wrappers; 16 | mod passthrough; 17 | 18 | struct ConsoleLogger; 19 | 20 | impl log::Log for ConsoleLogger { 21 | fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool { 22 | true 23 | } 24 | 25 | fn log(&self, record: &log::Record<'_>) { 26 | println!("{}: {}: {}", record.target(), record.level(), record.args()); 27 | } 28 | 29 | fn flush(&self) {} 30 | } 31 | 32 | static LOGGER: ConsoleLogger = ConsoleLogger; 33 | 34 | fn main() { 35 | log::set_logger(&LOGGER).unwrap(); 36 | log::set_max_level(log::LevelFilter::Debug); 37 | 38 | let args: Vec = env::args_os().collect(); 39 | 40 | if args.len() != 3 { 41 | println!("usage: {} ", &env::args().next().unwrap()); 42 | std::process::exit(-1); 43 | } 44 | 45 | let filesystem = passthrough::PassthroughFS { 46 | target: args[1].clone(), 47 | }; 48 | 49 | let fuse_args = [OsStr::new("-o"), OsStr::new("fsname=passthrufs")]; 50 | 51 | fuse_mt::mount(fuse_mt::FuseMT::new(filesystem, 1), &args[2], &fuse_args[..]).unwrap(); 52 | } 53 | -------------------------------------------------------------------------------- /example/src/passthrough.rs: -------------------------------------------------------------------------------- 1 | // PassthroughFS :: A filesystem that passes all calls through to another underlying filesystem. 2 | // 3 | // Implemented using fuse_mt::FilesystemMT. 4 | // 5 | // Copyright (c) 2016-2022 by William R. Fraser 6 | // 7 | 8 | use std::ffi::{CStr, CString, OsStr, OsString}; 9 | use std::fs::{self, File}; 10 | use std::io::{self, Read, Write, Seek, SeekFrom}; 11 | use std::mem; 12 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 13 | use std::os::unix::io::{FromRawFd, IntoRawFd}; 14 | use std::path::{Path, PathBuf}; 15 | use std::time::{Duration, SystemTime}; 16 | 17 | use crate::libc_extras::libc; 18 | use crate::libc_wrappers; 19 | 20 | use fuse_mt::*; 21 | 22 | pub struct PassthroughFS { 23 | pub target: OsString, 24 | } 25 | 26 | fn mode_to_filetype(mode: libc::mode_t) -> FileType { 27 | match mode & libc::S_IFMT { 28 | libc::S_IFDIR => FileType::Directory, 29 | libc::S_IFREG => FileType::RegularFile, 30 | libc::S_IFLNK => FileType::Symlink, 31 | libc::S_IFBLK => FileType::BlockDevice, 32 | libc::S_IFCHR => FileType::CharDevice, 33 | libc::S_IFIFO => FileType::NamedPipe, 34 | libc::S_IFSOCK => FileType::Socket, 35 | _ => { panic!("unknown file type"); } 36 | } 37 | } 38 | 39 | fn stat_to_fuse(stat: libc::stat64) -> FileAttr { 40 | // st_mode encodes both the kind and the permissions 41 | let kind = mode_to_filetype(stat.st_mode); 42 | let perm = (stat.st_mode & 0o7777) as u16; 43 | 44 | let time = |secs: i64, nanos: i64| 45 | SystemTime::UNIX_EPOCH + Duration::new(secs as u64, nanos as u32); 46 | 47 | // libc::nlink_t is wildly different sizes on different platforms: 48 | // linux amd64: u64 49 | // linux x86: u32 50 | // macOS amd64: u16 51 | #[allow(clippy::cast_lossless)] 52 | let nlink = stat.st_nlink as u32; 53 | 54 | FileAttr { 55 | size: stat.st_size as u64, 56 | blocks: stat.st_blocks as u64, 57 | atime: time(stat.st_atime, stat.st_atime_nsec), 58 | mtime: time(stat.st_mtime, stat.st_mtime_nsec), 59 | ctime: time(stat.st_ctime, stat.st_ctime_nsec), 60 | crtime: SystemTime::UNIX_EPOCH, 61 | kind, 62 | perm, 63 | nlink, 64 | uid: stat.st_uid, 65 | gid: stat.st_gid, 66 | rdev: stat.st_rdev as u32, 67 | flags: 0, 68 | } 69 | } 70 | 71 | #[cfg(target_os = "macos")] 72 | fn statfs_to_fuse(statfs: libc::statfs) -> Statfs { 73 | Statfs { 74 | blocks: statfs.f_blocks, 75 | bfree: statfs.f_bfree, 76 | bavail: statfs.f_bavail, 77 | files: statfs.f_files, 78 | ffree: statfs.f_ffree, 79 | bsize: statfs.f_bsize as u32, 80 | namelen: 0, // TODO 81 | frsize: 0, // TODO 82 | } 83 | } 84 | 85 | #[cfg(target_os = "linux")] 86 | fn statfs_to_fuse(statfs: libc::statfs) -> Statfs { 87 | Statfs { 88 | blocks: statfs.f_blocks, 89 | bfree: statfs.f_bfree, 90 | bavail: statfs.f_bavail, 91 | files: statfs.f_files, 92 | ffree: statfs.f_ffree, 93 | bsize: statfs.f_bsize as u32, 94 | namelen: statfs.f_namelen as u32, 95 | frsize: statfs.f_frsize as u32, 96 | } 97 | } 98 | 99 | impl PassthroughFS { 100 | fn real_path(&self, partial: &Path) -> OsString { 101 | PathBuf::from(&self.target) 102 | .join(partial.strip_prefix("/").unwrap()) 103 | .into_os_string() 104 | } 105 | 106 | fn stat_real(&self, path: &Path) -> io::Result { 107 | let real: OsString = self.real_path(path); 108 | debug!("stat_real: {:?}", real); 109 | 110 | match libc_wrappers::lstat(real) { 111 | Ok(stat) => { 112 | Ok(stat_to_fuse(stat)) 113 | }, 114 | Err(e) => { 115 | let err = io::Error::from_raw_os_error(e); 116 | error!("lstat({:?}): {}", path, err); 117 | Err(err) 118 | } 119 | } 120 | } 121 | } 122 | 123 | const TTL: Duration = Duration::from_secs(1); 124 | 125 | impl FilesystemMT for PassthroughFS { 126 | fn init(&self, _req: RequestInfo) -> ResultEmpty { 127 | debug!("init"); 128 | Ok(()) 129 | } 130 | 131 | fn destroy(&self) { 132 | debug!("destroy"); 133 | } 134 | 135 | fn getattr(&self, _req: RequestInfo, path: &Path, fh: Option) -> ResultEntry { 136 | debug!("getattr: {:?}", path); 137 | 138 | if let Some(fh) = fh { 139 | match libc_wrappers::fstat(fh) { 140 | Ok(stat) => Ok((TTL, stat_to_fuse(stat))), 141 | Err(e) => Err(e) 142 | } 143 | } else { 144 | match self.stat_real(path) { 145 | Ok(attr) => Ok((TTL, attr)), 146 | Err(e) => Err(e.raw_os_error().unwrap()) 147 | } 148 | } 149 | } 150 | 151 | fn opendir(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen { 152 | let real = self.real_path(path); 153 | debug!("opendir: {:?} (flags = {:#o})", real, _flags); 154 | match libc_wrappers::opendir(real) { 155 | Ok(fh) => Ok((fh, 0)), 156 | Err(e) => { 157 | let ioerr = io::Error::from_raw_os_error(e); 158 | error!("opendir({:?}): {}", path, ioerr); 159 | Err(e) 160 | } 161 | } 162 | } 163 | 164 | fn releasedir(&self, _req: RequestInfo, path: &Path, fh: u64, _flags: u32) -> ResultEmpty { 165 | debug!("releasedir: {:?}", path); 166 | libc_wrappers::closedir(fh) 167 | } 168 | 169 | fn readdir(&self, _req: RequestInfo, path: &Path, fh: u64) -> ResultReaddir { 170 | debug!("readdir: {:?}", path); 171 | let mut entries: Vec = vec![]; 172 | 173 | if fh == 0 { 174 | error!("readdir: missing fh"); 175 | return Err(libc::EINVAL); 176 | } 177 | 178 | loop { 179 | match libc_wrappers::readdir(fh) { 180 | Ok(Some(entry)) => { 181 | let name_c = unsafe { CStr::from_ptr(entry.d_name.as_ptr()) }; 182 | let name = OsStr::from_bytes(name_c.to_bytes()).to_owned(); 183 | 184 | let filetype = match entry.d_type { 185 | libc::DT_DIR => FileType::Directory, 186 | libc::DT_REG => FileType::RegularFile, 187 | libc::DT_LNK => FileType::Symlink, 188 | libc::DT_BLK => FileType::BlockDevice, 189 | libc::DT_CHR => FileType::CharDevice, 190 | libc::DT_FIFO => FileType::NamedPipe, 191 | libc::DT_SOCK => { 192 | warn!("FUSE doesn't support Socket file type; translating to NamedPipe instead."); 193 | FileType::NamedPipe 194 | }, 195 | _ => { 196 | let entry_path = PathBuf::from(path).join(&name); 197 | let real_path = self.real_path(&entry_path); 198 | match libc_wrappers::lstat(real_path) { 199 | Ok(stat64) => mode_to_filetype(stat64.st_mode), 200 | Err(errno) => { 201 | let ioerr = io::Error::from_raw_os_error(errno); 202 | panic!("lstat failed after readdir_r gave no file type for {:?}: {}", 203 | entry_path, ioerr); 204 | } 205 | } 206 | } 207 | }; 208 | 209 | entries.push(DirectoryEntry { 210 | name, 211 | kind: filetype, 212 | }) 213 | }, 214 | Ok(None) => { break; }, 215 | Err(e) => { 216 | error!("readdir: {:?}: {}", path, e); 217 | return Err(e); 218 | } 219 | } 220 | } 221 | 222 | Ok(entries) 223 | } 224 | 225 | fn open(&self, _req: RequestInfo, path: &Path, flags: u32) -> ResultOpen { 226 | debug!("open: {:?} flags={:#x}", path, flags); 227 | 228 | let real = self.real_path(path); 229 | match libc_wrappers::open(real, flags as libc::c_int) { 230 | Ok(fh) => Ok((fh, flags)), 231 | Err(e) => { 232 | error!("open({:?}): {}", path, io::Error::from_raw_os_error(e)); 233 | Err(e) 234 | } 235 | } 236 | } 237 | 238 | fn release(&self, _req: RequestInfo, path: &Path, fh: u64, _flags: u32, _lock_owner: u64, _flush: bool) -> ResultEmpty { 239 | debug!("release: {:?}", path); 240 | libc_wrappers::close(fh) 241 | } 242 | 243 | fn read(&self, _req: RequestInfo, path: &Path, fh: u64, offset: u64, size: u32, callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult) -> CallbackResult { 244 | debug!("read: {:?} {:#x} @ {:#x}", path, size, offset); 245 | let mut file = unsafe { UnmanagedFile::new(fh) }; 246 | 247 | let mut data = Vec::::with_capacity(size as usize); 248 | 249 | if let Err(e) = file.seek(SeekFrom::Start(offset)) { 250 | error!("seek({:?}, {}): {}", path, offset, e); 251 | return callback(Err(e.raw_os_error().unwrap())); 252 | } 253 | match file.read(unsafe { mem::transmute(data.spare_capacity_mut()) }) { 254 | Ok(n) => { unsafe { data.set_len(n) }; }, 255 | Err(e) => { 256 | error!("read {:?}, {:#x} @ {:#x}: {}", path, size, offset, e); 257 | return callback(Err(e.raw_os_error().unwrap())); 258 | } 259 | } 260 | 261 | callback(Ok(&data)) 262 | } 263 | 264 | fn write(&self, _req: RequestInfo, path: &Path, fh: u64, offset: u64, data: Vec, _flags: u32) -> ResultWrite { 265 | debug!("write: {:?} {:#x} @ {:#x}", path, data.len(), offset); 266 | let mut file = unsafe { UnmanagedFile::new(fh) }; 267 | 268 | if let Err(e) = file.seek(SeekFrom::Start(offset)) { 269 | error!("seek({:?}, {}): {}", path, offset, e); 270 | return Err(e.raw_os_error().unwrap()); 271 | } 272 | let nwritten: u32 = match file.write(&data) { 273 | Ok(n) => n as u32, 274 | Err(e) => { 275 | error!("write {:?}, {:#x} @ {:#x}: {}", path, data.len(), offset, e); 276 | return Err(e.raw_os_error().unwrap()); 277 | } 278 | }; 279 | 280 | Ok(nwritten) 281 | } 282 | 283 | fn flush(&self, _req: RequestInfo, path: &Path, fh: u64, _lock_owner: u64) -> ResultEmpty { 284 | debug!("flush: {:?}", path); 285 | let mut file = unsafe { UnmanagedFile::new(fh) }; 286 | 287 | if let Err(e) = file.flush() { 288 | error!("flush({:?}): {}", path, e); 289 | return Err(e.raw_os_error().unwrap()); 290 | } 291 | 292 | Ok(()) 293 | } 294 | 295 | fn fsync(&self, _req: RequestInfo, path: &Path, fh: u64, datasync: bool) -> ResultEmpty { 296 | debug!("fsync: {:?}, data={:?}", path, datasync); 297 | let file = unsafe { UnmanagedFile::new(fh) }; 298 | 299 | if let Err(e) = if datasync { 300 | file.sync_data() 301 | } else { 302 | file.sync_all() 303 | } { 304 | error!("fsync({:?}, {:?}): {}", path, datasync, e); 305 | return Err(e.raw_os_error().unwrap()); 306 | } 307 | 308 | Ok(()) 309 | } 310 | 311 | fn chmod(&self, _req: RequestInfo, path: &Path, fh: Option, mode: u32) -> ResultEmpty { 312 | debug!("chmod: {:?} to {:#o}", path, mode); 313 | 314 | let result = if let Some(fh) = fh { 315 | unsafe { libc::fchmod(fh as libc::c_int, mode as libc::mode_t) } 316 | } else { 317 | let real = self.real_path(path); 318 | unsafe { 319 | let path_c = CString::from_vec_unchecked(real.into_vec()); 320 | libc::chmod(path_c.as_ptr(), mode as libc::mode_t) 321 | } 322 | }; 323 | 324 | if -1 == result { 325 | let e = io::Error::last_os_error(); 326 | error!("chmod({:?}, {:#o}): {}", path, mode, e); 327 | Err(e.raw_os_error().unwrap()) 328 | } else { 329 | Ok(()) 330 | } 331 | } 332 | 333 | fn chown(&self, _req: RequestInfo, path: &Path, fh: Option, uid: Option, gid: Option) -> ResultEmpty { 334 | let uid = uid.unwrap_or(::std::u32::MAX); // docs say "-1", but uid_t is unsigned 335 | let gid = gid.unwrap_or(::std::u32::MAX); // ditto for gid_t 336 | debug!("chown: {:?} to {}:{}", path, uid, gid); 337 | 338 | let result = if let Some(fd) = fh { 339 | unsafe { libc::fchown(fd as libc::c_int, uid, gid) } 340 | } else { 341 | let real = self.real_path(path); 342 | unsafe { 343 | let path_c = CString::from_vec_unchecked(real.into_vec()); 344 | libc::chown(path_c.as_ptr(), uid, gid) 345 | } 346 | }; 347 | 348 | if -1 == result { 349 | let e = io::Error::last_os_error(); 350 | error!("chown({:?}, {}, {}): {}", path, uid, gid, e); 351 | Err(e.raw_os_error().unwrap()) 352 | } else { 353 | Ok(()) 354 | } 355 | } 356 | 357 | fn truncate(&self, _req: RequestInfo, path: &Path, fh: Option, size: u64) -> ResultEmpty { 358 | debug!("truncate: {:?} to {:#x}", path, size); 359 | 360 | let result = if let Some(fd) = fh { 361 | unsafe { libc::ftruncate64(fd as libc::c_int, size as i64) } 362 | } else { 363 | let real = self.real_path(path); 364 | unsafe { 365 | let path_c = CString::from_vec_unchecked(real.into_vec()); 366 | libc::truncate64(path_c.as_ptr(), size as i64) 367 | } 368 | }; 369 | 370 | if -1 == result { 371 | let e = io::Error::last_os_error(); 372 | error!("truncate({:?}, {}): {}", path, size, e); 373 | Err(e.raw_os_error().unwrap()) 374 | } else { 375 | Ok(()) 376 | } 377 | } 378 | 379 | fn utimens(&self, _req: RequestInfo, path: &Path, fh: Option, atime: Option, mtime: Option) -> ResultEmpty { 380 | debug!("utimens: {:?}: {:?}, {:?}", path, atime, mtime); 381 | 382 | let systemtime_to_libc = |time: Option| -> libc::timespec { 383 | if let Some(time) = time { 384 | let (secs, nanos) = match time.duration_since(SystemTime::UNIX_EPOCH) { 385 | Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()), 386 | Err(in_past) => { 387 | let duration = in_past.duration(); 388 | (-(duration.as_secs() as i64), duration.subsec_nanos()) 389 | } 390 | }; 391 | 392 | libc::timespec { 393 | tv_sec: secs, 394 | tv_nsec: i64::from(nanos), 395 | } 396 | } else { 397 | libc::timespec { 398 | tv_sec: 0, 399 | tv_nsec: libc::UTIME_OMIT, 400 | } 401 | } 402 | }; 403 | 404 | let times = [systemtime_to_libc(atime), systemtime_to_libc(mtime)]; 405 | 406 | let result = if let Some(fd) = fh { 407 | unsafe { libc::futimens(fd as libc::c_int, × as *const libc::timespec) } 408 | } else { 409 | let real = self.real_path(path); 410 | unsafe { 411 | let path_c = CString::from_vec_unchecked(real.into_vec()); 412 | libc::utimensat(libc::AT_FDCWD, path_c.as_ptr(), × as *const libc::timespec, libc::AT_SYMLINK_NOFOLLOW) 413 | } 414 | }; 415 | 416 | if -1 == result { 417 | let e = io::Error::last_os_error(); 418 | error!("utimens({:?}, {:?}, {:?}): {}", path, atime, mtime, e); 419 | Err(e.raw_os_error().unwrap()) 420 | } else { 421 | Ok(()) 422 | } 423 | } 424 | 425 | fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData { 426 | debug!("readlink: {:?}", path); 427 | 428 | let real = self.real_path(path); 429 | match ::std::fs::read_link(real) { 430 | Ok(target) => Ok(target.into_os_string().into_vec()), 431 | Err(e) => Err(e.raw_os_error().unwrap()), 432 | } 433 | } 434 | 435 | fn statfs(&self, _req: RequestInfo, path: &Path) -> ResultStatfs { 436 | debug!("statfs: {:?}", path); 437 | 438 | let real = self.real_path(path); 439 | let mut buf: libc::statfs = unsafe { ::std::mem::zeroed() }; 440 | let result = unsafe { 441 | let path_c = CString::from_vec_unchecked(real.into_vec()); 442 | libc::statfs(path_c.as_ptr(), &mut buf) 443 | }; 444 | 445 | if -1 == result { 446 | let e = io::Error::last_os_error(); 447 | error!("statfs({:?}): {}", path, e); 448 | Err(e.raw_os_error().unwrap()) 449 | } else { 450 | Ok(statfs_to_fuse(buf)) 451 | } 452 | } 453 | 454 | fn fsyncdir(&self, _req: RequestInfo, path: &Path, fh: u64, datasync: bool) -> ResultEmpty { 455 | debug!("fsyncdir: {:?} (datasync = {:?})", path, datasync); 456 | 457 | // TODO: what does datasync mean with regards to a directory handle? 458 | let result = unsafe { libc::fsync(fh as libc::c_int) }; 459 | if -1 == result { 460 | let e = io::Error::last_os_error(); 461 | error!("fsyncdir({:?}): {}", path, e); 462 | Err(e.raw_os_error().unwrap()) 463 | } else { 464 | Ok(()) 465 | } 466 | } 467 | 468 | fn mknod(&self, _req: RequestInfo, parent_path: &Path, name: &OsStr, mode: u32, rdev: u32) -> ResultEntry { 469 | debug!("mknod: {:?}/{:?} (mode={:#o}, rdev={})", parent_path, name, mode, rdev); 470 | 471 | let real = PathBuf::from(self.real_path(parent_path)).join(name); 472 | let result = unsafe { 473 | let path_c = CString::from_vec_unchecked(real.as_os_str().as_bytes().to_vec()); 474 | libc::mknod(path_c.as_ptr(), mode as libc::mode_t, rdev as libc::dev_t) 475 | }; 476 | 477 | if -1 == result { 478 | let e = io::Error::last_os_error(); 479 | error!("mknod({:?}, {}, {}): {}", real, mode, rdev, e); 480 | Err(e.raw_os_error().unwrap()) 481 | } else { 482 | match libc_wrappers::lstat(real.into_os_string()) { 483 | Ok(attr) => Ok((TTL, stat_to_fuse(attr))), 484 | Err(e) => Err(e), // if this happens, yikes 485 | } 486 | } 487 | } 488 | 489 | fn mkdir(&self, _req: RequestInfo, parent_path: &Path, name: &OsStr, mode: u32) -> ResultEntry { 490 | debug!("mkdir {:?}/{:?} (mode={:#o})", parent_path, name, mode); 491 | 492 | let real = PathBuf::from(self.real_path(parent_path)).join(name); 493 | let result = unsafe { 494 | let path_c = CString::from_vec_unchecked(real.as_os_str().as_bytes().to_vec()); 495 | libc::mkdir(path_c.as_ptr(), mode as libc::mode_t) 496 | }; 497 | 498 | if -1 == result { 499 | let e = io::Error::last_os_error(); 500 | error!("mkdir({:?}, {:#o}): {}", real, mode, e); 501 | Err(e.raw_os_error().unwrap()) 502 | } else { 503 | match libc_wrappers::lstat(real.clone().into_os_string()) { 504 | Ok(attr) => Ok((TTL, stat_to_fuse(attr))), 505 | Err(e) => { 506 | error!("lstat after mkdir({:?}, {:#o}): {}", real, mode, e); 507 | Err(e) // if this happens, yikes 508 | }, 509 | } 510 | } 511 | } 512 | 513 | fn unlink(&self, _req: RequestInfo, parent_path: &Path, name: &OsStr) -> ResultEmpty { 514 | debug!("unlink {:?}/{:?}", parent_path, name); 515 | 516 | let real = PathBuf::from(self.real_path(parent_path)).join(name); 517 | fs::remove_file(&real) 518 | .map_err(|ioerr| { 519 | error!("unlink({:?}): {}", real, ioerr); 520 | ioerr.raw_os_error().unwrap() 521 | }) 522 | } 523 | 524 | fn rmdir(&self, _req: RequestInfo, parent_path: &Path, name: &OsStr) -> ResultEmpty { 525 | debug!("rmdir: {:?}/{:?}", parent_path, name); 526 | 527 | let real = PathBuf::from(self.real_path(parent_path)).join(name); 528 | fs::remove_dir(&real) 529 | .map_err(|ioerr| { 530 | error!("rmdir({:?}): {}", real, ioerr); 531 | ioerr.raw_os_error().unwrap() 532 | }) 533 | } 534 | 535 | fn symlink(&self, _req: RequestInfo, parent_path: &Path, name: &OsStr, target: &Path) -> ResultEntry { 536 | debug!("symlink: {:?}/{:?} -> {:?}", parent_path, name, target); 537 | 538 | let real = PathBuf::from(self.real_path(parent_path)).join(name); 539 | match ::std::os::unix::fs::symlink(target, &real) { 540 | Ok(()) => { 541 | match libc_wrappers::lstat(real.clone().into_os_string()) { 542 | Ok(attr) => Ok((TTL, stat_to_fuse(attr))), 543 | Err(e) => { 544 | error!("lstat after symlink({:?}, {:?}): {}", real, target, e); 545 | Err(e) 546 | }, 547 | } 548 | }, 549 | Err(e) => { 550 | error!("symlink({:?}, {:?}): {}", real, target, e); 551 | Err(e.raw_os_error().unwrap()) 552 | } 553 | } 554 | } 555 | 556 | fn rename(&self, _req: RequestInfo, parent_path: &Path, name: &OsStr, newparent_path: &Path, newname: &OsStr) -> ResultEmpty { 557 | debug!("rename: {:?}/{:?} -> {:?}/{:?}", parent_path, name, newparent_path, newname); 558 | 559 | let real = PathBuf::from(self.real_path(parent_path)).join(name); 560 | let newreal = PathBuf::from(self.real_path(newparent_path)).join(newname); 561 | fs::rename(&real, &newreal) 562 | .map_err(|ioerr| { 563 | error!("rename({:?}, {:?}): {}", real, newreal, ioerr); 564 | ioerr.raw_os_error().unwrap() 565 | }) 566 | } 567 | 568 | fn link(&self, _req: RequestInfo, path: &Path, newparent: &Path, newname: &OsStr) -> ResultEntry { 569 | debug!("link: {:?} -> {:?}/{:?}", path, newparent, newname); 570 | 571 | let real = self.real_path(path); 572 | let newreal = PathBuf::from(self.real_path(newparent)).join(newname); 573 | match fs::hard_link(&real, &newreal) { 574 | Ok(()) => { 575 | match libc_wrappers::lstat(real.clone()) { 576 | Ok(attr) => Ok((TTL, stat_to_fuse(attr))), 577 | Err(e) => { 578 | error!("lstat after link({:?}, {:?}): {}", real, newreal, e); 579 | Err(e) 580 | }, 581 | } 582 | }, 583 | Err(e) => { 584 | error!("link({:?}, {:?}): {}", real, newreal, e); 585 | Err(e.raw_os_error().unwrap()) 586 | }, 587 | } 588 | } 589 | 590 | fn create(&self, _req: RequestInfo, parent: &Path, name: &OsStr, mode: u32, flags: u32) -> ResultCreate { 591 | debug!("create: {:?}/{:?} (mode={:#o}, flags={:#x})", parent, name, mode, flags); 592 | 593 | let real = PathBuf::from(self.real_path(parent)).join(name); 594 | let fd = unsafe { 595 | let real_c = CString::from_vec_unchecked(real.clone().into_os_string().into_vec()); 596 | libc::open(real_c.as_ptr(), flags as i32 | libc::O_CREAT | libc::O_EXCL, mode) 597 | }; 598 | 599 | if -1 == fd { 600 | let ioerr = io::Error::last_os_error(); 601 | error!("create({:?}): {}", real, ioerr); 602 | Err(ioerr.raw_os_error().unwrap()) 603 | } else { 604 | match libc_wrappers::lstat(real.clone().into_os_string()) { 605 | Ok(attr) => Ok(CreatedEntry { 606 | ttl: TTL, 607 | attr: stat_to_fuse(attr), 608 | fh: fd as u64, 609 | flags, 610 | }), 611 | Err(e) => { 612 | error!("lstat after create({:?}): {}", real, io::Error::from_raw_os_error(e)); 613 | Err(e) 614 | }, 615 | } 616 | } 617 | } 618 | 619 | fn listxattr(&self, _req: RequestInfo, path: &Path, size: u32) -> ResultXattr { 620 | debug!("listxattr: {:?}", path); 621 | 622 | let real = self.real_path(path); 623 | 624 | if size > 0 { 625 | let mut data = Vec::::with_capacity(size as usize); 626 | let nread = libc_wrappers::llistxattr( 627 | real, unsafe { mem::transmute(data.spare_capacity_mut()) })?; 628 | unsafe { data.set_len(nread) }; 629 | Ok(Xattr::Data(data)) 630 | } else { 631 | let nbytes = libc_wrappers::llistxattr(real, &mut[])?; 632 | Ok(Xattr::Size(nbytes as u32)) 633 | } 634 | } 635 | 636 | fn getxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, size: u32) -> ResultXattr { 637 | debug!("getxattr: {:?} {:?} {}", path, name, size); 638 | 639 | let real = self.real_path(path); 640 | 641 | if size > 0 { 642 | let mut data = Vec::::with_capacity(size as usize); 643 | let nread = libc_wrappers::lgetxattr( 644 | real, name.to_owned(), unsafe { mem::transmute(data.spare_capacity_mut()) })?; 645 | unsafe { data.set_len(nread) }; 646 | Ok(Xattr::Data(data)) 647 | } else { 648 | let nbytes = libc_wrappers::lgetxattr(real, name.to_owned(), &mut [])?; 649 | Ok(Xattr::Size(nbytes as u32)) 650 | } 651 | } 652 | 653 | fn setxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, value: &[u8], flags: u32, position: u32) -> ResultEmpty { 654 | debug!("setxattr: {:?} {:?} {} bytes, flags = {:#x}, pos = {}", path, name, value.len(), flags, position); 655 | let real = self.real_path(path); 656 | libc_wrappers::lsetxattr(real, name.to_owned(), value, flags, position) 657 | } 658 | 659 | fn removexattr(&self, _req: RequestInfo, path: &Path, name: &OsStr) -> ResultEmpty { 660 | debug!("removexattr: {:?} {:?}", path, name); 661 | let real = self.real_path(path); 662 | libc_wrappers::lremovexattr(real, name.to_owned()) 663 | } 664 | 665 | #[cfg(target_os = "macos")] 666 | fn setvolname(&self, _req: RequestInfo, name: &OsStr) -> ResultEmpty { 667 | info!("setvolname: {:?}", name); 668 | Err(libc::ENOTSUP) 669 | } 670 | 671 | #[cfg(target_os = "macos")] 672 | fn getxtimes(&self, _req: RequestInfo, path: &Path) -> ResultXTimes { 673 | debug!("getxtimes: {:?}", path); 674 | let xtimes = XTimes { 675 | bkuptime: SystemTime::UNIX_EPOCH, 676 | crtime: SystemTime::UNIX_EPOCH, 677 | }; 678 | Ok(xtimes) 679 | } 680 | } 681 | 682 | /// A file that is not closed upon leaving scope. 683 | struct UnmanagedFile { 684 | inner: Option, 685 | } 686 | 687 | impl UnmanagedFile { 688 | unsafe fn new(fd: u64) -> UnmanagedFile { 689 | UnmanagedFile { 690 | inner: Some(File::from_raw_fd(fd as i32)) 691 | } 692 | } 693 | fn sync_all(&self) -> io::Result<()> { 694 | self.inner.as_ref().unwrap().sync_all() 695 | } 696 | fn sync_data(&self) -> io::Result<()> { 697 | self.inner.as_ref().unwrap().sync_data() 698 | } 699 | } 700 | 701 | impl Drop for UnmanagedFile { 702 | fn drop(&mut self) { 703 | // Release control of the file descriptor so it is not closed. 704 | let file = self.inner.take().unwrap(); 705 | file.into_raw_fd(); 706 | } 707 | } 708 | 709 | impl Read for UnmanagedFile { 710 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 711 | self.inner.as_ref().unwrap().read(buf) 712 | } 713 | fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 714 | self.inner.as_ref().unwrap().read_to_end(buf) 715 | } 716 | } 717 | 718 | impl Write for UnmanagedFile { 719 | fn write(&mut self, buf: &[u8]) -> io::Result { 720 | self.inner.as_ref().unwrap().write(buf) 721 | } 722 | fn flush(&mut self) -> io::Result<()> { 723 | self.inner.as_ref().unwrap().flush() 724 | } 725 | } 726 | 727 | impl Seek for UnmanagedFile { 728 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 729 | self.inner.as_ref().unwrap().seek(pos) 730 | } 731 | } 732 | -------------------------------------------------------------------------------- /smoke_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd example 6 | mkdir mount 7 | cargo build 8 | cargo run src mount & 9 | 10 | delay=0 11 | while [ $delay -lt 5 ]; do 12 | if [ ! -f mount/main.rs ]; then 13 | echo "waiting for startup" 14 | delay=$(($delay + 1)) 15 | sleep 1 16 | else 17 | break 18 | fi 19 | done 20 | 21 | if [ $delay -eq 5 ]; then 22 | echo "took too long to start up" 23 | exit 2 24 | fi 25 | 26 | # exclude the initial "total" line because it's not really accurate for virtual filesystems 27 | diff <(ls -al src | grep -v '^total') <(ls -al mount | grep -v '^total') 28 | 29 | diff src/main.rs mount/main.rs 30 | 31 | if [ $(uname) = "Linux" ]; then 32 | fusermount -u mount 33 | else 34 | umount mount 35 | fi 36 | rmdir mount 37 | -------------------------------------------------------------------------------- /src/directory_cache.rs: -------------------------------------------------------------------------------- 1 | // DirectoryCache :: a cache for directory entries to simplify readdir calls. 2 | // 3 | // Copyright (c) 2017-2019 by William R. Fraser 4 | // 5 | 6 | use std::collections::HashMap; 7 | use std::num::Wrapping; 8 | 9 | use super::DirectoryEntry; 10 | 11 | /// Directory entry cache. 12 | /// 13 | /// The way FUSE does readdir() is it gives you a buffer and an offset and asks you to fill the 14 | /// buffer. If you have more entries than fit in the buffer, FUSE will call you again with a higher 15 | /// offset, until you return an empty buffer. 16 | /// 17 | /// Implementing this in the filesystem is tedious and a little tricky, so instead fuse-mt has the 18 | /// filesystem just return a Vec with *all* the directory entries, and it takes care of paginating 19 | /// it for FUSE. 20 | /// 21 | /// To do this, we need to cache the response from the filesystem, and we need to give FUSE our own 22 | /// file handle (the cache entry key) instead of the one the filesystem returned from opendir(), so 23 | /// we have to store that file handle as well. 24 | #[derive(Debug)] 25 | pub struct DirectoryCache { 26 | next_key: Wrapping, 27 | entries: HashMap, 28 | } 29 | 30 | impl DirectoryCache { 31 | pub fn new() -> DirectoryCache { 32 | DirectoryCache { 33 | next_key: Wrapping(1), 34 | entries: HashMap::new(), 35 | } 36 | } 37 | 38 | /// Add a new entry with the given file handle and an un-populated directory entry list. 39 | /// This is intended to be called on opendir(). 40 | pub fn new_entry(&mut self, fh: u64) -> u64 { 41 | let key = self.next_key.0; 42 | self.entries.insert(key, DirectoryCacheEntry::new(fh)); 43 | self.next_key += Wrapping(1); 44 | key 45 | } 46 | 47 | /// Get the real file handle (the one set by the filesystem) for a given cache entry key. 48 | /// Panics if there is no such key. 49 | pub fn real_fh(&self, key: u64) -> u64 { 50 | self.entries.get(&key).unwrap_or_else(|| { 51 | panic!("no such directory cache key {}", key); 52 | }).fh 53 | } 54 | 55 | /// Get a mutable reference to the cache entry (file handle and entries) for the given key. 56 | /// Panics if there is no such key. 57 | pub fn get_mut(&mut self, key: u64) -> &mut DirectoryCacheEntry { 58 | self.entries.get_mut(&key).unwrap_or_else(|| { 59 | panic!("no such directory cache key {}", key); 60 | }) 61 | } 62 | 63 | /// Delete the cache entry with the given key. 64 | /// This is intended to be called on releasedir(). 65 | /// Panics if there is no such key. 66 | pub fn delete(&mut self, key: u64) { 67 | self.entries.remove(&key); 68 | } 69 | } 70 | 71 | #[derive(Debug)] 72 | pub struct DirectoryCacheEntry { 73 | pub fh: u64, 74 | pub entries: Option>, 75 | } 76 | 77 | impl DirectoryCacheEntry { 78 | pub fn new(fh: u64) -> DirectoryCacheEntry { 79 | DirectoryCacheEntry { 80 | fh, 81 | entries: None, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/fusemt.rs: -------------------------------------------------------------------------------- 1 | // FuseMT :: A wrapper around FUSE that presents paths instead of inodes and dispatches I/O 2 | // operations to multiple threads. 3 | // 4 | // Copyright (c) 2016-2022 by William R. Fraser 5 | // 6 | 7 | use std::ffi::OsStr; 8 | use std::path::{Path, PathBuf}; 9 | use std::sync::Arc; 10 | use std::time::SystemTime; 11 | 12 | use fuser::TimeOrNow; 13 | use threadpool::ThreadPool; 14 | 15 | use crate::directory_cache::*; 16 | use crate::inode_table::*; 17 | use crate::types::*; 18 | 19 | trait IntoRequestInfo { 20 | fn info(&self) -> RequestInfo; 21 | } 22 | 23 | impl<'a> IntoRequestInfo for fuser::Request<'a> { 24 | fn info(&self) -> RequestInfo { 25 | RequestInfo { 26 | unique: self.unique(), 27 | uid: self.uid(), 28 | gid: self.gid(), 29 | pid: self.pid(), 30 | } 31 | } 32 | } 33 | 34 | fn fuse_fileattr(attr: FileAttr, ino: u64) -> fuser::FileAttr { 35 | fuser::FileAttr { 36 | ino, 37 | size: attr.size, 38 | blocks: attr.blocks, 39 | atime: attr.atime, 40 | mtime: attr.mtime, 41 | ctime: attr.ctime, 42 | crtime: attr.crtime, 43 | kind: attr.kind, 44 | perm: attr.perm, 45 | nlink: attr.nlink, 46 | uid: attr.uid, 47 | gid: attr.gid, 48 | rdev: attr.rdev, 49 | blksize: 4096, // TODO 50 | flags: attr.flags, 51 | } 52 | } 53 | 54 | trait TimeOrNowExt { 55 | fn time(self) -> SystemTime; 56 | } 57 | 58 | impl TimeOrNowExt for TimeOrNow { 59 | fn time(self) -> SystemTime { 60 | match self { 61 | TimeOrNow::SpecificTime(t) => t, 62 | TimeOrNow::Now => SystemTime::now(), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct FuseMT { 69 | target: Arc, 70 | inodes: InodeTable, 71 | threads: Option, 72 | num_threads: usize, 73 | directory_cache: DirectoryCache, 74 | } 75 | 76 | impl FuseMT { 77 | pub fn new(target_fs: T, num_threads: usize) -> FuseMT { 78 | FuseMT { 79 | target: Arc::new(target_fs), 80 | inodes: InodeTable::new(), 81 | threads: None, 82 | num_threads, 83 | directory_cache: DirectoryCache::new(), 84 | } 85 | } 86 | 87 | fn threadpool_run(&mut self, f: F) { 88 | if self.num_threads == 0 { 89 | f() 90 | } else { 91 | if self.threads.is_none() { 92 | debug!("initializing threadpool with {} threads", self.num_threads); 93 | self.threads = Some(ThreadPool::new(self.num_threads)); 94 | } 95 | self.threads.as_ref().unwrap().execute(f); 96 | } 97 | } 98 | } 99 | 100 | macro_rules! get_path { 101 | ($s:expr, $ino:expr, $reply:expr) => { 102 | if let Some(path) = $s.inodes.get_path($ino) { 103 | path 104 | } else { 105 | $reply.error(libc::EINVAL); 106 | return; 107 | } 108 | } 109 | } 110 | 111 | impl fuser::Filesystem for FuseMT { 112 | fn init( 113 | &mut self, 114 | req: &fuser::Request<'_>, 115 | _config: &mut fuser::KernelConfig, // TODO 116 | ) -> Result<(), libc::c_int> { 117 | debug!("init"); 118 | self.target.init(req.info()) 119 | } 120 | 121 | fn destroy(&mut self) { 122 | debug!("destroy"); 123 | self.target.destroy(); 124 | } 125 | 126 | fn lookup( 127 | &mut self, 128 | req: &fuser::Request<'_>, 129 | parent: u64, 130 | name: &OsStr, 131 | reply: fuser::ReplyEntry, 132 | ) { 133 | let parent_path = get_path!(self, parent, reply); 134 | debug!("lookup: {:?}, {:?}", parent_path, name); 135 | let path = Arc::new((*parent_path).clone().join(name)); 136 | match self.target.getattr(req.info(), &path, None) { 137 | Ok((ttl, attr)) => { 138 | let (ino, generation) = self.inodes.add_or_get(path.clone()); 139 | self.inodes.lookup(ino); 140 | reply.entry(&ttl, &fuse_fileattr(attr, ino), generation); 141 | }, 142 | Err(e) => reply.error(e), 143 | } 144 | } 145 | 146 | fn forget( 147 | &mut self, 148 | _req: &fuser::Request<'_>, 149 | ino: u64, 150 | nlookup: u64, 151 | ) { 152 | let path = self.inodes.get_path(ino).unwrap_or_else(|| { 153 | Arc::new(PathBuf::from("[unknown]")) 154 | }); 155 | let lookups = self.inodes.forget(ino, nlookup); 156 | debug!("forget: inode {} ({:?}) now at {} lookups", ino, path, lookups); 157 | } 158 | 159 | fn getattr( 160 | &mut self, 161 | req: &fuser::Request<'_>, 162 | ino: u64, 163 | reply: fuser::ReplyAttr, 164 | ) { 165 | let path = get_path!(self, ino, reply); 166 | debug!("getattr: {:?}", path); 167 | match self.target.getattr(req.info(), &path, None) { 168 | Ok((ttl, attr)) => { 169 | reply.attr(&ttl, &fuse_fileattr(attr, ino)) 170 | }, 171 | Err(e) => reply.error(e), 172 | } 173 | } 174 | 175 | fn setattr( 176 | &mut self, 177 | req: &fuser::Request<'_>, // passed to all 178 | ino: u64, // translated to path; passed to all 179 | mode: Option, // chmod 180 | uid: Option, // chown 181 | gid: Option, // chown 182 | size: Option, // truncate 183 | atime: Option, // utimens 184 | mtime: Option, // utimens 185 | _ctime: Option, // ? TODO 186 | fh: Option, // passed to all 187 | crtime: Option, // utimens_osx (OS X only) 188 | chgtime: Option, // utimens_osx (OS X only) 189 | bkuptime: Option, // utimens_osx (OS X only) 190 | flags: Option, // utimens_osx (OS X only) 191 | reply: fuser::ReplyAttr, 192 | ) { 193 | let path = get_path!(self, ino, reply); 194 | debug!("setattr: {:?}", path); 195 | 196 | debug!("\tino:\t{:?}", ino); 197 | debug!("\tmode:\t{:?}", mode); 198 | debug!("\tuid:\t{:?}", uid); 199 | debug!("\tgid:\t{:?}", gid); 200 | debug!("\tsize:\t{:?}", size); 201 | debug!("\tatime:\t{:?}", atime); 202 | debug!("\tmtime:\t{:?}", mtime); 203 | debug!("\tfh:\t{:?}", fh); 204 | 205 | // TODO: figure out what C FUSE does when only some of these are implemented. 206 | 207 | if let Some(mode) = mode { 208 | if let Err(e) = self.target.chmod(req.info(), &path, fh, mode) { 209 | reply.error(e); 210 | return; 211 | } 212 | } 213 | 214 | if uid.is_some() || gid.is_some() { 215 | if let Err(e) = self.target.chown(req.info(), &path, fh, uid, gid) { 216 | reply.error(e); 217 | return; 218 | } 219 | } 220 | 221 | if let Some(size) = size { 222 | if let Err(e) = self.target.truncate(req.info(), &path, fh, size) { 223 | reply.error(e); 224 | return; 225 | } 226 | } 227 | 228 | if atime.is_some() || mtime.is_some() { 229 | let atime = atime.map(TimeOrNowExt::time); 230 | let mtime = mtime.map(TimeOrNowExt::time); 231 | if let Err(e) = self.target.utimens(req.info(), &path, fh, atime, mtime) { 232 | reply.error(e); 233 | return; 234 | } 235 | } 236 | 237 | if crtime.is_some() || chgtime.is_some() || bkuptime.is_some() || flags.is_some() { 238 | if let Err(e) = self.target.utimens_macos(req.info(), &path, fh, crtime, chgtime, bkuptime, flags) { 239 | reply.error(e); 240 | return 241 | } 242 | } 243 | 244 | match self.target.getattr(req.info(), &path, fh) { 245 | Ok((ttl, attr)) => reply.attr(&ttl, &fuse_fileattr(attr, ino)), 246 | Err(e) => reply.error(e), 247 | } 248 | } 249 | 250 | fn readlink( 251 | &mut self, 252 | req: &fuser::Request<'_>, 253 | ino: u64, 254 | reply: fuser::ReplyData, 255 | ) { 256 | let path = get_path!(self, ino, reply); 257 | debug!("readlink: {:?}", path); 258 | match self.target.readlink(req.info(), &path) { 259 | Ok(data) => reply.data(&data), 260 | Err(e) => reply.error(e), 261 | } 262 | } 263 | 264 | fn mknod( 265 | &mut self, 266 | req: &fuser::Request<'_>, 267 | parent: u64, 268 | name: &OsStr, 269 | mode: u32, 270 | _umask: u32, // TODO 271 | rdev: u32, 272 | reply: fuser::ReplyEntry, 273 | ) { 274 | let parent_path = get_path!(self, parent, reply); 275 | debug!("mknod: {:?}/{:?}", parent_path, name); 276 | match self.target.mknod(req.info(), &parent_path, name, mode, rdev) { 277 | Ok((ttl, attr)) => { 278 | let (ino, generation) = self.inodes.add(Arc::new(parent_path.join(name))); 279 | reply.entry(&ttl, &fuse_fileattr(attr, ino), generation) 280 | }, 281 | Err(e) => reply.error(e), 282 | } 283 | } 284 | 285 | fn mkdir( 286 | &mut self, 287 | req: &fuser::Request<'_>, 288 | parent: u64, 289 | name: &OsStr, 290 | mode: u32, 291 | _umask: u32, // TODO 292 | reply: fuser::ReplyEntry, 293 | ) { 294 | let parent_path = get_path!(self, parent, reply); 295 | debug!("mkdir: {:?}/{:?}", parent_path, name); 296 | match self.target.mkdir(req.info(), &parent_path, name, mode) { 297 | Ok((ttl, attr)) => { 298 | let (ino, generation) = self.inodes.add(Arc::new(parent_path.join(name))); 299 | reply.entry(&ttl, &fuse_fileattr(attr, ino), generation) 300 | }, 301 | Err(e) => reply.error(e), 302 | } 303 | } 304 | 305 | fn unlink( 306 | &mut self, 307 | req: &fuser::Request<'_>, 308 | parent: u64, 309 | name: &OsStr, 310 | reply: fuser::ReplyEmpty, 311 | ) { 312 | let parent_path = get_path!(self, parent, reply); 313 | debug!("unlink: {:?}/{:?}", parent_path, name); 314 | match self.target.unlink(req.info(), &parent_path, name) { 315 | Ok(()) => { 316 | self.inodes.unlink(&parent_path.join(name)); 317 | reply.ok() 318 | }, 319 | Err(e) => reply.error(e), 320 | } 321 | } 322 | 323 | fn rmdir( 324 | &mut self, 325 | req: &fuser::Request<'_>, 326 | parent: u64, 327 | name: &OsStr, 328 | reply: fuser::ReplyEmpty, 329 | ) { 330 | let parent_path = get_path!(self, parent, reply); 331 | debug!("rmdir: {:?}/{:?}", parent_path, name); 332 | match self.target.rmdir(req.info(), &parent_path, name) { 333 | Ok(()) => reply.ok(), 334 | Err(e) => reply.error(e), 335 | } 336 | } 337 | 338 | fn symlink( 339 | &mut self, 340 | req: &fuser::Request<'_>, 341 | parent: u64, 342 | name: &OsStr, 343 | link: &Path, 344 | reply: fuser::ReplyEntry, 345 | ) { 346 | let parent_path = get_path!(self, parent, reply); 347 | debug!("symlink: {:?}/{:?} -> {:?}", parent_path, name, link); 348 | match self.target.symlink(req.info(), &parent_path, name, link) { 349 | Ok((ttl, attr)) => { 350 | let (ino, generation) = self.inodes.add(Arc::new(parent_path.join(name))); 351 | reply.entry(&ttl, &fuse_fileattr(attr, ino), generation) 352 | }, 353 | Err(e) => reply.error(e), 354 | } 355 | } 356 | 357 | fn rename( 358 | &mut self, 359 | req: &fuser::Request<'_>, 360 | parent: u64, 361 | name: &OsStr, 362 | newparent: u64, 363 | newname: &OsStr, 364 | _flags: u32, // TODO 365 | reply: fuser::ReplyEmpty, 366 | ) { 367 | let parent_path = get_path!(self, parent, reply); 368 | let newparent_path = get_path!(self, newparent, reply); 369 | debug!("rename: {:?}/{:?} -> {:?}/{:?}", parent_path, name, newparent_path, newname); 370 | match self.target.rename(req.info(), &parent_path, name, &newparent_path, newname) { 371 | Ok(()) => { 372 | self.inodes.rename(&parent_path.join(name), Arc::new(newparent_path.join(newname))); 373 | reply.ok() 374 | }, 375 | Err(e) => reply.error(e), 376 | } 377 | } 378 | 379 | fn link( 380 | &mut self, 381 | req: &fuser::Request<'_>, 382 | ino: u64, 383 | newparent: u64, 384 | newname: &OsStr, 385 | reply: fuser::ReplyEntry, 386 | ) { 387 | let path = get_path!(self, ino, reply); 388 | let newparent_path = get_path!(self, newparent, reply); 389 | debug!("link: {:?} -> {:?}/{:?}", path, newparent_path, newname); 390 | match self.target.link(req.info(), &path, &newparent_path, newname) { 391 | Ok((ttl, attr)) => { 392 | // NOTE: this results in the new link having a different inode from the original. 393 | // This is needed because our inode table is a 1:1 map between paths and inodes. 394 | let (new_ino, generation) = self.inodes.add(Arc::new(newparent_path.join(newname))); 395 | reply.entry(&ttl, &fuse_fileattr(attr, new_ino), generation); 396 | }, 397 | Err(e) => reply.error(e), 398 | } 399 | } 400 | 401 | fn open( 402 | &mut self, 403 | req: &fuser::Request<'_>, 404 | ino: u64, 405 | flags: i32, 406 | reply: fuser::ReplyOpen, 407 | ) { 408 | let path = get_path!(self, ino, reply); 409 | debug!("open: {:?}", path); 410 | match self.target.open(req.info(), &path, flags as u32) { // TODO: change flags to i32 411 | Ok((fh, flags)) => reply.opened(fh, flags), 412 | Err(e) => reply.error(e), 413 | } 414 | } 415 | 416 | fn read( 417 | &mut self, 418 | req: &fuser::Request<'_>, 419 | ino: u64, 420 | fh: u64, 421 | offset: i64, 422 | size: u32, 423 | _flags: i32, // TODO 424 | _lock_owner: Option, // TODO 425 | reply: fuser::ReplyData, 426 | ) { 427 | let path = get_path!(self, ino, reply); 428 | debug!("read: {:?} {:#x} @ {:#x}", path, size, offset); 429 | if offset < 0 { 430 | error!("read called with a negative offset"); 431 | reply.error(libc::EINVAL); 432 | return; 433 | } 434 | let target = self.target.clone(); 435 | let req_info = req.info(); 436 | self.threadpool_run(move || { 437 | target.read(req_info, &path, fh, offset as u64, size, |result| { 438 | match result { 439 | Ok(data) => reply.data(data), 440 | Err(e) => reply.error(e), 441 | } 442 | CallbackResult { 443 | _private: std::marker::PhantomData {}, 444 | } 445 | }); 446 | }); 447 | } 448 | 449 | fn write( 450 | &mut self, 451 | req: &fuser::Request<'_>, 452 | ino: u64, 453 | fh: u64, 454 | offset: i64, 455 | data: &[u8], 456 | _write_flags: u32, // TODO 457 | flags: i32, 458 | _lock_owner: Option, // TODO 459 | reply: fuser::ReplyWrite, 460 | ) { 461 | let path = get_path!(self, ino, reply); 462 | debug!("write: {:?} {:#x} @ {:#x}", path, data.len(), offset); 463 | if offset < 0 { 464 | error!("write called with a negative offset"); 465 | reply.error(libc::EINVAL); 466 | return; 467 | } 468 | let target = self.target.clone(); 469 | let req_info = req.info(); 470 | 471 | // The data needs to be copied here before dispatching to the threadpool because it's a 472 | // slice of a single buffer that `fuser` re-uses for the entire session. 473 | let data_buf = Vec::from(data); 474 | 475 | self.threadpool_run(move|| { 476 | match target.write(req_info, &path, fh, offset as u64, data_buf, flags as u32) { 477 | Ok(written) => reply.written(written), 478 | Err(e) => reply.error(e), 479 | } 480 | }); 481 | } 482 | 483 | fn flush( 484 | &mut self, 485 | req: &fuser::Request<'_>, 486 | ino: u64, 487 | fh: u64, 488 | lock_owner: u64, 489 | reply: fuser::ReplyEmpty, 490 | ) { 491 | let path = get_path!(self, ino, reply); 492 | debug!("flush: {:?}", path); 493 | let target = self.target.clone(); 494 | let req_info = req.info(); 495 | self.threadpool_run(move|| { 496 | match target.flush(req_info, &path, fh, lock_owner) { 497 | Ok(()) => reply.ok(), 498 | Err(e) => reply.error(e), 499 | } 500 | }); 501 | } 502 | 503 | fn release( 504 | &mut self, 505 | req: &fuser::Request<'_>, 506 | ino: u64, 507 | fh: u64, 508 | flags: i32, 509 | lock_owner: Option, 510 | flush: bool, 511 | reply: fuser::ReplyEmpty, 512 | ) { 513 | let path = get_path!(self, ino, reply); 514 | debug!("release: {:?}", path); 515 | match self.target.release( 516 | req.info(), &path, fh, flags as u32, lock_owner.unwrap_or(0) /* TODO */, flush) 517 | { 518 | Ok(()) => reply.ok(), 519 | Err(e) => reply.error(e), 520 | } 521 | } 522 | 523 | fn fsync( 524 | &mut self, 525 | req: &fuser::Request<'_>, 526 | ino: u64, 527 | fh: u64, 528 | datasync: bool, 529 | reply: fuser::ReplyEmpty, 530 | ) { 531 | let path = get_path!(self, ino, reply); 532 | debug!("fsync: {:?}", path); 533 | let target = self.target.clone(); 534 | let req_info = req.info(); 535 | self.threadpool_run(move|| { 536 | match target.fsync(req_info, &path, fh, datasync) { 537 | Ok(()) => reply.ok(), 538 | Err(e) => reply.error(e), 539 | } 540 | }); 541 | } 542 | 543 | fn opendir( 544 | &mut self, 545 | req: &fuser::Request<'_>, 546 | ino: u64, 547 | flags: i32, 548 | reply: fuser::ReplyOpen, 549 | ) { 550 | let path = get_path!(self, ino, reply); 551 | debug!("opendir: {:?}", path); 552 | match self.target.opendir(req.info(), &path, flags as u32) { 553 | Ok((fh, flags)) => { 554 | let dcache_key = self.directory_cache.new_entry(fh); 555 | reply.opened(dcache_key, flags); 556 | }, 557 | Err(e) => reply.error(e), 558 | } 559 | } 560 | 561 | fn readdir( 562 | &mut self, 563 | req: &fuser::Request<'_>, 564 | ino: u64, 565 | fh: u64, 566 | offset: i64, 567 | mut reply: fuser::ReplyDirectory, 568 | ) { 569 | let path = get_path!(self, ino, reply); 570 | debug!("readdir: {:?} @ {}", path, offset); 571 | 572 | if offset < 0 { 573 | error!("readdir called with a negative offset"); 574 | reply.error(libc::EINVAL); 575 | return; 576 | } 577 | 578 | let entries: &[DirectoryEntry] = { 579 | let dcache_entry = self.directory_cache.get_mut(fh); 580 | if let Some(ref entries) = dcache_entry.entries { 581 | entries 582 | } else { 583 | debug!("entries not yet fetched; requesting with fh {}", dcache_entry.fh); 584 | match self.target.readdir(req.info(), &path, dcache_entry.fh) { 585 | Ok(entries) => { 586 | dcache_entry.entries = Some(entries); 587 | dcache_entry.entries.as_ref().unwrap() 588 | }, 589 | Err(e) => { 590 | reply.error(e); 591 | return; 592 | } 593 | } 594 | } 595 | }; 596 | 597 | let parent_inode = if ino == 1 { 598 | ino 599 | } else { 600 | let parent_path: &Path = path.parent().unwrap(); 601 | match self.inodes.get_inode(parent_path) { 602 | Some(inode) => inode, 603 | None => { 604 | error!("readdir: unable to get inode for parent of {:?}", path); 605 | reply.error(libc::EIO); 606 | return; 607 | } 608 | } 609 | }; 610 | 611 | debug!("directory has {} entries", entries.len()); 612 | 613 | for (index, entry) in entries.iter().skip(offset as usize).enumerate() { 614 | let entry_inode = if entry.name == Path::new(".") { 615 | ino 616 | } else if entry.name == Path::new("..") { 617 | parent_inode 618 | } else { 619 | // Don't bother looking in the inode table for the entry; FUSE doesn't pre- 620 | // populate its inode cache with this value, so subsequent access to these 621 | // files is going to involve it issuing a LOOKUP operation anyway. 622 | !1 623 | }; 624 | 625 | debug!("readdir: adding entry #{}, {:?}", offset + index as i64, entry.name); 626 | 627 | let buffer_full: bool = reply.add( 628 | entry_inode, 629 | offset + index as i64 + 1, 630 | entry.kind, 631 | entry.name.as_os_str()); 632 | 633 | if buffer_full { 634 | debug!("readdir: reply buffer is full"); 635 | break; 636 | } 637 | } 638 | 639 | reply.ok(); 640 | } 641 | 642 | fn releasedir( 643 | &mut self, 644 | req: &fuser::Request<'_>, 645 | ino: u64, 646 | fh: u64, 647 | flags: i32, 648 | reply: fuser::ReplyEmpty, 649 | ) { 650 | let path = get_path!(self, ino, reply); 651 | debug!("releasedir: {:?}", path); 652 | let real_fh = self.directory_cache.real_fh(fh); 653 | match self.target.releasedir(req.info(), &path, real_fh, flags as u32) { 654 | Ok(()) => reply.ok(), 655 | Err(e) => reply.error(e), 656 | } 657 | self.directory_cache.delete(fh); 658 | } 659 | 660 | fn fsyncdir( 661 | &mut self, 662 | req: &fuser::Request<'_>, 663 | ino: u64, 664 | fh: u64, 665 | datasync: bool, 666 | reply: fuser::ReplyEmpty, 667 | ) { 668 | let path = get_path!(self, ino, reply); 669 | debug!("fsyncdir: {:?} (datasync: {:?})", path, datasync); 670 | let real_fh = self.directory_cache.real_fh(fh); 671 | match self.target.fsyncdir(req.info(), &path, real_fh, datasync) { 672 | Ok(()) => reply.ok(), 673 | Err(e) => reply.error(e), 674 | } 675 | } 676 | 677 | fn statfs( 678 | &mut self, 679 | req: &fuser::Request<'_>, 680 | ino: u64, 681 | reply: fuser::ReplyStatfs, 682 | ) { 683 | let path = if ino == 1 { 684 | Arc::new(PathBuf::from("/")) 685 | } else { 686 | get_path!(self, ino, reply) 687 | }; 688 | 689 | debug!("statfs: {:?}", path); 690 | match self.target.statfs(req.info(), &path) { 691 | Ok(statfs) => reply.statfs( 692 | statfs.blocks, 693 | statfs.bfree, 694 | statfs.bavail, 695 | statfs.files, 696 | statfs.ffree, 697 | statfs.bsize, 698 | statfs.namelen, 699 | statfs.frsize), 700 | Err(e) => reply.error(e), 701 | } 702 | } 703 | 704 | fn setxattr( 705 | &mut self, 706 | req: &fuser::Request<'_>, 707 | ino: u64, 708 | name: &OsStr, 709 | value: &[u8], 710 | flags: i32, 711 | position: u32, 712 | reply: fuser::ReplyEmpty, 713 | ) { 714 | let path = get_path!(self, ino, reply); 715 | debug!("setxattr: {:?} {:?} ({} bytes, flags={:#x}, pos={:#x}", 716 | path, name, value.len(), flags, position); 717 | match self.target.setxattr(req.info(), &path, name, value, flags as u32, position) { 718 | Ok(()) => reply.ok(), 719 | Err(e) => reply.error(e), 720 | } 721 | } 722 | 723 | fn getxattr( 724 | &mut self, 725 | req: &fuser::Request<'_>, 726 | ino: u64, 727 | name: &OsStr, 728 | size: u32, 729 | reply: fuser::ReplyXattr, 730 | ) { 731 | let path = get_path!(self, ino, reply); 732 | debug!("getxattr: {:?} {:?}", path, name); 733 | match self.target.getxattr(req.info(), &path, name, size) { 734 | Ok(Xattr::Size(size)) => { 735 | debug!("getxattr: sending size {}", size); 736 | reply.size(size) 737 | }, 738 | Ok(Xattr::Data(vec)) => { 739 | debug!("getxattr: sending {} bytes", vec.len()); 740 | reply.data(&vec) 741 | }, 742 | Err(e) => { 743 | debug!("getxattr: error {}", e); 744 | reply.error(e) 745 | }, 746 | } 747 | } 748 | 749 | fn listxattr( 750 | &mut self, 751 | req: &fuser::Request<'_>, 752 | ino: u64, 753 | size: u32, 754 | reply: fuser::ReplyXattr, 755 | ) { 756 | let path = get_path!(self, ino, reply); 757 | debug!("listxattr: {:?}", path); 758 | match self.target.listxattr(req.info(), &path, size) { 759 | Ok(Xattr::Size(size)) => { 760 | debug!("listxattr: sending size {}", size); 761 | reply.size(size) 762 | }, 763 | Ok(Xattr::Data(vec)) => { 764 | debug!("listxattr: sending {} bytes", vec.len()); 765 | reply.data(&vec) 766 | } 767 | Err(e) => reply.error(e), 768 | } 769 | } 770 | 771 | fn removexattr( 772 | &mut self, 773 | req: &fuser::Request<'_>, 774 | ino: u64, 775 | name: &OsStr, 776 | reply: fuser::ReplyEmpty, 777 | ) { 778 | let path = get_path!(self, ino, reply); 779 | debug!("removexattr: {:?}, {:?}", path, name); 780 | match self.target.removexattr(req.info(), &path, name) { 781 | Ok(()) => reply.ok(), 782 | Err(e) => reply.error(e), 783 | } 784 | } 785 | 786 | fn access( 787 | &mut self, 788 | req: &fuser::Request<'_>, 789 | ino: u64, 790 | mask: i32, 791 | reply: fuser::ReplyEmpty, 792 | ) { 793 | let path = get_path!(self, ino, reply); 794 | debug!("access: {:?}, mask={:#o}", path, mask); 795 | match self.target.access(req.info(), &path, mask as u32) { 796 | Ok(()) => reply.ok(), 797 | Err(e) => reply.error(e), 798 | } 799 | } 800 | 801 | fn create( 802 | &mut self, 803 | req: &fuser::Request<'_>, 804 | parent: u64, 805 | name: &OsStr, 806 | mode: u32, 807 | _umask: u32, // TODO 808 | flags: i32, 809 | reply: fuser::ReplyCreate, 810 | ) { 811 | let parent_path = get_path!(self, parent, reply); 812 | debug!("create: {:?}/{:?} (mode={:#o}, flags={:#x})", parent_path, name, mode, flags); 813 | match self.target.create(req.info(), &parent_path, name, mode, flags as u32) { 814 | Ok(create) => { 815 | let (ino, generation) = self.inodes.add(Arc::new(parent_path.join(name))); 816 | let attr = fuse_fileattr(create.attr, ino); 817 | reply.created(&create.ttl, &attr, generation, create.fh, create.flags); 818 | }, 819 | Err(e) => reply.error(e), 820 | } 821 | } 822 | 823 | // getlk 824 | 825 | // setlk 826 | 827 | // bmap 828 | 829 | #[cfg(target_os = "macos")] 830 | fn setvolname( 831 | &mut self, 832 | req: &fuser::Request<'_>, 833 | name: &OsStr, 834 | reply: fuser::ReplyEmpty, 835 | ) { 836 | debug!("setvolname: {:?}", name); 837 | match self.target.setvolname(req.info(), name) { 838 | Ok(()) => reply.ok(), 839 | Err(e) => reply.error(e), 840 | } 841 | } 842 | 843 | // exchange (macOS only, undocumented) 844 | 845 | #[cfg(target_os = "macos")] 846 | fn getxtimes( 847 | &mut self, 848 | req: &fuser::Request<'_>, 849 | ino: u64, 850 | reply: fuser::ReplyXTimes, 851 | ) { 852 | let path = get_path!(self, ino, reply); 853 | debug!("getxtimes: {:?}", path); 854 | match self.target.getxtimes(req.info(), &path) { 855 | Ok(xtimes) => { 856 | reply.xtimes(xtimes.bkuptime, xtimes.crtime); 857 | } 858 | Err(e) => reply.error(e), 859 | } 860 | } 861 | } 862 | -------------------------------------------------------------------------------- /src/inode_table.rs: -------------------------------------------------------------------------------- 1 | // InodeTable :: a bi-directional map of paths to inodes. 2 | // 3 | // Copyright (c) 2016-2022 by William R. Fraser 4 | // 5 | 6 | use std::borrow::Borrow; 7 | use std::cmp::{Eq, PartialEq}; 8 | use std::collections::{HashMap, VecDeque}; 9 | use std::collections::hash_map::Entry::*; 10 | use std::hash::{Hash, Hasher}; 11 | use std::path::{Path, PathBuf}; 12 | use std::sync::Arc; 13 | 14 | pub type Inode = u64; 15 | pub type Generation = u64; 16 | pub type LookupCount = u64; 17 | 18 | #[derive(Debug)] 19 | struct InodeTableEntry { 20 | path: Option>, 21 | lookups: LookupCount, 22 | generation: Generation, 23 | } 24 | 25 | /// A data structure for mapping paths to inodes and vice versa. 26 | #[derive(Debug)] 27 | pub struct InodeTable { 28 | table: Vec, 29 | free_list: VecDeque, 30 | by_path: HashMap, usize>, 31 | } 32 | 33 | impl InodeTable { 34 | /// Create a new inode table. 35 | /// 36 | /// inode table entries have a limited lifetime, controlled by a 'lookup count', which is 37 | /// manipulated with the `lookup` and `forget` functions. 38 | /// 39 | /// The table initially contains just the root directory ("/"), mapped to inode 1. 40 | /// inode 1 is special: it cannot be forgotten. 41 | pub fn new() -> InodeTable { 42 | let mut inode_table = InodeTable { 43 | table: Vec::new(), 44 | free_list: VecDeque::new(), 45 | by_path: HashMap::new() 46 | }; 47 | let root = Arc::new(PathBuf::from("/")); 48 | inode_table.table.push(InodeTableEntry { 49 | path: Some(root.clone()), 50 | lookups: 0, // not used for this entry; root is always present. 51 | generation: 0, 52 | }); 53 | inode_table.by_path.insert(root, 0); 54 | inode_table 55 | } 56 | 57 | /// Add a path to the inode table. 58 | /// 59 | /// Returns the inode number the path is now mapped to. 60 | /// The returned inode number may be a re-used number formerly assigned to a now-forgotten 61 | /// path. 62 | /// 63 | /// The path is added with an initial lookup count of 1. 64 | /// 65 | /// This operation runs in O(log n) time. 66 | pub fn add(&mut self, path: Arc) -> (Inode, Generation) { 67 | let (inode, generation) = { 68 | let (inode, entry) = Self::get_inode_entry(&mut self.free_list, &mut self.table); 69 | entry.path = Some(path.clone()); 70 | entry.lookups = 1; 71 | (inode, entry.generation) 72 | }; 73 | debug!("explicitly adding {} -> {:?} with 1 lookups", inode, path); 74 | let previous = self.by_path.insert(path, inode as usize - 1); 75 | if let Some(previous) = previous { 76 | error!("inode table buggered: {:?}", self); 77 | panic!("attempted to insert duplicate path into inode table: {:?}", previous); 78 | } 79 | (inode, generation) 80 | } 81 | 82 | /// Add a path to the inode table if it does not yet exist. 83 | /// 84 | /// Returns the inode number the path is now mapped to. 85 | /// 86 | /// If the path was not in the table, it is added with an initial lookup count of 0. 87 | /// 88 | /// This operation runs in O(log n) time. 89 | pub fn add_or_get(&mut self, path: Arc) -> (Inode, Generation) { 90 | match self.by_path.entry(path.clone()) { 91 | Vacant(path_entry) => { 92 | let (inode, entry) = Self::get_inode_entry(&mut self.free_list, &mut self.table); 93 | debug!("adding {} -> {:?} with 0 lookups", inode, path); 94 | entry.path = Some(path); 95 | path_entry.insert(inode as usize - 1); 96 | (inode, entry.generation) 97 | }, 98 | Occupied(path_entry) => { 99 | let idx = path_entry.get(); 100 | ((idx + 1) as Inode, self.table[*idx].generation) 101 | } 102 | } 103 | } 104 | 105 | /// Get the path that corresponds to an inode, if there is one, or None, if it is not in the 106 | /// table. 107 | /// Note that the file could be unlinked but still open, in which case it's not actually 108 | /// reachable from the path returned. 109 | /// 110 | /// This operation runs in O(1) time. 111 | pub fn get_path(&self, inode: Inode) -> Option> { 112 | self.table[inode as usize - 1].path.clone() 113 | } 114 | 115 | /// Get the inode that corresponds to a path, if there is one, or None, if it is not in the 116 | /// table. 117 | /// 118 | /// This operation runs in O(log n) time. 119 | pub fn get_inode(&mut self, path: &Path) -> Option { 120 | self.by_path 121 | .get(Pathish::new(path)) 122 | .map(|idx| (idx + 1) as Inode) 123 | } 124 | 125 | /// Increment the lookup count on a given inode. 126 | /// 127 | /// Calling this on an invalid inode will result in a panic. 128 | /// 129 | /// This operation runs in O(1) time. 130 | pub fn lookup(&mut self, inode: Inode) { 131 | if inode == 1 { 132 | return; 133 | } 134 | 135 | let entry = &mut self.table[inode as usize - 1]; 136 | entry.lookups += 1; 137 | debug!("lookups on {} -> {:?} now {}", inode, entry.path, entry.lookups); 138 | } 139 | 140 | /// Decrement the lookup count on a given inode by the given number. 141 | /// 142 | /// If the lookup count reaches 0, the path is removed from the table, and the inode number 143 | /// is eligible to be re-used. 144 | /// 145 | /// Returns the new lookup count of the inode. (If it returns 0, that means the inode was 146 | /// deleted.) 147 | /// 148 | /// Calling this on an invalid inode will result in a panic. 149 | /// 150 | /// This operation runs in O(1) time normally, or O(log n) time if the inode is deleted. 151 | pub fn forget(&mut self, inode: Inode, n: LookupCount) -> LookupCount { 152 | if inode == 1 { 153 | return 1; 154 | } 155 | 156 | let mut delete = false; 157 | let lookups: LookupCount; 158 | let idx = inode as usize - 1; 159 | 160 | { 161 | let entry = &mut self.table[idx]; 162 | debug!("forget entry {:?}", entry); 163 | assert!(n <= entry.lookups); 164 | entry.lookups -= n; 165 | lookups = entry.lookups; 166 | if lookups == 0 { 167 | delete = true; 168 | self.by_path.remove(entry.path.as_ref().unwrap()); 169 | } 170 | } 171 | 172 | if delete { 173 | self.table[idx].path = None; 174 | self.free_list.push_back(idx); 175 | } 176 | 177 | lookups 178 | } 179 | 180 | /// Change an inode's path to a different one, without changing the inode number. 181 | /// Lookup counts remain unchanged, even if this is replacing another file. 182 | pub fn rename(&mut self, oldpath: &Path, newpath: Arc) { 183 | let idx = self.by_path.remove(Pathish::new(oldpath)).unwrap(); 184 | self.table[idx].path = Some(newpath.clone()); 185 | self.by_path.insert(newpath, idx); // this can replace a path with a new inode 186 | } 187 | 188 | /// Remove the path->inode mapping for a given path, but keep the inode around. 189 | pub fn unlink(&mut self, path: &Path) { 190 | self.by_path.remove(Pathish::new(path)); 191 | // Note that the inode->path mapping remains. 192 | } 193 | 194 | /// Get a free indode table entry and its number, either by allocating a new one, or re-using 195 | /// one that had its lookup count previously go to zero. 196 | /// 197 | /// 1st arg should be `&mut self.free_list`; 2nd arg should be `&mut self.table`. 198 | /// This function's signature is like this instead of taking &mut self so that it can avoid 199 | /// mutably borrowing *all* fields of self when we only need those two. 200 | fn get_inode_entry<'a>(free_list: &mut VecDeque, table: &'a mut Vec) 201 | -> (Inode, &'a mut InodeTableEntry) { 202 | let idx = match free_list.pop_front() { 203 | Some(idx) => { 204 | debug!("re-using inode {}", idx + 1); 205 | table[idx].generation += 1; 206 | idx 207 | }, 208 | None => { 209 | table.push(InodeTableEntry { 210 | path: None, 211 | lookups: 0, 212 | generation: 0, 213 | }); 214 | table.len() - 1 215 | } 216 | }; 217 | ((idx + 1) as Inode, &mut table[idx]) 218 | } 219 | } 220 | 221 | /// Facilitates comparing Rc and &Path 222 | #[derive(Debug)] 223 | struct Pathish { 224 | inner: Path, 225 | } 226 | 227 | impl Pathish { 228 | pub fn new(p: &Path) -> &Pathish { 229 | unsafe { &*(p as *const _ as *const Pathish) } 230 | } 231 | } 232 | 233 | impl Borrow for Arc { 234 | fn borrow(&self) -> &Pathish { 235 | Pathish::new(self.as_path()) 236 | } 237 | } 238 | 239 | impl Hash for Pathish { 240 | fn hash(&self, state: &mut H) { 241 | self.inner.hash(state); 242 | } 243 | } 244 | 245 | impl Eq for Pathish { 246 | } 247 | 248 | impl PartialEq for Pathish { 249 | fn eq(&self, other: &Pathish) -> bool { 250 | self.inner.eq(&other.inner) 251 | } 252 | } 253 | 254 | #[test] 255 | fn test_inode_reuse() { 256 | let mut table = InodeTable::new(); 257 | let path1 = Arc::new(PathBuf::from("/foo/a")); 258 | let path2 = Arc::new(PathBuf::from("/foo/b")); 259 | 260 | // Add a path. 261 | let inode1 = table.add(path1.clone()).0; 262 | assert!(inode1 != 1); 263 | assert_eq!(*path1, *table.get_path(inode1).unwrap()); 264 | 265 | // Add a second path; verify that the inode number is different. 266 | let inode2 = table.add(path2.clone()).0; 267 | assert!(inode2 != inode1); 268 | assert!(inode2 != 1); 269 | assert_eq!(*path2, *table.get_path(inode2).unwrap()); 270 | 271 | // Forget the first inode; verify that lookups on it fail. 272 | assert_eq!(0, table.forget(inode1, 1)); 273 | assert!(table.get_path(inode1).is_none()); 274 | 275 | // Add a third path; verify that the inode is reused. 276 | let (inode3, generation3) = table.add(Arc::new(PathBuf::from("/foo/c"))); 277 | assert_eq!(inode1, inode3); 278 | assert_eq!(1, generation3); 279 | 280 | // Check that lookups on the third path succeed. 281 | assert_eq!(Path::new("/foo/c"), *table.get_path(inode3).unwrap()); 282 | } 283 | 284 | #[test] 285 | fn test_add_or_get() { 286 | let mut table = InodeTable::new(); 287 | let path1 = Arc::new(PathBuf::from("/foo/a")); 288 | let path2 = Arc::new(PathBuf::from("/foo/b")); 289 | 290 | // add_or_get() a path and verify that get by inode works before lookup() is done. 291 | let inode1 = table.add_or_get(path1.clone()).0; 292 | assert_eq!(*path1, *table.get_path(inode1).unwrap()); 293 | table.lookup(inode1); 294 | 295 | // add() a second path and verify that get by path and inode work. 296 | let inode2 = table.add(path2.clone()).0; 297 | assert_eq!(*path2, *table.get_path(inode2).unwrap()); 298 | assert_eq!(inode2, table.add_or_get(path2).0); 299 | table.lookup(inode2); 300 | 301 | // Check the ref counts by doing a single forget. 302 | assert_eq!(0, table.forget(inode1, 1)); 303 | assert_eq!(1, table.forget(inode2, 1)); 304 | } 305 | 306 | #[test] 307 | fn test_inode_rename() { 308 | let mut table = InodeTable::new(); 309 | let path1 = Arc::new(PathBuf::from("/foo/a")); 310 | let path2 = Arc::new(PathBuf::from("/foo/b")); 311 | 312 | // Add a path; verify that get by path and inode work. 313 | let inode = table.add(path1.clone()).0; 314 | assert_eq!(*path1, *table.get_path(inode).unwrap()); 315 | assert_eq!(inode, table.get_inode(&path1).unwrap()); 316 | 317 | // Rename the inode; verify that get by the new path works and old path doesn't, and get by 318 | // inode still works. 319 | table.rename(&path1, path2.clone()); 320 | assert!(table.get_inode(&path1).is_none()); 321 | assert_eq!(inode, table.get_inode(&path2).unwrap()); 322 | assert_eq!(*path2, *table.get_path(inode).unwrap()); 323 | } 324 | 325 | #[test] 326 | fn test_unlink() { 327 | let mut table = InodeTable::new(); 328 | let path = Arc::new(PathBuf::from("/foo/bar")); 329 | 330 | // Add a path. 331 | let inode = table.add(path.clone()).0; 332 | 333 | // Unlink it and verify that get by path fails. 334 | table.unlink(&path); 335 | assert!(table.get_inode(&path).is_none()); 336 | 337 | // Getting the path for the inode should still return the path. 338 | assert_eq!(*path, *table.get_path(inode).unwrap()); 339 | 340 | // Verify that forgetting it once drops the refcount to zero and then lookups by inode fail. 341 | assert_eq!(0, table.forget(inode, 1)); 342 | assert!(table.get_path(inode).is_none()); 343 | } 344 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! FuseMT -- A higher-level FUSE (Filesystem in Userspace) interface and wrapper around the 2 | //! low-level `fuser` library that makes implementing a filesystem a bit easier. 3 | //! 4 | //! FuseMT translates inodes to paths and dispatches I/O operations to multiple threads, and 5 | //! simplifies some details of filesystem implementation, for example: splitting the `setattr` call 6 | //! into multiple separate operations, and simplifying the `readdir` call so that filesystems don't 7 | //! need to deal with pagination. 8 | //! 9 | //! To implement a filesystem, implement the `FilesystemMT` trait. Not all functions in it need to 10 | //! be implemented -- the default behavior is to return `ENOSYS` ("Function not implemented"). For 11 | //! example, a read-only filesystem can skip implementing the `write` call and many others. 12 | 13 | // 14 | // Copyright (c) 2016-2022 by William R. Fraser 15 | // 16 | 17 | #![deny(rust_2018_idioms)] 18 | 19 | #[macro_use] 20 | extern crate log; 21 | 22 | mod directory_cache; 23 | mod fusemt; 24 | mod inode_table; 25 | mod types; 26 | 27 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 28 | 29 | pub use fuser::FileType; 30 | pub use crate::fusemt::*; 31 | pub use crate::types::*; 32 | 33 | // Forward to similarly-named fuser functions to work around deprecation for now. 34 | // When these are removed, we'll have to either reimplement or break reverse compat. 35 | // Keep the doc comments in sync with those in fuser. 36 | 37 | use std::ffi::OsStr; 38 | use std::io; 39 | use std::path::Path; 40 | 41 | /// Mount the given filesystem to the given mountpoint. This function will not return until the 42 | /// filesystem is unmounted. 43 | #[inline(always)] 44 | pub fn mount>( 45 | fs: FS, 46 | mountpoint: P, 47 | options: &[&OsStr], 48 | ) -> io::Result<()> { 49 | #[allow(deprecated)] 50 | fuser::mount(fs, mountpoint, options) 51 | } 52 | 53 | /// Mount the given filesystem to the given mountpoint. This function spawns a background thread to 54 | /// handle filesystem operations while being mounted and therefore returns immediately. The 55 | /// returned handle should be stored to reference the mounted filesystem. If it's dropped, the 56 | /// filesystem will be unmounted. 57 | #[inline(always)] 58 | pub fn spawn_mount>( 59 | fs: FS, 60 | mountpoint: P, 61 | options: &[&OsStr], 62 | ) -> io::Result { 63 | #[allow(deprecated)] 64 | fuser::spawn_mount(fs, mountpoint, options) 65 | } 66 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | // Public types exported by FuseMT. 2 | // 3 | // Copyright (c) 2016-2022 by William R. Fraser 4 | // 5 | 6 | use std::ffi::{OsStr, OsString}; 7 | use std::path::Path; 8 | use std::time::{Duration, SystemTime}; 9 | 10 | /// Info about a request. 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct RequestInfo { 13 | /// The unique ID assigned to this request by FUSE. 14 | pub unique: u64, 15 | /// The user ID of the process making the request. 16 | pub uid: u32, 17 | /// The group ID of the process making the request. 18 | pub gid: u32, 19 | /// The process ID of the process making the request. 20 | pub pid: u32, 21 | } 22 | 23 | /// A directory entry. 24 | #[derive(Clone, Debug)] 25 | pub struct DirectoryEntry { 26 | /// Name of the entry 27 | pub name: OsString, 28 | /// Kind of file (directory, file, pipe, etc.) 29 | pub kind: crate::FileType, 30 | } 31 | 32 | /// Filesystem statistics. 33 | #[derive(Clone, Copy, Debug)] 34 | pub struct Statfs { 35 | /// Total data blocks in the filesystem 36 | pub blocks: u64, 37 | /// Free blocks in filesystem 38 | pub bfree: u64, 39 | /// Free blocks available to unprivileged user 40 | pub bavail: u64, 41 | /// Total file nodes in filesystem 42 | pub files: u64, 43 | /// Free file nodes in filesystem 44 | pub ffree: u64, 45 | /// Optimal transfer block size 46 | pub bsize: u32, 47 | /// Maximum length of filenames 48 | pub namelen: u32, 49 | /// Fragment size 50 | pub frsize: u32, 51 | } 52 | 53 | /// File attributes. 54 | #[derive(Clone, Copy, Debug)] 55 | pub struct FileAttr { 56 | /// Size in bytes 57 | pub size: u64, 58 | /// Size in blocks 59 | pub blocks: u64, 60 | /// Time of last access 61 | pub atime: SystemTime, 62 | /// Time of last modification 63 | pub mtime: SystemTime, 64 | /// Time of last metadata change 65 | pub ctime: SystemTime, 66 | /// Time of creation (macOS only) 67 | pub crtime: SystemTime, 68 | /// Kind of file (directory, file, pipe, etc.) 69 | pub kind: crate::FileType, 70 | /// Permissions 71 | pub perm: u16, 72 | /// Number of hard links 73 | pub nlink: u32, 74 | /// User ID 75 | pub uid: u32, 76 | /// Group ID 77 | pub gid: u32, 78 | /// Device ID (if special file) 79 | pub rdev: u32, 80 | /// Flags (macOS only; see chflags(2)) 81 | pub flags: u32, 82 | } 83 | 84 | /// The return value for `create`: contains info on the newly-created file, as well as a handle to 85 | /// the opened file. 86 | #[derive(Clone, Debug)] 87 | pub struct CreatedEntry { 88 | pub ttl: Duration, 89 | pub attr: FileAttr, 90 | pub fh: u64, 91 | pub flags: u32, 92 | } 93 | 94 | /// Represents the return value from the `listxattr` and `getxattr` calls, which can be either a 95 | /// size or contain data, depending on how they are called. 96 | #[derive(Clone, Debug)] 97 | pub enum Xattr { 98 | Size(u32), 99 | Data(Vec), 100 | } 101 | 102 | #[cfg(target_os = "macos")] 103 | #[derive(Clone, Debug)] 104 | pub struct XTimes { 105 | pub bkuptime: SystemTime, 106 | pub crtime: SystemTime, 107 | } 108 | 109 | pub type ResultEmpty = Result<(), libc::c_int>; 110 | pub type ResultEntry = Result<(Duration, FileAttr), libc::c_int>; 111 | pub type ResultOpen = Result<(u64, u32), libc::c_int>; 112 | pub type ResultReaddir = Result, libc::c_int>; 113 | pub type ResultData = Result, libc::c_int>; 114 | pub type ResultSlice<'a> = Result<&'a [u8], libc::c_int>; 115 | pub type ResultWrite = Result; 116 | pub type ResultStatfs = Result; 117 | pub type ResultCreate = Result; 118 | pub type ResultXattr = Result; 119 | 120 | #[cfg(target_os = "macos")] 121 | pub type ResultXTimes = Result; 122 | 123 | #[deprecated(since = "0.3.0", note = "use ResultEntry instead")] 124 | pub type ResultGetattr = ResultEntry; 125 | 126 | /// Dummy struct returned by the callback in the `read()` method. Cannot be constructed outside 127 | /// this crate, `read()` requires you to return it, thus ensuring that you don't forget to call the 128 | /// callback. 129 | pub struct CallbackResult { 130 | pub(crate) _private: std::marker::PhantomData<()>, 131 | } 132 | 133 | /// This trait must be implemented to implement a filesystem with FuseMT. 134 | pub trait FilesystemMT { 135 | /// Called on mount, before any other function. 136 | fn init(&self, _req: RequestInfo) -> ResultEmpty { 137 | Ok(()) 138 | } 139 | 140 | /// Called on filesystem unmount. 141 | fn destroy(&self) { 142 | // Nothing. 143 | } 144 | 145 | /// Get the attributes of a filesystem entry. 146 | /// 147 | /// * `fh`: a file handle if this is called on an open file. 148 | fn getattr(&self, _req: RequestInfo, _path: &Path, _fh: Option) -> ResultEntry { 149 | Err(libc::ENOSYS) 150 | } 151 | 152 | // The following operations in the FUSE C API are all one kernel call: setattr 153 | // We split them out to match the C API's behavior. 154 | 155 | /// Change the mode of a filesystem entry. 156 | /// 157 | /// * `fh`: a file handle if this is called on an open file. 158 | /// * `mode`: the mode to change the file to. 159 | fn chmod(&self, _req: RequestInfo, _path: &Path, _fh: Option, _mode: u32) -> ResultEmpty { 160 | Err(libc::ENOSYS) 161 | } 162 | 163 | /// Change the owner UID and/or group GID of a filesystem entry. 164 | /// 165 | /// * `fh`: a file handle if this is called on an open file. 166 | /// * `uid`: user ID to change the file's owner to. If `None`, leave the UID unchanged. 167 | /// * `gid`: group ID to change the file's group to. If `None`, leave the GID unchanged. 168 | fn chown(&self, _req: RequestInfo, _path: &Path, _fh: Option, _uid: Option, _gid: Option) -> ResultEmpty { 169 | Err(libc::ENOSYS) 170 | } 171 | 172 | /// Set the length of a file. 173 | /// 174 | /// * `fh`: a file handle if this is called on an open file. 175 | /// * `size`: size in bytes to set as the file's length. 176 | fn truncate(&self, _req: RequestInfo, _path: &Path, _fh: Option, _size: u64) -> ResultEmpty { 177 | Err(libc::ENOSYS) 178 | } 179 | 180 | /// Set timestamps of a filesystem entry. 181 | /// 182 | /// * `fh`: a file handle if this is called on an open file. 183 | /// * `atime`: the time of last access. 184 | /// * `mtime`: the time of last modification. 185 | fn utimens(&self, _req: RequestInfo, _path: &Path, _fh: Option, _atime: Option, _mtime: Option) -> ResultEmpty { 186 | Err(libc::ENOSYS) 187 | } 188 | 189 | /// Set timestamps of a filesystem entry (with extra options only used on MacOS). 190 | #[allow(clippy::too_many_arguments)] 191 | fn utimens_macos(&self, _req: RequestInfo, _path: &Path, _fh: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option) -> ResultEmpty { 192 | Err(libc::ENOSYS) 193 | } 194 | 195 | // END OF SETATTR FUNCTIONS 196 | 197 | /// Read a symbolic link. 198 | fn readlink(&self, _req: RequestInfo, _path: &Path) -> ResultData { 199 | Err(libc::ENOSYS) 200 | } 201 | 202 | /// Create a special file. 203 | /// 204 | /// * `parent`: path to the directory to make the entry under. 205 | /// * `name`: name of the entry. 206 | /// * `mode`: mode for the new entry. 207 | /// * `rdev`: if mode has the bits `S_IFCHR` or `S_IFBLK` set, this is the major and minor numbers for the device file. Otherwise it should be ignored. 208 | fn mknod(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _mode: u32, _rdev: u32) -> ResultEntry { 209 | Err(libc::ENOSYS) 210 | } 211 | 212 | /// Create a directory. 213 | /// 214 | /// * `parent`: path to the directory to make the directory under. 215 | /// * `name`: name of the directory. 216 | /// * `mode`: permissions for the new directory. 217 | fn mkdir(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _mode: u32) -> ResultEntry { 218 | Err(libc::ENOSYS) 219 | } 220 | 221 | /// Remove a file. 222 | /// 223 | /// * `parent`: path to the directory containing the file to delete. 224 | /// * `name`: name of the file to delete. 225 | fn unlink(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr) -> ResultEmpty { 226 | Err(libc::ENOSYS) 227 | } 228 | 229 | /// Remove a directory. 230 | /// 231 | /// * `parent`: path to the directory containing the directory to delete. 232 | /// * `name`: name of the directory to delete. 233 | fn rmdir(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr) -> ResultEmpty { 234 | Err(libc::ENOSYS) 235 | } 236 | 237 | /// Create a symbolic link. 238 | /// 239 | /// * `parent`: path to the directory to make the link in. 240 | /// * `name`: name of the symbolic link. 241 | /// * `target`: path (may be relative or absolute) to the target of the link. 242 | fn symlink(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _target: &Path) -> ResultEntry { 243 | Err(libc::ENOSYS) 244 | } 245 | 246 | /// Rename a filesystem entry. 247 | /// 248 | /// * `parent`: path to the directory containing the existing entry. 249 | /// * `name`: name of the existing entry. 250 | /// * `newparent`: path to the directory it should be renamed into (may be the same as `parent`). 251 | /// * `newname`: name of the new entry. 252 | fn rename(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _newparent: &Path, _newname: &OsStr) -> ResultEmpty { 253 | Err(libc::ENOSYS) 254 | } 255 | 256 | /// Create a hard link. 257 | /// 258 | /// * `path`: path to an existing file. 259 | /// * `newparent`: path to the directory for the new link. 260 | /// * `newname`: name for the new link. 261 | fn link(&self, _req: RequestInfo, _path: &Path, _newparent: &Path, _newname: &OsStr) -> ResultEntry { 262 | Err(libc::ENOSYS) 263 | } 264 | 265 | /// Open a file. 266 | /// 267 | /// * `path`: path to the file. 268 | /// * `flags`: one of `O_RDONLY`, `O_WRONLY`, or `O_RDWR`, plus maybe additional flags. 269 | /// 270 | /// Return a tuple of (file handle, flags). The file handle will be passed to any subsequent 271 | /// calls that operate on the file, and can be any value you choose, though it should allow 272 | /// your filesystem to identify the file opened even without any path info. 273 | fn open(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen { 274 | Err(libc::ENOSYS) 275 | } 276 | 277 | /// Read from a file. 278 | /// 279 | /// Note that it is not an error for this call to request to read past the end of the file, and 280 | /// you should only return data up to the end of the file (i.e. the number of bytes returned 281 | /// will be fewer than requested; possibly even zero). Do not extend the file in this case. 282 | /// 283 | /// * `path`: path to the file. 284 | /// * `fh`: file handle returned from the `open` call. 285 | /// * `offset`: offset into the file to start reading. 286 | /// * `size`: number of bytes to read. 287 | /// * `callback`: a callback that must be invoked to return the result of the operation: either 288 | /// the result data as a slice, or an error code. 289 | /// 290 | /// Return the return value from the `callback` function. 291 | fn read(&self, _req: RequestInfo, _path: &Path, _fh: u64, _offset: u64, _size: u32, callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult) -> CallbackResult { 292 | callback(Err(libc::ENOSYS)) 293 | } 294 | 295 | /// Write to a file. 296 | /// 297 | /// * `path`: path to the file. 298 | /// * `fh`: file handle returned from the `open` call. 299 | /// * `offset`: offset into the file to start writing. 300 | /// * `data`: the data to write 301 | /// * `flags`: 302 | /// 303 | /// Return the number of bytes written. 304 | fn write(&self, _req: RequestInfo, _path: &Path, _fh: u64, _offset: u64, _data: Vec, _flags: u32) -> ResultWrite { 305 | Err(libc::ENOSYS) 306 | } 307 | 308 | /// Called each time a program calls `close` on an open file. 309 | /// 310 | /// Note that because file descriptors can be duplicated (by `dup`, `dup2`, `fork`) this may be 311 | /// called multiple times for a given file handle. The main use of this function is if the 312 | /// filesystem would like to return an error to the `close` call. Note that most programs 313 | /// ignore the return value of `close`, though. 314 | /// 315 | /// * `path`: path to the file. 316 | /// * `fh`: file handle returned from the `open` call. 317 | /// * `lock_owner`: if the filesystem supports locking (`setlk`, `getlk`), remove all locks 318 | /// belonging to this lock owner. 319 | fn flush(&self, _req: RequestInfo, _path: &Path, _fh: u64, _lock_owner: u64) -> ResultEmpty { 320 | Err(libc::ENOSYS) 321 | } 322 | 323 | /// Called when an open file is closed. 324 | /// 325 | /// There will be one of these for each `open` call. After `release`, no more calls will be 326 | /// made with the given file handle. 327 | /// 328 | /// * `path`: path to the file. 329 | /// * `fh`: file handle returned from the `open` call. 330 | /// * `flags`: the flags passed when the file was opened. 331 | /// * `lock_owner`: if the filesystem supports locking (`setlk`, `getlk`), remove all locks 332 | /// belonging to this lock owner. 333 | /// * `flush`: whether pending data must be flushed or not. 334 | fn release(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32, _lock_owner: u64, _flush: bool) -> ResultEmpty { 335 | Err(libc::ENOSYS) 336 | } 337 | 338 | /// Write out any pending changes of a file. 339 | /// 340 | /// When this returns, data should be written to persistent storage. 341 | /// 342 | /// * `path`: path to the file. 343 | /// * `fh`: file handle returned from the `open` call. 344 | /// * `datasync`: if `false`, also write metadata, otherwise just write file data. 345 | fn fsync(&self, _req: RequestInfo, _path: &Path, _fh: u64, _datasync: bool) -> ResultEmpty { 346 | Err(libc::ENOSYS) 347 | } 348 | 349 | /// Open a directory. 350 | /// 351 | /// Analogous to the `opend` call. 352 | /// 353 | /// * `path`: path to the directory. 354 | /// * `flags`: file access flags. Will contain `O_DIRECTORY` at least. 355 | /// 356 | /// Return a tuple of (file handle, flags). The file handle will be passed to any subsequent 357 | /// calls that operate on the directory, and can be any value you choose, though it should 358 | /// allow your filesystem to identify the directory opened even without any path info. 359 | fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen { 360 | Err(libc::ENOSYS) 361 | } 362 | 363 | /// Get the entries of a directory. 364 | /// 365 | /// * `path`: path to the directory. 366 | /// * `fh`: file handle returned from the `opendir` call. 367 | /// 368 | /// Return all the entries of the directory. 369 | fn readdir(&self, _req: RequestInfo, _path: &Path, _fh: u64) -> ResultReaddir { 370 | Err(libc::ENOSYS) 371 | } 372 | 373 | /// Close an open directory. 374 | /// 375 | /// This will be called exactly once for each `opendir` call. 376 | /// 377 | /// * `path`: path to the directory. 378 | /// * `fh`: file handle returned from the `opendir` call. 379 | /// * `flags`: the file access flags passed to the `opendir` call. 380 | fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty { 381 | Err(libc::ENOSYS) 382 | } 383 | 384 | /// Write out any pending changes to a directory. 385 | /// 386 | /// Analogous to the `fsync` call. 387 | fn fsyncdir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _datasync: bool) -> ResultEmpty { 388 | Err(libc::ENOSYS) 389 | } 390 | 391 | /// Get filesystem statistics. 392 | /// 393 | /// * `path`: path to some folder in the filesystem. 394 | /// 395 | /// See the `Statfs` struct for more details. 396 | fn statfs(&self, _req: RequestInfo, _path: &Path) -> ResultStatfs { 397 | Err(libc::ENOSYS) 398 | } 399 | 400 | /// Set a file extended attribute. 401 | /// 402 | /// * `path`: path to the file. 403 | /// * `name`: attribute name. 404 | /// * `value`: the data to set the value to. 405 | /// * `flags`: can be either `XATTR_CREATE` or `XATTR_REPLACE`. 406 | /// * `position`: offset into the attribute value to write data. 407 | fn setxattr(&self, _req: RequestInfo, _path: &Path, _name: &OsStr, _value: &[u8], _flags: u32, _position: u32) -> ResultEmpty { 408 | Err(libc::ENOSYS) 409 | } 410 | 411 | /// Get a file extended attribute. 412 | /// 413 | /// * `path`: path to the file 414 | /// * `name`: attribute name. 415 | /// * `size`: the maximum number of bytes to read. 416 | /// 417 | /// If `size` is 0, return `Xattr::Size(n)` where `n` is the size of the attribute data. 418 | /// Otherwise, return `Xattr::Data(data)` with the requested data. 419 | fn getxattr(&self, _req: RequestInfo, _path: &Path, _name: &OsStr, _size: u32) -> ResultXattr { 420 | Err(libc::ENOSYS) 421 | } 422 | 423 | /// List extended attributes for a file. 424 | /// 425 | /// * `path`: path to the file. 426 | /// * `size`: maximum number of bytes to return. 427 | /// 428 | /// If `size` is 0, return `Xattr::Size(n)` where `n` is the size required for the list of 429 | /// attribute names. 430 | /// Otherwise, return `Xattr::Data(data)` where `data` is all the null-terminated attribute 431 | /// names. 432 | fn listxattr(&self, _req: RequestInfo, _path: &Path, _size: u32) -> ResultXattr { 433 | Err(libc::ENOSYS) 434 | } 435 | 436 | /// Remove an extended attribute for a file. 437 | /// 438 | /// * `path`: path to the file. 439 | /// * `name`: name of the attribute to remove. 440 | fn removexattr(&self, _req: RequestInfo, _path: &Path, _name: &OsStr) -> ResultEmpty { 441 | Err(libc::ENOSYS) 442 | } 443 | 444 | /// Check for access to a file. 445 | /// 446 | /// * `path`: path to the file. 447 | /// * `mask`: mode bits to check for access to. 448 | /// 449 | /// Return `Ok(())` if all requested permissions are allowed, otherwise return `Err(EACCES)` 450 | /// or other error code as appropriate (e.g. `ENOENT` if the file doesn't exist). 451 | fn access(&self, _req: RequestInfo, _path: &Path, _mask: u32) -> ResultEmpty { 452 | Err(libc::ENOSYS) 453 | } 454 | 455 | /// Create and open a new file. 456 | /// 457 | /// * `parent`: path to the directory to create the file in. 458 | /// * `name`: name of the file to be created. 459 | /// * `mode`: the mode to set on the new file. 460 | /// * `flags`: flags like would be passed to `open`. 461 | /// 462 | /// Return a `CreatedEntry` (which contains the new file's attributes as well as a file handle 463 | /// -- see documentation on `open` for more info on that). 464 | fn create(&self, _req: RequestInfo, _parent: &Path, _name: &OsStr, _mode: u32, _flags: u32) -> ResultCreate { 465 | Err(libc::ENOSYS) 466 | } 467 | 468 | // getlk 469 | 470 | // setlk 471 | 472 | // bmap 473 | 474 | /// macOS only: Rename the volume. 475 | /// 476 | /// * `name`: new name for the volume 477 | #[cfg(target_os = "macos")] 478 | fn setvolname(&self, _req: RequestInfo, _name: &OsStr) -> ResultEmpty { 479 | Err(libc::ENOSYS) 480 | } 481 | 482 | // exchange (macOS only, undocumented) 483 | 484 | /// macOS only: Query extended times (bkuptime and crtime). 485 | /// 486 | /// * `path`: path to the file to get the times for. 487 | /// 488 | /// Return an `XTimes` struct with the times, or other error code as appropriate. 489 | #[cfg(target_os = "macos")] 490 | fn getxtimes(&self, _req: RequestInfo, _path: &Path) -> ResultXTimes { 491 | Err(libc::ENOSYS) 492 | } 493 | } 494 | --------------------------------------------------------------------------------