diff --git a/README.md b/README.md index fea0130..1d133bd 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Commands: edit Edit the password content gen Generate password export Export the passwords + import Import passwords help Print this message or the help of the given subcommand(s) Options: diff --git a/src/cli/export_command.rs b/src/cli/export_command.rs index 531d613..b8b4a9f 100644 --- a/src/cli/export_command.rs +++ b/src/cli/export_command.rs @@ -14,50 +14,53 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{fs, path::PathBuf}; +use std::{fs, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf}; -use clap::{Args, ValueEnum}; +use clap::Args; use crate::{ - password::{BitWardenPasswords, Passwords}, + password::{BitWardenPasswords, Format, Passwords}, LprsError, LprsResult, RunCommand, }; -#[derive(Clone, Debug, ValueEnum)] -pub enum ExportFormat { - Lprs, - BitWarden, -} - #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] pub struct Export { /// The path to export to path: PathBuf, /// Format to export passwords in - #[arg(short, long, value_name = "FORMAT", default_value_t= ExportFormat::Lprs)] - format: ExportFormat, -} - -impl ToString for ExportFormat { - fn to_string(&self) -> String { - self.to_possible_value() - .expect("There is no skiped values") - .get_name() - .to_owned() - } + #[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)] + format: Format, } impl RunCommand for Export { fn run(&self, password_manager: Passwords) -> LprsResult<()> { - let exported_data = match self.format { - ExportFormat::Lprs => serde_json::to_string(&password_manager.encrypt()?.passwords), - ExportFormat::BitWarden => { - serde_json::to_string(&BitWardenPasswords::from(password_manager)) - } - } - .map_err(LprsError::from)?; + if self + .path + .extension() + .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) + { + if !self.path.exists() { + let exported_data = match self.format { + Format::Lprs => serde_json::to_string(&password_manager.encrypt()?.passwords), + Format::BitWarden => { + serde_json::to_string(&BitWardenPasswords::from(password_manager)) + } + } + .map_err(LprsError::from)?; - fs::write(&self.path, exported_data).map_err(LprsError::from) + fs::write(&self.path, exported_data).map_err(LprsError::from) + } else { + Err(LprsError::Io(IoError::new( + IoErrorKind::AlreadyExists, + format!("file `{}` is already exists", self.path.display()), + ))) + } + } else { + Err(LprsError::Io(IoError::new( + IoErrorKind::InvalidInput, + format!("file `{}` is not a json file", self.path.display()), + ))) + } } } diff --git a/src/cli/import_command.rs b/src/cli/import_command.rs new file mode 100644 index 0000000..1315945 --- /dev/null +++ b/src/cli/import_command.rs @@ -0,0 +1,88 @@ +// Lprs - A local CLI password manager +// Copyright (C) 2024 Awiteb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::{fs::File, io::Error as IoError, io::ErrorKind as IoErrorKind, path::PathBuf}; + +use clap::Args; + +use crate::{ + password::{BitWardenPasswords, Format, Password, Passwords}, + LprsError, LprsResult, RunCommand, +}; + +#[derive(Debug, Args)] +#[command(author, version, about, long_about = None)] +pub struct Import { + /// The file path to import from + path: PathBuf, + + /// The format to import from + #[arg(short, long, default_value_t = Format::Lprs)] + format: Format, +} + +impl RunCommand for Import { + fn run(&self, mut password_manager: Passwords) -> LprsResult<()> { + if self.path.exists() { + if self + .path + .extension() + .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) + { + let imported_passwords_len = match self.format { + Format::Lprs => { + let passwords = Passwords::try_reload( + self.path.to_path_buf(), + password_manager.master_password.to_vec(), + )?; + let passwords_len = passwords.passwords.len(); + + password_manager.passwords.extend(passwords.passwords); + password_manager.try_export()?; + passwords_len + } + Format::BitWarden => { + let passwords: BitWardenPasswords = + serde_json::from_reader(File::open(&self.path)?)?; + let passwords_len = passwords.items.len(); + + password_manager + .passwords + .extend(passwords.items.into_iter().map(Password::from)); + password_manager.try_export()?; + passwords_len + } + }; + println!( + "{imported_passwords_len} password{s} were imported successfully", + s = if imported_passwords_len >= 2 { "s" } else { "" } + ); + + Ok(()) + } else { + Err(LprsError::Io(IoError::new( + IoErrorKind::InvalidInput, + format!("file `{}` is not a json file", self.path.display()), + ))) + } + } else { + Err(LprsError::Io(IoError::new( + IoErrorKind::NotFound, + format!("file `{}` not found", self.path.display()), + ))) + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cff9531..8036cf6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -28,6 +28,7 @@ pub mod clean_command; pub mod edit_command; pub mod export_command; pub mod gen_command; +pub mod import_command; pub mod list_command; pub mod remove_command; @@ -40,8 +41,7 @@ crate::create_commands!( "Edit the password content", Edit => edit_command::Edit "Generate password", Gen => gen_command::Gen "Export the passwords", Export => export_command::Export - // TODO: Export command - // TODO: Import command + "Import passwords", Import => import_command::Import ); #[derive(Parser, Debug)] diff --git a/src/password/bitwarden.rs b/src/password/bitwarden.rs index 5a89ce4..7b46ea4 100644 --- a/src/password/bitwarden.rs +++ b/src/password/bitwarden.rs @@ -39,6 +39,21 @@ pub struct BitWardenPasswords { pub items: Vec, } +impl From for Password { + fn from(value: BitWardenPassword) -> Self { + Self { + name: value.name, + username: value.login.username, + password: value.login.password, + service: value + .login + .uris + .and_then(|p| p.first().map(|u| u.uri.clone())), + note: value.notes, + } + } +} + impl From for BitWardenPassword { fn from(value: Password) -> Self { Self { diff --git a/src/password/mod.rs b/src/password/mod.rs index f5693a0..32bbe67 100644 --- a/src/password/mod.rs +++ b/src/password/mod.rs @@ -16,7 +16,7 @@ use std::{fs, path::PathBuf}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; use crate::{LprsError, LprsResult}; @@ -29,6 +29,12 @@ mod validator; pub use bitwarden::*; pub use validator::*; +#[derive(Clone, Debug, ValueEnum)] +pub enum Format { + Lprs, + BitWarden, +} + /// The password struct #[serde_with_macros::skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize, Parser)] @@ -145,3 +151,12 @@ impl Passwords { self.passwords.push(password) } } + +impl ToString for Format { + fn to_string(&self) -> String { + self.to_possible_value() + .expect("There is no skiped values") + .get_name() + .to_owned() + } +}