backbone_bucket/domain/entity/
content_hash.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::StorageBackend;
7use super::AuditMetadata;
8
9use super::*;
10
11use thiserror::Error;
12
13#[derive(Debug, Clone, Error)]
15pub enum DedupError {
16 #[error("{0}")]
17 Message(String),
18
19 #[error("Not found: {0}")]
20 NotFound(String),
21
22 #[error("Validation failed: {0}")]
23 ValidationFailed(String),
24
25 #[error("Conflict: {0}")]
26 Conflict(String),
27}
28
29impl From<String> for DedupError {
30 fn from(msg: String) -> Self { Self::Message(msg) }
31}
32
33impl From<&str> for DedupError {
34 fn from(msg: &str) -> Self { Self::Message(msg.to_string()) }
35}
36
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct ContentHashId(pub Uuid);
42
43impl ContentHashId {
44 pub fn new(id: Uuid) -> Self { Self(id) }
45 pub fn generate() -> Self { Self(Uuid::new_v4()) }
46 pub fn into_inner(self) -> Uuid { self.0 }
47}
48
49impl std::fmt::Display for ContentHashId {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(f, "{}", self.0)
52 }
53}
54
55impl std::str::FromStr for ContentHashId {
56 type Err = uuid::Error;
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 Ok(Self(Uuid::parse_str(s)?))
59 }
60}
61
62impl From<Uuid> for ContentHashId {
63 fn from(id: Uuid) -> Self { Self(id) }
64}
65
66impl From<ContentHashId> for Uuid {
67 fn from(id: ContentHashId) -> Self { id.0 }
68}
69
70impl AsRef<Uuid> for ContentHashId {
71 fn as_ref(&self) -> &Uuid { &self.0 }
72}
73
74impl std::ops::Deref for ContentHashId {
75 type Target = Uuid;
76 fn deref(&self) -> &Self::Target { &self.0 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
80pub struct ContentHash {
81 pub id: Uuid,
82 pub hash: String,
83 pub size_bytes: i64,
84 pub storage_key: String,
85 pub storage_backend: StorageBackend,
86 pub reference_count: i32,
87 pub first_uploaded_at: DateTime<Utc>,
88 pub last_referenced_at: DateTime<Utc>,
89 pub fingerprint: Option<String>,
90 #[serde(default)]
91 #[sqlx(json)]
92 pub metadata: AuditMetadata,
93}
94
95impl ContentHash {
96 pub fn builder() -> ContentHashBuilder {
98 <ContentHashBuilder as Default>::default()
99 }
100
101 pub fn new(hash: String, size_bytes: i64, storage_key: String, storage_backend: StorageBackend, reference_count: i32, first_uploaded_at: DateTime<Utc>, last_referenced_at: DateTime<Utc>) -> Self {
103 Self {
104 id: Uuid::new_v4(),
105 hash,
106 size_bytes,
107 storage_key,
108 storage_backend,
109 reference_count,
110 first_uploaded_at,
111 last_referenced_at,
112 fingerprint: None,
113 metadata: AuditMetadata::default(),
114 }
115 }
116
117 pub fn id(&self) -> &Uuid {
119 &self.id
120 }
121
122 pub fn typed_id(&self) -> ContentHashId {
124 ContentHashId(self.id)
125 }
126
127 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
129 self.metadata.created_at.as_ref()
130 }
131
132 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
134 self.metadata.updated_at.as_ref()
135 }
136
137 pub fn is_deleted(&self) -> bool {
139 self.metadata.deleted_at.is_some()
140 }
141
142 pub fn is_active(&self) -> bool {
144 self.metadata.deleted_at.is_none()
145 }
146
147 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
149 self.metadata.deleted_at.as_ref()
150 }
151
152 pub fn created_by(&self) -> Option<&Uuid> {
154 self.metadata.created_by.as_ref()
155 }
156
157 pub fn updated_by(&self) -> Option<&Uuid> {
159 self.metadata.updated_by.as_ref()
160 }
161
162 pub fn deleted_by(&self) -> Option<&Uuid> {
164 self.metadata.deleted_by.as_ref()
165 }
166
167
168 pub fn with_fingerprint(mut self, value: String) -> Self {
174 self.fingerprint = Some(value);
175 self
176 }
177
178 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
184 for (key, value) in fields {
185 match key.as_str() {
186 "hash" => {
187 if let Ok(v) = serde_json::from_value(value) { self.hash = v; }
188 }
189 "size_bytes" => {
190 if let Ok(v) = serde_json::from_value(value) { self.size_bytes = v; }
191 }
192 "storage_key" => {
193 if let Ok(v) = serde_json::from_value(value) { self.storage_key = v; }
194 }
195 "storage_backend" => {
196 if let Ok(v) = serde_json::from_value(value) { self.storage_backend = v; }
197 }
198 "reference_count" => {
199 if let Ok(v) = serde_json::from_value(value) { self.reference_count = v; }
200 }
201 "first_uploaded_at" => {
202 if let Ok(v) = serde_json::from_value(value) { self.first_uploaded_at = v; }
203 }
204 "last_referenced_at" => {
205 if let Ok(v) = serde_json::from_value(value) { self.last_referenced_at = v; }
206 }
207 "fingerprint" => {
208 if let Ok(v) = serde_json::from_value(value) { self.fingerprint = v; }
209 }
210 _ => {} }
212 }
213 }
214
215}
216
217impl super::Entity for ContentHash {
218 type Id = Uuid;
219
220 fn entity_id(&self) -> &Self::Id {
221 &self.id
222 }
223
224 fn entity_type() -> &'static str {
225 "ContentHash"
226 }
227}
228
229impl backbone_core::PersistentEntity for ContentHash {
230 fn entity_id(&self) -> String {
231 self.id.to_string()
232 }
233 fn set_entity_id(&mut self, id: String) {
234 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
235 self.id = uuid;
236 }
237 }
238 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
239 self.metadata.created_at
240 }
241 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
242 self.metadata.created_at = Some(ts);
243 }
244 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
245 self.metadata.updated_at
246 }
247 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
248 self.metadata.updated_at = Some(ts);
249 }
250 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
251 self.metadata.deleted_at
252 }
253 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
254 self.metadata.deleted_at = ts;
255 }
256}
257
258impl backbone_orm::EntityRepoMeta for ContentHash {
259 fn column_types() -> std::collections::HashMap<String, String> {
260 let mut m = std::collections::HashMap::new();
261 m.insert("id".to_string(), "uuid".to_string());
262 m.insert("storage_backend".to_string(), "storage_backend".to_string());
263 m.insert("first_uploaded_at".to_string(), "timestamptz".to_string());
264 m.insert("last_referenced_at".to_string(), "timestamptz".to_string());
265 m
266 }
267 fn search_fields() -> &'static [&'static str] {
268 &["hash", "storage_key"]
269 }
270}
271
272#[derive(Debug, Clone, Default)]
277pub struct ContentHashBuilder {
278 hash: Option<String>,
279 size_bytes: Option<i64>,
280 storage_key: Option<String>,
281 storage_backend: Option<StorageBackend>,
282 reference_count: Option<i32>,
283 first_uploaded_at: Option<DateTime<Utc>>,
284 last_referenced_at: Option<DateTime<Utc>>,
285 fingerprint: Option<String>,
286}
287
288impl ContentHashBuilder {
289 pub fn hash(mut self, value: String) -> Self {
291 self.hash = Some(value);
292 self
293 }
294
295 pub fn size_bytes(mut self, value: i64) -> Self {
297 self.size_bytes = Some(value);
298 self
299 }
300
301 pub fn storage_key(mut self, value: String) -> Self {
303 self.storage_key = Some(value);
304 self
305 }
306
307 pub fn storage_backend(mut self, value: StorageBackend) -> Self {
309 self.storage_backend = Some(value);
310 self
311 }
312
313 pub fn reference_count(mut self, value: i32) -> Self {
315 self.reference_count = Some(value);
316 self
317 }
318
319 pub fn first_uploaded_at(mut self, value: DateTime<Utc>) -> Self {
321 self.first_uploaded_at = Some(value);
322 self
323 }
324
325 pub fn last_referenced_at(mut self, value: DateTime<Utc>) -> Self {
327 self.last_referenced_at = Some(value);
328 self
329 }
330
331 pub fn fingerprint(mut self, value: String) -> Self {
333 self.fingerprint = Some(value);
334 self
335 }
336
337 pub fn build(self) -> Result<ContentHash, String> {
341 let hash = self.hash.ok_or_else(|| "hash is required".to_string())?;
342 let size_bytes = self.size_bytes.ok_or_else(|| "size_bytes is required".to_string())?;
343 let storage_key = self.storage_key.ok_or_else(|| "storage_key is required".to_string())?;
344
345 Ok(ContentHash {
346 id: Uuid::new_v4(),
347 hash,
348 size_bytes,
349 storage_key,
350 storage_backend: self.storage_backend.unwrap_or_default(),
351 reference_count: self.reference_count.unwrap_or(1),
352 first_uploaded_at: self.first_uploaded_at.unwrap_or(Utc::now()),
353 last_referenced_at: self.last_referenced_at.unwrap_or(Utc::now()),
354 fingerprint: self.fingerprint,
355 metadata: AuditMetadata::default(),
356 })
357 }
358}
359
360#[path = "content_hash.ext.rs"]
362mod content_hash_ext;
363pub use content_hash_ext::*;