#![warn(missing_docs)]
#![forbid(unsafe_code)]
pub mod error;
pub mod storage;
use std::path::PathBuf;
use std::str::FromStr;
pub use crate::error::{Error, Result};
use clap::{Parser, Subcommand};
use clap_complete::ArgValueCompleter;
use derive_more::Display;
use storage::{get_storage_record, storage_identifier_completer};
pub use storage::{
XvcLocalStorage, XvcStorage, XvcStorageEvent, XvcStorageGuid, XvcStorageOperations,
};
use xvc_core::XvcStore;
use xvc_core::XvcRoot;
use xvc_core::{output, XvcOutputSender};
#[derive(Debug, Parser, Clone)]
#[command(name = "storage", about = "")]
pub struct StorageCLI {
#[command(subcommand)]
pub subcommand: StorageSubCommand,
}
#[derive(Debug, Clone, Parser)]
#[command(about = "Manage storages containing tracked file content")]
pub enum StorageSubCommand {
#[command(visible_aliases=&["l"])]
List,
#[command(visible_aliases=&["R"])]
Remove {
#[arg(short, long, add = ArgValueCompleter::new(storage_identifier_completer))]
name: String,
},
#[command(subcommand, visible_aliases=&["n"])]
New(StorageNewSubCommand),
}
#[derive(Debug, Clone, Subcommand)]
#[command()]
pub enum StorageNewSubCommand {
#[command()]
Local {
#[arg(long, value_hint=clap::ValueHint::DirPath)]
path: PathBuf,
#[arg(long, short)]
name: String,
},
#[command()]
Generic {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long = "init", short = 'i', value_hint=clap::ValueHint::CommandString)]
init_command: String,
#[arg(long = "list", short = 'l', value_hint=clap::ValueHint::CommandString)]
list_command: String,
#[arg(long = "download", short = 'd', value_hint=clap::ValueHint::CommandString)]
download_command: String,
#[arg(long = "upload", short = 'u',value_hint=clap::ValueHint::CommandString )]
upload_command: String,
#[arg(long = "delete", short = 'D',value_hint=clap::ValueHint::CommandString )]
delete_command: String,
#[arg(long = "processes", short = 'M', default_value_t = 1)]
max_processes: usize,
#[arg(long, value_hint=clap::ValueHint::Url)]
url: Option<String>,
#[arg(long)]
storage_dir: Option<String>,
},
#[command()]
Rsync {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long, value_hint=clap::ValueHint::Hostname)]
host: String,
#[arg(long)]
port: Option<usize>,
#[arg(long, value_hint=clap::ValueHint::Username)]
user: Option<String>,
#[arg(long)]
storage_dir: String,
},
#[cfg(feature = "rclone")]
#[command()]
Rclone {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long)]
remote_name: String,
#[arg(long, default_value = "")]
storage_prefix: String,
},
#[cfg(feature = "s3")]
#[command()]
S3 {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long, default_value = "")]
storage_prefix: String,
#[arg(long)]
bucket_name: String,
#[arg(long)]
region: String,
},
#[cfg(feature = "minio")]
#[command()]
Minio {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long, value_hint=clap::ValueHint::Url)]
endpoint: String,
#[arg(long)]
bucket_name: String,
#[arg(long)]
region: String,
#[arg(long, default_value = "")]
storage_prefix: String,
},
#[cfg(feature = "digital-ocean")]
#[command()]
DigitalOcean {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long)]
bucket_name: String,
#[arg(long)]
region: String,
#[arg(long, default_value = "")]
storage_prefix: String,
},
#[cfg(feature = "r2")]
#[command()]
R2 {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long)]
account_id: String,
#[arg(long)]
bucket_name: String,
#[arg(long, default_value = "")]
storage_prefix: String,
},
#[cfg(feature = "gcs")]
#[command()]
Gcs {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long)]
bucket_name: String,
#[arg(long)]
region: String,
#[arg(long, default_value = "")]
storage_prefix: String,
},
#[cfg(feature = "wasabi")]
#[command()]
Wasabi {
#[arg(long = "name", short = 'n')]
name: String,
#[arg(long)]
bucket_name: String,
#[arg(long, default_value = "s3.wasabisys.com")]
endpoint: String,
#[arg(long, default_value = "")]
storage_prefix: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Display)]
pub enum StorageIdentifier {
Name(String),
Uuid(uuid::Uuid),
}
impl FromStr for StorageIdentifier {
fn from_str(s: &str) -> Result<Self> {
match uuid::Uuid::parse_str(s) {
Ok(uuid) => Ok(Self::Uuid(uuid)),
Err(_) => Ok(Self::Name(s.to_string())),
}
}
type Err = crate::Error;
}
pub fn cmd_storage(
input: std::io::StdinLock,
output_snd: &XvcOutputSender,
xvc_root: &XvcRoot,
opts: StorageCLI,
) -> Result<()> {
match opts.subcommand {
StorageSubCommand::List => cmd_storage_list(input, output_snd, xvc_root),
StorageSubCommand::Remove { name } => cmd_storage_remove(input, output_snd, xvc_root, name),
StorageSubCommand::New(new) => cmd_storage_new(input, output_snd, xvc_root, new),
}
}
fn cmd_storage_new(
input: std::io::StdinLock,
output_snd: &XvcOutputSender,
xvc_root: &XvcRoot,
sc: StorageNewSubCommand,
) -> Result<()> {
match sc {
StorageNewSubCommand::Local { path, name } => {
storage::local::cmd_storage_new_local(input, output_snd, xvc_root, path, name)
}
StorageNewSubCommand::Generic {
name,
init_command,
list_command,
download_command,
upload_command,
delete_command,
max_processes,
url,
storage_dir,
} => storage::generic::cmd_storage_new_generic(
input,
output_snd,
xvc_root,
name,
url,
storage_dir,
max_processes,
init_command,
list_command,
download_command,
upload_command,
delete_command,
),
#[cfg(feature = "rclone")]
StorageNewSubCommand::Rclone {
name,
remote_name,
storage_prefix,
} => {
storage::rclone::cmd_new_rclone(output_snd, xvc_root, name, remote_name, storage_prefix)
}
#[cfg(feature = "s3")]
StorageNewSubCommand::S3 {
name,
storage_prefix,
bucket_name,
region,
} => storage::s3::cmd_new_s3(
output_snd,
xvc_root,
name,
region,
bucket_name,
storage_prefix,
),
#[cfg(feature = "minio")]
StorageNewSubCommand::Minio {
name,
endpoint,
bucket_name,
storage_prefix,
region,
} => storage::minio::cmd_new_minio(
input,
output_snd,
xvc_root,
name,
endpoint,
bucket_name,
region,
storage_prefix,
),
#[cfg(feature = "digital-ocean")]
StorageNewSubCommand::DigitalOcean {
name,
bucket_name,
region,
storage_prefix,
} => storage::digital_ocean::cmd_new_digital_ocean(
input,
output_snd,
xvc_root,
name,
bucket_name,
region,
storage_prefix,
),
#[cfg(feature = "r2")]
StorageNewSubCommand::R2 {
name,
account_id,
bucket_name,
storage_prefix,
} => storage::r2::cmd_new_r2(
input,
output_snd,
xvc_root,
name,
account_id,
bucket_name,
storage_prefix,
),
#[cfg(feature = "gcs")]
StorageNewSubCommand::Gcs {
name,
bucket_name,
region,
storage_prefix,
} => storage::gcs::cmd_new_gcs(
input,
output_snd,
xvc_root,
name,
bucket_name,
region,
storage_prefix,
),
#[cfg(feature = "wasabi")]
StorageNewSubCommand::Wasabi {
name,
bucket_name,
endpoint,
storage_prefix,
} => storage::wasabi::cmd_new_wasabi(
output_snd,
xvc_root,
name,
bucket_name,
endpoint,
storage_prefix,
),
StorageNewSubCommand::Rsync {
name,
host,
port,
user,
storage_dir,
} => {
storage::rsync::cmd_new_rsync(output_snd, xvc_root, name, host, port, user, storage_dir)
}
}
}
fn cmd_storage_remove(
_input: std::io::StdinLock,
output_snd: &XvcOutputSender,
xvc_root: &XvcRoot,
identifier: String,
) -> Result<()> {
let identifier = StorageIdentifier::from_str(&identifier)?;
let storage = get_storage_record(output_snd, xvc_root, &identifier)?;
xvc_root.with_store_mut::<XvcStorage>(|store| {
let filtered = store.filter(|_xe, xs| xs.guid() == storage.guid());
if let Some((xe, xs)) = filtered.first() {
store.remove(*xe);
output!(output_snd, "Removed Storage {xs}");
Ok(())
} else {
Err(anyhow::anyhow!("Cannot find storage with identifier: {identifier}").into())
}
})?;
Ok(())
}
fn cmd_storage_list(
_input: std::io::StdinLock,
output_snd: &XvcOutputSender,
xvc_root: &XvcRoot,
) -> Result<()> {
let store: XvcStore<XvcStorage> = xvc_root.load_store()?;
for (_, s) in store.iter() {
output!(output_snd, "{}\n", s);
}
Ok(())
}