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::{Dirty, 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    dirty: Dirty,
87}
88
89impl<T> std::ops::Deref for File<T> {
90    type Target = T;
91    fn deref(&self) -> &T {
92        &self.value
93    }
94}
95
96impl<T> std::ops::DerefMut for File<T> {
97    fn deref_mut(&mut self) -> &mut T {
98        *self.dirty = true;
99        &mut self.value
100    }
101}
102
103/// Returns options for opening and writing to the file, creating it if it doesn't
104/// exist. On Unix, this restricts read and write permissions to the current user.
105// TODO(#1924): Implement better key management.
106// BUG(#2053): Use a separate lock file per staging file.
107fn open_options() -> fs_err::OpenOptions {
108    let mut options = fs_err::OpenOptions::new();
109    #[cfg(target_family = "unix")]
110    fs_err::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
111    options.create(true).read(true).write(true);
112    options
113}
114
115impl<T: serde::Serialize + serde::de::DeserializeOwned> File<T> {
116    /// Creates a new persistent file at `path` containing `value`.
117    pub fn new(path: &Path, value: T) -> Result<Self, Error> {
118        let this = Self {
119            _lock: Lock::new(
120                fs_err::OpenOptions::new()
121                    .read(true)
122                    .write(true)
123                    .create(true)
124                    .open(path)?,
125            )
126            .with_context(|| format!("locking path {}", path.display()))?,
127            path: path.into(),
128            value,
129            dirty: Dirty::new(true),
130        };
131        Ok(this)
132    }
133
134    /// Reads the value from a file at `path`, returning an error if it does not exist.
135    pub fn read(path: &Path) -> Result<Self, Error> {
136        Self::read_or_create(path, || {
137            Err(std::io::Error::new(
138                std::io::ErrorKind::NotFound,
139                format!("file is empty or does not exist: {}", path.display()),
140            )
141            .into())
142        })
143    }
144
145    /// Reads the value from a file at `path`, calling the `value` function to create it
146    /// if it does not exist. If it does exist, `value` will not be called.
147    pub fn read_or_create(
148        path: &Path,
149        value: impl FnOnce() -> Result<T, Error>,
150    ) -> Result<Self, Error> {
151        let lock = Lock::new(open_options().read(true).open(path)?)?;
152        let mut reader = io::BufReader::new(&lock.0);
153        let file_is_empty = reader.fill_buf()?.is_empty();
154
155        Ok(Self {
156            value: if file_is_empty {
157                value()?
158            } else {
159                serde_json::from_reader(reader)?
160            },
161            dirty: Dirty::new(file_is_empty),
162            path: path.into(),
163            _lock: lock,
164        })
165    }
166
167    fn save(&mut self) -> Result<(), Error> {
168        let mut temp_file_path = self.path.clone();
169        temp_file_path.set_extension("json.new");
170        let temp_file = open_options().open(&temp_file_path)?;
171        let mut temp_file_writer = std::io::BufWriter::new(temp_file);
172
173        let remove_temp_file = || fs_err::remove_file(&temp_file_path);
174
175        serde_json::to_writer_pretty(&mut temp_file_writer, &self.value)
176            .map_err(Error::from)
177            .or_cleanup(remove_temp_file)?;
178        temp_file_writer
179            .flush()
180            .map_err(Error::from)
181            .or_cleanup(remove_temp_file)?;
182        fs_err::rename(&temp_file_path, &self.path)?;
183        *self.dirty = false;
184        Ok(())
185    }
186}
187
188impl<T: serde::Serialize + serde::de::DeserializeOwned + Send> Persist for File<T> {
189    type Error = Error;
190
191    fn as_mut(&mut self) -> &mut T {
192        &mut self.value
193    }
194
195    /// Writes the value to disk.
196    ///
197    /// The contents of the file need to be over-written completely, so
198    /// a temporary file is created as a backup in case a crash occurs while
199    /// writing to disk.
200    ///
201    /// The temporary file is then renamed to the original filename. If
202    /// serialization or writing to disk fails, the temporary file is
203    /// deleted.
204    async fn persist(&mut self) -> Result<(), Error> {
205        self.save()
206    }
207
208    /// Takes the value out, releasing the lock on the persistent file.
209    fn into_value(self) -> T {
210        self.value
211    }
212}