From 3c9602bdd38b205d5a29ddc05026b61a6997d291 Mon Sep 17 00:00:00 2001 From: Russell Date: Mon, 28 Jul 2025 17:00:22 +0000 Subject: [PATCH] Added support for flatpak --- src/package_managers/dnf.rs | 1 - src/package_managers/flatpak.rs | 258 ++++++++++++++++++++++++++++++++ src/package_managers/mod.rs | 17 +-- 3 files changed, 265 insertions(+), 11 deletions(-) create mode 100644 src/package_managers/flatpak.rs diff --git a/src/package_managers/dnf.rs b/src/package_managers/dnf.rs index f4874ce..2ca3156 100644 --- a/src/package_managers/dnf.rs +++ b/src/package_managers/dnf.rs @@ -2,7 +2,6 @@ use std::path::{Path, PathBuf}; use std::process::Command; use crate::{PackageInfo, WhereError}; use super::{PackageManager, PackageSource}; - pub struct DnfManager; impl PackageManager for DnfManager { diff --git a/src/package_managers/flatpak.rs b/src/package_managers/flatpak.rs new file mode 100644 index 0000000..1e438d1 --- /dev/null +++ b/src/package_managers/flatpak.rs @@ -0,0 +1,258 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use crate::{PackageInfo, WhereError}; +use super::{PackageManager, PackageSource}; + +pub struct FlatpakManager; + +impl PackageManager for FlatpakManager { + fn name(&self) -> &'static str { "flatpak" } + + fn source_type(&self) -> PackageSource { + PackageSource::Flatpak + } + + fn is_available(&self) -> bool { + Path::new("/usr/bin/flatpak").exists() || + Command::new("flatpak").arg("--version").output().is_ok() + } + + fn get_installed_packages(&self) -> Result, WhereError> { + let output = Command::new("flatpak") + .args(["list", "--columns=application,name,version,branch,installation"]) + .output()?; + + if !output.status.success() { + return Err(WhereError::CommandFailed("flatpak list failed".to_string())); + } + + let stdout = String::from_utf8(output.stdout)?; + let mut packages = Vec::new(); + + for line in stdout.lines().skip(1) { // Skip header + let parts: Vec<&str> = line.split('\t').collect(); + if parts.len() >= 4 { + let app_id = parts[0]; + let name = if parts[1].is_empty() { app_id } else { parts[1] }; + let version = parts[2]; + let installation = parts.get(4).unwrap_or(&"system"); + + // Skip runtimes - we only want applications + if !app_id.starts_with("org.freedesktop.Platform") && + !app_id.starts_with("org.gnome.Platform") && + !app_id.starts_with("org.kde.Platform") { + + packages.push(PackageInfo { + name: format!("{} ({})", name, app_id), + version: version.to_string(), + source: PackageSource::Flatpak, + install_time: self.get_install_time(app_id, installation).unwrap_or(0), + size: self.get_package_size(app_id, installation).unwrap_or(0), + }); + } + } + } + + Ok(packages) + } + + fn get_package_files(&self, package: &str) -> Result, WhereError> { + // Extract app ID from package name (format: "Name (app.id)") + let app_id = if package.contains('(') && package.contains(')') { + package.split('(').nth(1) + .and_then(|s| s.split(')').next()) + .unwrap_or(package) + } else { + package + }; + + let mut files = Vec::new(); + + // Get installation location + let location = self.get_app_location(app_id)?; + if let Some(location_path) = location { + files.push(location_path); + } + + // Add common Flatpak locations + files.extend(self.get_flatpak_system_files(app_id)); + + // Add user data directories if they exist + if let Some(home) = std::env::var("HOME").ok() { + let user_data = PathBuf::from(format!("{}/.var/app/{}", home, app_id)); + if user_data.exists() { + files.push(user_data); + } + } + + // Filter to only existing files + Ok(files.into_iter().filter(|p| p.exists()).collect()) + } + + fn get_file_owner(&self, path: &Path) -> Result, WhereError> { + let path_str = path.to_string_lossy(); + + // Check if it's in a Flatpak directory + if path_str.contains("/var/lib/flatpak/app/") { + // Extract app ID from path like /var/lib/flatpak/app/org.signal.Signal/... + if let Some(app_part) = path_str.split("/var/lib/flatpak/app/").nth(1) { + if let Some(app_id) = app_part.split('/').next() { + return Ok(Some(app_id.to_string())); + } + } + } + + // Check user Flatpak directories + if path_str.contains("/.local/share/flatpak/app/") { + if let Some(app_part) = path_str.split("/.local/share/flatpak/app/").nth(1) { + if let Some(app_id) = app_part.split('/').next() { + return Ok(Some(app_id.to_string())); + } + } + } + + // Check user data directories (~/.var/app/*) + if path_str.contains("/.var/app/") { + if let Some(app_part) = path_str.split("/.var/app/").nth(1) { + if let Some(app_id) = app_part.split('/').next() { + return Ok(Some(app_id.to_string())); + } + } + } + + Ok(None) + } + + fn get_package_info(&self, package: &str) -> Result, WhereError> { + // Extract app ID from package name + let app_id = if package.contains('(') && package.contains(')') { + package.split('(').nth(1) + .and_then(|s| s.split(')').next()) + .unwrap_or(package) + } else { + package + }; + + let output = Command::new("flatpak") + .args(["info", app_id]) + .output()?; + + if !output.status.success() { + return Ok(None); + } + + let stdout = String::from_utf8(output.stdout)?; + let mut name = String::new(); + let mut version = String::new(); + let mut size = 0u64; + + for line in stdout.lines() { + if line.starts_with("Name: ") { + name = line.strip_prefix("Name: ").unwrap_or("").to_string(); + } else if line.starts_with("Version: ") { + version = line.strip_prefix("Version: ").unwrap_or("").to_string(); + } else if line.starts_with("Installed size: ") { + let size_str = line.strip_prefix("Installed size: ").unwrap_or(""); + size = self.parse_size(size_str); + } + } + + if !name.is_empty() || !app_id.is_empty() { + Ok(Some(PackageInfo { + name: if name.is_empty() { + format!("{} ({})", app_id, app_id) + } else { + format!("{} ({})", name, app_id) + }, + version: if version.is_empty() { "unknown".to_string() } else { version }, + source: PackageSource::Flatpak, + install_time: self.get_install_time(app_id, "system").unwrap_or(0), + size, + })) + } else { + Ok(None) + } + } +} + +impl FlatpakManager { + fn get_app_location(&self, app_id: &str) -> Result, WhereError> { + let output = Command::new("flatpak") + .args(["info", "--show-location", app_id]) + .output()?; + + if output.status.success() { + let location = String::from_utf8(output.stdout)?; + let path = PathBuf::from(location.trim()); + if path.exists() { + return Ok(Some(path)); + } + } + + Ok(None) + } + + fn get_flatpak_system_files(&self, app_id: &str) -> Vec { + let mut files = Vec::new(); + + // Common Flatpak locations + let locations = [ + format!("/var/lib/flatpak/app/{}", app_id), + format!("/var/lib/flatpak/exports/share/applications/{}.desktop", app_id), + format!("/var/lib/flatpak/exports/share/icons/hicolor/scalable/apps/{}.svg", app_id), + ]; + + for location in &locations { + let path = PathBuf::from(location); + if path.exists() { + files.push(path); + } + } + + files + } + + fn get_install_time(&self, _app_id: &str, _installation: &str) -> Option { + // Flatpak doesn't easily expose install times, so we return None + // Could potentially parse from filesystem timestamps + None + } + + fn get_package_size(&self, app_id: &str, _installation: &str) -> Option { + if let Ok(output) = Command::new("flatpak") + .args(["info", app_id]) + .output() { + + if let Ok(stdout) = String::from_utf8(output.stdout) { + for line in stdout.lines() { + if line.starts_with("Installed size: ") { + let size_str = line.strip_prefix("Installed size: ").unwrap_or(""); + return Some(self.parse_size(size_str)); + } + } + } + } + None + } + + fn parse_size(&self, size_str: &str) -> u64 { + // Parse sizes like "1.2 GB", "500 MB", "1,234 kB" + let cleaned = size_str.replace(',', ""); + let parts: Vec<&str> = cleaned.split_whitespace().collect(); + + if parts.len() >= 2 { + if let Ok(num) = parts[0].parse::() { + let multiplier: i64 = match parts[1].to_uppercase().as_str() { + "B" | "BYTES" => 1, + "KB" | "K" => 1_000, + "MB" | "M" => 1_000_000, + "GB" | "G" => 1_000_000_000, + "TB" | "T" => 1_000_000_000_000, + _ => 1, + }; + return (num * multiplier as f64) as u64; + } + } + 0 + } +} \ No newline at end of file diff --git a/src/package_managers/mod.rs b/src/package_managers/mod.rs index b85e240..480f31c 100644 --- a/src/package_managers/mod.rs +++ b/src/package_managers/mod.rs @@ -1,13 +1,11 @@ use std::path::{Path, PathBuf}; pub mod dnf; -// pub mod flatpak; -// pub mod snap; +pub mod flatpak; +use self::flatpak::FlatpakManager; use self::dnf::DnfManager; -// Uncomment these as we implement the managers -// use self::flatpak::FlatpakManager; -// use self::snap::SnapManager; + use crate::{PackageInfo, WhereError}; @@ -93,11 +91,10 @@ pub fn detect_available_managers() -> Vec> { managers.push(Box::new(dnf)); } - // Add other managers as we implement them - // let flatpak = FlatpakManager; - // if flatpak.is_available() { - // managers.push(Box::new(flatpak)); - // } + let flatpak = FlatpakManager; + if flatpak.is_available() { + managers.push(Box::new(flatpak)); + } // let snap = SnapManager; // if snap.is_available() {