Merge pull request #7 from veeso/fetch-new-release

Check for updates through Github API
This commit is contained in:
Christian Visintin 2021-02-28 14:47:55 +01:00 committed by GitHub
commit efbea63154
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 483 additions and 22 deletions

View file

@ -17,9 +17,12 @@
Released on 28/02/2021
- Format key attributes:
- **Format key attributes**:
- Added `EXTRA` and `LENGTH` parameters to format keys.
- Now keys are provided with this syntax `{KEY_NAME[:LEN[:EXTRA]}`
- **Check for updates**:
- TermSCP will now check for updates on startup and will show in the main page if there is a new version available
- This feature may be disabled from setup (Check for updates => No)
- Enhancements:
- Default choice for deleting file set to "NO" (way too easy to delete files by mistake)
- Added CLI options to set starting workind directory on both local and remote hosts

195
Cargo.lock generated
View file

@ -193,6 +193,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "cipher"
version = "0.2.5"
@ -433,6 +439,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "ftp4"
version = "4.0.2"
@ -535,6 +551,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "idna"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "instant"
version = "0.1.9"
@ -544,6 +571,12 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.46"
@ -654,6 +687,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "md-5"
version = "0.9.1"
@ -891,6 +930,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pkg-config"
version = "0.3.19"
@ -1055,6 +1100,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rpassword"
version = "5.0.1"
@ -1077,6 +1137,25 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rustls"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b"
dependencies = [
"base64",
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "schannel"
version = "0.1.19"
@ -1093,6 +1172,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "secret-service"
version = "1.1.3"
@ -1175,6 +1264,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.9.2"
@ -1231,6 +1331,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "ssh2"
version = "0.9.0"
@ -1301,6 +1407,7 @@ dependencies = [
"toml",
"tui",
"unicode-width",
"ureq",
"users",
"whoami",
]
@ -1346,6 +1453,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "toml"
version = "0.5.8"
@ -1374,6 +1496,24 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
dependencies = [
"matches",
]
[[package]]
name = "unicode-normalization"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
@ -1392,6 +1532,42 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6585dcbf3483242f77b502864478ede62431baf3442b99367d3456ec20c1707b"
dependencies = [
"base64",
"chunked_transfer",
"log",
"once_cell",
"rustls",
"serde",
"serde_json",
"url",
"webpki",
"webpki-roots",
]
[[package]]
name = "url"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "users"
version = "0.11.0"
@ -1490,6 +1666,25 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376"
dependencies = [
"webpki",
]
[[package]]
name = "which"
version = "3.1.1"

View file

@ -38,6 +38,7 @@ textwrap = "0.13.1"
toml = "0.5.8"
tui = { version = "0.14.0", features = ["crossterm"], default-features = false }
unicode-width = "0.1.7"
ureq = { version = "2.0.2", features = ["json"] }
whoami = "1.1.0"
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]

View file

@ -288,6 +288,7 @@ These parameters can be changed:
- **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument.
- **Text Editor**: the text editor to use. By default termscp will find the default editor for you; with this option you can force an editor to be used (e.g. `vim`). **Also GUI editors are supported**, unless they `nohup` from the parent process so if you ask: yes, you can use `notepad.exe`, and no: **Visual Studio Code doesn't work**.
- **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway.
- **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available.
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
### SSH Key Storage 🔐

View file

@ -55,6 +55,7 @@ pub struct UserInterfaceConfig {
pub text_editor: PathBuf,
pub default_protocol: String,
pub show_hidden_files: bool,
pub check_for_updates: Option<bool>, // @! Since 0.3.3
pub group_dirs: Option<String>,
pub file_fmt: Option<String>,
}
@ -85,6 +86,7 @@ impl Default for UserInterfaceConfig {
},
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
check_for_updates: Some(true),
group_dirs: None,
file_fmt: None,
}
@ -172,6 +174,7 @@ mod tests {
default_protocol: String::from("SFTP"),
text_editor: PathBuf::from("nano"),
show_hidden_files: true,
check_for_updates: Some(true),
group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")),
};
@ -189,6 +192,7 @@ mod tests {
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
}
@ -201,6 +205,7 @@ mod tests {
let cfg: UserConfig = UserConfig::default();
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.remote.ssh_keys.len(), 0);
}

View file

@ -107,8 +107,12 @@ mod tests {
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME} {PEX}")));
assert_eq!(
cfg.user_interface.file_fmt,
Some(String::from("{NAME} {PEX}"))
);
// Verify keys
assert_eq!(
*cfg.remote
@ -144,6 +148,7 @@ mod tests {
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.group_dirs, None);
assert!(cfg.user_interface.check_for_updates.is_none());
assert_eq!(cfg.user_interface.file_fmt, None);
// Verify keys
assert_eq!(
@ -200,6 +205,7 @@ mod tests {
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
check_for_updates = true
group_dirs = "last"
file_fmt = "{NAME} {PEX}"

View file

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

View file

@ -62,6 +62,7 @@ impl AuthActivity {
.constraints(
[
Constraint::Length(5),
Constraint::Length(1), // Version
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
@ -80,32 +81,33 @@ impl AuthActivity {
.split(chunks[1]);
// Draw header
f.render_widget(self.draw_header(), auth_chunks[0]);
f.render_widget(self.draw_new_version(), auth_chunks[1]);
// Draw input fields
f.render_widget(self.draw_remote_address(), auth_chunks[1]);
f.render_widget(self.draw_remote_port(), auth_chunks[2]);
f.render_widget(self.draw_protocol_select(), auth_chunks[3]);
f.render_widget(self.draw_protocol_username(), auth_chunks[4]);
f.render_widget(self.draw_protocol_password(), auth_chunks[5]);
f.render_widget(self.draw_remote_address(), auth_chunks[2]);
f.render_widget(self.draw_remote_port(), auth_chunks[3]);
f.render_widget(self.draw_protocol_select(), auth_chunks[4]);
f.render_widget(self.draw_protocol_username(), auth_chunks[5]);
f.render_widget(self.draw_protocol_password(), auth_chunks[6]);
// Draw footer
f.render_widget(self.draw_footer(), auth_chunks[6]);
f.render_widget(self.draw_footer(), auth_chunks[7]);
// Set cursor
if let InputForm::AuthCredentials = self.input_form {
match self.selected_field {
InputField::Address => f.set_cursor(
auth_chunks[1].x + self.address.width() as u16 + 1,
auth_chunks[1].y + 1,
),
InputField::Port => f.set_cursor(
auth_chunks[2].x + self.port.width() as u16 + 1,
auth_chunks[2].x + self.address.width() as u16 + 1,
auth_chunks[2].y + 1,
),
InputField::Port => f.set_cursor(
auth_chunks[3].x + self.port.width() as u16 + 1,
auth_chunks[3].y + 1,
),
InputField::Username => f.set_cursor(
auth_chunks[4].x + self.username.width() as u16 + 1,
auth_chunks[4].y + 1,
auth_chunks[5].x + self.username.width() as u16 + 1,
auth_chunks[5].y + 1,
),
InputField::Password => f.set_cursor(
auth_chunks[5].x + self.password_placeholder.width() as u16 + 1,
auth_chunks[5].y + 1,
auth_chunks[6].x + self.password_placeholder.width() as u16 + 1,
auth_chunks[6].y + 1,
),
_ => {}
}
@ -284,6 +286,21 @@ impl AuthActivity {
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
}
/// ### draw_new_version
///
/// Draw new version disclaimer
fn draw_new_version(&self) -> Paragraph {
let content: String = match self.new_version.as_ref() {
Some(ver) => format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", ver),
None => String::new(),
};
Paragraph::new(content).style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
}
/// ### draw_footer
///
/// Draw authentication page footer

View file

@ -40,6 +40,7 @@ use crate::filetransfer::FileTransferProtocol;
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::utils::git;
// Includes
use crossterm::event::Event as InputEvent;
@ -118,6 +119,8 @@ pub struct AuthActivity {
bookmarks_list: Vec<String>, // List of bookmarks
recents_idx: usize, // Index of selected recent
recents_list: Vec<String>, // list of recents
// misc
new_version: Option<String>, // Contains new version of termscp
}
impl Default for AuthActivity {
@ -154,6 +157,7 @@ impl AuthActivity {
bookmarks_list: Vec::new(),
recents_idx: 0,
recents_list: Vec::new(),
new_version: None,
}
}
@ -192,6 +196,27 @@ impl AuthActivity {
}
}
}
/// ### on_create
///
/// If enabled in configuration, check for updates from Github
fn check_for_updates(&mut self) {
if let Some(client) = self.config_client.as_ref() {
if client.get_check_for_updates() {
// Send request
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
Ok(version) => self.new_version = version,
Err(err) => {
// Report error
self.popup = Some(Popup::Alert(
Color::Red,
format!("Could not check for new updates: {}", err),
))
}
}
}
}
}
}
impl Activity for AuthActivity {
@ -216,6 +241,8 @@ impl Activity for AuthActivity {
if self.config_client.is_none() {
self.init_config_client();
}
// If check for updates is enabled, check for updates
self.check_for_updates();
}
/// ### on_draw

View file

@ -238,6 +238,10 @@ impl SetupActivity {
// Move left
config_cli.set_show_hidden_files(true);
}
UserInterfaceInputField::CheckForUpdates => {
// move left
config_cli.set_check_for_updates(true);
}
_ => { /* Not a tab field */ }
}
}
@ -275,6 +279,10 @@ impl SetupActivity {
// Move right
config_cli.set_show_hidden_files(false);
}
UserInterfaceInputField::CheckForUpdates => {
// move right
config_cli.set_check_for_updates(false);
}
_ => { /* Not a tab field */ }
}
}
@ -284,6 +292,9 @@ impl SetupActivity {
self.tab = SetupTab::UserInterface(match field {
UserInterfaceInputField::FileFmt => UserInterfaceInputField::GroupDirs,
UserInterfaceInputField::GroupDirs => {
UserInterfaceInputField::CheckForUpdates
}
UserInterfaceInputField::CheckForUpdates => {
UserInterfaceInputField::ShowHiddenFiles
}
UserInterfaceInputField::ShowHiddenFiles => {
@ -305,6 +316,9 @@ impl SetupActivity {
UserInterfaceInputField::ShowHiddenFiles
}
UserInterfaceInputField::ShowHiddenFiles => {
UserInterfaceInputField::CheckForUpdates
}
UserInterfaceInputField::CheckForUpdates => {
UserInterfaceInputField::GroupDirs
}
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::FileFmt,
@ -354,7 +368,8 @@ impl SetupActivity {
}
UserInterfaceInputField::FileFmt => {
// Push char to current file fmt
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
let mut file_fmt =
config_cli.get_file_fmt().unwrap_or_default();
file_fmt.push(ch);
// update value
config_cli.set_file_fmt(file_fmt);

View file

@ -90,6 +90,7 @@ impl SetupActivity {
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
@ -105,12 +106,15 @@ impl SetupActivity {
if let Some(tab) = self.draw_hidden_files_tab() {
f.render_widget(tab, ui_cfg_chunks[2]);
}
if let Some(tab) = self.draw_default_group_dirs_tab() {
if let Some(tab) = self.draw_check_for_updates_tab() {
f.render_widget(tab, ui_cfg_chunks[3]);
}
if let Some(tab) = self.draw_file_fmt_input() {
if let Some(tab) = self.draw_default_group_dirs_tab() {
f.render_widget(tab, ui_cfg_chunks[4]);
}
if let Some(tab) = self.draw_file_fmt_input() {
f.render_widget(tab, ui_cfg_chunks[5]);
}
// Set cursor
if let Some(cli) = &self.config_cli {
match form_field {
@ -317,9 +321,9 @@ impl SetupActivity {
}
}
/// ### draw_default_protocol_tab
/// ### draw_hidden_files_tab
///
/// Draw default protocol input tab
/// Draw default hidden files tab
fn draw_hidden_files_tab(&self) -> Option<Tabs> {
// Check if config client is some
match &self.config_cli {
@ -358,6 +362,47 @@ impl SetupActivity {
}
}
/// ### draw_check_for_updates_tab
///
/// Draw check for updates tab
fn draw_check_for_updates_tab(&self) -> Option<Tabs> {
// Check if config client is some
match &self.config_cli {
Some(cli) => {
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
let index: usize = match cli.get_check_for_updates() {
true => 0,
false => 1,
};
let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab {
SetupTab::UserInterface(field) => match field {
UserInterfaceInputField::CheckForUpdates => {
(Color::LightYellow, Color::Black, Color::LightYellow)
}
_ => (Color::Reset, Color::LightYellow, Color::Reset),
},
_ => (Color::Reset, Color::Reset, Color::Reset),
};
Some(
Tabs::new(choices)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().fg(block_fg))
.title("Check for updates?"),
)
.select(index)
.style(Style::default())
.highlight_style(
Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg),
),
)
}
None => None,
}
}
/// ### draw_default_group_dirs_tab
///
/// Draw group dirs input tab

View file

@ -54,6 +54,7 @@ enum UserInterfaceInputField {
DefaultProtocol,
TextEditor,
ShowHiddenFiles,
CheckForUpdates,
GroupDirs,
FileFmt,
}

85
src/utils/git.rs Normal file
View file

@ -0,0 +1,85 @@
//! ## git
//!
//! `git` is the module which provides utilities to interact through the GIT API and to perform some stuff at git level
/*
*
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Deps
extern crate ureq;
// Locals
use super::parser::parse_semver;
// Others
use serde::Deserialize;
#[derive(Deserialize)]
struct TagInfo {
tag_name: String,
}
/// ### check_for_updates
///
/// Check if there is a new version available for termscp.
/// This is performed through the Github API
/// In case of success returns Ok(Option<String>), where the Option is Some(new_version); otherwise if no version is available, return None
/// In case of error returns Error with the error description
pub fn check_for_updates(current_version: &str) -> Result<Option<String>, String> {
// Send request
let github_version: Result<String, String> =
match ureq::get("https://api.github.com/repos/veeso/termscp/releases/latest").call() {
Ok(response) => match response.into_json::<TagInfo>() {
Ok(tag_info) => Ok(tag_info.tag_name),
Err(err) => Err(err.to_string()),
},
Err(err) => Err(err.to_string()),
};
// Check version
match github_version {
Err(err) => Err(err),
Ok(version) => {
// Parse version
match parse_semver(version.as_str()) {
Some(new_version) => {
// Check if version is different
if new_version.as_str() > current_version {
Ok(Some(new_version)) // New version is available
} else {
Ok(None) // No new version
}
}
None => Err(String::from("Got bad response from Github")),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_utils_git_check_for_updates() {
assert!(check_for_updates("100.0.0").ok().unwrap().is_none());
assert!(check_for_updates("0.0.1").ok().unwrap().is_some());
}
}

View file

@ -26,5 +26,6 @@
// modules
pub mod crypto;
pub mod fmt;
pub mod git;
pub mod parser;
pub mod random;

View file

@ -54,6 +54,13 @@ lazy_static! {
* - group 5: Some(path) | None
*/
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
/**
* Regex matches:
* - group 1: Version
* E.g. termscp-0.3.2 => 0.3.2
* v0.4.0 => 0.4.0
*/
static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap();
}
pub struct RemoteOptions {
@ -211,6 +218,19 @@ pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
}
}
/// ### parse_semver
///
/// Parse semver string
pub fn parse_semver(haystack: &str) -> Option<String> {
match SEMVER_REGEX.captures(haystack) {
Some(groups) => match groups.get(1) {
Some(version) => Some(version.as_str().to_string()),
None => None,
},
None => None,
}
}
#[cfg(test)]
mod tests {
@ -384,4 +404,15 @@ mod tests {
// Not enough argument for datetime
assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err());
}
#[test]
fn test_utils_parse_semver() {
assert_eq!(
parse_semver("termscp-0.3.2").unwrap(),
String::from("0.3.2")
);
assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
assert!(parse_semver("v1.1").is_none());
}
}