use anyhow::{Context, anyhow, bail};
use colored::Colorize;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::exit;
use crate::bset;
use crate::cmd::{LsContentArgs, NewContentArgs, RemoveContentArgs, UseContentArgs};
use crate::entities::info::{ContentInfo, StrToInfo};
use crate::rw::copy_all;
use crate::{STORAGE_DIR, bmap};
pub const CUSTOM_STORAGE_PATH: &str = "DEPLOYER_STORAGE_PATH";
pub fn list_content(storage_dir: &Path, args: LsContentArgs) -> anyhow::Result<()> {
struct EntryInfo {
info: ContentInfo,
path: PathBuf,
}
let content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
println!("Available content in Deployer's storage:");
let mut content = bmap![];
match std::fs::read_dir(content_path) {
Err(_) => {}
Ok(entries) => {
for entry in entries {
let entry = entry?;
if !entry.path().is_dir() {
continue;
}
if let Ok(name) = entry.file_name().into_string()
&& let Ok(info) = ContentInfo::try_from_str(name.as_str())
{
content.insert(
info.clone(),
EntryInfo {
info,
path: entry.path(),
},
);
}
}
}
}
if args.all {
for content_entry in content.values() {
println!(
"• {} (path: {:?})",
content_entry.info.to_str().blue().bold(),
content_entry.path,
);
}
} else {
let mut unique_content = bmap![];
for (k, v) in content {
if !unique_content.contains_key(k.short_name()) {
unique_content.insert(k.short_name().to_string(), bmap![]);
}
unique_content
.get_mut(k.short_name())
.unwrap()
.insert(k.version().to_string(), v);
}
for list in unique_content.values() {
let content_entry = list.last_key_value().unwrap().1;
println!(
"• {} (latest version: {}, path: {:?})",
content_entry.info.short_name().blue().bold(),
content_entry.info.version().blue().bold(),
content_entry.path,
);
}
}
Ok(())
}
pub fn new_content(storage_dir: &Path, args: NewContentArgs) -> anyhow::Result<()> {
use inquire_reorder::{Confirm, Text};
let content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
let path = if let Some(arg_defined) = args.content_path {
arg_defined
} else {
println!("To add content, you need to specify the path to the content folder.");
println!(
"The content in it must be located in such a way that the paths to the required files are relative to the run folder."
);
println!(
"For example, if you need to place a Dockerfile at the root of the run folder, you place the file at the root of the content folder; if you need the file to be located in a subfolder, you place it in a subfolder with the same name inside the content folder."
);
PathBuf::from(Text::new("Specify content folder's path:").prompt()?)
};
if !path.exists() {
bail!("There is no such file or folder!")
}
let info = if let Some(arg_defined) = args.public_tag {
ContentInfo::try_from_str(arg_defined.as_str())?
} else {
println!(
"Now you need to specify the short name and version of the content (for example, `dockerfile` content with version `0.1.0`)."
);
println!("You will need this information to add a `UseFromStorage` Action.");
let short_name = Text::new("Write the content's short name:").prompt()?;
let version = Text::new("Specify the content's version:").prompt()?;
ContentInfo::new(short_name, version)?
};
let new_path = content_path.join(info.to_str());
if new_path.exists() {
if args.r#override
|| Confirm::new(&format!("Content with `{}` tag exists. Override? (y/n)", info.to_str())).prompt()?
{
std::fs::remove_dir_all(&new_path)?;
} else {
exit(0);
}
}
if path.is_dir() {
copy_all(&path, &path, &new_path, &[""])?;
} else {
copy_all(&path, &path, new_path.join(path.file_name().unwrap()), &[""])?;
}
println!(
"Content `{}` added to Deployer's storage successfully (path: {new_path:?})",
info.to_str()
);
Ok(())
}
pub fn use_from_storage(
storage_dir: &Path,
build_dir: &Path,
content_info: &ContentInfo,
) -> anyhow::Result<Vec<PathBuf>> {
let content_info_str = content_info.to_str();
let mut content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
if !content_info_str.ends_with("latest") {
content_path.push(&content_info_str);
if !content_path.exists() {
bail!("There is no such content: `{content_info_str}`. Consider to add this content via `depl add content`.")
}
Ok(copy_all(&content_path, &content_path, build_dir, &[""])?)
} else {
let mut versions = bmap!();
for entry in std::fs::read_dir(&content_path)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(content_info.short_name()) {
let version = name
.split('@')
.next_back()
.ok_or(anyhow!("There is no version spec in folder name!"))?;
let version = semver::Version::parse(version).context("Can't parse version as SemVer!")?;
versions.insert(version, name);
}
}
if versions.is_empty() {
bail!("There is no such content: `{content_info_str}`. Consider to add this content via `depl add content`.")
}
let max = versions.keys().max().ok_or(anyhow!("No version available!"))?;
let max = versions.get(max).ok_or(anyhow!("No version available!"))?;
crate::rw::log(format!("Decided to choose `{max}` from `latest`."));
content_path.push(max);
if !content_path.exists() {
bail!("There is no such content: `{content_info_str}`. Consider to add this content via `depl add content`.")
}
Ok(copy_all(&content_path, &content_path, build_dir, &[""])?)
}
}
pub fn add_to_storage(storage_dir: &Path, artifacts_dir: &Path, content_info: &ContentInfo) -> anyhow::Result<()> {
let content_info_str = content_info.to_str();
let mut content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
content_path.push(&content_info_str);
if content_path.exists() {
return Ok(());
}
copy_all(artifacts_dir, artifacts_dir, &content_path, &[""])?;
Ok(())
}
pub fn add_single_to_storage(
storage_dir: &Path,
from: &Path,
to: &Path,
content_info: &ContentInfo,
) -> anyhow::Result<()> {
let content_info_str = content_info.to_str();
let mut content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
content_path.push(&content_info_str);
copy_all(from, from, content_path.join(to), &[""])?;
Ok(())
}
fn find_all_content(content_path: &Path) -> anyhow::Result<BTreeSet<ContentInfo>> {
let mut versions = bset![];
for entry in std::fs::read_dir(content_path)? {
let entry = entry?;
let name = entry.file_name().to_str().unwrap().to_owned();
if let Ok(info) = ContentInfo::try_from_str(&name) {
versions.insert(info);
}
}
Ok(versions)
}
fn choose_content<'a>(content_storage: &'a BTreeSet<ContentInfo>, prompt: &str) -> anyhow::Result<&'a ContentInfo> {
if content_storage.is_empty() {
bail!("There is no actions in the Registry.");
}
let keys = content_storage.iter().map(|info| info.to_str()).collect::<Vec<_>>();
let selected = inquire_reorder::Select::new(prompt, keys).prompt()?;
content_storage
.iter()
.find(|info| info.to_str().eq(&selected))
.ok_or(anyhow!("No such action!"))
}
pub fn remove_content(storage_dir: &Path, args: RemoveContentArgs) -> anyhow::Result<()> {
let content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
let contents = find_all_content(&content_path)?;
let content = if let Some(info) = args.info {
info.to_info()?
} else {
choose_content(&contents, "Select content for removing from the storage:")?.clone()
};
let variant = content_path.join(content.to_str());
if variant.exists() && !args.yes && inquire_reorder::Confirm::new("Are you sure? (y/n)").prompt()? {
std::fs::remove_dir_all(variant)?;
}
Ok(())
}
pub fn use_content(storage_dir: &Path, args: UseContentArgs) -> anyhow::Result<()> {
let content_path = if let Ok(custom_storage_dir) = std::env::var(CUSTOM_STORAGE_PATH) {
PathBuf::from(custom_storage_dir)
} else {
PathBuf::from(storage_dir).join(STORAGE_DIR)
};
let contents = find_all_content(&content_path)?;
let content = if let Some(info) = args.info {
let chosen = contents
.iter()
.rev()
.find(|c| c.to_str().starts_with(&info))
.ok_or(anyhow!("Can't find `{info}` content!"))?
.clone();
println!("Selected to sync: {}", chosen.to_str());
chosen
} else {
choose_content(&contents, "Select content for usage:")?.clone()
};
let mut sync_into_folder = PathBuf::from(".");
if let Some(subfolder) = args.output {
sync_into_folder = sync_into_folder.join(subfolder);
if !sync_into_folder.exists() {
std::fs::create_dir_all(&sync_into_folder)?;
}
}
let copied = use_from_storage(storage_dir, &sync_into_folder, &content)?;
if !args.template.is_empty() {
use smart_patcher::{FilePath, Patch, PatchFile, Replacer};
let mut patch_file = PatchFile { patches: vec![] };
for template in args.template {
let (from, to) = template
.split_once('=')
.expect("Template values should be provided by `key=value` format!");
let patch = Patch {
files: copied.clone().into_iter().map(FilePath::Just).collect(),
replace: Some(Replacer::FromTo(from.to_owned(), to.to_owned())),
..Default::default()
};
patch_file.patches.push(patch);
}
patch_file.patch(&sync_into_folder, &sync_into_folder)?;
}
Ok(())
}