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]
fork = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.17"
[dev-dependencies]
# cap-std = "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
// player that is passed every path.
let mut groups: HashMap<Mime, Vec<PathBuf>> = HashMap::new();
for (mime, path) in paths
.iter()
.map(|path| (mime_icon::mime_for_path(path), path.as_ref().to_owned()))
{
for (mime, path) in paths.iter().map(|path| {
(
mime_icon::mime_for_path(path, None, false),
path.as_ref().to_owned(),
)
}) {
groups.entry(mime).or_default().push(path);
}

View file

@ -3,7 +3,7 @@
use cosmic::widget::icon;
use mime_guess::Mime;
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";
@ -50,14 +50,26 @@ impl MimeIconCache {
}
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();
// Try the shared mime info cache first
let guess = mime_icon_cache
.shared_mime_info
.guess_mime_type()
.path(&path)
.guess();
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
if remote {
if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
gb.file_name(file_name);
}
} else {
gb.path(&path);
}
if let Some(metadata) = metadata_opt {
gb.metadata(metadata.clone());
}
let guess = gb.guess();
if guess.uncertain() {
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
mime_guess::from_path(&path).first_or_octet_stream()

View file

@ -963,7 +963,7 @@ impl Operation {
op_sel.selected.push(new_dir.clone());
let controller = controller.clone();
let mime = mime_for_path(path);
let mime = mime_for_path(path, None, false);
let password = password.clone();
match mime.essence_str() {
"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
}
#[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>) {
let entry = match freedesktop_entry_parser::parse_entry(path) {
Ok(ok) => ok,
@ -554,6 +613,8 @@ pub fn item_from_entry(
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) =
if metadata.is_dir() {
(
@ -564,7 +625,7 @@ pub fn item_from_entry(
folder_icon(&path, sizes.list_condensed()),
)
} else {
let mime = mime_for_path(&path);
let mime = mime_for_path(&path, Some(&metadata), remote);
//TODO: clean this up, implement for trash
let icon_name_opt = if mime == "application/x-desktop" {
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?)
match fs::read_dir(&path) {
Ok(entries) => entries.count(),
Ok(entries) => {
children_opt = Some(entries.count());
}
Err(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 {
name,
display_name,
metadata: ItemMetadata::Path { metadata, children },
metadata: ItemMetadata::Path {
metadata,
children_opt,
},
hidden,
location_opt: Some(Location::Path(path)),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
thumbnail_opt: None,
thumbnail_opt: if remote {
Some(ItemThumbnail::NotImage)
} else {
None
},
button_id: widget::Id::unique(),
pos_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()),
),
trash::TrashItemSize::Bytes(_) => {
//TODO: do not use original path
let mime = mime_for_path(&original_path);
// This passes remote = true so it does not read from the original path
let mime = mime_for_path(&original_path, None, true);
(
mime.clone(),
mime_icon(mime.clone(), sizes.grid()),
@ -1316,7 +1380,7 @@ pub enum DirSize {
pub enum ItemMetadata {
Path {
metadata: Metadata,
children: usize,
children_opt: Option<usize>,
},
Trash {
metadata: trash::TrashItemMetadata,
@ -1646,16 +1710,23 @@ impl Item {
}
}
match &self.metadata {
ItemMetadata::Path { metadata, children } => {
ItemMetadata::Path {
metadata,
children_opt,
} => {
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 {
DirSize::Calculating(_) => fl!("calculating"),
DirSize::Directory(size) => format_size(*size),
DirSize::NotDirectory => String::new(),
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 {
details = details.push(widget::text::body(fl!(
"item-size",
@ -1769,9 +1840,14 @@ impl Item {
//TODO: translate!
//TODO: correct display of folder size?
match &self.metadata {
ItemMetadata::Path { metadata, children } => {
ItemMetadata::Path {
metadata,
children_opt,
} => {
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 {
column = column.push(widget::text::body(format!(
"Size: {}",
@ -3501,9 +3577,12 @@ impl Tab {
items.sort_by(|a, b| {
// entries take precedence over size
let get_size = |x: &Item| match &x.metadata {
ItemMetadata::Path { metadata, children } => {
ItemMetadata::Path {
metadata,
children_opt,
} => {
if metadata.is_dir() {
(true, *children as u64)
(true, children_opt.unwrap_or_default() as u64)
} else {
(false, metadata.len())
}
@ -4518,13 +4597,20 @@ impl Tab {
};
let size_text = match &item.metadata {
ItemMetadata::Path { metadata, children } => {
ItemMetadata::Path {
metadata,
children_opt,
} => {
if metadata.is_dir() {
//TODO: translate
if *children == 1 {
format!("{} item", children)
if let Some(children) = children_opt {
if *children == 1 {
format!("{} item", children)
} else {
format!("{} items", children)
}
} else {
format!("{} items", children)
String::new()
}
} else {
format_size(metadata.len())