Compare commits

..

No commits in common. 'main' and 'db-refactoring' have entirely different histories.

  1. 5
      Cargo.toml
  2. 49
      README.md
  3. 178
      src/db.rs
  4. 52
      src/editor.rs
  5. 95
      src/encoder.rs
  6. 43
      src/git.rs
  7. 297
      src/main.rs
  8. 24
      src/paths.rs
  9. 253
      src/storage.rs

5
Cargo.toml

@ -6,9 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
aes-gcm = "0.10.3" base64 = "0.22.1"
clap = { version = "4.5.16", features = ["derive"] } clap = { version = "4.5.16", features = ["derive"] }
hex = "0.4.3" once_cell = "1.19.0"
rpassword = "7.3.1" rpassword = "7.3.1"
tempfile = "3.12.0" tempfile = "3.12.0"

49
README.md

@ -1,50 +1,3 @@
# MPS # mps
A small tool for storing passwords locally with git sync A small tool for storing passwords locally with git sync
## Installation
1. Clone MPS repo:
```
git clone git@code.corpglory.net:corpglory/mps.git
```
2. Build MPS
```
cd mps
cargo build --release
```
3. Run init
```
mps init
```
4. Add your storage under git
```bash
cd storage
git init
git checkout -b main
git add db.mps
git commit -m "init mps"
git remote add origin <your_git_storage_url>
git push -u origin main
```
### Intilization with exisitng storage db repo
1. Create empty repository for storing your passwords on gitlab or somewhere you prefer
2. Clone to your home folder `<mps_path>/storage`: `git clone <your_git_storage_url> storage`
#### Exporting `MPS_HOME`
You can export variable `$MPS_HOME` to init storage for example in your home directory:
`export MPS_HOME="/home/<your_username>/.mps"` in your `~/.zshenv`
now `mps` will try to get storage from `$MPS_HOME/storage`
#### Add your mps build to PATH in your ~/.zshenv
```
export PATH=$PATH:"<mps_path>/target/release"
```

178
src/db.rs

@ -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(())
}
}

52
src/editor.rs

@ -2,23 +2,14 @@ use std::env;
use std::fs::read_to_string; use std::fs::read_to_string;
use std::process::Command; use std::process::Command;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use std::io::{self, Write}; use std::io;
const ENV_MPS_EDITOR: &str = "MPS_EDITOR"; pub fn open_to_edit() -> io::Result<String> {
const DEFAULT_EDITOR: &str = "nano";
// Use with content "" (empty string) for adding new item
pub fn open_to_edit(content: &String) -> io::Result<String> {
// Create a temporary file // Create a temporary file
let mut temp_file = NamedTempFile::new()?; let temp_file = NamedTempFile::new()?;
// Write content
write!(temp_file, "{}", content)?;
// Get the user's preferred editor from the $EDITOR environment variable // Get the user's preferred editor from the $EDITOR environment variable
// Default is 'nano' // Default is 'nano'
let editor = env::var(ENV_MPS_EDITOR).unwrap_or_else(|_| DEFAULT_EDITOR.to_string()); let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
// Get the path of the temp file // Get the path of the temp file
let file_path = temp_file.path().to_str().unwrap().to_string(); let file_path = temp_file.path().to_str().unwrap().to_string();
@ -27,43 +18,12 @@ pub fn open_to_edit(content: &String) -> io::Result<String> {
Command::new(editor) Command::new(editor)
.arg(&file_path) .arg(&file_path)
.status() .status()
.map_err(|e| io::Error::new( .expect("Failed to open editor");
io::ErrorKind::Other,
format!("Failed to launch editor: {}", e)
))?;
// Read the file content after editing // Read the file content after editing
let mut edited_content = read_to_string(&file_path)?; let edited_content = read_to_string(&file_path)?;
// Remove only one trailing newline if it exists
// because editor like vim or nano adds to the end new line
if edited_content.ends_with('\n') {
edited_content.pop();
}
// Print the edited content // Print the edited content
Ok(edited_content) Ok(edited_content)
}
pub fn open_to_show(content: &String) -> io::Result<()> {
let mut temp_file = NamedTempFile::new()?;
// Write the content to the file
write!(temp_file, "{}", content)?;
// Default is 'nano'
let editor = env::var(ENV_MPS_EDITOR).unwrap_or_else(|_| DEFAULT_EDITOR.to_string());
let file_path = temp_file.path().to_str().unwrap().to_string();
// Open the file in the external editor
Command::new(editor)
.arg(&file_path)
.status()
.map_err(|e| io::Error::new(
io::ErrorKind::Other,
format!("Failed to launch editor: {}", e)
))?;
Ok(())
} }

95
src/encoder.rs

@ -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())
}
}

43
src/git.rs

@ -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(())
}
}

297
src/main.rs

@ -1,24 +1,18 @@
mod paths; mod db;
mod encoder;
mod storage;
mod editor; mod editor;
mod git;
use storage::{Storage, Item}; use db::{DB, Item};
use clap::{Parser, Subcommand, ArgGroup}; use clap::{Parser, Subcommand};
use rpassword; use rpassword;
use std::io::{self, Write}; use std::io::{self, Write};
use std::process; use std::process;
const VERSION: &str = "0.0.1";
#[derive(Parser)] #[derive(Parser)]
#[command(name = "mps", version = VERSION, about = "MyPasswordStorage: Tool for storing your passwords locally with git synchronization")] #[command(name = "mps", version = "0.0.1", about = "MyPasswordStorage: Tool for storing your passwords locally with git synchronization")]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Option<Commands>,
@ -30,42 +24,18 @@ enum Commands {
/// Initialization of storage and config, use this in first time of usage of mps /// Initialization of storage and config, use this in first time of usage of mps
Init, Init,
/// Lists all ids stored in db /// Adds new item with unique id to the db
List,
/// Show content of an item
Show(ItemIdArgs),
/// Adds new item with unique id to the storage
Add { Add {
#[arg(value_name="item_id")] #[arg(value_name="item_id")]
id: String id: String
}, },
/// Edit item content /// Lists all ids stored in db
Edit(ItemIdArgs), List
/// Delete item
Delete(ItemIdArgs),
/// Set new passphrase
Password
}
#[derive(Parser)] // TODO: show
#[command(group( // TODO: edit
ArgGroup::new("item")
.required(true)
.args(&["id", "number"])
))]
struct ItemIdArgs {
/// Item id
#[arg(required=false)]
id: Option<String>,
#[arg(short='n', long, value_name="number")]
number: Option<u32>
} }
enum PROMPT { enum PROMPT {
@ -89,241 +59,90 @@ fn get_prompt(question: &str) -> io::Result<PROMPT> {
} }
struct MPS { // TODO: change password functionality
storage: Option<Storage> fn login() -> io::Result<String> {
} // TODO: check if inited
print!("Enter passphrase for storage: ");
impl MPS {
pub fn new() -> MPS {
MPS { storage: None }
}
fn print_init_hint() {
println!("1. Run `mps init`");
println!("2. Init `storage` under git:");
println!(" cd storage");
println!(" git init");
}
fn prompt_new_password() -> io::Result<String> {
print!("Enter passphrase: ");
io::stdout().flush()?;
let ps = rpassword::read_password()?;
if ps.len() < 8 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passphrase must be longer that 8 letters"));
}
if ps.len() > 32 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passphrase must not be longer than 32 letters"));
}
print!("Enter same passphrase again: ");
io::stdout().flush()?; io::stdout().flush()?;
let ps2 = rpassword::read_password()?; // TODO: check in db
if ps != ps2 { // TODO: return error if db is not inited
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passphrases must be equal")); let password = rpassword::read_password()?;
if password != "pass" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Wrong passphrase"));
} }
Ok(ps) Ok(String::from(password))
}
fn read_new_password() -> io::Result<String> {
let passphrase;
loop {
match MPS::prompt_new_password() {
Ok(ps) => {
passphrase = ps;
break;
},
Err(e) => {
println!("{}", e);
continue
}
}
}
Ok(passphrase)
} }
fn init() -> io::Result<()> { fn init() -> io::Result<()> {
if Storage::is_inited()? { if db::DB::is_inited() {
return Err(io::Error::new(io::ErrorKind::AlreadyExists, "Reinitialization attempted")); return Err(io::Error::new(io::ErrorKind::AlreadyExists, "Reinitialization attempted"));
} }
let passphrase = MPS::read_new_password()?;
Storage::init(passphrase)
}
pub fn login(&mut self) -> io::Result<()> {
if self.storage.is_some() {
return Ok(());
}
print!("Enter passphrase for storage: "); print!("Enter passphrase for storage: ");
io::stdout().flush()?; io::stdout().flush()?;
let passphrase = rpassword::read_password()?; // TODO: rename to passphrase
print!("\x1B[1A\x1B[2K"); // Move cursor up one line and clear that line let password = rpassword::read_password()?;
print!("Reenter passphrase: ");
io::stdout().flush()?; io::stdout().flush()?;
// storage will return error if passphrase is incorrect let password2 = rpassword::read_password()?;
self.storage = Some(Storage::from_db(passphrase)?);
Ok(())
}
pub fn password(&mut self) -> io::Result<()> { if password != password2 {
self.login()?; return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passwords must be equal"));
let st = self.storage.as_ref().unwrap();
let passphrase = MPS::read_new_password()?;
let new_st = st.new_from_passphrase(&passphrase);
new_st.dump()
} }
fn list(&mut self) -> io:: Result<()> { db::DB::init(password)?;
self.login()?;
let ids = self.storage.as_ref().unwrap().ids();
let mut counter = 1;
if ids.len() == 0 {
println!("No items");
} else {
for id in ids {
println!("[{}] {}", counter, id);
counter += 1;
}
}
Ok(())
}
/// Show item. Id must exists Ok(())
fn show(&mut self, id: &String) -> io::Result<()> {
self.login()?;
let st = self.storage.as_ref().unwrap();
let item = st.get(id);
editor::open_to_show(&item.content)
} }
fn add(&mut self, id: &String) -> io::Result<()> { fn add(id: &String) -> io::Result<()> {
self.login()?; // TODO: get login passphrase
let st = self.storage.as_mut().unwrap(); let mut db = DB::new(String::from(""))?;
if st.contains(id) { if db.contains(id) {
let question = format!("Item [{}] exist. Do you want to edit it instead?", id); // TODO: ask to edit existing in outer function which invoked this one
match get_prompt(&question)? { return Err(io::Error::new(
PROMPT::YES => { io::ErrorKind::InvalidInput,
self.edit(id)?; format!("Dublicate item id: {}. Item id's must be unique.", id)
return Ok(()); ))
},
PROMPT::NO => return Ok(()),
}
}
// set empty string because there is no content yet
let content = editor::open_to_edit(&String::from(""))?;
st.add(Item::from(id.clone(), content));
st.dump()
} }
/// Edit item, id need not to exist let content = editor::open_to_edit()?;
fn edit(&mut self, id: &String) -> io::Result<()> { db.items.insert(Item::from(id.clone(), content));
self.login()?; db.dump()?;
let st = self.storage.as_mut().unwrap();
if !st.contains(id) {
let question = format!("Item [{}] exist. Do you want to add it instead?", id);
match get_prompt(&question)? {
PROMPT::YES => {
self.add(id)?;
return Ok(());
}
PROMPT::NO => return Ok(())
}
}
let mut item = (*st.get(id)).clone();
let new_content = editor::open_to_edit(&item.content)?;
item.content = new_content;
st.update(item);
st.dump()
}
// Delete item by id, is must exist Ok(())
fn delete(&mut self, id: &String) -> io::Result<()> {
self.login()?;
let st = self.storage.as_mut().unwrap();
st.remove(id);
st.dump()
} }
/// Resolve id by ItemArgs. fn list() -> io:: Result<()> {
/// # Arguments let db = DB::new(String::from(""))?;
/// * `args` - arguments to parse let mut vec: Vec<_> = db.items.iter().collect();
/// * `check` - check that id existing vec.sort();
fn item_id_by_item_id_args(&mut self, args: &ItemIdArgs, check: bool) -> io::Result<String> { for item in &vec {
self.login()?; println!("{}", item.id);
let st = self.storage.as_mut().unwrap();
let mut item_id: String = "NOT_INITIALIZED".to_string();
if let Some(id) = &args.id {
item_id = id.clone();
if check && !st.contains(&id) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("No such item {}", id)
));
}
}
if let Some(number) = &args.number {
item_id = st.get_id_by_number(*number)?;
// we can guarantee that id exists because we take it by id
}
if args.id.is_none() && args.number.is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Bag arguments")
));
}
Ok(item_id.clone())
} }
Ok(())
} }
fn run_command() -> io::Result<()> { fn run_command() -> io::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
match &cli.command { match &cli.command {
Some(Commands::Init) => { Some(Commands::Init) => {
MPS::init()?; init()?;
}
Some(Commands::List) => {
let mut mps = MPS::new();
mps.list()?;
}
Some(Commands::Show(args)) => {
let mut mps = MPS::new();
let id = mps.item_id_by_item_id_args(args, true)?;
mps.show(&id)?;
} }
Some(Commands::Add{ id }) => { Some(Commands::Add{ id }) => {
let mut mps = MPS::new(); add(id)?;
mps.add(id)?;
}
Some(Commands::Edit(args)) => {
let mut mps = MPS::new();
let id = mps.item_id_by_item_id_args(args, false)?;
mps.edit(&id)?;
} }
Some(Commands::Delete(args)) => { Some(Commands::List) => {
let mut mps = MPS::new(); list()?;
let id = mps.item_id_by_item_id_args(args, true)?;
mps.delete(&id)?;
}
Some(Commands::Password) => {
let mut mps = MPS::new();
mps.password()?;
} }
None => { None => {
match Storage::check_installed() { if !db::DB::is_inited() {
Ok(()) => (), match get_prompt("Do you want to init your storage?")? {
Err(e) => { PROMPT::YES => init()?,
println!("{}", e); PROMPT::NO => db::DB::print_init_hint(),
MPS::print_init_hint();
return Ok(());
}
}
if !Storage::is_inited()? {
match get_prompt("Do you want to init storage db?")? {
PROMPT::YES => MPS::init()?,
PROMPT::NO => return Ok(()),
} }
} else { } else {
let mut mps = MPS::new(); login()?;
mps.list()? // TODO: list()
} }
} }
} }

24
src/paths.rs

@ -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)
}

253
src/storage.rs

@ -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…
Cancel
Save