cosmic-files/src/mounter/gvfs.rs

641 lines
26 KiB
Rust
Raw Normal View History

2024-04-22 13:14:25 -06:00
use cosmic::{
2024-10-21 13:51:10 -06:00
iced::{futures::SinkExt, stream, Subscription},
widget, Task,
2024-04-22 13:14:25 -06:00
};
use gio::{glib, prelude::*};
2025-07-15 10:55:21 -04:00
use std::{any::TypeId, cell::Cell, future::pending, path::PathBuf, sync::Arc};
2024-04-22 13:14:25 -06:00
use tokio::sync::{mpsc, Mutex};
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
2024-09-13 15:13:37 -06:00
use crate::{
config::IconSizes,
err_str,
tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location},
2024-09-13 15:13:37 -06:00
};
2025-07-15 22:07:28 -04:00
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
2024-04-22 13:14:25 -06:00
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
for name in themed_icon.names() {
let named = widget::icon::from_name(name.as_str()).size(size);
if let Some(path) = named.path() {
return Some(path);
}
}
}
//TODO: handle more gio icon types
None
}
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
let mut items = MounterItems::new();
for (i, mount) in monitor.mounts().into_iter().enumerate() {
items.push(MounterItem::Gvfs(Item {
uri: MountExt::root(&mount).uri().to_string(),
kind: ItemKind::Mount,
index: i,
name: MountExt::name(&mount).to_string(),
is_mounted: true,
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
path_opt: MountExt::root(&mount).path(),
}));
}
for (i, volume) in monitor.volumes().into_iter().enumerate() {
if volume.get_mount().is_some() {
// Volumes with mounts are already listed by mount
continue;
}
let uri = VolumeExt::activation_root(&volume)
.map(|f| f.uri().to_string())
.unwrap_or_default();
items.push(MounterItem::Gvfs(Item {
// TODO can we get URI for volumes with no mount?
uri,
kind: ItemKind::Volume,
index: i,
name: VolumeExt::name(&volume).to_string(),
is_mounted: false,
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
path_opt: None,
}));
}
items
}
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
2025-07-15 22:07:28 -04:00
let mut uri = uri.to_string();
let mut file = gio::File::for_uri(&uri);
let force_dir = uri.starts_with("network:///");
// Resolve the target-uri if it exists
if let Ok(file_info) = file.query_info(
TARGET_URI_ATTRIBUTE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
uri = resolved_uri.to_string();
file = gio::File::for_uri(&uri);
}
}
2024-09-13 15:13:37 -06:00
let mut items = Vec::new();
for info_res in file
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
.map_err(err_str)?
{
let info = info_res.map_err(err_str)?;
let name = info.name().to_string_lossy().to_string();
let display_name = info.display_name().to_string();
let uri = file.child(info.name()).uri().to_string();
2025-07-15 22:07:28 -04:00
2024-09-13 15:13:37 -06:00
//TODO: what is the best way to resolve shortcuts?
2025-07-15 22:07:28 -04:00
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
let is_dir = match info.file_type() {
gio::FileType::Directory => true,
_ => false,
};
let size_opt = match is_dir {
true => None,
false => Some(info.size() as u64),
};
let mut children_opt = None;
if is_dir {
if let Some(path) = file.child(&name).path() {
//TODO: calculate children in the background (and make it cancellable?)
match std::fs::read_dir(&path) {
Ok(entries) => {
children_opt = Some(entries.count());
}
Err(err) => {
log::warn!("failed to read directory {:?}: {}", path, err);
children_opt = Some(0);
2025-07-15 22:07:28 -04:00
}
}
} else {
children_opt = Some(0);
2025-07-15 22:07:28 -04:00
}
}
ItemMetadata::GvfsPath {
mtime,
size_opt,
children_opt,
}
} else {
ItemMetadata::SimpleDir { entries: 0 }
};
2024-09-13 15:13:37 -06:00
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
let file_icon = |size| {
info.icon()
.as_ref()
.and_then(|icon| gio_icon_to_path(icon, size))
.map(widget::icon::from_path)
2024-09-13 15:13:37 -06:00
.unwrap_or(
widget::icon::from_name(if metadata.is_dir() {
"folder"
} else {
"text-x-generic"
})
.size(size)
.handle(),
)
};
(
//TODO: get mime from content_type?
"inode/directory".parse().unwrap(),
file_icon(sizes.grid()),
file_icon(sizes.list()),
file_icon(sizes.list_condensed()),
)
};
items.push(tab::Item {
name,
is_mount_point: false,
2024-09-13 15:13:37 -06:00
display_name,
metadata,
hidden: false,
location_opt: Some(location),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(),
pos_opt: Cell::new(None),
rect_opt: Cell::new(None),
selected: false,
2024-10-16 10:22:25 +11:00
highlighted: false,
2024-09-13 15:13:37 -06:00
overlaps_drag_rect: false,
//TODO: scan directory size on gvfs mounts?
dir_size: DirSize::NotDirectory,
2025-04-28 23:35:27 +10:00
cut: false,
2024-09-13 15:13:37 -06:00
});
}
Ok(items)
}
2024-09-16 09:09:21 -06:00
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
let mount_op = gio::MountOperation::new();
mount_op.connect_ask_password(
move |mount_op, message, default_user, default_domain, flags| {
let auth = MounterAuth {
message: message.to_string(),
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
Some(default_user.to_string())
} else {
None
},
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
Some(default_domain.to_string())
} else {
None
},
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
Some(String::new())
} else {
None
},
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
Some(false)
} else {
None
},
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
Some(false)
} else {
None
},
};
let (auth_tx, mut auth_rx) = mpsc::channel(1);
event_tx
.send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
.unwrap();
//TODO: async recv?
if let Some(auth) = auth_rx.blocking_recv() {
if auth.anonymous_opt == Some(true) {
mount_op.set_anonymous(true);
} else {
mount_op.set_username(auth.username_opt.as_deref());
mount_op.set_domain(auth.domain_opt.as_deref());
mount_op.set_password(auth.password_opt.as_deref());
if auth.remember_opt == Some(true) {
mount_op.set_password_save(gio::PasswordSave::Permanently);
}
}
mount_op.reply(gio::MountOperationResult::Handled);
} else {
mount_op.reply(gio::MountOperationResult::Aborted);
}
},
);
mount_op
}
2024-04-22 13:14:25 -06:00
enum Cmd {
Items(IconSizes, mpsc::Sender<MounterItems>),
2024-04-22 13:14:25 -06:00
Rescan,
Mount(
MounterItem,
tokio::sync::oneshot::Sender<anyhow::Result<()>>,
),
NetworkDrive(String, tokio::sync::oneshot::Sender<anyhow::Result<()>>),
2024-09-13 15:13:37 -06:00
NetworkScan(
String,
IconSizes,
mpsc::Sender<Result<Vec<tab::Item>, String>>,
),
2024-04-24 13:59:55 -06:00
Unmount(MounterItem),
2024-04-22 13:14:25 -06:00
}
enum Event {
Changed,
Items(MounterItems),
MountResult(MounterItem, Result<bool, String>),
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
NetworkResult(String, Result<bool, String>),
2024-04-22 13:14:25 -06:00
}
#[derive(Clone, Debug)]
enum ItemKind {
Mount,
Volume,
}
//TODO: better method of matching items
#[derive(Clone, Debug)]
pub struct Item {
uri: String,
2024-04-22 13:14:25 -06:00
kind: ItemKind,
index: usize,
name: String,
is_mounted: bool,
icon_opt: Option<PathBuf>,
icon_symbolic_opt: Option<PathBuf>,
2024-04-22 13:14:25 -06:00
path_opt: Option<PathBuf>,
}
impl Item {
pub fn name(&self) -> String {
self.name.clone()
}
pub fn is_mounted(&self) -> bool {
self.is_mounted
}
pub fn uri(&self) -> String {
self.uri.clone()
}
pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {
if symbolic {
self.icon_symbolic_opt.as_ref()
} else {
self.icon_opt.as_ref()
}
.map(|icon| widget::icon::from_path(icon.clone()))
2024-04-22 13:14:25 -06:00
}
pub fn path(&self) -> Option<PathBuf> {
self.path_opt.clone()
}
}
pub struct Gvfs {
2024-04-22 13:14:25 -06:00
command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
}
impl Gvfs {
pub fn new() -> Self {
2024-04-22 13:14:25 -06:00
//TODO: switch to using gvfs-zbus which will better integrate with async rust
let (command_tx, mut command_rx) = mpsc::unbounded_channel();
let (event_tx, event_rx) = mpsc::unbounded_channel();
std::thread::spawn(move || {
let main_loop = glib::MainLoop::new(None, false);
main_loop.context().spawn_local(async move {
let monitor = gio::VolumeMonitor::get();
{
let event_tx = event_tx.clone();
monitor.connect_mount_changed(move |_monitor, mount| {
log::info!("mount changed {}", MountExt::name(mount));
2024-04-22 13:14:25 -06:00
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_mount_added(move |_monitor, mount| {
log::info!("mount added {}", MountExt::name(mount));
2024-04-22 13:14:25 -06:00
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_mount_removed(move |_monitor, mount| {
log::info!("mount removed {}", MountExt::name(mount));
2024-04-22 13:14:25 -06:00
event_tx.send(Event::Changed).unwrap();
});
}
2024-04-22 13:14:25 -06:00
{
let event_tx = event_tx.clone();
monitor.connect_volume_changed(move |_monitor, volume| {
log::info!("volume changed {}", VolumeExt::name(volume));
2024-04-22 13:14:25 -06:00
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_volume_added(move |_monitor, volume| {
log::info!("volume added {}", VolumeExt::name(volume));
2024-04-22 13:14:25 -06:00
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_volume_removed(move |_monitor, volume| {
log::info!("volume removed {}", VolumeExt::name(volume));
2024-04-22 13:14:25 -06:00
event_tx.send(Event::Changed).unwrap();
});
}
while let Some(command) = command_rx.recv().await {
match command {
Cmd::Items(sizes, items_tx) => {
items_tx.send(items(&monitor, sizes)).await.unwrap();
}
2024-04-22 13:14:25 -06:00
Cmd::Rescan => {
event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap();
2024-04-22 13:14:25 -06:00
}
Cmd::Mount(mounter_item, complete_tx) => {
let MounterItem::Gvfs(ref item) = mounter_item else {
_ = complete_tx.send(Err(anyhow::anyhow!("No mounter item")));
continue
};
let ItemKind::Volume = item.kind else {
_ = complete_tx.send(Err(anyhow::anyhow!("No mounter volume")));
continue
};
2024-04-22 13:14:25 -06:00
for (i, volume) in monitor.volumes().into_iter().enumerate() {
if i != item.index {
continue;
}
let name = VolumeExt::name(&volume);
if item.name != name {
log::warn!("trying to mount volume {} failed: name is {:?} when {:?} was expected", i, name, item.name);
continue;
}
log::info!("mount {}", name);
//TODO: do not use name as a URI for mount_op
let mount_op = mount_op(name.to_string(), event_tx.clone());
let event_tx = event_tx.clone();
let mounter_item = mounter_item.clone();
2024-04-22 13:14:25 -06:00
VolumeExt::mount(
&volume,
gio::MountMountFlags::NONE,
Some(&mount_op),
2024-04-22 13:14:25 -06:00
gio::Cancellable::NONE,
move |res| {
log::info!("mount {}: result {:?}", name, res);
event_tx.send(Event::MountResult(mounter_item, match res {
Ok(()) => {
_ = complete_tx.send(Ok(()));
Ok(true)
},
Err(err) => {
_ = complete_tx.send(Err(anyhow::anyhow!("{err:?}")));
match err.kind::<gio::IOErrorEnum>() {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{}", err))
}}
})).unwrap();
2024-04-22 13:14:25 -06:00
},
);
break;
2024-04-22 13:14:25 -06:00
}
}
Cmd::NetworkDrive(uri, result_tx) => {
let file = gio::File::for_uri(&uri);
2024-09-16 09:09:21 -06:00
let mount_op = mount_op(uri.clone(), event_tx.clone());
let event_tx = event_tx.clone();
file.mount_enclosing_volume(
gio::MountMountFlags::NONE,
Some(&mount_op),
gio::Cancellable::NONE,
move |res| {
log::info!("network drive {}: result {:?}", uri, res);
event_tx.send(Event::NetworkResult(uri, match res {
Ok(()) => {
_ = result_tx.send(Ok(()));
Ok(true)},
Err(err) => {
_ = result_tx.send(Err(anyhow::anyhow!("{err:?}")));
match err.kind::<gio::IOErrorEnum>() {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{}", err))
}}
})).unwrap();
}
);
}
2025-07-15 22:07:28 -04:00
Cmd::NetworkScan(mut uri, sizes, items_tx) => {
let original_uri = uri.clone();
let mut file = gio::File::for_uri(&uri);
if let Ok(file_info) = file.query_info(
TARGET_URI_ATTRIBUTE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
) {
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
uri = resolved_uri.to_string();
file = gio::File::for_uri(&uri);
}
}
let needs_mount = uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) {
2024-09-16 09:09:21 -06:00
Ok(_) => false,
Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
2024-09-16 09:09:21 -06:00
};
2025-07-15 22:07:28 -04:00
2024-09-16 09:09:21 -06:00
if needs_mount {
let mount_op = mount_op(uri.clone(), event_tx.clone());
let event_tx = event_tx.clone();
file.mount_enclosing_volume(
gio::MountMountFlags::empty(),
Some(&mount_op),
gio::Cancellable::NONE,
move |res| {
log::info!("network scan mounted {}: result {:?}", uri, res);
2025-07-15 22:07:28 -04:00
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
// seems to be related to uri with a path
items_tx.blocking_send(network_scan(&original_uri, sizes)).unwrap();
2024-09-16 09:09:21 -06:00
event_tx.send(Event::NetworkResult(uri, match res {
Ok(()) => {
Ok(true)
},
Err(err) => match err.kind::<gio::IOErrorEnum>() {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{}", err))
}
})).unwrap();
}
);
} else {
2025-07-15 22:07:28 -04:00
items_tx.send(network_scan(&original_uri, sizes)).await.unwrap();
2024-09-16 09:09:21 -06:00
}
2024-09-13 15:13:37 -06:00
}
2024-04-24 13:59:55 -06:00
Cmd::Unmount(mounter_item) => {
let MounterItem::Gvfs(item) = mounter_item else { continue };
let ItemKind::Mount = item.kind else { continue };
for (i, mount) in monitor.mounts().into_iter().enumerate() {
if i != item.index {
continue;
}
let name = MountExt::name(&mount);
if item.name != name {
log::warn!("trying to unmount mount {} failed: name is {:?} when {:?} was expected", i, name, item.name);
continue;
}
if MountExt::can_eject(&mount) {
log::info!("eject {}", name);
MountExt::eject_with_operation(
&mount,
gio::MountUnmountFlags::NONE,
gio::MountOperation::NONE,
gio::Cancellable::NONE,
move |result| {
log::info!("eject {}: result {:?}", name, result);
},
);
} else {
log::info!("unmount {}", name);
MountExt::unmount_with_operation(
&mount,
gio::MountUnmountFlags::NONE,
gio::MountOperation::NONE,
gio::Cancellable::NONE,
move |result| {
log::info!("unmount {}: result {:?}", name, result);
},
);
}
2024-04-24 13:59:55 -06:00
}
}
2024-04-22 13:14:25 -06:00
}
}
});
main_loop.run()
});
Self {
command_tx,
event_rx: Arc::new(Mutex::new(event_rx)),
}
}
}
2024-04-22 13:14:25 -06:00
impl Mounter for Gvfs {
fn items(&self, sizes: IconSizes) -> Option<MounterItems> {
let (items_tx, mut items_rx) = mpsc::channel(1);
self.command_tx.send(Cmd::Items(sizes, items_tx)).unwrap();
items_rx.blocking_recv()
}
2024-10-21 13:51:10 -06:00
fn mount(&self, item: MounterItem) -> Task<()> {
2024-04-22 13:14:25 -06:00
let command_tx = self.command_tx.clone();
2024-10-21 13:51:10 -06:00
Task::perform(
2024-04-22 13:14:25 -06:00
async move {
let (res_tx, res_rx) = tokio::sync::oneshot::channel();
command_tx.send(Cmd::Mount(item, res_tx)).unwrap();
res_rx.await
},
|x| {
if let Err(err) = x {
log::error!("{err:?}");
}
2024-04-22 13:14:25 -06:00
},
)
}
2024-10-21 13:51:10 -06:00
fn network_drive(&self, uri: String) -> Task<()> {
let command_tx = self.command_tx.clone();
2024-10-21 13:51:10 -06:00
Task::perform(
async move {
let (res_tx, res_rx) = tokio::sync::oneshot::channel();
command_tx.send(Cmd::NetworkDrive(uri, res_tx)).unwrap();
res_rx.await
},
|x| {
if let Err(err) = x {
log::error!("{err:?}");
}
},
)
}
2025-07-15 10:55:21 -04:00
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>> {
2024-09-13 15:13:37 -06:00
let (items_tx, mut items_rx) = mpsc::channel(1);
self.command_tx
2025-07-15 10:55:21 -04:00
.send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx))
2024-09-13 15:13:37 -06:00
.unwrap();
items_rx.blocking_recv()
}
2024-10-21 13:51:10 -06:00
fn unmount(&self, item: MounterItem) -> Task<()> {
2024-04-24 13:59:55 -06:00
let command_tx = self.command_tx.clone();
2024-10-21 13:51:10 -06:00
Task::perform(
2024-04-24 13:59:55 -06:00
async move {
command_tx.send(Cmd::Unmount(item)).unwrap();
},
|x| x,
)
}
2024-10-21 13:51:10 -06:00
fn subscription(&self) -> Subscription<MounterMessage> {
2024-04-22 13:14:25 -06:00
let command_tx = self.command_tx.clone();
let event_rx = self.event_rx.clone();
2024-10-21 13:51:10 -06:00
Subscription::run_with_id(
TypeId::of::<Self>(),
stream::channel(1, |mut output| async move {
command_tx.send(Cmd::Rescan).unwrap();
while let Some(event) = event_rx.lock().await.recv().await {
match event {
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
Event::Items(items) => {
output.send(MounterMessage::Items(items)).await.unwrap()
}
Event::MountResult(item, res) => output
.send(MounterMessage::MountResult(item, res))
.await
.unwrap(),
Event::NetworkAuth(uri, auth, auth_tx) => output
.send(MounterMessage::NetworkAuth(uri, auth, auth_tx))
.await
.unwrap(),
Event::NetworkResult(uri, res) => output
.send(MounterMessage::NetworkResult(uri, res))
.await
.unwrap(),
}
}
2024-10-21 13:51:10 -06:00
pending().await
}),
)
}
}