Option: prompt user when about to replace an existing file caused by a file transfer

This commit is contained in:
veeso 2021-09-18 15:57:05 +02:00
parent 63e7023342
commit 06ffbaa2f4
17 changed files with 406 additions and 33 deletions

View file

@ -36,6 +36,9 @@ Released on ??
- Possibility to update termscp directly via GUI or CLI. - Possibility to update termscp directly via GUI or CLI.
- Install update via CLI running `(sudo) termscp --update`. - Install update via CLI running `(sudo) termscp --update`.
- Install update via GUI from auth form: when the "new version message" is displayed press `<CTRL+R>`, then enter `YES` in the radio input asking whether to install the update. - Install update via GUI from auth form: when the "new version message" is displayed press `<CTRL+R>`, then enter `YES` in the radio input asking whether to install the update.
- **Prompt user when about to replace existing file on a file transfer**:
- Whenever a file transfer is about to replace an existing file on local/remote host, you will be prompted if you're sure you really want to replace that file.
- You may want to disable this option. You can go to configuration and set "Prompt when replacing existing files?" to "NO"
- **❗ BREAKING CHANGES ❗**: - **❗ BREAKING CHANGES ❗**:
- Added a new key in themes: `misc_info_dialog`: if your theme won't load, just reload it. If you're using a customised theme, you can add to it the missing key via a text editor. Just edit the `theme.toml` in your `$CONFIG_DIR/termscp/theme.toml` and add `misc_info_dialog` (Read more in manual at Themes). - Added a new key in themes: `misc_info_dialog`: if your theme won't load, just reload it. If you're using a customised theme, you can add to it the missing key via a text editor. Just edit the `theme.toml` in your `$CONFIG_DIR/termscp/theme.toml` and add `misc_info_dialog` (Read more in manual at Themes).
- Enhancements: - Enhancements:

View file

@ -300,6 +300,7 @@ These parameters can be changed:
- **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument. - **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument.
- **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway. - **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway.
- **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available. - **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available.
- **Prompt when replacing existing files?**: If set to `yes`, termscp will prompt for confirmation you whenever a file transfer would cause an existing file on target host to be replaced.
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected. - **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format) - **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format) - **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)

View file

@ -51,7 +51,8 @@ pub struct UserInterfaceConfig {
pub text_editor: PathBuf, pub text_editor: PathBuf,
pub default_protocol: String, pub default_protocol: String,
pub show_hidden_files: bool, pub show_hidden_files: bool,
pub check_for_updates: Option<bool>, // @! Since 0.3.3 pub check_for_updates: Option<bool>, // @! Since 0.3.3
pub prompt_on_file_replace: Option<bool>, // @! Since 0.7.0; Default True
pub group_dirs: Option<String>, pub group_dirs: Option<String>,
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility) pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
pub remote_file_fmt: Option<String>, // @! Since 0.5.0 pub remote_file_fmt: Option<String>, // @! Since 0.5.0
@ -84,6 +85,7 @@ impl Default for UserInterfaceConfig {
default_protocol: FileTransferProtocol::Sftp.to_string(), default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false, show_hidden_files: false,
check_for_updates: Some(true), check_for_updates: Some(true),
prompt_on_file_replace: Some(true),
group_dirs: None, group_dirs: None,
file_fmt: None, file_fmt: None,
remote_file_fmt: None, remote_file_fmt: None,
@ -120,6 +122,7 @@ mod tests {
text_editor: PathBuf::from("nano"), text_editor: PathBuf::from("nano"),
show_hidden_files: true, show_hidden_files: true,
check_for_updates: Some(true), check_for_updates: Some(true),
prompt_on_file_replace: Some(true),
group_dirs: Some(String::from("first")), group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")), file_fmt: Some(String::from("{NAME}")),
remote_file_fmt: Some(String::from("{USER}")), remote_file_fmt: Some(String::from("{USER}")),
@ -128,6 +131,7 @@ mod tests {
assert_eq!(ui.text_editor, PathBuf::from("nano")); assert_eq!(ui.text_editor, PathBuf::from("nano"));
assert_eq!(ui.show_hidden_files, true); assert_eq!(ui.show_hidden_files, true);
assert_eq!(ui.check_for_updates, Some(true)); assert_eq!(ui.check_for_updates, Some(true));
assert_eq!(ui.prompt_on_file_replace, Some(true));
assert_eq!(ui.group_dirs, Some(String::from("first"))); assert_eq!(ui.group_dirs, Some(String::from("first")));
assert_eq!(ui.file_fmt, Some(String::from("{NAME}"))); assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
let cfg: UserConfig = UserConfig { let cfg: UserConfig = UserConfig {
@ -145,6 +149,7 @@ mod tests {
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano")); assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
assert_eq!(cfg.user_interface.show_hidden_files, true); assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates, Some(true)); assert_eq!(cfg.user_interface.check_for_updates, Some(true));
assert_eq!(cfg.user_interface.prompt_on_file_replace, Some(true));
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first"))); assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}"))); assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
assert_eq!( assert_eq!(

View file

@ -201,6 +201,7 @@ mod tests {
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim")); assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true); assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true); assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.user_interface.prompt_on_file_replace.unwrap(), false);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last"))); assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!( assert_eq!(
cfg.user_interface.file_fmt, cfg.user_interface.file_fmt,
@ -244,6 +245,7 @@ mod tests {
assert_eq!(cfg.user_interface.show_hidden_files, true); assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, None); assert_eq!(cfg.user_interface.group_dirs, None);
assert!(cfg.user_interface.check_for_updates.is_none()); assert!(cfg.user_interface.check_for_updates.is_none());
assert!(cfg.user_interface.prompt_on_file_replace.is_none());
assert!(cfg.user_interface.file_fmt.is_none()); assert!(cfg.user_interface.file_fmt.is_none());
assert!(cfg.user_interface.remote_file_fmt.is_none()); assert!(cfg.user_interface.remote_file_fmt.is_none());
// Verify keys // Verify keys
@ -317,6 +319,7 @@ mod tests {
text_editor = "vim" text_editor = "vim"
show_hidden_files = true show_hidden_files = true
check_for_updates = true check_for_updates = true
prompt_on_file_replace = false
group_dirs = "last" group_dirs = "last"
file_fmt = "{NAME} {PEX}" file_fmt = "{NAME} {PEX}"
remote_file_fmt = "{NAME} {USER}" remote_file_fmt = "{NAME} {USER}"

View file

@ -181,6 +181,23 @@ impl ConfigClient {
self.config.user_interface.check_for_updates = Some(value); self.config.user_interface.check_for_updates = Some(value);
} }
/// ### get_prompt_on_file_replace
///
/// Get value of `prompt_on_file_replace`
pub fn get_prompt_on_file_replace(&self) -> bool {
self.config
.user_interface
.prompt_on_file_replace
.unwrap_or(true)
}
/// ### set_prompt_on_file_replace
///
/// Set new value for `prompt_on_file_replace`
pub fn set_prompt_on_file_replace(&mut self, value: bool) {
self.config.user_interface.prompt_on_file_replace = Some(value);
}
/// ### get_group_dirs /// ### get_group_dirs
/// ///
/// Get GroupDirs value from configuration (will be converted from string) /// Get GroupDirs value from configuration (will be converted from string)
@ -580,6 +597,20 @@ mod tests {
assert_eq!(client.get_check_for_updates(), false); assert_eq!(client.get_check_for_updates(), false);
} }
#[test]
fn test_system_config_prompt_on_file_replace() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
assert_eq!(client.get_prompt_on_file_replace(), true); // Null ?
client.set_prompt_on_file_replace(true);
assert_eq!(client.get_prompt_on_file_replace(), true);
client.set_prompt_on_file_replace(false);
assert_eq!(client.get_prompt_on_file_replace(), false);
}
#[test] #[test]
fn test_system_config_group_dirs() { fn test_system_config_group_dirs() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap(); let tmp_dir: TempDir = TempDir::new().ok().unwrap();

View file

@ -27,7 +27,9 @@
*/ */
// locals // locals
use super::super::browser::FileExplorerTab; use super::super::browser::FileExplorerTab;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; use super::{
FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload,
};
use std::path::PathBuf; use std::path::PathBuf;
@ -69,7 +71,7 @@ impl FileTransferActivity {
} }
} }
pub(crate) fn action_find_transfer(&mut self, save_as: Option<String>) { pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = match self.browser.tab() { let wrkdir: PathBuf = match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(), FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(), FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
@ -77,10 +79,19 @@ impl FileTransferActivity {
match self.get_found_selected_entries() { match self.get_found_selected_entries() {
SelectedEntry::One(entry) => match self.browser.tab() { SelectedEntry::One(entry) => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if let Err(err) = self.filetransfer_send( let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.remote_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(), wrkdir.as_path(),
save_as, opts.save_as,
) { ) {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
@ -90,10 +101,19 @@ impl FileTransferActivity {
} }
} }
FileExplorerTab::FindRemote | FileExplorerTab::Remote => { FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if let Err(err) = self.filetransfer_recv( let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.local_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(), wrkdir.as_path(),
save_as, opts.save_as,
) { ) {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
@ -106,7 +126,7 @@ impl FileTransferActivity {
SelectedEntry::Many(entries) => { SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input // In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir; let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as { if let Some(save_as) = opts.save_as {
dest_path.push(save_as); dest_path.push(save_as);
} }
// Iter files // Iter files

View file

@ -25,7 +25,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel, TransferPayload}; pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts,
TransferPayload,
};
use tuirealm::{Payload, Value}; use tuirealm::{Payload, Value};
// actions // actions

View file

@ -26,34 +26,85 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; use super::{
super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel,
SelectedEntry, TransferOpts, TransferPayload,
};
use std::path::PathBuf; use std::path::PathBuf;
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) { pub(crate) fn action_local_saveas(&mut self, input: String) {
self.action_local_send_file(Some(input)); self.local_send_file(TransferOpts::default().save_as(input));
} }
pub(crate) fn action_remote_saveas(&mut self, input: String) { pub(crate) fn action_remote_saveas(&mut self, input: String) {
self.action_remote_recv_file(Some(input)); self.remote_recv_file(TransferOpts::default().save_as(input));
} }
pub(crate) fn action_local_send(&mut self) { pub(crate) fn action_local_send(&mut self) {
self.action_local_send_file(None); self.local_send_file(TransferOpts::default());
} }
pub(crate) fn action_remote_recv(&mut self) { pub(crate) fn action_remote_recv(&mut self) {
self.action_remote_recv_file(None); self.remote_recv_file(TransferOpts::default());
} }
fn action_local_send_file(&mut self, save_as: Option<String>) { /// ### action_finalize_pending_transfer
///
/// Finalize "pending" transfer.
/// The pending transfer is created after a transfer which required a user action to be completed first.
/// The name of the file to transfer, is contained in the storage at `STORAGE_PENDING_TRANSFER`.
/// NOTE: Panics if `STORAGE_PENDING_TRANSFER` is undefined
pub(crate) fn action_finalize_pending_transfer(&mut self) {
// Retrieve pending transfer
let file_name: String = self
.context_mut()
.store_mut()
.take_string(STORAGE_PENDING_TRANSFER)
.unwrap();
// Send file
match self.browser.tab() {
FileExplorerTab::Local => self.local_send_file(
TransferOpts::default()
.save_as(file_name)
.check_replace(false),
),
FileExplorerTab::Remote => self.remote_recv_file(
TransferOpts::default()
.save_as(file_name)
.check_replace(false),
),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_transfer(
TransferOpts::default()
.save_as(file_name)
.check_replace(false),
),
}
// Reload browsers
match self.browser.tab() {
FileExplorerTab::Local => self.reload_remote_dir(),
FileExplorerTab::Remote => self.reload_local_dir(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {}
}
}
fn local_send_file(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = self.remote().wrkdir.clone(); let wrkdir: PathBuf = self.remote().wrkdir.clone();
match self.get_local_selected_entries() { match self.get_local_selected_entries() {
SelectedEntry::One(entry) => { SelectedEntry::One(entry) => {
if let Err(err) = self.filetransfer_send( let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.remote_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(), wrkdir.as_path(),
save_as, opts.save_as,
) { ) {
{ {
self.log_and_alert( self.log_and_alert(
@ -67,7 +118,7 @@ impl FileTransferActivity {
SelectedEntry::Many(entries) => { SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input // In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir; let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as { if let Some(save_as) = opts.save_as {
dest_path.push(save_as); dest_path.push(save_as);
} }
// Iter files // Iter files
@ -90,14 +141,23 @@ impl FileTransferActivity {
} }
} }
fn action_remote_recv_file(&mut self, save_as: Option<String>) { fn remote_recv_file(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = self.local().wrkdir.clone(); let wrkdir: PathBuf = self.local().wrkdir.clone();
match self.get_remote_selected_entries() { match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => { SelectedEntry::One(entry) => {
if let Err(err) = self.filetransfer_recv( let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.local_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(), wrkdir.as_path(),
save_as, opts.save_as,
) { ) {
{ {
self.log_and_alert( self.log_and_alert(
@ -111,7 +171,7 @@ impl FileTransferActivity {
SelectedEntry::Many(entries) => { SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input // In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir; let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as { if let Some(save_as) = opts.save_as {
dest_path.push(save_as); dest_path.push(save_as);
} }
// Iter files // Iter files
@ -133,4 +193,25 @@ impl FileTransferActivity {
SelectedEntry::None => {} SelectedEntry::None => {}
} }
} }
/// ### set_pending_transfer
///
/// Set pending transfer into storage
pub(crate) fn set_pending_transfer(&mut self, file_name: &str) {
self.mount_radio_replace(file_name);
// Put pending transfer in store
self.context_mut()
.store_mut()
.set_string(STORAGE_PENDING_TRANSFER, file_name.to_string());
}
/// ### file_to_check
///
/// Get file to check for path
pub(crate) fn file_to_check(e: &FsEntry, alt: Option<&String>) -> PathBuf {
match alt {
Some(s) => PathBuf::from(s),
None => PathBuf::from(e.get_name()),
}
}
} }

View file

@ -29,6 +29,8 @@ use bytesize::ByteSize;
use std::fmt; use std::fmt;
use std::time::Instant; use std::time::Instant;
// -- States and progress
/// ### TransferStates /// ### TransferStates
/// ///
/// TransferStates contains the states related to the transfer process /// TransferStates contains the states related to the transfer process
@ -195,6 +197,45 @@ impl ProgressStates {
} }
} }
// -- Options
/// ## TransferOpts
///
/// Defines the transfer options for transfer actions
pub struct TransferOpts {
/// Save file as
pub save_as: Option<String>,
/// Whether to check if file is being replaced
pub check_replace: bool,
}
impl Default for TransferOpts {
fn default() -> Self {
Self {
save_as: None,
check_replace: true,
}
}
}
impl TransferOpts {
/// ### save_as
///
/// Define the name of the file to be saved
pub fn save_as<S: AsRef<str>>(mut self, n: S) -> Self {
self.save_as = Some(n.as_ref().to_string());
self
}
/// ### check_replace
///
/// Set whether to check if the file being transferred will "replace" an existing one
pub fn check_replace(mut self, opt: bool) -> Self {
self.check_replace = opt;
self
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@ -265,4 +306,16 @@ mod test {
states.reset(); states.reset();
assert_eq!(states.aborted(), false); assert_eq!(states.aborted(), false);
} }
#[test]
fn transfer_opts() {
let opts = TransferOpts::default();
assert_eq!(opts.check_replace, true);
assert!(opts.save_as.is_none());
let opts = TransferOpts::default()
.check_replace(false)
.save_as("omar.txt");
assert_eq!(opts.save_as.as_deref().unwrap(), "omar.txt");
assert_eq!(opts.check_replace, false);
}
} }

View file

@ -44,7 +44,7 @@ use crate::host::Localhost;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
pub(self) use lib::browser; pub(self) use lib::browser;
use lib::browser::Browser; use lib::browser::Browser;
use lib::transfer::TransferStates; use lib::transfer::{TransferOpts, TransferStates};
pub(self) use session::TransferPayload; pub(self) use session::TransferPayload;
// Includes // Includes
@ -57,6 +57,7 @@ use tuirealm::View;
// -- Storage keys // -- Storage keys
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH"; const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER";
// -- components // -- components
@ -80,6 +81,7 @@ const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME"; const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS"; const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE"; const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT"; const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING"; const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";

View file

@ -1187,4 +1187,14 @@ impl FileTransferActivity {
} }
} }
} }
// -- file exist
pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool {
self.host.file_exists(p)
}
pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool {
self.client.stat(p).is_ok()
}
} }

View file

@ -27,14 +27,14 @@
*/ */
// locals // locals
use super::{ use super::{
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel, actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel, TransferOpts,
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH,
COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX,
COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE,
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING, COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_REPLACE,
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
}; };
use crate::fs::explorer::FileSorting; use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry; use crate::fs::FsEntry;
@ -358,7 +358,7 @@ impl Update for FileTransferActivity {
} }
(COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => { (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => {
// Get entry // Get entry
self.action_find_transfer(None); self.action_find_transfer(TransferOpts::default());
// Reload files // Reload files
match self.browser.tab() { match self.browser.tab() {
// NOTE: swapped by purpose // NOTE: swapped by purpose
@ -583,7 +583,7 @@ impl Update for FileTransferActivity {
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()), FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry // Get entry
self.action_find_transfer(Some(input.to_string())); self.action_find_transfer(TransferOpts::default().save_as(input));
} }
} }
self.umount_saveas(); self.umount_saveas();
@ -653,6 +653,21 @@ impl Update for FileTransferActivity {
} }
} }
(COMPONENT_RADIO_DELETE, _) => None, (COMPONENT_RADIO_DELETE, _) => None,
// -- replace
(COMPONENT_RADIO_REPLACE, key)
if key == &MSG_KEY_ESC
|| key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) =>
{
self.umount_radio_replace();
None
}
(COMPONENT_RADIO_REPLACE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Choice is 'YES'
self.umount_radio_replace();
self.action_finalize_pending_transfer();
None
}
(COMPONENT_RADIO_REPLACE, _) => None,
// -- disconnect // -- disconnect
(COMPONENT_RADIO_DISCONNECT, key) (COMPONENT_RADIO_DISCONNECT, key)
if key == &MSG_KEY_ESC if key == &MSG_KEY_ESC

View file

@ -308,6 +308,14 @@ impl FileTransferActivity {
self.view.render(super::COMPONENT_RADIO_DELETE, f, popup); self.view.render(super::COMPONENT_RADIO_DELETE, f, popup);
} }
} }
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_REPLACE) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_RADIO_REPLACE, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) { if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
if props.visible { if props.visible {
let popup = draw_area_in(f.size(), 30, 10); let popup = draw_area_in(f.size(), 30, 10);
@ -698,6 +706,32 @@ impl FileTransferActivity {
self.view.umount(super::COMPONENT_RADIO_DELETE); self.view.umount(super::COMPONENT_RADIO_DELETE);
} }
pub(super) fn mount_radio_replace(&mut self, file_name: &str) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_RADIO_REPLACE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, warn_color)
.with_title(
format!("File '{}' already exists. Overwrite file?", file_name),
Alignment::Center,
)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(0)
.rewind(true)
.build(),
)),
);
self.view.active(super::COMPONENT_RADIO_REPLACE);
}
pub(super) fn umount_radio_replace(&mut self) {
self.view.umount(super::COMPONENT_RADIO_REPLACE);
}
pub(super) fn mount_file_info(&mut self, file: &FsEntry) { pub(super) fn mount_file_info(&mut self, file: &FsEntry) {
let mut texts: TableBuilder = TableBuilder::default(); let mut texts: TableBuilder = TableBuilder::default();
// Abs path // Abs path

View file

@ -54,6 +54,7 @@ const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL"; const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES"; const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES"; const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE";
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS"; const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT"; const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT"; const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";

View file

@ -43,8 +43,8 @@ use super::{
COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME,
COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL,
COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE,
COMPONENT_TEXT_HELP, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
}; };
use crate::ui::keymap::*; use crate::ui::keymap::*;
use crate::utils::parser::parse_color; use crate::utils::parser::parse_color;
@ -87,6 +87,10 @@ impl SetupActivity {
None None
} }
(COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => { (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE);
None
}
(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_GROUP_DIRS); self.view.active(COMPONENT_RADIO_GROUP_DIRS);
None None
} }
@ -112,6 +116,10 @@ impl SetupActivity {
None None
} }
(COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => { (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE);
None
}
(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_UPDATES); self.view.active(COMPONENT_RADIO_UPDATES);
None None
} }

View file

@ -109,6 +109,19 @@ impl SetupActivity {
.build(), .build(),
)), )),
); );
self.view.mount(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightCyan)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_title("Prompt when replacing existing files?", Alignment::Left)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
);
self.view.mount( self.view.mount(
super::COMPONENT_RADIO_GROUP_DIRS, super::COMPONENT_RADIO_GROUP_DIRS,
Box::new(Radio::new( Box::new(Radio::new(
@ -178,6 +191,7 @@ impl SetupActivity {
Constraint::Length(3), // Protocol tab Constraint::Length(3), // Protocol tab
Constraint::Length(3), // Hidden files Constraint::Length(3), // Hidden files
Constraint::Length(3), // Updates tab Constraint::Length(3), // Updates tab
Constraint::Length(3), // Prompt file replace
Constraint::Length(3), // Group dirs Constraint::Length(3), // Group dirs
Constraint::Length(3), // Local Format input Constraint::Length(3), // Local Format input
Constraint::Length(3), // Remote Format input Constraint::Length(3), // Remote Format input
@ -193,12 +207,17 @@ impl SetupActivity {
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]); .render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
self.view self.view
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]); .render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
self.view.render(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
f,
ui_cfg_chunks[4],
);
self.view self.view
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]); .render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[5]);
self.view self.view
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]); .render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[6]);
self.view self.view
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]); .render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[7]);
// Popups // Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible { if props.visible {
@ -282,6 +301,20 @@ impl SetupActivity {
let props = RadioPropsBuilder::from(props).with_value(updates).build(); let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props); let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
} }
// File replace
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
{
let updates: usize = match self.config().get_prompt_on_file_replace() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props);
}
// Group dirs // Group dirs
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) { if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match self.config().get_group_dirs() { let dirs: usize = match self.config().get_group_dirs() {
@ -344,6 +377,13 @@ impl SetupActivity {
let check: bool = matches!(opt, 0); let check: bool = matches!(opt, 0);
self.config_mut().set_check_for_updates(check); self.config_mut().set_check_for_updates(check);
} }
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
{
let check: bool = matches!(opt, 0);
self.config_mut().set_prompt_on_file_replace(check);
}
if let Some(Payload::One(Value::Str(fmt))) = if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT) self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
{ {

View file

@ -67,6 +67,7 @@ impl Store {
} }
// -- getters // -- getters
/// ### get_string /// ### get_string
/// ///
/// Get string from store /// Get string from store
@ -168,6 +169,58 @@ impl Store {
pub fn set(&mut self, key: &str) { pub fn set(&mut self, key: &str) {
self.store.insert(key.to_string(), StoreState::Flag); self.store.insert(key.to_string(), StoreState::Flag);
} }
// -- Consumers
/// ### take_string
///
/// Take string from store
pub fn take_string(&mut self, key: &str) -> Option<String> {
match self.store.remove(key) {
Some(StoreState::Str(s)) => Some(s),
_ => None,
}
}
/// ### take_signed
///
/// Take signed from store
pub fn take_signed(&mut self, key: &str) -> Option<isize> {
match self.store.remove(key) {
Some(StoreState::Signed(i)) => Some(i),
_ => None,
}
}
/// ### take_unsigned
///
/// Take unsigned from store
pub fn take_unsigned(&mut self, key: &str) -> Option<usize> {
match self.store.remove(key) {
Some(StoreState::Unsigned(u)) => Some(u),
_ => None,
}
}
/// ### get_float
///
/// Take float from store
pub fn take_float(&mut self, key: &str) -> Option<f64> {
match self.store.remove(key) {
Some(StoreState::Float(f)) => Some(f),
_ => None,
}
}
/// ### get_boolean
///
/// Take boolean from store
pub fn take_boolean(&mut self, key: &str) -> Option<bool> {
match self.store.remove(key) {
Some(StoreState::Boolean(b)) => Some(b),
_ => None,
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -184,20 +237,30 @@ mod tests {
// Test string // Test string
store.set_string("test", String::from("hello")); store.set_string("test", String::from("hello"));
assert_eq!(*store.get_string("test").as_ref().unwrap(), "hello"); assert_eq!(*store.get_string("test").as_ref().unwrap(), "hello");
assert_eq!(store.take_string("test").unwrap(), "hello".to_string());
assert_eq!(store.take_string("test"), None);
// Test isize // Test isize
store.set_signed("number", 3005); store.set_signed("number", 3005);
assert_eq!(store.get_signed("number").unwrap(), 3005); assert_eq!(store.get_signed("number").unwrap(), 3005);
assert_eq!(store.take_signed("number").unwrap(), 3005);
assert_eq!(store.take_signed("number"), None);
store.set_signed("number", -123); store.set_signed("number", -123);
assert_eq!(store.get_signed("number").unwrap(), -123); assert_eq!(store.get_signed("number").unwrap(), -123);
// Test usize // Test usize
store.set_unsigned("unumber", 1024); store.set_unsigned("unumber", 1024);
assert_eq!(store.get_unsigned("unumber").unwrap(), 1024); assert_eq!(store.get_unsigned("unumber").unwrap(), 1024);
assert_eq!(store.take_unsigned("unumber").unwrap(), 1024);
assert_eq!(store.take_unsigned("unumber"), None);
// Test float // Test float
store.set_float("float", 3.33); store.set_float("float", 3.33);
assert_eq!(store.get_float("float").unwrap(), 3.33); assert_eq!(store.get_float("float").unwrap(), 3.33);
assert_eq!(store.take_float("float").unwrap(), 3.33);
assert_eq!(store.take_float("float"), None);
// Test boolean // Test boolean
store.set_boolean("bool", true); store.set_boolean("bool", true);
assert_eq!(store.get_boolean("bool").unwrap(), true); assert_eq!(store.get_boolean("bool").unwrap(), true);
assert_eq!(store.take_boolean("bool").unwrap(), true);
assert_eq!(store.take_boolean("bool"), None);
// Test flag // Test flag
store.set("myflag"); store.set("myflag");
assert_eq!(store.isset("myflag"), true); assert_eq!(store.isset("myflag"), true);