use std::{ collections::HashSet, fs::{self, OpenOptions}, path::{Path, PathBuf}, }; use serde::Serialize; use walkdir::{DirEntry, WalkDir}; const DEPTH: usize = 5; #[derive(Debug, Serialize)] pub struct Directory { name: String, path: String, parent: Option, is_root: bool, } fn is_readable_and_writable(path: &Path) -> bool { if fs::read_dir(path).is_err() { return false; } let test_file = path.join(".test_rw_check"); match OpenOptions::new() .write(true) .create_new(true) .open(&test_file) { Ok(_) => { let _ = fs::remove_file(&test_file); true } Err(_) => false, } } fn is_hidden(entry: &DirEntry) -> bool { let file_name_hidden = entry .file_name() .to_str() .map(|s| s.starts_with('.')) .unwrap_or(false); #[cfg(windows)] { use std::os::windows::fs::MetadataExt; use winapi::um::winnt::FILE_ATTRIBUTE_HIDDEN; if let Ok(metadata) = entry.metadata() { let attrs = metadata.file_attributes(); return file_name_hidden || (attrs & FILE_ATTRIBUTE_HIDDEN != 0); } } file_name_hidden } pub fn get_readable_writable_dirs(root: &Path) -> Vec { let mut dirs = Vec::new(); let mut all_paths = HashSet::new(); for entry in WalkDir::new(root) .follow_links(false) .max_depth(DEPTH) .into_iter() .filter_entry(|e| !is_hidden(e)) .filter_map(Result::ok) .filter(|e| e.file_type().is_dir()) { let path = entry.path(); if is_readable_and_writable(path) { all_paths.insert(path.to_path_buf()); } } for path in &all_paths { let parent = path.parent().map(|p| p.to_string_lossy().to_string()); let is_root = parent .as_ref() .map(|p| !all_paths.contains(&PathBuf::from(p))) .unwrap_or(true); dirs.push(Directory { name: path .file_name() .map(|f| f.to_string_lossy().to_string()) .unwrap_or_else(|| "/".to_string()), path: path.to_string_lossy().to_string(), parent, is_root, }); } dirs }