use anyhow::Context;
use serde_json::to_string_pretty;
use crate::Error;
use crate::{config::Config, db, utils::Dir};
use core::panic;
use indicatif::{ProgressBar, ProgressIterator};
use std::result;
use std::str::FromStr;
use std::time::Duration;
use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
pub const PACKS_DB: &str = "packs.idx";
const CONFIG_FILE: &str = "config.json";
const LOOSE: &str = "loose";
const PACKS: &str = "packs";
const DUPLICATES: &str = "duplicates";
const SANDBOX: &str = "sandbox";
#[derive(Debug)]
pub struct Container {
pub path: PathBuf,
}
#[derive(Debug)]
pub struct ContainerInfo {
pub location: String,
pub id: String,
pub compression_algorithm: String,
pub count: CountInfo,
pub size: SizeInfo,
}
#[derive(Debug)]
pub struct CountInfo {
pub loose: u64,
pub packs: u64,
pub packs_file: u64,
}
#[derive(Debug)]
pub struct SizeInfo {
pub loose: u64,
pub packs: u64,
pub packs_file: u64,
pub packs_db: u64,
}
#[derive(Debug, PartialEq)]
pub enum Compression {
Zlib(u32),
Zstd(i32),
Uncompressed,
}
#[allow(clippy::cast_sign_loss)]
impl FromStr for Compression {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim() == "none" {
return Ok(Compression::Uncompressed);
}
if s.trim() == "zlib+1" {
return Ok(Compression::Zlib(1));
}
let vs = s.split(':').collect::<Vec<_>>();
if vs.len() != 2 {
return Err(Error::ParseCompressionError { s: s.to_string() });
}
let (algo, level) = (
vs[0].trim(),
vs[1]
.trim()
.parse::<i32>()
.map_err(|_| Error::ParseCompressionError { s: s.to_string() })?,
);
match algo {
"zlib" => Ok(Compression::Zlib(level as u32)),
"zstd" => Ok(Compression::Zstd(level)), _ => Err(Error::ParseCompressionError { s: s.to_string() }),
}
}
}
impl Container {
pub fn new<P: AsRef<Path>>(path: P) -> Container {
Container {
path: path.as_ref().to_owned(),
}
}
pub fn compression(&self) -> Result<Compression, Error> {
let algo = self.config()?.compression_algorithm;
Compression::from_str(&algo)
}
pub fn reset(&self) {
fs::remove_dir_all(&self.path)
.unwrap_or_else(|err| panic!("not able to purge {}: {}", self.path.display(), err));
fs::create_dir_all(&self.path).unwrap_or_else(|err| {
panic!(
"not able to create after purge {}: {}",
self.path.display(),
err
)
});
}
pub fn initialize(&self, config: &Config) -> anyhow::Result<&Self> {
if Dir(&self.path).is_empty()? {
let json_string = to_string_pretty(&config)?;
let config = self.path.join(CONFIG_FILE);
fs::File::create(config.clone())?
.write_all(json_string.as_bytes())
.map_err(|err| Error::IoWrite {
source: err,
path: config.clone(),
})?;
Dir(&self.path).new_folder(LOOSE)?;
Dir(&self.path).new_folder(PACKS)?;
Dir(&self.path).new_folder(DUPLICATES)?;
Dir(&self.path).new_folder(SANDBOX)?;
let db = self.path.join(PACKS_DB);
db::create(&db).with_context(|| format!("create db at {}", db.display()))?;
} else {
let cnt = self.valid()?;
anyhow::bail!("{} already initialized", cnt.path.display())
}
Ok(self)
}
pub fn config(&self) -> Result<Config, Error> {
let config_path = self.config_file();
let config = fs::read_to_string(&config_path)?;
let config = serde_json::from_str(&config).map_err(|err| Error::ConfigFileError {
source: err.into(),
path: config_path.clone(),
})?;
Ok(config)
}
pub fn valid(&self) -> Result<&Self, Error> {
if !self.path.exists() || Dir(&self.path).is_empty()? {
return Err(Error::Uninitialized {
path: self.path.clone(),
});
}
if !self.path.is_dir() {
return Err(Error::UnableObtainDir {
path: self.path.clone(),
});
}
for entry in self.path.read_dir()? {
let path = entry?.path();
if let Some(filename) = path.file_name() {
let filename = filename.to_string_lossy();
match filename.as_ref() {
LOOSE | PACKS | DUPLICATES | SANDBOX => {
if !path.is_dir() {
return Err(Error::StoreComponentError {
path: self.path.clone(),
cause: "not a dir".to_string(),
});
}
}
CONFIG_FILE => {
if !path.is_file() {
return Err(Error::StoreComponentError {
path: self.path.clone(),
cause: "not a file".to_string(),
});
}
}
_ if filename.contains(PACKS_DB) => {
if !path.is_file() {
return Err(Error::StoreComponentError {
path: self.path.clone(),
cause: "not a file".to_string(),
});
}
}
_ => Err(Error::DirectoryNotEmpty { path })?,
}
}
}
Ok(self)
}
#[must_use]
pub fn loose(&self) -> PathBuf {
Dir(&self.path).at_path(LOOSE)
}
#[must_use]
pub fn sandbox(&self) -> PathBuf {
Dir(&self.path).at_path(SANDBOX)
}
#[must_use]
pub fn packs(&self) -> PathBuf {
Dir(&self.path).at_path(PACKS)
}
#[must_use]
pub fn packs_db(&self) -> PathBuf {
Dir(&self.path).at_path(PACKS_DB)
}
#[must_use]
pub fn config_file(&self) -> PathBuf {
Dir(&self.path).at_path(CONFIG_FILE)
}
}
pub fn traverse_loose(cnt: &Container) -> Result<impl Iterator<Item = PathBuf>, Error> {
let loose = cnt.loose();
Ok(loose
.read_dir()?
.filter_map(result::Result::ok)
.map(|entry| entry.path())
.flat_map(|path| {
path.read_dir()
.unwrap_or_else(|_| panic!("unable to read {}", path.display()))
})
.filter_map(result::Result::ok)
.map(|entry| entry.path()))
}
pub fn traverse_packs(cnt: &Container) -> Result<impl Iterator<Item = PathBuf>, Error> {
let spinnner = ProgressBar::new_spinner().with_message("Auditing container stat ...");
spinnner.enable_steady_tick(Duration::from_millis(500));
let packs = cnt.packs();
Ok(packs
.read_dir()?
.filter_map(result::Result::ok)
.map(|entry| entry.path())
.progress_with(spinnner))
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use crate::test_utils::{new_container, PACK_TARGET_SIZE};
use super::*;
#[test]
fn default_init() {
let (_tmp_dir, cnt) = new_container(PACK_TARGET_SIZE, "none");
assert!(!Dir(&cnt.path).is_empty().unwrap());
}
#[test]
fn init_in_inited_folder() {
let (_tmp_dir, cnt) = new_container(PACK_TARGET_SIZE, "none");
let err = cnt
.initialize(&Config::new(4 * 1024 * 1024, "none"))
.unwrap_err();
assert!(
err.to_string().contains("already initialized"),
"got err: {err}"
);
}
#[test]
fn init_in_non_empty_folder() {
let tmp = tempdir().unwrap();
let cnt = Container::new(&tmp);
let _ = fs::File::create(cnt.path.join("unexpected"));
let err = cnt
.initialize(&Config::new(4 * 1024 * 1024, "none"))
.unwrap_err();
assert!(
err.to_string()
.contains("Refusing to initialize in non-empty directory"),
"got err: {err}"
);
}
#[test]
fn parse_compression() {
assert_eq!(
Compression::from_str("none").unwrap(),
Compression::Uncompressed
);
assert_eq!(
Compression::from_str("zlib:+1").unwrap(),
Compression::Zlib(1)
);
assert_eq!(
Compression::from_str("zstd:-7").unwrap(),
Compression::Zstd(-7)
);
assert_eq!(
Compression::from_str("none ").unwrap(),
Compression::Uncompressed
);
assert_eq!(
Compression::from_str("zlib :+1 ").unwrap(),
Compression::Zlib(1)
);
assert_eq!(
Compression::from_str("zstd:-7 ").unwrap(),
Compression::Zstd(-7)
);
assert_eq!(
Compression::from_str("zlib+1").unwrap(),
Compression::Zlib(1)
);
assert!(Compression::from_str("zzzz").is_err());
}
}