depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Storage module.

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};

/// Environment variable name.
pub const CUSTOM_STORAGE_PATH: &str = "DEPLOYER_STORAGE_PATH";

/// Lists all available content in Deployer's storage.
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(())
}

/// Creates a new content from given folder.
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(())
}

/// Syncs content files to build folder.
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, &[""])?)
  }
}

/// Adds project artifacts as a content.
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(())
}

/// Adds project artifacts as a content.
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!"))
}

/// Removes the content from Deployer's storage.
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(())
}

/// Use content from storage to current directory.
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(())
}