redb/tree_store/page_store/file_backend/
optimized.rs

1use crate::{DatabaseError, Result, StorageBackend};
2use std::fs::{File, TryLockError};
3use std::io;
4
5#[cfg(feature = "logging")]
6use log::warn;
7
8#[cfg(unix)]
9use std::os::unix::fs::FileExt;
10
11#[cfg(windows)]
12use std::os::windows::fs::FileExt;
13
14/// Stores a database as a file on-disk.
15#[derive(Debug)]
16pub struct FileBackend {
17    lock_supported: bool,
18    file: File,
19}
20
21impl FileBackend {
22    /// Creates a new backend which stores data to the given file.
23    pub fn new(file: File) -> Result<Self, DatabaseError> {
24        Self::new_internal(file, false)
25    }
26
27    pub(crate) fn new_internal(file: File, read_only: bool) -> Result<Self, DatabaseError> {
28        let result = if read_only {
29            file.try_lock_shared()
30        } else {
31            file.try_lock()
32        };
33
34        match result {
35            Ok(()) => Ok(Self {
36                file,
37                lock_supported: true,
38            }),
39            Err(TryLockError::WouldBlock) => Err(DatabaseError::DatabaseAlreadyOpen),
40            Err(TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => {
41                #[cfg(feature = "logging")]
42                warn!(
43                    "File locks not supported on this platform. You must ensure that only a single process opens the database file, at a time"
44                );
45
46                Ok(Self {
47                    file,
48                    lock_supported: false,
49                })
50            }
51            Err(TryLockError::Error(err)) => Err(err.into()),
52        }
53    }
54}
55
56impl StorageBackend for FileBackend {
57    fn len(&self) -> Result<u64, io::Error> {
58        Ok(self.file.metadata()?.len())
59    }
60
61    #[cfg(unix)]
62    fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), io::Error> {
63        self.file.read_exact_at(out, offset)?;
64        Ok(())
65    }
66
67    #[cfg(target_os = "wasi")]
68    fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), io::Error> {
69        read_exact_at(&self.file, out, offset)?;
70        Ok(())
71    }
72
73    #[cfg(windows)]
74    fn read(&self, mut offset: u64, out: &mut [u8]) -> Result<(), io::Error> {
75        let mut data_offset = 0;
76        while data_offset < out.len() {
77            let read = self.file.seek_read(&mut out[data_offset..], offset)?;
78            offset += read as u64;
79            data_offset += read;
80        }
81        Ok(())
82    }
83
84    fn set_len(&self, len: u64) -> Result<(), io::Error> {
85        self.file.set_len(len)
86    }
87
88    fn sync_data(&self) -> Result<(), io::Error> {
89        self.file.sync_data()
90    }
91
92    #[cfg(unix)]
93    fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> {
94        self.file.write_all_at(data, offset)
95    }
96
97    #[cfg(target_os = "wasi")]
98    fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> {
99        write_all_at(&self.file, data, offset)
100    }
101
102    #[cfg(windows)]
103    fn write(&self, mut offset: u64, data: &[u8]) -> Result<(), io::Error> {
104        let mut data_offset = 0;
105        while data_offset < data.len() {
106            let written = self.file.seek_write(&data[data_offset..], offset)?;
107            offset += written as u64;
108            data_offset += written;
109        }
110        Ok(())
111    }
112
113    fn close(&self) -> Result<(), io::Error> {
114        if self.lock_supported {
115            self.file.unlock()?;
116        }
117
118        Ok(())
119    }
120}
121
122// TODO: replace these with wasi::FileExt when https://github.com/rust-lang/rust/issues/71213
123// is stable
124#[cfg(target_os = "wasi")]
125fn read_exact_at(file: &File, mut buf: &mut [u8], mut offset: u64) -> io::Result<()> {
126    use std::os::fd::AsRawFd;
127
128    while !buf.is_empty() {
129        let nbytes = unsafe {
130            libc::pread(
131                file.as_raw_fd(),
132                buf.as_mut_ptr() as _,
133                std::cmp::min(buf.len(), libc::ssize_t::MAX as _),
134                offset as _,
135            )
136        };
137        match nbytes {
138            0 => break,
139            -1 => match io::Error::last_os_error() {
140                err if err.kind() == io::ErrorKind::Interrupted => {}
141                err => return Err(err),
142            },
143            n => {
144                let tmp = buf;
145                buf = &mut tmp[n as usize..];
146                offset += n as u64;
147            }
148        }
149    }
150    if !buf.is_empty() {
151        Err(io::Error::new(
152            io::ErrorKind::UnexpectedEof,
153            "failed to fill whole buffer",
154        ))
155    } else {
156        Ok(())
157    }
158}
159
160#[cfg(target_os = "wasi")]
161fn write_all_at(file: &File, mut buf: &[u8], mut offset: u64) -> io::Result<()> {
162    use std::os::fd::AsRawFd;
163
164    while !buf.is_empty() {
165        let nbytes = unsafe {
166            libc::pwrite(
167                file.as_raw_fd(),
168                buf.as_ptr() as _,
169                std::cmp::min(buf.len(), libc::ssize_t::MAX as _),
170                offset as _,
171            )
172        };
173        match nbytes {
174            0 => {
175                return Err(io::Error::new(
176                    io::ErrorKind::WriteZero,
177                    "failed to write whole buffer",
178                ));
179            }
180            -1 => match io::Error::last_os_error() {
181                err if err.kind() == io::ErrorKind::Interrupted => {}
182                err => return Err(err),
183            },
184            n => {
185                buf = &buf[n as usize..];
186                offset += n as u64
187            }
188        }
189    }
190    Ok(())
191}