mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
committed by
GitHub
parent
2caa0432df
commit
816270d545
@@ -35,3 +35,4 @@ pub mod logging;
|
||||
pub mod notifications;
|
||||
pub mod sshkey_storage;
|
||||
pub mod theme_provider;
|
||||
pub mod watcher;
|
||||
|
||||
312
src/system/watcher/change.rs
Normal file
312
src/system/watcher/change.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
//! ## File system change
|
||||
//!
|
||||
//! this module exposes the types to describe a change to sync on the remote file system
|
||||
|
||||
use crate::utils::path as path_utils;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Describes an operation on the remote file system to sync
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum FsChange {
|
||||
/// Move file on remote
|
||||
Move(FileToRename),
|
||||
/// Remove file from remote
|
||||
Remove(FileToRemove),
|
||||
/// Upload file to remote
|
||||
Update(FileUpdate),
|
||||
}
|
||||
|
||||
impl FsChange {
|
||||
/// Instantiate a new `FsChange::Move`
|
||||
pub fn mov(
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self::Move(FileToRename::new(
|
||||
source,
|
||||
destination,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
))
|
||||
}
|
||||
|
||||
/// Instantiate a new `FsChange::Remove`
|
||||
pub fn remove(
|
||||
removed_path: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self::Remove(FileToRemove::new(
|
||||
removed_path,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
))
|
||||
}
|
||||
|
||||
/// Instantiate a new `FsChange::Update`
|
||||
pub fn update(
|
||||
changed_path: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self::Update(FileUpdate::new(
|
||||
changed_path,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a file to rename on the remote fs
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileToRename {
|
||||
/// Path to file which has to be renamed
|
||||
source: PathBuf,
|
||||
/// new filename
|
||||
destination: PathBuf,
|
||||
}
|
||||
|
||||
impl FileToRename {
|
||||
/// Instantiate a new `FileToRename` given
|
||||
///
|
||||
/// - the path of the source on local fs
|
||||
/// - the path of the destination on local fs
|
||||
/// - the path of the file/directory watched on the local fs
|
||||
/// - the path of the remote file/directory synched with the local fs
|
||||
///
|
||||
/// the `remote` is resolved pushing to `remote_synched_path` the diff between `changed_path` and `local_watched_path`
|
||||
fn new(
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self {
|
||||
source: remote_relative_path(&source, local_watched_path, remote_synched_path),
|
||||
destination: remote_relative_path(
|
||||
&destination,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to the source to rename
|
||||
pub fn source(&self) -> &Path {
|
||||
self.source.as_path()
|
||||
}
|
||||
|
||||
/// Get path to the destination name
|
||||
pub fn destination(&self) -> &Path {
|
||||
self.destination.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a file to remove on remote fs
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileToRemove {
|
||||
/// Path to the file which has to be removed
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileToRemove {
|
||||
/// Instantiate a new `FileToRemove` given
|
||||
///
|
||||
/// - the path of the file which has been removed on localhost
|
||||
/// - the path of the file/directory watched on the local fs
|
||||
/// - the path of the remote file/directory synched with the local fs
|
||||
///
|
||||
/// the `remote` is resolved pushing to `remote_synched_path` the diff between `removed_path` and `local_watched_path`
|
||||
fn new(removed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
path: remote_relative_path(&removed_path, local_watched_path, remote_synched_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to the file to unlink
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a file changed to sync
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileUpdate {
|
||||
/// Path to file which has changed
|
||||
local: PathBuf,
|
||||
/// Path to remote file to update
|
||||
remote: PathBuf,
|
||||
}
|
||||
|
||||
impl FileUpdate {
|
||||
/// Instantiate a new `FileUpdate` given
|
||||
///
|
||||
/// - the path of the file which has changed
|
||||
/// - the path of the file/directory watched on the local fs
|
||||
/// - the path of the remote file/directory synched with the local fs
|
||||
///
|
||||
/// the `remote` is resolved pushing to `remote_synched_path` the diff between `changed_path` and `local_watched_path`
|
||||
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path),
|
||||
local: changed_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to local file to sync
|
||||
pub fn local(&self) -> &Path {
|
||||
self.local.as_path()
|
||||
}
|
||||
|
||||
/// Get path to remote file to sync
|
||||
pub fn remote(&self) -> &Path {
|
||||
self.remote.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
// -- utils
|
||||
|
||||
/// Get remote relative path, given the local target, the path of the local watched path and the path of the remote synched directory/file
|
||||
fn remote_relative_path(
|
||||
target: &Path,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> PathBuf {
|
||||
let local_diff = path_utils::diff_paths(target, local_watched_path);
|
||||
// get absolute path to remote file associated to local file
|
||||
match local_diff {
|
||||
None => remote_synched_path.to_path_buf(),
|
||||
Some(p) => {
|
||||
let mut remote = remote_synched_path.to_path_buf();
|
||||
remote.push(p);
|
||||
remote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn should_get_remote_relative_path_from_subdir() {
|
||||
assert_eq!(
|
||||
remote_relative_path(
|
||||
Path::new("/tmp/abc/test.txt"),
|
||||
Path::new("/tmp"),
|
||||
Path::new("/home/foo")
|
||||
)
|
||||
.as_path(),
|
||||
Path::new("/home/foo/abc/test.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_get_remote_relative_path_same_path() {
|
||||
assert_eq!(
|
||||
remote_relative_path(
|
||||
Path::new("/tmp/abc/test.txt"),
|
||||
Path::new("/tmp/abc/test.txt"),
|
||||
Path::new("/home/foo/test.txt")
|
||||
)
|
||||
.as_path(),
|
||||
Path::new("/home/foo/test.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_move_from_same_directory() {
|
||||
let change = FsChange::mov(
|
||||
PathBuf::from("/tmp/foo.txt"),
|
||||
PathBuf::from("/tmp/bar.txt"),
|
||||
Path::new("/tmp"),
|
||||
Path::new("/home/foo"),
|
||||
);
|
||||
if let FsChange::Move(change) = change {
|
||||
assert_eq!(change.source(), Path::new("/home/foo/foo.txt"));
|
||||
assert_eq!(change.destination(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a Move");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_move_from_subdirectory() {
|
||||
let change = FsChange::mov(
|
||||
PathBuf::from("/tmp/abc/foo.txt"),
|
||||
PathBuf::from("/tmp/abc/bar.txt"),
|
||||
Path::new("/tmp/abc"),
|
||||
Path::new("/home/foo"),
|
||||
);
|
||||
if let FsChange::Move(change) = change {
|
||||
assert_eq!(change.source(), Path::new("/home/foo/foo.txt"));
|
||||
assert_eq!(change.destination(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a Move");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_remove_from_same_directory() {
|
||||
let change = FsChange::remove(
|
||||
PathBuf::from("/tmp/bar.txt"),
|
||||
Path::new("/tmp/bar.txt"),
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
if let FsChange::Remove(change) = change {
|
||||
assert_eq!(change.path(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a remove");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_remove_from_subdirectory() {
|
||||
let change = FsChange::remove(
|
||||
PathBuf::from("/tmp/abc/bar.txt"),
|
||||
Path::new("/tmp/abc"),
|
||||
Path::new("/home/foo"),
|
||||
);
|
||||
if let FsChange::Remove(change) = change {
|
||||
assert_eq!(change.path(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a remove");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_update_from_same_directory() {
|
||||
let change = FsChange::update(
|
||||
PathBuf::from("/tmp/bar.txt"),
|
||||
Path::new("/tmp/bar.txt"),
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/bar.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_update_from_subdirectory() {
|
||||
let change = FsChange::update(
|
||||
PathBuf::from("/tmp/abc/foo.txt"),
|
||||
Path::new("/tmp"),
|
||||
Path::new("/home/foo/temp"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/abc/foo.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
}
|
||||
}
|
||||
}
|
||||
390
src/system/watcher/mod.rs
Normal file
390
src/system/watcher/mod.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
//! ## File system watcher
|
||||
//!
|
||||
//! A watcher for file system paths, which reports changes on local fs
|
||||
|
||||
mod change;
|
||||
|
||||
// -- export
|
||||
pub use change::FsChange;
|
||||
|
||||
use crate::utils::path as path_utils;
|
||||
|
||||
use notify::{
|
||||
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
type FsWatcherResult<T> = Result<T, FsWatcherError>;
|
||||
|
||||
/// Describes an error returned by the `FsWatcher`
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FsWatcherError {
|
||||
#[error("unable to unwatch this path, since is not currently watched")]
|
||||
PathNotWatched,
|
||||
#[error("unable to watch path, since it's already watched")]
|
||||
PathAlreadyWatched,
|
||||
#[error("worker error: {0}")]
|
||||
WorkerError(WatcherError),
|
||||
}
|
||||
|
||||
impl From<WatcherError> for FsWatcherError {
|
||||
fn from(err: WatcherError) -> Self {
|
||||
Self::WorkerError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// File system watcher
|
||||
pub struct FsWatcher {
|
||||
paths: HashMap<PathBuf, PathBuf>,
|
||||
receiver: Receiver<DebouncedEvent>,
|
||||
watcher: RecommendedWatcher,
|
||||
}
|
||||
|
||||
impl FsWatcher {
|
||||
/// Initialize a new `FsWatcher`
|
||||
pub fn init(delay: Duration) -> FsWatcherResult<Self> {
|
||||
let (tx, receiver) = channel();
|
||||
|
||||
Ok(Self {
|
||||
paths: HashMap::default(),
|
||||
receiver,
|
||||
watcher: watcher(tx, delay)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll searching for the first available disk change
|
||||
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
|
||||
match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)),
|
||||
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)),
|
||||
Ok(DebouncedEvent::Chmod(p) | DebouncedEvent::Create(p) | DebouncedEvent::Write(p)) => {
|
||||
Ok(self.build_fs_update(p))
|
||||
}
|
||||
Ok(
|
||||
DebouncedEvent::Rescan
|
||||
| DebouncedEvent::NoticeRemove(_)
|
||||
| DebouncedEvent::NoticeWrite(_),
|
||||
) => Ok(None),
|
||||
Ok(DebouncedEvent::Error(e, _)) => {
|
||||
error!("FsWatcher reported error: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => Ok(None),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch `local` path on localhost
|
||||
pub fn watch(&mut self, local: &Path, remote: &Path) -> FsWatcherResult<()> {
|
||||
// Start watcher if unwatched
|
||||
if !self.watched(local) {
|
||||
self.watcher.watch(local, RecursiveMode::Recursive)?;
|
||||
// Insert new path to paths
|
||||
self.paths.insert(local.to_path_buf(), remote.to_path_buf());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(FsWatcherError::PathAlreadyWatched)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether `path` is currently watched.
|
||||
/// This method looks also in path ancestors.
|
||||
///
|
||||
/// Example:
|
||||
/// if `/home` is watched, then if we call `watched("/home/foo/file.txt")` will return `true`
|
||||
pub fn watched(&self, path: &Path) -> bool {
|
||||
self.find_watched_path(path).is_some()
|
||||
}
|
||||
|
||||
/// Returns the list of watched paths
|
||||
pub fn watched_paths(&self) -> Vec<&Path> {
|
||||
Vec::from_iter(self.paths.keys().map(|x| x.as_path()))
|
||||
}
|
||||
|
||||
/// Unwatch provided path.
|
||||
/// When unwatching the path, it searches for the ancestor watched path if any.
|
||||
/// Returns the unwatched resolved path
|
||||
pub fn unwatch(&mut self, path: &Path) -> FsWatcherResult<PathBuf> {
|
||||
let watched_path = self.find_watched_path(path).map(|x| x.0.to_path_buf());
|
||||
if let Some(watched_path) = watched_path {
|
||||
self.watcher.unwatch(watched_path.as_path())?;
|
||||
self.paths.remove(watched_path.as_path());
|
||||
Ok(watched_path)
|
||||
} else {
|
||||
Err(FsWatcherError::PathNotWatched)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a certain path, returns the path data associated to the path which
|
||||
/// is ancestor of that path in the current watched path
|
||||
fn find_watched_path(&self, p: &Path) -> Option<(&Path, &Path)> {
|
||||
self.paths
|
||||
.iter()
|
||||
.find(|(k, _)| path_utils::is_child_of(p, k))
|
||||
.map(|(k, v)| (k.as_path(), v.as_path()))
|
||||
}
|
||||
|
||||
/// Build `FsChange` from path to local `changed_file`
|
||||
fn build_fs_move(&self, source: PathBuf, destination: PathBuf) -> Option<FsChange> {
|
||||
if let Some((watched_local, watched_remote)) = self.find_watched_path(&source) {
|
||||
Some(FsChange::mov(
|
||||
source,
|
||||
destination,
|
||||
watched_local,
|
||||
watched_remote,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build `FsChange` from path to local `changed_file`
|
||||
fn build_fs_remove(&self, removed_path: PathBuf) -> Option<FsChange> {
|
||||
if let Some((watched_local, watched_remote)) = self.find_watched_path(&removed_path) {
|
||||
Some(FsChange::remove(
|
||||
removed_path,
|
||||
watched_local,
|
||||
watched_remote,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build `FsChange` from path to local `changed_file`
|
||||
fn build_fs_update(&self, changed_file: PathBuf) -> Option<FsChange> {
|
||||
if let Some((watched_local, watched_remote)) = self.find_watched_path(&changed_file) {
|
||||
Some(FsChange::update(
|
||||
changed_file,
|
||||
watched_local,
|
||||
watched_remote,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::test_helpers;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn should_init_fswatcher() {
|
||||
let watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.paths.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_watch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// check if in paths
|
||||
assert_eq!(
|
||||
watcher.paths.get(tempdir.path()).unwrap(),
|
||||
Path::new("/tmp/test")
|
||||
);
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_watch_path_if_subdir_of_watched_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// watch subdir
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
// should return already watched
|
||||
assert!(watcher
|
||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||
.is_err());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_unwatch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// unwatch
|
||||
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_unwatch_path_when_subdir() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// unwatch
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
assert_eq!(
|
||||
watcher.unwatch(subdir.as_path()).unwrap().as_path(),
|
||||
Path::new(tempdir.path())
|
||||
);
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_err_when_unwatching_unwatched_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.unwatch(Path::new("/tmp")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_tell_whether_path_is_watched() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert_eq!(watcher.watched(tempdir.path()), true);
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
assert_eq!(watcher.watched(subdir.as_path()), true);
|
||||
assert_eq!(watcher.watched(Path::new("/tmp")), false);
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_poll_file_update() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
// wait
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// wait till update
|
||||
loop {
|
||||
let fs_change = watcher.poll().unwrap();
|
||||
if let Some(FsChange::Update(_)) = fs_change {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
assert!(std::fs::remove_file(file_path.as_path()).is_ok());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_poll_file_removed() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// wait
|
||||
assert!(std::fs::remove_file(file_path.as_path()).is_ok());
|
||||
// poll till remove
|
||||
loop {
|
||||
let fs_change = watcher.poll().unwrap();
|
||||
if let Some(FsChange::Remove(remove)) = fs_change {
|
||||
assert_eq!(remove.path(), Path::new("/tmp/test/test.txt"));
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn should_poll_file_moved() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
// wait
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// move file
|
||||
let mut new_file_path = tempdir.path().to_path_buf();
|
||||
new_file_path.push("new.txt");
|
||||
assert!(std::fs::rename(file_path.as_path(), new_file_path.as_path()).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// wait till rename
|
||||
loop {
|
||||
let fs_change = watcher.poll().unwrap();
|
||||
if let Some(FsChange::Move(mov)) = fs_change {
|
||||
assert_eq!(mov.source(), Path::new("/tmp/test/test.txt"));
|
||||
assert_eq!(mov.destination(), Path::new("/tmp/test/new.txt"));
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
// remove file
|
||||
assert!(std::fs::remove_file(new_file_path.as_path()).is_ok());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_poll_nothing() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(watcher.poll().ok().unwrap().is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_get_watched_paths() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.watch(Path::new("/tmp"), Path::new("/tmp")).is_ok());
|
||||
assert!(watcher
|
||||
.watch(Path::new("/home"), Path::new("/home"))
|
||||
.is_ok());
|
||||
let mut watched_paths = watcher.watched_paths();
|
||||
watched_paths.sort();
|
||||
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
134
src/ui/activities/filetransfer/actions/watcher.rs
Normal file
134
src/ui/activities/filetransfer/actions/watcher.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
132
src/ui/activities/filetransfer/fswatcher.rs
Normal file
132
src/ui/activities/filetransfer/fswatcher.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
)))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
|
||||
@@ -37,10 +37,9 @@ pub fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf {
|
||||
///
|
||||
/// This function has been written by <https://github.com/Manishearth>
|
||||
/// and is licensed under the APACHE-2/MIT license <https://github.com/Manishearth/pathdiff>
|
||||
pub fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
|
||||
pub fn diff_paths<P>(path: P, base: P) -> Option<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let base = base.as_ref();
|
||||
@@ -82,6 +81,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether `p` is child (direct/indirect) of ancestor `ancestor`
|
||||
pub fn is_child_of<P: AsRef<Path>>(p: P, ancestor: P) -> bool {
|
||||
p.as_ref().ancestors().any(|x| x == ancestor.as_ref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
@@ -120,4 +124,27 @@ mod test {
|
||||
Path::new("foo/bar/chiedo.gif")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_tell_whether_path_is_child_of() {
|
||||
assert_eq!(
|
||||
is_child_of(Path::new("/home/foo/foo.txt"), Path::new("/home"),),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_child_of(Path::new("/home/foo/foo.txt"), Path::new("/home/foo/"),),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_child_of(
|
||||
Path::new("/home/foo/foo.txt"),
|
||||
Path::new("/home/foo/foo.txt"),
|
||||
),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_child_of(Path::new("/home/foo/foo.txt"), Path::new("/tmp"),),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn create_sample_file() -> NamedTempFile {
|
||||
/// ### make_file_at
|
||||
///
|
||||
/// Make a file with `name` at specified path
|
||||
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
|
||||
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<PathBuf> {
|
||||
let mut p: PathBuf = PathBuf::from(dir);
|
||||
p.push(filename);
|
||||
let mut file = StdFile::create(p.as_path())?;
|
||||
@@ -66,7 +66,7 @@ pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
|
||||
file,
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
|
||||
)?;
|
||||
Ok(())
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// ### make_dir_at
|
||||
|
||||
Reference in New Issue
Block a user