// 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")); } }