lla 0.1.6

A lightweight ls replacement
use crate::error::LlaError;
use std::fs::{self, Metadata};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::prelude::MetadataExt;
use std::path::PathBuf;
use std::vec::Vec;

pub trait FileLister {
    fn list_files(
        &self,
        directory: &str,
        recursive: Option<bool>,
        depth: Option<u32>,
    ) -> Result<Vec<PathBuf>, LlaError>;
}

pub struct BasicLister;

impl BasicLister {
    pub fn new() -> BasicLister {
        BasicLister {}
    }
}

impl FileLister for BasicLister {
    fn list_files(
        &self,
        directory: &str,
        _recursive: Option<bool>,
        _depth: Option<u32>,
    ) -> Result<Vec<PathBuf>, LlaError> {
        let entries =
            fs::read_dir(directory).map_err(|e| LlaError::FailedToReadDir(e.to_string()))?;
        let mut files = Vec::new();

        for entry in entries {
            if let Ok(entry) = entry {
                let path = entry.path();
                files.push(path);
            }
        }

        Ok(files)
    }
}

pub struct LongLister;

impl LongLister {
    pub fn new() -> LongLister {
        LongLister {}
    }

    pub fn get_file_permissions(&self, metadata: &Metadata) -> String {
        let mode = metadata.permissions().mode();
        let mut permissions = String::new();

        let modes_and_permissions = [
            (0o100000, "-"),
            (0o120000, "l"),
            (0o040000, "d"),
            (0o100000, "p"),
            (0o060000, "b"),
            (0o060000, "c"),
            (0o140000, "s"),
        ];
        for &(m, p) in modes_and_permissions.iter() {
            if mode & m == m {
                permissions.push_str(p);
            } else {
                permissions.push_str("-");
            }
        }
        permissions.push_str(&self.get_file_mode(mode));
        permissions
    }

    pub fn get_file_mode(&self, mode: u32) -> String {
        let mut file_mode = String::new();

        let modes_and_permissions = [
            (0o400, "r"),
            (0o200, "w"),
            (0o100, "x"),
            (0o040, "r"),
            (0o020, "w"),
            (0o010, "x"),
            (0o004, "r"),
            (0o002, "w"),
            (0o001, "x"),
        ];
        for &(m, p) in modes_and_permissions.iter() {
            if mode & m == m {
                file_mode.push_str(p);
            } else {
                file_mode.push_str("-");
            }
        }
        file_mode
    }

    pub fn get_user_name(&self, metadata: &Metadata) -> Result<String, LlaError> {
        let uid = metadata.uid();
        let user = users::get_user_by_uid(uid);
        match user {
            Some(user) => Ok(user.name().to_string_lossy().to_string()),
            None => Err(LlaError::FailedToGetUserByUID(uid.to_string())),
        }
    }

    pub fn get_group_name(&self, metadata: &Metadata) -> Result<String, LlaError> {
        let gid = metadata.gid();
        let group = users::get_group_by_gid(gid);
        match group {
            Some(group) => Ok(group.name().to_string_lossy().to_string()),
            None => Err(LlaError::FailedToGetGroupByGID(gid.to_string())),
        }
    }

    pub fn format_size(&self, size: u64) -> String {
        let sizes = ["B", "KB", "MB", "GB", "TB"];
        let mut size = size as f64;
        let mut idx = 0;

        while size >= 1024.0 && idx < sizes.len() - 1 {
            size /= 1024.0;
            idx += 1;
        }

        format!("{:.1} {}", size, sizes[idx])
    }

    pub fn get_git_status(&self, path: &str) -> String {
        if (git2::Repository::open_from_env()).is_err() {
            return String::new();
        }
        let repo = git2::Repository::open_from_env().unwrap();
        let mut status_options = git2::StatusOptions::new();
        status_options.include_untracked(true);
        let binding = repo.statuses(Some(&mut status_options)).unwrap();

        let statuses = binding.iter().filter(|e| {
            if let Some(e_path) = e.path() {
                e_path == path && !e.status().contains(git2::Status::WT_NEW)
            } else {
                false
            }
        });

        let mut status_str = String::new();

        for status in statuses {
            match status.status() {
                s if s.contains(git2::Status::WT_MODIFIED) => status_str.push_str("M"),
                s if s.contains(git2::Status::INDEX_MODIFIED) => status_str.push_str("M"),
                s if s.contains(git2::Status::WT_DELETED) => status_str.push_str("D"),
                s if s.contains(git2::Status::INDEX_DELETED) => status_str.push_str("D"),
                s if s.contains(git2::Status::WT_RENAMED) => status_str.push_str("R"),
                s if s.contains(git2::Status::INDEX_RENAMED) => status_str.push_str("R"),
                s if s.contains(git2::Status::WT_TYPECHANGE) => status_str.push_str("C"),
                s if s.contains(git2::Status::INDEX_TYPECHANGE) => status_str.push_str("C"),
                _ => status_str.push_str("?"),
            }
        }

        status_str
    }
}

impl FileLister for LongLister {
    fn list_files(
        &self,
        directory: &str,
        _recursive: Option<bool>,
        _depth: Option<u32>,
    ) -> Result<Vec<PathBuf>, LlaError> {
        let entries =
            fs::read_dir(directory).map_err(|e| LlaError::FailedToReadDir(e.to_string()))?;
        let mut files = Vec::new();

        for entry in entries {
            if let Ok(entry) = entry {
                let path = entry.path();
                files.push(path);
            }
        }

        Ok(files)
    }
}

pub struct TreeLister;

impl TreeLister {
    pub fn new() -> TreeLister {
        TreeLister {}
    }
}

impl FileLister for TreeLister {
    fn list_files(
        &self,
        directory: &str,
        recursive: Option<bool>,
        depth: Option<u32>,
    ) -> Result<Vec<PathBuf>, LlaError> {
        if (depth.is_some() && depth.unwrap() == 0) || (recursive.is_some() && !recursive.unwrap())
        {
            let entries =
                fs::read_dir(directory).map_err(|e| LlaError::FailedToReadDir(e.to_string()))?;
            let mut files = Vec::new();

            for entry in entries {
                if let Ok(entry) = entry {
                    let path = entry.path();
                    files.push(path);
                }
            }

            Ok(files)
        } else {
            let entries =
                fs::read_dir(directory).map_err(|e| LlaError::FailedToReadDir(e.to_string()))?;
            let mut files = Vec::new();

            for entry in entries {
                if let Ok(entry) = entry {
                    let path = entry.path();
                    if path.is_dir() {
                        let mut new_depth = depth.unwrap_or(0);
                        if depth.is_some() {
                            new_depth -= 1;
                        }
                        let mut new_recursive = recursive.unwrap_or(false);
                        if recursive.is_some() {
                            new_recursive = recursive.unwrap();
                        }
                        let mut new_directory = directory.to_string();
                        new_directory.push_str("/");
                        new_directory.push_str(path.file_name().unwrap().to_str().unwrap());
                        files.push(path);
                        let mut new_files =
                            self.list_files(&new_directory, Some(new_recursive), Some(new_depth))?;
                        files.append(&mut new_files);
                    } else {
                        files.push(path);
                    }
                }
            }

            Ok(files)
        }
    }
}