Add portable mode with removable media detection and scanning
This commit is contained in:
@@ -124,5 +124,10 @@
|
||||
<summary>Security notification threshold</summary>
|
||||
<description>Minimum CVE severity for desktop notifications: critical, high, medium, or low.</description>
|
||||
</key>
|
||||
<key name="watch-removable-media" type="b">
|
||||
<default>false</default>
|
||||
<summary>Watch removable media</summary>
|
||||
<description>Scan removable drives for AppImages when mounted.</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
|
||||
@@ -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<Vec<(String, u64)>> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
71
src/core/portable.rs
Normal file
71
src/core/portable.rs
Normal file
@@ -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<MountInfo> {
|
||||
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")));
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,12 @@ pub fn build_priority_badge(record: &AppImageRecord) -> Option<gtk::Label> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
|
||||
Reference in New Issue
Block a user