use crate::cli::{Cli, FileTypeFilter}; use crate::error::{LsPlusError, Result}; use chrono::{DateTime, Local}; use std::fs; use std::path::{Path, PathBuf}; use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct FileEntry {
pub path: PathBuf,
pub name: String,
pub size: u64,
pub modified: DateTime<Local>,
pub permissions: String,
pub file_type: EntryType,
pub is_hidden: bool,
pub is_symlink: bool,
pub symlink_target: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EntryType {
File,
Directory,
Symlink,
Other,
}
impl FileEntry {
pub fn from_path(path: &Path, follow_links: bool) -> Result<Self> {
let metadata = if follow_links {
fs::metadata(path)
} else {
fs::symlink_metadata(path)
}
.map_err(|e| {
LsPlusError::system_error(format!("Failed to read metadata for {path:?}: {e}"))
})?;
let name = path
.file_name()
.and_then(|n| n.to_str()) .unwrap_or("") .to_string();
let is_hidden = name.starts_with('.');
let is_symlink = metadata.file_type().is_symlink();
let file_type = if metadata.is_dir() {
EntryType::Directory } else if is_symlink {
EntryType::Symlink } else if metadata.is_file() {
EntryType::File } else {
EntryType::Other };
let symlink_target = if is_symlink {
fs::read_link(path).ok() } else {
None
};
let modified = metadata
.modified()
.map_err(|e| {
LsPlusError::system_error(format!("Failed to read modification time: {e}"))
})?
.into();
let permissions = format_permissions(&metadata);
Ok(FileEntry {
path: path.to_path_buf(),
name,
size: metadata.len(),
modified,
permissions,
file_type,
is_hidden,
is_symlink,
symlink_target,
})
}
pub fn matches_filter(&self, filter: &FileTypeFilter) -> bool {
match filter {
FileTypeFilter::File => self.file_type == EntryType::File, FileTypeFilter::Dir => self.file_type == EntryType::Directory, FileTypeFilter::Link => self.file_type == EntryType::Symlink,
FileTypeFilter::Exec => {
self.file_type == EntryType::File && self.is_executable() }
FileTypeFilter::Image => {
self.file_type == EntryType::File && self.is_image() }
FileTypeFilter::Audio => {
self.file_type == EntryType::File && self.is_audio() }
FileTypeFilter::Video => {
self.file_type == EntryType::File && self.is_video() }
FileTypeFilter::Archive => {
self.file_type == EntryType::File && self.is_archive() }
FileTypeFilter::Document => {
self.file_type == EntryType::File && self.is_document() }
}
}
fn is_executable(&self) -> bool {
self.name.ends_with(".exe") || self.name.ends_with(".sh") || self.name.ends_with(".bat") || !self.name.contains('.') }
fn is_image(&self) -> bool {
let extensions = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "ico"];
self.has_extension(&extensions)
}
fn is_audio(&self) -> bool {
let extensions = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
self.has_extension(&extensions)
}
fn is_video(&self) -> bool {
let extensions = ["mp4", "avi", "mov", "mkv", "wmv", "flv", "webm", "m4v"];
self.has_extension(&extensions)
}
fn is_archive(&self) -> bool {
let extensions = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar", "deb", "rpm"];
self.has_extension(&extensions)
}
fn is_document(&self) -> bool {
let extensions = ["pdf", "doc", "docx", "txt", "md", "rst", "odt", "rtf"];
self.has_extension(&extensions)
}
fn has_extension(&self, extensions: &[&str]) -> bool {
if let Some(ext) = self.path.extension().and_then(|e| e.to_str()) {
extensions.contains(&ext.to_lowercase().as_str())
} else {
false }
}
}
pub async fn list_directory(path: &Path, cli: &Cli) -> Result<Vec<FileEntry>> {
if !path.exists() {
return Err(LsPlusError::path_not_found(path.to_path_buf()));
}
if !path.is_dir() {
let entry = FileEntry::from_path(path, cli.follow_links)?;
return Ok(vec![entry]);
}
let mut entries = Vec::new();
let dir_entries = fs::read_dir(path).map_err(|e| match e.kind() {
std::io::ErrorKind::PermissionDenied => LsPlusError::permission_denied(path.to_path_buf()),
_ => LsPlusError::system_error(format!("Failed to read directory {path:?}: {e}")),
})?;
for entry in dir_entries {
let entry = entry.map_err(|e| {
LsPlusError::system_error(format!("Failed to read directory entry: {e}"))
})?;
let path = entry.path();
if let Ok(file_entry) = FileEntry::from_path(&path, cli.follow_links) {
if !cli.all && file_entry.is_hidden {
continue;
}
if let Some(ref filter) = cli.file_type {
if !file_entry.matches_filter(filter) {
continue;
}
}
entries.push(file_entry);
}
}
sort_entries(&mut entries, cli);
Ok(entries)
}
pub async fn list_directory_recursive(
path: &Path,
cli: &Cli,
max_depth: Option<usize>,
) -> Result<Vec<FileEntry>> {
if !path.exists() {
return Err(LsPlusError::path_not_found(path.to_path_buf()));
}
let mut entries = Vec::new();
let walker = if let Some(depth) = max_depth {
WalkDir::new(path).max_depth(depth) } else {
WalkDir::new(path) };
for entry in walker {
let entry = entry.map_err(|e| LsPlusError::system_error(format!("Walk error: {e}")))?;
let path = entry.path();
if let Ok(file_entry) = FileEntry::from_path(path, cli.follow_links) {
if !cli.all && file_entry.is_hidden {
continue;
}
if let Some(ref filter) = cli.file_type {
if !file_entry.matches_filter(filter) {
continue;
}
}
entries.push(file_entry);
}
}
sort_entries(&mut entries, cli);
Ok(entries)
}
pub async fn find_files(cli: &Cli) -> Result<Vec<FileEntry>> {
list_directory(&cli.path, cli).await
}
fn sort_entries(entries: &mut [FileEntry], cli: &Cli) {
if cli.directories_first {
entries.sort_by(|a, b| {
match (&a.file_type, &b.file_type) {
(EntryType::Directory, EntryType::Directory) => compare_entries(a, b, cli),
(EntryType::Directory, _) => std::cmp::Ordering::Less,
(_, EntryType::Directory) => std::cmp::Ordering::Greater,
_ => compare_entries(a, b, cli),
}
});
} else {
entries.sort_by(|a, b| compare_entries(a, b, cli));
}
if cli.reverse {
entries.reverse();
}
}
fn compare_entries(a: &FileEntry, b: &FileEntry, cli: &Cli) -> std::cmp::Ordering {
if cli.time {
a.modified.cmp(&b.modified)
} else {
a.name.to_lowercase().cmp(&b.name.to_lowercase())
}
}
fn format_permissions(metadata: &fs::Metadata) -> String {
if metadata.is_dir() {
"drwxr-xr-x".to_string() } else if metadata.is_file() {
"rw-r--r--".to_string() } else {
"lrwxrwxrwx".to_string() }
}