use std::error::Error as StdError;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use tokio::{
fs::{File, OpenOptions},
io::{self, AsyncRead, AsyncWriteExt, BufWriter},
};
use super::{
digest::Digest,
spec_v1::{ImageLayout, Index},
};
const OCI_LAYOUT_FILENAME: &str = "oci-layout";
const INDEX_JSON_FILENAME: &str = "index.json";
const BLOBS_DIRNAME: &str = "blobs";
#[derive(Debug)]
pub struct OCIImageLayoutError(String);
impl StdError for OCIImageLayoutError {}
impl Display for OCIImageLayoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Layout Error: {}", self.0)
}
}
impl From<std::io::Error> for OCIImageLayoutError {
fn from(e: std::io::Error) -> Self {
OCIImageLayoutError(format!("{}", e))
}
}
impl From<serde_json::Error> for OCIImageLayoutError {
fn from(e: serde_json::Error) -> Self {
OCIImageLayoutError(format!("{}", e))
}
}
#[derive(Debug, Clone)]
pub struct OCIImageLayout {
name: String,
tag: Option<String>,
image_path: PathBuf,
index: Index,
layout: ImageLayout,
}
impl OCIImageLayout {
pub fn new<P>(name: &str, tag: Option<&str>, path: P) -> Self
where
P: AsRef<Path>,
{
let mut image_path = PathBuf::from(path.as_ref());
if tag.is_none() {
let _ = image_path.push(name);
} else {
let _ = image_path.push(format!("{}/{}", name, tag.unwrap()));
}
let tag = match tag {
Some(t) => Some(t.to_string()),
None => None,
};
OCIImageLayout {
name: name.to_string(),
tag,
index: Index::default(),
layout: ImageLayout::default(),
image_path,
}
}
pub async fn create_fs_path(&mut self) -> Result<(), std::io::Error> {
let mut path = self.image_path.clone();
path.push(BLOBS_DIRNAME);
let _ = tokio::fs::create_dir_all(&path).await?;
Ok(())
}
pub async fn delete_fs_path(&mut self) -> Result<(), std::io::Error> {
let _ = tokio::fs::remove_dir_all(&self.image_path).await?;
Ok(())
}
pub async fn write_image_layout(&self) -> Result<(), std::io::Error> {
let mut layout_file_path = self.image_path.clone();
layout_file_path.push(OCI_LAYOUT_FILENAME);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(layout_file_path)
.await?;
let contents = serde_json::to_vec(&self.layout)?;
let mut writer = BufWriter::new(file);
writer.write(&contents).await?;
writer.flush().await?;
Ok(())
}
pub async fn write_index_json(&self) -> Result<(), std::io::Error> {
let mut index_json_path = self.image_path.clone();
index_json_path.push(INDEX_JSON_FILENAME);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(index_json_path)
.await?;
let contents = serde_json::to_vec(&self.index)?;
let mut writer = BufWriter::new(file);
writer.write(&contents).await?;
writer.flush().await?;
Ok(())
}
pub async fn write_blob_file<T>(
&self,
digest: &Digest,
blob: &mut T,
) -> Result<(), std::io::Error>
where
T: AsyncRead + Unpin,
{
let mut path = self.image_path.clone();
path.push(BLOBS_DIRNAME);
path.push(digest.algorithm());
if !path.exists() {
tokio::fs::create_dir(&path).await?;
}
let _ = path.push(digest.hex_digest());
let mut file = File::create(&path).await?;
io::copy(blob, &mut file).await?;
Ok(())
}
#[inline(always)]
pub fn tag(&self) -> Option<String> {
self.tag.clone()
}
#[inline(always)]
pub fn image_fs_path(&self) -> PathBuf {
self.image_path.clone()
}
#[inline(always)]
pub fn index(&self) -> Index {
self.index.clone()
}
pub fn update_index(&mut self, index: Index) {
let _ = std::mem::replace(&mut self.index, index);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_basic_layout() {
let mut oci_layout = OCIImageLayout::new("foo", None, "/tmp");
let r = oci_layout.create_fs_path().await;
assert!(r.is_ok());
let r = oci_layout.write_index_json().await;
assert!(r.is_ok(), "{:#?}", r.err());
let r = oci_layout.write_image_layout().await;
assert!(r.is_ok(), "{:#?}", r.err());
let r = oci_layout.delete_fs_path().await;
assert!(r.is_ok());
}
}