Better error management, Utf8Paths & other stuff.

This commit is contained in:
2022-08-08 11:40:45 +02:00
parent b1ceaaf636
commit 5d08b1ea57
33 changed files with 1483 additions and 1216 deletions

View File

@@ -2,15 +2,12 @@
name = "libbsv"
version = "0.1.0"
authors = ["Simon Boyé <sim.boye@gmail.com>"]
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" }

View File

@@ -14,11 +14,18 @@
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
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<Box<dyn Cas>>;
}
pub struct BsvConfig {
cas: Box<dyn CasConfig>,
dir_map: HashMap<Utf8PathBuf, Utf8PathBuf>,
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
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,
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Box<Error>>;
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to create repository: {message}")]
RepositoryCreationFailed {
message: String,
source: Option<Box<dyn std::error::Error>>,
},
#[error("invalid object id: {message}")]
InvalidObjectId {
message: String,
source: Option<Box<dyn std::error::Error>>,
},
#[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<PathBuf>,
},
#[error("{0}")]
Other(String),
}
pub fn repository_creation_failed<M: Into<String>>(message: M) -> Box<Error> {
Box::new(Error::RepositoryCreationFailed {
message: message.into(),
source: None,
})
}
pub fn repository_creation_failed_from<M: Into<String>>(source: Box<dyn std::error::Error>, message: M) -> Box<Error> {
Box::new(Error::RepositoryCreationFailed {
message: message.into(),
source: Some(source),
})
}
pub fn invalid_object_id<M: Into<String>>(message: M) -> Box<Error> {
Box::new(Error::InvalidObjectId {
message: message.into(),
source: None,
})
}
pub fn invalid_object_id_from<M: Into<String>>(source: Box<dyn std::error::Error>, message: M) -> Box<Error> {
Box::new(Error::InvalidObjectId {
message: message.into(),
source: Some(source),
})
}
fn format_optional_path(maybe_path: &Option<PathBuf>) -> String {
match maybe_path {
Some(path) => { format!(" ({:?})", path) },
None => { String::new() }
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
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;

View File

@@ -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 <https://www.gnu.org/licenses/>.
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<ObjectType> {
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<TreeItem> {
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<TreeItem>;

View File

@@ -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 <https://www.gnu.org/licenses/>.
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<u8>,
}
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<ObjectId> {
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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
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<Config>,
db: SimpleDb,
}
impl Repository {
pub fn create(path: &Path, device_name: &str) -> Result<Repository> {
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)?,
})
}
}

67
libbsv/src/ignore.rs Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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<Action>,
}
impl IgnoreRules {
pub fn new() -> Self {
Self {
patterns: GlobSet::default(),
actions: vec![],
}
}
pub fn from_vec(vec: Vec<(Action, Utf8PathBuf)>) -> Result<Self> {
err!("Todo")
}
pub fn from_ignore_file(ignore_file: &str, root: &Utf8Path) -> Result<Self> {
err!("Todo")
}
pub fn is_ignored<P: AsRef<Utf8PathBuf>>(&self, path: P) -> bool {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_map() {
}
}

View File

@@ -14,20 +14,23 @@
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
// #[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};

255
libbsv/src/path_map.rs Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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<PathPair>,
logic_order: Vec<usize>,
physic_order: Vec<usize>,
}
impl PathMap {
pub fn new() -> PathMap {
PathMap {
pairs: vec![],
logic_order: vec![],
physic_order: vec![],
}
}
pub fn from_vec(vec: Vec<PathPair>) -> Result<Self> {
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<Self> {
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::<Result<_>>()?
}
else {
err!("mapping must be a table, got {}", mapping)?
}
}
else {
vec![]
};
Self::from_vec(map)
}
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
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<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> {
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");
}
}

140
libbsv/src/permissions.rs Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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<Self> {
if metadata.permissions().readonly() {
Ok(Permissions::READ_ONLY)
}
else {
Ok(Permission::READ_WRITE)
}
}
#[cfg(unix)]
pub fn from_metadata(metadata: &std::fs::Metadata) -> Result<Self> {
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<Self> {
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<bool> {
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());
}
}

299
libbsv/src/repository.rs Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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<Box<dyn Cas>> {
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<dyn Cas>) }
_ => { err!("unknown cas engine {}", engine) }
}
}
pub fn open_cas(path: Utf8PathBuf, config: &Value) -> Result<Box<dyn Cas>> {
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<dyn Cas>) }
_ => { err!("unknown cas engine {}", engine) }
}
}
// pub trait FsVisitor {
// fn accept(&self, path: &Utf8Path, metadata: &Metadata) -> bool;
// fn handle_error(&mut self, error: Error) -> Option<Error>;
// fn handle_result<T>(&mut self, result: Result<T>) -> Result<T>
// {
// result.map_err(|error|
// self.handle_error(error)
// .unwrap_or(Error::Skipped)
// )
// }
// }
// fn read_path_map(config: &Value) -> Result<Vec<PathPair>> {
// 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<dyn Cas>,
// path_map: Vec<PathPair>,
}
impl Repository {
pub fn create(path: Utf8PathBuf, config: Value) -> Result<Self> {
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<Self> {
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<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<PathPair> {
// None
// }
// pub fn path_pair_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<PathPair> {
// None
// }
pub fn oid_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Result<ObjectId> {
err!("not implemented")
}
pub fn oid_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<ObjectId> {
err!("not implemented")
}
pub fn read_tree(&self, oid: &ObjectId) -> Result<Option<Vec<TreeItem>>> {
err!("not implemented")
}
// pub fn add<P, V>(&mut self, path: P, visitor: &mut V) -> Result<ObjectId>
// where
// P: AsRef<Utf8Path>,
// 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<V>(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result<ObjectId>
// 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<V>(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result<TreeItem>
// 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<ObjectId> {
// 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<V>(&mut self, path: &Utf8Path, metadata: &Metadata, visitor: &mut V) -> Result<ObjectId>
// 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::<Result<Vec<_>>>()?;
// 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<V>(&mut self, dir_path: &Utf8Path, entry: std::io::Result<std::fs::DirEntry>, visitor: &mut V) -> Option<Result<TreeItem>>
// 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<std::fs::DirEntry>) -> 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))
// }
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
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<SimpleDb> {
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<R: Read + Seek>(&self, otype: &ObjectType, mut reader: R)
-> Result<ObjectId> {
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<ObjectId> {
let file = OpenOptions::new().read(true).open(file_path)?;
self.store_object(OTYPE_BLOB, file)
}
pub fn read_object(&self, oid: &ObjectId) -> Result<ObjectReader<File>> {
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<NamedTempFile> {
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"));
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
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<W: Write, D: Digest> {
writer: GzEncoder<W>,
digest: D,
size: u64,
written_size: u64,
}
impl<W: Write, D: Digest> ObjectWriter<W, D> {
pub fn new(writer: W, otype: &ObjectType, size: u64)
-> Result<ObjectWriter<W, D>> {
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<W: Write, D: Digest> Write for ObjectWriter<W, D> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<R: Read> {
otype: ObjectType,
size: u64,
reader: GzDecoder<R>,
}
impl<R: Read> ObjectReader<R> {
pub fn new(reader: R)
-> Result<ObjectReader<R>> {
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<R> {
Ok(self.reader.into_inner())
}
}
impl<R: Read> Read for ObjectReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.reader.read(buf)
}
}
pub fn write_object<R, W>(reader: &mut R, writer: W,
otype: &ObjectType, size: u64)
-> Result<(ObjectId, W)> where
R: Read,
W: Write {
let mut owriter: ObjectWriter<W, sha2::Sha224>
= ObjectWriter::new(writer, otype, size)?;
copy(reader, &mut owriter)?;
owriter.finish()
}
pub trait WriteAsObject {
fn write_as_object<W: Write>(&mut self, writer: W, otype: &ObjectType) -> Result<ObjectId>;
fn object_id(&mut self, otype: &ObjectType) -> Result<ObjectId> {
self.write_as_object(sink(), otype)
}
}
impl<T: Read + Seek> WriteAsObject for T {
fn write_as_object<W: Write>(&mut self, writer: W, otype: &ObjectType) -> Result<ObjectId> {
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(())
}
}

243
libbsv/src/tree_item.rs Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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<W: Write>(&self, out: &mut W) -> Result<()>;
}
pub trait Deserialize {
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self>
where Self: Sized;
fn deserialize<R: BufRead>(stream: &mut R) -> Result<Self>
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<Self> {
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<W: Write>(&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<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self> {
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<W: Write>(&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<ObjectType> {
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<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>, 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<u8>, 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<R, I>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<I>
where
R: BufRead,
I: std::str::FromStr,
<I as 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());
}
}

143
libbsv/src/tree_walker.rs Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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<ReadDir>,
}
impl RecursiveDirIterator {
pub fn new<P: AsRef<Utf8Path>>(root_dir: P) -> Result<RecursiveDirIterator> {
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<DirEntry>;
fn next(&mut self) -> Option<Self::Item> {
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<Box<dyn Fn(&Utf8Path, Option<&TreeItem>) -> Result<Action>>>,
// default_action: Action,
// reporters: Vec<Box<dyn Fn(&Utf8Path, Action, Option<&ObjectId>) -> Result<()>>>,
// }
// impl<'repo> TreeWalker<'repo> {
// pub fn process<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<Option<ObjectId>>
// {
// 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<Vec<TreeItem>>) -> Result<ObjectId> {
// err!("not implemented")
// }
// fn eval_rules(&self, path: &Utf8Path, maybe_item: Option<&TreeItem>) -> Result<Action> {
// 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(())
// }
// }

View File

@@ -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 <https://www.gnu.org/licenses/>.
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(())
}