Add portable mode with removable media detection and scanning
This commit is contained in:
@@ -124,5 +124,10 @@
|
|||||||
<summary>Security notification threshold</summary>
|
<summary>Security notification threshold</summary>
|
||||||
<description>Minimum CVE severity for desktop notifications: critical, high, medium, or low.</description>
|
<description>Minimum CVE severity for desktop notifications: critical, high, medium, or low.</description>
|
||||||
</key>
|
</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>
|
</schema>
|
||||||
</schemalist>
|
</schemalist>
|
||||||
|
|||||||
@@ -1786,6 +1786,14 @@ impl Database {
|
|||||||
Ok(())
|
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 ---
|
// --- 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)>> {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod integrator;
|
|||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod orphan;
|
pub mod orphan;
|
||||||
|
pub mod portable;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod security;
|
pub mod security;
|
||||||
pub mod updater;
|
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 {
|
if let Some(ref ws) = record.wayland_status {
|
||||||
let status = WaylandStatus::from_str(ws);
|
let status = WaylandStatus::from_str(ws);
|
||||||
if status != WaylandStatus::Unknown && status != WaylandStatus::Native {
|
if status != WaylandStatus::Unknown && status != WaylandStatus::Native {
|
||||||
|
|||||||
@@ -1800,6 +1800,22 @@ fn build_storage_tab(
|
|||||||
.build();
|
.build();
|
||||||
size_group.add(&appimage_row);
|
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() {
|
if !fp.paths.is_empty() {
|
||||||
let categories = [
|
let categories = [
|
||||||
("Configuration", fp.config_size),
|
("Configuration", fp.config_size),
|
||||||
|
|||||||
@@ -265,6 +265,17 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
|
|||||||
});
|
});
|
||||||
automation_group.add(&auto_integrate_row);
|
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);
|
page.add(&automation_group);
|
||||||
|
|
||||||
// Backup group
|
// Backup group
|
||||||
|
|||||||
@@ -1058,6 +1058,17 @@ impl DriftwoodWindow {
|
|||||||
dirs.push(system_dir.to_string());
|
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 toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
|
||||||
let window_weak = self.downgrade();
|
let window_weak = self.downgrade();
|
||||||
|
|
||||||
@@ -1121,6 +1132,15 @@ impl DriftwoodWindow {
|
|||||||
new_count += 1;
|
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
|
// Mark for background analysis
|
||||||
bg_db.update_analysis_status(id, "pending").ok();
|
bg_db.update_analysis_status(id, "pending").ok();
|
||||||
needs_analysis.push((id, d.path.clone(), d.appimage_type.clone()));
|
needs_analysis.push((id, d.path.clone(), d.appimage_type.clone()));
|
||||||
|
|||||||
Reference in New Issue
Block a user