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 async_trait::async_trait;
7use std::io;
8use std::io::SeekFrom;
9use std::pin::Pin;
10use std::task::{Context, Poll};
11use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
12
13/// Options for opening a file.
14///
15/// This struct provides a builder-style API for configuring how a file
16/// should be opened, similar to [`std::fs::OpenOptions`].
17#[derive(Debug, Clone, Default)]
18pub struct OpenOptions {
19    /// Open file for reading.
20    pub read: bool,
21    /// Open file for writing.
22    pub write: bool,
23    /// Create the file if it doesn't exist.
24    pub create: bool,
25    /// Create a new file, failing if it already exists.
26    pub create_new: bool,
27    /// Truncate the file to zero length.
28    pub truncate: bool,
29    /// Append to the end of the file.
30    pub append: bool,
31}
32
33impl OpenOptions {
34    /// Create new open options with all flags set to false.
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Set the read flag.
40    pub fn read(mut self, read: bool) -> Self {
41        self.read = read;
42        self
43    }
44
45    /// Set the write flag.
46    pub fn write(mut self, write: bool) -> Self {
47        self.write = write;
48        self
49    }
50
51    /// Set the create flag.
52    pub fn create(mut self, create: bool) -> Self {
53        self.create = create;
54        self
55    }
56
57    /// Set the create_new flag.
58    pub fn create_new(mut self, create_new: bool) -> Self {
59        self.create_new = create_new;
60        self
61    }
62
63    /// Set the truncate flag.
64    pub fn truncate(mut self, truncate: bool) -> Self {
65        self.truncate = truncate;
66        self
67    }
68
69    /// Set the append flag.
70    pub fn append(mut self, append: bool) -> Self {
71        self.append = append;
72        self
73    }
74
75    /// Create options for read-only access.
76    pub fn read_only() -> Self {
77        Self::new().read(true)
78    }
79
80    /// Create options for creating and writing a new file (truncating if exists).
81    pub fn create_write() -> Self {
82        Self::new().write(true).create(true).truncate(true)
83    }
84
85    /// Create options for read and write access.
86    pub fn read_write() -> Self {
87        Self::new().read(true).write(true)
88    }
89
90    /// Create options for creating a new file for writing (fails if exists).
91    pub fn create_new_write() -> Self {
92        Self::new().write(true).create_new(true)
93    }
94}
95
96/// Provider trait for file storage operations.
97///
98/// Single-core design - no Send bounds needed.
99/// Clone allows sharing providers across multiple components efficiently.
100#[async_trait(?Send)]
101pub trait StorageProvider: Clone {
102    /// The file type for this provider.
103    type File: StorageFile + 'static;
104
105    /// Open a file with the given options.
106    async fn open(&self, path: &str, options: OpenOptions) -> io::Result<Self::File>;
107
108    /// Check if a file exists at the given path.
109    async fn exists(&self, path: &str) -> io::Result<bool>;
110
111    /// Delete a file at the given path.
112    async fn delete(&self, path: &str) -> io::Result<()>;
113
114    /// Rename a file from one path to another.
115    async fn rename(&self, from: &str, to: &str) -> io::Result<()>;
116}
117
118/// Trait for file handles that support async read/write/seek operations.
119#[async_trait(?Send)]
120pub trait StorageFile: AsyncRead + AsyncWrite + AsyncSeek + Unpin {
121    /// Flush all OS-internal metadata and data to disk.
122    async fn sync_all(&self) -> io::Result<()>;
123
124    /// Flush all data to disk (metadata may not be synced).
125    async fn sync_data(&self) -> io::Result<()>;
126
127    /// Get the current size of the file in bytes.
128    async fn size(&self) -> io::Result<u64>;
129
130    /// Set the length of the file.
131    async fn set_len(&self, size: u64) -> io::Result<()>;
132}
133
134/// Real Tokio storage implementation.
135#[derive(Debug, Clone)]
136pub struct TokioStorageProvider;
137
138impl TokioStorageProvider {
139    /// Create a new Tokio storage provider.
140    pub fn new() -> Self {
141        Self
142    }
143}
144
145impl Default for TokioStorageProvider {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151#[async_trait(?Send)]
152impl StorageProvider for TokioStorageProvider {
153    type File = TokioStorageFile;
154
155    async fn open(&self, path: &str, options: OpenOptions) -> io::Result<Self::File> {
156        let file = tokio::fs::OpenOptions::new()
157            .read(options.read)
158            .write(options.write)
159            .create(options.create)
160            .create_new(options.create_new)
161            .truncate(options.truncate)
162            .append(options.append)
163            .open(path)
164            .await?;
165        Ok(TokioStorageFile { inner: file })
166    }
167
168    async fn exists(&self, path: &str) -> io::Result<bool> {
169        match tokio::fs::metadata(path).await {
170            Ok(_) => Ok(true),
171            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
172            Err(e) => Err(e),
173        }
174    }
175
176    async fn delete(&self, path: &str) -> io::Result<()> {
177        tokio::fs::remove_file(path).await
178    }
179
180    async fn rename(&self, from: &str, to: &str) -> io::Result<()> {
181        tokio::fs::rename(from, to).await
182    }
183}
184
185/// Wrapper for Tokio File to implement our trait.
186#[derive(Debug)]
187pub struct TokioStorageFile {
188    inner: tokio::fs::File,
189}
190
191#[async_trait(?Send)]
192impl StorageFile for TokioStorageFile {
193    async fn sync_all(&self) -> io::Result<()> {
194        self.inner.sync_all().await
195    }
196
197    async fn sync_data(&self) -> io::Result<()> {
198        self.inner.sync_data().await
199    }
200
201    async fn size(&self) -> io::Result<u64> {
202        let metadata = self.inner.metadata().await?;
203        Ok(metadata.len())
204    }
205
206    async fn set_len(&self, size: u64) -> io::Result<()> {
207        self.inner.set_len(size).await
208    }
209}
210
211impl AsyncRead for TokioStorageFile {
212    fn poll_read(
213        mut self: Pin<&mut Self>,
214        cx: &mut Context<'_>,
215        buf: &mut ReadBuf<'_>,
216    ) -> Poll<io::Result<()>> {
217        Pin::new(&mut self.inner).poll_read(cx, buf)
218    }
219}
220
221impl AsyncWrite for TokioStorageFile {
222    fn poll_write(
223        mut self: Pin<&mut Self>,
224        cx: &mut Context<'_>,
225        buf: &[u8],
226    ) -> Poll<io::Result<usize>> {
227        Pin::new(&mut self.inner).poll_write(cx, buf)
228    }
229
230    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
231        Pin::new(&mut self.inner).poll_flush(cx)
232    }
233
234    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
235        Pin::new(&mut self.inner).poll_shutdown(cx)
236    }
237}
238
239impl AsyncSeek for TokioStorageFile {
240    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
241        Pin::new(&mut self.inner).start_seek(position)
242    }
243
244    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
245        Pin::new(&mut self.inner).poll_complete(cx)
246    }
247}