Initial upload: DotMigrate dotfiles migration tool with CI/CD
This commit is contained in:
273
src/detect/mod.rs
Normal file
273
src/detect/mod.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use crate::config::Config;
|
||||
use crate::cli::Platform;
|
||||
use glob::glob;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
use sha2::{Sha256, Digest};
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DetectedDotfile {
|
||||
pub path: PathBuf,
|
||||
pub content_hash: Option<String>,
|
||||
pub permissions: Option<u32>,
|
||||
pub size: u64,
|
||||
pub platform: Platform,
|
||||
}
|
||||
|
||||
impl DetectedDotfile {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
let (hash, perms, size) = Self::get_file_info(&path);
|
||||
Self {
|
||||
path,
|
||||
content_hash: hash,
|
||||
permissions: perms,
|
||||
size,
|
||||
platform: Platform::current(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_info(path: &PathBuf) -> (Option<String>, Option<u32>, u64) {
|
||||
let metadata = match fs::metadata(path) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return (None, None, 0),
|
||||
};
|
||||
|
||||
let size = metadata.len();
|
||||
|
||||
let perms = if metadata.permissions().readonly() {
|
||||
Some(0o444)
|
||||
} else {
|
||||
Some(0o644)
|
||||
};
|
||||
|
||||
let mut file = match fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return (None, perms, size),
|
||||
};
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = Vec::new();
|
||||
match file.read_to_end(&mut buffer) {
|
||||
Ok(_) => {
|
||||
hasher.update(&buffer);
|
||||
let hash = format!("{:x}", hasher.finalize());
|
||||
(Some(hash), perms, size)
|
||||
}
|
||||
Err(_) => (None, perms, size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Detector<'a> {
|
||||
config: &'a Config,
|
||||
home_dir: PathBuf,
|
||||
platform: Platform,
|
||||
}
|
||||
|
||||
impl<'a> Detector<'a> {
|
||||
pub fn new(config: &'a Config) -> Self {
|
||||
let home_dir = dirs::home_dir()
|
||||
.expect("Home directory not found");
|
||||
let platform = Platform::current();
|
||||
|
||||
Self {
|
||||
config,
|
||||
home_dir,
|
||||
platform,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_dotfiles(&self, include_system: bool) -> Result<Vec<DetectedDotfile>> {
|
||||
let mut detected: HashSet<PathBuf> = HashSet::new();
|
||||
|
||||
if include_system {
|
||||
self.scan_system_directories(&mut detected)
|
||||
.context("Failed to scan system directories")?;
|
||||
}
|
||||
|
||||
self.scan_home_directory(&mut detected)
|
||||
.context("Failed to scan home directory")?;
|
||||
|
||||
self.scan_config_patterns(&mut detected)
|
||||
.context("Failed to scan config patterns")?;
|
||||
|
||||
let mut result: Vec<DetectedDotfile> = detected
|
||||
.into_iter()
|
||||
.filter(|p| self.should_include(p))
|
||||
.map(DetectedDotfile::new)
|
||||
.collect();
|
||||
|
||||
result.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn scan_home_directory(&self, detected: &mut HashSet<PathBuf>) -> Result<()> {
|
||||
let walker = WalkDir::new(&self.home_dir)
|
||||
.max_depth(self.config.detect.scan_depth)
|
||||
.follow_links(false);
|
||||
|
||||
for entry in walker {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() {
|
||||
let relative = self.get_relative_path(path);
|
||||
if self.is_hidden_or_config(&relative) {
|
||||
detected.insert(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_system_directories(&self, detected: &mut HashSet<PathBuf>) -> Result<()> {
|
||||
let system_dirs: Vec<PathBuf> = match self.platform {
|
||||
Platform::Linux | Platform::Wsl => vec![
|
||||
PathBuf::from("/etc"),
|
||||
PathBuf::from("/usr/local"),
|
||||
],
|
||||
Platform::Macos => vec![
|
||||
PathBuf::from("/etc"),
|
||||
PathBuf::from("/usr/local"),
|
||||
PathBuf::from("/Library/Application Support"),
|
||||
],
|
||||
};
|
||||
|
||||
for dir in &system_dirs {
|
||||
if dir.exists() {
|
||||
let walker = WalkDir::new(dir)
|
||||
.max_depth(3)
|
||||
.follow_links(false);
|
||||
|
||||
for entry in walker {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() {
|
||||
let relative = path.strip_prefix("/")
|
||||
.unwrap_or_else(|| path)
|
||||
.to_path_buf();
|
||||
if self.is_hidden_or_config(&relative) {
|
||||
detected.insert(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_config_patterns(&self, detected: &mut HashSet<PathBuf>) -> Result<()> {
|
||||
for pattern in &self.config.detect.patterns {
|
||||
let full_pattern = self.home_dir.join(pattern);
|
||||
if let Some(pattern_str) = full_pattern.to_str() {
|
||||
for entry in glob(pattern_str)? {
|
||||
match entry {
|
||||
Ok(path) => {
|
||||
if path.is_file() {
|
||||
detected.insert(path);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Glob pattern error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_hidden_or_config(&self, path: &Path) -> bool {
|
||||
if let Some(name) = path.file_name() {
|
||||
if name.to_string_lossy().starts_with('.') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let path_str = path.to_string_lossy();
|
||||
if path_str.contains(".config/") || path_str.contains(".local/") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.platform == Platform::Macos {
|
||||
if path_str.contains("Library/Application Support/") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn get_relative_path(&self, path: &Path) -> PathBuf {
|
||||
if let Ok(rel) = path.strip_prefix(&self.home_dir) {
|
||||
rel.to_path_buf()
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
fn should_include(&self, path: &Path) -> bool {
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
for exclude in &self.config.detect.exclude_patterns {
|
||||
if path_str.contains(exclude) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
pub fn current() -> Self {
|
||||
match std::env::consts::OS {
|
||||
"linux" => {
|
||||
if std::env::var("WSL_DISTRO_NAME").is_ok() {
|
||||
Platform::Wsl
|
||||
} else {
|
||||
Platform::Linux
|
||||
}
|
||||
}
|
||||
"macos" => Platform::Macos,
|
||||
"windows" => Platform::Wsl,
|
||||
_ => Platform::Linux,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_dir(&self) -> PathBuf {
|
||||
match self {
|
||||
Platform::Linux => dirs::home_dir()
|
||||
.map(|p| p.join(".config"))
|
||||
.unwrap_or_else(|| PathBuf::from(".config")),
|
||||
Platform::Macos => dirs::home_dir()
|
||||
.map(|p| p.join("Library/Application Support"))
|
||||
.unwrap_or_else(|| PathBuf::from("Library/Application Support")),
|
||||
Platform::Wsl => dirs::home_dir()
|
||||
.map(|p| p.join(".config"))
|
||||
.unwrap_or_else(|| PathBuf::from(".config")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_dir(&self) -> PathBuf {
|
||||
match self {
|
||||
Platform::Linux => dirs::home_dir()
|
||||
.map(|p| p.join(".local/share"))
|
||||
.unwrap_or_else(|| PathBuf::from(".local/share")),
|
||||
Platform::Macos => dirs::home_dir()
|
||||
.map(|p| p.join("Library/Application Support"))
|
||||
.unwrap_or_else(|| PathBuf::from("Library/Application Support")),
|
||||
Platform::Wsl => dirs::home_dir()
|
||||
.map(|p| p.join(".local/share"))
|
||||
.unwrap_or_else(|| PathBuf::from(".local/share")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user