1use std::path::Path;
4use std::time::SystemTime;
5
6use anyhow::{Context, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum PathKind {
11 NotFound,
12 File(u64),
13 Dir,
14}
15
16pub fn read_dir(path: &Path) -> Result<Vec<(std::path::PathBuf, bool)>> {
18 let mut entries: Vec<_> = std::fs::read_dir(path)
19 .with_context(|| format!("Failed to read dir: {}", path.display()))?
20 .filter_map(|e| e.ok())
21 .map(|e| {
22 let p = e.path();
23 (p.clone(), p.is_dir())
24 })
25 .collect();
26 entries.sort_by_key(|(p, _)| p.file_name().unwrap_or_default().to_owned());
27 Ok(entries)
28}
29
30pub fn create_dir_all(path: &Path) -> Result<()> {
32 std::fs::create_dir_all(path)
33 .with_context(|| format!("Failed to create dir: {}", path.display()))
34}
35
36pub fn copy(from: &Path, to: &Path) -> Result<u64> {
38 std::fs::copy(from, to)
39 .with_context(|| format!("Failed to copy {} -> {}", from.display(), to.display()))
40}
41
42pub fn rename(from: &Path, to: &Path) -> Result<()> {
44 std::fs::rename(from, to)
45 .with_context(|| format!("Failed to rename {} -> {}", from.display(), to.display()))
46}
47
48pub fn remove_file(path: &Path) -> Result<()> {
50 std::fs::remove_file(path).with_context(|| format!("Failed to remove file: {}", path.display()))
51}
52
53pub fn modified_time(path: &Path) -> Result<SystemTime> {
55 std::fs::metadata(path)
56 .and_then(|m| m.modified())
57 .with_context(|| format!("Failed to get modified time: {}", path.display()))
58}
59
60pub fn file_exists(path: &Path) -> Result<PathKind> {
62 match std::fs::metadata(path) {
63 Ok(meta) => {
64 if meta.is_dir() {
65 Ok(PathKind::Dir)
66 } else {
67 Ok(PathKind::File(meta.len()))
68 }
69 }
70 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(PathKind::NotFound),
71 Err(e) => Err(e.into()),
72 }
73}
74
75fn format_size(bytes: u64) -> String {
76 if bytes < 1024 {
77 format!("{} B", bytes)
78 } else if bytes < 1024 * 1024 {
79 format!("{} KB", bytes / 1024)
80 } else if bytes < 1024 * 1024 * 1024 {
81 format!("{} MB", bytes / (1024 * 1024))
82 } else {
83 format!("{} GB", bytes / (1024 * 1024 * 1024))
84 }
85}
86
87pub fn list_directory(path: &Path, recursive: bool) -> Result<Vec<String>> {
89 if !path.exists() {
90 anyhow::bail!("Directory not found: {}", path.display());
91 }
92 if !path.is_dir() {
93 anyhow::bail!("Path is not a directory: {}", path.display());
94 }
95 let mut entries = Vec::new();
96 list_dir_impl(path, path, recursive, &mut entries, 0)?;
97 Ok(entries)
98}
99
100fn list_dir_impl(
101 base: &Path,
102 current: &Path,
103 recursive: bool,
104 entries: &mut Vec<String>,
105 depth: usize,
106) -> Result<()> {
107 let skip_dirs = [
108 "node_modules",
109 "__pycache__",
110 ".git",
111 "venv",
112 ".venv",
113 ".tox",
114 "target",
115 ];
116
117 let mut items: Vec<_> = std::fs::read_dir(current)
118 .with_context(|| format!("Failed to read dir: {}", current.display()))?
119 .filter_map(|e| e.ok())
120 .collect();
121 items.sort_by_key(|e| e.file_name());
122
123 for entry in items {
124 let name = entry.file_name().to_string_lossy().to_string();
125 let is_dir = entry.path().is_dir();
126
127 let rel = entry
128 .path()
129 .strip_prefix(base)
130 .unwrap_or(entry.path().as_path())
131 .to_string_lossy()
132 .to_string();
133
134 if name.starts_with('.') && depth == 0 && name != "." {
135 let prefix = if is_dir { "📁 " } else { " " };
136 entries.push(format!("{}{}", prefix, name));
137 continue;
138 }
139
140 if is_dir {
141 entries.push(format!("📁 {}/", rel));
142 if recursive && !skip_dirs.contains(&name.as_str()) {
143 list_dir_impl(base, &entry.path(), true, entries, depth + 1)?;
144 }
145 } else {
146 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
147 entries.push(format!(" {} ({})", rel, format_size(size)));
148 }
149 }
150 Ok(())
151}