Mostly improved tree walking I guess.
This commit is contained in:
@@ -6,11 +6,9 @@ edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
toml = "0.5.8"
|
||||
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" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.7.1"
|
||||
|
||||
@@ -1,31 +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.
|
||||
//
|
||||
// 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::collections::{HashMap};
|
||||
use camino::Utf8PathBuf;
|
||||
|
||||
use cas_core::{Cas, Result};
|
||||
|
||||
|
||||
pub trait CasConfig {
|
||||
fn build_cas(&self) -> Result<Box<dyn Cas>>;
|
||||
}
|
||||
|
||||
|
||||
pub struct BsvConfig {
|
||||
cas: Box<dyn CasConfig>,
|
||||
dir_map: HashMap<Utf8PathBuf, Utf8PathBuf>,
|
||||
}
|
||||
@@ -27,7 +27,7 @@ pub enum IgnoreAction {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IgnoreRules {
|
||||
patterns: RegexSet,
|
||||
actions: Vec<IgnoreAction>,
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
extern crate toml;
|
||||
extern crate camino;
|
||||
extern crate regex;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate tempfile;
|
||||
|
||||
extern crate cas_core;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use toml::Value;
|
||||
use toml::{Value, Table};
|
||||
|
||||
use cas_core::{err, Error, Result};
|
||||
|
||||
@@ -30,10 +30,28 @@ 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(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathMap {
|
||||
pairs: Vec<PathPair>,
|
||||
logic_order: Vec<usize>,
|
||||
@@ -113,38 +131,42 @@ impl PathMap {
|
||||
Self::from_vec(map)
|
||||
}
|
||||
|
||||
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
|
||||
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();
|
||||
|
||||
let pair = self.logic_order.iter()
|
||||
Some(self.logic_order.iter()
|
||||
.map(|&i| &self.pairs[i])
|
||||
.filter(|p| path.starts_with(&p.logic))
|
||||
.next()?;
|
||||
.next()?
|
||||
)
|
||||
}
|
||||
|
||||
let mut physic = pair.physic.clone();
|
||||
physic.push(
|
||||
path.strip_prefix(&pair.logic)
|
||||
.ok()?
|
||||
);
|
||||
pub fn path_pair_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<&PathPair> {
|
||||
let path = physic_path.as_ref();
|
||||
|
||||
return Some(physic)
|
||||
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> {
|
||||
let path = physic_path.as_ref();
|
||||
|
||||
let pair = self.physic_order.iter()
|
||||
.map(|&i| &self.pairs[i])
|
||||
.filter(|p| path.starts_with(&p.physic))
|
||||
.next()?;
|
||||
|
||||
let mut logic = pair.logic.clone();
|
||||
logic.push(
|
||||
path.strip_prefix(&pair.physic)
|
||||
.ok()?
|
||||
);
|
||||
|
||||
return Some(logic)
|
||||
self.path_pair_from_physic(&physic_path)?
|
||||
.logic_from_physic(&physic_path).ok()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,100 +14,118 @@
|
||||
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
use std::{fs::create_dir_all, io::BufReader};
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use toml::Value;
|
||||
use cas_simple::utils::config_path;
|
||||
use toml::{Value, Table};
|
||||
|
||||
use cas_core::{Cas, err, Error, ObjectId, Result};
|
||||
use cas_simple::SimpleCas;
|
||||
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};
|
||||
|
||||
|
||||
pub fn create_cas(path: Utf8PathBuf, config: Value) -> Result<Box<dyn Cas>> {
|
||||
let engine = config
|
||||
.get("cas")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas item"))?
|
||||
.get("engine")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas.engine item"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::unknown("cas.engine must be a string"))?;
|
||||
|
||||
match engine {
|
||||
"simple" => { SimpleCas::create(path, config).map(|cas| Box::new(cas) as Box<dyn Cas>) }
|
||||
_ => { err!("unknown cas engine {}", engine) }
|
||||
}
|
||||
#[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");
|
||||
}
|
||||
|
||||
pub fn open_cas(path: Utf8PathBuf, config: &Value) -> Result<Box<dyn Cas>> {
|
||||
let engine = config
|
||||
.get("cas")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas item"))?
|
||||
.get("engine")
|
||||
.ok_or_else(|| Error::unknown("config must have a cas.engine item"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::unknown("cas.engine must be a string"))?;
|
||||
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"))?
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
match engine {
|
||||
"simple" => { SimpleCas::open(path).map(|cas| Box::new(cas) as Box<dyn Cas>) }
|
||||
_ => { err!("unknown cas engine {}", engine) }
|
||||
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 trait FsVisitor {
|
||||
// fn accept(&self, path: &Utf8Path, metadata: &Metadata) -> bool;
|
||||
// fn handle_error(&mut self, error: Error) -> Option<Error>;
|
||||
|
||||
// fn handle_result<T>(&mut self, result: Result<T>) -> Result<T>
|
||||
// {
|
||||
// result.map_err(|error|
|
||||
// self.handle_error(error)
|
||||
// .unwrap_or(Error::Skipped)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// fn read_path_map(config: &Value) -> Result<Vec<PathPair>> {
|
||||
// if let Some(mapping) = config.get("mapping") {
|
||||
// if let Some(ref table) = mapping.as_table() {
|
||||
// table.iter()
|
||||
// .map(|(k, v)| Ok(PathPair{
|
||||
// logic: k.into(),
|
||||
// physic: v.as_str()
|
||||
// .ok_or_else(|| Error::unknown("mapping values must be strings"))?
|
||||
// .into()
|
||||
// }))
|
||||
// .collect()
|
||||
// }
|
||||
// else {
|
||||
// err!("mapping must be a table, got {}", mapping)
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// Ok(vec![])
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
pub struct Repository {
|
||||
cas: Box<dyn Cas>,
|
||||
// path_map: Vec<PathPair>,
|
||||
config: Config,
|
||||
cas: Box<dyn CasWithRef>,
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
pub fn create(path: Utf8PathBuf, config: Value) -> Result<Self> {
|
||||
pub fn create(path: Utf8PathBuf, config: Config) -> Result<Self> {
|
||||
if path.exists() {
|
||||
return err!("cannot create bsv repository, path {} already exists", path);
|
||||
}
|
||||
|
||||
let cas = create_cas(path, config)?;
|
||||
create_dir_all(&path)?;
|
||||
config.write_toml_file(&config_path(&path))?;
|
||||
let cas = SimpleCas::create(path.to_path_buf(), config.cas.clone())?;
|
||||
|
||||
Ok(Self {
|
||||
cas,
|
||||
// path_map: vec![],
|
||||
config,
|
||||
cas: Box::new(cas),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,39 +135,61 @@ impl Repository {
|
||||
}
|
||||
|
||||
let config_file = cas_simple::utils::config_path(&path);
|
||||
let config = cas_simple::utils::read_config(&config_file)?;
|
||||
let config = Config::from_toml_file(&config_file)?;
|
||||
|
||||
let cas = open_cas(path, &config)?;
|
||||
// let path_map = read_path_map(&config)?;
|
||||
let cas = SimpleCas::open(path, config.cas.clone())?;
|
||||
|
||||
Ok(Self {
|
||||
cas,
|
||||
// path_map,
|
||||
config,
|
||||
cas: Box::new(cas),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cas(&self) -> &dyn Cas {
|
||||
pub fn cas(&self) -> &dyn CasWithRef {
|
||||
self.cas.as_ref()
|
||||
}
|
||||
|
||||
// pub fn path_pair_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<PathPair> {
|
||||
// None
|
||||
// }
|
||||
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<PathPair> {
|
||||
// None
|
||||
// }
|
||||
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> {
|
||||
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> {
|
||||
err!("not implemented")
|
||||
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>>> {
|
||||
err!("not implemented")
|
||||
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>
|
||||
|
||||
@@ -27,10 +27,10 @@ pub trait Serialize {
|
||||
}
|
||||
|
||||
pub trait Deserialize {
|
||||
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self>
|
||||
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<Self>
|
||||
fn deserialize<R: BufRead>(stream: &mut R) -> Result<Option<Self>>
|
||||
where Self: Sized
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
@@ -39,6 +39,15 @@ pub trait Deserialize {
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
@@ -52,6 +61,10 @@ pub struct TreeItem {
|
||||
|
||||
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)?;
|
||||
|
||||
@@ -70,7 +83,7 @@ impl TreeItem {
|
||||
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{}/",
|
||||
writeln!(out, "{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
self.oid,
|
||||
self.otype,
|
||||
self.size,
|
||||
@@ -87,28 +100,39 @@ impl Serialize for TreeItem {
|
||||
}
|
||||
|
||||
impl Deserialize for TreeItem {
|
||||
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self> {
|
||||
let oid: ObjectId = read_field_parse(stream, buf, "object ID", b'\t')?;
|
||||
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')?;
|
||||
let permissions: Permissions = read_field_parse(stream, buf, "permissions", b'\t')?;
|
||||
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'/')?
|
||||
let name = read_field_str(stream, buf, "name", b'\n')?
|
||||
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
|
||||
.to_string();
|
||||
|
||||
stream.read_exact(&mut buf[..1])
|
||||
.or_else(|err| err!("failed to read new line character: {}", err))?;
|
||||
if buf[0] != b'\n' {
|
||||
err!("expected new line character, got {:x}", buf[0])
|
||||
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(TreeItem {
|
||||
Ok(Some(TreeItem {
|
||||
name,
|
||||
otype,
|
||||
size,
|
||||
@@ -116,7 +140,7 @@ impl Deserialize for TreeItem {
|
||||
modified,
|
||||
permissions,
|
||||
oid,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +157,22 @@ impl Serialize for [TreeItem] {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -152,29 +192,45 @@ pub fn otype_from_metadata(metadata: &std::fs::Metadata) -> Result<ObjectType> {
|
||||
}
|
||||
|
||||
|
||||
fn read_field<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<()> {
|
||||
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(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn read_field_str<'a, R: BufRead>(stream: &mut R, buf: &'a mut Vec<u8>, field_name: &str, byte: u8) -> Result<&'a str> {
|
||||
read_field(stream, buf, field_name, byte)?;
|
||||
std::str::from_utf8(buf)
|
||||
.or_else(|err| err!("TreeItem {} is not valid utf-8: {}", field_name, err))
|
||||
fn read_field_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<I>
|
||||
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 int_str = read_field_str(stream, buf, field_name, byte)?;
|
||||
I::from_str(int_str)
|
||||
.or_else(|err| err!("failed to parse TreeItem {}: {}", field_name, err))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +252,7 @@ mod tests {
|
||||
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 expected = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\n".as_bytes();
|
||||
|
||||
let mut result = Vec::new();
|
||||
item.serialize(&mut result).unwrap();
|
||||
@@ -208,7 +264,7 @@ mod tests {
|
||||
fn test_deserialize_tree_item() {
|
||||
use std::io::Cursor;
|
||||
|
||||
let item_bytes = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes();
|
||||
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(),
|
||||
@@ -220,19 +276,20 @@ mod tests {
|
||||
oid: ObjectId::from_str("0123456789abcdef").unwrap(),
|
||||
};
|
||||
|
||||
let item = TreeItem::deserialize(&mut item_cursor).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()
|
||||
"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()
|
||||
"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());
|
||||
|
||||
@@ -18,10 +18,11 @@ use std::iter::Peekable;
|
||||
use std::fs::read_dir;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use camino::Utf8Path;
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
use cas_core::{Error, ObjectId, Result};
|
||||
|
||||
use crate::{IgnoreAction, IgnoreRules};
|
||||
use crate::TreeItem;
|
||||
|
||||
|
||||
@@ -35,13 +36,19 @@ pub enum Action {
|
||||
}
|
||||
|
||||
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>) -> Result<Self> {
|
||||
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<_>>>()?;
|
||||
@@ -70,10 +77,26 @@ impl TreeWalker {
|
||||
});
|
||||
|
||||
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 {
|
||||
@@ -82,7 +105,8 @@ impl Iterator for TreeWalker {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.dir_it.peek(), self.prev_tree_it.peek()) {
|
||||
(Some(Err(_)), _) => {
|
||||
Some(Err(self.dir_it.next().unwrap().unwrap_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 {
|
||||
@@ -95,21 +119,28 @@ impl Iterator for TreeWalker {
|
||||
else {
|
||||
Action::Skip
|
||||
};
|
||||
let item = self.dir_it.next().unwrap().unwrap();
|
||||
self.prev_tree_it.next().unwrap();
|
||||
Some(Ok((action, self.dir_it.next().unwrap().unwrap())))
|
||||
Some(Ok((action, item)))
|
||||
}
|
||||
else if curr_item.name < prev_item.name {
|
||||
Some(Ok((Action::Add, self.dir_it.next().unwrap().unwrap())))
|
||||
let item = self.dir_it.next().unwrap().unwrap();
|
||||
let action = self.test_ignore(&item.name, Action::Add);
|
||||
Some(Ok((action, item)))
|
||||
}
|
||||
else {
|
||||
Some(Ok((Action::Remove, self.prev_tree_it.next().unwrap())))
|
||||
let item = self.prev_tree_it.next().unwrap();
|
||||
Some(Ok((Action::Remove, item)))
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
Some(Ok((Action::Add, self.dir_it.next().unwrap().unwrap())))
|
||||
let item = self.dir_it.next().unwrap().unwrap();
|
||||
let action = self.test_ignore(&item.name, Action::Add);
|
||||
Some(Ok((action, item)))
|
||||
},
|
||||
(None, Some(_)) => {
|
||||
Some(Ok((Action::Remove, self.prev_tree_it.next().unwrap())))
|
||||
let item = self.prev_tree_it.next().unwrap();
|
||||
Some(Ok((Action::Remove, item)))
|
||||
},
|
||||
(None, None) => None,
|
||||
}
|
||||
@@ -157,11 +188,13 @@ mod tests {
|
||||
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![]).unwrap().collect();
|
||||
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);
|
||||
@@ -178,7 +211,7 @@ mod tests {
|
||||
|
||||
write_file(root, "abc", b"xxxx")?;
|
||||
|
||||
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items)).unwrap().collect();
|
||||
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);
|
||||
@@ -195,7 +228,7 @@ mod tests {
|
||||
|
||||
remove_file(root, "readme")?;
|
||||
|
||||
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items)).unwrap().collect();
|
||||
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);
|
||||
@@ -210,7 +243,7 @@ mod tests {
|
||||
write_file(root, "abc", b"ab")?;
|
||||
write_file(root, "test/foobar.txt", b"redacted")?;
|
||||
|
||||
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items)).unwrap().collect();
|
||||
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);
|
||||
@@ -220,6 +253,21 @@ mod tests {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user