258 lines
9.2 KiB
Rust
258 lines
9.2 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use crate::{PackageInfo, CartoError};
|
|
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>, CartoError> {
|
|
let output = Command::new("flatpak")
|
|
.args(["list", "--columns=application,name,version,branch,installation"])
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
return Err(CartoError::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>, CartoError> {
|
|
// 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>, CartoError> {
|
|
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>, CartoError> {
|
|
// 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>, CartoError> {
|
|
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
|
|
}
|
|
} |