1use chrono::{DateTime, Duration, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use uuid::Uuid;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(into = "String", try_from = "String")]
9#[derive(utoipa::ToSchema)]
10#[schema(value_type=String, example="d7ef132b5d7711eea8c8000c29c33806")]
11pub struct SpoolId(Uuid);
12
13impl std::fmt::Display for SpoolId {
14 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
15 self.0.simple().fmt(fmt)
16 }
17}
18
19impl From<Uuid> for SpoolId {
20 fn from(uuid: Uuid) -> Self {
21 Self(uuid)
22 }
23}
24
25impl From<SpoolId> for String {
26 fn from(id: SpoolId) -> String {
27 id.to_string()
28 }
29}
30
31impl TryFrom<String> for SpoolId {
32 type Error = uuid::Error;
33
34 fn try_from(s: String) -> Result<Self, Self::Error> {
35 let uuid = Uuid::parse_str(&s)?;
36 Ok(Self(uuid))
37 }
38}
39
40impl Default for SpoolId {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl SpoolId {
47 pub fn new() -> Self {
48 Self(uuid_helper::now_v1())
51 }
52
53 pub fn compute_path(&self, in_dir: &Path) -> PathBuf {
54 let (a, b, c, [d, e, f, g, h, i, j, k]) = self.0.as_fields();
55 let [a1, a2, a3, a4] = a.to_be_bytes();
58 let name = format!(
59 "{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}"
60 );
61 in_dir.join(name)
62 }
63
64 pub fn as_bytes(&self) -> &[u8; 16] {
65 self.0.as_bytes()
66 }
67
68 pub fn from_slice(s: &[u8]) -> Option<Self> {
69 let uuid = Uuid::from_slice(s).ok()?;
70 Some(Self(uuid))
71 }
72
73 pub fn from_ascii_bytes(s: &[u8]) -> Option<Self> {
74 let uuid = Uuid::try_parse_ascii(s).ok()?;
75 Some(Self(uuid))
76 }
77
78 #[allow(clippy::should_implement_trait)]
79 pub fn from_str(s: &str) -> Option<Self> {
80 let uuid = Uuid::parse_str(s).ok()?;
81 Some(Self(uuid))
82 }
83
84 pub fn from_path(mut path: &Path) -> Option<Self> {
85 let mut components = vec![];
86
87 for _ in 0..5 {
88 components.push(path.file_name()?.to_str()?);
89 path = path.parent()?;
90 }
91
92 components.reverse();
93 Some(Self(Uuid::parse_str(&components.join("")).ok()?))
94 }
95
96 pub fn age(&self, now: DateTime<Utc>) -> Duration {
99 let created = self.created();
100 now - created
101 }
102
103 pub fn created(&self) -> DateTime<Utc> {
104 let (seconds, nanos) = self.0.get_timestamp().unwrap().to_unix();
105 Utc.timestamp_opt(seconds.try_into().unwrap(), nanos)
106 .unwrap()
107 }
108}
109
110#[cfg(test)]
111mod test {
112 use super::*;
113
114 #[test]
115 fn roundtrip_path() {
116 let id = SpoolId::new();
117 eprintln!("{id}");
118 let path = id.compute_path(Path::new("."));
119 let id2 = SpoolId::from_path(&path).unwrap();
120 assert_eq!(id, id2);
121 }
122
123 #[test]
124 fn roundtrip_bytes() {
125 let id = SpoolId::new();
126 eprintln!("{id}");
127 let bytes = id.as_bytes();
128 let id2 = SpoolId::from_slice(bytes.as_slice()).unwrap();
129 assert_eq!(id, id2);
130 }
131}