use crate::paths; use crate::encoder; use crate::git; use encoder::Encoder; use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::fs; use std::path::Path; use std::io::{self, Write, BufRead}; use std::fmt; use std::cmp::{PartialEq, Ordering}; #[derive(Clone)] pub struct Item { pub id: String, pub content: String } impl Item { pub fn from(s: String, c: String) -> Item { Item { id: s, content: c } } // used only to search in HashSet pub fn from_empty(s: String) -> Item { Item { id: s, content: String::from("") } } } impl PartialEq for Item { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for Item {} impl Hash for Item { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl fmt::Display for Item { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", self.id)?; writeln!(f, "---------")?; writeln!(f, "{}", self.content) } } impl PartialOrd for Item { fn partial_cmp(&self, other: &Self) -> Option { self.id.partial_cmp(&other.id) } } impl Ord for Item { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } pub struct Storage { items: HashSet::, encoder: Encoder } impl Storage { pub fn new(passphrase: String) -> Storage { Storage { items: HashSet::::new(), encoder: Encoder::from(&passphrase) } } pub fn from_db(passphrase: String) -> io::Result { if !Storage::is_inited()? { return Err(io::Error::new( io::ErrorKind::Other, "Storage is not initialized" )); } let encoder = Encoder::from(&passphrase); let file = fs::File::open(paths::get_db_path()?)?; let reader = io::BufReader::new(file); let mut items = HashSet::::new(); let mut id: Option = None; let mut lines = reader.lines(); let passtest = match lines.next() { Some(line) => line?, None => return Err( io::Error::new(io::ErrorKind::InvalidData, "Bad storage db format: no passphrase in the beginnning" )), }; // TODO: only in debug mode //println!("passphrase ok"); if !encoder.test_encoded_passphrase(passtest)? { return Err(io::Error::new(io::ErrorKind::InvalidData, "Wrong passphrase")); } for line in lines { match line { Ok(line) => { if id.is_none() { let line = encoder.decrypt(line)?; // TODO: only in debug mode //println!("{}", line); id = Some(line); } else { let content = encoder.decrypt(line)?; // TODO: only in debug //println!("{}", content); items.insert(Item::from(id.unwrap(), content)); id = None; } }, Err(e) => { eprintln!("Error reading line, {}", e); } } } Ok(Storage { items, encoder }) } pub fn init(passphrase: String) -> io::Result<()> { //Storage::check_installed()?; let sp = paths::get_storage_path()?; fs::create_dir(sp)?; let st = Storage::new(passphrase); st.dump_db()?; println!("Storage db created"); Ok(()) } pub fn check_installed() -> io::Result<()> { let sp = paths::get_storage_path()?; let storage_path = Path::new(&sp); // Check if the folder exists and is a directory if !storage_path.exists() || !storage_path.is_dir() { return Err(io::Error::new( io::ErrorKind::NotFound, format!("{} does not exist or not a dir", sp) )); } let git_path = storage_path.join(".git"); if !git_path.exists() || !git_path.is_dir() { return Err(io::Error::new( io::ErrorKind::NotFound, format!("{} not under git", sp) )); } Ok(()) } pub fn is_inited() -> io::Result { //Storage::check_installed()?; let db = paths::get_db_path()?; let db_path = Path::new(&db); Ok(db_path.exists()) } pub fn ids(&self) -> Vec { let mut result = Vec::new(); for item in self.items.iter() { result.push(item.id.clone()); } result.sort(); result } pub fn contains(&self, id: &String) -> bool { let item = Item::from_empty(id.clone()); self.items.contains(&item) } // TODO: return Result pub fn get(&self, id: &String) -> &Item { let item = Item::from_empty(id.clone()); self.items.get(&item).unwrap() } /// Counting starts from 1, according to UI pub fn get_id_by_number(&self, number: u32) -> io::Result { let number = number as usize; if number == 0 { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Items numbering starts from 1" )); } let ids = self.ids(); if number > ids.len() { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("There are only {} items, but asked with number {}", ids.len(), number) )); } Ok(ids[number - 1].clone()) } pub fn add(&mut self, item: Item) { self.items.insert(item); } pub fn update(&mut self, item: Item) { self.items.remove(&item); self.items.insert(item); } pub fn remove(&mut self, id: &String) { let item = Item::from_empty(id.clone()); self.items.remove(&item); } pub fn dump(&self) -> io::Result<()> { self.dump_db()?; git::Git::sync()?; Ok(()) } fn dump_db(&self) -> io::Result<()> { let mut file = fs::OpenOptions::new() .write(true) .truncate(true) // Clear the file content before writing .create(true) .open(paths::get_db_path()?)?; writeln!(file, "{}", self.encoder.get_encoded_test_passphrase()?)?; for item in self.items.iter() { writeln!(file, "{}", self.encoder.encrypt(&item.id)?)?; let content = self.encoder.encrypt(&item.content)?; writeln!(file, "{}", content)?; } Ok(()) } pub fn new_from_passphrase(&self, passphrase: &String) -> Storage { return Storage { encoder: Encoder::from(passphrase), items: self.items.clone() } } }