refactor: separate subcommands

This commit is contained in:
qwjyh 2024-02-27 13:02:09 +09:00
parent bf1a5e23fe
commit 4502113e41
4 changed files with 296 additions and 273 deletions

80
src/cmd_init.rs Normal file
View file

@ -0,0 +1,80 @@
use std::{path::{self, Path}, fs::File, io::{BufWriter, Write}};
use git2::Repository;
use anyhow::{Result, Context, anyhow};
use crate::{add_and_commit, full_status, set_device_name, devices::{Device, get_devices, write_devices, DEVICESFILE}};
pub fn cmd_init(repo_url: Option<String>, config_dir: &path::PathBuf) -> Result<()> {
let is_first_device: bool;
// get repo or initialize it
let repo = match repo_url {
Some(repo_url) => {
trace!("repo: {}", repo_url);
let repo = Repository::clone(&repo_url, &config_dir)?;
is_first_device = false;
repo
}
None => {
trace!("No repo provided");
println!("Initializing for the first device...");
// create repository
let repo = Repository::init(&config_dir)?;
// set up gitignore
{
let f = File::create(&config_dir.join(".gitignore"))?;
{
let mut buf = BufWriter::new(f);
buf.write("devname".as_bytes())?;
}
add_and_commit(&repo, Path::new(".gitignore"), "Add devname to gitignore.")?;
full_status(&repo)?;
}
is_first_device = true;
repo
}
};
full_status(&repo)?;
// set device name
let device = set_device_name()?;
// save devname
let devname_path = &config_dir.join("devname");
{
let f = File::create(devname_path)
.context("Failed to create a file to store local device name")?;
let writer = BufWriter::new(f);
serde_yaml::to_writer(writer, &device.name()).unwrap();
};
full_status(&repo)?;
// Add new device to devices.yml
{
let mut devices: Vec<Device> = if is_first_device {
vec![]
} else {
get_devices(&config_dir)?
};
trace!("devices: {:?}", devices);
if devices.iter().any(|x| x.name() == device.name()) {
return Err(anyhow!("device name is already used."));
}
devices.push(device.clone());
trace!("Devices: {:?}", devices);
write_devices(&config_dir, devices)?;
}
full_status(&repo)?;
// commit
add_and_commit(
&repo,
&Path::new(DEVICESFILE),
&format!("Add new devname: {}", &device.name()),
)?;
println!("Device added");
full_status(&repo)?;
Ok(())
}

68
src/cmd_options.rs Normal file
View file

@ -0,0 +1,68 @@
/// CLI arguments definition
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
use crate::storages::StorageType;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub(crate) struct Cli {
#[command(subcommand)]
pub command: Commands,
#[command(flatten)]
pub verbose: Verbosity,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Initialize for this device.
/// Provide `repo_url` to use existing repository, otherwise this device will be configured as the
/// first device.
Init {
repo_url: Option<String>, // url?
},
/// Manage storages.
Storage(StorageArgs),
/// Print config dir.
Path {},
/// Sync with git repo.
Sync {},
}
#[derive(clap::Args)]
#[command(args_conflicts_with_subcommands = true)]
pub(crate) struct StorageArgs {
#[command(subcommand)]
pub(crate) command: StorageCommands,
}
#[derive(Subcommand)]
pub(crate) enum StorageCommands {
/// Add new storage.
Add {
#[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 {},
/// Add new device-specific name to existing storage.
/// For physical disk, the name is taken from system info automatically.
Bind { storage: String },
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert()
}

136
src/cmd_storage.rs Normal file
View file

@ -0,0 +1,136 @@
use std::{path::{self, Path}, collections::HashMap, io::ErrorKind};
use anyhow::{Result, Context, Ok};
use clap::{CommandFactory, error};
use git2::Repository;
use inquire::Text;
use sysinfo::{SystemExt, DiskExt};
use crate::{anyhow, StorageCommands, cmd_options::{StorageArgs, Cli}, storages::{Storage, get_storages, StorageType, physical_drive_partition::{select_physical_storage, select_sysinfo_disk}, self, write_storages, STORAGESFILE, StorageExt}, devices::get_device, ask_unique_name, add_and_commit};
pub fn cmd_storage(storage: StorageArgs, config_dir: &path::PathBuf) -> Result<()> {
let repo = Repository::open(&config_dir)
.context("Repository doesn't exist. Please run init to initialize the repository.")?;
trace!("repo state: {:?}", repo.state());
match storage.command {
StorageCommands::Add { storage_type, path } => {
trace!("Storage Add {:?}, {:?}", storage_type, path);
// Get storages
// let mut storages: Vec<Storage> = get_storages(&config_dir)?;
let mut storages: HashMap<String, Storage> = get_storages(&config_dir)?;
trace!("found storages: {:?}", storages);
let device = get_device(&config_dir)?;
let (key, storage) = match storage_type {
StorageType::Physical => {
// select storage
let (key, storage) = select_physical_storage(device, &storages)?;
println!("storage: {}: {:?}", key, storage);
(key, Storage::PhysicalStorage(storage))
}
StorageType::SubDirectory => {
if storages.is_empty() {
return Err(anyhow!(
"No storages found. Please add at least 1 physical storage first."
));
}
let path = path.unwrap_or_else(|| {
let mut cmd = Cli::command();
cmd.error(
error::ErrorKind::MissingRequiredArgument,
"<PATH> is required with sub-directory",
)
.exit();
});
trace!("SubDirectory arguments: path: {:?}", path);
// Nightly feature std::path::absolute
let path = path.canonicalize()?;
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 = storages::directory::Directory::try_from_device_path(
key_name.clone(),
path,
notes,
&device,
&storages,
)?;
(key_name, Storage::SubDirectory(storage))
}
StorageType::Online => todo!(),
};
// add to storages
storages.insert(key.clone(), storage);
trace!("updated storages: {:?}", storages);
// write to file
write_storages(&config_dir, storages)?;
// commit
add_and_commit(
&repo,
&Path::new(STORAGESFILE),
&format!("Add new storage(physical drive): {}", key),
)?;
println!("Added new storage.");
trace!("Finished adding storage");
Ok(())
}
StorageCommands::List {} => {
// Get storages
let storages: HashMap<String, Storage> = get_storages(&config_dir)?;
trace!("found storages: {:?}", storages);
let device = get_device(&config_dir)?;
for (k, storage) in &storages {
println!("{}: {}", k, storage);
println!(" {}", storage.mount_path(&device, &storages)?.display());
}
Ok(())
}
StorageCommands::Bind {
storage: storage_name,
} => {
// get storages
let mut storages: HashMap<String, Storage> = get_storages(&config_dir)?;
let commit_comment = {
// find matching storage
let storage = storages
.get_mut(&storage_name)
.context(format!("No storage has name {}", storage_name))?;
// get disk from sysinfo
let mut sysinfo = sysinfo::System::new_all();
sysinfo.refresh_disks();
let disk = select_sysinfo_disk(&sysinfo)?;
let system_name = disk
.name()
.to_str()
.context("Failed to convert disk name to valid string")?;
// add to storages
storage.bind_device(disk, &config_dir)?;
trace!("storage: {}", storage);
format!("{} to {}", system_name, storage.name())
};
trace!("bound new system name to the storage");
trace!("storages: {:#?}", storages);
write_storages(&config_dir, storages)?;
// commit
add_and_commit(
&repo,
&Path::new(STORAGESFILE),
&format!(
"Bound new storage name to physical drive ({})",
commit_comment
),
)?;
println!(
"Bound new storage name to physical drive ({})",
commit_comment
);
Ok(())
}
}
}

View file

@ -12,83 +12,24 @@ extern crate log;
extern crate dirs;
use anyhow::{anyhow, Context, Result};
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
use clap::{CommandFactory, Parser};
use git2::{Commit, Oid, Repository};
use inquire::{validator::Validation, Text};
use serde_yaml;
use std::collections::HashMap;
use std::path::PathBuf;
use std::{env, io::BufReader, path::Path};
use std::{
ffi::OsString,
io::{self, BufWriter},
};
use std::{fmt::Debug, fs::File};
use std::{fs, io::prelude::*};
use sysinfo::{Disk, DiskExt, SystemExt};
use std::path::Path;
use crate::cmd_options::{Cli, Commands, StorageCommands};
use crate::devices::get_device;
use crate::storages::{
directory::Directory, get_storages, local_info, online_storage, physical_drive_partition::*,
write_storages, Storage, StorageExt, StorageType, STORAGESFILE,
get_storages, physical_drive_partition::*, write_storages, Storage, StorageExt, StorageType,
STORAGESFILE,
};
use devices::{Device, DEVICESFILE, *};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[command(flatten)]
verbose: Verbosity,
}
#[derive(Subcommand)]
enum Commands {
/// Initialize for this device.
/// Provide `repo_url` to use existing repository, otherwise this device will be configured as the
/// first device.
Init {
repo_url: Option<String>, // url?
},
/// Manage storages.
Storage(StorageArgs),
/// Print config dir.
Path {},
/// Sync with git repo.
Sync {},
}
#[derive(clap::Args)]
#[command(args_conflicts_with_subcommands = true)]
struct StorageArgs {
#[command(subcommand)]
command: StorageCommands,
}
#[derive(Subcommand)]
enum StorageCommands {
/// Add new storage.
Add {
#[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 {},
/// Add new device-specific name to existing storage.
/// For physical disk, the name is taken from system info automatically.
Bind { storage: String },
}
use devices::Device;
mod cmd_init;
mod cmd_options;
mod cmd_storage;
mod devices;
mod storages;
@ -108,204 +49,8 @@ fn main() -> Result<()> {
trace!("Config dir: {:?}", config_dir);
match cli.command {
Commands::Init { repo_url } => {
let is_first_device: bool;
// get repo or initialize it
let repo = match repo_url {
Some(repo_url) => {
trace!("repo: {}", repo_url);
let repo = Repository::clone(&repo_url, &config_dir)?;
is_first_device = false;
repo
}
None => {
trace!("No repo provided");
println!("Initializing for the first device...");
// create repository
let repo = Repository::init(&config_dir)?;
// set up gitignore
{
let f = File::create(&config_dir.join(".gitignore"))?;
{
let mut buf = BufWriter::new(f);
buf.write("devname".as_bytes())?;
}
add_and_commit(
&repo,
Path::new(".gitignore"),
"Add devname to gitignore.",
)?;
full_status(&repo)?;
}
is_first_device = true;
repo
}
};
full_status(&repo)?;
// set device name
let device = set_device_name()?;
// save devname
let devname_path = &config_dir.join("devname");
{
let f = File::create(devname_path)
.context("Failed to create a file to store local device name")?;
let writer = BufWriter::new(f);
serde_yaml::to_writer(writer, &device.name()).unwrap();
};
full_status(&repo)?;
// Add new device to devices.yml
{
let mut devices: Vec<Device> = if is_first_device {
vec![]
} else {
get_devices(&config_dir)?
};
trace!("devices: {:?}", devices);
if devices.iter().any(|x| x.name() == device.name()) {
return Err(anyhow!("device name is already used."));
}
devices.push(device.clone());
trace!("Devices: {:?}", devices);
write_devices(&config_dir, devices)?;
}
full_status(&repo)?;
// commit
add_and_commit(
&repo,
&Path::new(DEVICESFILE),
&format!("Add new devname: {}", &device.name()),
)?;
println!("Device added");
full_status(&repo)?;
}
Commands::Storage(storage) => {
let repo = Repository::open(&config_dir).context(
"Repository doesn't exist. Please run init to initialize the repository.",
)?;
trace!("repo state: {:?}", repo.state());
match storage.command {
StorageCommands::Add { storage_type, path } => {
trace!("Storage Add {:?}, {:?}", storage_type, path);
// Get storages
// let mut storages: Vec<Storage> = get_storages(&config_dir)?;
let mut storages: HashMap<String, Storage> = get_storages(&config_dir)?;
trace!("found storages: {:?}", storages);
let device = get_device(&config_dir)?;
let (key, storage) = match storage_type {
StorageType::Physical => {
// select storage
let (key, storage) = select_physical_storage(device, &storages)?;
println!("storage: {}: {:?}", key, storage);
(key, Storage::PhysicalStorage(storage))
}
StorageType::SubDirectory => {
if storages.is_empty() {
return Err(anyhow!("No storages found. Please add at least 1 physical storage first."));
}
let path = path.unwrap_or_else(|| {
let mut cmd = Cli::command();
cmd.error(
ErrorKind::MissingRequiredArgument,
"<PATH> is required with sub-directory",
)
.exit();
});
trace!("SubDirectory arguments: path: {:?}", path);
// Nightly feature std::path::absolute
let path = path.canonicalize()?;
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 = storages::directory::Directory::try_from_device_path(
key_name.clone(),
path,
notes,
&device,
&storages,
)?;
(key_name, Storage::SubDirectory(storage))
}
StorageType::Online => todo!(),
};
// add to storages
storages.insert(key.clone(), storage);
trace!("updated storages: {:?}", storages);
// write to file
write_storages(&config_dir, storages)?;
// commit
add_and_commit(
&repo,
&Path::new(STORAGESFILE),
&format!("Add new storage(physical drive): {}", key),
)?;
println!("Added new storage.");
trace!("Finished adding storage");
}
StorageCommands::List {} => {
// Get storages
let storages: HashMap<String, Storage> = get_storages(&config_dir)?;
trace!("found storages: {:?}", storages);
let device = get_device(&config_dir)?;
for (k, storage) in &storages {
println!("{}: {}", k, storage);
println!(" {}", storage.mount_path(&device, &storages)?.display());
}
}
StorageCommands::Bind {
storage: storage_name,
} => {
// get storages
let mut storages: HashMap<String, Storage> = get_storages(&config_dir)?;
let commit_comment = {
// find matching storage
let storage = storages
.get_mut(&storage_name)
.context(format!("No storage has name {}", storage_name))?;
// get disk from sysinfo
let mut sysinfo = sysinfo::System::new_all();
sysinfo.refresh_disks();
let disk = select_sysinfo_disk(&sysinfo)?;
let system_name = disk
.name()
.to_str()
.context("Failed to convert disk name to valid string")?;
// add to storages
storage.bind_device(disk, &config_dir)?;
trace!("storage: {}", storage);
format!("{} to {}", system_name, storage.name())
};
trace!("bound new system name to the storage");
trace!("storages: {:#?}", storages);
write_storages(&config_dir, storages)?;
// commit
add_and_commit(
&repo,
&Path::new(STORAGESFILE),
&format!(
"Bound new storage name to physical drive ({})",
commit_comment
),
)?;
println!(
"Bound new storage name to physical drive ({})",
commit_comment
);
}
}
}
Commands::Init { repo_url } => cmd_init::cmd_init(repo_url, &config_dir)?,
Commands::Storage(storage) => cmd_storage::cmd_storage(storage, &config_dir)?,
Commands::Path {} => {
println!("{}", &config_dir.display());
}
@ -418,9 +163,3 @@ fn full_status(repo: &Repository) -> Result<()> {
}
Ok(())
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert()
}