mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
858 lines
34 KiB
Rust
858 lines
34 KiB
Rust
//! ## FileTransferActivity
|
|
//!
|
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
|
|
|
/*
|
|
*
|
|
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
|
*
|
|
* This file is part of "TermSCP"
|
|
*
|
|
* TermSCP is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* TermSCP is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
// Deps
|
|
extern crate bytesize;
|
|
extern crate content_inspector;
|
|
extern crate crossterm;
|
|
extern crate tempfile;
|
|
|
|
// Locals
|
|
use super::{FileTransferActivity, InputMode, LogLevel, PopupType};
|
|
use crate::fs::{FsEntry, FsFile};
|
|
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 tui::style::Color;
|
|
|
|
impl FileTransferActivity {
|
|
/// ### connect
|
|
///
|
|
/// Connect to remote
|
|
pub(super) fn connect(&mut self) {
|
|
// Connect to remote
|
|
match self.client.connect(
|
|
self.params.address.clone(),
|
|
self.params.port,
|
|
self.params.username.clone(),
|
|
self.params.password.clone(),
|
|
) {
|
|
Ok(welcome) => {
|
|
if let Some(banner) = welcome {
|
|
// Log welcome
|
|
self.log(
|
|
LogLevel::Info,
|
|
format!(
|
|
"Established connection with '{}': \"{}\"",
|
|
self.params.address, banner
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
// Set state to explorer
|
|
self.input_mode = InputMode::Explorer;
|
|
self.reload_remote_dir();
|
|
}
|
|
Err(err) => {
|
|
// Set popup fatal error
|
|
self.input_mode = InputMode::Popup(PopupType::Fatal(format!("{}", err)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ### disconnect
|
|
///
|
|
/// disconnect from remote
|
|
pub(super) fn disconnect(&mut self) {
|
|
// Show popup disconnecting
|
|
self.input_mode = InputMode::Popup(PopupType::Alert(
|
|
Color::Red,
|
|
String::from("Disconnecting from remote..."),
|
|
));
|
|
// Disconnect
|
|
let _ = self.client.disconnect();
|
|
// Quit
|
|
self.disconnected = true;
|
|
}
|
|
|
|
/// ### disconnect_and_quit
|
|
///
|
|
/// disconnect from remote and then quit
|
|
pub(super) fn disconnect_and_quit(&mut self) {
|
|
self.disconnect();
|
|
self.quit = true;
|
|
}
|
|
|
|
/// ### 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.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(),
|
|
};
|
|
self.input_mode = InputMode::Popup(PopupType::Wait(format!("Uploading \"{}\"", file_name)));
|
|
// Draw
|
|
self.draw();
|
|
// 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) => {
|
|
let _ = self.filetransfer_send_file(file, remote_path.as_path());
|
|
}
|
|
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()).as_ref(),
|
|
);
|
|
// Get files in dir
|
|
match self
|
|
.context
|
|
.as_ref()
|
|
.unwrap()
|
|
.local
|
|
.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, Reset input mode to explorer (if input mode is wait or progress)
|
|
if let InputMode::Popup(ptype) = &self.input_mode {
|
|
if matches!(ptype, PopupType::Wait(_) | PopupType::Progress(_)) {
|
|
self.input_mode = InputMode::Explorer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ### 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(),
|
|
};
|
|
self.input_mode =
|
|
InputMode::Popup(PopupType::Wait(format!("Downloading \"{}\"...", file_name)));
|
|
// Draw
|
|
self.draw();
|
|
// 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) {
|
|
self.log_and_alert(LogLevel::Error, 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
|
|
.context
|
|
.as_mut()
|
|
.unwrap()
|
|
.local
|
|
.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
|
|
.context
|
|
.as_ref()
|
|
.unwrap()
|
|
.local
|
|
.chmod(local_dir_path.as_path(), pex)
|
|
{
|
|
self.log(
|
|
LogLevel::Error,
|
|
format!(
|
|
"Could not apply file mode {:?} to \"{}\": {}",
|
|
pex,
|
|
local_dir_path.display(),
|
|
err
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
self.log(
|
|
LogLevel::Info,
|
|
format!("Created directory \"{}\"", local_dir_path.display()).as_ref(),
|
|
);
|
|
// 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
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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.input_mode = InputMode::Explorer;
|
|
}
|
|
}
|
|
|
|
/// ### filetransfer_send_file
|
|
///
|
|
/// Send local file and write it to remote path
|
|
fn filetransfer_send_file(&mut self, local: &FsFile, remote: &Path) -> Result<(), String> {
|
|
// Upload file
|
|
// Try to open local file
|
|
match self
|
|
.context
|
|
.as_ref()
|
|
.unwrap()
|
|
.local
|
|
.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(format!("Could not rewind local file: {}", err));
|
|
}
|
|
// Write remote file
|
|
let mut total_bytes_written: usize = 0;
|
|
// Set input state to popup progress
|
|
self.input_mode = InputMode::Popup(PopupType::Progress(format!(
|
|
"Uploading \"{}\"",
|
|
local.name
|
|
)));
|
|
// Reset transfer states
|
|
self.transfer.reset();
|
|
let mut last_progress_val: f64 = 0.0;
|
|
let mut last_input_event_fetch: Instant = Instant::now();
|
|
// 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) => {
|
|
return Err(format!(
|
|
"Could not write remote file: {}",
|
|
err
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => return Err(format!("Could not read local file: {}", 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.draw();
|
|
last_progress_val = self.transfer.progress;
|
|
}
|
|
}
|
|
// Finalize stream
|
|
if let Err(err) = self.client.on_sent(rhnd) {
|
|
self.log(
|
|
LogLevel::Warn,
|
|
format!("Could not finalize remote stream: \"{}\"", err).as_str(),
|
|
);
|
|
}
|
|
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()),
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
Err(err) => {
|
|
return Err(format!(
|
|
"Failed to upload file \"{}\": {}",
|
|
local.abs_path.display(),
|
|
err
|
|
))
|
|
}
|
|
},
|
|
Err(err) => {
|
|
return Err(format!(
|
|
"Failed to open file \"{}\": {}",
|
|
local.abs_path.display(),
|
|
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) -> Result<(), String> {
|
|
// Try to open local file
|
|
match self.context.as_ref().unwrap().local.open_file_write(local) {
|
|
Ok(mut local_file) => {
|
|
// Download file from remote
|
|
match self.client.recv_file(remote) {
|
|
Ok(mut rhnd) => {
|
|
// Set popup progress
|
|
self.input_mode = InputMode::Popup(PopupType::Progress(format!(
|
|
"Downloading \"{}\"...",
|
|
remote.name,
|
|
)));
|
|
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();
|
|
// 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) => {
|
|
return Err(format!(
|
|
"Could not write local file: {}",
|
|
err
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
return Err(format!("Could not read remote file: {}", 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.draw();
|
|
last_progress_val = self.transfer.progress;
|
|
}
|
|
}
|
|
// Finalize stream
|
|
if let Err(err) = self.client.on_recv(rhnd) {
|
|
self.log(
|
|
LogLevel::Warn,
|
|
format!("Could not finalize remote stream: \"{}\"", err).as_str(),
|
|
);
|
|
}
|
|
// 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.context.as_ref().unwrap().local.chmod(local, pex)
|
|
{
|
|
self.log(
|
|
LogLevel::Error,
|
|
format!(
|
|
"Could not apply file mode {:?} to \"{}\": {}",
|
|
pex,
|
|
local.display(),
|
|
err
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
// 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()),
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
Err(err) => {
|
|
return Err(format!(
|
|
"Failed to download file \"{}\": {}",
|
|
remote.abs_path.display(),
|
|
err
|
|
))
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
return Err(format!(
|
|
"Failed to open local file for write \"{}\": {}",
|
|
local.display(),
|
|
err
|
|
))
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// ### local_scan
|
|
///
|
|
/// Scan current local directory
|
|
pub(super) fn local_scan(&mut self, path: &Path) {
|
|
match self.context.as_ref().unwrap().local.scan_dir(path) {
|
|
Ok(files) => {
|
|
// Set files and sort (sorting is implicit)
|
|
self.local.set_files(files);
|
|
// Set index; keep if possible, otherwise set to last item
|
|
self.local.set_index(match self.local.get_current_file() {
|
|
Some(_) => self.local.get_index(),
|
|
None => match self.local.count() {
|
|
0 => 0,
|
|
_ => self.local.count() - 1,
|
|
},
|
|
});
|
|
}
|
|
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.set_files(files);
|
|
// Set index; keep if possible, otherwise set to last item
|
|
self.remote.set_index(match self.remote.get_current_file() {
|
|
Some(_) => self.remote.get_index(),
|
|
None => match self.remote.count() {
|
|
0 => 0,
|
|
_ => self.remote.count() - 1,
|
|
},
|
|
});
|
|
}
|
|
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.context.as_mut().unwrap().local.change_wrkdir(path) {
|
|
Ok(_) => {
|
|
self.log(
|
|
LogLevel::Info,
|
|
format!("Changed directory on local: {}", path.display()).as_str(),
|
|
);
|
|
// Reload files
|
|
self.local_scan(path);
|
|
// Reset index
|
|
self.local.set_index(0);
|
|
// Set wrkdir
|
|
self.local.wrkdir = PathBuf::from(path);
|
|
// Push prev_dir to stack
|
|
if push {
|
|
self.local.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()).as_str(),
|
|
);
|
|
// Update files
|
|
self.remote_scan(path);
|
|
// Reset index
|
|
self.remote.set_index(0);
|
|
// Set wrkdir
|
|
self.remote.wrkdir = PathBuf::from(path);
|
|
// Push prev_dir to stack
|
|
if push {
|
|
self.remote.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()
|
|
)
|
|
.as_str(),
|
|
),
|
|
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) {
|
|
return Err(err);
|
|
}
|
|
// Get current file modification time
|
|
let prev_mtime: SystemTime = match self.context.as_ref().unwrap().local.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.context.as_ref().unwrap().local.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()
|
|
)
|
|
.as_ref(),
|
|
);
|
|
// Get local fs entry
|
|
let tmpfile_entry: FsEntry =
|
|
match self.context.as_ref().unwrap().local.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())
|
|
{
|
|
return Err(err);
|
|
}
|
|
}
|
|
false => {
|
|
self.log(
|
|
LogLevel::Info,
|
|
format!("File \"{}\" hasn't changed", file.abs_path.display()).as_ref(),
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|