mountpoint_s3_fs/metablock.rs
1//! Metablock trait and related types for generic filesystem implementations
2
3use async_trait::async_trait;
4use mountpoint_s3_client::types::ETag;
5use std::ffi::{OsStr, OsString};
6use time::OffsetDateTime;
7
8// Import core types from submodules
9mod error;
10mod expiry;
11mod lookup;
12mod path;
13mod pending_upload;
14mod stat;
15
16// Re-export all the core types
17pub use error::{InodeError, InodeErrorInfo};
18pub use expiry::{Expiry, NEVER_EXPIRE_TTL};
19pub use lookup::{InodeInformation, Lookup};
20pub use path::{S3Location, ValidKey, ValidKeyError, ValidName};
21pub use pending_upload::PendingUploadHook;
22pub use stat::{InodeKind, InodeNo, InodeStat};
23
24use crate::fs::OpenFlags;
25
26pub const ROOT_INODE_NO: InodeNo = crate::fs::FUSE_ROOT_INODE;
27
28/// A trait for a generic implementation of a structure managing a filesystem backed by S3.
29///
30/// In general, an implementation may implement a subset of the operations and respond with
31/// `InodeError::UnsupportedOperations` for the unsupported ones.
32/// However, minimally `lookup`, `getattr` and the three readdir functions should be implemented
33/// to have a minimally viable file system view.
34/// To allow reading from files, additionally `start_reading` and `finish_reading` must be implemented.
35/// For writing, it is required to implement `start_writing`, `inc_file_size` and `finish_writing`.
36#[async_trait]
37pub trait Metablock: Send + Sync {
38 /// Lookup an inode in the parent directory with the given name.
39 async fn lookup(&self, parent_ino: InodeNo, name: &OsStr) -> Result<Lookup, InodeError>;
40
41 /// Retrieve the attributes for an inode
42 async fn getattr(&self, ino: InodeNo, force_revalidate_if_remote: bool) -> Result<Lookup, InodeError>;
43
44 /// Set the attributes for an inode
45 async fn setattr(
46 &self,
47 ino: InodeNo,
48 atime: Option<OffsetDateTime>,
49 mtime: Option<OffsetDateTime>,
50 ) -> Result<Lookup, InodeError>;
51
52 /// Create a new regular file or directory inode ready to be opened in write-only mode
53 async fn create(&self, dir: InodeNo, name: &OsStr, kind: InodeKind) -> Result<Lookup, InodeError>;
54
55 /// The kernel tells us when it removes a reference to an [InodeNo] from its internal caches via a forget call.
56 /// The kernel may forget a number of references (`n`) in one forget message to our FUSE implementation.
57 async fn forget(&self, ino: InodeNo, n: u64);
58
59 /// Open a new file handle for the given inode in read or write mode depending on flags and inode state.
60 async fn open_handle(
61 &self,
62 ino: InodeNo,
63 fh: u64,
64 write_mode: &WriteMode,
65 flags: OpenFlags,
66 ) -> Result<NewHandle, InodeError>;
67
68 /// Increase the size of a file open for writing.
69 /// Parameter `len` refers to the additional
70 /// Returns the new size after the increase.
71 async fn inc_file_size(&self, ino: InodeNo, len: usize) -> Result<usize, InodeError>;
72
73 /// Called when the filesystem has finished writing to the inode referenced by `ino` using the
74 /// file handle `fh`.
75 ///
76 /// Returns the latest state in S3 for the inode after the writing to S3 is concluded.
77 async fn finish_writing(&self, ino: InodeNo, etag: Option<ETag>, fh: u64) -> Result<Lookup, InodeError>;
78
79 /// Finish reading from the inode (referenced by `ino`) using the file handle `fh`.
80 async fn finish_reading(&self, ino: InodeNo, fh: u64) -> Result<(), InodeError>;
81
82 /// Called when the filesystem has called `flush` on a read handle `fh` for the inode
83 /// referenced by `ino`.
84 async fn flush_reader(&self, ino: InodeNo, fh: u64) -> Result<(), InodeError>;
85
86 /// Called when the filesystem has called `flush` on a write handle `fh` for the inode
87 /// referenced by `ino`.
88 ///
89 /// Attaches a reference to the pending upload hook for this writer, if there's not one attached
90 /// already.
91 ///
92 /// Returns a reference to the existing/new upload hook linked to the inode `ino`, which the
93 /// caller may choose to await the completion of.
94 async fn flush_writer(
95 &self,
96 ino: InodeNo,
97 fh: u64,
98 pending_upload_hook: PendingUploadHook,
99 ) -> Result<Option<PendingUploadHook>, InodeError>;
100
101 /// Called when the filesystem has released a write handle for the inode referenced by `ino`.
102 ///
103 /// The implementer owns completing any pending uploads and cleaning up the internal state for
104 /// this handle.
105 async fn release_writer(
106 &self,
107 ino: InodeNo,
108 fh: u64,
109 pending_upload_hook: PendingUploadHook,
110 location: &S3Location,
111 ) -> Result<(), InodeError>;
112
113 /// Called by filesystem's read/write methods to attempt re-activation of a deactivated file
114 /// handle `fh` for the inode referenced by `ino`.
115 ///
116 /// Returns whether the handle was successfully reactivated.
117 async fn try_reactivate_handle(&self, ino: InodeNo, fh: u64, mode: ReadWriteMode) -> Result<bool, InodeError>;
118
119 /// Start a readdir stream for the given directory referenced inode (`dir_ino`)
120 ///
121 /// Returns a number with which this stream can be accessed in `readdir` and `releasedir`.
122 async fn new_readdir_handle(&self, dir_ino: InodeNo) -> Result<u64, InodeError>;
123
124 /// Reads entries from the readdir stream, for the directory `parent`, referred to by `fh` starting at offset `offset`.
125 ///
126 /// Entries shall be passed to `add` as described in its documentation.
127 async fn readdir<'a>(
128 &self,
129 parent: InodeNo,
130 fh: u64,
131 offset: i64,
132 is_readdirplus: bool,
133 mut add: AddDirEntry<'a>,
134 ) -> Result<(), InodeError>;
135
136 /// Closes the readdir handle.
137 async fn releasedir(&self, fh: u64) -> Result<(), InodeError>;
138
139 /// Rename inode described by source parent and name to instead be linked under the given destination and name.
140 /// If the parameter `allow_overwrite` is set to false, renames where the destination would be replaced shall fail with `InodeError::RenameDestinationExists`.
141 async fn rename(
142 &self,
143 src_parent_ino: InodeNo,
144 src_name: &OsStr,
145 dst_parent_ino: InodeNo,
146 dst_name: &OsStr,
147 allow_overwrite: bool,
148 ) -> Result<(), InodeError>;
149
150 /// Remove a directory given by name and parent directory.
151 async fn rmdir(&self, parent_ino: InodeNo, name: &OsStr) -> Result<(), InodeError>;
152
153 /// Unlink the entry described by `parent_ino` and `name`.
154 async fn unlink(&self, parent_ino: InodeNo, name: &OsStr) -> Result<(), InodeError>;
155}
156
157/// Callback to the file system which adds directory entries to the reply buffer.
158///
159/// # Parameters (in order)
160///
161/// * `information` - Contains metadata about the inode
162/// * `name` - The name of the directory entry
163/// * `offset` - Position of this entry
164/// * `generation` - Generation number[^1]
165///
166/// # Returns
167///
168/// - [AddDirEntryResult::EntryAdded] if the entry was added, or
169/// - [AddDirEntryResult::ReplyBufferFull] if the reply buffer was full.
170///
171///
172/// [^1]: The generation number is used to ensure uniqueness of inode/generation pairs.
173/// If the file system were exported over NFS, these pairs would need to be unique.
174/// For more information, see the [libfuse documentation](https://github.com/libfuse/libfuse/blob/fc1c8da0cf8a18d222cb1feed0057ba44ea4d18f/include/fuse_lowlevel.h#L70).
175pub type AddDirEntry<'r> = Box<dyn FnMut(InodeInformation, OsString, i64, u64) -> AddDirEntryResult + Send + Sync + 'r>;
176
177/// Result of a call to `AddDirEntry`.
178#[derive(Debug, PartialEq, Eq)]
179pub enum AddDirEntryResult {
180 /// The entry was added successfully.
181 EntryAdded,
182 /// The entry was not added because the reply buffer was full.
183 ReplyBufferFull,
184}
185
186#[derive(Debug, Default)]
187pub struct WriteMode {
188 /// Allow overwrite
189 pub allow_overwrite: bool,
190 /// Enable incremental uploads
191 pub incremental_upload: bool,
192}
193
194impl WriteMode {
195 pub fn is_inode_writable(&self, is_truncate: bool) -> bool {
196 if self.incremental_upload || (self.allow_overwrite && is_truncate) {
197 true
198 } else {
199 if is_truncate {
200 tracing::warn!(
201 "file overwrite is disabled by default, you need to remount with --allow-overwrite flag to enable it"
202 );
203 } else {
204 tracing::warn!(
205 "modifying an existing file is disabled by default, you need to remount with the --allow-overwrite or the --incremental-upload flag to enable it"
206 );
207 }
208 false
209 }
210 }
211}
212
213/// The mode in which a handle is provisioned to access the (existing in S3 or local) data for a file
214/// (mapped to an inode and backed by a corresponding S3 object)
215#[derive(Debug, Clone, Copy)]
216pub enum ReadWriteMode {
217 /// Allow reads from this and other concurrent readers (but no writer)
218 Read,
219 /// Allow writes from this exclusive writer (but no other writer or readers)
220 Write,
221}
222
223/// A metablock-level abstraction on a file, providing the user with the latest metadata in the
224/// linked inode and the ReadWriteMode in which they're allowed to access the file
225#[derive(Debug)]
226pub struct NewHandle {
227 pub lookup: Lookup,
228 pub mode: ReadWriteMode,
229}
230
231impl NewHandle {
232 pub fn read(lookup: Lookup) -> Self {
233 Self {
234 lookup,
235 mode: ReadWriteMode::Read,
236 }
237 }
238
239 pub fn write(lookup: Lookup) -> Self {
240 Self {
241 lookup,
242 mode: ReadWriteMode::Write,
243 }
244 }
245}