apt-ostree/src/lib/apt.rs
robojerk 3dec23f8f7 Fix YAML linting issues and update system requirements to Debian 13+
- Fix trailing spaces and blank lines in Forgejo workflows
- Update system requirements from Ubuntu Jammy/Bookworm to Debian 13+ (Trixie)
- Update test treefile to use Debian Trixie instead of Ubuntu Jammy
- Update documentation to reflect modern system requirements
- Fix yamllint errors for CI/CD functionality
- Ensure compatibility with modern OSTree and libapt versions
2025-08-18 11:39:58 -07:00

395 lines
14 KiB
Rust

use crate::lib::error::{AptOstreeError, AptOstreeResult};
use std::process::Command;
use std::path::Path;
use serde::{Deserialize, Serialize};
/// Basic APT functionality
#[allow(dead_code, clippy::new_without_default)]
pub struct AptManager {
cache_dir: String,
apt_config_path: String,
}
impl AptManager {
/// Create a new APT manager instance
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
cache_dir: "/var/cache/apt".to_string(),
apt_config_path: "/etc/apt".to_string(),
}
}
}
impl AptManager {
/// Check APT database health
pub fn check_database_health(&self) -> AptOstreeResult<bool> {
// Check if APT cache is accessible
if !Path::new(&self.cache_dir).exists() {
return Err(AptOstreeError::System("APT cache directory not found".to_string()));
}
// Check if apt-get is available
if Command::new("apt-get").arg("--version").output().is_err() {
return Err(AptOstreeError::System("apt-get not available on this system".to_string()));
}
// Check if apt-cache is available
if Command::new("apt-cache").arg("--version").output().is_err() {
return Err(AptOstreeError::System("apt-cache not available on this system".to_string()));
}
Ok(true)
}
/// Install a package with real APT functionality
pub fn install_package(&self, package: &str) -> AptOstreeResult<()> {
// First check if package exists
let package_info = self.get_package_info(package)?;
if package_info.is_none() {
return Err(AptOstreeError::InvalidArgument(
format!("Package '{}' not found in APT repositories", package)
));
}
// Check dependencies
let dependencies = self.resolve_dependencies(package)?;
println!("Installing package: {} with {} dependencies", package, dependencies.len());
// Use apt-get to install the package
let output = Command::new("apt-get")
.arg("install")
.arg("--yes")
.arg("--no-install-recommends")
.arg(package)
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to install package: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AptOstreeError::System(format!("Package installation failed: {}", stderr)));
}
println!("Successfully installed package: {}", package);
Ok(())
}
/// Remove a package with real APT functionality
pub fn remove_package(&self, package: &str) -> AptOstreeResult<()> {
// Check if package is installed
if !self.is_package_installed(package)? {
return Err(AptOstreeError::InvalidArgument(
format!("Package '{}' is not installed", package)
));
}
// Use apt-get to remove the package
let output = Command::new("apt-get")
.arg("remove")
.arg("--yes")
.arg(package)
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to remove package: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AptOstreeError::System(format!("Package removal failed: {}", stderr)));
}
println!("Successfully removed package: {}", package);
Ok(())
}
/// Update package cache with real APT functionality
pub fn update_cache(&self) -> AptOstreeResult<()> {
println!("Updating APT package cache...");
let output = Command::new("apt-get")
.arg("update")
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to update cache: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AptOstreeError::System(format!("Cache update failed: {}", stderr)));
}
println!("APT package cache updated successfully");
Ok(())
}
/// Check if authorization is required for an action
pub fn requires_authorization(&self, action: &str) -> bool {
// Package installation and removal require root privileges
matches!(action, "install" | "remove" | "upgrade" | "dist-upgrade")
}
/// Check if user is authorized for an action
pub fn check_authorization(&self, action: &str) -> AptOstreeResult<bool> {
if !self.requires_authorization(action) {
return Ok(true);
}
// Check if running as root
if unsafe { libc::geteuid() } == 0 {
Ok(true)
} else {
// TODO: Implement Polkit authorization check
Ok(false)
}
}
/// Search packages with real APT functionality
pub fn search_packages(&self, query: &str) -> AptOstreeResult<Vec<PackageInfo>> {
println!("Searching for packages matching: {}", query);
let output = Command::new("apt-cache")
.arg("search")
.arg(query)
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to search packages: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AptOstreeError::System(format!("Package search failed: {}", stderr)));
}
let search_output = String::from_utf8_lossy(&output.stdout);
let mut packages = Vec::new();
for line in search_output.lines() {
if let Some(package) = self.parse_search_line(line) {
packages.push(package);
}
}
println!("Found {} packages matching '{}'", packages.len(), query);
Ok(packages)
}
/// Search packages with exact match
pub fn search_packages_exact(&self, query: &str) -> AptOstreeResult<Vec<PackageInfo>> {
// Use apt-cache policy for exact package information
let output = Command::new("apt-cache")
.arg("policy")
.arg(query)
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to get package policy: {}", e)))?;
if !output.status.success() {
return Ok(Vec::new()); // Package not found
}
let policy_output = String::from_utf8_lossy(&output.stdout);
if let Some(package) = self.parse_policy_output(query, &policy_output) {
Ok(vec![package])
} else {
Ok(Vec::new())
}
}
/// Search packages with regex
pub fn search_packages_regex(&self, query: &str) -> AptOstreeResult<Vec<PackageInfo>> {
// For regex search, we'll use apt-cache search and then filter with regex
let all_packages = self.search_packages("")?;
// Simple regex-like matching (basic wildcard support)
let pattern = query.replace("*", ".*");
let regex = regex::Regex::new(&pattern)
.map_err(|e| AptOstreeError::System(format!("Invalid regex pattern: {}", e)))?;
let filtered_packages: Vec<PackageInfo> = all_packages
.into_iter()
.filter(|pkg| regex.is_match(&pkg.name))
.collect();
Ok(filtered_packages)
}
/// Check if a package is installed
pub fn is_package_installed(&self, package: &str) -> AptOstreeResult<bool> {
let output = Command::new("dpkg")
.arg("-s")
.arg(package)
.output();
match output {
Ok(output) => Ok(output.status.success()),
Err(_) => Ok(false),
}
}
/// Get detailed package information
pub fn get_package_info(&self, package: &str) -> AptOstreeResult<Option<PackageInfo>> {
let output = Command::new("apt-cache")
.arg("show")
.arg(package)
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to get package info: {}", e)))?;
if !output.status.success() {
return Ok(None); // Package not found
}
let show_output = String::from_utf8_lossy(&output.stdout);
Ok(self.parse_show_output(package, &show_output))
}
/// Resolve package dependencies
pub fn resolve_dependencies(&self, package: &str) -> AptOstreeResult<Vec<String>> {
let output = Command::new("apt-cache")
.arg("depends")
.arg(package)
.output()
.map_err(|e| AptOstreeError::System(format!("Failed to get dependencies: {}", e)))?;
if !output.status.success() {
return Ok(Vec::new());
}
let depends_output = String::from_utf8_lossy(&output.stdout);
let mut dependencies = Vec::new();
for line in depends_output.lines() {
if line.trim().starts_with("Depends:") {
let deps = line.split("Depends:").nth(1).unwrap_or("").trim();
if !deps.is_empty() {
dependencies.extend(deps.split(',').map(|s| s.trim().to_string()));
}
}
}
Ok(dependencies)
}
/// Get cache status for daemon
pub fn get_cache_status(&self) -> AptOstreeResult<String> {
if !Path::new(&self.cache_dir).exists() {
return Ok("not-available".to_string());
}
// Check if cache is up to date
let output = Command::new("apt-get")
.arg("check")
.output();
match output {
Ok(output) => {
if output.status.success() {
Ok("healthy".to_string())
} else {
Ok("needs-update".to_string())
}
}
Err(_) => Ok("error".to_string()),
}
}
/// Parse search output line
fn parse_search_line(&self, line: &str) -> Option<PackageInfo> {
// Format: "package_name - package_description"
let parts: Vec<&str> = line.splitn(2, " - ").collect();
if parts.len() != 2 {
return None;
}
let name = parts[0].trim();
let description = parts[1].trim();
Some(PackageInfo {
name: name.to_string(),
version: "unknown".to_string(),
description: description.to_string(),
installed: false, // Will be checked separately
section: "unknown".to_string(),
priority: "unknown".to_string(),
depends: Vec::new(),
})
}
/// Parse policy output
fn parse_policy_output(&self, package_name: &str, policy_output: &str) -> Option<PackageInfo> {
let mut package = PackageInfo {
name: package_name.to_string(),
version: "unknown".to_string(),
description: "unknown".to_string(),
installed: false,
section: "unknown".to_string(),
priority: "unknown".to_string(),
depends: Vec::new(),
};
for line in policy_output.lines() {
if line.contains("Installed:") {
let version = line.split("Installed:").nth(1)?.trim();
if version != "(none)" {
package.version = version.to_string();
package.installed = true;
}
}
}
Some(package)
}
/// Parse show output
fn parse_show_output(&self, package_name: &str, show_output: &str) -> Option<PackageInfo> {
let mut package = PackageInfo {
name: package_name.to_string(),
version: "unknown".to_string(),
description: "unknown".to_string(),
installed: false,
section: "unknown".to_string(),
priority: "unknown".to_string(),
depends: Vec::new(),
};
for line in show_output.lines() {
if line.starts_with("Version:") {
package.version = line.split("Version:").nth(1)?.trim().to_string();
} else if line.starts_with("Description:") {
package.description = line.split("Description:").nth(1)?.trim().to_string();
} else if line.starts_with("Section:") {
package.section = line.split("Section:").nth(1)?.trim().to_string();
} else if line.starts_with("Priority:") {
package.priority = line.split("Priority:").nth(1)?.trim().to_string();
} else if line.starts_with("Depends:") {
let deps = line.split("Depends:").nth(1)?.trim();
if !deps.is_empty() {
package.depends = deps.split(',').map(|s| s.trim().to_string()).collect();
}
}
}
// Check if installed
package.installed = self.is_package_installed(package_name).unwrap_or(false);
Some(package)
}
}
/// Package information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageInfo {
pub name: String,
pub version: String,
pub description: String,
pub installed: bool,
pub section: String,
pub priority: String,
pub depends: Vec<String>,
}
impl PackageInfo {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
version: "0.0.0".to_string(),
description: "No description available".to_string(),
installed: false,
section: "unknown".to_string(),
priority: "unknown".to_string(),
depends: Vec::new(),
}
}
}