refactor from golang by cursor.
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "empty-dir"
|
name = "empty-dir"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "empty-dir"
|
name = "empty-dir"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
authors = ["licsber <admin@licsber.site>"]
|
authors = ["licsber <admin@licsber.site>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -1,3 +1,21 @@
|
|||||||
# empty-dir
|
# empty-dir
|
||||||
|
|
||||||
Remove empty dirs.
|
一个用于清理空目录和 macOS 元数据文件(`.DS_Store`、`._*`)的极简 CLI。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install empty-dir
|
||||||
|
# or
|
||||||
|
cargo run -- <需要清理的目录>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 若未传参,则默认清理当前目录。
|
||||||
|
- 仅会删除完全为空的子目录。
|
||||||
|
- 符合条件的 macOS 元数据文件会被直接删除。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo publish
|
||||||
|
```
|
||||||
|
|||||||
64
src/cleaner.rs
Normal file
64
src/cleaner.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use std::{fs, io, path::Path};
|
||||||
|
|
||||||
|
use crate::{logger::log_error, walker::Walker};
|
||||||
|
|
||||||
|
pub struct Cleaner {
|
||||||
|
walker: Walker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cleaner {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
walker: Walker::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cleaner {
|
||||||
|
pub fn clean(&self, path: &Path) -> bool {
|
||||||
|
match self.run(path) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(err) => {
|
||||||
|
log_error(path, &err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, path: &Path) -> io::Result<()> {
|
||||||
|
if !ensure_directory(path)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let became_empty = self.walker.prune(path)?;
|
||||||
|
if became_empty {
|
||||||
|
println!("删除空目录 `{}`", path.display());
|
||||||
|
if let Err(err) = fs::remove_dir(path) {
|
||||||
|
if err.kind() != io::ErrorKind::NotFound {
|
||||||
|
log_error(path, &err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_directory(path: &Path) -> io::Result<bool> {
|
||||||
|
match fs::metadata(path) {
|
||||||
|
Ok(metadata) => {
|
||||||
|
if !metadata.is_dir() {
|
||||||
|
eprintln!("路径 `{}` 不是文件夹,已跳过。", path.display());
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if err.kind() == io::ErrorKind::PermissionDenied {
|
||||||
|
eprintln!("无法访问 `{}`:{}", path.display(), err);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/logger.rs
Normal file
5
src/logger.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn log_error(path: &Path, err: &dyn std::error::Error) {
|
||||||
|
eprintln!("处理 `{}` 时发生错误:{}", path.display(), err);
|
||||||
|
}
|
||||||
15
src/mac_meta.rs
Normal file
15
src/mac_meta.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const MAC_META_STR1: &str = "com.apple.quarantine";
|
||||||
|
const MAC_META_STR2: &str = "This resource fork intentionally left blank";
|
||||||
|
|
||||||
|
pub fn is_mac_meta_file(path: &Path) -> bool {
|
||||||
|
match fs::read(path) {
|
||||||
|
Ok(buffer) => {
|
||||||
|
let content = String::from_utf8_lossy(&buffer);
|
||||||
|
content.contains(MAC_META_STR1) || content.contains(MAC_META_STR2)
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main.rs
29
src/main.rs
@@ -1,3 +1,30 @@
|
|||||||
|
mod cleaner;
|
||||||
|
mod logger;
|
||||||
|
mod mac_meta;
|
||||||
|
mod walker;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
|
};
|
||||||
|
|
||||||
|
use cleaner::Cleaner;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
let targets: Vec<PathBuf> = env::args().skip(1).map(PathBuf::from).collect();
|
||||||
|
let cleaner = Cleaner::default();
|
||||||
|
|
||||||
|
let success = if targets.is_empty() {
|
||||||
|
cleaner.clean(Path::new("."))
|
||||||
|
} else {
|
||||||
|
targets
|
||||||
|
.iter()
|
||||||
|
.map(|path| cleaner.clean(path))
|
||||||
|
.all(|result| result)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/walker.rs
Normal file
131
src/walker.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::OsStr,
|
||||||
|
fs::{self, DirEntry, ReadDir},
|
||||||
|
io,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{logger::log_error, mac_meta};
|
||||||
|
|
||||||
|
pub struct Walker;
|
||||||
|
|
||||||
|
impl Walker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune(&self, path: &Path) -> io::Result<bool> {
|
||||||
|
let entries = match self.read_entries(path)? {
|
||||||
|
EntryStream::Entries(entries) => entries,
|
||||||
|
EntryStream::Skip => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut is_empty = true;
|
||||||
|
|
||||||
|
for entry_result in entries {
|
||||||
|
if !self.process_entry(path, entry_result) {
|
||||||
|
is_empty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(is_empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_entries(&self, path: &Path) -> io::Result<EntryStream> {
|
||||||
|
match fs::read_dir(path) {
|
||||||
|
Ok(entries) => Ok(EntryStream::Entries(entries)),
|
||||||
|
Err(err) => {
|
||||||
|
if err.kind() == io::ErrorKind::PermissionDenied {
|
||||||
|
eprintln!("无法读取 `{}`:{}", path.display(), err);
|
||||||
|
Ok(EntryStream::Skip)
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_entry(&self, parent: &Path, entry_result: io::Result<DirEntry>) -> bool {
|
||||||
|
let entry = match entry_result {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(err) => {
|
||||||
|
log_error(parent, &err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_path = entry.path();
|
||||||
|
let file_type = match entry.file_type() {
|
||||||
|
Ok(file_type) => file_type,
|
||||||
|
Err(err) => {
|
||||||
|
log_error(&child_path, &err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
return self.handle_directory(&child_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_type.is_file() || file_type.is_symlink() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
return self.handle_metadata_file(&name, &child_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_directory(&self, child_path: &Path) -> bool {
|
||||||
|
match self.prune(child_path) {
|
||||||
|
Ok(true) => match fs::remove_dir(child_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("删除空目录 `{}`", child_path.display());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log_error(child_path, &err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(false) => false,
|
||||||
|
Err(err) => {
|
||||||
|
log_error(child_path, &err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_metadata_file(&self, name: &OsStr, path: &Path) -> bool {
|
||||||
|
if !should_remove_file(name, path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("删除元数据文件 `{}`", path.display());
|
||||||
|
match fs::remove_file(path) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(err) => {
|
||||||
|
log_error(path, &err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EntryStream {
|
||||||
|
Entries(ReadDir),
|
||||||
|
Skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_remove_file(name: &OsStr, path: &Path) -> bool {
|
||||||
|
if name == OsStr::new(".DS_Store") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name_str) = name.to_str() {
|
||||||
|
if name_str.starts_with("._") {
|
||||||
|
return mac_meta::is_mac_meta_file(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user