greentic_bundle/catalog/
cache.rs1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6
7use super::CACHE_ROOT_DIR;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
10pub struct CatalogCacheIndex {
11 #[serde(default)]
12 pub refs: BTreeMap<String, String>,
13}
14
15pub fn ref_cache_path(root: &Path, reference: &str) -> PathBuf {
16 root.join(CACHE_ROOT_DIR)
17 .join("by-ref")
18 .join(format!("{}.json", slug(reference)))
19}
20
21pub fn digest_cache_path(root: &Path, digest: &str) -> PathBuf {
22 root.join(CACHE_ROOT_DIR)
23 .join("by-digest")
24 .join(format!("{digest}.json"))
25}
26
27pub fn index_path(root: &Path) -> PathBuf {
28 root.join(CACHE_ROOT_DIR).join("index.json")
29}
30
31pub fn load_index(root: &Path) -> Result<CatalogCacheIndex> {
32 let path = index_path(root);
33 if !path.exists() {
34 return Ok(CatalogCacheIndex::default());
35 }
36 let raw = std::fs::read_to_string(&path)
37 .with_context(|| format!("read catalog cache index {}", path.display()))?;
38 serde_json::from_str(&raw)
39 .with_context(|| format!("parse catalog cache index {}", path.display()))
40}
41
42pub fn write_index(root: &Path, index: &CatalogCacheIndex) -> Result<()> {
43 let path = index_path(root);
44 if let Some(parent) = path.parent() {
45 std::fs::create_dir_all(parent)?;
46 }
47 std::fs::write(&path, format!("{}\n", serde_json::to_string_pretty(index)?))
48 .with_context(|| format!("write catalog cache index {}", path.display()))?;
49 Ok(())
50}
51
52pub fn cache_catalog_bytes(
53 root: &Path,
54 reference: &str,
55 digest: &str,
56 bytes: &[u8],
57) -> Result<Vec<PathBuf>> {
58 let digest_path = digest_cache_path(root, digest);
59 let ref_path = ref_cache_path(root, reference);
60 for path in [&digest_path, &ref_path] {
61 if let Some(parent) = path.parent() {
62 std::fs::create_dir_all(parent)?;
63 }
64 std::fs::write(path, bytes).with_context(|| format!("write {}", path.display()))?;
65 }
66
67 let mut index = load_index(root)?;
68 index.refs.insert(reference.to_string(), digest.to_string());
69 write_index(root, &index)?;
70
71 Ok(vec![digest_path, ref_path, index_path(root)])
72}
73
74pub fn resolve_cached_path(root: &Path, reference: &str) -> Result<Option<PathBuf>> {
75 let by_ref = ref_cache_path(root, reference);
76 if by_ref.exists() {
77 return Ok(Some(by_ref));
78 }
79 let index = load_index(root)?;
80 let Some(digest) = index.refs.get(reference) else {
81 return Ok(None);
82 };
83 let by_digest = digest_cache_path(root, digest);
84 if by_digest.exists() {
85 return Ok(Some(by_digest));
86 }
87 Ok(None)
88}
89
90pub fn slug(value: &str) -> String {
91 let mut out = String::new();
92 let mut prev_dash = false;
93 for ch in value.chars() {
94 if ch.is_ascii_alphanumeric() {
95 out.push(ch.to_ascii_lowercase());
96 prev_dash = false;
97 } else if !prev_dash {
98 out.push('-');
99 prev_dash = true;
100 }
101 }
102 let out = out.trim_matches('-').to_string();
103 if out.is_empty() {
104 "catalog".to_string()
105 } else {
106 out
107 }
108}