use std::collections::HashSet;
use std::fs::create_dir_all;
use camino::{Utf8Path, Utf8PathBuf};
use itertools::Itertools;
use plow_linter::lints::field_manifest_lints;
use plow_linter::lints::HasRegistryPackageName;
use plow_linter::lints::HasRegistryPackageVersion;
use plow_linter::lints::LintSet;
use plow_linter::lints::PlowLint;
use plow_linter::lints::ValidRegistryDependencies;
use rayon::prelude::IntoParallelRefIterator;
use rayon::prelude::ParallelIterator;
use sha2::{Digest, Sha256};
use crate::config::PlowConfig;
use crate::error::CliError;
use crate::error::WorkspaceInitializationError::*;
use crate::manifest::FieldManifest;
use crate::subcommand::lint::lint_file_fail_on_failure;
fn hash_file_with_name(path: &Utf8Path) -> Option<String> {
if let Ok(mut file) = std::fs::File::open(path) {
let mut hasher = Sha256::new();
std::io::copy(&mut file, &mut hasher).ok()?;
if let Some(name) = path.file_name() {
hasher.update(name.as_bytes());
}
let result = hasher.finalize();
return Some(format!("{:x}", result));
}
None
}
#[derive(Default, Clone, Eq, PartialOrd, Ord)]
pub struct FieldPath {
inner: Utf8PathBuf,
}
impl FieldPath {
#[allow(clippy::missing_const_for_fn)]
pub fn into_inner(self) -> Utf8PathBuf {
self.inner
}
pub fn as_path(&self) -> &Utf8Path {
self.inner.as_path()
}
pub fn as_path_buf(&self) -> Utf8PathBuf {
self.inner.clone()
}
pub fn update_path(&mut self, path: Utf8PathBuf) {
self.inner = path;
}
}
impl std::fmt::Display for FieldPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
impl std::fmt::Debug for FieldPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:#?}", self.inner)
}
}
impl From<Utf8PathBuf> for FieldPath {
fn from(path: Utf8PathBuf) -> Self {
Self { inner: path }
}
}
impl From<&Utf8PathBuf> for FieldPath {
fn from(path: &Utf8PathBuf) -> Self {
Self {
inner: path.clone(),
}
}
}
impl From<&Utf8Path> for FieldPath {
fn from(path: &Utf8Path) -> Self {
Self {
inner: path.to_path_buf(),
}
}
}
impl PartialEq for FieldPath {
fn eq(&self, other: &Self) -> bool {
if let Ok(names_are_same) = crate::utils::file_names_are_same(&self.inner, &other.inner) {
if let Ok(files_are_same) = crate::utils::files_are_same(&self.inner, &other.inner) {
return names_are_same && files_are_same;
}
return false;
}
false
}
}
pub struct FieldsDirectory {
pub children: Vec<FieldPath>,
pub path: Utf8PathBuf,
}
impl FieldsDirectory {
pub fn create_empty_at(root: &Utf8Path) -> Self {
Self {
children: vec![],
path: root.join("fields").to_path_buf(),
}
}
pub fn dedup(&mut self) {
self.children.sort();
let mut container = HashSet::new();
let mut all: Vec<FieldPath> = vec![];
for child in &self.children {
if let Some(hash) = hash_file_with_name(child.as_path()) {
if container.contains(&hash) {
continue;
}
container.insert(hash);
all.push(child.clone());
}
}
self.children = all;
}
#[allow(clippy::unwrap_in_result)]
#[allow(clippy::unwrap_used)]
pub fn fill_from_backup(root: &Utf8Path) -> Result<Self, CliError> {
let mut fields_dir = Self::create_empty_at(root.parent().unwrap());
fields_dir.children = crate::utils::list_files(root, "ttl")
.map_err(|err| FailedRecursiveListingFields {
reason: err.to_string(),
})?
.iter()
.map(std::convert::Into::into)
.collect();
Ok(fields_dir)
}
pub fn fill_from_root(root: &Utf8Path) -> Result<Self, CliError> {
let mut fields_dir = Self::create_empty_at(root);
fields_dir.children = crate::utils::list_files(root, "ttl")
.map_err(|err| FailedRecursiveListingFields {
reason: err.to_string(),
})?
.iter()
.filter(|path| {
!path
.components()
.contains(&camino::Utf8Component::Normal(".plow_backup"))
})
.map(std::convert::Into::into)
.collect();
Ok(fields_dir)
}
#[allow(clippy::unwrap_used)]
pub fn extend_from_root_excluding_fields_dir_and_plow_backup(
&mut self,
root: &Utf8Path,
) -> Result<(), CliError> {
self.children = crate::utils::list_files(root, "ttl")
.map_err(|err| FailedRecursiveListingFields {
reason: err.to_string(),
})?
.iter()
.filter(|path| {
!path
.canonicalize_utf8()
.unwrap()
.components()
.contains(&camino::Utf8Component::Normal("fields"))
|| !path
.components()
.contains(&camino::Utf8Component::Normal(".plow_backup"))
})
.map(std::convert::Into::into)
.collect();
Ok(())
}
pub fn exists_in_filesystem(&self) -> bool {
self.path.exists()
}
pub fn lint_all_children(&self) -> Option<(Vec<String>, CliError)> {
let failed_field_paths_on_linting = self
.children
.par_iter()
.filter_map(|child| {
let lints = LintSet::new(
"plow init lints",
vec![
Box::new(HasRegistryPackageName::default()) as PlowLint,
Box::new(HasRegistryPackageVersion::default()) as PlowLint,
Box::new(ValidRegistryDependencies::default()) as PlowLint,
],
None,
);
if let Err(err) = lint_file_fail_on_failure(child.as_path().as_ref(), lints) {
Some(err)
} else {
None
}
})
.filter_map(|err| match err {
CliError::LintSubcommand(
crate::error::LintSubcommandError::FailedToParseField { field_path }
| crate::error::LintSubcommandError::SingleLintContainsFailure { field_path }
| crate::error::LintSubcommandError::FailedToReadField { field_path, .. },
) => Some(field_path),
_ => None,
})
.collect::<Vec<_>>();
if !failed_field_paths_on_linting.is_empty() {
return Some((
failed_field_paths_on_linting.clone(),
crate::error::LintSubcommandError::LintsContainFailures {
field_paths: failed_field_paths_on_linting,
}
.into(),
));
}
None
}
#[allow(clippy::unwrap_in_result)]
#[allow(clippy::indexing_slicing)]
#[allow(clippy::unwrap_used)]
pub fn write_with_children(&mut self) -> Result<(), CliError> {
std::fs::create_dir_all(&self.path)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
let backup_path = &self.path.parent().unwrap().join(".plow_backup");
if !backup_path.exists() {
std::fs::create_dir_all(&self.path.parent().unwrap().join(".plow_backup"))
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
}
let mut copy_number = 1;
for child in &mut self.children {
if let Ok(full_name) = FieldManifest::quick_extract_field_full_name(&child.as_path()) {
let full_name: Vec<&str> = full_name.split('/').collect();
let namespace = full_name[0];
let name = full_name[1];
std::fs::create_dir_all(self.path.join(&namespace).join(&name))
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
let new_file_name = child.as_path().file_name().unwrap();
let mut new_field_destination = self
.path
.join(&namespace)
.join(&name)
.join(new_file_name);
if new_field_destination.exists() {
new_field_destination = new_field_destination
.with_file_name(format!("{new_file_name}.copy_{copy_number}"));
copy_number += 1;
}
std::fs::copy(child.as_path(), &new_field_destination)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
child.update_path(new_field_destination);
}
}
let paths = std::fs::read_dir(&self.path.parent().unwrap()).unwrap();
for path in paths {
let dir = path.unwrap();
let path = Utf8PathBuf::from_path_buf(dir.path()).unwrap();
if path.is_dir() {
if dir
.file_name()
.to_string_lossy()
.as_ref()
.chars()
.next()
.unwrap()
!= '.'
&& dir.file_name() != "fields"
{
let path = Utf8PathBuf::from_path_buf(dir.path()).unwrap();
fs_extra::dir::copy(
&path,
&self.path.parent().unwrap().join(".plow_backup"),
&fs_extra::dir::CopyOptions::default(),
)
.unwrap();
std::fs::remove_dir_all(&path).unwrap();
}
}
if path.is_file() {
if path.file_name().unwrap() != "LICENSE" {
std::fs::copy(
&path,
&self
.path
.parent()
.unwrap()
.join(".plow_backup")
.join(path.file_name().unwrap()),
)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
std::fs::remove_file(&path).unwrap();
}
}
}
Ok(())
}
pub fn explode(path_to_fields_dir: &Utf8Path, config: &PlowConfig) -> Result<(), CliError> {
if path_to_fields_dir.exists() {
let fields = crate::utils::list_files(path_to_fields_dir, "ttl")
.map_err(|err| FailedToReadFieldsDirectory(err.to_string()))?;
for field in fields {
std::fs::copy(
&field,
&config
.working_dir
.path
.join(field.file_name().ok_or_else(|| {
FailedToCreateFieldsDirectory("Found field is not a file.".to_owned())
})?),
)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
}
std::fs::remove_dir_all(&path_to_fields_dir)
.map_err(|err| FailedToRemoveFieldsDirectory(err.to_string()))?;
return Ok(());
}
Err(FailedToReadFieldsDirectory("Fields directory does not exist".to_owned()).into())
}
pub fn backup_if_already_exists(
path_to_fields_dir: &Utf8Path,
config: &PlowConfig,
) -> Result<Option<Utf8PathBuf>, CliError> {
if path_to_fields_dir.exists() {
let fields = crate::utils::list_files(path_to_fields_dir, "ttl")
.map_err(|err| FailedToReadFieldsDirectory(err.to_string()))?;
let tmp_uuid = uuid::Uuid::new_v4();
let backup_dir = config.working_dir.path.join(format!(".plow_{}", tmp_uuid));
create_dir_all(&backup_dir)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
for field in fields {
std::fs::copy(
&field,
&backup_dir.join(field.file_name().ok_or_else(|| {
FailedToCreateFieldsDirectory("Found field is not a file.".to_owned())
})?),
)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?;
}
std::fs::remove_dir_all(&path_to_fields_dir)
.map_err(|err| FailedToRemoveFieldsDirectory(err.to_string()))?;
return Ok(Some(backup_dir));
}
Ok(None)
}
pub fn remove(&self) -> Result<(), CliError> {
Ok(std::fs::remove_dir_all(&self.path)
.map_err(|err| FailedToRemoveFieldsDirectory(err.to_string()))?)
}
pub fn write_empty(&self) -> Result<(), CliError> {
Ok(std::fs::create_dir_all(&self.path)
.map_err(|err| FailedToCreateFieldsDirectory(err.to_string()))?)
}
}