|
|
|
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<H: Hasher>(&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<Ordering> {
|
|
|
|
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::<Item>,
|
|
|
|
encoder: Encoder
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Storage {
|
|
|
|
pub fn new(passphrase: String) -> Storage {
|
|
|
|
Storage {
|
|
|
|
items: HashSet::<Item>::new(),
|
|
|
|
encoder: Encoder::from(&passphrase)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn from_db(passphrase: String) -> io::Result<Storage> {
|
|
|
|
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::<Item>::new();
|
|
|
|
let mut id: Option<String> = 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<bool> {
|
|
|
|
//Storage::check_installed()?;
|
|
|
|
let db = paths::get_db_path()?;
|
|
|
|
let db_path = Path::new(&db);
|
|
|
|
Ok(db_path.exists())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn ids(&self) -> Vec<String> {
|
|
|
|
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<Item>
|
|
|
|
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<String> {
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|