Reduce features used on remote filesystems

This attempts to detect remote filesystems on Linux using the
/proc/self/mountinfo file and checking the filesystem against a
hardcoded list of remote filesystems. Remote filesystems will not
thumbnail, read file data to determine mime types, or calculate
directory sizes.
This commit is contained in:
Jeremy Soller 2025-04-29 14:42:23 -06:00
parent 8ced8b0551
commit dd98622cfa
6 changed files with 372 additions and 243 deletions

430
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -105,6 +105,9 @@ debug = true
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
fork = "0.2" fork = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.17"
[dev-dependencies] [dev-dependencies]
# cap-std = "3" # cap-std = "3"
# cap-tempfile = "3" # cap-tempfile = "3"

View file

@ -597,10 +597,12 @@ impl App {
// This allows handling paths as groups if possible, such as launching a single video // This allows handling paths as groups if possible, such as launching a single video
// player that is passed every path. // player that is passed every path.
let mut groups: HashMap<Mime, Vec<PathBuf>> = HashMap::new(); let mut groups: HashMap<Mime, Vec<PathBuf>> = HashMap::new();
for (mime, path) in paths for (mime, path) in paths.iter().map(|path| {
.iter() (
.map(|path| (mime_icon::mime_for_path(path), path.as_ref().to_owned())) mime_icon::mime_for_path(path, None, false),
{ path.as_ref().to_owned(),
)
}) {
groups.entry(mime).or_default().push(path); groups.entry(mime).or_default().push(path);
} }

View file

@ -3,7 +3,7 @@
use cosmic::widget::icon; use cosmic::widget::icon;
use mime_guess::Mime; use mime_guess::Mime;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{collections::HashMap, path::Path, sync::Mutex}; use std::{collections::HashMap, fs, path::Path, sync::Mutex};
pub const FALLBACK_MIME_ICON: &str = "text-x-generic"; pub const FALLBACK_MIME_ICON: &str = "text-x-generic";
@ -50,14 +50,26 @@ impl MimeIconCache {
} }
static MIME_ICON_CACHE: Lazy<Mutex<MimeIconCache>> = Lazy::new(|| Mutex::new(MimeIconCache::new())); static MIME_ICON_CACHE: Lazy<Mutex<MimeIconCache>> = Lazy::new(|| Mutex::new(MimeIconCache::new()));
pub fn mime_for_path<P: AsRef<Path>>(path: P) -> Mime { pub fn mime_for_path<P: AsRef<Path>>(
path: P,
metadata_opt: Option<&fs::Metadata>,
remote: bool,
) -> Mime {
let path = path.as_ref();
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
// Try the shared mime info cache first // Try the shared mime info cache first
let guess = mime_icon_cache let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
.shared_mime_info if remote {
.guess_mime_type() if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
.path(&path) gb.file_name(file_name);
.guess(); }
} else {
gb.path(&path);
}
if let Some(metadata) = metadata_opt {
gb.metadata(metadata.clone());
}
let guess = gb.guess();
if guess.uncertain() { if guess.uncertain() {
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info // If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
mime_guess::from_path(&path).first_or_octet_stream() mime_guess::from_path(&path).first_or_octet_stream()

View file

@ -963,7 +963,7 @@ impl Operation {
op_sel.selected.push(new_dir.clone()); op_sel.selected.push(new_dir.clone());
let controller = controller.clone(); let controller = controller.clone();
let mime = mime_for_path(path); let mime = mime_for_path(path, None, false);
let password = password.clone(); let password = password.clone();
match mime.essence_str() { match mime.essence_str() {
"application/gzip" | "application/x-compressed-tar" => { "application/gzip" | "application/x-compressed-tar" => {

View file

@ -524,6 +524,65 @@ fn hidden_attribute(metadata: &Metadata) -> bool {
metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN
} }
#[cfg(target_os = "linux")]
fn remote_fs(metadata: &Metadata) -> bool {
//TODO: method to reload remote filesystems dynamically
//TODO: fix for https://github.com/eminence/procfs/issues/262
static DEVICES: Lazy<HashMap<u64, bool>> = Lazy::new(|| {
let mut devices = HashMap::new();
match procfs::process::Process::myself() {
Ok(process) => match process.mountinfo() {
Ok(mount_infos) => {
for mount_info in mount_infos.iter() {
let mut parts = mount_info.majmin.split(':');
let Some(major_str) = parts.next() else {
continue;
};
let Some(minor_str) = parts.next() else {
continue;
};
let Ok(major) = major_str.parse::<libc::c_uint>() else {
continue;
};
let Ok(minor) = minor_str.parse::<libc::c_uint>() else {
continue;
};
let dev = libc::makedev(major, minor);
//TODO: make sure this list is exhaustive
let remote = [
"cifs",
//TODO: check with GVFS?
"fuse.gvfsd-fuse",
"fuse.rclone",
"fuse.sshfs",
"nfs",
"nfs4",
"smb",
"smb2",
]
.contains(&mount_info.fs_type.as_str());
devices.insert(dev, remote);
}
}
Err(err) => {
log::warn!("failed to get mount info: {err}");
}
},
Err(err) => {
log::warn!("failed to get process info: {err}");
}
}
devices
});
DEVICES.get(&metadata.dev()).map_or(false, |x| *x)
}
#[cfg(not(target_os = "linux"))]
fn remote_fs(_metadata: &Metadata) -> bool {
//TODO: support BSD, macOS, Windows?
false
}
pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) { pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
let entry = match freedesktop_entry_parser::parse_entry(path) { let entry = match freedesktop_entry_parser::parse_entry(path) {
Ok(ok) => ok, Ok(ok) => ok,
@ -554,6 +613,8 @@ pub fn item_from_entry(
let hidden = name.starts_with(".") || hidden_attribute(&metadata); let hidden = name.starts_with(".") || hidden_attribute(&metadata);
let remote = remote_fs(&metadata);
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
if metadata.is_dir() { if metadata.is_dir() {
( (
@ -564,7 +625,7 @@ pub fn item_from_entry(
folder_icon(&path, sizes.list_condensed()), folder_icon(&path, sizes.list_condensed()),
) )
} else { } else {
let mime = mime_for_path(&path); let mime = mime_for_path(&path, Some(&metadata), remote);
//TODO: clean this up, implement for trash //TODO: clean this up, implement for trash
let icon_name_opt = if mime == "application/x-desktop" { let icon_name_opt = if mime == "application/x-desktop" {
let (desktop_name_opt, icon_name_opt) = parse_desktop_file(&path); let (desktop_name_opt, icon_name_opt) = parse_desktop_file(&path);
@ -598,36 +659,39 @@ pub fn item_from_entry(
} }
}; };
let children = if metadata.is_dir() { let mut children_opt = None;
let mut dir_size = DirSize::NotDirectory;
if metadata.is_dir() && !remote {
dir_size = DirSize::Calculating(Controller::default());
//TODO: calculate children in the background (and make it cancellable?) //TODO: calculate children in the background (and make it cancellable?)
match fs::read_dir(&path) { match fs::read_dir(&path) {
Ok(entries) => entries.count(), Ok(entries) => {
children_opt = Some(entries.count());
}
Err(err) => { Err(err) => {
log::warn!("failed to read directory {:?}: {}", path, err); log::warn!("failed to read directory {:?}: {}", path, err);
0
} }
} }
} else { }
0
};
let dir_size = if metadata.is_dir() {
DirSize::Calculating(Controller::default())
} else {
DirSize::NotDirectory
};
Item { Item {
name, name,
display_name, display_name,
metadata: ItemMetadata::Path { metadata, children }, metadata: ItemMetadata::Path {
metadata,
children_opt,
},
hidden, hidden,
location_opt: Some(Location::Path(path)), location_opt: Some(Location::Path(path)),
mime, mime,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
thumbnail_opt: None, thumbnail_opt: if remote {
Some(ItemThumbnail::NotImage)
} else {
None
},
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
rect_opt: Cell::new(None), rect_opt: Cell::new(None),
@ -832,8 +896,8 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
folder_icon(&original_path, sizes.list_condensed()), folder_icon(&original_path, sizes.list_condensed()),
), ),
trash::TrashItemSize::Bytes(_) => { trash::TrashItemSize::Bytes(_) => {
//TODO: do not use original path // This passes remote = true so it does not read from the original path
let mime = mime_for_path(&original_path); let mime = mime_for_path(&original_path, None, true);
( (
mime.clone(), mime.clone(),
mime_icon(mime.clone(), sizes.grid()), mime_icon(mime.clone(), sizes.grid()),
@ -1316,7 +1380,7 @@ pub enum DirSize {
pub enum ItemMetadata { pub enum ItemMetadata {
Path { Path {
metadata: Metadata, metadata: Metadata,
children: usize, children_opt: Option<usize>,
}, },
Trash { Trash {
metadata: trash::TrashItemMetadata, metadata: trash::TrashItemMetadata,
@ -1646,16 +1710,23 @@ impl Item {
} }
} }
match &self.metadata { match &self.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path {
metadata,
children_opt,
} => {
if metadata.is_dir() { if metadata.is_dir() {
details = details.push(widget::text::body(fl!("items", items = children))); if let Some(children) = children_opt {
details = details.push(widget::text::body(fl!("items", items = children)));
}
let size = match &self.dir_size { let size = match &self.dir_size {
DirSize::Calculating(_) => fl!("calculating"), DirSize::Calculating(_) => fl!("calculating"),
DirSize::Directory(size) => format_size(*size), DirSize::Directory(size) => format_size(*size),
DirSize::NotDirectory => String::new(), DirSize::NotDirectory => String::new(),
DirSize::Error(err) => err.clone(), DirSize::Error(err) => err.clone(),
}; };
details = details.push(widget::text::body(fl!("item-size", size = size))); if !size.is_empty() {
details = details.push(widget::text::body(fl!("item-size", size = size)));
}
} else { } else {
details = details.push(widget::text::body(fl!( details = details.push(widget::text::body(fl!(
"item-size", "item-size",
@ -1769,9 +1840,14 @@ impl Item {
//TODO: translate! //TODO: translate!
//TODO: correct display of folder size? //TODO: correct display of folder size?
match &self.metadata { match &self.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path {
metadata,
children_opt,
} => {
if metadata.is_dir() { if metadata.is_dir() {
column = column.push(widget::text::body(format!("Items: {}", children))); if let Some(children) = children_opt {
column = column.push(widget::text::body(format!("Items: {}", children)));
}
} else { } else {
column = column.push(widget::text::body(format!( column = column.push(widget::text::body(format!(
"Size: {}", "Size: {}",
@ -3501,9 +3577,12 @@ impl Tab {
items.sort_by(|a, b| { items.sort_by(|a, b| {
// entries take precedence over size // entries take precedence over size
let get_size = |x: &Item| match &x.metadata { let get_size = |x: &Item| match &x.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path {
metadata,
children_opt,
} => {
if metadata.is_dir() { if metadata.is_dir() {
(true, *children as u64) (true, children_opt.unwrap_or_default() as u64)
} else { } else {
(false, metadata.len()) (false, metadata.len())
} }
@ -4518,13 +4597,20 @@ impl Tab {
}; };
let size_text = match &item.metadata { let size_text = match &item.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path {
metadata,
children_opt,
} => {
if metadata.is_dir() { if metadata.is_dir() {
//TODO: translate //TODO: translate
if *children == 1 { if let Some(children) = children_opt {
format!("{} item", children) if *children == 1 {
format!("{} item", children)
} else {
format!("{} items", children)
}
} else { } else {
format!("{} items", children) String::new()
} }
} else { } else {
format_size(metadata.len()) format_size(metadata.len())