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(())
|
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 ---
|
// --- Launch statistics ---
|
||||||
|
|
||||||
pub fn get_top_launched(&self, limit: i32) -> SqlResult<Vec<(String, u64)>> {
|
pub fn get_top_launched(&self, limit: i32) -> SqlResult<Vec<(String, u64)>> {
|
||||||
|
|||||||
@@ -535,6 +535,160 @@ pub fn set_default_app(
|
|||||||
Ok(())
|
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() {
|
fn update_desktop_database() {
|
||||||
let apps_dir = applications_dir();
|
let apps_dir = applications_dir();
|
||||||
Command::new("update-desktop-database")
|
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);
|
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);
|
inner.append(&integration_group);
|
||||||
|
|
||||||
// Version Rollback group
|
// Version Rollback group
|
||||||
|
|||||||
Reference in New Issue
Block a user