redesign storage add Commands

- replace storage add args with subcommands of physical, directory,
  online
- to make argument dependencies clearer
This commit is contained in:
qwjyh 2024-03-12 16:18:24 +09:00
parent e3675632c1
commit 7c8ee7a500
8 changed files with 169 additions and 142 deletions

View file

@ -1,14 +1,19 @@
# TODO: # TODO:
- [x] split subcommands to functions - [x] split subcommands to functions
- [x] write test for init subcommand - [x] write test for init subcommand
- [ ] write test with existing repo - [x] write test with existing repo
- [x] with ssh credential - [x] with ssh credential
- [x] ssh-agent - [x] ssh-agent
- [x] specify key - [x] specify key
- [ ] write test for storage subcommand
- [ ] storage add online
- [ ] storage add directory
- [ ] storage list
- [ ] add storage remove command - [ ] add storage remove command
- [ ] add sync subcommand - [ ] add sync subcommand
- [ ] add check subcommand - [ ] add check subcommand
- [ ] reorganize cmd option for storage - [x] reorganize cmd option for storage
- [ ] use subcommand - [x] use subcommand
- [ ] no commit option
<!-- vim: set sw=2 ts=2: --> <!-- vim: set sw=2 ts=2: -->

View file

@ -3,6 +3,7 @@
use crate::StorageType; use crate::StorageType;
use crate::path; use crate::path;
use crate::PathBuf; use crate::PathBuf;
use clap::Args;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity; use clap_verbosity_flag::Verbosity;
@ -50,7 +51,7 @@ pub(crate) enum Commands {
Check {}, Check {},
} }
#[derive(clap::Args, Debug)] #[derive(Args, Debug)]
#[command(args_conflicts_with_subcommands = true)] #[command(args_conflicts_with_subcommands = true)]
pub(crate) struct StorageArgs { pub(crate) struct StorageArgs {
#[command(subcommand)] #[command(subcommand)]
@ -60,14 +61,7 @@ pub(crate) struct StorageArgs {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub(crate) enum StorageCommands { pub(crate) enum StorageCommands {
/// Add new storage. /// Add new storage.
Add { Add(StorageAddArgs),
#[arg(value_enum)]
storage_type: StorageType,
// TODO: set this require and select matching disk for physical
#[arg(short, long, value_name = "PATH")]
path: Option<PathBuf>,
},
/// List all storages. /// List all storages.
List { List {
/// Show note on the storages. /// Show note on the storages.
@ -87,3 +81,50 @@ pub(crate) enum StorageCommands {
path: path::PathBuf, path: path::PathBuf,
}, },
} }
#[derive(Args, Debug)]
pub(crate) struct StorageAddArgs {
#[command(subcommand)]
pub(crate) command: StorageAddCommands,
}
#[derive(Subcommand, Debug)]
pub(crate) enum StorageAddCommands {
/// Physical drive partition.
Physical {
/// Unique name for the storage.
name: String,
/// Path where the storage is mounted on this device.
/// leave blank to fetch system info automatically.
path: Option<PathBuf>,
},
/// Sub directory of other storages.
Directory {
/// Unique name for the storage.
name: String,
/// Path where the storage is mounted on this device.
path: PathBuf,
/// Additional info. Empty by default.
#[arg(short, long, default_value = "")]
notes: String,
/// Device specific alias for the storage.
#[arg(short, long)]
alias: String,
},
/// Online storage.
Online {
/// Unique name for the storage.
name: String,
/// Path where the storage is mounted on this device.
path: PathBuf,
/// Provider name (for the common information).
#[arg(short, long)]
provider: String,
/// Capacity in bytes.
#[arg(short, long)]
capacity: u64,
/// Device specific alias for the storage.
#[arg(short, long)]
alias: String,
}
}

View file

@ -76,6 +76,10 @@ pub(crate) fn cmd_init(
ssh_key: Option<PathBuf>, ssh_key: Option<PathBuf>,
config_dir: &path::PathBuf, config_dir: &path::PathBuf,
) -> Result<()> { ) -> Result<()> {
if config_dir.join(DEVICESFILE).exists() {
debug!("{} already exists.", DEVICESFILE);
return Err(anyhow!("This device is already added."));
}
// validate device name // validate device name
if device_name.chars().count() == 0 { if device_name.chars().count() == 0 {
log::error!("Device name cannnot by empty"); log::error!("Device name cannnot by empty");

View file

@ -16,149 +16,84 @@ use unicode_width::{self, UnicodeWidthStr};
use crate::{ use crate::{
add_and_commit, add_and_commit,
cmd_args::Cli, cmd_args::{Cli, StorageAddCommands},
devices::{self, Device}, devices::{self, Device},
inquire_filepath_completer::FilePathCompleter, inquire_filepath_completer::FilePathCompleter,
storages::{ storages::{
self, directory, local_info, physical_drive_partition, Storage, StorageExt, StorageType, self, directory, local_info,
Storages, physical_drive_partition::{self, PhysicalDrivePartition},
Storage, StorageExt, StorageType, Storages,
}, },
}; };
pub(crate) fn cmd_storage_add( pub(crate) fn cmd_storage_add(
storage_type: storages::StorageType, args: StorageAddCommands,
path: Option<PathBuf>,
repo: Repository, repo: Repository,
config_dir: &PathBuf, config_dir: &PathBuf,
) -> Result<()> { ) -> Result<()> {
trace!("Storage Add {:?}, {:?}", storage_type, path); trace!("Storage Add with args: {:?}", args);
// Get storages // Get storages
let mut storages = Storages::read(&config_dir)?; let mut storages = Storages::read(&config_dir)?;
trace!("found storages: {:?}", storages); trace!("found storages: {:?}", storages);
let device = devices::get_device(&config_dir)?; let device = devices::get_device(&config_dir)?;
let storage = match storage_type { let storage = match args {
StorageType::P => { StorageAddCommands::Physical { name, path } => {
let use_sysinfo = { if !is_unique_name(&name, &storages) {
let options = vec![ return Err(anyhow!(
"Fetch disk information automatically.", "The name {} is already used for another storage.",
"Type disk information manually.", name
]; ));
let ans = Select::new( }
"Do you fetch disk information automatically? (it may take a few minutes)", let use_sysinfo = path.is_none();
options,
)
.prompt()
.context("Failed to get response. Please try again.")?;
match ans {
"Fetch disk information automatically." => true,
_ => false,
}
};
let storage = if use_sysinfo { let storage = if use_sysinfo {
// select storage physical_drive_partition::select_physical_storage(name, device)?
physical_drive_partition::select_physical_storage(device, &storages)?
} else { } else {
let mut name = String::new(); manually_construct_physical_drive_partition(name, path.unwrap(), &device)?
loop {
name = Text::new("Name for the storage:")
.with_validator(min_length!(0, "At least 1 character"))
.prompt()
.context("Failed to get Name")?;
if storages.list.iter().all(|(k, _v)| k != &name) {
break;
}
println!("The name {} is already used.", name);
}
let kind = Text::new("Kind of storage (ex. SSD):")
.prompt()
.context("Failed to get kind.")?;
let capacity: u64 = CustomType::<u64>::new("Capacity (byte):")
.with_error_message("Please type number.")
.prompt()
.context("Failed to get capacity.")?;
let fs = Text::new("filesystem:")
.prompt()
.context("Failed to get fs.")?;
let is_removable = Confirm::new("Is removable")
.prompt()
.context("Failed to get is_removable")?;
let mount_path: PathBuf = PathBuf::from(
Text::new("mount path:")
.with_autocomplete(FilePathCompleter::default())
.prompt()?,
);
let local_info = local_info::LocalInfo::new("".to_string(), mount_path);
physical_drive_partition::PhysicalDrivePartition::new(
name,
kind,
capacity,
fs,
is_removable,
local_info,
&device,
)
}; };
println!("storage: {}: {:?}", storage.name(), storage); println!("storage: {}: {:?}", storage.name(), storage);
Storage::PhysicalStorage(storage) Storage::PhysicalStorage(storage)
} }
StorageType::S => { StorageAddCommands::Directory {
name,
path,
notes,
alias,
} => {
if !is_unique_name(&name, &storages) {
return Err(anyhow!(
"The name {} is already used for another storage.",
name
));
}
if storages.list.is_empty() { if storages.list.is_empty() {
return Err(anyhow!( return Err(anyhow!(
"No storages found. Please add at least 1 physical/online storage first to add sub directory." "No storages found. Please add at least 1 physical/online storage first to add sub directory."
)); ));
} }
let path = path.unwrap_or_else(|| {
let mut cmd = Cli::command();
// TODO: weired def of cmd argument
cmd.error(
ErrorKind::MissingRequiredArgument,
"<PATH> is required with sub-directory",
)
.exit();
});
trace!("SubDirectory arguments: path: {:?}", path); trace!("SubDirectory arguments: path: {:?}", path);
// Nightly feature std::path::absolute // Nightly feature std::path::absolute
let path = path.canonicalize()?; let path = path.canonicalize()?;
trace!("canonicalized: path: {:?}", path); trace!("canonicalized: path: {:?}", path);
let key_name = ask_unique_name(&storages, "sub-directory".to_string())?;
let notes = Text::new("Notes for this sub-directory:").prompt()?;
let storage = directory::Directory::try_from_device_path( let storage = directory::Directory::try_from_device_path(
key_name, path, notes, &device, &storages, name, path, notes, alias, &device, &storages,
)?; )?;
Storage::SubDirectory(storage) Storage::SubDirectory(storage)
} }
StorageType::O => { StorageAddCommands::Online {
let path = path.unwrap_or_else(|| { name,
let mut cmd = Cli::command(); path,
cmd.error( provider,
ErrorKind::MissingRequiredArgument, capacity,
"<PATH> is required with sub-directory", alias,
) } => {
.exit(); if !is_unique_name(&name, &storages) {
}); return Err(anyhow!(
let mut name = String::new(); "The name {} is already used for another storage.",
loop { name
name = Text::new("Name for the storage:") ));
.with_validator(min_length!(0, "At least 1 character"))
.prompt()
.context("Failed to get Name")?;
if storages.list.iter().all(|(k, _v)| k != &name) {
break;
}
println!("The name {} is already used.", name);
} }
let provider = Text::new("Provider:")
.prompt()
.context("Failed to get provider")?;
let capacity: u64 = CustomType::<u64>::new("Capacity (byte):")
.with_error_message("Please type number.")
.prompt()
.context("Failed to get capacity.")?;
let alias = Text::new("Alias:")
.prompt()
.context("Failed to get provider")?;
let storage = storages::online_storage::OnlineStorage::new( let storage = storages::online_storage::OnlineStorage::new(
name, provider, capacity, alias, path, &device, name, provider, capacity, alias, path, &device,
); );
@ -168,6 +103,7 @@ pub(crate) fn cmd_storage_add(
// add to storages // add to storages
let new_storage_name = storage.name().clone(); let new_storage_name = storage.name().clone();
let new_storage_type = storage.typename().to_string();
storages.add(storage)?; storages.add(storage)?;
trace!("updated storages: {:?}", storages); trace!("updated storages: {:?}", storages);
@ -178,7 +114,10 @@ pub(crate) fn cmd_storage_add(
add_and_commit( add_and_commit(
&repo, &repo,
&Path::new(storages::STORAGESFILE), &Path::new(storages::STORAGESFILE),
&format!("Add new storage(physical drive): {}", new_storage_name), &format!(
"Add new storage({}): {}",
new_storage_type, new_storage_name
),
)?; )?;
println!("Added new storage."); println!("Added new storage.");
@ -186,10 +125,51 @@ pub(crate) fn cmd_storage_add(
Ok(()) Ok(())
} }
fn is_unique_name(newname: &String, storages: &Storages) -> bool {
storages.list.iter().all(|(name, _)| name != newname)
}
fn manually_construct_physical_drive_partition(
name: String,
path: PathBuf,
device: &Device,
) -> Result<PhysicalDrivePartition> {
let kind = Text::new("Kind of storage (ex. SSD):")
.prompt()
.context("Failed to get kind.")?;
let capacity: u64 = CustomType::<u64>::new("Capacity (byte):")
.with_error_message("Please type number.")
.prompt()
.context("Failed to get capacity.")?;
let fs = Text::new("filesystem:")
.prompt()
.context("Failed to get fs.")?;
let is_removable = Confirm::new("Is removable")
.prompt()
.context("Failed to get is_removable")?;
let alias = Text::new("Alias of the storage for this device")
.prompt()
.context("Failed to get alias.")?;
let local_info = local_info::LocalInfo::new(alias, path);
Ok(physical_drive_partition::PhysicalDrivePartition::new(
name,
kind,
capacity,
fs,
is_removable,
local_info,
&device,
))
}
pub(crate) fn cmd_storage_list(config_dir: &PathBuf, with_note: bool) -> Result<()> { pub(crate) fn cmd_storage_list(config_dir: &PathBuf, with_note: bool) -> Result<()> {
// Get storages // Get storages
let storages = Storages::read(&config_dir)?; let storages = Storages::read(&config_dir)?;
trace!("found storages: {:?}", storages); trace!("found storages: {:?}", storages);
if storages.list.is_empty() {
println!("No storages found");
return Ok(());
}
let device = devices::get_device(&config_dir)?; let device = devices::get_device(&config_dir)?;
let mut stdout = io::BufWriter::new(io::stdout()); let mut stdout = io::BufWriter::new(io::stdout());
write_storages_list(&mut stdout, &storages, &device, with_note)?; write_storages_list(&mut stdout, &storages, &device, with_note)?;

View file

@ -71,9 +71,9 @@ fn main() -> Result<()> {
)?; )?;
trace!("repo state: {:?}", repo.state()); trace!("repo state: {:?}", repo.state());
match storage.command { match storage.command {
StorageCommands::Add { storage_type, path } => { StorageCommands::Add(storageargs) => {
cmd_storage::cmd_storage_add(storage_type, path, repo, &config_dir)? cmd_storage::cmd_storage_add(storageargs.command, repo, &config_dir)?
} },
StorageCommands::List { long } => cmd_storage::cmd_storage_list(&config_dir, long)?, StorageCommands::List { long } => cmd_storage::cmd_storage_list(&config_dir, long)?,
StorageCommands::Bind { StorageCommands::Bind {
storage: storage_name, storage: storage_name,

View file

@ -15,10 +15,14 @@ use super::{local_info::LocalInfo, Storage, StorageExt, Storages};
/// Subdirectory of other [Storage]s. /// Subdirectory of other [Storage]s.
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Directory { pub struct Directory {
/// ID.
name: String, name: String,
/// ID of parent storage.
parent: String, parent: String,
/// Relative path to the parent storage.
relative_path: path::PathBuf, relative_path: path::PathBuf,
pub notes: String, pub notes: String,
/// Device and localinfo pairs.
local_infos: HashMap<String, LocalInfo>, local_infos: HashMap<String, LocalInfo>,
} }
@ -47,6 +51,7 @@ impl Directory {
name: String, name: String,
path: path::PathBuf, path: path::PathBuf,
notes: String, notes: String,
alias: String,
device: &devices::Device, device: &devices::Device,
storages: &Storages, storages: &Storages,
) -> Result<Directory> { ) -> Result<Directory> {
@ -69,7 +74,7 @@ impl Directory {
}) })
.context(format!("Failed to compare diff of paths"))?; .context(format!("Failed to compare diff of paths"))?;
trace!("Selected parent: {}", parent_name); trace!("Selected parent: {}", parent_name);
let local_info = LocalInfo::new("".to_string(), path); let local_info = LocalInfo::new(alias, path);
Ok(Directory::new( Ok(Directory::new(
name, name,
parent_name.clone(), parent_name.clone(),

View file

@ -16,13 +16,19 @@ use super::{
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct OnlineStorage { pub struct OnlineStorage {
/// ID.
name: String, name: String,
/// Provider string (for the common information).
pub provider: String, pub provider: String,
/// Capacity in bytes.
capacity: u64, capacity: u64,
/// Device and local info pairs.
local_infos: HashMap<String, LocalInfo>, local_infos: HashMap<String, LocalInfo>,
} }
impl OnlineStorage { impl OnlineStorage {
/// # Arguments
/// - alias: for [`LocalInfo`]
pub fn new( pub fn new(
name: String, name: String,
provider: String, provider: String,

View file

@ -7,11 +7,8 @@ use anyhow::{anyhow, Context, Result};
use byte_unit::Byte; use byte_unit::Byte;
use inquire::Text; use inquire::Text;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::{self, PathBuf}; use std::path;
use std::{ use std::{collections::HashMap, fmt};
collections::{hash_map::RandomState, HashMap},
fmt,
};
use sysinfo::{Disk, DiskExt, SystemExt}; use sysinfo::{Disk, DiskExt, SystemExt};
use super::local_info::{self, LocalInfo}; use super::local_info::{self, LocalInfo};
@ -191,8 +188,8 @@ impl fmt::Display for PhysicalDrivePartition {
/// Interactively select physical storage from available disks in sysinfo. /// Interactively select physical storage from available disks in sysinfo.
pub fn select_physical_storage( pub fn select_physical_storage(
disk_name: String,
device: Device, device: Device,
storages: &Storages,
) -> Result<PhysicalDrivePartition> { ) -> Result<PhysicalDrivePartition> {
trace!("select_physical_storage"); trace!("select_physical_storage");
// get disk info fron sysinfo // get disk info fron sysinfo
@ -206,22 +203,11 @@ pub fn select_physical_storage(
trace!("{:?}", disk) trace!("{:?}", disk)
} }
let disk = select_sysinfo_disk(&sys_disks)?; let disk = select_sysinfo_disk(&sys_disks)?;
// name the disk
let mut disk_name = String::new();
trace!("{}", disk_name);
loop {
disk_name = Text::new("Name for the disk:").prompt()?;
if storages.list.iter().all(|(k, v)| k != &disk_name) {
break;
}
println!("The name {} is already used.", disk_name);
}
trace!("selected name: {}", disk_name);
let storage = PhysicalDrivePartition::try_from_sysinfo_disk(&disk, disk_name, device)?; let storage = PhysicalDrivePartition::try_from_sysinfo_disk(&disk, disk_name, device)?;
Ok(storage) Ok(storage)
} }
pub fn select_sysinfo_disk(sysinfo: &sysinfo::System) -> Result<&Disk> { fn select_sysinfo_disk(sysinfo: &sysinfo::System) -> Result<&Disk> {
let available_disks = sysinfo let available_disks = sysinfo
.disks() .disks()
.iter() .iter()