1use serde::{Deserialize, Serialize};
4
5use crate::error::AppError;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum BaseVersion {
12 Version(i32),
14 Sha256(String),
16}
17
18impl BaseVersion {
19 pub fn from_json(value: &serde_json::Value) -> Result<BaseVersion, AppError> {
21 let invalid = || {
22 AppError::BadRequest(
23 "A base must be a version number or the SHA-256 of the checked-out content.".into(),
24 )
25 };
26 match value {
27 serde_json::Value::Number(n) => n
28 .as_i64()
29 .and_then(|v| i32::try_from(v).ok())
30 .filter(|v| *v >= 0)
31 .map(BaseVersion::Version)
32 .ok_or_else(invalid),
33 serde_json::Value::String(s) => BaseVersion::parse(s).ok_or_else(invalid),
34 _ => Err(invalid()),
35 }
36 }
37
38 pub fn parse(raw: &str) -> Option<BaseVersion> {
39 let raw = raw.trim();
40 let raw = raw.strip_prefix('v').unwrap_or(raw);
41 if raw.len() == 64 && raw.bytes().all(|b| b.is_ascii_hexdigit()) {
42 return Some(BaseVersion::Sha256(raw.to_ascii_lowercase()));
43 }
44 if !raw.is_empty() && raw.len() <= 9 && raw.bytes().all(|b| b.is_ascii_digit()) {
45 return raw.parse().ok().map(BaseVersion::Version);
46 }
47 None
48 }
49}
50
51#[derive(Clone, Copy, Debug)]
53pub struct ServerFile<'a> {
54 pub version: i32,
55 pub sha256: &'a str,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum BaseConflict {
61 Moved,
63 AddedMeanwhile,
65 Removed,
67}
68
69pub fn check_base(
72 base: &BaseVersion,
73 current: Option<ServerFile<'_>>,
74 upload_sha256: &str,
75) -> Result<(), BaseConflict> {
76 match (base, current) {
77 (_, Some(cur)) if cur.sha256 == upload_sha256 => Ok(()),
78 (BaseVersion::Version(0), None) => Ok(()),
79 (BaseVersion::Version(0), Some(_)) => Err(BaseConflict::AddedMeanwhile),
80 (_, None) => Err(BaseConflict::Removed),
81 (BaseVersion::Version(v), Some(cur)) if *v == cur.version => Ok(()),
82 (BaseVersion::Sha256(s), Some(cur)) if s.eq_ignore_ascii_case(cur.sha256) => Ok(()),
83 (_, Some(_)) => Err(BaseConflict::Moved),
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum FileSyncState {
91 UpToDate,
93 Modified,
95 Outdated,
97 Conflict,
100 New,
102 Missing,
104}
105
106impl FileSyncState {
107 pub fn as_str(self) -> &'static str {
108 match self {
109 FileSyncState::UpToDate => "up_to_date",
110 FileSyncState::Modified => "modified",
111 FileSyncState::Outdated => "outdated",
112 FileSyncState::Conflict => "conflict",
113 FileSyncState::New => "new",
114 FileSyncState::Missing => "missing",
115 }
116 }
117}
118
119pub fn sync_state(
127 current: Option<ServerFile<'_>>,
128 history: &[(i32, &str)],
129 local_sha256: Option<&str>,
130 base: Option<&BaseVersion>,
131) -> FileSyncState {
132 let (cur, local) = match (current, local_sha256) {
133 (None, None) => return FileSyncState::Missing,
134 (None, Some(_)) => return FileSyncState::New,
135 (Some(_), None) => return FileSyncState::Missing,
136 (Some(cur), Some(local)) => (cur, local.to_ascii_lowercase()),
137 };
138 if cur.sha256.eq_ignore_ascii_case(&local) {
139 return FileSyncState::UpToDate;
140 }
141 let base_sha = match base {
142 Some(BaseVersion::Sha256(s)) => Some(s.to_ascii_lowercase()),
143 Some(BaseVersion::Version(v)) => history
144 .iter()
145 .find(|(n, _)| n == v)
146 .map(|(_, s)| s.to_ascii_lowercase()),
147 None => None,
148 };
149 match base_sha {
150 Some(base) if base.eq_ignore_ascii_case(cur.sha256) => FileSyncState::Modified,
151 Some(base) if base == local => FileSyncState::Outdated,
152 Some(_) => FileSyncState::Conflict,
153 None if base.is_some() => FileSyncState::Conflict,
156 None if history.iter().any(|(_, s)| s.eq_ignore_ascii_case(&local)) => {
157 FileSyncState::Outdated
158 }
159 None => FileSyncState::Modified,
160 }
161}
162
163pub fn format_age(seconds: i64) -> String {
165 let s = seconds.max(0);
166 match s {
167 0..=59 => "just now".into(),
168 60..=3_599 => format!("{} min", s / 60),
169 3_600..=86_399 => format!("{} h", s / 3_600),
170 86_400..=172_799 => "1 day".into(),
171 _ => format!("{} days", s / 86_400),
172 }
173}