// This file is part of bsv.
//
// bsv is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// cdb is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
// more details.
//
// You should have received a copy of the Affero GNU General Public License
// along with cdb. If not, see .
use std::str::FromStr;
use digest::DynDigest;
use toml::Value;
use camino::{Utf8Path, Utf8PathBuf};
use cas_core::{
Cas, DefaultPipeline, err, Error, ObjectId, ObjectMetadata, ObjectType,
Pipeline, Reader, ReadWrapper, RefStore, Result, Writer,
};
use crate::utils::{
obj_dir, obj_path, ref_dir, tmp_dir,
read_config, write_config,
read_metadata, write_metadata,
new_digest,
};
use crate::wfile::WFile;
pub struct SimpleCas {
db_path: Utf8PathBuf,
digest: Box,
pipeline: DefaultPipeline,
// config: Value,
}
impl SimpleCas {
pub fn create(db_path: Utf8PathBuf, mut config: Value) -> Result {
if !config.is_table() {
return Error::err("invalid config object: must be table");
}
let maybe_engine = config.as_table_mut().unwrap()
.entry("cas")
.or_insert_with(|| toml::value::Table::new().into())
.as_table_mut().unwrap()
.entry("engine")
.or_insert("simple".into())
.as_str();
match maybe_engine {
Some(engine) if engine != "simple" => {
return err!("invalid cas.engine in config: got {}, expected simple", engine);
},
None => {
return Error::err("invalid casengine in config: expected String");
},
_ => {}
}
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::unknown(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
let pipeline = DefaultPipeline::new(digest.box_clone());
if db_path.exists() {
return err!(
"failed to create SimpleCas: target directory already exists ({})",
db_path,
);
}
for path in [
&db_path,
&obj_dir(&db_path),
&ref_dir(&db_path),
&tmp_dir(&db_path),
] {
std::fs::create_dir(path).or_else(|e|
err!(
"failed to create directory ({}): {}",
path, e,
)
)?;
}
write_config(&config, &db_path)?;
Ok(SimpleCas {
db_path,
digest,
pipeline,
// config,
})
}
pub fn open(db_path: Utf8PathBuf) -> Result {
let config = read_config(&db_path)?;
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::unknown(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
let pipeline = DefaultPipeline::new(digest.box_clone());
Ok(SimpleCas {
db_path,
digest,
pipeline,
// config,
})
}
// pub fn save_config(&self) -> Result<()> {
// write_config(&self.config, &self.db_path)
// }
}
impl Cas for SimpleCas {
fn object_id_from_string(&self, hex: &str) -> Result {
if hex.len() == self.digest.output_size() * 2 {
ObjectId::from_str(hex)
}
else {
err!(
"invalid object id size: got {}, expected {}",
hex.len(), self.digest.output_size() * 2,
)
}
}
fn has_object_id(&self, oid: &ObjectId) -> Result {
let opath = obj_path(&self.db_path, oid);
Ok(opath.is_file())
}
fn iter_object_id<'s>(&'s self) -> Result>>> {
Ok(Box::new(ObjectIdIterator::new(self)?))
}
fn open_object(&self, oid: &ObjectId) -> Result<(ObjectMetadata, Box)> {
let opath = obj_path(&self.db_path, oid);
if !opath.is_file() {
return err!("object not found: {}", oid);
}
let file = std::fs::File::open(opath).or_else(|err|
err!("failed to open object {}: {}", oid, err))?;
let wrapper = Box::new(ReadWrapper::new(file));
let mut reader = self.pipeline.new_reader(wrapper);
let metadata = read_metadata(&mut reader)?;
Ok((metadata, reader))
}
fn new_writer(&mut self, otype: &ObjectType, size: u64) -> Result> {
let file = Box::new(WFile::new(self.db_path.clone())?);
let mut writer = self.pipeline.new_writer(file);
write_metadata(&mut writer, otype, size)?;
Ok(writer)
}
fn remove_object(&mut self, oid: &ObjectId) -> Result<()> {
let opath = obj_path(&self.db_path, oid);
if !opath.is_file() {
return err!("object not found: {}", oid);
}
std::fs::remove_file(opath).or_else(|err|
err!("failed to remove object {}: {}", oid, err))?;
Ok(())
}
}
impl RefStore for SimpleCas {
fn get_ref>(&self, key: P) -> Result {
let path = ref_dir(&self.db_path).join(key.as_ref());
if !path.exists() {
err!("reference {} does not exists", key.as_ref())
}
else if !path.is_file() {
err!("reference {} is not a file", key.as_ref())
}
else {
let file = std::fs::read(path).or_else(|err|
err!("failed to read reference file for {}: {}", key.as_ref(), err)
)?;
Ok(
ObjectId::from_str(
std::str::from_utf8(&file).or_else(|err|
err!("invalid reference file at {}: {}", key.as_ref(), err)
)?
)?
)
}
}
fn set_ref>(&mut self, key: P, value: &ObjectId) -> Result<()> {
let path = ref_dir(&self.db_path).join(key.as_ref());
std::fs::create_dir_all(path.parent().ok_or_else(||
Error::unknown(format!("reference file {} has no parent dir?", key.as_ref()))
)?).or_else(|err|
err!("failed to create reference dir for {}: {}", key.as_ref(), err)
)?;
std::fs::write(path, value.to_string()).or_else(|err|
err!("failed to write reference {}: {}", key.as_ref(), err)
)
}
fn remove_ref>(&mut self, key: P) -> Result<()> {
let path = ref_dir(&self.db_path).join(key.as_ref());
if !path.exists() {
err!("reference {} does not exists", key.as_ref())
}
else if !path.is_file() {
err!("reference {} is not a file", key.as_ref())
}
else {
std::fs::remove_file(path).or_else(|err|
err!("failed to remove reference file {}: {}", key.as_ref(), err)
)
}
}
}
pub struct ObjectIdIterator {
root_dirs: Vec,
inner_dirs: Vec,
objects: Vec,
root_index: usize,
inner_index: usize,
object_index: usize,
}
impl ObjectIdIterator {
fn new(cas: &SimpleCas) -> Result {
let root_dirs = read_dir(&obj_dir(&cas.db_path))?;
let mut it = Self {
root_dirs,
inner_dirs: Vec::new(),
objects: Vec::new(),
root_index: 0,
inner_index: 0,
object_index: 0,
};
it.fetch_inner_dirs()?;
if !it.is_item_valid() {
it.next_valid_item()?;
}
Ok(it)
}
fn get_object_id(&mut self) -> Result {
let path = &self.objects[self.object_index];
if !path.is_file() {
return err!("item in object database is not a file: {:?}", path);
}
let id = path.ancestors()
.take(3)
.collect::>()
.iter()
.rev()
.map(|p| p.file_name())
.collect::