1use chrono::{DateTime, Duration, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5use uuid::{ClockSequence, ContextV1, Timestamp, Uuid};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(into = "String", try_from = "String")]
10#[derive(utoipa::ToSchema)]
11#[schema(value_type=String, example="d7ef132b5d7711eea8c8000c29c33806")]
12pub struct SpoolId(Uuid);
13
14impl std::fmt::Display for SpoolId {
15 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
16 self.0.simple().fmt(fmt)
17 }
18}
19
20impl From<Uuid> for SpoolId {
21 fn from(uuid: Uuid) -> Self {
22 Self(uuid)
23 }
24}
25
26impl From<SpoolId> for String {
27 fn from(id: SpoolId) -> String {
28 id.to_string()
29 }
30}
31
32impl TryFrom<String> for SpoolId {
33 type Error = uuid::Error;
34
35 fn try_from(s: String) -> Result<Self, Self::Error> {
36 let uuid = Uuid::parse_str(&s)?;
37 Ok(Self(uuid))
38 }
39}
40
41impl Default for SpoolId {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl SpoolId {
48 pub fn new() -> Self {
49 Self(uuid_helper::now_v1())
52 }
53
54 pub fn compute_path(&self, in_dir: &Path) -> PathBuf {
55 let (a, b, c, [d, e, f, g, h, i, j, k]) = self.0.as_fields();
56 let [a1, a2, a3, a4] = a.to_be_bytes();
59 let name = format!(
60 "{a1:02x}/{a2:02x}/{a3:02x}/{a4:02x}/{b:04x}{c:04x}{d:02x}{e:02x}{f:02x}{g:02x}{h:02x}{i:02x}{j:02x}{k:02x}"
61 );
62 in_dir.join(name)
63 }
64
65 pub fn as_bytes(&self) -> &[u8; 16] {
66 self.0.as_bytes()
67 }
68
69 pub fn from_slice(s: &[u8]) -> Option<Self> {
70 let uuid = Uuid::from_slice(s).ok()?;
71 Some(Self(uuid))
72 }
73
74 pub fn from_ascii_bytes(s: &[u8]) -> Option<Self> {
75 let uuid = Uuid::try_parse_ascii(s).ok()?;
76 Some(Self(uuid))
77 }
78
79 #[allow(clippy::should_implement_trait)]
80 pub fn from_str(s: &str) -> Option<Self> {
81 let uuid = Uuid::parse_str(s).ok()?;
82 Some(Self(uuid))
83 }
84
85 pub fn from_path(mut path: &Path) -> Option<Self> {
86 let mut components = vec![];
87
88 for _ in 0..5 {
89 components.push(path.file_name()?.to_str()?);
90 path = path.parent()?;
91 }
92
93 components.reverse();
94 Some(Self(Uuid::parse_str(&components.join("")).ok()?))
95 }
96
97 pub fn age(&self, now: DateTime<Utc>) -> Duration {
100 let created = self.created();
101 now - created
102 }
103
104 pub fn created(&self) -> DateTime<Utc> {
105 let (seconds, nanos) = self.0.get_timestamp().unwrap().to_unix();
106 Utc.timestamp_opt(seconds.try_into().unwrap(), nanos)
107 .unwrap()
108 }
109
110 pub fn derive_new_with_cloned_timestamp(&self) -> Self {
118 let ts = self.0.get_timestamp().unwrap();
119
120 let candidate = Self(uuid_helper::new_v1(ts));
121
122 if candidate != *self {
123 return candidate;
124 }
125
126 static CONTEXT: LazyLock<ContextV1> = LazyLock::new(ContextV1::new_random);
136
137 let (mut seconds, mut subsec_nanos) = ts.to_unix();
138 loop {
139 let (counter, secs, nanos) = CONTEXT.generate_timestamp_sequence(seconds, subsec_nanos);
140 seconds = secs;
141 subsec_nanos = nanos;
142
143 let ts = Timestamp::from_unix_time(
144 seconds,
145 subsec_nanos,
146 counter.into(),
147 CONTEXT.usable_bits() as u8,
148 );
149
150 let candidate = Self(uuid_helper::new_v1(ts));
151
152 if candidate != *self {
153 return candidate;
154 }
155 }
156 }
157}
158
159#[cfg(test)]
160mod test {
161 use super::*;
162
163 #[test]
164 fn roundtrip_path() {
165 let id = SpoolId::new();
166 eprintln!("{id}");
167 let path = id.compute_path(Path::new("."));
168 let id2 = SpoolId::from_path(&path).unwrap();
169 assert_eq!(id, id2);
170 }
171
172 #[test]
173 fn roundtrip_bytes() {
174 let id = SpoolId::new();
175 eprintln!("{id}");
176 let bytes = id.as_bytes();
177 let id2 = SpoolId::from_slice(bytes.as_slice()).unwrap();
178 assert_eq!(id, id2);
179 }
180
181 #[test]
182 fn cloned_timestamp_preserves_time() {
183 let id = SpoolId::new();
187 let derived = id.derive_new_with_cloned_timestamp();
188 assert_ne!(id, derived);
189
190 let delta = (derived.created() - id.created()).abs();
191 assert!(
192 delta < Duration::seconds(1),
193 "derived timestamp {} should be close to source {}, delta {delta:?}",
194 derived.created(),
195 id.created()
196 );
197 }
198}