git_bug/replica/entity/lamport/
persistent.rs

1// git-bug-rs - A rust library for interfacing with git-bug repositories
2//
3// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
4// SPDX-License-Identifier: GPL-3.0-or-later
5//
6// This file is part of git-bug-rs/git-gub.
7//
8// You should have received a copy of the License along with this program.
9// If not, see <https://www.gnu.org/licenses/agpl.txt>.
10
11//! A persistent [`Clock`] implementation.
12
13use std::{
14    fs::{File, OpenOptions},
15    io::{Read, Write},
16    path::{Path, PathBuf},
17};
18
19use super::{Clock, mem::MemClock};
20
21/// A [`Clock`] implementation, that can be persisted to
22/// disk, via the  [`from_path`][`PersistedClock::from_path`] or
23/// [`save_to_file`][`PersistedClock::save_to_file`] functions.
24#[derive(Debug)]
25pub struct PersistedClock {
26    clock: MemClock,
27    file_path: PathBuf,
28}
29
30impl Clock for PersistedClock {
31    type IncrementError = write::Error;
32    type WitnessError = write::Error;
33
34    fn time(&self) -> super::Time {
35        self.clock.time()
36    }
37
38    fn increment(&mut self) -> Result<super::Time, Self::IncrementError> {
39        let previous = self.clock.increment().expect("This error is infallible");
40        self.save_to_file(false).map(|()| previous)
41    }
42
43    fn witness(&mut self, time: super::Time) -> Result<(), Self::WitnessError> {
44        self.clock.witness(time).expect("The error is infallible");
45        self.save_to_file(false)
46    }
47}
48
49/// The Error returned by [`PersistedClock`].
50pub mod write {
51    #![allow(missing_docs)]
52    use std::{io, num::ParseIntError, path::PathBuf};
53
54    #[derive(Debug, thiserror::Error)]
55    /// The Error returned by [`PersistedClock`][`super::PersistedClock`] write operations.
56    pub enum Error {
57        #[error("Failed to write this clock to {path}, because of {error}")]
58        /// Failed to write clock
59        Write { path: PathBuf, error: io::Error },
60
61        #[error("Failed to open the path '{path}' for clock, because of {error}")]
62        /// Failed to open the path to the clock
63        Open { path: PathBuf, error: io::Error },
64
65        #[error("Failed to read the contents of the path {path} for clock, because of {error}")]
66        /// Failed to read to clock from file
67        Read { path: PathBuf, error: io::Error },
68
69        #[error(
70            "Failed to parse the contents ('{contents}') of the path {path} as clock value, \
71             because of {error}"
72        )]
73        /// Failed to parse the clock representation on disk
74        Parse {
75            path: PathBuf,
76            error: ParseIntError,
77            contents: String,
78        },
79    }
80}
81
82impl PersistedClock {
83    fn open_file(path: &Path, options: &mut OpenOptions) -> Result<File, write::Error> {
84        options.open(path).map_err(|err| write::Error::Open {
85            error: err,
86            path: path.to_owned(),
87        })
88    }
89
90    /// Create a new [`PersistedClock`].
91    /// This will override the contents of `path`.
92    /// If you want to re-load a previously persisted clock, use
93    /// [`PersistedClock::from_path`].
94    ///
95    /// # Errors
96    /// If the `path` cannot be opened for writing.
97    pub fn new(path: PathBuf) -> Result<Self, write::Error> {
98        let mut me = Self {
99            clock: MemClock::new(),
100            file_path: path,
101        };
102
103        me.save_to_file(true)?;
104
105        Ok(me)
106    }
107
108    /// Loads this clock from a previously persisted clock.
109    ///
110    /// # Errors
111    /// If the `path` cannot be opened for reading and writing.
112    pub fn from_path(path: PathBuf) -> Result<Self, write::Error> {
113        let me = Self {
114            clock: MemClock::new_with_value(Self::time_from_file(&path)?),
115            file_path: path,
116        };
117
118        Ok(me)
119    }
120
121    fn time_from_file(path: &Path) -> Result<u64, write::Error> {
122        let mut file = Self::open_file(path, OpenOptions::new().read(true))?;
123
124        let mut contents = String::new();
125        file.read_to_string(&mut contents)
126            .map_err(|err| write::Error::Read {
127                error: err,
128                path: path.to_owned(),
129            })?;
130
131        let value: u64 = contents.parse().map_err(|err| write::Error::Parse {
132            error: err,
133            path: path.to_owned(),
134            contents,
135        })?;
136
137        Ok(value)
138    }
139
140    /// Save this clock to disk.
141    ///
142    /// # Note
143    /// The file needs to be opened in read-write mode, because we check that
144    /// the file's content reflect our clock value.
145    ///
146    /// # Errors
147    /// If the IO operations fail.
148    ///
149    /// # Panics
150    /// If our time is behind the files stored time.
151    pub fn save_to_file(&mut self, create: bool) -> Result<(), write::Error> {
152        if self.file_path.exists() {
153            assert!(
154                Self::time_from_file(&self.file_path)? <= self.time().0,
155                "The time should have not been changed under our feet."
156            );
157        }
158
159        let mut file = Self::open_file(
160            &self.file_path,
161            OpenOptions::new().write(true).create(create),
162        )?;
163
164        write!(file, "{}", self.time().0).map_err(|err| write::Error::Write {
165            error: err,
166            path: self.file_path.clone(),
167        })
168    }
169}