Add file operations and UI components for file management application
This commit is contained in:
267
src/file_ops.rs
Normal file
267
src/file_ops.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FileType {
|
||||
Directory,
|
||||
File,
|
||||
Symlink,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileEntry {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub file_type: FileType,
|
||||
pub size: u64,
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
impl FileEntry {
|
||||
pub fn icon(&self) -> &'static str {
|
||||
match &self.file_type {
|
||||
FileType::Directory => "📁",
|
||||
FileType::Symlink => "🔗",
|
||||
FileType::File => {
|
||||
let ext = self
|
||||
.name
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
match ext.as_str() {
|
||||
"rs" => "🦀",
|
||||
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "🖼",
|
||||
"mp4" | "mkv" | "avi" | "mov" | "webm" => "🎬",
|
||||
"mp3" | "flac" | "ogg" | "wav" | "aac" => "🎵",
|
||||
"zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "📦",
|
||||
"pdf" => "📕",
|
||||
"txt" | "md" | "log" => "📄",
|
||||
"json" | "yaml" | "yml" | "toml" | "xml" => "⚙",
|
||||
"sh" | "bash" | "zsh" | "fish" => "🐚",
|
||||
"py" => "🐍",
|
||||
"js" | "ts" => "📜",
|
||||
"c" | "cpp" | "h" | "hpp" => "🔧",
|
||||
"exe" | "bin" | "out" | "run" => "⚙",
|
||||
_ => "📎",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dir(&self) -> bool {
|
||||
self.file_type == FileType::Directory
|
||||
}
|
||||
|
||||
pub fn formatted_size(&self) -> String {
|
||||
if self.file_type == FileType::Directory {
|
||||
return String::from("<dir>");
|
||||
}
|
||||
format_size(self.size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_size(bytes: u64) -> String {
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = 1024 * KB;
|
||||
const GB: u64 = 1024 * MB;
|
||||
|
||||
if bytes >= GB {
|
||||
format!("{:.1} GB", bytes as f64 / GB as f64)
|
||||
} else if bytes >= MB {
|
||||
format!("{:.1} MB", bytes as f64 / MB as f64)
|
||||
} else if bytes >= KB {
|
||||
format!("{} KB", bytes / KB)
|
||||
} else {
|
||||
format!("{} B", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_dir(path: &Path) -> Vec<FileEntry> {
|
||||
let mut entries = Vec::new();
|
||||
let Ok(dir) = fs::read_dir(path) else {
|
||||
return entries;
|
||||
};
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let path = entry.path();
|
||||
let is_hidden = name.starts_with('.');
|
||||
|
||||
let (file_type, size) = if let Ok(meta) = entry.metadata() {
|
||||
let ft = if meta.is_symlink() {
|
||||
FileType::Symlink
|
||||
} else if meta.is_dir() {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
(ft, meta.len())
|
||||
} else {
|
||||
(FileType::File, 0)
|
||||
};
|
||||
|
||||
entries.push(FileEntry { name, path, file_type, size, is_hidden });
|
||||
}
|
||||
|
||||
// Sort: dirs first, then files, both alphabetically
|
||||
entries.sort_by(|a, b| {
|
||||
match (&a.file_type, &b.file_type) {
|
||||
(FileType::Directory, FileType::Directory) => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||
(FileType::Directory, _) => std::cmp::Ordering::Less,
|
||||
(_, FileType::Directory) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||
}
|
||||
});
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
/// Recursively compute total size and file count of a path
|
||||
pub fn recursive_info(path: &Path) -> (u64, u64) {
|
||||
let mut total_size: u64 = 0;
|
||||
let mut file_count: u64 = 0;
|
||||
|
||||
if path.is_file() {
|
||||
let size = path.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
return (size, 1);
|
||||
}
|
||||
|
||||
if let Ok(dir) = fs::read_dir(path) {
|
||||
for entry in dir.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_dir() {
|
||||
let (s, c) = recursive_info(&p);
|
||||
total_size += s;
|
||||
file_count += c;
|
||||
} else {
|
||||
total_size += p.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(total_size, file_count)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn file_permissions(path: &Path) -> String {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = path.metadata().map(|m| m.permissions().mode()).unwrap_or(0);
|
||||
format!("{:o}", mode & 0o777)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn file_permissions(_path: &Path) -> String {
|
||||
String::from("N/A")
|
||||
}
|
||||
|
||||
pub fn file_owner(path: &Path) -> (String, String) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
if let Ok(meta) = path.metadata() {
|
||||
let uid = meta.uid();
|
||||
let gid = meta.gid();
|
||||
let user_name = lookup_name("/etc/passwd", uid)
|
||||
.map(|n| format!("{} ({})", n, uid))
|
||||
.unwrap_or_else(|| uid.to_string());
|
||||
let group_name = lookup_name("/etc/group", gid)
|
||||
.map(|n| format!("{} ({})", n, gid))
|
||||
.unwrap_or_else(|| gid.to_string());
|
||||
return (user_name, group_name);
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = path;
|
||||
}
|
||||
(String::from("N/A"), String::from("N/A"))
|
||||
}
|
||||
|
||||
/// Parse /etc/passwd or /etc/group and return the name for a given numeric id.
|
||||
/// Both files share the same format: `name:x:id:...`
|
||||
#[cfg(unix)]
|
||||
fn lookup_name(db_file: &str, id: u32) -> Option<String> {
|
||||
let content = std::fs::read_to_string(db_file).ok()?;
|
||||
for line in content.lines() {
|
||||
let mut parts = line.splitn(4, ':');
|
||||
let name = parts.next()?;
|
||||
parts.next(); // password/placeholder
|
||||
let entry_id: u32 = parts.next()?.parse().ok()?;
|
||||
if entry_id == id {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Copy a file or directory recursively
|
||||
pub fn copy_entry(src: &Path, dst_dir: &Path) -> Result<(), String> {
|
||||
let name = src.file_name().ok_or("No filename")?;
|
||||
let dst = dst_dir.join(name);
|
||||
|
||||
if src.is_dir() {
|
||||
fs_extra::dir::copy(
|
||||
src,
|
||||
dst_dir,
|
||||
&fs_extra::dir::CopyOptions::new(),
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
fs::copy(src, &dst).map(|_| ()).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Move (cut/paste) a file or directory
|
||||
pub fn move_entry(src: &Path, dst_dir: &Path) -> Result<(), String> {
|
||||
let name = src.file_name().ok_or("No filename")?;
|
||||
let dst = dst_dir.join(name);
|
||||
|
||||
if src.is_dir() {
|
||||
fs_extra::dir::move_dir(
|
||||
src,
|
||||
dst_dir,
|
||||
&fs_extra::dir::CopyOptions::new(),
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
fs::rename(src, &dst).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_entry(path: &Path) -> Result<(), String> {
|
||||
if path.is_dir() {
|
||||
fs::remove_dir_all(path).map_err(|e| e.to_string())
|
||||
} else {
|
||||
fs::remove_file(path).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_root_dirs() -> Vec<PathBuf> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let roots = ["/", "/home", "/etc", "/usr", "/var", "/opt", "/tmp"];
|
||||
roots
|
||||
.iter()
|
||||
.filter_map(|r| {
|
||||
let p = PathBuf::from(r);
|
||||
if p.exists() { Some(p) } else { None }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut drives = Vec::new();
|
||||
for letter in b'A'..=b'Z' {
|
||||
let d = format!("{}:\\", letter as char);
|
||||
let p = PathBuf::from(&d);
|
||||
if p.exists() {
|
||||
drives.push(p);
|
||||
}
|
||||
}
|
||||
drives
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user