Fs watcher (#113)

fs watcher
This commit is contained in:
Christian Visintin
2022-06-09 13:03:02 +02:00
committed by GitHub
parent 2caa0432df
commit 816270d545
25 changed files with 1665 additions and 47 deletions

View File

@@ -27,7 +27,7 @@
*/
pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg,
TransferOpts, TransferPayload,
TransferMsg, TransferOpts, TransferPayload, UiMsg,
};
pub(self) use remotefs::File;
use tuirealm::{State, StateValue};
@@ -47,6 +47,7 @@ pub(crate) mod rename;
pub(crate) mod save;
pub(crate) mod submit;
pub(crate) mod symlink;
pub(crate) mod watcher;
#[derive(Debug)]
pub(crate) enum SelectedFile {

View File

@@ -96,7 +96,7 @@ impl FileTransferActivity {
}
}
fn remote_rename_file(&mut self, entry: &File, dest: &Path) {
pub(crate) fn remote_rename_file(&mut self, entry: &File, dest: &Path) {
match self.client.as_mut().mov(entry.path(), dest) {
Ok(_) => {
self.log(

View File

@@ -0,0 +1,134 @@
//! # watcher actions
//!
//! actions associated to the file watcher
use super::{FileTransferActivity, LogLevel, Msg, SelectedFile, TransferMsg, UiMsg};
use std::path::{Path, PathBuf};
impl FileTransferActivity {
pub fn action_show_radio_watch(&mut self) {
// return if fswatcher is not working
if self.fswatcher.is_none() {
return;
}
// get local entry
if let Some((watched, local, remote)) = self.get_watcher_dirs() {
self.mount_radio_watch(
watched,
local.to_string_lossy().to_string().as_str(),
remote.to_string_lossy().to_string().as_str(),
);
}
}
pub fn action_show_watched_paths_list(&mut self) {
// return if fswatcher is not working
if self.fswatcher.is_none() {
return;
}
let watched_paths: Vec<PathBuf> = self
.map_on_fswatcher(|w| w.watched_paths().iter().map(|p| p.to_path_buf()).collect())
.unwrap_or_default();
self.mount_watched_paths_list(watched_paths.as_slice());
}
pub fn action_toggle_watch(&mut self) {
// umount radio
self.umount_radio_watcher();
// return if fswatcher is not working
if self.fswatcher.is_none() {
return;
}
match self.get_watcher_dirs() {
Some((true, local, _)) => self.unwatch_path(&local),
Some((false, local, remote)) => self.watch_path(&local, &remote),
None => {}
}
}
pub fn action_toggle_watch_for(&mut self, index: usize) {
// umount
self.umount_watched_paths_list();
// return if fswatcher is not working
if self.fswatcher.is_none() {
return;
}
// get path
if let Some(path) = self
.map_on_fswatcher(|w| w.watched_paths().get(index).map(|p| p.to_path_buf()))
.flatten()
{
// ask whether to unwatch
self.mount_radio_watch(true, path.to_string_lossy().to_string().as_str(), "");
// wait for response
if let Msg::Transfer(TransferMsg::ToggleWatch) = self.wait_for_pending_msg(&[
Msg::Ui(UiMsg::CloseWatcherPopup),
Msg::Transfer(TransferMsg::ToggleWatch),
]) {
// unwatch path
self.unwatch_path(&path);
}
self.umount_radio_watcher();
}
self.action_show_watched_paths_list();
}
fn watch_path(&mut self, local: &Path, remote: &Path) {
debug!(
"tracking changes at {} to {}",
local.display(),
remote.display()
);
match self.map_on_fswatcher(|w| w.watch(local, remote)) {
Some(Ok(())) => {
self.log(
LogLevel::Info,
format!(
"changes to {} will now be synched with {}",
local.display(),
remote.display()
),
);
}
Some(Err(err)) => {
self.log_and_alert(
LogLevel::Error,
format!("could not track changes to {}: {}", local.display(), err),
);
}
None => {}
}
}
fn unwatch_path(&mut self, path: &Path) {
debug!("unwatching path at {}", path.display());
match self.map_on_fswatcher(|w| w.unwatch(path)) {
Some(Ok(path)) => {
self.log(
LogLevel::Info,
format!("{} is no longer watched", path.display()),
);
}
Some(Err(err)) => {
self.log_and_alert(LogLevel::Error, format!("could not unwatch path: {}", err));
}
None => {}
}
}
fn get_watcher_dirs(&mut self) -> Option<(bool, PathBuf, PathBuf)> {
if let SelectedFile::One(file) = self.get_local_selected_entries() {
// check if entry is already watched
let watched = self
.map_on_fswatcher(|w| w.watched(file.path()))
.unwrap_or(false);
// mount dialog
let mut remote = self.remote().wrkdir.clone();
remote.push(file.name().as_str());
Some((watched, file.path().to_path_buf(), remote))
} else {
None
}
}
}

View File

@@ -46,7 +46,7 @@ pub use popups::{
FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
};
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};

View File

@@ -683,6 +683,7 @@ impl KeybindingsPopup {
.step(8)
.highlighted_str("? ")
.title("Keybindings", Alignment::Center)
.rewind(true)
.rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(key_color))
@@ -759,9 +760,12 @@ impl KeybindingsPopup {
.add_col(TextSpan::new("<R|F6>").bold().fg(key_color))
.add_col(TextSpan::from(" Rename file"))
.add_row()
.add_col(TextSpan::new("<F2|S>").bold().fg(key_color))
.add_col(TextSpan::new("<S|F2>").bold().fg(key_color))
.add_col(TextSpan::from(" Save file as"))
.add_row()
.add_col(TextSpan::new("<T>").bold().fg(key_color))
.add_col(TextSpan::from(" Watch/unwatch file changes"))
.add_row()
.add_col(TextSpan::new("<U>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to parent directory"))
.add_row()
@@ -791,6 +795,9 @@ impl KeybindingsPopup {
.add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Interrupt file transfer"))
.add_row()
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color))
.add_col(TextSpan::from(" Show watched paths"))
.build(),
),
}
@@ -1030,7 +1037,7 @@ impl OpenWithPopup {
"Open file with…",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title("vscode", Alignment::Center),
.title("Type the program to open the file with", Alignment::Center),
}
}
}
@@ -1852,3 +1859,150 @@ impl Component<Msg, NoUserEvent> for WaitPopup {
None
}
}
#[derive(MockComponent)]
pub struct WatchedPathsList {
component: List,
}
impl WatchedPathsList {
pub fn new(paths: &[std::path::PathBuf], color: Color) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.rewind(true)
.scroll(true)
.step(4)
.highlighted_color(color)
.highlighted_str("")
.title(
"These files are currently synched with the remote host",
Alignment::Center,
)
.rows(
paths
.iter()
.map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())])
.collect(),
),
}
}
}
impl Component<Msg, NoUserEvent> for WatchedPathsList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseWatchedPathsList))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
// get state
if let State::One(StateValue::Usize(idx)) = self.component.state() {
Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx)))
} else {
Some(Msg::None)
}
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct WatcherPopup {
component: Radio,
}
impl WatcherPopup {
pub fn new(watched: bool, local: &str, remote: &str, color: Color) -> Self {
let text = match watched {
false => format!(r#"Synchronize changes from "{}" to "{}"?"#, local, remote),
true => format!(r#"Stop synchronizing changes at "{}"?"#, local),
};
Self {
component: Radio::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.title(text, Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for WatcherPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseWatcherPopup))
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::Transfer(TransferMsg::ToggleWatch))
} else {
Some(Msg::Ui(UiMsg::CloseWatcherPopup))
}
}
_ => None,
}
}
}

View File

@@ -306,6 +306,14 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('t'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowWatcherPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('t'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Ui(UiMsg::ShowWatchedPathsList)),
Event::Keyboard(KeyEvent {
code: Key::Char('u'),
modifiers: KeyModifiers::NONE,
@@ -478,6 +486,14 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char('s') | Key::Function(2),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('t'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowWatcherPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('t'),
modifiers: KeyModifiers::CONTROL,
}) => Some(Msg::Ui(UiMsg::ShowWatchedPathsList)),
Event::Keyboard(KeyEvent {
code: Key::Char('u'),
modifiers: KeyModifiers::NONE,

View File

@@ -0,0 +1,132 @@
use super::{FileTransferActivity, LogLevel, TransferPayload};
use crate::system::watcher::FsChange;
use std::path::Path;
impl FileTransferActivity {
/// poll file watcher
pub(super) fn poll_watcher(&mut self) {
if self.fswatcher.is_none() {
return;
}
let watcher = self.fswatcher.as_mut().unwrap();
match watcher.poll() {
Ok(None) => {}
Ok(Some(FsChange::Move(mov))) => {
debug!(
"fs watcher reported a `Move` from {} to {}",
mov.source().display(),
mov.destination().display()
);
self.move_watched_file(mov.source(), mov.destination());
}
Ok(Some(FsChange::Remove(remove))) => {
debug!(
"fs watcher reported a `Remove` of {}",
remove.path().display()
);
self.remove_watched_file(remove.path());
}
Ok(Some(FsChange::Update(update))) => {
debug!(
"fs watcher reported an `Update` from {} to {}",
update.local().display(),
update.remote().display()
);
self.upload_watched_file(update.local(), update.remote());
}
Err(err) => {
self.log(
LogLevel::Error,
format!("error while polling file watcher: {}", err),
);
}
}
}
fn move_watched_file(&mut self, source: &Path, destination: &Path) {
// stat remote file
trace!(
"renaming watched file {} to {}",
source.display(),
destination.display()
);
// stat fs entry
let origin = match self.client.stat(source) {
Ok(f) => f,
Err(err) => {
self.log(
LogLevel::Error,
format!(
"failed to stat file to rename {}: {}",
source.display(),
err
),
);
return;
}
};
// rename using action
self.remote_rename_file(&origin, destination)
}
fn remove_watched_file(&mut self, file: &Path) {
match self.client.remove_dir_all(file) {
Ok(()) => {
self.log(
LogLevel::Info,
format!("removed watched file at {}", file.display()),
);
}
Err(err) => {
self.log(
LogLevel::Error,
format!("failed to remove watched file {}: {}", file.display(), err),
);
}
}
}
fn upload_watched_file(&mut self, local: &Path, remote: &Path) {
// stat local file
let entry = match self.host.stat(local) {
Ok(e) => e,
Err(err) => {
self.log(
LogLevel::Error,
format!(
"failed to sync file {} with remote (stat failed): {}",
remote.display(),
err
),
);
return;
}
};
// send
trace!(
"syncing local file {} with remote {}",
local.display(),
remote.display()
);
let remote_path = remote.parent().unwrap_or_else(|| Path::new("/"));
match self.filetransfer_send(TransferPayload::Any(entry), remote_path, None) {
Ok(()) => {
self.log(
LogLevel::Info,
format!(
"synched watched file {} with {}",
local.display(),
remote.display()
),
);
}
Err(err) => {
self.log(
LogLevel::Error,
format!("failed to sync watched file {}: {}", remote.display(), err),
);
}
}
}
}

View File

@@ -81,6 +81,8 @@ impl FileTransferActivity {
self.log_records.push_front(record);
// Update log
self.update_logbox();
// flag redraw
self.redraw = true;
}
/// Add message to log events and also display it as an alert

View File

@@ -28,6 +28,7 @@
// This module is split into files, cause it's just too big
mod actions;
mod components;
mod fswatcher;
mod lib;
mod misc;
mod session;
@@ -41,6 +42,7 @@ use crate::explorer::{FileExplorer, FileSorting};
use crate::filetransfer::{Builder, FileTransferParams};
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
use crate::system::watcher::FsWatcher;
pub(self) use lib::browser;
use lib::browser::Browser;
use lib::transfer::{TransferOpts, TransferStates};
@@ -90,6 +92,8 @@ enum Id {
SymlinkPopup,
SyncBrowsingMkdirPopup,
WaitPopup,
WatchedPathsList,
WatcherPopup,
}
#[derive(Debug, PartialEq)]
@@ -128,6 +132,8 @@ enum TransferMsg {
RenameFile(String),
SaveFileAs(String),
SearchFile(String),
ToggleWatch,
ToggleWatchFor(usize),
TransferFile,
}
@@ -154,6 +160,8 @@ enum UiMsg {
CloseRenamePopup,
CloseSaveAsPopup,
CloseSymlinkPopup,
CloseWatchedPathsList,
CloseWatcherPopup,
Disconnect,
ExplorerBackTabbed,
LogBackTabbed,
@@ -175,6 +183,8 @@ enum UiMsg {
ShowRenamePopup,
ShowSaveAsPopup,
ShowSymlinkPopup,
ShowWatchedPathsList,
ShowWatcherPopup,
ToggleHiddenFiles,
ToggleSyncBrowsing,
WindowResized,
@@ -226,6 +236,8 @@ pub struct FileTransferActivity {
transfer: TransferStates,
/// Temporary directory where to store temporary stuff
cache: Option<TempDir>,
/// Fs watcher
fswatcher: Option<FsWatcher>,
}
impl FileTransferActivity {
@@ -251,6 +263,13 @@ impl FileTransferActivity {
Ok(d) => Some(d),
Err(_) => None,
},
fswatcher: match FsWatcher::init(Duration::from_secs(5)) {
Ok(w) => Some(w),
Err(e) => {
error!("failed to initialize fs watcher: {}", e);
None
}
},
}
}
@@ -315,6 +334,14 @@ impl FileTransferActivity {
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
/// Map a function to fs watcher if any
fn map_on_fswatcher<F, T>(&mut self, mapper: F) -> Option<T>
where
F: FnOnce(&mut FsWatcher) -> T,
{
self.fswatcher.as_mut().map(mapper)
}
}
/**
@@ -377,6 +404,8 @@ impl Activity for FileTransferActivity {
self.redraw = true;
}
self.tick();
// poll
self.poll_watcher();
// View
if self.redraw {
self.view();

View File

@@ -342,6 +342,8 @@ impl FileTransferActivity {
}
}
}
TransferMsg::ToggleWatch => self.action_toggle_watch(),
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
TransferMsg::TransferFile => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_send(),
@@ -421,6 +423,8 @@ impl FileTransferActivity {
UiMsg::CloseRenamePopup => self.umount_rename(),
UiMsg::CloseSaveAsPopup => self.umount_saveas(),
UiMsg::CloseSymlinkPopup => self.umount_symlink(),
UiMsg::CloseWatchedPathsList => self.umount_watched_paths_list(),
UiMsg::CloseWatcherPopup => self.umount_radio_watcher(),
UiMsg::Disconnect => {
self.disconnect();
self.umount_disconnect();
@@ -487,6 +491,8 @@ impl FileTransferActivity {
);
}
}
UiMsg::ShowWatchedPathsList => self.action_show_watched_paths_list(),
UiMsg::ShowWatcherPopup => self.action_show_radio_watch(),
UiMsg::ToggleHiddenFiles => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.browser.local_mut().toggle_hidden_files();

View File

@@ -287,13 +287,22 @@ impl FileTransferActivity {
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::QuitPopup, f, popup);
} else if self.app.mounted(&Id::WatchedPathsList) {
let popup = draw_area_in(f.size(), 60, 50);
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::WatchedPathsList, f, popup);
} else if self.app.mounted(&Id::WatcherPopup) {
let popup = draw_area_in(f.size(), 60, 10);
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::WatcherPopup, f, popup);
} else if self.app.mounted(&Id::SortingPopup) {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::SortingPopup, f, popup);
} else if self.app.mounted(&Id::ErrorPopup) {
// TODO: inject dynamic height here
let popup = draw_area_in(
f.size(),
50,
@@ -303,7 +312,6 @@ impl FileTransferActivity {
// make popup
self.app.view(&Id::ErrorPopup, f, popup);
} else if self.app.mounted(&Id::FatalPopup) {
// TODO: inject dynamic height here
let popup = draw_area_in(
f.size(),
50,
@@ -717,6 +725,42 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::DeletePopup);
}
pub(super) fn mount_radio_watch(&mut self, watch: bool, local: &str, remote: &str) {
let info_color = self.theme().misc_info_dialog;
assert!(self
.app
.remount(
Id::WatcherPopup,
Box::new(components::WatcherPopup::new(
watch, local, remote, info_color
)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::WatcherPopup).is_ok());
}
pub(super) fn umount_radio_watcher(&mut self) {
let _ = self.app.umount(&Id::WatcherPopup);
}
pub(super) fn mount_watched_paths_list(&mut self, paths: &[std::path::PathBuf]) {
let info_color = self.theme().misc_info_dialog;
assert!(self
.app
.remount(
Id::WatchedPathsList,
Box::new(components::WatchedPathsList::new(paths, info_color)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::WatchedPathsList).is_ok());
}
pub(super) fn umount_watched_paths_list(&mut self) {
let _ = self.app.umount(&Id::WatchedPathsList);
}
pub(super) fn mount_radio_replace(&mut self, file_name: &str) {
let warn_color = self.theme().misc_warn_dialog;
assert!(self
@@ -1040,9 +1084,19 @@ impl FileTransferActivity {
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SymlinkPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatcherPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WatchedPathsList,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
)),
)),
)),
)),
)),