Skip to main content

greentic_bundle/catalog/
cache.rs

1use 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}