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-providers")]
9use std::io::SeekFrom;
10#[cfg(feature = "tokio-providers")]
11use std::pin::Pin;
12#[cfg(feature = "tokio-providers")]
13use std::task::{Context, Poll};
14#[cfg(feature = "tokio-providers")]
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
81impl OpenOptions {
82    /// Returns true if the file will be opened for reading.
83    #[must_use]
84    pub fn is_read(&self) -> bool {
85        self.flags & Self::FLAG_READ != 0
86    }
87
88    /// Returns true if the file will be opened for writing.
89    #[must_use]
90    pub fn is_write(&self) -> bool {
91        self.flags & Self::FLAG_WRITE != 0
92    }
93
94    /// Returns true if the file will be created if it does not exist.
95    #[must_use]
96    pub fn is_create(&self) -> bool {
97        self.flags & Self::FLAG_CREATE != 0
98    }
99
100    /// Returns true if the file must be created new (failing if it exists).
101    #[must_use]
102    pub fn is_create_new(&self) -> bool {
103        self.flags & Self::FLAG_CREATE_NEW != 0
104    }
105
106    /// Returns true if the file will be truncated to zero length on open.
107    #[must_use]
108    pub fn is_truncate(&self) -> bool {
109        self.flags & Self::FLAG_TRUNCATE != 0
110    }
111
112    /// Returns true if writes will be appended to the end of the file.
113    #[must_use]
114    pub fn is_append(&self) -> bool {
115        self.flags & Self::FLAG_APPEND != 0
116    }
117
118    /// Create options for read-only access.
119    #[must_use]
120    pub fn read_only() -> Self {
121        Self::new().read(true)
122    }
123
124    /// Create options for creating and writing a new file (truncating if exists).
125    #[must_use]
126    pub fn create_write() -> Self {
127        Self::new().write(true).create(true).truncate(true)
128    }
129
130    /// Create options for creating a new file for writing (fails if exists).
131    #[must_use]
132    pub fn create_new_write() -> Self {
133        Self::new().write(true).create_new(true)
134    }
135}
136
137/// Provider trait for file storage operations.
138///
139/// Clone allows sharing providers across multiple components efficiently.
140pub trait StorageProvider: Clone + Send + Sync + 'static {
141    /// The file type for this provider.
142    type File: StorageFile + 'static;
143
144    /// Open a file with the given options.
145    fn open(
146        &self,
147        path: &str,
148        options: OpenOptions,
149    ) -> impl std::future::Future<Output = io::Result<Self::File>> + Send;
150
151    /// Check if a file exists at the given path.
152    fn exists(&self, path: &str) -> impl std::future::Future<Output = io::Result<bool>> + Send;
153
154    /// Delete a file at the given path.
155    fn delete(&self, path: &str) -> impl std::future::Future<Output = io::Result<()>> + Send;
156
157    /// Rename a file from one path to another.
158    fn rename(
159        &self,
160        from: &str,
161        to: &str,
162    ) -> impl std::future::Future<Output = io::Result<()>> + Send;
163}
164
165/// Trait for file handles that support async read/write/seek operations.
166pub trait StorageFile: AsyncRead + AsyncWrite + AsyncSeek + Unpin + Send + Sync + 'static {
167    /// Flush all OS-internal metadata and data to disk.
168    fn sync_all(&self) -> impl std::future::Future<Output = io::Result<()>> + Send;
169
170    /// Flush all data to disk (metadata may not be synced).
171    fn sync_data(&self) -> impl std::future::Future<Output = io::Result<()>> + Send;
172
173    /// Get the current size of the file in bytes.
174    fn size(&self) -> impl std::future::Future<Output = io::Result<u64>> + Send;
175
176    /// Set the length of the file.
177    fn set_len(&self, size: u64) -> impl std::future::Future<Output = io::Result<()>> + Send;
178}
179
180/// Real Tokio storage implementation.
181#[cfg(feature = "tokio-providers")]
182#[derive(Debug, Clone, Default)]
183pub struct TokioStorageProvider;
184
185#[cfg(feature = "tokio-providers")]
186impl TokioStorageProvider {
187    /// Create a new Tokio storage provider.
188    #[must_use]
189    pub fn new() -> Self {
190        Self
191    }
192}
193
194#[cfg(feature = "tokio-providers")]
195impl StorageProvider for TokioStorageProvider {
196    type File = TokioStorageFile;
197
198    async fn open(&self, path: &str, options: OpenOptions) -> io::Result<Self::File> {
199        let file = tokio::fs::OpenOptions::new()
200            .read(options.is_read())
201            .write(options.is_write())
202            .create(options.is_create())
203            .create_new(options.is_create_new())
204            .truncate(options.is_truncate())
205            .append(options.is_append())
206            .open(path)
207            .await?;
208        Ok(TokioStorageFile {
209            inner: file.compat(),
210        })
211    }
212
213    async fn exists(&self, path: &str) -> io::Result<bool> {
214        match tokio::fs::metadata(path).await {
215            Ok(_) => Ok(true),
216            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
217            Err(e) => Err(e),
218        }
219    }
220
221    async fn delete(&self, path: &str) -> io::Result<()> {
222        tokio::fs::remove_file(path).await
223    }
224
225    async fn rename(&self, from: &str, to: &str) -> io::Result<()> {
226        tokio::fs::rename(from, to).await
227    }
228}
229
230/// Wrapper for Tokio File to implement our trait.
231///
232/// Holds the underlying `tokio::fs::File` inside `tokio_util::compat::Compat`
233/// so the `futures::io` trait impls come through automatically. The four custom
234/// methods on [`StorageFile`] reach the inner [`tokio::fs::File`] via
235/// [`Compat::get_ref`] / [`Compat::get_mut`] to call tokio-specific APIs
236/// (`sync_all`, `sync_data`, `metadata`, `set_len`) that `Compat` itself
237/// does not expose.
238#[cfg(feature = "tokio-providers")]
239#[derive(Debug)]
240pub struct TokioStorageFile {
241    inner: Compat<tokio::fs::File>,
242}
243
244#[cfg(feature = "tokio-providers")]
245impl StorageFile for TokioStorageFile {
246    async fn sync_all(&self) -> io::Result<()> {
247        self.inner.get_ref().sync_all().await
248    }
249
250    async fn sync_data(&self) -> io::Result<()> {
251        self.inner.get_ref().sync_data().await
252    }
253
254    async fn size(&self) -> io::Result<u64> {
255        let metadata = self.inner.get_ref().metadata().await?;
256        Ok(metadata.len())
257    }
258
259    async fn set_len(&self, size: u64) -> io::Result<()> {
260        self.inner.get_ref().set_len(size).await
261    }
262}
263
264#[cfg(feature = "tokio-providers")]
265impl AsyncRead for TokioStorageFile {
266    fn poll_read(
267        mut self: Pin<&mut Self>,
268        cx: &mut Context<'_>,
269        buf: &mut [u8],
270    ) -> Poll<io::Result<usize>> {
271        Pin::new(&mut self.inner).poll_read(cx, buf)
272    }
273}
274
275#[cfg(feature = "tokio-providers")]
276impl AsyncWrite for TokioStorageFile {
277    fn poll_write(
278        mut self: Pin<&mut Self>,
279        cx: &mut Context<'_>,
280        buf: &[u8],
281    ) -> Poll<io::Result<usize>> {
282        Pin::new(&mut self.inner).poll_write(cx, buf)
283    }
284
285    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
286        Pin::new(&mut self.inner).poll_flush(cx)
287    }
288
289    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
290        Pin::new(&mut self.inner).poll_close(cx)
291    }
292}
293
294#[cfg(feature = "tokio-providers")]
295impl AsyncSeek for TokioStorageFile {
296    fn poll_seek(
297        mut self: Pin<&mut Self>,
298        cx: &mut Context<'_>,
299        pos: SeekFrom,
300    ) -> Poll<io::Result<u64>> {
301        Pin::new(&mut self.inner).poll_seek(cx, pos)
302    }
303}