greentic_component/store/
mod.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4#[cfg(not(feature = "oci"))]
5use anyhow::bail;
6use anyhow::{Context, Result, anyhow};
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9use tracing::instrument;
10
11use self::cache::Cache;
12use crate::path_safety::normalize_under_root;
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct ComponentId(pub String);
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum ComponentLocator {
19 Fs { path: PathBuf },
20 Oci { reference: String },
21}
22
23#[derive(Clone, Debug)]
24pub struct ComponentBytes {
25 pub id: ComponentId,
26 pub bytes: Bytes,
27 pub meta: MetaInfo,
28}
29
30pub type SourceId = String;
31
32#[derive(Clone, Debug)]
33pub struct ComponentStore {
34 sources: HashMap<SourceId, ComponentLocator>,
35 cache: Cache,
36 compat: CompatPolicy,
37}
38
39impl Default for ComponentStore {
40 fn default() -> Self {
41 Self::with_cache_dir(None, CompatPolicy::default())
42 }
43}
44
45impl ComponentStore {
46 pub fn with_cache_dir(cache_dir: Option<PathBuf>, compat: CompatPolicy) -> Self {
47 Self {
48 sources: HashMap::new(),
49 cache: Cache::new(cache_dir),
50 compat,
51 }
52 }
53
54 pub fn add_fs(&mut self, id: impl Into<SourceId>, path: impl Into<PathBuf>) -> &mut Self {
55 self.sources
56 .insert(id.into(), ComponentLocator::Fs { path: path.into() });
57 self
58 }
59
60 pub fn add_oci(&mut self, id: impl Into<SourceId>, reference: impl Into<String>) -> &mut Self {
61 self.sources.insert(
62 id.into(),
63 ComponentLocator::Oci {
64 reference: reference.into(),
65 },
66 );
67 self
68 }
69
70 #[instrument(level = "trace", skip_all, fields(source = %source_id))]
71 pub async fn get(&self, source_id: &str) -> Result<ComponentBytes> {
72 let loc = self
73 .sources
74 .get(source_id)
75 .ok_or_else(|| anyhow!("unknown source id: {source_id}"))?;
76
77 if let Some(hit) = self.cache.try_load(loc).await? {
78 compat::check(&self.compat, &hit.meta).map_err(anyhow::Error::new)?;
79 return Ok(hit);
80 }
81
82 let bytes = match loc {
83 ComponentLocator::Fs { path } => {
84 let (fs_root, candidate) = filesystem_root_and_path(path.as_path())?;
85 fs_source::fetch(&fs_root, &candidate).await?
86 }
87 ComponentLocator::Oci { reference } => {
88 #[cfg(feature = "oci")]
89 {
90 oci_source::fetch(reference).await?
91 }
92 #[cfg(not(feature = "oci"))]
93 {
94 bail!("OCI support disabled: enable the `oci` feature to fetch {reference}");
95 }
96 }
97 };
98
99 let (id, meta) = meta::compute_id_and_meta(bytes.as_ref()).await?;
100 let cb = ComponentBytes { id, bytes, meta };
101
102 compat::check(&self.compat, &cb.meta).map_err(anyhow::Error::new)?;
103 self.cache.store(loc, &cb).await?;
104 Ok(cb)
105 }
106}
107
108fn filesystem_root_and_path(path: &Path) -> Result<(PathBuf, PathBuf)> {
109 let canonical = path
110 .canonicalize()
111 .with_context(|| format!("failed to canonicalize {}", path.display()))?;
112 let root = canonical
113 .parent()
114 .map(Path::to_path_buf)
115 .context("filesystem source path has no parent")?;
116 let relative = canonical
117 .strip_prefix(&root)
118 .with_context(|| {
119 format!(
120 "failed to compute relative path for {}",
121 canonical.display()
122 )
123 })?
124 .to_path_buf();
125 normalize_under_root(&root, &relative)?;
127 Ok((root, relative))
128}
129
130mod cache;
131mod compat;
132mod fs_source;
133mod meta;
134#[cfg(feature = "oci")]
135mod oci_source;
136
137pub use compat::{CompatError, CompatPolicy};
138pub use meta::MetaInfo;