1use std::fs::{self, File};
2use std::io::{self, Read, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::digest::Hasher;
8use crate::error::Context;
9use crate::{apply_layer_file, Digest, Error, Result, UnpackOptions};
10
11#[derive(Debug, Clone)]
31pub struct LayerStore {
32 root: PathBuf,
33}
34
35impl LayerStore {
36 pub fn open(root: &Path) -> Result<Self> {
38 for d in ["blobs/sha256", "layers", "images", "rootfs", "tmp"] {
39 let p = root.join(d);
40 fs::create_dir_all(&p).ctx(|| format!("creating {}", p.display()))?;
41 }
42 Ok(Self {
43 root: root.to_path_buf(),
44 })
45 }
46
47 pub fn root(&self) -> &Path {
48 &self.root
49 }
50
51 pub fn blob_path(&self, digest: &Digest) -> PathBuf {
52 self.root.join("blobs/sha256").join(digest.hex())
53 }
54
55 pub fn has_blob(&self, digest: &Digest) -> bool {
56 self.blob_path(digest).is_file()
57 }
58
59 pub fn put_blob(&self, digest: &Digest, mut reader: impl Read) -> Result<PathBuf> {
62 let mut w = self.blob_writer()?;
63 let mut buf = vec![0u8; 1 << 16];
64 loop {
65 let n = reader.read(&mut buf).ctx(|| format!("reading {digest}"))?;
66 if n == 0 {
67 break;
68 }
69 w.write(&buf[..n])?;
70 }
71 w.commit(digest)
72 }
73
74 pub fn blob_writer(&self) -> Result<BlobWriter> {
76 let path = self.tmp_path("blob");
77 let file = File::create(&path).ctx(|| format!("creating {}", path.display()))?;
78 Ok(BlobWriter {
79 file: Some(io::BufWriter::with_capacity(1 << 20, file)),
80 path,
81 hasher: Hasher::default(),
82 dest: self.root.join("blobs/sha256"),
83 })
84 }
85
86 pub fn rootfs(&self, layers: &[Digest], opts: &UnpackOptions) -> Result<PathBuf> {
88 let dir = self.rootfs_path(layers);
89 if dir.is_dir() {
90 return Ok(dir);
91 }
92 if let Some(missing) = layers.iter().find(|d| !self.has_blob(d)) {
93 return Err(Error::MissingBlob(missing.clone()));
94 }
95 let tmp = match self.unpack(layers, opts) {
96 Err(_) if opts.parallel_gzip => self.unpack(
100 layers,
101 &UnpackOptions {
102 parallel_gzip: false,
103 ..opts.clone()
104 },
105 )?,
106 r => r?,
107 };
108 if fs::rename(&tmp, &dir).is_err() {
109 let _ = fs::remove_dir_all(&tmp);
111 if !dir.is_dir() {
112 return Err(Error::Invalid(format!("could not place {}", dir.display())));
113 }
114 }
115 Ok(dir)
116 }
117
118 fn unpack(&self, layers: &[Digest], opts: &UnpackOptions) -> Result<PathBuf> {
120 let tmp = self.tmp_path("rootfs");
121 fs::create_dir_all(&tmp).ctx(|| format!("creating {}", tmp.display()))?;
122 let unpacked = layers
123 .iter()
124 .try_for_each(|d| apply_layer_file(&self.blob_path(d), &tmp, opts).map(drop));
125 match unpacked {
126 Ok(()) => Ok(tmp),
127 Err(e) => {
128 let _ = fs::remove_dir_all(&tmp);
129 Err(e)
130 }
131 }
132 }
133
134 pub fn rootfs_path(&self, layers: &[Digest]) -> PathBuf {
136 self.root.join("rootfs").join(chain_id(layers).hex())
137 }
138
139 fn tmp_path(&self, kind: &str) -> PathBuf {
140 static N: AtomicU64 = AtomicU64::new(0);
141 let nanos = SystemTime::now()
142 .duration_since(UNIX_EPOCH)
143 .map_or(0, |d| d.as_nanos());
144 self.root.join("tmp").join(format!(
145 "{kind}-{}-{}-{nanos}",
146 std::process::id(),
147 N.fetch_add(1, Ordering::Relaxed)
148 ))
149 }
150}
151
152pub fn chain_id(layers: &[Digest]) -> Digest {
154 let mut h = Hasher::default();
155 for d in layers {
156 h.update(d.to_string().as_bytes());
157 h.update(b"\n");
158 }
159 h.finish()
160}
161
162pub struct BlobWriter {
165 file: Option<io::BufWriter<File>>,
166 path: PathBuf,
167 hasher: Hasher,
168 dest: PathBuf,
169}
170
171impl BlobWriter {
172 pub fn write(&mut self, bytes: &[u8]) -> Result<()> {
173 self.hasher.update(bytes);
174 let f = self.file.as_mut().expect("open until commit");
175 f.write_all(bytes)
176 .ctx(|| format!("writing {}", self.path.display()))
177 }
178
179 pub fn commit(mut self, digest: &Digest) -> Result<PathBuf> {
181 let f = self.file.take().expect("open until commit");
182 f.into_inner()
183 .map_err(|e| e.into_error())
184 .ctx(|| format!("writing {}", self.path.display()))?;
185 digest.verify(&std::mem::take(&mut self.hasher).finish())?;
186 let dest = self.dest.join(digest.hex());
187 fs::rename(&self.path, &dest).ctx(|| format!("placing {digest}"))?;
188 Ok(dest)
189 }
190}
191
192impl Drop for BlobWriter {
193 fn drop(&mut self) {
194 let _ = fs::remove_file(&self.path);
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
201pub struct ImageLayer {
202 pub blob: Digest,
203 pub diff_id: Digest,
204}
205
206#[derive(Debug, Clone, Default, PartialEq, Eq)]
208pub struct GcStats {
209 pub layers: usize,
210 pub blobs: usize,
211}
212
213#[derive(serde::Serialize, serde::Deserialize)]
214struct ImageRecord {
215 layers: Vec<ImageLayer>,
216}
217
218impl LayerStore {
219 pub fn layer_path(&self, diff_id: &Digest) -> PathBuf {
221 self.root.join("layers").join(diff_id.hex())
222 }
223
224 pub fn has_layer(&self, diff_id: &Digest) -> bool {
225 self.layer_path(diff_id).is_dir()
226 }
227
228 pub fn missing_layers(&self, layers: &[ImageLayer]) -> Vec<ImageLayer> {
230 layers
231 .iter()
232 .filter(|l| !self.has_layer(&l.diff_id))
233 .cloned()
234 .collect()
235 }
236
237 pub fn unpack_layer(&self, layer: &ImageLayer, opts: &UnpackOptions) -> Result<PathBuf> {
240 let dir = self.layer_path(&layer.diff_id);
241 if dir.is_dir() {
242 return Ok(dir);
243 }
244 if !self.has_blob(&layer.blob) {
245 return Err(Error::MissingBlob(layer.blob.clone()));
246 }
247 let attempt = |o: &UnpackOptions| -> Result<PathBuf> {
248 let tmp = self.tmp_path("layer");
249 fs::create_dir_all(&tmp).ctx(|| format!("creating {}", tmp.display()))?;
250 let r = crate::layer::apply_layer_file_digest(&self.blob_path(&layer.blob), &tmp, o)
251 .and_then(|(_, d)| layer.diff_id.verify(&d));
252 match r {
253 Ok(()) => Ok(tmp),
254 Err(e) => {
255 let _ = fs::remove_dir_all(&tmp);
256 Err(e)
257 }
258 }
259 };
260 let o = UnpackOptions {
261 layout: match opts.layout {
262 crate::Layout::Flatten => crate::Layout::Overlay(crate::OverlayXattr::Trusted),
263 l => l,
264 },
265 ..opts.clone()
266 };
267 let tmp = match attempt(&o) {
268 Err(e @ Error::DigestMismatch { .. }) => return Err(e),
269 Err(_) if o.parallel_gzip => attempt(&UnpackOptions {
270 parallel_gzip: false,
271 ..o.clone()
272 })?,
273 r => r?,
274 };
275 if fs::rename(&tmp, &dir).is_err() {
276 let _ = fs::remove_dir_all(&tmp);
277 if !dir.is_dir() {
278 return Err(Error::Invalid(format!("could not place {}", dir.display())));
279 }
280 }
281 Ok(dir)
282 }
283
284 pub fn add_image(
287 &self,
288 id: &Digest,
289 layers: &[ImageLayer],
290 opts: &UnpackOptions,
291 ) -> Result<Vec<PathBuf>> {
292 let dirs = layers
293 .iter()
294 .map(|l| self.unpack_layer(l, opts))
295 .collect::<Result<Vec<_>>>()?;
296 let rec = ImageRecord {
297 layers: layers.to_vec(),
298 };
299 let path = self.image_record(id);
300 let tmp = self.tmp_path("image");
301 let bytes = serde_json::to_vec_pretty(&rec).map_err(|e| Error::Invalid(e.to_string()))?;
302 fs::write(&tmp, bytes).ctx(|| format!("writing {}", tmp.display()))?;
303 fs::rename(&tmp, &path).ctx(|| format!("placing {}", path.display()))?;
304 Ok(dirs)
305 }
306
307 pub fn image_layers(&self, id: &Digest) -> Result<Vec<PathBuf>> {
309 Ok(self
310 .record(id)?
311 .layers
312 .iter()
313 .map(|l| self.layer_path(&l.diff_id))
314 .collect())
315 }
316
317 pub fn remove_image(&self, id: &Digest) -> Result<()> {
319 let p = self.image_record(id);
320 fs::remove_file(&p).ctx(|| format!("removing {}", p.display()))
321 }
322
323 pub fn refcount(&self, diff_id: &Digest) -> Result<usize> {
325 Ok(self
326 .records()?
327 .iter()
328 .filter(|r| r.layers.iter().any(|l| &l.diff_id == diff_id))
329 .count())
330 }
331
332 pub fn gc(&self) -> Result<GcStats> {
335 let recs = self.records()?;
336 let diff_ids: std::collections::HashSet<String> = recs
337 .iter()
338 .flat_map(|r| r.layers.iter().map(|l| l.diff_id.hex().to_string()))
339 .collect();
340 let blobs: std::collections::HashSet<String> = recs
341 .iter()
342 .flat_map(|r| r.layers.iter().map(|l| l.blob.hex().to_string()))
343 .collect();
344 let mut st = GcStats::default();
345 for (dir, keep, n) in [
346 ("layers", &diff_ids, &mut st.layers),
347 ("blobs/sha256", &blobs, &mut st.blobs),
348 ] {
349 let d = self.root.join(dir);
350 for e in fs::read_dir(&d)
351 .ctx(|| format!("listing {}", d.display()))?
352 .flatten()
353 {
354 let name = e.file_name().to_string_lossy().into_owned();
355 if !keep.contains(&name) {
356 let p = e.path();
357 let r = if p.is_dir() {
358 fs::remove_dir_all(&p)
359 } else {
360 fs::remove_file(&p)
361 };
362 r.ctx(|| format!("removing {}", p.display()))?;
363 *n += 1;
364 }
365 }
366 }
367 let t = self.root.join("tmp");
368 for e in fs::read_dir(&t)
369 .ctx(|| format!("listing {}", t.display()))?
370 .flatten()
371 {
372 let p = e.path();
373 let _ = if p.is_dir() {
374 fs::remove_dir_all(&p)
375 } else {
376 fs::remove_file(&p)
377 };
378 }
379 Ok(st)
380 }
381
382 pub fn holds_blob(&self, blob: &Digest) -> bool {
387 self.has_blob(blob)
388 || self.records().is_ok_and(|rs| {
389 rs.iter()
390 .flat_map(|r| &r.layers)
391 .any(|l| &l.blob == blob && self.has_layer(&l.diff_id))
392 })
393 }
394
395 pub fn prune_unpacked_blobs(&self) -> Result<usize> {
398 let mut n = 0;
399 for r in self.records()? {
400 for l in r.layers {
401 if self.has_layer(&l.diff_id) && fs::remove_file(self.blob_path(&l.blob)).is_ok() {
402 n += 1;
403 }
404 }
405 }
406 Ok(n)
407 }
408
409 fn image_record(&self, id: &Digest) -> PathBuf {
410 self.root.join("images").join(format!("{}.json", id.hex()))
411 }
412
413 fn record(&self, id: &Digest) -> Result<ImageRecord> {
414 let p = self.image_record(id);
415 let b = fs::read(&p).ctx(|| format!("reading {}", p.display()))?;
416 serde_json::from_slice(&b).map_err(|e| Error::Invalid(format!("{}: {e}", p.display())))
417 }
418
419 fn records(&self) -> Result<Vec<ImageRecord>> {
420 let d = self.root.join("images");
421 let mut out = Vec::new();
422 for e in fs::read_dir(&d)
423 .ctx(|| format!("listing {}", d.display()))?
424 .flatten()
425 {
426 if let Ok(b) = fs::read(e.path()) {
427 if let Ok(r) = serde_json::from_slice(&b) {
428 out.push(r);
429 }
430 }
431 }
432 Ok(out)
433 }
434}