Compare commits
4 Commits
cec1d5409c
...
57105f17f3
Author | SHA1 | Date |
---|---|---|
|
57105f17f3 | |
|
d115716d45 | |
|
8108221bd0 | |
|
6b4d342f3f |
|
@ -140,6 +140,7 @@ dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -168,6 +169,7 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
@ -305,6 +307,12 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
|
@ -457,6 +465,12 @@ version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -486,6 +500,18 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.141"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
|
@ -8,6 +8,7 @@ clap = { version = "4.0", features = ["derive"] }
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
inotify = "0.10"
|
inotify = "0.10"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
walkdir = "2.0"
|
walkdir = "2.0"
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4", features = ["serde"] }
|
23
readme.md
23
readme.md
|
@ -1,6 +1,6 @@
|
||||||
# CARTO - Universal System File and Package Mapper
|
# CARTO - Universal System File and Package Mapper
|
||||||
|
|
||||||
A command-line tool that provides comprehensive observability into your Linux system's package installations and file ownership. Built in Rust for performance and reliability.
|
A command-line tool that provides comprehensive observability into your Linux system's package installations and file ownership. Providing a clear, tracable providence for every executable, and configuration file on your system.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -13,22 +13,27 @@ just install
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
# Show package file tree
|
# Show package file tree
|
||||||
carto tree --package bash
|
`carto tree --package bash`
|
||||||
carto tree --package "Signal Desktop (org.signal.Signal)"
|
|
||||||
|
`carto tree --package "Signal Desktop (org.signal.Signal)"`
|
||||||
|
|
||||||
# Package information
|
# Package information
|
||||||
carto package bash
|
`carto package bash`
|
||||||
|
|
||||||
# File ownership
|
# File ownership
|
||||||
carto file /usr/bin/bash
|
|
||||||
|
`carto file /usr/bin/bash`
|
||||||
|
|
||||||
# List packages
|
# List packages
|
||||||
carto packages
|
`carto packages`
|
||||||
carto packages --source dnf
|
|
||||||
carto packages --source flatpak
|
`carto packages --source dnf`
|
||||||
|
|
||||||
|
`carto packages --source flatpak`
|
||||||
|
|
||||||
# Find files in package
|
# Find files in package
|
||||||
carto find --package bash
|
|
||||||
|
`carto find --package bash`
|
||||||
|
|
||||||
## Example Output
|
## Example Output
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
|
pub mod scan;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
@ -9,11 +10,39 @@ pub struct Cli {
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||||
|
pub enum OutputFormat {
|
||||||
|
Simple,
|
||||||
|
Detailed,
|
||||||
|
Summary,
|
||||||
|
Json,
|
||||||
|
Tree,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Scan system and build/update database
|
|
||||||
Scan {
|
Scan {
|
||||||
#[arg(short, long, help = "Force full rescan")]
|
/// Directories to scan (defaults to /)
|
||||||
|
paths: Vec<String>,
|
||||||
|
|
||||||
|
/// Output format
|
||||||
|
#[arg(short, long, value_enum, default_value = "summary")]
|
||||||
|
format: OutputFormat,
|
||||||
|
|
||||||
|
/// Show detailed file information
|
||||||
|
#[arg(long)]
|
||||||
|
detailed: bool,
|
||||||
|
|
||||||
|
/// Show file sizes
|
||||||
|
#[arg(long)]
|
||||||
|
sizes: bool,
|
||||||
|
|
||||||
|
/// Include expected orphans (/tmp, /var/log)
|
||||||
|
#[arg(long)]
|
||||||
|
include_expected: bool,
|
||||||
|
|
||||||
|
/// Force full rescan
|
||||||
|
#[arg(short = 'F', long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,442 @@
|
||||||
|
use crate::{CartoError, PackageManager};
|
||||||
|
use crate::database::format::{OrphanCategory, ScanSummary};
|
||||||
|
use crate::cli::OutputFormat;
|
||||||
|
use crate::package_managers::find_file_owner;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct OrphanFile {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub category: OrphanCategory,
|
||||||
|
pub size: u64,
|
||||||
|
pub modified: u64,
|
||||||
|
pub created: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ScanResults {
|
||||||
|
pub scan_time: DateTime<Utc>,
|
||||||
|
pub scanned_paths: Vec<String>,
|
||||||
|
pub total_files_scanned: u64,
|
||||||
|
pub total_orphans: usize,
|
||||||
|
pub orphans_by_category: HashMap<String, Vec<OrphanFile>>,
|
||||||
|
pub total_orphan_size: u64,
|
||||||
|
pub scan_duration_seconds: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScanResults {
|
||||||
|
pub fn new(scanned_paths: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
scan_time: Utc::now(),
|
||||||
|
scanned_paths,
|
||||||
|
total_files_scanned: 0,
|
||||||
|
total_orphans: 0,
|
||||||
|
orphans_by_category: HashMap::new(),
|
||||||
|
total_orphan_size: 0,
|
||||||
|
scan_duration_seconds: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Scanner<'a> {
|
||||||
|
managers: &'a [Box<dyn PackageManager>],
|
||||||
|
exclude_paths: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
impl<'a> Scanner<'a> {
|
||||||
|
pub fn new(managers: &'a [Box<dyn PackageManager>]) -> Self {
|
||||||
|
let exclude_paths = vec![
|
||||||
|
// Existing exclusions
|
||||||
|
PathBuf::from("/proc"),
|
||||||
|
PathBuf::from("/sys"),
|
||||||
|
PathBuf::from("/dev"),
|
||||||
|
PathBuf::from("/run"),
|
||||||
|
PathBuf::from("/var/run"),
|
||||||
|
PathBuf::from("/tmp/.X11-unix"),
|
||||||
|
PathBuf::from("/tmp/.ICE-unix"),
|
||||||
|
|
||||||
|
PathBuf::from("/var/lib/containers"),
|
||||||
|
PathBuf::from("/var/lib/docker"),
|
||||||
|
PathBuf::from("/home/.local/share/containers"), // Your specific issue
|
||||||
|
PathBuf::from("/var/lib/flatpak"),
|
||||||
|
PathBuf::from("/var/cache"),
|
||||||
|
PathBuf::from("/var/tmp"),
|
||||||
|
PathBuf::from("/tmp"),
|
||||||
|
PathBuf::from("/var/log"),
|
||||||
|
PathBuf::from("/var/spool"),
|
||||||
|
PathBuf::from("/boot"),
|
||||||
|
PathBuf::from("/usr/src"), // Kernel sources
|
||||||
|
PathBuf::from("/home"), // User data isn't "orphaned"
|
||||||
|
PathBuf::from("/root"),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
managers,
|
||||||
|
exclude_paths,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan_paths(
|
||||||
|
&self,
|
||||||
|
paths: &[String],
|
||||||
|
include_expected: bool,
|
||||||
|
) -> Result<ScanResults, CartoError> {
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
let scan_paths = if paths.is_empty() {
|
||||||
|
vec![
|
||||||
|
"/usr".to_string(),
|
||||||
|
"/etc".to_string(),
|
||||||
|
"/opt".to_string(),
|
||||||
|
"/lib".to_string(),
|
||||||
|
"/lib64".to_string(),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
paths.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut results = ScanResults::new(scan_paths.clone());
|
||||||
|
let mut orphans: Vec<OrphanFile> = Vec::new();
|
||||||
|
|
||||||
|
for path_str in &scan_paths {
|
||||||
|
let path = Path::new(path_str);
|
||||||
|
if !path.exists() {
|
||||||
|
eprintln!("Warning: Path {} does not exist, skipping", path_str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Scanning: {}", path_str);
|
||||||
|
self.scan_directory(path, &mut orphans, &mut results)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categorize orphans
|
||||||
|
self.categorize_orphans(&mut orphans, include_expected, &mut results);
|
||||||
|
|
||||||
|
results.total_orphans = orphans.len();
|
||||||
|
results.total_orphan_size = orphans.iter().map(|o| o.size).sum();
|
||||||
|
results.scan_duration_seconds = start_time.elapsed().as_secs_f64();
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_directory(
|
||||||
|
&self,
|
||||||
|
root: &Path,
|
||||||
|
orphans: &mut Vec<OrphanFile>,
|
||||||
|
results: &mut ScanResults,
|
||||||
|
) -> Result<(), CartoError> {
|
||||||
|
let walker = WalkDir::new(root)
|
||||||
|
.follow_links(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter_entry(|e| !self.should_exclude_path(e.path()));
|
||||||
|
|
||||||
|
for entry in walker {
|
||||||
|
let entry = match entry {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Error accessing {}: {}", e.path().unwrap_or(Path::new("unknown")).display(), e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !entry.file_type().is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.total_files_scanned += 1;
|
||||||
|
|
||||||
|
// Check if file is owned by any package manager
|
||||||
|
let path = entry.path();
|
||||||
|
let is_owned = self.is_file_owned(path)?;
|
||||||
|
|
||||||
|
if !is_owned {
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
let orphan = OrphanFile {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
category: OrphanCategory::Unknown, // Will be categorized later
|
||||||
|
size: metadata.len(),
|
||||||
|
modified: metadata.modified()
|
||||||
|
.unwrap_or(std::time::UNIX_EPOCH)
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs(),
|
||||||
|
created: metadata.created()
|
||||||
|
.unwrap_or(std::time::UNIX_EPOCH)
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs(),
|
||||||
|
};
|
||||||
|
orphans.push(orphan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress indicator for large scans
|
||||||
|
if results.total_files_scanned % 10000 == 0 {
|
||||||
|
println!("Scanned {} files, found {} orphans",
|
||||||
|
results.total_files_scanned, orphans.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_exclude_path(&self, path: &Path) -> bool {
|
||||||
|
for exclude in &self.exclude_paths {
|
||||||
|
if path.starts_with(exclude) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
|
||||||
|
// Container storage patterns
|
||||||
|
if path_str.contains("/containers/storage/") ||
|
||||||
|
path_str.contains("/overlay/") ||
|
||||||
|
path_str.contains("/docker/") ||
|
||||||
|
|
||||||
|
// Build and cache directories
|
||||||
|
path_str.contains("/.git/") ||
|
||||||
|
path_str.contains("/.cache/") ||
|
||||||
|
path_str.contains("/cache/") ||
|
||||||
|
path_str.contains("/.local/share/Trash/") ||
|
||||||
|
|
||||||
|
// Mount points and removable media
|
||||||
|
path_str.starts_with("/media/") ||
|
||||||
|
path_str.starts_with("/mnt/") ||
|
||||||
|
|
||||||
|
// Snap and flatpak user data
|
||||||
|
path_str.contains("/snap/") ||
|
||||||
|
path_str.contains("/.var/app/") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file_owned(&self, path: &Path) -> Result<bool, CartoError> {
|
||||||
|
// Check all package managers to see if any owns this file
|
||||||
|
match find_file_owner(path) {
|
||||||
|
Ok(Some(_)) => Ok(true),
|
||||||
|
Ok(None) => Ok(false),
|
||||||
|
Err(e) => {
|
||||||
|
// Don't fail the entire scan for individual file errors
|
||||||
|
eprintln!("Warning: Error checking ownership of {}: {}", path.display(), e);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn categorize_orphans(
|
||||||
|
&self,
|
||||||
|
orphans: &mut [OrphanFile],
|
||||||
|
include_expected: bool,
|
||||||
|
results: &mut ScanResults,
|
||||||
|
) {
|
||||||
|
for orphan in orphans.iter_mut() {
|
||||||
|
orphan.category = self.categorize_file(&orphan.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by category
|
||||||
|
for orphan in orphans.iter() {
|
||||||
|
if !include_expected && orphan.category == OrphanCategory::Expected {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let category_name = orphan.category.name().to_string();
|
||||||
|
results.orphans_by_category
|
||||||
|
.entry(category_name)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(orphan.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn categorize_file(&self, path: &Path) -> OrphanCategory {
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
|
||||||
|
// Expected orphans in common temporary/log locations
|
||||||
|
if path_str.starts_with("/tmp/") ||
|
||||||
|
path_str.starts_with("/var/tmp/") ||
|
||||||
|
path_str.starts_with("/var/log/") ||
|
||||||
|
path_str.starts_with("/var/cache/") ||
|
||||||
|
path_str.starts_with("/var/spool/") {
|
||||||
|
return OrphanCategory::Expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User data in home directories
|
||||||
|
if path_str.starts_with("/home/") ||
|
||||||
|
path_str.starts_with("/root/") {
|
||||||
|
return OrphanCategory::UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// System generated files
|
||||||
|
if path_str.starts_with("/var/lib/") ||
|
||||||
|
path_str.starts_with("/var/run/") ||
|
||||||
|
path_str.contains("/.cache/") ||
|
||||||
|
path_str.contains("/cache/") {
|
||||||
|
return OrphanCategory::SystemGenerated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration backups
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
let ext = extension.to_string_lossy().to_lowercase();
|
||||||
|
if ext == "bak" || ext == "orig" || ext == "old" || ext == "backup" {
|
||||||
|
return OrphanCategory::ConfigBackup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary files in unusual locations
|
||||||
|
if let Some(filename) = path.file_name() {
|
||||||
|
let name = filename.to_string_lossy().to_lowercase();
|
||||||
|
if name.starts_with("tmp") ||
|
||||||
|
name.starts_with(".tmp") ||
|
||||||
|
name.ends_with(".tmp") ||
|
||||||
|
name.ends_with("~") {
|
||||||
|
return OrphanCategory::Temporary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OrphanCategory::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_scan_results(
|
||||||
|
results: &ScanResults,
|
||||||
|
format: &OutputFormat,
|
||||||
|
detailed: bool,
|
||||||
|
show_sizes: bool,
|
||||||
|
) -> Result<(), CartoError> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => print_json_results(results),
|
||||||
|
OutputFormat::Summary => print_summary_results(results, show_sizes),
|
||||||
|
OutputFormat::Detailed => print_detailed_results(results, show_sizes),
|
||||||
|
OutputFormat::Simple => print_simple_results(results),
|
||||||
|
OutputFormat::Tree => print_tree_results(results, show_sizes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_json_results(results: &ScanResults) -> Result<(), CartoError> {
|
||||||
|
match serde_json::to_string_pretty(results) {
|
||||||
|
Ok(json) => {
|
||||||
|
println!("{}", json);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(CartoError::CommandFailed(format!("JSON serialization failed: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_summary_results(results: &ScanResults, show_sizes: bool) -> Result<(), CartoError> {
|
||||||
|
println!("Scan Summary");
|
||||||
|
println!("============");
|
||||||
|
println!("Scan completed: {}", results.scan_time.format("%Y-%m-%d %H:%M:%S UTC"));
|
||||||
|
println!("Duration: {:.2} seconds", results.scan_duration_seconds);
|
||||||
|
println!("Scanned paths: {}", results.scanned_paths.join(", "));
|
||||||
|
println!("Total files scanned: {}", results.total_files_scanned);
|
||||||
|
println!("Total orphan files: {}", results.total_orphans);
|
||||||
|
|
||||||
|
if show_sizes {
|
||||||
|
println!("Total orphan size: {}", format_size(results.total_orphan_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nOrphans by Category:");
|
||||||
|
for (category, orphans) in &results.orphans_by_category {
|
||||||
|
let category_size: u64 = orphans.iter().map(|o| o.size).sum();
|
||||||
|
if show_sizes {
|
||||||
|
println!(" {}: {} files ({})", category, orphans.len(), format_size(category_size));
|
||||||
|
} else {
|
||||||
|
println!(" {}: {} files", category, orphans.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_detailed_results(results: &ScanResults, show_sizes: bool) -> Result<(), CartoError> {
|
||||||
|
print_summary_results(results, show_sizes)?;
|
||||||
|
|
||||||
|
println!("\nDetailed File Listings:");
|
||||||
|
for (category, orphans) in &results.orphans_by_category {
|
||||||
|
println!("\n{} ({} files):", category.to_uppercase(), orphans.len());
|
||||||
|
println!("{}", "=".repeat(50));
|
||||||
|
|
||||||
|
for orphan in orphans.iter().take(20) { // Limit to first 20 per category
|
||||||
|
if show_sizes {
|
||||||
|
println!(" {} ({})", orphan.path.display(), format_size(orphan.size));
|
||||||
|
} else {
|
||||||
|
println!(" {}", orphan.path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if orphans.len() > 20 {
|
||||||
|
println!(" ... and {} more files", orphans.len() - 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_simple_results(results: &ScanResults) -> Result<(), CartoError> {
|
||||||
|
println!("Found {} orphan files in {:.1}s", results.total_orphans, results.scan_duration_seconds);
|
||||||
|
for (category, orphans) in &results.orphans_by_category {
|
||||||
|
println!("{}: {}", category, orphans.len());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tree_results(results: &ScanResults, show_sizes: bool) -> Result<(), CartoError> {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
println!("Orphan Files by Directory Tree");
|
||||||
|
println!("==============================");
|
||||||
|
|
||||||
|
for (category, orphans) in &results.orphans_by_category {
|
||||||
|
println!("\n{} ({} files):", category.to_uppercase(), orphans.len());
|
||||||
|
|
||||||
|
// Group files by their parent directory
|
||||||
|
let mut dir_map: BTreeMap<PathBuf, Vec<&OrphanFile>> = BTreeMap::new();
|
||||||
|
for orphan in orphans {
|
||||||
|
if let Some(parent) = orphan.path.parent() {
|
||||||
|
dir_map.entry(parent.to_path_buf()).or_insert_with(Vec::new).push(orphan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dir, files) in dir_map.iter().take(10) { // Limit directories shown
|
||||||
|
println!(" {}/", dir.display());
|
||||||
|
for file in files.iter().take(5) { // Limit files per directory
|
||||||
|
let filename = file.path.file_name().unwrap_or_default().to_string_lossy();
|
||||||
|
if show_sizes {
|
||||||
|
println!(" ├── {} ({})", filename, format_size(file.size));
|
||||||
|
} else {
|
||||||
|
println!(" ├── {}", filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if files.len() > 5 {
|
||||||
|
println!(" └── ... and {} more files", files.len() - 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dir_map.len() > 10 {
|
||||||
|
println!(" ... and {} more directories", dir_map.len() - 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_size(bytes: u64) -> String {
|
||||||
|
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
|
||||||
|
let mut size = bytes as f64;
|
||||||
|
let mut unit_idx = 0;
|
||||||
|
|
||||||
|
while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
|
||||||
|
size /= 1024.0;
|
||||||
|
unit_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit_idx == 0 {
|
||||||
|
format!("{} {}", bytes, UNITS[unit_idx])
|
||||||
|
} else {
|
||||||
|
format!("{:.1} {}", size, UNITS[unit_idx])
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,22 @@
|
||||||
use memmap2::Mmap;
|
use memmap2::Mmap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
use crate::{CartoError, PackageSource};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
#[repr(C, packed)]
|
#[repr(C, packed)]
|
||||||
pub struct DatabaseHeader {
|
pub struct DatabaseHeader {
|
||||||
magic: [u8; 8], // "WHEREDB\0"
|
magic: [u8; 8], // "CARTODB\0"
|
||||||
version: u32,
|
version: u32,
|
||||||
created: u64, // Unix timestamp
|
created: u64, // Unix timestamp
|
||||||
file_count: u64,
|
file_count: u64,
|
||||||
package_count: u32,
|
package_count: u32,
|
||||||
|
orphan_count: u64, // Count of orphaned files
|
||||||
|
scan_count: u32, // Number of scans performed
|
||||||
files_offset: u64,
|
files_offset: u64,
|
||||||
packages_offset: u64,
|
packages_offset: u64,
|
||||||
|
orphans_offset: u64, // Offset to orphan records
|
||||||
|
scans_offset: u64, // Offset to scan metadata
|
||||||
strings_offset: u64,
|
strings_offset: u64,
|
||||||
index_offset: u64,
|
index_offset: u64,
|
||||||
}
|
}
|
||||||
|
@ -36,14 +43,99 @@ pub struct PackageRecord {
|
||||||
first_file_idx: u32,
|
first_file_idx: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WhereDatabase {
|
#[repr(C, packed)]
|
||||||
|
pub struct OrphanRecord {
|
||||||
|
path_hash: u64, // FNV-1a hash for quick lookups
|
||||||
|
category: u8, // OrphanCategory as u8
|
||||||
|
permissions: u16,
|
||||||
|
size: u64,
|
||||||
|
mtime: u64,
|
||||||
|
ctime: u64, // Creation time for cleanup planning
|
||||||
|
path_offset: u32, // offset into string pool
|
||||||
|
scan_id: u32, // Which scan discovered this orphan
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct ScanRecord {
|
||||||
|
id: u32,
|
||||||
|
scan_time: u64, // Unix timestamp when scan was performed
|
||||||
|
scanned_paths_offset: u32, // Comma-separated list of scanned paths
|
||||||
|
orphans_found: u32, // Number of orphans found in this scan
|
||||||
|
total_files_scanned: u64,
|
||||||
|
scan_duration: u32, // Duration in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
|
pub enum OrphanCategory {
|
||||||
|
Unknown = 0, // Files with no clear ownership
|
||||||
|
Expected = 1, // Files in /tmp, /var/tmp, /var/log, etc.
|
||||||
|
UserData = 2, // Files in /home directories
|
||||||
|
SystemGenerated = 3, // Runtime files, caches
|
||||||
|
ConfigBackup = 4, // .bak, .orig config files
|
||||||
|
Temporary = 5, // Temp files in unusual locations
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrphanCategory {
|
||||||
|
pub fn from_u8(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => OrphanCategory::Unknown,
|
||||||
|
1 => OrphanCategory::Expected,
|
||||||
|
2 => OrphanCategory::UserData,
|
||||||
|
3 => OrphanCategory::SystemGenerated,
|
||||||
|
4 => OrphanCategory::ConfigBackup,
|
||||||
|
5 => OrphanCategory::Temporary,
|
||||||
|
_ => OrphanCategory::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
OrphanCategory::Unknown => "unknown",
|
||||||
|
OrphanCategory::Expected => "expected",
|
||||||
|
OrphanCategory::UserData => "user-data",
|
||||||
|
OrphanCategory::SystemGenerated => "system-generated",
|
||||||
|
OrphanCategory::ConfigBackup => "config-backup",
|
||||||
|
OrphanCategory::Temporary => "temporary",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
OrphanCategory::Unknown => "Files with no clear package ownership",
|
||||||
|
OrphanCategory::Expected => "Files in temporary/log directories",
|
||||||
|
OrphanCategory::UserData => "Files in user home directories",
|
||||||
|
OrphanCategory::SystemGenerated => "Runtime and cache files",
|
||||||
|
OrphanCategory::ConfigBackup => "Configuration backup files",
|
||||||
|
OrphanCategory::Temporary => "Temporary files in unusual locations",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileInfo {
|
||||||
|
pub path: String,
|
||||||
|
pub package: Option<String>,
|
||||||
|
pub source: Option<PackageSource>,
|
||||||
|
pub size: u64,
|
||||||
|
pub modified: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OrphanInfo {
|
||||||
|
pub path: String,
|
||||||
|
pub category: OrphanCategory,
|
||||||
|
pub size: u64,
|
||||||
|
pub modified: u64,
|
||||||
|
pub created: u64,
|
||||||
|
pub scan_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CartoDatabase {
|
||||||
_file: File,
|
_file: File,
|
||||||
mmap: Mmap,
|
mmap: Mmap,
|
||||||
header: &'static DatabaseHeader,
|
header: &'static DatabaseHeader,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WhereDatabase {
|
impl CartoDatabase {
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, WhereError> {
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, CartoError> {
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
|
let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
|
||||||
|
|
||||||
|
@ -52,11 +144,11 @@ impl WhereDatabase {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate magic number
|
// Validate magic number
|
||||||
if &header.magic != b"WHEREDB\0" {
|
if &header.magic != b"CARTODB\0" {
|
||||||
return Err(WhereError::InvalidDatabase);
|
return Err(CartoError::InvalidDatabase);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(WhereDatabase {
|
Ok(CartoDatabase {
|
||||||
_file: file,
|
_file: file,
|
||||||
mmap,
|
mmap,
|
||||||
header,
|
header,
|
||||||
|
@ -64,17 +156,60 @@ impl WhereDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_file(&self, path: &str) -> Option<FileInfo> {
|
pub fn find_file(&self, path: &str) -> Option<FileInfo> {
|
||||||
let hash = fnv1a_hash(path.as_bytes());
|
let hash = self.fnv1a_hash(path.as_bytes());
|
||||||
|
|
||||||
// Binary search through sorted file records
|
// First check package-owned files
|
||||||
|
if let Some(file_info) = self.find_package_file(hash, path) {
|
||||||
|
return Some(file_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check orphaned files
|
||||||
|
self.find_orphan_file(hash, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_orphans_by_category(&self, category: OrphanCategory) -> Vec<OrphanInfo> {
|
||||||
|
let orphans = self.get_orphan_records();
|
||||||
|
orphans.iter()
|
||||||
|
.filter(|record| OrphanCategory::from_u8(record.category) == category)
|
||||||
|
.map(|record| OrphanInfo {
|
||||||
|
path: self.get_string(record.path_offset),
|
||||||
|
category: OrphanCategory::from_u8(record.category),
|
||||||
|
size: record.size,
|
||||||
|
modified: record.mtime,
|
||||||
|
created: record.ctime,
|
||||||
|
scan_id: record.scan_id,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scan_summary(&self) -> ScanSummary {
|
||||||
|
let orphans = self.get_orphan_records();
|
||||||
|
let mut category_counts = std::collections::HashMap::new();
|
||||||
|
let mut total_size = 0u64;
|
||||||
|
|
||||||
|
for record in orphans {
|
||||||
|
let category = OrphanCategory::from_u8(record.category);
|
||||||
|
*category_counts.entry(category).or_insert(0) += 1;
|
||||||
|
total_size += record.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanSummary {
|
||||||
|
total_orphans: orphans.len(),
|
||||||
|
category_counts,
|
||||||
|
total_size,
|
||||||
|
last_scan: self.get_last_scan_time(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_package_file(&self, hash: u64, path: &str) -> Option<FileInfo> {
|
||||||
let files = self.get_file_records();
|
let files = self.get_file_records();
|
||||||
match files.binary_search_by_key(&hash, |record| record.path_hash) {
|
match files.binary_search_by_key(&hash, |record| record.path_hash) {
|
||||||
Ok(idx) => {
|
Ok(idx) => {
|
||||||
let record = &files[idx];
|
let record = &files[idx];
|
||||||
Some(FileInfo {
|
Some(FileInfo {
|
||||||
path: self.get_string(record.path_offset),
|
path: self.get_string(record.path_offset),
|
||||||
package: self.get_package_name(record.package_id),
|
package: Some(self.get_package_name(record.package_id)),
|
||||||
source: PackageSource::from_u8(record.source),
|
source: Some(PackageSource::from_u8(record.source)),
|
||||||
size: record.size,
|
size: record.size,
|
||||||
modified: record.mtime,
|
modified: record.mtime,
|
||||||
})
|
})
|
||||||
|
@ -82,4 +217,85 @@ impl WhereDatabase {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_orphan_file(&self, hash: u64, path: &str) -> Option<FileInfo> {
|
||||||
|
let orphans = self.get_orphan_records();
|
||||||
|
match orphans.binary_search_by_key(&hash, |record| record.path_hash) {
|
||||||
|
Ok(idx) => {
|
||||||
|
let record = &orphans[idx];
|
||||||
|
Some(FileInfo {
|
||||||
|
path: self.get_string(record.path_offset),
|
||||||
|
package: None,
|
||||||
|
source: None,
|
||||||
|
size: record.size,
|
||||||
|
modified: record.mtime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_file_records(&self) -> &[FileRecord] {
|
||||||
|
unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
self.mmap.as_ptr().add(self.header.files_offset as usize) as *const FileRecord,
|
||||||
|
self.header.file_count as usize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_orphan_records(&self) -> &[OrphanRecord] {
|
||||||
|
unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
self.mmap.as_ptr().add(self.header.orphans_offset as usize) as *const OrphanRecord,
|
||||||
|
self.header.orphan_count as usize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string(&self, offset: u32) -> String {
|
||||||
|
// Implementation for reading from string pool
|
||||||
|
// This is a placeholder - actual implementation depends on string storage format
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_package_name(&self, package_id: u32) -> String {
|
||||||
|
// Implementation for getting package name by ID
|
||||||
|
// This is a placeholder - actual implementation depends on package storage
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_last_scan_time(&self) -> Option<u64> {
|
||||||
|
if self.header.scan_count == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scans = unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
self.mmap.as_ptr().add(self.header.scans_offset as usize) as *const ScanRecord,
|
||||||
|
self.header.scan_count as usize
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
scans.last().map(|scan| scan.scan_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fnv1a_hash(&self, bytes: &[u8]) -> u64 {
|
||||||
|
const FNV_OFFSET_BASIS: u64 = 14695981039346656037;
|
||||||
|
const FNV_PRIME: u64 = 1099511628211;
|
||||||
|
|
||||||
|
let mut hash = FNV_OFFSET_BASIS;
|
||||||
|
for byte in bytes {
|
||||||
|
hash ^= *byte as u64;
|
||||||
|
hash = hash.wrapping_mul(FNV_PRIME);
|
||||||
|
}
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScanSummary {
|
||||||
|
pub total_orphans: usize,
|
||||||
|
pub category_counts: std::collections::HashMap<OrphanCategory, usize>,
|
||||||
|
pub total_size: u64,
|
||||||
|
pub last_scan: Option<u64>,
|
||||||
}
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod format;
|
153
src/main.rs
153
src/main.rs
|
@ -3,12 +3,15 @@
|
||||||
|
|
||||||
pub mod package_managers;
|
pub mod package_managers;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod database;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::{self, PathBuf};
|
||||||
use package_managers::{detect_available_managers, PackageManager, PackageSource};
|
use package_managers::{detect_available_managers, PackageManager, PackageSource};
|
||||||
use cli::{Cli, Commands};
|
use cli::{Cli, Commands};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::cli::OutputFormat;
|
||||||
|
|
||||||
// Define the types that your modules need
|
// Define the types that your modules need
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PackageInfo {
|
pub struct PackageInfo {
|
||||||
|
@ -61,6 +64,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let managers = detect_available_managers();
|
let managers = detect_available_managers();
|
||||||
|
|
||||||
|
if managers.is_empty() {
|
||||||
|
eprintln!("Warning: No package managers detected");
|
||||||
|
}
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Tree { depth, package, source } => {
|
Commands::Tree { depth, package, source } => {
|
||||||
handle_tree_command(&managers, depth, package, source)?;
|
handle_tree_command(&managers, depth, package, source)?;
|
||||||
|
@ -77,8 +84,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Commands::Find { name, package, size } => {
|
Commands::Find { name, package, size } => {
|
||||||
handle_find_command(&managers, name, package, size)?;
|
handle_find_command(&managers, name, package, size)?;
|
||||||
}
|
}
|
||||||
Commands::Scan { force } => {
|
Commands::Scan { force, paths, format, detailed, sizes, include_expected } => {
|
||||||
handle_scan_command(&managers, force)?;
|
handle_scan_command(&managers, paths, format, force, detailed, sizes, include_expected)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,23 +96,54 @@ fn handle_tree_command(
|
||||||
managers: &[Box<dyn PackageManager>],
|
managers: &[Box<dyn PackageManager>],
|
||||||
_depth: Option<usize>,
|
_depth: Option<usize>,
|
||||||
package_filter: Option<String>,
|
package_filter: Option<String>,
|
||||||
_source_filter: Option<String>
|
source_filter: Option<String>
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
|
let target_managers: Vec<&Box<dyn PackageManager>> = if let Some(source_name) = source_filter {
|
||||||
|
// Filter to specific manager
|
||||||
|
managers.iter().filter(|m| m.name().to_lowercase() == source_name.to_lowercase()).collect()
|
||||||
|
} else {
|
||||||
|
// Use all managers
|
||||||
|
managers.iter().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
if target_managers.is_empty() {
|
||||||
|
println!("No suitable package managers found");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(package_name) = package_filter {
|
if let Some(package_name) = package_filter {
|
||||||
// Show tree for specific package
|
// Show tree for specific package across all managers
|
||||||
cli::tree::print_package_tree(dnf.as_ref(), &package_name)?;
|
let mut found = false;
|
||||||
} else {
|
for manager in &target_managers {
|
||||||
// Show tree for first few packages as demo
|
match manager.get_package_info(&package_name) {
|
||||||
println!("Package file trees (first 5 packages):");
|
Ok(Some(_)) => {
|
||||||
let packages = dnf.get_installed_packages()?;
|
println!("Package '{}' from {}:", package_name, manager.name());
|
||||||
for package in packages.iter().take(5) {
|
cli::tree::print_package_tree(manager.as_ref(), &package_name)?;
|
||||||
cli::tree::print_package_tree(dnf.as_ref(), &package.name)?;
|
found = true;
|
||||||
println!(); // Empty line between packages
|
println!();
|
||||||
|
}
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(e) => eprintln!("Error checking {} in {}: {}", package_name, manager.name(), e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !found {
|
||||||
|
println!("Package '{}' not found in any package manager", package_name);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("No DNF package manager found");
|
// Show tree for first few packages from each manager as demo
|
||||||
|
for manager in &target_managers {
|
||||||
|
match manager.get_installed_packages() {
|
||||||
|
Ok(packages) => {
|
||||||
|
println!("{} package file trees (first 3 packages):", manager.name());
|
||||||
|
for package in packages.iter().take(3) {
|
||||||
|
println!(" Package: {}", package.name);
|
||||||
|
cli::tree::print_package_tree(manager.as_ref(), &package.name)?;
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Error getting packages from {}: {}", manager.name(), e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -114,10 +152,13 @@ fn handle_package_command(
|
||||||
managers: &[Box<dyn PackageManager>],
|
managers: &[Box<dyn PackageManager>],
|
||||||
name: &str
|
name: &str
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
|
let mut found = false;
|
||||||
match dnf.get_package_info(name) {
|
|
||||||
|
// Search all managers for the package
|
||||||
|
for manager in managers {
|
||||||
|
match manager.get_package_info(name) {
|
||||||
Ok(Some(info)) => {
|
Ok(Some(info)) => {
|
||||||
println!("Package: {}", info.name);
|
println!("Package: {} (from {})", info.name, manager.name());
|
||||||
println!("Version: {}", info.version);
|
println!("Version: {}", info.version);
|
||||||
println!("Source: {}", info.source.name());
|
println!("Source: {}", info.source.name());
|
||||||
println!("Install time: {}", info.install_time);
|
println!("Install time: {}", info.install_time);
|
||||||
|
@ -125,18 +166,25 @@ fn handle_package_command(
|
||||||
|
|
||||||
// Show file tree for this package
|
// Show file tree for this package
|
||||||
println!("\nFiles:");
|
println!("\nFiles:");
|
||||||
cli::tree::print_package_tree(dnf.as_ref(), name)?;
|
cli::tree::print_package_tree(manager.as_ref(), name)?;
|
||||||
|
found = true;
|
||||||
|
println!();
|
||||||
}
|
}
|
||||||
Ok(None) => println!("Package '{}' not found", name),
|
Ok(None) => continue,
|
||||||
Err(e) => println!("Error: {}", e),
|
Err(e) => eprintln!("Error checking {} in {}: {}", name, manager.name(), e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
println!("Package '{}' not found in any package manager", name);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_packages_command(
|
fn handle_packages_command(
|
||||||
managers: &[Box<dyn PackageManager>],
|
managers: &[Box<dyn PackageManager>],
|
||||||
source_filter: Option<String> // Removed the underscore!
|
source_filter: Option<String>
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(source_name) = source_filter {
|
if let Some(source_name) = source_filter {
|
||||||
// Filter by specific source
|
// Filter by specific source
|
||||||
|
@ -184,13 +232,32 @@ fn handle_file_command(
|
||||||
managers: &[Box<dyn PackageManager>],
|
managers: &[Box<dyn PackageManager>],
|
||||||
path: &str
|
path: &str
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
|
let path_obj = std::path::Path::new(path);
|
||||||
match dnf.get_file_owner(std::path::Path::new(path)) {
|
let mut owners = Vec::new();
|
||||||
Ok(Some(owner)) => println!("{} is owned by package: {}", path, owner),
|
|
||||||
Ok(None) => println!("{} is not owned by any package", path),
|
// Check all managers for file ownership
|
||||||
Err(e) => println!("Error checking {}: {}", path, e),
|
for manager in managers {
|
||||||
|
match manager.get_file_owner(path_obj) {
|
||||||
|
Ok(Some(owner)) => {
|
||||||
|
owners.push((manager.name(), owner));
|
||||||
|
}
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(e) => eprintln!("Warning: Error checking {} in {}: {}", path, manager.name(), e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if owners.is_empty() {
|
||||||
|
println!("{} is not owned by any package", path);
|
||||||
|
} else if owners.len() == 1 {
|
||||||
|
let (manager, owner) = &owners[0];
|
||||||
|
println!("{} is owned by package: {} ({})", path, owner, manager);
|
||||||
|
} else {
|
||||||
|
println!("{} is owned by multiple packages:", path);
|
||||||
|
for (manager, owner) in owners {
|
||||||
|
println!(" {} ({} package manager)", owner, manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,16 +268,27 @@ fn handle_find_command(
|
||||||
_size: Option<String>
|
_size: Option<String>
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(package_name) = package {
|
if let Some(package_name) = package {
|
||||||
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
|
let mut found = false;
|
||||||
match dnf.get_package_files(&package_name) {
|
|
||||||
|
// Search all managers for the package
|
||||||
|
for manager in managers {
|
||||||
|
match manager.get_package_files(&package_name) {
|
||||||
Ok(files) => {
|
Ok(files) => {
|
||||||
println!("Files in package '{}':", package_name);
|
if !files.is_empty() {
|
||||||
|
println!("Files in package '{}' from {}:", package_name, manager.name());
|
||||||
for file in files {
|
for file in files {
|
||||||
println!(" {}", file.display());
|
println!(" {}", file.display());
|
||||||
}
|
}
|
||||||
|
found = true;
|
||||||
|
println!();
|
||||||
}
|
}
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
}
|
||||||
|
Err(e) => eprintln!("Error checking {} in {}: {}", package_name, manager.name(), e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
println!("Package '{}' not found in any package manager", package_name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("Find command needs more specific criteria");
|
println!("Find command needs more specific criteria");
|
||||||
|
@ -219,9 +297,16 @@ fn handle_find_command(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_scan_command(
|
fn handle_scan_command(
|
||||||
_managers: &[Box<dyn PackageManager>],
|
managers: &[Box<dyn PackageManager>],
|
||||||
_force: bool
|
paths: Vec<String>,
|
||||||
|
format: OutputFormat,
|
||||||
|
_force: bool,
|
||||||
|
detailed: bool,
|
||||||
|
sizes: bool,
|
||||||
|
include_expected: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Scan functionality not yet implemented");
|
let scanner = cli::scan::Scanner::new(managers);
|
||||||
|
let results = scanner.scan_paths(&paths, include_expected)?;
|
||||||
|
cli::scan::print_scan_results(&results, &format, detailed, sizes)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
Loading…
Reference in New Issue