use std::fmt::Debug;
use std::fmt::Formatter;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use crypto::CryptKey;
mod any_snapshot;
pub use any_snapshot::AnySnapshot;
const DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES: usize = 1024 * 4;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SnapshotWriter {
dir: PathBuf,
key: Option<CryptKey>,
}
impl Debug for SnapshotWriter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotWriter")
.field("dir", &format!("{:?}", self.dir))
.field("key", if self.key.is_some() { &"Some" } else { &"None" })
.finish()
}
}
impl SnapshotWriter {
pub fn new(root: PathBuf, encrypt: bool) -> Result<Self> {
std::fs::create_dir(&root)
.with_context(|| format!("failed to create snapshot root dir: {}", root.display()))?;
if encrypt {
let key = crypto::generate_random_key();
let mut writer = crypto::CryptWriter::new_from_key(
File::create(root.join("enc_metadata")).context("failed to create enc_metadata")?,
1024,
&key,
)
.context("failed to create enc_metadata writer")?;
writer.flush().context("flush of enc_metadata failed")?;
return Ok(Self {
dir: root,
key: Some(key),
});
}
Ok(Self {
dir: root,
key: None,
})
}
pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Write>> {
self.raw_fragment_with_chunk_size(name, DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES)
}
pub fn raw_fragment_with_chunk_size(
&self,
name: &str,
chunk_size_bytes: usize,
) -> Result<Box<dyn Write>> {
let path = self.dir.join(name);
let file = File::options()
.write(true)
.create_new(true)
.open(&path)
.with_context(|| {
format!(
"failed to create snapshot fragment {name:?} at {}",
path.display()
)
})?;
if let Some(key) = self.key.as_ref() {
return Ok(Box::new(crypto::CryptWriter::new_from_key(
file,
chunk_size_bytes,
key,
)?));
}
Ok(Box::new(file))
}
pub fn write_fragment<T: serde::Serialize>(&self, name: &str, v: &T) -> Result<()> {
let mut w = std::io::BufWriter::new(self.raw_fragment(name)?);
ciborium::into_writer(v, &mut w)?;
w.flush()?;
Ok(())
}
pub fn add_namespace(&self, name: &str) -> Result<Self> {
let dir = self.dir.join(name);
std::fs::create_dir(&dir).with_context(|| {
format!(
"failed to create nested snapshot writer {name:?} at {}",
dir.display()
)
})?;
Ok(Self {
dir,
key: self.key.clone(),
})
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SnapshotReader {
dir: PathBuf,
key: Option<CryptKey>,
}
impl Debug for SnapshotReader {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotReader")
.field("dir", &format!("{:?}", self.dir))
.field("key", if self.key.is_some() { &"Some" } else { &"None" })
.finish()
}
}
impl SnapshotReader {
pub fn new(root: &Path, require_encrypted: bool) -> Result<Self> {
let enc_metadata_path = root.join("enc_metadata");
if Path::exists(&enc_metadata_path) {
let key = Some(
crypto::CryptReader::extract_key(
File::open(&enc_metadata_path).context("failed to open encryption metadata")?,
)
.context("failed to load snapshot key")?,
);
return Ok(Self {
dir: root.to_path_buf(),
key,
});
} else if require_encrypted {
return Err(anyhow::anyhow!("snapshot was not encrypted"));
}
Ok(Self {
dir: root.to_path_buf(),
key: None,
})
}
pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Read>> {
let path = self.dir.join(name);
let file = File::open(&path).with_context(|| {
format!(
"failed to open snapshot fragment {name:?} at {}",
path.display()
)
})?;
if let Some(key) = self.key.as_ref() {
return Ok(Box::new(crypto::CryptReader::from_file_and_key(file, key)?));
}
Ok(Box::new(file))
}
pub fn read_fragment<T: serde::de::DeserializeOwned>(&self, name: &str) -> Result<T> {
Ok(ciborium::from_reader(BufReader::new(
self.raw_fragment(name)?,
))?)
}
pub fn list_fragments(&self) -> Result<Vec<String>> {
let mut result = Vec::new();
for entry in std::fs::read_dir(&self.dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
result.push(entry.file_name().to_string_lossy().into_owned());
}
}
Ok(result)
}
pub fn namespace(&self, name: &str) -> Result<Self> {
let dir = self.dir.join(name);
Ok(Self {
dir,
key: self.key.clone(),
})
}
pub fn list_namespaces(&self) -> Result<Vec<String>> {
let mut result = Vec::new();
for entry in std::fs::read_dir(&self.dir)? {
let entry = entry?;
if entry.path().is_dir() {
if let Some(file_name) = entry.path().file_name() {
result.push(file_name.to_string_lossy().into_owned());
}
}
}
Ok(result)
}
}