Skip to main content

skilllite_fs/
dir.rs

1//! 目录与路径操作
2
3use std::path::Path;
4use std::time::SystemTime;
5
6use anyhow::{Context, Result};
7
8/// 路径类型
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum PathKind {
11    NotFound,
12    File(u64),
13    Dir,
14}
15
16/// 读取目录条目,返回 (完整路径, 是否目录),已排序
17pub 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
30/// 确保目录存在
31pub 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
36/// 复制文件
37pub 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
42/// 重命名/移动
43pub 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
48/// 删除文件
49pub fn remove_file(path: &Path) -> Result<()> {
50    std::fs::remove_file(path).with_context(|| format!("Failed to remove file: {}", path.display()))
51}
52
53/// 获取修改时间
54pub 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
60/// 检查路径是否存在及类型
61pub 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
87/// 列出目录内容,返回排序后的条目
88pub 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}