1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10use uuid::Uuid;
11
12use crate::crypto::FipsCrypto;
13use crate::error::Result;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ModelVersion {
18 pub version: u32,
20
21 pub checkpoint_id: String,
23
24 pub timestamp: DateTime<Utc>,
26
27 pub parent_version: Option<u32>,
29
30 pub format: String,
32
33 pub size_bytes: u64,
35
36 pub compressed_size_bytes: u64,
38
39 pub checksum_sha256: String,
41
42 pub metadata: HashMap<String, String>,
44
45 pub file_path: String,
47}
48
49pub struct VersionControl {
62 vault_path: PathBuf,
63 version_file: PathBuf,
64 pub(crate) versions: HashMap<String, Vec<ModelVersion>>,
65}
66
67impl VersionControl {
68 const VERSION_FILE: &'static str = "versions.json";
69
70 pub fn new(vault_path: &Path) -> Result<Self> {
72 let version_file = vault_path.join(Self::VERSION_FILE);
73 let mut vc = Self {
74 vault_path: vault_path.to_path_buf(),
75 version_file,
76 versions: HashMap::new(),
77 };
78 vc.load_versions()?;
79 Ok(vc)
80 }
81
82 pub fn vault_path(&self) -> &std::path::Path {
84 &self.vault_path
85 }
86
87 fn load_versions(&mut self) -> Result<()> {
89 if self.version_file.exists() {
90 let contents = fs::read_to_string(&self.version_file)?;
91 self.versions = serde_json::from_str(&contents)?;
92 }
93 Ok(())
94 }
95
96 fn save_versions(&self) -> Result<()> {
98 let contents = serde_json::to_string_pretty(&self.versions)?;
99 fs::write(&self.version_file, contents)?;
100 crate::permissions::restrict_file(&self.version_file)?;
101
102 Ok(())
103 }
104
105 #[allow(clippy::too_many_arguments)]
107 pub fn add_version(
108 &mut self,
109 model_name: &str,
110 file_path: &str,
111 format: &str,
112 size_bytes: u64,
113 compressed_size_bytes: u64,
114 checksum: &str,
115 metadata: Option<HashMap<String, String>>,
116 parent_version: Option<u32>,
117 ) -> Result<ModelVersion> {
118 let versions = self.versions.entry(model_name.to_string()).or_default();
119
120 let version = if versions.is_empty() {
122 1
123 } else {
124 versions.iter().map(|v| v.version).max().unwrap_or(0) + 1
125 };
126
127 let timestamp = Utc::now();
128 let checkpoint_id = Self::generate_checkpoint_id(model_name, version, ×tamp);
129
130 let model_version = ModelVersion {
131 version,
132 checkpoint_id,
133 timestamp,
134 parent_version,
135 format: format.to_string(),
136 size_bytes,
137 compressed_size_bytes,
138 checksum_sha256: checksum.to_string(),
139 metadata: metadata.unwrap_or_default(),
140 file_path: file_path.to_string(),
141 };
142
143 versions.push(model_version.clone());
144 self.save_versions()?;
145
146 Ok(model_version)
147 }
148
149 pub fn get_version(&self, model_name: &str, version: Option<u32>) -> Option<&ModelVersion> {
151 let versions = self.versions.get(model_name)?;
152
153 if versions.is_empty() {
154 return None;
155 }
156
157 if let Some(v) = version {
158 versions.iter().find(|mv| mv.version == v)
159 } else {
160 versions.iter().max_by_key(|mv| mv.version)
162 }
163 }
164
165 pub fn list_versions(&self, model_name: &str) -> Vec<&ModelVersion> {
167 self.versions
168 .get(model_name)
169 .map(|v| {
170 let mut sorted: Vec<&ModelVersion> = v.iter().collect();
171 sorted.sort_by_key(|mv| mv.version);
172 sorted
173 })
174 .unwrap_or_default()
175 }
176
177 pub fn get_lineage(&self, model_name: &str, version: u32) -> Vec<&ModelVersion> {
179 let mut lineage = Vec::new();
180
181 if let Some(mut current) = self.get_version(model_name, Some(version)) {
182 lineage.push(current);
183
184 while let Some(parent_ver) = current.parent_version {
185 if let Some(parent) = self.get_version(model_name, Some(parent_ver)) {
186 lineage.insert(0, parent);
187 current = parent;
188 } else {
189 break;
190 }
191 }
192 }
193
194 lineage
195 }
196
197 pub fn delete_version(&mut self, model_name: &str, version: u32) -> Result<bool> {
199 if let Some(versions) = self.versions.get_mut(model_name) {
200 let original_len = versions.len();
201 versions.retain(|v| v.version != version);
202
203 if versions.len() < original_len {
204 self.save_versions()?;
205 return Ok(true);
206 }
207 }
208 Ok(false)
209 }
210
211 pub fn cleanup_old_versions(
213 &mut self,
214 model_name: &str,
215 keep_count: usize,
216 ) -> Result<Vec<u32>> {
217 let Some(versions) = self.versions.get_mut(model_name) else {
218 return Ok(Vec::new());
219 };
220
221 if versions.len() <= keep_count {
222 return Ok(Vec::new());
223 }
224
225 versions.sort_by_key(|v| std::cmp::Reverse(v.version));
227
228 let to_delete: Vec<u32> = versions
230 .iter()
231 .skip(keep_count)
232 .map(|v| v.version)
233 .collect();
234
235 versions.truncate(keep_count);
236 self.save_versions()?;
237
238 Ok(to_delete)
239 }
240
241 pub fn verify_checksum(&self, model_name: &str, version: u32, data: &[u8]) -> bool {
243 if let Some(model_version) = self.get_version(model_name, Some(version)) {
244 let checksum = hex::encode(FipsCrypto::hash_sha256(data));
245 return checksum == model_version.checksum_sha256;
246 }
247 false
248 }
249
250 pub fn update_metadata(
252 &mut self,
253 model_name: &str,
254 version: u32,
255 key: &str,
256 value: String,
257 ) -> Result<()> {
258 if let Some(versions) = self.versions.get_mut(model_name) {
259 if let Some(model_version) = versions.iter_mut().find(|v| v.version == version) {
260 model_version.metadata.insert(key.to_string(), value);
261 self.save_versions()?;
262 return Ok(());
263 }
264 }
265 Err(crate::error::VaultError::VersionNotFound(
266 version,
267 model_name.to_string(),
268 ))
269 }
270
271 pub fn get_metadata(&self, model_name: &str, version: u32, key: &str) -> Option<String> {
273 self.get_version(model_name, Some(version))
274 .and_then(|v| v.metadata.get(key).cloned())
275 }
276
277 pub fn list_models_owned(&self) -> Vec<String> {
280 self.versions.keys().cloned().collect()
281 }
282
283 pub fn import_version(&mut self, model_name: &str, version: ModelVersion) -> Result<()> {
286 self.versions
287 .entry(model_name.to_string())
288 .or_default()
289 .push(version);
290 self.save_versions()
291 }
292
293 fn generate_checkpoint_id(
295 model_name: &str,
296 version: u32,
297 _timestamp: &DateTime<Utc>,
298 ) -> String {
299 let uuid = Uuid::new_v4();
300 format!("{}-v{}-{}", model_name, version, uuid)
301 }
302}
303
304impl crate::traits::VersionRepo for VersionControl {
307 fn add_version(
308 &mut self,
309 model: &str,
310 file_path: &str,
311 format: &str,
312 size_bytes: u64,
313 compressed_size_bytes: u64,
314 checksum: &str,
315 metadata: Option<HashMap<String, String>>,
316 parent_version: Option<u32>,
317 ) -> Result<ModelVersion> {
318 VersionControl::add_version(
319 self,
320 model,
321 file_path,
322 format,
323 size_bytes,
324 compressed_size_bytes,
325 checksum,
326 metadata,
327 parent_version,
328 )
329 }
330
331 fn get_version(&self, model: &str, version: Option<u32>) -> Option<&ModelVersion> {
332 VersionControl::get_version(self, model, version)
333 }
334
335 fn list_versions(&self, model: &str) -> Vec<&ModelVersion> {
336 VersionControl::list_versions(self, model)
337 }
338
339 fn get_lineage(&self, model: &str, version: u32) -> Vec<&ModelVersion> {
340 VersionControl::get_lineage(self, model, version)
341 }
342
343 fn delete_version(&mut self, model: &str, version: u32) -> Result<bool> {
344 VersionControl::delete_version(self, model, version)
345 }
346
347 fn cleanup_old_versions(&mut self, model: &str, keep_count: usize) -> Result<Vec<u32>> {
348 VersionControl::cleanup_old_versions(self, model, keep_count)
349 }
350
351 fn verify_checksum(&self, model: &str, version: u32, data: &[u8]) -> bool {
352 VersionControl::verify_checksum(self, model, version, data)
353 }
354
355 fn update_metadata(
356 &mut self,
357 model: &str,
358 version: u32,
359 key: &str,
360 value: String,
361 ) -> Result<()> {
362 VersionControl::update_metadata(self, model, version, key, value)
363 }
364
365 fn get_metadata(&self, model: &str, version: u32, key: &str) -> Option<String> {
366 VersionControl::get_metadata(self, model, version, key)
367 }
368
369 fn list_models(&self) -> Vec<String> {
370 self.versions.keys().cloned().collect()
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use tempfile::tempdir;
378
379 #[test]
380 fn test_version_control() {
381 let temp_dir = tempdir().unwrap();
382 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
383
384 let v1 = vc
385 .add_version(
386 "test_model",
387 "test_file.enc",
388 "pytorch",
389 1000,
390 500,
391 "abc123",
392 None,
393 None,
394 )
395 .unwrap();
396
397 assert_eq!(v1.version, 1);
398 assert_eq!(v1.format, "pytorch");
399
400 let v2 = vc
401 .add_version(
402 "test_model",
403 "test_file2.enc",
404 "pytorch",
405 2000,
406 1000,
407 "def456",
408 None,
409 Some(1),
410 )
411 .unwrap();
412
413 assert_eq!(v2.version, 2);
414 assert_eq!(v2.parent_version, Some(1));
415
416 let lineage = vc.get_lineage("test_model", 2);
417 assert_eq!(lineage.len(), 2);
418 }
419
420 #[test]
421 fn test_version_control_list_versions() {
422 let temp_dir = tempdir().unwrap();
424 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
425
426 vc.add_version("m", "f1.enc", "pt", 100, 50, "c1", None, None)
427 .unwrap();
428 vc.add_version("m", "f2.enc", "pt", 200, 100, "c2", None, Some(1))
429 .unwrap();
430 vc.add_version("m", "f3.enc", "pt", 300, 150, "c3", None, Some(2))
431 .unwrap();
432
433 let versions = vc.list_versions("m");
434 assert_eq!(versions.len(), 3);
435 assert_eq!(versions[0].version, 1);
436 assert_eq!(versions[2].version, 3);
437
438 let empty = vc.list_versions("nonexistent");
440 assert!(empty.is_empty());
441 }
442
443 #[test]
444 fn test_version_control_reopen() {
445 let temp_dir = tempdir().unwrap();
447 {
448 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
449 vc.add_version("m", "f1.enc", "pt", 100, 50, "c1", None, None)
450 .unwrap();
451 }
452 let vc2 = VersionControl::new(temp_dir.path()).unwrap();
453 assert_eq!(vc2.list_versions("m").len(), 1);
454 }
455
456 #[test]
457 fn test_delete_version() {
458 let temp_dir = tempdir().unwrap();
459 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
460 vc.add_version("m", "f1.enc", "pt", 100, 50, "c1", None, None)
461 .unwrap();
462 vc.add_version("m", "f2.enc", "pt", 200, 100, "c2", None, Some(1))
463 .unwrap();
464
465 let deleted = vc.delete_version("m", 1).unwrap();
466 assert!(deleted);
467 assert_eq!(vc.list_versions("m").len(), 1);
468
469 let not_deleted = vc.delete_version("m", 99).unwrap();
471 assert!(!not_deleted);
472
473 let not_deleted2 = vc.delete_version("nonexistent", 1).unwrap();
475 assert!(!not_deleted2);
476 }
477
478 #[test]
479 fn test_cleanup_old_versions() {
480 let temp_dir = tempdir().unwrap();
481 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
482 for i in 0..5 {
483 vc.add_version(
484 "m",
485 &format!("f{}.enc", i),
486 "pt",
487 100,
488 50,
489 &format!("c{}", i),
490 None,
491 if i == 0 { None } else { Some(i as u32) },
492 )
493 .unwrap();
494 }
495 assert_eq!(vc.list_versions("m").len(), 5);
496
497 let removed = vc.cleanup_old_versions("m", 2).unwrap();
499 assert_eq!(removed.len(), 3);
500 assert_eq!(vc.list_versions("m").len(), 2);
501
502 let removed2 = vc.cleanup_old_versions("m", 10).unwrap();
504 assert!(removed2.is_empty());
505
506 let removed3 = vc.cleanup_old_versions("nonexistent", 1).unwrap();
508 assert!(removed3.is_empty());
509 }
510
511 #[test]
512 fn test_verify_checksum() {
513 let temp_dir = tempdir().unwrap();
514 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
515 let data = b"hello world";
516 let checksum = hex::encode(crate::crypto::FipsCrypto::hash_sha256(data));
517 vc.add_version(
518 "m",
519 "f.enc",
520 "pt",
521 data.len() as u64,
522 data.len() as u64,
523 &checksum,
524 None,
525 None,
526 )
527 .unwrap();
528
529 assert!(vc.verify_checksum("m", 1, data));
530 assert!(!vc.verify_checksum("m", 1, b"wrong data"));
531 assert!(!vc.verify_checksum("m", 99, data));
532 assert!(!vc.verify_checksum("nonexistent", 1, data));
533 }
534
535 #[test]
536 fn test_update_and_get_metadata() {
537 let temp_dir = tempdir().unwrap();
538 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
539 vc.add_version("m", "f.enc", "pt", 100, 50, "c1", None, None)
540 .unwrap();
541
542 vc.update_metadata("m", 1, "author", "Alice".to_string())
543 .unwrap();
544 assert_eq!(vc.get_metadata("m", 1, "author"), Some("Alice".to_string()));
545
546 assert_eq!(vc.get_metadata("m", 1, "missing_key"), None);
548
549 let err = vc.update_metadata("m", 99, "key", "val".to_string());
551 assert!(err.is_err());
552
553 let err2 = vc.update_metadata("nonexistent", 1, "key", "val".to_string());
555 assert!(err2.is_err());
556 }
557
558 #[test]
559 fn test_get_lineage_chain() {
560 let temp_dir = tempdir().unwrap();
561 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
562 vc.add_version("m", "f1.enc", "pt", 100, 50, "c1", None, None)
563 .unwrap();
564 vc.add_version("m", "f2.enc", "pt", 200, 100, "c2", None, Some(1))
565 .unwrap();
566 vc.add_version("m", "f3.enc", "pt", 300, 150, "c3", None, Some(2))
567 .unwrap();
568
569 let lineage = vc.get_lineage("m", 3);
570 assert_eq!(lineage.len(), 3);
571 assert_eq!(lineage[0].version, 1); assert_eq!(lineage[2].version, 3); let empty = vc.get_lineage("m", 99);
576 assert!(empty.is_empty());
577
578 let empty2 = vc.get_lineage("nonexistent", 1);
580 assert!(empty2.is_empty());
581 }
582
583 #[test]
584 fn test_add_version_with_metadata() {
585 let temp_dir = tempdir().unwrap();
586 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
587 let mut meta = std::collections::HashMap::new();
588 meta.insert("framework".to_string(), "pytorch".to_string());
589 let v = vc
590 .add_version("m", "f.enc", "pt", 100, 50, "c1", Some(meta), None)
591 .unwrap();
592 assert_eq!(v.metadata.get("framework"), Some(&"pytorch".to_string()));
593 }
594
595 #[test]
596 fn test_version_repo_trait_forwarding() {
597 use crate::traits::VersionRepo;
599 let temp_dir = tempdir().unwrap();
600 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
601
602 let v = VersionRepo::add_version(
604 &mut vc,
605 "m",
606 "f.enc",
607 "pt",
608 100,
609 50,
610 "checksum1",
611 None,
612 None,
613 )
614 .unwrap();
615 assert_eq!(v.version, 1);
616
617 let got = VersionRepo::get_version(&vc, "m", Some(1));
619 assert!(got.is_some());
620 assert_eq!(got.unwrap().version, 1);
621
622 let list = VersionRepo::list_versions(&vc, "m");
624 assert_eq!(list.len(), 1);
625
626 let lineage = VersionRepo::get_lineage(&vc, "m", 1);
628 assert_eq!(lineage.len(), 1);
629
630 let valid = VersionRepo::verify_checksum(&vc, "m", 1, b"dummy");
632 assert!(!valid); VersionRepo::update_metadata(&mut vc, "m", 1, "key1", "val1".into()).unwrap();
636
637 let val = VersionRepo::get_metadata(&vc, "m", 1, "key1");
639 assert_eq!(val, Some("val1".into()));
640
641 let deleted = VersionRepo::delete_version(&mut vc, "m", 1).unwrap();
643 assert!(deleted);
644
645 for i in 0..5 {
647 VersionRepo::add_version(
648 &mut vc,
649 "m2",
650 &format!("f{}.enc", i),
651 "pt",
652 100,
653 50,
654 &format!("c{}", i),
655 None,
656 None,
657 )
658 .unwrap();
659 }
660 let cleaned = VersionRepo::cleanup_old_versions(&mut vc, "m2", 2).unwrap();
661 assert_eq!(cleaned.len(), 3);
662
663 let models = VersionRepo::list_models(&vc);
665 assert!(models.contains(&"m2".to_string()));
666 }
667
668 #[test]
669 fn test_vault_path_method() {
670 let temp_dir = tempdir().unwrap();
672 let vc = VersionControl::new(temp_dir.path()).unwrap();
673 assert_eq!(vc.vault_path(), temp_dir.path());
674 }
675
676 #[test]
677 fn test_load_existing_versions() {
678 let temp_dir = tempdir().unwrap();
680 {
681 let mut vc = VersionControl::new(temp_dir.path()).unwrap();
682 vc.add_version("m", "f.enc", "pt", 100, 50, "c1", None, None)
683 .unwrap();
684 }
685 let vc2 = VersionControl::new(temp_dir.path()).unwrap();
687 let versions = vc2.list_versions("m");
688 assert_eq!(versions.len(), 1);
689 }
690}