Add system-wide installation via pkexec with polkit policy
This commit is contained in:
20
data/app.driftwood.Driftwood.policy
Normal file
20
data/app.driftwood.Driftwood.policy
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC
|
||||
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||
<policyconfig>
|
||||
|
||||
<vendor>Driftwood</vendor>
|
||||
<vendor_url>https://github.com/driftwood-appimage</vendor_url>
|
||||
|
||||
<action id="app.driftwood.Driftwood.system-install">
|
||||
<description>Install AppImage system-wide</description>
|
||||
<message>Authentication is required to install an AppImage for all users.</message>
|
||||
<defaults>
|
||||
<allow_any>auth_admin</allow_any>
|
||||
<allow_inactive>auth_admin</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
|
||||
</policyconfig>
|
||||
@@ -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<Vec<(String, u64)>> {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1017,6 +1017,55 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, 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
|
||||
|
||||
Reference in New Issue
Block a user