Added support for flatpak
This commit is contained in:
parent
7bc060c49a
commit
3c9602bdd3
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue