use crate::commands::{ColorSupport, CommandError};
use std::fs::ReadDir;
use std::path::{Path, PathBuf};
use std::{fs, os};
use tracing::{debug, info, instrument, warn};
pub trait CommandOperation<T: Iterator<Item = Result<PathBuf, CommandError>>> {
fn link_item(&mut self, target: &Path, source: &Path) -> Result<(), CommandError>;
fn remove_link(&mut self, target: &Path) -> Result<(), CommandError>;
fn remove_item(&mut self, target: &Path) -> Result<(), CommandError>;
fn create_directory(&mut self, target: &Path) -> Result<(), CommandError>;
fn is_directory(&self, target: &Path) -> bool;
fn is_file(&self, target: &Path) -> bool;
fn is_symlink(&self, target: &Path) -> bool;
fn read_link(&self, target: &Path) -> Result<PathBuf, CommandError>;
fn exists(&self, target: &Path) -> bool;
fn read_directory(&self, target: &Path) -> Result<T, CommandError>;
}
#[derive(Debug, Default, PartialEq, Eq)]
pub enum CommandOperationImpl {
#[default]
Default,
Simulated(SimulatedData),
}
#[derive(Debug, Default, PartialEq, Eq)]
struct LinkedDirectory {
path: PathBuf,
link_path: PathBuf,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SimulatedData {
created_directories: Vec<PathBuf>,
created_links: Vec<LinkedDirectory>,
color_support: ColorSupport,
}
impl SimulatedData {
pub const fn with_color_support(mut self, color_support: ColorSupport) -> Self {
self.color_support = color_support;
self
}
}
pub struct DirectoryReader {
read_dir: ReadDir,
}
impl From<ReadDir> for DirectoryReader {
fn from(read_dir: ReadDir) -> Self {
Self { read_dir }
}
}
impl Iterator for DirectoryReader {
type Item = Result<PathBuf, CommandError>;
fn next(&mut self) -> Option<Self::Item> {
self.read_dir
.next()
.map(|res| res.map_err(Into::into).map(|entry| entry.path()))
}
}
impl CommandOperation<DirectoryReader> for CommandOperationImpl {
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[instrument(level = "trace")]
fn link_item(&mut self, item: &Path, target: &Path) -> Result<(), CommandError> {
match self {
Self::Default => {
info!("Linking {} {}", item.display(), target.display());
os::unix::fs::symlink(item, target)?;
}
Self::Simulated(data) => {
data.created_links.push(LinkedDirectory {
path: item.to_path_buf(),
link_path: target.to_path_buf(),
});
data.color_support.print_link_text(item, target);
}
}
Ok(())
}
#[cfg(target_os = "windows")]
#[instrument(level = "trace")]
fn link_item(&mut self, item: &Path, target: &Path) -> Result<(), CommandError> {
match self {
Self::Default => {
info!("Linking {} {}", item.display(), target.display());
if self.is_directory(item) {
os::windows::fs::symlink_dir(item, target)?;
} else {
os::windows::fs::symlink_file(item, target)?;
}
}
Self::Simulated(data) => {
data.created_links.push(LinkedDirectory {
path: item.to_path_buf(),
link_path: target.to_path_buf(),
});
data.color_support.print_link_text(item, target);
}
}
Ok(())
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[instrument(level = "trace")]
fn remove_link(&mut self, entry_path: &Path) -> Result<(), CommandError> {
if !self.is_symlink(entry_path) {
warn!("Not a symlink: {}", entry_path.display());
return Ok(());
}
match self {
Self::Default => {
debug!("Deleting symlink: {}", entry_path.display());
fs::remove_file(entry_path)?;
}
Self::Simulated(data) => {
data.color_support.print_unlink_text(entry_path);
}
}
Ok(())
}
#[cfg(target_os = "windows")]
#[instrument(level = "trace")]
fn remove_link(&mut self, entry_path: &Path) -> Result<(), CommandError> {
if !self.is_symlink(entry_path) {
warn!("Not a symlink: {}", entry_path.display());
return Ok(());
}
match self {
Self::Default => {
debug!("Deleting symlink: {}", entry_path.display());
if self.is_directory(entry_path) {
fs::remove_dir(entry_path)?;
} else {
fs::remove_file(entry_path)?;
}
}
Self::Simulated(data) => {
data.color_support.print_unlink_text(entry_path);
}
}
Ok(())
}
#[instrument(level = "trace")]
fn remove_item(&mut self, target: &Path) -> Result<(), CommandError> {
match self {
Self::Default => {
if self.is_directory(target) {
debug!("Deleting directory: {}", target.display());
fs::remove_dir(target)?;
} else {
debug!("Deleting file: {}", target.display());
fs::remove_file(target)?;
}
}
Self::Simulated(data) => data.color_support.print_remove_text(target),
}
Ok(())
}
#[instrument(level = "trace")]
fn create_directory(&mut self, target: &Path) -> Result<(), CommandError> {
match self {
Self::Default => {
debug!("Creating directory: {}", target.display());
fs::create_dir_all(target)?;
}
Self::Simulated(data) => {
data.created_directories.push(target.to_owned());
data.color_support.print_create_text(target);
}
}
Ok(())
}
#[instrument(level = "trace")]
fn is_directory(&self, target: &Path) -> bool {
if fs::metadata(target).is_ok_and(|meta| meta.is_dir()) {
return true;
}
match self {
Self::Default => false,
Self::Simulated(data) => data.created_directories.contains(&target.to_path_buf()),
}
}
#[instrument(level = "trace")]
fn is_file(&self, target: &Path) -> bool {
fs::metadata(target).is_ok_and(|meta| meta.is_file())
}
#[instrument(level = "trace")]
fn is_symlink(&self, target: &Path) -> bool {
fs::symlink_metadata(target).is_ok_and(|meta| meta.is_symlink())
}
#[instrument(level = "trace")]
fn read_link(&self, target: &Path) -> Result<PathBuf, CommandError> {
target.canonicalize().map_err(Into::into)
}
#[instrument(level = "trace")]
fn exists(&self, target: &Path) -> bool {
if fs::exists(target).unwrap_or(false) {
return true;
}
match self {
Self::Default => false,
Self::Simulated(data) => {
data.created_directories.contains(&target.to_path_buf())
|| data
.created_links
.iter()
.any(|link| link.link_path == target)
}
}
}
#[instrument(level = "trace")]
fn read_directory(&self, target: &Path) -> Result<DirectoryReader, CommandError> {
fs::read_dir(target).map_err(Into::into).map(Into::into)
}
}