Changed activity paths

This commit is contained in:
veeso
2021-05-08 19:28:47 +02:00
parent 7ba1b316f7
commit 300256b196
31 changed files with 9 additions and 10 deletions

View File

@@ -0,0 +1,184 @@
//! ## 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, FsEntry};
use std::path::PathBuf;
impl FileTransferActivity {
/// ### action_enter_local_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(crate) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
}
}
/// ### action_enter_remote_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(crate) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
}
}
/// ### action_change_local_dir
///
/// Change local directory reading value from input
pub(crate) fn action_change_local_dir(&mut self, input: String, block_sync: bool) {
let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.local_changedir(dir_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(input, true);
}
}
/// ### action_change_remote_dir
///
/// Change remote directory reading value from input
pub(crate) fn action_change_remote_dir(&mut self, input: String, block_sync: bool) {
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.remote_changedir(dir_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(input, true);
}
}
/// ### action_go_to_previous_local_dir
///
/// Go to previous directory from localhost
pub(crate) fn action_go_to_previous_local_dir(&mut self, block_sync: bool) {
if let Some(d) = self.local_mut().popd() {
self.local_changedir(d.as_path(), false);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_go_to_previous_remote_dir(true);
}
}
}
/// ### action_go_to_previous_remote_dir
///
/// Go to previous directory from remote host
pub(crate) fn action_go_to_previous_remote_dir(&mut self, block_sync: bool) {
if let Some(d) = self.remote_mut().popd() {
self.remote_changedir(d.as_path(), false);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_go_to_previous_local_dir(true);
}
}
}
/// ### action_go_to_local_upper_dir
///
/// Go to upper directory on local host
pub(crate) fn action_go_to_local_upper_dir(&mut self, block_sync: bool) {
// Get pwd
let path: PathBuf = self.local().wrkdir.clone();
// Go to parent directory
if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true);
// If sync is enabled update remote too
if self.browser.sync_browsing && !block_sync {
self.action_go_to_remote_upper_dir(true);
}
}
}
/// #### action_go_to_remote_upper_dir
///
/// Go to upper directory on remote host
pub(crate) fn action_go_to_remote_upper_dir(&mut self, block_sync: bool) {
// Get pwd
let path: PathBuf = self.remote().wrkdir.clone();
// Go to parent directory
if let Some(parent) = path.as_path().parent() {
self.remote_changedir(parent, true);
// If sync is enabled update local too
if self.browser.sync_browsing && !block_sync {
self.action_go_to_local_upper_dir(true);
}
}
}
}

View File

@@ -0,0 +1,98 @@
//! ## 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, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
/// ### action_local_copy
///
/// Copy file on local
pub(crate) fn action_local_copy(&mut self, input: String) {
if let Some(idx) = self.get_local_file_idx() {
let dest_path: PathBuf = PathBuf::from(input);
let entry: FsEntry = self.local().get(idx).unwrap().clone();
match self.host.copy(&entry, dest_path.as_path()) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest_path.display()
),
);
// Reload entries
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.local_scan(wrkdir.as_path());
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest_path.display(),
err
),
),
}
}
}
/// ### action_remote_copy
///
/// Copy file on remote
pub(crate) fn action_remote_copy(&mut self, input: String) {
if let Some(idx) = self.get_remote_file_idx() {
let dest_path: PathBuf = PathBuf::from(input);
let entry: FsEntry = self.remote().get(idx).unwrap().clone();
match self.client.as_mut().copy(&entry, dest_path.as_path()) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest_path.display()
),
);
self.reload_remote_dir();
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest_path.display(),
err
),
),
}
}
}
}

View File

@@ -0,0 +1,84 @@
//! ## 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, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_delete(&mut self) {
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
if let Some(entry) = entry {
let full_path: PathBuf = entry.get_abs_path();
// Delete file or directory and report status as popup
match self.host.remove(&entry) {
Ok(_) => {
// Reload files
let p: PathBuf = self.local().wrkdir.clone();
self.local_scan(p.as_path());
// Log
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", full_path.display()),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not delete file \"{}\": {}", full_path.display(), err),
);
}
}
}
}
pub(crate) fn action_remote_delete(&mut self) {
if let Some(idx) = self.get_remote_file_idx() {
// Check if file entry exists
let entry = self.remote().get(idx).cloned();
if let Some(entry) = entry {
let full_path: PathBuf = entry.get_abs_path();
// Delete file
match self.client.remove(&entry) {
Ok(_) => {
self.reload_remote_dir();
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", full_path.display()),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not delete file \"{}\": {}", full_path.display(), err),
);
}
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
//! ## 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, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_edit_local_file(&mut self) {
if self.get_local_file_entry().is_some() {
let fsentry: FsEntry = self.get_local_file_entry().unwrap().clone();
// Check if file
if fsentry.is_file() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", fsentry.get_abs_path().display()),
);
// Edit file
match self.edit_local_file(fsentry.get_abs_path().as_path()) {
Ok(_) => {
// Reload directory
let pwd: PathBuf = self.local().wrkdir.clone();
self.local_scan(pwd.as_path());
}
Err(err) => self.log_and_alert(LogLevel::Error, err),
}
}
}
}
pub(crate) fn action_edit_remote_file(&mut self) {
if self.get_remote_file_entry().is_some() {
let fsentry: FsEntry = self.get_remote_file_entry().unwrap().clone();
// Check if file
if let FsEntry::File(file) = fsentry.clone() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", fsentry.get_abs_path().display()),
);
// Edit file
match self.edit_remote_file(&file) {
Ok(_) => {
// Reload directory
let pwd: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(pwd.as_path());
}
Err(err) => self.log_and_alert(LogLevel::Error, err),
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
//! ## 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};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_exec(&mut self, input: String) {
match self.host.exec(input.as_str()) {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.local_scan(wrkdir.as_path());
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not execute command \"{}\": {}", input, err),
);
}
}
}
pub(crate) fn action_remote_exec(&mut self, input: String) {
match self.client.as_mut().exec(input.as_str()) {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
self.reload_remote_dir();
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not execute command \"{}\": {}", input, err),
);
}
}
}
}

View File

@@ -0,0 +1,146 @@
//! ## 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::super::browser::FileExplorerTab;
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
match self.host.find(input.as_str()) {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {}", err)),
}
}
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
match self.client.as_mut().find(input.as_str()) {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {}", err)),
}
}
pub(crate) fn action_find_changedir(&mut self, idx: usize) {
// Match entry
if let Some(entry) = self.found().as_ref().unwrap().get(idx) {
// Get path: if a directory, use directory path; if it is a File, get parent path
let path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => match file.abs_path.parent() {
None => PathBuf::from("."),
Some(p) => p.to_path_buf(),
},
};
// Change directory
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.local_changedir(path.as_path(), true)
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.remote_changedir(path.as_path(), true)
}
}
}
}
pub(crate) fn action_find_transfer(&mut self, idx: usize, name: Option<String>) {
let entry: Option<FsEntry> = self.found().as_ref().unwrap().get(idx).cloned();
if let Some(entry) = entry {
// Download file
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
let wrkdir: PathBuf = self.remote().wrkdir.clone();
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), name);
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), name);
}
}
}
}
pub(crate) fn action_find_delete(&mut self, idx: usize) {
let entry: Option<FsEntry> = self.found().as_ref().unwrap().get(idx).cloned();
if let Some(entry) = entry {
// Download file
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
let full_path: PathBuf = entry.get_abs_path();
// Delete file or directory and report status as popup
match self.host.remove(&entry) {
Ok(_) => {
// Reload files
let p: PathBuf = self.local().wrkdir.clone();
self.local_scan(p.as_path());
// Log
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", full_path.display()),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
full_path.display(),
err
),
);
}
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
let full_path: PathBuf = entry.get_abs_path();
// Delete file
match self.client.remove(&entry) {
Ok(_) => {
self.reload_remote_dir();
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", full_path.display()),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
full_path.display(),
err
),
);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
//! ## 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};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_mkdir(&mut self, input: String) {
match self.host.mkdir(PathBuf::from(input.as_str()).as_path()) {
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.local_scan(wrkdir.as_path());
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not create directory \"{}\": {}", input, err),
);
}
}
}
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
match self
.client
.as_mut()
.mkdir(PathBuf::from(input.as_str()).as_path())
{
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
self.reload_remote_dir();
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not create directory \"{}\": {}", input, err),
);
}
}
}
}

View File

@@ -0,0 +1,85 @@
//! ## 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.
*/
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel};
use tuirealm::{Payload, Value};
// actions
pub(crate) mod change_dir;
pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
pub(crate) mod exec;
pub(crate) mod find;
pub(crate) mod mkdir;
pub(crate) mod newfile;
pub(crate) mod rename;
pub(crate) mod save;
impl FileTransferActivity {
/// ### get_local_file_entry
///
/// Get local file entry
pub(crate) fn get_local_file_entry(&self) -> Option<&FsEntry> {
match self.get_local_file_idx() {
None => None,
Some(idx) => self.local().get(idx),
}
}
/// ### get_remote_file_entry
///
/// Get remote file entry
pub(crate) fn get_remote_file_entry(&self) -> Option<&FsEntry> {
match self.get_remote_file_idx() {
None => None,
Some(idx) => self.remote().get(idx),
}
}
// -- private
/// ### get_local_file_idx
///
/// Get index of selected file in the local tab
fn get_local_file_idx(&self) -> Option<usize> {
match self.view.get_state(super::COMPONENT_EXPLORER_LOCAL) {
Some(Payload::One(Value::Usize(idx))) => Some(idx),
_ => None,
}
}
/// ### get_remote_file_idx
///
/// Get index of selected file in the remote file
fn get_remote_file_idx(&self) -> Option<usize> {
match self.view.get_state(super::COMPONENT_EXPLORER_REMOTE) {
Some(Payload::One(Value::Usize(idx))) => Some(idx),
_ => None,
}
}
}

View File

@@ -0,0 +1,130 @@
//! ## 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, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_newfile(&mut self, input: String) {
// Check if file exists
let mut file_exists: bool = false;
for file in self.local().iter_files_all() {
if input == file.get_name() {
file_exists = true;
}
}
if file_exists {
self.log_and_alert(
LogLevel::Warn,
format!("File \"{}\" already exists", input,),
);
return;
}
// Create file
let file_path: PathBuf = PathBuf::from(input.as_str());
if let Err(err) = self.host.open_file_write(file_path.as_path()) {
self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
// Reload files
let path: PathBuf = self.local().wrkdir.clone();
self.local_scan(path.as_path());
}
pub(crate) fn action_remote_newfile(&mut self, input: String) {
// Check if file exists
let mut file_exists: bool = false;
for file in self.remote().iter_files_all() {
if input == file.get_name() {
file_exists = true;
}
}
if file_exists {
self.log_and_alert(
LogLevel::Warn,
format!("File \"{}\" already exists", input,),
);
return;
}
// Get path on remote
let file_path: PathBuf = PathBuf::from(input.as_str());
// Create file (on local)
match tempfile::NamedTempFile::new() {
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create tempfile: {}", err),
),
Ok(tfile) => {
// Stat tempfile
let local_file: FsEntry = match self.host.stat(tfile.path()) {
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not stat tempfile: {}", err),
);
return;
}
Ok(f) => f,
};
if let FsEntry::File(local_file) = local_file {
// Create file
match self.client.send_file(&local_file, file_path.as_path()) {
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
),
Ok(writer) => {
// Finalize write
if let Err(err) = self.client.on_sent(writer) {
self.log_and_alert(
LogLevel::Warn,
format!("Could not finalize file: {}", err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
// Reload files
let path: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(path.as_path());
}
}
}
}
}
}
}

View File

@@ -0,0 +1,102 @@
//! ## 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, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_rename(&mut self, input: String) {
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
if let Some(entry) = entry {
let mut dst_path: PathBuf = PathBuf::from(input);
// Check if path is relative
if dst_path.as_path().is_relative() {
let mut wrkdir: PathBuf = self.local().wrkdir.clone();
wrkdir.push(dst_path);
dst_path = wrkdir;
}
let full_path: PathBuf = entry.get_abs_path();
// Rename file or directory and report status as popup
match self.host.rename(&entry, dst_path.as_path()) {
Ok(_) => {
// Reload files
let path: PathBuf = self.local().wrkdir.clone();
self.local_scan(path.as_path());
// Log
self.log(
LogLevel::Info,
format!(
"Renamed file \"{}\" to \"{}\"",
full_path.display(),
dst_path.display()
),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not rename file \"{}\": {}", full_path.display(), err),
);
}
}
}
}
pub(crate) fn action_remote_rename(&mut self, input: String) {
if let Some(idx) = self.get_remote_file_idx() {
let entry = self.remote().get(idx).cloned();
if let Some(entry) = entry {
let dst_path: PathBuf = PathBuf::from(input);
let full_path: PathBuf = entry.get_abs_path();
// Rename file or directory and report status as popup
match self.client.as_mut().rename(&entry, dst_path.as_path()) {
Ok(_) => {
// Reload files
let path: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(path.as_path());
// Log
self.log(
LogLevel::Info,
format!(
"Renamed file \"{}\" to \"{}\"",
full_path.display(),
dst_path.display()
),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not rename file \"{}\": {}", full_path.display(), err),
);
}
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
//! ## 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, FsEntry};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) {
if let Some(idx) = self.get_local_file_idx() {
// Get pwd
let wrkdir: PathBuf = self.remote().wrkdir.clone();
if self.local().get(idx).is_some() {
let file: FsEntry = self.local().get(idx).unwrap().clone();
// Call upload; pass realfile, keep link name
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input));
}
}
}
pub(crate) fn action_remote_saveas(&mut self, input: String) {
if let Some(idx) = self.get_remote_file_idx() {
// Get pwd
let wrkdir: PathBuf = self.local().wrkdir.clone();
if self.remote().get(idx).is_some() {
let file: FsEntry = self.remote().get(idx).unwrap().clone();
// Call upload; pass realfile, keep link name
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input));
}
}
}
}

View File

@@ -0,0 +1,177 @@
//! ## 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.
*/
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
use crate::fs::FsEntry;
use crate::system::config_client::ConfigClient;
/// ## FileExplorerTab
///
/// File explorer tab
#[derive(Clone, Copy)]
pub(super) enum FileExplorerTab {
Local,
Remote,
FindLocal, // Find result tab
FindRemote, // Find result tab
}
/// ## Browser
///
/// Browser contains the browser options
pub(super) struct Browser {
local: FileExplorer, // Local File explorer state
remote: FileExplorer, // Remote File explorer state
found: Option<FileExplorer>, // File explorer for find result
tab: FileExplorerTab, // Current selected tab
pub sync_browsing: bool,
}
impl Browser {
/// ### new
///
/// Build a new `Browser` struct
pub(super) fn new(cli: Option<&ConfigClient>) -> Self {
Self {
local: Self::build_local_explorer(cli),
remote: Self::build_remote_explorer(cli),
found: None,
tab: FileExplorerTab::Local,
sync_browsing: false,
}
}
pub fn local(&self) -> &FileExplorer {
&self.local
}
pub fn local_mut(&mut self) -> &mut FileExplorer {
&mut self.local
}
pub fn remote(&self) -> &FileExplorer {
&self.remote
}
pub fn remote_mut(&mut self) -> &mut FileExplorer {
&mut self.remote
}
pub fn found(&self) -> Option<&FileExplorer> {
self.found.as_ref()
}
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
self.found.as_mut()
}
pub fn set_found(&mut self, files: Vec<FsEntry>) {
let mut explorer = Self::build_found_explorer();
explorer.set_files(files);
self.found = Some(explorer);
}
pub fn del_found(&mut self) {
self.found = None;
}
pub(super) fn tab(&self) -> FileExplorerTab {
self.tab
}
/// ### change_tab
///
/// Update tab value
pub(super) fn change_tab(&mut self, tab: FileExplorerTab) {
self.tab = tab;
}
/// ### toggle_sync_browsing
///
/// Invert the current state for the sync browsing
pub fn toggle_sync_browsing(&mut self) {
self.sync_browsing = !self.sync_browsing;
}
/// ### build_local_explorer
///
/// Build a file explorer with local host setup
pub fn build_local_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
let mut builder = Self::build_explorer(cli);
if let Some(cli) = cli {
builder.with_formatter(cli.get_local_file_fmt().as_deref());
}
builder.build()
}
/// ### build_remote_explorer
///
/// Build a file explorer with remote host setup
pub fn build_remote_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
let mut builder = Self::build_explorer(cli);
if let Some(cli) = cli {
builder.with_formatter(cli.get_remote_file_fmt().as_deref());
}
builder.build()
}
/// ### build_explorer
///
/// Build explorer reading configuration from `ConfigClient`
fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorerBuilder {
let mut builder: FileExplorerBuilder = FileExplorerBuilder::new();
// Set common keys
builder
.with_file_sorting(FileSorting::ByName)
.with_stack_size(16);
match &cli {
Some(cli) => {
builder // Build according to current configuration
.with_group_dirs(cli.get_group_dirs())
.with_hidden_files(cli.get_show_hidden_files());
}
None => {
builder // Build default
.with_group_dirs(Some(GroupDirs::First));
}
};
builder
}
/// ### build_found_explorer
///
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
fn build_found_explorer() -> FileExplorer {
FileExplorerBuilder::new()
.with_file_sorting(FileSorting::ByName)
.with_group_dirs(Some(GroupDirs::First))
.with_hidden_files(true)
.with_stack_size(0)
.with_formatter(Some("{NAME} {SYMLINK}"))
.build()
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage;
// Ext
use std::env;
use std::path::{Path, PathBuf};
const LOG_CAPACITY: usize = 256;
impl FileTransferActivity {
/// ### log
///
/// Add message to log events
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
// Create log record
let record: LogRecord = LogRecord::new(level, msg);
//Check if history overflows the size
if self.log_records.len() + 1 > LOG_CAPACITY {
self.log_records.pop_back(); // Start cleaning events from back
}
// Eventually push front the new record
self.log_records.push_front(record);
// Update log
let msg = self.update_logbox();
self.update(msg);
}
/// ### log_and_alert
///
/// Add message to log events and also display it as an alert
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
self.mount_error(msg.as_str());
self.log(level, msg);
// Update log
let msg = self.update_logbox();
self.update(msg);
}
/// ### init_config_client
///
/// Initialize configuration client if possible.
/// This function doesn't return errors.
pub(super) fn init_config_client() -> Option<ConfigClient> {
match environment::init_config_dir() {
Ok(termscp_dir) => match termscp_dir {
Some(termscp_dir) => {
// Make configuration file path and ssh keys path
let (config_path, ssh_keys_path): (PathBuf, PathBuf) =
environment::get_config_paths(termscp_dir.as_path());
match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) {
Ok(config_client) => Some(config_client),
Err(_) => None,
}
}
None => None,
},
Err(_) => None,
}
}
/// ### make_ssh_storage
///
/// Make ssh storage from `ConfigClient` if possible, empty otherwise
pub(super) fn make_ssh_storage(cli: Option<&ConfigClient>) -> SshKeyStorage {
match cli {
Some(cli) => SshKeyStorage::storage_from_config(cli),
None => SshKeyStorage::empty(),
}
}
/// ### setup_text_editor
///
/// Set text editor to use
pub(super) fn setup_text_editor(&self) {
if let Some(config_cli) = self.context.as_ref().unwrap().config_client.as_ref() {
// Set text editor
env::set_var("EDITOR", config_cli.get_text_editor());
}
}
/// ### read_input_event
///
/// Read one event.
/// Returns whether at least one event has been handled
pub(super) fn read_input_event(&mut self) -> bool {
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
// Handle event
let msg = self.view.on(event);
self.update(msg);
// Return true
true
} else {
// Error
false
}
}
/// ### local_to_abs_path
///
/// Convert a path to absolute according to local explorer
pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf {
match path.is_relative() {
true => {
let mut d: PathBuf = self.local().wrkdir.clone();
d.push(path);
d
}
false => path.to_path_buf(),
}
}
/// ### remote_to_abs_path
///
/// Convert a path to absolute according to remote explorer
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
match path.is_relative() {
true => {
let mut wrkdir: PathBuf = self.remote().wrkdir.clone();
wrkdir.push(path);
wrkdir
}
false => path.to_path_buf(),
}
}
}

View File

@@ -0,0 +1,358 @@
//! ## 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.
*/
// This module is split into files, cause it's just too big
pub(self) mod actions;
pub(self) mod browser;
pub(self) mod misc;
pub(self) mod session;
pub(self) mod update;
pub(self) mod view;
// Dependencies
extern crate chrono;
extern crate crossterm;
extern crate textwrap;
extern crate tuirealm;
// locals
use super::{Activity, Context, ExitReason};
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
use crate::filetransfer::scp_transfer::ScpFileTransfer;
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
use browser::Browser;
// Includes
use chrono::{DateTime, Local};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::collections::VecDeque;
use std::path::PathBuf;
use std::time::Instant;
use tuirealm::View;
// -- Storage keys
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
// -- components
const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL";
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND";
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
const COMPONENT_PROGRESS_BAR: &str = "PROGRESS_BAR";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
const COMPONENT_INPUT_COPY: &str = "INPUT_COPY";
const COMPONENT_INPUT_EXEC: &str = "INPUT_EXEC";
const COMPONENT_INPUT_FIND: &str = "INPUT_FIND";
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
const COMPONENT_SPAN_STATUS_BAR: &str = "STATUS_BAR";
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
/// ## LogLevel
///
/// Log level type
enum LogLevel {
Error,
Warn,
Info,
}
/// ## LogRecord
///
/// Log record entry
struct LogRecord {
pub time: DateTime<Local>,
pub level: LogLevel,
pub msg: String,
}
impl LogRecord {
/// ### new
///
/// Instantiates a new LogRecord
pub fn new(level: LogLevel, msg: String) -> LogRecord {
LogRecord {
time: Local::now(),
level,
msg,
}
}
}
/// ### TransferStates
///
/// TransferStates contains the states related to the transfer process
struct TransferStates {
pub progress: f64, // Current read/write progress (percentage)
pub started: Instant, // Instant the transfer process started
pub aborted: bool, // Describes whether the transfer process has been aborted
pub bytes_written: usize, // Bytes written during transfer
pub bytes_total: usize, // Total bytes to write
}
impl TransferStates {
/// ### new
///
/// Instantiates a new transfer states
pub fn new() -> TransferStates {
TransferStates {
progress: 0.0,
started: Instant::now(),
aborted: false,
bytes_written: 0,
bytes_total: 0,
}
}
/// ### reset
///
/// Re-intiialize transfer states
pub fn reset(&mut self) {
self.progress = 0.0;
self.started = Instant::now();
self.aborted = false;
self.bytes_written = 0;
self.bytes_total = 0;
}
/// ### set_progress
///
/// Calculate progress percentage based on current progress
pub fn set_progress(&mut self, w: usize, sz: usize) {
self.bytes_written = w;
self.bytes_total = sz;
let mut prog: f64 = ((self.bytes_written as f64) * 100.0) / (self.bytes_total as f64);
// Check value
if prog > 100.0 {
prog = 100.0;
} else if prog < 0.0 {
prog = 0.0;
}
self.progress = prog;
}
/// ### byte_per_second
///
/// Calculate bytes per second
pub fn bytes_per_second(&self) -> u64 {
// bytes_written : elapsed_secs = x : 1
let elapsed_secs: u64 = self.started.elapsed().as_secs();
match elapsed_secs {
0 => match self.bytes_written == self.bytes_total {
// NOTE: would divide by 0 :D
true => self.bytes_total as u64, // Download completed in less than 1 second
false => 0, // 0 B/S
},
_ => self.bytes_written as u64 / elapsed_secs,
}
}
}
impl Default for TransferStates {
fn default() -> Self {
Self::new()
}
}
/// ## FileTransferActivity
///
/// FileTransferActivity is the data holder for the file transfer activity
pub struct FileTransferActivity {
exit_reason: Option<ExitReason>, // Exit reason
context: Option<Context>, // Context holder
view: View, // View
host: Localhost, // Localhost
client: Box<dyn FileTransfer>, // File transfer client
browser: Browser, // Browser
log_records: VecDeque<LogRecord>, // Log records
transfer: TransferStates, // Transfer states
}
impl FileTransferActivity {
/// ### new
///
/// Instantiates a new FileTransferActivity
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
// Get config client
let config_client: Option<ConfigClient> = Self::init_config_client();
FileTransferActivity {
exit_reason: None,
context: None,
view: View::init(),
host,
client: match protocol {
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
Self::make_ssh_storage(config_client.as_ref()),
)),
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new(
Self::make_ssh_storage(config_client.as_ref()),
)),
},
browser: Browser::new(config_client.as_ref()),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
transfer: TransferStates::default(),
}
}
pub(crate) fn local(&self) -> &FileExplorer {
self.browser.local()
}
pub(crate) fn local_mut(&mut self) -> &mut FileExplorer {
self.browser.local_mut()
}
pub(crate) fn remote(&self) -> &FileExplorer {
self.browser.remote()
}
pub(crate) fn remote_mut(&mut self) -> &mut FileExplorer {
self.browser.remote_mut()
}
pub(crate) fn found(&self) -> Option<&FileExplorer> {
self.browser.found()
}
pub(crate) fn found_mut(&mut self) -> Option<&mut FileExplorer> {
self.browser.found_mut()
}
}
/**
* Activity Trait
* Keep it clean :)
* Use methods instead!
*/
impl Activity for FileTransferActivity {
/// ### on_create
///
/// `on_create` is the function which must be called to initialize the activity.
/// `on_create` must initialize all the data structures used by the activity
fn on_create(&mut self, context: Context) {
// Set context
self.context = Some(context);
// Clear terminal
self.context.as_mut().unwrap().clear_screen();
// Put raw mode on enabled
let _ = enable_raw_mode();
// Set working directory
let pwd: PathBuf = self.host.pwd();
// Get files at current wd
self.local_scan(pwd.as_path());
self.local_mut().wrkdir = pwd;
// Configure text editor
self.setup_text_editor();
// init view
self.init();
// Verify error state from context
if let Some(err) = self.context.as_mut().unwrap().get_error() {
self.mount_fatal(&err);
}
}
/// ### on_draw
///
/// `on_draw` is the function which draws the graphical interface.
/// This function must be called at each tick to refresh the interface
fn on_draw(&mut self) {
// Should ui actually be redrawned?
let mut redraw: bool = false;
// Context must be something
if self.context.is_none() {
return;
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
// Set init state to connecting popup
self.mount_wait(msg.as_str());
// Force ui draw
self.view();
// Connect to remote
self.connect();
// Redraw
redraw = true;
}
// Handle input events (if false, becomes true; otherwise remains true)
redraw |= self.read_input_event();
// @! draw interface
if redraw {
self.view();
}
}
/// ### will_umount
///
/// `will_umount` is the method which must be able to report to the activity manager, whether
/// the activity should be terminated or not.
/// If not, the call will return `None`, otherwise return`Some(ExitReason)`
fn will_umount(&self) -> Option<&ExitReason> {
self.exit_reason.as_ref()
}
/// ### on_destroy
///
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
/// This function must be called once before terminating the activity.
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
let _ = disable_raw_mode();
// Disconnect client
if self.client.is_connected() {
let _ = self.client.disconnect();
}
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
}
}
}

View File

@@ -0,0 +1,898 @@
//! ## 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.
*/
// Deps
extern crate bytesize;
extern crate content_inspector;
extern crate crossterm;
extern crate tempfile;
// Locals
use super::{FileTransferActivity, LogLevel};
use crate::filetransfer::FileTransferError;
use crate::fs::{FsEntry, FsFile};
use crate::host::HostError;
use crate::utils::fmt::fmt_millis;
// Ext
use bytesize::ByteSize;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::fs::OpenOptions;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime};
use thiserror::Error;
/// ## TransferErrorReason
///
/// Describes the reason that caused an error during a file transfer
#[derive(Error, Debug)]
enum TransferErrorReason {
#[error("File transfer aborted")]
Abrupted,
#[error("Failed to seek file: {0}")]
CouldNotRewind(std::io::Error),
#[error("I/O error on localhost: {0}")]
LocalIoError(std::io::Error),
#[error("Host error: {0}")]
HostError(HostError),
#[error("I/O error on remote: {0}")]
RemoteIoError(std::io::Error),
#[error("File transfer error: {0}")]
FileTransferError(FileTransferError),
}
impl FileTransferActivity {
/// ### connect
///
/// Connect to remote
pub(super) fn connect(&mut self) {
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
let addr: String = params.address.clone();
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
// Connect to remote
match self.client.connect(
params.address.clone(),
params.port,
params.username.clone(),
params.password.clone(),
) {
Ok(welcome) => {
if let Some(banner) = welcome {
// Log welcome
self.log(
LogLevel::Info,
format!("Established connection with '{}': \"{}\"", addr, banner),
);
}
// Try to change directory to entry directory
let mut remote_chdir: Option<PathBuf> = None;
if let Some(entry_directory) = &entry_dir {
remote_chdir = Some(entry_directory.clone());
}
if let Some(entry_directory) = remote_chdir {
self.remote_changedir(entry_directory.as_path(), false);
}
// Set state to explorer
self.umount_wait();
self.reload_remote_dir();
// Update file lists
self.update_local_filelist();
self.update_remote_filelist();
}
Err(err) => {
// Set popup fatal error
self.mount_fatal(&err.to_string());
}
}
}
/// ### disconnect
///
/// disconnect from remote
pub(super) fn disconnect(&mut self) {
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
let msg: String = format!("Disconnecting from {}...", params.address);
// Show popup disconnecting
self.mount_wait(msg.as_str());
// Disconnect
let _ = self.client.disconnect();
// Quit
self.exit_reason = Some(super::ExitReason::Disconnect);
}
/// ### disconnect_and_quit
///
/// disconnect from remote and then quit
pub(super) fn disconnect_and_quit(&mut self) {
self.disconnect();
self.exit_reason = Some(super::ExitReason::Quit);
}
/// ### reload_remote_dir
///
/// Reload remote directory entries
pub(super) fn reload_remote_dir(&mut self) {
// Get current entries
if let Ok(pwd) = self.client.pwd() {
self.remote_scan(pwd.as_path());
// Set wrkdir
self.remote_mut().wrkdir = pwd;
}
}
/// ### filetransfer_send
///
/// Send fs entry to remote.
/// If dst_name is Some, entry will be saved with a different name.
/// If entry is a directory, this applies to directory only
pub(super) fn filetransfer_send(
&mut self,
entry: &FsEntry,
curr_remote_path: &Path,
dst_name: Option<String>,
) {
// Write popup
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Get remote path
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
let remote_file_name: PathBuf = match dst_name {
Some(s) => PathBuf::from(s.as_str()),
None => PathBuf::from(file_name.as_str()),
};
remote_path.push(remote_file_name);
// Match entry
match entry {
FsEntry::File(file) => {
if let Err(err) =
self.filetransfer_send_file(file, remote_path.as_path(), file_name)
{
// Log error
self.log_and_alert(
LogLevel::Error,
format!("Failed to upload file {}: {}", file.name, err),
);
// If transfer was abrupted or there was an IO error on remote, remove file
if matches!(
err,
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
) {
// Stat file on remote and remove it if exists
match self.client.stat(remote_path.as_path()) {
Err(err) => self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
remote_path.display(),
err
),
),
Ok(entry) => {
if let Err(err) = self.client.remove(&entry) {
self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
remote_path.display(),
err
),
);
}
}
}
}
}
}
FsEntry::Directory(dir) => {
// Create directory on remote
match self.client.mkdir(remote_path.as_path()) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Created directory \"{}\"", remote_path.display()),
);
// Get files in dir
match self.host.scan_dir(dir.abs_path.as_path()) {
Ok(entries) => {
// Iterate over files
for entry in entries.iter() {
// If aborted; break
if self.transfer.aborted {
break;
}
// Send entry; name is always None after first call
self.filetransfer_send(&entry, remote_path.as_path(), None);
}
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not scan directory \"{}\": {}",
dir.abs_path.display(),
err
),
);
}
}
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Failed to create directory \"{}\": {}",
remote_path.display(),
err
),
);
}
}
}
}
// Scan dir on remote
let path: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(path.as_path());
// If aborted; show popup
if self.transfer.aborted {
// Log abort
self.log_and_alert(
LogLevel::Warn,
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
);
// Set aborted to false
self.transfer.aborted = false;
} else {
// @! Successful
// Eventually, Remove progress bar
self.umount_progress_bar();
}
}
/// ### filetransfer_recv
///
/// Recv fs entry from remote.
/// If dst_name is Some, entry will be saved with a different name.
/// If entry is a directory, this applies to directory only
pub(super) fn filetransfer_recv(
&mut self,
entry: &FsEntry,
local_path: &Path,
dst_name: Option<String>,
) {
// Write popup
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Match entry
match entry {
FsEntry::File(file) => {
// Get local file
let mut local_file_path: PathBuf = PathBuf::from(local_path);
let local_file_name: String = match dst_name {
Some(n) => n,
None => file.name.clone(),
};
local_file_path.push(local_file_name.as_str());
// Download file
if let Err(err) =
self.filetransfer_recv_file(local_file_path.as_path(), file, file_name)
{
self.log_and_alert(
LogLevel::Error,
format!("Could not download file {}: {}", file.name, err),
);
// If transfer was abrupted or there was an IO error on remote, remove file
if matches!(
err,
TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_)
) {
// Stat file
match self.host.stat(local_file_path.as_path()) {
Err(err) => self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
local_file_path.display(),
err
),
),
Ok(entry) => {
if let Err(err) = self.host.remove(&entry) {
self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
local_file_path.display(),
err
),
);
}
}
}
}
}
}
FsEntry::Directory(dir) => {
// Get dir name
let mut local_dir_path: PathBuf = PathBuf::from(local_path);
match dst_name {
Some(name) => local_dir_path.push(name),
None => local_dir_path.push(dir.name.as_str()),
}
// Create directory on local
match self.host.mkdir_ex(local_dir_path.as_path(), true) {
Ok(_) => {
// Apply file mode to directory
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
if let Some(pex) = dir.unix_pex {
if let Err(err) = self.host.chmod(local_dir_path.as_path(), pex) {
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
pex,
local_dir_path.display(),
err
),
);
}
}
self.log(
LogLevel::Info,
format!("Created directory \"{}\"", local_dir_path.display()),
);
// Get files in dir
match self.client.list_dir(dir.abs_path.as_path()) {
Ok(entries) => {
// Iterate over files
for entry in entries.iter() {
// If transfer has been aborted; break
if self.transfer.aborted {
break;
}
// Receive entry; name is always None after first call
// Local path becomes local_dir_path
self.filetransfer_recv(&entry, local_dir_path.as_path(), None);
}
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not scan directory \"{}\": {}",
dir.abs_path.display(),
err
),
);
}
}
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Failed to create directory \"{}\": {}",
local_dir_path.display(),
err
),
);
}
}
}
}
// Reload directory on local
self.local_scan(local_path);
// if aborted; show alert
if self.transfer.aborted {
// Log abort
self.log_and_alert(
LogLevel::Warn,
format!(
"Download aborted for \"{}\"!",
entry.get_abs_path().display()
),
);
// Reset aborted to false
self.transfer.aborted = false;
} else {
// Eventually, Reset input mode to explorer
self.umount_progress_bar();
}
}
/// ### filetransfer_send_file
///
/// Send local file and write it to remote path
fn filetransfer_send_file(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Upload file
// Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) {
Ok(mut fhnd) => match self.client.send_file(local, remote) {
Ok(mut rhnd) => {
// Write file
let file_size: usize =
fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// rewind
if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
// Reset transfer states
self.transfer.reset();
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Instant = Instant::now();
// Mount progress bar
self.mount_progress_bar();
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted {
// Handle input events (each 500ms)
if last_input_event_fetch.elapsed().as_millis() >= 500 {
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Instant::now();
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
match fhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut buf_start: usize = 0;
while buf_start < bytes_read {
// Write bytes
match rhnd.write(&buffer[buf_start..bytes_read]) {
Ok(bytes) => {
buf_start += bytes;
}
Err(err) => {
self.umount_progress_bar();
return Err(TransferErrorReason::RemoteIoError(
err,
));
}
}
}
}
}
Err(err) => {
self.umount_progress_bar();
return Err(TransferErrorReason::LocalIoError(err));
}
}
// Increase progress
self.transfer.set_progress(total_bytes_written, file_size);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.progress - 1.0 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"...", file_name));
self.view();
last_progress_val = self.transfer.progress;
}
}
// Umount progress bar
self.umount_progress_bar();
// Finalize stream
if let Err(err) = self.client.on_sent(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.started.elapsed()),
ByteSize(self.transfer.bytes_per_second()),
),
);
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => return Err(TransferErrorReason::HostError(err)),
}
Ok(())
}
/// ### filetransfer_recv_file
///
/// Receive file from remote and write it to local path
fn filetransfer_recv_file(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Try to open local file
match self.host.open_file_write(local) {
Ok(mut local_file) => {
// Download file from remote
match self.client.recv_file(remote) {
Ok(mut rhnd) => {
let mut total_bytes_written: usize = 0;
// Reset transfer states
self.transfer.reset();
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Instant = Instant::now();
// Mount progress bar
self.mount_progress_bar();
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted {
// Handle input events (each 500 ms)
if last_input_event_fetch.elapsed().as_millis() >= 500 {
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Instant::now();
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
match rhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut buf_start: usize = 0;
while buf_start < bytes_read {
// Write bytes
match local_file.write(&buffer[buf_start..bytes_read]) {
Ok(bytes) => buf_start += bytes,
Err(err) => {
self.umount_progress_bar();
return Err(TransferErrorReason::LocalIoError(
err,
));
}
}
}
}
}
Err(err) => {
self.umount_progress_bar();
return Err(TransferErrorReason::RemoteIoError(err));
}
}
// Set progress
self.transfer.set_progress(total_bytes_written, remote.size);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.progress - 1.0 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.progress;
}
}
// Umount progress bar
self.umount_progress_bar();
// Finalize stream
if let Err(err) = self.client.on_recv(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
if let Some(pex) = remote.unix_pex {
if let Err(err) = self.host.chmod(local, pex) {
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
pex,
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.started.elapsed()),
ByteSize(self.transfer.bytes_per_second()),
),
);
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
}
}
Err(err) => return Err(TransferErrorReason::HostError(err)),
}
Ok(())
}
/// ### local_scan
///
/// Scan current local directory
pub(super) fn local_scan(&mut self, path: &Path) {
match self.host.scan_dir(path) {
Ok(files) => {
// Set files and sort (sorting is implicit)
self.local_mut().set_files(files);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current directory: {}", err),
);
}
}
}
/// ### remote_scan
///
/// Scan current remote directory
pub(super) fn remote_scan(&mut self, path: &Path) {
match self.client.list_dir(path) {
Ok(files) => {
// Set files and sort (sorting is implicit)
self.remote_mut().set_files(files);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current directory: {}", err),
);
}
}
}
/// ### local_changedir
///
/// Change directory for local
pub(super) fn local_changedir(&mut self, path: &Path, push: bool) {
// Get current directory
let prev_dir: PathBuf = self.local().wrkdir.clone();
// Change directory
match self.host.change_wrkdir(path) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Changed directory on local: {}", path.display()),
);
// Reload files
self.local_scan(path);
// Set wrkdir
self.local_mut().wrkdir = PathBuf::from(path);
// Push prev_dir to stack
if push {
self.local_mut().pushd(prev_dir.as_path())
}
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not change working directory: {}", err),
);
}
}
}
pub(super) fn remote_changedir(&mut self, path: &Path, push: bool) {
// Get current directory
let prev_dir: PathBuf = self.remote().wrkdir.clone();
// Change directory
match self.client.as_mut().change_dir(path) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Changed directory on remote: {}", path.display()),
);
// Update files
self.remote_scan(path);
// Set wrkdir
self.remote_mut().wrkdir = PathBuf::from(path);
// Push prev_dir to stack
if push {
self.remote_mut().pushd(prev_dir.as_path())
}
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not change working directory: {}", err),
);
}
}
}
/// ### edit_local_file
///
/// Edit a file on localhost
pub(super) fn edit_local_file(&mut self, path: &Path) -> Result<(), String> {
// Read first 2048 bytes or less from file to check if it is textual
match OpenOptions::new().read(true).open(path) {
Ok(mut f) => {
// Read
let mut buff: [u8; 2048] = [0; 2048];
match f.read(&mut buff) {
Ok(size) => {
if content_inspector::inspect(&buff[0..size]).is_binary() {
return Err("Could not open file in editor: file is binary".to_string());
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
// Put input mode back to normal
let _ = disable_raw_mode();
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Open editor
match edit::edit_file(path) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Changes performed through editor saved to \"{}\"!",
path.display()
),
),
Err(err) => return Err(format!("Could not open editor: {}", err)),
}
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
// Enter alternate mode
ctx.enter_alternate_screen();
}
// Re-enable raw mode
let _ = enable_raw_mode();
Ok(())
}
/// ### edit_remote_file
///
/// Edit file on remote host
pub(super) fn edit_remote_file(&mut self, file: &FsFile) -> Result<(), String> {
// Create temp file
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
Ok(f) => f,
Err(err) => {
return Err(format!("Could not create temporary file: {}", err));
}
};
// Download file
if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file, file.name.clone()) {
return Err(format!("Could not open file {}: {}", file.name, err));
}
// Get current file modification time
let prev_mtime: SystemTime = match self.host.stat(tmpfile.path()) {
Ok(e) => e.get_last_change_time(),
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.path().display(),
err
))
}
};
// Edit file
if let Err(err) = self.edit_local_file(tmpfile.path()) {
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.path()) {
Ok(e) => e,
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.path().display(),
err
))
}
};
// Check if file has changed
match prev_mtime != tmpfile_entry.get_last_change_time() {
true => {
self.log(
LogLevel::Info,
format!(
"File \"{}\" has changed; writing changes to remote",
file.abs_path.display()
),
);
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.path()) {
Ok(e) => e,
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.path().display(),
err
))
}
};
// Write file
let tmpfile_entry: &FsFile = match &tmpfile_entry {
FsEntry::Directory(_) => panic!("tempfile is a directory for some reason"),
FsEntry::File(f) => f,
};
// Send file
if let Err(err) = self.filetransfer_send_file(
tmpfile_entry,
file.abs_path.as_path(),
file.name.clone(),
) {
return Err(format!(
"Could not write file {}: {}",
file.abs_path.display(),
err
));
}
}
false => {
self.log(
LogLevel::Info,
format!("File \"{}\" hasn't changed", file.abs_path.display()),
);
}
}
Ok(())
}
}

View File

@@ -0,0 +1,912 @@
//! ## 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.
*/
// deps
extern crate bytesize;
// locals
use super::{
browser::FileExplorerTab, FileTransferActivity, LogLevel, COMPONENT_EXPLORER_FIND,
COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY,
COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR,
COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE,
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder};
use crate::ui::keymap::*;
// externals
use bytesize::ByteSize;
use std::path::{Path, PathBuf};
use tuirealm::{
components::progress_bar::ProgressBarPropsBuilder,
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
tui::style::Color,
Msg, Payload, Value,
};
impl FileTransferActivity {
// -- update
/// ### update
///
/// Update auth activity model based on msg
/// The function exits when returns None
pub(super) fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None, // Exit after None
Some(msg) => match msg {
// -- local tab
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_RIGHT) => {
// Change tab
self.view.active(COMPONENT_EXPLORER_REMOTE);
self.browser.change_tab(FileExplorerTab::Remote);
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
self.action_go_to_previous_local_dir(false);
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
// Match selected file
let mut entry: Option<FsEntry> = None;
if let Some(e) = self.local().get(*idx) {
entry = Some(e.clone());
}
if let Some(entry) = entry {
if self.action_enter_local_dir(entry, false) {
// Update file list if sync
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
self.update_local_filelist()
} else {
None
}
} else {
None
}
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => {
// Get pwd
let wrkdir: PathBuf = self.remote().wrkdir.clone();
// Get file and clone (due to mutable / immutable stuff...)
if self.get_local_file_entry().is_some() {
let file: FsEntry = self.get_local_file_entry().unwrap().clone();
let name: String = file.get_name().to_string();
// Call upload; pass realfile, keep link name
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(name));
self.update_remote_filelist()
} else {
None
}
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
self.local_mut().toggle_hidden_files();
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
let file: Option<FsEntry> = self.get_local_file_entry().cloned();
if let Some(file) = file {
self.mount_file_info(&file);
}
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => {
// Reload directory
let pwd: PathBuf = self.local().wrkdir.clone();
self.local_scan(pwd.as_path());
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_O) => {
self.action_edit_local_file();
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_U) => {
self.action_go_to_local_upper_dir(false);
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
// Reload file list component
self.update_local_filelist()
}
// -- remote tab
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_LEFT) => {
// Change tab
self.view.active(COMPONENT_EXPLORER_LOCAL);
self.browser.change_tab(FileExplorerTab::Local);
None
}
(COMPONENT_EXPLORER_REMOTE, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
// Match selected file
let mut entry: Option<FsEntry> = None;
if let Some(e) = self.remote().get(*idx) {
entry = Some(e.clone());
}
if let Some(entry) = entry {
if self.action_enter_remote_dir(entry, false) {
// Update file list if sync
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
self.update_remote_filelist()
} else {
None
}
} else {
None
}
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => {
// Get file and clone (due to mutable / immutable stuff...)
if self.get_remote_file_entry().is_some() {
let file: FsEntry = self.get_remote_file_entry().unwrap().clone();
let name: String = file.get_name().to_string();
// Call upload; pass realfile, keep link name
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(name));
self.update_local_filelist()
} else {
None
}
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
self.action_go_to_previous_remote_dir(false);
// If sync is enabled update local too
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
self.remote_mut().toggle_hidden_files();
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
let file: Option<FsEntry> = self.get_remote_file_entry().cloned();
if let Some(file) = file {
self.mount_file_info(&file);
}
None
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => {
// Reload directory
let pwd: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(pwd.as_path());
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_O) => {
// Edit file
self.action_edit_remote_file();
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_U) => {
self.action_go_to_remote_upper_dir(false);
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
// Reload file list component
self.update_remote_filelist()
}
// -- common explorer keys
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_B)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_B) => {
// Show sorting file
self.mount_file_sorting();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_C)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_C) => {
self.mount_copy();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_D)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_D) => {
self.mount_mkdir();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_F)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_F) => {
self.mount_find_input();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_G)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_G) => {
self.mount_goto();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_H)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_H) => {
self.mount_help();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_N)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_N) => {
self.mount_newfile();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Q)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Q)
| (COMPONENT_LOG_BOX, &MSG_KEY_CHAR_Q) => {
self.mount_quit();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_R)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_R) => {
// Mount rename
self.mount_rename();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_S)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_S)
| (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_S) => {
// Mount save as
self.mount_saveas();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_X)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_X) => {
// Mount exec
self.mount_exec();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Y)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Y) => {
// Toggle browser sync
self.browser.toggle_sync_browsing();
// Update status bar
self.refresh_status_bar();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC)
| (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => {
self.mount_disconnect();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_DEL)
| (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_E)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_DEL)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_E)
| (COMPONENT_EXPLORER_FIND, &MSG_KEY_DEL)
| (COMPONENT_EXPLORER_FIND, &MSG_KEY_CHAR_E) => {
self.mount_radio_delete();
None
}
// -- find result explorer
(COMPONENT_EXPLORER_FIND, &MSG_KEY_ESC) => {
// Umount find
self.umount_find();
// Finalize find
self.finalize_find();
None
}
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
// Find changedir
self.action_find_changedir(*idx);
// Umount find
self.umount_find();
// Finalize find
self.finalize_find();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
// Get entry
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
Some(Payload::One(Value::Usize(idx))) => {
self.action_find_transfer(idx, None);
// Reload files
match self.browser.tab() {
// NOTE: swapped by purpose
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
_ => None,
}
}
_ => None,
}
}
// -- switch to log
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_TAB)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_TAB) => {
self.view.active(COMPONENT_LOG_BOX); // Active log box
None
}
// -- Log box
(COMPONENT_LOG_BOX, &MSG_KEY_TAB) => {
self.view.blur(); // Blur log box
None
}
// -- copy popup
(COMPONENT_INPUT_COPY, &MSG_KEY_ESC) => {
self.umount_copy();
None
}
(COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
// Copy file
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_copy(input.to_string()),
FileExplorerTab::Remote => self.action_remote_copy(input.to_string()),
_ => panic!("Found tab doesn't support COPY"),
}
self.umount_copy();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- exec popup
(COMPONENT_INPUT_EXEC, &MSG_KEY_ESC) => {
self.umount_exec();
None
}
(COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
// Exex command
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_exec(input.to_string()),
FileExplorerTab::Remote => self.action_remote_exec(input.to_string()),
_ => panic!("Found tab doesn't support EXEC"),
}
self.umount_exec();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- find popup
(COMPONENT_INPUT_FIND, &MSG_KEY_ESC) => {
self.umount_find_input();
None
}
(COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
self.umount_find_input();
// Find
let res: Result<Vec<FsEntry>, String> = match self.browser.tab() {
FileExplorerTab::Local => self.action_local_find(input.to_string()),
FileExplorerTab::Remote => self.action_remote_find(input.to_string()),
_ => panic!("Trying to search for files, while already in a find result"),
};
// Match result
match res {
Err(err) => {
// Mount error
self.mount_error(err.as_str());
}
Ok(files) => {
// Create explorer and load files
self.browser.set_found(files);
// Mount result widget
self.mount_find(input);
self.update_find_list();
// Initialize tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal,
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal,
});
}
}
None
}
// -- goto popup
(COMPONENT_INPUT_GOTO, &MSG_KEY_ESC) => {
self.umount_goto();
None
}
(COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => {
self.action_change_local_dir(input.to_string(), false)
}
FileExplorerTab::Remote => {
self.action_change_remote_dir(input.to_string(), false)
}
_ => panic!("Found tab doesn't support GOTO"),
}
// Umount
self.umount_goto();
// Reload files if sync
if self.browser.sync_browsing {
match self.browser.tab() {
FileExplorerTab::Remote => self.update_local_filelist(),
FileExplorerTab::Local => self.update_remote_filelist(),
_ => None,
};
}
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- make directory
(COMPONENT_INPUT_MKDIR, &MSG_KEY_ESC) => {
self.umount_mkdir();
None
}
(COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_mkdir(input.to_string()),
FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()),
_ => panic!("Found tab doesn't support MKDIR"),
}
self.umount_mkdir();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- new file
(COMPONENT_INPUT_NEWFILE, &MSG_KEY_ESC) => {
self.umount_newfile();
None
}
(COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_newfile(input.to_string()),
FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()),
_ => panic!("Found tab doesn't support NEWFILE"),
}
self.umount_newfile();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- rename
(COMPONENT_INPUT_RENAME, &MSG_KEY_ESC) => {
self.umount_rename();
None
}
(COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_rename(input.to_string()),
FileExplorerTab::Remote => self.action_remote_rename(input.to_string()),
_ => panic!("Found tab doesn't support RENAME"),
}
self.umount_rename();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- save as
(COMPONENT_INPUT_SAVEAS, &MSG_KEY_ESC) => {
self.umount_saveas();
None
}
(COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_saveas(input.to_string()),
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
if let Some(Payload::One(Value::Usize(idx))) =
self.view.get_state(COMPONENT_EXPLORER_FIND)
{
self.action_find_transfer(idx, Some(input.to_string()));
}
}
}
self.umount_saveas();
// Reload files
match self.browser.tab() {
// NOTE: Swapped is intentional
FileExplorerTab::Local => self.update_remote_filelist(),
FileExplorerTab::Remote => self.update_local_filelist(),
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
}
}
// -- fileinfo
(COMPONENT_LIST_FILEINFO, &MSG_KEY_ENTER)
| (COMPONENT_LIST_FILEINFO, &MSG_KEY_ESC) => {
self.umount_file_info();
None
}
// -- delete
(COMPONENT_RADIO_DELETE, &MSG_KEY_ESC)
| (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
self.umount_radio_delete();
None
}
(COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Choice is 'YES'
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_delete(),
FileExplorerTab::Remote => self.action_remote_delete(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
if let Some(Payload::One(Value::Usize(idx))) =
self.view.get_state(COMPONENT_EXPLORER_FIND)
{
self.action_find_delete(idx);
// Reload entries
self.found_mut().unwrap().del_entry(idx);
self.update_find_list();
}
}
}
self.umount_radio_delete();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
FileExplorerTab::FindLocal => self.update_local_filelist(),
FileExplorerTab::FindRemote => self.update_remote_filelist(),
}
}
// -- disconnect
(COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC)
| (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
self.umount_disconnect();
None
}
(COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
self.disconnect();
self.umount_disconnect();
None
}
// -- quit
(COMPONENT_RADIO_QUIT, &MSG_KEY_ESC)
| (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
self.disconnect_and_quit();
self.umount_quit();
None
}
// -- sorting
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC)
| (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => {
self.umount_file_sorting();
None
}
(COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::One(Value::Usize(mode)))) => {
// Get sorting mode
let sorting: FileSorting = match mode {
1 => FileSorting::ByModifyTime,
2 => FileSorting::ByCreationTime,
3 => FileSorting::BySize,
_ => FileSorting::ByName,
};
match self.browser.tab() {
FileExplorerTab::Local => self.local_mut().sort_by(sorting),
FileExplorerTab::Remote => self.remote_mut().sort_by(sorting),
_ => panic!("Found result doesn't support SORTING"),
}
// Update status bar
self.refresh_status_bar();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
// -- error
(COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => {
self.umount_error();
None
}
// -- fatal
(COMPONENT_TEXT_FATAL, &MSG_KEY_ESC) | (COMPONENT_TEXT_FATAL, &MSG_KEY_ENTER) => {
self.exit_reason = Some(super::ExitReason::Disconnect);
None
}
// -- help
(COMPONENT_TEXT_HELP, &MSG_KEY_ESC) | (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) => {
self.umount_help();
None
}
// -- progress bar
(COMPONENT_PROGRESS_BAR, &MSG_KEY_CTRL_C) => {
// Set transfer aborted to True
self.transfer.aborted = true;
None
}
// -- fallback
(_, _) => None, // Nothing to do
},
}
}
/// ### update_local_filelist
///
/// Update local file list
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_EXPLORER_LOCAL) {
Some(props) => {
// Get width
let width: usize = self
.context
.as_ref()
.unwrap()
.store
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let hostname: String = match hostname::get() {
Ok(h) => {
let hostname: String = h.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.get(0).unwrap_or(&"localhost"))
}
Err(_) => String::from("localhost"),
};
let hostname: String = format!(
"{}:{} ",
hostname,
FileTransferActivity::elide_wrkdir_path(
self.local().wrkdir.as_path(),
hostname.as_str(),
width
)
.display()
);
let files: Vec<String> = self
.local()
.iter_files()
.map(|x: &FsEntry| self.local().fmt_file(x))
.collect();
// Update
let props = FileListPropsBuilder::from(props)
.with_files(Some(hostname), files)
.build();
// Update
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
}
None => None,
}
}
/// ### update_remote_filelist
///
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_EXPLORER_REMOTE) {
Some(props) => {
// Get width
let width: usize = self
.context
.as_ref()
.unwrap()
.store
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
let hostname: String = format!(
"{}:{} ",
params.address,
FileTransferActivity::elide_wrkdir_path(
self.remote().wrkdir.as_path(),
params.address.as_str(),
width
)
.display()
);
let files: Vec<String> = self
.remote()
.iter_files()
.map(|x: &FsEntry| self.remote().fmt_file(x))
.collect();
// Update
let props = FileListPropsBuilder::from(props)
.with_files(Some(hostname), files)
.build();
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
}
None => None,
}
}
/// ### update_logbox
///
/// Update log box
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_LOG_BOX) {
Some(props) => {
// Make log entries
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in self.log_records.iter().enumerate() {
// Add row if not first row
if idx > 0 {
table.add_row();
}
let fg = match record.level {
LogLevel::Error => Color::Red,
LogLevel::Warn => Color::Yellow,
LogLevel::Info => Color::Green,
};
table
.add_col(TextSpan::from(format!(
"{}",
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
)))
.add_col(TextSpan::from(" ["))
.add_col(
TextSpanBuilder::new(
format!(
"{:5}",
match record.level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
}
)
.as_str(),
)
.with_foreground(fg)
.build(),
)
.add_col(TextSpan::from("]: "))
.add_col(TextSpan::from(record.msg.as_ref()));
}
let table = table.build();
let props = LogboxPropsBuilder::from(props)
.with_log(Some(String::from("Log")), table)
.build();
self.view.update(super::COMPONENT_LOG_BOX, props)
}
None => None,
}
}
pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_PROGRESS_BAR) {
Some(props) => {
// Calculate ETA
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
let eta: String = match self.transfer.progress as u64 {
0 => String::from("--:--"), // NOTE: would divide by 0 :D
_ => {
let eta: u64 =
((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs;
format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2)
}
};
// Calculate bytes/s
let label = format!(
"{:.2}% - ETA {} ({}/s)",
self.transfer.progress,
eta,
ByteSize(self.transfer.bytes_per_second())
);
let props = ProgressBarPropsBuilder::from(props)
.with_texts(Some(text), label)
.with_progress(self.transfer.progress / 100.0)
.build();
self.view.update(COMPONENT_PROGRESS_BAR, props)
}
None => None,
}
}
/// ### finalize_find
///
/// Finalize find process
fn finalize_find(&mut self) {
// Set found to none
self.browser.del_found();
// Restore tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::FindLocal => FileExplorerTab::Local,
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
_ => FileExplorerTab::Local,
});
}
fn update_find_list(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_EXPLORER_FIND) {
None => None,
Some(props) => {
let title: String = props.texts.title.clone().unwrap_or_default();
// Prepare files
let files: Vec<String> = self
.found()
.unwrap()
.iter_files()
.map(|x: &FsEntry| self.found().unwrap().fmt_file(x))
.collect();
let props = FileListPropsBuilder::from(props)
.with_files(Some(title), files)
.build();
self.view.update(COMPONENT_EXPLORER_FIND, props)
}
}
}
/// ### elide_wrkdir_path
///
/// Elide working directory path if longer than width + host.len
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: usize) -> PathBuf {
let fmt_path: String = format!("{}", wrkdir.display());
// NOTE: +5 is const
match fmt_path.len() + host.len() + 5 > width {
false => PathBuf::from(wrkdir),
true => {
// Elide
let ancestors_len: usize = wrkdir.ancestors().count();
let mut ancestors = wrkdir.ancestors();
let mut elided_path: PathBuf = PathBuf::new();
// If ancestors_len's size is bigger than 2, push count - 2
if ancestors_len > 2 {
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
}
// If ancestors_len is bigger than 3, push '...' and parent too
if ancestors_len > 3 {
elided_path.push("...");
if let Some(parent) = wrkdir.ancestors().nth(1) {
elided_path.push(parent.file_name().unwrap());
}
}
// Push file_name
if let Some(name) = wrkdir.file_name() {
elided_path.push(name);
}
elided_path
}
}
}
}

File diff suppressed because it is too large Load Diff