1use std::collections::HashSet;
2
3use iroh_blobs::Hash;
4use iroh_docs::NamespaceId;
5
6use crate::{Result, SyncwebError, storage::node_db::NodeDatabase};
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
10#[non_exhaustive]
11pub enum EntryStatus {
12 #[default]
13 Completed,
14 Failed,
15 Skipped,
16}
17
18impl EntryStatus {
19 #[must_use]
20 pub const fn as_str(self) -> &'static str {
21 match self {
22 Self::Completed => "completed",
23 Self::Failed => "failed",
24 Self::Skipped => "skipped",
25 }
26 }
27}
28
29impl TryFrom<&str> for EntryStatus {
30 type Error = SyncwebError;
31
32 fn try_from(value: &str) -> Result<Self> {
33 match value {
34 "completed" => Ok(Self::Completed),
35 "failed" => Ok(Self::Failed),
36 "skipped" => Ok(Self::Skipped),
37 other => Err(SyncwebError::InvalidConfig(format!("invalid entry status: {other}"))),
38 }
39 }
40}
41
42#[derive(Clone, Debug)]
48pub struct SyncCheckpoint {
49 namespace_id: NamespaceId,
50 session_id: String,
51 database: NodeDatabase,
52}
53
54#[derive(Clone, Debug)]
56#[non_exhaustive]
57pub struct EntryProgress {
58 pub entry_key: Vec<u8>,
59 pub hash: Hash,
60 pub size: u64,
61 pub status: String,
62 pub retries: u32,
63 pub error_message: Option<String>,
64}
65
66#[derive(Clone, Copy, Debug)]
68#[non_exhaustive]
69pub struct CheckpointProgress {
70 pub total: usize,
71 pub completed: usize,
72 pub failed: usize,
73 pub skipped: usize,
74 pub pending: usize,
75 pub bytes_transferred: u64,
76 pub bytes_total: Option<u64>,
77 pub percentage: f64,
78}
79
80impl SyncCheckpoint {
81 #[must_use]
83 pub fn new(database: &NodeDatabase, namespace_id: NamespaceId) -> Self {
84 let session_id = uuid::Uuid::new_v4().to_string();
85 Self {
86 namespace_id,
87 session_id,
88 database: database.clone(),
89 }
90 }
91
92 #[must_use]
94 pub fn session_id(&self) -> &str {
95 &self.session_id
96 }
97
98 #[must_use]
100 pub const fn namespace_id(&self) -> NamespaceId {
101 self.namespace_id
102 }
103
104 pub fn create_session(database: &NodeDatabase, namespace_id: NamespaceId) -> Result<Self> {
110 let cp = Self::new(database, namespace_id);
111 database.create_sync_checkpoint(&cp.namespace_id.to_string(), &cp.session_id)?;
112 Ok(cp)
113 }
114
115 pub fn mark_completed(&self, entry_key: &[u8], hash: Hash, size: u64) -> Result<()> {
121 self.database
122 .upsert_sync_entry(&crate::storage::node_db::SyncEntryParams {
123 namespace_id: &self.namespace_id.to_string(),
124 session_id: &self.session_id,
125 entry_key,
126 hash: &hash.to_string().into_bytes(),
127 size,
128 status: EntryStatus::Completed.as_str(),
129 retries: 0,
130 error_message: None,
131 })
132 }
133
134 pub fn mark_failed(&self, entry_key: &[u8], error: &str) -> Result<()> {
140 self.database
141 .upsert_sync_entry(&crate::storage::node_db::SyncEntryParams {
142 namespace_id: &self.namespace_id.to_string(),
143 session_id: &self.session_id,
144 entry_key,
145 hash: &[],
146 size: 0,
147 status: EntryStatus::Failed.as_str(),
148 retries: 0,
149 error_message: Some(error),
150 })
151 }
152
153 pub fn mark_skipped(&self, entry_key: &[u8], hash: Hash, size: u64) -> Result<()> {
159 self.database
160 .upsert_sync_entry(&crate::storage::node_db::SyncEntryParams {
161 namespace_id: &self.namespace_id.to_string(),
162 session_id: &self.session_id,
163 entry_key,
164 hash: &hash.to_string().into_bytes(),
165 size,
166 status: EntryStatus::Skipped.as_str(),
167 retries: 0,
168 error_message: None,
169 })
170 }
171
172 pub fn completed_entries(&self) -> Result<Vec<EntryProgress>> {
178 self.database.list_sync_entries(
179 &self.namespace_id.to_string(),
180 &self.session_id,
181 EntryStatus::Completed.as_str(),
182 )
183 }
184
185 pub fn failed_entries(&self) -> Result<Vec<EntryProgress>> {
191 self.database.list_sync_entries(
192 &self.namespace_id.to_string(),
193 &self.session_id,
194 EntryStatus::Failed.as_str(),
195 )
196 }
197
198 pub fn completed_keys(&self) -> Result<HashSet<Vec<u8>>> {
204 let entries = self.completed_entries()?;
205 Ok(entries.into_iter().map(|e| e.entry_key).collect())
206 }
207
208 pub fn progress(&self) -> Result<CheckpointProgress> {
214 self.database
215 .get_checkpoint_progress(&self.namespace_id.to_string(), &self.session_id)
216 }
217
218 pub fn complete(&self) -> Result<()> {
224 self.database
225 .update_checkpoint_status(&self.namespace_id.to_string(), &self.session_id, "completed")
226 }
227
228 pub const fn leave_unfinished(&self) -> Result<()> {
237 Ok(())
239 }
240
241 pub fn delete(&self) -> Result<()> {
247 self.database
248 .delete_sync_checkpoint(&self.namespace_id.to_string(), &self.session_id)
249 }
250
251 pub fn resume(database: &NodeDatabase, namespace_id: NamespaceId) -> Result<Option<Self>> {
259 let ns = namespace_id.to_string();
260 Ok(database.find_unfinished_checkpoint(&ns)?.map(|sid| Self {
261 namespace_id,
262 session_id: sid,
263 database: database.clone(),
264 }))
265 }
266}