Skip to content

Commit 5c8f4e7

Browse files
committed
chore: ignore os metadata files
1 parent 6ab8ee3 commit 5c8f4e7

File tree

4 files changed

+86
-16
lines changed

4 files changed

+86
-16
lines changed

src/fs_service.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod file_info;
22
pub mod utils;
33
use crate::{
44
error::{ServiceError, ServiceResult},
5+
fs_service::utils::is_system_metadata_file,
56
tools::EditOperation,
67
};
78
use async_zip::tokio::{read::seek::ZipFileReader, write::ZipFileWriter};
@@ -1398,9 +1399,10 @@ impl FileSystemService {
13981399

13991400
/// Recursively finds all empty directories within the given root path.
14001401
///
1401-
/// A directory is considered empty if it contains no files or subdirectories.
1402-
/// You can optionally provide a list of glob-style patterns in `exclude_patterns`
1403-
/// to ignore certain paths during the search (e.g., to skip system folders or hidden directories).
1402+
/// A directory is considered empty if it contains no files in itself or any of its subdirectories
1403+
/// except OS metadata files: `.DS_Store` (macOS) and `Thumbs.db` (Windows)
1404+
/// Empty subdirectories are allowed. You can optionally provide a list of glob-style patterns in
1405+
/// `exclude_patterns` to ignore certain paths during the search (e.g., to skip system folders or hidden directories).
14041406
///
14051407
/// # Arguments
14061408
/// - `root_path`: The starting directory to search.
@@ -1411,7 +1413,23 @@ impl FileSystemService {
14111413
/// Returns an error if the root path is invalid or inaccessible.
14121414
///
14131415
/// # Returns
1414-
/// A list of paths to empty directories, relative to the root path.
1416+
/// A list of paths to empty directories, as strings, including parent directories that contain only empty subdirectories.
1417+
/// Recursively finds all empty directories within the given root path.
1418+
///
1419+
/// A directory is considered empty if it contains no files in itself or any of its subdirectories.
1420+
/// Empty subdirectories are allowed. You can optionally provide a list of glob-style patterns in
1421+
/// `exclude_patterns` to ignore certain paths during the search (e.g., to skip system folders or hidden directories).
1422+
///
1423+
/// # Arguments
1424+
/// - `root_path`: The starting directory to search.
1425+
/// - `exclude_patterns`: Optional list of glob patterns to exclude from the search.
1426+
/// Directories matching these patterns will be ignored.
1427+
///
1428+
/// # Errors
1429+
/// Returns an error if the root path is invalid or inaccessible.
1430+
///
1431+
/// # Returns
1432+
/// A list of paths to all empty directories, as strings, including parent directories that contain only empty subdirectories.
14151433
pub async fn find_empty_directories(
14161434
&self,
14171435
root_path: &Path,
@@ -1433,11 +1451,9 @@ impl FileSystemService {
14331451
// Check each directory for emptiness
14341452
for entry in walker {
14351453
let is_empty = WalkDir::new(entry.path())
1436-
.min_depth(1)
1437-
.max_depth(1)
14381454
.into_iter()
1439-
.next()
1440-
.is_none(); // Directory is empty if it has no entries
1455+
.filter_map(|e| e.ok())
1456+
.all(|e| !e.file_type().is_file() || is_system_metadata_file(&e.file_name())); // Directory is empty if no files are found in it or subdirs, ".DS_Store" will be ignores on Mac
14411457

14421458
if is_empty {
14431459
if let Some(path_str) = entry.path().to_str() {

src/fs_service/utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::os::unix::fs::PermissionsExt;
77
#[cfg(windows)]
88
use std::os::windows::fs::MetadataExt;
99
use std::{
10+
ffi::OsStr,
1011
fs::{self},
1112
path::{Component, Path, PathBuf, Prefix},
1213
time::SystemTime,
@@ -145,3 +146,14 @@ pub fn contains_symlink<P: AsRef<Path>>(path: P) -> std::io::Result<bool> {
145146

146147
Ok(false)
147148
}
149+
150+
/// Checks if a given filename is a system metadata file commonly
151+
/// used by operating systems to store folder metadata.
152+
///
153+
/// Specifically detects:
154+
/// - `.DS_Store` (macOS)
155+
/// - `Thumbs.db` (Windows)
156+
///
157+
pub fn is_system_metadata_file(filename: &OsStr) -> bool {
158+
filename == ".DS_Store" || filename == "Thumbs.db"
159+
}

src/tools/find_duplicate_files.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub struct FindDuplicateFilesTool {
3333
/// Maximum file size (in bytes) to include in the search (optional).
3434
pub max_bytes: Option<u64>,
3535
/// Specify the output format, accepts either `text` or `json` (default: text).
36-
pub output_format: OutputFormat,
36+
pub output_format: Option<OutputFormat>,
3737
}
3838

3939
impl FindDuplicateFilesTool {
@@ -90,8 +90,11 @@ impl FindDuplicateFilesTool {
9090
.await
9191
.map_err(CallToolError::new)?;
9292

93-
let result_content = Self::format_output(duplicate_files, params.output_format)
94-
.map_err(CallToolError::new)?;
93+
let result_content = Self::format_output(
94+
duplicate_files,
95+
params.output_format.unwrap_or(OutputFormat::Text),
96+
)
97+
.map_err(CallToolError::new)?;
9598

9699
Ok(CallToolResult::text_content(vec![TextContent::from(
97100
result_content,

src/tools/find_empty_directories.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
use std::path::Path;
2-
31
use rust_mcp_sdk::{
42
macros::{JsonSchema, mcp_tool},
53
schema::{CallToolResult, TextContent, schema_utils::CallToolError},
64
};
5+
use std::fmt::Write;
6+
use std::path::Path;
77

8-
use crate::fs_service::{FileSystemService, OS_LINE_ENDING};
8+
use crate::fs_service::{FileSystemService, utils::OutputFormat};
99

1010
// head_file
1111
#[mcp_tool(
1212
name = "find_empty_directories",
1313
title="Find Empty Directories",
1414
description = concat!("Recursively finds all empty directories within the given root path.",
15-
"A directory is considered empty if it contains no files or subdirectories.",
15+
"A directory is considered empty if it contains no files in itself or any of its subdirectories.",
16+
"Operating system metadata files `.DS_Store` (macOS) and `Thumbs.db` (Windows) will be ignored.",
1617
"The optional exclude_patterns argument accepts glob-style patterns to exclude specific paths from the search.",
1718
"Only works within allowed directories."),
1819
destructive_hint = false,
@@ -26,6 +27,8 @@ pub struct FindEmptyDirectoriesTool {
2627
pub path: String,
2728
/// Optional list of glob patterns to exclude from the search. Directories matching these patterns will be ignored.
2829
pub exclude_patterns: Option<Vec<String>>,
30+
/// Specify the output format, accepts either `text` or `json` (default: text).
31+
pub output_format: Option<OutputFormat>,
2932
}
3033

3134
impl FindEmptyDirectoriesTool {
@@ -37,9 +40,45 @@ impl FindEmptyDirectoriesTool {
3740
.find_empty_directories(&Path::new(&params.path), params.exclude_patterns)
3841
.await
3942
.map_err(CallToolError::new)?;
40-
let content = result.join(OS_LINE_ENDING);
43+
44+
let content =
45+
Self::format_output(result, params.output_format.unwrap_or(OutputFormat::Text))
46+
.map_err(CallToolError::new)?;
47+
4148
Ok(CallToolResult::text_content(vec![TextContent::from(
4249
content,
4350
)]))
4451
}
52+
53+
fn format_output(
54+
empty_dirs: Vec<String>,
55+
output_format: OutputFormat,
56+
) -> std::result::Result<String, CallToolError> {
57+
let output = match output_format {
58+
OutputFormat::Text => {
59+
let mut output = String::new();
60+
61+
let header = if empty_dirs.len() == 0 {
62+
"No empty directories were found.".to_string()
63+
} else {
64+
format!(
65+
"Found {} empty directorie{}:\n",
66+
empty_dirs.len(),
67+
(if empty_dirs.len() == 1 { "" } else { "s" }),
68+
)
69+
};
70+
output.push_str(&header);
71+
72+
for dir in empty_dirs {
73+
writeln!(output, " {}", dir).map_err(CallToolError::new)?;
74+
}
75+
output
76+
}
77+
OutputFormat::Json => {
78+
serde_json::to_string_pretty(&empty_dirs).map_err(CallToolError::new)?
79+
}
80+
};
81+
82+
Ok(output)
83+
}
4584
}

0 commit comments

Comments
 (0)