use dialoguer::{Confirm, Input, theme::ColorfulTheme};
use crate::commands::keyring::store::{Store, keyring_path_override};
use crate::core::wizard::WizardInput;
pub(crate) fn load_keyring() -> Result<Store, String> {
let path = Store::resolve_path()?;
if let Some(explicit) = keyring_path_override()
&& !explicit.exists()
{
return Err(format!(
"--keyring-path {} does not exist",
explicit.display()
));
}
Store::load(&path)
}
pub(crate) async fn prepare_rclone(store: &Store) -> Result<(), String> {
crate::core::rclone::check_available().await.map_err(|_| {
"'rclone' was not found on PATH -- required for every bucket-addressed job".to_string()
})?;
let config_path = crate::core::rclone::config::rclone_config_path()?;
crate::core::rclone::config::write_sandboxed_config(store, &config_path)
}
pub(crate) struct ReportBucketInput<'a> {
pub flag: Option<String>,
pub store: &'a Store,
}
impl WizardInput for ReportBucketInput<'_> {
type Value = String;
fn flag_value(&self) -> Option<Result<String, String>> {
self.flag.clone().map(Ok)
}
fn prompt(&self) -> Result<String, String> {
self.store
.prompt_select_bucket()
.map(|bucket_config| bucket_config.alias.clone())
}
fn non_interactive_fallback(&self) -> Result<String, String> {
Err("--report-bucket is required when not running interactively".to_string())
}
}
pub(crate) struct UploadTargetInput<'a> {
pub flag: Option<String>,
pub store: &'a Store,
}
impl WizardInput for UploadTargetInput<'_> {
type Value = Option<String>;
fn flag_value(&self) -> Option<Result<Option<String>, String>> {
self.flag.clone().map(|alias| Ok(Some(alias)))
}
fn prompt(&self) -> Result<Option<String>, String> {
let upload = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Upload to a bucket-config?")
.default(false)
.interact()
.map_err(|err| format!("failed to read confirmation: {err}"))?;
if !upload {
return Ok(None);
}
match self.store.prompt_select_bucket() {
Ok(bucket_config) => Ok(Some(bucket_config.alias.clone())),
Err(message) => {
println!("{message}");
Ok(None)
}
}
}
fn non_interactive_fallback(&self) -> Result<Option<String>, String> {
Ok(None)
}
}
pub(crate) fn default_concurrency() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
}
const CONCURRENCY_CLAMP_MULTIPLIER: usize = 2;
pub(crate) struct CpuConcurrencyInput {
pub flag: Option<usize>,
}
impl WizardInput for CpuConcurrencyInput {
type Value = usize;
fn flag_value(&self) -> Option<Result<usize, String>> {
self.flag.map(|value| {
let value = value.max(1);
let ceiling = default_concurrency() * CONCURRENCY_CLAMP_MULTIPLIER;
if value > ceiling {
tracing::warn!(
requested = value,
clamped_to = ceiling,
"--concurrency exceeds this host's core-count-based safe ceiling; clamping"
);
Ok(ceiling)
} else {
Ok(value)
}
})
}
fn prompt(&self) -> Result<usize, String> {
let value = Input::<usize>::new()
.with_prompt("Concurrency")
.default(default_concurrency())
.interact_text()
.map_err(|err| format!("failed to read concurrency: {err}"))?;
Ok(value.max(1))
}
fn non_interactive_fallback(&self) -> Result<usize, String> {
Err("--concurrency is required when not running interactively".to_string())
}
}
const TRANSFERS_DEFAULT: usize = 8;
pub(crate) struct TransfersInput {
pub flag: Option<usize>,
}
impl WizardInput for TransfersInput {
type Value = usize;
fn flag_value(&self) -> Option<Result<usize, String>> {
self.flag.map(|value| Ok(value.max(1)))
}
fn prompt(&self) -> Result<usize, String> {
let value = Input::<usize>::new()
.with_prompt("rclone --transfers")
.default(TRANSFERS_DEFAULT)
.interact_text()
.map_err(|err| format!("failed to read transfers: {err}"))?;
Ok(value.max(1))
}
fn non_interactive_fallback(&self) -> Result<usize, String> {
Ok(TRANSFERS_DEFAULT)
}
}
const CHECKERS_DEFAULT: usize = 16;
pub(crate) struct CheckersInput {
pub flag: Option<usize>,
}
impl WizardInput for CheckersInput {
type Value = usize;
fn flag_value(&self) -> Option<Result<usize, String>> {
self.flag.map(|value| Ok(value.max(1)))
}
fn prompt(&self) -> Result<usize, String> {
let value = Input::<usize>::new()
.with_prompt("rclone --checkers")
.default(CHECKERS_DEFAULT)
.interact_text()
.map_err(|err| format!("failed to read checkers: {err}"))?;
Ok(value.max(1))
}
fn non_interactive_fallback(&self) -> Result<usize, String> {
Ok(CHECKERS_DEFAULT)
}
}
pub(crate) struct TpslimitInput {
pub flag: Option<usize>,
}
impl WizardInput for TpslimitInput {
type Value = Option<usize>;
fn flag_value(&self) -> Option<Result<Option<usize>, String>> {
self.flag.map(|value| Ok(Some(value)))
}
fn prompt(&self) -> Result<Option<usize>, String> {
let value = Input::<String>::new()
.with_prompt("rclone --tpslimit (blank = no cap)")
.allow_empty(true)
.interact_text()
.map_err(|err| format!("failed to read tpslimit: {err}"))?;
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
}
trimmed
.parse::<usize>()
.map(Some)
.map_err(|_| format!("'{trimmed}' is not a valid tpslimit"))
}
fn non_interactive_fallback(&self) -> Result<Option<usize>, String> {
Ok(None)
}
}
pub(crate) struct ConfirmInput {
pub non_interactive: bool,
}
impl WizardInput for ConfirmInput {
type Value = bool;
fn flag_value(&self) -> Option<Result<bool, String>> {
self.non_interactive.then_some(Ok(true))
}
fn prompt(&self) -> Result<bool, String> {
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Proceed?")
.default(true)
.interact()
.map_err(|err| format!("failed to read confirmation: {err}"))
}
fn non_interactive_fallback(&self) -> Result<bool, String> {
Err(
"confirmation is required when not running interactively (pass --non-interactive to skip)"
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_concurrency_is_at_least_one() {
assert!(default_concurrency() >= 1);
}
#[test]
fn cpu_concurrency_input_flag_value_overrides_the_default() {
let input = CpuConcurrencyInput { flag: Some(1) };
assert_eq!(input.flag_value(), Some(Ok(1)));
}
#[test]
fn cpu_concurrency_input_requires_a_flag_when_not_interactive() {
let input = CpuConcurrencyInput { flag: None };
assert!(input.non_interactive_fallback().is_err());
}
#[test]
fn cpu_concurrency_input_clamps_a_flag_value_far_above_the_core_count() {
let ceiling = default_concurrency() * CONCURRENCY_CLAMP_MULTIPLIER;
let input = CpuConcurrencyInput {
flag: Some(ceiling + 1000),
};
assert_eq!(input.flag_value(), Some(Ok(ceiling)));
}
#[test]
fn cpu_concurrency_input_does_not_clamp_a_flag_value_at_the_ceiling() {
let ceiling = default_concurrency() * CONCURRENCY_CLAMP_MULTIPLIER;
let input = CpuConcurrencyInput {
flag: Some(ceiling),
};
assert_eq!(input.flag_value(), Some(Ok(ceiling)));
}
#[test]
fn transfers_input_flag_value_overrides_the_default() {
let input = TransfersInput { flag: Some(32) };
assert_eq!(input.flag_value(), Some(Ok(32)));
}
#[test]
fn transfers_input_falls_back_to_a_default_when_not_interactive() {
let input = TransfersInput { flag: None };
assert_eq!(input.non_interactive_fallback(), Ok(TRANSFERS_DEFAULT));
}
#[test]
fn checkers_input_flag_value_overrides_the_default() {
let input = CheckersInput { flag: Some(64) };
assert_eq!(input.flag_value(), Some(Ok(64)));
}
#[test]
fn checkers_input_falls_back_to_a_default_when_not_interactive() {
let input = CheckersInput { flag: None };
assert_eq!(input.non_interactive_fallback(), Ok(CHECKERS_DEFAULT));
}
#[test]
fn tpslimit_input_flag_value_overrides_the_default() {
let input = TpslimitInput { flag: Some(10) };
assert_eq!(input.flag_value(), Some(Ok(Some(10))));
}
#[test]
fn tpslimit_input_falls_back_to_no_cap_when_not_interactive() {
let input = TpslimitInput { flag: None };
assert_eq!(input.non_interactive_fallback(), Ok(None));
}
}