linera_persistent/
file.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    io::{self, BufRead as _, Write as _},
6    path::Path,
7};
8
9use fs4::FileExt;
10use thiserror_context::Context;
11
12use super::Persist;
13
14/// A guard that keeps an exclusive lock on a file.
15struct Lock(fs_err::File);
16
17#[derive(Debug, thiserror::Error)]
18enum ErrorInner {
19    #[error("I/O error: {0}")]
20    IoError(#[from] std::io::Error),
21    #[error("JSON error: {0}")]
22    JsonError(#[from] serde_json::Error),
23}
24
25thiserror_context::impl_context!(Error(ErrorInner));
26
27/// Utility: run a fallible cleanup function if an operation failed, attaching the
28/// original operation as context to its error.
29trait CleanupExt {
30    type Ok;
31    type Error;
32
33    fn or_cleanup<E>(self, f: impl FnOnce() -> Result<(), E>) -> Result<Self::Ok, Self::Error>
34    where
35        E: Into<Self::Error>,
36        Result<(), E>: Context<Self::Error, Self::Ok, E>;
37}
38
39impl<T, W> CleanupExt for Result<T, W>
40where
41    W: std::fmt::Display + Send + Sync + 'static,
42{
43    type Ok = T;
44    type Error = W;
45
46    fn or_cleanup<E>(self, cleanup: impl FnOnce() -> Result<(), E>) -> Self
47    where
48        E: Into<W>,
49        Result<(), E>: Context<W, T, E>,
50    {
51        self.or_else(|error| {
52            if let Err(cleanup_error) = cleanup() {
53                Err(cleanup_error).context(error)
54            } else {
55                Err(error)
56            }
57        })
58    }
59}
60
61impl Lock {
62    /// Acquires an exclusive lock on a provided `file`, returning a [`Lock`] which will
63    /// release the lock when dropped.
64    pub fn new(file: fs_err::File) -> std::io::Result<Self> {
65        file.file().try_lock_exclusive()?;
66        Ok(Lock(file))
67    }
68}
69
70impl Drop for Lock {
71    fn drop(&mut self) {
72        if let Err(error) = FileExt::unlock(self.0.file()) {
73            tracing::warn!("Failed to unlock wallet file: {error}");
74        }
75    }
76}
77
78/// An implementation of [`Persist`] based on an atomically-updated file at a given path.
79/// An exclusive lock is taken using `flock(2)` to ensure that concurrent updates cannot
80/// happen, and writes are saved to a staging file before being moved over the old file,
81/// an operation that is atomic on all Unixes.
82pub struct File<T> {
83    _lock: Lock,
84    path: std::path::PathBuf,
85    value: T,
86}
87
88impl<T> std::ops::Deref for File<T> {
89    type Target = T;
90    fn deref(&self) -> &T {
91        &self.value
92    }
93}
94
95impl<T> std::ops::DerefMut for File<T> {
96    fn deref_mut(&mut self) -> &mut T {
97        &mut self.value
98    }
99}
100
101/// Returns options for opening and writing to the file, creating it if it doesn't
102/// exist. On Unix, this restricts read and write permissions to the current user.
103// TODO(#1924): Implement better key management.
104// BUG(#2053): Use a separate lock file per staging file.
105fn open_options() -> fs_err::OpenOptions {
106    let mut options = fs_err::OpenOptions::new();
107    #[cfg(target_family = "unix")]
108    fs_err::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
109    options.create(true).read(true).write(true);
110    options
111}
112
113impl<T: serde::Serialize + serde::de::DeserializeOwned> File<T> {
114    /// Creates a new persistent file at `path` containing `value`.
115    pub fn new(path: &Path, value: T) -> Result<Self, Error> {
116        let this = Self {
117            _lock: Lock::new(
118                fs_err::OpenOptions::new()
119                    .read(true)
120                    .write(true)
121                    .create(true)
122                    .open(path)?,
123            )
124            .with_context(|| format!("locking path {}", path.display()))?,
125            path: path.into(),
126            value,
127        };
128        this.save()?;
129        Ok(this)
130    }
131
132    /// Reads the value from a file at `path`, returning an error if it does not exist.
133    pub fn read(path: &Path) -> Result<Self, Error> {
134        Self::read_or_create(path, || {
135            Err(std::io::Error::new(
136                std::io::ErrorKind::NotFound,
137                format!("file is empty or does not exist: {}", path.display()),
138            )
139            .into())
140        })
141    }
142
143    /// Reads the value from a file at `path`, calling the `value` function to create it
144    /// if it does not exist. If it does exist, `value` will not be called.
145    pub fn read_or_create(
146        path: &Path,
147        value: impl FnOnce() -> Result<T, Error>,
148    ) -> Result<Self, Error> {
149        let lock = Lock::new(open_options().read(true).open(path)?)?;
150        let mut reader = io::BufReader::new(&lock.0);
151        let file_is_empty = reader.fill_buf()?.is_empty();
152
153        let me = Self {
154            value: if file_is_empty {
155                value()?
156            } else {
157                serde_json::from_reader(reader)?
158            },
159            path: path.into(),
160            _lock: lock,
161        };
162
163        me.save()?;
164
165        Ok(me)
166    }
167
168    pub fn save(&self) -> Result<(), Error> {
169        let mut temp_file_path = self.path.clone();
170        temp_file_path.set_extension("json.new");
171        let temp_file = open_options().open(&temp_file_path)?;
172        let mut temp_file_writer = std::io::BufWriter::new(temp_file);
173
174        let remove_temp_file = || fs_err::remove_file(&temp_file_path);
175
176        serde_json::to_writer_pretty(&mut temp_file_writer, &self.value)
177            .map_err(Error::from)
178            .or_cleanup(remove_temp_file)?;
179        temp_file_writer
180            .flush()
181            .map_err(Error::from)
182            .or_cleanup(remove_temp_file)?;
183        drop(temp_file_writer);
184        fs_err::rename(&temp_file_path, &self.path)?;
185        Ok(())
186    }
187}
188
189impl<T: serde::Serialize + serde::de::DeserializeOwned + Send> Persist for File<T> {
190    type Error = Error;
191
192    fn as_mut(&mut self) -> &mut T {
193        &mut self.value
194    }
195
196    /// Writes the value to disk.
197    ///
198    /// The contents of the file need to be over-written completely, so
199    /// a temporary file is created as a backup in case a crash occurs while
200    /// writing to disk.
201    ///
202    /// The temporary file is then renamed to the original filename. If
203    /// serialization or writing to disk fails, the temporary file is
204    /// deleted.
205    fn persist(&mut self) -> impl std::future::Future<Output = Result<(), Error>> {
206        let result = self.save();
207        async { result }
208    }
209
210    /// Takes the value out, releasing the lock on the persistent file.
211    fn into_value(self) -> T {
212        self.value
213    }
214}