From 27eb9f259d49620cf50f3824c1581574f7c901b4 Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 28 Feb 2026 00:11:57 +0200 Subject: [PATCH] Add system-wide installation via pkexec with polkit policy --- data/app.driftwood.Driftwood.policy | 20 ++++ src/core/database.rs | 8 ++ src/core/integrator.rs | 154 ++++++++++++++++++++++++++++ src/ui/detail_view.rs | 49 +++++++++ 4 files changed, 231 insertions(+) create mode 100644 data/app.driftwood.Driftwood.policy diff --git a/data/app.driftwood.Driftwood.policy b/data/app.driftwood.Driftwood.policy new file mode 100644 index 0000000..381b25c --- /dev/null +++ b/data/app.driftwood.Driftwood.policy @@ -0,0 +1,20 @@ + + + + + Driftwood + https://github.com/driftwood-appimage + + + Install AppImage system-wide + Authentication is required to install an AppImage for all users. + + auth_admin + auth_admin + auth_admin_keep + + + + diff --git a/src/core/database.rs b/src/core/database.rs index a25590a..7d17d9c 100644 --- a/src/core/database.rs +++ b/src/core/database.rs @@ -1778,6 +1778,14 @@ impl Database { Ok(()) } + pub fn set_system_wide(&self, id: i64, system_wide: bool) -> SqlResult<()> { + self.conn.execute( + "UPDATE appimages SET system_wide = ?2 WHERE id = ?1", + params![id, system_wide as i32], + )?; + Ok(()) + } + // --- Launch statistics --- pub fn get_top_launched(&self, limit: i32) -> SqlResult> { diff --git a/src/core/integrator.rs b/src/core/integrator.rs index b3baaae..507392d 100644 --- a/src/core/integrator.rs +++ b/src/core/integrator.rs @@ -535,6 +535,160 @@ pub fn set_default_app( Ok(()) } +/// Install an AppImage system-wide via pkexec. +/// Copies the binary to /opt/driftwood-apps/, desktop file to +/// /usr/share/applications/, and icon to /usr/share/icons/hicolor/. +pub fn install_system_wide( + record: &AppImageRecord, + db: &Database, +) -> Result<(), String> { + let app_name = record + .app_name + .as_deref() + .or(Some(&record.filename)) + .ok_or("No app name")?; + let app_id = make_app_id(app_name); + + let dest_dir = "/opt/driftwood-apps"; + let dest_binary = format!("{}/{}", dest_dir, record.filename); + let desktop_filename = format!("driftwood-{}.desktop", app_id); + let system_desktop = format!("/usr/share/applications/{}", desktop_filename); + let icon_id = format!("driftwood-{}", app_id); + + // Create destination directory + let status = Command::new("pkexec") + .args(["mkdir", "-p", dest_dir]) + .status() + .map_err(|e| format!("pkexec failed: {}", e))?; + if !status.success() { + return Err("Failed to create system directory".to_string()); + } + + // Copy AppImage binary + let status = Command::new("pkexec") + .args(["cp", &record.path, &dest_binary]) + .status() + .map_err(|e| format!("pkexec cp failed: {}", e))?; + if !status.success() { + return Err("Failed to copy AppImage to system directory".to_string()); + } + + // Make executable + let status = Command::new("pkexec") + .args(["chmod", "+x", &dest_binary]) + .status() + .map_err(|e| format!("pkexec chmod failed: {}", e))?; + if !status.success() { + return Err("Failed to set execute permission".to_string()); + } + + db.register_modification(record.id, "system_binary", &dest_binary, None).ok(); + + // Generate system desktop file content + let categories = record.categories.as_deref().unwrap_or(""); + let comment = record.description.as_deref().unwrap_or(""); + + let mut desktop_content = format!("\ +[Desktop Entry] +Type=Application +Name={name} +Exec=\"{exec}\" %U +Icon={icon} +Categories={categories} +Comment={comment} +Terminal=false +X-AppImage-Managed-By=Driftwood +", + name = app_name, + exec = escape_exec_arg(&dest_binary), + icon = icon_id, + categories = categories, + comment = comment, + ); + + let mime_types = record.mime_types.as_deref().unwrap_or(""); + let wm_class = record.startup_wm_class.as_deref().unwrap_or(""); + if !mime_types.is_empty() { + desktop_content.push_str(&format!("MimeType={}\n", mime_types)); + } + if !wm_class.is_empty() { + desktop_content.push_str(&format!("StartupWMClass={}\n", wm_class)); + } + + // Write desktop file via temp file + pkexec mv + let tmp = std::env::temp_dir().join(&desktop_filename); + fs::write(&tmp, &desktop_content) + .map_err(|e| format!("Failed to write temp desktop file: {}", e))?; + + let status = Command::new("pkexec") + .args(["mv", &tmp.to_string_lossy(), &system_desktop]) + .status() + .map_err(|e| format!("pkexec mv failed: {}", e))?; + if !status.success() { + return Err("Failed to install system desktop file".to_string()); + } + + db.register_modification(record.id, "system_desktop", &system_desktop, None).ok(); + + // Copy icon if available + if let Some(ref cached_icon) = record.icon_path { + let cached = Path::new(cached_icon); + if cached.exists() { + let ext = cached.extension().and_then(|e| e.to_str()).unwrap_or("png"); + let (subdir, icon_filename) = if ext == "svg" { + ("scalable/apps", format!("{}.svg", icon_id)) + } else { + ("256x256/apps", format!("{}.png", icon_id)) + }; + let system_icon_dir = format!("/usr/share/icons/hicolor/{}", subdir); + let system_icon = format!("{}/{}", system_icon_dir, icon_filename); + + Command::new("pkexec") + .args(["mkdir", "-p", &system_icon_dir]) + .status() + .ok(); + + let status = Command::new("pkexec") + .args(["cp", &cached.to_string_lossy(), &system_icon]) + .status(); + if let Ok(s) = status { + if s.success() { + db.register_modification(record.id, "system_icon", &system_icon, None).ok(); + } + } + } + } + + db.set_system_wide(record.id, true).ok(); + + Ok(()) +} + +/// Remove a system-wide installation. +pub fn remove_system_wide( + db: &Database, + appimage_id: i64, +) -> Result<(), String> { + let mods = db.get_modifications(appimage_id).unwrap_or_default(); + for m in &mods { + match m.mod_type.as_str() { + "system_binary" | "system_desktop" | "system_icon" => { + let status = Command::new("pkexec") + .args(["rm", "-f", &m.file_path]) + .status(); + if let Ok(s) = status { + if s.success() { + db.remove_modification(m.id).ok(); + } + } + } + _ => {} + } + } + db.set_system_wide(appimage_id, false).ok(); + Ok(()) +} + fn update_desktop_database() { let apps_dir = applications_dir(); Command::new("update-desktop-database") diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index d1457a6..6602aeb 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -1017,6 +1017,55 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & }); integration_group.add(&wm_class_row); + // System-wide install toggle + if record.integrated { + let syswide_row = adw::SwitchRow::builder() + .title("Install system-wide") + .subtitle(if record.system_wide { + "Installed for all users in /opt/driftwood-apps/" + } else { + "Make available to all users on this computer" + }) + .active(record.system_wide) + .tooltip_text( + "Copies the AppImage and its shortcut to system directories \ + so all users on this computer can access it. Requires \ + administrator privileges." + ) + .build(); + + let record_sw = record.clone(); + let db_sw = db.clone(); + let toast_sw = toast_overlay.clone(); + let record_id_sw = record.id; + syswide_row.connect_active_notify(move |row| { + if row.is_active() { + match integrator::install_system_wide(&record_sw, &db_sw) { + Ok(()) => { + toast_sw.add_toast(adw::Toast::new("Installed system-wide")); + } + Err(e) => { + log::error!("System-wide install failed: {}", e); + toast_sw.add_toast(adw::Toast::new("System-wide install failed")); + row.set_active(false); + } + } + } else { + match integrator::remove_system_wide(&db_sw, record_id_sw) { + Ok(()) => { + toast_sw.add_toast(adw::Toast::new("System-wide install removed")); + } + Err(e) => { + log::error!("Failed to remove system-wide install: {}", e); + toast_sw.add_toast(adw::Toast::new("Failed to remove system-wide install")); + row.set_active(true); + } + } + } + }); + integration_group.add(&syswide_row); + } + inner.append(&integration_group); // Version Rollback group