commit
6cd9657446
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -28,6 +28,11 @@ A clear and concise description of what you expected to happen.
|
|||
- Protocol used
|
||||
- Remote server version and name
|
||||
|
||||
## Log
|
||||
|
||||
Report the snippet of the log file containing the unexpected behaviour.
|
||||
If there is any information you consider to be confidential, shadow it.
|
||||
|
||||
## Additional information
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
|
|
@ -33,6 +33,9 @@ Released on FIXME: ??
|
|||
- Select a file with `<M>`, the file when selected will have a `*` prepended to its name
|
||||
- Select all files in the current directory with `<CTRL+A>`
|
||||
- Read more on manual: [Work on multiple files](docs/man.md#Work-on-multiple-files-)
|
||||
- **Logging**:
|
||||
- termscp now writes a log file, useful to debug and to contribute to fix issues.
|
||||
- Read more on [manual](docs/man.md)
|
||||
- **File transfer changes**
|
||||
- *SFTP*
|
||||
- Added **COPY** command to SFTP (Please note that Copy command is not supported by SFTP natively, so here it just uses the `cp` shell command as it does in SCP).
|
||||
|
@ -46,6 +49,7 @@ Released on FIXME: ??
|
|||
- when you change the protocol in the authentication form and the current port is standard (`< 1024`), the port will be automatically changed to default value for the selected protocol (e.g. current port: `123`, protocol is changes to `FTP`, port becomes `21`)
|
||||
- Bugfix:
|
||||
- Fixed wrong text wrap in log box
|
||||
- Fixed empty bookmark name causing termscp to crash
|
||||
- Fixed error message not being shown after an upload failure
|
||||
- Fixed default protocol not being loaded from config
|
||||
- [Issue 23](https://github.com/veeso/termscp/issues/23): Remove created file if transfer failed or was abrupted
|
||||
|
|
|
@ -61,6 +61,7 @@ Don't set other labels to your issue, not even priority.
|
|||
|
||||
When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think.
|
||||
Please always provide the environment you're working on and consider that we don't provide any support for older version of termscp, at least for those not classified as LTS (if we'll ever have them).
|
||||
If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file.
|
||||
Last but not least: the template I've written must be used. Full stop.
|
||||
|
||||
Maintainers will may add additional labels to your issue:
|
||||
|
@ -68,7 +69,7 @@ Maintainers will may add additional labels to your issue:
|
|||
- **duplicate**: the issue is duplicated; the reference to the related issue will be added to your description. Your issue will be closed.
|
||||
- **priority**: this must be fixed asap
|
||||
- **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments.
|
||||
- **wontfix**: your bug has a very high ratio between the probability to encounter it and the difficult to fix it, or it just isn't a bug, but a feature.
|
||||
- **wontfix**: your bug has a very high ratio between the difficulty to fix it and the probability to encounter it, or it just isn't a bug, but a feature.
|
||||
|
||||
### Feature requests
|
||||
|
||||
|
|
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -1230,6 +1230,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"log",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.6.1"
|
||||
|
@ -1291,6 +1302,15 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termscp"
|
||||
version = "0.5.0"
|
||||
|
@ -1307,6 +1327,7 @@ dependencies = [
|
|||
"hostname",
|
||||
"keyring",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"magic-crypt",
|
||||
"path-slash",
|
||||
"pretty_assertions",
|
||||
|
@ -1314,6 +1335,7 @@ dependencies = [
|
|||
"regex",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"simplelog",
|
||||
"ssh2",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
|
@ -1664,6 +1686,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -37,10 +37,12 @@ edit = "0.1.3"
|
|||
getopts = "0.2.21"
|
||||
hostname = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.14"
|
||||
magic-crypt = "3.1.7"
|
||||
rand = "0.8.3"
|
||||
regex = "1.5.4"
|
||||
rpassword = "5.0.1"
|
||||
simplelog = "0.10.0"
|
||||
ssh2 = "0.9.0"
|
||||
tempfile = "3.1.0"
|
||||
textwrap = "0.13.4"
|
||||
|
|
32
docs/man.md
32
docs/man.md
|
@ -15,6 +15,7 @@
|
|||
- [File Explorer Format](#file-explorer-format)
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
||||
- [Logging 🩺](#logging-)
|
||||
|
||||
---
|
||||
|
||||
|
@ -25,6 +26,7 @@ termscp can be started with the following options:
|
|||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument
|
||||
- `-q, --quiet` Disable logging
|
||||
- `-v, --version` Print version info
|
||||
- `-h, --help` Print help page
|
||||
|
||||
|
@ -266,3 +268,33 @@ Just a reminder: **you can edit only textual file**; binary files are not suppor
|
|||
### How do I configure the text editor 🦥
|
||||
|
||||
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor to use, change it in termscp configuration. [Read more](#configuration-️)
|
||||
|
||||
---
|
||||
|
||||
## Logging 🩺
|
||||
|
||||
termscp writes a log file for each session, which is written at
|
||||
|
||||
- `$HOME/.config/termscp/termscp.log` on Linux/BSD
|
||||
- `$HOME/Library/Application Support/termscp/termscp.log` on MacOs
|
||||
- `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows
|
||||
|
||||
the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again.
|
||||
The log file always reports in *trace* level, so it is kinda verbose.
|
||||
I know you might have some questions regarding log files, so I made a kind of a Q/A:
|
||||
|
||||
> Is it possible to reduce verbosity?
|
||||
|
||||
No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set.
|
||||
|
||||
> If trace level is set for logging, is the file going to reach a huge size?
|
||||
|
||||
Probably not, unless you never quit termscp, but I think that's likely to happne. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
|
||||
|
||||
> I don't want logging, can I turn it off?
|
||||
|
||||
Yes, you can. Just start termscp with `-q or --quiet` option. You can alias termscp to make it persistent. Remember that logging is used to diagnose issues, so since behind every open source project, there should always be this kind of mutual help, keeping log files might be your way to support the project 😉. I don't want you to feel guilty, but just to say.
|
||||
|
||||
> Is logging safe?
|
||||
|
||||
If you're concerned about security, the log file doesn't contain any plain password, so don't worry and exposes the same information the sibling file `bookmarks` reports.
|
||||
|
|
|
@ -69,7 +69,10 @@ impl ActivityManager {
|
|||
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
||||
match Self::init_config_client() {
|
||||
Ok(cli) => (Some(cli), None),
|
||||
Err(err) => (None, Some(err)),
|
||||
Err(err) => {
|
||||
error!("Failed to initialize config client: {}", err);
|
||||
(None, Some(err))
|
||||
}
|
||||
};
|
||||
let ctx: Context = Context::new(config_client, error);
|
||||
Ok(ActivityManager {
|
||||
|
@ -131,6 +134,7 @@ impl ActivityManager {
|
|||
/// Returns when activity terminates.
|
||||
/// Returns the next activity to run
|
||||
fn run_authentication(&mut self) -> Option<NextActivity> {
|
||||
info!("Starting AuthActivity...");
|
||||
// Prepare activity
|
||||
let mut activity: AuthActivity = AuthActivity::default();
|
||||
// Prepare result
|
||||
|
@ -138,7 +142,10 @@ impl ActivityManager {
|
|||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None,
|
||||
None => {
|
||||
error!("Failed to start AuthActivity: context is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
|
@ -149,16 +156,19 @@ impl ActivityManager {
|
|||
if let Some(exit_reason) = activity.will_umount() {
|
||||
match exit_reason {
|
||||
ExitReason::Quit => {
|
||||
info!("AuthActivity terminated due to 'Quit'");
|
||||
result = None;
|
||||
break;
|
||||
}
|
||||
ExitReason::EnterSetup => {
|
||||
// User requested activity
|
||||
info!("AuthActivity terminated due to 'EnterSetup'");
|
||||
result = Some(NextActivity::SetupActivity);
|
||||
break;
|
||||
}
|
||||
ExitReason::Connect => {
|
||||
// User submitted, set next activity
|
||||
info!("AuthActivity terminated due to 'Connect'");
|
||||
result = Some(NextActivity::FileTransfer);
|
||||
break;
|
||||
}
|
||||
|
@ -170,6 +180,7 @@ impl ActivityManager {
|
|||
}
|
||||
// Destroy activity
|
||||
self.context = activity.on_destroy();
|
||||
info!("AuthActivity destroyed");
|
||||
result
|
||||
}
|
||||
|
||||
|
@ -179,15 +190,22 @@ impl ActivityManager {
|
|||
/// Returns when activity terminates.
|
||||
/// Returns the next activity to run
|
||||
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
||||
info!("Starting FileTransferActivity");
|
||||
// Get context
|
||||
let mut ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: context is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// If ft params is None, return None
|
||||
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
|
||||
Some(ft_params) => &ft_params,
|
||||
None => return None,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Prepare activity
|
||||
let protocol: FileTransferProtocol = ft_params.protocol;
|
||||
|
@ -195,6 +213,7 @@ impl ActivityManager {
|
|||
Ok(host) => host,
|
||||
Err(err) => {
|
||||
// Set error in context
|
||||
error!("Failed to initialize localhost: {}", err);
|
||||
ctx.set_error(format!("Could not initialize localhost: {}", err));
|
||||
return None;
|
||||
}
|
||||
|
@ -211,11 +230,13 @@ impl ActivityManager {
|
|||
if let Some(exit_reason) = activity.will_umount() {
|
||||
match exit_reason {
|
||||
ExitReason::Quit => {
|
||||
info!("FileTransferActivity terminated due to 'Quit'");
|
||||
result = None;
|
||||
break;
|
||||
}
|
||||
ExitReason::Disconnect => {
|
||||
// User disconnected, set next activity to authentication
|
||||
info!("FileTransferActivity terminated due to 'Authentication'");
|
||||
result = Some(NextActivity::Authentication);
|
||||
break;
|
||||
}
|
||||
|
@ -241,7 +262,10 @@ impl ActivityManager {
|
|||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None,
|
||||
None => {
|
||||
error!("Failed to start SetupActivity: context is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
|
@ -250,6 +274,7 @@ impl ActivityManager {
|
|||
activity.on_draw();
|
||||
// Check if activity has terminated
|
||||
if let Some(ExitReason::Quit) = activity.will_umount() {
|
||||
info!("SetupActivity terminated due to 'Quit'");
|
||||
break;
|
||||
}
|
||||
// Sleep for ticks
|
||||
|
|
|
@ -50,6 +50,7 @@ impl BookmarkSerializer {
|
|||
))
|
||||
}
|
||||
};
|
||||
trace!("Serialized new bookmarks data: {}", data);
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
|
@ -72,9 +73,13 @@ impl BookmarkSerializer {
|
|||
err.to_string(),
|
||||
));
|
||||
}
|
||||
trace!("Read bookmarks from file: {}", data);
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(hosts) => Ok(hosts),
|
||||
Ok(bookmarks) => {
|
||||
debug!("Read bookmarks from file {:?}", bookmarks);
|
||||
Ok(bookmarks)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
|
|
|
@ -50,6 +50,7 @@ impl ConfigSerializer {
|
|||
))
|
||||
}
|
||||
};
|
||||
trace!("Serialized new configuration data: {}", data);
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
|
@ -72,9 +73,13 @@ impl ConfigSerializer {
|
|||
err.to_string(),
|
||||
));
|
||||
}
|
||||
trace!("Read configuration from file: {}", data);
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(hosts) => Ok(hosts),
|
||||
Ok(config) => {
|
||||
debug!("Read config from file {:?}", config);
|
||||
Ok(config)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
|
|
|
@ -34,6 +34,7 @@ extern crate regex;
|
|||
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||
use crate::utils::parser::{parse_datetime, parse_lstime};
|
||||
|
||||
// Includes
|
||||
|
@ -105,6 +106,7 @@ impl FtpFileTransfer {
|
|||
lazy_static! {
|
||||
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
|
||||
}
|
||||
debug!("Parsing LIST (UNIX) line: '{}'", line);
|
||||
// Apply regex to result
|
||||
match LS_RE.captures(line) {
|
||||
// String matches regex
|
||||
|
@ -182,6 +184,7 @@ impl FtpFileTransfer {
|
|||
};
|
||||
// Check if file_name is '.' or '..'
|
||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||
debug!("File name is {}; ignoring entry", file_name);
|
||||
return Err(());
|
||||
}
|
||||
// Get symlink
|
||||
|
@ -236,6 +239,19 @@ impl FtpFileTransfer {
|
|||
.extension()
|
||||
.map(|s| String::from(s.to_string_lossy()));
|
||||
// Return
|
||||
debug!("Follows LIST line '{}' attributes", line);
|
||||
debug!("Is directory? {}", is_dir);
|
||||
debug!("Is symlink? {}", is_symlink);
|
||||
debug!("name: {}", file_name);
|
||||
debug!("abs_path: {}", abs_path.display());
|
||||
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("symlink: {:?}", symlink);
|
||||
debug!("user: {:?}", uid);
|
||||
debug!("group: {:?}", gid);
|
||||
debug!("unix_pex: {:?}", unix_pex);
|
||||
debug!("---------------------------------------");
|
||||
// Push to entries
|
||||
Ok(match is_dir {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
|
@ -287,6 +303,7 @@ impl FtpFileTransfer {
|
|||
)
|
||||
.unwrap();
|
||||
}
|
||||
debug!("Parsing LIST (DOS) line: '{}'", line);
|
||||
// Apply regex to result
|
||||
match DOS_RE.captures(line) {
|
||||
// String matches regex
|
||||
|
@ -324,6 +341,14 @@ impl FtpFileTransfer {
|
|||
.as_path()
|
||||
.extension()
|
||||
.map(|s| String::from(s.to_string_lossy()));
|
||||
debug!("Follows LIST line '{}' attributes", line);
|
||||
debug!("Is directory? {}", is_dir);
|
||||
debug!("name: {}", file_name);
|
||||
debug!("abs_path: {}", abs_path.display());
|
||||
debug!("last_change_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("last_access_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("creation_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("---------------------------------------");
|
||||
// Return entry
|
||||
Ok(match is_dir {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
|
@ -382,17 +407,20 @@ impl FileTransfer for FtpFileTransfer {
|
|||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Get stream
|
||||
info!("Connecting to {}:{}", address, port);
|
||||
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to connect: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// If SSL, open secure session
|
||||
if self.ftps {
|
||||
info!("Setting up TLS stream...");
|
||||
let ctx = match TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
|
@ -400,19 +428,21 @@ impl FileTransfer for FtpFileTransfer {
|
|||
{
|
||||
Ok(tls) => tls,
|
||||
Err(err) => {
|
||||
error!("Failed to setup TLS stream: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::SslError,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
stream = match stream.into_secure(ctx, address.as_str()) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Failed to setup TLS stream: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::SslError,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -425,14 +455,22 @@ impl FileTransfer for FtpFileTransfer {
|
|||
Some(pwd) => pwd,
|
||||
None => String::new(),
|
||||
};
|
||||
info!(
|
||||
"Signin in with username: {}, password: {}",
|
||||
username,
|
||||
shadow_password(password.as_str())
|
||||
);
|
||||
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
|
||||
error!("Login failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
debug!("Setting transfer type to Binary");
|
||||
// Initialize file type
|
||||
if let Err(err) = stream.transfer_type(FileType::Binary) {
|
||||
error!("Failed to set transfer type to binary: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
err.to_string(),
|
||||
|
@ -440,6 +478,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
}
|
||||
// Set stream
|
||||
self.stream = Some(stream);
|
||||
info!("Connection successfully established");
|
||||
// Return OK
|
||||
Ok(self.stream.as_ref().unwrap().get_welcome_msg())
|
||||
}
|
||||
|
@ -449,6 +488,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// Disconnect from the remote server
|
||||
|
||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||
info!("Disconnecting from FTP server...");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.quit() {
|
||||
Ok(_) => Ok(()),
|
||||
|
@ -475,6 +515,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// Print working directory
|
||||
|
||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||
info!("PWD");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.pwd() {
|
||||
Ok(path) => Ok(PathBuf::from(path.as_str())),
|
||||
|
@ -495,6 +536,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
|
||||
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
|
||||
let dir: PathBuf = Self::resolve(dir);
|
||||
info!("Changing directory to {}", dir.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
|
||||
Ok(_) => Ok(dir),
|
||||
|
@ -514,6 +556,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// Copy file to destination
|
||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
||||
// FTP doesn't support file copy
|
||||
debug!("COPY issues (will fail, since unsupported)");
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
|
@ -525,9 +568,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||
|
||||
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
|
||||
let dir: PathBuf = Self::resolve(path);
|
||||
info!("LIST dir {}", dir.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
|
||||
Ok(entries) => {
|
||||
debug!("Got {} lines in LIST result", entries.len());
|
||||
// Prepare result
|
||||
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
|
||||
// Iterate over entries
|
||||
|
@ -536,6 +581,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||
result.push(file);
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"{} out of {} were valid entries",
|
||||
result.len(),
|
||||
entries.len()
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -554,6 +604,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// Make directory
|
||||
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
|
||||
let dir: PathBuf = Self::resolve(dir);
|
||||
info!("MKDIR {}", dir.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
|
||||
Ok(_) => Ok(()),
|
||||
|
@ -577,9 +628,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||
FileTransferErrorType::UninitializedSession,
|
||||
));
|
||||
}
|
||||
info!("Removing entry {}", fsentry.get_abs_path().display());
|
||||
match fsentry {
|
||||
// Match fs entry...
|
||||
FsEntry::File(file) => {
|
||||
debug!("entry is a file; removing file");
|
||||
// Remove file directly
|
||||
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
|
||||
Ok(_) => Ok(()),
|
||||
|
@ -591,9 +644,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
// Get directory files
|
||||
debug!("Entry is a directory; iterating directory entries");
|
||||
match self.list_dir(dir.abs_path.as_path()) {
|
||||
Ok(files) => {
|
||||
// Remove recursively files
|
||||
debug!("Removing {} entries from directory...", files.len());
|
||||
for file in files.iter() {
|
||||
if let Err(err) = self.remove(&file) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -603,6 +658,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
}
|
||||
}
|
||||
// Once all files in directory have been deleted, remove directory
|
||||
debug!("Finally removing directory {}", dir.name);
|
||||
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -625,6 +681,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// Rename file or a directory
|
||||
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
||||
let dst: PathBuf = Self::resolve(dst);
|
||||
info!(
|
||||
"Renaming {} to {}",
|
||||
file.get_abs_path().display(),
|
||||
dst.display()
|
||||
);
|
||||
match &mut self.stream {
|
||||
Some(stream) => {
|
||||
// Get name
|
||||
|
@ -691,6 +752,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
file_name: &Path,
|
||||
) -> Result<Box<dyn Write>, FileTransferError> {
|
||||
let file_name: PathBuf = Self::resolve(file_name);
|
||||
info!("Sending file {}", file_name.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
|
||||
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
|
||||
|
@ -710,6 +772,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// Receive file from remote with provided name
|
||||
/// Returns file and its size
|
||||
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||
info!("Receiving file {}", file.abs_path.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) {
|
||||
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
|
||||
|
@ -732,6 +795,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// This is necessary for some protocols such as FTP.
|
||||
/// You must call this method each time you want to finalize the write of the remote file.
|
||||
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
||||
info!("Finalizing put stream");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.finalize_put_stream(writable) {
|
||||
Ok(_) => Ok(()),
|
||||
|
@ -754,6 +818,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||
info!("Finalizing get");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.finalize_get(readable) {
|
||||
Ok(_) => Ok(()),
|
||||
|
|
|
@ -319,14 +319,14 @@ impl std::string::ToString for FileTransferProtocol {
|
|||
}
|
||||
|
||||
impl std::str::FromStr for FileTransferProtocol {
|
||||
type Err = ();
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||
_ => Err(()),
|
||||
_ => Err(s.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ extern crate ssh2;
|
|||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||
use crate::utils::parser::parse_lstime;
|
||||
|
||||
// Includes
|
||||
|
@ -90,6 +91,7 @@ impl ScpFileTransfer {
|
|||
lazy_static! {
|
||||
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
|
||||
}
|
||||
debug!("Parsing LS line: '{}'", line);
|
||||
// Apply regex to result
|
||||
match LS_RE.captures(line) {
|
||||
// String matches regex
|
||||
|
@ -167,6 +169,7 @@ impl ScpFileTransfer {
|
|||
};
|
||||
// Check if file_name is '.' or '..'
|
||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||
debug!("File name is {}; ignoring entry", file_name);
|
||||
return Err(());
|
||||
}
|
||||
// Get symlink; PATH mustn't be equal to filename
|
||||
|
@ -200,6 +203,19 @@ impl ScpFileTransfer {
|
|||
.extension()
|
||||
.map(|s| String::from(s.to_string_lossy()));
|
||||
// Return
|
||||
debug!("Follows LS line '{}' attributes", line);
|
||||
debug!("Is directory? {}", is_dir);
|
||||
debug!("Is symlink? {}", is_symlink);
|
||||
debug!("name: {}", file_name);
|
||||
debug!("abs_path: {}", abs_path.display());
|
||||
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("symlink: {:?}", symlink);
|
||||
debug!("user: {:?}", uid);
|
||||
debug!("group: {:?}", gid);
|
||||
debug!("unix_pex: {:?}", unix_pex);
|
||||
debug!("---------------------------------------");
|
||||
// Push to entries
|
||||
Ok(match is_dir {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
|
@ -262,6 +278,7 @@ impl ScpFileTransfer {
|
|||
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
||||
match self.session.as_mut() {
|
||||
Some(session) => {
|
||||
debug!("Running command: {}", cmd);
|
||||
// Create channel
|
||||
let mut channel: Channel = match session.channel_session() {
|
||||
Ok(ch) => ch,
|
||||
|
@ -285,6 +302,7 @@ impl ScpFileTransfer {
|
|||
Ok(_) => {
|
||||
// Wait close
|
||||
let _ = channel.wait_close();
|
||||
debug!("Command output: {}", output);
|
||||
Ok(output)
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -312,6 +330,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Setup tcp stream
|
||||
info!("Connecting to {}:{}", address, port);
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
|
@ -325,8 +344,10 @@ impl FileTransfer for ScpFileTransfer {
|
|||
let mut tcp: Option<TcpStream> = None;
|
||||
// Try addresses
|
||||
for socket_addr in socket_addresses.iter() {
|
||||
debug!("Trying socket address {}", socket_addr);
|
||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||
Ok(stream) => {
|
||||
debug!("{} succeded", socket_addr);
|
||||
tcp = Some(stream);
|
||||
break;
|
||||
}
|
||||
|
@ -337,26 +358,30 @@ impl FileTransfer for ScpFileTransfer {
|
|||
let tcp: TcpStream = match tcp {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
error!("No suitable socket address found; connection timeout");
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
String::from("Connection timeout"),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Create session
|
||||
let mut session: Session = match Session::new() {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Could not create session: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Set TCP stream
|
||||
session.set_tcp_stream(tcp);
|
||||
// Open connection
|
||||
debug!("Initializing handshake");
|
||||
if let Err(err) = session.handshake() {
|
||||
error!("Handshake failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
|
@ -372,6 +397,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||
.resolve(address.as_str(), username.as_str())
|
||||
{
|
||||
Some(rsa_key) => {
|
||||
debug!(
|
||||
"Authenticating with user {} and RSA key {}",
|
||||
username,
|
||||
rsa_key.display()
|
||||
);
|
||||
// Authenticate with RSA key
|
||||
if let Err(err) = session.userauth_pubkey_file(
|
||||
username.as_str(),
|
||||
|
@ -379,6 +409,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
rsa_key.as_path(),
|
||||
password.as_deref(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
err.to_string(),
|
||||
|
@ -387,10 +418,16 @@ impl FileTransfer for ScpFileTransfer {
|
|||
}
|
||||
None => {
|
||||
// Proceeed with username/password authentication
|
||||
debug!(
|
||||
"Authenticating with username {} and password {}",
|
||||
username,
|
||||
shadow_password(password.as_deref().unwrap_or(""))
|
||||
);
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
err.to_string(),
|
||||
|
@ -400,13 +437,22 @@ impl FileTransfer for ScpFileTransfer {
|
|||
}
|
||||
// Get banner
|
||||
let banner: Option<String> = session.banner().map(String::from);
|
||||
debug!(
|
||||
"Connection established: {}",
|
||||
banner.as_deref().unwrap_or("")
|
||||
);
|
||||
// Set session
|
||||
self.session = Some(session);
|
||||
// Get working directory
|
||||
debug!("Getting working directory...");
|
||||
match self.perform_shell_cmd("pwd") {
|
||||
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
info!(
|
||||
"Connection established; working directory: {}",
|
||||
self.wrkdir.display()
|
||||
);
|
||||
Ok(banner)
|
||||
}
|
||||
|
||||
|
@ -414,6 +460,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
///
|
||||
/// Disconnect from the remote server
|
||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||
info!("Disconnecting from remote...");
|
||||
match self.session.as_ref() {
|
||||
Some(session) => {
|
||||
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
||||
|
@ -447,6 +494,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
/// Print working directory
|
||||
|
||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||
info!("PWD: {}", self.wrkdir.display());
|
||||
match self.is_connected() {
|
||||
true => Ok(self.wrkdir.clone()),
|
||||
false => Err(FileTransferError::new(
|
||||
|
@ -471,6 +519,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
Self::resolve(p.as_path())
|
||||
}
|
||||
};
|
||||
info!("Changing working directory to {}", remote_path.display());
|
||||
// Change directory
|
||||
match self.perform_shell_cmd_with_path(
|
||||
p.as_path(),
|
||||
|
@ -484,6 +533,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
true => {
|
||||
// Set working directory
|
||||
self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
|
||||
info!("Changed working directory to {}", self.wrkdir.display());
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
false => Err(FileTransferError::new_ex(
|
||||
|
@ -512,6 +562,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||
match self.is_connected() {
|
||||
true => {
|
||||
let dst: PathBuf = Self::resolve(dst);
|
||||
info!(
|
||||
"Copying {} to {}",
|
||||
src.get_abs_path().display(),
|
||||
dst.display()
|
||||
);
|
||||
// Run `cp -rf`
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
match self.perform_shell_cmd_with_path(
|
||||
|
@ -555,6 +610,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
match self.is_connected() {
|
||||
true => {
|
||||
// Send ls -l to path
|
||||
info!("Getting file entries in {}", path.display());
|
||||
let path: PathBuf = Self::resolve(path);
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
match self.perform_shell_cmd_with_path(
|
||||
|
@ -572,6 +628,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"Found {} out of {} valid file entries",
|
||||
entries.len(),
|
||||
lines.len()
|
||||
);
|
||||
Ok(entries)
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -594,6 +655,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
match self.is_connected() {
|
||||
true => {
|
||||
let dir: PathBuf = Self::resolve(dir);
|
||||
info!("Making directory {}", dir.display());
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
// Mkdir dir && echo 0
|
||||
match self.perform_shell_cmd_with_path(
|
||||
|
@ -632,6 +694,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
true => {
|
||||
// Get path
|
||||
let path: PathBuf = file.get_abs_path();
|
||||
info!("Removing file {}", path.display());
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
match self.perform_shell_cmd_with_path(
|
||||
p.as_path(),
|
||||
|
@ -669,6 +732,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
// Get path
|
||||
let dst: PathBuf = Self::resolve(dst);
|
||||
let path: PathBuf = file.get_abs_path();
|
||||
info!("Renaming {} to {}", path.display(), dst.display());
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
match self.perform_shell_cmd_with_path(
|
||||
p.as_path(),
|
||||
|
@ -717,6 +781,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
match self.is_connected() {
|
||||
true => {
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
info!("Stat {}", path.display());
|
||||
// make command; Directories require `-d` option
|
||||
let cmd: String = match path.to_string_lossy().ends_with('/') {
|
||||
true => format!("ls -ld \"{}\"", path.display()),
|
||||
|
@ -760,6 +825,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
match self.is_connected() {
|
||||
true => {
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
info!("Executing command {}", cmd);
|
||||
match self.perform_shell_cmd_with_path(p.as_path(), cmd) {
|
||||
Ok(output) => Ok(output),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -788,7 +854,13 @@ impl FileTransfer for ScpFileTransfer {
|
|||
match self.session.as_ref() {
|
||||
Some(session) => {
|
||||
let file_name: PathBuf = Self::resolve(file_name);
|
||||
info!(
|
||||
"Sending file {} to {}",
|
||||
local.abs_path.display(),
|
||||
file_name.display()
|
||||
);
|
||||
// Set blocking to true
|
||||
debug!("blocking channel...");
|
||||
session.set_blocking(true);
|
||||
// Calculate file mode
|
||||
let mode: i32 = match local.unix_pex {
|
||||
|
@ -818,6 +890,10 @@ impl FileTransfer for ScpFileTransfer {
|
|||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
|
||||
};
|
||||
debug!(
|
||||
"File mode {:?}; mtime: {}, atime: {}; file size: {}",
|
||||
mode, times.0, times.1, file_size
|
||||
);
|
||||
// Send file
|
||||
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
|
||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
||||
|
@ -840,7 +916,9 @@ impl FileTransfer for ScpFileTransfer {
|
|||
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||
match self.session.as_ref() {
|
||||
Some(session) => {
|
||||
info!("Receiving file {}", file.abs_path.display());
|
||||
// Set blocking to true
|
||||
debug!("Set blocking...");
|
||||
session.set_blocking(true);
|
||||
match session.scp_recv(file.abs_path.as_path()) {
|
||||
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
|
||||
|
|
|
@ -32,6 +32,7 @@ extern crate ssh2;
|
|||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||
|
||||
// Includes
|
||||
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp};
|
||||
|
@ -159,6 +160,19 @@ impl SftpFileTransfer {
|
|||
}
|
||||
false => None,
|
||||
};
|
||||
debug!("Follows {} attributes", path.display());
|
||||
debug!("Is directory? {}", metadata.is_dir());
|
||||
debug!("Is symlink? {}", is_symlink);
|
||||
debug!("name: {}", file_name);
|
||||
debug!("abs_path: {}", path.display());
|
||||
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
|
||||
debug!("symlink: {:?}", symlink);
|
||||
debug!("user: {:?}", uid);
|
||||
debug!("group: {:?}", gid);
|
||||
debug!("unix_pex: {:?}", pex);
|
||||
debug!("---------------------------------------");
|
||||
// Is a directory?
|
||||
match metadata.is_dir() {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
|
@ -205,6 +219,7 @@ impl SftpFileTransfer {
|
|||
match self.session.as_mut() {
|
||||
Some(session) => {
|
||||
// Create channel
|
||||
debug!("Running command: {}", cmd);
|
||||
let mut channel: Channel = match session.channel_session() {
|
||||
Ok(ch) => ch,
|
||||
Err(err) => {
|
||||
|
@ -227,6 +242,7 @@ impl SftpFileTransfer {
|
|||
Ok(_) => {
|
||||
// Wait close
|
||||
let _ = channel.wait_close();
|
||||
debug!("Command output: {}", output);
|
||||
Ok(output)
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -254,6 +270,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Setup tcp stream
|
||||
info!("Connecting to {}:{}", address, port);
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
|
@ -267,6 +284,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
let mut tcp: Option<TcpStream> = None;
|
||||
// Try addresses
|
||||
for socket_addr in socket_addresses.iter() {
|
||||
debug!("Trying socket address {}", socket_addr);
|
||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||
Ok(stream) => {
|
||||
tcp = Some(stream);
|
||||
|
@ -279,26 +297,30 @@ impl FileTransfer for SftpFileTransfer {
|
|||
let tcp: TcpStream = match tcp {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
error!("No suitable socket address found; connection timeout");
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
String::from("Connection timeout"),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Create session
|
||||
let mut session: Session = match Session::new() {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Could not create session: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Set TCP stream
|
||||
session.set_tcp_stream(tcp);
|
||||
// Open connection
|
||||
debug!("Initializing handshake");
|
||||
if let Err(err) = session.handshake() {
|
||||
error!("Handshake failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
|
@ -314,6 +336,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||
.resolve(address.as_str(), username.as_str())
|
||||
{
|
||||
Some(rsa_key) => {
|
||||
debug!(
|
||||
"Authenticating with user {} and RSA key {}",
|
||||
username,
|
||||
rsa_key.display()
|
||||
);
|
||||
// Authenticate with RSA key
|
||||
if let Err(err) = session.userauth_pubkey_file(
|
||||
username.as_str(),
|
||||
|
@ -321,6 +348,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
rsa_key.as_path(),
|
||||
password.as_deref(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
err.to_string(),
|
||||
|
@ -329,10 +357,16 @@ impl FileTransfer for SftpFileTransfer {
|
|||
}
|
||||
None => {
|
||||
// Proceeed with username/password authentication
|
||||
debug!(
|
||||
"Authenticating with username {} and password {}",
|
||||
username,
|
||||
shadow_password(password.as_deref().unwrap_or(""))
|
||||
);
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
err.to_string(),
|
||||
|
@ -343,16 +377,19 @@ impl FileTransfer for SftpFileTransfer {
|
|||
// Set blocking to true
|
||||
session.set_blocking(true);
|
||||
// Get Sftp client
|
||||
debug!("Getting SFTP client...");
|
||||
let sftp: Sftp = match session.sftp() {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Could not get sftp client: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
// Get working directory
|
||||
debug!("Getting working directory...");
|
||||
self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
|
@ -367,6 +404,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||
self.session = Some(session);
|
||||
// Set sftp
|
||||
self.sftp = Some(sftp);
|
||||
info!(
|
||||
"Connection established: {}; working directory {}",
|
||||
banner.as_deref().unwrap_or(""),
|
||||
self.wrkdir.display()
|
||||
);
|
||||
Ok(banner)
|
||||
}
|
||||
|
||||
|
@ -374,6 +416,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
///
|
||||
/// Disconnect from the remote server
|
||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||
info!("Disconnecting from remote...");
|
||||
match self.session.as_ref() {
|
||||
Some(session) => {
|
||||
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
||||
|
@ -407,6 +450,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
///
|
||||
/// Print working directory
|
||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||
info!("PWD: {}", self.wrkdir.display());
|
||||
match self.sftp {
|
||||
Some(_) => Ok(self.wrkdir.clone()),
|
||||
None => Err(FileTransferError::new(
|
||||
|
@ -426,6 +470,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
info!("Changed working directory to {}", self.wrkdir.display());
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
None => Err(FileTransferError::new(
|
||||
|
@ -442,6 +487,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||
match self.is_connected() {
|
||||
true => {
|
||||
let dst: PathBuf = self.get_abs_path(dst);
|
||||
info!(
|
||||
"Copying {} to {}",
|
||||
src.get_abs_path().display(),
|
||||
dst.display()
|
||||
);
|
||||
// Run `cp -rf`
|
||||
match self.perform_shell_cmd_with_path(
|
||||
format!(
|
||||
|
@ -486,6 +536,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
info!("Getting file entries in {}", path.display());
|
||||
// Get files
|
||||
match sftp.readdir(dir.as_path()) {
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -517,6 +568,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
Some(sftp) => {
|
||||
// Make directory
|
||||
let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path());
|
||||
info!("Making directory {}", path.display());
|
||||
match sftp.mkdir(path.as_path(), 0o775) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
|
@ -541,6 +593,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
));
|
||||
}
|
||||
// Match if file is a file or a directory
|
||||
info!("Removing file {}", file.get_abs_path().display());
|
||||
match file {
|
||||
FsEntry::File(f) => {
|
||||
// Remove file
|
||||
|
@ -554,6 +607,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
}
|
||||
FsEntry::Directory(d) => {
|
||||
// Remove recursively
|
||||
debug!("{} is a directory; removing all directory entries", d.name);
|
||||
// Get directory files
|
||||
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
|
||||
Ok(entries) => entries,
|
||||
|
@ -585,6 +639,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
Some(sftp) => {
|
||||
info!(
|
||||
"Moving {} to {}",
|
||||
file.get_abs_path().display(),
|
||||
dst.display()
|
||||
);
|
||||
// Resolve destination path
|
||||
let abs_dst: PathBuf = self.get_abs_path(dst);
|
||||
// Get abs path of entry
|
||||
|
@ -611,6 +670,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
info!("Stat file {}", dir.display());
|
||||
// Get file
|
||||
match sftp.stat(dir.as_path()) {
|
||||
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
|
||||
|
@ -630,6 +690,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
///
|
||||
/// Execute a command on remote host
|
||||
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
||||
info!("Executing command {}", cmd);
|
||||
match self.is_connected() {
|
||||
true => match self.perform_shell_cmd_with_path(cmd) {
|
||||
Ok(output) => Ok(output),
|
||||
|
@ -660,14 +721,20 @@ impl FileTransfer for SftpFileTransfer {
|
|||
)),
|
||||
Some(sftp) => {
|
||||
let remote_path: PathBuf = self.get_abs_path(file_name);
|
||||
info!(
|
||||
"Sending file {} to {}",
|
||||
local.abs_path.display(),
|
||||
remote_path.display()
|
||||
);
|
||||
// Calculate file mode
|
||||
let mode: i32 = match local.unix_pex {
|
||||
None => 0o644,
|
||||
Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32),
|
||||
};
|
||||
debug!("File mode {:?}", mode);
|
||||
match sftp.open_mode(
|
||||
remote_path.as_path(),
|
||||
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::APPEND | OpenFlags::TRUNCATE,
|
||||
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
|
||||
mode,
|
||||
OpenType::File,
|
||||
) {
|
||||
|
@ -695,6 +762,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
Ok(p) => p,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
info!("Receiving file {}", remote_path.display());
|
||||
// Open remote file
|
||||
match sftp.open(remote_path.as_path()) {
|
||||
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
|
||||
|
|
212
src/host/mod.rs
212
src/host/mod.rs
|
@ -125,12 +125,17 @@ impl Localhost {
|
|||
///
|
||||
/// Instantiates a new Localhost struct
|
||||
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
|
||||
debug!("Initializing localhost at {}", wrkdir.display());
|
||||
let mut host: Localhost = Localhost {
|
||||
wrkdir,
|
||||
files: Vec::new(),
|
||||
};
|
||||
// Check if dir exists
|
||||
if !host.file_exists(host.wrkdir.as_path()) {
|
||||
error!(
|
||||
"Failed to initialize localhost: {} doesn't exist",
|
||||
host.wrkdir.display()
|
||||
);
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
None,
|
||||
|
@ -140,8 +145,15 @@ impl Localhost {
|
|||
// Retrieve files for provided path
|
||||
host.files = match host.scan_dir(host.wrkdir.as_path()) {
|
||||
Ok(files) => files,
|
||||
Err(err) => return Err(err),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to initialize localhost: could not scan wrkdir: {}",
|
||||
err
|
||||
);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
info!("Localhost initialized with success");
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
|
@ -165,8 +177,10 @@ impl Localhost {
|
|||
/// Change working directory with the new provided directory
|
||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
||||
info!("Changing localhost directory to {}...", new_dir.display());
|
||||
// Check whether directory exists
|
||||
if !self.file_exists(new_dir.as_path()) {
|
||||
error!("Could not change directory: No such file or directory");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
None,
|
||||
|
@ -174,10 +188,11 @@ impl Localhost {
|
|||
));
|
||||
}
|
||||
// Change directory
|
||||
if std::env::set_current_dir(new_dir.as_path()).is_err() {
|
||||
if let Err(err) = std::env::set_current_dir(new_dir.as_path()) {
|
||||
error!("Could not enter directory: {}", err);
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
None,
|
||||
Some(err),
|
||||
new_dir.as_path(),
|
||||
));
|
||||
}
|
||||
|
@ -189,11 +204,13 @@ impl Localhost {
|
|||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||
Ok(files) => files,
|
||||
Err(err) => {
|
||||
error!("Could not scan new directory: {}", err);
|
||||
// Restore directory
|
||||
self.wrkdir = prev_dir;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
debug!("Changed directory to {}", self.wrkdir.display());
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
|
||||
|
@ -210,6 +227,7 @@ impl Localhost {
|
|||
/// ignex: don't report error if directory already exists
|
||||
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
||||
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
||||
info!("Making directory {}", dir_path.display());
|
||||
// If dir already exists, return Error
|
||||
if dir_path.exists() {
|
||||
match ignex {
|
||||
|
@ -229,13 +247,17 @@ impl Localhost {
|
|||
if dir_name.is_relative() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
info!("Created directory {}", dir_path.display());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(err),
|
||||
dir_path.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Could not make directory: {}", err);
|
||||
Err(HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(err),
|
||||
dir_path.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,7 +268,9 @@ impl Localhost {
|
|||
match entry {
|
||||
FsEntry::Directory(dir) => {
|
||||
// If file doesn't exist; return error
|
||||
debug!("Removing directory {}", dir.abs_path.display());
|
||||
if !dir.abs_path.as_path().exists() {
|
||||
error!("Directory doesn't exist");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
None,
|
||||
|
@ -258,18 +282,24 @@ impl Localhost {
|
|||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
info!("Removed directory {}", dir.abs_path.display());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::DeleteFailed,
|
||||
Some(err),
|
||||
dir.abs_path.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Could not remove directory: {}", err);
|
||||
Err(HostError::new(
|
||||
HostErrorType::DeleteFailed,
|
||||
Some(err),
|
||||
dir.abs_path.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
FsEntry::File(file) => {
|
||||
// If file doesn't exist; return error
|
||||
debug!("Removing file {}", file.abs_path.display());
|
||||
if !file.abs_path.as_path().exists() {
|
||||
error!("File doesn't exist");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
None,
|
||||
|
@ -281,13 +311,17 @@ impl Localhost {
|
|||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
info!("Removed file {}", file.abs_path.display());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::DeleteFailed,
|
||||
Some(err),
|
||||
file.abs_path.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Could not remove file: {}", err);
|
||||
Err(HostError::new(
|
||||
HostErrorType::DeleteFailed,
|
||||
Some(err),
|
||||
file.abs_path.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -302,13 +336,26 @@ impl Localhost {
|
|||
Ok(_) => {
|
||||
// Scan dir
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
debug!(
|
||||
"Moved file {} to {}",
|
||||
entry.get_abs_path().display(),
|
||||
dst_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(err),
|
||||
abs_path.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to move {} to {}: {}",
|
||||
entry.get_abs_path().display(),
|
||||
dst_path.display(),
|
||||
err
|
||||
);
|
||||
Err(HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(err),
|
||||
abs_path.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,6 +365,11 @@ impl Localhost {
|
|||
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
|
||||
// Get absolute path of dest
|
||||
let dst: PathBuf = self.to_abs_path(dst);
|
||||
info!(
|
||||
"Copying file {} to {}",
|
||||
entry.get_abs_path().display(),
|
||||
dst.display()
|
||||
);
|
||||
// Match entry
|
||||
match entry {
|
||||
FsEntry::File(file) => {
|
||||
|
@ -333,16 +385,19 @@ impl Localhost {
|
|||
};
|
||||
// Copy entry path to dst path
|
||||
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
|
||||
error!("Failed to copy file: {}", err);
|
||||
return Err(HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(err),
|
||||
file.abs_path.as_path(),
|
||||
));
|
||||
}
|
||||
info!("File copied");
|
||||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
// If destination path doesn't exist, create destination
|
||||
if !dst.exists() {
|
||||
debug!("Directory {} doesn't exist; creating it", dst.display());
|
||||
self.mkdir(dst.as_path())?;
|
||||
}
|
||||
// Scan dir
|
||||
|
@ -386,15 +441,17 @@ impl Localhost {
|
|||
/// Stat file and create a FsEntry
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||
info!("Stating file {}", path.display());
|
||||
let path: PathBuf = self.to_abs_path(path);
|
||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
error!("Could not read file metadata: {}", err);
|
||||
return Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
path.as_path(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||
|
@ -454,14 +511,16 @@ impl Localhost {
|
|||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||
let path: PathBuf = self.to_abs_path(path);
|
||||
info!("Stating file {}", path.display());
|
||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
error!("Could not read file metadata: {}", err);
|
||||
return Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
path.as_path(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||
|
@ -523,16 +582,23 @@ impl Localhost {
|
|||
let args: Vec<&str> = cmd.split(' ').collect();
|
||||
let cmd: &str = args.first().unwrap();
|
||||
let argv: &[&str] = &args[1..];
|
||||
info!("Executing command: {} {:?}", cmd, argv);
|
||||
match std::process::Command::new(cmd).args(argv).output() {
|
||||
Ok(output) => match std::str::from_utf8(&output.stdout) {
|
||||
Ok(s) => Ok(s.to_string()),
|
||||
Ok(s) => {
|
||||
info!("Command output: {}", s);
|
||||
Ok(s.to_string())
|
||||
}
|
||||
Err(_) => Ok(String::new()),
|
||||
},
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::ExecutionFailed,
|
||||
Some(err),
|
||||
self.wrkdir.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to run command: {}", err);
|
||||
Err(HostError::new(
|
||||
HostErrorType::ExecutionFailed,
|
||||
Some(err),
|
||||
self.wrkdir.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -548,19 +614,32 @@ impl Localhost {
|
|||
let mut mpex = metadata.permissions();
|
||||
mpex.set_mode(self.mode_to_u32(pex));
|
||||
match set_permissions(path.as_path(), mpex) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
path.as_path(),
|
||||
)),
|
||||
Ok(_) => {
|
||||
info!("Changed mode for {} to {:?}", path.display(), pex);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not change mode for file {}: {}", path.display(), err);
|
||||
Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
path.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
path.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Chmod failed; could not read metadata for file {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
path.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -569,7 +648,9 @@ impl Localhost {
|
|||
/// Open file for read
|
||||
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
||||
let file: PathBuf = self.to_abs_path(file);
|
||||
info!("Opening file {} for read", file.display());
|
||||
if !self.file_exists(file.as_path()) {
|
||||
error!("File doesn't exist!");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
None,
|
||||
|
@ -583,11 +664,14 @@ impl Localhost {
|
|||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(f),
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
file.as_path(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Could not open file for read: {}", err);
|
||||
Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
file.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -596,6 +680,7 @@ impl Localhost {
|
|||
/// Open file for write
|
||||
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
||||
let file: PathBuf = self.to_abs_path(file);
|
||||
info!("Opening file {} for write", file.display());
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
|
@ -603,18 +688,21 @@ impl Localhost {
|
|||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(f),
|
||||
Err(err) => match self.file_exists(file.as_path()) {
|
||||
true => Err(HostError::new(
|
||||
HostErrorType::ReadonlyFile,
|
||||
Some(err),
|
||||
file.as_path(),
|
||||
)),
|
||||
false => Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
file.as_path(),
|
||||
)),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Failed to open file: {}", err);
|
||||
match self.file_exists(file.as_path()) {
|
||||
true => Err(HostError::new(
|
||||
HostErrorType::ReadonlyFile,
|
||||
Some(err),
|
||||
file.as_path(),
|
||||
)),
|
||||
false => Err(HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(err),
|
||||
file.as_path(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -629,13 +717,15 @@ impl Localhost {
|
|||
///
|
||||
/// Get content of the current directory as a list of fs entry
|
||||
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
|
||||
info!("Reading directory {}", dir.display());
|
||||
match std::fs::read_dir(dir) {
|
||||
Ok(e) => {
|
||||
let mut fs_entries: Vec<FsEntry> = Vec::new();
|
||||
for entry in e.flatten() {
|
||||
// NOTE: 0.4.1, don't fail if stat for one file fails
|
||||
if let Ok(entry) = self.stat(entry.path().as_path()) {
|
||||
fs_entries.push(entry);
|
||||
match self.stat(entry.path().as_path()) {
|
||||
Ok(entry) => fs_entries.push(entry),
|
||||
Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
|
||||
}
|
||||
}
|
||||
Ok(fs_entries)
|
||||
|
|
|
@ -35,6 +35,8 @@ extern crate bitflags;
|
|||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
||||
pub mod activity_manager;
|
||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -32,6 +32,8 @@ extern crate bitflags;
|
|||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
extern crate rpassword;
|
||||
|
||||
|
@ -55,6 +57,7 @@ mod utils;
|
|||
// namespaces
|
||||
use activity_manager::{ActivityManager, NextActivity};
|
||||
use filetransfer::FileTransferProtocol;
|
||||
use system::logging;
|
||||
|
||||
/// ### print_usage
|
||||
///
|
||||
|
@ -79,10 +82,12 @@ fn main() {
|
|||
let mut remote_wrkdir: Option<PathBuf> = None;
|
||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
||||
let mut ticks: Duration = Duration::from_millis(10);
|
||||
let mut log_enabled: bool = true;
|
||||
//Process options
|
||||
let mut opts = Options::new();
|
||||
opts.optopt("P", "password", "Provide password from CLI", "<password>");
|
||||
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
|
||||
opts.optflag("q", "quiet", "Disable logging");
|
||||
opts.optflag("v", "version", "");
|
||||
opts.optflag("h", "help", "Print this menu");
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
|
@ -105,6 +110,10 @@ fn main() {
|
|||
);
|
||||
std::process::exit(255);
|
||||
}
|
||||
// Logging
|
||||
if matches.opt_present("q") {
|
||||
log_enabled = false;
|
||||
}
|
||||
// Match password
|
||||
if let Some(passwd) = matches.opt_str("P") {
|
||||
password = Some(passwd);
|
||||
|
@ -155,9 +164,17 @@ fn main() {
|
|||
Ok(dir) => dir,
|
||||
Err(_) => PathBuf::from("/"),
|
||||
};
|
||||
// Setup logging
|
||||
if log_enabled {
|
||||
if let Err(err) = logging::init() {
|
||||
eprintln!("Failed to initialize logging: {}", err);
|
||||
}
|
||||
}
|
||||
info!("termscp {} started!", TERMSCP_VERSION);
|
||||
// Initialize client if necessary
|
||||
let mut start_activity: NextActivity = NextActivity::Authentication;
|
||||
if address.is_some() {
|
||||
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", address, port, protocol, username, utils::fmt::shadow_password(password.as_deref().unwrap_or("")));
|
||||
if password.is_none() {
|
||||
// Ask password if unspecified
|
||||
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||
|
@ -173,6 +190,10 @@ fn main() {
|
|||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"Read password from tty: {}",
|
||||
utils::fmt::shadow_password(password.as_deref().unwrap_or(""))
|
||||
);
|
||||
}
|
||||
// In this case the first activity will be FileTransfer
|
||||
start_activity = NextActivity::FileTransfer;
|
||||
|
@ -190,7 +211,9 @@ fn main() {
|
|||
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
||||
}
|
||||
// Run
|
||||
info!("Starting activity manager...");
|
||||
manager.run(start_activity);
|
||||
info!("termscp terminated");
|
||||
// Then return
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
|
|
@ -68,9 +68,11 @@ impl BookmarksClient {
|
|||
) -> Result<BookmarksClient, SerializerError> {
|
||||
// Create default hosts
|
||||
let default_hosts: UserHosts = Default::default();
|
||||
debug!("Setting up bookmarks client...");
|
||||
// Make a key storage (windows / macos)
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
|
@ -79,8 +81,14 @@ impl BookmarksClient {
|
|||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => (Box::new(storage), app_name),
|
||||
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
|
||||
true => {
|
||||
debug!("Using KeyringStorage");
|
||||
(Box::new(storage), app_name)
|
||||
}
|
||||
false => {
|
||||
warn!("KeyringStorage is not supported; using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
||||
}
|
||||
}
|
||||
};
|
||||
// Make a key storage (linux / unix)
|
||||
|
@ -90,16 +98,22 @@ impl BookmarksClient {
|
|||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
debug!("Using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
};
|
||||
// Load key
|
||||
let key: String = match key_storage.get_key(service_id) {
|
||||
Ok(k) => k,
|
||||
Ok(k) => {
|
||||
debug!("Key loaded with success");
|
||||
k
|
||||
}
|
||||
Err(e) => match e {
|
||||
KeyStorageError::NoSuchKey => {
|
||||
// If no such key, generate key and set it into the storage
|
||||
let key: String = Self::generate_key();
|
||||
debug!("Key doesn't exist yet or could not be loaded; generated a new key");
|
||||
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
|
||||
error!("Failed to set new key into storage: {}", e);
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!("Could not write key to storage: {}", e),
|
||||
|
@ -109,10 +123,11 @@ impl BookmarksClient {
|
|||
key
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to get key from storage: {}", e);
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!("Could not get key from storage: {}", e),
|
||||
))
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -124,15 +139,19 @@ impl BookmarksClient {
|
|||
};
|
||||
// If bookmark file doesn't exist, initialize it
|
||||
if !bookmarks_file.exists() {
|
||||
info!("Bookmarks file doesn't exist yet; creating it...");
|
||||
if let Err(err) = client.write_bookmarks() {
|
||||
error!("Failed to create bookmarks file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
// Load bookmarks from file
|
||||
if let Err(err) = client.read_bookmarks() {
|
||||
error!("Failed to load bookmarks: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
info!("Bookmarks client initialized");
|
||||
// Load key
|
||||
Ok(client)
|
||||
}
|
||||
|
@ -152,19 +171,29 @@ impl BookmarksClient {
|
|||
key: &str,
|
||||
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
||||
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
||||
debug!("Getting bookmark {}", key);
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||
err
|
||||
);
|
||||
FileTransferProtocol::Sftp // Default
|
||||
}
|
||||
},
|
||||
entry.username.clone(),
|
||||
match &entry.password {
|
||||
// Decrypted password if Some; if decryption fails return None
|
||||
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
||||
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
||||
Err(_) => None,
|
||||
Err(err) => {
|
||||
error!("Failed to decrypt password for bookmark: {}", err);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
|
@ -184,9 +213,11 @@ impl BookmarksClient {
|
|||
password: Option<String>,
|
||||
) {
|
||||
if name.is_empty() {
|
||||
error!("Fatal error; bookmark name is empty");
|
||||
panic!("Bookmark name can't be empty");
|
||||
}
|
||||
// Make bookmark
|
||||
info!("Added bookmark {} with address {}", name, addr);
|
||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
||||
self.hosts.bookmarks.insert(name, host);
|
||||
}
|
||||
|
@ -196,6 +227,7 @@ impl BookmarksClient {
|
|||
/// Delete entry from bookmarks
|
||||
pub fn del_bookmark(&mut self, name: &str) {
|
||||
let _ = self.hosts.bookmarks.remove(name);
|
||||
info!("Removed bookmark {}", name);
|
||||
}
|
||||
/// ### iter_recents
|
||||
///
|
||||
|
@ -209,13 +241,20 @@ impl BookmarksClient {
|
|||
/// Get recent associated to key
|
||||
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
||||
// NOTE: password is not decrypted; recents will never have password
|
||||
info!("Getting bookmark {}", key);
|
||||
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||
err
|
||||
);
|
||||
FileTransferProtocol::Sftp // Default
|
||||
}
|
||||
},
|
||||
entry.username.clone(),
|
||||
))
|
||||
|
@ -236,6 +275,7 @@ impl BookmarksClient {
|
|||
// Check if duplicated
|
||||
for recent_host in self.hosts.recents.values() {
|
||||
if *recent_host == host {
|
||||
debug!("Discarding recent since duplicated ({})", host.address);
|
||||
// Don't save duplicates
|
||||
return;
|
||||
}
|
||||
|
@ -252,6 +292,7 @@ impl BookmarksClient {
|
|||
// Delete keys starting from the last one
|
||||
for key in keys.iter() {
|
||||
let _ = self.hosts.recents.remove(key);
|
||||
debug!("Removed recent bookmark {}", key);
|
||||
// If length is < self.recents_size; break
|
||||
if self.hosts.recents.len() < self.recents_size {
|
||||
break;
|
||||
|
@ -259,6 +300,7 @@ impl BookmarksClient {
|
|||
}
|
||||
}
|
||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||
info!("Saved recent host {} ({})", name, host.address);
|
||||
self.hosts.recents.insert(name, host);
|
||||
}
|
||||
|
||||
|
@ -267,6 +309,7 @@ impl BookmarksClient {
|
|||
/// Delete entry from recents
|
||||
pub fn del_recent(&mut self, name: &str) {
|
||||
let _ = self.hosts.recents.remove(name);
|
||||
info!("Removed recent host {}", name);
|
||||
}
|
||||
|
||||
/// ### write_bookmarks
|
||||
|
@ -274,6 +317,7 @@ impl BookmarksClient {
|
|||
/// Write bookmarks to file
|
||||
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
||||
// Open file
|
||||
debug!("Writing bookmarks");
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
|
@ -284,10 +328,13 @@ impl BookmarksClient {
|
|||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.hosts)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to write bookmarks: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,6 +343,7 @@ impl BookmarksClient {
|
|||
/// Read bookmarks from file
|
||||
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
||||
// Open bookmarks file for read
|
||||
debug!("Reading bookmarks");
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.open(self.bookmarks_file.as_path())
|
||||
|
@ -311,10 +359,13 @@ impl BookmarksClient {
|
|||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to read bookmarks: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,11 @@ impl ConfigClient {
|
|||
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
|
||||
// Initialize a default configuration
|
||||
let default_config: UserConfig = UserConfig::default();
|
||||
info!(
|
||||
"Setting up config client with config path {} and SSH key directory {}",
|
||||
config_path.display(),
|
||||
ssh_key_dir.display()
|
||||
);
|
||||
// Create client
|
||||
let mut client: ConfigClient = ConfigClient {
|
||||
config: default_config,
|
||||
|
@ -67,6 +72,7 @@ impl ConfigClient {
|
|||
// If ssh key directory doesn't exist, create it
|
||||
if !ssh_key_dir.exists() {
|
||||
if let Err(err) = create_dir(ssh_key_dir) {
|
||||
error!("Failed to create SSH key dir: {}", err);
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!(
|
||||
|
@ -76,17 +82,22 @@ impl ConfigClient {
|
|||
),
|
||||
));
|
||||
}
|
||||
debug!("Created SSH key directory");
|
||||
}
|
||||
// If Config file doesn't exist, create it
|
||||
if !config_path.exists() {
|
||||
if let Err(err) = client.write_config() {
|
||||
error!("Couldn't create configuration file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Config file didn't exist; created file");
|
||||
} else {
|
||||
// otherwise Load configuration from file
|
||||
if let Err(err) = client.read_config() {
|
||||
error!("Couldn't read configuration file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Read configuration file");
|
||||
}
|
||||
Ok(client)
|
||||
}
|
||||
|
@ -230,12 +241,18 @@ impl ConfigClient {
|
|||
p.push(format!("{}.key", host_name));
|
||||
p
|
||||
};
|
||||
info!(
|
||||
"Writing SSH file to {} for host {}",
|
||||
ssh_key_path.display(),
|
||||
host_name
|
||||
);
|
||||
// Write key to file
|
||||
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Self::make_io_err(err),
|
||||
};
|
||||
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
|
||||
error!("Failed to write SSH key to file: {}", err);
|
||||
return Self::make_io_err(err);
|
||||
}
|
||||
// Add host to keys
|
||||
|
@ -251,6 +268,7 @@ impl ConfigClient {
|
|||
/// and also commits changes to configuration, to prevent incoerent data
|
||||
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
||||
// Remove key from configuration and get key path
|
||||
info!("Removing key for {}@{}", host, username);
|
||||
let key_path: PathBuf = match self
|
||||
.config
|
||||
.remote
|
||||
|
@ -262,6 +280,7 @@ impl ConfigClient {
|
|||
};
|
||||
// Remove file
|
||||
if let Err(err) = remove_file(key_path.as_path()) {
|
||||
error!("Failed to remove key file {}: {}", key_path.display(), err);
|
||||
return Self::make_io_err(err);
|
||||
}
|
||||
// Commit changes to configuration
|
||||
|
@ -310,10 +329,13 @@ impl ConfigClient {
|
|||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.config)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to write configuration file: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,10 +359,13 @@ impl ConfigClient {
|
|||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to read configuration: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,15 @@ pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
|||
(bookmarks_file, keys_dir)
|
||||
}
|
||||
|
||||
/// ### get_log_paths
|
||||
///
|
||||
/// Returns the path for the supposed log file
|
||||
pub fn get_log_paths(config_dir: &Path) -> PathBuf {
|
||||
let mut log_file: PathBuf = PathBuf::from(config_dir);
|
||||
log_file.push("termscp.log");
|
||||
log_file
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
@ -143,4 +152,12 @@ mod tests {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_log_paths() {
|
||||
assert_eq!(
|
||||
get_log_paths(&Path::new("/home/omar/.config/termscp/")),
|
||||
PathBuf::from("/home/omar/.config/termscp/termscp.log"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
72
src/system/logging.rs
Normal file
72
src/system/logging.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
//! ## Logging
|
||||
//!
|
||||
//! `logging` is the module which initializes the logging system for termscp
|
||||
|
||||
/**
|
||||
* 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 crate::system::environment::{get_log_paths, init_config_dir};
|
||||
use crate::utils::file::open_file;
|
||||
// ext
|
||||
use simplelog::{ConfigBuilder, LevelFilter, WriteLogger};
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// ### init
|
||||
///
|
||||
/// Initialize logger
|
||||
pub fn init() -> Result<(), String> {
|
||||
// Init config dir
|
||||
let config_dir: PathBuf = match init_config_dir() {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => {
|
||||
return Err(String::from(
|
||||
"This system doesn't seem to support CONFIG_DIR",
|
||||
))
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let log_file_path: PathBuf = get_log_paths(config_dir.as_path());
|
||||
// Open log file
|
||||
let file: File = open_file(log_file_path.as_path(), true, true, false)
|
||||
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
|
||||
// Prepare log config
|
||||
let config = ConfigBuilder::new()
|
||||
.set_time_format_str("%Y-%m-%dT%H:%M:%S%z")
|
||||
.build();
|
||||
// Make logger
|
||||
WriteLogger::init(LevelFilter::Trace, config, file)
|
||||
.map_err(|e| format!("Failed to initialize logger: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_logging_setup() {
|
||||
assert!(init().is_ok());
|
||||
}
|
||||
}
|
|
@ -30,4 +30,5 @@ pub mod bookmarks_client;
|
|||
pub mod config_client;
|
||||
pub mod environment;
|
||||
pub(crate) mod keys;
|
||||
pub mod logging;
|
||||
pub mod sshkey_storage;
|
||||
|
|
|
@ -42,6 +42,7 @@ impl SshKeyStorage {
|
|||
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
||||
let mut hosts: HashMap<String, PathBuf> =
|
||||
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
||||
debug!("Setting up SSH key storage");
|
||||
// Iterate over keys
|
||||
for key in cfg_client.iter_ssh_keys() {
|
||||
match cfg_client.get_ssh_key(key) {
|
||||
|
@ -52,8 +53,12 @@ impl SshKeyStorage {
|
|||
}
|
||||
None => continue,
|
||||
},
|
||||
Err(_) => continue,
|
||||
Err(err) => {
|
||||
error!("Failed to get SSH key for {}: {}", key, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
info!("Got SSH key for {}", key);
|
||||
}
|
||||
// Return storage
|
||||
SshKeyStorage { hosts }
|
||||
|
|
|
@ -108,17 +108,24 @@ impl AuthActivity {
|
|||
///
|
||||
/// If enabled in configuration, check for updates from Github
|
||||
fn check_for_updates(&mut self) {
|
||||
debug!("Check for updates...");
|
||||
// Check version only if unset in the store
|
||||
let ctx: &Context = self.context.as_ref().unwrap();
|
||||
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
|
||||
debug!("Version is not set in storage");
|
||||
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
|
||||
Some(client) => {
|
||||
if client.get_check_for_updates() {
|
||||
debug!("Check for updates is enabled");
|
||||
// Send request
|
||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||
Ok(version) => version,
|
||||
Ok(version) => {
|
||||
info!("Latest version is: {:?}", version);
|
||||
version
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
error!("Failed to get latest version: {}", err);
|
||||
self.mount_error(
|
||||
format!("Could not check for new updates: {}", err).as_str(),
|
||||
);
|
||||
|
@ -127,6 +134,7 @@ impl AuthActivity {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
info!("Check for updates is disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +157,7 @@ impl Activity for AuthActivity {
|
|||
/// `on_create` must initialize all the data structures used by the activity
|
||||
/// Context is taken from activity manager and will be released only when activity is destroyed
|
||||
fn on_create(&mut self, mut context: Context) {
|
||||
debug!("Initializing activity");
|
||||
// Initialize file transfer params
|
||||
context.ft_params = Some(FileTransferParams::default());
|
||||
// Set context
|
||||
|
@ -156,7 +165,9 @@ impl Activity for AuthActivity {
|
|||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Init bookmarks client
|
||||
if self.bookmarks_client.is_none() {
|
||||
self.init_bookmarks_client();
|
||||
|
@ -169,6 +180,7 @@ impl Activity for AuthActivity {
|
|||
self.check_for_updates();
|
||||
// Initialize view
|
||||
self.init();
|
||||
info!("Activity initialized");
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
|
@ -213,7 +225,9 @@ impl Activity for AuthActivity {
|
|||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
|
|
|
@ -271,7 +271,9 @@ impl AuthActivity {
|
|||
Some(Payload::One(Value::Usize(0)))
|
||||
);
|
||||
// Save bookmark
|
||||
self.save_bookmark(bookmark_name, save_pwd);
|
||||
if !bookmark_name.is_empty() {
|
||||
self.save_bookmark(bookmark_name, save_pwd);
|
||||
}
|
||||
// Umount popup
|
||||
self.umount_bookmark_save_dialog();
|
||||
// Reload bookmarks
|
||||
|
|
|
@ -36,6 +36,12 @@ impl FileTransferActivity {
|
|||
///
|
||||
/// Add message to log events
|
||||
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
|
||||
// Log to file
|
||||
match level {
|
||||
LogLevel::Error => error!("{}", msg),
|
||||
LogLevel::Info => info!("{}", msg),
|
||||
LogLevel::Warn => warn!("{}", msg),
|
||||
}
|
||||
// Create log record
|
||||
let record: LogRecord = LogRecord::new(level, msg);
|
||||
//Check if history overflows the size
|
||||
|
|
|
@ -273,25 +273,33 @@ impl Activity for FileTransferActivity {
|
|||
/// `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) {
|
||||
debug!("Initializing activity...");
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// 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;
|
||||
debug!("Read working directory");
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
debug!("Setup text editor");
|
||||
// init view
|
||||
self.init();
|
||||
debug!("Initialized view");
|
||||
// Verify error state from context
|
||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||
error!("Fatal error on create: {}", err);
|
||||
self.mount_fatal(&err);
|
||||
}
|
||||
info!("Created FileTransferActivity");
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
|
@ -308,6 +316,10 @@ impl Activity for FileTransferActivity {
|
|||
// 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();
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {}:{}",
|
||||
params.address, params.port
|
||||
);
|
||||
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
|
||||
// Set init state to connecting popup
|
||||
self.mount_wait(msg.as_str());
|
||||
|
@ -341,7 +353,9 @@ impl Activity for FileTransferActivity {
|
|||
/// This function must be called once before terminating the activity.
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
// Disconnect client
|
||||
if self.client.is_connected() {
|
||||
let _ = self.client.disconnect();
|
||||
|
|
|
@ -783,7 +783,9 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
// Put input mode back to normal
|
||||
let _ = disable_raw_mode();
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
|
|
|
@ -111,13 +111,17 @@ impl SetupActivity {
|
|||
env::set_var("EDITOR", cli.get_text_editor());
|
||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||
// Put input mode back to normal
|
||||
let _ = disable_raw_mode();
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
|
|
|
@ -88,7 +88,9 @@ impl SetupActivity {
|
|||
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||
}
|
||||
// Prepare terminal
|
||||
let _ = disable_raw_mode();
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
// Leave alternate mode
|
||||
ctx.leave_alternate_screen();
|
||||
// Get result
|
||||
|
@ -121,7 +123,9 @@ impl SetupActivity {
|
|||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Return result
|
||||
result
|
||||
}
|
||||
|
|
|
@ -110,7 +110,9 @@ impl Activity for SetupActivity {
|
|||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
if let Err(err) = enable_raw_mode() {
|
||||
error!("Failed to enter raw mode: {}", err);
|
||||
}
|
||||
// Init view
|
||||
self.init_setup();
|
||||
// Verify error state from context
|
||||
|
@ -161,7 +163,9 @@ impl Activity for SetupActivity {
|
|||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
error!("Failed to disable raw mode: {}", err);
|
||||
}
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
|
|
|
@ -104,29 +104,38 @@ impl Context {
|
|||
///
|
||||
/// Enter alternate screen (gui window)
|
||||
pub fn enter_alternate_screen(&mut self) {
|
||||
let _ = execute!(
|
||||
match execute!(
|
||||
self.terminal.backend_mut(),
|
||||
EnterAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
) {
|
||||
Err(err) => error!("Failed to enter alternate screen: {}", err),
|
||||
Ok(_) => info!("Entered alternate screen"),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### leave_alternate_screen
|
||||
///
|
||||
/// Go back to normal screen (gui window)
|
||||
pub fn leave_alternate_screen(&mut self) {
|
||||
let _ = execute!(
|
||||
match execute!(
|
||||
self.terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
) {
|
||||
Err(err) => error!("Failed to leave alternate screen: {}", err),
|
||||
Ok(_) => info!("Left alternate screen"),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### clear_screen
|
||||
///
|
||||
/// Clear terminal screen
|
||||
pub fn clear_screen(&mut self) {
|
||||
let _ = self.terminal.clear();
|
||||
match self.terminal.clear() {
|
||||
Err(err) => error!("Failed to clear screen: {}", err),
|
||||
Ok(_) => info!("Cleared screen"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
57
src/utils/file.rs
Normal file
57
src/utils/file.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
//! ## File
|
||||
//!
|
||||
//! `file` is the module which exposes file related utilities
|
||||
|
||||
/**
|
||||
* 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 std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
/// ### open_file
|
||||
///
|
||||
/// Open file provided as parameter
|
||||
pub fn open_file<P>(filename: P, create: bool, write: bool, append: bool) -> io::Result<File>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
OpenOptions::new()
|
||||
.create(create)
|
||||
.write(write)
|
||||
.append(append)
|
||||
.truncate(!append)
|
||||
.open(filename)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_utils_file_open() {
|
||||
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
assert!(open_file(tmpfile.path(), true, true, true).is_ok());
|
||||
}
|
||||
}
|
|
@ -152,6 +152,13 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// ### shadow_password
|
||||
///
|
||||
/// Return a string with the same length of input string, but each character is replaced by '*'
|
||||
pub fn shadow_password(s: &str) -> String {
|
||||
(0..s.len()).map(|_| '*').collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
@ -219,4 +226,9 @@ mod tests {
|
|||
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_fmt_shadow_password() {
|
||||
assert_eq!(shadow_password("foobar"), String::from("******"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
*/
|
||||
// modules
|
||||
pub mod crypto;
|
||||
pub mod file;
|
||||
pub mod fmt;
|
||||
pub mod git;
|
||||
pub mod parser;
|
||||
|
|
Loading…
Reference in a new issue