change init command & add integration test

- now need to specify device name via cmd arg
- can use private repository with ssh key or ssh-agent
- adding integration test utility crates
This commit is contained in:
qwjyh 2024-03-07 14:59:58 +09:00
parent 9935f79920
commit 4283e1e98a
8 changed files with 387 additions and 59 deletions

View file

@ -6,7 +6,7 @@ use crate::PathBuf;
use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
#[derive(Parser)]
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub(crate) struct Cli {
#[command(subcommand)]
@ -20,13 +20,21 @@ pub(crate) struct Cli {
pub(crate) verbose: Verbosity,
}
#[derive(Subcommand)]
#[derive(Subcommand, Debug)]
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 {
/// Name for this device
device_name: String,
/// Url for existing repository. Empty if init for the first time.
#[arg(short, long)]
repo_url: Option<String>, // url?
/// Whether to use ssh-agent
#[arg(long)]
use_sshagent: bool,
/// Manually specify ssh key
#[arg(long)]
ssh_key: Option<PathBuf>,
},
/// Manage storages.
@ -42,14 +50,14 @@ pub(crate) enum Commands {
Check {},
}
#[derive(clap::Args)]
#[derive(clap::Args, Debug)]
#[command(args_conflicts_with_subcommands = true)]
pub(crate) struct StorageArgs {
#[command(subcommand)]
pub(crate) command: StorageCommands,
}
#[derive(Subcommand)]
#[derive(Subcommand, Debug)]
pub(crate) enum StorageCommands {
/// Add new storage.
Add {

View file

@ -1,24 +1,91 @@
//! Init subcommand.
//! Initialize xdbm for the device.
use crate::{
add_and_commit, full_status, get_devices, set_device_name, write_devices, Device, DEVICESFILE,
};
use anyhow::{anyhow, Context, Result};
use git2::Repository;
use crate::{add_and_commit, full_status, get_devices, write_devices, Device, DEVICESFILE};
use anyhow::{anyhow, Context, Ok, Result};
use core::panic;
use git2::{Cred, RemoteCallbacks, Repository};
use inquire::Password;
use std::env;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{self, Path};
use std::path::{self, Path, PathBuf};
pub(crate) fn cmd_init(repo_url: Option<String>, config_dir: &path::PathBuf) -> Result<()> {
let is_first_device: bool;
fn clone_repo(
repo_url: &String,
use_sshagent: bool,
ssh_key: Option<PathBuf>,
config_dir: &path::PathBuf,
) -> Result<Repository> {
// dont use credentials
if ssh_key.is_none() && !use_sshagent {
info!("No authentication will be used.");
info!("Use either ssh_key or ssh-agent to access private repository");
return Ok(Repository::clone(&repo_url, &config_dir)?);
}
// using credentials
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, _allowed_types| {
if let Some(key) = &ssh_key {
info!("Using provided ssh key to access the repository");
let passwd = match Password::new("SSH passphrase").prompt() {
std::result::Result::Ok(s) => Some(s),
Err(err) => {
error!("Failed to get ssh passphrase: {:?}", err);
None
}
};
Cred::ssh_key(
username_from_url
.context("No username found from the url")
.unwrap(),
None,
&key as &Path,
passwd.as_deref(),
)
} else if use_sshagent {
// use ssh agent
info!("Using ssh agent to access the repository");
Cred::ssh_key_from_agent(
username_from_url
.context("No username found from the url")
.unwrap(),
)
} else {
error!("no ssh_key and use_sshagent");
panic!("This option must be unreachable.")
}
});
// fetch options
let mut fo = git2::FetchOptions::new();
fo.remote_callbacks(callbacks);
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fo);
Ok(builder.clone(&repo_url, config_dir)?)
}
pub(crate) fn cmd_init(
device_name: String,
repo_url: Option<String>,
use_sshagent: bool,
ssh_key: Option<PathBuf>,
config_dir: &path::PathBuf,
) -> Result<()> {
// validate device name
if device_name.chars().count() == 0 {
log::error!("Device name cannnot by empty");
return Err(anyhow!("Device name is empty"));
}
// get repo or initialize it
let repo = match repo_url {
let (is_first_device, repo) = match repo_url {
Some(repo_url) => {
trace!("repo: {}", repo_url);
let repo = Repository::clone(&repo_url, &config_dir)?;
is_first_device = false;
repo
let repo = clone_repo(&repo_url, use_sshagent, ssh_key, config_dir)?;
(false, repo)
}
None => {
trace!("No repo provided");
@ -37,14 +104,15 @@ pub(crate) fn cmd_init(repo_url: Option<String>, config_dir: &path::PathBuf) ->
add_and_commit(&repo, Path::new(".gitignore"), "Add devname to gitignore.")?;
full_status(&repo)?;
}
is_first_device = true;
repo
(true, repo)
}
};
full_status(&repo)?;
// set device name
let device = set_device_name()?;
// let device = set_device_name()?;
let device = Device::new(device_name);
trace!("Device information: {:?}", device);
// save devname
let devname_path = &config_dir.join("devname");

View file

@ -87,7 +87,8 @@ pub fn get_device(config_dir: &Path) -> Result<Device> {
/// Get `Vec<Device>` from yaml file in `config_dir`.
pub fn get_devices(config_dir: &Path) -> Result<Vec<Device>> {
trace!("get_devices");
let f = File::open(config_dir.join(DEVICESFILE))?;
let f =
File::open(config_dir.join(DEVICESFILE)).context(format!("{} not found", DEVICESFILE))?;
let reader = BufReader::new(f);
let yaml: Vec<Device> =
serde_yaml::from_reader(reader).context("Failed to parse devices.yml")?;

View file

@ -45,11 +45,13 @@ fn main() -> Result<()> {
.filter_level(cli.verbose.log_level_filter())
.init();
trace!("Start logging...");
trace!("args: {:?}", cli);
let config_dir: std::path::PathBuf = match cli.config_dir {
Some(path) => path,
None => {
let mut config_dir = dirs::config_local_dir().context("Failed to get default config dir.")?;
let mut config_dir =
dirs::config_local_dir().context("Failed to get default config dir.")?;
config_dir.push("xdbm");
config_dir
}
@ -57,7 +59,12 @@ fn main() -> Result<()> {
trace!("Config dir: {:?}", config_dir);
match cli.command {
Commands::Init { repo_url } => cmd_init::cmd_init(repo_url, &config_dir)?,
Commands::Init {
device_name,
repo_url,
use_sshagent,
ssh_key,
} => cmd_init::cmd_init(device_name, repo_url, use_sshagent, ssh_key, &config_dir)?,
Commands::Storage(storage) => {
let repo = Repository::open(&config_dir).context(
"Repository doesn't exist on the config path. Please run init to initialize the repository.",
@ -98,38 +105,6 @@ fn main() -> Result<()> {
Ok(())
}
/// Set device name interactively.
fn set_device_name() -> Result<Device> {
let validator = |input: &str| {
if input.chars().count() == 0 {
Ok(Validation::Invalid("Need at least 1 character.".into()))
} else {
Ok(Validation::Valid)
}
};
let device_name = Text::new("Provide name for this device:")
.with_validator(validator)
.prompt();
let device_name = match device_name {
Ok(device_name) => {
println!("device name: {}", device_name);
device_name
}
Err(err) => {
println!("Error {}", err);
return Err(anyhow!(err));
}
};
let device = Device::new(device_name);
trace!("Device information: {:?}", device);
trace!("Serialized: \n{}", serde_yaml::to_string(&device).unwrap());
return Ok(device);
}
fn ask_unique_name(storages: &HashMap<String, Storage>, target: String) -> Result<String> {
let mut disk_name = String::new();
loop {