Skip to main content

moonpool_core/
storage.rs

1//! Storage provider abstraction for simulation and real file I/O.
2//!
3//! This module provides trait-based file storage that allows seamless swapping
4//! between real Tokio file I/O and simulated storage for testing.
5
6use futures::io::{AsyncRead, AsyncSeek, AsyncWrite};
7use std::io;
8#[cfg(feature = "tokio-fs")]
9use std::io::SeekFrom;
10#[cfg(feature = "tokio-fs")]
11use std::pin::Pin;
12#[cfg(feature = "tokio-fs")]
13use std::task::{Context, Poll};
14#[cfg(feature = "tokio-fs")]
15use tokio_util::compat::{Compat, TokioAsyncReadCompatExt};
16
17/// Options for opening a file.
18///
19/// This struct provides a builder-style API for configuring how a file
20/// should be opened, similar to [`std::fs::OpenOptions`]. The individual
21/// flags are stored in a single bitfield and exposed via `is_*` accessors.
22#[derive(Debug, Clone, Default)]
23pub struct OpenOptions {
24    /// Bit-packed flags. See the `FLAG_*` constants on this type.
25    flags: u8,
26}
27
28impl OpenOptions {
29    const FLAG_READ: u8 = 1 << 0;
30    const FLAG_WRITE: u8 = 1 << 1;
31    const FLAG_CREATE: u8 = 1 << 2;
32    const FLAG_CREATE_NEW: u8 = 1 << 3;
33    const FLAG_TRUNCATE: u8 = 1 << 4;
34    const FLAG_APPEND: u8 = 1 << 5;
35
36    /// Create new open options with all flags set to false.
37    #[must_use]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    fn set_flag(mut self, flag: u8, value: bool) -> Self {
43        if value {
44            self.flags |= flag;
45        } else {
46            self.flags &= !flag;
47        }
48        self
49    }
50}
51
52macro_rules! flag_setters {
53    ($($(#[$attr:meta])* $name:ident => $flag:ident),* $(,)?) => {
54        impl OpenOptions {
55            $(
56                $(#[$attr])*
57                #[must_use]
58                pub fn $name(self, value: bool) -> Self {
59                    self.set_flag(Self::$flag, value)
60                }
61            )*
62        }
63    };
64}
65
66flag_setters! {
67    /// Set the read flag.
68    read => FLAG_READ,
69    /// Set the write flag.
70    write => FLAG_WRITE,
71    /// Set the create flag.
72    create => FLAG_CREATE,
73    /// Set the `create_new` flag.
74    create_new => FLAG_CREATE_NEW,
75    /// Set the truncate flag.
76    truncate => FLAG_TRUNCATE,
77    /// Set the append flag.
78    append => FLAG_APPEND,
79}
80
81macro_rules! flag_getters {
82    ($($(#[$attr:meta])* $name:ident => $flag:ident),* $(,)?) => {
83        impl OpenOptions {
84            $(
85                $(#[$attr])*
86                #[must_use]
87                pub fn $name(&self) -> bool {
88                    self.flags & Self::$flag != 0
89                }
90            )*
91        }
92    };
93}
94
95flag_getters! {
96    /// Returns true if the file will be opened for reading.
97    is_read => FLAG_READ,
98    /// Returns true if the file will be opened for writing.
99    is_write => FLAG_WRITE,
100    /// Returns true if the file will be created if it does not exist.
101    is_create => FLAG_CREATE,
102    /// Returns true if the file must be created new (failing if it exists).
103    is_create_new => FLAG_CREATE_NEW,
104    /// Returns true if the file will be truncated to zero length on open.
105    is_truncate => FLAG_TRUNCATE,
106    /// Returns true if writes will be appended to the end of the file.
107    is_append => FLAG_APPEND,
108}
109
110impl OpenOptions {
111    /// Create options for read-only access.
112    #[must_use]
113    pub fn read_only() -> Self {
114        Self::new().read(true)
115    }
116
117    /// Create options for creating and writing a new file (truncating if exists).
118    #[must_use]
119    pub fn create_write() -> Self {
120        Self::new().write(true).create(true).truncate(true)
121    }
122
123    /// Create options for creating a new file for writing (fails if exists).
124    #[must_use]
125    pub fn create_new_write() -> Self {
126        Self::new().write(true).create_new(true)
127    }
128}
129
130/// Provider trait for file storage operations.
131///
132/// Clone allows sharing providers across multiple components efficiently.
133pub trait StorageProvider: Clone + Send + Sync + 'static {
134    /// The file type for this provider.
135    type File: StorageFile + 'static;
136
137    /// Open a file with the given options.
138    fn open(
139        &self,
140        path: &str,
141        options: OpenOptions,
142    ) -> impl std::future::Future<Output = io::Result<Self::File>> + Send;
143
144    /// Check if a file exists at the given path.
145    fn exists(&self, path: &str) -> impl std::future::Future<Output = io::Result<bool>> + Send;
146
147    /// Delete a file at the given path.
148    fn delete(&self, path: &str) -> impl std::future::Future<Output = io::Result<()>> + Send;
149
150    /// Rename a file from one path to another.
151    fn rename(
152        &self,
153        from: &str,
154        to: &str,
155    ) -> impl std::future::Future<Output = io::Result<()>> + Send;
156}
157
158/// Trait for file handles that support async read/write/seek operations.
159pub trait StorageFile: AsyncRead + AsyncWrite + AsyncSeek + Unpin + Send + Sync + 'static {
160    /// Flush all OS-internal metadata and data to disk.
161    fn sync_all(&self) -> impl std::future::Future<Output = io::Result<()>> + Send;
162
163    /// Flush all data to disk (metadata may not be synced).
164    fn sync_data(&self) -> impl std::future::Future<Output = io::Result<()>> + Send;
165
166    /// Get the current size of the file in bytes.
167    fn size(&self) -> impl std::future::Future<Output = io::Result<u64>> + Send;
168
169    /// Set the length of the file.
170    fn set_len(&self, size: u64) -> impl std::future::Future<Output = io::Result<()>> + Send;
171}
172
173/// Real Tokio storage implementation.
174#[cfg(feature = "tokio-fs")]
175#[derive(Debug, Clone, Default)]
176pub struct TokioStorageProvider;
177
178#[cfg(feature = "tokio-fs")]
179impl TokioStorageProvider {
180    /// Create a new Tokio storage provider.
181    #[must_use]
182    pub fn new() -> Self {
183        Self
184    }
185}
186
187#[cfg(feature = "tokio-fs")]
188impl StorageProvider for TokioStorageProvider {
189    type File = TokioStorageFile;
190
191    async fn open(&self, path: &str, options: OpenOptions) -> io::Result<Self::File> {
192        let file = tokio::fs::OpenOptions::new()
193            .read(options.is_read())
194            .write(options.is_write())
195            .create(options.is_create())
196            .create_new(options.is_create_new())
197            .truncate(options.is_truncate())
198            .append(options.is_append())
199            .open(path)
200            .await?;
201        Ok(TokioStorageFile {
202            inner: file.compat(),
203        })
204    }
205
206    async fn exists(&self, path: &str) -> io::Result<bool> {
207        match tokio::fs::metadata(path).await {
208            Ok(_) => Ok(true),
209            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
210            Err(e) => Err(e),
211        }
212    }
213
214    async fn delete(&self, path: &str) -> io::Result<()> {
215        tokio::fs::remove_file(path).await
216    }
217
218    async fn rename(&self, from: &str, to: &str) -> io::Result<()> {
219        tokio::fs::rename(from, to).await
220    }
221}
222
223/// Wrapper for Tokio File to implement our trait.
224///
225/// Holds the underlying `tokio::fs::File` inside `tokio_util::compat::Compat`
226/// so the `futures::io` trait impls come through automatically. The four custom
227/// methods on [`StorageFile`] reach the inner [`tokio::fs::File`] via
228/// [`Compat::get_ref`] / [`Compat::get_mut`] to call tokio-specific APIs
229/// (`sync_all`, `sync_data`, `metadata`, `set_len`) that `Compat` itself
230/// does not expose.
231#[cfg(feature = "tokio-fs")]
232#[derive(Debug)]
233pub struct TokioStorageFile {
234    inner: Compat<tokio::fs::File>,
235}
236
237#[cfg(feature = "tokio-fs")]
238impl StorageFile for TokioStorageFile {
239    async fn sync_all(&self) -> io::Result<()> {
240        self.inner.get_ref().sync_all().await
241    }
242
243    async fn sync_data(&self) -> io::Result<()> {
244        self.inner.get_ref().sync_data().await
245    }
246
247    async fn size(&self) -> io::Result<u64> {
248        let metadata = self.inner.get_ref().metadata().await?;
249        Ok(metadata.len())
250    }
251
252    async fn set_len(&self, size: u64) -> io::Result<()> {
253        self.inner.get_ref().set_len(size).await
254    }
255}
256
257#[cfg(feature = "tokio-fs")]
258impl AsyncRead for TokioStorageFile {
259    fn poll_read(
260        mut self: Pin<&mut Self>,
261        cx: &mut Context<'_>,
262        buf: &mut [u8],
263    ) -> Poll<io::Result<usize>> {
264        Pin::new(&mut self.inner).poll_read(cx, buf)
265    }
266}
267
268#[cfg(feature = "tokio-fs")]
269impl AsyncWrite for TokioStorageFile {
270    fn poll_write(
271        mut self: Pin<&mut Self>,
272        cx: &mut Context<'_>,
273        buf: &[u8],
274    ) -> Poll<io::Result<usize>> {
275        Pin::new(&mut self.inner).poll_write(cx, buf)
276    }
277
278    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
279        Pin::new(&mut self.inner).poll_flush(cx)
280    }
281
282    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
283        Pin::new(&mut self.inner).poll_close(cx)
284    }
285}
286
287#[cfg(feature = "tokio-fs")]
288impl AsyncSeek for TokioStorageFile {
289    fn poll_seek(
290        mut self: Pin<&mut Self>,
291        cx: &mut Context<'_>,
292        pos: SeekFrom,
293    ) -> Poll<io::Result<u64>> {
294        Pin::new(&mut self.inner).poll_seek(cx, pos)
295    }
296}