Added support for flatpak

This commit is contained in:
Russell 2025-07-28 17:00:22 +00:00
parent 7bc060c49a
commit 3c9602bdd3
3 changed files with 265 additions and 11 deletions

View File

@ -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 {

View File

@ -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<Vec<PackageInfo>, 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<Vec<PathBuf>, 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<Option<String>, 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<Option<PackageInfo>, 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<Option<PathBuf>, 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<PathBuf> {
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<u64> {
// 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<u64> {
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::<f64>() {
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
}
}

View File

@ -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<Box<dyn PackageManager>> {
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() {