Skip to main content

made_client/
progress_checkpoint.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4use tokio::io::AsyncWriteExt;
5use uuid::Uuid;
6
7use crate::MadeClientError;
8
9/// The only durable local console state: a per-ceremony G6 resume coordinate.
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11pub struct ProgressCheckpoint {
12    ceremony_id: String,
13    after_sequence: u64,
14}
15
16impl ProgressCheckpoint {
17    pub fn new(ceremony_id: impl Into<String>, after_sequence: u64) -> Self {
18        Self {
19            ceremony_id: ceremony_id.into(),
20            after_sequence,
21        }
22    }
23
24    #[must_use]
25    pub fn ceremony_id(&self) -> &str {
26        &self.ceremony_id
27    }
28
29    #[must_use]
30    pub fn after_sequence(&self) -> u64 {
31        self.after_sequence
32    }
33
34    pub async fn load(path: &Path, expected_ceremony_id: &str) -> Result<Self, MadeClientError> {
35        let bytes = tokio::fs::read(path)
36            .await
37            .map_err(|error| MadeClientError::io(path, error))?;
38        let checkpoint: Self = serde_json::from_slice(&bytes)
39            .map_err(|error| MadeClientError::CursorCheckpointCorrupt(error.to_string()))?;
40        if checkpoint.ceremony_id != expected_ceremony_id {
41            return Err(MadeClientError::CursorScopeMismatch {
42                expected: expected_ceremony_id.to_owned(),
43                actual: checkpoint.ceremony_id,
44            });
45        }
46        Ok(checkpoint)
47    }
48
49    pub async fn save(&self, path: &Path) -> Result<(), MadeClientError> {
50        let parent = path.parent().unwrap_or_else(|| Path::new("."));
51        tokio::fs::create_dir_all(parent)
52            .await
53            .map_err(|error| MadeClientError::io(parent, error))?;
54        let name = path
55            .file_name()
56            .and_then(|name| name.to_str())
57            .unwrap_or("made-cursor");
58        let temporary = parent.join(format!(".{name}.{}.part", Uuid::new_v4()));
59        let payload = serde_json::to_vec(self)
60            .map_err(|error| MadeClientError::CursorCheckpointCorrupt(error.to_string()))?;
61        let result = async {
62            let mut file = tokio::fs::File::create(&temporary)
63                .await
64                .map_err(|error| MadeClientError::io(&temporary, error))?;
65            file.write_all(&payload)
66                .await
67                .map_err(|error| MadeClientError::io(&temporary, error))?;
68            file.sync_all()
69                .await
70                .map_err(|error| MadeClientError::io(&temporary, error))?;
71            drop(file);
72            tokio::fs::rename(&temporary, path)
73                .await
74                .map_err(|error| MadeClientError::io(path, error))
75        }
76        .await;
77        if result.is_err() {
78            let _ = tokio::fs::remove_file(&temporary).await;
79        }
80        result
81    }
82}