diff --git a/data/app.driftwood.Driftwood.gschema.xml b/data/app.driftwood.Driftwood.gschema.xml index 9872109..379a2b4 100644 --- a/data/app.driftwood.Driftwood.gschema.xml +++ b/data/app.driftwood.Driftwood.gschema.xml @@ -124,5 +124,10 @@ Security notification threshold Minimum CVE severity for desktop notifications: critical, high, medium, or low. + + false + Watch removable media + Scan removable drives for AppImages when mounted. + diff --git a/src/core/database.rs b/src/core/database.rs index 7d17d9c..73d2e17 100644 --- a/src/core/database.rs +++ b/src/core/database.rs @@ -1786,6 +1786,14 @@ impl Database { Ok(()) } + pub fn set_portable(&self, id: i64, portable: bool, mount_point: Option<&str>) -> SqlResult<()> { + self.conn.execute( + "UPDATE appimages SET is_portable = ?2, mount_point = ?3 WHERE id = ?1", + params![id, portable as i32, mount_point], + )?; + Ok(()) + } + // --- Launch statistics --- pub fn get_top_launched(&self, limit: i32) -> SqlResult> { diff --git a/src/core/mod.rs b/src/core/mod.rs index 10e4554..56e97b3 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -11,6 +11,7 @@ pub mod integrator; pub mod launcher; pub mod notification; pub mod orphan; +pub mod portable; pub mod report; pub mod security; pub mod updater; diff --git a/src/core/portable.rs b/src/core/portable.rs new file mode 100644 index 0000000..bbad943 --- /dev/null +++ b/src/core/portable.rs @@ -0,0 +1,71 @@ +use std::path::{Path, PathBuf}; + +/// Information about a mounted removable device. +#[derive(Debug, Clone)] +pub struct MountInfo { + pub device: String, + pub mount_point: PathBuf, + pub fs_type: String, +} + +/// Detect removable media mount points by parsing /proc/mounts. +/// Looks for user-accessible removable paths with common removable fs types. +pub fn detect_removable_mounts() -> Vec { + let content = match std::fs::read_to_string("/proc/mounts") { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + + let removable_prefixes = ["/media/", "/run/media/", "/mnt/"]; + let removable_fs = ["vfat", "exfat", "ntfs", "ntfs3", "fuseblk", "ext4", "ext3", "btrfs"]; + + content + .lines() + .filter_map(|line| { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 3 { + return None; + } + let device = parts[0]; + let mount_point = parts[1]; + let fs_type = parts[2]; + + // Check if mount point is in a removable location + let is_removable = removable_prefixes + .iter() + .any(|prefix| mount_point.starts_with(prefix)); + + if !is_removable { + return None; + } + + // Check filesystem type + if !removable_fs.contains(&fs_type) { + return None; + } + + Some(MountInfo { + device: device.to_string(), + mount_point: PathBuf::from(mount_point), + fs_type: fs_type.to_string(), + }) + }) + .collect() +} + +/// Check if a given path is located on a removable device. +pub fn is_path_on_removable(path: &Path) -> bool { + let mounts = detect_removable_mounts(); + mounts.iter().any(|m| path.starts_with(&m.mount_point)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_path_on_removable_with_no_mounts() { + // On a test system, /home paths shouldn't be on removable media + assert!(!is_path_on_removable(Path::new("/home/user/test.AppImage"))); + } +} diff --git a/src/ui/app_card.rs b/src/ui/app_card.rs index c88707e..71fdced 100644 --- a/src/ui/app_card.rs +++ b/src/ui/app_card.rs @@ -149,7 +149,12 @@ pub fn build_priority_badge(record: &AppImageRecord) -> Option { } } - // 3. Wayland issue (not Native or Unknown) + // 3. Portable / removable media + if record.is_portable { + return Some(widgets::status_badge("Portable", "info")); + } + + // 4. Wayland issue (not Native or Unknown) if let Some(ref ws) = record.wayland_status { let status = WaylandStatus::from_str(ws); if status != WaylandStatus::Unknown && status != WaylandStatus::Native { diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index 6602aeb..05b7ab2 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -1800,6 +1800,22 @@ fn build_storage_tab( .build(); size_group.add(&appimage_row); + if record.is_portable { + let location_label = if let Some(ref mp) = record.mount_point { + format!("Portable ({})", mp) + } else { + "Portable (removable media)".to_string() + }; + let location_row = adw::ActionRow::builder() + .title("Location") + .subtitle(&location_label) + .build(); + let badge = widgets::status_badge("Portable", "info"); + badge.set_valign(gtk::Align::Center); + location_row.add_suffix(&badge); + size_group.add(&location_row); + } + if !fp.paths.is_empty() { let categories = [ ("Configuration", fp.config_size), diff --git a/src/ui/preferences.rs b/src/ui/preferences.rs index 64babc5..27fa269 100644 --- a/src/ui/preferences.rs +++ b/src/ui/preferences.rs @@ -265,6 +265,17 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage { }); automation_group.add(&auto_integrate_row); + let removable_row = adw::SwitchRow::builder() + .title(&i18n("Watch removable media")) + .subtitle(&i18n("Scan USB drives and other removable media for AppImages")) + .active(settings.boolean("watch-removable-media")) + .build(); + let settings_rem = settings.clone(); + removable_row.connect_active_notify(move |row| { + settings_rem.set_boolean("watch-removable-media", row.is_active()).ok(); + }); + automation_group.add(&removable_row); + page.add(&automation_group); // Backup group diff --git a/src/window.rs b/src/window.rs index f5ffe4b..f348270 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1058,6 +1058,17 @@ impl DriftwoodWindow { dirs.push(system_dir.to_string()); } + // Include removable media mount points if enabled + if settings.boolean("watch-removable-media") { + for mount in crate::core::portable::detect_removable_mounts() { + let mp = mount.mount_point.to_string_lossy().to_string(); + if !dirs.iter().any(|d| d == &mp) { + log::info!("Including removable mount: {} ({}, {})", mp, mount.device, mount.fs_type); + dirs.push(mp); + } + } + } + let toast_overlay = self.imp().toast_overlay.get().unwrap().clone(); let window_weak = self.downgrade(); @@ -1121,6 +1132,15 @@ impl DriftwoodWindow { new_count += 1; } + // Detect portable/removable media + if crate::core::portable::is_path_on_removable(&d.path) { + let mounts = crate::core::portable::detect_removable_mounts(); + let mount_point = mounts.iter() + .find(|m| d.path.starts_with(&m.mount_point)) + .map(|m| m.mount_point.to_string_lossy().to_string()); + bg_db.set_portable(id, true, mount_point.as_deref()).ok(); + } + // Mark for background analysis bg_db.update_analysis_status(id, "pending").ok(); needs_analysis.push((id, d.path.clone(), d.appimage_type.clone()));