cherenkov/model/index/
local.rs1use anyhow::{Context, Result, ensure};
2use sha2::{Digest, Sha256};
3use std::{io::Read, path::Path};
4
5pub(super) fn fingerprint(path: &Path) -> Result<String> {
8 let files = if path.is_file() {
9 vec![path.to_owned()]
10 } else {
11 let mut files = std::fs::read_dir(path)?
12 .map(|entry| entry.map(|e| e.path()))
13 .collect::<std::io::Result<Vec<_>>>()?;
14
15 files.retain(|p| {
16 p.is_file()
17 && p.extension().is_some_and(|e| {
18 matches!(
19 e.to_str(),
20 Some("json" | "jinja" | "safetensors" | "gguf" | "bin")
21 )
22 })
23 });
24 files.sort();
25
26 files
27 };
28 let mut hash = Sha256::new();
29
30 for file in files {
31 let meta = std::fs::metadata(&file)?;
32
33 hash.update(
34 file.file_name()
35 .context("missing filename")?
36 .as_encoded_bytes(),
37 );
38 hash.update(meta.len().to_le_bytes());
39 hash.update(
40 meta.modified()?
41 .duration_since(std::time::UNIX_EPOCH)?
42 .as_nanos()
43 .to_le_bytes(),
44 );
45
46 if file
47 .extension()
48 .is_some_and(|e| e == "json" || e == "jinja")
49 {
50 ensure!(
51 meta.len() <= 64 * 1024 * 1024,
52 "metadata file exceeds 64 MiB"
53 );
54
55 let mut reader = std::fs::File::open(file)?;
56 let mut buf = [0_u8; 65536];
57
58 loop {
59 let len = reader.read(&mut buf)?;
60
61 if len == 0 {
62 break;
63 }
64
65 hash.update(&buf[..len]);
66 }
67 }
68 }
69
70 Ok(hash
71 .finalize()
72 .iter()
73 .map(|byte| format!("{byte:02x}"))
74 .collect())
75}
76
77pub(super) fn copy_store(source: &Path, target: &Path, link: bool) -> Result<()> {
78 for entry in std::fs::read_dir(source)? {
81 let entry = entry?;
82
83 if !entry.path().is_file() {
84 continue;
85 }
86
87 let dest = target.join(entry.file_name());
88
89 if link && entry.path().extension().is_some_and(|e| e == "bin") {
90 std::fs::hard_link(entry.path(), &dest)
91 .or_else(|_| std::fs::copy(entry.path(), &dest).map(|_| ()))?;
92 } else {
93 std::fs::copy(entry.path(), dest)?;
94 }
95 }
96
97 Ok(())
98}