Compare commits

...

5 Commits

Author SHA1 Message Date
grimhilt
80ef41970f improve status 2023-06-14 17:12:01 +02:00
grimhilt
51120f6268 add service req props 2023-06-14 16:29:58 +02:00
grimhilt
3da439ccf6 add store 2023-06-14 11:16:19 +02:00
grimhilt
e405ce7f8f cleaning some code 2023-06-12 01:30:39 +02:00
grimhilt
05d17c296f store blob 2023-06-12 01:16:39 +02:00
16 changed files with 351 additions and 58 deletions

13
docs/store.md Normal file
View File

@ -0,0 +1,13 @@
## Blob object
```
file_name hash timestamp
```
## Tree object
```
folder_name
tree hash_path folder_name
blob hash_path file_name
```

View File

@ -1,5 +1,6 @@
use clap::Values; use clap::Values;
use crate::utils; use crate::utils;
use crate::store;
use std::path::Path; use std::path::Path;
use std::io::Write; use std::io::Write;
@ -14,7 +15,7 @@ pub fn add(files: Values<'_>) {
let mut index_path = root.clone(); let mut index_path = root.clone();
index_path.push(".nextsync"); index_path.push(".nextsync");
let mut index_file = utils::index::open(index_path); let mut index_file = store::index::open(index_path);
// todo avoid duplicate // todo avoid duplicate
// ./folder ./folder/file // ./folder ./folder/file

View File

@ -1,7 +1,7 @@
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::fs::DirBuilder; use std::fs::DirBuilder;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::Cursor; use std::io::{self, Cursor};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::Values; use clap::Values;
use regex::Regex; use regex::Regex;
@ -9,7 +9,7 @@ use xml::reader::{EventReader, XmlEvent};
use crate::services::api::ApiError; use crate::services::api::ApiError;
use crate::services::list_folders::ListFolders; use crate::services::list_folders::ListFolders;
use crate::services::download_files::DownloadFiles; use crate::services::download_files::DownloadFiles;
use crate::utils::object; use crate::store::object;
use crate::commands; use crate::commands;
use crate::global::global::{DIR_PATH, set_dir_path}; use crate::global::global::{DIR_PATH, set_dir_path};
@ -61,7 +61,6 @@ pub fn clone(remote: Values<'_>) {
// create folder // create folder
if first_iter { if first_iter {
// first element how path or last element of given path
if DirBuilder::new().create(local_path.clone()).is_err() { if DirBuilder::new().create(local_path.clone()).is_err() {
eprintln!("fatal: directory already exist"); eprintln!("fatal: directory already exist");
// destination path 'path' already exists and is not an empty directory. // destination path 'path' already exists and is not an empty directory.
@ -72,11 +71,15 @@ pub fn clone(remote: Values<'_>) {
} else { } else {
// create folder // create folder
let local_folder = get_local_path(folder, local_path.clone(), username, dist_path_str); let local_folder = get_local_path(folder, local_path.clone(), username, dist_path_str);
dbg!(DirBuilder::new().recursive(true).create(local_folder.clone())); if let Err(err) = DirBuilder::new().recursive(true).create(local_folder.clone()) {
eprintln!("error: cannot create directory {}: {}", local_folder.display(), err);
}
// add tree // add tree
let path_folder = local_folder.strip_prefix(local_path.clone()).unwrap(); let path_folder = local_folder.strip_prefix(local_path.clone()).unwrap();
object::add_tree(&path_folder); if object::add_tree(&path_folder).is_err() {
eprintln!("error: cannot store object {}", path_folder.display());
}
} }
// find folders and files in response // find folders and files in response
@ -105,6 +108,19 @@ fn get_local_path(p: String, local_p: PathBuf, username: &str, dist_p: &str) ->
local_p.clone().join(final_p.clone()) local_p.clone().join(final_p.clone())
} }
fn write_file(path: PathBuf, content: &Vec<u8>, local_p: PathBuf) -> io::Result<()> {
let mut f = OpenOptions::new()
.write(true)
.create(true)
.open(path.clone())?;
f.write_all(&content)?;
let relative_p = Path::new(&path).strip_prefix(local_p).unwrap();
object::add_blob(relative_p, "tmpdate")?;
Ok(())
}
fn download_files(domain: &str, local_p: PathBuf, username: &str, dist_p: &str, files: Vec<String>) { fn download_files(domain: &str, local_p: PathBuf, username: &str, dist_p: &str, files: Vec<String>) {
for file in files { for file in files {
let mut url_request = String::from(domain.clone()); let mut url_request = String::from(domain.clone());
@ -112,14 +128,11 @@ fn download_files(domain: &str, local_p: PathBuf, username: &str, dist_p: &str,
tokio::runtime::Runtime::new().unwrap().block_on(async { tokio::runtime::Runtime::new().unwrap().block_on(async {
match DownloadFiles::new(url_request.as_str()).send_with_err().await { match DownloadFiles::new(url_request.as_str()).send_with_err().await {
Ok(b) => { Ok(b) => {
let p_to_save = get_local_path(file, local_p.clone(), username, dist_p); let p_to_save = get_local_path(file.clone(), local_p.clone(), username, dist_p);
let mut f = OpenOptions::new() if let Err(_) = write_file(p_to_save, &b, local_p.clone()) {
.write(true) eprintln!("error writing {}", file);
.create(true) }
.open(p_to_save).unwrap();
f.write_all(&b);
}, },
Err(ApiError::IncorrectRequest(err)) => { Err(ApiError::IncorrectRequest(err)) => {
eprintln!("fatal: {}", err.status()); eprintln!("fatal: {}", err.status());

View File

@ -1,9 +1,8 @@
use crate::utils::path; use crate::utils::{path, read};
use crate::utils::read;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::{self, Write};
pub fn set(var: &str, val: &str) { pub fn set(var: &str, val: &str) -> io::Result<()> {
let mut root = match path::nextsync() { let mut root = match path::nextsync() {
Some(path) => path, Some(path) => path,
None => { None => {
@ -19,12 +18,13 @@ pub fn set(var: &str, val: &str) {
.write(true) .write(true)
.create(true) .create(true)
.append(true) .append(true)
.open(root).unwrap(); .open(root)?;
let mut line = var.to_owned(); let mut line = var.to_owned();
line.push_str(" "); line.push_str(" ");
line.push_str(val); line.push_str(val);
writeln!(file, "{}", line); writeln!(file, "{}", line)?;
Ok(())
} }
pub fn get(var: &str) -> Option<String> { pub fn get(var: &str) -> Option<String> {

View File

@ -12,5 +12,7 @@ pub fn push() {
}; };
let (staged_obj, new_obj, del_obj) = status::get_diff(); let (staged_obj, new_obj, del_obj) = status::get_diff();
// read index
// if dir upload dir
} }

View File

@ -1,12 +1,12 @@
use std::fs::File; use std::fs::File;
use crypto::digest::Digest; use crypto::digest::Digest;
use crypto::sha1::Sha1; use crypto::sha1::Sha1;
use std::collections::HashMap; use std::collections::{HashSet, HashMap};
use colored::Colorize; use colored::Colorize;
use std::path::PathBuf; use std::path::PathBuf;
use std::path::Path;
use std::io::{self, Lines, BufReader}; use std::io::{self, Lines, BufReader};
use crate::utils::{self, object}; use crate::utils;
use crate::store::{self, object};
#[derive(PartialEq)] #[derive(PartialEq)]
enum RemoveSide { enum RemoveSide {
@ -15,24 +15,82 @@ enum RemoveSide {
Right, Right,
} }
#[derive(PartialEq)]
#[derive(Debug)]
#[derive(Clone)]
enum State {
Default,
New,
Renamed,
Modified,
Deleted,
}
// todo: relative path, filename, get modified // todo: relative path, filename, get modified
pub fn status() { pub fn status() {
let (staged_objs, new_objs, del_objs) = get_diff(); let (mut new_objs, mut del_objs) = get_diff();
dbg!(get_diff()); dbg!(get_diff());
print_status(staged_objs.clone(), del_objs.iter().map(|x| x.name.to_owned()).collect(), new_objs.clone()); let mut renamed_objs = get_renamed(&mut new_objs, &mut del_objs);
// get copy, modified
let mut objs = new_objs;
objs.append(&mut del_objs);
objs.append(&mut renamed_objs);
let staged_objs = get_staged(&mut objs);
print_status(staged_objs, objs);
} }
#[derive(Debug)] #[derive(Debug)]
#[derive(Clone)]
pub struct Obj { pub struct Obj {
otype: String, otype: String,
name: String, name: String,
path: PathBuf, path: PathBuf,
state: State,
} }
pub fn get_diff() -> (Vec<String>, Vec<String>, Vec<Obj>) { fn get_renamed(new_obj: &mut Vec<Obj>, del_obj: &mut Vec<Obj>) -> Vec<Obj> {
// get hash of all new obj, compare to hash of all del
let renamed_objs = vec![];
renamed_objs
}
fn get_staged(objs: &mut Vec<Obj>) -> Vec<Obj> {
let mut indexes = HashSet::new();
let mut staged_objs: Vec<Obj> = vec![];
let nextsync_path = utils::path::nextsync().unwrap();
if let Ok(entries) = store::index::read_line(nextsync_path.clone()) {
for entry in entries {
// todo hash this
indexes.insert(entry.unwrap());
}
}
let mut to_remove: Vec<usize> = vec![];
let mut index = 0;
for obj in &mut *objs {
dbg!(obj.clone().path.to_str().unwrap());
if indexes.contains(obj.clone().path.to_str().unwrap()) {
staged_objs.push(obj.clone());
to_remove.push(index);
}
index += 1;
}
let mut offset = 0;
for i in to_remove {
objs.remove(i + offset.clone());
offset += 1;
}
staged_objs
}
pub fn get_diff() -> (Vec<Obj>, Vec<Obj>) {
let mut hashes = HashMap::new(); let mut hashes = HashMap::new();
let mut objs: Vec<String> = vec![]; let mut objs: Vec<String> = vec![];
let mut staged_objs: Vec<String> = vec![];
let root = match utils::path::nextsync_root() { let root = match utils::path::nextsync_root() {
Some(path) => path, Some(path) => path,
@ -45,9 +103,7 @@ pub fn get_diff() -> (Vec<String>, Vec<String>, Vec<Obj>) {
dbg!(utils::path::current()); dbg!(utils::path::current());
let nextsync_path = utils::path::nextsync().unwrap(); let nextsync_path = utils::path::nextsync().unwrap();
let current_p = utils::path::current().unwrap(); let current_p = utils::path::current().unwrap();
let mut dist_path = current_p.strip_prefix(root.clone()).unwrap().to_path_buf(); let dist_path = current_p.strip_prefix(root.clone()).unwrap().to_path_buf();
dbg!(dist_path.clone());
if let Ok(lines) = read_head(nextsync_path.clone()) { if let Ok(lines) = read_head(nextsync_path.clone()) {
add_to_hashmap(lines, &mut hashes, dist_path.clone()); add_to_hashmap(lines, &mut hashes, dist_path.clone());
@ -83,17 +139,25 @@ pub fn get_diff() -> (Vec<String>, Vec<String>, Vec<Obj>) {
} }
if let Ok(entries) = utils::index::read_line(nextsync_path.clone()) {
for entry in entries {
// todo hash this
staged_objs.push(String::from(entry.unwrap()));
}
}
let del_objs: Vec<Obj> = hashes.iter().map(|x| { let del_objs: Vec<Obj> = hashes.iter().map(|x| {
Obj {otype: x.1.otype.clone(), name: x.1.name.clone(), path: x.1.path.clone()} Obj {
otype: x.1.otype.clone(),
name: x.1.name.clone(),
path: x.1.path.clone(),
state: State::Deleted
}
}).collect(); }).collect();
(staged_objs.clone(), objs.clone(), del_objs)
let new_objs: Vec<Obj> = objs.iter().map(|x| {
// todo otype and name
Obj {
otype: String::from(""),
name: x.to_string(),
path: PathBuf::from(x.to_string()),
state: State::New
}
}).collect();
(new_objs, del_objs)
} }
fn add_to_hashmap(lines: Lines<BufReader<File>>, hashes: &mut HashMap<String, Obj>, path: PathBuf) { fn add_to_hashmap(lines: Lines<BufReader<File>>, hashes: &mut HashMap<String, Obj>, path: PathBuf) {
@ -107,6 +171,7 @@ fn add_to_hashmap(lines: Lines<BufReader<File>>, hashes: &mut HashMap<String, Ob
otype: String::from(ftype), otype: String::from(ftype),
name: String::from(name), name: String::from(name),
path: p, path: p,
state: State::Default,
}); });
} }
} }
@ -123,33 +188,57 @@ fn add_to_vec(entries: Vec<PathBuf>, objects: &mut Vec<String>, root: PathBuf) {
} }
fn print_status(staged_objs: Vec<String>, del_objs: Vec<String>, new_objs: Vec<String>) { fn print_status(staged_objs: Vec<Obj>, objs: Vec<Obj>) {
if staged_objs.len() == 0 && del_objs.len() == 0 && new_objs.len() == 0 { dbg!(staged_objs.clone());
dbg!(objs.clone());
if staged_objs.len() == 0 && objs.len() == 0 {
println!("Nothing to push, working tree clean"); println!("Nothing to push, working tree clean");
return; return;
} }
// staged file // staged file
if staged_objs.len() != 0 { if staged_objs.len() != 0 {
println!("Changes to be pushed:"); println!("Changes to be pushed:");
println!(" (Use \"nextsync reset\" to unstage)"); println!(" (Use \"nextsync reset\" to unstage)");
for staged in staged_objs { for object in staged_objs {
println!(" {} {}", String::from("staged:").green(), staged.green()); print_staged_object(object);
} }
} }
// not staged files // not staged files
if new_objs.len() != 0 || del_objs.len() != 0 { if objs.len() != 0 {
println!("Changes not staged for push:"); println!("Changes not staged for push:");
println!(" (Use\"nextsync add <file>...\" to update what will be pushed)"); println!(" (Use\"nextsync add <file>...\" to update what will be pushed)");
}
for object in new_objs { for object in objs {
println!(" {} {}", String::from("added:").red(), object.red()); print_object(object);
} }
for object in del_objs {
println!(" {} {}", String::from("deleted:").red(), object.red());
} }
} }
fn print_object(obj: Obj) {
if obj.state == State::Deleted {
println!(" {} {}", String::from("deleted:").red(), obj.name.red());
} else if obj.state == State::Renamed {
println!(" {} {}", String::from("renamed:").red(), obj.name.red());
} else if obj.state == State::New {
println!(" {} {}", String::from("new file:").red(), obj.name.red());
} else if obj.state == State::Modified {
println!(" {} {}", String::from("modified:").red(), obj.name.red());
}
}
fn print_staged_object(obj: Obj) {
if obj.state == State::Deleted {
println!(" {} {}", String::from("deleted:").green(), obj.name.green());
} else if obj.state == State::Renamed {
println!(" {} {}", String::from("renamed:").green(), obj.name.green());
} else if obj.state == State::New {
println!(" {} {}", String::from("new file:").green(), obj.name.green());
} else if obj.state == State::Modified {
println!(" {} {}", String::from("modified:").green(), obj.name.green());
}
}
fn remove_duplicate(hashes: &mut HashMap<String, Obj>, objects: &mut Vec<String>, remove_option: RemoveSide) -> Vec<String> { fn remove_duplicate(hashes: &mut HashMap<String, Obj>, objects: &mut Vec<String>, remove_option: RemoveSide) -> Vec<String> {
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();

View File

@ -3,6 +3,7 @@ mod commands;
mod utils; mod utils;
mod services; mod services;
mod global; mod global;
mod store;
fn main() { fn main() {
let matches = App::new("NextSync") let matches = App::new("NextSync")
@ -100,7 +101,9 @@ fn main() {
} else if let Some(matches) = matches.subcommand_matches("config") { } else if let Some(matches) = matches.subcommand_matches("config") {
if let Some(mut var) = matches.values_of("variable") { if let Some(mut var) = matches.values_of("variable") {
if let Some(mut val) = matches.values_of("value") { if let Some(mut val) = matches.values_of("value") {
commands::config::set(var.next().unwrap(), val.next().unwrap()); if commands::config::set(var.next().unwrap(), val.next().unwrap()).is_err() {
eprintln!("fatal: cannot save the value");
}
} }
} }
} }

View File

@ -1,3 +1,4 @@
pub mod api; pub mod api;
pub mod list_folders; pub mod list_folders;
pub mod download_files; pub mod download_files;
pub mod req_props;

View File

@ -45,6 +45,19 @@ impl ApiBuilder {
self self
} }
pub fn set_xml(&mut self, xml_payload: String) -> &mut ApiBuilder {
match self.request.take() {
None => {
eprintln!("fatal: incorrect request");
std::process::exit(1);
},
Some(req) => {
self.request = Some(req.body(xml_payload));
}
}
self
}
pub async fn send(&mut self) -> Result<Response, Error> { pub async fn send(&mut self) -> Result<Response, Error> {
self.set_auth(); self.set_auth();
match self.request.take() { match self.request.take() {

View File

@ -21,7 +21,7 @@ async fn send_propfind_request() -> Result<(), Box<dyn Error>> {
<oc:permissions/> <oc:permissions/>
<d:resourcetype/> <d:resourcetype/>
<d:getetag/> <d:getetag/>
</d:prop> ) </d:prop>
</d:propfind>"#; </d:propfind>"#;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();

135
src/services/req_props.rs Normal file
View File

@ -0,0 +1,135 @@
use crate::services::api::{ApiBuilder, ApiError};
use xml::reader::{EventReader, XmlEvent};
use std::io::{self, Cursor};
use reqwest::{Method, IntoUrl, Response, Error};
pub struct ReqProps {
api_builder: ApiBuilder,
xml_list: Vec<String>,
xml_payload: String,
}
impl ReqProps {
pub fn new<U: IntoUrl>(url: U) -> Self {
ReqProps {
api_builder: ApiBuilder::new()
.set_request(Method::from_bytes(b"PROPFIND").unwrap(), url),
xml_list: vec![],
xml_payload: String::new(),
}
}
pub fn getlastmodified(&mut self) -> &mut ReqProps {
self.xml_list.push(String::from("getlastmodified"));
self.xml_payload.push_str(r#"<d:getlastmodified/>"#);
self
}
pub fn getcontentlenght(&mut self) -> &mut ReqProps {
self.xml_list.push(String::from("getcontentlength"));
self.xml_payload.push_str(r#"<d:getcontentlength/>"#);
self
}
pub fn getcontenttype(&mut self) -> &mut ReqProps {
self.xml_list.push(String::from("getcontenttype"));
self.xml_payload.push_str(r#"<d:getcontenttype/>"#);
self
}
pub fn getpermissions(&mut self) -> &mut ReqProps {
self.xml_list.push(String::from("permissions"));
self.xml_payload.push_str(r#"<oc:permissions/>"#);
self
}
pub fn getressourcetype(&mut self) -> &mut ReqProps {
self.xml_list.push(String::from("resourcetype"));
self.xml_payload.push_str(r#"<d:resourcetype/>"#);
self
}
pub fn getetag(&mut self) -> &mut ReqProps {
self.xml_list.push(String::from("getetag"));
self.xml_payload.push_str(r#"<d:getetag/>"#);
self
}
fn validate_xml(&mut self) -> &mut ReqProps {
let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8"?><d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"><d:prop>"#);
xml.push_str(&self.xml_payload.clone());
xml.push_str(r#"</d:prop></d:propfind>"#);
self.api_builder.set_xml(xml);
self
}
pub async fn send(&mut self) -> Result<Response, Error> {
self.validate_xml();
self.api_builder.send().await
}
pub async fn send_with_err(&mut self) -> Result<String, ApiError> {
let res = self.send().await.map_err(ApiError::RequestError)?;
if res.status().is_success() {
let body = res.text().await.map_err(ApiError::EmptyError)?;
Ok(body)
} else {
Err(ApiError::IncorrectRequest(res))
}
}
pub async fn send_with_res(&mut self) -> Vec<String> {
match self.send_with_err().await {
Ok(body) => self.parse(body),
Err(ApiError::IncorrectRequest(err)) => {
eprintln!("fatal: {}", err.status());
std::process::exit(1);
},
Err(ApiError::EmptyError(_)) => {
eprintln!("Failed to get body");
vec![]
}
Err(ApiError::RequestError(err)) => {
eprintln!("fatal: {}", err);
std::process::exit(1);
}
}
}
pub fn parse(&self, xml: String) -> Vec<String> {
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut should_get = false;
let mut values: Vec<String> = vec![];
let mut iter = self.xml_list.iter();
let mut val = iter.next();
for event in parser {
match event {
Ok(XmlEvent::StartElement { name, .. }) => {
if let Some(v) = val.clone() {
should_get = &name.local_name == v;
} else {
break;
}
}
Ok(XmlEvent::Characters(text)) => {
if !text.trim().is_empty() && should_get {
values.push(text);
val = iter.next()
}
}
Ok(XmlEvent::EndElement { .. }) => {
should_get = false;
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
_ => {}
}
}
values
}
}

3
src/store.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod index;
pub mod head;
pub mod object;

View File

@ -1,5 +1,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::utils::{read, head, path}; use crate::utils::{read, path};
use crate::store::head;
use crypto::sha1::Sha1; use crypto::sha1::Sha1;
use crypto::digest::Digest; use crypto::digest::Digest;
use std::fs::{OpenOptions, self}; use std::fs::{OpenOptions, self};
@ -11,14 +12,14 @@ use std::fs::File;
/// # Examples /// # Examples
/// Input: /foo/bar /// Input: /foo/bar
/// Result: ("tree hash(/foo/bar) bar", hash(/foo/bar), bar) /// Result: ("tree hash(/foo/bar) bar", hash(/foo/bar), bar)
fn parse_path(path: &Path) -> (String, String, String) { fn parse_path(path: &Path, is_blob: bool) -> (String, String, String) {
let file_name = path.file_name().unwrap().to_str().unwrap(); let file_name = path.file_name().unwrap().to_str().unwrap();
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.input_str(path.clone().to_str().unwrap()); hasher.input_str(path.clone().to_str().unwrap());
let hash = hasher.result_str(); let hash = hasher.result_str();
let mut line = String::from("tree"); let mut line = String::from(if is_blob { "tree" } else { "blob" });
line.push_str(" "); line.push_str(" ");
line.push_str(&hash); line.push_str(&hash);
line.push_str(" "); line.push_str(" ");
@ -40,7 +41,7 @@ pub fn parse_line(line: String) -> (String, String, String) {
} }
pub fn add_tree(path: &Path) -> io::Result<()> { pub fn add_tree(path: &Path) -> io::Result<()> {
let (line, hash, name) = parse_path(path.clone()); let (line, hash, name) = parse_path(path.clone(), false);
// add tree reference to parent // add tree reference to parent
if path.iter().count() == 1 { if path.iter().count() == 1 {
@ -55,6 +56,28 @@ pub fn add_tree(path: &Path) -> io::Result<()> {
Ok(()) Ok(())
} }
pub fn add_blob(path: &Path, date: &str) -> io::Result<()> {
let (line, hash, name) = parse_path(path.clone(), true);
// add tree reference to parent
if path.iter().count() == 1 {
head::add_line(line)?;
} else {
add_node(path.parent().unwrap(), &line)?;
}
let mut content = name.clone().to_owned();
content.push_str(" ");
content.push_str("tmp_hash");
content.push_str(" ");
content.push_str(date);
// create blob object
create_object(hash, &content)?;
Ok(())
}
fn hash_obj(obj: &str) -> (String, String) { fn hash_obj(obj: &str) -> (String, String) {
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.input_str(obj); hasher.input_str(obj);

View File

@ -1,5 +1,2 @@
pub mod path; pub mod path;
pub mod index;
pub mod head;
pub mod read; pub mod read;
pub mod object;