mirror of
https://github.com/veeso/termscp.git
synced 2026-09-26 05:51:20 -07:00
@@ -28,6 +28,11 @@ A clear and concise description of what you expected to happen.
|
|||||||
- Protocol used
|
- Protocol used
|
||||||
- Remote server version and name
|
- 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
|
## Additional information
|
||||||
|
|
||||||
Add any other context about the problem here.
|
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 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>`
|
- 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-)
|
- 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**
|
- **File transfer changes**
|
||||||
- *SFTP*
|
- *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).
|
- 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`)
|
- 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:
|
- Bugfix:
|
||||||
- Fixed wrong text wrap in log box
|
- 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 error message not being shown after an upload failure
|
||||||
- Fixed default protocol not being loaded from config
|
- 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
|
- [Issue 23](https://github.com/veeso/termscp/issues/23): Remove created file if transfer failed or was abrupted
|
||||||
|
|||||||
+2
-1
@@ -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.
|
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).
|
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.
|
Last but not least: the template I've written must be used. Full stop.
|
||||||
|
|
||||||
Maintainers will may add additional labels to your issue:
|
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.
|
- **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
|
- **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.
|
- **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
|
### Feature requests
|
||||||
|
|
||||||
|
|||||||
Generated
+31
@@ -1230,6 +1230,17 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -1291,6 +1302,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termscp"
|
name = "termscp"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1307,6 +1327,7 @@ dependencies = [
|
|||||||
"hostname",
|
"hostname",
|
||||||
"keyring",
|
"keyring",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"log",
|
||||||
"magic-crypt",
|
"magic-crypt",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -1314,6 +1335,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"serde",
|
"serde",
|
||||||
|
"simplelog",
|
||||||
"ssh2",
|
"ssh2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
@@ -1664,6 +1686,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
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]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|||||||
@@ -37,10 +37,12 @@ edit = "0.1.3"
|
|||||||
getopts = "0.2.21"
|
getopts = "0.2.21"
|
||||||
hostname = "0.3.1"
|
hostname = "0.3.1"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.14"
|
||||||
magic-crypt = "3.1.7"
|
magic-crypt = "3.1.7"
|
||||||
rand = "0.8.3"
|
rand = "0.8.3"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
rpassword = "5.0.1"
|
rpassword = "5.0.1"
|
||||||
|
simplelog = "0.10.0"
|
||||||
ssh2 = "0.9.0"
|
ssh2 = "0.9.0"
|
||||||
tempfile = "3.1.0"
|
tempfile = "3.1.0"
|
||||||
textwrap = "0.13.4"
|
textwrap = "0.13.4"
|
||||||
|
|||||||
+32
@@ -15,6 +15,7 @@
|
|||||||
- [File Explorer Format](#file-explorer-format)
|
- [File Explorer Format](#file-explorer-format)
|
||||||
- [Text Editor ✏](#text-editor-)
|
- [Text Editor ✏](#text-editor-)
|
||||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-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]`
|
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||||
|
|
||||||
- `-P, --password <password>` if address is provided, password will be this argument
|
- `-P, --password <password>` if address is provided, password will be this argument
|
||||||
|
- `-q, --quiet` Disable logging
|
||||||
- `-v, --version` Print version info
|
- `-v, --version` Print version info
|
||||||
- `-h, --help` Print help page
|
- `-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 🦥
|
### 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-️)
|
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.
|
||||||
|
|||||||
+30
-5
@@ -69,7 +69,10 @@ impl ActivityManager {
|
|||||||
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
let (config_client, error): (Option<ConfigClient>, Option<String>) =
|
||||||
match Self::init_config_client() {
|
match Self::init_config_client() {
|
||||||
Ok(cli) => (Some(cli), None),
|
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);
|
let ctx: Context = Context::new(config_client, error);
|
||||||
Ok(ActivityManager {
|
Ok(ActivityManager {
|
||||||
@@ -131,6 +134,7 @@ impl ActivityManager {
|
|||||||
/// Returns when activity terminates.
|
/// Returns when activity terminates.
|
||||||
/// Returns the next activity to run
|
/// Returns the next activity to run
|
||||||
fn run_authentication(&mut self) -> Option<NextActivity> {
|
fn run_authentication(&mut self) -> Option<NextActivity> {
|
||||||
|
info!("Starting AuthActivity...");
|
||||||
// Prepare activity
|
// Prepare activity
|
||||||
let mut activity: AuthActivity = AuthActivity::default();
|
let mut activity: AuthActivity = AuthActivity::default();
|
||||||
// Prepare result
|
// Prepare result
|
||||||
@@ -138,7 +142,10 @@ impl ActivityManager {
|
|||||||
// Get context
|
// Get context
|
||||||
let ctx: Context = match self.context.take() {
|
let ctx: Context = match self.context.take() {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start AuthActivity: context is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Create activity
|
// Create activity
|
||||||
activity.on_create(ctx);
|
activity.on_create(ctx);
|
||||||
@@ -149,16 +156,19 @@ impl ActivityManager {
|
|||||||
if let Some(exit_reason) = activity.will_umount() {
|
if let Some(exit_reason) = activity.will_umount() {
|
||||||
match exit_reason {
|
match exit_reason {
|
||||||
ExitReason::Quit => {
|
ExitReason::Quit => {
|
||||||
|
info!("AuthActivity terminated due to 'Quit'");
|
||||||
result = None;
|
result = None;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ExitReason::EnterSetup => {
|
ExitReason::EnterSetup => {
|
||||||
// User requested activity
|
// User requested activity
|
||||||
|
info!("AuthActivity terminated due to 'EnterSetup'");
|
||||||
result = Some(NextActivity::SetupActivity);
|
result = Some(NextActivity::SetupActivity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ExitReason::Connect => {
|
ExitReason::Connect => {
|
||||||
// User submitted, set next activity
|
// User submitted, set next activity
|
||||||
|
info!("AuthActivity terminated due to 'Connect'");
|
||||||
result = Some(NextActivity::FileTransfer);
|
result = Some(NextActivity::FileTransfer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -170,6 +180,7 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
// Destroy activity
|
// Destroy activity
|
||||||
self.context = activity.on_destroy();
|
self.context = activity.on_destroy();
|
||||||
|
info!("AuthActivity destroyed");
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,15 +190,22 @@ impl ActivityManager {
|
|||||||
/// Returns when activity terminates.
|
/// Returns when activity terminates.
|
||||||
/// Returns the next activity to run
|
/// Returns the next activity to run
|
||||||
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
||||||
|
info!("Starting FileTransferActivity");
|
||||||
// Get context
|
// Get context
|
||||||
let mut ctx: Context = match self.context.take() {
|
let mut ctx: Context = match self.context.take() {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start FileTransferActivity: context is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// If ft params is None, return None
|
// If ft params is None, return None
|
||||||
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
|
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
|
||||||
Some(ft_params) => &ft_params,
|
Some(ft_params) => &ft_params,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Prepare activity
|
// Prepare activity
|
||||||
let protocol: FileTransferProtocol = ft_params.protocol;
|
let protocol: FileTransferProtocol = ft_params.protocol;
|
||||||
@@ -195,6 +213,7 @@ impl ActivityManager {
|
|||||||
Ok(host) => host,
|
Ok(host) => host,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Set error in context
|
// Set error in context
|
||||||
|
error!("Failed to initialize localhost: {}", err);
|
||||||
ctx.set_error(format!("Could not initialize localhost: {}", err));
|
ctx.set_error(format!("Could not initialize localhost: {}", err));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -211,11 +230,13 @@ impl ActivityManager {
|
|||||||
if let Some(exit_reason) = activity.will_umount() {
|
if let Some(exit_reason) = activity.will_umount() {
|
||||||
match exit_reason {
|
match exit_reason {
|
||||||
ExitReason::Quit => {
|
ExitReason::Quit => {
|
||||||
|
info!("FileTransferActivity terminated due to 'Quit'");
|
||||||
result = None;
|
result = None;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ExitReason::Disconnect => {
|
ExitReason::Disconnect => {
|
||||||
// User disconnected, set next activity to authentication
|
// User disconnected, set next activity to authentication
|
||||||
|
info!("FileTransferActivity terminated due to 'Authentication'");
|
||||||
result = Some(NextActivity::Authentication);
|
result = Some(NextActivity::Authentication);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -241,7 +262,10 @@ impl ActivityManager {
|
|||||||
// Get context
|
// Get context
|
||||||
let ctx: Context = match self.context.take() {
|
let ctx: Context = match self.context.take() {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => return None,
|
None => {
|
||||||
|
error!("Failed to start SetupActivity: context is None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Create activity
|
// Create activity
|
||||||
activity.on_create(ctx);
|
activity.on_create(ctx);
|
||||||
@@ -250,6 +274,7 @@ impl ActivityManager {
|
|||||||
activity.on_draw();
|
activity.on_draw();
|
||||||
// Check if activity has terminated
|
// Check if activity has terminated
|
||||||
if let Some(ExitReason::Quit) = activity.will_umount() {
|
if let Some(ExitReason::Quit) = activity.will_umount() {
|
||||||
|
info!("SetupActivity terminated due to 'Quit'");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Sleep for ticks
|
// Sleep for ticks
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ impl BookmarkSerializer {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
trace!("Serialized new bookmarks data: {}", data);
|
||||||
// Write file
|
// Write file
|
||||||
match writable.write_all(data.as_bytes()) {
|
match writable.write_all(data.as_bytes()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -72,9 +73,13 @@ impl BookmarkSerializer {
|
|||||||
err.to_string(),
|
err.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
trace!("Read bookmarks from file: {}", data);
|
||||||
// Deserialize
|
// Deserialize
|
||||||
match toml::de::from_str(data.as_str()) {
|
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(
|
Err(err) => Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::SyntaxError,
|
SerializerErrorKind::SyntaxError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ impl ConfigSerializer {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
trace!("Serialized new configuration data: {}", data);
|
||||||
// Write file
|
// Write file
|
||||||
match writable.write_all(data.as_bytes()) {
|
match writable.write_all(data.as_bytes()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -72,9 +73,13 @@ impl ConfigSerializer {
|
|||||||
err.to_string(),
|
err.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
trace!("Read configuration from file: {}", data);
|
||||||
// Deserialize
|
// Deserialize
|
||||||
match toml::de::from_str(data.as_str()) {
|
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(
|
Err(err) => Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::SyntaxError,
|
SerializerErrorKind::SyntaxError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ extern crate regex;
|
|||||||
|
|
||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
|
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||||
use crate::utils::parser::{parse_datetime, parse_lstime};
|
use crate::utils::parser::{parse_datetime, parse_lstime};
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
@@ -105,6 +106,7 @@ impl FtpFileTransfer {
|
|||||||
lazy_static! {
|
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();
|
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
|
// Apply regex to result
|
||||||
match LS_RE.captures(line) {
|
match LS_RE.captures(line) {
|
||||||
// String matches regex
|
// String matches regex
|
||||||
@@ -182,6 +184,7 @@ impl FtpFileTransfer {
|
|||||||
};
|
};
|
||||||
// Check if file_name is '.' or '..'
|
// Check if file_name is '.' or '..'
|
||||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||||
|
debug!("File name is {}; ignoring entry", file_name);
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
// Get symlink
|
// Get symlink
|
||||||
@@ -236,6 +239,19 @@ impl FtpFileTransfer {
|
|||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_string_lossy()));
|
.map(|s| String::from(s.to_string_lossy()));
|
||||||
// Return
|
// 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
|
// Push to entries
|
||||||
Ok(match is_dir {
|
Ok(match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -287,6 +303,7 @@ impl FtpFileTransfer {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
debug!("Parsing LIST (DOS) line: '{}'", line);
|
||||||
// Apply regex to result
|
// Apply regex to result
|
||||||
match DOS_RE.captures(line) {
|
match DOS_RE.captures(line) {
|
||||||
// String matches regex
|
// String matches regex
|
||||||
@@ -324,6 +341,14 @@ impl FtpFileTransfer {
|
|||||||
.as_path()
|
.as_path()
|
||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_string_lossy()));
|
.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
|
// Return entry
|
||||||
Ok(match is_dir {
|
Ok(match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -382,17 +407,20 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<Option<String>, FileTransferError> {
|
) -> Result<Option<String>, FileTransferError> {
|
||||||
// Get stream
|
// Get stream
|
||||||
|
info!("Connecting to {}:{}", address, port);
|
||||||
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
|
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
|
||||||
Ok(stream) => stream,
|
Ok(stream) => stream,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Failed to connect: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// If SSL, open secure session
|
// If SSL, open secure session
|
||||||
if self.ftps {
|
if self.ftps {
|
||||||
|
info!("Setting up TLS stream...");
|
||||||
let ctx = match TlsConnector::builder()
|
let ctx = match TlsConnector::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.danger_accept_invalid_hostnames(true)
|
.danger_accept_invalid_hostnames(true)
|
||||||
@@ -400,19 +428,21 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
{
|
{
|
||||||
Ok(tls) => tls,
|
Ok(tls) => tls,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Failed to setup TLS stream: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::SslError,
|
FileTransferErrorType::SslError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
stream = match stream.into_secure(ctx, address.as_str()) {
|
stream = match stream.into_secure(ctx, address.as_str()) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Failed to setup TLS stream: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::SslError,
|
FileTransferErrorType::SslError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -425,14 +455,22 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
Some(pwd) => pwd,
|
Some(pwd) => pwd,
|
||||||
None => String::new(),
|
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()) {
|
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
|
||||||
|
error!("Login failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
debug!("Setting transfer type to Binary");
|
||||||
// Initialize file type
|
// Initialize file type
|
||||||
if let Err(err) = stream.transfer_type(FileType::Binary) {
|
if let Err(err) = stream.transfer_type(FileType::Binary) {
|
||||||
|
error!("Failed to set transfer type to binary: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ProtocolError,
|
FileTransferErrorType::ProtocolError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -440,6 +478,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
}
|
}
|
||||||
// Set stream
|
// Set stream
|
||||||
self.stream = Some(stream);
|
self.stream = Some(stream);
|
||||||
|
info!("Connection successfully established");
|
||||||
// Return OK
|
// Return OK
|
||||||
Ok(self.stream.as_ref().unwrap().get_welcome_msg())
|
Ok(self.stream.as_ref().unwrap().get_welcome_msg())
|
||||||
}
|
}
|
||||||
@@ -449,6 +488,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Disconnect from the remote server
|
/// Disconnect from the remote server
|
||||||
|
|
||||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||||
|
info!("Disconnecting from FTP server...");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.quit() {
|
Some(stream) => match stream.quit() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -475,6 +515,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Print working directory
|
/// Print working directory
|
||||||
|
|
||||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||||
|
info!("PWD");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.pwd() {
|
Some(stream) => match stream.pwd() {
|
||||||
Ok(path) => Ok(PathBuf::from(path.as_str())),
|
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> {
|
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
|
||||||
let dir: PathBuf = Self::resolve(dir);
|
let dir: PathBuf = Self::resolve(dir);
|
||||||
|
info!("Changing directory to {}", dir.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
|
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
|
||||||
Ok(_) => Ok(dir),
|
Ok(_) => Ok(dir),
|
||||||
@@ -514,6 +556,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Copy file to destination
|
/// Copy file to destination
|
||||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
||||||
// FTP doesn't support file copy
|
// FTP doesn't support file copy
|
||||||
|
debug!("COPY issues (will fail, since unsupported)");
|
||||||
Err(FileTransferError::new(
|
Err(FileTransferError::new(
|
||||||
FileTransferErrorType::UnsupportedFeature,
|
FileTransferErrorType::UnsupportedFeature,
|
||||||
))
|
))
|
||||||
@@ -525,9 +568,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
|
|
||||||
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
|
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
|
||||||
let dir: PathBuf = Self::resolve(path);
|
let dir: PathBuf = Self::resolve(path);
|
||||||
|
info!("LIST dir {}", dir.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
|
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
|
debug!("Got {} lines in LIST result", entries.len());
|
||||||
// Prepare result
|
// Prepare result
|
||||||
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
|
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
|
||||||
// Iterate over entries
|
// Iterate over entries
|
||||||
@@ -536,6 +581,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
result.push(file);
|
result.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debug!(
|
||||||
|
"{} out of {} were valid entries",
|
||||||
|
result.len(),
|
||||||
|
entries.len()
|
||||||
|
);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -554,6 +604,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Make directory
|
/// Make directory
|
||||||
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
|
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
|
||||||
let dir: PathBuf = Self::resolve(dir);
|
let dir: PathBuf = Self::resolve(dir);
|
||||||
|
info!("MKDIR {}", dir.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
|
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -577,9 +628,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
FileTransferErrorType::UninitializedSession,
|
FileTransferErrorType::UninitializedSession,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
info!("Removing entry {}", fsentry.get_abs_path().display());
|
||||||
match fsentry {
|
match fsentry {
|
||||||
// Match fs entry...
|
// Match fs entry...
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
|
debug!("entry is a file; removing file");
|
||||||
// Remove file directly
|
// Remove file directly
|
||||||
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
|
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -591,9 +644,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
// Get directory files
|
// Get directory files
|
||||||
|
debug!("Entry is a directory; iterating directory entries");
|
||||||
match self.list_dir(dir.abs_path.as_path()) {
|
match self.list_dir(dir.abs_path.as_path()) {
|
||||||
Ok(files) => {
|
Ok(files) => {
|
||||||
// Remove recursively files
|
// Remove recursively files
|
||||||
|
debug!("Removing {} entries from directory...", files.len());
|
||||||
for file in files.iter() {
|
for file in files.iter() {
|
||||||
if let Err(err) = self.remove(&file) {
|
if let Err(err) = self.remove(&file) {
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
@@ -603,6 +658,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Once all files in directory have been deleted, remove directory
|
// 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()) {
|
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -625,6 +681,11 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// Rename file or a directory
|
/// Rename file or a directory
|
||||||
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
||||||
let dst: PathBuf = Self::resolve(dst);
|
let dst: PathBuf = Self::resolve(dst);
|
||||||
|
info!(
|
||||||
|
"Renaming {} to {}",
|
||||||
|
file.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => {
|
Some(stream) => {
|
||||||
// Get name
|
// Get name
|
||||||
@@ -691,6 +752,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
file_name: &Path,
|
file_name: &Path,
|
||||||
) -> Result<Box<dyn Write>, FileTransferError> {
|
) -> Result<Box<dyn Write>, FileTransferError> {
|
||||||
let file_name: PathBuf = Self::resolve(file_name);
|
let file_name: PathBuf = Self::resolve(file_name);
|
||||||
|
info!("Sending file {}", file_name.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
|
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
|
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
|
/// Receive file from remote with provided name
|
||||||
/// Returns file and its size
|
/// Returns file and its size
|
||||||
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||||
|
info!("Receiving file {}", file.abs_path.display());
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) {
|
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
|
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.
|
/// 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.
|
/// 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> {
|
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
||||||
|
info!("Finalizing put stream");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.finalize_put_stream(writable) {
|
Some(stream) => match stream.finalize_put_stream(writable) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -754,6 +818,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||||||
/// This mighe be necessary for some protocols.
|
/// This mighe be necessary for some protocols.
|
||||||
/// You must call this method each time you want to finalize the read of the remote file.
|
/// 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> {
|
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||||
|
info!("Finalizing get");
|
||||||
match &mut self.stream {
|
match &mut self.stream {
|
||||||
Some(stream) => match stream.finalize_get(readable) {
|
Some(stream) => match stream.finalize_get(readable) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
|
|||||||
@@ -319,14 +319,14 @@ impl std::string::ToString for FileTransferProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for FileTransferProtocol {
|
impl std::str::FromStr for FileTransferProtocol {
|
||||||
type Err = ();
|
type Err = String;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_ascii_uppercase().as_str() {
|
match s.to_ascii_uppercase().as_str() {
|
||||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||||
_ => Err(()),
|
_ => Err(s.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ extern crate ssh2;
|
|||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
|
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||||
use crate::utils::parser::parse_lstime;
|
use crate::utils::parser::parse_lstime;
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
@@ -90,6 +91,7 @@ impl ScpFileTransfer {
|
|||||||
lazy_static! {
|
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();
|
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
|
// Apply regex to result
|
||||||
match LS_RE.captures(line) {
|
match LS_RE.captures(line) {
|
||||||
// String matches regex
|
// String matches regex
|
||||||
@@ -167,6 +169,7 @@ impl ScpFileTransfer {
|
|||||||
};
|
};
|
||||||
// Check if file_name is '.' or '..'
|
// Check if file_name is '.' or '..'
|
||||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||||
|
debug!("File name is {}; ignoring entry", file_name);
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
// Get symlink; PATH mustn't be equal to filename
|
// Get symlink; PATH mustn't be equal to filename
|
||||||
@@ -200,6 +203,19 @@ impl ScpFileTransfer {
|
|||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_string_lossy()));
|
.map(|s| String::from(s.to_string_lossy()));
|
||||||
// Return
|
// 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
|
// Push to entries
|
||||||
Ok(match is_dir {
|
Ok(match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -262,6 +278,7 @@ impl ScpFileTransfer {
|
|||||||
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
||||||
match self.session.as_mut() {
|
match self.session.as_mut() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
|
debug!("Running command: {}", cmd);
|
||||||
// Create channel
|
// Create channel
|
||||||
let mut channel: Channel = match session.channel_session() {
|
let mut channel: Channel = match session.channel_session() {
|
||||||
Ok(ch) => ch,
|
Ok(ch) => ch,
|
||||||
@@ -285,6 +302,7 @@ impl ScpFileTransfer {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Wait close
|
// Wait close
|
||||||
let _ = channel.wait_close();
|
let _ = channel.wait_close();
|
||||||
|
debug!("Command output: {}", output);
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -312,6 +330,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<Option<String>, FileTransferError> {
|
) -> Result<Option<String>, FileTransferError> {
|
||||||
// Setup tcp stream
|
// Setup tcp stream
|
||||||
|
info!("Connecting to {}:{}", address, port);
|
||||||
let socket_addresses: Vec<SocketAddr> =
|
let socket_addresses: Vec<SocketAddr> =
|
||||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||||
Ok(s) => s.collect(),
|
Ok(s) => s.collect(),
|
||||||
@@ -325,8 +344,10 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
let mut tcp: Option<TcpStream> = None;
|
let mut tcp: Option<TcpStream> = None;
|
||||||
// Try addresses
|
// Try addresses
|
||||||
for socket_addr in socket_addresses.iter() {
|
for socket_addr in socket_addresses.iter() {
|
||||||
|
debug!("Trying socket address {}", socket_addr);
|
||||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
|
debug!("{} succeded", socket_addr);
|
||||||
tcp = Some(stream);
|
tcp = Some(stream);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -337,26 +358,30 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
let tcp: TcpStream = match tcp {
|
let tcp: TcpStream = match tcp {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
|
error!("No suitable socket address found; connection timeout");
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
String::from("Connection timeout"),
|
String::from("Connection timeout"),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Create session
|
// Create session
|
||||||
let mut session: Session = match Session::new() {
|
let mut session: Session = match Session::new() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not create session: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Set TCP stream
|
// Set TCP stream
|
||||||
session.set_tcp_stream(tcp);
|
session.set_tcp_stream(tcp);
|
||||||
// Open connection
|
// Open connection
|
||||||
|
debug!("Initializing handshake");
|
||||||
if let Err(err) = session.handshake() {
|
if let Err(err) = session.handshake() {
|
||||||
|
error!("Handshake failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -372,6 +397,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
.resolve(address.as_str(), username.as_str())
|
.resolve(address.as_str(), username.as_str())
|
||||||
{
|
{
|
||||||
Some(rsa_key) => {
|
Some(rsa_key) => {
|
||||||
|
debug!(
|
||||||
|
"Authenticating with user {} and RSA key {}",
|
||||||
|
username,
|
||||||
|
rsa_key.display()
|
||||||
|
);
|
||||||
// Authenticate with RSA key
|
// Authenticate with RSA key
|
||||||
if let Err(err) = session.userauth_pubkey_file(
|
if let Err(err) = session.userauth_pubkey_file(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
@@ -379,6 +409,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
rsa_key.as_path(),
|
rsa_key.as_path(),
|
||||||
password.as_deref(),
|
password.as_deref(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -387,10 +418,16 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Proceeed with username/password authentication
|
// 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(
|
if let Err(err) = session.userauth_password(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -400,13 +437,22 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
}
|
}
|
||||||
// Get banner
|
// Get banner
|
||||||
let banner: Option<String> = session.banner().map(String::from);
|
let banner: Option<String> = session.banner().map(String::from);
|
||||||
|
debug!(
|
||||||
|
"Connection established: {}",
|
||||||
|
banner.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
// Set session
|
// Set session
|
||||||
self.session = Some(session);
|
self.session = Some(session);
|
||||||
// Get working directory
|
// Get working directory
|
||||||
|
debug!("Getting working directory...");
|
||||||
match self.perform_shell_cmd("pwd") {
|
match self.perform_shell_cmd("pwd") {
|
||||||
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
|
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
}
|
}
|
||||||
|
info!(
|
||||||
|
"Connection established; working directory: {}",
|
||||||
|
self.wrkdir.display()
|
||||||
|
);
|
||||||
Ok(banner)
|
Ok(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +460,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Disconnect from the remote server
|
/// Disconnect from the remote server
|
||||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||||
|
info!("Disconnecting from remote...");
|
||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
||||||
@@ -447,6 +494,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
/// Print working directory
|
/// Print working directory
|
||||||
|
|
||||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||||
|
info!("PWD: {}", self.wrkdir.display());
|
||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => Ok(self.wrkdir.clone()),
|
true => Ok(self.wrkdir.clone()),
|
||||||
false => Err(FileTransferError::new(
|
false => Err(FileTransferError::new(
|
||||||
@@ -471,6 +519,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
Self::resolve(p.as_path())
|
Self::resolve(p.as_path())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
info!("Changing working directory to {}", remote_path.display());
|
||||||
// Change directory
|
// Change directory
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
p.as_path(),
|
p.as_path(),
|
||||||
@@ -484,6 +533,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
true => {
|
true => {
|
||||||
// Set working directory
|
// Set working directory
|
||||||
self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
|
self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
|
||||||
|
info!("Changed working directory to {}", self.wrkdir.display());
|
||||||
Ok(self.wrkdir.clone())
|
Ok(self.wrkdir.clone())
|
||||||
}
|
}
|
||||||
false => Err(FileTransferError::new_ex(
|
false => Err(FileTransferError::new_ex(
|
||||||
@@ -512,6 +562,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let dst: PathBuf = Self::resolve(dst);
|
let dst: PathBuf = Self::resolve(dst);
|
||||||
|
info!(
|
||||||
|
"Copying {} to {}",
|
||||||
|
src.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Run `cp -rf`
|
// Run `cp -rf`
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
@@ -555,6 +610,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
// Send ls -l to path
|
// Send ls -l to path
|
||||||
|
info!("Getting file entries in {}", path.display());
|
||||||
let path: PathBuf = Self::resolve(path);
|
let path: PathBuf = Self::resolve(path);
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
@@ -572,6 +628,11 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!(
|
||||||
|
"Found {} out of {} valid file entries",
|
||||||
|
entries.len(),
|
||||||
|
lines.len()
|
||||||
|
);
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -594,6 +655,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let dir: PathBuf = Self::resolve(dir);
|
let dir: PathBuf = Self::resolve(dir);
|
||||||
|
info!("Making directory {}", dir.display());
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
// Mkdir dir && echo 0
|
// Mkdir dir && echo 0
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
@@ -632,6 +694,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
true => {
|
true => {
|
||||||
// Get path
|
// Get path
|
||||||
let path: PathBuf = file.get_abs_path();
|
let path: PathBuf = file.get_abs_path();
|
||||||
|
info!("Removing file {}", path.display());
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
p.as_path(),
|
p.as_path(),
|
||||||
@@ -669,6 +732,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
// Get path
|
// Get path
|
||||||
let dst: PathBuf = Self::resolve(dst);
|
let dst: PathBuf = Self::resolve(dst);
|
||||||
let path: PathBuf = file.get_abs_path();
|
let path: PathBuf = file.get_abs_path();
|
||||||
|
info!("Renaming {} to {}", path.display(), dst.display());
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
p.as_path(),
|
p.as_path(),
|
||||||
@@ -717,6 +781,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
|
info!("Stat {}", path.display());
|
||||||
// make command; Directories require `-d` option
|
// make command; Directories require `-d` option
|
||||||
let cmd: String = match path.to_string_lossy().ends_with('/') {
|
let cmd: String = match path.to_string_lossy().ends_with('/') {
|
||||||
true => format!("ls -ld \"{}\"", path.display()),
|
true => format!("ls -ld \"{}\"", path.display()),
|
||||||
@@ -760,6 +825,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let p: PathBuf = self.wrkdir.clone();
|
let p: PathBuf = self.wrkdir.clone();
|
||||||
|
info!("Executing command {}", cmd);
|
||||||
match self.perform_shell_cmd_with_path(p.as_path(), cmd) {
|
match self.perform_shell_cmd_with_path(p.as_path(), cmd) {
|
||||||
Ok(output) => Ok(output),
|
Ok(output) => Ok(output),
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -788,7 +854,13 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
let file_name: PathBuf = Self::resolve(file_name);
|
let file_name: PathBuf = Self::resolve(file_name);
|
||||||
|
info!(
|
||||||
|
"Sending file {} to {}",
|
||||||
|
local.abs_path.display(),
|
||||||
|
file_name.display()
|
||||||
|
);
|
||||||
// Set blocking to true
|
// Set blocking to true
|
||||||
|
debug!("blocking channel...");
|
||||||
session.set_blocking(true);
|
session.set_blocking(true);
|
||||||
// Calculate file mode
|
// Calculate file mode
|
||||||
let mode: i32 = match local.unix_pex {
|
let mode: i32 = match local.unix_pex {
|
||||||
@@ -818,6 +890,10 @@ impl FileTransfer for ScpFileTransfer {
|
|||||||
Ok(metadata) => metadata.len(),
|
Ok(metadata) => metadata.len(),
|
||||||
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
|
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
|
// Send file
|
||||||
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
|
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
|
||||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
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> {
|
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
|
info!("Receiving file {}", file.abs_path.display());
|
||||||
// Set blocking to true
|
// Set blocking to true
|
||||||
|
debug!("Set blocking...");
|
||||||
session.set_blocking(true);
|
session.set_blocking(true);
|
||||||
match session.scp_recv(file.abs_path.as_path()) {
|
match session.scp_recv(file.abs_path.as_path()) {
|
||||||
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
|
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ extern crate ssh2;
|
|||||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
|
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp};
|
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp};
|
||||||
@@ -159,6 +160,19 @@ impl SftpFileTransfer {
|
|||||||
}
|
}
|
||||||
false => None,
|
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?
|
// Is a directory?
|
||||||
match metadata.is_dir() {
|
match metadata.is_dir() {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => FsEntry::Directory(FsDirectory {
|
||||||
@@ -205,6 +219,7 @@ impl SftpFileTransfer {
|
|||||||
match self.session.as_mut() {
|
match self.session.as_mut() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
// Create channel
|
// Create channel
|
||||||
|
debug!("Running command: {}", cmd);
|
||||||
let mut channel: Channel = match session.channel_session() {
|
let mut channel: Channel = match session.channel_session() {
|
||||||
Ok(ch) => ch,
|
Ok(ch) => ch,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -227,6 +242,7 @@ impl SftpFileTransfer {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Wait close
|
// Wait close
|
||||||
let _ = channel.wait_close();
|
let _ = channel.wait_close();
|
||||||
|
debug!("Command output: {}", output);
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -254,6 +270,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<Option<String>, FileTransferError> {
|
) -> Result<Option<String>, FileTransferError> {
|
||||||
// Setup tcp stream
|
// Setup tcp stream
|
||||||
|
info!("Connecting to {}:{}", address, port);
|
||||||
let socket_addresses: Vec<SocketAddr> =
|
let socket_addresses: Vec<SocketAddr> =
|
||||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||||
Ok(s) => s.collect(),
|
Ok(s) => s.collect(),
|
||||||
@@ -267,6 +284,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
let mut tcp: Option<TcpStream> = None;
|
let mut tcp: Option<TcpStream> = None;
|
||||||
// Try addresses
|
// Try addresses
|
||||||
for socket_addr in socket_addresses.iter() {
|
for socket_addr in socket_addresses.iter() {
|
||||||
|
debug!("Trying socket address {}", socket_addr);
|
||||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
tcp = Some(stream);
|
tcp = Some(stream);
|
||||||
@@ -279,26 +297,30 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
let tcp: TcpStream = match tcp {
|
let tcp: TcpStream = match tcp {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
|
error!("No suitable socket address found; connection timeout");
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
String::from("Connection timeout"),
|
String::from("Connection timeout"),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Create session
|
// Create session
|
||||||
let mut session: Session = match Session::new() {
|
let mut session: Session = match Session::new() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not create session: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Set TCP stream
|
// Set TCP stream
|
||||||
session.set_tcp_stream(tcp);
|
session.set_tcp_stream(tcp);
|
||||||
// Open connection
|
// Open connection
|
||||||
|
debug!("Initializing handshake");
|
||||||
if let Err(err) = session.handshake() {
|
if let Err(err) = session.handshake() {
|
||||||
|
error!("Handshake failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ConnectionError,
|
FileTransferErrorType::ConnectionError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -314,6 +336,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
.resolve(address.as_str(), username.as_str())
|
.resolve(address.as_str(), username.as_str())
|
||||||
{
|
{
|
||||||
Some(rsa_key) => {
|
Some(rsa_key) => {
|
||||||
|
debug!(
|
||||||
|
"Authenticating with user {} and RSA key {}",
|
||||||
|
username,
|
||||||
|
rsa_key.display()
|
||||||
|
);
|
||||||
// Authenticate with RSA key
|
// Authenticate with RSA key
|
||||||
if let Err(err) = session.userauth_pubkey_file(
|
if let Err(err) = session.userauth_pubkey_file(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
@@ -321,6 +348,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
rsa_key.as_path(),
|
rsa_key.as_path(),
|
||||||
password.as_deref(),
|
password.as_deref(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -329,10 +357,16 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Proceeed with username/password authentication
|
// 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(
|
if let Err(err) = session.userauth_password(
|
||||||
username.as_str(),
|
username.as_str(),
|
||||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||||
) {
|
) {
|
||||||
|
error!("Authentication failed: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::AuthenticationFailed,
|
FileTransferErrorType::AuthenticationFailed,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -343,16 +377,19 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
// Set blocking to true
|
// Set blocking to true
|
||||||
session.set_blocking(true);
|
session.set_blocking(true);
|
||||||
// Get Sftp client
|
// Get Sftp client
|
||||||
|
debug!("Getting SFTP client...");
|
||||||
let sftp: Sftp = match session.sftp() {
|
let sftp: Sftp = match session.sftp() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not get sftp client: {}", err);
|
||||||
return Err(FileTransferError::new_ex(
|
return Err(FileTransferError::new_ex(
|
||||||
FileTransferErrorType::ProtocolError,
|
FileTransferErrorType::ProtocolError,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Get working directory
|
// Get working directory
|
||||||
|
debug!("Getting working directory...");
|
||||||
self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) {
|
self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -367,6 +404,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
self.session = Some(session);
|
self.session = Some(session);
|
||||||
// Set sftp
|
// Set sftp
|
||||||
self.sftp = Some(sftp);
|
self.sftp = Some(sftp);
|
||||||
|
info!(
|
||||||
|
"Connection established: {}; working directory {}",
|
||||||
|
banner.as_deref().unwrap_or(""),
|
||||||
|
self.wrkdir.display()
|
||||||
|
);
|
||||||
Ok(banner)
|
Ok(banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +416,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Disconnect from the remote server
|
/// Disconnect from the remote server
|
||||||
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
fn disconnect(&mut self) -> Result<(), FileTransferError> {
|
||||||
|
info!("Disconnecting from remote...");
|
||||||
match self.session.as_ref() {
|
match self.session.as_ref() {
|
||||||
Some(session) => {
|
Some(session) => {
|
||||||
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
// Disconnect (greet server with 'Mandi' as they do in Friuli)
|
||||||
@@ -407,6 +450,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Print working directory
|
/// Print working directory
|
||||||
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
|
||||||
|
info!("PWD: {}", self.wrkdir.display());
|
||||||
match self.sftp {
|
match self.sftp {
|
||||||
Some(_) => Ok(self.wrkdir.clone()),
|
Some(_) => Ok(self.wrkdir.clone()),
|
||||||
None => Err(FileTransferError::new(
|
None => Err(FileTransferError::new(
|
||||||
@@ -426,6 +470,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Changed working directory to {}", self.wrkdir.display());
|
||||||
Ok(self.wrkdir.clone())
|
Ok(self.wrkdir.clone())
|
||||||
}
|
}
|
||||||
None => Err(FileTransferError::new(
|
None => Err(FileTransferError::new(
|
||||||
@@ -442,6 +487,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => {
|
true => {
|
||||||
let dst: PathBuf = self.get_abs_path(dst);
|
let dst: PathBuf = self.get_abs_path(dst);
|
||||||
|
info!(
|
||||||
|
"Copying {} to {}",
|
||||||
|
src.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Run `cp -rf`
|
// Run `cp -rf`
|
||||||
match self.perform_shell_cmd_with_path(
|
match self.perform_shell_cmd_with_path(
|
||||||
format!(
|
format!(
|
||||||
@@ -486,6 +536,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Getting file entries in {}", path.display());
|
||||||
// Get files
|
// Get files
|
||||||
match sftp.readdir(dir.as_path()) {
|
match sftp.readdir(dir.as_path()) {
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -517,6 +568,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Some(sftp) => {
|
Some(sftp) => {
|
||||||
// Make directory
|
// Make directory
|
||||||
let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path());
|
let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path());
|
||||||
|
info!("Making directory {}", path.display());
|
||||||
match sftp.mkdir(path.as_path(), 0o775) {
|
match sftp.mkdir(path.as_path(), 0o775) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => Err(FileTransferError::new_ex(
|
Err(err) => Err(FileTransferError::new_ex(
|
||||||
@@ -541,6 +593,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Match if file is a file or a directory
|
// Match if file is a file or a directory
|
||||||
|
info!("Removing file {}", file.get_abs_path().display());
|
||||||
match file {
|
match file {
|
||||||
FsEntry::File(f) => {
|
FsEntry::File(f) => {
|
||||||
// Remove file
|
// Remove file
|
||||||
@@ -554,6 +607,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
}
|
}
|
||||||
FsEntry::Directory(d) => {
|
FsEntry::Directory(d) => {
|
||||||
// Remove recursively
|
// Remove recursively
|
||||||
|
debug!("{} is a directory; removing all directory entries", d.name);
|
||||||
// Get directory files
|
// Get directory files
|
||||||
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
|
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
|
||||||
Ok(entries) => entries,
|
Ok(entries) => entries,
|
||||||
@@ -585,6 +639,11 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
FileTransferErrorType::UninitializedSession,
|
FileTransferErrorType::UninitializedSession,
|
||||||
)),
|
)),
|
||||||
Some(sftp) => {
|
Some(sftp) => {
|
||||||
|
info!(
|
||||||
|
"Moving {} to {}",
|
||||||
|
file.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Resolve destination path
|
// Resolve destination path
|
||||||
let abs_dst: PathBuf = self.get_abs_path(dst);
|
let abs_dst: PathBuf = self.get_abs_path(dst);
|
||||||
// Get abs path of entry
|
// Get abs path of entry
|
||||||
@@ -611,6 +670,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Stat file {}", dir.display());
|
||||||
// Get file
|
// Get file
|
||||||
match sftp.stat(dir.as_path()) {
|
match sftp.stat(dir.as_path()) {
|
||||||
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
|
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
|
||||||
@@ -630,6 +690,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
///
|
///
|
||||||
/// Execute a command on remote host
|
/// Execute a command on remote host
|
||||||
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
|
||||||
|
info!("Executing command {}", cmd);
|
||||||
match self.is_connected() {
|
match self.is_connected() {
|
||||||
true => match self.perform_shell_cmd_with_path(cmd) {
|
true => match self.perform_shell_cmd_with_path(cmd) {
|
||||||
Ok(output) => Ok(output),
|
Ok(output) => Ok(output),
|
||||||
@@ -660,14 +721,20 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
)),
|
)),
|
||||||
Some(sftp) => {
|
Some(sftp) => {
|
||||||
let remote_path: PathBuf = self.get_abs_path(file_name);
|
let remote_path: PathBuf = self.get_abs_path(file_name);
|
||||||
|
info!(
|
||||||
|
"Sending file {} to {}",
|
||||||
|
local.abs_path.display(),
|
||||||
|
remote_path.display()
|
||||||
|
);
|
||||||
// Calculate file mode
|
// Calculate file mode
|
||||||
let mode: i32 = match local.unix_pex {
|
let mode: i32 = match local.unix_pex {
|
||||||
None => 0o644,
|
None => 0o644,
|
||||||
Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32),
|
Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32),
|
||||||
};
|
};
|
||||||
|
debug!("File mode {:?}", mode);
|
||||||
match sftp.open_mode(
|
match sftp.open_mode(
|
||||||
remote_path.as_path(),
|
remote_path.as_path(),
|
||||||
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::APPEND | OpenFlags::TRUNCATE,
|
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
|
||||||
mode,
|
mode,
|
||||||
OpenType::File,
|
OpenType::File,
|
||||||
) {
|
) {
|
||||||
@@ -695,6 +762,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
info!("Receiving file {}", remote_path.display());
|
||||||
// Open remote file
|
// Open remote file
|
||||||
match sftp.open(remote_path.as_path()) {
|
match sftp.open(remote_path.as_path()) {
|
||||||
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
|
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
|
||||||
|
|||||||
+151
-61
@@ -125,12 +125,17 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// Instantiates a new Localhost struct
|
/// Instantiates a new Localhost struct
|
||||||
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
|
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
|
||||||
|
debug!("Initializing localhost at {}", wrkdir.display());
|
||||||
let mut host: Localhost = Localhost {
|
let mut host: Localhost = Localhost {
|
||||||
wrkdir,
|
wrkdir,
|
||||||
files: Vec::new(),
|
files: Vec::new(),
|
||||||
};
|
};
|
||||||
// Check if dir exists
|
// Check if dir exists
|
||||||
if !host.file_exists(host.wrkdir.as_path()) {
|
if !host.file_exists(host.wrkdir.as_path()) {
|
||||||
|
error!(
|
||||||
|
"Failed to initialize localhost: {} doesn't exist",
|
||||||
|
host.wrkdir.display()
|
||||||
|
);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -140,8 +145,15 @@ impl Localhost {
|
|||||||
// Retrieve files for provided path
|
// Retrieve files for provided path
|
||||||
host.files = match host.scan_dir(host.wrkdir.as_path()) {
|
host.files = match host.scan_dir(host.wrkdir.as_path()) {
|
||||||
Ok(files) => files,
|
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)
|
Ok(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +177,10 @@ impl Localhost {
|
|||||||
/// Change working directory with the new provided directory
|
/// Change working directory with the new provided directory
|
||||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||||
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
||||||
|
info!("Changing localhost directory to {}...", new_dir.display());
|
||||||
// Check whether directory exists
|
// Check whether directory exists
|
||||||
if !self.file_exists(new_dir.as_path()) {
|
if !self.file_exists(new_dir.as_path()) {
|
||||||
|
error!("Could not change directory: No such file or directory");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -174,10 +188,11 @@ impl Localhost {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Change directory
|
// 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(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
Some(err),
|
||||||
new_dir.as_path(),
|
new_dir.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -189,11 +204,13 @@ impl Localhost {
|
|||||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||||
Ok(files) => files,
|
Ok(files) => files,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not scan new directory: {}", err);
|
||||||
// Restore directory
|
// Restore directory
|
||||||
self.wrkdir = prev_dir;
|
self.wrkdir = prev_dir;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
debug!("Changed directory to {}", self.wrkdir.display());
|
||||||
Ok(self.wrkdir.clone())
|
Ok(self.wrkdir.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +227,7 @@ impl Localhost {
|
|||||||
/// ignex: don't report error if directory already exists
|
/// ignex: don't report error if directory already exists
|
||||||
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
||||||
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
||||||
|
info!("Making directory {}", dir_path.display());
|
||||||
// If dir already exists, return Error
|
// If dir already exists, return Error
|
||||||
if dir_path.exists() {
|
if dir_path.exists() {
|
||||||
match ignex {
|
match ignex {
|
||||||
@@ -229,13 +247,17 @@ impl Localhost {
|
|||||||
if dir_name.is_relative() {
|
if dir_name.is_relative() {
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
}
|
}
|
||||||
|
info!("Created directory {}", dir_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::CouldNotCreateFile,
|
error!("Could not make directory: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
dir_path.as_path(),
|
HostErrorType::CouldNotCreateFile,
|
||||||
)),
|
Some(err),
|
||||||
|
dir_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +268,9 @@ impl Localhost {
|
|||||||
match entry {
|
match entry {
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
// If file doesn't exist; return error
|
// If file doesn't exist; return error
|
||||||
|
debug!("Removing directory {}", dir.abs_path.display());
|
||||||
if !dir.abs_path.as_path().exists() {
|
if !dir.abs_path.as_path().exists() {
|
||||||
|
error!("Directory doesn't exist");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -258,18 +282,24 @@ impl Localhost {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Update dir
|
// Update dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
|
info!("Removed directory {}", dir.abs_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::DeleteFailed,
|
error!("Could not remove directory: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
dir.abs_path.as_path(),
|
HostErrorType::DeleteFailed,
|
||||||
)),
|
Some(err),
|
||||||
|
dir.abs_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
// If file doesn't exist; return error
|
// If file doesn't exist; return error
|
||||||
|
debug!("Removing file {}", file.abs_path.display());
|
||||||
if !file.abs_path.as_path().exists() {
|
if !file.abs_path.as_path().exists() {
|
||||||
|
error!("File doesn't exist");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -281,13 +311,17 @@ impl Localhost {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Update dir
|
// Update dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
|
info!("Removed file {}", file.abs_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::DeleteFailed,
|
error!("Could not remove file: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
file.abs_path.as_path(),
|
HostErrorType::DeleteFailed,
|
||||||
)),
|
Some(err),
|
||||||
|
file.abs_path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,13 +336,26 @@ impl Localhost {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Scan dir
|
// Scan dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
|
debug!(
|
||||||
|
"Moved file {} to {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dst_path.display()
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::CouldNotCreateFile,
|
error!(
|
||||||
Some(err),
|
"Failed to move {} to {}: {}",
|
||||||
abs_path.as_path(),
|
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> {
|
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
|
||||||
// Get absolute path of dest
|
// Get absolute path of dest
|
||||||
let dst: PathBuf = self.to_abs_path(dst);
|
let dst: PathBuf = self.to_abs_path(dst);
|
||||||
|
info!(
|
||||||
|
"Copying file {} to {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => {
|
FsEntry::File(file) => {
|
||||||
@@ -333,16 +385,19 @@ impl Localhost {
|
|||||||
};
|
};
|
||||||
// Copy entry path to dst path
|
// Copy entry path to dst path
|
||||||
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_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(
|
return Err(HostError::new(
|
||||||
HostErrorType::CouldNotCreateFile,
|
HostErrorType::CouldNotCreateFile,
|
||||||
Some(err),
|
Some(err),
|
||||||
file.abs_path.as_path(),
|
file.abs_path.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
info!("File copied");
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
FsEntry::Directory(dir) => {
|
||||||
// If destination path doesn't exist, create destination
|
// If destination path doesn't exist, create destination
|
||||||
if !dst.exists() {
|
if !dst.exists() {
|
||||||
|
debug!("Directory {} doesn't exist; creating it", dst.display());
|
||||||
self.mkdir(dst.as_path())?;
|
self.mkdir(dst.as_path())?;
|
||||||
}
|
}
|
||||||
// Scan dir
|
// Scan dir
|
||||||
@@ -386,15 +441,17 @@ impl Localhost {
|
|||||||
/// Stat file and create a FsEntry
|
/// Stat file and create a FsEntry
|
||||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||||
|
info!("Stating file {}", path.display());
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_abs_path(path);
|
||||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not read file metadata: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::FileNotAccessible,
|
HostErrorType::FileNotAccessible,
|
||||||
Some(err),
|
Some(err),
|
||||||
path.as_path(),
|
path.as_path(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||||
@@ -454,14 +511,16 @@ impl Localhost {
|
|||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_abs_path(path);
|
||||||
|
info!("Stating file {}", path.display());
|
||||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
error!("Could not read file metadata: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::FileNotAccessible,
|
HostErrorType::FileNotAccessible,
|
||||||
Some(err),
|
Some(err),
|
||||||
path.as_path(),
|
path.as_path(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
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 args: Vec<&str> = cmd.split(' ').collect();
|
||||||
let cmd: &str = args.first().unwrap();
|
let cmd: &str = args.first().unwrap();
|
||||||
let argv: &[&str] = &args[1..];
|
let argv: &[&str] = &args[1..];
|
||||||
|
info!("Executing command: {} {:?}", cmd, argv);
|
||||||
match std::process::Command::new(cmd).args(argv).output() {
|
match std::process::Command::new(cmd).args(argv).output() {
|
||||||
Ok(output) => match std::str::from_utf8(&output.stdout) {
|
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(_) => Ok(String::new()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::ExecutionFailed,
|
error!("Failed to run command: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
self.wrkdir.as_path(),
|
HostErrorType::ExecutionFailed,
|
||||||
)),
|
Some(err),
|
||||||
|
self.wrkdir.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,19 +614,32 @@ impl Localhost {
|
|||||||
let mut mpex = metadata.permissions();
|
let mut mpex = metadata.permissions();
|
||||||
mpex.set_mode(self.mode_to_u32(pex));
|
mpex.set_mode(self.mode_to_u32(pex));
|
||||||
match set_permissions(path.as_path(), mpex) {
|
match set_permissions(path.as_path(), mpex) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => {
|
||||||
Err(err) => Err(HostError::new(
|
info!("Changed mode for {} to {:?}", path.display(), pex);
|
||||||
HostErrorType::FileNotAccessible,
|
Ok(())
|
||||||
Some(err),
|
}
|
||||||
path.as_path(),
|
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(
|
Err(err) => {
|
||||||
HostErrorType::FileNotAccessible,
|
error!(
|
||||||
Some(err),
|
"Chmod failed; could not read metadata for file {}: {}",
|
||||||
path.as_path(),
|
path.display(),
|
||||||
)),
|
err
|
||||||
|
);
|
||||||
|
Err(HostError::new(
|
||||||
|
HostErrorType::FileNotAccessible,
|
||||||
|
Some(err),
|
||||||
|
path.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,7 +648,9 @@ impl Localhost {
|
|||||||
/// Open file for read
|
/// Open file for read
|
||||||
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
||||||
let file: PathBuf = self.to_abs_path(file);
|
let file: PathBuf = self.to_abs_path(file);
|
||||||
|
info!("Opening file {} for read", file.display());
|
||||||
if !self.file_exists(file.as_path()) {
|
if !self.file_exists(file.as_path()) {
|
||||||
|
error!("File doesn't exist!");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
@@ -583,11 +664,14 @@ impl Localhost {
|
|||||||
.open(file.as_path())
|
.open(file.as_path())
|
||||||
{
|
{
|
||||||
Ok(f) => Ok(f),
|
Ok(f) => Ok(f),
|
||||||
Err(err) => Err(HostError::new(
|
Err(err) => {
|
||||||
HostErrorType::FileNotAccessible,
|
error!("Could not open file for read: {}", err);
|
||||||
Some(err),
|
Err(HostError::new(
|
||||||
file.as_path(),
|
HostErrorType::FileNotAccessible,
|
||||||
)),
|
Some(err),
|
||||||
|
file.as_path(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +680,7 @@ impl Localhost {
|
|||||||
/// Open file for write
|
/// Open file for write
|
||||||
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
||||||
let file: PathBuf = self.to_abs_path(file);
|
let file: PathBuf = self.to_abs_path(file);
|
||||||
|
info!("Opening file {} for write", file.display());
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -603,18 +688,21 @@ impl Localhost {
|
|||||||
.open(file.as_path())
|
.open(file.as_path())
|
||||||
{
|
{
|
||||||
Ok(f) => Ok(f),
|
Ok(f) => Ok(f),
|
||||||
Err(err) => match self.file_exists(file.as_path()) {
|
Err(err) => {
|
||||||
true => Err(HostError::new(
|
error!("Failed to open file: {}", err);
|
||||||
HostErrorType::ReadonlyFile,
|
match self.file_exists(file.as_path()) {
|
||||||
Some(err),
|
true => Err(HostError::new(
|
||||||
file.as_path(),
|
HostErrorType::ReadonlyFile,
|
||||||
)),
|
Some(err),
|
||||||
false => Err(HostError::new(
|
file.as_path(),
|
||||||
HostErrorType::FileNotAccessible,
|
)),
|
||||||
Some(err),
|
false => Err(HostError::new(
|
||||||
file.as_path(),
|
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
|
/// Get content of the current directory as a list of fs entry
|
||||||
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
|
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
|
||||||
|
info!("Reading directory {}", dir.display());
|
||||||
match std::fs::read_dir(dir) {
|
match std::fs::read_dir(dir) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
let mut fs_entries: Vec<FsEntry> = Vec::new();
|
let mut fs_entries: Vec<FsEntry> = Vec::new();
|
||||||
for entry in e.flatten() {
|
for entry in e.flatten() {
|
||||||
// NOTE: 0.4.1, don't fail if stat for one file fails
|
// NOTE: 0.4.1, don't fail if stat for one file fails
|
||||||
if let Ok(entry) = self.stat(entry.path().as_path()) {
|
match self.stat(entry.path().as_path()) {
|
||||||
fs_entries.push(entry);
|
Ok(entry) => fs_entries.push(entry),
|
||||||
|
Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(fs_entries)
|
Ok(fs_entries)
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ extern crate bitflags;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
|
|
||||||
pub mod activity_manager;
|
pub mod activity_manager;
|
||||||
|
|||||||
+23
@@ -32,6 +32,8 @@ extern crate bitflags;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ mod utils;
|
|||||||
// namespaces
|
// namespaces
|
||||||
use activity_manager::{ActivityManager, NextActivity};
|
use activity_manager::{ActivityManager, NextActivity};
|
||||||
use filetransfer::FileTransferProtocol;
|
use filetransfer::FileTransferProtocol;
|
||||||
|
use system::logging;
|
||||||
|
|
||||||
/// ### print_usage
|
/// ### print_usage
|
||||||
///
|
///
|
||||||
@@ -79,10 +82,12 @@ fn main() {
|
|||||||
let mut remote_wrkdir: Option<PathBuf> = None;
|
let mut remote_wrkdir: Option<PathBuf> = None;
|
||||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
||||||
let mut ticks: Duration = Duration::from_millis(10);
|
let mut ticks: Duration = Duration::from_millis(10);
|
||||||
|
let mut log_enabled: bool = true;
|
||||||
//Process options
|
//Process options
|
||||||
let mut opts = Options::new();
|
let mut opts = Options::new();
|
||||||
opts.optopt("P", "password", "Provide password from CLI", "<password>");
|
opts.optopt("P", "password", "Provide password from CLI", "<password>");
|
||||||
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
|
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
|
||||||
|
opts.optflag("q", "quiet", "Disable logging");
|
||||||
opts.optflag("v", "version", "");
|
opts.optflag("v", "version", "");
|
||||||
opts.optflag("h", "help", "Print this menu");
|
opts.optflag("h", "help", "Print this menu");
|
||||||
let matches = match opts.parse(&args[1..]) {
|
let matches = match opts.parse(&args[1..]) {
|
||||||
@@ -105,6 +110,10 @@ fn main() {
|
|||||||
);
|
);
|
||||||
std::process::exit(255);
|
std::process::exit(255);
|
||||||
}
|
}
|
||||||
|
// Logging
|
||||||
|
if matches.opt_present("q") {
|
||||||
|
log_enabled = false;
|
||||||
|
}
|
||||||
// Match password
|
// Match password
|
||||||
if let Some(passwd) = matches.opt_str("P") {
|
if let Some(passwd) = matches.opt_str("P") {
|
||||||
password = Some(passwd);
|
password = Some(passwd);
|
||||||
@@ -155,9 +164,17 @@ fn main() {
|
|||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
Err(_) => PathBuf::from("/"),
|
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
|
// Initialize client if necessary
|
||||||
let mut start_activity: NextActivity = NextActivity::Authentication;
|
let mut start_activity: NextActivity = NextActivity::Authentication;
|
||||||
if address.is_some() {
|
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() {
|
if password.is_none() {
|
||||||
// Ask password if unspecified
|
// Ask password if unspecified
|
||||||
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||||
@@ -173,6 +190,10 @@ fn main() {
|
|||||||
std::process::exit(255);
|
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
|
// In this case the first activity will be FileTransfer
|
||||||
start_activity = NextActivity::FileTransfer;
|
start_activity = NextActivity::FileTransfer;
|
||||||
@@ -190,7 +211,9 @@ fn main() {
|
|||||||
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
||||||
}
|
}
|
||||||
// Run
|
// Run
|
||||||
|
info!("Starting activity manager...");
|
||||||
manager.run(start_activity);
|
manager.run(start_activity);
|
||||||
|
info!("termscp terminated");
|
||||||
// Then return
|
// Then return
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,11 @@ impl BookmarksClient {
|
|||||||
) -> Result<BookmarksClient, SerializerError> {
|
) -> Result<BookmarksClient, SerializerError> {
|
||||||
// Create default hosts
|
// Create default hosts
|
||||||
let default_hosts: UserHosts = Default::default();
|
let default_hosts: UserHosts = Default::default();
|
||||||
|
debug!("Setting up bookmarks client...");
|
||||||
// Make a key storage (windows / macos)
|
// Make a key storage (windows / macos)
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||||
|
debug!("Setting up KeyStorage");
|
||||||
let username: String = whoami::username();
|
let username: String = whoami::username();
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||||
// Check if keyring storage is supported
|
// Check if keyring storage is supported
|
||||||
@@ -79,8 +81,14 @@ impl BookmarksClient {
|
|||||||
#[cfg(test)] // NOTE: when running test, add -test
|
#[cfg(test)] // NOTE: when running test, add -test
|
||||||
let app_name: &str = "termscp-test";
|
let app_name: &str = "termscp-test";
|
||||||
match storage.is_supported() {
|
match storage.is_supported() {
|
||||||
true => (Box::new(storage), app_name),
|
true => {
|
||||||
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
|
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)
|
// Make a key storage (linux / unix)
|
||||||
@@ -90,16 +98,22 @@ impl BookmarksClient {
|
|||||||
let app_name: &str = "bookmarks";
|
let app_name: &str = "bookmarks";
|
||||||
#[cfg(test)] // NOTE: when running test, add -test
|
#[cfg(test)] // NOTE: when running test, add -test
|
||||||
let app_name: &str = "bookmarks-test";
|
let app_name: &str = "bookmarks-test";
|
||||||
|
debug!("Using FileStorage");
|
||||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||||
};
|
};
|
||||||
// Load key
|
// Load key
|
||||||
let key: String = match key_storage.get_key(service_id) {
|
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 {
|
Err(e) => match e {
|
||||||
KeyStorageError::NoSuchKey => {
|
KeyStorageError::NoSuchKey => {
|
||||||
// If no such key, generate key and set it into the storage
|
// If no such key, generate key and set it into the storage
|
||||||
let key: String = Self::generate_key();
|
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()) {
|
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(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::IoError,
|
SerializerErrorKind::IoError,
|
||||||
format!("Could not write key to storage: {}", e),
|
format!("Could not write key to storage: {}", e),
|
||||||
@@ -109,10 +123,11 @@ impl BookmarksClient {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
error!("Failed to get key from storage: {}", e);
|
||||||
return Err(SerializerError::new_ex(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::IoError,
|
SerializerErrorKind::IoError,
|
||||||
format!("Could not get key from storage: {}", e),
|
format!("Could not get key from storage: {}", e),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -124,15 +139,19 @@ impl BookmarksClient {
|
|||||||
};
|
};
|
||||||
// If bookmark file doesn't exist, initialize it
|
// If bookmark file doesn't exist, initialize it
|
||||||
if !bookmarks_file.exists() {
|
if !bookmarks_file.exists() {
|
||||||
|
info!("Bookmarks file doesn't exist yet; creating it...");
|
||||||
if let Err(err) = client.write_bookmarks() {
|
if let Err(err) = client.write_bookmarks() {
|
||||||
|
error!("Failed to create bookmarks file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Load bookmarks from file
|
// Load bookmarks from file
|
||||||
if let Err(err) = client.read_bookmarks() {
|
if let Err(err) = client.read_bookmarks() {
|
||||||
|
error!("Failed to load bookmarks: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!("Bookmarks client initialized");
|
||||||
// Load key
|
// Load key
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
@@ -152,19 +171,29 @@ impl BookmarksClient {
|
|||||||
key: &str,
|
key: &str,
|
||||||
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
||||||
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
||||||
|
debug!("Getting bookmark {}", key);
|
||||||
Some((
|
Some((
|
||||||
entry.address.clone(),
|
entry.address.clone(),
|
||||||
entry.port,
|
entry.port,
|
||||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||||
Ok(proto) => proto,
|
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(),
|
entry.username.clone(),
|
||||||
match &entry.password {
|
match &entry.password {
|
||||||
// Decrypted password if Some; if decryption fails return None
|
// Decrypted password if Some; if decryption fails return None
|
||||||
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
||||||
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
||||||
Err(_) => None,
|
Err(err) => {
|
||||||
|
error!("Failed to decrypt password for bookmark: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
@@ -184,9 +213,11 @@ impl BookmarksClient {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) {
|
) {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
|
error!("Fatal error; bookmark name is empty");
|
||||||
panic!("Bookmark name can't be empty");
|
panic!("Bookmark name can't be empty");
|
||||||
}
|
}
|
||||||
// Make bookmark
|
// Make bookmark
|
||||||
|
info!("Added bookmark {} with address {}", name, addr);
|
||||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
||||||
self.hosts.bookmarks.insert(name, host);
|
self.hosts.bookmarks.insert(name, host);
|
||||||
}
|
}
|
||||||
@@ -196,6 +227,7 @@ impl BookmarksClient {
|
|||||||
/// Delete entry from bookmarks
|
/// Delete entry from bookmarks
|
||||||
pub fn del_bookmark(&mut self, name: &str) {
|
pub fn del_bookmark(&mut self, name: &str) {
|
||||||
let _ = self.hosts.bookmarks.remove(name);
|
let _ = self.hosts.bookmarks.remove(name);
|
||||||
|
info!("Removed bookmark {}", name);
|
||||||
}
|
}
|
||||||
/// ### iter_recents
|
/// ### iter_recents
|
||||||
///
|
///
|
||||||
@@ -209,13 +241,20 @@ impl BookmarksClient {
|
|||||||
/// Get recent associated to key
|
/// Get recent associated to key
|
||||||
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
||||||
// NOTE: password is not decrypted; recents will never have password
|
// NOTE: password is not decrypted; recents will never have password
|
||||||
|
info!("Getting bookmark {}", key);
|
||||||
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
||||||
Some((
|
Some((
|
||||||
entry.address.clone(),
|
entry.address.clone(),
|
||||||
entry.port,
|
entry.port,
|
||||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||||
Ok(proto) => proto,
|
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(),
|
entry.username.clone(),
|
||||||
))
|
))
|
||||||
@@ -236,6 +275,7 @@ impl BookmarksClient {
|
|||||||
// Check if duplicated
|
// Check if duplicated
|
||||||
for recent_host in self.hosts.recents.values() {
|
for recent_host in self.hosts.recents.values() {
|
||||||
if *recent_host == host {
|
if *recent_host == host {
|
||||||
|
debug!("Discarding recent since duplicated ({})", host.address);
|
||||||
// Don't save duplicates
|
// Don't save duplicates
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -252,6 +292,7 @@ impl BookmarksClient {
|
|||||||
// Delete keys starting from the last one
|
// Delete keys starting from the last one
|
||||||
for key in keys.iter() {
|
for key in keys.iter() {
|
||||||
let _ = self.hosts.recents.remove(key);
|
let _ = self.hosts.recents.remove(key);
|
||||||
|
debug!("Removed recent bookmark {}", key);
|
||||||
// If length is < self.recents_size; break
|
// If length is < self.recents_size; break
|
||||||
if self.hosts.recents.len() < self.recents_size {
|
if self.hosts.recents.len() < self.recents_size {
|
||||||
break;
|
break;
|
||||||
@@ -259,6 +300,7 @@ impl BookmarksClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
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);
|
self.hosts.recents.insert(name, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +309,7 @@ impl BookmarksClient {
|
|||||||
/// Delete entry from recents
|
/// Delete entry from recents
|
||||||
pub fn del_recent(&mut self, name: &str) {
|
pub fn del_recent(&mut self, name: &str) {
|
||||||
let _ = self.hosts.recents.remove(name);
|
let _ = self.hosts.recents.remove(name);
|
||||||
|
info!("Removed recent host {}", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### write_bookmarks
|
/// ### write_bookmarks
|
||||||
@@ -274,6 +317,7 @@ impl BookmarksClient {
|
|||||||
/// Write bookmarks to file
|
/// Write bookmarks to file
|
||||||
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
||||||
// Open file
|
// Open file
|
||||||
|
debug!("Writing bookmarks");
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -284,10 +328,13 @@ impl BookmarksClient {
|
|||||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||||
serializer.serialize(Box::new(writer), &self.hosts)
|
serializer.serialize(Box::new(writer), &self.hosts)
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to write bookmarks: {}", err);
|
||||||
err.to_string(),
|
Err(SerializerError::new_ex(
|
||||||
)),
|
SerializerErrorKind::IoError,
|
||||||
|
err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +343,7 @@ impl BookmarksClient {
|
|||||||
/// Read bookmarks from file
|
/// Read bookmarks from file
|
||||||
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
||||||
// Open bookmarks file for read
|
// Open bookmarks file for read
|
||||||
|
debug!("Reading bookmarks");
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.open(self.bookmarks_file.as_path())
|
.open(self.bookmarks_file.as_path())
|
||||||
@@ -311,10 +359,13 @@ impl BookmarksClient {
|
|||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to read bookmarks: {}", err);
|
||||||
err.to_string(),
|
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> {
|
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
|
||||||
// Initialize a default configuration
|
// Initialize a default configuration
|
||||||
let default_config: UserConfig = UserConfig::default();
|
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
|
// Create client
|
||||||
let mut client: ConfigClient = ConfigClient {
|
let mut client: ConfigClient = ConfigClient {
|
||||||
config: default_config,
|
config: default_config,
|
||||||
@@ -67,6 +72,7 @@ impl ConfigClient {
|
|||||||
// If ssh key directory doesn't exist, create it
|
// If ssh key directory doesn't exist, create it
|
||||||
if !ssh_key_dir.exists() {
|
if !ssh_key_dir.exists() {
|
||||||
if let Err(err) = create_dir(ssh_key_dir) {
|
if let Err(err) = create_dir(ssh_key_dir) {
|
||||||
|
error!("Failed to create SSH key dir: {}", err);
|
||||||
return Err(SerializerError::new_ex(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::IoError,
|
SerializerErrorKind::IoError,
|
||||||
format!(
|
format!(
|
||||||
@@ -76,17 +82,22 @@ impl ConfigClient {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
debug!("Created SSH key directory");
|
||||||
}
|
}
|
||||||
// If Config file doesn't exist, create it
|
// If Config file doesn't exist, create it
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
if let Err(err) = client.write_config() {
|
if let Err(err) = client.write_config() {
|
||||||
|
error!("Couldn't create configuration file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
debug!("Config file didn't exist; created file");
|
||||||
} else {
|
} else {
|
||||||
// otherwise Load configuration from file
|
// otherwise Load configuration from file
|
||||||
if let Err(err) = client.read_config() {
|
if let Err(err) = client.read_config() {
|
||||||
|
error!("Couldn't read configuration file: {}", err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
debug!("Read configuration file");
|
||||||
}
|
}
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
@@ -230,12 +241,18 @@ impl ConfigClient {
|
|||||||
p.push(format!("{}.key", host_name));
|
p.push(format!("{}.key", host_name));
|
||||||
p
|
p
|
||||||
};
|
};
|
||||||
|
info!(
|
||||||
|
"Writing SSH file to {} for host {}",
|
||||||
|
ssh_key_path.display(),
|
||||||
|
host_name
|
||||||
|
);
|
||||||
// Write key to file
|
// Write key to file
|
||||||
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(err) => return Self::make_io_err(err),
|
Err(err) => return Self::make_io_err(err),
|
||||||
};
|
};
|
||||||
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
|
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);
|
return Self::make_io_err(err);
|
||||||
}
|
}
|
||||||
// Add host to keys
|
// Add host to keys
|
||||||
@@ -251,6 +268,7 @@ impl ConfigClient {
|
|||||||
/// and also commits changes to configuration, to prevent incoerent data
|
/// and also commits changes to configuration, to prevent incoerent data
|
||||||
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
||||||
// Remove key from configuration and get key path
|
// Remove key from configuration and get key path
|
||||||
|
info!("Removing key for {}@{}", host, username);
|
||||||
let key_path: PathBuf = match self
|
let key_path: PathBuf = match self
|
||||||
.config
|
.config
|
||||||
.remote
|
.remote
|
||||||
@@ -262,6 +280,7 @@ impl ConfigClient {
|
|||||||
};
|
};
|
||||||
// Remove file
|
// Remove file
|
||||||
if let Err(err) = remove_file(key_path.as_path()) {
|
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);
|
return Self::make_io_err(err);
|
||||||
}
|
}
|
||||||
// Commit changes to configuration
|
// Commit changes to configuration
|
||||||
@@ -310,10 +329,13 @@ impl ConfigClient {
|
|||||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||||
serializer.serialize(Box::new(writer), &self.config)
|
serializer.serialize(Box::new(writer), &self.config)
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to write configuration file: {}", err);
|
||||||
err.to_string(),
|
Err(SerializerError::new_ex(
|
||||||
)),
|
SerializerErrorKind::IoError,
|
||||||
|
err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,10 +359,13 @@ impl ConfigClient {
|
|||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(SerializerError::new_ex(
|
Err(err) => {
|
||||||
SerializerErrorKind::IoError,
|
error!("Failed to read configuration: {}", err);
|
||||||
err.to_string(),
|
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)
|
(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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 config_client;
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
pub(crate) mod keys;
|
pub(crate) mod keys;
|
||||||
|
pub mod logging;
|
||||||
pub mod sshkey_storage;
|
pub mod sshkey_storage;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ impl SshKeyStorage {
|
|||||||
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
||||||
let mut hosts: HashMap<String, PathBuf> =
|
let mut hosts: HashMap<String, PathBuf> =
|
||||||
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
||||||
|
debug!("Setting up SSH key storage");
|
||||||
// Iterate over keys
|
// Iterate over keys
|
||||||
for key in cfg_client.iter_ssh_keys() {
|
for key in cfg_client.iter_ssh_keys() {
|
||||||
match cfg_client.get_ssh_key(key) {
|
match cfg_client.get_ssh_key(key) {
|
||||||
@@ -52,8 +53,12 @@ impl SshKeyStorage {
|
|||||||
}
|
}
|
||||||
None => continue,
|
None => continue,
|
||||||
},
|
},
|
||||||
Err(_) => continue,
|
Err(err) => {
|
||||||
|
error!("Failed to get SSH key for {}: {}", key, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
info!("Got SSH key for {}", key);
|
||||||
}
|
}
|
||||||
// Return storage
|
// Return storage
|
||||||
SshKeyStorage { hosts }
|
SshKeyStorage { hosts }
|
||||||
|
|||||||
@@ -108,17 +108,24 @@ impl AuthActivity {
|
|||||||
///
|
///
|
||||||
/// If enabled in configuration, check for updates from Github
|
/// If enabled in configuration, check for updates from Github
|
||||||
fn check_for_updates(&mut self) {
|
fn check_for_updates(&mut self) {
|
||||||
|
debug!("Check for updates...");
|
||||||
// Check version only if unset in the store
|
// Check version only if unset in the store
|
||||||
let ctx: &Context = self.context.as_ref().unwrap();
|
let ctx: &Context = self.context.as_ref().unwrap();
|
||||||
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
|
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() {
|
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
|
||||||
Some(client) => {
|
Some(client) => {
|
||||||
if client.get_check_for_updates() {
|
if client.get_check_for_updates() {
|
||||||
|
debug!("Check for updates is enabled");
|
||||||
// Send request
|
// Send request
|
||||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||||
Ok(version) => version,
|
Ok(version) => {
|
||||||
|
info!("Latest version is: {:?}", version);
|
||||||
|
version
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report error
|
// Report error
|
||||||
|
error!("Failed to get latest version: {}", err);
|
||||||
self.mount_error(
|
self.mount_error(
|
||||||
format!("Could not check for new updates: {}", err).as_str(),
|
format!("Could not check for new updates: {}", err).as_str(),
|
||||||
);
|
);
|
||||||
@@ -127,6 +134,7 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
info!("Check for updates is disabled");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +157,7 @@ impl Activity for AuthActivity {
|
|||||||
/// `on_create` must initialize all the data structures used by the activity
|
/// `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
|
/// Context is taken from activity manager and will be released only when activity is destroyed
|
||||||
fn on_create(&mut self, mut context: Context) {
|
fn on_create(&mut self, mut context: Context) {
|
||||||
|
debug!("Initializing activity");
|
||||||
// Initialize file transfer params
|
// Initialize file transfer params
|
||||||
context.ft_params = Some(FileTransferParams::default());
|
context.ft_params = Some(FileTransferParams::default());
|
||||||
// Set context
|
// Set context
|
||||||
@@ -156,7 +165,9 @@ impl Activity for AuthActivity {
|
|||||||
// Clear terminal
|
// Clear terminal
|
||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// 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
|
// Init bookmarks client
|
||||||
if self.bookmarks_client.is_none() {
|
if self.bookmarks_client.is_none() {
|
||||||
self.init_bookmarks_client();
|
self.init_bookmarks_client();
|
||||||
@@ -169,6 +180,7 @@ impl Activity for AuthActivity {
|
|||||||
self.check_for_updates();
|
self.check_for_updates();
|
||||||
// Initialize view
|
// Initialize view
|
||||||
self.init();
|
self.init();
|
||||||
|
info!("Activity initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on_draw
|
/// ### on_draw
|
||||||
@@ -213,7 +225,9 @@ impl Activity for AuthActivity {
|
|||||||
/// This function finally releases the context
|
/// This function finally releases the context
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Disable raw mode
|
// 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()?;
|
self.context.as_ref()?;
|
||||||
// Clear terminal and return
|
// Clear terminal and return
|
||||||
match self.context.take() {
|
match self.context.take() {
|
||||||
|
|||||||
@@ -271,7 +271,9 @@ impl AuthActivity {
|
|||||||
Some(Payload::One(Value::Usize(0)))
|
Some(Payload::One(Value::Usize(0)))
|
||||||
);
|
);
|
||||||
// Save bookmark
|
// Save bookmark
|
||||||
self.save_bookmark(bookmark_name, save_pwd);
|
if !bookmark_name.is_empty() {
|
||||||
|
self.save_bookmark(bookmark_name, save_pwd);
|
||||||
|
}
|
||||||
// Umount popup
|
// Umount popup
|
||||||
self.umount_bookmark_save_dialog();
|
self.umount_bookmark_save_dialog();
|
||||||
// Reload bookmarks
|
// Reload bookmarks
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Add message to log events
|
/// Add message to log events
|
||||||
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
|
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
|
// Create log record
|
||||||
let record: LogRecord = LogRecord::new(level, msg);
|
let record: LogRecord = LogRecord::new(level, msg);
|
||||||
//Check if history overflows the size
|
//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` is the function which must be called to initialize the activity.
|
||||||
/// `on_create` must initialize all the data structures used by the activity
|
/// `on_create` must initialize all the data structures used by the activity
|
||||||
fn on_create(&mut self, context: Context) {
|
fn on_create(&mut self, context: Context) {
|
||||||
|
debug!("Initializing activity...");
|
||||||
// Set context
|
// Set context
|
||||||
self.context = Some(context);
|
self.context = Some(context);
|
||||||
// Clear terminal
|
// Clear terminal
|
||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// 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
|
// Set working directory
|
||||||
let pwd: PathBuf = self.host.pwd();
|
let pwd: PathBuf = self.host.pwd();
|
||||||
// Get files at current wd
|
// Get files at current wd
|
||||||
self.local_scan(pwd.as_path());
|
self.local_scan(pwd.as_path());
|
||||||
self.local_mut().wrkdir = pwd;
|
self.local_mut().wrkdir = pwd;
|
||||||
|
debug!("Read working directory");
|
||||||
// Configure text editor
|
// Configure text editor
|
||||||
self.setup_text_editor();
|
self.setup_text_editor();
|
||||||
|
debug!("Setup text editor");
|
||||||
// init view
|
// init view
|
||||||
self.init();
|
self.init();
|
||||||
|
debug!("Initialized view");
|
||||||
// Verify error state from context
|
// Verify error state from context
|
||||||
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
if let Some(err) = self.context.as_mut().unwrap().get_error() {
|
||||||
|
error!("Fatal error on create: {}", err);
|
||||||
self.mount_fatal(&err);
|
self.mount_fatal(&err);
|
||||||
}
|
}
|
||||||
|
info!("Created FileTransferActivity");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### on_draw
|
/// ### 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)
|
// 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() {
|
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
||||||
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
|
let 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);
|
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
|
||||||
// Set init state to connecting popup
|
// Set init state to connecting popup
|
||||||
self.mount_wait(msg.as_str());
|
self.mount_wait(msg.as_str());
|
||||||
@@ -341,7 +353,9 @@ impl Activity for FileTransferActivity {
|
|||||||
/// This function must be called once before terminating the activity.
|
/// This function must be called once before terminating the activity.
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Disable raw mode
|
// Disable raw mode
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
// Disconnect client
|
// Disconnect client
|
||||||
if self.client.is_connected() {
|
if self.client.is_connected() {
|
||||||
let _ = self.client.disconnect();
|
let _ = self.client.disconnect();
|
||||||
|
|||||||
@@ -783,7 +783,9 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Put input mode back to normal
|
// 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
|
// Leave alternate mode
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
if let Some(ctx) = self.context.as_mut() {
|
||||||
ctx.leave_alternate_screen();
|
ctx.leave_alternate_screen();
|
||||||
|
|||||||
@@ -111,13 +111,17 @@ impl SetupActivity {
|
|||||||
env::set_var("EDITOR", cli.get_text_editor());
|
env::set_var("EDITOR", cli.get_text_editor());
|
||||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||||
// Put input mode back to normal
|
// 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
|
// Leave alternate mode
|
||||||
if let Some(ctx) = self.context.as_mut() {
|
if let Some(ctx) = self.context.as_mut() {
|
||||||
ctx.leave_alternate_screen();
|
ctx.leave_alternate_screen();
|
||||||
}
|
}
|
||||||
// Re-enable raw mode
|
// 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
|
// Write key to file
|
||||||
match edit::edit(placeholder.as_bytes()) {
|
match edit::edit(placeholder.as_bytes()) {
|
||||||
Ok(rsa_key) => {
|
Ok(rsa_key) => {
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ impl SetupActivity {
|
|||||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||||
}
|
}
|
||||||
// Prepare terminal
|
// Prepare terminal
|
||||||
let _ = disable_raw_mode();
|
if let Err(err) = disable_raw_mode() {
|
||||||
|
error!("Failed to disable raw mode: {}", err);
|
||||||
|
}
|
||||||
// Leave alternate mode
|
// Leave alternate mode
|
||||||
ctx.leave_alternate_screen();
|
ctx.leave_alternate_screen();
|
||||||
// Get result
|
// Get result
|
||||||
@@ -121,7 +123,9 @@ impl SetupActivity {
|
|||||||
// Enter alternate mode
|
// Enter alternate mode
|
||||||
ctx.enter_alternate_screen();
|
ctx.enter_alternate_screen();
|
||||||
// Re-enable raw mode
|
// Re-enable raw mode
|
||||||
let _ = enable_raw_mode();
|
if let Err(err) = enable_raw_mode() {
|
||||||
|
error!("Failed to enter raw mode: {}", err);
|
||||||
|
}
|
||||||
// Return result
|
// Return result
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ impl Activity for SetupActivity {
|
|||||||
// Clear terminal
|
// Clear terminal
|
||||||
self.context.as_mut().unwrap().clear_screen();
|
self.context.as_mut().unwrap().clear_screen();
|
||||||
// Put raw mode on enabled
|
// 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
|
// Init view
|
||||||
self.init_setup();
|
self.init_setup();
|
||||||
// Verify error state from context
|
// Verify error state from context
|
||||||
@@ -161,7 +163,9 @@ impl Activity for SetupActivity {
|
|||||||
/// This function finally releases the context
|
/// This function finally releases the context
|
||||||
fn on_destroy(&mut self) -> Option<Context> {
|
fn on_destroy(&mut self) -> Option<Context> {
|
||||||
// Disable raw mode
|
// 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()?;
|
self.context.as_ref()?;
|
||||||
// Clear terminal and return
|
// Clear terminal and return
|
||||||
match self.context.take() {
|
match self.context.take() {
|
||||||
|
|||||||
+14
-5
@@ -104,29 +104,38 @@ impl Context {
|
|||||||
///
|
///
|
||||||
/// Enter alternate screen (gui window)
|
/// Enter alternate screen (gui window)
|
||||||
pub fn enter_alternate_screen(&mut self) {
|
pub fn enter_alternate_screen(&mut self) {
|
||||||
let _ = execute!(
|
match execute!(
|
||||||
self.terminal.backend_mut(),
|
self.terminal.backend_mut(),
|
||||||
EnterAlternateScreen,
|
EnterAlternateScreen,
|
||||||
DisableMouseCapture
|
DisableMouseCapture
|
||||||
);
|
) {
|
||||||
|
Err(err) => error!("Failed to enter alternate screen: {}", err),
|
||||||
|
Ok(_) => info!("Entered alternate screen"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### leave_alternate_screen
|
/// ### leave_alternate_screen
|
||||||
///
|
///
|
||||||
/// Go back to normal screen (gui window)
|
/// Go back to normal screen (gui window)
|
||||||
pub fn leave_alternate_screen(&mut self) {
|
pub fn leave_alternate_screen(&mut self) {
|
||||||
let _ = execute!(
|
match execute!(
|
||||||
self.terminal.backend_mut(),
|
self.terminal.backend_mut(),
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen,
|
||||||
DisableMouseCapture
|
DisableMouseCapture
|
||||||
);
|
) {
|
||||||
|
Err(err) => error!("Failed to leave alternate screen: {}", err),
|
||||||
|
Ok(_) => info!("Left alternate screen"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### clear_screen
|
/// ### clear_screen
|
||||||
///
|
///
|
||||||
/// Clear terminal screen
|
/// Clear terminal screen
|
||||||
pub fn clear_screen(&mut self) {
|
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"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
@@ -219,4 +226,9 @@ mod tests {
|
|||||||
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
||||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../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
|
// modules
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
pub mod file;
|
||||||
pub mod fmt;
|
pub mod fmt;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|||||||
Reference in New Issue
Block a user