Compare commits

..

54 Commits

Author SHA1 Message Date
Coin de Gamma 0d6fcbbd11 change passeord 3 months ago
Coin de Gamma c3c5a9c11a resolve item_id 3 months ago
Coin de Gamma 19717f8f22 Merge pull request 'item id arggroup' (#28) from item-number-refactoring into main 3 months ago
Coin de Gamma c5f6cdd485 item id arggroup 3 months ago
Coin de Gamma f71562ab91 list / show / edit numbers 3 months ago
Coin de Gamma 391b456459 README++ 3 months ago
Coin de Gamma 9da8c60340 optional & new init inistructions 3 months ago
Coin de Gamma e19cd571f9 init withoud db #24 3 months ago
Coin de Gamma d021281449 rm debug print in dump 3 months ago
Coin de Gamma 3d8b5fdfdd rm debug print in remove 3 months ago
Coin de Gamma ed40720295 Merge pull request 'fix typo' (#17) from fix-typo into main 3 months ago
Coin de Gamma 0e854941bf Merge pull request 'Bug: `mps delete` not working #20' (#22) from mpd-delete-not-working-#20 into main 3 months ago
Coin de Gamma d8eb9018a7 truncate db 3 months ago
rozetko d438efde15 fix typo 3 months ago
Coin de Gamma 956a964303 Update 'README.md' 3 months ago
Coin de Gamma c66fa2792d basic delete 3 months ago
Coin de Gamma 60d4f53ce1 installation++ 3 months ago
Coin de Gamma 1d4a94eefc README++ 3 months ago
Coin de Gamma 83dd5c3b83 rm couonter 3 months ago
Coin de Gamma 4013a9dd08 hints++ 3 months ago
Coin de Gamma feb2a7d272 intallation++ 3 months ago
Coin de Gamma 2dfe467b4d reenter passphrase message 3 months ago
Coin de Gamma 0296a1fb0e git push fix 3 months ago
Coin de Gamma b1de728eb9 git begin 3 months ago
Coin de Gamma cc6b069082 git begin 3 months ago
Coin de Gamma e4d2217d6e prints++ 3 months ago
Coin de Gamma be2bb6a9b4 init++ 3 months ago
Coin de Gamma d7a6259a7e empty show 3 months ago
Coin de Gamma 1b96335853 init++ 3 months ago
Coin de Gamma 6dd1bc7d04 init++ 3 months ago
Coin de Gamma 76ff813f98 init++ 3 months ago
Coin de Gamma 4190c93e9e storage & git begin 3 months ago
Coin de Gamma 381fc1f6aa encrypt ids 3 months ago
Coin de Gamma 209e49abe8 encoder++ 3 months ago
Coin de Gamma de6a028e8b encrypt pass 3 months ago
Coin de Gamma 3cea889755 encoder++ 3 months ago
Coin de Gamma 9d7a82d75c encoder++ 3 months ago
Coin de Gamma 22a25a3565 encoder 3 months ago
Coin de Gamma 174244fcef passphrasse test in db 3 months ago
Coin de Gamma ef2537e204 MPS struct++ 3 months ago
Coin de Gamma 8eb62289a1 mps struct begin 3 months ago
Coin de Gamma 421d861b2e new line after editing 3 months ago
Coin de Gamma c679d4bf74 check inited in storage 3 months ago
Coin de Gamma d0adf49fee basic editing 3 months ago
Coin de Gamma 3d4d657f87 edit on add existing 3 months ago
Coin de Gamma 791606ad1b edit begin 3 months ago
Coin de Gamma a7f8d8d689 passphrase++ 3 months ago
Coin de Gamma 02ab45c9c7 basic show 3 months ago
Coin de Gamma dae49bcb61 code clearup 3 months ago
Coin de Gamma 542ecd60ce rm pass: 3 months ago
Coin de Gamma 1e738bb907 basic show 3 months ago
Coin de Gamma 1e6fcb56c0 password continue 3 months ago
Coin de Gamma 5008e7b45c db -> storage 3 months ago
Coin de Gamma 8e671dbe08 Merge pull request 'db-refactoring' (#14) from db-refactoring into main 3 months ago
  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. 307
      src/main.rs
  8. 24
      src/paths.rs
  9. 253
      src/storage.rs

5
Cargo.toml

@ -6,8 +6,9 @@ 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]
base64 = "0.22.1" aes-gcm = "0.10.3"
clap = { version = "4.5.16", features = ["derive"] } clap = { version = "4.5.16", features = ["derive"] }
once_cell = "1.19.0" hex = "0.4.3"
rpassword = "7.3.1" rpassword = "7.3.1"
tempfile = "3.12.0" tempfile = "3.12.0"

49
README.md

@ -1,3 +1,50 @@
# 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

@ -1,178 +0,0 @@
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,14 +2,23 @@ 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; use std::io::{self, Write};
pub fn open_to_edit() -> io::Result<String> { const ENV_MPS_EDITOR: &str = "MPS_EDITOR";
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 temp_file = NamedTempFile::new()?; let mut 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("EDITOR").unwrap_or_else(|_| "nano".to_string()); let editor = env::var(ENV_MPS_EDITOR).unwrap_or_else(|_| DEFAULT_EDITOR.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();
@ -18,12 +27,43 @@ pub fn open_to_edit() -> io::Result<String> {
Command::new(editor) Command::new(editor)
.arg(&file_path) .arg(&file_path)
.status() .status()
.expect("Failed to open editor"); .map_err(|e| io::Error::new(
io::ErrorKind::Other,
format!("Failed to launch editor: {}", e)
))?;
// Read the file content after editing // Read the file content after editing
let edited_content = read_to_string(&file_path)?; let mut 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

@ -0,0 +1,95 @@
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

@ -0,0 +1,43 @@
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(())
}
}

307
src/main.rs

@ -1,18 +1,24 @@
mod db; mod paths;
mod encoder;
mod storage;
mod editor; mod editor;
mod git;
use db::{DB, Item}; use storage::{Storage, Item};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand, ArgGroup};
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 = "0.0.1", about = "MyPasswordStorage: Tool for storing your passwords locally with git synchronization")] #[command(name = "mps", version = VERSION, 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>,
@ -24,20 +30,44 @@ 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,
/// Adds new item with unique id to the db /// Lists all ids stored in 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
}, },
/// Lists all ids stored in db /// Edit item content
List Edit(ItemIdArgs),
/// Delete item
Delete(ItemIdArgs),
// TODO: show /// Set new passphrase
// TODO: edit Password
} }
#[derive(Parser)]
#[command(group(
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 {
YES, YES,
NO NO
@ -59,90 +89,241 @@ fn get_prompt(question: &str) -> io::Result<PROMPT> {
} }
// TODO: change password functionality struct MPS {
fn login() -> io::Result<String> { storage: Option<Storage>
// 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()?; io::stdout().flush()?;
// TODO: check in db let ps = rpassword::read_password()?;
// TODO: return error if db is not inited if ps.len() < 8 {
let password = rpassword::read_password()?; return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passphrase must be longer that 8 letters"));
if password != "pass" { }
return Err(io::Error::new(io::ErrorKind::InvalidData, "Wrong passphrase")); 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()?;
let ps2 = rpassword::read_password()?;
if ps != ps2 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passphrases must be equal"));
}
Ok(ps)
} }
Ok(String::from(password))
}
fn init() -> io::Result<()> { fn read_new_password() -> io::Result<String> {
if db::DB::is_inited() { let passphrase;
loop {
match MPS::prompt_new_password() {
Ok(ps) => {
passphrase = ps;
break;
},
Err(e) => {
println!("{}", e);
continue
}
}
}
Ok(passphrase)
}
fn init() -> io::Result<()> {
if Storage::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()?;
// TODO: rename to passphrase let passphrase = rpassword::read_password()?;
let password = rpassword::read_password()?; print!("\x1B[1A\x1B[2K"); // Move cursor up one line and clear that line
print!("Reenter passphrase: ");
io::stdout().flush()?; io::stdout().flush()?;
let password2 = rpassword::read_password()?; // storage will return error if passphrase is incorrect
self.storage = Some(Storage::from_db(passphrase)?);
if password != password2 { Ok(())
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Passwords must be equal"));
} }
db::DB::init(password)?; pub fn password(&mut self) -> io::Result<()> {
self.login()?;
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<()> {
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(()) Ok(())
} }
fn add(id: &String) -> io::Result<()> { /// Show item. Id must exists
// TODO: get login passphrase fn show(&mut self, id: &String) -> io::Result<()> {
let mut db = DB::new(String::from(""))?; self.login()?;
if db.contains(id) { let st = self.storage.as_ref().unwrap();
// TODO: ask to edit existing in outer function which invoked this one let item = st.get(id);
return Err(io::Error::new( editor::open_to_show(&item.content)
io::ErrorKind::InvalidInput,
format!("Dublicate item id: {}. Item id's must be unique.", id)
))
} }
let content = editor::open_to_edit()?; fn add(&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 edit it instead?", id);
match get_prompt(&question)? {
PROMPT::YES => {
self.edit(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()
}
Ok(()) /// Edit item, id need not to exist
} fn edit(&mut self, id: &String) -> io::Result<()> {
self.login()?;
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()
}
fn list() -> io:: Result<()> { // Delete item by id, is must exist
let db = DB::new(String::from(""))?; fn delete(&mut self, id: &String) -> io::Result<()> {
let mut vec: Vec<_> = db.items.iter().collect(); self.login()?;
vec.sort(); let st = self.storage.as_mut().unwrap();
for item in &vec { st.remove(id);
println!("{}", item.id); st.dump()
}
/// Resolve id by ItemArgs.
/// # Arguments
/// * `args` - arguments to parse
/// * `check` - check that id existing
fn item_id_by_item_id_args(&mut self, args: &ItemIdArgs, check: bool) -> io::Result<String> {
self.login()?;
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) => {
init()?; MPS::init()?;
}
Some(Commands::Add{ id }) => {
add(id)?;
} }
Some(Commands::List) => { Some(Commands::List) => {
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 }) => {
let mut mps = MPS::new();
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)) => {
let mut mps = MPS::new();
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 => {
if !db::DB::is_inited() { match Storage::check_installed() {
match get_prompt("Do you want to init your storage?")? { Ok(()) => (),
PROMPT::YES => init()?, Err(e) => {
PROMPT::NO => db::DB::print_init_hint(), println!("{}", e);
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 {
login()?; let mut mps = MPS::new();
// TODO: list() mps.list()?
} }
} }
} }

24
src/paths.rs

@ -0,0 +1,24 @@
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

@ -0,0 +1,253 @@
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