Skip to main content

mountpoint_s3_fs/
fuse.rs

1//! Links _fuser_ method calls into Mountpoint's filesystem code in [crate::fs].
2
3use futures::executor::block_on;
4use mountpoint_s3_client::ObjectClient;
5use std::ffi::OsStr;
6use std::path::Path;
7use std::time::SystemTime;
8use time::OffsetDateTime;
9use tracing::{Instrument, field, instrument};
10
11use crate::fs::{
12    DirectoryEntry, DirectoryReplier, InodeNo, S3Filesystem, ToErrno, error_metadata::MOUNTPOINT_EVENT_READY,
13};
14use crate::metrics::defs::{ATTR_FUSE_REQUEST, FUSE_IO_SIZE, FUSE_REQUEST_ERRORS};
15#[cfg(target_os = "macos")]
16use fuser::ReplyXTimes;
17use fuser::{
18    Filesystem, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData, ReplyEmpty, ReplyEntry, ReplyIoctl,
19    ReplyLock, ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow,
20};
21
22pub mod config;
23pub mod session;
24
25/// A trait that can be implemented to log errors returned by fuse operations.
26pub trait ErrorLogger: std::fmt::Debug {
27    /// Log an error returned by a fuse operation.
28    ///
29    /// This method will be invoked immediately before a fuse operation returns an error response, with the exception of those
30    /// operations which are not implemented.
31    fn error(&self, err: &crate::fs::Error, fuse_operation: &str, fuse_request_id: u64);
32
33    /// Log an event with the given operation and code
34    fn event(&self, operation: &str, event_code: &str);
35}
36
37/// `tracing` doesn't allow dynamic levels but we want to dynamically choose the log level for
38/// requests based on their response status. https://github.com/tokio-rs/tracing/issues/372
39macro_rules! event {
40    ($level:expr, $($args:tt)*) => {
41        match $level {
42            ::tracing::Level::ERROR => ::tracing::event!(::tracing::Level::ERROR, $($args)*),
43            ::tracing::Level::WARN => ::tracing::event!(::tracing::Level::WARN, $($args)*),
44            ::tracing::Level::INFO => ::tracing::event!(::tracing::Level::INFO, $($args)*),
45            ::tracing::Level::DEBUG => ::tracing::event!(::tracing::Level::DEBUG, $($args)*),
46            ::tracing::Level::TRACE => ::tracing::event!(::tracing::Level::TRACE, $($args)*),
47        }
48    };
49}
50
51/// Handle an error in a FUSE handler. This logs the appropriate error message and then calls
52/// `reply` on the given replier with the error's corresponding errno.
53macro_rules! fuse_error {
54    ($name:literal, $reply:expr, $err:expr, $fs:expr, $request:expr) => {{
55        let err = $err;
56        event!(err.level, "{} failed with errno {}: {:#}", $name, err.to_errno(), err);
57        ::metrics::counter!(FUSE_REQUEST_ERRORS, ATTR_FUSE_REQUEST => $name).increment(1);
58        if let Some(error_logger) = $fs.error_logger.as_ref() {
59            error_logger.error(&err, $name, $request.unique());
60        }
61        $reply.error(err.to_errno());
62    }};
63}
64
65/// Generic handler for unimplemented FUSE operations
66macro_rules! fuse_unsupported {
67    ($name:literal, $reply:expr, $err:expr, $level:expr) => {{
68        event!($level, "{} failed: operation not supported by Mountpoint", $name);
69        ::metrics::counter!(FUSE_REQUEST_ERRORS, ATTR_FUSE_REQUEST => $name).increment(1);
70        ::metrics::counter!("fuse.op_unimplemented","op" => $name).increment(1);
71        $reply.error($err);
72    }};
73    ($name:literal, $reply:expr) => {
74        fuse_unsupported!($name, $reply, libc::ENOSYS, tracing::Level::WARN)
75    };
76    ($name:literal, $reply:expr, $err:expr) => {
77        fuse_unsupported!($name, $reply, $err, tracing::Level::WARN)
78    };
79}
80
81/// This is just a thin wrapper around [S3Filesystem] that implements the actual `fuser` protocol,
82/// so that we can test our actual filesystem implementation without having actual FUSE in the loop.
83pub struct S3FuseFilesystem<Client>
84where
85    Client: ObjectClient + Clone + Send + Sync + 'static,
86{
87    fs: S3Filesystem<Client>,
88    error_logger: Option<Box<dyn ErrorLogger + Send + Sync>>,
89}
90
91impl<Client> S3FuseFilesystem<Client>
92where
93    Client: ObjectClient + Clone + Send + Sync + 'static,
94{
95    pub fn new(fs: S3Filesystem<Client>, error_logger: Option<Box<dyn ErrorLogger + Send + Sync>>) -> Self {
96        Self { fs, error_logger }
97    }
98}
99
100impl<Client> Filesystem for S3FuseFilesystem<Client>
101where
102    Client: ObjectClient + Clone + Send + Sync + 'static,
103{
104    #[instrument(level="warn", skip_all, fields(req=req.unique()))]
105    fn init(&self, req: &Request<'_>, config: &mut KernelConfig) -> Result<(), libc::c_int> {
106        if let Some(error_logger) = self.error_logger.as_ref() {
107            error_logger.event("mount", MOUNTPOINT_EVENT_READY);
108        }
109        block_on(self.fs.init(config).in_current_span())
110    }
111
112    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=parent, name=?name))]
113    fn lookup(&self, req: &Request<'_>, parent: InodeNo, name: &OsStr, reply: ReplyEntry) {
114        match block_on(self.fs.lookup(parent, name).in_current_span()) {
115            Ok(entry) => reply.entry(&entry.ttl, &entry.attr, entry.generation),
116            Err(e) => fuse_error!("lookup", reply, e, self, req),
117        }
118    }
119
120    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, name=field::Empty))]
121    fn getattr(&self, req: &Request<'_>, ino: InodeNo, _fh: Option<u64>, reply: ReplyAttr) {
122        match block_on(self.fs.getattr(ino).in_current_span()) {
123            Ok(attr) => reply.attr(&attr.ttl, &attr.attr),
124            Err(e) => fuse_error!("getattr", reply, e, self, req),
125        }
126    }
127
128    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino, nlookup, name=field::Empty))]
129    fn forget(&self, req: &Request<'_>, ino: u64, nlookup: u64) {
130        block_on(self.fs.forget(ino, nlookup));
131    }
132
133    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, pid=req.pid(), name=field::Empty))]
134    fn open(&self, req: &Request<'_>, ino: InodeNo, flags: i32, reply: ReplyOpen) {
135        match block_on(self.fs.open(ino, flags.into(), req.pid()).in_current_span()) {
136            Ok(opened) => reply.opened(opened.fh, opened.flags),
137            Err(e) => fuse_error!("open", reply, e, self, req),
138        }
139    }
140
141    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, fh=fh, offset=offset, size=size, name=field::Empty))]
142    fn read(
143        &self,
144        req: &Request<'_>,
145        ino: InodeNo,
146        fh: u64,
147        offset: i64,
148        size: u32,
149        flags: i32,
150        lock: Option<u64>,
151        reply: ReplyData,
152    ) {
153        let mut bytes_sent = 0;
154
155        match block_on(self.fs.read(ino, fh, offset, size, flags, lock).in_current_span()) {
156            Ok(data) => {
157                bytes_sent = data.len();
158                reply.data(&data);
159            }
160            Err(err) => fuse_error!("read", reply, err, self, req),
161        }
162
163        metrics::counter!("fuse.total_bytes", "type" => "read").increment(bytes_sent as u64);
164        metrics::histogram!(FUSE_IO_SIZE, ATTR_FUSE_REQUEST => "read").record(bytes_sent as f64);
165    }
166
167    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=parent, name=field::Empty))]
168    fn opendir(&self, req: &Request<'_>, parent: InodeNo, flags: i32, reply: ReplyOpen) {
169        match block_on(self.fs.opendir(parent, flags).in_current_span()) {
170            Ok(opened) => reply.opened(opened.fh, opened.flags),
171            Err(e) => fuse_error!("opendir", reply, e, self, req),
172        }
173    }
174
175    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=parent, fh=fh, offset=offset))]
176    fn readdir(&self, req: &Request<'_>, parent: InodeNo, fh: u64, offset: i64, mut reply: fuser::ReplyDirectory) {
177        struct ReplyDirectory<'a> {
178            inner: &'a mut fuser::ReplyDirectory,
179            count: &'a mut usize,
180        }
181
182        impl DirectoryReplier for ReplyDirectory<'_> {
183            fn add(&mut self, entry: DirectoryEntry) -> bool {
184                let result = self.inner.add(entry.ino, entry.offset, entry.attr.kind, entry.name);
185                if !result {
186                    *self.count += 1;
187                }
188                result
189            }
190        }
191
192        let mut count = 0;
193        let replier = ReplyDirectory {
194            inner: &mut reply,
195            count: &mut count,
196        };
197
198        match block_on(self.fs.readdir(parent, fh, offset, replier).in_current_span()) {
199            Ok(_) => {
200                reply.ok();
201                metrics::histogram!("fuse.readdir.entries").record(count as f64);
202            }
203            Err(e) => fuse_error!("readdir", reply, e, self, req),
204        }
205    }
206
207    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=parent, fh=fh, offset=offset))]
208    fn readdirplus(
209        &self,
210        req: &Request<'_>,
211        parent: InodeNo,
212        fh: u64,
213        offset: i64,
214        mut reply: fuser::ReplyDirectoryPlus,
215    ) {
216        struct ReplyDirectoryPlus<'a> {
217            inner: &'a mut fuser::ReplyDirectoryPlus,
218            count: &'a mut usize,
219        }
220
221        impl DirectoryReplier for ReplyDirectoryPlus<'_> {
222            fn add(&mut self, entry: DirectoryEntry) -> bool {
223                let result = self.inner.add(
224                    entry.ino,
225                    entry.offset,
226                    entry.name,
227                    &entry.ttl,
228                    &entry.attr,
229                    entry.generation,
230                );
231                if !result {
232                    *self.count += 1;
233                }
234                result
235            }
236        }
237
238        let mut count = 0;
239        let replier = ReplyDirectoryPlus {
240            inner: &mut reply,
241            count: &mut count,
242        };
243
244        match block_on(self.fs.readdirplus(parent, fh, offset, replier).in_current_span()) {
245            Ok(_) => {
246                reply.ok();
247                metrics::histogram!("fuse.readdirplus.entries").record(count as f64);
248            }
249            Err(e) => fuse_error!("readdirplus", reply, e, self, req),
250        }
251    }
252
253    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, fh=fh, datasync=datasync, name=field::Empty))]
254    fn fsync(&self, req: &Request<'_>, ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) {
255        match block_on(self.fs.fsync(ino, fh, datasync).in_current_span()) {
256            Ok(()) => reply.ok(),
257            Err(e) => fuse_error!("fsync", reply, e, self, req),
258        }
259    }
260
261    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, fh=fh, pid=req.pid(), name=field::Empty))]
262    fn flush(&self, req: &Request<'_>, ino: u64, fh: u64, lock_owner: u64, reply: ReplyEmpty) {
263        match block_on(self.fs.flush(ino, fh, lock_owner, req.pid()).in_current_span()) {
264            Ok(()) => reply.ok(),
265            Err(e) => fuse_error!("flush", reply, e, self, req),
266        }
267    }
268
269    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, fh=fh, name=field::Empty))]
270    fn release(
271        &self,
272        req: &Request<'_>,
273        ino: InodeNo,
274        fh: u64,
275        flags: i32,
276        lock_owner: Option<u64>,
277        flush: bool,
278        reply: ReplyEmpty,
279    ) {
280        match block_on(self.fs.release(ino, fh, flags, lock_owner, flush).in_current_span()) {
281            Ok(()) => reply.ok(),
282            Err(e) => fuse_error!("release", reply, e, self, req),
283        }
284    }
285
286    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, fh=fh))]
287    fn releasedir(&self, req: &Request<'_>, ino: u64, fh: u64, flags: i32, reply: ReplyEmpty) {
288        match block_on(self.fs.releasedir(ino, fh, flags).in_current_span()) {
289            Ok(()) => reply.ok(),
290            Err(e) => fuse_error!("releasedir", reply, e, self, req),
291        }
292    }
293
294    #[instrument(level="warn", skip_all, fields(req=req.unique(), parent=parent, name=?name))]
295    fn mknod(
296        &self,
297        req: &Request<'_>,
298        parent: InodeNo,
299        name: &OsStr,
300        mode: u32,
301        umask: u32,
302        rdev: u32,
303        reply: ReplyEntry,
304    ) {
305        // mode_t is u32 on Linux but u16 on macOS, so cast it here
306        let mode = mode as libc::mode_t;
307
308        match block_on(self.fs.mknod(parent, name, mode, umask, rdev).in_current_span()) {
309            Ok(entry) => reply.entry(&entry.ttl, &entry.attr, entry.generation),
310            Err(e) => fuse_error!("mknod", reply, e, self, req),
311        }
312    }
313
314    #[instrument(level="warn", skip_all, fields(req=req.unique(), parent=parent, name=?name))]
315    fn mkdir(&self, req: &Request<'_>, parent: u64, name: &OsStr, mode: u32, umask: u32, reply: ReplyEntry) {
316        // mode_t is u32 on Linux but u16 on macOS, so cast it here
317        let mode = mode as libc::mode_t;
318
319        match block_on(self.fs.mkdir(parent, name, mode, umask).in_current_span()) {
320            Ok(entry) => reply.entry(&entry.ttl, &entry.attr, entry.generation),
321            Err(e) => fuse_error!("mkdir", reply, e, self, req),
322        }
323    }
324
325    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, fh=fh, offset=offset, length=data.len(), pid=req.pid(), name=field::Empty))]
326    fn write(
327        &self,
328        req: &Request<'_>,
329        ino: InodeNo,
330        fh: u64,
331        offset: i64,
332        data: &[u8],
333        write_flags: u32,
334        flags: i32,
335        lock_owner: Option<u64>,
336        reply: ReplyWrite,
337    ) {
338        match block_on(
339            self.fs
340                .write(ino, fh, offset, data, write_flags, flags, lock_owner)
341                .in_current_span(),
342        ) {
343            Ok(bytes_written) => {
344                reply.written(bytes_written);
345                metrics::counter!("fuse.total_bytes", "type" => "write").increment(bytes_written as u64);
346                metrics::histogram!(FUSE_IO_SIZE, ATTR_FUSE_REQUEST => "write").record(bytes_written as f64);
347            }
348            Err(e) => fuse_error!("write", reply, e, self, req),
349        }
350    }
351
352    #[instrument(level="warn", skip_all, fields(req=req.unique(), parent=parent, name=?name))]
353    fn rmdir(&self, req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
354        match block_on(self.fs.rmdir(parent, name).in_current_span()) {
355            Ok(()) => reply.ok(),
356            Err(e) => fuse_error!("rmdir", reply, e, self, req),
357        }
358    }
359
360    #[instrument(level="warn", skip_all, fields(req=req.unique(), parent=parent, name=?name))]
361    fn unlink(&self, req: &Request<'_>, parent: InodeNo, name: &OsStr, reply: ReplyEmpty) {
362        match block_on(self.fs.unlink(parent, name).in_current_span()) {
363            Ok(()) => reply.ok(),
364            Err(e) => fuse_error!("unlink", reply, e, self, req),
365        }
366    }
367
368    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino, name=field::Empty))]
369    fn setattr(
370        &self,
371        req: &Request<'_>,
372        ino: u64,
373        _mode: Option<u32>,
374        _uid: Option<u32>,
375        _gid: Option<u32>,
376        size: Option<u64>,
377        atime: Option<TimeOrNow>,
378        mtime: Option<TimeOrNow>,
379        _ctime: Option<SystemTime>,
380        _fh: Option<u64>,
381        _crtime: Option<SystemTime>,
382        _chgtime: Option<SystemTime>,
383        _bkuptime: Option<SystemTime>,
384        flags: Option<u32>,
385        reply: ReplyAttr,
386    ) {
387        let atime = atime.map(|t| match t {
388            TimeOrNow::SpecificTime(st) => OffsetDateTime::from(st),
389            TimeOrNow::Now => OffsetDateTime::now_utc(),
390        });
391        let mtime = mtime.map(|t| match t {
392            TimeOrNow::SpecificTime(st) => OffsetDateTime::from(st),
393            TimeOrNow::Now => OffsetDateTime::now_utc(),
394        });
395        match block_on(self.fs.setattr(ino, atime, mtime, size, flags).in_current_span()) {
396            Ok(attr) => reply.attr(&attr.ttl, &attr.attr),
397            Err(e) => fuse_error!("setattr", reply, e, self, req),
398        }
399    }
400
401    // Everything below here is stubs for unsupported functions so we log them correctly
402
403    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino))]
404    fn readlink(&self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
405        fuse_unsupported!("readlink", reply);
406    }
407
408    #[instrument(level="warn", skip_all, fields(req=_req.unique(), parent=parent, name=?name, link=?link))]
409    fn symlink(&self, _req: &Request<'_>, parent: u64, name: &OsStr, link: &Path, reply: ReplyEntry) {
410        // Userspace expects EPERM for link/symlink if unsupported
411        fuse_unsupported!("symlink", reply, libc::EPERM);
412    }
413
414    #[instrument(level="warn", skip_all, fields(req=req.unique(), parent=parent, name=?name, newparent=newparent, newname=?newname))]
415    fn rename(
416        &self,
417        req: &Request<'_>,
418        parent: u64,
419        name: &OsStr,
420        newparent: u64,
421        newname: &OsStr,
422        flags: u32,
423        reply: ReplyEmpty,
424    ) {
425        match block_on(
426            self.fs
427                .rename(parent, name, newparent, newname, flags.into())
428                .in_current_span(),
429        ) {
430            Ok(()) => reply.ok(),
431            Err(e) => fuse_error!("rename", reply, e, self, req),
432        }
433    }
434
435    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, newparent=newparent, newname=?newname))]
436    fn link(&self, _req: &Request<'_>, ino: u64, newparent: u64, newname: &OsStr, reply: ReplyEntry) {
437        // Userspace expects EPERM for link/symlink if unsupported
438        fuse_unsupported!("link", reply, libc::EPERM);
439    }
440
441    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, fh=fh, datasync=datasync))]
442    fn fsyncdir(&self, _req: &Request<'_>, ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) {
443        fuse_unsupported!("fsyncdir", reply);
444    }
445
446    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, name=?name))]
447    fn setxattr(
448        &self,
449        _req: &Request<'_>,
450        ino: u64,
451        name: &OsStr,
452        _value: &[u8],
453        _flags: i32,
454        _position: u32,
455        reply: ReplyEmpty,
456    ) {
457        fuse_unsupported!("setxattr", reply);
458    }
459
460    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, name=?name))]
461    fn getxattr(&self, _req: &Request<'_>, ino: u64, name: &OsStr, _size: u32, reply: ReplyXattr) {
462        fuse_unsupported!("getxattr", reply);
463    }
464
465    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino))]
466    fn listxattr(&self, _req: &Request<'_>, ino: u64, _size: u32, reply: ReplyXattr) {
467        fuse_unsupported!("listxattr", reply);
468    }
469
470    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, name=?name))]
471    fn removexattr(&self, _req: &Request<'_>, ino: u64, name: &OsStr, reply: ReplyEmpty) {
472        fuse_unsupported!("removexattr", reply);
473    }
474
475    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, mask=mask))]
476    fn access(&self, _req: &Request<'_>, ino: u64, mask: i32, reply: ReplyEmpty) {
477        fuse_unsupported!("access", reply);
478    }
479
480    #[instrument(level="warn", skip_all, fields(req=_req.unique(), parent=parent, name=?name))]
481    fn create(
482        &self,
483        _req: &Request<'_>,
484        parent: u64,
485        name: &OsStr,
486        _mode: u32,
487        _umask: u32,
488        _flags: i32,
489        reply: ReplyCreate,
490    ) {
491        fuse_unsupported!("create", reply, libc::ENOSYS, tracing::Level::DEBUG);
492    }
493
494    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, fh=fh, pid=pid))]
495    fn getlk(
496        &self,
497        _req: &Request<'_>,
498        ino: u64,
499        fh: u64,
500        _lock_owner: u64,
501        _start: u64,
502        _end: u64,
503        _typ: i32,
504        pid: u32,
505        reply: ReplyLock,
506    ) {
507        fuse_unsupported!("getlk", reply);
508    }
509
510    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, fh=fh, pid=pid))]
511    fn setlk(
512        &self,
513        _req: &Request<'_>,
514        ino: u64,
515        fh: u64,
516        _lock_owner: u64,
517        _start: u64,
518        _end: u64,
519        _typ: i32,
520        pid: u32,
521        _sleep: bool,
522        reply: ReplyEmpty,
523    ) {
524        fuse_unsupported!("setlk", reply);
525    }
526
527    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino))]
528    fn bmap(&self, _req: &Request<'_>, ino: u64, _blocksize: u32, _idx: u64, reply: ReplyBmap) {
529        fuse_unsupported!("bmap", reply);
530    }
531
532    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, fh=fh, cmd=cmd))]
533    fn ioctl(
534        &self,
535        _req: &Request<'_>,
536        ino: u64,
537        fh: u64,
538        _flags: u32,
539        cmd: u32,
540        _in_data: &[u8],
541        _out_size: u32,
542        reply: ReplyIoctl,
543    ) {
544        fuse_unsupported!("ioctl", reply, libc::ENOSYS, tracing::Level::DEBUG);
545    }
546
547    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, fh=fh, offset=offset, length=length))]
548    fn fallocate(
549        &self,
550        _req: &Request<'_>,
551        ino: u64,
552        fh: u64,
553        offset: i64,
554        length: i64,
555        _mode: i32,
556        reply: ReplyEmpty,
557    ) {
558        fuse_unsupported!("fallocate", reply);
559    }
560
561    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino, fh=fh, offset=offset, whence=whence))]
562    fn lseek(&self, _req: &Request<'_>, ino: u64, fh: u64, offset: i64, whence: i32, reply: ReplyLseek) {
563        fuse_unsupported!("lseek", reply);
564    }
565
566    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino_in=ino_in, fh_in=fh_in, offset_in=offset_in, ino_out=ino_out, fh_out=fh_out, offset_out=offset_out, len=len))]
567    fn copy_file_range(
568        &self,
569        _req: &Request<'_>,
570        ino_in: u64,
571        fh_in: u64,
572        offset_in: i64,
573        ino_out: u64,
574        fh_out: u64,
575        offset_out: i64,
576        len: u64,
577        _flags: u32,
578        reply: ReplyWrite,
579    ) {
580        fuse_unsupported!("copy_file_range", reply);
581    }
582
583    #[cfg(target_os = "macos")]
584    #[instrument(level="warn", skip_all, fields(req=_req.unique(), name=?name))]
585    fn setvolname(&self, _req: &Request<'_>, name: &OsStr, reply: ReplyEmpty) {
586        fuse_unsupported!("setvolname", reply);
587    }
588
589    #[cfg(target_os = "macos")]
590    #[instrument(level="warn", skip_all, fields(req=_req.unique(), parent=parent, name=?name, newparent=newparent, newname=?newname))]
591    fn exchange(
592        &self,
593        _req: &Request<'_>,
594        parent: u64,
595        name: &OsStr,
596        newparent: u64,
597        newname: &OsStr,
598        _options: u64,
599        reply: ReplyEmpty,
600    ) {
601        fuse_unsupported!("exchange", reply);
602    }
603
604    #[cfg(target_os = "macos")]
605    #[instrument(level="warn", skip_all, fields(req=_req.unique(), ino=ino))]
606    fn getxtimes(&self, _req: &Request<'_>, ino: u64, reply: ReplyXTimes) {
607        fuse_unsupported!("getxtimes", reply);
608    }
609
610    #[instrument(level="warn", skip_all, fields(req=req.unique(), ino=ino))]
611    fn statfs(&self, req: &Request<'_>, ino: u64, reply: ReplyStatfs) {
612        match block_on(self.fs.statfs(ino).in_current_span()) {
613            Ok(statfs) => reply.statfs(
614                statfs.total_blocks,
615                statfs.free_blocks,
616                statfs.available_blocks,
617                statfs.total_inodes,
618                statfs.free_inodes,
619                statfs.block_size,
620                statfs.maximum_name_length,
621                statfs.fragment_size,
622            ),
623            Err(e) => fuse_error!("statfs", reply, e, self, req),
624        }
625    }
626}