Compare commits

...

6 Commits

Author SHA1 Message Date
25c7d0d470 Mostly improved tree walking I guess. 2023-10-21 19:55:17 +02:00
Simon Boyé
2b27b788e5 Test TreeWalker + some fixes. 2023-08-12 23:52:26 +02:00
Simon Boyé
7230127f26 Fixed a mistake in copyright disclamers. 2023-03-04 18:06:22 +01:00
Simon Boyé
956d2d337c TreeWalker WIP. 2023-03-04 18:02:30 +01:00
5cca1829ad Implement ignore rules. 2022-08-15 22:52:27 +02:00
5d08b1ea57 Better error management, Utf8Paths & other stuff. 2022-08-08 11:40:45 +02:00
41 changed files with 2127 additions and 1447 deletions

325
Cargo.lock generated
View File

@@ -2,12 +2,27 @@
# 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bitflags"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "block-buffer"
version = "0.9.0"
@@ -17,10 +32,17 @@ dependencies = [
"generic-array",
]
[[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 +52,7 @@ dependencies = [
name = "cas-simple"
version = "0.1.0"
dependencies = [
"camino",
"cas-core",
"digest",
"sha2",
@@ -37,6 +60,15 @@ dependencies = [
"toml",
]
[[package]]
name = "cc"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -61,6 +93,39 @@ dependencies = [
"generic-array",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fastrand"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]]
name = "generic-array"
version = "0.14.4"
@@ -72,21 +137,50 @@ dependencies = [
]
[[package]]
name = "getrandom"
version = "0.2.3"
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"cfg-if",
"libc",
"wasi",
"equivalent",
"hashbrown",
]
[[package]]
name = "libbsv"
version = "0.1.0"
dependencies = [
"camino",
"cas-core",
"cas-simple",
"regex",
"tempfile",
"toml",
]
[[package]]
name = "libc"
version = "0.2.98"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "linux-raw-sys"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "opaque-debug"
@@ -94,12 +188,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "ppv-lite86"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro2"
version = "1.0.27"
@@ -118,69 +206,59 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.2.9"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
"bitflags 1.2.1",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
name = "regex"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"winapi",
"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 = "rustix"
version = "0.38.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
dependencies = [
"bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.127"
version = "1.0.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "sha2"
@@ -208,16 +286,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.2.0"
version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651"
dependencies = [
"cfg-if",
"libc",
"rand",
"fastrand",
"redox_syscall",
"remove_dir_all",
"winapi",
"rustix",
"windows-sys",
]
[[package]]
@@ -242,11 +319,36 @@ dependencies = [
[[package]]
name = "toml"
version = "0.5.8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
@@ -268,29 +370,76 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
"windows-targets",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
name = "windows-targets"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d"
dependencies = [
"memchr",
]

View File

@@ -3,6 +3,6 @@
members = [
"cas-core",
"cas-simple",
# "libbsv",
"libbsv",
# "bsv",
]

View File

@@ -2,7 +2,7 @@
name = "bsv"
version = "0.1.0"
authors = ["Simon Boyé <sim.boye@gmail.com>"]
edition = "2018"
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]

View File

@@ -5,13 +5,13 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use std::collections::{hash_map, HashMap};

View File

@@ -5,13 +5,13 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use std::path::PathBuf;

View File

@@ -5,13 +5,13 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
pub mod command;

View File

@@ -5,13 +5,13 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
#[macro_use]

12
bsvfs/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "cas-core"
version = "0.1.0"
authors = ["Simon Boyé <sim.boye@gmail.com>"]
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
thiserror = "1.0.25"
camino = { version = "1.0.7" }
[dev-dependencies]

0
bsvfs/src/lib.rs Normal file
View File

View File

@@ -2,12 +2,13 @@
name = "cas-core"
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"
digest = { version = "0.9.0", features = ["alloc"] }
camino = { version = "1.0.7" }
[dev-dependencies]
sha2 = "0.9.5"

View File

@@ -5,17 +5,18 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
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<ObjectId> {
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,10 @@ pub trait Cas {
}
pub trait RefStore {
fn get_ref<P: AsRef<Path>>(&self, key: P) -> Result<ObjectId>;
fn set_ref<P: AsRef<Path>>(&mut self, key: P, value: &ObjectId) -> Result<()>;
fn remove_ref<P: AsRef<Path>>(&mut self, key: P) -> Result<()>;
fn get_ref(&self, key: &Utf8Path) -> Result<ObjectId>;
fn set_ref(&mut self, key: &Utf8Path, value: &ObjectId) -> Result<()>;
fn remove_ref(&mut self, key: &Utf8Path) -> Result<()>;
}
pub trait CasWithRef: Cas + RefStore {
}

View File

@@ -5,24 +5,28 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
// use std::path::PathBuf;
// use std::path::Utf8PathBuf;
use std::path::StripPrefixError;
use crate::ObjectId;
/// Result type used through cas-core.
pub type Result<T> = std::result::Result<T, Box<Error>>;
pub type Result<T> = std::result::Result<T, Error>;
/// 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 +48,7 @@ pub enum Error {
// #[error("non-empty directory ({dir})")]
// NonEmptyDirectory {
// dir: PathBuf
// dir: Utf8PathBuf
// },
// #[error("invalid character(s) ({characters})")]
@@ -67,16 +71,31 @@ 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<PathBuf>,
// path: Option<Utf8PathBuf>,
// },
#[error("item skipped")]
Skipped,
#[error("logic error: {0}")]
LogicError(String),
#[error("io error: {0}")]
IoError(#[from] std::io::Error),
#[error("non-unicode file name: '{0}'")]
NonUnicodeFileName(String),
#[error("object {0} does not exists")]
ObjectDoesNotExists(ObjectId),
#[error("{0}")]
Error(String),
UnknownError(String),
}
impl Error {
@@ -108,17 +127,22 @@ impl Error {
// })
// }
pub fn error<M: Into<String>>(message: M) -> Box<Error> {
Box::new(Error::Error(message.into()))
pub fn unknown<M: Into<String>>(message: M) -> Error {
Error::UnknownError(message.into())
}
pub fn err<M: Into<String>, T>(message: M) -> Result<T> {
Err(Self::error(message))
Err(Self::unknown(message))
}
}
impl From<StripPrefixError> for Error {
fn from(value: StripPrefixError) -> Self {
Error::unknown(format!("Invalid operation: {}", value))
}
}
// fn format_optional_path(maybe_path: &Option<PathBuf>) -> String {
// fn format_optional_path(maybe_path: &Option<Utf8PathBuf>) -> String {
// match maybe_path {
// Some(path) => { format!(" ({:?})", path) },
// None => { String::new() }

View File

@@ -5,24 +5,23 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
//! # cas-core
//!
//! `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;
@@ -36,9 +35,17 @@ mod cas;
pub use crate::{
error::{Error, Result},
object_id::{ObjectId, hex, write_hex},
object_type::{ObjectType},
object_metadata::{ObjectMetadata},
object_type::ObjectType,
object_metadata::ObjectMetadata,
pipeline::{Pipeline, DefaultPipeline, Reader, Writer, ReadWrapper, WriteWrapper},
cas::{Cas, RefStore},
cas::{Cas, CasWithRef, RefStore},
};
#[macro_export]
macro_rules! err {
($($arg:tt)*) => {{
let res = std::fmt::format(format_args!($($arg)*));
Error::err(res)
}}
}

View File

@@ -5,19 +5,20 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
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<Error>;
type Err = Error;
fn from_str(id_str: &str) -> Result<ObjectId> {
Ok(ObjectId {
@@ -56,6 +57,14 @@ impl FromStr for ObjectId {
}
}
impl Default for ObjectId {
fn default() -> Self {
return Self {
id: Arc::new(vec![]),
}
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
write_hex(f, self.id.as_slice())
@@ -138,7 +147,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 +157,7 @@ fn parse_byte(maybe_str_byte: Result<&str>) -> Result<u8> {
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 +203,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"),
}
}
}

View File

@@ -5,16 +5,16 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use super::object_type::{ObjectType};
use super::object_type::ObjectType;
#[derive(Clone, Eq, PartialEq)]

View File

@@ -5,14 +5,15 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use super::err;
use super::error::*;
@@ -24,7 +25,10 @@ pub struct ObjectType {
impl ObjectType {
pub fn new(id: &[u8]) -> Result<Self> {
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 {

View File

@@ -5,16 +5,17 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use digest::DynDigest;
use super::err;
use super::error::*;
use super::object_id::ObjectId;
@@ -53,7 +54,7 @@ impl<R: std::io::Read> std::io::Read for ReadWrapper<R> {
impl<R: std::io::Read> Reader for ReadWrapper<R> {
fn finalize(self: Box<Self>) -> Result<ObjectId> {
Err(Error::error("reader pipline has no digest step"))
err!("reader pipline has no digest step")
}
}
@@ -80,7 +81,7 @@ impl<W: std::io::Write> std::io::Write for WriteWrapper<W> {
impl<W: std::io::Write> Writer for WriteWrapper<W> {
fn finalize(self: Box<Self>) -> Result<ObjectId> {
Err(Error::error("reader pipline has no digest step"))
err!("reader pipline has no digest step")
}
fn _finalize(self: Box<Self>, oid: ObjectId) -> Result<ObjectId> {
@@ -97,7 +98,7 @@ pub struct DigestReader<'a> {
impl<'a> DigestReader<'a> {
pub fn new(reader: Box<dyn Reader + 'a>, digest: Box<dyn DynDigest + 'a>) -> 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<Self>) -> Result<ObjectId> {
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<dyn Writer + 'a>, digest: Box<dyn DynDigest + 'a>) -> Self {
return DigestWriter {
DigestWriter {
writer,
digest,
}
@@ -166,7 +167,7 @@ impl<'a> Writer for DigestWriter<'a> {
}
fn _finalize(self: Box<Self>, _oid: ObjectId) -> Result<ObjectId> {
Err(Error::error("writer pipeline has several digest steps"))
err!("writer pipeline has several digest steps")
}
}

View File

@@ -1,15 +1,16 @@
[package]
name = "cas-simple"
version = "0.1.0"
edition = "2018"
authors = ["Simon Boyé <sim.boye@gmail.com>"]
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"
toml = "0.5.8"
camino = { version = "1.0.7" }
toml = "0.7.6"
cas-core = { path = "../cas-core" }
tempfile = "3.2.0"
[dev-dependencies]

View File

@@ -5,56 +5,92 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use std::str::FromStr;
use std::path::{Path, PathBuf};
use digest::DynDigest;
use toml::Value;
use toml::{Value, Table};
use camino::{Utf8Path, Utf8PathBuf};
use cas_core::{
Cas, DefaultPipeline, Error, ObjectId, ObjectMetadata, ObjectType,
Pipeline, Reader, ReadWrapper, RefStore, Result, Writer,
Cas, DefaultPipeline, err, Error, ObjectId, ObjectMetadata, ObjectType,
Pipeline, Reader, ReadWrapper, RefStore, Result, Writer, CasWithRef,
};
use crate::utils::{
obj_dir, obj_path, ref_dir, tmp_dir,
read_config, write_config,
read_metadata, write_metadata,
new_digest,
};
use crate::wfile::WFile;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimpleCasConfig {
pub digest_id: String,
}
impl SimpleCasConfig {
pub fn new(digest_id: String) -> Self {
Self {
digest_id,
}
}
pub fn from_toml_value(value: &Value) -> Result<Self> {
if let Value::Table(ref table) = value {
let engine =
table.get("engine")
.ok_or_else(|| Error::unknown("missing engine field in cas config"))?
.as_str()
.ok_or_else(|| Error::unknown("cas engine must be a string"))?;
if engine != "simple" {
return err!("expected simple cas engine");
}
let digest_id =
table.get("digest")
.ok_or_else(|| Error::unknown("missing digest field in cas config"))?
.as_str()
.ok_or_else(|| Error::unknown("cas digest must be a string"))?;
Ok(Self {
digest_id: digest_id.to_string(),
})
}
else {
err!("cas config must be a table")
}
}
pub fn to_toml_value(&self) -> Result<Value> {
let mut table = Table::new();
table["engine"] = "simple".into();
table["digest"] = "sha256".into();
return Ok(Value::Table(table))
}
}
pub struct SimpleCas {
db_path: PathBuf,
config: SimpleCasConfig,
db_path: Utf8PathBuf,
digest: Box<dyn DynDigest>,
pipeline: DefaultPipeline,
config: Value,
}
impl SimpleCas {
pub fn create(db_path: PathBuf, config: Value) -> Result<Self> {
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::error(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
let pipeline = DefaultPipeline::new(digest.box_clone());
pub fn create(db_path: Utf8PathBuf, config: SimpleCasConfig) -> Result<Self> {
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 [
@@ -62,45 +98,32 @@ impl SimpleCas {
&obj_dir(&db_path),
&ref_dir(&db_path),
&tmp_dir(&db_path),
] {
std::fs::create_dir(path).or_else(|e|
Err(Error::error(format!(
] {
std::fs::create_dir(path).or_else(|e|
err!(
"failed to create directory ({}): {}",
path.to_string_lossy(), e,
)))
path, e,
)
)?;
}
write_config(&config, &db_path)?;
Ok(SimpleCas {
db_path,
digest,
pipeline,
config,
})
Self::open(db_path, config)
}
pub fn open(db_path: PathBuf) -> Result<Self> {
let config = read_config(&db_path)?;
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::error(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
pub fn open(db_path: Utf8PathBuf, config: SimpleCasConfig) -> Result<Self> {
let digest = new_digest(&config.digest_id)?;
let pipeline = DefaultPipeline::new(digest.box_clone());
Ok(SimpleCas {
config,
db_path,
digest,
pipeline,
config,
})
}
pub fn save_config(&self) -> Result<()> {
write_config(&self.config, &self.db_path)
pub fn get_config(&self) -> &SimpleCasConfig {
&self.config
}
}
@@ -111,10 +134,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 +154,11 @@ impl Cas for SimpleCas {
fn open_object(&self, oid: &ObjectId) -> Result<(ObjectMetadata, Box<dyn Reader>)> {
let opath = obj_path(&self.db_path, oid);
if !opath.is_file() {
return Err(Error::error(format!("object not found: {}", oid)));
return Err(Error::ObjectDoesNotExists(oid.clone()));
}
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,70 +177,73 @@ 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<P: AsRef<Path>>(&self, key: P) -> Result<ObjectId> {
let path = ref_dir(&self.db_path).join(key.as_ref());
fn get_ref(&self, key: &Utf8Path) -> Result<ObjectId> {
let path = ref_dir(&self.db_path).join(key);
if !path.exists() {
Error::err(format!("reference {} does not exists", key.as_ref().to_string_lossy()))
err!("reference {} does not exists", key)
}
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)
}
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, 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, err)
)?
)?
)
}
}
fn set_ref<P: AsRef<Path>>(&mut self, key: P, value: &ObjectId) -> Result<()> {
let path = ref_dir(&self.db_path).join(key.as_ref());
fn set_ref(&mut self, key: &Utf8Path, value: &ObjectId) -> Result<()> {
let path = ref_dir(&self.db_path).join(key);
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))
)?).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, 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, err)
)
}
fn remove_ref<P: AsRef<Path>>(&mut self, key: P) -> Result<()> {
let path = ref_dir(&self.db_path).join(key.as_ref());
fn remove_ref(&mut self, key: &Utf8Path) -> Result<()> {
let path = ref_dir(&self.db_path).join(key);
if !path.exists() {
Error::err(format!("reference {} does not exists", key.as_ref().to_string_lossy()))
err!("reference {} does not exists", key)
}
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)
}
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, err)
)
}
}
}
impl CasWithRef for SimpleCas {
}
pub struct ObjectIdIterator {
root_dirs: Vec<PathBuf>,
inner_dirs: Vec<PathBuf>,
objects: Vec<PathBuf>,
root_dirs: Vec<Utf8PathBuf>,
inner_dirs: Vec<Utf8PathBuf>,
objects: Vec<Utf8PathBuf>,
root_index: usize,
inner_index: usize,
object_index: usize,
@@ -247,7 +273,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 +281,9 @@ impl ObjectIdIterator {
.collect::<Vec<_>>()
.iter()
.rev()
.map(|p| p.file_name()?.to_str())
.map(|p| p.file_name())
.collect::<Option<String>>()
.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 +368,19 @@ impl Iterator for ObjectIdIterator {
}
}
fn read_dir(path: &Path) -> Result<Vec<PathBuf>> {
fn read_dir(path: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
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::<std::io::Result<Vec<_>>>()
.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::<Result<Vec<_>>>()
.or_else(|err| err!(
"error while reading directory {:?}: {}", path, err))?;
paths.sort();
Ok(paths)
}
@@ -357,18 +388,11 @@ fn read_dir(path: &Path) -> Result<Vec<PathBuf>> {
#[cfg(test)]
mod tests {
use std::path::Path;
use camino::Utf8Path;
use super::*;
fn get_config() -> Value {
toml::toml!(
[cas]
digest = "sha256"
)
}
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,8 +401,8 @@ 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 config = get_config();
let cas_path = get_cas_path(Utf8Path::from_path(dir.path()).unwrap());
let config = SimpleCasConfig::new("sha256".into());
let cas = SimpleCas::create(cas_path, config)
.expect("failed to create SimpleCas object");
@@ -391,8 +415,8 @@ 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 config = get_config();
let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"Hello World!";
@@ -421,8 +445,8 @@ 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 config = get_config();
let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"Hello World!";
@@ -460,8 +484,8 @@ 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 config = get_config();
let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"This is a test.";
@@ -480,8 +504,8 @@ 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 config = get_config();
let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"This is a test.";
@@ -501,8 +525,8 @@ 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 config = get_config();
let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = SimpleCasConfig::new("sha256".into());
let mut cas = SimpleCas::create(cas_path.clone(), config)
.expect("failed to create SimpleCas object");
@@ -615,22 +639,22 @@ 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 config = get_config();
let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = SimpleCasConfig::new("sha256".into());
let mut cas = SimpleCas::create(cas_path.clone(), config)
.expect("failed to create SimpleCas object");
let oid_0 = ObjectId::from_str("f731f6bc6a6a73bad170e56452473ef6930b7a0ab33cc54be44221a89b49d786").unwrap();
assert!(cas.get_ref("foo/bar").is_err());
assert!(cas.remove_ref("foo/bar").is_err());
assert!(cas.get_ref("foo/bar".into()).is_err());
assert!(cas.remove_ref("foo/bar".into()).is_err());
cas.set_ref("foo/bar", &oid_0).unwrap();
assert_eq!(cas.get_ref("foo/bar").unwrap(), oid_0);
cas.set_ref("foo/bar".into(), &oid_0).unwrap();
assert_eq!(cas.get_ref("foo/bar".into()).unwrap(), oid_0);
cas.remove_ref("foo/bar").unwrap();
cas.remove_ref("foo/bar".into()).unwrap();
assert!(cas.get_ref("foo/bar").is_err());
assert!(cas.get_ref("foo/bar".into()).is_err());
}
}

View File

@@ -5,13 +5,13 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
//! # cas-simple
//!
@@ -21,14 +21,15 @@
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;
pub use cas::SimpleCas;
pub use cas::{SimpleCas, SimpleCasConfig};

View File

@@ -6,30 +6,29 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use std::path::{Path, PathBuf};
use digest::DynDigest;
use toml::Value;
// 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,65 +36,65 @@ 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<Value> {
use std::io::Read;
// pub fn read_config(db_path: &Utf8Path) -> Result<Value> {
// use std::io::Read;
let mut file = std::fs::File::open(config_path(db_path)).or_else(|err|
Err(Error::error(format!("invalid repository: no config file: {}", err)))
)?;
let mut config_str = String::new();
file.read_to_string(&mut config_str).or_else(|err|
Err(Error::error(format!("cannot read config file: {}", err)))
)?;
let config = toml::from_str(&config_str).or_else(|err|
Err(Error::error(format!("error while reading config file: {}", err)))
)?;
Ok(config)
}
// let mut file = std::fs::File::open(config_path(db_path)).or_else(|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!("cannot read config file: {}", err)
// )?;
// let config = toml::from_str(&config_str).or_else(|err|
// err!("error while reading config file: {}", err)
// )?;
// Ok(config)
// }
pub fn write_config(config: &Value, db_path: &Path) -> Result<()> {
use std::io::Write;
// 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)))
)?;
let mut file = tempfile::NamedTempFile::new_in(tmp_dir(db_path)).or_else(|err|
Err(Error::error(format!("cannot create temp config file: {}", err)))
)?;
file.write_all(config_str.as_bytes()).or_else(|err|
Err(Error::error(format!("failed to write to temp config: {}", err)))
)?;
file.persist(config_path(db_path)).or_else(|err|
Err(Error::error(format!("failed to (over)write config: {}", err)))
)?;
Ok(())
}
// let config_str = toml::to_string_pretty(config).or_else(|err|
// err!("failed to serialize config: {}", err)
// )?;
// let mut file = tempfile::NamedTempFile::new_in(tmp_dir(db_path)).or_else(|err|
// err!("cannot create temp config file: {}", err)
// )?;
// file.write_all(config_str.as_bytes()).or_else(|err|
// err!("failed to write to temp config: {}", err)
// )?;
// file.persist(config_path(db_path)).or_else(|err|
// err!("failed to (over)write config: {}", err)
// )?;
// Ok(())
// }
pub fn read_metadata(read: &mut dyn std::io::Read) -> Result<ObjectMetadata> {
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<ObjectMetadata> {
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<Box<dyn DynDigest>> {
match id {
"sha256" => { Ok(Box::new(sha2::Sha256::default())) },
_ => { Err(Error::error(format!("unknown digest '{}'", id))) }
_ => { err!("unknown digest '{}'", id) }
}
}
@@ -126,54 +125,54 @@ 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")
);
}
#[test]
fn test_read_write_config() {
let config = toml::toml!{
[cas]
path = "/foo/bar"
digest = "sha1"
// #[test]
// fn test_read_write_config() {
// let config = toml::toml!{
// [cas]
// path = "/foo/bar"
// digest = "sha1"
[extra]
test = 42
};
// [extra]
// test = 42
// };
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");
// let dir = tempfile::TempDir::new()
// .expect("failed to create 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);
// assert_eq!(config2, config);
dir.close().expect("failed to close tmp dir")
}
// dir.close().expect("failed to close tmp dir")
// }
#[test]
fn test_read_write_metadata() {

View File

@@ -5,19 +5,19 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
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<Self> {
pub fn new(db_path: Utf8PathBuf) -> Result<Self> {
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<Self>) -> Result<ObjectId> {
Err(Error::error("reader pipline has no digest step"))
err!("reader pipline has no digest step")
}
fn _finalize(self: Box<Self>, oid: ObjectId) -> Result<ObjectId> {
@@ -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");

2
libbsv/.gitignore vendored
View File

@@ -1,2 +0,0 @@
/target
Cargo.lock

View File

@@ -2,15 +2,13 @@
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 = { version = "0.7.6", features = ["parse"] }
camino = "1.0.7"
regex = "1.6.0"
cas-core = { path = "../cas-core" }
cas-simple = { path = "../cas-simple" }
tempfile = "3.7.1"

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)?,
})
}
}

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

@@ -0,0 +1,165 @@
// 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.
//
// bsv 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 bsv. If not, see <https://www.gnu.org/licenses/>.
use camino::Utf8Path;
use regex::RegexSet;
use cas_core::{err, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IgnoreAction {
Ignore,
Accept,
}
#[derive(Clone, Debug)]
pub struct IgnoreRules {
patterns: RegexSet,
actions: Vec<IgnoreAction>,
}
impl IgnoreRules {
pub fn new() -> Self {
Self {
patterns: RegexSet::new(&[] as &[&str]).unwrap(),
actions: vec![],
}
}
pub fn from_source<P: AsRef<Utf8Path>>(source: &str, root: P) -> Result<Self> {
assert!(root.as_ref().is_absolute());
let separator = if std::path::MAIN_SEPARATOR == '/' {
"/"
}
else {
assert_eq!(std::path::MAIN_SEPARATOR, '\\');
"\\\\"
};
let mut patterns = Vec::<String>::default();
let mut actions = Vec::<IgnoreAction>::default();
for line in source.lines() {
let rule = line.trim();
let mut rule_it = rule.chars().peekable();
if rule_it.peek().is_none() || rule_it.peek() == Some(&'#') {
continue;
}
if rule_it.peek() == Some(&'!') {
rule_it.next();
actions.push(IgnoreAction::Accept);
}
else {
actions.push(IgnoreAction::Ignore);
}
let mut pat = String::new();
let mut last_is_sep = false;
if rule_it.peek() == Some(&'/') {
pat.push_str("^");
pat.push_str(&regex::escape(root.as_ref().as_str()));
}
while let Some(c) = rule_it.next() {
if c == '/' {
pat.push_str(separator);
last_is_sep = true;
continue;
}
else if c == '*' {
if rule_it.peek() == Some(&'*') {
rule_it.next();
if !last_is_sep || (!rule_it.peek().is_none() && rule_it.peek() != Some(&'/')) {
return err!("** pattern can only be used as a whole path segment");
}
pat.push_str(".*");
}
else {
pat.push_str("[^/]*");
}
}
else if c == '\\' {
let c2 = rule_it.next().ok_or(Error::unknown("invalid \\ at end of rule"))?;
let mut buf = [0u8; 4];
pat.push_str(&regex::escape(c2.encode_utf8(&mut buf)));
}
else {
let mut buf = [0u8; 4];
pat.push_str(&regex::escape(c.encode_utf8(&mut buf)));
}
last_is_sep = false;
}
if last_is_sep {
pat.pop();
}
pat.push_str("(/.*)?$");
dbg!(&pat);
patterns.push(pat);
}
Ok(Self {
patterns: RegexSet::new(patterns)
.or_else(|err| err!("failed to compile ignore rules: {err}"))?,
actions: actions,
})
}
pub fn action_for<P: AsRef<Utf8Path>>(&self, path: P) -> IgnoreAction {
assert!(path.as_ref().is_absolute());
let index = self.patterns.matches(path.as_ref().as_str())
.iter()
.next()
.unwrap_or(self.actions.len());
*self.actions.get(index).unwrap_or(&IgnoreAction::Accept)
}
}
#[cfg(test)]
mod tests {
use camino::Utf8PathBuf;
use super::*;
#[test]
fn test_path_map() {
let root = Utf8PathBuf::from("/foo.dir/bar");
let patterns = "!hello/world\nhello\n/world/\n\\!\\*\n*.bak";
let ignore = IgnoreRules::from_source(patterns, root).unwrap();
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test")), IgnoreAction::Accept);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/hello/aoeu")), IgnoreAction::Ignore);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/hello_world/aoeu")), IgnoreAction::Accept);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/world/aoeu")), IgnoreAction::Accept);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/hello/world/aoeu")), IgnoreAction::Accept);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/world/aoeu")), IgnoreAction::Ignore);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/world")), IgnoreAction::Ignore);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/fooXdir/bar/world")), IgnoreAction::Accept);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/!*/aoeu")), IgnoreAction::Ignore);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/file.bak")), IgnoreAction::Ignore);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test/fileXbak")), IgnoreAction::Accept);
assert_eq!(ignore.action_for(Utf8PathBuf::from("/foo.dir/bar/test.bak/file")), IgnoreAction::Ignore);
}
}

View File

@@ -5,29 +5,35 @@
// 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
// bsv 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/>.
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
// #[macro_use]
extern crate thiserror;
extern crate serde;
extern crate toml;
extern crate camino;
extern crate regex;
extern crate tempfile;
extern crate cas_core;
pub mod core;
// pub mod simple_db;
// mod config;
mod ignore;
mod path_map;
mod permissions;
mod repository;
mod tree_item;
mod tree_walker;
// pub use crate::core::{
// Error, Result,
// Config, RepositoryConfig,
// ObjectId, ObjectType,
// Repository,
// OTYPE_BLOB, OTYPE_TREE,
// };
pub use crate::ignore::{IgnoreAction, IgnoreRules};
pub use crate::path_map::{PathMap, PathPair};
pub use crate::permissions::Permissions;
pub use crate::repository::Repository;
pub use crate::tree_item::{Serialize, TreeItem};
pub use crate::tree_walker::{Action, TreeWalker};

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

@@ -0,0 +1,277 @@
// 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.
//
// bsv 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 bsv. If not, see <https://www.gnu.org/licenses/>.
use camino::{Utf8Path, Utf8PathBuf};
use toml::{Value, Table};
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 }
}
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Result<Utf8PathBuf> {
let mut physic = self.physic.clone();
physic.push(
logic_path.as_ref().strip_prefix(&self.logic)?
);
Ok(physic)
}
pub fn logic_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<Utf8PathBuf> {
let mut logic = self.logic.clone();
logic.push(
physic_path.as_ref().strip_prefix(&self.physic)?
);
Ok(logic)
}
}
#[derive(Clone, 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 to_toml_value(&self) -> Result<Value> {
let mut table = Table::new();
for pair in self.pairs.iter() {
table[pair.logic.as_str()] = pair.physic.as_str().into();
}
Ok(Value::Table(table))
}
pub fn path_pair_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<&PathPair> {
let path = logic_path.as_ref();
Some(self.logic_order.iter()
.map(|&i| &self.pairs[i])
.filter(|p| path.starts_with(&p.logic))
.next()?
)
}
pub fn path_pair_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<&PathPair> {
let path = physic_path.as_ref();
Some(self.physic_order.iter()
.map(|&i| &self.pairs[i])
.filter(|p| path.starts_with(&p.physic))
.next()?
)
}
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
self.path_pair_from_logic(&logic_path)?
.physic_from_logic(&logic_path).ok()
}
pub fn logic_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> {
self.path_pair_from_physic(&physic_path)?
.logic_from_physic(&physic_path).ok()
}
}
#[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.
//
// bsv 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 bsv. 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 & 0o400 != 0,
write: mode & 0o200 != 0,
execute: mode & 0o100 != 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());
}
}

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

@@ -0,0 +1,339 @@
// 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.
//
// bsv 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 bsv. If not, see <https://www.gnu.org/licenses/>.
use std::{fs::create_dir_all, io::BufReader};
use camino::{Utf8Path, Utf8PathBuf};
use cas_simple::utils::config_path;
use toml::{Value, Table};
use cas_core::{err, Error, ObjectId, Result, ObjectType, CasWithRef};
use cas_simple::{SimpleCas, SimpleCasConfig};
use crate::PathMap;
pub use crate::permissions::Permissions;
use crate::tree_item::Deserialize;
pub use crate::tree_item::{Serialize, TreeItem};
#[derive(Clone, Debug)]
pub struct Config {
pub device_name: String,
pub cas: SimpleCasConfig,
pub path_map: PathMap,
}
impl Config {
pub fn from_toml_value(value: &Value) -> Result<Self> {
let bsv_value = value.get("bsv")
.ok_or_else(|| Error::unknown("bsv section missing from config"))?;
if !bsv_value.is_table() {
return err!("bsv section must be a table");
}
Ok(Self {
device_name: bsv_value.get("device_name")
.ok_or_else(|| Error::unknown("bsv.device_name missing from config"))?
.as_str()
.ok_or_else(|| Error::unknown("bsv.device_name must be a string"))?
.to_string(),
cas: SimpleCasConfig::from_toml_value(
value.get("cas").ok_or_else(|| Error::unknown("config must have a cas section"))?
)?,
path_map: PathMap::from_toml_value(
value.get("mapping").ok_or_else(|| Error::unknown("config must have a mapping section"))?
)?,
})
}
pub fn from_toml_file(config_path: &Utf8Path) -> Result<Self> {
use std::io::Read;
let mut file = std::fs::File::open(config_path).or_else(|err|
err!("invalid repository: failed to read config file: {}", err)
)?;
let mut config_str = String::new();
file.read_to_string(&mut config_str).or_else(|err|
err!("failed to read config file: {}", err)
)?;
let value = config_str.parse::<Value>().or_else(|err|
err!("parse error while reading config file: {}", err)
)?;
Self::from_toml_value(&value)
}
pub fn to_toml_value(&self) -> Result<Value> {
let mut bsv = Table::new();
bsv["device_name"] = Value::String(self.device_name.clone());
let mut table = Table::new();
table["bsv"] = Value::Table(bsv);
table["cas"] = self.cas.to_toml_value()?;
table["mapping"] = self.path_map.to_toml_value()?;
Ok(Value::Table(table))
}
pub fn write_toml_file(&self, config_path: &Utf8Path) -> Result<()> {
use std::io::Write;
let value = self.to_toml_value()?;
let config_str = toml::to_string_pretty(&value).or_else(|err|
err!("failed to serialize config: {}", err)
)?;
let mut file = tempfile::NamedTempFile::new_in(config_path.parent().unwrap()).or_else(|err|
err!("cannot create temp config file: {}", err)
)?;
file.write_all(config_str.as_bytes()).or_else(|err|
err!("failed to write to temp config: {}", err)
)?;
file.persist(config_path).or_else(|err|
err!("failed to (over)write config: {}", err)
)?;
Ok(())
}
}
pub struct Repository {
config: Config,
cas: Box<dyn CasWithRef>,
}
impl Repository {
pub fn create(path: Utf8PathBuf, config: Config) -> Result<Self> {
if path.exists() {
return err!("cannot create bsv repository, path {} already exists", path);
}
create_dir_all(&path)?;
config.write_toml_file(&config_path(&path))?;
let cas = SimpleCas::create(path.to_path_buf(), config.cas.clone())?;
Ok(Self {
config,
cas: Box::new(cas),
})
}
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 = Config::from_toml_file(&config_file)?;
let cas = SimpleCas::open(path, config.cas.clone())?;
Ok(Self {
config,
cas: Box::new(cas),
})
}
pub fn cas(&self) -> &dyn CasWithRef {
self.cas.as_ref()
}
pub fn physic_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
self.config.path_map.physic_from_logic(&logic_path)
}
pub fn path_pair_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> {
self.config.path_map.logic_from_physic(&physic_path)
}
pub fn oid_from_logic_path<P: AsRef<Utf8Path>>(&self, _logic_path: P) -> Result<ObjectId> {
// let snapshot_oid = self.cas.get_ref(Utf8Path::new("latest"))?;
// let snapshot = self.read_snapshot(snapshot_oid)?;
err!("not implemented")
}
pub fn oid_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<ObjectId> {
let logic_path = self.config.path_map.logic_from_physic(physic_path)
.ok_or_else(|| Error::unknown("physic path do not map to a logic path"))?;
self.oid_from_logic_path(logic_path)
}
pub fn read_tree(&self, oid: &ObjectId) -> Result<Option<Vec<TreeItem>>> {
match self.cas.open_object(oid) {
Ok((metadata, mut reader)) => {
if metadata.otype() != &ObjectType::new(b"tree")? {
err!("object is not a tree")
}
else {
let mut buf_read = BufReader::new(reader.as_mut());
Ok(Some(
Vec::<TreeItem>::deserialize(&mut buf_read)?
.unwrap_or_else(|| Vec::new())
))
}
},
Err(Error::ObjectDoesNotExists(_)) => {
Ok(None)
}
Err(err) => {
Err(err)
}
}
}
// 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,24 +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 db;
pub mod object;
pub use db::SimpleDb;
pub use object::{
ObjectReader,
};

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(())
}
}

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

@@ -0,0 +1,300 @@
// 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.
//
// bsv 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 bsv. 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<Option<Self>>
where Self: Sized;
fn deserialize<R: BufRead>(stream: &mut R) -> Result<Option<Self>>
where Self: Sized
{
let mut buf = Vec::new();
Self::deserialize_with_buf(stream, &mut buf)
}
}
pub fn is_tree_item_name_valid(name: &str) -> bool {
return
!name.contains('/') &&
!name.contains('\\') &&
!name.contains('\n') &&
!name.contains('\0');
}
#[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: String, metadata: &std::fs::Metadata, oid: ObjectId) -> Result<Self> {
if !is_tree_item_name_valid(&name) {
return err!("invalid item name {:?}", name);
}
let otype = otype_from_metadata(metadata)?;
let permissions = Permissions::from_metadata(metadata)?;
Ok(Self {
name: name,
otype,
size: if metadata.is_file() { metadata.len() } else { 0 },
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<Option<Self>> {
let oid: ObjectId = if let Some(oid) = read_field_parse(stream, buf, "object ID", b'\t')? {
oid
}
else {
return Ok(None);
};
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')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?;
let permissions: Permissions = read_field_parse(stream, buf, "permissions", b'\t')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?;
let created = UNIX_EPOCH + Duration::from_millis(
read_field_parse(stream, buf, "creation date", b'\t')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
);
let modified = UNIX_EPOCH + Duration::from_millis(
read_field_parse(stream, buf, "modification date", b'\t')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
);
let name = read_field_str(stream, buf, "name", b'\n')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
.to_string();
if name.is_empty() {
err!("tree item name is empty")
}
else if !is_tree_item_name_valid(&name) {
err!("tree item name has invalid character(s)")
}
else {
Ok(Some(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(())
}
}
impl Deserialize for Vec<TreeItem> {
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Option<Self>> {
let mut items = Vec::new();
while let Some(item) = TreeItem::deserialize_with_buf(stream, buf)? {
items.push(item);
}
Ok(if items.is_empty() {
None
}
else {
Some(items)
})
}
}
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<bool> {
buf.clear();
stream.read_until(byte, buf)
.or_else(|err| err!("failed to read TreeItem {}: {}", field_name, err))?;
if buf.is_empty() {
return Ok(false)
}
buf.pop();
Ok(true)
}
fn read_field_str<'a, R: BufRead>(stream: &mut R, buf: &'a mut Vec<u8>, field_name: &str, byte: u8) -> Result<Option<&'a str>> {
if read_field(stream, buf, field_name, byte)? {
Ok(Some(
std::str::from_utf8(buf)
.or_else(|err| err!("TreeItem {} is not valid utf-8: {}", field_name, err))?
))
}
else {
Ok(None)
}
}
fn read_field_parse<R, I>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<Option<I>>
where
R: BufRead,
I: std::str::FromStr,
<I as std::str::FromStr>::Err: std::fmt::Display
{
let maybe_str = read_field_str(stream, buf, field_name, byte)?;
if let Some(int_str) = maybe_str {
Ok(Some(
I::from_str(int_str)
.or_else(|err| err!("failed to parse TreeItem {}: {}", field_name, err))?
))
}
else {
Ok(None)
}
}
#[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().unwrap();
assert_eq!(item, expected);
assert!(TreeItem::deserialize(&mut Cursor::new("")).unwrap().is_none());
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());
}
}

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

@@ -0,0 +1,273 @@
// 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.
//
// bsv 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 bsv. If not, see <https://www.gnu.org/licenses/>.
use std::iter::Peekable;
use std::fs::read_dir;
use std::vec::IntoIter;
use camino::{Utf8Path, Utf8PathBuf};
use cas_core::{Error, ObjectId, Result};
use crate::{IgnoreAction, IgnoreRules};
use crate::TreeItem;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Add,
Update,
Remove,
Skip,
Ignore,
}
pub struct TreeWalker {
path: Utf8PathBuf,
dir_it: Peekable<IntoIter<Result<TreeItem>>>,
prev_tree_it: Peekable<IntoIter<TreeItem>>,
ignore_rules: Option<IgnoreRules>,
}
impl TreeWalker {
pub fn new<P: AsRef<Utf8Path>>(
path: P,
prev_tree: Vec<TreeItem>,
ignore_rules: Option<IgnoreRules>
) -> Result<Self> {
let dir_entries = read_dir(path.as_ref().to_path_buf())?
.map(|res| res.map_err(|err| err.into()))
.collect::<Result<Vec<_>>>()?;
let mut dir_items: Vec<_> = dir_entries.into_iter()
.map(|dir_entry| {
let file_name = dir_entry
.file_name()
.into_string()
// .or_else(|os_string| err!("non-unicode file name '{}'", os_string.to_string_lossy()))?;
.or_else(|os_string| Err(Error::NonUnicodeFileName(os_string.to_string_lossy().into())))?;
let metadata = dir_entry.metadata()?;
Ok(TreeItem::from_metadata(
file_name,
&metadata,
ObjectId::default()
)?
)
})
.collect();
dir_items.sort_unstable_by_key(|result| {
match result {
Ok(entry) => entry.name.clone(),
Err(_) => String::default(),
}
});
Ok(Self {
path: path.as_ref().to_path_buf(),
dir_it: dir_items.into_iter().peekable(),
prev_tree_it: prev_tree.into_iter().peekable(),
ignore_rules: ignore_rules,
})
}
fn test_ignore(&self, item_name: &str, default_action: Action) -> Action {
if let Some(ignore_rules) = self.ignore_rules.as_ref() {
let mut path = self.path.clone();
path.push(item_name);
match ignore_rules.action_for(path) {
IgnoreAction::Accept => { default_action },
IgnoreAction::Ignore => { Action::Ignore },
}
}
else {
default_action
}
}
}
impl Iterator for TreeWalker {
type Item = Result<(Action, TreeItem)>;
fn next(&mut self) -> Option<Self::Item> {
match (self.dir_it.peek(), self.prev_tree_it.peek()) {
(Some(Err(_)), _) => {
let error = self.dir_it.next().unwrap().unwrap_err();
Some(Err(error))
}
(Some(Ok(curr_item)), Some(prev_item)) => {
if curr_item.name == prev_item.name {
let action =
if curr_item.modified != prev_item.modified
|| curr_item.otype != prev_item.otype
|| curr_item.size != prev_item.size {
Action::Update
}
else {
Action::Skip
};
let item = self.dir_it.next().unwrap().unwrap();
self.prev_tree_it.next().unwrap();
Some(Ok((action, item)))
}
else if curr_item.name < prev_item.name {
let item = self.dir_it.next().unwrap().unwrap();
let action = self.test_ignore(&item.name, Action::Add);
Some(Ok((action, item)))
}
else {
let item = self.prev_tree_it.next().unwrap();
Some(Ok((Action::Remove, item)))
}
},
(Some(_), None) => {
let item = self.dir_it.next().unwrap().unwrap();
let action = self.test_ignore(&item.name, Action::Add);
Some(Ok((action, item)))
},
(None, Some(_)) => {
let item = self.prev_tree_it.next().unwrap();
Some(Ok((Action::Remove, item)))
},
(None, None) => None,
}
}
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, remove_file as fs_remove_file, write};
use tempfile::tempdir;
use cas_core::ObjectType;
use crate::Permissions;
use super::*;
fn mkdir(root: &Utf8Path, path: &str) -> Result<()> {
let mut full_path = root.to_path_buf();
full_path.push(path);
create_dir(full_path).map_err(|err| err.into())
}
fn write_file(root: &Utf8Path, path: &str, content: &[u8]) -> Result<()> {
let mut full_path = root.to_path_buf();
full_path.push(path);
write(full_path, content).map_err(|err| err.into())
}
fn remove_file(root: &Utf8Path, path: &str) -> Result<()> {
let mut full_path = root.to_path_buf();
full_path.push(path);
fs_remove_file(full_path).map_err(|err| err.into())
}
fn to_tree_items(items: Vec<Result<(Action, TreeItem)>>) -> Vec<TreeItem> {
items.into_iter()
.filter(|ref item| item.as_ref().unwrap().0 != Action::Remove)
.map(|item| item.unwrap().1)
.collect()
}
#[test]
fn test_tree_walker() -> Result<()>{
let root_dir = tempdir()?;
let root = Utf8Path::from_path(root_dir.path()).unwrap();
let ignore_rules = IgnoreRules::from_source("*.bak", root).unwrap();
mkdir(root, "test")?;
write_file(root, "test/foobar.txt", b"baz")?;
write_file(root, "readme", b"hello world!")?;
let items: Vec<_> = TreeWalker::new(root, vec![], Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 2);
let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Add);
assert_eq!(item.name, "readme");
assert_eq!(item.otype, ObjectType::new(b"blob").unwrap());
assert_eq!(item.size, 12);
assert_eq!(item.permissions, Permissions{read: true, write: true, execute: false});
let (action, item) = items[1].as_ref().unwrap();
assert_eq!(action, &Action::Add);
assert_eq!(item.name, "test");
assert_eq!(item.otype, ObjectType::new(b"tree").unwrap());
assert_eq!(item.size, 0);
assert_eq!(item.permissions, Permissions{read: true, write: true, execute: true});
write_file(root, "abc", b"xxxx")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 3);
let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Add);
assert_eq!(item.name, "abc");
assert_eq!(item.otype, ObjectType::new(b"blob").unwrap());
assert_eq!(item.size, 4);
assert_eq!(item.permissions, Permissions{read: true, write: true, execute: false});
let (action, item) = items[1].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "readme");
let (action, item) = items[2].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "test");
remove_file(root, "readme")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 3);
let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "abc");
let (action, item) = items[1].as_ref().unwrap();
assert_eq!(action, &Action::Remove);
assert_eq!(item.name, "readme");
let (action, item) = items[2].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "test");
write_file(root, "abc", b"ab")?;
write_file(root, "test/foobar.txt", b"redacted")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 2);
let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Update);
assert_eq!(item.name, "abc");
assert_eq!(item.size, 2);
let (action, item) = items[1].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "test");
write_file(root, "test.bak", b"ignore this")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 3);
let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "abc");
let (action, item) = items[1].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "test");
let (action, item) = items[2].as_ref().unwrap();
assert_eq!(action, &Action::Ignore);
assert_eq!(item.name, "test.bak");
assert_eq!(item.size, 11);
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(())
}