symlink command

This commit is contained in:
veeso
2021-12-14 15:52:56 +01:00
committed by Christian Visintin
parent 24788fa894
commit ced7573241
26 changed files with 433 additions and 162 deletions

View File

@@ -43,7 +43,7 @@ use std::time::Duration;
/// ### NextActivity
///
/// NextActivity identified the next identity to run once the current has ended
/// NextActivity identifies the next identity to run once the current has ended
pub enum NextActivity {
Authentication,
FileTransfer,

View File

@@ -672,6 +672,21 @@ impl Localhost {
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
}
/// Create a symlink at path pointing at target
#[cfg(target_family = "unix")]
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> {
let path = self.to_path(path);
std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| {
error!(
"Failed to create symlink at {} pointing at {}: {}",
path.display(),
target.display(),
e
);
HostError::new(HostErrorType::CouldNotCreateFile, Some(e), path.as_path())
})
}
// -- privates
/// Recursive call for `find` method.
@@ -1179,6 +1194,24 @@ mod tests {
assert_eq!(result[1].name(), "examples.csv");
}
#[test]
fn should_create_symlink() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let dir_path: &Path = tmpdir.path();
// Make file
assert!(make_file_at(dir_path, "pippo.txt").is_ok());
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
let mut p = dir_path.to_path_buf();
p.push("pippo.txt");
// Make symlink
assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok());
// Fail symlink
assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err());
assert!(host
.symlink(Path::new("/tmp/oooo/aaaa"), p.as_path())
.is_err());
}
#[test]
fn test_host_fmt_error() {
let err: HostError = HostError::new(

View File

@@ -38,8 +38,6 @@ impl FileTransferActivity {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.local_copy_file(&entry, dest_path.as_path());
// Reload entries
self.reload_local_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
@@ -50,8 +48,6 @@ impl FileTransferActivity {
dest_path.push(entry.name());
self.local_copy_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
@@ -63,8 +59,6 @@ impl FileTransferActivity {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_copy_file(entry, dest_path.as_path());
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
@@ -75,8 +69,6 @@ impl FileTransferActivity {
dest_path.push(entry.name());
self.remote_copy_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::None => {}
}

View File

@@ -36,8 +36,6 @@ impl FileTransferActivity {
SelectedEntry::One(entry) => {
// Delete file
self.local_remove_file(&entry);
// Reload
self.reload_local_dir();
}
SelectedEntry::Many(entries) => {
// Iter files
@@ -45,8 +43,6 @@ impl FileTransferActivity {
// Delete file
self.local_remove_file(entry);
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
@@ -57,8 +53,6 @@ impl FileTransferActivity {
SelectedEntry::One(entry) => {
// Delete file
self.remote_remove_file(&entry);
// Reload
self.reload_remote_dir();
}
SelectedEntry::Many(entries) => {
// Iter files
@@ -66,8 +60,6 @@ impl FileTransferActivity {
// Delete file
self.remote_remove_file(entry);
}
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::None => {}
}

View File

@@ -56,8 +56,6 @@ impl FileTransferActivity {
}
}
}
// Reload entries
self.reload_local_dir();
}
pub(crate) fn action_edit_remote_file(&mut self) {
@@ -80,8 +78,6 @@ impl FileTransferActivity {
}
}
}
// Reload entries
self.reload_remote_dir();
}
/// Edit a file on localhost

View File

@@ -34,8 +34,6 @@ impl FileTransferActivity {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
// Reload entries
self.reload_local_dir();
}
Err(err) => {
// Report err
@@ -55,7 +53,6 @@ impl FileTransferActivity {
LogLevel::Info,
format!("\"{}\" (exitcode: {}): {}", input, rc, output),
);
self.reload_remote_dir();
}
Err(err) => {
// Report err

View File

@@ -36,8 +36,6 @@ impl FileTransferActivity {
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
// Reload entries
self.reload_local_dir();
}
Err(err) => {
// Report err
@@ -56,7 +54,6 @@ impl FileTransferActivity {
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
self.reload_remote_dir();
}
Err(err) => {
// Report err

View File

@@ -46,6 +46,7 @@ mod pending;
pub(crate) mod rename;
pub(crate) mod save;
pub(crate) mod submit;
pub(crate) mod symlink;
#[derive(Debug)]
pub(crate) enum SelectedEntry {
@@ -109,6 +110,16 @@ impl FileTransferActivity {
}
}
/// Returns whether only one entry is selected on local host
pub(crate) fn is_local_selected_one(&self) -> bool {
matches!(self.get_local_selected_entries(), SelectedEntry::One(_))
}
/// Returns whether only one entry is selected on remote host
pub(crate) fn is_remote_selected_one(&self) -> bool {
matches!(self.get_remote_selected_entries(), SelectedEntry::One(_))
}
/// Get remote file entry
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(&Id::ExplorerFind) {

View File

@@ -59,8 +59,6 @@ impl FileTransferActivity {
format!("Created file \"{}\"", file_path.display()),
);
}
// Reload files
self.reload_local_dir();
}
pub(crate) fn action_remote_newfile(&mut self, input: String) {
@@ -123,8 +121,6 @@ impl FileTransferActivity {
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
// Reload files
self.reload_remote_dir();
}
}
}

View File

@@ -37,8 +37,6 @@ impl FileTransferActivity {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.local_rename_file(&entry, dest_path.as_path());
// Reload entries
self.reload_local_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
@@ -49,8 +47,6 @@ impl FileTransferActivity {
dest_path.push(entry.name());
self.local_rename_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
@@ -61,8 +57,6 @@ impl FileTransferActivity {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_rename_file(&entry, dest_path.as_path());
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
@@ -73,8 +67,6 @@ impl FileTransferActivity {
dest_path.push(entry.name());
self.remote_rename_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::None => {}
}

View File

@@ -0,0 +1,97 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, LogLevel, SelectedEntry};
use std::path::PathBuf;
impl FileTransferActivity {
/// Create symlink on localhost
#[cfg(target_family = "unix")]
pub(crate) fn action_local_symlink(&mut self, name: String) {
if let SelectedEntry::One(entry) = self.get_local_selected_entries() {
match self
.host
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
{
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Created symlink at {}, pointing to {}",
name,
entry.path().display()
),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not create symlink: {}", err),
);
}
}
}
}
#[cfg(target_family = "windows")]
pub(crate) fn action_local_symlink(&mut self, _name: String) {
self.mount_error("Symlinks are not supported on Windows hosts");
}
/// Copy file on remote
pub(crate) fn action_remote_symlink(&mut self, name: String) {
if let SelectedEntry::One(entry) = self.get_remote_selected_entries() {
match self
.client
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
{
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Created symlink at {}, pointing to {}",
name,
entry.path().display()
),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not create symlink pointing to {}: {}",
entry.path().display(),
err
),
);
}
}
}
}
}

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,
SyncBrowsingMkdirPopup, WaitPopup,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup,
};
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};

View File

@@ -724,6 +724,11 @@ impl KeybindingsPopup {
" Show info about selected file",
))
.add_row()
.add_col(TextSpan::new("<K>").bold().fg(key_color))
.add_col(TextSpan::from(
" Create symlink pointing to the current selected entry",
))
.add_row()
.add_col(TextSpan::new("<L>").bold().fg(key_color))
.add_col(TextSpan::from(" Reload directory content"))
.add_row()
@@ -1657,6 +1662,95 @@ fn hidden_files_label(visible: bool) -> &'static str {
}
}
#[derive(MockComponent)]
pub struct SymlinkPopup {
component: Input,
}
impl SymlinkPopup {
pub fn new(color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Text)
.placeholder(
"Symlink name",
Style::default().fg(Color::Rgb(128, 128, 128)),
)
.title(
"Create a symlink pointing to the selected entry",
Alignment::Center,
),
}
}
}
impl Component<Msg, NoUserEvent> for SymlinkPopup {
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::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::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::CreateSymlink(i)))
}
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseSymlinkPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct SyncBrowsingMkdirPopup {
component: Radio,

View File

@@ -282,6 +282,10 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('k'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSymlinkPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('l'),
modifiers: KeyModifiers::NONE,
@@ -450,6 +454,10 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char('i'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowFileInfoPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('k'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::Ui(UiMsg::ShowSymlinkPopup)),
Event::Keyboard(KeyEvent {
code: Key::Char('l'),
modifiers: KeyModifiers::NONE,

View File

@@ -213,6 +213,7 @@ impl FileTransferActivity {
/// Update local file list
pub(super) fn update_local_filelist(&mut self) {
self.reload_local_dir();
// Get width
let width = self
.context_mut()
@@ -260,6 +261,7 @@ impl FileTransferActivity {
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) {
self.reload_remote_dir();
let width = self
.context_mut()
.terminal()

View File

@@ -87,6 +87,7 @@ enum Id {
SortingPopup,
StatusBarLocal,
StatusBarRemote,
SymlinkPopup,
SyncBrowsingMkdirPopup,
WaitPopup,
}
@@ -111,6 +112,7 @@ enum PendingActionMsg {
enum TransferMsg {
AbortTransfer,
CopyFileTo(String),
CreateSymlink(String),
DeleteFile,
EnterDirectory,
ExecuteCmd(String),
@@ -151,6 +153,7 @@ enum UiMsg {
CloseQuitPopup,
CloseRenamePopup,
CloseSaveAsPopup,
CloseSymlinkPopup,
Disconnect,
ExplorerBackTabbed,
LogBackTabbed,
@@ -171,6 +174,7 @@ enum UiMsg {
ShowQuitPopup,
ShowRenamePopup,
ShowSaveAsPopup,
ShowSymlinkPopup,
ToggleHiddenFiles,
ToggleSyncBrowsing,
}

View File

@@ -1051,8 +1051,6 @@ impl FileTransferActivity {
LogLevel::Info,
format!("Changed directory on local: {}", path.display()),
);
// Reload files
self.reload_local_dir();
// Push prev_dir to stack
if push {
self.local_mut().pushd(prev_dir.as_path())

View File

@@ -70,6 +70,18 @@ impl FileTransferActivity {
// Reload files
self.update_browser_file_list()
}
TransferMsg::CreateSymlink(name) => {
self.umount_symlink();
self.mount_blocking_wait("Creating symlink…");
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_symlink(name),
FileExplorerTab::Remote => self.action_remote_symlink(name),
_ => panic!("Found tab doesn't support SYMLINK"),
}
self.umount_wait();
// Reload files
self.update_browser_file_list()
}
TransferMsg::DeleteFile => {
self.umount_radio_delete();
self.mount_blocking_wait("Removing file(s)…");
@@ -408,6 +420,7 @@ impl FileTransferActivity {
UiMsg::CloseQuitPopup => self.umount_quit(),
UiMsg::CloseRenamePopup => self.umount_rename(),
UiMsg::CloseSaveAsPopup => self.umount_saveas(),
UiMsg::CloseSymlinkPopup => self.umount_symlink(),
UiMsg::Disconnect => {
self.disconnect();
self.umount_disconnect();
@@ -460,6 +473,20 @@ impl FileTransferActivity {
UiMsg::ShowQuitPopup => self.mount_quit(),
UiMsg::ShowRenamePopup => self.mount_rename(),
UiMsg::ShowSaveAsPopup => self.mount_saveas(),
UiMsg::ShowSymlinkPopup => {
if match self.browser.tab() {
FileExplorerTab::Local => self.is_local_selected_one(),
FileExplorerTab::Remote => self.is_remote_selected_one(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => false,
} {
// Only if only one entry is selected
self.mount_symlink();
} else {
self.mount_error(
"Symlink cannot be performed if more than one file is selected",
);
}
}
UiMsg::ToggleHiddenFiles => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.browser.local_mut().toggle_hidden_files();

View File

@@ -216,6 +216,11 @@ impl FileTransferActivity {
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::SaveAsPopup, f, popup);
} else if self.app.mounted(&Id::SymlinkPopup) {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::SymlinkPopup, f, popup);
} else if self.app.mounted(&Id::ExecPopup) {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
@@ -797,6 +802,23 @@ impl FileTransferActivity {
.is_ok());
}
pub(super) fn mount_symlink(&mut self) {
let input_color = self.theme().misc_input_dialog;
assert!(self
.app
.remount(
Id::SymlinkPopup,
Box::new(components::SymlinkPopup::new(input_color)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::SymlinkPopup).is_ok());
}
pub(super) fn umount_symlink(&mut self) {
let _ = self.app.umount(&Id::SymlinkPopup);
}
pub(super) fn mount_sync_browsing_mkdir_popup(&mut self, dir_name: &str) {
let color = self.theme().misc_info_dialog;
assert!(self
@@ -969,9 +991,14 @@ impl FileTransferActivity {
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SyncBrowsingMkdirPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
Box::new(SubClause::And(
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::SymlinkPopup,
)))),
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
Id::WaitPopup,
)))),
)),
)),
)),
)),