tokio recommends using a sync mutex with a notifier instead of the async mutex where possible. Rust forbids holding a sync mutex guard across await points so we can prevent a potential deadlock this way. This adds a custom channel based on the tokio mpmc example for handling gvfs events from callbacks to avoid the async mutex requirement. Messages are held in a `VecDeque` behind a sync mutex and the receiver will get notified via the notifier when a message is added to the queue. Weak references used in gio callbacks in case the sender is dropped by the application.
754 lines
31 KiB
Rust
754 lines
31 KiB
Rust
use cosmic::{
|
|
Task,
|
|
iced::{Subscription, futures::SinkExt, stream},
|
|
widget,
|
|
};
|
|
use gio::{glib, prelude::*};
|
|
use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc};
|
|
use tokio::sync::mpsc;
|
|
|
|
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
|
use crate::{
|
|
config::IconSizes,
|
|
err_str,
|
|
tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location},
|
|
};
|
|
|
|
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
|
|
|
|
fn resolve_uri(uri: &str) -> (String, gio::File) {
|
|
let file = gio::File::for_uri(uri);
|
|
// Resolve the target-uri if it exists
|
|
if let Ok(file_info) = file.query_info(
|
|
TARGET_URI_ATTRIBUTE,
|
|
gio::FileQueryInfoFlags::NONE,
|
|
gio::Cancellable::NONE,
|
|
) && let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE)
|
|
{
|
|
let resolved_uri = String::from(resolved_uri);
|
|
let file = gio::File::for_uri(&resolved_uri);
|
|
return (resolved_uri, file);
|
|
}
|
|
|
|
(uri.to_string(), file)
|
|
}
|
|
|
|
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 = (monitor.mounts().into_iter())
|
|
.enumerate()
|
|
// Hide shadowed mounts
|
|
.filter(|(_, mount)| !mount.is_shadowed())
|
|
.map(|(i, mount)| {
|
|
let root = MountExt::root(&mount);
|
|
let is_remote = root
|
|
.query_filesystem_info(
|
|
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
|
gio::Cancellable::NONE,
|
|
)
|
|
.ok()
|
|
.map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
|
|
.unwrap_or(true); // Default to remote if query fails
|
|
|
|
MounterItem::Gvfs(Item {
|
|
uri: mount.root().uri().into(),
|
|
kind: ItemKind::Mount,
|
|
index: i,
|
|
name: mount.name().into(),
|
|
is_mounted: true,
|
|
is_remote,
|
|
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: root.path(),
|
|
})
|
|
})
|
|
.collect();
|
|
items.extend(
|
|
(monitor.volumes().into_iter())
|
|
.enumerate()
|
|
// Volumes with mounts are already listed by mount
|
|
.filter(|(_, volume)| volume.get_mount().is_none())
|
|
.map(|(i, volume)| {
|
|
let uri = VolumeExt::activation_root(&volume)
|
|
.map(|f| f.uri().into())
|
|
.unwrap_or_default();
|
|
MounterItem::Gvfs(Item {
|
|
// TODO can we get URI for volumes with no mount?
|
|
uri,
|
|
kind: ItemKind::Volume,
|
|
index: i,
|
|
name: volume.name().into(),
|
|
is_mounted: false,
|
|
is_remote: 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> {
|
|
let force_dir = uri.starts_with("network:///");
|
|
let (_, file) = resolve_uri(uri);
|
|
|
|
// Read .hidden file if present
|
|
let hidden_files: Box<[String]> = if let Some(path) = file.path() {
|
|
let hidden_file_path = path.join(".hidden");
|
|
if hidden_file_path.is_file() {
|
|
tab::parse_hidden_file(&hidden_file_path)
|
|
} else {
|
|
Box::from([])
|
|
}
|
|
} else {
|
|
Box::from([])
|
|
};
|
|
|
|
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().into_owned();
|
|
let display_name = String::from(info.display_name());
|
|
|
|
let uri = String::from(file.child(info.name()).uri());
|
|
|
|
//TODO: what is the best way to resolve shortcuts?
|
|
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 = matches!(info.file_type(), gio::FileType::Directory);
|
|
let size_opt = (!is_dir).then_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.display(), err);
|
|
children_opt = Some(0);
|
|
}
|
|
}
|
|
} else {
|
|
children_opt = Some(0);
|
|
}
|
|
}
|
|
ItemMetadata::GvfsPath {
|
|
mtime,
|
|
size_opt,
|
|
children_opt,
|
|
}
|
|
} else {
|
|
ItemMetadata::SimpleDir { entries: 0 }
|
|
};
|
|
|
|
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)
|
|
.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()),
|
|
)
|
|
};
|
|
|
|
// Check if item is hidden
|
|
let hidden = name.starts_with('.')
|
|
|| info.boolean(gio::FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)
|
|
|| hidden_files.contains(&name);
|
|
|
|
items.push(tab::Item {
|
|
name,
|
|
is_mount_point: false,
|
|
display_name,
|
|
metadata,
|
|
hidden,
|
|
location_opt: Some(location),
|
|
image_dimensions: None,
|
|
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,
|
|
highlighted: false,
|
|
overlaps_drag_rect: false,
|
|
//TODO: scan directory size on gvfs mounts?
|
|
dir_size: DirSize::NotDirectory,
|
|
cut: false,
|
|
});
|
|
}
|
|
Ok(items)
|
|
}
|
|
|
|
fn dir_info(uri: &str) -> Result<(String, String, Option<PathBuf>), glib::Error> {
|
|
let (resolved_uri, file) = resolve_uri(uri);
|
|
let info = file.query_info(
|
|
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
|
|
gio::FileQueryInfoFlags::NONE,
|
|
gio::Cancellable::NONE,
|
|
)?;
|
|
|
|
Ok((resolved_uri, info.display_name().into(), file.path()))
|
|
}
|
|
|
|
fn mount_op(
|
|
uri: String,
|
|
event_tx: std::sync::Weak<crate::channel::Sender<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: flags
|
|
.contains(gio::AskPasswordFlags::NEED_USERNAME)
|
|
.then(|| default_user.to_string()),
|
|
domain_opt: flags
|
|
.contains(gio::AskPasswordFlags::NEED_DOMAIN)
|
|
.then(|| default_domain.to_string()),
|
|
password_opt: flags
|
|
.contains(gio::AskPasswordFlags::NEED_PASSWORD)
|
|
.then(String::new),
|
|
remember_opt: flags
|
|
.contains(gio::AskPasswordFlags::SAVING_SUPPORTED)
|
|
.then_some(false),
|
|
anonymous_opt: flags
|
|
.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED)
|
|
.then_some(false),
|
|
};
|
|
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx));
|
|
}
|
|
//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
|
|
}
|
|
|
|
enum Cmd {
|
|
Items(IconSizes, mpsc::Sender<MounterItems>),
|
|
Rescan,
|
|
Mount(
|
|
MounterItem,
|
|
tokio::sync::oneshot::Sender<anyhow::Result<()>>,
|
|
),
|
|
NetworkDrive(String, tokio::sync::oneshot::Sender<anyhow::Result<()>>),
|
|
NetworkScan(
|
|
String,
|
|
IconSizes,
|
|
mpsc::Sender<Result<Vec<tab::Item>, String>>,
|
|
),
|
|
DirInfo(
|
|
String,
|
|
mpsc::Sender<Result<(String, String, Option<PathBuf>), glib::Error>>,
|
|
),
|
|
Unmount(MounterItem),
|
|
}
|
|
|
|
enum Event {
|
|
Changed,
|
|
Items(MounterItems),
|
|
MountResult(MounterItem, Result<bool, String>),
|
|
NetworkAuth(String, MounterAuth, mpsc::Sender<MounterAuth>),
|
|
NetworkResult(String, Result<bool, String>),
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum ItemKind {
|
|
Mount,
|
|
Volume,
|
|
}
|
|
|
|
//TODO: better method of matching items
|
|
#[derive(Clone, Debug)]
|
|
pub struct Item {
|
|
uri: String,
|
|
kind: ItemKind,
|
|
index: usize,
|
|
name: String,
|
|
is_mounted: bool,
|
|
is_remote: bool,
|
|
icon_opt: Option<PathBuf>,
|
|
icon_symbolic_opt: Option<PathBuf>,
|
|
path_opt: Option<PathBuf>,
|
|
}
|
|
|
|
impl Item {
|
|
pub fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
pub const fn is_mounted(&self) -> bool {
|
|
self.is_mounted
|
|
}
|
|
|
|
pub const fn is_remote(&self) -> bool {
|
|
self.is_remote
|
|
}
|
|
|
|
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()))
|
|
}
|
|
|
|
pub fn path(&self) -> Option<PathBuf> {
|
|
self.path_opt.clone()
|
|
}
|
|
}
|
|
|
|
pub struct Gvfs {
|
|
command_tx: mpsc::UnboundedSender<Cmd>,
|
|
event_rx: Arc<crate::channel::Receiver<Event>>,
|
|
}
|
|
|
|
impl Gvfs {
|
|
pub fn new() -> Self {
|
|
//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) = crate::channel::channel();
|
|
let event_tx = Arc::new(event_tx);
|
|
std::thread::spawn(move || {
|
|
let main_loop = glib::MainLoop::new(None, false);
|
|
main_loop.context().spawn_local(async move {
|
|
let event_tx = Arc::downgrade(&event_tx);
|
|
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));
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::Changed);
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let event_tx = event_tx.clone();
|
|
monitor.connect_mount_added(move |_monitor, mount| {
|
|
log::info!("mount added {}", MountExt::name(mount));
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::Changed);
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let event_tx = event_tx.clone();
|
|
monitor.connect_mount_removed(move |_monitor, mount| {
|
|
log::info!("mount removed {}", MountExt::name(mount));
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::Changed);
|
|
}
|
|
});
|
|
}
|
|
|
|
{
|
|
let event_tx = event_tx.clone();
|
|
monitor.connect_volume_changed(move |_monitor, volume| {
|
|
log::info!("volume changed {}", VolumeExt::name(volume));
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::Changed);
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let event_tx = event_tx.clone();
|
|
monitor.connect_volume_added(move |_monitor, volume| {
|
|
log::info!("volume added {}", VolumeExt::name(volume));
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::Changed);
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let event_tx = event_tx.clone();
|
|
monitor.connect_volume_removed(move |_monitor, volume| {
|
|
log::info!("volume removed {}", VolumeExt::name(volume));
|
|
if let Some(event_tx) = event_tx.upgrade() {
|
|
event_tx.send(Event::Changed);
|
|
}
|
|
});
|
|
}
|
|
|
|
while let Some(command) = command_rx.recv().await {
|
|
match command {
|
|
Cmd::Items(sizes, items_tx) => {
|
|
items_tx.send(items(&monitor, sizes)).await.unwrap();
|
|
}
|
|
Cmd::Rescan => {
|
|
let Some(event_tx) = event_tx.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
event_tx.send(Event::Items(items(&monitor, IconSizes::default())));
|
|
}
|
|
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
|
|
};
|
|
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();
|
|
let volume_for_callback = volume.clone();
|
|
VolumeExt::mount(
|
|
&volume,
|
|
gio::MountMountFlags::NONE,
|
|
Some(&mount_op),
|
|
gio::Cancellable::NONE,
|
|
move |res| {
|
|
log::info!("mount {name}: result {res:?}");
|
|
// Update the mounter_item with mount information after successful mount
|
|
let mut updated_item = mounter_item.clone();
|
|
if res.is_ok()
|
|
&& let MounterItem::Gvfs(ref mut item) = updated_item
|
|
&& let Some(mount) = volume_for_callback.get_mount() {
|
|
let root = MountExt::root(&mount);
|
|
item.path_opt = root.path();
|
|
item.is_mounted = true;
|
|
// Query if remote
|
|
item.is_remote = root
|
|
.query_filesystem_info(
|
|
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
|
gio::Cancellable::NONE,
|
|
)
|
|
.ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
|
|
.unwrap_or(true);
|
|
}
|
|
let Some(event_tx) = event_tx.upgrade() else {
|
|
return;
|
|
};
|
|
event_tx.send(Event::MountResult(updated_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}"))
|
|
}}
|
|
}));
|
|
},
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
Cmd::NetworkDrive(uri, result_tx) => {
|
|
let file = gio::File::for_uri(&uri);
|
|
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 {uri}: result {res:?}");
|
|
let Some(event_tx) = event_tx.upgrade() else {
|
|
return;
|
|
};
|
|
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}"))
|
|
}}
|
|
}));
|
|
}
|
|
);
|
|
}
|
|
Cmd::NetworkScan(uri, sizes, items_tx) => {
|
|
let (resolved_uri, file) = resolve_uri(&uri);
|
|
|
|
let needs_mount = resolved_uri != "network:///" && match file.find_enclosing_mount(gio::Cancellable::NONE) {
|
|
Ok(_) => false,
|
|
Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
|
|
};
|
|
|
|
if needs_mount {
|
|
let mount_op = mount_op(resolved_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 {resolved_uri}: result {res:?}");
|
|
// 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(&uri, sizes)).unwrap();
|
|
let Some(event_tx) = event_tx.upgrade() else {
|
|
return;
|
|
};
|
|
event_tx.send(Event::NetworkResult(resolved_uri, match res {
|
|
Ok(()) => {
|
|
Ok(true)
|
|
},
|
|
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
|
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
|
_ => Err(format!("{err}"))
|
|
}
|
|
}));
|
|
}
|
|
);
|
|
} else {
|
|
items_tx.send(network_scan(&uri, sizes)).await.unwrap();
|
|
}
|
|
}
|
|
Cmd::DirInfo(uri, result_tx) => {
|
|
result_tx.send(dir_info(&uri)).await.unwrap();
|
|
}
|
|
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 {name}: result {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 {name}: result {result:?}");
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
main_loop.run();
|
|
});
|
|
Self {
|
|
command_tx,
|
|
event_rx: Arc::new(event_rx),
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
fn mount(&self, item: MounterItem) -> Task<()> {
|
|
let command_tx = self.command_tx.clone();
|
|
Task::perform(
|
|
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:?}");
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
fn network_drive(&self, uri: String) -> Task<()> {
|
|
let command_tx = self.command_tx.clone();
|
|
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:?}");
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>> {
|
|
let (items_tx, mut items_rx) = mpsc::channel(1);
|
|
self.command_tx
|
|
.send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx))
|
|
.unwrap();
|
|
items_rx.blocking_recv()
|
|
}
|
|
|
|
fn dir_info(&self, uri: &str) -> Option<(String, String, Option<PathBuf>)> {
|
|
let (result_tx, mut result_rx) = mpsc::channel(1);
|
|
self.command_tx
|
|
.send(Cmd::DirInfo(uri.to_string(), result_tx))
|
|
.unwrap();
|
|
result_rx.blocking_recv().and_then(|res| res.ok())
|
|
}
|
|
|
|
fn unmount(&self, item: MounterItem) -> Task<()> {
|
|
let command_tx = self.command_tx.clone();
|
|
Task::future(async move {
|
|
command_tx.send(Cmd::Unmount(item)).unwrap();
|
|
})
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<MounterMessage> {
|
|
let command_tx = self.command_tx.clone();
|
|
let event_rx = self.event_rx.clone();
|
|
struct Wrapper {
|
|
command_tx: mpsc::UnboundedSender<Cmd>,
|
|
event_rx: Arc<crate::channel::Receiver<Event>>,
|
|
}
|
|
impl Hash for Wrapper {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
TypeId::of::<Self>().hash(state);
|
|
}
|
|
}
|
|
Subscription::run_with(
|
|
Wrapper {
|
|
command_tx,
|
|
event_rx,
|
|
},
|
|
|Wrapper {
|
|
command_tx,
|
|
event_rx,
|
|
}| {
|
|
let command_tx = command_tx.clone();
|
|
let event_rx = event_rx.clone();
|
|
stream::channel(
|
|
1,
|
|
move |mut output: cosmic::iced::futures::channel::mpsc::Sender<
|
|
MounterMessage,
|
|
>| async move {
|
|
command_tx.send(Cmd::Rescan).unwrap();
|
|
while let Some(event) = event_rx.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(),
|
|
}
|
|
}
|
|
pending().await
|
|
},
|
|
)
|
|
},
|
|
)
|
|
}
|
|
}
|