Skip to main content

asupersync/fs/
file.rs

1//! Async file implementation.
2//!
3//! This module provides async filesystem I/O by running blocking operations
4//! on a background thread via `spawn_blocking_io`. The file handle is wrapped
5//! in `Arc` to allow sharing across the async boundary.
6//!
7//! # Phase 0 Limitations
8//!
9//! The poll-based traits (`AsyncRead`, `AsyncWrite`, `AsyncSeek`) still use
10//! direct blocking I/O. Full async poll support requires reactor integration.
11
12#![allow(clippy::unused_async)]
13
14use crate::fs::OpenOptions;
15use crate::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
16use crate::runtime::spawn_blocking_io;
17use std::fs::{Metadata, Permissions};
18use std::io::{self, Read, Seek, SeekFrom, Write};
19use std::path::Path;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll};
23
24/// An open file on the filesystem.
25///
26/// The file handle is wrapped in `Arc` to allow sharing across
27/// `spawn_blocking_io` boundaries for async operations.
28#[derive(Debug)]
29pub struct File {
30    pub(crate) inner: Arc<std::fs::File>,
31}
32
33impl File {
34    /// Opens a file in read-only mode.
35    ///
36    /// See [`OpenOptions::open`] for more options.
37    pub async fn open(path: impl AsRef<Path>) -> io::Result<Self> {
38        let path = path.as_ref().to_owned();
39        let file = spawn_blocking_io(move || std::fs::File::open(&path)).await?;
40        Ok(Self {
41            inner: Arc::new(file),
42        })
43    }
44
45    /// Opens a file in write-only mode.
46    ///
47    /// This function will create a file if it does not exist, and will truncate it if it does.
48    pub async fn create(path: impl AsRef<Path>) -> io::Result<Self> {
49        let path = path.as_ref().to_owned();
50        let file = spawn_blocking_io(move || std::fs::File::create(&path)).await?;
51        Ok(Self {
52            inner: Arc::new(file),
53        })
54    }
55
56    /// Returns a new `OpenOptions` object.
57    #[must_use]
58    pub fn options() -> OpenOptions {
59        OpenOptions::new()
60    }
61
62    pub(crate) fn from_std(file: std::fs::File) -> Self {
63        Self {
64            inner: Arc::new(file),
65        }
66    }
67
68    /// Attempts to sync all OS-internal metadata to disk.
69    pub async fn sync_all(&self) -> io::Result<()> {
70        let inner = Arc::clone(&self.inner);
71        spawn_blocking_io(move || inner.sync_all()).await
72    }
73
74    /// This function is similar to `sync_all`, except that it will not sync file metadata.
75    pub async fn sync_data(&self) -> io::Result<()> {
76        let inner = Arc::clone(&self.inner);
77        spawn_blocking_io(move || inner.sync_data()).await
78    }
79
80    /// Truncates or extends the underlying file.
81    pub async fn set_len(&self, size: u64) -> io::Result<()> {
82        let inner = Arc::clone(&self.inner);
83        spawn_blocking_io(move || inner.set_len(size)).await
84    }
85
86    /// Queries metadata about the underlying file.
87    pub async fn metadata(&self) -> io::Result<Metadata> {
88        let inner = Arc::clone(&self.inner);
89        spawn_blocking_io(move || inner.metadata()).await
90    }
91
92    /// Creates a new `File` instance that shares the same underlying file handle.
93    pub async fn try_clone(&self) -> io::Result<Self> {
94        let inner = Arc::clone(&self.inner);
95        let file = spawn_blocking_io(move || inner.try_clone()).await?;
96        Ok(Self {
97            inner: Arc::new(file),
98        })
99    }
100
101    /// Changes the permissions on the underlying file.
102    pub async fn set_permissions(&self, perm: Permissions) -> io::Result<()> {
103        let inner = Arc::clone(&self.inner);
104        spawn_blocking_io(move || inner.set_permissions(perm)).await
105    }
106
107    // Helper methods that match std::fs::File but async
108    // Note: These require &mut self due to seek position state.
109    // Phase 0: Still blocking since we can't safely share mutable state.
110
111    /// Reads a number of bytes starting from a given offset.
112    /// Note: using seek + read
113    pub async fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
114        // Phase 0: Direct blocking. Requires reactor integration for true async.
115        Arc::get_mut(&mut self.inner)
116            .ok_or_else(|| io::Error::other("file handle is shared"))?
117            .seek(pos)
118    }
119
120    /// Gets the current stream position.
121    pub async fn stream_position(&mut self) -> io::Result<u64> {
122        // Phase 0: Direct blocking. Requires reactor integration for true async.
123        Arc::get_mut(&mut self.inner)
124            .ok_or_else(|| io::Error::other("file handle is shared"))?
125            .stream_position()
126    }
127
128    /// Rewinds the stream to the beginning.
129    pub async fn rewind(&mut self) -> io::Result<()> {
130        // Phase 0: Direct blocking. Requires reactor integration for true async.
131        Arc::get_mut(&mut self.inner)
132            .ok_or_else(|| io::Error::other("file handle is shared"))?
133            .rewind()
134    }
135}
136
137// Phase 0: Poll-based traits use direct blocking I/O.
138// True async support requires reactor integration to wake on readiness.
139
140impl AsyncRead for File {
141    fn poll_read(
142        mut self: Pin<&mut Self>,
143        _cx: &mut Context<'_>,
144        buf: &mut ReadBuf<'_>,
145    ) -> Poll<io::Result<()>> {
146        let inner = Arc::get_mut(&mut self.inner)
147            .ok_or_else(|| io::Error::other("file handle is shared during poll_read"))?;
148        let n = inner.read(buf.unfilled())?;
149        buf.advance(n);
150        Poll::Ready(Ok(()))
151    }
152}
153
154impl AsyncWrite for File {
155    fn poll_write(
156        mut self: Pin<&mut Self>,
157        _cx: &mut Context<'_>,
158        buf: &[u8],
159    ) -> Poll<io::Result<usize>> {
160        let inner = Arc::get_mut(&mut self.inner)
161            .ok_or_else(|| io::Error::other("file handle is shared during poll_write"))?;
162        let n = inner.write(buf)?;
163        Poll::Ready(Ok(n))
164    }
165
166    fn poll_flush(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
167        let inner = Arc::get_mut(&mut self.inner)
168            .ok_or_else(|| io::Error::other("file handle is shared during poll_flush"))?;
169        inner.flush()?;
170        Poll::Ready(Ok(()))
171    }
172
173    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
174        Poll::Ready(Ok(()))
175    }
176}
177
178impl AsyncSeek for File {
179    fn poll_seek(
180        mut self: Pin<&mut Self>,
181        _cx: &mut Context<'_>,
182        pos: SeekFrom,
183    ) -> Poll<io::Result<u64>> {
184        let inner = Arc::get_mut(&mut self.inner)
185            .ok_or_else(|| io::Error::other("file handle is shared during poll_seek"))?;
186        let n = inner.seek(pos)?;
187        Poll::Ready(Ok(n))
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::io::{AsyncReadExt, AsyncWriteExt}; // Extension traits for read_to_string etc
195    use tempfile::tempdir;
196
197    fn init_test(name: &str) {
198        crate::test_utils::init_test_logging();
199        crate::test_phase!(name);
200    }
201
202    #[test]
203    fn test_file_create_write_read() {
204        init_test("test_file_create_write_read");
205        // Phase 0 is synchronous; we use a simple block_on for async tests.
206
207        futures_lite::future::block_on(async {
208            let dir = tempdir().unwrap();
209            let path = dir.path().join("test.txt");
210
211            // Create and write
212            let mut file = File::create(&path).await.unwrap();
213            file.write_all(b"hello world").await.unwrap();
214            file.sync_all().await.unwrap();
215            drop(file);
216
217            // Read back
218            let mut file = File::open(&path).await.unwrap();
219            let mut contents = String::new();
220            file.read_to_string(&mut contents).await.unwrap();
221            crate::assert_with_log!(
222                contents == "hello world",
223                "contents",
224                "hello world",
225                contents
226            );
227        });
228        crate::test_complete!("test_file_create_write_read");
229    }
230
231    #[test]
232    fn test_file_seek() {
233        init_test("test_file_seek");
234        futures_lite::future::block_on(async {
235            let dir = tempdir().unwrap();
236            let path = dir.path().join("test_seek.txt");
237
238            let mut file = OpenOptions::new()
239                .read(true)
240                .write(true)
241                .create(true)
242                .open(&path)
243                .await
244                .unwrap();
245
246            file.write_all(b"0123456789").await.unwrap();
247
248            file.seek(SeekFrom::Start(5)).await.unwrap();
249            let mut buf = [0u8; 5];
250            file.read_exact(&mut buf).await.unwrap();
251            crate::assert_with_log!(&buf == b"56789", "seek contents", b"56789", buf);
252        });
253        crate::test_complete!("test_file_seek");
254    }
255
256    #[test]
257    fn test_file_metadata() {
258        init_test("test_file_metadata");
259        futures_lite::future::block_on(async {
260            let dir = tempdir().unwrap();
261            let path = dir.path().join("test_metadata.txt");
262
263            // Create file with known content
264            let mut file = File::create(&path).await.unwrap();
265            file.write_all(b"test content").await.unwrap();
266            file.sync_all().await.unwrap();
267            drop(file);
268
269            // Read metadata
270            let file = File::open(&path).await.unwrap();
271            let metadata = file.metadata().await.unwrap();
272
273            crate::assert_with_log!(metadata.is_file(), "is_file", true, metadata.is_file());
274            crate::assert_with_log!(metadata.len() == 12, "file length", 12u64, metadata.len());
275        });
276        crate::test_complete!("test_file_metadata");
277    }
278
279    #[test]
280    fn test_file_set_len() {
281        init_test("test_file_set_len");
282        futures_lite::future::block_on(async {
283            let dir = tempdir().unwrap();
284            let path = dir.path().join("test_truncate.txt");
285
286            // Create and write using async API
287            let mut file = File::create(&path).await.unwrap();
288            file.write_all(b"hello world").await.unwrap();
289            file.sync_all().await.unwrap();
290
291            // Truncate
292            file.set_len(5).await.unwrap();
293            file.sync_all().await.unwrap();
294            drop(file);
295
296            // Verify
297            let mut file = File::open(&path).await.unwrap();
298            let mut contents = String::new();
299            file.read_to_string(&mut contents).await.unwrap();
300            crate::assert_with_log!(contents == "hello", "truncated contents", "hello", contents);
301        });
302        crate::test_complete!("test_file_set_len");
303    }
304
305    #[test]
306    fn test_cancellation_safety_soft_cancel() {
307        // Test that dropping an in-flight file operation doesn't corrupt state.
308        // With spawn_blocking, the blocking op continues but result is discarded.
309        init_test("test_cancellation_safety_soft_cancel");
310        futures_lite::future::block_on(async {
311            let dir = tempdir().unwrap();
312            let path = dir.path().join("test_cancel.txt");
313
314            // Create file first
315            let file = File::create(&path).await.unwrap();
316            drop(file);
317
318            // Open the file - this should complete
319            let file = File::open(&path).await.unwrap();
320
321            // File should be usable after the operation completed
322            let metadata = file.metadata().await.unwrap();
323            crate::assert_with_log!(metadata.is_file(), "file exists", true, metadata.is_file());
324        });
325        crate::test_complete!("test_cancellation_safety_soft_cancel");
326    }
327}