commit
e8d9941fee
6 changed files with 153 additions and 31 deletions
|
@ -38,6 +38,7 @@ Commands:
|
||||||
edit Edit the password content
|
edit Edit the password content
|
||||||
gen Generate password
|
gen Generate password
|
||||||
export Export the passwords
|
export Export the passwords
|
||||||
|
import Import passwords
|
||||||
help Print this message or the help of the given subcommand(s)
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
|
@ -14,50 +14,53 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
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::{
|
use crate::{
|
||||||
password::{BitWardenPasswords, Passwords},
|
password::{BitWardenPasswords, Format, Passwords},
|
||||||
LprsError, LprsResult, RunCommand,
|
LprsError, LprsResult, RunCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, ValueEnum)]
|
|
||||||
pub enum ExportFormat {
|
|
||||||
Lprs,
|
|
||||||
BitWarden,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
pub struct Export {
|
pub struct Export {
|
||||||
/// The path to export to
|
/// The path to export to
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
/// Format to export passwords in
|
/// Format to export passwords in
|
||||||
#[arg(short, long, value_name = "FORMAT", default_value_t= ExportFormat::Lprs)]
|
#[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)]
|
||||||
format: ExportFormat,
|
format: Format,
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for ExportFormat {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
self.to_possible_value()
|
|
||||||
.expect("There is no skiped values")
|
|
||||||
.get_name()
|
|
||||||
.to_owned()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunCommand for Export {
|
impl RunCommand for Export {
|
||||||
fn run(&self, password_manager: Passwords) -> LprsResult<()> {
|
fn run(&self, password_manager: Passwords) -> LprsResult<()> {
|
||||||
let exported_data = match self.format {
|
if self
|
||||||
ExportFormat::Lprs => serde_json::to_string(&password_manager.encrypt()?.passwords),
|
.path
|
||||||
ExportFormat::BitWarden => {
|
.extension()
|
||||||
serde_json::to_string(&BitWardenPasswords::from(password_manager))
|
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
|
||||||
}
|
{
|
||||||
}
|
if !self.path.exists() {
|
||||||
.map_err(LprsError::from)?;
|
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()),
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
88
src/cli/import_command.rs
Normal file
88
src/cli/import_command.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
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()),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ pub mod clean_command;
|
||||||
pub mod edit_command;
|
pub mod edit_command;
|
||||||
pub mod export_command;
|
pub mod export_command;
|
||||||
pub mod gen_command;
|
pub mod gen_command;
|
||||||
|
pub mod import_command;
|
||||||
pub mod list_command;
|
pub mod list_command;
|
||||||
pub mod remove_command;
|
pub mod remove_command;
|
||||||
|
|
||||||
|
@ -40,8 +41,7 @@ crate::create_commands!(
|
||||||
"Edit the password content", Edit => edit_command::Edit
|
"Edit the password content", Edit => edit_command::Edit
|
||||||
"Generate password", Gen => gen_command::Gen
|
"Generate password", Gen => gen_command::Gen
|
||||||
"Export the passwords", Export => export_command::Export
|
"Export the passwords", Export => export_command::Export
|
||||||
// TODO: Export command
|
"Import passwords", Import => import_command::Import
|
||||||
// TODO: Import command
|
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
|
|
@ -39,6 +39,21 @@ pub struct BitWardenPasswords {
|
||||||
pub items: Vec<BitWardenPassword>,
|
pub items: Vec<BitWardenPassword>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BitWardenPassword> 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<Password> for BitWardenPassword {
|
impl From<Password> for BitWardenPassword {
|
||||||
fn from(value: Password) -> Self {
|
fn from(value: Password) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Parser, ValueEnum};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{LprsError, LprsResult};
|
use crate::{LprsError, LprsResult};
|
||||||
|
@ -29,6 +29,12 @@ mod validator;
|
||||||
pub use bitwarden::*;
|
pub use bitwarden::*;
|
||||||
pub use validator::*;
|
pub use validator::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, ValueEnum)]
|
||||||
|
pub enum Format {
|
||||||
|
Lprs,
|
||||||
|
BitWarden,
|
||||||
|
}
|
||||||
|
|
||||||
/// The password struct
|
/// The password struct
|
||||||
#[serde_with_macros::skip_serializing_none]
|
#[serde_with_macros::skip_serializing_none]
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, Parser)]
|
#[derive(Clone, Debug, Deserialize, Serialize, Parser)]
|
||||||
|
@ -145,3 +151,12 @@ impl Passwords {
|
||||||
self.passwords.push(password)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue