diff --git a/Cargo.lock b/Cargo.lock index 39207ee..be2e32e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -17,10 +26,26 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "camino" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3132262930b0522068049f5870a856ab8affc80c70d08b6ecb785771a6fc23" + [[package]] name = "cas-core" version = "0.1.0" dependencies = [ + "camino", "digest", "sha2", "thiserror", @@ -30,6 +55,7 @@ dependencies = [ name = "cas-simple" version = "0.1.0" dependencies = [ + "camino", "cas-core", "digest", "sha2", @@ -61,6 +87,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "generic-array" version = "0.14.4" @@ -82,12 +114,51 @@ dependencies = [ "wasi", ] +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "libbsv" +version = "0.1.0" +dependencies = [ + "camino", + "cas-core", + "cas-simple", + "globset", + "toml", +] + [[package]] name = "libc" version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -167,6 +238,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index b56b9b5..fd9f82d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ members = [ "cas-core", "cas-simple", - # "libbsv", + "libbsv", # "bsv", ] diff --git a/bsv/Cargo.toml b/bsv/Cargo.toml index 7987315..c98fc5e 100644 --- a/bsv/Cargo.toml +++ b/bsv/Cargo.toml @@ -2,7 +2,7 @@ name = "bsv" version = "0.1.0" authors = ["Simon Boyé "] -edition = "2018" +edition = "2021" license = "AGPL-3.0-or-later" [dependencies] diff --git a/cas-core/Cargo.toml b/cas-core/Cargo.toml index 59adf6f..57700d7 100644 --- a/cas-core/Cargo.toml +++ b/cas-core/Cargo.toml @@ -2,12 +2,13 @@ name = "cas-core" version = "0.1.0" authors = ["Simon Boyé "] -edition = "2018" +edition = "2021" license = "AGPL-3.0-or-later" [dependencies] thiserror = "1.0.25" digest = { version = "0.9.0", features = ["alloc"] } +camino = { version = "1.0.7" } [dev-dependencies] sha2 = "0.9.5" diff --git a/cas-core/src/cas.rs b/cas-core/src/cas.rs index 27750e4..22d9940 100644 --- a/cas-core/src/cas.rs +++ b/cas-core/src/cas.rs @@ -14,8 +14,9 @@ // along with cdb. If not, see . -use std::path::Path; +use camino::Utf8Path; +use super::err; use super::error::{Error, Result}; use super::object_id::ObjectId; use super::object_type::ObjectType; @@ -40,21 +41,21 @@ pub trait Cas { let mut data = Vec::new(); reader.read_to_end(&mut data).or_else(|err| - Err(Error::error(format!("failed to read object {}: {}", oid, err))))?; + err!("failed to read object {}: {}", oid, err))?; let result_oid = reader.finalize()?; if &result_oid == oid { Ok((metadata, data)) } else { - Err(Error::error(format!("object id mismatch: requested {}, read {}", oid, result_oid))) + err!("object id mismatch: requested {}, read {}", oid, result_oid) } } fn write_object(&mut self, otype: &ObjectType, data: &[u8]) -> Result { let mut writer = self.new_writer(otype, data.len() as u64)?; writer.write_all(data).or_else(|err| - Err(Error::error(format!("failed to write object: {}", err))))?; + err!("failed to write object: {}", err))?; writer.finalize() } @@ -62,7 +63,7 @@ pub trait Cas { } pub trait RefStore { - fn get_ref>(&self, key: P) -> Result; - fn set_ref>(&mut self, key: P, value: &ObjectId) -> Result<()>; - fn remove_ref>(&mut self, key: P) -> Result<()>; + fn get_ref>(&self, key: P) -> Result; + fn set_ref>(&mut self, key: P, value: &ObjectId) -> Result<()>; + fn remove_ref>(&mut self, key: P) -> Result<()>; } \ No newline at end of file diff --git a/cas-core/src/error.rs b/cas-core/src/error.rs index 9903ecd..c266474 100644 --- a/cas-core/src/error.rs +++ b/cas-core/src/error.rs @@ -14,15 +14,15 @@ // along with cdb. If not, see . -// use std::path::PathBuf; +// use std::path::Utf8PathBuf; /// Result type used through cas-core. -pub type Result = std::result::Result>; +pub type Result = std::result::Result; /// Error type used through cas-core. #[non_exhaustive] -#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum Error { // #[error("failed to create repository: {message}")] // RepositoryCreationFailed { @@ -44,7 +44,7 @@ pub enum Error { // #[error("non-empty directory ({dir})")] // NonEmptyDirectory { - // dir: PathBuf + // dir: Utf8PathBuf // }, // #[error("invalid character(s) ({characters})")] @@ -67,16 +67,25 @@ pub enum Error { // UnsupportedFileType, // #[error("invalid path ({path})")] - // InvalidPath { path: PathBuf }, + // InvalidPath { path: Utf8PathBuf }, // #[error("io error{}", format_optional_path(path))] // IoError { // source: std::io::Error, - // path: Option, + // path: Option, // }, + #[error("item skipped")] + Skipped, + + #[error("logic error: {0}")] + LogicError(String), + + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + #[error("{0}")] - Error(String), + UnknownError(String), } impl Error { @@ -108,17 +117,17 @@ impl Error { // }) // } - pub fn error>(message: M) -> Box { - Box::new(Error::Error(message.into())) + pub fn unknown>(message: M) -> Error { + Error::UnknownError(message.into()) } pub fn err, T>(message: M) -> Result { - Err(Self::error(message)) + Err(Self::unknown(message)) } } -// fn format_optional_path(maybe_path: &Option) -> String { +// fn format_optional_path(maybe_path: &Option) -> String { // match maybe_path { // Some(path) => { format!(" ({:?})", path) }, // None => { String::new() } diff --git a/cas-core/src/lib.rs b/cas-core/src/lib.rs index 5db84a5..0710d47 100644 --- a/cas-core/src/lib.rs +++ b/cas-core/src/lib.rs @@ -18,11 +18,10 @@ //! `cas-core` provides traits and types to interface with content-addressable //! storage. -#![feature(inherent_ascii_escape)] - extern crate thiserror; extern crate digest; +extern crate camino; mod error; @@ -42,3 +41,11 @@ pub use crate::{ cas::{Cas, RefStore}, }; + +#[macro_export] +macro_rules! err { + ($($arg:tt)*) => {{ + let res = std::fmt::format(format_args!($($arg)*)); + Error::err(res) + }} +} diff --git a/cas-core/src/object_id.rs b/cas-core/src/object_id.rs index 2ce5c88..898af3a 100644 --- a/cas-core/src/object_id.rs +++ b/cas-core/src/object_id.rs @@ -18,6 +18,7 @@ use std::fmt; use std::str::{FromStr}; use std::sync::Arc; +use super::err; use super::error::*; @@ -43,7 +44,7 @@ impl ObjectId { } impl FromStr for ObjectId { - type Err = Box; + type Err = Error; fn from_str(id_str: &str) -> Result { Ok(ObjectId { @@ -138,7 +139,7 @@ impl<'a> Iterator for CharPairs<'a> { Some(Ok(&self.string[start_index..self.index])) } else { - Some(Err(Error::error(&format!("invalid string: got {} characters, expected even number", self.char_count)))) + Some(err!("invalid string: got {} characters, expected even number", self.char_count)) } } } @@ -148,7 +149,7 @@ fn parse_byte(maybe_str_byte: Result<&str>) -> Result { match maybe_str_byte { Ok(str_byte) => u8::from_str_radix(str_byte, 16) - .map_err(|_| Error::error("invalid character")), + .map_err(|_| Error::unknown("invalid character")), Err(err) => Err(err), } } @@ -194,19 +195,23 @@ mod tests { fn str_to_object_id_invalid_size() { let result = ObjectId::from_str("000102030405060"); - assert_eq!( - result.expect_err("object id parsing should have failed"), - Error::error("invalid string: got 15 characters, expected even number"), - ); + assert!(result.is_err()); + + match result.expect_err("result is not an error") { + Error::UnknownError(ref msg) if msg == "invalid string: got 15 characters, expected even number" + => {}, + _ => panic!("result is not the expected error"), + } } #[test] fn str_to_object_id_invalid_character() { let result = ObjectId::from_str("00010203 4050607"); - assert_eq!( - result.expect_err("object id parsing should have failed"), - Error::error("invalid character"), - ); + match result.expect_err("result is not an error") { + Error::UnknownError(ref msg) if msg == "invalid character" + => {}, + _ => panic!("result is not the expected error"), + } } } diff --git a/cas-core/src/object_type.rs b/cas-core/src/object_type.rs index 3063d8c..deb9434 100644 --- a/cas-core/src/object_type.rs +++ b/cas-core/src/object_type.rs @@ -13,6 +13,7 @@ // You should have received a copy of the Affero GNU General Public License // along with cdb. If not, see . +use super::err; use super::error::*; @@ -24,7 +25,10 @@ pub struct ObjectType { impl ObjectType { pub fn new(id: &[u8]) -> Result { if id.len() != 4 { - Err(Error::error("Invalid object type size.")) + err!("invalid object type size") + } + else if let Err(err) = std::str::from_utf8(id) { + err!("invalid object type, contain non-utf8 sequence: {}", err) } else { let mut buf = [0; 4]; @@ -48,6 +52,16 @@ impl std::fmt::Debug for ObjectType { } } +impl std::fmt::Display for ObjectType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + // Safe, because we check at ObjectType creation that it is valid utf8 + let id = unsafe { + std::str::from_utf8_unchecked(&self.id) + }; + write!(f, "{}", id) + } +} + #[cfg(test)] mod tests { diff --git a/cas-core/src/pipeline.rs b/cas-core/src/pipeline.rs index 744a14f..b3c1777 100644 --- a/cas-core/src/pipeline.rs +++ b/cas-core/src/pipeline.rs @@ -15,6 +15,7 @@ use digest::DynDigest; +use super::err; use super::error::*; use super::object_id::ObjectId; @@ -53,7 +54,7 @@ impl std::io::Read for ReadWrapper { impl Reader for ReadWrapper { fn finalize(self: Box) -> Result { - Err(Error::error("reader pipline has no digest step")) + err!("reader pipline has no digest step") } } @@ -80,7 +81,7 @@ impl std::io::Write for WriteWrapper { impl Writer for WriteWrapper { fn finalize(self: Box) -> Result { - Err(Error::error("reader pipline has no digest step")) + err!("reader pipline has no digest step") } fn _finalize(self: Box, oid: ObjectId) -> Result { @@ -97,7 +98,7 @@ pub struct DigestReader<'a> { impl<'a> DigestReader<'a> { pub fn new(reader: Box, digest: Box) -> Self { - return DigestReader { + DigestReader { reader, digest: Some(digest), oid: None, @@ -128,7 +129,7 @@ impl<'a> std::io::Read for DigestReader<'a> { impl<'a> Reader for DigestReader<'a> { fn finalize(self: Box) -> Result { - self.oid.ok_or_else(|| Error::error("Reader not finalized")) + self.oid.ok_or_else(|| Error::unknown("Reader not finalized")) } } @@ -140,7 +141,7 @@ pub struct DigestWriter<'a> { impl<'a> DigestWriter<'a> { pub fn new(writer: Box, digest: Box) -> Self { - return DigestWriter { + DigestWriter { writer, digest, } @@ -166,7 +167,7 @@ impl<'a> Writer for DigestWriter<'a> { } fn _finalize(self: Box, _oid: ObjectId) -> Result { - Err(Error::error("writer pipeline has several digest steps")) + err!("writer pipeline has several digest steps") } } diff --git a/cas-simple/Cargo.toml b/cas-simple/Cargo.toml index 749d252..4a941d8 100644 --- a/cas-simple/Cargo.toml +++ b/cas-simple/Cargo.toml @@ -1,13 +1,16 @@ [package] name = "cas-simple" version = "0.1.0" -edition = "2018" +authors = ["Simon Boyé "] +edition = "2021" +license = "AGPL-3.0-or-later" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] digest = { version = "0.9.0", features = ["alloc"] } sha2 = "0.9.5" +camino = { version = "1.0.7" } toml = "0.5.8" cas-core = { path = "../cas-core" } tempfile = "3.2.0" diff --git a/cas-simple/src/cas.rs b/cas-simple/src/cas.rs index 52e5360..4163af3 100644 --- a/cas-simple/src/cas.rs +++ b/cas-simple/src/cas.rs @@ -15,12 +15,12 @@ use std::str::FromStr; -use std::path::{Path, PathBuf}; use digest::DynDigest; use toml::Value; +use camino::{Utf8Path, Utf8PathBuf}; use cas_core::{ - Cas, DefaultPipeline, Error, ObjectId, ObjectMetadata, ObjectType, + Cas, DefaultPipeline, err, Error, ObjectId, ObjectMetadata, ObjectType, Pipeline, Reader, ReadWrapper, RefStore, Result, Writer, }; @@ -34,27 +34,48 @@ use crate::wfile::WFile; pub struct SimpleCas { - db_path: PathBuf, + db_path: Utf8PathBuf, digest: Box, pipeline: DefaultPipeline, - config: Value, + // config: Value, } impl SimpleCas { - pub fn create(db_path: PathBuf, config: Value) -> Result { + 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::error( + .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(Error::error(format!( + return err!( "failed to create SimpleCas: target directory already exists ({})", - db_path.to_string_lossy(), - ))); + db_path, + ); } for path in [ @@ -64,10 +85,10 @@ impl SimpleCas { &tmp_dir(&db_path), ] { std::fs::create_dir(path).or_else(|e| - Err(Error::error(format!( + err!( "failed to create directory ({}): {}", - path.to_string_lossy(), e, - ))) + path, e, + ) )?; } @@ -77,15 +98,15 @@ impl SimpleCas { db_path, digest, pipeline, - config, + // config, }) } - pub fn open(db_path: PathBuf) -> Result { + 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::error( + .ok_or_else(|| Error::unknown( "mandatory cas.digest value is invalid or missing from config" ))?; let digest = new_digest(digest_id)?; @@ -95,13 +116,13 @@ impl SimpleCas { db_path, digest, pipeline, - config, + // config, }) } - pub fn save_config(&self) -> Result<()> { - write_config(&self.config, &self.db_path) - } + // pub fn save_config(&self) -> Result<()> { + // write_config(&self.config, &self.db_path) + // } } @@ -111,10 +132,10 @@ impl Cas for SimpleCas { ObjectId::from_str(hex) } else { - Err(Error::error(format!( + err!( "invalid object id size: got {}, expected {}", hex.len(), self.digest.output_size() * 2, - ))) + ) } } @@ -131,11 +152,11 @@ impl Cas for SimpleCas { fn open_object(&self, oid: &ObjectId) -> Result<(ObjectMetadata, Box)> { let opath = obj_path(&self.db_path, oid); if !opath.is_file() { - return Err(Error::error(format!("object not found: {}", oid))); + return err!("object not found: {}", oid); } let file = std::fs::File::open(opath).or_else(|err| - Err(Error::error(format!("failed to open object {}: {}", oid, 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)?; @@ -154,60 +175,60 @@ impl Cas for SimpleCas { fn remove_object(&mut self, oid: &ObjectId) -> Result<()> { let opath = obj_path(&self.db_path, oid); if !opath.is_file() { - return Err(Error::error(format!("object not found: {}", oid))); + return err!("object not found: {}", oid); } std::fs::remove_file(opath).or_else(|err| - Err(Error::error(format!("failed to remove object {}: {}", oid, err))))?; + err!("failed to remove object {}: {}", oid, err))?; Ok(()) } } impl RefStore for SimpleCas { - fn get_ref>(&self, key: P) -> Result { + fn get_ref>(&self, key: P) -> Result { let path = ref_dir(&self.db_path).join(key.as_ref()); if !path.exists() { - Error::err(format!("reference {} does not exists", key.as_ref().to_string_lossy())) + err!("reference {} does not exists", key.as_ref()) } else if !path.is_file() { - Error::err(format!("reference {} is not a file", key.as_ref().to_string_lossy())) + err!("reference {} is not a file", key.as_ref()) } else { let file = std::fs::read(path).or_else(|err| - Error::err(format!("failed to read reference file for {}: {}", key.as_ref().to_string_lossy(), err)) + err!("failed to read reference file for {}: {}", key.as_ref(), err) )?; Ok( ObjectId::from_str( std::str::from_utf8(&file).or_else(|err| - Error::err(format!("invalid reference file at {}: {}", key.as_ref().to_string_lossy(), err)) + err!("invalid reference file at {}: {}", key.as_ref(), err) )? )? ) } } - fn set_ref>(&mut self, key: P, value: &ObjectId) -> Result<()> { + 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::error(format!("reference file {} has no parent dir?", key.as_ref().to_string_lossy())) + Error::unknown(format!("reference file {} has no parent dir?", key.as_ref())) )?).or_else(|err| - Error::err(format!("failed to create reference dir for {}: {}", key.as_ref().to_string_lossy(), err)) + err!("failed to create reference dir for {}: {}", key.as_ref(), err) )?; std::fs::write(path, value.to_string()).or_else(|err| - Error::err(format!("failed to write reference {}: {}", key.as_ref().to_string_lossy(), err)) + err!("failed to write reference {}: {}", key.as_ref(), err) ) } - fn remove_ref>(&mut self, key: P) -> Result<()> { + fn remove_ref>(&mut self, key: P) -> Result<()> { let path = ref_dir(&self.db_path).join(key.as_ref()); if !path.exists() { - Error::err(format!("reference {} does not exists", key.as_ref().to_string_lossy())) + err!("reference {} does not exists", key.as_ref()) } else if !path.is_file() { - Error::err(format!("reference {} is not a file", key.as_ref().to_string_lossy())) + err!("reference {} is not a file", key.as_ref()) } else { std::fs::remove_file(path).or_else(|err| - Error::err(format!("failed to remove reference file {}: {}", key.as_ref().to_string_lossy(), err)) + err!("failed to remove reference file {}: {}", key.as_ref(), err) ) } } @@ -215,9 +236,9 @@ impl RefStore for SimpleCas { pub struct ObjectIdIterator { - root_dirs: Vec, - inner_dirs: Vec, - objects: Vec, + root_dirs: Vec, + inner_dirs: Vec, + objects: Vec, root_index: usize, inner_index: usize, object_index: usize, @@ -247,7 +268,7 @@ impl ObjectIdIterator { let path = &self.objects[self.object_index]; if !path.is_file() { - return Error::err(format!("item in object database is not a file: {:?}", path)); + return err!("item in object database is not a file: {:?}", path); } let id = path.ancestors() @@ -255,9 +276,9 @@ impl ObjectIdIterator { .collect::>() .iter() .rev() - .map(|p| p.file_name()?.to_str()) + .map(|p| p.file_name()) .collect::>() - .ok_or_else(|| Error::error(format!("invalid object in object database: {:?}", path)))?; + .ok_or_else(|| Error::unknown(format!("invalid object in object database: {:?}", path)))?; ObjectId::from_str(&id) } @@ -342,14 +363,19 @@ impl Iterator for ObjectIdIterator { } } -fn read_dir(path: &Path) -> Result> { +fn read_dir(path: &Utf8Path) -> Result> { let mut paths = std::fs::read_dir(path) - .or_else(|err| Err(Error::error(format!( - "failed to read directory {:?}: {}", path, err))))? - .map(|dir| dir.map(|dir| dir.path())) - .collect::>>() - .or_else(|err| Err(Error::error(format!( - "error while reading directory {:?}: {}", path, err))))?; + .or_else(|err| err!("failed to read directory {:?}: {}", path, err))? + .map(|res| match res { + Ok(dir) => Ok( + Utf8PathBuf::from_path_buf(dir.path()) + .or_else(|e| err!("non-unicode character in path: {}: {:?}", path, e))? + ), + Err(err) => err!("error while reading directory {:?}: {}", path, err), + }) + .collect::>>() + .or_else(|err| err!( + "error while reading directory {:?}: {}", path, err))?; paths.sort(); Ok(paths) } @@ -357,7 +383,7 @@ fn read_dir(path: &Path) -> Result> { #[cfg(test)] mod tests { - use std::path::Path; + use camino::Utf8Path; use super::*; @@ -368,7 +394,7 @@ mod tests { ) } - fn get_cas_path(dir: &Path) -> PathBuf { + fn get_cas_path(dir: &Utf8Path) -> Utf8PathBuf { let mut cas_path = dir.to_path_buf(); cas_path.push(".bsv"); cas_path @@ -377,7 +403,7 @@ mod tests { #[test] fn test_create_simple_cas() { let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(Utf8Path::from_path(dir.path()).unwrap()); let config = get_config(); let cas = SimpleCas::create(cas_path, config) @@ -391,7 +417,7 @@ mod tests { #[test] fn test_write_object() { let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(dir.path().try_into().unwrap()); let config = get_config(); let otype = ObjectType::new(b"blob").expect("failed to create object type"); @@ -421,7 +447,7 @@ mod tests { use std::io::Write; let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(dir.path().try_into().unwrap()); let config = get_config(); let otype = ObjectType::new(b"blob").expect("failed to create object type"); @@ -460,7 +486,7 @@ mod tests { #[test] fn test_read_write_object() { let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(dir.path().try_into().unwrap()); let config = get_config(); let otype = ObjectType::new(b"blob").expect("failed to create object type"); @@ -480,7 +506,7 @@ mod tests { #[test] fn test_remove_object() { let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(dir.path().try_into().unwrap()); let config = get_config(); let otype = ObjectType::new(b"blob").expect("failed to create object type"); @@ -501,7 +527,7 @@ mod tests { #[test] fn test_object_id_iterator() { let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(dir.path().try_into().unwrap()); let config = get_config(); let mut cas = SimpleCas::create(cas_path.clone(), config) @@ -615,7 +641,7 @@ mod tests { #[test] fn test_reference() { let dir = tempfile::tempdir().expect("failed to create temp test dir"); - let cas_path = get_cas_path(dir.path()); + let cas_path = get_cas_path(dir.path().try_into().unwrap()); let config = get_config(); let mut cas = SimpleCas::create(cas_path.clone(), config) diff --git a/cas-simple/src/lib.rs b/cas-simple/src/lib.rs index 57cfba3..6f8e87b 100644 --- a/cas-simple/src/lib.rs +++ b/cas-simple/src/lib.rs @@ -21,12 +21,13 @@ extern crate digest; extern crate sha2; +extern crate camino; extern crate toml; extern crate cas_core; extern crate tempfile; -mod utils; +pub mod utils; mod wfile; mod cas; diff --git a/cas-simple/src/utils.rs b/cas-simple/src/utils.rs index 5cbe802..b4af219 100644 --- a/cas-simple/src/utils.rs +++ b/cas-simple/src/utils.rs @@ -15,21 +15,20 @@ // along with cdb. If not, see . -use std::path::{Path, PathBuf}; - use digest::DynDigest; use toml::Value; +use camino::{Utf8Path, Utf8PathBuf}; use cas_core::{ - Error, hex, ObjectId, ObjectMetadata, ObjectType, Result, + err, Error, hex, ObjectId, ObjectMetadata, ObjectType, Result, }; -pub fn obj_dir(cas_path: &Path) -> PathBuf { +pub fn obj_dir(cas_path: &Utf8Path) -> Utf8PathBuf { cas_path.join("obj") } -pub fn obj_path(cas_path: &Path, oid: &ObjectId) -> PathBuf { +pub fn obj_path(cas_path: &Utf8Path, oid: &ObjectId) -> Utf8PathBuf { let mut path = obj_dir(cas_path); path.push(hex(&oid.id()[0..1])); path.push(hex(&oid.id()[1..2])); @@ -37,49 +36,49 @@ pub fn obj_path(cas_path: &Path, oid: &ObjectId) -> PathBuf { path } -pub fn ref_dir(cas_path: &Path) -> PathBuf { +pub fn ref_dir(cas_path: &Utf8Path) -> Utf8PathBuf { cas_path.join("ref") } -pub fn tmp_dir(cas_path: &Path) -> PathBuf { +pub fn tmp_dir(cas_path: &Utf8Path) -> Utf8PathBuf { cas_path.join("tmp") } -pub fn config_path(cas_path: &Path) -> PathBuf { +pub fn config_path(cas_path: &Utf8Path) -> Utf8PathBuf { cas_path.join("config") } -pub fn read_config(db_path: &Path) -> Result { +pub fn read_config(db_path: &Utf8Path) -> 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))) + err!("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))) + err!("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))) + err!("error while reading config file: {}", err) )?; Ok(config) } -pub fn write_config(config: &Value, db_path: &Path) -> Result<()> { +pub fn write_config(config: &Value, db_path: &Utf8Path) -> 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))) + err!("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))) + err!("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))) + err!("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))) + err!("failed to (over)write config: {}", err) )?; Ok(()) } @@ -89,13 +88,13 @@ pub fn read_metadata(read: &mut dyn std::io::Read) -> Result { let otype = ObjectType::new(&{ let mut buf = [0; 4]; read.read_exact(&mut buf).or_else(|err| - Err(Error::error(format!("failed to read object type: {}", err))))?; + err!("failed to read object type: {}", err))?; buf })?; let size = u64::from_be_bytes({ let mut buf = [0; 8]; read.read_exact(&mut buf).or_else(|err| - Err(Error::error(format!("failed to read object size: {}", err))))?; + err!("failed to read object size: {}", err))?; buf }); Ok(ObjectMetadata::new(otype, size)) @@ -103,9 +102,9 @@ pub fn read_metadata(read: &mut dyn std::io::Read) -> Result { pub fn write_metadata(write: &mut dyn std::io::Write, otype: &ObjectType, size: u64) -> Result<()> { write.write_all(otype.id()).or_else(|err| - Err(Error::error(format!("failed to write object type: {}", err))))?; + err!("failed to write object type: {}", err))?; write.write_all(&size.to_be_bytes()).or_else(|err| - Err(Error::error(format!("failed to write object size: {}", err))))?; + err!("failed to write object size: {}", err))?; Ok(()) } @@ -113,7 +112,7 @@ pub fn write_metadata(write: &mut dyn std::io::Write, otype: &ObjectType, size: pub fn new_digest(id: &str) -> Result> { match id { "sha256" => { Ok(Box::new(sha2::Sha256::default())) }, - _ => { Err(Error::error(format!("unknown digest '{}'", id))) } + _ => { err!("unknown digest '{}'", id) } } } @@ -126,29 +125,29 @@ mod tests { #[test] fn test_dirs() { - let base_path = PathBuf::from("/foo/bar"); + let base_path = Utf8PathBuf::from("/foo/bar"); let oid = ObjectId::from_str("0123456789abcdef") .expect("failed to create object id"); assert_eq!( config_path(&base_path), - PathBuf::from("/foo/bar/config") + Utf8PathBuf::from("/foo/bar/config") ); assert_eq!( obj_dir(&base_path), - PathBuf::from("/foo/bar/obj") + Utf8PathBuf::from("/foo/bar/obj") ); assert_eq!( obj_path(&base_path, &oid), - PathBuf::from("/foo/bar/obj/01/23/456789abcdef") + Utf8PathBuf::from("/foo/bar/obj/01/23/456789abcdef") ); assert_eq!( ref_dir(&base_path), - PathBuf::from("/foo/bar/ref") + Utf8PathBuf::from("/foo/bar/ref") ); assert_eq!( tmp_dir(&base_path), - PathBuf::from("/foo/bar/tmp") + Utf8PathBuf::from("/foo/bar/tmp") ); } @@ -165,10 +164,10 @@ mod tests { let dir = tempfile::TempDir::new() .expect("failed to create tmp dir"); - std::fs::create_dir(tmp_dir(dir.path())).expect("failed to create db/tmp dir"); + std::fs::create_dir(tmp_dir(Utf8Path::from_path(dir.path()).unwrap())).expect("failed to create db/tmp dir"); - write_config(&config, dir.path()).expect("failed to write config"); - let config2 = read_config(dir.path()).expect("failed to read config"); + write_config(&config, Utf8Path::from_path(dir.path()).unwrap()).expect("failed to write config"); + let config2 = read_config(Utf8Path::from_path(dir.path()).unwrap()).expect("failed to read config"); assert_eq!(config2, config); diff --git a/cas-simple/src/wfile.rs b/cas-simple/src/wfile.rs index af03e58..499a30d 100644 --- a/cas-simple/src/wfile.rs +++ b/cas-simple/src/wfile.rs @@ -14,10 +14,10 @@ // along with cdb. If not, see . -use std::path::{PathBuf}; +use camino::{Utf8PathBuf}; use cas_core::{ - Error, ObjectId, Result, Writer, + err, Error, ObjectId, Result, Writer, }; use crate::utils::{obj_path, tmp_dir}; @@ -25,13 +25,13 @@ use crate::utils::{obj_path, tmp_dir}; pub struct WFile { file: tempfile::NamedTempFile, - db_path: PathBuf, + db_path: Utf8PathBuf, } impl WFile { - pub fn new(db_path: PathBuf) -> Result { + pub fn new(db_path: Utf8PathBuf) -> Result { let file = tempfile::NamedTempFile::new_in(tmp_dir(&db_path)).or_else(|err| - Err(Error::error(format!("failed to create_file: {}", err))))?; + err!("failed to create_file: {}", err))?; Ok(WFile { file, db_path, @@ -51,7 +51,7 @@ impl std::io::Write for WFile { impl Writer for WFile { fn finalize(self: Box) -> Result { - Err(Error::error("reader pipline has no digest step")) + err!("reader pipline has no digest step") } fn _finalize(self: Box, oid: ObjectId) -> Result { @@ -59,9 +59,9 @@ impl Writer for WFile { std::fs::create_dir_all( path.parent().expect("cannot access to object parent directory") ).or_else(|err| - Err(Error::error(format!("failed to create object dir: {}", err))))?; + err!("failed to create object dir: {}", err))?; self.file.persist(path).or_else(|err| - Err(Error::error(format!("failed to persist file: {}", err))))?; + err!("failed to persist file: {}", err))?; Ok(oid) } } @@ -86,18 +86,18 @@ mod tests { let dir = tempfile::TempDir::new() .expect("failed to create tmp dir"); - std::fs::create_dir(tmp_dir(dir.path())).expect("failed to create db/obj dir"); - std::fs::create_dir(obj_dir(dir.path())).expect("failed to create db/tmp dir"); + std::fs::create_dir(tmp_dir(dir.path().try_into().unwrap())).expect("failed to create db/obj dir"); + std::fs::create_dir(obj_dir(dir.path().try_into().unwrap())).expect("failed to create db/tmp dir"); { - let mut writer = Box::new(WFile::new(dir.path().to_path_buf()).expect("failed to create WFile")); + let mut writer = Box::new(WFile::new(dir.path().to_path_buf().try_into().unwrap()).expect("failed to create WFile")); writer.write(data).expect("failed to write to WFile"); let oid2 = writer._finalize(oid.clone()).expect("failed to finalize WFile"); assert_eq!(oid2, oid); } - let mut file = std::fs::File::open(obj_path(dir.path(), &oid)).expect("failed to open object file"); + let mut file = std::fs::File::open(obj_path(dir.path().try_into().unwrap(), &oid)).expect("failed to open object file"); let mut buf = Vec::new(); file.read_to_end(&mut buf).expect("failed to read object file"); diff --git a/libbsv/Cargo.toml b/libbsv/Cargo.toml index fe6d4bb..f4ad236 100644 --- a/libbsv/Cargo.toml +++ b/libbsv/Cargo.toml @@ -2,15 +2,12 @@ name = "libbsv" version = "0.1.0" authors = ["Simon Boyé "] -edition = "2018" +edition = "2021" license = "AGPL-3.0-or-later" [dependencies] -thiserror = "1.0.25" -serde = { version = "1.0.106", features = ["derive"] } -toml = "0.5.6" -uuid = { version = "0.8.1", features = ["serde", "v4"] } -tempfile = "3.1.0" -digest = "0.9.0" -sha2 = "0.9.1" -flate2 = "1.0.17" +toml = "0.5.8" +camino = { version = "1.0.7" } +globset = "0.4.9" +cas-core = { path = "../cas-core" } +cas-simple = { path = "../cas-simple" } \ No newline at end of file diff --git a/libbsv/src/simple_db/mod.rs b/libbsv/src/config.rs similarity index 71% rename from libbsv/src/simple_db/mod.rs rename to libbsv/src/config.rs index c304443..8566db5 100644 --- a/libbsv/src/simple_db/mod.rs +++ b/libbsv/src/config.rs @@ -14,11 +14,18 @@ // along with cdb. If not, see . -pub mod db; -pub mod object; +use std::collections::{HashMap}; +use camino::Utf8PathBuf; + +use cas_core::{Cas, Result}; -pub use db::SimpleDb; -pub use object::{ - ObjectReader, -}; +pub trait CasConfig { + fn build_cas(&self) -> Result>; +} + + +pub struct BsvConfig { + cas: Box, + dir_map: HashMap, +} diff --git a/libbsv/src/core/config.rs b/libbsv/src/core/config.rs deleted file mode 100644 index 9200afc..0000000 --- a/libbsv/src/core/config.rs +++ /dev/null @@ -1,34 +0,0 @@ -// 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::path::PathBuf; - -use uuid::Uuid; -use serde::{Serialize, Deserialize}; - - -#[derive(Debug, Serialize, Deserialize)] -pub struct RepositoryConfig { - pub path: PathBuf, - pub device_name: String, - pub uuid: Uuid, -} - - -#[derive(Debug, Serialize, Deserialize)] -pub struct Config { - pub repository: RepositoryConfig, -} diff --git a/libbsv/src/core/error.rs b/libbsv/src/core/error.rs deleted file mode 100644 index 7332121..0000000 --- a/libbsv/src/core/error.rs +++ /dev/null @@ -1,115 +0,0 @@ -// 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::path::PathBuf; - - -pub type Result = std::result::Result>; - -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("failed to create repository: {message}")] - RepositoryCreationFailed { - message: String, - source: Option>, - }, - - #[error("invalid object id: {message}")] - InvalidObjectId { - message: String, - source: Option>, - }, - - #[error("invalid size (got: {size}, expected: {expected})")] - InvalidSize { - size: usize, - expected: usize, - }, - - #[error("non-empty directory ({dir})")] - NonEmptyDirectory { - dir: PathBuf - }, - - #[error("invalid character(s) ({characters})")] - InvalidObjectIdCharacter { - characters: String, - }, - - #[error("invalid object type ({otype:?})")] - InvalidObjectType { - otype: [u8; 4], - }, - - #[error("invalid object size (expected {expected}, got {size})")] - InvalidObjectSize { - size: u64, - expected: u64, - }, - - #[error("unsupported file type")] - UnsupportedFileType, - - #[error("invalid path ({path})")] - InvalidPath { path: PathBuf }, - - #[error("io error{}", format_optional_path(path))] - IoError { - source: std::io::Error, - path: Option, - }, - - #[error("{0}")] - Other(String), -} - - -pub fn repository_creation_failed>(message: M) -> Box { - Box::new(Error::RepositoryCreationFailed { - message: message.into(), - source: None, - }) -} - -pub fn repository_creation_failed_from>(source: Box, message: M) -> Box { - Box::new(Error::RepositoryCreationFailed { - message: message.into(), - source: Some(source), - }) -} - -pub fn invalid_object_id>(message: M) -> Box { - Box::new(Error::InvalidObjectId { - message: message.into(), - source: None, - }) -} - -pub fn invalid_object_id_from>(source: Box, message: M) -> Box { - Box::new(Error::InvalidObjectId { - message: message.into(), - source: Some(source), - }) -} - - -fn format_optional_path(maybe_path: &Option) -> String { - match maybe_path { - Some(path) => { format!(" ({:?})", path) }, - None => { String::new() } - } -} diff --git a/libbsv/src/core/mod.rs b/libbsv/src/core/mod.rs deleted file mode 100644 index 06b2689..0000000 --- a/libbsv/src/core/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -// 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 . - - -pub mod error; -// pub mod config; -// pub mod object_id; -// pub mod object; -// pub mod repository; - - -pub const NAME: &str = env!("CARGO_PKG_NAME"); -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); - - -pub use error::{Error, Result}; -// pub use config::{Config, RepositoryConfig}; -// pub use object_id::ObjectId; -// pub use object::{ObjectType, OTYPE_BLOB, OTYPE_TREE}; -// pub use repository::Repository; diff --git a/libbsv/src/core/object.rs b/libbsv/src/core/object.rs deleted file mode 100644 index 4959820..0000000 --- a/libbsv/src/core/object.rs +++ /dev/null @@ -1,180 +0,0 @@ -// 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::path::{Path}; -use std::fs::Metadata; -use std::os::unix::fs::MetadataExt; - -use crate::core::error::*; -use crate::{ObjectId}; - - -pub type ObjectType = [u8; 4]; - -pub const OTYPE_BLOB: &ObjectType = b"blob"; -pub const OTYPE_TREE: &ObjectType = b"tree"; - -pub fn object_type_from_metadata(md: &Metadata) -> Result { - if md.is_file() { - Ok(*OTYPE_BLOB) - } - else if md.is_dir() { - Ok(*OTYPE_TREE) - } - else { - Err(Error::UnsupportedFileType) - } -} - - -enum PermissionsFlag { - Read = 0x04, - Write = 0x02, - Execute = 0x01, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Permissions { - flags: u8, -} - -impl Default for Permissions { - fn default() -> Permissions { - Permissions { flags: 0 } - } -} - -impl Permissions { - pub fn read_only() -> Permissions { - *Self::default().set_read(true) - } - - pub fn read_write() -> Permissions { - *Self::read_only().set_write(true) - } - - pub fn read_execute() -> Permissions { - *Self::read_only().set_execute(true) - } - - pub fn read_write_execute() -> Permissions { - *Self::read_write().set_execute(true) - } - - pub fn from_unix_mode(mode: u32) -> Permissions { - *Self::default() - .set_read(mode & 0o400 != 0) - .set_write(mode & 0o200 != 0) - .set_execute(mode & 0o100 != 0) - } - - pub fn is_read(&self) -> bool { - self.test_flag(PermissionsFlag::Read) - } - - pub fn is_write(&self) -> bool { - self.test_flag(PermissionsFlag::Write) - } - - pub fn is_execute(&self) -> bool { - self.test_flag(PermissionsFlag::Execute) - } - - pub fn set_read(&mut self, read: bool) -> &mut Self { - self.set_flag(PermissionsFlag::Read, read) - } - - pub fn set_write(&mut self, write: bool) -> &mut Self { - self.set_flag(PermissionsFlag::Write, write) - } - - pub fn set_execute(&mut self, execute: bool) -> &mut Self { - self.set_flag(PermissionsFlag::Execute, execute) - } - - fn test_flag(&self, flag: PermissionsFlag) -> bool { - (self.flags & flag as u8) != 0 - } - - fn set_flag(&mut self, flag: PermissionsFlag, value: bool) -> &mut Self { - if value { self.flags |= flag as u8; } - else { self.flags &= !(flag as u8); } - self - } -} - - -pub struct TreeItem { - otype: ObjectType, - oid: ObjectId, - modification_time: i64, - permissions: Permissions, - name: String, -} - -impl TreeItem { - pub fn new(otype: &ObjectType, oid: &ObjectId, name: &str) -> TreeItem { - TreeItem { - otype: *otype, - oid: oid.clone(), - modification_time: 0, - permissions: Permissions::read_write_execute(), - name: name.into(), - } - } - - pub fn from_file_path_and_id(file_path: &Path, oid: &ObjectId) -> Result { - let md = file_path.symlink_metadata() - .map_err(|err| Error::IoError { - source: err, - path: Some(file_path.to_path_buf()) - })?; - - Ok(TreeItem { - otype: object_type_from_metadata(&md)?, - oid: oid.clone(), - modification_time: md.mtime(), - permissions: Permissions::from_unix_mode(md.mode()), - name: file_path.file_name() - .and_then(|n| n.to_str()) - .ok_or_else(|| Error::InvalidPath { path: file_path.into() })? - .into(), - }) - } - - pub fn object_type(&self) -> ObjectType { - self.otype - } - - pub fn object_id(&self) -> &ObjectId { - &self.oid - } - - pub fn modification_time(&self) -> i64 { - self.modification_time - } - - pub fn permissions(&self) -> &Permissions { - &self.permissions - } - - pub fn name(&self) -> &str { - &self.name - } -} - - -pub type TreeObject = Vec; diff --git a/libbsv/src/core/object_id.rs b/libbsv/src/core/object_id.rs deleted file mode 100644 index 5ac00d6..0000000 --- a/libbsv/src/core/object_id.rs +++ /dev/null @@ -1,118 +0,0 @@ -// 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::fmt; -use std::str::{FromStr}; - -use super::error::*; - - -/// A unique identifier for an object. -/// -/// This is the handle used to reference an Object. This is an opaque type that uniquely identify an -/// object. It can be compared to another ObjectId, be hashed and that's about it. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ObjectId { - id: Vec, -} - - -impl ObjectId { - pub fn new(id: &[u8]) -> ObjectId { - ObjectId { - id: id.into(), - } - } -} - - -impl FromStr for ObjectId { - type Err = Error; - - fn from_str(id_str: &str) -> Result { - if id_str.len() % 2 != 0 { - return Err(Error::InvalidObjectIdSize); - } - - let byte_count = id_str.len() / 2; - let mut id = Vec::with_capacity(byte_count); - - for byte_index in 0..byte_count { - let str_index = byte_index * 2; - let byte_str = id_str.get(str_index..(str_index + 2)) - .ok_or(Error::InvalidObjectIdCharacter)?; - id.push(u8::from_str_radix(byte_str, 16) - .or(Err(Error::InvalidObjectIdCharacter))?); - } - - Ok(ObjectId { - id - }) - } -} - - -impl fmt::Display for ObjectId { - fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> { - for byte in self.id.iter() { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - - -impl fmt::Debug for ObjectId { - fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> { - write!(f, "ObjectId::new(\"{}\")", self) - } -} - - -#[cfg(test)] -mod tests { - use std::str::{FromStr}; - - use super::ObjectId; - - #[test] - fn object_id_display() { - let obj = ObjectId::new(&[ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - ]); - - assert_eq!(format!("{}", obj), "0001020304050607"); - } - - #[test] - fn object_id_debug() { - let obj = ObjectId::new(&[ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - ]); - - assert_eq!(format!("{:?}", obj), "ObjectId::new(\"0001020304050607\")"); - } - - #[test] - fn str_to_object_id() { - let obj = ObjectId::from_str("0001020304050607").unwrap(); - let obj_ref = ObjectId::new(&[ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - ]); - - assert_eq!(obj, obj_ref); - } -} diff --git a/libbsv/src/core/repository.rs b/libbsv/src/core/repository.rs deleted file mode 100644 index b46f842..0000000 --- a/libbsv/src/core/repository.rs +++ /dev/null @@ -1,80 +0,0 @@ -// 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::path::{Path, PathBuf}; -use std::fs::{self, create_dir}; -use std::rc::Rc; - -use uuid::Uuid; - -use super::error::*; -use super::config::{Config, RepositoryConfig}; - -use crate::simple_db::SimpleDb; - - -const CONFIG_FILENAME: &str = ".bsv"; - - -#[derive(Debug)] -pub struct Repository { - config: Rc, - db: SimpleDb, -} - - -impl Repository { - pub fn create(path: &Path, device_name: &str) -> Result { - if path.exists() { - return Err(Error::NonEmptyDirectory { dir: path.to_path_buf() }); - } - - if device_name.is_empty() { - return Err(Error::RepositoryCreationFailed("Device name must not be empty.".to_string())); - } - - create_dir(&path).map_err(|err| Error::IoError("Failed to create repository.")?; - - let config = Rc::new( - Config { - repository: RepositoryConfig { - path: path.into(), - device_name: device_name.into(), - uuid: Uuid::new_v4(), - }, - } - ); - - let config_path = { - let mut config_path = PathBuf::from(path); - config_path.push(CONFIG_FILENAME); - config_path - }; - - fs::write( - config_path, - toml::to_string(&*config) - .chain_err(|| format!("Failed to serialize {}.", CONFIG_FILENAME))?, - ).chain_err(|| - format!("Failed to create repository: failed to write {}.", CONFIG_FILENAME) - )?; - - Ok(Repository { - config: Rc::clone(&config), - db: SimpleDb::new(path)?, - }) - } -} diff --git a/libbsv/src/ignore.rs b/libbsv/src/ignore.rs new file mode 100644 index 0000000..8f78c4e --- /dev/null +++ b/libbsv/src/ignore.rs @@ -0,0 +1,67 @@ +// 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 camino::{Utf8Path, Utf8PathBuf}; +use globset::GlobSet; + +use cas_core::{err, Error, Result}; + + +#[derive(Debug, Clone)] +pub enum Action { + Ignore, + Accept, +} + + +#[derive(Debug)] +pub struct IgnoreRules { + patterns: GlobSet, + actions: Vec, +} + + +impl IgnoreRules { + pub fn new() -> Self { + Self { + patterns: GlobSet::default(), + actions: vec![], + } + } + + pub fn from_vec(vec: Vec<(Action, Utf8PathBuf)>) -> Result { + err!("Todo") + } + + pub fn from_ignore_file(ignore_file: &str, root: &Utf8Path) -> Result { + err!("Todo") + } + + pub fn is_ignored>(&self, path: P) -> bool { + + } +} + + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_map() { + } +} diff --git a/libbsv/src/lib.rs b/libbsv/src/lib.rs index d68827c..2b90c91 100644 --- a/libbsv/src/lib.rs +++ b/libbsv/src/lib.rs @@ -14,20 +14,23 @@ // along with cdb. If not, see . -// #[macro_use] -extern crate thiserror; -extern crate serde; +extern crate toml; +extern crate camino; +extern crate globset; + +extern crate cas_core; -pub mod core; -// pub mod simple_db; +mod permissions; +mod tree_item; +// mod tree_walker; +// mod config; +mod path_map; +// mod ignore; +mod repository; -// pub use crate::core::{ -// Error, Result, -// Config, RepositoryConfig, -// ObjectId, ObjectType, -// Repository, - -// OTYPE_BLOB, OTYPE_TREE, -// }; +pub use crate::permissions::Permissions; +pub use crate::tree_item::{Serialize, TreeItem}; +pub use crate::path_map::{PathMap, PathPair}; +pub use crate::repository::{Repository}; diff --git a/libbsv/src/path_map.rs b/libbsv/src/path_map.rs new file mode 100644 index 0000000..db4f91c --- /dev/null +++ b/libbsv/src/path_map.rs @@ -0,0 +1,255 @@ +// 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 camino::{Utf8Path, Utf8PathBuf}; +use toml::Value; + +use cas_core::{err, Error, Result}; + + +#[derive(Debug, Clone)] +pub struct PathPair { + pub logic: Utf8PathBuf, + pub physic: Utf8PathBuf, +} + +impl PathPair { + pub fn new(logic: Utf8PathBuf, physic: Utf8PathBuf) -> Self { + Self { logic, physic } + } +} + + +#[derive(Debug)] +pub struct PathMap { + pairs: Vec, + logic_order: Vec, + physic_order: Vec, +} + + +impl PathMap { + pub fn new() -> PathMap { + PathMap { + pairs: vec![], + logic_order: vec![], + physic_order: vec![], + } + } + + pub fn from_vec(vec: Vec) -> Result { + if let Some(pair) = + vec.iter() + .filter(|p| !p.logic.is_absolute()) + .next() { + return err!("relative logic path: {}", pair.logic); + } + + let mut logic_order: Vec<_> = (0..vec.len()).collect(); + logic_order.sort_by(|&i0, &i1| vec[i0].logic.cmp(&vec[i1].logic).reverse()); + + if let Some((&index, _)) = + logic_order[..logic_order.len()-1].iter() + .zip(logic_order[1..].iter()) + .filter(|(&i0, &i1)| vec[i0].logic == vec[i1].logic) + .next() { + return err!("duplicate logic path: {:?}", vec[index]); + } + + let mut physic_order: Vec<_> = (0..vec.len()).collect(); + physic_order.sort_by(|&i0, &i1| vec[i0].physic.cmp(&vec[i1].physic).reverse()); + + if let Some((&index, _)) = + physic_order[..physic_order.len()-1].iter() + .zip(physic_order[1..].iter()) + .filter(|(&i0, &i1)| vec[i0].physic == vec[i1].physic) + .next() { + return err!("duplicate physic path: {:?}", vec[index]); + } + + Ok(Self { + pairs: vec, + logic_order, + physic_order, + }) + } + + pub fn from_toml_value(config: &Value) -> Result { + let map = if let Some(mapping) = config.get("mapping") { + if let Some(ref table) = mapping.as_table() { + table.iter() + .map(|(k, v)| Ok(PathPair{ + logic: k.into(), + physic: Utf8PathBuf::from_path_buf( + Utf8Path::new( + v.as_str() + .ok_or_else(|| Error::unknown("mapping values must be strings"))? + ).canonicalize()? + ).or_else(|e| err!("non-unicode character in path: {:?}", e))? + })) + .collect::>()? + } + else { + err!("mapping must be a table, got {}", mapping)? + } + } + else { + vec![] + }; + + Self::from_vec(map) + } + + pub fn physic_from_logic>(&self, logic_path: P) -> Option { + let path = logic_path.as_ref(); + + let pair = self.logic_order.iter() + .map(|&i| &self.pairs[i]) + .filter(|p| path.starts_with(&p.logic)) + .next()?; + + let mut physic = pair.physic.clone(); + physic.push( + path.strip_prefix(&pair.logic) + .ok()? + ); + + return Some(physic) + } + + pub fn logic_from_physic>(&self, physic_path: P) -> Option { + let path = physic_path.as_ref(); + + let pair = self.physic_order.iter() + .map(|&i| &self.pairs[i]) + .filter(|p| path.starts_with(&p.physic)) + .next()?; + + let mut logic = pair.logic.clone(); + logic.push( + path.strip_prefix(&pair.physic) + .ok()? + ); + + return Some(logic) + } +} + + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_map() { + let path_map = PathMap::from_vec(vec![ + PathPair::new("/foo/bar".into(), "/home/user/bar".into()), + ]).unwrap(); + + assert_eq!( + path_map.physic_from_logic("/bar"), + None, + ); + assert_eq!( + path_map.physic_from_logic("/foo/bar"), + Some("/home/user/bar".into()), + ); + assert_eq!( + path_map.physic_from_logic("/foo/bar/file.txt"), + Some("/home/user/bar/file.txt".into()), + ); + + assert_eq!( + path_map.logic_from_physic("/home/user"), + None, + ); + assert_eq!( + path_map.logic_from_physic("/home/user/bar"), + Some("/foo/bar".into()), + ); + assert_eq!( + path_map.logic_from_physic("/home/user/bar/file.txt"), + Some("/foo/bar/file.txt".into()), + ); + } + + #[test] + fn test_path_map_subdirs() { + let path_map = PathMap::from_vec(vec![ + PathPair::new("/foo/bar/baz".into(), "/home/user/bar/test".into()), + PathPair::new("/foo/bar".into(), "/home/user/bar".into()), + ]).unwrap(); + + assert_eq!( + path_map.physic_from_logic("/foo/bar/file.txt"), + Some("/home/user/bar/file.txt".into()), + ); + assert_eq!( + path_map.physic_from_logic("/foo/bar/baz/file.txt"), + Some("/home/user/bar/test/file.txt".into()), + ); + + assert_eq!( + path_map.logic_from_physic("/home/user/bar/file.txt"), + Some("/foo/bar/file.txt".into()), + ); + assert_eq!( + path_map.logic_from_physic("/home/user/bar/test/file.txt"), + Some("/foo/bar/baz/file.txt".into()), + ); + } + + #[test] + fn test_path_map_crossed_subdirs() { + let path_map = PathMap::from_vec(vec![ + PathPair::new("/foo/bar/baz".into(), "/home/user/bar".into()), + PathPair::new("/foo/bar".into(), "/home/user/bar/test".into()), + ]).unwrap(); + + assert_eq!( + path_map.physic_from_logic("/foo/bar/baz/file.txt"), + Some("/home/user/bar/file.txt".into()), + ); + assert_eq!( + path_map.physic_from_logic("/foo/bar/file.txt"), + Some("/home/user/bar/test/file.txt".into()), + ); + + assert_eq!( + path_map.logic_from_physic("/home/user/bar/test/file.txt"), + Some("/foo/bar/file.txt".into()), + ); + assert_eq!( + path_map.logic_from_physic("/home/user/bar/file.txt"), + Some("/foo/bar/baz/file.txt".into()), + ); + } + + + #[test] + fn test_path_fail_on_duplicates() { + PathMap::from_vec(vec![ + PathPair::new("/foo/bar".into(), "/home/user/bar".into()), + PathPair::new("/foo/bar".into(), "/home/user/bar/test".into()), + ]).expect_err("should fail on duplicate logic path"); + + PathMap::from_vec(vec![ + PathPair::new("/foo/bar/baz".into(), "/home/user/bar".into()), + PathPair::new("/foo/bar".into(), "/home/user/bar".into()), + ]).expect_err("should fail on duplicate physic path"); + } +} diff --git a/libbsv/src/permissions.rs b/libbsv/src/permissions.rs new file mode 100644 index 0000000..acc01be --- /dev/null +++ b/libbsv/src/permissions.rs @@ -0,0 +1,140 @@ +// 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 cas_core::{err, Error, Result}; + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Permissions { + pub read: bool, + pub write: bool, + pub execute: bool, +} + +impl Permissions { + #[cfg(not(unix))] + pub fn from_metadata(metadata: &std::fs::Metadata) -> Result { + if metadata.permissions().readonly() { + Ok(Permissions::READ_ONLY) + } + else { + Ok(Permission::READ_WRITE) + } + } + + #[cfg(unix)] + pub fn from_metadata(metadata: &std::fs::Metadata) -> Result { + use std::os::unix::fs::MetadataExt; + let mode = metadata.mode(); + Ok(Self { + read: mode & 0o100 != 0, + write: mode & 0o200 != 0, + execute: mode & 0o400 != 0, + }) + } + + pub const READ_ONLY: Self = Self { read: true, write: false, execute: false }; + pub const READ_WRITE: Self = Self { read: true, write: true, execute: false }; +} + +impl std::fmt::Display for Permissions { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + use std::fmt::Write; + + f.write_char(if self.read { 'r' } else { '-' })?; + f.write_char(if self.write { 'w' } else { '-' })?; + f.write_char(if self.execute { 'x' } else { '-' })?; + + Ok(()) + } +} + +impl std::str::FromStr for Permissions { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut chars = s.chars(); + let read = parse_permission_char(&mut chars, 'r', 0)?; + let write = parse_permission_char(&mut chars, 'w', 0)?; + let execute = parse_permission_char(&mut chars, 'x', 0)?; + if chars.next().is_some() { + return err!("expected 3 characters, got {}", s.len()); + } + + Ok(Self { read, write, execute }) + } +} + +fn parse_permission_char(chars: &mut std::str::Chars, bit_char: char, index: u8) -> Result { + let c = chars.next().ok_or_else(|| Error::unknown(format!("expected 3 characters, got {}", index)))?; + if c != bit_char && c != '-' { + err!("expected character {} or -, got {}", bit_char, c) + } + else { + Ok(c == bit_char) + } +} + + + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use std::string::ToString; + + use super::*; + + #[test] + fn test_permission_display() { + let expected = [ + "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx", + ]; + + for i in 0..8 { + let read = (i & 0x04) != 0; + let write = (i & 0x02) != 0; + let execute = (i & 0x01) != 0; + + let perm = Permissions { read, write, execute }; + let perm_str = perm.to_string(); + + assert_eq!(perm_str, expected[i]); + } + } + + #[test] + fn test_permission_from_str() { + let perms = [ + "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx", + ]; + + for i in 0..8 { + let read = (i & 0x04) != 0; + let write = (i & 0x02) != 0; + let execute = (i & 0x01) != 0; + + let perm = Permissions::from_str(perms[i]).unwrap(); + let expected = Permissions { read, write, execute }; + + assert_eq!(perm, expected); + } + + assert!(Permissions::from_str("rw").is_err()); + assert!(Permissions::from_str("rw--").is_err()); + assert!(Permissions::from_str("-x-").is_err()); + assert!(Permissions::from_str("123").is_err()); + } +} diff --git a/libbsv/src/repository.rs b/libbsv/src/repository.rs new file mode 100644 index 0000000..eac6a90 --- /dev/null +++ b/libbsv/src/repository.rs @@ -0,0 +1,299 @@ +// 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 camino::{Utf8Path, Utf8PathBuf}; +use toml::Value; + +use cas_core::{Cas, err, Error, ObjectId, Result}; +use cas_simple::{SimpleCas}; + +pub use crate::permissions::Permissions; +pub use crate::tree_item::{Serialize, TreeItem}; + + +pub fn create_cas(path: Utf8PathBuf, config: Value) -> Result> { + let engine = config + .get("cas") + .ok_or_else(|| Error::unknown("config must have a cas item"))? + .get("engine") + .ok_or_else(|| Error::unknown("config must have a cas.engine item"))? + .as_str() + .ok_or_else(|| Error::unknown("cas.engine must be a string"))?; + + match engine { + "simple" => { SimpleCas::create(path, config).map(|cas| Box::new(cas) as Box) } + _ => { err!("unknown cas engine {}", engine) } + } +} + + +pub fn open_cas(path: Utf8PathBuf, config: &Value) -> Result> { + let engine = config + .get("cas") + .ok_or_else(|| Error::unknown("config must have a cas item"))? + .get("engine") + .ok_or_else(|| Error::unknown("config must have a cas.engine item"))? + .as_str() + .ok_or_else(|| Error::unknown("cas.engine must be a string"))?; + + match engine { + "simple" => { SimpleCas::open(path).map(|cas| Box::new(cas) as Box) } + _ => { err!("unknown cas engine {}", engine) } + } +} + + +// pub trait FsVisitor { +// fn accept(&self, path: &Utf8Path, metadata: &Metadata) -> bool; +// fn handle_error(&mut self, error: Error) -> Option; + +// fn handle_result(&mut self, result: Result) -> Result +// { +// result.map_err(|error| +// self.handle_error(error) +// .unwrap_or(Error::Skipped) +// ) +// } +// } + + +// fn read_path_map(config: &Value) -> Result> { +// if let Some(mapping) = config.get("mapping") { +// if let Some(ref table) = mapping.as_table() { +// table.iter() +// .map(|(k, v)| Ok(PathPair{ +// logic: k.into(), +// physic: v.as_str() +// .ok_or_else(|| Error::unknown("mapping values must be strings"))? +// .into() +// })) +// .collect() +// } +// else { +// err!("mapping must be a table, got {}", mapping) +// } +// } +// else { +// Ok(vec![]) +// } +// } + + +pub struct Repository { + cas: Box, + // path_map: Vec, +} + +impl Repository { + pub fn create(path: Utf8PathBuf, config: Value) -> Result { + if path.exists() { + return err!("cannot create bsv repository, path {} already exists", path); + } + + let cas = create_cas(path, config)?; + + Ok(Self { + cas, + // path_map: vec![], + }) + } + + pub fn open(path: Utf8PathBuf) -> Result { + if !path.is_dir() { + return err!("failed to open repository, path {} is not a directory", path); + } + + let config_file = cas_simple::utils::config_path(&path); + let config = cas_simple::utils::read_config(&config_file)?; + + let cas = open_cas(path, &config)?; + // let path_map = read_path_map(&config)?; + + Ok(Self { + cas, + // path_map, + }) + } + + pub fn cas(&self) -> &dyn Cas { + self.cas.as_ref() + } + + // pub fn path_pair_from_logic_path>(&self, logic_path: P) -> Option { + // None + // } + + // pub fn path_pair_from_physic_path>(&self, physic_path: P) -> Option { + // None + // } + + pub fn oid_from_logic_path>(&self, logic_path: P) -> Result { + err!("not implemented") + } + + pub fn oid_from_physic_path>(&self, physic_path: P) -> Result { + err!("not implemented") + } + + pub fn read_tree(&self, oid: &ObjectId) -> Result>> { + err!("not implemented") + } + + // pub fn add(&mut self, path: P, visitor: &mut V) -> Result + // where + // P: AsRef, + // V: FsVisitor, + // { + // let path_ref = path.as_ref(); + // let metadata = std::fs::symlink_metadata(path_ref) + // .or_else(|err| err!("failed to read file {}: {}", path_ref, err))?; + + // if visitor.accept(path_ref, &metadata) { + // self.add_impl(path_ref, &metadata, visitor) + // } + // else { + // err!("cannot add {}: path is excluded", path_ref) + // } + // } + + // pub fn add_impl(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result + // where + // V: FsVisitor, + // { + // let file_type = metadata.file_type(); + + // if file_type.is_file() { + // self.add_file(path, metadata) + // } + // else if file_type.is_dir() { + // self.add_tree(path, metadata, visitor) + // } + // else if file_type.is_symlink() { + // err!("symlink are not supported yet") + // } + // else { + // err!("not implemented") + // } + // } + + // pub fn add_tree_item(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result + // where + // V: FsVisitor, + // { + // let item_name = path + // .file_name() + // .ok_or_else(|| Error::unknown("dir entry with no name?"))? + // .to_str() + // .ok_or_else(|| Error::unknown(format!("file name contains non-unicode characters: {}", path)))?; + + // let oid = self.add(&path, visitor)?; + + // TreeItem::from_metadata( + // item_name, + // &metadata, + // oid, + // ) + // } + + // fn add_file(&mut self, path: &Utf8Path, metadata: &Metadata) -> Result { + // if !metadata.is_file() { + // return err!("expected file"); + // } + + // let file_size = metadata.len(); + + // let mut reader = std::fs::File::open(path) + // .or_else(|err| err!("failed to open file {}: {}", path, err))?; + + // let mut writer = self.cas.new_writer( + // &ObjectType::new(b"blob")?, + // file_size + // )?; + + // let mut buffer = [0; BUFFER_SIZE]; + // let mut byte_count = 0u64; + // loop { + // use std::io::{Read, Write}; + // let count = reader.read(&mut buffer) + // .or_else(|err| err!("error while reading {}: {}", path, err))?; + // if count == 0 { + // break; + // } + // byte_count += count as u64; + // writer.write_all(&buffer[..count]) + // .or_else(|err| err!("error while writing blob for {}: {}", path, err))?; + // } + + // if byte_count == file_size { + // writer.finalize() + // } + // else { + // err!("file size changed during processing for {}", path) + // } + // } + + // fn add_tree(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result + // where + // V: FsVisitor, + // { + // if !metadata.is_dir() { + // return err!("expected directory"); + // } + + // let mut tree = std::fs::read_dir(path) + // .or_else(|err| err!("error while reading dir {}: {}", path, err))? + // .filter_map(|entry| self.add_dir_entry(path, entry, visitor)) + // .collect::>>()?; + + // tree.sort_unstable_by(|item0, item1| item0.name.cmp(&item1.name)); + + // let mut out = Vec::new(); + // tree.serialize(&mut out)?; + // self.cas.write_object( + // &ObjectType::new(b"tree")?, + // &out, + // ) + // } + + // fn add_dir_entry(&mut self, dir_path: &Utf8Path, entry: std::io::Result, visitor: &mut V) -> Option> + // where + // V: FsVisitor + // { + // let (entry_path, metadata) = match visitor.handle_result(Self::dir_entry_path_metadata(dir_path, entry)) { + // Ok(path_metadata) => { path_metadata }, + // Err(error) => { return Some(Err(error)) } + // }; + + // if visitor.accept(&entry_path, &metadata) { + // let result = self.add_tree_item(&entry_path, &metadata, visitor); + // Some(visitor.handle_result(result)) + // } + // else { + // None + // } + // } + + // fn dir_entry_path_metadata(dir_path: &Utf8Path, entry: std::io::Result) -> Result<(Utf8PathBuf, Metadata)> { + // let dir_entry = entry + // .or_else(|err| err!("error while reading dir {}: {}", dir_path, err))?; + + // let entry_path = dir_entry.path(); + // let metadata = dir_entry.metadata() + // .or_else(|err| err!("failed to read metadata for dir entry {}: {}", entry_path, err))?; + + // Ok((entry_path, metadata)) + // } +} diff --git a/libbsv/src/simple_db/db.rs b/libbsv/src/simple_db/db.rs deleted file mode 100644 index f2c723a..0000000 --- a/libbsv/src/simple_db/db.rs +++ /dev/null @@ -1,145 +0,0 @@ -// 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 . - - -extern crate tempfile; - -use std::path::{Path, PathBuf}; -use std::io::{Read, Seek}; -use std::fs::{File, OpenOptions, create_dir, create_dir_all}; - -use tempfile::{NamedTempFile}; - -use crate::core::error::*; -use crate::core::ObjectId; -use crate::core::{ObjectType, OTYPE_BLOB}; - -use super::object::{ - ObjectReader, WriteAsObject, -}; - - -const OBJECTS_DIR: &str = "objects"; -const TMP_DIR: &str = "tmp"; - - -#[derive(Debug)] -pub struct SimpleDb { - path: PathBuf, -} - - -impl SimpleDb { - pub fn new(path: &Path) -> Result { - Ok(SimpleDb { - path: path.into(), - }) - } - - - pub fn setup(&self) -> Result<()> { - create_dir(self.objects_dir())?; - create_dir(self.tmp_dir())?; - - Ok(()) - } - - - pub fn has_object(&self, oid: &ObjectId) -> bool { - let obj_path = self.path_from_id(oid); - if let Ok(metadata) = std::fs::metadata(obj_path) { - metadata.is_file() - } - else { - false - } - } - - pub fn store_object(&self, otype: &ObjectType, mut reader: R) - -> Result { - let mut tmp_file = self.create_tmp_file()?; - let oid = reader.write_as_object(&mut tmp_file, otype)?; - let dst_file_path = self.path_from_id(&oid); - - // TODO: Check if dst_file_path exists - - create_dir_all(dst_file_path.parent().unwrap())?; - match tmp_file.persist(dst_file_path) { - Ok(file) => { - file.sync_data()?; - Ok(oid) - }, - Err(err) => Err(err.error.into()), - } - } - - pub fn store_file_as_blob(&self, file_path: &Path) -> Result { - let file = OpenOptions::new().read(true).open(file_path)?; - self.store_object(OTYPE_BLOB, file) - } - - pub fn read_object(&self, oid: &ObjectId) -> Result> { - let path = self.path_from_id(oid); - let file = OpenOptions::new().read(true).open(path)?; - ObjectReader::new(file) - } - - fn objects_dir(&self) -> PathBuf { - let mut path = self.path.clone(); - path.push(OBJECTS_DIR); - path - } - - fn tmp_dir(&self) -> PathBuf { - let mut path = self.path.clone(); - path.push(TMP_DIR); - path - } - - fn path_from_id(&self, oid: &ObjectId) -> PathBuf { - let oid_str = oid.to_string(); - let mut path = self.path.clone(); - - path.push(OBJECTS_DIR); - path.push(oid_str.get(..4).unwrap()); // unwrap is ok here because we know oid_str is a - path.push(oid_str.get(4..).unwrap()); // hexadecimal number (i.e. ascii only). - - path - } - - fn create_tmp_file(&self) -> Result { - let tmp_dir = self.tmp_dir(); - - Ok(tempfile::Builder::new().tempfile_in(tmp_dir)?) - } -} - - -#[cfg(test)] -mod tests { - use std::str::{FromStr}; - - use super::ObjectId; - use super::SimpleDb; - - #[test] - fn simple_db_path_from_id() { - let db = SimpleDb::new(std::path::Path::new(".bsv")).unwrap(); - let oid = ObjectId::from_str("0001020304050607").unwrap(); - - let path = db.path_from_id(&oid); - assert_eq!(path, std::path::PathBuf::from(".bsv/objects/0001/020304050607")); - } -} diff --git a/libbsv/src/simple_db/object.rs b/libbsv/src/simple_db/object.rs deleted file mode 100644 index e07a957..0000000 --- a/libbsv/src/simple_db/object.rs +++ /dev/null @@ -1,248 +0,0 @@ -// 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 . - - -extern crate digest; -extern crate sha2; -extern crate flate2; - -use std::io::{Read, Seek, SeekFrom, Write, copy, sink}; - -use digest::Digest; - -use flate2::{ - Compression, - write::GzEncoder, - read::GzDecoder, -}; - -use crate::core::error::*; -use crate::core::ObjectId; -use crate::core::ObjectType; - - -pub struct ObjectWriter { - writer: GzEncoder, - digest: D, - size: u64, - written_size: u64, -} - - -impl ObjectWriter { - pub fn new(writer: W, otype: &ObjectType, size: u64) - -> Result> { - - let mut digest = D::new(); - - digest.update(otype); - digest.update(&size.to_be_bytes()); - - let mut zwriter = GzEncoder::new(writer, Compression::default()); - - zwriter.write_all(otype)?; - zwriter.write_all(&size.to_be_bytes())?; - - Ok(ObjectWriter { - writer: zwriter, - digest, - size, - written_size: 0, - }) - } - - pub fn finish(mut self) -> Result<(ObjectId, W)> { - self.writer.try_finish()?; - - if self.written_size == self.size { - Ok(( - ObjectId::new(&self.digest.finalize()), - self.writer.finish()?, - )) - } - else { - Err(ErrorKind::MismatchingObjectSize(self.written_size, self.size).into()) - } - } -} - - -impl Write for ObjectWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let size = self.writer.write(buf)?; - self.digest.update(&buf[..size]); - self.written_size += size as u64; - Ok(size) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.writer.flush() - } -} - - -pub struct ObjectReader { - otype: ObjectType, - size: u64, - reader: GzDecoder, -} - - -impl ObjectReader { - pub fn new(reader: R) - -> Result> { - - let mut zreader = GzDecoder::new(reader); - - let mut buffer = [0u8; 12]; - zreader.read_exact(&mut buffer)?; - - let otype = { - let mut otype = [0; 4]; - otype.copy_from_slice(&buffer[0..4]); - otype - }; - let size = { - let mut size_bytes = [0; 8]; - size_bytes.copy_from_slice(&buffer[4..12]); - u64::from_be_bytes(size_bytes) - }; - - Ok(ObjectReader { - otype, - size, - reader: zreader, - }) - } - - pub fn object_type(&self) -> ObjectType { - self.otype - } - - pub fn size(&self) -> u64 { - self.size - } - - pub fn close(self) -> Result { - Ok(self.reader.into_inner()) - } -} - - -impl Read for ObjectReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - self.reader.read(buf) - } -} - - -pub fn write_object(reader: &mut R, writer: W, - otype: &ObjectType, size: u64) - -> Result<(ObjectId, W)> where - R: Read, - W: Write { - let mut owriter: ObjectWriter - = ObjectWriter::new(writer, otype, size)?; - - copy(reader, &mut owriter)?; - - owriter.finish() -} - - -pub trait WriteAsObject { - fn write_as_object(&mut self, writer: W, otype: &ObjectType) -> Result; - - fn object_id(&mut self, otype: &ObjectType) -> Result { - self.write_as_object(sink(), otype) - } -} - - -impl WriteAsObject for T { - fn write_as_object(&mut self, writer: W, otype: &ObjectType) -> Result { - let start = self.seek(SeekFrom::Current(0))?; - let end = self.seek(SeekFrom::End(0))?; - self.seek(SeekFrom::Start(start))?; - - let (oid, _) = write_object(self, writer, otype, end - start)?; - Ok(oid) - } -} - - -#[cfg(test)] -mod tests { - use std::str::{FromStr}; - - use crate::core::OTYPE_BLOB; - - use super::*; - - const PAYLOAD: &[u8; 12] = b"Hello World!"; - const PAYLOAD_OID: &str = "c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083"; - - #[test] - fn object_read_write() -> Result<()> { - use std::io::{Cursor, Seek, SeekFrom}; - - let mut source = Cursor::new(PAYLOAD); - let mut fake_file = Cursor::new(vec![]); - let mut output = vec![]; - - let (oid, _) = write_object(&mut source, &mut fake_file, - OTYPE_BLOB, PAYLOAD.len() as u64)?; - - assert_eq!(oid, ObjectId::from_str(PAYLOAD_OID)?); - - fake_file.seek(SeekFrom::Start(0))?; - let mut reader = ObjectReader::new(fake_file)?; - let read_size = reader.read_to_end(&mut output)?; - - assert_eq!(reader.object_type(), *OTYPE_BLOB); - assert_eq!(reader.size(), PAYLOAD.len() as u64); - assert_eq!(read_size, PAYLOAD.len()); - assert_eq!(output, PAYLOAD); - - Ok(()) - } - - #[test] - fn object_write_invalid_size() { - let mut source = PAYLOAD.as_ref(); - let fake_file = vec![]; - - let result = write_object(&mut source, fake_file, OTYPE_BLOB, 13); - - match result { - Err(Error(ErrorKind::MismatchingObjectSize(actual, expected), _)) - if actual == 12 && expected == 13 => (), - Err(error) => panic!("Unexpected error: {:?}", error), - Ok(_) => panic!("Unexpected success"), - } - } - - #[test] - fn write_as_object() -> Result<()> { - use std::io::{Cursor, sink}; - - let mut source = Cursor::new(PAYLOAD); - let oid = source.write_as_object(sink(), OTYPE_BLOB)?; - - assert_eq!(oid, ObjectId::from_str(PAYLOAD_OID)?); - - Ok(()) - } -} diff --git a/libbsv/src/tree_item.rs b/libbsv/src/tree_item.rs new file mode 100644 index 0000000..7bb3ebd --- /dev/null +++ b/libbsv/src/tree_item.rs @@ -0,0 +1,243 @@ +// 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::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::io::{BufRead, Write}; + +use cas_core::{err, Error, ObjectId, ObjectType, Result}; + +use crate::Permissions; + + +pub trait Serialize { + fn serialize(&self, out: &mut W) -> Result<()>; +} + +pub trait Deserialize { + fn deserialize_with_buf(stream: &mut R, buf: &mut Vec) -> Result + where Self: Sized; + + fn deserialize(stream: &mut R) -> Result + where Self: Sized + { + let mut buf = Vec::new(); + Self::deserialize_with_buf(stream, &mut buf) + } +} + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TreeItem { + pub name: String, + pub otype: ObjectType, + pub size: u64, + pub created: SystemTime, + pub modified: SystemTime, + pub permissions: Permissions, + pub oid: ObjectId, +} + +impl TreeItem { + pub fn from_metadata(name: &str, metadata: &std::fs::Metadata, oid: ObjectId) -> Result { + let otype = otype_from_metadata(metadata)?; + let permissions = Permissions::from_metadata(metadata)?; + + Ok(Self { + name: name.to_string(), + otype, + size: metadata.len(), + created: metadata.created().unwrap_or(UNIX_EPOCH), + modified: metadata.modified().unwrap_or(UNIX_EPOCH), + permissions, + oid, + }) + } +} + +impl Serialize for TreeItem { + fn serialize(&self, out: &mut W) -> Result<()> { + // TODO: Check that name do not contain invalid characters + writeln!(out, "{}\t{}\t{}\t{}\t{}\t{}\t{}/", + self.oid, + self.otype, + self.size, + self.permissions, + self.created.duration_since(UNIX_EPOCH) + .or_else(|err| err!("failed to serialize creation time while serializing tree item: {}", err))? + .as_millis(), + self.modified.duration_since(UNIX_EPOCH) + .or_else(|err| err!("failed to serialize creation time while serializing tree item: {}", err))? + .as_millis(), + self.name, + ).or_else(|err| err!("error while serializing item: {}", err)) + } +} + +impl Deserialize for TreeItem { + fn deserialize_with_buf(stream: &mut R, buf: &mut Vec) -> Result { + let oid: ObjectId = read_field_parse(stream, buf, "object ID", b'\t')?; + read_field(stream, buf, "object type", b'\t')?; + let otype = ObjectType::new(buf)?; + let size: u64 = read_field_parse(stream, buf, "object type", b'\t')?; + let permissions: Permissions = read_field_parse(stream, buf, "permissions", b'\t')?; + let created = UNIX_EPOCH + Duration::from_millis( + read_field_parse(stream, buf, "creation date", b'\t')? + ); + let modified = UNIX_EPOCH + Duration::from_millis( + read_field_parse(stream, buf, "modification date", b'\t')? + ); + let name = read_field_str(stream, buf, "name", b'/')? + .to_string(); + + stream.read_exact(&mut buf[..1]) + .or_else(|err| err!("failed to read new line character: {}", err))?; + if buf[0] != b'\n' { + err!("expected new line character, got {:x}", buf[0]) + } + else { + Ok(TreeItem { + name, + otype, + size, + created, + modified, + permissions, + oid, + }) + } + } +} + +impl Serialize for [TreeItem] { + fn serialize(&self, out: &mut W) -> Result<()> { + // assert!(self.is_sorted_by_key(|item| &item.name)); + + for item in self.iter() { + item.serialize(out)? + } + + Ok(()) + } +} + + +pub fn otype_from_metadata(metadata: &std::fs::Metadata) -> Result { + let file_type = metadata.file_type(); + + if file_type.is_file() { + Ok(ObjectType::new(b"blob")?) + } + else if file_type.is_dir() { + Ok(ObjectType::new(b"tree")?) + } + else if file_type.is_symlink() { + Ok(ObjectType::new(b"link")?) + } + else { + err!("unsupported file type, must be file, dir or symlink") + } +} + + +fn read_field(stream: &mut R, buf: &mut Vec, field_name: &str, byte: u8) -> Result<()> { + buf.clear(); + stream.read_until(byte, buf) + .or_else(|err| err!("failed to read TreeItem {}: {}", field_name, err))?; + buf.pop(); + Ok(()) +} + +fn read_field_str<'a, R: BufRead>(stream: &mut R, buf: &'a mut Vec, field_name: &str, byte: u8) -> Result<&'a str> { + read_field(stream, buf, field_name, byte)?; + std::str::from_utf8(buf) + .or_else(|err| err!("TreeItem {} is not valid utf-8: {}", field_name, err)) +} + +fn read_field_parse(stream: &mut R, buf: &mut Vec, field_name: &str, byte: u8) -> Result + where + R: BufRead, + I: std::str::FromStr, + ::Err: std::fmt::Display +{ + let int_str = read_field_str(stream, buf, field_name, byte)?; + I::from_str(int_str) + .or_else(|err| err!("failed to parse TreeItem {}: {}", field_name, err)) +} + + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use std::string::ToString; + + use super::*; + + #[test] + fn test_serialize_tree_item() { + let item = TreeItem { + name: "Test $¢ह€한".to_string(), + otype: ObjectType::new(b"test").unwrap(), + size: 42, + created: UNIX_EPOCH + Duration::from_secs(1234), + modified: UNIX_EPOCH + Duration::from_secs(5678), + permissions: Permissions { read: true, write: false, execute: true }, + oid: ObjectId::from_str("0123456789abcdef").unwrap(), + }; + let expected = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes(); + + let mut result = Vec::new(); + item.serialize(&mut result).unwrap(); + + assert_eq!(result, expected); + } + + #[test] + fn test_deserialize_tree_item() { + use std::io::Cursor; + + let item_bytes = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes(); + let mut item_cursor = Cursor::new(item_bytes); + let expected = TreeItem { + name: "Test $¢ह€한".to_string(), + otype: ObjectType::new(b"test").unwrap(), + size: 42, + created: UNIX_EPOCH + Duration::from_secs(1234), + modified: UNIX_EPOCH + Duration::from_secs(5678), + permissions: Permissions { read: true, write: false, execute: true }, + oid: ObjectId::from_str("0123456789abcdef").unwrap(), + }; + + let item = TreeItem::deserialize(&mut item_cursor).unwrap(); + + assert_eq!(item, expected); + + assert!(TreeItem::deserialize(&mut Cursor::new( + "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/".as_bytes() + )).is_err()); + assert!(TreeItem::deserialize(&mut Cursor::new( + "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/bar/\n".as_bytes() + )).is_err()); + assert!(TreeItem::deserialize(&mut Cursor::new( + "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\t\n".as_bytes() + )).is_err()); + assert!(TreeItem::deserialize(&mut Cursor::new( + "0123456789abcdef\ttest\t42\tr-x\t5678000\tTest $¢ह€한\t\n".as_bytes() + )).is_err()); + assert!(TreeItem::deserialize(&mut Cursor::new( + "0123456789abcdef\ttest\tab\tr-x\t1234000\t5678000\tTest $¢ह€한\t\n".as_bytes() + )).is_err()); + } +} diff --git a/libbsv/src/tree_walker.rs b/libbsv/src/tree_walker.rs new file mode 100644 index 0000000..7fafba6 --- /dev/null +++ b/libbsv/src/tree_walker.rs @@ -0,0 +1,143 @@ +// 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::path::Utf8Path; +// use std::fs::Metadata; +use std::fs::{DirEntry, ReadDir, read_dir}; + +use cas_core::{err, Error, Result}; + +// use crate::{PathPair, Repository, TreeItem}; + + +#[derive(Debug)] +pub struct RecursiveDirIterator { + dir_iterators: Vec, +} + +impl RecursiveDirIterator { + pub fn new>(root_dir: P) -> Result { + Ok(RecursiveDirIterator { + dir_iterators: vec![read_dir(root_dir)?], + }) + } + + pub fn pop_dir(&mut self) -> Result<()> { + match self.dir_iterators.pop() { + Some(_) => Ok(()), + None => err!("cannot pop directory: iterator reached the end"), + } + } +} + +impl Iterator for RecursiveDirIterator { + type Item = Result; + + fn next(&mut self) -> Option { + while let Some(top_it) = self.dir_iterators.last_mut() { + let next = top_it.next(); + if let Some(item) = next { + let item = item.and_then(|dir_entry| { + if let Ok(file_type) = dir_entry.file_type() { + if file_type.is_dir() { + self.dir_iterators.push(read_dir(dir_entry.path())?) + } + } + Ok(dir_entry) + }); + return Some(item.map_err(Into::into)); + } + else { + self.dir_iterators.pop(); + } + } + None + } +} + + +// pub trait FsWalker { +// fn visit(&self) +// } + + +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// pub enum Action { +// Default, +// Add, +// Update, +// Remove, +// Skip, +// Ignore, +// } + + +// #[derive()] +// pub struct TreeWalker<'repo> { +// repository: &'repo Repository, +// rules: Vec) -> Result>>, +// default_action: Action, +// reporters: Vec) -> Result<()>>>, +// } + + +// impl<'repo> TreeWalker<'repo> { +// pub fn process>(&self, physic_path: P) -> Result> +// { +// let physic_path_ref = physic_path.as_ref(); + +// let metadata = std::fs::symlink_metadata(physic_path_ref) +// .or_else(|err| err!("failed to read {}: {}", physic_path_ref, err))?; + +// let path_pair = match self.repository.path_pair_from_physic_path(physic_path)? { +// Some(path_pair) => path_pair, +// None => return Ok(None), +// }; + +// let maybe_tree = self.repository +// .oid_from_logic_path(&path_pair.logic) +// .ok() +// .map(|oid| self.repository.read_tree(&oid)) +// .transpose()? +// .flatten(); + +// self.process_impl(&path_pair, &metadata, &maybe_tree) +// .map(|oid| Some(oid)) +// } + +// fn process_impl(&self, path_pair: &PathPair, metadata: &Metadata, maybe_tree: &Option>) -> Result { +// err!("not implemented") +// } + +// fn eval_rules(&self, path: &Utf8Path, maybe_item: Option<&TreeItem>) -> Result { +// self.rules.iter() +// .map(|rule| +// rule(path, maybe_item) +// ) +// .skip_while(|action_result| +// *action_result == Ok(Action::Default) +// ) +// .nth(0) +// .unwrap_or(Ok(self.default_action)) +// } + +// fn report(&self, path: &Utf8Path, action: Action, maybe_oid: Option<&ObjectId>) -> Result<()> { +// for reporter in &self.reporters { +// reporter(path, action, maybe_oid)? +// } +// Ok(()) +// } +// } diff --git a/libbsv/tests/simple_db.rs b/libbsv/tests/simple_db.rs deleted file mode 100644 index 7d86536..0000000 --- a/libbsv/tests/simple_db.rs +++ /dev/null @@ -1,90 +0,0 @@ -// 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 . - - -extern crate tempfile; -extern crate libbsv; - - -use std::str::{FromStr}; -use std::path::{PathBuf}; -use std::io::{Cursor, Read}; -use std::fs::{create_dir_all, write}; - -use tempfile::{TempDir}; - -use libbsv::{ - Result, - ObjectId, - OTYPE_BLOB, - simple_db::{ - SimpleDb, - } -}; - - -const PAYLOAD: &[u8; 12] = b"Hello World!"; -const PAYLOAD_OID: &str = "c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083"; - - -#[test] -fn simple_db_has_object() { - let temp_dir = TempDir::new().unwrap(); - let db = SimpleDb::new(temp_dir.path()).unwrap(); - db.setup().unwrap(); - - let oid = ObjectId::from_str("0001020304050607").unwrap(); - - assert!(!db.has_object(&oid)); - - let mut object_path: PathBuf = temp_dir.path().into(); - object_path.push("objects"); - object_path.push("0001"); - - create_dir_all(&object_path).unwrap(); - - object_path.push("020304050607"); - - write(object_path, "test").unwrap(); - - assert!(db.has_object(&oid)); -} - -#[test] -fn simple_db_store_read() -> Result<()> { - let expected_oid = ObjectId::from_str(PAYLOAD_OID)?; - let temp_dir = TempDir::new()?; - let db = SimpleDb::new(temp_dir.path())?; - db.setup()?; - - assert!(!db.has_object(&expected_oid)); - - let oid = db.store_object(OTYPE_BLOB, Cursor::new(PAYLOAD))?; - - assert!(db.has_object(&expected_oid)); - assert_eq!(oid, expected_oid); - - let mut reader = db.read_object(&oid)?; - - assert_eq!(reader.object_type(), *OTYPE_BLOB); - assert_eq!(reader.size(), PAYLOAD.len() as u64); - - let mut data = vec![]; - reader.read_to_end(&mut data)?; - - assert_eq!(data, *PAYLOAD); - - Ok(()) -}