1use crate::cas::{BlobStore, FsBlobStore};
9use crate::digest::Digest256;
10use crate::error::Result;
11use crate::manifest::Manifest;
12
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16pub struct PfStore {
20 blobs: Arc<dyn BlobStore>,
21 root: PathBuf,
22}
23
24impl PfStore {
25 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
29 let root = root.as_ref().to_path_buf();
30 std::fs::create_dir_all(root.join("images"))?;
31 let blobs: Arc<dyn BlobStore> = Arc::new(FsBlobStore::open(&root)?);
32 Ok(Self { blobs, root })
33 }
34
35 pub fn with_blobstore(root: impl AsRef<Path>, blobs: Arc<dyn BlobStore>) -> Result<Self> {
37 let root = root.as_ref().to_path_buf();
38 std::fs::create_dir_all(root.join("images"))?;
39 Ok(Self { blobs, root })
40 }
41
42 pub fn blobs(&self) -> &dyn BlobStore {
44 self.blobs.as_ref()
45 }
46
47 pub fn blobs_arc(&self) -> Arc<dyn BlobStore> {
50 Arc::clone(&self.blobs)
51 }
52
53 pub fn root(&self) -> &Path {
55 &self.root
56 }
57
58 pub fn put_manifest(&self, m: &Manifest) -> Result<Digest256> {
64 let json = serde_json::to_vec(m)?;
65 let cid = self.blobs.put(&json)?;
66 let marker = self.root.join("images").join(format!("{}.json", cid.hex()));
67 if !marker.exists() {
68 std::fs::write(&marker, &json)?;
69 }
70 Ok(cid)
71 }
72
73 pub fn get_manifest(&self, cid: &Digest256) -> Result<Manifest> {
75 let bytes = self.blobs.get(cid)?;
76 Ok(serde_json::from_slice(&bytes)?)
77 }
78
79 pub fn iter_manifests(&self) -> Result<impl Iterator<Item = (Digest256, Manifest)> + '_> {
81 let entries = std::fs::read_dir(self.root.join("images"))?;
82 Ok(entries.filter_map(move |e| {
83 let e = e.ok()?;
84 let name = e.file_name().to_string_lossy().to_string();
85 let hex = name.strip_suffix(".json")?;
86 let cid = Digest256::parse(&format!("sha256:{hex}")).ok()?;
87 let m: Manifest = serde_json::from_slice(&std::fs::read(e.path()).ok()?).ok()?;
88 Some((cid, m))
89 }))
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::manifest::{
97 AgentInfo, CacheLayer, EffectsLayer, MEDIATYPE_V1, ModelLayer, TraceLayer, WorldLayer,
98 };
99 use chrono::Utc;
100 use tempfile::TempDir;
101
102 fn fixture(blobs: &dyn BlobStore) -> Manifest {
103 let d = blobs.put(b"x").unwrap();
104 Manifest {
105 schema_version: 1,
106 media_type: MEDIATYPE_V1.to_owned(),
107 agent: AgentInfo {
108 kind: "test".into(),
109 version: "0".into(),
110 fingerprint: "f".into(),
111 },
112 model: ModelLayer {
113 base: d.clone(),
114 diff: d.clone(),
115 },
116 cache: CacheLayer {
117 layout: "paged-batchinvariant-v1".into(),
118 manifest: d.clone(),
119 },
120 world: WorldLayer {
121 fs: d.clone(),
122 env: d.clone(),
123 procs: d.clone(),
124 },
125 effects: EffectsLayer { ledger: d.clone() },
126 trace: TraceLayer { messages: d },
127 created_at: Utc::now(),
128 parents: vec![],
129 }
130 }
131
132 #[test]
133 fn put_get_manifest_round_trip() {
134 let dir = TempDir::new().unwrap();
135 let store = PfStore::open(dir.path()).unwrap();
136 let m = fixture(store.blobs());
137 let cid = store.put_manifest(&m).unwrap();
138 let back = store.get_manifest(&cid).unwrap();
139 assert_eq!(back.schema_version, 1);
140 assert_eq!(back.parents.len(), 0);
141 }
142
143 #[test]
144 fn iter_manifests_lists_what_was_written() {
145 let dir = TempDir::new().unwrap();
146 let store = PfStore::open(dir.path()).unwrap();
147 let m1 = fixture(store.blobs());
148 let cid1 = store.put_manifest(&m1).unwrap();
149 let mut m2 = m1.clone();
150 m2.agent.version = "1".into();
151 let cid2 = store.put_manifest(&m2).unwrap();
152 assert_ne!(cid1, cid2);
153 let listed: std::collections::HashSet<_> = store
154 .iter_manifests()
155 .unwrap()
156 .map(|(digest, _)| digest)
157 .collect();
158 assert!(listed.contains(&cid1));
159 assert!(listed.contains(&cid2));
160 }
161}