Skip to content

Commit fb45756

Browse files
authored
Merge pull request #29 from tofay/generic
Support custom image compression
2 parents 21b2e82 + fe0bd15 commit fb45756

File tree

3 files changed

+149
-80
lines changed

3 files changed

+149
-80
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "ocidir"
33
description = "A Rust library for reading and writing OCI (opencontainers) layout directories"
4-
version = "0.4.0"
4+
version = "0.5.0"
55
edition = "2021"
66
license = "MIT OR Apache-2.0"
77
repository = "https://github.com/containers/ocidir-rs"

examples/custom_compressor.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/// Example that shows how to use a custom compression and media type for image layers.
2+
/// The example below does no compression.
3+
use std::{env, io, path::PathBuf};
4+
5+
use oci_spec::image::Platform;
6+
use ocidir::{cap_std::fs::Dir, BlobWriter, OciDir, WriteComplete};
7+
8+
struct NoCompression<'a>(BlobWriter<'a>);
9+
10+
impl io::Write for NoCompression<'_> {
11+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
12+
self.0.write(buf)
13+
}
14+
15+
fn flush(&mut self) -> io::Result<()> {
16+
self.0.flush()
17+
}
18+
}
19+
20+
impl<'a> WriteComplete<BlobWriter<'a>> for NoCompression<'a> {
21+
fn complete(self) -> io::Result<BlobWriter<'a>> {
22+
Ok(self.0)
23+
}
24+
}
25+
26+
fn main() {
27+
let dir = Dir::open_ambient_dir(env::temp_dir(), ocidir::cap_std::ambient_authority()).unwrap();
28+
let oci_dir = OciDir::ensure(dir).unwrap();
29+
30+
let mut manifest = oci_dir.new_empty_manifest().unwrap().build().unwrap();
31+
let mut config = ocidir::oci_spec::image::ImageConfigurationBuilder::default()
32+
.build()
33+
.unwrap();
34+
35+
// Add the src as a layer
36+
let writer = oci_dir
37+
.create_custom_layer(
38+
|bw| Ok(NoCompression(bw)),
39+
oci_spec::image::MediaType::ImageLayer,
40+
)
41+
.unwrap();
42+
let mut builder = tar::Builder::new(writer);
43+
builder.follow_symlinks(false);
44+
45+
builder
46+
.append_dir_all(".", PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"))
47+
.unwrap();
48+
49+
let layer = builder.into_inner().unwrap().complete().unwrap();
50+
oci_dir.push_layer(&mut manifest, &mut config, layer, "src", None);
51+
52+
println!(
53+
"Created image with manifest: {}",
54+
manifest.to_string_pretty().unwrap()
55+
);
56+
57+
// Add the image manifest
58+
let _descriptor = oci_dir
59+
.insert_manifest_and_config(manifest.clone(), config, None, Platform::default())
60+
.unwrap();
61+
}

src/lib.rs

Lines changed: 87 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use serde::{Deserialize, Serialize};
1515
use std::collections::{HashMap, HashSet};
1616
use std::fmt::Debug;
1717
use std::fs::File;
18-
use std::io::{prelude::*, BufReader};
18+
use std::io::{prelude::*, BufReader, BufWriter};
19+
use std::marker::PhantomData;
1920
use std::path::{Path, PathBuf};
2021
use std::str::FromStr;
2122
use thiserror::Error;
@@ -141,7 +142,7 @@ pub struct BlobWriter<'a> {
141142
/// Compute checksum
142143
hash: Hasher,
143144
/// Target file
144-
target: Option<cap_tempfile::TempFile<'a>>,
145+
target: Option<BufWriter<cap_tempfile::TempFile<'a>>>,
145146
size: u64,
146147
}
147148

@@ -154,13 +155,6 @@ impl Debug for BlobWriter<'_> {
154155
}
155156
}
156157

157-
/// Create an OCI tar+gzip layer.
158-
pub struct GzipLayerWriter<'a>(Sha256Writer<GzEncoder<BlobWriter<'a>>>);
159-
160-
#[cfg(feature = "zstd")]
161-
/// Writer for a OCI tar+zstd layer.
162-
pub struct ZstdLayerWriter<'a>(Sha256Writer<zstd::Encoder<'static, BlobWriter<'a>>>);
163-
164158
#[derive(Debug)]
165159
/// An opened OCI directory.
166160
pub struct OciDir {
@@ -280,17 +274,32 @@ impl OciDir {
280274
BlobWriter::new(&self.dir)
281275
}
282276

277+
/// Create a layer writer with a custom encoder and
278+
/// media type
279+
pub fn create_custom_layer<'a, W: WriteComplete<BlobWriter<'a>>>(
280+
&'a self,
281+
create: impl FnOnce(BlobWriter<'a>) -> std::io::Result<W>,
282+
media_type: MediaType,
283+
) -> Result<LayerWriter<'a, W>> {
284+
let bw = BlobWriter::new(&self.dir)?;
285+
Ok(LayerWriter::new(create(bw)?, media_type))
286+
}
287+
283288
/// Create a writer for a new gzip+tar blob; the contents
284289
/// are not parsed, but are expected to be a tarball.
285-
pub fn create_gzip_layer(&self, c: Option<flate2::Compression>) -> Result<GzipLayerWriter> {
286-
GzipLayerWriter::new(&self.dir, c)
290+
pub fn create_gzip_layer<'a>(
291+
&'a self,
292+
c: Option<flate2::Compression>,
293+
) -> Result<LayerWriter<'a, GzEncoder<BlobWriter<'a>>>> {
294+
let creator = |bw: BlobWriter<'a>| Ok(GzEncoder::new(bw, c.unwrap_or_default()));
295+
self.create_custom_layer(creator, MediaType::ImageLayerGzip)
287296
}
288297

289298
/// Create a tar output stream, backed by a blob
290299
pub fn create_layer(
291300
&self,
292301
c: Option<flate2::Compression>,
293-
) -> Result<tar::Builder<GzipLayerWriter>> {
302+
) -> Result<tar::Builder<LayerWriter<GzEncoder<BlobWriter>>>> {
294303
Ok(tar::Builder::new(self.create_gzip_layer(c)?))
295304
}
296305

@@ -299,8 +308,12 @@ impl OciDir {
299308
/// are not parsed, but are expected to be a tarball.
300309
///
301310
/// This method is only available when the `zstd` feature is enabled.
302-
pub fn create_layer_zstd(&self, compression_level: Option<i32>) -> Result<ZstdLayerWriter> {
303-
ZstdLayerWriter::new(&self.dir, compression_level)
311+
pub fn create_layer_zstd<'a>(
312+
&'a self,
313+
compression_level: Option<i32>,
314+
) -> Result<LayerWriter<'a, zstd::Encoder<'static, BlobWriter<'a>>>> {
315+
let creator = |bw: BlobWriter<'a>| zstd::Encoder::new(bw, compression_level.unwrap_or(0));
316+
self.create_custom_layer(creator, MediaType::ImageLayerZstd)
304317
}
305318

306319
#[cfg(feature = "zstdmt")]
@@ -312,12 +325,17 @@ impl OciDir {
312325
/// [zstd::Encoder::multithread]]
313326
///
314327
/// This method is only available when the `zstdmt` feature is enabled.
315-
pub fn create_layer_zstd_multithread(
316-
&self,
328+
pub fn create_layer_zstd_multithread<'a>(
329+
&'a self,
317330
compression_level: Option<i32>,
318331
n_workers: u32,
319-
) -> Result<ZstdLayerWriter> {
320-
ZstdLayerWriter::multithread(&self.dir, compression_level, n_workers)
332+
) -> Result<LayerWriter<'a, zstd::Encoder<'static, BlobWriter<'a>>>> {
333+
let creator = |bw: BlobWriter<'a>| {
334+
let mut encoder = zstd::Encoder::new(bw, compression_level.unwrap_or(0))?;
335+
encoder.multithread(n_workers)?;
336+
Ok(encoder)
337+
};
338+
self.create_custom_layer(creator, MediaType::ImageLayerZstd)
321339
}
322340

323341
/// Add a layer to the top of the image stack. The firsh pushed layer becomes the root.
@@ -655,7 +673,7 @@ impl<'a> BlobWriter<'a> {
655673
Ok(Self {
656674
hash: Hasher::new(MessageDigest::sha256())?,
657675
// FIXME add ability to choose filename after completion
658-
target: Some(cap_tempfile::TempFile::new(ocidir)?),
676+
target: Some(BufWriter::new(cap_tempfile::TempFile::new(ocidir)?)),
659677
size: 0,
660678
})
661679
}
@@ -684,7 +702,7 @@ impl<'a> BlobWriter<'a> {
684702
fn complete_as(mut self, sha256_digest: &str) -> Result<Blob> {
685703
let destname = &format!("{}/{}", BLOBDIR, sha256_digest);
686704
let target = self.target.take().unwrap();
687-
target.replace(destname)?;
705+
target.into_inner().unwrap().replace(destname)?;
688706
Ok(Blob {
689707
sha256: Sha256Digest::from_str(sha256_digest).unwrap(),
690708
size: self.size,
@@ -700,94 +718,84 @@ impl<'a> BlobWriter<'a> {
700718

701719
impl std::io::Write for BlobWriter<'_> {
702720
fn write(&mut self, srcbuf: &[u8]) -> std::io::Result<usize> {
703-
self.hash.update(srcbuf)?;
704-
self.target
705-
.as_mut()
706-
.unwrap()
707-
.as_file_mut()
708-
.write_all(srcbuf)?;
709-
self.size += srcbuf.len() as u64;
710-
Ok(srcbuf.len())
721+
let written = self.target.as_mut().unwrap().write(srcbuf)?;
722+
self.hash.update(&srcbuf[..written])?;
723+
self.size += written as u64;
724+
Ok(written)
711725
}
712726

713727
fn flush(&mut self) -> std::io::Result<()> {
714728
Ok(())
715729
}
716730
}
717731

718-
impl<'a> GzipLayerWriter<'a> {
719-
/// Create a writer for a gzip compressed layer blob.
720-
fn new(ocidir: &'a Dir, c: Option<flate2::Compression>) -> Result<Self> {
721-
let bw = BlobWriter::new(ocidir)?;
722-
let enc = flate2::write::GzEncoder::new(bw, c.unwrap_or_default());
723-
Ok(Self(Sha256Writer::new(enc)))
724-
}
732+
/// A writer that can be finalized to return an inner writer.
733+
pub trait WriteComplete<W>: Write {
734+
fn complete(self) -> std::io::Result<W>;
735+
}
725736

726-
/// Consume this writer, flushing buffered data and put the blob in place.
727-
pub fn complete(self) -> Result<Layer> {
728-
let (uncompressed_sha256, enc) = self.0.finish();
729-
let blob = enc.finish()?.complete()?;
730-
Ok(Layer {
731-
blob,
732-
uncompressed_sha256,
733-
media_type: MediaType::ImageLayerGzip,
734-
})
737+
impl<W> WriteComplete<W> for GzEncoder<W>
738+
where
739+
W: Write,
740+
{
741+
fn complete(self) -> std::io::Result<W> {
742+
self.finish()
735743
}
736744
}
737745

738-
impl std::io::Write for GzipLayerWriter<'_> {
739-
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
740-
self.0.write(data)
746+
#[cfg(feature = "zstd")]
747+
impl<W> WriteComplete<W> for zstd::Encoder<'_, W>
748+
where
749+
W: Write,
750+
{
751+
fn complete(self) -> std::io::Result<W> {
752+
self.finish()
741753
}
754+
}
742755

743-
fn flush(&mut self) -> std::io::Result<()> {
744-
self.0.flush()
745-
}
756+
/// A writer for a layer.
757+
pub struct LayerWriter<'a, W>
758+
where
759+
W: WriteComplete<BlobWriter<'a>>,
760+
{
761+
inner: Sha256Writer<W>,
762+
media_type: MediaType,
763+
marker: PhantomData<&'a ()>,
746764
}
747765

748-
#[cfg(feature = "zstd")]
749-
impl<'a> ZstdLayerWriter<'a> {
750-
/// Create a writer for a gzip compressed layer blob.
751-
fn new(ocidir: &'a Dir, c: Option<i32>) -> Result<Self> {
752-
let bw = BlobWriter::new(ocidir)?;
753-
let encoder = zstd::Encoder::new(bw, c.unwrap_or(0))?;
754-
Ok(Self(Sha256Writer::new(encoder)))
766+
impl<'a, W> LayerWriter<'a, W>
767+
where
768+
W: WriteComplete<BlobWriter<'a>>,
769+
{
770+
pub fn new(inner: W, media_type: oci_image::MediaType) -> Self {
771+
Self {
772+
inner: Sha256Writer::new(inner),
773+
media_type,
774+
marker: PhantomData,
775+
}
755776
}
756777

757-
/// Consume this writer, flushing buffered data and put the blob in place.
758778
pub fn complete(self) -> Result<Layer> {
759-
let (uncompressed_sha256, enc) = self.0.finish();
760-
let blob = enc.finish()?.complete()?;
779+
let (uncompressed_sha256, enc) = self.inner.finish();
780+
let blob = enc.complete()?.complete()?;
761781
Ok(Layer {
762782
blob,
763783
uncompressed_sha256,
764-
media_type: MediaType::ImageLayerZstd,
784+
media_type: self.media_type,
765785
})
766786
}
767787
}
768788

769-
#[cfg(feature = "zstdmt")]
770-
impl<'a> ZstdLayerWriter<'a> {
771-
/// Create a writer for a zstd compressed layer blob, with multithreaded compression enabled.
772-
///
773-
/// The `n_workers` parameter specifies the number of threads to use for compression, per
774-
/// [Encoder::multithread]]
775-
fn multithread(ocidir: &'a Dir, c: Option<i32>, n_workers: u32) -> Result<Self> {
776-
let bw = BlobWriter::new(ocidir)?;
777-
let mut encoder = zstd::Encoder::new(bw, c.unwrap_or(0))?;
778-
encoder.multithread(n_workers)?;
779-
Ok(Self(Sha256Writer::new(encoder)))
780-
}
781-
}
782-
783-
#[cfg(feature = "zstd")]
784-
impl std::io::Write for ZstdLayerWriter<'_> {
789+
impl<'a, W> std::io::Write for LayerWriter<'a, W>
790+
where
791+
W: WriteComplete<BlobWriter<'a>>,
792+
{
785793
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
786-
self.0.write(data)
794+
self.inner.write(data)
787795
}
788796

789797
fn flush(&mut self) -> std::io::Result<()> {
790-
self.0.flush()
798+
self.inner.flush()
791799
}
792800
}
793801

0 commit comments

Comments
 (0)