use aws_config::meta::region::RegionProviderChain;
use aws_config::BehaviorVersion;
use aws_types::region::Region;
use std::fs;
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::process::exit;
use clap::Parser;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use crate::constants;
use colored::*;
use dirs::home_dir;
use chrono;
use std::process::{Command, Stdio};
pub fn setup_logging(verbose: bool) {
let env =
env_logger::Env::default().filter_or("SHUK_LOG", if verbose { "trace" } else { "warn" });
env_logger::Builder::from_env(env)
.format(|buf, record| {
writeln!(
buf,
"{} [{}] {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.args()
)
})
.init();
}
pub async fn configure_aws(
fallback_region: String,
profile_name: Option<&String>,
) -> aws_config::SdkConfig {
let mut loader = aws_config::defaults(BehaviorVersion::latest());
if let Some(profile) = profile_name.map(|s| s.as_str()) {
loader = loader.profile_name(profile);
} else {
let region_provider = RegionProviderChain::first_try(
aws_config::environment::EnvironmentVariableRegionProvider::new(),
)
.or_else(aws_config::imds::region::ImdsRegionProvider::builder().build())
.or_else(Region::new(fallback_region));
loader = loader.region(region_provider);
}
loader.load().await
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub bucket_name: String,
#[serde(deserialize_with = "deserialize_prefix")]
pub bucket_prefix: Option<String>,
pub presigned_time: u64,
pub aws_profile: Option<String>,
pub use_clipboard: Option<bool>,
pub fallback_region: Option<String>,
}
fn deserialize_prefix<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let mut prefix = String::deserialize(deserializer)?;
if prefix.is_empty() {
Ok(None)
} else if prefix.ends_with('/') {
Ok(Some(prefix))
} else {
prefix = format!("{}/", prefix);
Ok(Some(prefix))
}
}
impl Config {
pub fn load_config() -> Result<Self, anyhow::Error> {
log::trace!("Parsing the configuration file");
let home_dir = home_dir().expect("Failed to get HOME directory");
log::trace!("Home directory: {:?}", &home_dir);
let config_dir = home_dir.join(".config/shuk");
log::trace!("Config directory: {:?}", &config_dir);
let config_file_path = config_dir.join(constants::CONFIG_FILE_NAME);
log::trace!("Config file path: {:?}", &config_file_path);
if check_for_config() {
let _contents: String = match fs::read_to_string(config_file_path) {
Ok(c) => {
let config: Config = toml::from_str::<Config>(&c).unwrap();
return Ok(config);
}
Err(e) => {
eprintln!("Could not read config file! {}", e);
eprintln!("Your configuration file needs to be in $HOME/.config/shuk/shuk.toml; Please run the configuration command: shuk --init");
exit(1);
}
};
} else {
eprintln!("Could not read config file!");
eprintln!("Your configuration file needs to be in $HOME/.config/shuk/shuk.toml; Please run the configuration command: shuk --init");
exit(1);
}
}
}
pub fn check_for_config() -> bool {
log::trace!("Checking for the configuration file");
let home_dir = home_dir().expect("Failed to get HOME directory");
log::trace!("Home directory: {:?}", &home_dir);
let config_dir = home_dir.join(".config/shuk");
log::trace!("Config directory: {:?}", &config_dir);
let config_file_path = config_dir.join(constants::CONFIG_FILE_NAME);
log::trace!("Config file path: {:?}", &config_file_path);
match config_file_path.try_exists() {
Ok(b) => {
log::trace!("Config file path: {:?} exists", &config_file_path);
b
}
Err(e) => {
log::warn!(
"I was unable to determine if the config file path: {:?} exists",
&config_file_path
);
eprintln!("Was unable to determine if the config file exists: {}", e);
exit(1);
}
}
}
pub async fn initialize_config() -> Result<(), anyhow::Error> {
log::trace!("Initializing the configuration");
let home_dir = home_dir().expect("Failed to get HOME directory");
log::trace!("Home directory: {:?}", &home_dir);
let config_dir = home_dir.join(format!(".config/{}", constants::CONFIG_DIR_NAME));
log::trace!("Config directory: {:?}", &config_dir);
log::trace!("Creating the config directory: {:?}", &config_dir);
fs::create_dir_all(&config_dir)?;
let config_file_path = config_dir.join(constants::CONFIG_FILE_NAME);
log::trace!("Config file path: {:?}", &config_file_path);
let config_content = constants::CONFIG_FILE.to_string();
log::trace!("Config file contents: {:?}", &config_content);
log::trace!("Parsing default config into TOML");
let mut default_config: Config =
toml::from_str::<Config>(&config_content).expect("default config must be valid");
let mut bucket_name = String::new();
print!("Enter the name of the bucket you wish to use for file uploads: ");
io::stdout().flush()?; io::stdin().read_line(&mut bucket_name)?;
default_config.bucket_name = bucket_name.trim().to_string();
log::trace!("Using bucket name: {}", &default_config.bucket_name);
let mut bucket_prefix = String::new();
print!("Enter the prefix (folder) in that bucket where the files will be uploaded (leave blank for the root of the bucket): ");
io::stdout().flush()?; io::stdin().read_line(&mut bucket_prefix)?;
default_config.bucket_prefix = Some(bucket_prefix.trim().to_string());
log::trace!("Using bucket prefix: {:?}", &default_config.bucket_prefix);
let mut config_profile = String::new();
print!("Enter the AWS profile name (enter for None): ");
io::stdout().flush()?; io::stdin().read_line(&mut config_profile)?;
let config_profile = config_profile.trim();
default_config.aws_profile = if config_profile.is_empty() {
None
} else {
Some(config_profile.to_string())
};
log::trace!("Using profile : {:?}", &default_config.aws_profile);
fs::write(&config_file_path, toml::to_string_pretty(&default_config)?)?;
println!(
"⏳| Shuk configuration file created at: {:?}",
config_file_path
);
println!("This file is used to store configuration items for the shuk application.");
println!("✅ | Shuk configuration has been initialized in ~/.config/shuk. You may now use it as normal.");
Ok(())
}
pub fn print_warning(s: &str) {
println!("{}", s.yellow());
}
pub fn set_into_clipboard(s: String) -> Result<(), Box<dyn std::error::Error>> {
log::trace!("Attempting to set clipboard content");
log::debug!("Content length to be copied: {}", s.len());
match std::env::consts::OS {
"linux" => {
log::trace!("Detected Linux OS, attempting clipboard operations");
log::debug!("Attempting Wayland clipboard (wl-copy)");
if let Ok(output) = Command::new("wl-copy")
.stdin(Stdio::piped())
.arg(&s)
.output()
{
if output.status.success() {
log::debug!("Successfully copied to Wayland clipboard");
return Ok(());
}
log::debug!("Wayland clipboard attempt failed, falling back to X11");
} else {
log::debug!("wl-copy not available, falling back to X11");
}
log::debug!("Attempting X11 clipboard (xclip)");
let mut child = Command::new("xclip")
.arg("-selection")
.arg("clipboard")
.stdin(Stdio::piped())
.spawn()
.map_err(|e| {
log::error!("Failed to spawn xclip: {}", e);
format!("Failed to spawn xclip (is it installed?): {}", e)
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(s.as_bytes()).map_err(|e| {
log::error!("Failed to write to xclip stdin: {}", e);
format!("Failed to write to xclip: {}", e)
})?;
} else {
log::error!("Failed to open stdin for xclip");
return Err("Failed to open stdin for xclip".into());
}
let status = child.wait().map_err(|e| {
log::error!("Failed to wait for xclip process: {}", e);
format!("Failed to wait for xclip: {}", e)
})?;
if !status.success() {
log::error!("xclip process failed with status: {}", status);
return Err(format!("xclip failed with status: {}", status).into());
}
log::debug!("Successfully copied to X11 clipboard");
},
"macos" => {
log::trace!("Detected macOS, attempting clipboard operation with pbcopy");
let mut child = Command::new("pbcopy")
.stdin(Stdio::piped())
.spawn()
.map_err(|e| {
log::error!("Failed to spawn pbcopy: {}", e);
format!("Failed to spawn pbcopy: {}", e)
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(s.as_bytes()).map_err(|e| {
log::error!("Failed to write to pbcopy stdin: {}", e);
format!("Failed to write to pbcopy: {}", e)
})?;
} else {
log::error!("Failed to open stdin for pbcopy");
return Err("Failed to open stdin for pbcopy".into());
}
let status = child.wait().map_err(|e| {
log::error!("Failed to wait for pbcopy process: {}", e);
format!("Failed to wait for pbcopy: {}", e)
})?;
if !status.success() {
log::error!("pbcopy process failed with status: {}", status);
return Err(format!("pbcopy failed with status: {}", status).into());
}
log::debug!("Successfully copied to macOS clipboard");
},
"windows" => {
log::trace!("Detected Windows, attempting clipboard operation with clip.exe");
let mut child = Command::new("clip")
.stdin(Stdio::piped())
.spawn()
.map_err(|e| {
log::error!("Failed to spawn clip.exe: {}", e);
format!("Failed to spawn clip.exe: {}", e)
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(s.as_bytes()).map_err(|e| {
log::error!("Failed to write to clip.exe stdin: {}", e);
format!("Failed to write to clip.exe: {}", e)
})?;
} else {
log::error!("Failed to open stdin for clip.exe");
return Err("Failed to open stdin for clip.exe".into());
}
let status = child.wait().map_err(|e| {
log::error!("Failed to wait for clip.exe process: {}", e);
format!("Failed to wait for clip.exe: {}", e)
})?;
if !status.success() {
log::error!("clip.exe process failed with status: {}", status);
return Err(format!("clip.exe failed with status: {}", status).into());
}
log::debug!("Successfully copied to Windows clipboard");
},
os => {
log::error!("Unsupported operating system: {}", os);
return Err(format!("Unsupported operating system: {}", os).into());
}
}
log::trace!("Clipboard operation completed successfully");
Ok(())
}
#[derive(Debug, Parser, Default)]
#[command(version, about, long_about = None)]
pub struct Args {
#[arg(required_unless_present("init"))]
pub filename: Option<PathBuf>,
#[arg(long, conflicts_with("filename"))]
pub init: bool,
#[arg(short, long, help = "Enable verbose logging")]
pub verbose: bool,
}