Compare commits
No commits in common. 'main' and 'db-refactoring' have entirely different histories.
main
...
db-refacto
9 changed files with 257 additions and 763 deletions
@ -0,0 +1,178 @@
|
||||
use base64::prelude::*; |
||||
|
||||
use once_cell::sync::Lazy; |
||||
|
||||
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}; |
||||
|
||||
|
||||
static STORAGE_FOLDER: Lazy<String> = Lazy::new(|| "storage".to_string() ); |
||||
static STORAGE_PATH: Lazy<String> = Lazy::new(|| {
|
||||
format!("{}/db.mps", &*STORAGE_FOLDER) |
||||
}); |
||||
|
||||
|
||||
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) |
||||
} |
||||
} |
||||
|
||||
struct Encoder { |
||||
password: String |
||||
} |
||||
|
||||
impl Encoder { |
||||
pub fn from(password: String) -> Encoder { |
||||
Encoder { password: password } |
||||
} |
||||
|
||||
// TODO: get by ref
|
||||
pub fn encode(&self, line: String) -> String { |
||||
BASE64_STANDARD.encode(line) |
||||
} |
||||
|
||||
// TODO: review error type
|
||||
pub fn decode(&self, line: String) -> io::Result<String> { |
||||
let content = BASE64_STANDARD.decode(line).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; |
||||
match String::from_utf8(content) { |
||||
Ok(s) => Ok(s), |
||||
Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub struct DB { |
||||
pub items: HashSet::<Item>, |
||||
encoder: Encoder |
||||
} |
||||
|
||||
impl DB { |
||||
// TODO: make path as String too
|
||||
pub fn new(password: String) -> io::Result<DB> { |
||||
let encoder = Encoder::from(password); |
||||
// TODO: throw error is password is incorrect
|
||||
let file = fs::File::open(&*STORAGE_PATH)?; |
||||
let reader = io::BufReader::new(file); |
||||
let mut items = HashSet::<Item>::new(); |
||||
let mut id: Option<String> = None; |
||||
for line in reader.lines() { |
||||
match line { |
||||
Ok(line) => { |
||||
if id.is_none() { |
||||
id = Some(line); |
||||
} else { |
||||
let content = encoder.decode(line)?; |
||||
items.insert(Item::from(id.unwrap(), content)); |
||||
id = None; |
||||
} |
||||
}, |
||||
Err(e) => { |
||||
eprintln!("Error reading line, {}", e); |
||||
} |
||||
} |
||||
} |
||||
let result = DB { |
||||
items: items, |
||||
encoder: encoder |
||||
}; |
||||
Ok(result) |
||||
} |
||||
|
||||
pub fn init(_passphrase: String) -> io::Result<()> { |
||||
// TODO: use pasphrase
|
||||
fs::create_dir(&*STORAGE_FOLDER)?; |
||||
println!("Storage folder created"); |
||||
//let mut db = DB::init(&*STORAGE_PATH, pass)?;
|
||||
|
||||
fs::File::create(&*STORAGE_PATH)?; |
||||
println!("Storage db created."); |
||||
println!("Initialization complete."); |
||||
println!(""); |
||||
println!("Now it's required to add folder `{}` under git manually.", &*STORAGE_FOLDER); |
||||
println!("Don't worry it's going to be encrypted."); |
||||
Ok(()) |
||||
} |
||||
|
||||
pub fn print_init_hint() { |
||||
println!("mps can work only when storage inited."); |
||||
println!("Hint: you can restore your storage if you have it already:"); |
||||
println!(" git clone <your_storage_git_url> {}", &*STORAGE_FOLDER); |
||||
println!("to init manually your storage and config") |
||||
} |
||||
|
||||
pub fn is_inited() -> bool { |
||||
let path = Path::new(&*STORAGE_FOLDER); |
||||
return path.exists(); |
||||
} |
||||
|
||||
pub fn contains(&self, id: &String) -> bool { |
||||
let item = Item::from_empty(id.clone()); |
||||
self.items.contains(&item) |
||||
} |
||||
|
||||
pub fn dump(&self) -> io::Result<()> { |
||||
let mut file = fs::OpenOptions::new() |
||||
.write(true) |
||||
.append(false) |
||||
.open(&*STORAGE_PATH)?; |
||||
|
||||
for item in self.items.iter() { |
||||
writeln!(file, "{}", item.id)?; |
||||
let content = self.encoder.encode(item.content.clone()); |
||||
writeln!(file, "{}", content)?; |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
||||
|
@ -1,95 +0,0 @@
|
||||
use aes_gcm::{
|
||||
aead::{Aead, AeadCore, KeyInit, OsRng}, |
||||
Aes256Gcm, Key, Nonce |
||||
}; |
||||
|
||||
use std::io; |
||||
|
||||
|
||||
static PASSWORD_TEST: &str = "MyPasswordStorage"; // will be added with nonce for storing each time
|
||||
|
||||
|
||||
pub struct Encoder { |
||||
// will be stored with padding 32 bytes
|
||||
passphrase: String |
||||
} |
||||
|
||||
impl Encoder { |
||||
pub fn from(passphrase: &String) -> Encoder { |
||||
// TODO: throw error if password longer that 32 bytes
|
||||
let padded_passphrase = Encoder::get_passhrase_with_padding(passphrase); |
||||
Encoder { passphrase: padded_passphrase } |
||||
} |
||||
|
||||
fn get_passhrase_with_padding(passphrase: &String) -> String { |
||||
let mut result = passphrase.clone(); |
||||
while result.len() < 32 { |
||||
// use '-' for padding, can be anything else
|
||||
result.push('-'); |
||||
} |
||||
result |
||||
} |
||||
|
||||
// TODO: error type
|
||||
pub fn encrypt(&self, plain_text: &String) -> io::Result<String> { |
||||
let key = Key::<Aes256Gcm>::from_slice(self.passphrase.as_bytes());
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng); |
||||
|
||||
let cipher = Aes256Gcm::new(key); |
||||
|
||||
// TODO: mar error inted of expect
|
||||
let ciphered_data = cipher.encrypt(&nonce, plain_text.as_bytes()) |
||||
.map_err(|_| io::Error::new( |
||||
io::ErrorKind::Other, |
||||
"Failed to encrypt" |
||||
))?; |
||||
|
||||
// combining nonce and encrypted data together
|
||||
// for storage purpose
|
||||
let mut encrypted_data: Vec<u8> = nonce.to_vec(); |
||||
encrypted_data.extend_from_slice(&ciphered_data); |
||||
|
||||
Ok(hex::encode(encrypted_data)) |
||||
|
||||
} |
||||
|
||||
// TODO: review error type
|
||||
pub fn decrypt(&self, encrypted_data: String) -> io::Result<String> { |
||||
let encrypted_data = hex::decode(encrypted_data) |
||||
.map_err(|_| io::Error::new( |
||||
io::ErrorKind::Other, |
||||
"failed to decode hex string into vec" |
||||
))?; |
||||
|
||||
let key = Key::<Aes256Gcm>::from_slice(self.passphrase.as_bytes()); |
||||
let (nonce_arr, ciphered_data) = encrypted_data.split_at(12); |
||||
let nonce = Nonce::from_slice(nonce_arr); |
||||
let cipher = Aes256Gcm::new(key); |
||||
let plaintext = cipher.decrypt(nonce, ciphered_data) |
||||
.map_err(|_| io::Error::new( |
||||
io::ErrorKind::InvalidData, |
||||
"failed to decrypt data" |
||||
))?; |
||||
|
||||
let result = String::from_utf8(plaintext) |
||||
.map_err(|_| io::Error::new( |
||||
io::ErrorKind::InvalidData, |
||||
"failed to convert vector of bytes to string" |
||||
))?; |
||||
Ok(result) |
||||
} |
||||
|
||||
pub fn test_encoded_passphrase(&self, passphrase_encrypted: String) -> io::Result<bool> { |
||||
// TODO: better way to check error
|
||||
let decrypted = match self.decrypt(passphrase_encrypted) { |
||||
Ok(decrypted) => decrypted, |
||||
Err(_) => return Ok(false) |
||||
}; |
||||
Ok(PASSWORD_TEST == decrypted) |
||||
} |
||||
|
||||
pub fn get_encoded_test_passphrase(&self) -> io::Result<String> { |
||||
self.encrypt(&PASSWORD_TEST.to_string()) |
||||
} |
||||
|
||||
} |
@ -1,43 +0,0 @@
|
||||
use crate::paths; |
||||
|
||||
use std::process::Command; |
||||
use std::io; |
||||
|
||||
|
||||
pub struct Git { |
||||
} |
||||
|
||||
impl Git { |
||||
pub fn sync() -> io::Result<()> { |
||||
let sp = paths::get_storage_path()?; |
||||
Command::new("git") |
||||
.arg("add") |
||||
.arg(paths::PATH_DB) |
||||
.current_dir(&sp) |
||||
.status() |
||||
.map_err(|e| io::Error::new( |
||||
io::ErrorKind::Other, |
||||
format!("Failed to execute git add: {}", e) |
||||
))?; |
||||
Command::new("git") |
||||
.arg("commit") |
||||
.arg("-m") |
||||
.arg("\"sync\"") |
||||
.current_dir(&sp) |
||||
.status() |
||||
.map_err(|e| io::Error::new( |
||||
io::ErrorKind::Other, |
||||
format!("Failed to execute git commit: {}", e) |
||||
))?; |
||||
Command::new("git") |
||||
.arg("push") |
||||
.current_dir(&sp) |
||||
.status() |
||||
.map_err(|e| io::Error::new( |
||||
io::ErrorKind::Other, |
||||
format!("Failed to execute git push: {}", e) |
||||
))?; |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
@ -1,24 +0,0 @@
|
||||
use std::env; |
||||
use std::io; |
||||
|
||||
|
||||
pub static ENV_MPS_HOME: &str = "MPS_HOME"; |
||||
|
||||
pub static PATH_STORAGE: &str = "storage"; // should be under git
|
||||
pub static PATH_DB: &str = "db.mps"; |
||||
|
||||
|
||||
pub fn get_storage_path() -> io::Result<String> { |
||||
let result = match env::var(ENV_MPS_HOME) { |
||||
Ok(mps_home) => format!("{}/{}", mps_home, PATH_STORAGE), |
||||
Err(_) => PATH_STORAGE.to_string() |
||||
}; |
||||
Ok(result) |
||||
} |
||||
|
||||
pub fn get_db_path() -> io::Result<String> { |
||||
let st = get_storage_path()?; |
||||
let result = format!("{}/{}", st, PATH_DB); |
||||
Ok(result) |
||||
} |
||||
|
@ -1,253 +0,0 @@
|
||||
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() |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue