// 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 .
//! # cas-simple
//!
//! `cas-simple` implements a simple content addressable storage using
//! cas-core.
extern crate digest;
extern crate sha2;
extern crate toml;
extern crate cas_core;
use std::str::FromStr;
use std::path::{Path, PathBuf};
use digest::DynDigest;
use toml::Value;
use cas_core::{Cas, Error, hex, ObjectId, ObjectMetadata, ObjectType, Result};
fn new_digest(id: &str) -> Result> {
match id {
"sha256" => { Ok(Box::new(sha2::Sha256::default())) },
_ => { Err(Error::error(format!("unknown digest '{}'", id))) }
}
}
pub struct SimpleCas {
db_path: PathBuf,
digest: Box,
config: Value,
}
impl SimpleCas {
pub fn create(db_path: PathBuf, config: Value) -> Result {
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::error(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
if db_path.exists() {
return Err(Error::error(format!(
"failed to create SimpleCas: target directory already exists ({})",
db_path.to_string_lossy(),
)));
}
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(Error::error(format!(
"failed to create directory ({}): {}",
path.to_string_lossy(), e,
)))
)?;
}
write_config(&config, &db_path)?;
Ok(SimpleCas {
db_path,
digest,
config,
})
}
pub fn open(db_path: PathBuf) -> Result {
let config = read_config(&db_path)?;
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::error(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
Ok(SimpleCas {
db_path,
digest,
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(Error::error(format!(
"invalid object id size: got {}, expected {}",
hex.len(), self.digest.output_size() * 2,
)))
}
}
fn object_id_from_partial(&self, _hex: &str) -> Result {
Err(Error::error("Not implemented"))
}
fn has_object_id(&self, oid: &ObjectId) -> Result {
let opath = obj_path(&self.db_path, oid);
Ok(opath.is_file())
}
fn read_object(&self, _oid: &ObjectId) -> Result<(ObjectMetadata, &dyn std::io::Read)> {
Err(Error::error("Not implemented"))
}
fn write_object(&mut self, _otype: ObjectType, _data: &mut dyn std::io::Read) -> Result {
Err(Error::error("Not implemented"))
}
fn remove_object(&mut self, _oid: &ObjectId) -> Result<()> {
Err(Error::error("Not implemented"))
}
fn read_ref(&self, _key: &str) -> Result {
Err(Error::error("Not implemented"))
}
fn write_ref(&mut self, _key: &str, _value: &ObjectId) -> Result<()> {
Err(Error::error("Not implemented"))
}
fn remove_ref(&mut self, _key: &str) -> Result<()> {
Err(Error::error("Not implemented"))
}
}
fn obj_dir(cas_path: &Path) -> PathBuf {
cas_path.join("obj")
}
fn obj_path(cas_path: &Path, oid: &ObjectId) -> PathBuf {
let mut path = cas_path.to_path_buf();
path.push(hex(&oid.id()[0..1]));
path.push(hex(&oid.id()[1..2]));
path.push(hex(&oid.id()[2..]));
path
}
fn ref_dir(cas_path: &Path) -> PathBuf {
cas_path.join("ref")
}
fn tmp_dir(cas_path: &Path) -> PathBuf {
cas_path.join("tmp")
}
fn config_path(cas_path: &Path) -> PathBuf {
cas_path.join("config")
}
fn read_config(db_path: &Path) -> Result {
use std::io::Read;
let mut file = std::fs::File::open(config_path(db_path)).or_else(|err|
Err(Error::error(format!("invalid repository: no config file: {}", err)))
)?;
let mut config_str = String::new();
file.read_to_string(&mut config_str).or_else(|err|
Err(Error::error(format!("cannot read config file: {}", err)))
)?;
let config = toml::from_str(&config_str).or_else(|err|
Err(Error::error(format!("error while reading config file: {}", err)))
)?;
Ok(config)
}
fn write_config(config: &Value, db_path: &Path) -> Result<()> {
use std::io::Write;
let config_str = toml::to_string_pretty(config).or_else(|err|
Err(Error::error(format!("failed to serialize config: {}", err)))
)?;
let mut file = tempfile::NamedTempFile::new_in(tmp_dir(db_path)).or_else(|err|
Err(Error::error(format!("cannot create temp config file: {}", err)))
)?;
file.write_all(config_str.as_bytes()).or_else(|err|
Err(Error::error(format!("failed to write to temp config: {}", err)))
)?;
file.persist(config_path(db_path)).or_else(|err|
Err(Error::error(format!("failed to (over)write config: {}", err)))
)?;
Ok(())
}
#[cfg(test)]
mod tests {
extern crate tempfile;
use super::*;
#[test]
fn create_simple_cas() {
let dir = tempfile::tempdir()
.expect("failed to create temp test dir");
let cas_path = {
let mut cas_path = dir.path().to_path_buf();
cas_path.push(".bsv");
cas_path
};
let config = toml::toml!(
[cas]
digest = "sha256"
);
let cas = SimpleCas::create(cas_path, config)
.expect("failed to create SimpleCas object");
let oid = cas.object_id_from_string("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.expect("failed to create object id");
assert!(!cas.has_object_id(&oid).expect("has_object_id failed"));
}
}