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