665 Commits

Author SHA1 Message Date
veeso
4c1e4fef7a Removed BSD packages 2021-08-30 15:03:53 +02:00
Christian Visintin
ddd4824567 Merge pull request #62 from veeso/0.6.1
0.6.1
2021-08-30 15:00:06 +02:00
veeso
22bb143820 0.6.1 2021-08-24 15:03:20 +02:00
veeso
78b918087e absolutize path common functions 2021-08-24 09:29:40 +02:00
veeso
91ce709d7b lint 2021-08-24 09:29:23 +02:00
veeso
214ec0c5a5 absolutize path common functions 2021-08-24 09:28:49 +02:00
veeso
0cb9254e63 fmt 2021-08-23 18:01:45 +02:00
veeso
4bbb339844 test fixed 2021-08-23 17:57:18 +02:00
veeso
92b081c076 Replaced u8 pex with UnixPex struct 2021-08-23 17:52:55 +02:00
veeso
32ab0267fb Removed unused readonly attribute for FsEntry 2021-08-23 17:18:27 +02:00
Christian Visintin
8a0f190474 Merge pull request #61 from veeso/issue-59-when-copying-files-with-tricky-copy-the-upper-progress-bar-shows-no-text
Fix: When copying files with tricky copy, the upper progress bar shows no text
2021-08-23 15:32:27 +02:00
veeso
a189a4c754 Changelog 2021-08-23 14:59:47 +02:00
veeso
b7b765c16e Fixed: When copying files with tricky copy, the upper progress bar shows no text 2021-08-23 14:56:27 +02:00
veeso
7713c6c21d suppaftp 4.1.2 2021-08-23 12:20:25 +02:00
Christian Visintin
c49184dcb1 Merge pull request #60 from veeso/issue-58-possible-to-upload-a-directory-that-already-exists-on-remote
Issue 58 possible to upload a directory that already exists on remote
2021-08-23 12:12:07 +02:00
veeso
fc3803991a Fixed scp transfer non returning DirectoryAlreadyExists 2021-08-23 11:38:41 +02:00
veeso
f6011543c0 Fixed ui test units 2021-08-23 11:18:11 +02:00
veeso
7390bb58c5 Changed FTP library from ftp4 to suppaftp; Handle directory already exists on FTP transfer; mkdir already exists test units 2021-08-23 11:11:14 +02:00
veeso
81482b47f4 Merged 0.6.1; added new DirectoryAlreadyExists; return new variant for SFTP/SCP 2021-08-18 09:33:27 +02:00
veeso
6cb1dcaa43 upcoming features 2021-08-17 18:05:02 +02:00
veeso
78e4a4899c Rewind radio groups 2021-08-17 12:44:23 +02:00
veeso
d15997cca0 Updated dependencies; migrated tui-realm to 0.6.0 2021-08-17 12:38:24 +02:00
veeso
1558f4ffe4 init 0.6.1 2021-08-17 11:03:35 +02:00
veeso
764cca73d1 linter 2021-08-10 22:27:20 +02:00
veeso
1757eb5bec When uploading a directory, create directory only if it doesn't exist 2021-08-10 22:17:36 +02:00
veeso
6cfac57162 Shortened url for install.sh 2021-08-03 17:09:37 +02:00
veeso
d36d330244 badge 2021-07-23 21:37:12 +02:00
veeso
4203de6d3f Merge branch 'main' of github.com:veeso/termscp into main 2021-07-23 21:05:19 +02:00
veeso
098e3c709a fixed deps rpm 2021-07-23 21:05:11 +02:00
Christian Visintin
1a8e151efb Merge pull request #57 from veeso/imgbot
[ImgBot] Optimize images
2021-07-23 21:05:03 +02:00
ImgBotApp
6c8d22d396 [ImgBot] Optimize images
*Total -- 565.02kb -> 417.74kb (26.07%)

/assets/images/themes.gif -- 327.91kb -> 227.34kb (30.67%)
/assets/images/auth.gif -- 237.10kb -> 190.40kb (19.7%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-07-23 18:36:46 +00:00
Christian Visintin
a0ee1d886a Merge pull request #56 from veeso/0.6.0
0.6.0
2021-07-23 20:36:25 +02:00
veeso
bf6f1625ec 0.6.0 release notes 2021-07-23 14:32:50 +02:00
veeso
3271377b7e Removed redundant remoteOpts struct; use FileTransferParams only 2021-07-23 14:32:15 +02:00
veeso
f36bb65b45 Removed redundant remoteOpts struct; use FileTransferParams only 2021-07-23 14:31:29 +02:00
veeso
c722982c39 Removed unecessary Option<&ConfigClient> in filetransfer; use degraded mode instead 2021-07-16 15:49:00 +02:00
veeso
e1109fff15 From now on, if you try to leave setup without making any change, you won't be prompted whether to save configuration or not 2021-07-16 15:32:39 +02:00
veeso
59c6567ff3 Auth view enhanchements: check if port and host are valid 2021-07-15 13:00:55 +02:00
veeso
61f6901767 Save both theme and config at the same time 2021-07-15 12:47:47 +02:00
veeso
8277c80860 Fixed config save and theme layout 2021-07-15 12:33:15 +02:00
veeso
4093ba169c Replaced '...' with '…' in texts 2021-07-15 12:24:20 +02:00
veeso
e3a9d253f7 Show a 'wait' message when deleting, copying and moving files and when executing commands 2021-07-15 11:58:57 +02:00
veeso
80c67c8aa8 Fixed transfer interruption: it was not possible to abort a transfer if the size of the file was less than 65k 2021-07-13 16:43:27 +02:00
veeso
421969c3da argh instead of getopts 2021-07-10 20:19:29 +02:00
veeso
e15b79750b main() refactoring 2021-07-08 16:21:39 +02:00
veeso
37abe596c7 context getters 2021-07-08 15:43:23 +02:00
veeso
e6b44e1461 ConfigClient is an option no more; config client degraded mode 2021-07-08 15:07:24 +02:00
veeso
b9cb961da6 full and partial progress bar colors 2021-07-08 14:29:28 +02:00
veeso
55c4b777fb Fixed save bookmark dialog: you could switch out from dialog with <TAB> 2021-07-07 21:03:21 +02:00
Christian Visintin
2231727adb Merge pull request #55 from veeso/theme-provider
Theme provider and '-t' and '-c' CLI options
2021-07-07 14:38:22 +02:00
veeso
0a7e29d92f Theme provider and '-t' and '-c' CLI options 2021-07-07 12:54:30 +02:00
veeso
a105a42519 Dependencies 2021-07-03 15:56:26 +02:00
Christian Visintin
d20c2c786b Merge pull request #53 from veeso/keyring-rs-linux
Keyring-rs for linux
2021-07-03 15:15:00 +02:00
veeso
14abed44a5 Keyring-rs support for Linux 2021-07-03 14:43:31 +02:00
veeso
65fee64132 Merged 0.6.0 in keyring-rs-linux 2021-07-03 14:35:17 +02:00
Christian Visintin
348688b5ce Merge pull request #54 from veeso/release-notes-in-app
Release notes in-app
2021-06-26 19:18:39 +02:00
veeso
0cb84fc531 I've no idea of what user input buffer was meant to be 2021-06-26 18:26:21 +02:00
veeso
8a36c459d5 Restored version from env 2021-06-26 18:06:16 +02:00
veeso
c7414ab070 Release notes in-app 2021-06-26 18:00:03 +02:00
veeso
d16f9e7ad8 Merge branch '0.6.0' into keyring-rs-linux 2021-06-26 15:13:31 +02:00
veeso
3cbd2ed013 Removed archlinux build stuff 2021-06-26 14:40:23 +02:00
veeso
398f518b2f test_utils_git_check_for_updates not when github and mac and bsd 2021-06-26 14:39:09 +02:00
veeso
97a62def11 Brought all extern crate to top level 2021-06-26 12:32:11 +02:00
veeso
7ed49126a4 security policy 2021-06-26 12:25:24 +02:00
veeso
d976a34705 --lib tests 2021-06-26 12:08:05 +02:00
veeso
8b01c71819 Coverage 2021-06-26 12:06:49 +02:00
veeso
1ee5e368f6 Install cargo dependencies when building with cargo (rust too); build without default features on bsd 2021-06-26 11:02:07 +02:00
veeso
8fc71aae66 Merge branch '0.6.0' into keyring-rs-linux 2021-06-26 10:00:22 +02:00
veeso
0589cc2f0c Merge branch 'main' into 0.6.0 2021-06-26 10:00:08 +02:00
veeso
931a66498f filemode manifest freebsd 2021-06-26 09:51:13 +02:00
veeso
8ec0a0289a AUR distro: install rust too; fixed tempfile rpm 2021-06-22 09:35:52 +02:00
veeso
2d8ce475f8 Merge branch '0.6.0' into keyring-rs-linux 2021-06-21 20:51:24 +02:00
veeso
f8766722d4 Merge branch 'main' into 0.6.0 2021-06-21 20:46:10 +02:00
veeso
1319e0b17b Fixed install for freebsd 2021-06-21 20:45:20 +02:00
veeso
5e17bf0736 Merge branch '0.5.1' into 0.6.0 2021-06-21 18:23:01 +02:00
veeso
ca50bddc56 Merge branch '0.5.1' into 0.6.0 2021-06-21 18:22:19 +02:00
veeso
c78fc583b0 Fix: target_family unix means also macos and linux; use BSD target_os 2021-06-21 09:11:28 +02:00
veeso
04cafc4181 distros 2021-06-20 17:59:27 +02:00
veeso
f8300fa587 fixed tests 2021-06-20 17:00:35 +02:00
veeso
407ca567f1 keyring-rs-linux docs 2021-06-20 16:55:17 +02:00
veeso
320fd5c2dd Updated dockerfiles for dbus 2021-06-20 15:13:20 +02:00
Christian Visintin
e4c616245a Merge pull request #52 from veeso/freebsd-porting
FreeBSD Build
2021-06-20 15:09:36 +02:00
veeso
5986130dfa Unused variant if keyring is not enabled 2021-06-20 15:08:35 +02:00
veeso
19610479bd keyring support for linux 2021-06-20 15:04:16 +02:00
veeso
e3d2151bad Unix Build 2021-06-20 14:38:03 +02:00
veeso
89d205e946 Fixed UI not showing connection error 2021-06-20 11:03:55 +02:00
veeso
15d13af7d5 wip 2021-06-20 11:00:31 +02:00
Christian Visintin
3df8ed13a4 Merge pull request #41 from veeso/open-rs
Open-rs
2021-06-19 15:35:48 +02:00
veeso
5a4a364250 Fixed windows dying when opening text files 2021-06-19 15:06:49 +02:00
veeso
00bee04c2c open-rs fixes 2021-06-19 15:03:56 +02:00
veeso
4475e8c24c 0.5.1 ready 2021-06-19 13:44:38 +02:00
veeso
5a49987338 acceptance tests in PR 2021-06-18 14:31:11 +02:00
Christian Visintin
76f1f1dcf3 Merge pull request #51 from veeso/issue-38-transfer-size-is-wrong-when-transferring-selected-files
Fixed transfer size when sending multiple entries
2021-06-18 14:30:12 +02:00
veeso
0175cfbfb6 Code enhancements 2021-06-18 14:22:35 +02:00
veeso
48483a5c99 Unique function to send and receive files in session.rs via TransferPayload. Fixed transfer size when sending multiple entries 2021-06-18 13:02:04 +02:00
Christian Visintin
71e4cc9413 Merge pull request #50 from veeso/known-size-for-layouts
Window too small error message
2021-06-16 22:19:01 +02:00
veeso
a00c0117a2 If the terminal window has less than 24 lines, then an error message is displayed in the auth activity 2021-06-16 22:00:28 +02:00
veeso
c1bc81c664 typo 2021-06-16 17:48:42 +02:00
Christian Visintin
1b7ef30a50 Merge pull request #49 from veeso/status-bar-v2
Status bar improvements
2021-06-16 14:07:58 +02:00
veeso
efad2b96db Status bar improvements: 'Show hidden files' in status bar; Status bar is has now been splitted into two, one for each explorer tab 2021-06-16 13:57:11 +02:00
Christian Visintin
56a200499e Merge pull request #46 from veeso/github-actions-containers
GitHub actions containers
2021-06-15 17:59:54 +02:00
veeso
1c58f1d623 Use containers to test file transfers
Use containers to test file transfers

Container setup

Container setup

tests with docker-compose

these tests won't work with containers

ftp tests with containers; removed crap servers; tests only lib

hostname for github

booooooh

fixed recursive remove FTP

Use containers to test file transfers

Container setup

Container setup

tests with docker-compose

these tests won't work with containers

ftp tests with containers; removed crap servers; tests only lib

hostname for github

booooooh

fixed recursive remove FTP

fixed rename

changelog

Desperate attempt

Fixed ftp tests; migrated sftp tests to containers; use env for services

github actions are just broken imo

github actions are just broken imo

Don't use services, since they just don't fuckin work...

docker compose not supported yet?

deprecated typo

Now explain this: github actions have services (which don't work) and then you find out docker-compose is already installed *BLOWMIND*

Fixed hostname for tests

scp tests

maybe

tests

wtf

Restored host tests

Changelog

Improving coverage

Improving coverage

Restored coverage task

More tests for file transfers; test ssh keys too

typo

tests; code improvements

Use tests helpers

fixed tempdir

fixed tempdir
2021-06-15 17:51:06 +02:00
veeso
6cdd56e22f changelog 2021-06-13 10:20:16 +02:00
Christian Visintin
53acb847bc Merge pull request #47 from veeso/issue-44-ftp-cant-move-files-to-other-directories
Ftp: cant move files to other directories
2021-06-13 10:20:00 +02:00
veeso
2b006457c7 fixed rename 2021-06-13 10:10:04 +02:00
Christian Visintin
fbc1672e3d Merge pull request #45 from veeso/issue-43-unable-to-remove-non-empty-directories-ftp
fixed recursive remove FTP
2021-06-13 10:04:51 +02:00
veeso
542123ce04 fixed recursive remove FTP 2021-06-13 09:47:17 +02:00
Christian Visintin
59eb7f7935 Merge pull request #42 from veeso/issue-37-progress-bar-not-visible-when-editing-remote-files
Fixed progress bar not visible when editing remote files
2021-06-12 15:33:11 +02:00
veeso
74482d6e2c Fixed progress bar not visible when editing remote files; moved edit and tricky_copy to actions; added new function recv/send one 2021-06-12 14:36:01 +02:00
Christian Visintin
529ac2f778 Merge pull request #40 from veeso/issue-39-help-window-content-is-not-fully-visible
Help panels as ScrollTable
2021-06-12 09:27:05 +02:00
veeso
b73c3228e4 manual 2021-06-12 09:26:33 +02:00
veeso
26f7c1f9d1 Help panels as ScrollTable to allow displaying entire content on small screens 2021-06-12 09:05:02 +02:00
veeso
f078232499 tui-realm 0.4.1 2021-06-11 15:32:45 +02:00
veeso
8cd91f6df3 tui-realm 0.3.2 2021-06-11 15:31:33 +02:00
veeso
5670c0f975 0.5.1 setup 2021-06-11 15:28:14 +02:00
veeso
99d4177e89 changelog 2021-06-11 15:20:49 +02:00
veeso
d07b1c86be tui-realm 0.4.1 2021-06-11 14:48:38 +02:00
veeso
fe494c52b1 Clear screen once opened file, to prevent crap on stderr 2021-06-11 14:46:20 +02:00
veeso
bc50328006 open-rs docs 2021-06-11 13:01:28 +02:00
veeso
90afe204b1 Open files with <V>; fixed cache file names 2021-06-11 09:52:50 +02:00
veeso
d981b77ed7 Working on open 2021-06-10 22:36:29 +02:00
veeso
541a9a55b5 Handle shift enter (file list) 2021-06-10 20:55:12 +02:00
veeso
cd3fc15bcb Fmt symlink with len 48 in found dialog 2021-06-10 12:16:48 +02:00
veeso
4e50038b41 Open file with 2021-06-10 12:14:09 +02:00
veeso
a8354ee38f Open file on <SUBMIT> 2021-06-10 11:08:17 +02:00
veeso
c4907c8ce5 Added open-rs to deps 2021-06-07 22:28:52 +02:00
veeso
3c3c680b00 tui-realm 0.4.0 2021-06-07 22:09:33 +02:00
veeso
3a32f45334 tui-realm 0.3.2 2021-06-06 14:19:20 +02:00
veeso
8e1843f90b Working on 0.6.0 2021-06-02 22:32:52 +02:00
veeso
dc01de98dc Defined summer update 2021-06-01 17:10:25 +02:00
Christian Visintin
971061d231 Fixed AUR pub workflow
See <https://github.com/KSXGitHub/github-actions-deploy-aur/issues/15>
2021-05-24 11:09:56 +02:00
veeso
d04dd831e4 README 2021-05-23 15:56:18 +02:00
Christian Visintin
8dcfda9787 Merge pull request #35 from veeso/imgbot
[ImgBot] Optimize images
2021-05-23 15:53:56 +02:00
veeso
c938021bef AUR pkgs 2021-05-23 15:52:18 +02:00
ImgBotApp
3fcf0fb72a [ImgBot] Optimize images
*Total -- 3,507.59kb -> 2,729.63kb (22.18%)

/assets/images/config.gif -- 453.13kb -> 313.39kb (30.84%)
/assets/images/explorer.gif -- 2,764.58kb -> 2,185.64kb (20.94%)
/assets/images/bookmarks.gif -- 289.89kb -> 230.60kb (20.45%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-05-23 13:46:14 +00:00
veeso
e99406fbe4 Merged 0.5.0 into main 2021-05-23 15:45:04 +02:00
veeso
60073be993 Going to release 0.5.0 2021-05-23 15:42:51 +02:00
veeso
d3df4c7020 install.sh 2021-05-23 14:48:46 +02:00
veeso
adce92e312 install.sh 2021-05-23 14:48:32 +02:00
veeso
4973545fe1 Docs 2021-05-23 14:47:55 +02:00
veeso
8425c97102 docs 2021-05-22 16:04:42 +02:00
veeso
1876b8bdc7 Migrated documentation to website 2021-05-22 16:03:07 +02:00
veeso
99ff3a8e45 New gifs 2021-05-22 10:54:06 +02:00
veeso
cae251905c Fixed new ssh key box 2021-05-22 10:53:51 +02:00
veeso
f738ce90e0 Fixed bookmarks not being loaded properly 2021-05-22 10:27:37 +02:00
veeso
2f2bb20550 readme 2021-05-21 12:59:42 +02:00
veeso
591ede0507 changelog 2021-05-21 12:45:25 +02:00
veeso
09665fead3 Fixed error overlay for auth activity 2021-05-21 12:31:30 +02:00
Christian Visintin
60ee2f1c2b Merge pull request #34 from veeso/double-progress-bar
Double progress bar
2021-05-21 12:09:03 +02:00
veeso
d6d8197869 feature changelog 2021-05-21 12:08:53 +02:00
veeso
b1da42b543 Double progress bar 2021-05-21 11:49:44 +02:00
veeso
e874550d29 Refactored transfer states 2021-05-20 22:44:17 +02:00
Christian Visintin
6cd9657446 Merge pull request #33 from veeso/logging
Logging
2021-05-18 09:03:50 +02:00
veeso
2790c613aa Logging docs 2021-05-17 22:35:28 +02:00
veeso
7b868bc60b Activity log 2021-05-17 22:15:20 +02:00
veeso
c1b3511c06 Working on logging 2021-05-16 22:26:16 +02:00
veeso
1e3c859ae0 Fixed empty bookmark name causing termscp to crash 2021-05-16 22:05:33 +02:00
veeso
faab67800f Log always to trace; may be disabled 2021-05-16 15:12:01 +02:00
veeso
7f9d92cbe9 Working on logging 2021-05-16 15:09:17 +02:00
veeso
4e287a0231 Logging setup 2021-05-16 10:36:16 +02:00
veeso
8c9c331d7e Tricky-copy in case copy is not supported 2021-05-15 22:07:20 +02:00
veeso
37ce54051e codecov badge 2021-05-15 19:11:12 +02:00
veeso
f31b047671 Copy command support for SFTP 2021-05-15 19:09:58 +02:00
veeso
aaff616332 grcov: exclude activities 2021-05-15 18:53:07 +02:00
veeso
cfc1b73c74 Updated dependencies 2021-05-15 18:04:51 +02:00
Christian Visintin
0d2967423b Merge pull request #32 from veeso/file-group-select
Work on multiple files in explorers
2021-05-15 18:00:02 +02:00
veeso
cb3681ffdd Ignore files 2021-05-15 17:58:16 +02:00
veeso
4c343c6f34 Grcov with coverall 2021-05-15 17:55:20 +02:00
veeso
ff158add1f Restored '<A>' key behaviour for file list 2021-05-15 17:52:58 +02:00
veeso
2c64cd5cce Changed Multi variant to Many 2021-05-15 17:46:29 +02:00
veeso
c43ab28fb0 Work on multiple files docs 2021-05-15 17:43:33 +02:00
veeso
dd00e1f55a Selection help; Fixed find actions for selections 2021-05-15 17:20:06 +02:00
veeso
2b3acee97c Handle file selections in activity 2021-05-15 17:03:44 +02:00
veeso
d96558c3df wip 2021-05-12 20:55:17 +02:00
veeso
b8db557ffe Added file selection to file_list component 2021-05-09 21:08:31 +02:00
veeso
300256b196 Changed activity paths 2021-05-08 19:28:47 +02:00
Christian Visintin
7ba1b316f7 Merge pull request #31 from veeso/mini-refactoring-file-transfer-activity
Mini refactoring file transfer activity
2021-05-08 19:05:37 +02:00
veeso
822d41e885 Split actions on multi files 2021-05-08 18:52:13 +02:00
veeso
0253572975 Clippy rust 1.52 2021-05-08 18:21:24 +02:00
veeso
0328c1de81 Browser with explorers 2021-05-08 15:28:56 +02:00
veeso
c6ee9dedb9 Changed log function to accept String instead of &str 2021-05-08 14:32:21 +02:00
veeso
88136811b9 Fixed logbox wrap 2021-05-06 22:28:27 +02:00
veeso
7c816d0822 Removed log width 2021-05-06 22:24:55 +02:00
veeso
a5fe62e502 Localhost in activity; no more in Context 2021-05-06 22:16:38 +02:00
veeso
892be42988 pretty assert 2021-05-06 21:57:04 +02:00
veeso
c0eb98c3f9 Renamed to lowercase termscp 2021-05-05 22:13:57 +02:00
veeso
1ddbd0aa4e Use default port method 2021-05-05 22:07:49 +02:00
veeso
4f1e505a7a Moved file transfer protocol input on top; port will now be set to default for current protocol on change 2021-05-05 22:05:46 +02:00
veeso
dcec804681 Fixed default protocol not being loaded 2021-05-04 10:17:07 +02:00
veeso
a127f46a06 Removed the good old figlet termscp title with a simple label 2021-05-04 09:46:18 +02:00
Christian Visintin
c8d277c51d Merge pull request #30 from veeso/issue-8-synchronized-browsing-of-local-and-remote-directories
Synchronized browsing of local and remote directories
2021-05-04 09:13:42 +02:00
veeso
42477d6893 tui-realm 0.2.2 2021-05-03 22:24:17 +02:00
veeso
4c1fb826be Status bar 2021-05-03 22:23:55 +02:00
veeso
a0c29d1174 Fixed sync browser 2021-05-03 18:08:05 +02:00
veeso
2f0b340fe0 Working on sync 2021-05-02 21:04:03 +02:00
veeso
cbe242bb94 WIP 2021-05-02 14:59:21 +02:00
veeso
a088c6dbd3 <Y> key toggle sync browsing 2021-05-02 14:35:45 +02:00
veeso
036aba2420 Merge branch '0.5.0' into issue-8-synchronized-browsing-of-local-and-remote-directories 2021-05-02 12:09:52 +02:00
veeso
60269c7777 wip 2021-05-02 12:09:50 +02:00
veeso
e90f561584 Bumped tui-realm to 0.2.1 2021-05-02 12:09:32 +02:00
Christian Visintin
4bd18c1386 Merge pull request #29 from veeso/issue-23-remove-file-if-transfer-failed
Remove created file if transfer failed
2021-05-02 11:01:13 +02:00
veeso
fe51185f74 If transfer error reason is Abrupted or IO, then stat created file and remove it 2021-05-02 10:45:14 +02:00
veeso
a35395bd51 Fixed upload transfer error not being logged 2021-05-01 21:59:52 +02:00
Christian Visintin
f1225b1ff3 Merge pull request #27 from veeso/local-and-remote-file-fmt
Remote file syntax for formatter
2021-05-01 19:29:37 +02:00
Christian Visintin
63d727d5c5 Merge pull request #28 from veeso/imgbot
[ImgBot] Optimize images
2021-05-01 18:02:45 +02:00
veeso
46864ee23a Merge branch 'main' into 0.5.0 2021-05-01 18:00:18 +02:00
ImgBotApp
46728d8bc3 [ImgBot] Optimize images
*Total -- 500.68kb -> 489.72kb (2.19%)

/assets/images/termscp.svg -- 4.29kb -> 1.46kb (66.06%)
/assets/images/termscp-128.png -- 3.40kb -> 2.19kb (35.41%)
/assets/images/termscp-96.png -- 2.63kb -> 1.79kb (31.8%)
/assets/images/termscp-512.png -- 15.31kb -> 10.58kb (30.91%)
/assets/images/termscp-64.png -- 1.81kb -> 1.31kb (27.93%)
/assets/images/config.gif -- 473.23kb -> 472.39kb (0.18%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-05-01 15:59:27 +00:00
veeso
daa9a7c012 termscp logo 2021-05-01 17:58:00 +02:00
veeso
0f595b8cbf Coverage: githubActions 2021-05-01 17:54:37 +02:00
veeso
26b001a5fc Coverage: githubActions 2021-05-01 17:26:28 +02:00
veeso
3704d2520d Remote file syntax for formatter 2021-05-01 17:12:48 +02:00
veeso
41e89605f0 Sponsor uri in help 2021-05-01 16:41:47 +02:00
veeso
065ea59114 Restored context test (not for github actions); Context destructor: just call leave_alternate_screen 2021-05-01 16:34:56 +02:00
veeso
64bb2ed101 Merge branch 'main' into 0.5.0 2021-05-01 16:22:47 +02:00
veeso
c48c73909d User manual; cleaned readme up 2021-05-01 10:38:13 +02:00
Christian Visintin
68d113f335 Merge pull request #25 from veeso/tui-realm-integration
Tui realm integration
2021-05-01 10:13:55 +02:00
veeso
8bb66b53e0 Removed tui; added tuirealm 2021-04-25 23:00:49 +02:00
veeso
84c36f1789 0.5.0 setup 2021-04-23 22:05:05 +02:00
veeso
2214c1dee3 Removed winscp references 2021-04-23 22:03:12 +02:00
veeso
150a3cf346 Arch pkgs 2021-04-13 20:42:59 +02:00
veeso
b31de185c5 Merge branch '0.4.2' into main 2021-04-13 18:44:02 +02:00
Christian Visintin
477930bef9 Merge pull request #24 from veeso/imgbot
[ImgBot] Optimize images
2021-04-13 12:03:06 +02:00
ImgBotApp
03bbd6420e [ImgBot] Optimize images
*Total -- 3,783.77kb -> 3,022.10kb (20.13%)

/assets/images/auth.gif -- 314.23kb -> 212.31kb (32.43%)
/assets/images/config.gif -- 689.24kb -> 473.23kb (31.34%)
/assets/images/bookmarks.gif -- 291.46kb -> 222.71kb (23.59%)
/assets/images/explorer.gif -- 635.33kb -> 503.95kb (20.68%)
/assets/images/text-editor.gif -- 1,853.52kb -> 1,609.90kb (13.14%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-04-13 10:01:21 +00:00
veeso
b21607cd77 Use highlight symbol instead of an additional span 2021-04-13 09:17:07 +02:00
veeso
6c6dadc4e7 Removed eprintln! in ftp file transfer causing break when on Windows 2021-04-13 09:09:04 +02:00
veeso
08b8946429 Working on 0.4.2 2021-04-13 09:06:22 +02:00
Christian Visintin
23882df474 Create FUNDING.yml 2021-04-12 21:09:27 +02:00
Christian Visintin
d8547d8f21 fixed readme 2021-04-12 21:03:30 +02:00
veeso
c6de101808 Merge branch 'main' of github.com:veeso/termscp into main 2021-04-12 21:01:11 +02:00
veeso
d0688be5cb updated readme 2021-04-12 21:00:48 +02:00
Christian Visintin
35f37cc2d3 Removed codecov badge
tarpaulin is not saying the truth anyway
2021-04-07 15:58:19 +02:00
veeso
81ae310e3d Arch sha 2021-04-06 22:31:31 +02:00
veeso
3be890c63a Merge branch 'main' of github.com:veeso/termscp into main 2021-04-06 21:00:22 +02:00
veeso
64a08e1440 Scheduled release for 06/04/2021 2021-04-06 21:00:03 +02:00
veeso
fe5c35d789 Fixed cargo.toml 2021-04-06 20:59:02 +02:00
Christian Visintin
df391dfb6f Merge pull request #20 from veeso/issue-17-still-some-problems-with-symlinks
[BUG] Problems with symlinks
2021-04-05 18:07:45 +02:00
Christian Visintin
f5ac4207e8 Merge pull request #21 from maelvls/patch-1
One-liner for installing with Homebrew 😅
2021-04-05 17:40:39 +02:00
Maël Valais
b8c54b53d9 Readme: one-liner for Homebrew
The one-liner command

  brew install veeso/termscp/termscp

is equivalent to the two commands

  brew tap veeso/termscp
  brew install termscp
2021-04-05 17:22:30 +02:00
veeso
6be9294e11 stable toolchain 2021-04-05 10:04:53 +02:00
veeso
7676e6e3a1 Improved coverage 2021-04-05 09:59:10 +02:00
veeso
9776ecbe60 Removed codecov path fix 2021-04-05 09:40:57 +02:00
veeso
a1632492ed Restored coverage with tarpaulin 2021-04-05 09:32:59 +02:00
veeso
8d74d4c4e5 FTP: added support for symlinks for Linux servers 2021-04-04 18:03:11 +02:00
veeso
e29ce3d0dd Merge branch '0.4.1' into issue-17-still-some-problems-with-symlinks 2021-04-04 17:33:00 +02:00
veeso
e6b952966c SCP: fixed symlink not properly detected 2021-04-04 17:29:17 +02:00
Christian Visintin
1ba139aed1 Merge pull request #19 from veeso/issue-18-every-ftp-transfer-is-made-in-ascii-mode
[BUG] every ftp transfer is made in ascii mode
2021-04-04 17:18:49 +02:00
veeso
f136057484 File transfer errors: to_string instead of format! 2021-04-04 16:32:54 +02:00
veeso
47cd112e69 FTP: transfer type set to binary 2021-04-04 16:32:39 +02:00
veeso
871a02c8b5 Updated contributing, issue templates and docs 2021-04-04 16:08:16 +02:00
veeso
44ba1111af Fixed test for edit 0.1.3 2021-04-03 22:20:20 +02:00
veeso
52b35f9232 Updated dependencies 2021-04-03 17:45:02 +02:00
veeso
f8a448f5e9 Use default color if Text span part is Reset 2021-04-03 17:27:43 +02:00
Christian Visintin
37da49f4f8 Merge pull request #16 from veeso/issue-13-could-not-start-activity-manager-no-such-file-or-directory
[BUG] Issue 13 could not start activity manager no such file or directory
2021-04-03 17:19:23 +02:00
veeso
c0ae922264 format 2021-04-03 16:48:37 +02:00
veeso
91081cb86a Use thiserror to format error messages 2021-04-03 16:33:18 +02:00
veeso
af678802bb Added path to HostError; scan_dir won't fail if it is not possible to stat an entry 2021-04-03 16:21:37 +02:00
veeso
66068ec73c Merge branch '0.4.1' into issue-13-could-not-start-activity-manager-no-such-file-or-directory 2021-04-03 15:47:31 +02:00
Christian Visintin
32e939c183 Merge pull request #15 from veeso/issue-9-some-problems-with-symlinks-hash-and-backslash-characters
[BUG] Issue 9 some problems with symlinks hash and backslash characters
2021-04-02 22:31:56 +02:00
veeso
b610da16a9 Fix remote paths for Windows 2021-04-02 22:09:58 +02:00
veeso
5dfcba3c51 Merge branch '0.4.1' into issue-13-could-not-start-activity-manager-no-such-file-or-directory 2021-04-01 22:20:50 +02:00
veeso
d48e05cd74 Merge branch '0.4.1' of github.com:veeso/termscp into 0.4.1 2021-04-01 22:20:29 +02:00
veeso
6f4cb46d94 Merge branch 'main' into 0.4.1 2021-04-01 22:19:19 +02:00
veeso
5886d90d16 Merge branch '0.4.1' into issue-13-could-not-start-activity-manager-no-such-file-or-directory 2021-04-01 22:18:45 +02:00
Christian Visintin
ade7160c20 Merge pull request #11 from veeso/issue-10-port-number-isnt-correctly-retrieved-from-the-bookmarks.toml
Issue 10 port number isnt correctly retrieved from the bookmarks.toml
2021-04-01 22:06:22 +02:00
veeso
0c22b322ae Fixed sha256sum 2021-03-30 22:07:23 +02:00
veeso
a5ba118393 changelog 2021-03-29 21:16:14 +02:00
veeso
7acf119c77 Fixed port not being loaded from bookmarks into gui 2021-03-29 21:14:35 +02:00
veeso
bd00ba7971 Working on 0.4.1 2021-03-29 20:59:55 +02:00
veeso
f94a811dd9 Missing 'S' key in keymap 2021-03-28 16:55:26 +02:00
veeso
deb46eaf41 pkgbuild 2021-03-27 20:58:10 +01:00
veeso
7812d1c37b Fixed missing color for update notice 2021-03-27 20:43:45 +01:00
veeso
c2d401723f Updated contributing 2021-03-27 15:28:01 +01:00
veeso
142169ee42 Fixed logbox multi-lines not working properly; fixed exec command format 2021-03-27 14:59:46 +01:00
veeso
55e884889c Clippy 2021-03-27 12:17:35 +01:00
veeso
67e36fa38f Fixed system test which deleted the termscp configuration 2021-03-27 11:56:18 +01:00
veeso
3dbe024029 Fixed warnings 2021-03-27 11:41:47 +01:00
veeso
96b7aff3b6 format 2021-03-26 22:30:47 +01:00
veeso
1ad75adf87 License changed to MIT 2021-03-26 22:25:10 +01:00
veeso
423f84353d Clippy 2021-03-26 22:19:24 +01:00
veeso
de3983e53f Clippy: don't allow warnings 2021-03-26 21:51:53 +01:00
veeso
15bea93da8 find test is broken on windows 2021-03-26 21:46:23 +01:00
veeso
f5f84ac346 Fixed del_entry 2021-03-26 21:26:18 +01:00
veeso
d44e2d8eb0 Fixed log message 2021-03-26 21:04:04 +01:00
veeso
39bcd5e83b From now on cargo fmt must pass the tests 2021-03-26 20:42:13 +01:00
veeso
3dd6dbbb37 Mount find explorer instead of local/remote; don't use a popup 2021-03-26 20:41:08 +01:00
veeso
5a5d2fb162 fmt 2021-03-26 20:38:47 +01:00
veeso
08b855e779 Find dialog in view 2021-03-25 22:59:42 +01:00
veeso
8a22259eba Fixed tests on windows 2021-03-21 22:58:38 +01:00
veeso
24d727dec8 Fixed host tests 2021-03-21 22:58:32 +01:00
ChristianVisintin
93c4d1c2e3 File explorer features 2021-03-21 22:58:00 +01:00
ChristianVisintin
66f9ace7bd Find command now supports also directories 2021-03-21 22:57:45 +01:00
ChristianVisintin
f3788ef61a Find method for localhost 2021-03-21 22:57:37 +01:00
ChristianVisintin
b9d801e8bc find method for FileTransfer trait 2021-03-21 22:57:31 +01:00
veeso
3a1c6cac95 Exec command 2021-03-21 22:57:05 +01:00
veeso
8ff7040a0a Localhost exec for windows 2021-03-21 22:47:50 +01:00
veeso
118467e079 Exec on Localhos 2021-03-21 22:47:15 +01:00
veeso
51f0c56b84 File transfer exec command 2021-03-21 22:46:39 +01:00
ChristianVisintin
88a014807f Fixed duplicated bookmarks after overwrite 2021-03-21 22:45:33 +01:00
ChristianVisintin
7a5861f32f prevent infinite loops while performing stat on symbolic links pointing to themselves 2021-03-21 22:44:33 +01:00
veeso
63a627e4d0 bin/ no more accessible in github actions? 2021-03-21 22:42:09 +01:00
veeso
f0d87ff8c4 will_umount method in Activity 2021-03-21 17:16:52 +01:00
veeso
30c2aa144b FileTransferParams as member of Context 2021-03-21 16:38:11 +01:00
veeso
bf4f24ceec Removed unicode-width 2021-03-21 16:01:12 +01:00
veeso
3520499289 Removed index from explorer 2021-03-21 15:58:14 +01:00
veeso
977becd5c9 File transfer activity refactoring OK 2021-03-21 13:18:53 +01:00
veeso
fd4c4a3772 Handle transfer aborted 2021-03-21 12:59:47 +01:00
veeso
7c6a22d3e1 Fixed msgbox wrap 2021-03-21 12:56:31 +01:00
veeso
41360bb2c5 Wrap message box texts; renamed ctext to msgbox 2021-03-21 12:53:11 +01:00
veeso
9b00feb286 Fixed file list to refresh after download / upload 2021-03-21 12:23:46 +01:00
veeso
f2681ba0b9 Fixed centered text 2021-03-21 12:21:50 +01:00
veeso
7c9548c668 Prevent subtract with underflow 2021-03-21 12:16:11 +01:00
veeso
7caa4575bd Keep index in file_list if possible 2021-03-21 12:06:42 +01:00
veeso
16a8fc3ad8 Handle enter/space for explorers 2021-03-21 12:03:08 +01:00
veeso
cd31cc1fc9 File transfer activity refactoring 2021-03-20 21:06:12 +01:00
veeso
2f3c1e7f7f Removed unused keys from keymap 2021-03-20 20:36:41 +01:00
veeso
61404cfbec Get method for file explorer 2021-03-20 15:31:53 +01:00
veeso
12ec87235f Keymap 2021-03-20 15:31:20 +01:00
veeso
b822e2131a From String for TextSpan 2021-03-20 11:28:52 +01:00
veeso
3b99a5401f Migrated setup activity to new activity lifecycle 2021-03-17 21:00:26 +01:00
veeso
5156928bdc Merge branch 'rethink-context' into rethink-activities 2021-03-15 21:09:02 +01:00
veeso
20e7a66ded Working on setup activity; need to merge rethink-context 2021-03-15 21:00:55 +01:00
veeso
411f734aef changelog 2021-03-14 20:59:35 +01:00
veeso
2c898a91da Auth activity OK! 2021-03-14 20:56:36 +01:00
veeso
28f8c82ccf Fixed styles not properly being rendered on text 2021-03-14 20:55:25 +01:00
veeso
54342178e0 Don't blur if new active component is the same as before 2021-03-14 20:53:55 +01:00
veeso
11af0666ea Border to props 2021-03-14 20:48:44 +01:00
veeso
a72ecb39e0 Borders to component properties 2021-03-14 20:48:30 +01:00
veeso
47c23c6828 Fixed bookmark list colors 2021-03-14 20:40:27 +01:00
ChristianVisintin
00e2a1db31 Fixed crash due to bookmark delete 2021-03-14 19:37:25 +01:00
veeso
36cc6f445a Title component 2021-03-14 19:21:41 +01:00
veeso
ad9ed8facf Fixed radio colors 2021-03-14 17:12:02 +01:00
veeso
4a7eb831b8 Fixed radio colors 2021-03-14 17:09:05 +01:00
veeso
28d51fdcf6 Active component after blur 2021-03-14 17:08:23 +01:00
veeso
4ebb8a3b51 Fixed popups 2021-03-14 16:54:27 +01:00
veeso
e76dcd4638 Quit popup 2021-03-14 15:44:21 +01:00
veeso
eaada667b3 Fixed cursor 2021-03-14 15:35:02 +01:00
veeso
5bc46dd720 Something is working, but it is still unusable 2021-03-14 15:31:49 +01:00
veeso
fe6e0eeab5 Blur previous active component after active 2021-03-14 15:31:21 +01:00
veeso
7e075c5b3d Fixed cursor 2021-03-14 15:25:20 +01:00
veeso
86dfc2bf97 Blur previous active component after active 2021-03-14 15:24:47 +01:00
veeso
bda69c661f Layout utils 2021-03-14 14:30:37 +01:00
veeso
8f3fe14843 Removed who_has_focus method 2021-03-14 14:09:36 +01:00
veeso
5c952169b3 Removed layout 2021-03-14 14:09:23 +01:00
veeso
371ba5c399 Fixed input 2021-03-14 14:09:03 +01:00
veeso
0f4649ab8d ctext component 2021-03-14 14:08:27 +01:00
veeso
2b6f7e4868 Components will now render and set cursor 2021-03-14 12:22:50 +01:00
veeso
2e3dc7f7a5 Working on auth activity view 2021-03-13 17:30:57 +01:00
veeso
2d1af97590 who_has_focus method on View 2021-03-10 16:35:29 +01:00
veeso
1e813b0d4d Handle focus on umount 2021-03-10 15:35:08 +01:00
veeso
cdbfb3977b view render method 2021-03-10 14:22:00 +01:00
ChristianVisintin
e9d3684f87 Working on view 2021-03-10 12:29:41 +01:00
ChristianVisintin
021bcf0c97 Char 'E' in addition to <DEL> for bookmarks 2021-03-10 12:14:46 +01:00
ChristianVisintin
fba6da8120 auth activity update 2021-03-10 12:10:36 +01:00
ChristianVisintin
9dbfbd0dc3 View: return String instead of id 2021-03-10 11:26:40 +01:00
veeso
5980bc1fcb Working on activity refactoring 2021-03-09 21:52:11 +01:00
veeso
b2aaf5c57f Allow value update in input 2021-03-09 15:40:45 +01:00
veeso
e17224184e bookmarks list 2021-03-09 15:12:32 +01:00
veeso
042007d9ed Layout View 2021-03-09 14:19:52 +01:00
veeso
c832a6cb6f Codecov: ignore activities, context, input but not layout/ 2021-03-09 08:56:12 +01:00
veeso
3aed691cb8 Removed will_umount from components 2021-03-09 08:54:24 +01:00
veeso
7b92bd22e7 Removed ligatures 2021-03-09 08:17:46 +01:00
veeso
581badd101 Working on view 2021-03-09 08:16:28 +01:00
ChristianVisintin
c4bc0af58a Allow store in codecov 2021-03-08 14:23:42 +01:00
ChristianVisintin
f75dd5d4e3 Cache version fetched from Github 2021-03-08 14:20:13 +01:00
ChristianVisintin
a4544e35f6 Store as part of the Context 2021-03-08 13:57:16 +01:00
ChristianVisintin
56d705e253 Config client shared in the context 2021-03-08 12:01:40 +01:00
veeso
43c177e04d All components must have focus 2021-03-07 17:40:45 +01:00
veeso
5a2d0b7b0b Logbox 2021-03-07 12:41:00 +01:00
veeso
43298dee1c Table component 2021-03-07 12:24:58 +01:00
veeso
26014ecb58 Table in text parts 2021-03-07 12:05:47 +01:00
veeso
57dd06d774 Progress bar; use render_value for input 2021-03-07 11:32:55 +01:00
veeso
e21eb72705 Text component 2021-03-06 22:48:54 +01:00
veeso
00b1dbdffa TextSpanBuilder 2021-03-06 20:49:39 +01:00
veeso
55f74a8244 TextSpan instead of strings 2021-03-06 20:34:32 +01:00
veeso
db0c54b781 PropsBuilder: use from trait 2021-03-06 20:15:23 +01:00
veeso
5c9cb7eece Merge branch '0.4.0' into rethink-activities 2021-03-06 16:19:20 +01:00
veeso
b2d816d20c Radio group 2021-03-06 16:16:36 +01:00
veeso
5b832cea8b Input tests 2021-03-06 15:46:11 +01:00
veeso
c1780230e9 Changed Unumber and Number names 2021-03-06 15:10:54 +01:00
veeso
b90953f65e PropValue enum 2021-03-06 15:10:19 +01:00
veeso
44041863ad typo 2021-03-04 20:08:15 +01:00
veeso
2692041329 Moved focus to states 2021-03-04 20:06:59 +01:00
veeso
4a8ea185e6 Empty structs 2021-03-04 15:49:13 +01:00
veeso
98861a6bbd Working on 0.4.0 2021-03-04 15:05:27 +01:00
veeso
135d947c39 Working on input 2021-03-04 15:03:29 +01:00
veeso
f3cbbb8d81 File list tests 2021-03-04 13:55:10 +01:00
veeso
3ecf172fb5 Input len 2021-03-04 13:36:07 +01:00
veeso
c9871a0079 file list 2021-03-04 09:13:29 +01:00
veeso
744e5a251a Render struct instead of Widget; get_value method 2021-03-04 08:53:49 +01:00
veeso
e89198d9bb InputType prop 2021-03-04 08:53:19 +01:00
veeso
e61e0c018c File list component 2021-03-03 22:02:58 +01:00
veeso
b57763e688 Msg instead of callbacks 2021-03-03 15:47:25 +01:00
veeso
3ccbb325b3 Defined Component and State 2021-03-03 12:08:47 +01:00
veeso
3ea345ee8f Layout props tests 2021-03-03 09:32:53 +01:00
veeso
ed2c50daac Defined properties 2021-03-02 21:01:36 +01:00
ChristianVisintin
f27d5ea08f diagrams of new arch 2021-03-02 15:37:24 +01:00
veeso
35ab9ae202 Added githubActions features to handle github tests; set git fetch test under github actions exclude pattern 2021-03-01 20:33:04 +01:00
veeso
bd99665d1c AUR ok 2021-02-28 21:23:22 +01:00
veeso
93bab299ec debian 8 is too old? 2021-02-28 21:07:51 +01:00
veeso
eb33f93322 rpm name changed 2021-02-28 16:24:24 +01:00
veeso
d165b699f0 Removed archlinux from build (it doesn't work anymore 2021-02-28 16:01:07 +01:00
veeso
7817295d8e The archlinux image is so broken 2021-02-28 15:12:34 +01:00
veeso
a986e531d9 Docker is broken 2021-02-28 15:11:08 +01:00
veeso
8a3b652dcd It seems they broke the archlinux docker image with latest version (gg) 2021-02-28 15:07:44 +01:00
Christian Visintin
efbea63154 Merge pull request #7 from veeso/fetch-new-release
Check for updates through Github API
2021-02-28 14:47:55 +01:00
veeso
61045fa548 Check for updates OK 2021-02-28 13:10:59 +01:00
veeso
da5e1f315d Show new version available in auth activity 2021-02-28 13:01:51 +01:00
veeso
85c57ce027 Handle check for updates in setup activity 2021-02-28 12:47:55 +01:00
veeso
6682c07eb6 Added check_for_updates to config 2021-02-28 12:44:00 +01:00
veeso
4e887c3429 Git: check for new updates (utils) 2021-02-28 12:33:12 +01:00
veeso
6435271be8 Parse semver util 2021-02-28 12:21:28 +01:00
veeso
c9a77fa65d 0.3.3 is ready for release I guess 2021-02-27 20:57:07 +01:00
veeso
cc5399d36e Cargo clippy 2021-02-27 20:49:20 +01:00
veeso
ca1aa5675a Try test threads: 1 2021-02-27 20:30:50 +01:00
ChristianVisintin
e21bfbbd14 Use a regex to parse the remote host args 2021-02-26 16:56:03 +01:00
ChristianVisintin
e948d598b0 Convert to lowercase when sorting bookmarks 2021-02-26 08:13:38 +01:00
Christian Visintin
0173d67a3b Merge pull request #6 from veeso/fmt-props
Format key attributes
2021-02-25 20:08:01 +01:00
veeso
025547a3dc Format key attributes 2021-02-25 17:47:50 +01:00
veeso
af830d603d Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime) 2021-02-25 16:15:06 +01:00
veeso
669fd23868 Support for older distributions 2021-02-25 14:43:27 +01:00
veeso
4ff7fc079c Added CLI options to set starting working directory on both local and remote hosts 2021-02-25 14:27:34 +01:00
ChristianVisintin
7f24d6db5c Default choice for deleting file set to NO (way too easy to delete files by mistake) 2021-02-16 12:53:28 +01:00
ChristianVisintin
8c8d01c29c working on 0.3.3 2021-02-16 12:50:04 +01:00
ChristianVisintin
d23e6bb60c centos7 dockerfile 2021-01-28 12:03:11 +01:00
veeso
87aa900bc6 0.3.2 2021-01-24 20:48:46 +01:00
veeso
e8e4cd22a1 0.3.2 ready 2021-01-24 20:31:24 +01:00
veeso
780cf592e4 Updated dependencies 2021-01-24 12:04:27 +01:00
Christian Visintin
45db58a0b3 Merge pull request #5 from veeso/ls-fmt
File explorer formatter
2021-01-24 11:59:31 +01:00
veeso
f5218bc582 test_utils_fmt_path_elide: don't run on windows 2021-01-24 11:48:00 +01:00
veeso
c5e2e02415 Optimized formatter: instead of replacing in fmt_str, keep 'prefix' in the call chain 2021-01-24 11:29:20 +01:00
veeso
e088772685 Docs 2021-01-23 16:38:46 +01:00
veeso
859daa3107 Clippy 2021-01-23 16:38:36 +01:00
veeso
56c580fc80 Use file_fmt in explorer activity 2021-01-23 16:26:25 +01:00
veeso
7a9ee697ff Added fmt_file to setupt activity 2021-01-23 16:22:32 +01:00
veeso
c16a2f6441 Improved getters/setters config client 2021-01-23 16:21:30 +01:00
veeso
b3c4385617 Added to ConfigClient getters/setters for file_fmt 2021-01-23 16:09:13 +01:00
veeso
e92370bd05 Added file_fmt to configuration 2021-01-23 16:00:41 +01:00
veeso
da0d5231bf Use formatter to fmt fs entries instead of fmt::Display trait 2021-01-23 15:51:46 +01:00
veeso
c1f6308795 Explorer formatter module 2021-01-23 15:41:07 +01:00
veeso
54ab24fc0c fmt_path_elide 2021-01-23 15:03:43 +01:00
veeso
d99efb9de4 SCP File transfer: when listing directory entries, check if a symlink points to a directory or to a file 2021-01-23 12:20:34 +01:00
ChristianVisintin
0c9ed38eb7 Solved file index in explorer files at start of termscp, in case the first entry is an hidden file 2021-01-19 09:13:08 +01:00
ChristianVisintin
ec801f1555 Working on 0.3.2 2021-01-19 09:12:59 +01:00
veeso
e0d8879961 typos 2021-01-18 19:09:54 +01:00
veeso
3f2be65a6c termscp 0.3.1 2021-01-18 17:56:13 +01:00
veeso
a5a745e444 0.3.1 2021-01-18 17:36:53 +01:00
Christian Visintin
7ee142314e Merge pull request #4 from Fenex/refactoring/1
Refactoring FtpFileTransfer::parse_unix_list_line
2021-01-18 14:25:24 +01:00
Christian Visintin
c412d98ec7 Merge branch '0.3.1' into refactoring/1 2021-01-18 08:07:00 +01:00
Vitaliy Busko
d7e5eacd79 Refactoring ScpFileTransfer::parse_ls_output 2021-01-18 11:31:35 +07:00
Vitaliy Busko
367fb235f6 Refactoring FtpFileTransfer::parse_unix_list_line 2021-01-18 10:51:22 +07:00
Christian Visintin
d68d63b978 Merge pull request #3 from veeso/keystorage
Keystorage
2021-01-16 18:10:18 +01:00
ChristianVisintin
23ca2baa8c Cargo clippy 2021-01-16 18:02:12 +01:00
ChristianVisintin
ac02928e69 Don't run bookmarks tests on macos 2021-01-16 17:37:03 +01:00
ChristianVisintin
cb20589b01 Macos test thread (?) 2021-01-16 17:27:07 +01:00
ChristianVisintin
1acbf89717 keyring ok 2021-01-16 17:13:41 +01:00
ChristianVisintin
08d8a3621c Keyring storage in bookmarks client (if possible) 2021-01-16 16:57:00 +01:00
ChristianVisintin
0192b86422 Check if supported (test) 2021-01-16 16:07:53 +01:00
ChristianVisintin
0e4caaecfd Keyring storage 2021-01-16 16:07:11 +01:00
ChristianVisintin
215927d432 Fixed copyright header 2021-01-16 15:37:29 +01:00
ChristianVisintin
eee08bd623 Key storage (file) 2021-01-16 15:37:19 +01:00
ChristianVisintin
76fdd9864c Fixed copyright header 2021-01-16 15:13:58 +01:00
ChristianVisintin
350443ec99 SCP file transfer: fixed possible wrong file size when sending file, due to a possible incoherent size between the file explorer and the actual file size 2021-01-16 11:49:59 +01:00
ChristianVisintin
928fc1b450 Solved index of files list no more kept after 0.3.0 (use set_abs_index instead) 2021-01-16 11:35:33 +01:00
ChristianVisintin
03e1bf53d0 Solved index of files list no more kept after 0.3.0 2021-01-16 11:16:31 +01:00
ChristianVisintin
9330025d07 Connection timeout for SFTP/SCP clients 2021-01-16 10:58:07 +01:00
ChristianVisintin
bf56a269e0 Replaced Box<dyn Iterator... with impl Iterator 2021-01-16 10:37:53 +01:00
ChristianVisintin
0393c1a850 working on 0.3.1... 2021-01-16 10:33:34 +01:00
ChristianVisintin
69ece00ae2 working on 0.3.1... 2021-01-16 10:31:24 +01:00
Christian Visintin
97656536d4 Cargo build requirements 2021-01-13 10:49:46 +01:00
Christian Visintin
94b78d85ef Merge pull request #1 from Byron/main
run `cargo diet` to reduce crate size by 85%
2021-01-11 08:11:52 +01:00
Sebastian Thiel
ba3a888d26 run cargo diet to reduce crate size by 85%
I noticed the crate is pretty big and took a quick look, here
is the outcome of slimming it down.

```
➜  termscp git:(main) cargo diet
┌───────────────────────────────────────────┬─────────────┐
│ File                                      │ Size (Byte) │
├───────────────────────────────────────────┼─────────────┤
│ codecov.yml                               │          96 │
│ .github/ISSUE_TEMPLATE/feature_request.md │         203 │
│ dist/deb.sh                               │         210 │
│ .github/workflows/macos.yml               │         319 │
│ .github/workflows/windows.yml             │         321 │
│ dist/rpm.sh                               │         336 │
│ dist/build/README.md                      │         345 │
│ .gitignore                                │         356 │
│ dist/build/x86_64/Dockerfile              │         509 │
│ dist/pkgs/arch/.SRCINFO                   │         511 │
│ .github/workflows/aur-pub.yml             │         570 │
│ dist/pkgs/arch/PKGBUILD                   │         580 │
│ .github/ISSUE_TEMPLATE/bug_report.md      │         598 │
│ dist/build/x86_64_archlinux/Dockerfile    │         905 │
│ .github/workflows/linux.yml               │        1013 │
│ .github/PULL_REQUEST_TEMPLATE.md          │        1093 │
│ dist/build/deploy.sh                      │        1291 │
│ docs/drawio/UI.drawio                     │        1993 │
│ CODE_OF_CONDUCT.md                        │        3368 │
│ CONTRIBUTING.md                           │       10756 │
│ assets/images/bookmarks.gif               │      298453 │
│ assets/images/auth.gif                    │      321769 │
│ assets/images/explorer.gif                │      650583 │
│ assets/images/config.gif                  │      705780 │
│ assets/images/text-editor.gif             │     1898000 │
└───────────────────────────────────────────┴─────────────┘
Saved 85% or 3.9 MB in 25 files (of 4.6 MB and 75 files in entire crate)
```

Please let me know if you would like some other files to be included
or whatever else is needed to make this PR mergeable.

Thanks :).
2021-01-11 09:09:50 +08:00
veeso
d62a6e98c8 0.3.0 2021-01-10 18:22:53 +01:00
veeso
eeed99b013 Merge branch '0.3.0' into main 2021-01-10 17:31:31 +01:00
veeso
2b54326334 0.3.0 ready 2021-01-10 17:30:20 +01:00
veeso
2bd3d33ff6 Updated copyright 2021-01-09 20:45:06 +01:00
veeso
c54a9ef5c9 termscp 0.3.0 ready 2021-01-09 20:35:55 +01:00
veeso
a7ae0159cc config gif 2021-01-09 15:28:20 +01:00
veeso
f981602221 Definetely fixed FTP issues 2021-01-09 15:05:49 +01:00
veeso
fa5468be4a Fixed time check tests 2021-01-09 14:29:30 +01:00
veeso
591182414f Updated references to veeso 2021-01-08 22:02:40 +01:00
veeso
49790b4704 Updated references to veeso 2021-01-02 13:04:49 +01:00
ChristianVisintin
daa3b3e549 Fixed 0 B/S transfer rate displayed after completing download in less than 1 second 2020-12-28 22:54:28 +01:00
ChristianVisintin
120dc8ecb4 Fixed buffer sizes for transfers 2020-12-28 22:45:28 +01:00
ChristianVisintin
06a2373776 parse dos line test 2020-12-27 20:42:52 +01:00
ChristianVisintin
32ae5cc182 Removed test for macos 2020-12-27 19:28:41 +01:00
ChristianVisintin
6975beaf30 Fixed file extension not found in SCP/FTP 2020-12-27 15:08:57 +01:00
ChristianVisintin
c141c6c44d Added LIST command parser for Windows server (DOS-like syntax) 2020-12-27 15:08:42 +01:00
ChristianVisintin
68cd77a9b3 Added utils::parser::parse_datetime 2020-12-27 12:01:20 +01:00
ChristianVisintin
e20a78acef Don't collapse bookmarks tabs 2020-12-27 11:05:01 +01:00
ChristianVisintin
6dd4cfaa3c InputMode as Option<Popup> in AuthActivity 2020-12-27 11:03:44 +01:00
ChristianVisintin
d756bf7786 InputMode as Option<Popup> in FileTransferActivity 2020-12-27 10:59:12 +01:00
ChristianVisintin
65e7ff22f7 Explorers: append '/' to directories name 2020-12-27 10:47:11 +01:00
ChristianVisintin
655084c5f6 typo 2020-12-27 10:36:48 +01:00
ChristianVisintin
09bc8a92a2 show_hidden_files and group_dirs in termscp configuration; instantiate FileExplorer based on current configuration in FileTransferActivity 2020-12-27 10:31:33 +01:00
ChristianVisintin
99fd0b199d FileTransferActivity: sort files with <B> 2020-12-26 21:47:48 +01:00
ChristianVisintin
740d906eb3 ToString, FromStr for FileSorting and GroupDirs 2020-12-26 19:03:54 +01:00
ChristianVisintin
b137fecc12 FileSorting and GroupDirs as enums 2020-12-26 18:55:14 +01:00
ChristianVisintin
14125f673a Added options to explorer, in order to define sorting modes and other options. Added bitflags to dependencies; Moved Explorer to Fs module 2020-12-26 17:29:12 +01:00
ChristianVisintin
4911cc5410 Removed issues badge 2020-12-26 15:53:14 +01:00
ChristianVisintin
e0d9ac2ed8 FileTransferActivity::Explorer refactoring; toggle hidden files with <A> 2020-12-26 15:50:57 +01:00
ChristianVisintin
5b042e86ef FsEntry::is_hidden() method 2020-12-26 11:58:28 +01:00
ChristianVisintin
8ccf5eb0bb FsEntry::get_name() returns &str 2020-12-26 10:51:01 +01:00
ChristianVisintin
c0fdc9b8f8 Added new keybindings to help; log new file created 2020-12-26 10:40:45 +01:00
ChristianVisintin
545544ebe2 Added test to config client 2020-12-26 10:33:23 +01:00
ChristianVisintin
644ea1566c Create new file with <N> 2020-12-26 10:16:01 +01:00
ChristianVisintin
f6d1f24b60 cargo clippy 2020-12-26 09:40:24 +01:00
ChristianVisintin
c9a4706c24 Fixed paths 2020-12-25 21:54:10 +01:00
ChristianVisintin
7e275a9075 Docs: private keys with passwords 2020-12-25 21:35:28 +01:00
ChristianVisintin
22a9eb03b6 Added configuration and ssh key storage features to docs 2020-12-25 19:45:49 +01:00
ChristianVisintin
46ee01e073 SetupActivity: <CTRL+E> as <DEL> 2020-12-25 19:38:17 +01:00
ChristianVisintin
16a011e81e Use default protocol also in opt parser 2020-12-25 19:10:28 +01:00
ChristianVisintin
90f28d9f27 SetupActivity ok 2020-12-25 18:39:18 +01:00
ChristianVisintin
2e4ff78124 Refuse empty ssh key 2020-12-25 18:22:12 +01:00
ChristianVisintin
9e66207bf7 SetupActivity layout 2020-12-25 18:20:30 +01:00
ChristianVisintin
226ad8cc50 Show CTRL+C to enter setup in auth activity 2020-12-25 17:25:35 +01:00
ChristianVisintin
00731d67d2 ToString for protocol in AuthActivity 2020-12-25 16:44:01 +01:00
ChristianVisintin
e354d17c70 SetupActivity logic 2020-12-25 16:41:49 +01:00
ChristianVisintin
264b5afad6 Typo in system 2020-12-25 11:43:32 +01:00
ChristianVisintin
76c4f1b67f Close popups also with <ENTER> 2020-12-25 10:00:24 +01:00
ChristianVisintin
96a395615b FileTransferActivity: load ConfigClient; set text editor to configuration's value 2020-12-24 19:01:42 +01:00
ChristianVisintin
82d8bd0342 Read default protocol in auth activity 2020-12-24 18:47:00 +01:00
ChristianVisintin
39e8d1f704 AuthActivity: enter setup with <CTRL+C> 2020-12-24 18:27:38 +01:00
ChristianVisintin
6bf503331e SSH key storage in scp/sftp file transfers 2020-12-24 17:27:57 +01:00
ChristianVisintin
920d3b4af4 get_bookmarks_paths; get_config_paths 2020-12-24 17:19:47 +01:00
ChristianVisintin
f601a0451c fix relative paths codecov 2020-12-24 17:01:11 +01:00
ChristianVisintin
841db02b30 empty method sshkey_storage 2020-12-24 16:57:04 +01:00
ChristianVisintin
1449b5a524 SSH Key Storage 2020-12-24 16:49:02 +01:00
ChristianVisintin
fbb60c7cbc ConfigClient: return key path, not content 2020-12-24 16:42:42 +01:00
ChristianVisintin
f0144e3bc2 ConfigClient 2020-12-24 16:21:53 +01:00
ChristianVisintin
ce924bb294 random utils 2020-12-24 11:23:31 +01:00
ChristianVisintin
797174446c Config module 2020-12-24 11:12:02 +01:00
ChristianVisintin
9b9dc43a7f FileTransferProtocol ToString and FromStr traits 2020-12-24 10:03:48 +01:00
ChristianVisintin
53ee0f618c Updated known issues 2020-12-23 17:49:30 +01:00
ChristianVisintin
c35a5590b5 Updated dependencies 2020-12-22 18:33:38 +01:00
ChristianVisintin
d23fe09f86 fmt 2020-12-22 17:35:07 +01:00
ChristianVisintin
e761a90826 Crypto as utility module 2020-12-22 17:34:52 +01:00
ChristianVisintin
b5abe4538f Replaced sha256 sum with last modification time check, to verify if a file has been changed in the text editor 2020-12-22 17:23:16 +01:00
ChristianVisintin
7202b19d45 Working on 0.3.0 2020-12-22 17:14:44 +01:00
ChristianVisintin
88f9c8910b Fixed download paths 2020-12-21 15:12:47 +01:00
ChristianVisintin
c9948162b9 lol, for some reason cargo-aur renames my crate into termscp-bin. WHY OMG WHY. 2020-12-21 14:42:26 +01:00
ChristianVisintin
4e8e0d1b41 Fixed that f aur action 2020-12-21 14:33:00 +01:00
ChristianVisintin
95b7a773d7 aur runs on 2020-12-21 14:24:25 +01:00
ChristianVisintin
07c924d6a0 0.2.0 2020-12-21 14:21:04 +01:00
ChristianVisintin
50efebdd63 Fixed deploy script 0.2.0 2020-12-21 14:07:43 +01:00
ChristianVisintin
50b8f3cd71 Improved abs paths in host 2020-12-21 13:59:03 +01:00
ChristianVisintin
093dc4f33d Fixed test 2020-12-21 13:43:05 +01:00
ChristianVisintin
963ef88ae5 Arch package (0.2.0) 2020-12-21 12:22:13 +01:00
ChristianVisintin
03e63e11ac 0.2.0 2020-12-21 12:06:44 +01:00
ChristianVisintin
9f478227a9 0.2.0 readme 2020-12-21 12:02:45 +01:00
ChristianVisintin
1d112b3f32 Arch publish workflow and build with docker 2020-12-21 11:48:41 +01:00
ChristianVisintin
1a5bd394b6 Optimizations 2020-12-21 11:12:49 +01:00
ChristianVisintin
3901ed54c6 Copy feature in ui; new keybinding <C> 2020-12-21 11:11:29 +01:00
ChristianVisintin
08728bf55e Copy method (host/transfer) 2020-12-21 10:49:31 +01:00
ChristianVisintin
3220d00b14 archlinux dist 2020-12-21 10:27:33 +01:00
ChristianVisintin
eb12da0308 Utils into multiple files 2020-12-20 15:36:48 +01:00
ChristianVisintin
77545ec87d Text editor documentation 2020-12-20 15:22:52 +01:00
ChristianVisintin
3f6f03af33 Text editor input and session 2020-12-20 15:05:22 +01:00
ChristianVisintin
47f9b39630 Sha256 in utils 2020-12-20 14:49:32 +01:00
ChristianVisintin
9171e0789f Moved recv_file and send_file to an independent method (fix) 2020-12-20 14:47:37 +01:00
ChristianVisintin
5eabaf8ac2 Moved recv_file and send_file to an independent method 2020-12-20 12:33:19 +01:00
ChristianVisintin
b8ad1e7feb edit file method 2020-12-19 21:49:53 +01:00
ChristianVisintin
4d7ea1cdb4 Context methods 2020-12-19 21:47:03 +01:00
ChristianVisintin
021f860ca6 context enter_alternate_screen leave_alternate_screen clear_screen 2020-12-19 21:39:31 +01:00
ChristianVisintin
d0774fd7ed FsEntry::is_file 2020-12-19 21:38:56 +01:00
ChristianVisintin
5632ac6f0b Optimizing log code 2020-12-19 21:12:30 +01:00
ChristianVisintin
7a115e5dc3 wrkdir as member of FileExplorer 2020-12-19 21:10:00 +01:00
ChristianVisintin
d95cda3dfc Optimizing log code 2020-12-19 21:05:56 +01:00
ChristianVisintin
dd9f54acae Working on text-editor 2020-12-19 21:04:55 +01:00
ChristianVisintin
6bc2bcb89e Include set_permissions in UNIX/macos/linux only 2020-12-18 20:58:57 +01:00
ChristianVisintin
9c2e751e11 Rounded borders; collapsed bookmarks borders 2020-12-18 20:54:34 +01:00
ChristianVisintin
2a52a19552 Display transfer speed 2020-12-18 17:06:17 +01:00
ChristianVisintin
1b99d63c47 Added one more space to size in FsEntry::fmt, since sometimes size has 4 digits 2020-12-18 16:42:06 +01:00
ChristianVisintin
33683bc8ce SFTP speed issue marked as solved 2020-12-18 16:16:23 +01:00
ChristianVisintin
61b4a3b76e Log how long it took to download/upload a file 2020-12-18 16:14:23 +01:00
ChristianVisintin
386db6278b Updated github uri 2020-12-18 14:47:48 +01:00
ChristianVisintin
47664b98ad Updated github uri 2020-12-18 14:47:32 +01:00
ChristianVisintin
898b57943b Improved test coverage 2020-12-18 14:40:37 +01:00
ChristianVisintin
d37cc4f796 test name elide in fs fmt 2020-12-18 12:27:37 +01:00
ChristianVisintin
b3fed60d12 Fixed fmt explorer windows 2020-12-18 12:06:54 +01:00
ChristianVisintin
a3d1db3fa2 Fs test fmt 2020-12-18 11:56:08 +01:00
ChristianVisintin
0a79fb3687 Scp: when username was not provided, it didn't fallback to current username 2020-12-18 11:40:51 +01:00
ChristianVisintin
900d9ac3c6 Apply file mode of file downloaded from remote 2020-12-18 11:31:51 +01:00
ChristianVisintin
50b523f9f4 codecov.yml 2020-12-18 10:49:41 +01:00
ChristianVisintin
0759651ee4 codecov 2020-12-18 10:47:37 +01:00
ChristianVisintin
daedee3f66 Documentation about bookmarks 2020-12-16 21:09:59 +01:00
ChristianVisintin
61c58e227e Merge branch 'bookmarks' into 0.2.0 2020-12-16 20:42:47 +01:00
ChristianVisintin
3cd9fc407e Bookmarks layout OK 2020-12-16 20:41:42 +01:00
ChristianVisintin
dd6e2be75d Panic if bookmark name is empty 2020-12-16 20:39:35 +01:00
ChristianVisintin
38e015efe4 removed args from SaveBookmark 2020-12-16 20:34:36 +01:00
ChristianVisintin
335bfc8460 Added 'save password' tab to auth activity when saving bookmarks 2020-12-16 17:01:11 +01:00
ChristianVisintin
344bf8604f Fixed crash in auth_activity 2020-12-16 16:51:21 +01:00
ChristianVisintin
562a1b3ae8 Clippy optimizations 2020-12-16 16:35:11 +01:00
ChristianVisintin
65c541ff2a bookmarks client OK 2020-12-16 16:32:50 +01:00
ChristianVisintin
14ddba022f Implemented BookmarkClient in AuthActivity 2020-12-16 16:01:29 +01:00
ChristianVisintin
10df5abae2 Added Option<String> password to bookmarks 2020-12-16 16:00:43 +01:00
ChristianVisintin
443789698b Copy trait for FileTransferProtocol 2020-12-16 15:57:46 +01:00
ChristianVisintin
52df9bc73b Macro use for magic-crypt 2020-12-16 15:47:02 +01:00
ChristianVisintin
8cb3637954 Magic-crypt 2020-12-16 15:46:46 +01:00
ChristianVisintin
ee55d1fd31 Bookmarks client 2020-12-16 12:09:34 +01:00
ChristianVisintin
dcc289153f Working on bookmarksClient 2020-12-15 22:17:19 +01:00
ChristianVisintin
940d8d94e5 Moving bookmarks from ui to system 2020-12-15 20:48:43 +01:00
ChristianVisintin
c9f1086408 Merge branch 'bookmarks' into 0.2.0 2020-12-15 16:45:34 +01:00
ChristianVisintin
ff4f35e5f5 Clippy ok 2020-12-15 16:31:21 +01:00
ChristianVisintin
b865fed7e9 Removed bookmarks from upcoming features (since implemented) 2020-12-15 16:23:09 +01:00
ChristianVisintin
274c5e309b Set password as InputField after loading bookmark 2020-12-15 16:17:56 +01:00
ChristianVisintin
e7d53a7d00 Help page in auth activity 2020-12-15 15:00:21 +01:00
ChristianVisintin
99117e067e Bookmarks layouts 2020-12-15 14:46:59 +01:00
ChristianVisintin
88e27ee8fe New popup handlers 2020-12-15 14:18:16 +01:00
ChristianVisintin
d3fe546264 Callbacks and handler for Bookmarks events 2020-12-15 14:15:35 +01:00
ChristianVisintin
982b3ec8d0 Delete recents/bookmarks 2020-12-15 14:03:50 +01:00
ChristianVisintin
b8cf2bea77 load bookmarks 2020-12-15 13:55:41 +01:00
ChristianVisintin
a511cd4ac3 Read one input event at a time 2020-12-15 13:44:51 +01:00
ChristianVisintin
0bab9a77a2 Working on bookmarks 2020-12-15 12:28:06 +01:00
ChristianVisintin
1f9b616de7 Bookmarks in auth activity (logic) 2020-12-15 12:22:47 +01:00
ChristianVisintin
71593e3ea7 PartialEq for bookmark 2020-12-15 12:18:35 +01:00
ChristianVisintin
db7ee624e3 Bookmark default 2020-12-15 12:03:31 +01:00
ChristianVisintin
0e7527fb3f file_exists as pub 2020-12-15 11:36:07 +01:00
ChristianVisintin
50fcba63c4 Bookmarks serializer and data types 2020-12-15 11:13:29 +01:00
ChristianVisintin
8046f82214 AuthActivity refactoring 2020-12-15 09:21:52 +01:00
ChristianVisintin
c33045b4b8 Refactoring auth activity... 2020-12-15 09:14:08 +01:00
ChristianVisintin
bb2a60e776 Updated gif 2020-12-14 19:26:16 +01:00
ChristianVisintin
04e2dc86e8 TermSCP 0.1.3 2020-12-14 19:11:42 +01:00
ChristianVisintin
ad44f020a5 Highlight selected entry in tabs, only when the tab is active 2020-12-14 15:52:01 +01:00
ChristianVisintin
ad339aa959 Added missing help entry 2020-12-14 15:47:41 +01:00
ChristianVisintin
3ea8188805 Docker build and deploy 2020-12-14 15:35:41 +01:00
ChristianVisintin
18aacef89f Bookmarks in upcoming features 2020-12-14 13:54:42 +01:00
ChristianVisintin
b8b29371ca Reload directory content with '<L>' 2020-12-14 13:52:49 +01:00
ChristianVisintin
bd99a0c2b2 align auth popup error to center 2020-12-14 13:39:47 +01:00
ChristianVisintin
c493ba45a8 align_text_center as part of utils 2020-12-14 13:33:24 +01:00
ChristianVisintin
e9d5093a52 Read buffer is now 65536 bytes long 2020-12-14 10:03:20 +01:00
ChristianVisintin
7aa8a892f8 Cargo toml 0.1.3 2020-12-14 09:14:58 +01:00
ChristianVisintin
99a3b64833 Explorer tabs have now 70% of layout height, while logging area is 30% 2020-12-14 09:14:23 +01:00
ChristianVisintin
d94c331569 Fixed keybindings readme 2020-12-14 09:03:57 +01:00
ChristianVisintin
e2e25e4ac1 Fixed memory vulnerability in Windows version 2020-12-14 09:03:40 +01:00
ChristianVisintin
2b8be4d1e9 Fixed color mismatch in local explorer tab (Yellow/LightYellow) 2020-12-14 09:01:35 +01:00
ChristianVisintin
db38a53172 Filemode explorer.gif 2020-12-14 08:55:43 +01:00
ChristianVisintin
29f7e3c759 0.1.2 2020-12-13 20:42:20 +01:00
ChristianVisintin
83bc7db048 Optimized performance for file transfer 2020-12-13 11:43:04 +01:00
ChristianVisintin
e273192f19 Working on 0.2.0 2020-12-13 10:27:21 +01:00
ChristianVisintin
55d05be626 README 2020-12-13 10:25:22 +01:00
ChristianVisintin
5c26b10671 0.1.2 is ready for release 2020-12-13 10:04:27 +01:00
ChristianVisintin
d29428a53b Reduced layout margin 2020-12-13 10:02:54 +01:00
ChristianVisintin
2a8ad5e0ed Selected file has now colourful background, instead of foreground, for a better readability 2020-12-13 09:55:16 +01:00
ChristianVisintin
f06f718b47 when file index is at the end of the list, moving down will set the current index to the first element and viceversa 2020-12-13 09:46:36 +01:00
ChristianVisintin
db532cc4b7 Added <CTRL+C> to help page 2020-12-13 09:41:41 +01:00
ChristianVisintin
962843bb97 Log transfer aborted 2020-12-13 09:39:12 +01:00
ChristianVisintin
619ac4e753 Windows fix for fs mod 2020-12-12 22:04:59 +01:00
ChristianVisintin
a8a9cb9d2e Abort upload/download pressing Ctrl+C 2020-12-12 22:00:12 +01:00
ChristianVisintin
3e72787625 Transfer states 2020-12-12 21:32:11 +01:00
ChristianVisintin
794dfb8cee Upcoming features 2020-12-12 19:14:45 +01:00
ChristianVisintin
d89839443d Trim hostname 2020-12-12 18:04:47 +01:00
ChristianVisintin
99db7b6d8e Fixed FsEntry fmt 2020-12-12 17:50:01 +01:00
ChristianVisintin
1fb614642c Added 'E' to keybindings; behaves as <DEL> (added because some keyboards don't have <DEL> 2020-12-12 17:44:04 +01:00
ChristianVisintin
3b33c55d97 Removed redundant code for fsEntry fmt 2020-12-12 17:36:39 +01:00
ChristianVisintin
5aea1d286b Code optimizations 2020-12-12 17:34:15 +01:00
ChristianVisintin
3dcf33ebe2 Optimized code, with the newest fsentry methods 2020-12-12 17:33:15 +01:00
ChristianVisintin
912da10696 When downloading symlinks, the filename and its size is now known (thanks to the new symlinks management) 2020-12-12 17:32:49 +01:00
ChristianVisintin
8281a840a7 Optimizing FsEntry and relatd stuff 2020-12-12 17:03:31 +01:00
ChristianVisintin
57e83a63dd Optimized code for fsentry 2020-12-12 16:32:21 +01:00
ChristianVisintin
f73e43304e FsEntry::*::symlink is now a Option<Box<FsEntry>>; this improved symlinks, which gave errors some times 2020-12-12 16:26:03 +01:00
ChristianVisintin
23ac5089a7 Added clippy to workflows 2020-12-12 12:17:33 +01:00
ChristianVisintin
55bda874f0 Optimized code and performance using clippy 2020-12-12 12:14:51 +01:00
ChristianVisintin
0eae159bb9 Working on 0.1.2 2020-12-12 10:06:11 +01:00
ChristianVisintin
6d49fe4e7d changed emoji in readme 2020-12-10 14:26:39 +01:00
ChristianVisintin
5b8c03babd typo in readme 2020-12-10 14:26:13 +01:00
144 changed files with 28631 additions and 7449 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/veeso']

View File

@@ -1,9 +1,9 @@
---
name: Bug report
about: Create a report to help me improving TermSCP
title: "[BUG]"
about: Create a report of the bug you've encountered
title: "[BUG] - ISSUE_TITLE"
labels: bug
assignees: ChristianVisintin
assignees: veeso
---
@@ -24,10 +24,15 @@ A clear and concise description of what you expected to happen.
- OS: [e.g. GNU/Linux Debian 10]
- Architecture [Arm, x86_64, ...]
- Rust version
- TermSCP version
- termscp version
- Protocol used
- Remote server version and name
## Log
Report the snippet of the log file containing the unexpected behaviour.
If there is any information you consider to be confidential, shadow it.
## Additional information
Add any other context about the problem here.

View File

@@ -1,12 +1,23 @@
---
name: Feature request
about: Suggest an idea for TermSCP
title: "[Feature Request]"
labels: enhancement
assignees: ChristianVisintin
about: Suggest an idea to improve termscp
title: "[Feature Request] - FEATURE_TITLE"
labels: "new feature"
assignees: veeso
---
## Description
Describe the feature you'd like to be added
Put here a brief introduction to your suggestion.
### Changes
The following changes to the application are expected
- ...
## Implementation
Provide any kind of suggestion you propose on how to implement the feature.
If you have none, delete this section.

8
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,8 @@
---
name: Question
about: Ask what you want about the project
title: "[QUESTION] - TITLE"
labels: question
assignees: veeso
---

23
.github/ISSUE_TEMPLATE/security.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Security report
about: Create a report of a security vulnerability
title: "[SECURITY] - ISSUE_TITLE"
labels: security
assignees: veeso
---
## Description
Severity:
- [ ] **critical**
- [ ] high
- [ ] medium
- [ ] low
A clear and concise description of the security vulnerability.
## Additional information
Add any other context about the problem here.

View File

@@ -1,4 +1,4 @@
# Pull Request Title
# ISSUE _NUMBER_ - PULL_REQUEST_TITLE
Fixes # (issue)
@@ -25,7 +25,16 @@ Please select relevant options.
- [ ] My code follows the contribution guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I formatted the code with `cargo fmt`
- [ ] I checked my code using `cargo clippy` and reports no warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] Any dependent changes have been merged and published in downstream modules
- [ ] I have introduced no new *C-bindings*
- [ ] The changes I've made are Windows, MacOS, UNIX, Linux compatible (or I've handled them using `cfg target_os`)
- [ ] I increased or maintained the code coverage for the project, compared to the previous commit
## Acceptance tests
wait for a *project maintainer* to fulfill this section...
- [ ] regression test: ...

15
.github/actions-rs/grcov.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
branch: false
ignore-not-existing: true
llvm: true
output-type: lcov
ignore:
- "/*"
- "C:/*"
- "../*"
- src/main.rs
- src/lib.rs
- src/activity_manager.rs
- src/support.rs
- "src/ui/activities/*"
- src/ui/context.rs
- src/ui/input.rs

37
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Coverage
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- name: Setup nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Run tests (nightly)
uses: actions-rs/cargo@v1
with:
command: test
args: --lib --no-default-features --features github-actions --features with-containers --no-fail-fast
env:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
- name: Coverage with grcov
id: coverage
uses: actions-rs/grcov@v0.1
- name: Coveralls
uses: coverallsapp/github-action@v1.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ${{ steps.coverage.outputs.report }}

22
.github/workflows/freebsd.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: FreeBSD
on: [push, pull_request]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: FreeBSD build
id: test
uses: vmactions/freebsd-vm@v0.1.4
with:
usesh: true
prepare: pkg install -y curl wget libssh gcc vim
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup.sh && \
chmod +x /tmp/rustup.sh && \
/tmp/rustup.sh -y
. $HOME/.cargo/env
cargo build --no-default-features
cargo test --no-default-features --verbose --lib --features github-actions -- --test-threads 1

View File

@@ -7,12 +7,23 @@ env:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v2
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --lib --no-default-features --features github-actions --features with-containers --no-fail-fast
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy -- -Dwarnings

View File

@@ -7,12 +7,13 @@ env:
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v2
- name: Build
run: cargo build
- name: Run tests
run: cargo test --verbose --lib --features github-actions -- --test-threads 1
- name: Clippy
run: cargo clippy

View File

@@ -7,12 +7,13 @@ env:
jobs:
build:
runs-on: windows-2019
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v2
- name: Build
run: cargo build
- name: Run tests
run: cargo test --verbose --lib --features github-actions -- --test-threads 1
- name: Clippy
run: cargo clippy

8
.gitignore vendored
View File

@@ -13,3 +13,11 @@
**/*.rs.bk
# End of https://www.gitignore.io/api/rust
# Distributions
*.rpm
*.deb
dist/pkgs/arch/*.tar.gz
# Macos
.DS_Store

View File

@@ -1,11 +1,393 @@
# Changelog
- [Changelog](#changelog)
- [0.6.1](#061)
- [0.6.0](#060)
- [0.5.1](#051)
- [0.5.0](#050)
- [0.4.2](#042)
- [0.4.1](#041)
- [0.4.0](#040)
- [0.3.3](#033)
- [0.3.2](#032)
- [0.3.1](#031)
- [0.3.0](#030)
- [0.2.0](#020)
- [0.1.3](#013)
- [0.1.2](#012)
- [0.1.1](#011)
- [0.1.0](#010)
---
## 0.6.1
Released on 31/08/2021
- Enhancements:
- Now that tui-rs supports title alignment, UI has been improved
- Added new `Directory already exists` variant for file transfer errors
- Bugfix:
- Fixed [Issue 58](https://github.com/veeso/termscp/issues/58):When uploading a directory, create directory only if it doesn't exist
- Fixed [Issue 59](https://github.com/veeso/termscp/issues/59): When copying files with tricky copy, the upper progress bar shows no text
- Dependencies:
- Updated `bitflags` to `1.3.2`
- Updated `bytesize` to `1.1.0`
- Updated `crossterm` to `0.20`
- Updated `open` to `2.0.1`
- Added `tui-realm-stdlib 0.6.0`
- Replaced `ftp4` with `suppaftp 4.1.2`
- Updated `tui-realm` to `0.6.0`
## 0.6.0
Released on 23/07/2021
> 🍹 Summer update 2021 🍨
- **Open any file** in explorer:
- Open file with default program for file type with `<V>`
- Open file with a specific program with `<W>`
- **Themes**:
- You can now set colors for 26 elements in the application
- Colors can be any RGB, also **CSS colors** syntax is supported (e.g. `aquamarine`)
- Configure theme from settings or import from CLI using the `-t <theme file>` argument
- You can find several themes in the `themes/` directory
- **Keyring support for Linux**
- From now on keyring will be available for Linux only
- Read the manual to find out if your system supports the keyring and how you can enable it
- libdbus is now a dependency
- added `with-keyring` feature
- **❗ BREAKING CHANGE ❗**: if you start using keyring on Linux, all the saved password will be lost
- **In-app release notes**
- Possibility to see the release note of the new available release whenever a new version is available
- Just press `<CTRL+R>` when a new version is available from the auth activity to read the release notes
- **Installation script**:
- From now on, in case cargo is used to install termscp, all the cargo dependencies will be installed
- **Start termscp from configuration**: Start termscp with `-c` or `--config` to start termscp from configuration page
- Enhancements:
- Show a "wait" message when deleting, copying and moving files and when executing commands
- Replaced all `...` with `…` in texts
- Check if remote host is valid in authentication form
- Check if port number is valid in authentication form
- From now on, if you try to leave setup without making any change, you won't be prompted whether to save configuration or not
- Bugfix:
- Fixed broken input cursor when typing UTF8 characters (tui-realm 0.3.2)
- Fixed save bookmark dialog: you could switch out from dialog with `<TAB>`
- Fixed transfer interruption: it was not possible to abort a transfer if the size of the file was less than 65k
- Changed `Remote address` to `Remote host` in authentication form
- Dependencies:
- Added `argh 0.1.5`
- Added `open 1.7.0`
- Removed `getopts`
- Updated `rand` to `0.8.4`
- Updated `textwrap` to `0.14.2`
- Updated `tui-realm` to `0.4.3`
## 0.5.1
Released on 21/06/2021
- Enhancements:
- **CI now uses containers to test file transfers (SSH/FTP)**
- Improved coverage
- Found many bugs which has now been fixed
- Build in CI won't fail due to test servers not responding
- We're now able to test all the functionalities of the file transfers
- **Status bar improvements**
- "Show hidden files" in status bar
- Status bar has now been splitted into two, one for each explorer tab
- **Error message if terminal window is too small**
- If the terminal window has less than 24 lines, then an error message is displayed in the auth activity
- Changed auth layout to absolute sizes
- Bugfix:
- Fixed UI not showing connection errors
- Fixed termscp on Windows dying whenever opening a file with text editor
- Fixed broken input cursor when typing UTF8 characters (tui-realm 0.3.2)
- Fixed [Issue 44](https://github.com/veeso/termscp/issues/44): Could not move files to other paths in FTP
- Fixed [Issue 43](https://github.com/veeso/termscp/issues/43): Could not remove non-empty directories in FTP
- Fixed [Issue 39](https://github.com/veeso/termscp/issues/39): Help panels as `ScrollTable` to allow displaying entire content on small screens
- Fixed [Issue 38](https://github.com/veeso/termscp/issues/38): Transfer size was wrong when transferring "selected" files (with mark)
- Fixed [Issue 37](https://github.com/veeso/termscp/issues/37): progress bar not visible when editing remote files
- Dependencies:
- Updated `textwrap` to `0.14.0`
- Updated `tui-realm` to `0.4.2`
## 0.5.0
Released on 23/05/2021
> 🌸 Spring Update 2021 🌷
- **Synchronized browsing**:
- Added the possibility to enabled the synchronized brower navigation
- when you enter a directory, the same directory will be entered on the other tab
- Enable sync browser with `<Y>`
- Read more on manual: [Synchronized browsing](docs/man.md#Synchronized-browsing-)
- **Remote and Local hosts file formatter**:
- Added the possibility to set different formatters for local and remote hosts
- **Work on multiple files**:
- Added the possibility to work on **multiple files simultaneously**
- Select a file with `<M>`, the file when selected will have a `*` prepended to its name
- Select all files in the current directory with `<CTRL+A>`
- Read more on manual: [Work on multiple files](docs/man.md#Work-on-multiple-files-)
- **Logging**:
- termscp now writes a log file, useful to debug and to contribute to fix issues.
- Read more on [manual](docs/man.md)
- **File transfer changes**
- *SFTP*
- Added **COPY** command to SFTP (Please note that Copy command is not supported by SFTP natively, so here it just uses the `cp` shell command as it does in SCP).
- *FTP*
- Added support for file copy (achieved through *tricky-copy*: the file is first downloaded, then uploaded with a different file name)
- **Double progress bar**:
- From now one two progress bar will be displayed:
- the first, on top, displays the full transfer state (e.g. when downloading a directory of 10 files, the progress of the entire transfer)
- the second, on bottom, displays the transfer of the individual file being written (as happened for the old versions)
- changed the progress bar colour from `LightGreen` to `Green`
- Enhancements
- Added a status bar in the file explorer showing whether the sync browser is enabled and which file sorting mode is selected
- Removed the goold old figlet title
- Protocol input as first field in UI
- Port is now updated to standard for selected protocol
- 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 changed to `FTP`, port becomes `21`)
- Bugfix:
- Fixed wrong text wrap in log box
- Fixed empty bookmark name causing termscp to crash
- Fixed error message not being shown after an upload failure
- Fixed default protocol not being loaded from config
- [Issue 23](https://github.com/veeso/termscp/issues/23): Remove created file if transfer failed or was abrupted
- Dependencies:
- Added `tui-realm 0.3.0`
- Removed `tui` (as direct dependency)
- Updated `regex` to `1.5.4`
## 0.4.2
Released on 13/04/2021
- Enhancements:
- Use highlight symbol for logbox of `tui-rs` instead of adding a `Span`
- Bugfix:
- removed `eprintln!` in ftp transfer causing UI to break in Windows
## 0.4.1
Released on 07/04/2021
- Enhancements:
- SCP file transfer:
- Added possibility to stat directories.
- Bugfix:
- [Issue 18](https://github.com/veeso/termscp/issues/18): Set file transfer type to `Binary` for FTP
- [Issue 17](https://github.com/veeso/termscp/issues/17)
- SCP: fixed symlink not properly detected
- FTP: added symlink support for Linux targets
- [Issue 10](https://github.com/veeso/termscp/issues/10): Fixed port not being loaded from bookmarks into gui
- [Issue 9](https://github.com/veeso/termscp/issues/9): Fixed issues related to paths on remote when using Windows
- Dependencies:
- Added `path-slash 0.1.4` (Windows only)
- Added `thiserror 1.0.24`
- Updated `edit` to `0.1.3`
- Updated `magic-crypt` to `3.1.7`
- Updated `rand` to `0.8.3`
- Updated `regex` to `1.4.5`
- Updated `textwrap` to `0.13.4`
- Updated `ureq` to `2.1.0`
- Updated `whoami` to `1.1.1`
- Updated `wildmatch` to `2.0.0`
## 0.4.0
Released on 27/03/2021
> The UI refactoring update
- **New explorer features**:
- **Execute** a command pressing `X`. This feature is supported on both local and remote hosts (only SFTP/SCP protocols support this feature).
- **Find**: search for files pressing `F` using wild matches.
- Enhancements:
- Input fields will now support **"input keys"** (such as moving cursor, DEL, END, HOME, ...)
- Improved performance regarding configuration I/O (config client is now shared in the activity context)
- Fetch latest version from Github once; cache previous value in the Context Storage.
- Bugfix:
- Prevent resetting explorer index on remote tab after performing certain actions (list dir, exec, ...)
- SCP file transfer: prevent infinite loops while performing `stat` on symbolic links pointing to themselves (e.g. `mylink -> mylink`)
- Fixed a bug causing termscp to crash if removing a bookmark
- Fixed file format cursor position in the GUI
- Fixed a bug causing termscp to show two equal bookmarks when overwriting one.
- Fixed system tests which deleted the termscp configuration when launched
- **LICENSE**: changed license to MIT
- Dependencies:
- Removed `unicode-width`
- Added `wildmatch 1.0.13`
- For developers:
- Activity refactoring
- Developed an internal library used to create components, components are then nested inside a View
- The new engine works through properties and states, then returns Messages. I was inspired by both React and Elm.
## 0.3.3
Released on 28/02/2021
- **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
- Parse remote host now uses a Regex to gather parts (increased stability).
- Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime)
- Improved stability
## 0.3.2
Released on 24/01/2021
- **Explorer Formatter**:
- Added possibility to customize the format when listing files in the explorers (Read more on README)
- Added `file_fmt` key to configuration (if missing, default will be used).
- Added the text input to the Settings view to set the value for `file_fmt`.
- Bugfix:
- Solved file index in explorer files at start of termscp, in case the first entry is an hidden file
- SCP File transfer: when listing directory entries, check if a symlink points to a directory or to a file
- Dependencies:
- updated `crossterm` to `0.19.0`
- updated `rand` to `0.8.2`
- updated `rpassword` to `5.0.1`
- updated `serde` to `1.0.121`
- updated `tui` to `0.14.0`
- updated `whoami` to `1.1.0`
## 0.3.1
Released on 18/01/2021
- **Keyring to store secrets**
- On both MacOS and Windows, the secret used to encrypt passwords in bookmarks it is now store in the OS secret vault. This provides much more security to store the password
- Enhancements:
- Added connection timeout to 30 seconds to SFTP/SCP clients and improved name lookup system.
- Bugfix:
- Solved index in explorer files list which was no more kept after 0.3.0
- SCP file transfer: fixed possible wrong file size when sending file, due to a possible incoherent size between the file explorer and the actual file size.
- Breaking changes: on **MacOS / Windows systems only**, the password you saved for bookmarks won't be working anymore if you have support for the keyring crate. Because of the migration to keyring, the previously used secret hasn't been migrated to the storage, instead a new secret will be used. To solve this, just save the bookmark again with the password.
## 0.3.0
Released on 10/01/2021
> The SSH Key Storage Update
- **SSH Key Storage**
- Added the possibility to store SSH private keys to access to remote hosts; this feature is supported in both SFTP and SCP.
- SSH Keys can be manipulated through the new **Setup Interface**
- **Setup Interface**
- Added a new area in the interface, where is possible to customize termscp. Access to this interface is achieved pressing `<CTRL+C>` from the home page (`AuthActivity`).
- **Configuration**:
- Added configuration; configuration is stored at
- Linux: `/home/alice/.config/termscp/config.toml`
- MacOS: `/Users/Alice/Library/Application Support/termscp/config.toml`
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\config.toml`
- Added Text editor to configuration
- Added Default File transfer protocol to configuration
- Added "Show hidden files" to configuration
- Added "Group directories" to configuration
- Added SSH keys to configuration; SSH keys will be stored at
- Linux: `/home/alice/.config/termscp/.ssh/`
- MacOS: `/Users/Alice/Library/Application Support/termscp/.ssh/`
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\.ssh\`
- Enhancements:
- Replaced `sha256` sum with last modification time check, to verify if a file has been changed in the text editor
- **FTP**
- Added `LIST` command parser for Windows server (DOS-like syntax)
- Default protocol changed to default protocol in configuration when providing address as CLI argument
- Explorers:
- Hidden files are now not shown by default; use `A` to show hidden files.
- Append `/` to directories name.
- Keybindings:
- `A`: Toggle hidden files
- `B`: Sort files by (name, size, creation time, modify time)
- `N`: New file
- Bugfix:
- SCP client didn't show file types for files
- FTP client didn't show file types for files
- FTP file transfer not working properly with `STOR` and `RETR`.
- Fixed `0 B/S` transfer rate displayed after completing download in less than 1 second
- Dependencies:
- added `bitflags 1.2.1`
- removed `data-encoding`
- updated `ftp` to `4.0.2`
- updated `rand` to `0.8.0`
- removed `ring`
- updated `textwrap` to `0.13.1`
- updated `toml` to `0.5.8`
- updated `whoami` to `1.0.1`
## 0.2.0
Released on 21/12/2020
> The Bookmarks Update
- **Bookmarks**
- Bookmarks and recent connections are now displayed in the home page
- Bookmarks are saved at
- Linux: `/home/alice/.config/termscp/bookmarks.toml`
- MacOS: `/Users/Alice/Library/Application Support/termscp/bookmarks.toml`
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\bookmarks.toml`
- **Text Editor**
- Added text editor feature to explorer view
- Added `o` to keybindings to open a text file
- Keybindings:
- `C`: Copy file/directory
- `O`: Open text file in editor
- Enhancements:
- User interface
- Collpased borders to make everything more *aesthetic*
- Rounded input field boards
- File explorer:
- Log how long it took to upload/download a file and the transfer speed
- Display in progress bar the transfer speed (bytes/seconds)
- Bugfix:
- File mode of file on remote is now reported on local file after being downloaded (unix, linux, macos only)
- Scp: when username was not provided, it didn't fallback to current username
- Explorer: fixed UID format in Windows
## 0.1.3
Released on 14/12/2020
- Enhancements:
- File transfer:
- Read buffer is now 65536 bytes long
- File explorer:
- Fixed color mismatch in local explorer
- Explorer tabs have now 70% of layout height, while logging area is 30%
- Highlight selected entry in tabs, only when the tab is active
- Auth page:
- align popup text to center
- Keybindings:
- `L`: Refresh directory content
- Bugfix:
- Fixed memory vulnerability in Windows version
## 0.1.2
Released on 13/12/2020
- General performance and code improvements
- Improved symlinks management
- Possibility to abort file transfers
- Enhancements:
- File explorer:
- When file index is at the end of the list, moving down will set the current index to the first element and viceversa.
- Selected file has now colourful background, instead of foreground, for a better readability.
- Keybindings:
- `E`: Delete file (Same as `DEL`); added because some keyboards don't have `DEL` (hey, that's my MacBook Air's keyboard!)
- `Ctrl+C`: Abort transfer process
## 0.1.1
Released on 10/12/2020

View File

@@ -1,15 +1,94 @@
# Contributing
Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change.
Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
- [Contributing](#contributing)
- [Project mission](#project-mission)
- [Project goals](#project-goals)
- [Open an issue](#open-an-issue)
- [Questions](#questions)
- [Bug reports](#bug-reports)
- [Feature requests](#feature-requests)
- [Preferred contributions](#preferred-contributions)
- [Pull Request Process](#pull-request-process)
- [Software guidelines](#software-guidelines)
- [Developer contributions guide](#developer-contributions-guide)
- [How TermSCP works](#how-termscp-works)
- [Activities](#activities)
- [Implementing File Transfers](#implementing-file-transfers)
---
## Project mission
termscp was born because, as a terminal lover and Linux user, I wanted something like WinSCP on Linux and on terminal. I my previous job I used SFTP/SCP pratically everyday and that made me to desire an application like termscp so much, that eventually I started to work on it in the spare time. I saw there was a very cool library to create terminal user interface (`tui-rs`), so I started to code it. I wrote termscp as an experiment, I designed kinda nothing at the time. I just said
> Ok, there must be a `FileTransfer` trait somehow, I'll have more views, so I'll use something like Android activities, and there must be a module to interact with the local host".
And so in december 2020 I had the first version of termscp running and it worked, but was very simple, raw and minimal.
A lot of things have changed since them, both the features the project provides and my personal view of this project.
Today I don't see termscp as a WinSCP clone anymore. I've also thought about changing the name as the time passed by, but I liked it and it would be hard to change the name on the registries, etc.
Right now I see termscp as a **rich-featured file transfer client for terminals**. All I want is to provide all the features users need to use it correctly, I want it to be **safe and reliable** and eventually I want people to consider termscp **the first choice as a file transfer client**.
### Project goals
- Have support for all the most used file transfer protocol
- Provide all the features a file explorer requires
- Have a well designed application
- Make a reliable, safe and fast application
---
## Open an issue
Open an issue when:
- You have questions or concerns regarding the project or the application itself.
- You have a bug to report.
- You have a feature or a suggestion to improve termscp to submit.
### Questions
If you have a question open an issue using the `Question` template.
By default your question should already be labeled with the `question` label, if you need help with your installation, please also add the `help wanted` label.
Check the issue is always assigned to `veeso`.
### Bug reports
If you want to report an issue or a bug you've encountered while using termscp, open an issue using the `Bug report` template.
The `Bug` label should already be set and the issue should already be assigned to `veeso`.
Don't set other labels to your issue, not even priority.
When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think.
Please always provide the environment you're working on and consider that we don't provide any support for older version of termscp, at least for those not classified as LTS (if we'll ever have them).
If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file.
Last but not least: the template I've written must be used. Full stop.
Maintainers will may add additional labels to your issue:
- **duplicate**: the issue is duplicated; the reference to the related issue will be added to your description. Your issue will be closed.
- **priority**: this must be fixed asap
- **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments.
- **wontfix**: your bug has a very high ratio between the difficulty to fix it and the probability to encounter it, or it just isn't a bug, but a feature.
### Feature requests
Whenever you have a good idea which chould improve the project, it is a good idea to submit it to the project owner.
The first thing you should do though, is not starting to write the code, but is to become concern about how termscp works, what kind
of contribution I appreciate and what kind of contribution I won't consider.
Said so, follow these steps:
- Read the contributing guidelines, entirely
- Think on whether your idea would fit in the project mission and guidelines or not
- Think about the impact your idea would have on the project
- Open an issue using the `feature request` template describing with accuracy your suggestion
- Wait for the maintainer feedback on your idea
If you want to implement the feature by yourself and your suggestion gets approved, start writing the code. Remember that on [docs.rs](https://docs.rs/termscp) there is the documentation for the project. Open a PR related to your issue. See [Pull request process for more details](#pull-request-process)
It is very important to follow these steps, since it will prevent you from working on a feature that will be rejected and trust me, none of us wants to deal with this situation.
Always mind that your suggestion, may be rejected: I'll always provide a feedback on the reasons that brought me to reject your feature, just try not to get mad about that.
---
@@ -17,191 +96,43 @@ Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it
At the moment, these kind of contributions are more appreciated and should be preferred:
- Fix for issues described in [Known Issues](./README.md#known-issues) or [issues reported by the community](https://github.com/ChristianVisintin/TermSCP/issues)
- Fix for issues described in [Known Issues](./README.md#known-issues-) or [issues reported by the community](https://github.com/veeso/termscp/issues)
- New file transfers: for further details see [Implementing File Transfer](#implementing-file-transfers)
- Improvements to translators: any improvement to transliteration is accepted if makes sense, consider that my implementations could be not 100% correct (and probably they're not), indeed consider that I don't speak all these languages (tbh I only can speak Russian as a language with a different alphabet from latin - and I can't even speak it very well).
- Code optimizations: any optimization to the code is welcome
- See also features described in [Upcoming features](./README.md##upcoming-features-). Open an issue first though.
For any other kind of contribution, especially for new features, please submit an issue first.
For any other kind of contribution, especially for new features, please submit a new issue first.
## Pull Request Process
Let's make it simple and clear:
1. Open an issue with an **appropriate label** (e.g. bug, enhancement, ...).
2. Write a **properly documentation** compliant with **rustdoc** standard.
3. Write tests for your code. This doesn't apply necessarily for implementation regarding the user-interface module (`ui`).
4. Report changes to the issue you opened, writing a report of what you changed and what you have introduced.
5. Update the `CHANGELOG.md` file with details of changes to the application.
6. Request maintainers to merge your changes.
1. Open a PR with an **appropriate label** (e.g. bug, enhancement, ...).
2. Write a **properly documentation** for your software compliant with **rustdoc** standard.
3. Write tests for your code. This doesn't apply necessarily for implementation regarding the user-interface module (`ui/activities`) and (if a test server is not available) for file transfers.
4. Check your code with `cargo clippy`.
5. Check if the CI for your commits reports three-green.
6. Report changes to the PR you opened, writing a report of what you changed and what you have introduced.
7. Update the `CHANGELOG.md` file with details of changes to the application. In changelog report changes under a chapter called `PR{PULL_REQUEST_NUMBER}` (e.g. PR12).
8. Assign a maintainer to the reviewers.
9. Wait for a maintainer to fullfil the acceptance tests
10. Wait for a maintainer to complete the acceptance tests
11. Request maintainers to merge your changes.
### Software guidelines
In addition to the process described for the PRs, I've also decided to introduce a list of guidelines to follow when writing the code, that should be followed:
1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on termscp, try not to add useless dependencies.
2. **No C-bindings**: personally I think that Rust still relies too much on C. And that's bad, really bad. Many libraries in Rust are just wrappers to C libraries, which is a huge problem, especially considering this is a multiplatform project. Everytime you add a C-binding to your project, you're forcing your users to install additional libraries to their systems. Sometimes these libraries are already installed on their systems (as happens for libssh2 or openssl in this case), but sometimes not. So if you really have to add a dependency to this project, please AVOID completely adding C-bounded libraries.
3. **Test units matter**: Whenever you implement something new to this project, always implement test units which cover the most cases as possible.
4. **Comments are useful**: Many people say that the code should be that simple to talk by itself about what it does, and comments should then be useless. I personally don't agree. I'm not saying they're wrong, but I'm just saying that this approach has, in my personal opinion, many aspects which are underrated:
1. What's obvious for me, might not be for the others.
2. Our capacity to work on a code depends mostly on **time and experience**, not on complexity: I'm not denying complexity matter, but the most decisive factor when working on code is the experience we've acquired working on it and the time we've spent. As the author of the project, I know the project like the back of my hands, but if I didn't work on it for a year, then I would probably have some problems in working on it again as the same speed as before. And do you know what's really time-saving in these cases? Comments.
## Developer contributions guide
Welcome to the contributions guide for TermSCP. This chapter DOESN'T contain the documentation for TermSCP, which can instead be found on Rust Docs at <https://docs.rs/termscp>
This chapter describes how TermSCP works and the guide lines to implement stuff such as file transfers and add features to the user interface.
### How TermSCP works
TermSCP is basically made up of 4 components:
- the **filetransfer**: the filetransfer takes care of managing the remote file system; it provides function to establish a connection with the remote, operating on the remote server file system (e.g. remove files, make directories, rename files, ...), read files and write files. The FileTransfer, as we'll see later, is actually a trait, and for each protocol a FileTransfer must be implement the trait.
- the **host**: the host module provides functions to interact with the local host file system.
- the **ui**: this module contains the implementation of the user interface, as we'll see in the next chapter, this is achieved through **activities**.
- the **activity_manager**: the activity manager takes care of managing activities, basically it runs the activities of the user interface, and chooses, based on their state, when is the moment to terminate the current activity and which activity to run after the current one.
#### Activities
Just a little paragraph about activities. Really, read the code and the documentation to have a clear idea of how the ui works.
I think there are many ways to implement a user interface; for termscp, I decided to go for a **Android-like** approach.
Android works with Activity, each activity represents a view and there is a context shared between them, and so it works here.
Just a little note about activities, activities work with a `Context`, the context is a data holder for different data, which are shared and common between the activities.
I've implemented a Trait called `Activity`, which, is a very very reduced version of the Android activity of course.
This trait provides only 3 methods:
- `on_create`: this method must initialize the activity; the context is passed to the activity, which will be the only owner of the Context, until the activity terminates.
- `on_draw`: this method must be called each time you want to perform an update of the user interface. This is basically the run method of the activity. This method also cares about handling input events. The developer shouldn't draw the interface on each call of this method (consider that this method might be called hundreds of times per second), but only when actually something has changed (for example after an input event has been raised).
- `on_destroy`: this method finalizes the activity and drops it; this method returns the Context to the caller (the activity manager).
---
### Implementing File Transfers
This chapter describes how to implement a file transfer in TermSCP. A file transfer is a module which implements the `FileTransfer` trait. The file transfer provides different modules to interact with a remote server, which in addition to the most obvious methods, used to download and upload files, provides also methods to list files, delete files, create directories etc.
In the following steps I will describe how to implement a new file transfer, in this case I will be implementing the SCP file transfer (which I'm actually implementing the moment I'm writing this lines).
1. Add the Scp protocol to the `FileTransferProtocol` enum.
Move to `src/filetransfer/mod.rs` and add `Scp` to the `FileTransferProtocol` enum
```rs
/// ## FileTransferProtocol
///
/// This enum defines the different transfer protocol available in TermSCP
#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)]
pub enum FileTransferProtocol {
Sftp,
Ftp(bool), // Bool is for secure (true => ftps)
Scp, // <-- here
}
```
In this case Scp is a "plain" enum type. If you need particular options, follow the implementation of `Ftp` which uses a boolean flag for indicating if using FTPS or FTP.
2. Implement the FileTransfer struct
Create a file at `src/filetransfer/mytransfer.rs`
Declare your file transfer struct
```rs
/// ## ScpFileTransfer
///
/// SFTP file transfer structure
pub struct ScpFileTransfer {
session: Option<Session>,
sftp: Option<Sftp>,
wrkdir: PathBuf,
}
```
3. Implement the `FileTransfer` trait for it
You'll have to implement the following methods for your file transfer:
- connect: connect to remote server
- disconnect: disconnect from remote server
- is_connected: returns whether the file transfer is connected to remote
- pwd: get working directory
- change_dir: change working directory.
- list_dir: get files and directories at a certain path
- mkdir: make a new directory. Return an error in case the directory already exists
- remove: remove a file or a directory. In case the protocol doesn't support recursive removing of directories you MUST implement this through a recursive algorithm
- rename: rename a file or a directory
- stat: returns detail for a certain path
- send_file: opens a stream to a remote path for write purposes (write a remote file)
- recv_file: opens a stream to a remote path for read purposes (write a local file)
- on_sent: finalize a stream when writing a remote file. In case it's not necessary just return `Ok(())`
- on_recv: fianlize a stream when reading a remote file. In case it's not necessary just return `Ok(())`
In case the protocol you're working on doesn't support any of this features, just return `Err(FileTransferError::new(FileTransferErrorType::UnsupportedFeature))`
4. Add your transfer to filetransfers:
Move to `src/filetransfer/mod.rs` and declare your file transfer:
```rs
// Transfers
pub mod ftp_transfer;
pub mod scp_transfer; // <-- here
pub mod sftp_transfer;
```
5. Handle FileTransfer in `FileTransferActivity::new`
Move to `src/ui/activities/filetransfer_activity/mod.rs` and add the new protocol to the client match
```rs
client: match protocol {
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new()),
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new()), // <--- here
},
```
6. Handle right/left input events in `AuthActivity`:
Move to `src/ui/activities/auth_activity.rs` and handle the new protocol in `handle_input_event_mode_text` for `KeyCode::Left` and `KeyCode::Right`.
Consider that the order they "rotate" must match the way they will be drawned in the interface.
For newer protocols, please put them always at the end of the list. In this list I won't, because Scp is more important than Ftp imo.
```rs
KeyCode::Left => {
// If current field is Protocol handle event... (move element left)
if self.selected_field == InputField::Protocol {
self.protocol = match self.protocol {
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
FileTransferProtocol::Ftp(ftps) => match ftps {
false => FileTransferProtocol::Scp,
true => FileTransferProtocol::Ftp(false),
}
};
}
}
KeyCode::Right => {
// If current field is Protocol handle event... ( move element right )
if self.selected_field == InputField::Protocol {
self.protocol = match self.protocol {
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
FileTransferProtocol::Ftp(ftps) => match ftps {
false => FileTransferProtocol::Ftp(true),
true => FileTransferProtocol::Sftp, // End of list (wrap)
}
};
}
}
```
7. Add your new file transfer to the protocol input field
Move to `AuthActivity::draw_protocol_select` method.
Here add your new protocol to the `Spans` vector and to the match case, which chooses which element to highlight.
```rs
let protocols: Vec<Spans> = vec![Spans::from("SFTP"), Spans::from("SCP"), Spans::from("FTP"), Spans::from("FTPS")];
let index: usize = match self.protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(ftps) => match ftps {
false => 2,
true => 3,
}
};
```
You can view the developer guide [here](docs/developer.md).
---

1257
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,17 @@
[package]
name = "termscp"
version = "0.1.1"
authors = ["Christian Visintin"]
edition = "2018"
license = "GPL-3.0"
keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"]
categories = ["command-line-utilities"]
description = "TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
homepage = "https://github.com/ChristianVisintin/TermSCP"
repository = "https://github.com/ChristianVisintin/TermSCP"
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP"
documentation = "https://docs.rs/termscp"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossterm = "0.18.2"
ftp4 = { version = "^4.0.1", features = ["secure"] }
getopts = "0.2.21"
ssh2 = "0.9.0"
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
whoami = "1.0.0"
rpassword = "5.0.0"
unicode-width = "0.1.7"
chrono = "0.4.19"
bytesize = "1.0.1"
textwrap = "0.13.0"
regex = "1.4.2"
lazy_static = "1.4.0"
hostname = "0.3.1"
[target.'cfg(any(unix, macos, linux))'.dependencies]
users = "0.11.0"
[dev-dependencies]
tempfile = "3"
#[patch.crates-io]
#ftp = { git = "https://github.com/ChristianVisintin/rust-ftp" }
[[bin]]
edition = "2018"
homepage = "https://veeso.github.io/termscp/"
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"]
license = "MIT"
name = "termscp"
path = "src/main.rs"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
version = "0.6.1"
[package.metadata.rpm]
package = "termscp"
@@ -51,3 +21,56 @@ buildflags = ["--release"]
[package.metadata.rpm.targets]
termscp = { path = "/usr/bin/termscp" }
[[bin]]
name = "termscp"
path = "src/main.rs"
[dependencies]
argh = "0.1.5"
bitflags = "1.3.2"
bytesize = "1.1.0"
chrono = "0.4.19"
content_inspector = "0.2.4"
crossterm = "0.20"
dirs = "3.0.1"
edit = "0.1.3"
hostname = "0.3.1"
keyring = { version = "0.10.1", optional = true }
lazy_static = "1.4.0"
log = "0.4.14"
magic-crypt = "3.1.7"
open = "2.0.1"
rand = "0.8.4"
regex = "1.5.4"
rpassword = "5.0.1"
serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0"
ssh2 = "0.9.0"
suppaftp = { version = "4.1.2", features = [ "secure" ] }
tempfile = "3.1.0"
textwrap = "0.14.2"
thiserror = "^1.0.0"
toml = "0.5.8"
tui-realm-stdlib = "0.6.0"
tuirealm = "0.6.0"
ureq = { version = "2.1.0", features = [ "json" ] }
whoami = "1.1.1"
wildmatch = "2.0.0"
[dev-dependencies]
pretty_assertions = "0.7.2"
[features]
default = [ "with-keyring" ]
github-actions = []
with-containers = []
with-keyring = [ "keyring" ]
[target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies]
users = "0.11.0"
[target."cfg(target_os = \"windows\")"]
[target."cfg(target_os = \"windows\")".dependencies]
path-slash = "0.1.4"

695
LICENSE
View File

@@ -1,674 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
TermSCP
Copyright (C) 2020 Christian Visintin
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
TermSCP Copyright (C) 2020 Christian Visintin
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
MIT License
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.

293
README.md
View File

@@ -1,206 +1,106 @@
# TermSCP
# termscp
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP) [![Issues](https://img.shields.io/github/issues/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP/issues) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.1.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
<p align="center">
<img src="/assets/images/termscp.svg" width="256" height="256" />
</p>
[![Build](https://github.com/ChristianVisintin/TermSCP/workflows/Linux/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions) [![Build](https://github.com/ChristianVisintin/TermSCP/workflows/MacOS/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions) [![Build](https://github.com/ChristianVisintin/TermSCP/workflows/Windows/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions)
<p align="center">~ A feature rich terminal file transfer ~</p>
<p align="center">
<a href="https://veeso.github.io/termscp/" target="_blank">Website</a>
·
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">Installation</a>
·
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">User manual</a>
</p>
~ Basically, WinSCP on a terminal ~
Developed by Christian Visintin
Current version: 0.1.1 (10/12/2020)
<p align="center">Developed by <a href="https://veeso.github.io/">@veeso</a></p>
<p align="center">Current version: 0.6.1 (31/08/2021)</p>
[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.6.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Linux](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![MacOs](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Windows](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![FreeBSD](https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg)](https://github.com/veeso/termscp/actions) [![Coverage Status](https://coveralls.io/repos/github/veeso/termscp/badge.svg)](https://coveralls.io/github/veeso/termscp)
---
- [TermSCP](#termscp)
- [About TermSCP 🖥](#about-termscp-)
- [Why TermSCP 🤔](#why-termscp-)
- [Features 🎁](#features-)
- [Installation ▶](#installation-)
- [Cargo 🦀](#cargo-)
- [Deb package 📦](#deb-package-)
- [RPM Package 📦](#rpm-package-)
- [Chocolatey 🍫](#chocolatey-)
- [Brew 🍻](#brew-)
- [Usage ❓](#usage-)
- [Address argument](#address-argument)
- [How Password can be provided](#how-password-can-be-provided)
- [Keybindings ⌨](#keybindings-)
- [Documentation 📚](#documentation-)
- [Known issues 🧻](#known-issues-)
- [Upcoming Features 🧪](#upcoming-features-)
- [Contributions 🤙🏻](#contributions-)
- [Changelog ⏳](#changelog-)
- [Powered by 🚀](#powered-by-)
- [Gallery 🎬](#gallery-)
- [License 📃](#license-)
## About termscp 🖥
---
## About TermSCP 🖥
TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It works both on **Linux**, **MacOS**, **BSD** and **Windows** and supports SFTP, SCP, FTP and FTPS.
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP and FTPS.
![Explorer](assets/images/explorer.gif)
---
### Why TermSCP 🤔
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
## Features 🎁
- Different communication protocols
- 📁 Different communication protocols
- SFTP
- SCP
- FTP and FTPS
- Practical user interface to explore and operate on the remote and on the local machine file system
- Compatible with Windows, Linux, BSD and MacOS
- Written in Rust
- Easy to extend with new file transfers protocols
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
- Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
- 📝 View and edit text files with your favourite text editor
- 💁 SFTP/SCP authentication through SSH keys and username/password
- 🐧 Compatible with Windows, Linux, BSD and MacOS
- ✏ Customizable
- Themes
- Custom file explorer format
- Customizable text editor
- Customizable file sorting
- 🔐 Save your password in your operating system key vault
- 🦀 Rust-powered
- 🤝 Easy to extend with new file transfers protocols
- 👀 Developed keeping an eye on performance
- 🦄 Frequent awesome updates
---
## Installation ▶
## Get started 🚀
If you're considering to install TermSCP I want to thank you 💛 ! I hope you will enjoy TermSCP!
If you're considering to install termscp I want to thank you 💜 ! I hope you will enjoy termscp!
If you want to contribute to this project, don't forget to check out our contribute guide. [Read More](CONTRIBUTING.md)
### Cargo 🦀
If you are a Linux, a FreeBSD or a MacOS user this simple shell script will install termscp on your system with a single command:
```sh
# Install termscp through cargo
cargo install termscp
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
```
### Deb package 📦
while if you're a Windows user, you can install termscp with [Chocolatey](https://chocolatey.org/).
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.1_amd64.deb)
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.1_amd64.deb`
For more information or other platforms, please visit [veeso.github.io](https://veeso.github.io/termscp/#get-started) to view all installation methods.
then install through dpkg:
### Requirements ❗
```sh
dpkg -i termscp_*.deb
# Or even better with gdebi
gdebi termscp_*.deb
```
- **Linux** users:
- libssh
- libdbus-1
- **BSD** users:
- libssh
### RPM Package 📦
### Optional Requirements ✔️
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.1-1.x86_64.rpm)
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.1-1.x86_64.rpm`
These requirements are not forcely required to run termscp, but to enjoy all of its features
then install through rpm:
```sh
rpm -U termscp_*.rpm
```
### Chocolatey 🍫
You can install TermSCP on Windows using [chocolatey](https://chocolatey.org/)
Start PowerShell as administrator and run
```ps
choco install termscp
```
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.1.nupkg)
and then with PowerShell started with administrator previleges, run:
```ps
choco install termscp -s .
```
### Brew 🍻
You can install TermSCP on MacOS using [brew](https://brew.sh/)
From your terminal run
```sh
brew tap ChristianVisintin/termscp
brew install termscp
```
- **Linux/BSD** users:
- To **open** files via `V` (at least one of these)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **Linux** users:
- A keyring manager: read more in the [User manual](docs/man.md#linux-keyring)
- **WSL** users
- To **open** files via `V` (at least one of these)
- [wslu](https://github.com/wslutilities/wslu)
---
## Usage
## Buy me a coffee
TermSCP can be started with the following options:
If you like termscp and you'd love to see the project to grow, please consider a little donation 🥳
- `-P, --password <password>` if address is provided, password will be this argument
- `-v, --version` Print version info
- `-h, --help` Print help page
TermSCP can be started in two different mode, if no extra arguments is provided, TermSCP will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer.
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
### Address argument
The address argument has the following syntax:
```txt
[protocol]://[username@]<address>[:port]
```
Let's see some example of this particular syntax, since it's very comfortable and you'll probably going to use this instead of the other one...
- Connect using default protocol (sftp) to 192.168.1.31, port is default for this protocol (22); username is current user's name
```sh
termscp 192.168.1.31
```
- Connect using default protocol (sftp) to 192.168.1.31, port is default for this protocol (22); username is `root`
```sh
termscp root@192.168.1.31
```
- Connect using scp to 192.168.1.31, port is 4022; username is `omar`
```sh
termscp scp://omar@192.168.1.31:4022
```
#### How Password can be provided
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
Password can be basically provided through 3 ways when address argument is provided:
- `-P, --password` option: just use this CLI option providing the password. I strongly unrecommend this method, since it's very unsecure (since you might keep the password in the shell history)
- Via `sshpass`: you can provide password via `sshpass`, e.g. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- You will be prompted for it: if you don't use any of the previous methods, you will be prompted for the password, as happens with the more classics tools such as `scp`, `ssh`, etc.
---
## Keybindings ⌨
| Key | Command |
|---------------|-------------------------------------------------------|
| `<ESC>` | Disconnect from remote; return to authentication page |
| `<TAB>` | Switch between log tab and explorer |
| `<BACKSPACE>` | Go to previous directory in stack |
| `<RIGHT>` | Move to remote explorer tab |
| `<LEFT>` | Move to local explorer tab |
| `<UP>` | Move up in selected list |
| `<DOWN>` | Move down in selected list |
| `<PGUP>` | Move up in selected list by 8 rows |
| `<PGDOWN>` | Move down in selected list by 8 rows |
| `<ENTER>` | Enter directory |
| `<SPACE>` | Upload / download selected file |
| `<D>` | Make directory |
| `<G>` | Go to supplied path |
| `<H>` | Show help |
| `<H>` | Show info about selected file or directory |
| `<Q>` | Quit TermSCP |
| `<R>` | Rename file |
| `<U>` | Go to parent directory |
| `<CANC>` | Delete file |
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
---
@@ -212,59 +112,86 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
## Known issues 🧻
- Ftp:
- Time in explorer is `1 Jan 1970`, but shouldn't be: that's because chrono can't parse date in a different locale. So if your server has a locale different from the one on your machine, it won't be able to parse the date.
- Some servers don't work: yes, some kind of ftp server don't work correctly, sometimes it won't display any files in the directories, some other times uploading files will fail. Up to date, `vsftpd` is the only one server which I saw working correctly with TermSCP. Am I going to solve this? I'd like to, but it's not my fault at all. Unfortunately [rust-ftp](https://github.com/mattnenterprise/rust-ftp) is an abandoned project (up to 2020), indeed I had to patch many stuff by myself. I'll try to solve these issues, but it will take a long time.
- Sftp:
- sftp is much slower than scp: Okay this is an annoying issue, and again: not my fault. It seems there is an issue with [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) library. If you want to stay up to date with the status of this issue, subscribe to [this issue](https://github.com/alexcrichton/ssh2-rs/issues/206)
- `NoSuchFileOrDirectory` on connect: let me guess, you're running on WSL and you've installed termscp through cargo. I know about this issue and it's a glitch of WSL I guess. Don't worry about it, just move the termscp executable into another PATH location, such as `/usr/bin`.
- `NoSuchFileOrDirectory` on connect (WSL1): I know about this issue and it's a glitch of WSL I guess. Don't worry about it, just move the termscp executable into another PATH location, such as `/usr/bin`, or install it through the appropriate package format (e.g. deb).
---
## Upcoming Features 🧪
- **File viewer**: possibility to show in a popup the file content from the explorer.
Major termscp releases will now be seasonal, so expect 4 major updates during the year.
Planned for *🍁 Autumn update 🍇*:
- **Self-update ⬇️**: In order to increase users updating termscp, I want to provide the possibility to update termscp directly from application, when a new update is available.
- **AWS S3 support 🪣**: I'll use `rust-s3` library to implement this. This is really big **Maybe** for the autumn update and might be moved to the Winter update.
- **Prompt before replacing files ☢️**: Possibility to configure whether a prompt should be displayed before replacing files.
Planned for *❄️ Winter update ⛄*:
- **SMB Support 🎉**: This will require a long time to be implemented, since I'm currently working on a Rust native SMB library, since I don't want to add new C-bindings. ~~Fear the 🦚~~
- **Configuration profile for bookmarks 📚**: Basically this feature adds the possibility to have a specific setup for a certain host, instead of having only one global configuration.
Along to new features, termscp developments is now focused on UX and performance improvements, so if you have any suggestion, feel free to open an issue.
---
## Contributions 🤙🏻
## Contributing and issues 🤝🏻
Contributions are welcome! 😉
Contributions, bug reports, new features and questions are welcome! 😉
If you have any question or concern, or you want to suggest a new feature, or you want just want to improve termscp, feel free to open an issue or a PR.
If you think you can contribute to TermSCP, please follow [TermSCP's contributions guide](CONTRIBUTING.md)
Please follow [our contributing guidelines](CONTRIBUTING.md)
---
## Changelog ⏳
View TermSCP's changelog [HERE](CHANGELOG.md)
View termscp's changelog [HERE](CHANGELOG.md)
---
## Powered by 🚀
## Powered by 💪
TermSCP is powered by these aweseome projects:
termscp is powered by these aweseome projects:
- [bytesize](https://github.com/hyunsik/bytesize)
- [crossterm](https://github.com/crossterm-rs/crossterm)
- [edit](https://github.com/milkey-mouse/edit)
- [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)
- [whoami](https://github.com/libcala/whoami)
- [wildmatch](https://github.com/becheran/wildmatch)
---
## Gallery 🎬
> Termscp Home
![Auth](assets/images/auth.gif)
> Bookmarks
![Bookmarks](assets/images/bookmarks.gif)
> Setup
![Setup](assets/images/config.gif)
> Text editor
![TextEditor](assets/images/text-editor.gif)
---
## License 📃
Licensed under the GNU GPLv3 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
<http://www.gnu.org/licenses/gpl-3.0.txt>
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
termscp is licensed under the MIT license.
You can read the entire license [HERE](LICENSE)

10
SECURITY.md Normal file
View File

@@ -0,0 +1,10 @@
# Security Policy
## Supported Versions
Only latst version of termscp has the latest security updates.
Because of that, **you should always consider updating termscp to the latest version**.
## Reporting a Vulnerability
If you have any security vulnerability or concern to report, please open an issue using the `Security report` template.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/images/bookmarks.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
assets/images/config.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com" viewBox="0 0 500 500"><path style="fill:#404040" d="M 15 0 H 484 A 16 16 0 0 1 500 16 V 47.312 H 0 V 15 A 15 15 0 0 1 15 0 Z" bx:shape="rect 0 0 500 47.312 15 16 0 0 1@05baa068"/><circle style="fill:#ea4444" cx="29.473" cy="23.011" r="7.344"/><circle style="fill:#eac944" cx="58.177" cy="22.543" r="7.344" transform="matrix(1, 0, 0, 1.031863, -5.389861, -0.016285)"/><circle style="fill:#34b938" cx="58.177" cy="22.543" r="7.344" transform="matrix(1, 0, 0, 1.031863, 17.888227, -0.016285)"/><rect width="500" height="452.688" y="47.312" style="fill:#31363b"/><polygon style="stroke:#000;fill:#f0f0f0" points="71.581 100 224.35 252.769 77.119 400 25.235 348.115 122.193 251.156 22.129 151.093"/><rect style="fill:#f0f0f0" width="242.408" height="49.262" x="235.188" y="381.519" rx="10" ry="10"/><g transform="matrix(0.513677, 0, 0, 0.513677, 257.101013, 152.253387)"><polygon style="fill:#f0f0f0" points="196.4 0 292 49.2 388 98 292 147.2 196.4 196.4 100.8 147.2 4.8 98 100.8 49.2"/><polygon style="fill:#31363b" points="316 179.6 316 135.2 268 159.6 268 204 294.4 171.6"/><polygon style="fill:#f0f0f0" points="196.4 196.4 196.4 392.8 388 294.8 388 98 316 135.2 314.4 136 314.4 179.2 294.4 171.6 268 204 268 159.6"/><polygon style="fill:#f0f0f0" points="196.4 392.8 196.4 196.4 100.8 147.2 4.8 98 4.8 294.8"/><polygon style="fill:#31363b" points="76.8 61.2 268 159.6 314.4 136 316 135.2 124.8 36.8 100.8 49.2"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
assets/images/themes.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

25
dist/build/README.md vendored Normal file
View File

@@ -0,0 +1,25 @@
# Build with Docker
- [Build with Docker](#build-with-docker)
- [Prerequisites](#prerequisites)
- [Build](#build)
---
## Prerequisites
- Docker
## Build
1. Build x86_64
this will build termscp for:
- Linux x86_64 Deb packages
- Linux x86_64 RPM packages
- Windows x86_64 MSVC packages
```sh
```

32
dist/build/deploy.sh vendored Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: deploy.sh <version>"
exit 1
fi
VERSION=$1
set -e # Don't fail
# Create pkgs directory
cd ..
PKGS_DIR=$(pwd)/pkgs
cd -
mkdir -p ${PKGS_DIR}/
# Build x86_64_deb
cd x86_64_debian9/
docker build --tag termscp-${VERSION}-x86_64_debian9 .
cd -
mkdir -p ${PKGS_DIR}/deb/
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_debian9 termscp-${VERSION}-x86_64_debian9)
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}_amd64.deb ${PKGS_DIR}/deb/
# Build x86_64_centos7
cd x86_64_centos7/
docker build --tag termscp-${VERSION}-x86_64_centos7 .
cd -
mkdir -p ${PKGS_DIR}/rpm/
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_centos7 termscp-${VERSION}-x86_64_centos7)
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.el7.x86_64.rpm ${PKGS_DIR}/rpm/termscp-${VERSION}-1.x86_64.rpm
exit $?

19
dist/build/x86_64/Dockerfile vendored Normal file
View File

@@ -0,0 +1,19 @@
FROM rust:1.48.0 AS builder
WORKDIR /usr/src/
# Add toolchains
RUN rustup target add x86_64-unknown-linux-gnu
# Install dependencies
RUN apt update && apt install -y rpm
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo RPM/Deb
RUN cargo install cargo-deb cargo-rpm
# Build for x86_64
RUN cargo build --release --target x86_64-unknown-linux-gnu
# Build pkgs
RUN cargo deb && cargo rpm init && cargo rpm build
CMD ["sh"]

26
dist/build/x86_64_centos7/Dockerfile vendored Normal file
View File

@@ -0,0 +1,26 @@
FROM centos:centos7 as builder
WORKDIR /usr/src/
# Install dependencies
RUN yum -y install \
git \
gcc \
openssl \
pkgconfig \
dbus-devel \
openssl-devel
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo arxch
RUN source $HOME/.cargo/env && cargo install cargo-rpm
# Build for x86_64
RUN source $HOME/.cargo/env && cargo build --release
# Build pkgs
RUN source $HOME/.cargo/env && yum -y install rpm-build && cargo rpm init && cargo rpm build
CMD ["sh"]

29
dist/build/x86_64_debian8/Dockerfile vendored Normal file
View File

@@ -0,0 +1,29 @@
FROM debian:jessie
WORKDIR /usr/src/
# Install dependencies
RUN apt update && apt install -y \
git \
gcc \
pkg-config \
libssl-dev \
libssh2-1-dev \
libdbus-1-dev \
curl
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo deb
RUN . $HOME/.cargo/env && cargo install cargo-deb
# Build for x86_64
RUN . $HOME/.cargo/env && cargo build --release
# Build pkgs
RUN . $HOME/.cargo/env && cargo deb
CMD ["sh"]

29
dist/build/x86_64_debian9/Dockerfile vendored Normal file
View File

@@ -0,0 +1,29 @@
FROM debian:stretch
WORKDIR /usr/src/
# Install dependencies
RUN apt update && apt install -y \
git \
gcc \
pkg-config \
libssl-dev \
libssh2-1-dev \
libdbus-1-dev \
curl
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo deb
RUN . $HOME/.cargo/env && cargo install cargo-deb
# Build for x86_64
RUN . $HOME/.cargo/env && cargo build --release
# Build pkgs
RUN . $HOME/.cargo/env && cargo deb
CMD ["sh"]

17
dist/pkgs/freebsd/manifest vendored Normal file
View File

@@ -0,0 +1,17 @@
name: "termscp"
version: 0.6.1
origin: veeso/termscp
comment: "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP"
desc: <<EOD
A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP
EOD
arch: "amd64"
www: "https://veeso.github.io/termscp/"
maintainer: "christian.visintin1997@gmail.com"
prefix: "/usr/local/bin"
deps: {
libssh: {origin: security/libssh, version: 0.9.5}
}
files: {
/usr/local/bin/termscp: "87543d13b11b6e601ba8cdde9d704c80dc3515f1681fbf71fd0b31d7206efc09"
}

73
docs/developer.md Normal file
View File

@@ -0,0 +1,73 @@
# Developer Manual
Document audience: developers
- [Developer Manual](#developer-manual)
- [How to test](#how-to-test)
- [How termscp works](#how-termscp-works)
- [Activities](#activities)
- [The Context](#the-context)
Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface.
## How to test
First an introduction to tests.
Usually it's enough to run `cargo test`, but please note that whenever you're working on file transfer you'll need one more step.
In order to run tests with file transfers, you need to start the file transfer server containers, which can be started via `docker`.
To run all tests with file transfers just run: `./tests/test.sh`
---
## How termscp works
termscp is basically made up of 4 components:
- the **filetransfer**: the filetransfer takes care of managing the remote file system; it provides function to establish a connection with the remote, operating on the remote server file system (e.g. remove files, make directories, rename files, ...), read files and write files. The FileTransfer, as we'll see later, is actually a trait, and for each protocol a FileTransfer must be implement the trait.
- the **host**: the host module provides functions to interact with the local host file system.
- the **ui**: this module contains the implementation of the user interface, as we'll see in the next chapter, this is achieved through **activities**.
- the **activity_manager**: the activity manager takes care of managing activities, basically it runs the activities of the user interface, and chooses, based on their state, when is the moment to terminate the current activity and which activity to run after the current one.
In addition to the 4 main components, other have been added through the time:
- **config**: this module provides the configuration schema and serialization methods for it.
- **fs**: this modules exposes the FsEntry entity and the explorers. The explorers are structs which hold the content of the current directory; they also they take of filtering files up to your preferences and format file entries based on your configuration.
- **system**: the system module provides a way to actually interact with the configuration, the ssh key storage and with the bookmarks.
- **utils**: contains the utilities used by pretty much all the project.
## Activities
Just a little paragraph about activities. Really, read the code and the documentation to have a clear idea of how the ui works.
I think there are many ways to implement a user interface and I've worked with different languages and frameworks in my career, so for this project I've decided to get what I like the most from different frameworks to implement it.
My approach was this:
- **Activities on top**: each "page" is an Activity and an `Activity Manager` handles them. I got inspired by Android for this case. I think that's a good way to implement the ui in case like this, where you have different pages, each one with their view, their components and their logics. Activities work with the `Context`, which is a data holder for different data, which are shared and common between the activities.
- **Activities display Views**: Each activity can show different views. A view is basically a list of **components**, each one with its properties. The view is a facade to the components and also handles the focus, which is the current active component. You cannot have more than one component active, so you need to handle this; but at the same time you also have to give focus to the previously active component if the current one is destroyed. So basically view takes care of all this stuff.
- **Components**: I've decided to write around `tui` in order to re-use widgets. To do so I've implemented the `Component` trait. To implement traits I got inspired by [React](https://reactjs.org/). Each component has its *Properties* and can have its *States*. Then each component must be able to handle input events and to be updated with new properties. Last but not least, each component must provide a method to **render** itself.
- **Messages: an Elm based approach**: I was really satisfied with my implementation choices; the problem at this point was solving one of the biggest teardrops I've ever had with this project: **events**. Input events were really a pain to handle, since I had to handle states in the activity to handle which component was enabled etc. To solve this I got inspired by a wonderful language I had recently studied, which is [Elm](https://elm-lang.org/). Basically in Elm you implement your ui using three basic functions: **update**, **view** and **init**. View and init were pretty much already implemented here, but at this point I decided to implement also something like the **elm update function**. I came out with a huge match case to handle events inside a recursive function, which you can basically find in the `update.rs` file inside each activity. This match case handles a tuple, made out of the **component id** and the **input event** received from the view. It matches the two propeties against the input event we want to handle for each component *et voilà*.
I've implemented a Trait called `Activity`, which, is a very very reduced version of the Android activity of course.
This trait provides only 3 methods:
- `on_create`: this method must initialize the activity; the context is passed to the activity, which will be the only owner of the Context, until the activity terminates.
- `on_draw`: this method must be called each time you want to perform an update of the user interface. This is basically the run method of the activity. This method also cares about handling input events. The developer shouldn't draw the interface on each call of this method (consider that this method might be called hundreds of times per second), but only when actually something has changed (for example after an input event has been raised).
- `will_umount`: this method was added in 0.4.0 and returns whethere the activity should be destroyed. If so returns an ExitReason, which indicates why the activity should be terminated. Based on the reason, the activity manager chooses whether to stop the execution of termscp or to start a new activity and which one.
- `on_destroy`: this method finalizes the activity and drops it; this method returns the Context to the caller (the activity manager).
### The Context
The context is a structure which holds data which must be shared between activities. Everytime an Activity starts, the Context is taken by the activity, until it is destroyed, where finally the context is returned to the activity manager.
The context basically holds the following data:
- The **Localhost**: the local host structure
- The **File Transfer Params**: the current parameters set to connect to the remote
- The **Config Client**: the configuration client is a structure which provides functions to access the user configuration
- The **Store**: the store is a key-value storage which can hold any kind of data. This can be used to store states to share between activities or to keep persistence for heavy/slow tasks (such as checking for updates).
- The **Input handler**: the input handler is used to read input events from the keyboard
- The **Terminal**: the terminal is used to view the tui on the terminal
---

View File

@@ -1 +0,0 @@
<mxfile host="Electron" modified="2020-11-21T19:08:13.709Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.3.1 Chrome/83.0.4103.119 Electron/9.0.5 Safari/537.36" etag="CH09h_mpCNwCuwQp3CzT" version="13.3.1" type="device" pages="2"><diagram id="SlpWaeKyUTlHltKKkHdB" name="ScpActivity">7ZlRk5owEMc/DY/HABGEx2rv2pnWmXaczj3nJEKmgdAQD+yn7wJBoOhUrbbcTHwx2SSb5L+/4EoMtEzKDwJn8YqHhBmOFZYGem84ju26FnxVln1j8QO3MUSChqpTZ1jTn0QZ1bhoR0OSDzpKzpmk2dC44WlKNnJgw0LwYthty9lw1gxHZGRYbzAbW59pKGO1C2fe2T8SGsXtzLYXNC0JbjurneQxDnnRM6FHAy0F57IpJeWSsEq8Vpdm3NOJ1sPCBEnlOQOyRbEsv31ixSv+vi6c588r9vXBaby8YrZTG1aLlftWAcF3aUgqJ5aBFkVMJVlneFO1FhBzsMUyYVCzobiljC054wLqKU+h00LNQIQk5cml2wdBgCTCEyLFHrqoAchEqBmzH9JRdCFBgWd6KipxLyJopvpiRUJ0cN+JBQWl1wXaoTeh3azl77R0M8s6pptj+t6dpJsdkc5jMPPiBQqRrLfeGLYc9tkX1fux423DQ14/MN5BB9Ck7BpbL1+qU6g8wUobZ8MJwNyb9MYBxIxGKVQZ2crbxBMo9/58Fo4E1L3XOXB1MK8/nO60YumNYvlEGUlxQkBVnFQapS95Vu/e+semGo8b+seS3nZbGREJzXPK0/w6H7xIiahTIb7L3gC+o2fR/Bi/8Os9GyPs3QvhuUZYI3x1ejQFgn1NsCb4fILNwA96H3+YUaAJAB1ooDXQl2QVbh/o+RDoYAJAt3m6JloT/ddEH/7N/VeibU20JvqStNm0+p/f3klNIeuwxy+3NdIa6bNflE8i0RhfMmiGNcOnGbaHqcYUM43x5c+K5Hl1D+tYL7y8m8wbEBGi2Vw2rJV/u603N8+Oe6NAmEEwkB55Y+l92zUteyz94clzgfZQ7a6S67behTx6/AU=</diagram><diagram id="SK1VvSCf6-f5suE94Ksw" name="AuthActivity">5ZfRbpswFIafhstIAQNtL1OarlI1qUq6RdrNZLADnowPs00ge/qZYCA0ldZOSiqVK+z/2Mec/zuWwEFRXn+RuMi+AqHc8eakdtCd43luEMzNo1H2rXJ9E7RCKhmxiwZhzf5QK9p9ackIVaOFGoBrVozFBISgiR5pWEqoxsu2wMenFjilJ8I6wfxU3TCiM1uFdzXoD5SlWXeyG960kRx3i20lKsMEqiMJLR0USQDdjvI6orwxr/Pl8ennZvUQzVZ3uwXDz8pfPaNZm+z+PVv6EiQV+r9Tb/ax+q59lCz5D/5rN4sf0z71DvPS+mVr1fvOQAmlILRJMnfQbZUxTdcFTppoZVrGaJnOuZm5ZrhlnEfAQZq5AEEbCYS2beEFZo45S4WZcLo1xdzaF6BS0/oFsX+U6/YMTPNSyKmWe7PP9qlvqVXH0K2WHQFHXaNi22hpn2ow0wysn+/w1jvxdlHqzNTEEqwZiKk5HZ7LaPRKE4dcW0dGHoe/S+gCM3XwamEWuH5RD0EzSu3zkCXuhAUhkip12NOGzPvGL5cbrT23kz8HZnf+Rs7+uTj7l+L8BFJPFPJbL/PZIAcXgyxBQwK83aQob74+Jkj8+qOJh5ci/k1RKXBOp3m1++/YDwN9dbGrjZWqQJIWNM4bJiJWxRjyZMCH5wNvpsMP0CF29BuJln8B</diagram></mxfile>

418
docs/man.md Normal file
View File

@@ -0,0 +1,418 @@
# User manual 🎓
- [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-)
- [How Password can be provided 🔐](#how-password-can-be-provided-)
- [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-)
- [Work on multiple files 🥷](#work-on-multiple-files-)
- [Synchronized browsing ⏲️](#synchronized-browsing-)
- [Open and Open With 🚪](#open-and-open-with-)
- [Bookmarks ⭐](#bookmarks-)
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
- [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
- [Themes 🎨](#themes-)
- [Styles 💈](#styles-)
- [Authentication page](#authentication-page)
- [Transfer page](#transfer-page)
- [Misc](#misc)
- [Text Editor ✏](#text-editor-)
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
- [Logging 🩺](#logging-)
## Usage ❓
termscp can be started with the following options:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument
- `-c, --config` Open termscp starting from the configuration page
- `-q, --quiet` Disable logging
- `-t, --theme <path>` Import specified theme
- `-v, --version` Print version info
- `-h, --help` Print help page
termscp can be started in two different mode, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer.
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
If address argument is provided you can also provide the start working directory for local host
### Address argument 🌎
The address argument has the following syntax:
```txt
[protocol://][username@]<address>[:port][:wrkdir]
```
Let's see some example of this particular syntax, since it's very comfortable and you'll probably going to use this instead of the other one...
- Connect using default protocol (*defined in configuration*) to 192.168.1.31, port if not provided is default for the selected protocol (in this case depends on your configuration); username is current user's name
```sh
termscp 192.168.1.31
```
- Connect using default protocol (*defined in configuration*) to 192.168.1.31; username is `root`
```sh
termscp root@192.168.1.31
```
- Connect using scp to 192.168.1.31, port is 4022; username is `omar`
```sh
termscp scp://omar@192.168.1.31:4022
```
- Connect using scp to 192.168.1.31, port is 4022; username is `omar`. You will start in directory `/tmp`
```sh
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
Password can be basically provided through 3 ways when address argument is provided:
- `-P, --password` option: just use this CLI option providing the password. I strongly unrecommend this method, since it's very unsecure (since you might keep the password in the shell history)
- Via `sshpass`: you can provide password via `sshpass`, e.g. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- You will be prompted for it: if you don't use any of the previous methods, you will be prompted for the password, as happens with the more classics tools such as `scp`, `ssh`, etc.
---
## File explorer 📂
When we refer to file explorers in termscp, we refer to the panels you can see after establishing a connection with the remote.
These panels are basically 3 (yes, three actually):
- Local explorer panel: it is displayed on the left of your screen and shows the current directory entries for localhost
- Remote explorer panel: it is displayed on the right of your screen and shows the current directory entries for the remote host.
- Find results panel: depending on where you're searching for files (local/remote) it will replace the local or the explorer panel. This panel shows the entries matching the search query you performed.
In order to change panel you need to type `<LEFT>` to move the remote explorer panel and `<RIGHT>` to move back to the local explorer panel. Whenever you are in the find results panel, you need to press `<ESC>` to exit panel and go back to the previous panel.
### Keybindings ⌨
| Key | Command | Reminder |
|---------------|-------------------------------------------------------|-------------|
| `<ESC>` | Disconnect from remote; return to authentication page | |
| `<TAB>` | Switch between log tab and explorer | |
| `<BACKSPACE>` | Go to previous directory in stack | |
| `<RIGHT>` | Move to remote explorer tab | |
| `<LEFT>` | Move to local explorer tab | |
| `<UP>` | Move up in selected list | |
| `<DOWN>` | Move down in selected list | |
| `<PGUP>` | Move up in selected list by 8 rows | |
| `<PGDOWN>` | Move down in selected list by 8 rows | |
| `<ENTER>` | Enter directory | |
| `<SPACE>` | Upload / download selected file | |
| `<A>` | Toggle hidden files | All |
| `<B>` | Sort files by | Bubblesort? |
| `<C>` | Copy file/directory | Copy |
| `<D>` | Make directory | Directory |
| `<E>` | Delete file (Same as `DEL`) | Erase |
| `<F>` | Search for files (wild match is supported) | Find |
| `<G>` | Go to supplied path | Go to |
| `<H>` | Show help | Help |
| `<I>` | Show info about selected file or directory | Info |
| `<L>` | Reload current directory's content / Clear selection | List |
| `<M>` | Select a file | Mark |
| `<N>` | Create new file with provided name | New |
| `<O>` | Edit file; see Text editor | Open |
| `<Q>` | Quit termscp | Quit |
| `<R>` | Rename file | Rename |
| `<S>` | Save file as... | Save |
| `<U>` | Go to parent directory | Upper |
| `<V>` | Open file with default program for filetype | View |
| `<W>` | Open file with provided program | With |
| `<X>` | Execute a command | eXecute |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<DEL>` | Delete file | |
| `<CTRL+A>` | Select all files | |
| `<CTRL+C>` | Abort file transfer process | |
### Work on multiple files 🥷
You can opt to work on multiple files, selecting them pressing `<M>`, in order to select the current file, or pressing `<CTRL+A>`, which will select all the files in the working directory.
Once a file is marked for selection, it will be displayed with a `*` on the left.
When working on selection, only selected file will be processed for actions, while the current highlighted item will be ignored.
It is possible to work on multiple files also when in the find result panel.
All the actions are available when working with multiple files, but be aware that some actions work in a slightly different way. Let's dive in:
- *Copy*: whenever you copy a file, you'll be prompted to insert the destination name. When working with multiple file, this name refers to the destination directory where all these files will be copied.
- *Rename*: same as copy, but will move files there.
- *Save as*: same as copy, but will write them there.
### Synchronized browsing ⏲️
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press `<Y>`; press twice to disable. While enabled, the synchronized browising state will be reported on the status bar on `ON`.
*Warning*: at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
### Open and Open With 🚪
Open and open with commands are powered by [open-rs](https://docs.rs/crate/open/1.7.0).
When opening files with View command (`<V>`), the system default application for the file type will be used. To do so, the default operting system service will be used, so be sure to have at least one of these installed on your system:
- **Windows** users: you don't have to worry about it, since the crate will use the `start` command.
- **MacOS** users: you don't have to worry either, since the crate will use `open`, which is already installed on your system.
- **Linux** users: one of these should be installed
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **WSL** users: *wslview* is required, you must install [wslu](https://github.com/wslutilities/wslu).
> Q: Can I edit remote files using the view command?
> A: No, at least not directly from the "remote panel". You have to download it to a local directory first, that's due to the fact that when you open a remote file, the file is downloaded into a temporary directory, but there's no way to create a watcher for the file to check when the program you used to open it was closed, so termscp is not able to know when you're done editing the file.
---
## Bookmarks ⭐
In termscp it is possible to save favourites hosts, which can be then loaded quickly from the main layout of termscp.
termscp will also save the last 16 hosts you connected to.
This feature allows you to load all the parameters required to connect to a certain remote, simply selecting the bookmark in the tab under the authentication form.
Bookmarks will be saved, if possible at:
- `$HOME/.config/termscp/` on Linux/BSD
- `$HOME/Library/Application Support/termscp` on MacOs
- `FOLDERID_RoamingAppData\termscp\` on Windows
For bookmarks only (this won't apply to recent hosts) it is also possible to save the password used to authenticate. The password is not saved by default and must be specified through the prompt when saving a new Bookmark.
> I was very undecided about storing passwords in termscp. The reason? Saving a password on your computer might give access to a hacker to any server you've registered. But I must admit by myself that for many machines typing the password everytime is really boring, also many times I have to work with machines in LAN, which wouldn't provide any advantage to an attacker, So I came out with a good compromise for passwords.
I warmly suggest you to follow these guidelines in order to decide whether you should or you shouldn't save passwords:
- **DON'T** save passwords for machines which are exposed on the internet, save passwords only for machines in LAN
- Make sure your machine is protected by attackers. If possible encrypt your disk and don't leave your PC unlocked while you're away.
- Preferably, save passwords only when a compromising of the target machine wouldn't be a problem.
In order to create a new bookmark, just follow these steps:
1. Type in the authentication form the parameters to connect to your remote server
2. Press `<CTRL+S>`
3. Type in the name you want to give to the bookmark
4. Choose whether to remind the password or not
5. Press `<ENTER>` to submit
whenever you want to use the previously saved connection, just press `<TAB>` to navigate to the bookmarks list and load the bookmark parameters into the form pressing `<ENTER>`.
![Bookmarks](https://github.com/veeso/termscp/blob/main/assets/images/bookmarks.gif?raw=true)
### Are my passwords Safe 😈
Well, Yep 😉.
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Absolutely! (except for BSD and WSL users 😢)
On **Windows**, **Linux** and **MacOS** the passwords are stored, if possible (but should be), respectively in the *Windows Vault*, in the *system keyring* and into the *Keychain*. This is actually super-safe and is directly managed by your operating system.
❗ Please, notice that if you're a Linux user, you should really read the [chapter below 👀](#linux-keyring), because the keyring might not be enabled or supported on your system!
On *BSD* and *WSL*, on the other hand, the key used to encrypt your passwords is stored on your drive (at $HOME/.config/termscp). It is then, still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
#### Linux Keyring
We all love Linux thanks to the freedom it gives to the users. You can basically do anything you want as a Linux user, but this has also some cons, such as the fact that often there is no standard applications across different distributions. And this involves keyring too.
This means that on Linux there might be no keyring installed on your system. Unfortunately the library we use to work with the key storage requires a service which exposes `org.freedesktop.secrets` on D-BUS and the worst fact is that there only two services exposing it.
- ❗ If you use GNOME as desktop environment (e.g. ubuntu users), you should already be fine, since keyring is already provided by `gnome-keyring` and everything should already be working.
- ❗ For other desktop environment users there is a nice program you can use to get a keyring which is [KeepassXC](https://keepassxc.org/), which I use on my Manjaro installation (with KDE) and works fine. The only problem is that you have to setup it to be used along with termscp (but it's quite simple). To get started with KeepassXC read more [here](#keepassxc-setup-for-termscp).
- ❗ What about you don't want to install any of these services? Well, there's no problem! **termscp will keep working as usual**, but it will save the key in a file, as it usually does for BSD and WSL.
##### KeepassXC setup for termscp
Follow these steps in order to setup keepassXC for termscp:
1. Install KeepassXC
2. Go to "tools" > "settings" in toolbar
3. Select "Secret service integration" and toggle "Enable KeepassXC freedesktop.org secret service integration"
4. Create a database, if you don't have one yet: from toolbar "Database" > "New database"
5. From toolbar: "Database" > "Database settings"
6. Select "Secret service integration" and toggle "Expose entries under this group"
7. Select the group in the list where you want the termscp secret to be kept. Remember that this group might be used by any other application to store secrets via DBUS.
---
## Configuration ⚙️
termscp supports some user defined parameters, which can be defined in the configuration.
Underhood termscp has a TOML file and some other directories where all the parameters will be saved, but don't worry, you won't touch any of these files manually, since I made possible to configure termscp from its user interface entirely.
termscp, like for bookmarks, just requires to have these paths accessible:
- `$HOME/.config/termscp/` on Linux/BSD
- `$HOME/Library/Application Support/termscp` on MacOs
- `FOLDERID_RoamingAppData\termscp\` on Windows
To access configuration, you just have to press `<CTRL+C>` from the home of termscp.
These parameters can be changed:
- **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**.
- **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.
- **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.
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)
### SSH Key Storage 🔐
Along with configuration, termscp provides also an **essential** feature for **SFTP/SCP clients**: the SSH key storage.
You can access the SSH key storage, from configuration moving to the `SSH Keys` tab, once there you can:
- **Add a new key**: just press `<CTRL+N>` and you will be prompted to create a new key. Provide the hostname/ip address and the username associated to the key and finally a text editor will open up: paste the **PRIVATE** ssh key into the text editor, save and quit.
- **Remove an existing key**: just press `<DEL>` or `<CTRL+E>` on the key you want to remove, to delete persistently the key from termscp.
- **Edit an existing key**: just press `<ENTER>` on the key you want to edit, to change the private key.
> Q: Wait, my private key is protected with password, can I use it?
> A: Of course you can. The password provided for authentication in termscp, is valid both for username/password authentication and for RSA key authentication.
### File Explorer Format
It is possible through configuration to define a custom format for the file explorer. This is possible both for local and remote host, so you can have two different syntax in use. These fields, with name `File formatter syntax (local)` and `File formatter syntax (remote)` will define how the file entries will be displayed in the file explorer.
The syntax for the formatter is the following `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
- The key name is mandatory and must be one of the keys below
- The length describes the length reserved to display the field. Static attributes doesn't support this (GROUP, PEX, SIZE, USER)
- Extra is supported only by some parameters and is an additional options. See keys to check if extra is supported.
These are the keys supported by the formatter:
- `ATIME`: Last access time (with default syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{ATIME:8:%H:%M}`)
- `CTIME`: Creation time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{CTIME:8:%H:%M}`)
- `GROUP`: Owner group
- `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{MTIME:8:%H:%M}`)
- `NAME`: File name (Elided if longer than 24)
- `PEX`: File permissions (UNIX format)
- `SIZE`: File size (omitted for directories)
- `SYMLINK`: Symlink (if any `-> {FILE_PATH}`)
- `USER`: Owner user
If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`
---
## Themes 🎨
Termscp provides you with an awesome feature: the possibility to set the colors for several components in the application.
If you want to customize termscp there are two available ways to do so:
- From the **configuration menu**
- Importing a **theme file**
In order to create your own customization from termscp, all you have to do so is to enter the configuration from the auth activity, pressing `<CTRL+C>` and then `<TAB>` twice. You should have now moved to the `themes` panel.
Here you can move with `<UP>` and `<DOWN>` to change the style you want to change, as shown in the gif below:
![Themes](../assets/images/themes.gif)
termscp supports both the traditional explicit hex (`#rrggbb`) and rgb `rgb(r, g, b)` syntax to provide colors, but also **[css colors](https://www.w3schools.com/cssref/css_colors.asp)** (such as `crimson`) are accepted 😉. There is also a special keywork which is `Default`. Default means that the color used will be the default foreground or background color based on the situation (foreground for texts and lines, background for well, guess what).
As said before, you can also import theme files. You can take inspiration from or directly use one of the themes provided along with termscp, located in the `themes/` directory of this repository and import them running termscp as `termscp -t <theme_file>`. If everything was fine, it should tell you the theme has successfully been imported.
### Styles 💈
You can find in the table below, the description for each style field.
Please, notice that **styles won't apply to configuration page**, in order to make it always accessible in case you mess everything up
#### Authentication page
| Key | Description |
|----------------|------------------------------------------|
| auth_address | Color of the input field for IP address |
| auth_bookmarks | Color of the bookmarks panel |
| auth_password | Color of the input field for password |
| auth_port | Color of the input field for port number |
| auth_protocol | Color of the radio group for protocol |
| auth_recents | Color of the recents panel |
| auth_username | Color of the input field for username |
#### Transfer page
| Key | Description |
|--------------------------------------|---------------------------------------------------------------------------|
| transfer_local_explorer_background | Background color of localhost explorer |
| transfer_local_explorer_foreground | Foreground coloor of localhost explorer |
| transfer_local_explorer_highlighted | Border and highlighted color for localhost explorer |
| transfer_remote_explorer_background | Background color of remote explorer |
| transfer_remote_explorer_foreground | Foreground coloor of remote explorer |
| transfer_remote_explorer_highlighted | Border and highlighted color for remote explorer |
| transfer_log_background | Background color for log panel |
| transfer_log_window | Window color for log panel |
| transfer_progress_bar_partial | Partial progress bar color |
| transfer_progress_bar_total | Total progress bar color |
| transfer_status_hidden | Color for status bar "hidden" label |
| transfer_status_sorting | Color for status bar "sorting" label; applies also to file sorting dialog |
| transfer_status_sync_browsing | Color for status bar "sync browsing" label |
#### Misc
These styles applie to different part of the application.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Color for error messages |
| misc_input_dialog | Color for input dialogs (such as copy file) |
| misc_keys | Color of text for key strokes |
| misc_quit_dialog | Color for quit dialogs |
| misc_save_dialog | Color for save dialogs |
| misc_warn_dialog | Color for warn dialogs |
---
## Text Editor ✏
termscp has, as you might have noticed, many features, one of these is the possibility to view and edit text file. It doesn't matter if the file is located on the local host or on the remote host, termscp provides the possibility to open a file in your favourite text editor.
In case the file is located on remote host, the file will be first downloaded into your temporary file directory and then, **only** if changes were made to the file, re-uploaded to the remote host. termscp checks if you made changes to the file verifying the last modification time of the file.
Just a reminder: **you can edit only textual file**; binary files are not supported.
### How do I configure the text editor 🦥
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor to use, change it in termscp configuration. [Read more](#configuration-)
---
## Logging 🩺
termscp writes a log file for each session, which is written at
- `$HOME/.config/termscp/termscp.log` on Linux/BSD
- `$HOME/Library/Application Support/termscp/termscp.log` on MacOs
- `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows
the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again.
The log file always reports in *trace* level, so it is kinda verbose.
I know you might have some questions regarding log files, so I made a kind of a Q/A:
> Is it possible to reduce verbosity?
No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set.
> If trace level is set for logging, is the file going to reach a huge size?
Probably not, unless you never quit termscp, but I think that's likely to happne. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
> I don't want logging, can I turn it off?
Yes, you can. Just start termscp with `-q or --quiet` option. You can alias termscp to make it persistent. Remember that logging is used to diagnose issues, so since behind every open source project, there should always be this kind of mutual help, keeping log files might be your way to support the project 😉. I don't want you to feel guilty, but just to say.
> Is logging safe?
If you're concerned about security, the log file doesn't contain any plain password, so don't worry and exposes the same information the sibling file `bookmarks` reports.

454
install.sh Executable file
View File

@@ -0,0 +1,454 @@
#!/usr/bin/env sh
# Options
#
# -V, --verbose
# Enable verbose output for the installer
#
# -f, -y, --force, --yes
# Skip the confirmation prompt during installation
TERMSCP_VERSION="0.6.1"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
RPM_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
set -eu
printf "\n"
BOLD="$(tput bold 2>/dev/null || printf '')"
GREY="$(tput setaf 0 2>/dev/null || printf '')"
UNDERLINE="$(tput smul 2>/dev/null || printf '')"
RED="$(tput setaf 1 2>/dev/null || printf '')"
GREEN="$(tput setaf 2 2>/dev/null || printf '')"
YELLOW="$(tput setaf 3 2>/dev/null || printf '')"
BLUE="$(tput setaf 4 2>/dev/null || printf '')"
MAGENTA="$(tput setaf 5 2>/dev/null || printf '')"
NO_COLOR="$(tput sgr0 2>/dev/null || printf '')"
# Functions
info() {
printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*"
}
warn() {
printf '%s\n' "${YELLOW}! $*${NO_COLOR}"
}
error() {
printf '%s\n' "${RED}x $*${NO_COLOR}" >&2
}
completed() {
printf '%s\n' "${GREEN}${NO_COLOR} $*"
}
has() {
command -v "$1" 1>/dev/null 2>&1
}
get_tmpfile() {
local suffix
suffix="$1"
if has mktemp; then
printf "%s.%s" "$(mktemp)" "${suffix}"
else
# No really good options here--let's pick a default + hope
printf "/tmp/termscp.%s" "${suffix}"
fi
}
download() {
output="$1"
url="$2"
if has curl; then
cmd="curl --fail --silent --location --output $output $url"
elif has wget; then
cmd="wget --quiet --output-document=$output $url"
elif has fetch; then
cmd="fetch --quiet --output=$output $url"
else
error "No HTTP download program (curl, wget, fetch) found, exiting…"
return 1
fi
$cmd && return 0 || rc=$?
error "Command failed (exit code $rc): ${BLUE}${cmd}${NO_COLOR}"
warn "If you believe this is a bug, please report immediately an issue to <https://github.com/veeso/termscp/issues/new>"
return $rc
}
test_writeable() {
local path
path="${1:-}/test.txt"
if touch "${path}" 2>/dev/null; then
rm "${path}"
return 0
else
return 1
fi
}
elevate_priv() {
if ! has sudo; then
error 'Could not find the command "sudo", needed to install termscp on your system.'
info "If you are on Windows, please run your shell as an administrator, then"
info "rerun this script. Otherwise, please run this script as root, or install"
info "sudo."
exit 1
fi
if ! sudo -v; then
error "Superuser not granted, aborting installation"
exit 1
fi
}
elevate_priv_ex() {
check_dir="$1"
if test_writeable "$check_dir"; then
sudo=""
else
warn "Root permissions are required to install dependecies"
elevate_priv
sudo="sudo"
fi
echo $sudo
}
# Currently supporting:
# - macos
# - linux
# - freebsd
detect_platform() {
local platform
platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "${platform}" in
linux) platform="linux" ;;
darwin) platform="macos" ;;
freebsd) platform="freebsd" ;;
esac
printf '%s' "${platform}"
}
# Currently supporting:
# - x86_64
detect_arch() {
local arch
arch="$(uname -m | tr '[:upper:]' '[:lower:]')"
case "${arch}" in
amd64) arch="x86_64" ;;
armv*) arch="arm" ;;
arm64) arch="aarch64" ;;
esac
# `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check
if [ "${arch}" = "x86_64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
arch="i686"
elif [ "${arch}" = "aarch64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
arch="arm"
fi
if [ "${arch}" != "x86_64" ]; then
error "Unsupported arch ${arch}"
return 1
fi
printf '%s' "${arch}"
}
confirm() {
if [ -z "${FORCE-}" ]; then
printf "%s " "${MAGENTA}?${NO_COLOR} $* ${BOLD}[y/N]${NO_COLOR}"
set +e
read -r yn </dev/tty
rc=$?
set -e
if [ $rc -ne 0 ]; then
error "Error reading from prompt (please re-run with the '--yes' option)"
exit 1
fi
if [ "$yn" != "y" ] && [ "$yn" != "yes" ]; then
error 'Aborting (please answer "yes" to continue)'
exit 1
fi
fi
}
# Installers
install_on_bsd() {
try_with_cargo "packages for freeBSD are distribuited no more. Only cargo installations are supported."
}
install_on_arch_linux() {
pkg="$1"
info "Detected ${YELLOW}${pkg}${NO_COLOR} on your system"
confirm "${YELLOW}rust${NO_COLOR} is required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
$pkg -S rust
info "Installing ${GREEN}termscp${NO_COLOR} AUR package…"
$pkg -S termscp
}
install_on_linux() {
local msg
local sudo
local archive
if has yay; then
install_on_arch_linux yay
elif has pakku; then
install_on_arch_linux pakku
elif has paru; then
install_on_arch_linux paru
elif has aurutils; then
install_on_arch_linux aurutils
elif has pamac; then
install_on_arch_linux pamac
elif has pikaur; then
install_on_arch_linux pikaur
elif has dpkg; then
if [ "${ARCH}" != "x86_64" ]; then # It's okay on AUR; not on other distros
try_with_cargo "we don't distribute packages for ${ARCH} at the moment"
else
info "Detected dpkg on your system"
info "Installing ${GREEN}termscp${NO_COLOR} via Debian package"
archive=$(get_tmpfile "deb")
download "${archive}" "${DEB_URL}"
info "Downloaded debian package to ${archive}"
if test_writeable "/usr/bin"; then
sudo=""
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
else
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}"
elevate_priv
sudo="sudo"
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
fi
info "$msg"
$sudo dpkg -i "${archive}"
fi
elif has rpm; then
if [ "${ARCH}" != "x86_64" ]; then # It's okay on AUR; not on other distros
try_with_cargo "we don't distribute packages for ${ARCH} at the moment"
else
info "Detected rpm on your system"
info "Installing ${GREEN}termscp${NO_COLOR} via RPM package"
archive=$(get_tmpfile "rpm")
download "${archive}" "${RPM_URL}"
info "Downloaded rpm package to ${archive}"
if test_writeable "/usr/bin"; then
sudo=""
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
else
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}"
elevate_priv
sudo="sudo"
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
fi
info "$msg"
$sudo rpm -U "${archive}"
fi
else
try_with_cargo "No suitable installation method found for your Linux distribution; if you're running on Arch linux, please install an AUR package manager (such as yay). Currently only Arch, Debian based and Red Hat based distros are supported"
fi
}
install_on_macos() {
if has brew; then
if has termscp; then
info "Upgrading ${GREEN}termscp${NO_COLOR}"
# The OR is used since someone could have installed via cargo previously
brew update && brew upgrade termscp || brew install veeso/termscp/termscp
else
info "Installing ${GREEN}termscp${NO_COLOR}"
brew install veeso/termscp/termscp
fi
else
try_with_cargo "brew is missing on your system; please install it from <https://brew.sh/>"
fi
}
# -- cargo installation
install_bsd_cargo_deps() {
set -e
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
sudo="$(elevate_priv_ex /usr/local/bin)"
$sudo pkg install -y curl wget libssh gcc
info "Dependencies installed successfully"
}
install_linux_cargo_deps() {
local debian_deps="gcc pkg-config libssl-dev libssh2-1-dev libdbus-1-dev"
local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel"
local arch_deps="gcc openssl pkg-config dbus"
local deps_cmd=""
# Get pkg manager
if has apt; then
deps_cmd="apt install -y $debian_deps"
elif has apt-get; then
deps_cmd="apt-get install -y $debian_deps"
elif has yum; then
deps_cmd="yum -y install $rpm_deps"
elif has dnf; then
deps_cmd="dnf -y install $rpm_deps"
elif has pacman; then
deps_cmd="pacman -S --noconfirm $arch_deps"
else
error "Could not find any suitable package manager for your linux distro 🙄"
error "Supported package manager are: 'apt', 'apt-get', 'yum', 'dnf', 'pacman'"
exit 1
fi
set -e
confirm "${YELLOW}libssh, gcc, openssl, pkg-config, libdbus${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}. The following command will be used to install the dependencies: '${BOLD}${YELLOW}${deps_cmd}${NO_COLOR}'. Would you like to proceed?"
sudo="$(elevate_priv_ex /usr/local/bin)"
$sudo $deps_cmd
info "Dependencies installed successfully"
}
install_cargo() {
if has cargo; then
return 0
fi
cargo_env="$HOME/.cargo/env"
# Check if cargo is already installed (actually), but not loaded
if [ -f $cargo_env ]; then
. $cargo_env
fi
# Check again cargo
if has cargo; then
return 0
else
confirm "${YELLOW}rust${NO_COLOR} is required to build termscp with cargo; would you like to install it now?"
set -e
rustup=$(get_tmpfile "sh")
info "Downloading rustup.sh…"
download "${rustup}" "https://sh.rustup.rs"
chmod +x $rustup
$rustup -y
info "Rust installed with success"
. $cargo_env
fi
}
try_with_cargo() {
err="$1"
# Install cargo
install_cargo
if has cargo; then
info "Installing ${GREEN}termscp${NO_COLOR} via Cargo…"
case $PLATFORM in
"freebsd")
install_bsd_cargo_deps
cargo install --no-default-features termscp
;;
"linux")
install_linux_cargo_deps
cargo install termscp
;;
*)
cargo install termscp
;;
esac
else
error "$err"
error "Alternatively you can opt for installing Cargo <https://www.rust-lang.org/tools/install>"
return 1
fi
}
# defaults
if [ -z "${PLATFORM-}" ]; then
PLATFORM="$(detect_platform)"
fi
if [ -z "${BIN_DIR-}" ]; then
BIN_DIR=/usr/local/bin
fi
if [ -z "${ARCH-}" ]; then
ARCH="$(detect_arch)"
fi
if [ -z "${BASE_URL-}" ]; then
BASE_URL="https://github.com/starship/starship/releases"
fi
# parse argv variables
while [ "$#" -gt 0 ]; do
case "$1" in
-V | --verbose)
VERBOSE=1
shift 1
;;
-f | -y | --force | --yes)
FORCE=1
shift 1
;;
-V=* | --verbose=*)
VERBOSE="${1#*=}"
shift 1
;;
-f=* | -y=* | --force=* | --yes=*)
FORCE="${1#*=}"
shift 1
;;
*)
error "Unknown option: $1"
exit 1
;;
esac
done
printf " %s\n" "${UNDERLINE}Termscp configuration${NO_COLOR}"
info "${BOLD}Platform${NO_COLOR}: ${GREEN}${PLATFORM}${NO_COLOR}"
info "${BOLD}Arch${NO_COLOR}: ${GREEN}${ARCH}${NO_COLOR}"
# non-empty VERBOSE enables verbose untarring
if [ -n "${VERBOSE-}" ]; then
VERBOSE=v
info "${BOLD}Verbose${NO_COLOR}: yes"
else
VERBOSE=
fi
printf "\n"
confirm "Install ${GREEN}termscp ${TERMSCP_VERSION}${NO_COLOR}?"
# Installation based on arch
case $PLATFORM in
"freebsd")
install_on_bsd
;;
"linux")
install_on_linux
;;
"macos")
install_on_macos
;;
*)
error "${PLATFORM} is not supported by this installer"
exit 1
;;
esac
completed "Congratulations! Termscp has successfully been installed on your system!"
info "If you're a new user, you might be interested in reading the user manual <https://veeso.github.io/termscp/#user-manual>"
info "While if you've just updated your termscp version, you can find the changelog at this link <https://veeso.github.io/termscp/#changelog>"
info "Remember that if you encounter any issue, you can report them on Github <https://github.com/veeso/termscp/issues/new>"
info "Feel free to open an issue also if you have an idea which could improve the project"
info "If you want to support the project, please, consider a little donation <https://www.buymeacoffee.com/veeso>"
info "I hope you'll enjoy using termscp :D"
exit 0

View File

@@ -2,40 +2,43 @@
//!
//! `activity_manager` is the module which provides run methods and handling for activities
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
use std::path::PathBuf;
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Deps
use crate::filetransfer::FileTransferProtocol;
use crate::host::Localhost;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::host::{HostError, Localhost};
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::system::theme_provider::ThemeProvider;
use crate::ui::activities::{
auth_activity::AuthActivity,
filetransfer_activity::FileTransferActivity, filetransfer_activity::FileTransferParams,
Activity,
auth::AuthActivity, filetransfer::FileTransferActivity, setup::SetupActivity, Activity,
ExitReason,
};
use crate::ui::context::Context;
// Namespaces
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::Duration;
@@ -45,6 +48,7 @@ use std::time::Duration;
pub enum NextActivity {
Authentication,
FileTransfer,
SetupActivity,
}
/// ### ActivityManager
@@ -52,49 +56,40 @@ pub enum NextActivity {
/// The activity manager takes care of running activities and handling them until the application has ended
pub struct ActivityManager {
context: Option<Context>,
ftparams: Option<FileTransferParams>,
interval: Duration,
local_dir: PathBuf,
}
impl ActivityManager {
/// ### new
///
/// Initializes a new Activity Manager
pub fn new(
local_dir: &PathBuf,
interval: Duration,
) -> Result<ActivityManager, ()> {
pub fn new(local_dir: &Path, interval: Duration) -> Result<ActivityManager, HostError> {
// Prepare Context
let host: Localhost = match Localhost::new(local_dir.clone()) {
Ok(h) => h,
Err(_) => return Err(()),
};
let ctx: Context = Context::new(host);
// Initialize configuration client
let (config_client, error): (ConfigClient, Option<String>) =
match Self::init_config_client() {
Ok(cli) => (cli, None),
Err(err) => {
error!("Failed to initialize config client: {}", err);
(ConfigClient::degraded(), Some(err))
}
};
let theme_provider: ThemeProvider = Self::init_theme_provider();
let ctx: Context = Context::new(config_client, theme_provider, error);
Ok(ActivityManager {
context: Some(ctx),
ftparams: None,
interval: interval,
local_dir: local_dir.to_path_buf(),
interval,
})
}
/// ### set_filetransfer_params
///
/// Set file transfer params
pub fn set_filetransfer_params(
&mut self,
address: String,
port: u16,
protocol: FileTransferProtocol,
username: Option<String>,
password: Option<String>,
) {
self.ftparams = Some(FileTransferParams {
address: address,
port: port,
protocol: protocol,
username: username,
password: password,
});
pub fn set_filetransfer_params(&mut self, params: FileTransferParams) {
// Put params into the context
self.context.as_mut().unwrap().set_ftparams(params);
}
/// ### run
@@ -109,6 +104,7 @@ impl ActivityManager {
Some(activity) => match activity {
NextActivity::Authentication => self.run_authentication(),
NextActivity::FileTransfer => self.run_filetransfer(),
NextActivity::SetupActivity => self.run_setup(),
},
None => break, // Exit
}
@@ -117,7 +113,7 @@ impl ActivityManager {
drop(self.context.take());
}
// Loops
// -- Activity Loops
/// ### run_authentication
///
@@ -125,14 +121,18 @@ impl ActivityManager {
/// Returns when activity terminates.
/// Returns the next activity to run
fn run_authentication(&mut self) -> Option<NextActivity> {
info!("Starting AuthActivity...");
// Prepare activity
let mut activity: AuthActivity = AuthActivity::new();
let mut activity: AuthActivity = AuthActivity::default();
// Prepare result
let result: Option<NextActivity>;
// Get context
let ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => return None
None => {
error!("Failed to start AuthActivity: context is None");
return None;
}
};
// Create activity
activity.on_create(ctx);
@@ -140,35 +140,34 @@ impl ActivityManager {
// Draw activity
activity.on_draw();
// Check if has to be terminated
if activity.quit {
// Quit activities
result = None;
break;
}
if activity.submit {
// User submitted, set next activity
result = Some(NextActivity::FileTransfer);
// Get params
self.ftparams = Some(FileTransferParams {
address: activity.address.clone(),
port: activity.port.parse::<u16>().ok().unwrap(),
username: match activity.username.len() {
0 => None,
_ => Some(activity.username.clone()),
},
password: match activity.password.len() {
0 => None,
_ => Some(activity.password.clone()),
},
protocol: activity.protocol.clone(),
});
break;
if let Some(exit_reason) = activity.will_umount() {
match exit_reason {
ExitReason::Quit => {
info!("AuthActivity terminated due to 'Quit'");
result = None;
break;
}
ExitReason::EnterSetup => {
// User requested activity
info!("AuthActivity terminated due to 'EnterSetup'");
result = Some(NextActivity::SetupActivity);
break;
}
ExitReason::Connect => {
// User submitted, set next activity
info!("AuthActivity terminated due to 'Connect'");
result = Some(NextActivity::FileTransfer);
break;
}
_ => { /* Nothing to do */ }
}
}
// Sleep for ticks
sleep(self.interval);
}
// Destroy activity
self.context = activity.on_destroy();
info!("AuthActivity destroyed");
result
}
@@ -178,34 +177,58 @@ impl ActivityManager {
/// Returns when activity terminates.
/// Returns the next activity to run
fn run_filetransfer(&mut self) -> Option<NextActivity> {
if self.ftparams.is_none() {
return Some(NextActivity::Authentication);
}
info!("Starting FileTransferActivity");
// Get context
let mut ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => {
error!("Failed to start FileTransferActivity: context is None");
return None;
}
};
// If ft params is None, return None
let ft_params: &FileTransferParams = match ctx.ft_params() {
Some(ft_params) => ft_params,
None => {
error!("Failed to start FileTransferActivity: file transfer params is None");
return None;
}
};
// Prepare activity
let mut activity: FileTransferActivity =
FileTransferActivity::new(self.ftparams.take().unwrap());
let protocol: FileTransferProtocol = ft_params.protocol;
let host: Localhost = match Localhost::new(self.local_dir.clone()) {
Ok(host) => host,
Err(err) => {
// Set error in context
error!("Failed to initialize localhost: {}", err);
ctx.set_error(format!("Could not initialize localhost: {}", err));
return None;
}
};
let mut activity: FileTransferActivity = FileTransferActivity::new(host, protocol);
// Prepare result
let result: Option<NextActivity>;
// Get context
let ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => return None
};
// Create activity
activity.on_create(ctx);
loop {
// Draw activity
activity.on_draw();
// Check if has to be terminated
if activity.quit {
// Quit activities
result = None;
break;
}
if activity.disconnected {
// User disconnected, set next activity to authentication
result = Some(NextActivity::Authentication);
break;
if let Some(exit_reason) = activity.will_umount() {
match exit_reason {
ExitReason::Quit => {
info!("FileTransferActivity terminated due to 'Quit'");
result = None;
break;
}
ExitReason::Disconnect => {
// User disconnected, set next activity to authentication
info!("FileTransferActivity terminated due to 'Authentication'");
result = Some(NextActivity::Authentication);
break;
}
_ => { /* Nothing to do */ }
}
}
// Sleep for ticks
sleep(self.interval);
@@ -214,4 +237,98 @@ impl ActivityManager {
self.context = activity.on_destroy();
result
}
/// ### run_setup
///
/// `SetupActivity` run loop.
/// Returns when activity terminates.
/// Returns the next activity to run
fn run_setup(&mut self) -> Option<NextActivity> {
// Prepare activity
let mut activity: SetupActivity = SetupActivity::default();
// Get context
let ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => {
error!("Failed to start SetupActivity: context is None");
return None;
}
};
// Create activity
activity.on_create(ctx);
loop {
// Draw activity
activity.on_draw();
// Check if activity has terminated
if let Some(ExitReason::Quit) = activity.will_umount() {
info!("SetupActivity terminated due to 'Quit'");
break;
}
// Sleep for ticks
sleep(self.interval);
}
// Destroy activity
self.context = activity.on_destroy();
// This activity always returns to AuthActivity
Some(NextActivity::Authentication)
}
// -- misc
/// ### init_config_client
///
/// Initialize configuration client
fn init_config_client() -> Result<ConfigClient, String> {
// Get config dir
match environment::init_config_dir() {
Ok(config_dir) => {
match config_dir {
Some(config_dir) => {
// Get config client paths
let (config_path, ssh_dir): (PathBuf, PathBuf) =
environment::get_config_paths(config_dir.as_path());
match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) {
Ok(cli) => Ok(cli),
Err(err) => Err(format!("Could not read configuration: {}", err)),
}
}
None => Err(String::from(
"Your system doesn't provide a configuration directory",
)),
}
}
Err(err) => Err(format!(
"Could not initialize configuration directory: {}",
err
)),
}
}
fn init_theme_provider() -> ThemeProvider {
match environment::init_config_dir() {
Ok(config_dir) => {
match config_dir {
Some(config_dir) => {
// Get config client paths
let theme_path: PathBuf = environment::get_theme_path(config_dir.as_path());
match ThemeProvider::new(theme_path.as_path()) {
Ok(provider) => provider,
Err(err) => {
error!("Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err);
ThemeProvider::degraded()
}
}
}
None => {
error!("This system doesn't provide a configuration directory; using theme provider in degraded mode");
ThemeProvider::degraded()
}
}
}
Err(err) => {
error!("Could not initialize configuration directory: {}; using theme provider in degraded mode", err);
ThemeProvider::degraded()
}
}
}
}

124
src/config/bookmarks.rs Normal file
View File

@@ -0,0 +1,124 @@
//! ## Bookmarks
//!
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
/**
* 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 serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserHosts
///
/// UserHosts contains all the hosts saved by the user in the data storage
/// It contains both `Bookmark`
pub struct UserHosts {
pub bookmarks: HashMap<String, Bookmark>,
pub recents: HashMap<String, Bookmark>,
}
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
/// ## Bookmark
///
/// Bookmark describes a single bookmark entry in the user hosts storage
pub struct Bookmark {
pub address: String,
pub port: u16,
pub protocol: String,
pub username: String,
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
}
impl Default for UserHosts {
fn default() -> Self {
Self {
bookmarks: HashMap::new(),
recents: HashMap::new(),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_bookmarks_default() {
let bookmarks: UserHosts = UserHosts::default();
assert_eq!(bookmarks.bookmarks.len(), 0);
assert_eq!(bookmarks.recents.len(), 0);
}
#[test]
fn test_bookmarks_bookmark_new() {
let bookmark: Bookmark = Bookmark {
address: String::from("192.168.1.1"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
password: Some(String::from("password")),
};
let recent: Bookmark = Bookmark {
address: String::from("192.168.1.2"),
port: 22,
protocol: String::from("SCP"),
username: String::from("admin"),
password: Some(String::from("password")),
};
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
bookmarks.insert(String::from("test"), bookmark);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(String::from("ISO20201218T181432"), recent);
let hosts: UserHosts = UserHosts {
bookmarks: bookmarks,
recents: recents,
};
// Verify
let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.1"));
assert_eq!(bookmark.port, 22);
assert_eq!(bookmark.protocol, String::from("SFTP"));
assert_eq!(bookmark.username, String::from("root"));
assert_eq!(
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
let bookmark: &Bookmark = hosts
.recents
.get(&String::from("ISO20201218T181432"))
.unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.2"));
assert_eq!(bookmark.port, 22);
assert_eq!(bookmark.protocol, String::from("SCP"));
assert_eq!(bookmark.username, String::from("admin"));
assert_eq!(
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
}
}

34
src/config/mod.rs Normal file
View File

@@ -0,0 +1,34 @@
//! ## Config
//!
//! `config` is the module which provides access to all the termscp configurations
/**
* 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.
*/
// export
pub use params::*;
pub mod bookmarks;
pub mod params;
pub mod serialization;
pub mod themes;

155
src/config/params.rs Normal file
View File

@@ -0,0 +1,155 @@
//! ## Config
//!
//! `config` is the module which provides access to termscp configuration
/**
* 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::filetransfer::FileTransferProtocol;
// Ext
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserConfig
///
/// UserConfig contains all the configurations for the user,
/// supported by termscp
pub struct UserConfig {
pub user_interface: UserInterfaceConfig,
pub remote: RemoteConfig,
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserInterfaceConfig
///
/// UserInterfaceConfig provides all the keys to configure the user interface
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>, // Refers to local host (for backward compatibility)
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## RemoteConfig
///
/// Contains configuratio related to remote hosts
pub struct RemoteConfig {
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
}
impl Default for UserConfig {
fn default() -> Self {
UserConfig {
user_interface: UserInterfaceConfig::default(),
remote: RemoteConfig::default(),
}
}
}
impl Default for UserInterfaceConfig {
fn default() -> Self {
UserInterfaceConfig {
text_editor: match edit::get_editor() {
Ok(p) => p,
Err(_) => PathBuf::from("nano"), // Default to nano
},
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
check_for_updates: Some(true),
group_dirs: None,
file_fmt: None,
remote_file_fmt: None,
}
}
}
impl Default for RemoteConfig {
fn default() -> Self {
RemoteConfig {
ssh_keys: HashMap::new(),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_config_mod_new() {
let mut keys: HashMap<String, PathBuf> = HashMap::with_capacity(1);
keys.insert(
String::from("192.168.1.31"),
PathBuf::from("/tmp/private.key"),
);
let remote: RemoteConfig = RemoteConfig { ssh_keys: keys };
let ui: UserInterfaceConfig = UserInterfaceConfig {
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}")),
remote_file_fmt: Some(String::from("{USER}")),
};
assert_eq!(ui.default_protocol, String::from("SFTP"));
assert_eq!(ui.text_editor, PathBuf::from("nano"));
assert_eq!(ui.show_hidden_files, true);
assert_eq!(ui.check_for_updates, Some(true));
assert_eq!(ui.group_dirs, Some(String::from("first")));
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
let cfg: UserConfig = UserConfig {
user_interface: ui,
remote: remote,
};
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/tmp/private.key")
);
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}")));
assert_eq!(
cfg.user_interface.remote_file_fmt,
Some(String::from("{USER}"))
);
}
}

572
src/config/serialization.rs Normal file
View File

@@ -0,0 +1,572 @@
//! ## Serialization
//!
//! `serialization` provides serialization and deserialization for configurations
/**
* 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 serde::{de::DeserializeOwned, Serialize};
use std::io::{Read, Write};
use thiserror::Error;
/// ## SerializerError
///
/// Contains the error for serializer/deserializer
#[derive(std::fmt::Debug)]
pub struct SerializerError {
kind: SerializerErrorKind,
msg: Option<String>,
}
/// ## SerializerErrorKind
///
/// Describes the kind of error for the serializer/deserializer
#[derive(Error, Debug)]
pub enum SerializerErrorKind {
#[error("Operation failed")]
Generic,
#[error("IO error")]
Io,
#[error("Serialization error")]
Serialization,
#[error("Syntax error")]
Syntax,
}
impl SerializerError {
/// ### new
///
/// Instantiate a new `SerializerError`
pub fn new(kind: SerializerErrorKind) -> SerializerError {
SerializerError { kind, msg: None }
}
/// ### new_ex
///
/// Instantiates a new `SerializerError` with description message
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
let mut err: SerializerError = SerializerError::new(kind);
err.msg = Some(msg);
err
}
}
impl std::fmt::Display for SerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.msg {
Some(msg) => write!(f, "{} ({})", self.kind, msg),
None => write!(f, "{}", self.kind),
}
}
}
/// ### serialize
///
/// Serialize `UserHosts` into TOML and write content to writable
pub fn serialize<S>(serializable: &S, mut writable: Box<dyn Write>) -> Result<(), SerializerError>
where
S: Serialize + Sized,
{
// Serialize content
let data: String = match toml::ser::to_string(serializable) {
Ok(dt) => dt,
Err(err) => {
return Err(SerializerError::new_ex(
SerializerErrorKind::Serialization,
err.to_string(),
))
}
};
trace!("Serialized new bookmarks data: {}", data);
// Write file
match writable.write_all(data.as_bytes()) {
Ok(_) => Ok(()),
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
)),
}
}
/// ### deserialize
///
/// Read data from readable and deserialize its content as TOML
pub fn deserialize<S>(mut readable: Box<dyn Read>) -> Result<S, SerializerError>
where
S: DeserializeOwned + Sized + std::fmt::Debug,
{
// Read file content
let mut data: String = String::new();
if let Err(err) = readable.read_to_string(&mut data) {
return Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
));
}
trace!("Read bookmarks from file: {}", data);
// Deserialize
match toml::de::from_str(data.as_str()) {
Ok(deserialized) => {
debug!("Read bookmarks from file {:?}", deserialized);
Ok(deserialized)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::Syntax,
err.to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::io::{Seek, SeekFrom};
use std::path::PathBuf;
use tuirealm::tui::style::Color;
use crate::config::bookmarks::{Bookmark, UserHosts};
use crate::config::params::UserConfig;
use crate::config::themes::Theme;
use crate::utils::test_helpers::create_file_ioers;
#[test]
fn test_config_serialization_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax);
assert!(error.msg.is_none());
assert_eq!(format!("{}", error), String::from("Syntax error"));
let error: SerializerError =
SerializerError::new_ex(SerializerErrorKind::Syntax, String::from("bad syntax"));
assert!(error.msg.is_some());
assert_eq!(
format!("{}", error),
String::from("Syntax error (bad syntax)")
);
// Fmt
assert_eq!(
format!("{}", SerializerError::new(SerializerErrorKind::Generic)),
String::from("Operation failed")
);
assert_eq!(
format!("{}", SerializerError::new(SerializerErrorKind::Io)),
String::from("IO error")
);
assert_eq!(
format!(
"{}",
SerializerError::new(SerializerErrorKind::Serialization)
),
String::from("Serialization error")
);
}
// -- Serialization of params
#[test]
fn test_config_serialization_params_deserialize_ok() {
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks_params();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let cfg = deserialize(Box::new(toml_file));
assert!(cfg.is_ok());
let cfg: UserConfig = cfg.ok().unwrap();
// Verify configuration
// Verify ui
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.remote_file_fmt,
Some(String::from("{NAME} {USER}")),
);
// Verify keys
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/raspberry.key")
);
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.32"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/beaglebone.key")
);
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
}
#[test]
fn test_config_serialization_params_deserialize_ok_no_opts() {
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks_params_no_opts();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let cfg = deserialize(Box::new(toml_file));
assert!(cfg.is_ok());
let cfg: UserConfig = cfg.ok().unwrap();
// Verify configuration
// Verify ui
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.group_dirs, None);
assert!(cfg.user_interface.check_for_updates.is_none());
assert!(cfg.user_interface.file_fmt.is_none());
assert!(cfg.user_interface.remote_file_fmt.is_none());
// Verify keys
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.31"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/raspberry.key")
);
assert_eq!(
*cfg.remote
.ssh_keys
.get(&String::from("192.168.1.32"))
.unwrap(),
PathBuf::from("/home/omar/.ssh/beaglebone.key")
);
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
}
#[test]
fn test_config_serialization_params_deserialize_nok() {
let toml_file: tempfile::NamedTempFile = create_bad_toml_bookmarks_params();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
assert!(deserialize::<UserConfig>(Box::new(toml_file)).is_err());
}
#[test]
fn test_config_serialization_params_serialize() {
let mut cfg: UserConfig = UserConfig::default();
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
// Insert key
cfg.remote.ssh_keys.insert(
String::from("192.168.1.31"),
PathBuf::from("/home/omar/.ssh/id_rsa"),
);
// Serialize
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
assert!(serialize(&cfg, writer).is_ok());
// Reload configuration and check if it's ok
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(deserialize::<UserConfig>(Box::new(toml_file)).is_ok());
}
#[test]
fn test_config_serialization_params_fail_write() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let writer: Box<dyn Write> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
let cfg: UserConfig = UserConfig::default();
assert!(serialize(&cfg, writer).is_err());
}
#[test]
fn test_config_serialization_params_fail_read() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let reader: Box<dyn Read> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
assert!(deserialize::<UserConfig>(reader).is_err());
}
fn create_good_toml_bookmarks_params() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
check_for_updates = true
group_dirs = "last"
file_fmt = "{NAME} {PEX}"
remote_file_fmt = "{NAME} {USER}"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_good_toml_bookmarks_params_no_opts() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_bad_toml_bookmarks_params() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[user_interface]
default_protocol = "SFTP"
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
// -- bookmarks
#[test]
fn test_config_serializer_bookmarks_serializer_deserialize_ok() {
let toml_file: tempfile::NamedTempFile = create_good_toml_bookmarks();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
let hosts = deserialize(Box::new(toml_file));
assert!(hosts.is_ok());
let hosts: UserHosts = hosts.ok().unwrap();
// Verify hosts
// Verify recents
assert_eq!(hosts.recents.len(), 1);
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
assert_eq!(host.address, String::from("172.16.104.10"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SCP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(host.password, None);
// Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 3);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address, String::from("192.168.1.31"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
assert_eq!(host.address, String::from("192.168.1.30"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("cvisintin"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
assert_eq!(host.address, String::from("51.23.67.12"));
assert_eq!(host.port, 21);
assert_eq!(host.protocol, String::from("FTPS"));
assert_eq!(host.username, String::from("aws001"));
assert_eq!(host.password, None);
}
#[test]
fn test_config_serializer_bookmarks_serializer_deserialize_nok() {
let toml_file: tempfile::NamedTempFile = create_bad_toml_bookmarks();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
// Parse
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
}
#[test]
fn test_config_serializer_bookmarks_serializer_serialize() {
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
// Push two samples
bookmarks.insert(
String::from("raspberrypi2"),
Bookmark {
address: String::from("192.168.1.31"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
password: None,
},
);
bookmarks.insert(
String::from("msi-estrem"),
Bookmark {
address: String::from("192.168.1.30"),
port: 4022,
protocol: String::from("SFTP"),
username: String::from("cvisintin"),
password: Some(String::from("password")),
},
);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(
String::from("ISO20201215T094000Z"),
Bookmark {
address: String::from("192.168.1.254"),
port: 3022,
protocol: String::from("SCP"),
username: String::from("omar"),
password: Some(String::from("aaa")),
},
);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
// Serialize
let hosts: UserHosts = UserHosts { bookmarks, recents };
assert!(serialize(&hosts, Box::new(tmpfile)).is_ok());
}
#[test]
fn test_config_serialization_theme_serialize() {
let mut theme: Theme = Theme::default();
theme.auth_address = Color::Rgb(240, 240, 240);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let (reader, writer) = create_file_ioers(tmpfile.path());
assert!(serialize(&theme, Box::new(writer)).is_ok());
// Try to deserialize
let deserialized_theme: Theme = deserialize(Box::new(reader)).ok().unwrap();
assert_eq!(theme, deserialized_theme);
}
#[test]
fn test_config_serialization_theme_deserialize() {
let toml_file = create_good_toml_theme();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
let toml_file = create_bad_toml_theme();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
}
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
tmpfile
}
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", protocol = "SCP", username = "root", port = 22 }
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_good_toml_theme() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r##"auth_address = "Yellow"
auth_bookmarks = "LightGreen"
auth_password = "LightBlue"
auth_port = "LightCyan"
auth_protocol = "LightGreen"
auth_recents = "LightBlue"
auth_username = "LightMagenta"
misc_error_dialog = "Red"
misc_input_dialog = "240,240,240"
misc_keys = "Cyan"
misc_quit_dialog = "Yellow"
misc_save_dialog = "Cyan"
misc_warn_dialog = "LightRed"
transfer_local_explorer_background = "rgb(240, 240, 240)"
transfer_local_explorer_foreground = "rgb(60, 60, 60)"
transfer_local_explorer_highlighted = "Yellow"
transfer_log_background = "255, 255, 255"
transfer_log_window = "LightGreen"
transfer_progress_bar_full = "forestgreen"
transfer_progress_bar_partial = "Green"
transfer_remote_explorer_background = "#f0f0f0"
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
transfer_remote_explorer_highlighted = "LightBlue"
transfer_status_hidden = "LightBlue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "LightGreen"
"##;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_bad_toml_theme() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
auth_address = "Yellow"
auth_bookmarks = "LightGreen"
auth_password = "LightBlue"
auth_port = "LightCyan"
auth_protocol = "LightGreen"
auth_recents = "LightBlue"
auth_username = "LightMagenta"
misc_error_dialog = "Red"
misc_input_dialog = "240,240,240"
misc_keys = "Cyan"
misc_quit_dialog = "Yellow"
misc_warn_dialog = "LightRed"
transfer_local_explorer_text = "rgb(240, 240, 240)"
transfer_local_explorer_window = "Yellow"
transfer_log_text = "255, 255, 255"
transfer_log_window = "LightGreen"
transfer_progress_bar = "Green"
transfer_remote_explorer_text = "verdazzurro"
transfer_remote_explorer_window = "LightBlue"
transfer_status_hidden = "LightBlue"
transfer_status_sorting = "LightYellow"
transfer_status_sync_browsing = "LightGreen"
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
}

267
src/config/themes.rs Normal file
View File

@@ -0,0 +1,267 @@
//! ## Themes
//!
//! `themes` is the module which provides the themes configurations and the serializers
/**
* 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::utils::fmt::fmt_color;
use crate::utils::parser::parse_color;
// ext
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
use tuirealm::tui::style::Color;
/// ### Theme
///
/// Theme contains all the colors lookup table for termscp
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Theme {
// -- auth
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_address: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_bookmarks: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_password: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_port: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_protocol: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_recents: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_username: Color,
// -- misc
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_error_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_input_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_keys: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_quit_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_save_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_warn_dialog: Color,
// -- transfer
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_background: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_foreground: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_highlighted: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_log_background: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_log_window: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_progress_bar_full: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_progress_bar_partial: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_background: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_foreground: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_highlighted: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_hidden: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_sorting: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_sync_browsing: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
auth_address: Color::Yellow,
auth_bookmarks: Color::LightGreen,
auth_password: Color::LightBlue,
auth_port: Color::LightCyan,
auth_protocol: Color::LightGreen,
auth_recents: Color::LightBlue,
auth_username: Color::LightMagenta,
misc_error_dialog: Color::Red,
misc_input_dialog: Color::Reset,
misc_keys: Color::Cyan,
misc_quit_dialog: Color::Yellow,
misc_save_dialog: Color::LightCyan,
misc_warn_dialog: Color::LightRed,
transfer_local_explorer_background: Color::Reset,
transfer_local_explorer_foreground: Color::Reset,
transfer_local_explorer_highlighted: Color::Yellow,
transfer_log_background: Color::Reset,
transfer_log_window: Color::LightGreen,
transfer_progress_bar_partial: Color::Green,
transfer_progress_bar_full: Color::Green,
transfer_remote_explorer_background: Color::Reset,
transfer_remote_explorer_foreground: Color::Reset,
transfer_remote_explorer_highlighted: Color::LightBlue,
transfer_status_hidden: Color::LightBlue,
transfer_status_sorting: Color::LightYellow,
transfer_status_sync_browsing: Color::LightGreen,
}
}
}
// -- deserializer
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(deserializer)?;
// Parse color
match parse_color(s) {
None => Err(DeError::custom("Invalid color")),
Some(color) => Ok(color),
}
}
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Convert color to string
let s: String = fmt_color(color);
serializer.serialize_str(s.as_str())
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_config_themes_default() {
let theme: Theme = Theme::default();
assert_eq!(theme.auth_address, Color::Yellow);
assert_eq!(theme.auth_bookmarks, Color::LightGreen);
assert_eq!(theme.auth_password, Color::LightBlue);
assert_eq!(theme.auth_port, Color::LightCyan);
assert_eq!(theme.auth_protocol, Color::LightGreen);
assert_eq!(theme.auth_recents, Color::LightBlue);
assert_eq!(theme.auth_username, Color::LightMagenta);
assert_eq!(theme.misc_error_dialog, Color::Red);
assert_eq!(theme.misc_input_dialog, Color::Reset);
assert_eq!(theme.misc_keys, Color::Cyan);
assert_eq!(theme.misc_quit_dialog, Color::Yellow);
assert_eq!(theme.misc_save_dialog, Color::LightCyan);
assert_eq!(theme.misc_warn_dialog, Color::LightRed);
assert_eq!(theme.transfer_local_explorer_background, Color::Reset);
assert_eq!(theme.transfer_local_explorer_foreground, Color::Reset);
assert_eq!(theme.transfer_local_explorer_highlighted, Color::Yellow);
assert_eq!(theme.transfer_log_background, Color::Reset);
assert_eq!(theme.transfer_log_window, Color::LightGreen);
assert_eq!(theme.transfer_progress_bar_full, Color::Green);
assert_eq!(theme.transfer_progress_bar_partial, Color::Green);
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
assert_eq!(theme.transfer_remote_explorer_highlighted, Color::LightBlue);
assert_eq!(theme.transfer_status_hidden, Color::LightBlue);
assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,42 +2,49 @@
//!
//! `filetransfer` is the module which provides the trait file transfers must implement and the different file transfers
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* 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::fs::{FsEntry, FsFile};
// ext
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use crate::fs::{FsEntry, FsFile};
// Transfers
use thiserror::Error;
use wildmatch::WildMatch;
// exports
pub mod ftp_transfer;
pub mod params;
pub mod scp_transfer;
pub mod sftp_transfer;
pub use params::FileTransferParams;
/// ## FileTransferProtocol
///
/// This enum defines the different transfer protocol available in TermSCP
/// This enum defines the different transfer protocol available in termscp
#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)]
#[derive(PartialEq, Debug, std::clone::Clone, Copy)]
pub enum FileTransferProtocol {
Sftp,
Scp,
@@ -47,29 +54,49 @@ pub enum FileTransferProtocol {
/// ## FileTransferError
///
/// FileTransferError defines the possible errors available for a file transfer
#[derive(std::fmt::Debug)]
#[derive(Debug)]
pub struct FileTransferError {
code: FileTransferErrorType,
msg: Option<String>,
}
impl FileTransferError {
/// ### kind
///
/// Returns the error kind
pub fn kind(&self) -> FileTransferErrorType {
self.code
}
}
/// ## FileTransferErrorType
///
/// FileTransferErrorType defines the possible errors available for a file transfer
#[allow(dead_code)]
#[derive(std::fmt::Debug)]
#[derive(Error, Debug, Clone, Copy, PartialEq)]
pub enum FileTransferErrorType {
#[error("Authentication failed")]
AuthenticationFailed,
#[error("Bad address syntax")]
BadAddress,
#[error("Connection error")]
ConnectionError,
#[error("SSL error")]
SslError,
#[error("Could not stat directory")]
DirStatFailed,
#[error("Directory already exists")]
DirectoryAlreadyExists,
#[error("Failed to create file")]
FileCreateDenied,
IoErr(std::io::Error),
#[error("No such file or directory")]
NoSuchFileOrDirectory,
#[error("Not enough permissions")]
PexError,
#[error("Protocol error")]
ProtocolError,
#[error("Uninitialized session")]
UninitializedSession,
#[error("Unsupported feature")]
UnsupportedFeature,
}
@@ -78,10 +105,7 @@ impl FileTransferError {
///
/// Instantiates a new FileTransferError
pub fn new(code: FileTransferErrorType) -> FileTransferError {
FileTransferError {
code: code,
msg: None,
}
FileTransferError { code, msg: None }
}
/// ### new_ex
@@ -96,25 +120,9 @@ impl FileTransferError {
impl std::fmt::Display for FileTransferError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let err: String = match &self.code {
FileTransferErrorType::AuthenticationFailed => String::from("Authentication failed"),
FileTransferErrorType::BadAddress => String::from("Bad address syntax"),
FileTransferErrorType::ConnectionError => String::from("Connection error"),
FileTransferErrorType::DirStatFailed => String::from("Could not stat directory"),
FileTransferErrorType::FileCreateDenied => String::from("Failed to create file"),
FileTransferErrorType::IoErr(err) => format!("IO Error: {}", err),
FileTransferErrorType::NoSuchFileOrDirectory => {
String::from("No such file or directory")
}
FileTransferErrorType::PexError => String::from("Not enough permissions"),
FileTransferErrorType::ProtocolError => String::from("Protocol error"),
FileTransferErrorType::SslError => String::from("SSL error"),
FileTransferErrorType::UninitializedSession => String::from("Uninitialized session"),
FileTransferErrorType::UnsupportedFeature => String::from("Unsupported feature"),
};
match &self.msg {
Some(msg) => write!(f, "{} ({})", err, msg),
None => write!(f, "{}", err),
Some(msg) => write!(f, "{} ({})", self.code, msg),
None => write!(f, "{}", self.code),
}
}
}
@@ -160,6 +168,11 @@ pub trait FileTransfer {
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError>;
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError>;
/// ### list_dir
///
/// List directory entries
@@ -169,7 +182,7 @@ pub trait FileTransfer {
/// ### mkdir
///
/// Make directory
/// You must return error in case the directory already exists
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError>;
/// ### remove
@@ -187,13 +200,22 @@ pub trait FileTransfer {
/// Stat file and return FsEntry
fn stat(&mut self, path: &Path) -> Result<FsEntry, FileTransferError>;
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError>;
/// ### send_file
///
/// Send file to remote
/// File name is referred to the name of the file as it will be saved
/// Data contains the file data
/// Returns file and its size
fn send_file(&mut self, local: &FsFile, file_name: &Path) -> Result<Box<dyn Write>, FileTransferError>;
fn send_file(
&mut self,
local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError>;
/// ### recv_file
///
@@ -218,4 +240,255 @@ pub trait FileTransfer {
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError>;
/// ### find
///
/// Find files from current directory (in all subdirectories) whose name matches the provided search
/// Search supports wildcards ('?', '*')
fn find(&mut self, search: &str) -> Result<Vec<FsEntry>, FileTransferError> {
match self.is_connected() {
true => {
// Starting from current directory, iter dir
match self.pwd() {
Ok(p) => self.iter_search(p.as_path(), &WildMatch::new(search)),
Err(err) => Err(err),
}
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### iter_search
///
/// Search recursively in `dir` for file matching the wildcard.
/// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so
/// NOTE: don't call this method from outside; consider it as private
fn iter_search(
&mut self,
dir: &Path,
filter: &WildMatch,
) -> Result<Vec<FsEntry>, FileTransferError> {
let mut drained: Vec<FsEntry> = Vec::new();
// Scan directory
match self.list_dir(dir) {
Ok(entries) => {
/* For each entry:
- if is dir: call iter_search with `dir`
- push `iter_search` result to `drained`
- if is file: check if it matches `filter`
- if it matches `filter`: push to to filter
*/
for entry in entries.iter() {
match entry {
FsEntry::Directory(dir) => {
// If directory name, matches wildcard, push it to drained
if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone()));
}
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
}
FsEntry::File(file) => {
if filter.matches(file.name.as_str()) {
drained.push(FsEntry::File(file.clone()));
}
}
}
}
Ok(drained)
}
Err(err) => Err(err),
}
}
}
// Traits
impl std::string::ToString for FileTransferProtocol {
fn to_string(&self) -> String {
String::from(match self {
FileTransferProtocol::Ftp(secure) => match secure {
true => "FTPS",
false => "FTP",
},
FileTransferProtocol::Scp => "SCP",
FileTransferProtocol::Sftp => "SFTP",
})
}
}
impl std::str::FromStr for FileTransferProtocol {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"SCP" => Ok(FileTransferProtocol::Scp),
"SFTP" => Ok(FileTransferProtocol::Sftp),
_ => Err(s.to_string()),
}
}
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::str::FromStr;
use std::string::ToString;
#[test]
fn test_filetransfer_mod_protocol() {
assert_eq!(
FileTransferProtocol::Ftp(true),
FileTransferProtocol::Ftp(true)
);
assert_eq!(
FileTransferProtocol::Ftp(false),
FileTransferProtocol::Ftp(false)
);
// From str
assert_eq!(
FileTransferProtocol::from_str("FTPS").ok().unwrap(),
FileTransferProtocol::Ftp(true)
);
assert_eq!(
FileTransferProtocol::from_str("ftps").ok().unwrap(),
FileTransferProtocol::Ftp(true)
);
assert_eq!(
FileTransferProtocol::from_str("FTP").ok().unwrap(),
FileTransferProtocol::Ftp(false)
);
assert_eq!(
FileTransferProtocol::from_str("ftp").ok().unwrap(),
FileTransferProtocol::Ftp(false)
);
assert_eq!(
FileTransferProtocol::from_str("SFTP").ok().unwrap(),
FileTransferProtocol::Sftp
);
assert_eq!(
FileTransferProtocol::from_str("sftp").ok().unwrap(),
FileTransferProtocol::Sftp
);
assert_eq!(
FileTransferProtocol::from_str("SCP").ok().unwrap(),
FileTransferProtocol::Scp
);
assert_eq!(
FileTransferProtocol::from_str("scp").ok().unwrap(),
FileTransferProtocol::Scp
);
// Error
assert!(FileTransferProtocol::from_str("dummy").is_err());
// To String
assert_eq!(
FileTransferProtocol::Ftp(true).to_string(),
String::from("FTPS")
);
assert_eq!(
FileTransferProtocol::Ftp(false).to_string(),
String::from("FTP")
);
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
}
#[test]
fn test_filetransfer_mod_error() {
let err: FileTransferError = FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
String::from("non va una mazza"),
);
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
assert_eq!(
format!("{}", err),
String::from("No such file or directory (non va una mazza)")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::AuthenticationFailed)
),
String::from("Authentication failed")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::BadAddress)
),
String::from("Bad address syntax")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::ConnectionError)
),
String::from("Connection error")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::DirStatFailed)
),
String::from("Could not stat directory")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::FileCreateDenied)
),
String::from("Failed to create file")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::NoSuchFileOrDirectory)
),
String::from("No such file or directory")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::PexError)
),
String::from("Not enough permissions")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::ProtocolError)
),
String::from("Protocol error")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::SslError)
),
String::from("SSL error")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::UninitializedSession)
),
String::from("Uninitialized session")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::UnsupportedFeature)
),
String::from("Unsupported feature")
);
let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature);
assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature);
}
}

137
src/filetransfer/params.rs Normal file
View File

@@ -0,0 +1,137 @@
//! ## Params
//!
//! file transfer parameters
/**
* 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 super::FileTransferProtocol;
use std::path::{Path, PathBuf};
/// ### FileTransferParams
///
/// Holds connection parameters for file transfers
#[derive(Clone)]
pub struct FileTransferParams {
pub address: String,
pub port: u16,
pub protocol: FileTransferProtocol,
pub username: Option<String>,
pub password: Option<String>,
pub entry_directory: Option<PathBuf>,
}
impl FileTransferParams {
/// ### new
///
/// Instantiates a new `FileTransferParams`
pub fn new<S: AsRef<str>>(address: S) -> Self {
Self {
address: address.as_ref().to_string(),
port: 22,
protocol: FileTransferProtocol::Sftp,
username: None,
password: None,
entry_directory: None,
}
}
/// ### port
///
/// Set port for params
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// ### protocol
///
/// Set protocol for params
pub fn protocol(mut self, protocol: FileTransferProtocol) -> Self {
self.protocol = protocol;
self
}
/// ### username
///
/// Set username for params
pub fn username<S: AsRef<str>>(mut self, username: Option<S>) -> Self {
self.username = username.map(|x| x.as_ref().to_string());
self
}
/// ### password
///
/// Set password for params
pub fn password<S: AsRef<str>>(mut self, password: Option<S>) -> Self {
self.password = password.map(|x| x.as_ref().to_string());
self
}
/// ### entry_directory
///
/// Set entry directory
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
self
}
}
impl Default for FileTransferParams {
fn default() -> Self {
Self::new("localhost")
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_filetransfer_params() {
let params: FileTransferParams = FileTransferParams::new("test.rebex.net")
.port(2222)
.protocol(FileTransferProtocol::Scp)
.username(Some("omar"))
.password(Some("foobar"))
.entry_directory(Some(&Path::new("/tmp")));
assert_eq!(params.address.as_str(), "test.rebex.net");
assert_eq!(params.port, 2222);
assert_eq!(params.protocol, FileTransferProtocol::Scp);
assert_eq!(params.username.as_ref().unwrap(), "omar");
assert_eq!(params.password.as_ref().unwrap(), "foobar");
}
#[test]
fn test_filetransfer_params_default() {
let params: FileTransferParams = FileTransferParams::default();
assert_eq!(params.address.as_str(), "localhost");
assert_eq!(params.port, 22);
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
assert!(params.username.is_none());
assert!(params.password.is_none());
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

147
src/fs/explorer/builder.rs Normal file
View File

@@ -0,0 +1,147 @@
//! ## Builder
//!
//! `builder` is the module which provides a builder for FileExplorer
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::formatter::Formatter;
use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs};
// Ext
use std::collections::VecDeque;
/// ## FileExplorerBuilder
///
/// Struct used to create a `FileExplorer`
pub struct FileExplorerBuilder {
explorer: Option<FileExplorer>,
}
impl FileExplorerBuilder {
/// ### new
///
/// Build a new `FileExplorerBuilder`
pub fn new() -> Self {
FileExplorerBuilder {
explorer: Some(FileExplorer::default()),
}
}
/// ### build
///
/// Take FileExplorer out of builder
pub fn build(&mut self) -> FileExplorer {
self.explorer.take().unwrap()
}
/// ### with_hidden_files
///
/// Enable HIDDEN_FILES option
pub fn with_hidden_files(&mut self, val: bool) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
match val {
true => e.opts.insert(ExplorerOpts::SHOW_HIDDEN_FILES),
false => e.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES),
}
}
self
}
/// ### with_file_sorting
///
/// Set sorting method
pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
e.sort_by(sorting);
}
self
}
/// ### with_dirs_first
///
/// Enable DIRS_FIRST option
pub fn with_group_dirs(&mut self, group_dirs: Option<GroupDirs>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
e.group_dirs_by(group_dirs);
}
self
}
/// ### with_stack_size
///
/// Set stack size for FileExplorer
pub fn with_stack_size(&mut self, sz: usize) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
e.stack_size = sz;
e.dirstack = VecDeque::with_capacity(sz);
}
self
}
/// ### with_formatter
///
/// Set formatter for FileExplorer
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
if let Some(fmt_str) = fmt_str {
e.fmt = Formatter::new(fmt_str);
}
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_fs_explorer_builder_new_default() {
let explorer: FileExplorer = FileExplorerBuilder::new().build();
// Verify
assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
assert_eq!(explorer.file_sorting, FileSorting::Name); // Default
assert_eq!(explorer.group_dirs, None);
assert_eq!(explorer.stack_size, 16);
}
#[test]
fn test_fs_explorer_builder_new_all() {
let explorer: FileExplorer = FileExplorerBuilder::new()
.with_file_sorting(FileSorting::ModifyTime)
.with_group_dirs(Some(GroupDirs::First))
.with_hidden_files(true)
.with_stack_size(24)
.with_formatter(Some("{NAME}"))
.build();
// Verify
assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
assert_eq!(explorer.file_sorting, FileSorting::ModifyTime); // Default
assert_eq!(explorer.group_dirs, Some(GroupDirs::First));
assert_eq!(explorer.stack_size, 24);
}
}

View File

@@ -0,0 +1,896 @@
//! ## Formatter
//!
//! `formatter` is the module which provides formatting utilities for `FileExplorer`
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::FsEntry;
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
// Ext
use bytesize::ByteSize;
use regex::Regex;
#[cfg(target_family = "unix")]
use users::{get_group_by_gid, get_user_by_uid};
// Types
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String;
// Keys
const FMT_KEY_ATIME: &str = "ATIME";
const FMT_KEY_CTIME: &str = "CTIME";
const FMT_KEY_GROUP: &str = "GROUP";
const FMT_KEY_MTIME: &str = "MTIME";
const FMT_KEY_NAME: &str = "NAME";
const FMT_KEY_PEX: &str = "PEX";
const FMT_KEY_SIZE: &str = "SIZE";
const FMT_KEY_SYMLINK: &str = "SYMLINK";
const FMT_KEY_USER: &str = "USER";
// Default
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
// Regex
lazy_static! {
/**
* Regex matches:
* - group 0: KEY NAME
* - group 1?: LENGTH
* - group 2?: EXTRA
*/
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap();
}
/// ## CallChainBlock
///
/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry.
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
/// a chain of function is made using the Formatters method.
/// This method provides an extremely fast way to format fs entries
struct CallChainBlock {
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
next_block: Option<Box<CallChainBlock>>,
}
impl CallChainBlock {
/// ### new
///
/// Create a new `CallChainBlock`
pub fn new(
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) -> Self {
CallChainBlock {
func,
prefix,
fmt_len,
fmt_extra,
next_block: None,
}
}
/// ### next
///
/// Call next callback in the CallChain
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
// Call func
let new_str: String = (self.func)(
fmt,
fsentry,
cur_str,
self.prefix.as_str(),
self.fmt_len.as_ref(),
self.fmt_extra.as_ref(),
);
// If next is some, call next, otherwise (END OF CHAIN) return new_str
match &self.next_block {
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
None => new_str,
}
}
/// ### push
///
/// Push func to the last element in the Call chain
pub fn push(
&mut self,
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) {
// Call recursively until an element with next_block equal to None is found
match &mut self.next_block {
None => {
self.next_block = Some(Box::new(CallChainBlock::new(
func, prefix, fmt_len, fmt_extra,
)))
}
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
}
}
}
/// ## Formatter
///
/// Formatter takes care of formatting FsEntries according to the provided keys.
/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
/// at each fmt call.
pub struct Formatter {
call_chain: CallChainBlock,
}
impl Default for Formatter {
/// ### default
///
/// Instantiates a Formatter with the default fmt syntax
fn default() -> Self {
Formatter {
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
}
}
}
impl Formatter {
/// ### new
///
/// Instantiates a new `Formatter` with the provided format string
pub fn new(fmt_str: &str) -> Self {
Formatter {
call_chain: Self::make_callchain(fmt_str),
}
}
/// ### fmt
///
/// Format fsentry
pub fn fmt(&self, fsentry: &FsEntry) -> String {
// Execute callchain blocks
self.call_chain.next(self, fsentry, "")
}
// Fmt methods
/// ### fmt_atime
///
/// Format last access time
fn fmt_atime(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date (use extra args as format or default "%b %d %Y %H:%M")
let datetime: String = fmt_time(
fsentry.get_last_access_time(),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_ctime
///
/// Format creation time
fn fmt_ctime(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date
let datetime: String = fmt_time(
fsentry.get_creation_time(),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_group
///
/// Format owner group
fn fmt_group(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(target_family = "unix")]
let group: String = match fsentry.get_group() {
Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let group: String = match fsentry.get_group() {
Some(gid) => gid.to_string(),
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
group,
width = fmt_len.unwrap_or(&12)
)
}
/// ### fmt_mtime
///
/// Format last change time
fn fmt_mtime(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date
let datetime: String = fmt_time(
fsentry.get_last_change_time(),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_name
///
/// Format file name
fn fmt_name(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get file name (or elide if too long)
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 24,
};
let name: &str = fsentry.get_name();
let last_idx: usize = match fsentry.is_dir() {
// NOTE: For directories is l - 2, since we push '/' to name
true => file_len - 2,
false => file_len - 1,
};
let mut name: String = match name.len() >= file_len {
false => name.to_string(),
true => format!("{}", &name[0..last_idx]),
};
if fsentry.is_dir() {
name.push('/');
}
// Add to cur str, prefix and the key value
format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len)
}
/// ### fmt_pex
///
/// Format file permissions
fn fmt_pex(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Create mode string
let mut pex: String = String::with_capacity(10);
let file_type: char = match fsentry.is_symlink() {
true => 'l',
false => match fsentry.is_dir() {
true => 'd',
false => '-',
},
};
pex.push(file_type);
match fsentry.get_unix_pex() {
None => pex.push_str("?????????"),
Some((owner, group, others)) => pex.push_str(
format!("{}{}{}", fmt_pex(owner), fmt_pex(group), fmt_pex(others)).as_str(),
),
}
// Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, pex)
}
/// ### fmt_size
///
/// Format file size
fn fmt_size(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
if fsentry.is_file() {
// Get byte size
let size: ByteSize = ByteSize(fsentry.get_size() as u64);
// Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, size.to_string())
} else {
// Add to cur str, prefix and the key value
format!("{}{} ", cur_str, prefix)
}
}
/// ### fmt_symlink
///
/// Format file symlink (if any)
fn fmt_symlink(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get file name (or elide if too long)
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 21,
};
// Replace `FMT_KEY_NAME` with name
match fsentry.is_symlink() {
false => format!("{}{} ", cur_str, prefix),
true => format!(
"{}{}-> {:0width$}",
cur_str,
prefix,
fmt_path_elide(
fsentry.get_realfile().get_abs_path().as_path(),
file_len - 1
),
width = file_len
),
}
}
/// ### fmt_user
///
/// Format owner user
fn fmt_user(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(target_family = "unix")]
let username: String = match fsentry.get_user() {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let username: String = match fsentry.get_user() {
Some(uid) => uid.to_string(),
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!("{}{}{:12}", cur_str, prefix, username)
}
/// ### fmt_fallback
///
/// Fallback function in case the format key is unknown
/// It does nothing, just returns cur_str
fn fmt_fallback(
&self,
_fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Add to cur str and prefix
format!("{}{}", cur_str, prefix)
}
// Static
/// ### make_callchain
///
/// Make a callchain starting from the fmt str
fn make_callchain(fmt_str: &str) -> CallChainBlock {
// Init chain block
let mut callchain: Option<CallChainBlock> = None;
// Track index of the last match found, to get the prefix for each token
let mut last_index: usize = 0;
// Match fmt str against regex
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
// Get match index (unwrap is safe, since always exists)
let index: usize = fmt_str.find(&regex_match[0]).unwrap();
// Get prefix
let prefix: String = String::from(&fmt_str[last_index..index]);
// Increment last index (sum prefix lenght and the length of the key)
last_index += prefix.len() + regex_match[0].len();
// Match attributes
match FMT_ATTR_REGEX.captures(&regex_match[1]) {
Some(regex_match) => {
// Match group 0 (which is name)
let callback: FmtCallback = match &regex_match.get(1) {
Some(key) => match key.as_str() {
FMT_KEY_ATIME => Self::fmt_atime,
FMT_KEY_CTIME => Self::fmt_ctime,
FMT_KEY_GROUP => Self::fmt_group,
FMT_KEY_MTIME => Self::fmt_mtime,
FMT_KEY_NAME => Self::fmt_name,
FMT_KEY_PEX => Self::fmt_pex,
FMT_KEY_SIZE => Self::fmt_size,
FMT_KEY_SYMLINK => Self::fmt_symlink,
FMT_KEY_USER => Self::fmt_user,
_ => Self::fmt_fallback,
},
None => Self::fmt_fallback,
};
// Match format length: group 3
let fmt_len: Option<usize> = match &regex_match.get(3) {
Some(len) => match len.as_str().parse::<usize>() {
Ok(len) => Some(len),
Err(_) => None,
},
None => None,
};
// Match format extra: group 2 + 1
let fmt_extra: Option<String> = regex_match
.get(5)
.as_ref()
.map(|extra| extra.as_str().to_string());
// Create a callchain or push new element to its back
match callchain.as_mut() {
None => {
callchain =
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
}
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
}
}
None => continue,
}
}
// Finalize and return
match callchain {
Some(callchain) => callchain,
None => CallChainBlock::new(Self::fmt_fallback, String::new(), None, None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{FsDirectory, FsFile, UnixPex};
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use std::time::SystemTime;
#[test]
fn test_fs_explorer_formatter_callchain() {
// Make a dummy formatter
let dummy_formatter: Formatter = Formatter::new("");
// Make a dummy entry
let t_now: SystemTime = SystemTime::now();
let dummy_entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
assert!(callchain.next_block.is_none());
assert_eq!(callchain.prefix, String::from("h"));
// Execute
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hA")
);
// Push 4 new blocks
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
// Verify
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hAhAhAhAhA")
);
}
#[test]
fn test_fs_explorer_formatter_format_files() {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// Elide name
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("piroparoporoperoperupupu.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No pex
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
// No user
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_format_dirs() {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x root {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
// No pex, no user
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_all_together_now() {
let formatter: Formatter =
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let pointer: FsEntry = FsEntry::File(FsFile {
name: String::from("project.info"),
abs_path: PathBuf::from("/project.info"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: None, // UNIX only
});
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t,
last_access_time: t,
creation_time: t,
symlink: Some(Box::new(pointer)), // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// Directory without symlink
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t,
last_access_time: t,
creation_time: t,
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File with symlink
let pointer: FsEntry = FsEntry::File(FsFile {
name: String::from("project.info"),
abs_path: PathBuf::from("/project.info"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: None, // UNIX only
});
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: Some(Box::new(pointer)), // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File without symlink
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
}
/// ### dummy_fmt
///
/// Dummy formatter, just yelds an 'A' at the end of the current string
fn dummy_fmt(
_fmt: &Formatter,
_entry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
format!("{}{}A", cur_str, prefix)
}
}

721
src/fs/explorer/mod.rs Normal file
View File

@@ -0,0 +1,721 @@
//! ## Explorer
//!
//! `explorer` is the module which provides an Helper in handling Directory status through
/**
* 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.
*/
// Mods
pub(crate) mod builder;
mod formatter;
// Locals
use super::FsEntry;
use formatter::Formatter;
// Ext
use std::cmp::Reverse;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
bitflags! {
/// ## ExplorerOpts
///
/// ExplorerOpts are bit options which provides different behaviours to `FileExplorer`
pub(crate) struct ExplorerOpts: u32 {
const SHOW_HIDDEN_FILES = 0b00000001;
}
}
/// ## FileSorting
///
/// FileSorting defines the criteria for sorting files
#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
pub enum FileSorting {
Name,
ModifyTime,
CreationTime,
Size,
}
/// ## GroupDirs
///
/// GroupDirs defines how directories should be grouped in sorting files
#[derive(PartialEq, std::fmt::Debug)]
pub enum GroupDirs {
First,
Last,
}
/// ## FileExplorer
///
/// File explorer states
pub struct FileExplorer {
pub wrkdir: PathBuf, // Current directory
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
pub(crate) stack_size: usize, // Directory stack size
pub(crate) file_sorting: FileSorting, // File sorting criteria
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
pub(crate) opts: ExplorerOpts, // Explorer options
pub(crate) fmt: Formatter, // FsEntry formatter
files: Vec<FsEntry>, // Files in directory
}
impl Default for FileExplorer {
fn default() -> Self {
FileExplorer {
wrkdir: PathBuf::from("/"),
dirstack: VecDeque::with_capacity(16),
stack_size: 16,
file_sorting: FileSorting::Name,
group_dirs: None,
opts: ExplorerOpts::empty(),
fmt: Formatter::default(),
files: Vec::new(),
}
}
}
impl FileExplorer {
/// ### pushd
///
/// push directory to stack
pub fn pushd(&mut self, dir: &Path) {
// Check if stack would overflow the size
while self.dirstack.len() >= self.stack_size {
self.dirstack.pop_front(); // Start cleaning events from back
}
// Eventually push front the new record
self.dirstack.push_back(PathBuf::from(dir));
}
/// ### popd
///
/// Pop directory from the stack and return the directory
pub fn popd(&mut self) -> Option<PathBuf> {
self.dirstack.pop_back()
}
/// ### set_files
///
/// Set Explorer files
/// This method will also sort entries based on current options
/// Once all sorting have been performed, index is moved to first valid entry.
pub fn set_files(&mut self, files: Vec<FsEntry>) {
self.files = files;
// Sort
self.sort();
}
/// ### del_entry
///
/// Delete file at provided index
pub fn del_entry(&mut self, idx: usize) {
if self.files.len() > idx {
self.files.remove(idx);
}
}
/*
/// ### count
///
/// Return amount of files
pub fn count(&self) -> usize {
self.files.len()
}
*/
/// ### iter_files
///
/// Iterate over files
/// Filters are applied based on current options (e.g. hidden files not returned)
pub fn iter_files(&self) -> impl Iterator<Item = &FsEntry> + '_ {
// Filter
let opts: ExplorerOpts = self.opts;
Box::new(self.files.iter().filter(move |x| {
// If true, element IS NOT filtered
let mut pass: bool = true;
// If hidden files SHOULDN'T be shown, AND pass with not hidden
if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
pass &= !x.is_hidden();
}
pass
}))
}
/// ### iter_files_all
///
/// Iterate all files; doesn't care about options
pub fn iter_files_all(&self) -> impl Iterator<Item = &FsEntry> + '_ {
Box::new(self.files.iter())
}
/// ### get
///
/// Get file at relative index
pub fn get(&self, idx: usize) -> Option<&FsEntry> {
let opts: ExplorerOpts = self.opts;
let filtered = self
.files
.iter()
.filter(move |x| {
// If true, element IS NOT filtered
let mut pass: bool = true;
// If hidden files SHOULDN'T be shown, AND pass with not hidden
if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
pass &= !x.is_hidden();
}
pass
})
.collect::<Vec<_>>();
filtered.get(idx).copied()
}
// Formatting
/// ### fmt_file
///
/// Format a file entry
pub fn fmt_file(&self, entry: &FsEntry) -> String {
self.fmt.fmt(entry)
}
// Sorting
/// ### sort_by
///
/// Choose sorting method; then sort files
pub fn sort_by(&mut self, sorting: FileSorting) {
// If method HAS ACTUALLY CHANGED, sort (performance!)
if self.file_sorting != sorting {
self.file_sorting = sorting;
self.sort();
}
}
/// ### get_file_sorting
///
/// Get current file sorting method
pub fn get_file_sorting(&self) -> FileSorting {
self.file_sorting
}
/// ### group_dirs_by
///
/// Choose group dirs method; then sort files
pub fn group_dirs_by(&mut self, group_dirs: Option<GroupDirs>) {
// If method HAS ACTUALLY CHANGED, sort (performance!)
if self.group_dirs != group_dirs {
self.group_dirs = group_dirs;
self.sort();
}
}
/// ### sort
///
/// Sort files based on Explorer options.
fn sort(&mut self) {
// Choose sorting method
match &self.file_sorting {
FileSorting::Name => self.sort_files_by_name(),
FileSorting::CreationTime => self.sort_files_by_creation_time(),
FileSorting::ModifyTime => self.sort_files_by_mtime(),
FileSorting::Size => self.sort_files_by_size(),
}
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
// Group directories if necessary
if let Some(group_dirs) = &self.group_dirs {
match group_dirs {
GroupDirs::First => self.sort_files_directories_first(),
GroupDirs::Last => self.sort_files_directories_last(),
}
}
}
/// ### sort_files_by_name
///
/// Sort explorer files by their name. All names are converted to lowercase
fn sort_files_by_name(&mut self) {
self.files
.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
}
/// ### sort_files_by_mtime
///
/// Sort files by mtime; the newest comes first
fn sort_files_by_mtime(&mut self) {
self.files.sort_by(|a: &FsEntry, b: &FsEntry| {
b.get_last_change_time().cmp(&a.get_last_change_time())
});
}
/// ### sort_files_by_creation_time
///
/// Sort files by creation time; the newest comes first
fn sort_files_by_creation_time(&mut self) {
self.files
.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
}
/// ### sort_files_by_size
///
/// Sort files by size
fn sort_files_by_size(&mut self) {
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size()));
}
/// ### sort_files_directories_first
///
/// Sort files; directories come first
fn sort_files_directories_first(&mut self) {
self.files.sort_by_key(|x: &FsEntry| x.is_file());
}
/// ### sort_files_directories_last
///
/// Sort files; directories come last
fn sort_files_directories_last(&mut self) {
self.files.sort_by_key(|x: &FsEntry| x.is_dir());
}
/// ### toggle_hidden_files
///
/// Enable/disable hidden files
pub fn toggle_hidden_files(&mut self) {
self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES);
}
/// ### hidden_files_visible
///
/// Returns whether hidden files are visible
pub fn hidden_files_visible(&self) -> bool {
self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)
}
}
// Traits
impl ToString for FileSorting {
fn to_string(&self) -> String {
String::from(match self {
FileSorting::CreationTime => "by_creation_time",
FileSorting::ModifyTime => "by_mtime",
FileSorting::Name => "by_name",
FileSorting::Size => "by_size",
})
}
}
impl FromStr for FileSorting {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"by_creation_time" => Ok(FileSorting::CreationTime),
"by_mtime" => Ok(FileSorting::ModifyTime),
"by_name" => Ok(FileSorting::Name),
"by_size" => Ok(FileSorting::Size),
_ => Err(()),
}
}
}
impl ToString for GroupDirs {
fn to_string(&self) -> String {
String::from(match self {
GroupDirs::First => "first",
GroupDirs::Last => "last",
})
}
}
impl FromStr for GroupDirs {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"first" => Ok(GroupDirs::First),
"last" => Ok(GroupDirs::Last),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{FsDirectory, FsFile, UnixPex};
use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq;
use std::thread::sleep;
use std::time::{Duration, SystemTime};
#[test]
fn test_fs_explorer_new() {
let explorer: FileExplorer = FileExplorer::default();
// Verify
assert_eq!(explorer.dirstack.len(), 0);
assert_eq!(explorer.files.len(), 0);
assert_eq!(explorer.opts, ExplorerOpts::empty());
assert_eq!(explorer.wrkdir, PathBuf::from("/"));
assert_eq!(explorer.stack_size, 16);
assert_eq!(explorer.group_dirs, None);
assert_eq!(explorer.file_sorting, FileSorting::Name);
assert_eq!(explorer.get_file_sorting(), FileSorting::Name);
}
#[test]
fn test_fs_explorer_stack() {
let mut explorer: FileExplorer = FileExplorer::default();
explorer.stack_size = 2;
explorer.dirstack = VecDeque::with_capacity(2);
// Push dir
explorer.pushd(&Path::new("/tmp"));
explorer.pushd(&Path::new("/home/omar"));
// Pop
assert_eq!(explorer.popd().unwrap(), PathBuf::from("/home/omar"));
assert_eq!(explorer.dirstack.len(), 1);
assert_eq!(explorer.popd().unwrap(), PathBuf::from("/tmp"));
assert_eq!(explorer.dirstack.len(), 0);
// Dirstack is empty now
assert!(explorer.popd().is_none());
// Exceed limit
explorer.pushd(&Path::new("/tmp"));
explorer.pushd(&Path::new("/home/omar"));
explorer.pushd(&Path::new("/dev"));
assert_eq!(explorer.dirstack.len(), 2);
assert_eq!(*explorer.dirstack.get(1).unwrap(), PathBuf::from("/dev"));
assert_eq!(
*explorer.dirstack.get(0).unwrap(),
PathBuf::from("/home/omar")
);
}
#[test]
fn test_fs_explorer_files() {
let mut explorer: FileExplorer = FileExplorer::default();
// Don't show hidden files
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
assert_eq!(explorer.hidden_files_visible(), false);
// Create files
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry(".git/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("codecov.yml", false),
make_fs_entry(".gitignore", false),
]);
assert!(explorer.get(0).is_some());
assert!(explorer.get(100).is_none());
//assert_eq!(explorer.count(), 6);
// Verify (files are sorted by name)
assert_eq!(
explorer.files.get(0).unwrap().get_name(),
String::from(".git/")
);
// Iter files (all)
assert_eq!(explorer.iter_files_all().count(), 6);
// Iter files (hidden excluded) (.git, .gitignore are hidden)
assert_eq!(explorer.iter_files().count(), 4);
// Toggle hidden
explorer.toggle_hidden_files();
assert_eq!(explorer.hidden_files_visible(), true);
assert_eq!(explorer.iter_files().count(), 6); // All files are returned now
}
#[test]
fn test_fs_explorer_sort_by_name() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
]);
explorer.sort_by(FileSorting::Name);
// First entry should be "Cargo.lock"
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock");
// Last should be "src/"
assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/");
}
#[test]
fn test_fs_explorer_sort_by_mtime() {
let mut explorer: FileExplorer = FileExplorer::default();
let entry1: FsEntry = make_fs_entry("README.md", false);
// Wait 1 sec
sleep(Duration::from_secs(1));
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false);
// Create files (files are then sorted by name)
explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::ModifyTime);
// First entry should be "CODE_OF_CONDUCT.md"
assert_eq!(
explorer.files.get(0).unwrap().get_name(),
"CODE_OF_CONDUCT.md"
);
// Last should be "src/"
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
}
#[test]
fn test_fs_explorer_sort_by_creation_time() {
let mut explorer: FileExplorer = FileExplorer::default();
let entry1: FsEntry = make_fs_entry("README.md", false);
// Wait 1 sec
sleep(Duration::from_secs(1));
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false);
// Create files (files are then sorted by name)
explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::CreationTime);
// First entry should be "CODE_OF_CONDUCT.md"
assert_eq!(
explorer.files.get(0).unwrap().get_name(),
"CODE_OF_CONDUCT.md"
);
// Last should be "src/"
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
}
#[test]
fn test_fs_explorer_sort_by_size() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry_with_size("README.md", false, 1024),
make_fs_entry("src/", true),
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
]);
explorer.sort_by(FileSorting::Size);
// Directory has size 4096
assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/");
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md");
}
#[test]
fn test_fs_explorer_sort_by_name_and_dirs_first() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry("docs/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
]);
explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::First));
// First entry should be "docs"
assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/");
assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/");
// 3rd is file first for alphabetical order
assert_eq!(explorer.files.get(2).unwrap().get_name(), "Cargo.lock");
// Last should be "README.md" (last file for alphabetical ordening)
assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md");
}
#[test]
fn test_fs_explorer_sort_by_name_and_dirs_last() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("README.md", false),
make_fs_entry("src/", true),
make_fs_entry("docs/", true),
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("CODE_OF_CONDUCT.md", false),
make_fs_entry("CHANGELOG.md", false),
make_fs_entry("LICENSE", false),
make_fs_entry("Cargo.toml", false),
make_fs_entry("Cargo.lock", false),
make_fs_entry("codecov.yml", false),
]);
explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::Last));
// Last entry should be "src"
assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/");
assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/");
// first is file for alphabetical order
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock");
// Last in files should be "README.md" (last file for alphabetical ordening)
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md");
}
#[test]
fn test_fs_explorer_fmt() {
let explorer: FileExplorer = FileExplorer::default();
// Create fs entry
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
#[cfg(target_family = "unix")]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_to_string_from_str_traits() {
// File Sorting
assert_eq!(FileSorting::CreationTime.to_string(), "by_creation_time");
assert_eq!(FileSorting::ModifyTime.to_string(), "by_mtime");
assert_eq!(FileSorting::Name.to_string(), "by_name");
assert_eq!(FileSorting::Size.to_string(), "by_size");
assert_eq!(
FileSorting::from_str("by_creation_time").ok().unwrap(),
FileSorting::CreationTime
);
assert_eq!(
FileSorting::from_str("by_mtime").ok().unwrap(),
FileSorting::ModifyTime
);
assert_eq!(
FileSorting::from_str("by_name").ok().unwrap(),
FileSorting::Name
);
assert_eq!(
FileSorting::from_str("by_size").ok().unwrap(),
FileSorting::Size
);
assert!(FileSorting::from_str("omar").is_err());
// Group dirs
assert_eq!(GroupDirs::First.to_string(), "first");
assert_eq!(GroupDirs::Last.to_string(), "last");
assert_eq!(GroupDirs::from_str("first").ok().unwrap(), GroupDirs::First);
assert_eq!(GroupDirs::from_str("last").ok().unwrap(), GroupDirs::Last);
assert!(GroupDirs::from_str("omar").is_err());
}
#[test]
fn test_fs_explorer_del_entry() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("docs/", true),
make_fs_entry("src/", true),
make_fs_entry("README.md", false),
]);
explorer.del_entry(0);
assert_eq!(explorer.files.len(), 3);
assert_eq!(explorer.files[0].get_name(), "docs/");
explorer.del_entry(5);
assert_eq!(explorer.files.len(), 3);
}
fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry {
let t_now: SystemTime = SystemTime::now();
match is_dir {
false => FsEntry::File(FsFile {
name: name.to_string(),
abs_path: PathBuf::from(name),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 64,
ftype: None, // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}),
true => FsEntry::Directory(FsDirectory {
name: name.to_string(),
abs_path: PathBuf::from(name),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
}),
}
}
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry {
let t_now: SystemTime = SystemTime::now();
match is_dir {
false => FsEntry::File(FsFile {
name: name.to_string(),
abs_path: PathBuf::from(name),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: size,
ftype: None, // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}),
true => FsEntry::Directory(FsDirectory {
name: name.to_string(),
abs_path: PathBuf::from(name),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
}),
}
}
}

View File

@@ -2,38 +2,34 @@
//!
//! `fs` is the module which provides file system entities
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
extern crate bytesize;
#[cfg(any(unix, macos, linux))]
extern crate users;
use crate::utils::{fmt_pex, time_to_str};
use bytesize::ByteSize;
/**
* 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.
*/
// Mod
pub mod explorer;
// Ext
use std::path::PathBuf;
use std::time::SystemTime;
#[cfg(any(unix, macos, linux))]
use users::get_user_by_uid;
/// ## FsEntry
///
@@ -56,11 +52,10 @@ pub struct FsDirectory {
pub last_change_time: SystemTime,
pub last_access_time: SystemTime,
pub creation_time: SystemTime,
pub readonly: bool,
pub symlink: Option<PathBuf>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(u8, u8, u8)>, // UNIX only
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only
}
/// ### FsFile
@@ -75,207 +70,509 @@ pub struct FsFile {
pub last_access_time: SystemTime,
pub creation_time: SystemTime,
pub size: usize,
pub ftype: Option<String>, // File type
pub readonly: bool,
pub symlink: Option<PathBuf>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(u8, u8, u8)>, // UNIX only
pub ftype: Option<String>, // File type
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only
}
impl std::fmt::Display for FsEntry {
/// ### fmt_ls
/// ## UnixPex
///
/// Describes the permissions on POSIX system.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct UnixPex {
read: bool,
write: bool,
execute: bool,
}
impl UnixPex {
/// ### new
///
/// Format File Entry as `ls` does
#[cfg(any(unix, macos, linux))]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FsEntry::Directory(dir) => {
// Create mode string
let mut mode: String = String::with_capacity(10);
let file_type: char = match dir.symlink {
Some(_) => 'l',
None => 'd',
};
mode.push(file_type);
match dir.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
let username: String = match dir.user {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => String::from("0"),
};
// Get group
/*
let group: String = match dir.group {
Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => String::from("0"),
};
*/
// Get byte size
let size: String = String::from("4096");
// Get date
let datetime: String = time_to_str(dir.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let dir_name: String = match dir.name.len() >= 24 {
false => dir.name.clone(),
true => format!("{}...", &dir.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
dir_name, mode, username, size, datetime
)
}
FsEntry::File(file) => {
// Create mode string
let mut mode: String = String::with_capacity(10);
let file_type: char = match file.symlink {
Some(_) => 'l',
None => '-',
};
mode.push(file_type);
match file.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
let username: String = match file.user {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => String::from("0"),
};
// Get group
/*
let group: String = match file.group {
Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => String::from("0"),
};
*/
// Get byte size
let size: ByteSize = ByteSize(file.size as u64);
// Get date
let datetime: String = time_to_str(file.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let file_name: String = match file.name.len() >= 24 {
false => file.name.clone(),
true => format!("{}...", &file.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
file_name, mode, username, size, datetime
)
}
/// Instantiates a new `UnixPex`
pub fn new(read: bool, write: bool, execute: bool) -> Self {
Self {
read,
write,
execute,
}
}
/// ### fmt_ls
/// ### can_read
///
/// Format File Entry as `ls` does
#[cfg(target_os = "windows")]
#[cfg(not(tarpaulin_include))]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FsEntry::Directory(dir) => {
// Create mode string
let mut mode: String = String::with_capacity(10);
let file_type: char = match dir.symlink {
Some(_) => 'l',
None => 'd',
};
mode.push(file_type);
match dir.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
let username: String = match dir.user {
Some(uid) => uid.to_string(),
None => String::from("0"),
};
// Get group
/*
let group: String = match dir.group {
Some(gid) => gid.to_string(),
None => String::from("0"),
};
*/
// Get byte size
let size: String = String::from("4096");
// Get date
let datetime: String = time_to_str(dir.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let dir_name: String = match dir.name.len() >= 24 {
false => dir.name.clone(),
true => format!("{}...", &dir.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
dir_name, mode, username, size, datetime
)
}
FsEntry::File(file) => {
// Create mode string
let mut mode: String = String::with_capacity(10);
let file_type: char = match file.symlink {
Some(_) => 'l',
None => '-',
};
mode.push(file_type);
match file.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
let username: String = match file.user {
Some(uid) => uid.to_string(),
None => String::from("0"),
};
// Get group
/*
let group: String = match file.group {
Some(gid) => gid.to_string(),
None => String::from("0"),
};
*/
// Get byte size
let size: ByteSize = ByteSize(file.size as u64);
// Get date
let datetime: String = time_to_str(file.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let file_name: String = match file.name.len() >= 24 {
false => file.name.clone(),
true => format!("{}...", &file.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
file_name, mode, username, size, datetime
)
}
/// Returns whether user can read
pub fn can_read(&self) -> bool {
self.read
}
/// ### can_write
///
/// Returns whether user can write
pub fn can_write(&self) -> bool {
self.write
}
/// ### can_execute
///
/// Returns whether user can execute
pub fn can_execute(&self) -> bool {
self.execute
}
/// ### as_byte
///
/// Convert permission to byte as on POSIX systems
pub fn as_byte(&self) -> u8 {
((self.read as u8) << 2) + ((self.write as u8) << 1) + (self.execute as u8)
}
}
impl From<u8> for UnixPex {
fn from(bits: u8) -> Self {
Self {
read: ((bits >> 2) & 0x01) != 0,
write: ((bits >> 1) & 0x01) != 0,
execute: (bits & 0x01) != 0,
}
}
}
impl FsEntry {
/// ### get_abs_path
///
/// Get absolute path from `FsEntry`
pub fn get_abs_path(&self) -> PathBuf {
match self {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => file.abs_path.clone(),
}
}
/// ### get_name
///
/// Get file name from `FsEntry`
pub fn get_name(&self) -> &'_ str {
match self {
FsEntry::Directory(dir) => dir.name.as_ref(),
FsEntry::File(file) => file.name.as_ref(),
}
}
/// ### get_last_change_time
///
/// Get last change time from `FsEntry`
pub fn get_last_change_time(&self) -> SystemTime {
match self {
FsEntry::Directory(dir) => dir.last_change_time,
FsEntry::File(file) => file.last_change_time,
}
}
/// ### get_last_access_time
///
/// Get access time from `FsEntry`
pub fn get_last_access_time(&self) -> SystemTime {
match self {
FsEntry::Directory(dir) => dir.last_access_time,
FsEntry::File(file) => file.last_access_time,
}
}
/// ### get_creation_time
///
/// Get creation time from `FsEntry`
pub fn get_creation_time(&self) -> SystemTime {
match self {
FsEntry::Directory(dir) => dir.creation_time,
FsEntry::File(file) => file.creation_time,
}
}
/// ### get_size
///
/// Get size from `FsEntry`. For directories is always 4096
pub fn get_size(&self) -> usize {
match self {
FsEntry::Directory(_) => 4096,
FsEntry::File(file) => file.size,
}
}
/// ### get_ftype
///
/// Get file type from `FsEntry`. For directories is always None
pub fn get_ftype(&self) -> Option<String> {
match self {
FsEntry::Directory(_) => None,
FsEntry::File(file) => file.ftype.clone(),
}
}
/// ### get_user
///
/// Get uid from `FsEntry`
pub fn get_user(&self) -> Option<u32> {
match self {
FsEntry::Directory(dir) => dir.user,
FsEntry::File(file) => file.user,
}
}
/// ### get_group
///
/// Get gid from `FsEntry`
pub fn get_group(&self) -> Option<u32> {
match self {
FsEntry::Directory(dir) => dir.group,
FsEntry::File(file) => file.group,
}
}
/// ### get_unix_pex
///
/// Get unix pex from `FsEntry`
pub fn get_unix_pex(&self) -> Option<(UnixPex, UnixPex, UnixPex)> {
match self {
FsEntry::Directory(dir) => dir.unix_pex,
FsEntry::File(file) => file.unix_pex,
}
}
/// ### is_symlink
///
/// Returns whether the `FsEntry` is a symlink
pub fn is_symlink(&self) -> bool {
match self {
FsEntry::Directory(dir) => dir.symlink.is_some(),
FsEntry::File(file) => file.symlink.is_some(),
}
}
/// ### is_dir
///
/// Returns whether a FsEntry is a directory
pub fn is_dir(&self) -> bool {
matches!(self, FsEntry::Directory(_))
}
/// ### is_file
///
/// Returns whether a FsEntry is a File
pub fn is_file(&self) -> bool {
matches!(self, FsEntry::File(_))
}
/// ### is_hidden
///
/// Returns whether FsEntry is hidden
pub fn is_hidden(&self) -> bool {
self.get_name().starts_with('.')
}
/// ### get_realfile
///
/// Return the real file pointed by a `FsEntry`
pub fn get_realfile(&self) -> FsEntry {
match self {
FsEntry::Directory(dir) => match &dir.symlink {
Some(symlink) => symlink.get_realfile(),
None => self.clone(),
},
FsEntry::File(file) => match &file.symlink {
Some(symlink) => symlink.get_realfile(),
None => self.clone(),
},
}
}
/// ### unwrap_file
///
/// Unwrap FsEntry as FsFile
pub fn unwrap_file(self) -> FsFile {
match self {
FsEntry::File(file) => file,
_ => panic!("unwrap_file: not a file"),
}
}
#[cfg(test)]
/// ### unwrap_dir
///
/// Unwrap FsEntry as FsDirectory
pub fn unwrap_dir(self) -> FsDirectory {
match self {
FsEntry::Directory(dir) => dir,
_ => panic!("unwrap_dir: not a directory"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_fs_fsentry_dir() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("foo"),
abs_path: PathBuf::from("/foo"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(entry.get_abs_path(), PathBuf::from("/foo"));
assert_eq!(entry.get_name(), String::from("foo"));
assert_eq!(entry.get_last_access_time(), t_now);
assert_eq!(entry.get_last_change_time(), t_now);
assert_eq!(entry.get_creation_time(), t_now);
assert_eq!(entry.get_size(), 4096);
assert_eq!(entry.get_ftype(), None);
assert_eq!(entry.get_user(), Some(0));
assert_eq!(entry.get_group(), Some(0));
assert_eq!(entry.is_symlink(), false);
assert_eq!(entry.is_dir(), true);
assert_eq!(entry.is_file(), false);
assert_eq!(
entry.get_unix_pex(),
Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5)))
);
assert_eq!(entry.unwrap_dir().abs_path, PathBuf::from("/foo"));
}
#[test]
fn test_fs_fsentry_file() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(entry.get_abs_path(), PathBuf::from("/bar.txt"));
assert_eq!(entry.get_name(), String::from("bar.txt"));
assert_eq!(entry.get_last_access_time(), t_now);
assert_eq!(entry.get_last_change_time(), t_now);
assert_eq!(entry.get_creation_time(), t_now);
assert_eq!(entry.get_size(), 8192);
assert_eq!(entry.get_ftype(), Some(String::from("txt")));
assert_eq!(entry.get_user(), Some(0));
assert_eq!(entry.get_group(), Some(0));
assert_eq!(
entry.get_unix_pex(),
Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4)))
);
assert_eq!(entry.is_symlink(), false);
assert_eq!(entry.is_dir(), false);
assert_eq!(entry.is_file(), true);
assert_eq!(entry.unwrap_file().abs_path, PathBuf::from("/bar.txt"));
}
#[test]
#[should_panic]
fn test_fs_fsentry_file_unwrap_bad() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
entry.unwrap_dir();
}
#[test]
#[should_panic]
fn test_fs_fsentry_dir_unwrap_bad() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("foo"),
abs_path: PathBuf::from("/foo"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
entry.unwrap_file();
}
#[test]
fn test_fs_fsentry_hidden_files() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(entry.is_hidden(), false);
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from(".gitignore"),
abs_path: PathBuf::from("/.gitignore"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(entry.is_hidden(), true);
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from(".git"),
abs_path: PathBuf::from("/.git"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(entry.is_hidden(), true);
}
#[test]
fn test_fs_fsentry_realfile_none() {
let t_now: SystemTime = SystemTime::now();
// With file...
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
// Symlink is None...
assert_eq!(
entry.get_realfile().get_abs_path(),
PathBuf::from("/bar.txt")
);
// With directory...
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("foo"),
abs_path: PathBuf::from("/foo"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(entry.get_realfile().get_abs_path(), PathBuf::from("/foo"));
}
#[test]
fn test_fs_fsentry_realfile_some() {
let t_now: SystemTime = SystemTime::now();
// Prepare entries
// root -> child -> target
let entry_target: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), // UNIX only
});
let entry_child: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/develop/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: Some(Box::new(entry_target)),
user: Some(0),
group: Some(0),
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
});
let entry_root: FsEntry = FsEntry::File(FsFile {
name: String::from("projects"),
abs_path: PathBuf::from("/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8,
ftype: None,
symlink: Some(Box::new(entry_child)),
user: Some(0),
group: Some(0),
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
});
assert_eq!(entry_root.is_symlink(), true);
// get real file
let real_file: FsEntry = entry_root.get_realfile();
// real file must be projects in /home/cvisintin
assert_eq!(
real_file.get_abs_path(),
PathBuf::from("/home/cvisintin/projects")
);
}
#[test]
fn unix_pex() {
let pex: UnixPex = UnixPex::from(4);
assert_eq!(pex.can_read(), true);
assert_eq!(pex.can_write(), false);
assert_eq!(pex.can_execute(), false);
let pex: UnixPex = UnixPex::from(0);
assert_eq!(pex.can_read(), false);
assert_eq!(pex.can_write(), false);
assert_eq!(pex.can_execute(), false);
let pex: UnixPex = UnixPex::from(3);
assert_eq!(pex.can_read(), false);
assert_eq!(pex.can_write(), true);
assert_eq!(pex.can_execute(), true);
let pex: UnixPex = UnixPex::from(7);
assert_eq!(pex.can_read(), true);
assert_eq!(pex.can_write(), true);
assert_eq!(pex.can_execute(), true);
let pex: UnixPex = UnixPex::from(3);
assert_eq!(pex.as_byte(), 3);
let pex: UnixPex = UnixPex::from(7);
assert_eq!(pex.as_byte(), 7);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,75 @@
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
#![doc(html_playground_url = "https://play.rust-lang.org")]
#![doc(
html_favicon_url = "https://raw.githubusercontent.com/veeso/termscp/main/assets/images/termscp-128.png"
)]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/veeso/termscp/main/assets/images/termscp-512.png"
)]
#[macro_use] extern crate lazy_static;
/**
* 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.
*/
#[macro_use]
extern crate bitflags;
extern crate bytesize;
extern crate chrono;
extern crate content_inspector;
extern crate crossterm;
extern crate dirs;
extern crate edit;
extern crate hostname;
#[cfg(feature = "with-keyring")]
extern crate keyring;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate magic_crypt;
extern crate open;
#[cfg(target_os = "windows")]
extern crate path_slash;
extern crate rand;
extern crate regex;
extern crate ssh2;
extern crate suppaftp;
extern crate tempfile;
extern crate textwrap;
extern crate tui_realm_stdlib;
extern crate tuirealm;
extern crate ureq;
#[cfg(target_family = "unix")]
extern crate users;
extern crate whoami;
extern crate wildmatch;
pub mod activity_manager;
pub mod config;
pub mod filetransfer;
pub mod fs;
pub mod host;
pub mod support;
pub mod system;
pub mod ui;
pub mod utils;

View File

@@ -1,175 +1,274 @@
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* 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.
*/
const TERMSCP_VERSION: &'static str = env!("CARGO_PKG_VERSION");
const TERMSCP_AUTHORS: &'static str = env!("CARGO_PKG_AUTHORS");
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
// Crates
extern crate getopts;
#[macro_use] extern crate lazy_static;
extern crate argh;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate magic_crypt;
extern crate rpassword;
// External libs
use getopts::Options;
use argh::FromArgs;
use std::env;
use std::path::PathBuf;
use std::time::Duration;
// Include
mod activity_manager;
mod config;
mod filetransfer;
mod fs;
mod host;
mod support;
mod system;
mod ui;
mod utils;
// namespaces
use activity_manager::{ActivityManager, NextActivity};
use filetransfer::FileTransferProtocol;
use filetransfer::FileTransferParams;
use system::logging;
/// ### print_usage
///
/// Print usage
enum Task {
Activity(NextActivity),
ImportTheme(PathBuf),
}
fn print_usage(opts: Options) {
let brief = format!("Usage: termscp [options]... [protocol://user@address:port]");
print!("{}", opts.usage(&brief));
println!("\nPlease, report issues to <https://github.com/ChristianVisintin/TermSCP>");
#[derive(FromArgs)]
#[argh(description = "
where positional can be: [protocol://user@address:port:wrkdir] [local-wrkdir]
Please, report issues to <https://github.com/veeso/termscp>
Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")]
struct Args {
#[argh(switch, short = 'c', description = "open termscp configuration")]
config: bool,
#[argh(option, short = 'P', description = "provide password from CLI")]
password: Option<String>,
#[argh(switch, short = 'q', description = "disable logging")]
quiet: bool,
#[argh(option, short = 't', description = "import specified theme")]
theme: Option<String>,
#[argh(
option,
short = 'T',
default = "10",
description = "set UI ticks; default 10ms"
)]
ticks: u64,
#[argh(switch, short = 'v', description = "print version")]
version: bool,
// -- positional
#[argh(
positional,
description = "protocol://user@address:port:wrkdir local-wrkdir"
)]
positional: Vec<String>,
}
struct RunOpts {
remote: Option<FileTransferParams>,
ticks: Duration,
log_enabled: bool,
task: Task,
}
impl Default for RunOpts {
fn default() -> Self {
Self {
remote: None,
ticks: Duration::from_millis(10),
log_enabled: true,
task: Task::Activity(NextActivity::Authentication),
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
//Program CLI options
let mut address: Option<String> = None; // None
let mut port: u16 = 22; // Default port
let mut username: Option<String> = None; // Default username
let mut password: Option<String> = None; // Default password
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
let mut ticks: Duration = Duration::from_millis(10);
//Process options
let mut opts = Options::new();
opts.optopt(
"P",
"password",
"Provide password from CLI (use at your own risk)",
"<password>",
);
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
opts.optflag("v", "version", "");
opts.optflag("h", "help", "Print this menu");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
println!("{}", f.to_string());
let args: Args = argh::from_env();
// Parse args
let mut run_opts: RunOpts = match parse_args(args) {
Ok(opts) => opts,
Err(err) => {
eprintln!("{}", err);
std::process::exit(255);
}
};
// Help
if matches.opt_present("h") {
print_usage(opts);
// Setup logging
if run_opts.log_enabled {
if let Err(err) = logging::init() {
eprintln!("Failed to initialize logging: {}", err);
}
}
// Read password from remote
if let Err(err) = read_password(&mut run_opts) {
eprintln!("{}", err);
std::process::exit(255);
}
info!("termscp {} started!", TERMSCP_VERSION);
// Run
info!("Starting activity manager...");
let rc: i32 = run(run_opts);
info!("termscp terminated");
// Then return
std::process::exit(rc);
}
/// ### parse_args
///
/// Parse arguments
/// In case of success returns `RunOpts`
/// in case something is wrong returns the error message
fn parse_args(args: Args) -> Result<RunOpts, String> {
let mut run_opts: RunOpts = RunOpts::default();
// Version
if matches.opt_present("v") {
eprintln!(
"TermSCP - {} - Developed by {}",
if args.version {
return Err(format!(
"termscp - {} - Developed by {}",
TERMSCP_VERSION, TERMSCP_AUTHORS,
);
std::process::exit(255);
));
}
// Match password
if let Some(passwd) = matches.opt_str("P") {
password = Some(String::from(passwd));
// Setup activity?
if args.config {
run_opts.task = Task::Activity(NextActivity::SetupActivity);
}
// Logging
if args.quiet {
run_opts.log_enabled = false;
}
// Match ticks
if let Some(val) = matches.opt_str("T") {
match val.parse::<usize>() {
Ok(val) => ticks = Duration::from_millis(val as u64),
Err(_) => {
eprintln!("Ticks is not a number '{}'", val);
print_usage(opts);
std::process::exit(255);
}
}
run_opts.ticks = Duration::from_millis(args.ticks);
// @! extra modes
if let Some(theme) = args.theme {
run_opts.task = Task::ImportTheme(PathBuf::from(theme));
}
// Check free args
let extra_args: Vec<String> = matches.free.clone();
if let Some(remote) = extra_args.get(0) {
// @! Ordinary mode
// Remote argument
if let Some(remote) = args.positional.get(0) {
// Parse address
match utils::parse_remote_opt(remote) {
Ok((addr, portn, proto, user)) => {
match utils::parser::parse_remote_opt(remote.as_str()) {
Ok(mut remote) => {
// If password is provided, set password
if let Some(passwd) = args.password {
remote = remote.password(Some(passwd));
}
// Set params
address = Some(addr);
port = portn;
protocol = proto;
username = user;
run_opts.remote = Some(remote);
// In this case the first activity will be FileTransfer
run_opts.task = Task::Activity(NextActivity::FileTransfer);
}
Err(err) => {
eprintln!("Bad address option: {}", err);
print_usage(opts);
std::process::exit(255);
return Err(format!("Bad address option: {}", err));
}
}
}
// Get working directory
let wrkdir: PathBuf = match env::current_dir() {
Ok(dir) => dir,
Err(_) => PathBuf::from("/"),
};
// Local directory
if let Some(localdir) = args.positional.get(1) {
// Change working directory if local dir is set
let localdir: PathBuf = PathBuf::from(localdir);
if let Err(err) = env::set_current_dir(localdir.as_path()) {
return Err(format!("Bad working directory argument: {}", err));
}
}
Ok(run_opts)
}
/// ### read_password
///
/// Read password from tty if address is specified
fn read_password(run_opts: &mut RunOpts) -> Result<(), String> {
// Initialize client if necessary
let mut start_activity: NextActivity = NextActivity::Authentication;
if address.is_some() {
if password.is_none() {
if let Some(remote) = run_opts.remote.as_mut() {
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", remote.address, remote.port, remote.protocol, remote.username, utils::fmt::shadow_password(remote.password.as_deref().unwrap_or("")));
if remote.password.is_none() {
// Ask password if unspecified
password = match rpassword::read_password_from_tty(Some("Password: ")) {
remote.password = match rpassword::read_password_from_tty(Some("Password: ")) {
Ok(p) => {
if p.len() > 0 {
Some(p)
} else {
if p.is_empty() {
None
} else {
debug!(
"Read password from tty: {}",
utils::fmt::shadow_password(p.as_str())
);
Some(p)
}
}
Err(_) => {
eprintln!("Could not read password from prompt");
std::process::exit(255);
return Err("Could not read password from prompt".to_string());
}
};
}
// In this case the first activity will be FileTransfer
start_activity = NextActivity::FileTransfer;
}
// Create activity manager (and context too)
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
Ok(m) => m,
Err(_) => {
eprintln!("Invalid directory '{}'", wrkdir.display());
std::process::exit(255);
}
};
// Set file transfer params if set
if let Some(address) = address {
manager.set_filetransfer_params(address, port, protocol, username, password);
}
// Run
manager.run(start_activity);
// Then return
std::process::exit(0);
Ok(())
}
/// ### run
///
/// Run task and return rc
fn run(mut run_opts: RunOpts) -> i32 {
match run_opts.task {
Task::ImportTheme(theme) => match support::import_theme(theme.as_path()) {
Ok(_) => {
println!("Theme has been successfully imported!");
0
}
Err(err) => {
eprintln!("{}", err);
1
}
},
Task::Activity(activity) => {
// Get working directory
let wrkdir: PathBuf = match env::current_dir() {
Ok(dir) => dir,
Err(_) => PathBuf::from("/"),
};
// Create activity manager (and context too)
let mut manager: ActivityManager =
match ActivityManager::new(wrkdir.as_path(), run_opts.ticks) {
Ok(m) => m,
Err(err) => {
eprintln!("Could not start activity manager: {}", err);
return 1;
}
};
// Set file transfer params if set
if let Some(remote) = run_opts.remote.take() {
manager.set_filetransfer_params(remote);
}
manager.run(activity);
0
}
}
}

68
src/support.rs Normal file
View File

@@ -0,0 +1,68 @@
//! ## Support
//!
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
/**
* 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.
*/
// mod
use crate::system::{environment, theme_provider::ThemeProvider};
use std::fs;
use std::path::{Path, PathBuf};
/// ### import_theme
///
/// Import theme at provided path into termscp
pub fn import_theme(p: &Path) -> Result<(), String> {
if !p.exists() {
return Err(String::from(
"Could not import theme: No such file or directory",
));
}
// Validate theme file
ThemeProvider::new(p).map_err(|e| format!("Invalid theme error: {}", e))?;
// get config dir
let cfg_dir: PathBuf = get_config_dir()?;
// Get theme directory
let theme_file: PathBuf = environment::get_theme_path(cfg_dir.as_path());
// Copy theme to theme_dir
fs::copy(p, theme_file.as_path())
.map(|_| ())
.map_err(|e| format!("Could not import theme: {}", e))
}
/// ### get_config_dir
///
/// Get configuration directory
fn get_config_dir() -> Result<PathBuf, String> {
match environment::init_config_dir() {
Ok(Some(config_dir)) => Ok(config_dir),
Ok(None) => Err(String::from(
"Your system doesn't provide a configuration directory",
)),
Err(err) => Err(format!(
"Could not initialize configuration directory: {}",
err
)),
}
}

View File

@@ -0,0 +1,727 @@
//! ## BookmarksClient
//!
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
/**
* 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.
*/
// Crate
#[cfg(feature = "with-keyring")]
use super::keys::keyringstorage::KeyringStorage;
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
// Local
use crate::config::{
bookmarks::{Bookmark, UserHosts},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
use crate::utils::crypto;
use crate::utils::fmt::fmt_time;
use crate::utils::random::random_alphanumeric_with_len;
// Ext
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use std::time::SystemTime;
/// ## BookmarksClient
///
/// BookmarksClient provides a layer between the host system and the bookmarks module
pub struct BookmarksClient {
hosts: UserHosts,
bookmarks_file: PathBuf,
key: String,
recents_size: usize,
}
impl BookmarksClient {
/// ### BookmarksClient
///
/// Instantiates a new BookmarksClient
/// Bookmarks file path must be provided
/// Storage path for file provider must be provided
pub fn new(
bookmarks_file: &Path,
storage_path: &Path,
recents_size: usize,
) -> Result<BookmarksClient, SerializerError> {
// Create default hosts
let default_hosts: UserHosts = UserHosts::default();
debug!("Setting up bookmarks client...");
// Make a key storage (with-keyring)
#[cfg(feature = "with-keyring")]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
debug!("Setting up KeyStorage");
let username: String = whoami::username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
// Check if keyring storage is supported
#[cfg(not(test))]
let app_name: &str = "termscp";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "termscp-test";
match storage.is_supported() {
true => {
debug!("Using KeyringStorage");
(Box::new(storage), app_name)
}
false => {
warn!("KeyringStorage is not supported; using FileStorage");
(Box::new(FileStorage::new(storage_path)), "bookmarks")
}
}
};
// Make a key storage (wno-keyring)
#[cfg(not(feature = "with-keyring"))]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
#[cfg(not(test))]
let app_name: &str = "bookmarks";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "bookmarks-test";
debug!("Using FileStorage");
(Box::new(FileStorage::new(storage_path)), app_name)
};
// Load key
let key: String = match key_storage.get_key(service_id) {
Ok(k) => {
debug!("Key loaded with success");
k
}
Err(e) => match e {
KeyStorageError::NoSuchKey => {
// If no such key, generate key and set it into the storage
let key: String = Self::generate_key();
debug!("Key doesn't exist yet or could not be loaded; generated a new key");
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
error!("Failed to set new key into storage: {}", e);
return Err(SerializerError::new_ex(
SerializerErrorKind::Io,
format!("Could not write key to storage: {}", e),
));
}
// Return key
key
}
_ => {
error!("Failed to get key from storage: {}", e);
return Err(SerializerError::new_ex(
SerializerErrorKind::Io,
format!("Could not get key from storage: {}", e),
));
}
},
};
let mut client: BookmarksClient = BookmarksClient {
hosts: default_hosts,
bookmarks_file: PathBuf::from(bookmarks_file),
key,
recents_size,
};
// If bookmark file doesn't exist, initialize it
if !bookmarks_file.exists() {
info!("Bookmarks file doesn't exist yet; creating it...");
if let Err(err) = client.write_bookmarks() {
error!("Failed to create bookmarks file: {}", err);
return Err(err);
}
} else {
// Load bookmarks from file
if let Err(err) = client.read_bookmarks() {
error!("Failed to load bookmarks: {}", err);
return Err(err);
}
}
info!("Bookmarks client initialized");
// Load key
Ok(client)
}
/// ### iter_bookmarks
///
/// Iterate over bookmarks keys
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.hosts.bookmarks.keys())
}
/// ### get_bookmark
///
/// Get bookmark associated to key
pub fn get_bookmark(
&self,
key: &str,
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
debug!("Getting bookmark {}", key);
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
}
},
entry.username.clone(),
match &entry.password {
// Decrypted password if Some; if decryption fails return None
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
Ok(decrypted_pwd) => Some(decrypted_pwd),
Err(err) => {
error!("Failed to decrypt password for bookmark: {}", err);
None
}
},
None => None,
},
))
}
/// ### add_recent
///
/// Add a new recent to bookmarks
pub fn add_bookmark(
&mut self,
name: String,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) {
if name.is_empty() {
error!("Fatal error; bookmark name is empty");
panic!("Bookmark name can't be empty");
}
// Make bookmark
info!("Added bookmark {} with address {}", name, addr);
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
self.hosts.bookmarks.insert(name, host);
}
/// ### del_bookmark
///
/// Delete entry from bookmarks
pub fn del_bookmark(&mut self, name: &str) {
let _ = self.hosts.bookmarks.remove(name);
info!("Removed bookmark {}", name);
}
/// ### iter_recents
///
/// Iterate over recents keys
pub fn iter_recents(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.hosts.recents.keys())
}
/// ### get_recent
///
/// Get recent associated to key
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
// NOTE: password is not decrypted; recents will never have password
info!("Getting bookmark {}", key);
let entry: &Bookmark = self.hosts.recents.get(key)?;
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
}
},
entry.username.clone(),
))
}
/// ### add_recent
///
/// Add a new recent to bookmarks
pub fn add_recent(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
) {
// Make bookmark
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, None);
// Check if duplicated
for recent_host in self.hosts.recents.values() {
if *recent_host == host {
debug!("Discarding recent since duplicated ({})", host.address);
// Don't save duplicates
return;
}
}
// If hosts size is bigger than self.recents_size; pop last
if self.hosts.recents.len() >= self.recents_size {
// Get keys
let mut keys: Vec<String> = Vec::with_capacity(self.hosts.recents.len());
for key in self.hosts.recents.keys() {
keys.push(key.clone());
}
// Sort keys; NOTE: most recent is the last element
keys.sort();
// Delete keys starting from the last one
for key in keys.iter() {
let _ = self.hosts.recents.remove(key);
debug!("Removed recent bookmark {}", key);
// If length is < self.recents_size; break
if self.hosts.recents.len() < self.recents_size {
break;
}
}
}
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);
}
/// ### del_recent
///
/// Delete entry from recents
pub fn del_recent(&mut self, name: &str) {
let _ = self.hosts.recents.remove(name);
info!("Removed recent host {}", name);
}
/// ### write_bookmarks
///
/// Write bookmarks to file
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
// Open file
debug!("Writing bookmarks");
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.bookmarks_file.as_path())
{
Ok(writer) => serialize(&self.hosts, Box::new(writer)),
Err(err) => {
error!("Failed to write bookmarks: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
}
/// ### read_bookmarks
///
/// Read bookmarks from file
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
// Open bookmarks file for read
debug!("Reading bookmarks");
match OpenOptions::new()
.read(true)
.open(self.bookmarks_file.as_path())
{
Ok(reader) => {
// Deserialize
match deserialize(Box::new(reader)) {
Ok(hosts) => {
self.hosts = hosts;
Ok(())
}
Err(err) => Err(err),
}
}
Err(err) => {
error!("Failed to read bookmarks: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
}
/// ### generate_key
///
/// Generate a new AES key
fn generate_key() -> String {
// Generate 256 bytes (2048 bits) key
random_alphanumeric_with_len(256)
}
/// ### make_bookmark
///
/// Make bookmark from credentials
fn make_bookmark(
&self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) -> Bookmark {
Bookmark {
address: addr,
port,
username,
protocol: protocol.to_string(),
password: password.map(|p| self.encrypt_str(p.as_str())),
}
}
/// ### encrypt_str
///
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
fn encrypt_str(&self, txt: &str) -> String {
crypto::aes128_b64_crypt(self.key.as_str(), txt)
}
/// ### decrypt_str
///
/// Decrypt provided string using AES-128
fn decrypt_str(&self, secret: &str) -> Result<String, SerializerError> {
match crypto::aes128_b64_decrypt(self.key.as_str(), secret) {
Ok(txt) => Ok(txt),
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::Syntax,
err.to_string(),
)),
}
}
}
#[cfg(test)]
#[cfg(not(target_os = "macos"))] // CI/CD blocks
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::thread::sleep;
use std::time::Duration;
use tempfile::TempDir;
#[test]
fn test_system_bookmarks_new() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Verify client
assert_eq!(client.hosts.bookmarks.len(), 0);
assert_eq!(client.hosts.recents.len(), 0);
assert_eq!(client.key.len(), 256);
assert_eq!(client.bookmarks_file, cfg_path);
assert_eq!(client.recents_size, 16);
}
#[test]
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn test_system_bookmarks_new_err() {
assert!(BookmarksClient::new(
Path::new("/tmp/oifoif/omar"),
Path::new("/tmp/efnnu/omar"),
16
)
.is_err());
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
assert!(
BookmarksClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar"), 16).is_err()
);
}
#[test]
fn test_system_bookmarks_new_from_existing() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add some bookmarks
client.add_bookmark(
String::from("raspberry"),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
);
client.add_recent(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
let recent_key: String = String::from(client.iter_recents().next().unwrap());
assert!(client.write_bookmarks().is_ok());
let key: String = client.key.clone();
// Re-initialize a client
let client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Verify it loaded parameters correctly
assert_eq!(client.key, key);
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
client.get_bookmark(&String::from("raspberry")).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
let bookmark: (String, u16, FileTransferProtocol, String) =
client.get_recent(&recent_key).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
}
#[test]
fn test_system_bookmarks_manipulate_bookmarks() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from("raspberry"),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
);
client.add_bookmark(
String::from("raspberry2"),
String::from("192.168.1.32"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword2")),
);
// Iter
assert_eq!(client.iter_bookmarks().count(), 2);
// Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
client.get_bookmark(&String::from("raspberry")).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
// Write bookmarks
assert!(client.write_bookmarks().is_ok());
// Delete bookmark
client.del_bookmark(&String::from("raspberry"));
// Get unexisting bookmark
assert!(client.get_bookmark(&String::from("raspberry")).is_none());
// Write bookmarks
assert!(client.write_bookmarks().is_ok());
}
#[test]
#[should_panic]
fn test_system_bookmarks_bad_bookmark_name() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from(""),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
);
}
#[test]
fn test_system_bookmarks_manipulate_recents() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_recent(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
// Iter
assert_eq!(client.iter_recents().count(), 1);
let key: String = String::from(client.iter_recents().next().unwrap());
// Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String) =
client.get_recent(&key).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
// Write bookmarks
assert!(client.write_bookmarks().is_ok());
// Delete bookmark
client.del_recent(&key);
// Get unexisting bookmark
assert!(client.get_bookmark(&key).is_none());
// Write bookmarks
assert!(client.write_bookmarks().is_ok());
}
#[test]
fn test_system_bookmarks_dup_recent() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_recent(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
client.add_recent(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
// There should be only one recent
assert_eq!(client.iter_recents().count(), 1);
}
#[test]
fn test_system_bookmarks_recents_more_than_limit() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
// Add recent, wait 1 second for each one (cause the name depends on time)
// 1
client.add_recent(
String::from("192.168.1.1"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
sleep(Duration::from_secs(1));
// 2
client.add_recent(
String::from("192.168.1.2"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
sleep(Duration::from_secs(1));
// 3
client.add_recent(
String::from("192.168.1.3"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
);
// Limit is 2
assert_eq!(client.iter_recents().count(), 2);
// Check that 192.168.1.1 has been removed
let key: String = client.iter_recents().nth(0).unwrap().to_string();
assert!(matches!(
client.hosts.recents.get(&key).unwrap().address.as_str(),
"192.168.1.2" | "192.168.1.3"
));
let key: String = client.iter_recents().nth(1).unwrap().to_string();
assert!(matches!(
client.hosts.recents.get(&key).unwrap().address.as_str(),
"192.168.1.2" | "192.168.1.3"
));
}
#[test]
#[should_panic]
fn test_system_bookmarks_add_bookmark_empty() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from(""),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
);
}
#[test]
fn test_system_bookmarks_decrypt_str() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
client.key = "MYSUPERSECRETKEY".to_string();
assert_eq!(
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
"Hello world!"
);
assert!(client.decrypt_str("bidoof").is_err());
}
/// ### get_paths
///
/// Get paths for configuration and key for bookmarks
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
c.push("bookmarks.toml");
(c, k)
}
}

701
src/system/config_client.rs Normal file
View File

@@ -0,0 +1,701 @@
//! ## ConfigClient
//!
//! `config_client` is the module which provides an API between the Config module and the system
/**
* 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::config::{
params::UserConfig,
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
// Ext
use std::fs::{create_dir, remove_file, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
// Types
pub type SshHost = (String, String, PathBuf); // 0: host, 1: username, 2: RSA key path
/// ## ConfigClient
///
/// ConfigClient provides a high level API to communicate with the termscp configuration
pub struct ConfigClient {
config: UserConfig, // Configuration loaded
config_path: PathBuf, // Configuration TOML Path
ssh_key_dir: PathBuf, // SSH Key storage directory
degraded: bool, // Indicates the `ConfigClient` is working in degraded mode
}
impl ConfigClient {
/// ### new
///
/// Instantiate a new `ConfigClient` with provided path
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<Self, SerializerError> {
// Initialize a default configuration
let default_config: UserConfig = UserConfig::default();
info!(
"Setting up config client with config path {} and SSH key directory {}",
config_path.display(),
ssh_key_dir.display()
);
// Create client
let mut client: ConfigClient = ConfigClient {
config: default_config,
config_path: PathBuf::from(config_path),
ssh_key_dir: PathBuf::from(ssh_key_dir),
degraded: false,
};
// If ssh key directory doesn't exist, create it
if !ssh_key_dir.exists() {
if let Err(err) = create_dir(ssh_key_dir) {
error!("Failed to create SSH key dir: {}", err);
return Err(SerializerError::new_ex(
SerializerErrorKind::Io,
format!(
"Could not create SSH key directory \"{}\": {}",
ssh_key_dir.display(),
err
),
));
}
debug!("Created SSH key directory");
}
// If Config file doesn't exist, create it
if !config_path.exists() {
if let Err(err) = client.write_config() {
error!("Couldn't create configuration file: {}", err);
return Err(err);
}
debug!("Config file didn't exist; created file");
} else {
// otherwise Load configuration from file
if let Err(err) = client.read_config() {
error!("Couldn't read configuration file: {}", err);
return Err(err);
}
debug!("Read configuration file");
}
Ok(client)
}
/// ### degraded
///
/// Instantiate a ConfigClient in degraded mode.
/// When in degraded mode, the configuration in use will be the default configuration
/// and the IO operation on configuration won't be available
pub fn degraded() -> Self {
Self {
config: UserConfig::default(),
config_path: PathBuf::default(),
ssh_key_dir: PathBuf::default(),
degraded: true,
}
}
// Text editor
/// ### get_text_editor
///
/// Get text editor from configuration
pub fn get_text_editor(&self) -> PathBuf {
self.config.user_interface.text_editor.clone()
}
/// ### set_text_editor
///
/// Set text editor path
pub fn set_text_editor(&mut self, path: PathBuf) {
self.config.user_interface.text_editor = path;
}
// Default protocol
/// ### get_default_protocol
///
/// Get default protocol from configuration
pub fn get_default_protocol(&self) -> FileTransferProtocol {
match FileTransferProtocol::from_str(self.config.user_interface.default_protocol.as_str()) {
Ok(p) => p,
Err(_) => FileTransferProtocol::Sftp,
}
}
/// ### set_default_protocol
///
/// Set default protocol to configuration
pub fn set_default_protocol(&mut self, proto: FileTransferProtocol) {
self.config.user_interface.default_protocol = proto.to_string();
}
/// ### get_show_hidden_files
///
/// Get value of `show_hidden_files`
pub fn get_show_hidden_files(&self) -> bool {
self.config.user_interface.show_hidden_files
}
/// ### set_show_hidden_files
///
/// Set new value for `show_hidden_files`
pub fn set_show_hidden_files(&mut self, value: bool) {
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)
pub fn get_group_dirs(&self) -> Option<GroupDirs> {
// Convert string to `GroupDirs`
match &self.config.user_interface.group_dirs {
None => None,
Some(val) => match GroupDirs::from_str(val.as_str()) {
Ok(val) => Some(val),
Err(_) => None,
},
}
}
/// ### set_group_dirs
///
/// Set value for group_dir in configuration.
/// Provided value, if `Some` will be converted to `GroupDirs`
pub fn set_group_dirs(&mut self, val: Option<GroupDirs>) {
self.config.user_interface.group_dirs = val.map(|val| val.to_string());
}
/// ### get_local_file_fmt
///
/// Get current file fmt for local host
pub fn get_local_file_fmt(&self) -> Option<String> {
self.config.user_interface.file_fmt.clone()
}
/// ### set_local_file_fmt
///
/// Set file fmt parameter for local host
pub fn set_local_file_fmt(&mut self, s: String) {
self.config.user_interface.file_fmt = match s.is_empty() {
true => None,
false => Some(s),
};
}
/// ### get_remote_file_fmt
///
/// Get current file fmt for remote host
pub fn get_remote_file_fmt(&self) -> Option<String> {
self.config.user_interface.remote_file_fmt.clone()
}
/// ### set_remote_file_fmt
///
/// Set file fmt parameter for remote host
pub fn set_remote_file_fmt(&mut self, s: String) {
self.config.user_interface.remote_file_fmt = match s.is_empty() {
true => None,
false => Some(s),
};
}
// SSH Keys
/// ### save_ssh_key
///
/// Save a SSH key into configuration.
/// This operation also creates the key file in `ssh_key_dir`
/// and also commits changes to configuration, to prevent incoerent data
pub fn add_ssh_key(
&mut self,
host: &str,
username: &str,
ssh_key: &str,
) -> Result<(), SerializerError> {
if self.degraded {
return Err(SerializerError::new_ex(
SerializerErrorKind::Generic,
String::from("Configuration won't be saved, since in degraded mode"),
));
}
let host_name: String = Self::make_ssh_host_key(host, username);
// Get key path
let ssh_key_path: PathBuf = {
let mut p: PathBuf = self.ssh_key_dir.clone();
p.push(format!("{}.key", host_name));
p
};
info!(
"Writing SSH file to {} for host {}",
ssh_key_path.display(),
host_name
);
// Write key to file
let mut f: File = match File::create(ssh_key_path.as_path()) {
Ok(f) => f,
Err(err) => return Self::make_io_err(err),
};
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
error!("Failed to write SSH key to file: {}", err);
return Self::make_io_err(err);
}
// Add host to keys
self.config.remote.ssh_keys.insert(host_name, ssh_key_path);
// Write config
self.write_config()
}
/// ### del_ssh_key
///
/// Delete a ssh key from configuration, using host as key.
/// This operation also unlinks the key file in `ssh_key_dir`
/// and also commits changes to configuration, to prevent incoerent data
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
if self.degraded {
return Err(SerializerError::new_ex(
SerializerErrorKind::Generic,
String::from("Configuration won't be saved, since in degraded mode"),
));
}
// Remove key from configuration and get key path
info!("Removing key for {}@{}", host, username);
let key_path: PathBuf = match self
.config
.remote
.ssh_keys
.remove(&Self::make_ssh_host_key(host, username))
{
Some(p) => p,
None => return Ok(()), // Return ok if host doesn't exist
};
// Remove file
if let Err(err) = remove_file(key_path.as_path()) {
error!("Failed to remove key file {}: {}", key_path.display(), err);
return Self::make_io_err(err);
}
// Commit changes to configuration
self.write_config()
}
/// ### get_ssh_key
///
/// Get ssh key from host.
/// None is returned if key doesn't exist
/// `std::io::Error` is returned in case it was not possible to read the key file
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
if self.degraded {
return Ok(None);
}
// Check if Key exists
match self.config.remote.ssh_keys.get(mkey) {
None => Ok(None),
Some(key_path) => {
// Get host and username
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
// Return key
Ok(Some((host, username, PathBuf::from(key_path))))
}
}
}
/// ### iter_ssh_keys
///
/// Get an iterator through hosts in the ssh key storage
pub fn iter_ssh_keys(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.config.remote.ssh_keys.keys())
}
// I/O
/// ### write_config
///
/// Write configuration to file
pub fn write_config(&self) -> Result<(), SerializerError> {
if self.degraded {
return Err(SerializerError::new_ex(
SerializerErrorKind::Generic,
String::from("Configuration won't be saved, since in degraded mode"),
));
}
// Open file
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.config_path.as_path())
{
Ok(writer) => serialize(&self.config, Box::new(writer)),
Err(err) => {
error!("Failed to write configuration file: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
}
/// ### read_config
///
/// Read configuration from file (or reload it if already read)
pub fn read_config(&mut self) -> Result<(), SerializerError> {
if self.degraded {
return Err(SerializerError::new_ex(
SerializerErrorKind::Generic,
String::from("Configuration won't be loaded, since in degraded mode"),
));
}
// Open bookmarks file for read
match OpenOptions::new()
.read(true)
.open(self.config_path.as_path())
{
Ok(reader) => {
// Deserialize
match deserialize(Box::new(reader)) {
Ok(config) => {
self.config = config;
Ok(())
}
Err(err) => Err(err),
}
}
Err(err) => {
error!("Failed to read configuration: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
}
/// ### make_ssh_host_key
///
/// Hosts are saved as `username@host` into configuration.
/// This method creates the key name, starting from host and username
fn make_ssh_host_key(host: &str, username: &str) -> String {
format!("{}@{}", username, host)
}
/// ### get_ssh_tokens
///
/// Get ssh tokens starting from ssh host key
/// Panics if key has invalid syntax
/// Returns: (host, username)
fn get_ssh_tokens(host_key: &str) -> (String, String) {
let tokens: Vec<&str> = host_key.split('@').collect();
assert_eq!(tokens.len(), 2);
(String::from(tokens[1]), String::from(tokens[0]))
}
/// ### make_io_err
///
/// Make serializer error from `std::io::Error`
fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> {
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::UserConfig;
use crate::utils::random::random_alphanumeric_with_len;
use pretty_assertions::assert_eq;
use std::io::Read;
use tempfile::TempDir;
#[test]
fn test_system_config_new() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
.ok()
.unwrap();
// Verify parameters
let default_config: UserConfig = UserConfig::default();
assert_eq!(client.degraded, false);
assert_eq!(client.config.remote.ssh_keys.len(), 0);
assert_eq!(
client.config.user_interface.default_protocol,
default_config.user_interface.default_protocol
);
assert_eq!(
client.config.user_interface.text_editor,
default_config.user_interface.text_editor
);
assert_eq!(client.config_path, cfg_path);
assert_eq!(client.ssh_key_dir, ssh_keys_path);
}
#[test]
fn test_system_config_degraded() {
let mut client: ConfigClient = ConfigClient::degraded();
assert_eq!(client.degraded, true);
assert_eq!(client.config_path, PathBuf::default());
assert_eq!(client.ssh_key_dir, PathBuf::default());
// I/O
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
assert!(client.del_ssh_key("omar", "omar").is_err());
assert!(client.get_ssh_key("omar").ok().unwrap().is_none());
assert!(client.write_config().is_err());
assert!(client.read_config().is_err());
}
#[test]
fn test_system_config_new_err() {
assert!(
ConfigClient::new(Path::new("/tmp/oifoif/omar"), Path::new("/tmp/efnnu/omar"),)
.is_err()
);
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
assert!(ConfigClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar")).is_err());
}
#[test]
fn test_system_config_from_existing() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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();
// Change some stuff
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
client.set_default_protocol(FileTransferProtocol::Scp);
assert!(client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok());
assert!(client.write_config().is_ok());
// Istantiate a new client
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
// Verify client has updated parameters
assert_eq!(client.get_default_protocol(), FileTransferProtocol::Scp);
assert_eq!(client.get_text_editor(), PathBuf::from("/usr/bin/vim"));
let mut expected_key_path: PathBuf = key_path.clone();
expected_key_path.push("pi@192.168.1.31.key");
assert_eq!(
client.get_ssh_key("pi@192.168.1.31").unwrap().unwrap(),
(
String::from("192.168.1.31"),
String::from("pi"),
expected_key_path,
)
);
}
#[test]
fn test_system_config_text_editor() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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();
client.set_text_editor(PathBuf::from("mcedit"));
assert_eq!(client.get_text_editor(), PathBuf::from("mcedit"));
}
#[test]
fn test_system_config_default_protocol() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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();
client.set_default_protocol(FileTransferProtocol::Ftp(true));
assert_eq!(
client.get_default_protocol(),
FileTransferProtocol::Ftp(true)
);
}
#[test]
fn test_system_config_show_hidden_files() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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();
client.set_show_hidden_files(true);
assert_eq!(client.get_show_hidden_files(), true);
}
#[test]
fn test_system_config_check_for_updates() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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: TempDir = TempDir::new().ok().unwrap();
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();
client.set_group_dirs(Some(GroupDirs::First));
assert_eq!(client.get_group_dirs(), Some(GroupDirs::First),);
client.set_group_dirs(None);
assert_eq!(client.get_group_dirs(), None,);
}
#[test]
fn test_system_config_local_file_fmt() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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_local_file_fmt(), None);
client.set_local_file_fmt(String::from("{NAME}"));
assert_eq!(client.get_local_file_fmt().unwrap(), String::from("{NAME}"));
// Delete
client.set_local_file_fmt(String::from(""));
assert_eq!(client.get_local_file_fmt(), None);
}
#[test]
fn test_system_config_remote_file_fmt() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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_remote_file_fmt(), None);
client.set_remote_file_fmt(String::from("{NAME}"));
assert_eq!(
client.get_remote_file_fmt().unwrap(),
String::from("{NAME}")
);
// Delete
client.set_remote_file_fmt(String::from(""));
assert_eq!(client.get_remote_file_fmt(), None);
}
#[test]
fn test_system_config_ssh_keys() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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();
// Add a new key
let rsa_key: String = get_sample_rsa_key();
assert!(client
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
.is_ok());
// Iterate keys
for key in client.iter_ssh_keys() {
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
assert_eq!(host.0, String::from("192.168.1.31"));
assert_eq!(host.1, String::from("pi"));
let mut expected_key_path: PathBuf = key_path.clone();
expected_key_path.push("pi@192.168.1.31.key");
assert_eq!(host.2, expected_key_path);
// Read rsa key
let mut key_file: File = File::open(expected_key_path.as_path()).ok().unwrap();
// Read
let mut key: String = String::new();
assert!(key_file.read_to_string(&mut key).is_ok());
// Verify rsa key
assert_eq!(key, rsa_key);
}
// Unexisting key
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
// Delete key
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
}
#[test]
fn test_system_config_make_key() {
assert_eq!(
ConfigClient::make_ssh_host_key("192.168.1.31", "pi"),
String::from("pi@192.168.1.31")
);
assert_eq!(
ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
(String::from("192.168.1.31"), String::from("pi"))
);
}
#[test]
fn test_system_config_make_io_err() {
let err: SerializerError =
ConfigClient::make_io_err(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
.err()
.unwrap();
assert_eq!(err.to_string(), "IO error (permission denied)");
}
/// ### get_paths
///
/// Get paths for configuration and keys directory
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let mut k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
k.push("ssh-keys/");
c.push("config.toml");
(c, k)
}
fn get_sample_rsa_key() -> String {
format!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n{}\n-----END OPENSSH PRIVATE KEY-----",
random_alphanumeric_with_len(2536)
)
}
}

179
src/system/environment.rs Normal file
View File

@@ -0,0 +1,179 @@
//! ## Environment
//!
//! `environment` is the module which provides Path and values for the system environment
/**
* 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.
*/
// Ext
use std::path::{Path, PathBuf};
/// ### get_config_dir
///
/// Get termscp configuration directory path.
/// Returns None, if it's not possible to get it
pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
// Get file
#[cfg(not(test))]
lazy_static! {
static ref CONF_DIR: Option<PathBuf> = dirs::config_dir();
}
#[cfg(test)]
lazy_static! {
static ref CONF_DIR: Option<PathBuf> = Some(std::env::temp_dir());
}
if CONF_DIR.is_some() {
// Get path of bookmarks
let mut p: PathBuf = CONF_DIR.as_ref().unwrap().clone();
// Append termscp dir
p.push("termscp/");
// If directory doesn't exist, create it
match p.exists() {
true => Ok(Some(p)),
false => match std::fs::create_dir(p.as_path()) {
Ok(_) => Ok(Some(p)),
Err(err) => Err(err.to_string()),
},
}
} else {
Ok(None)
}
}
/// ### get_bookmarks_paths
///
/// Get paths for bookmarks client
/// Returns: path of bookmarks.toml
pub fn get_bookmarks_paths(config_dir: &Path) -> PathBuf {
// Prepare paths
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
bookmarks_file.push("bookmarks.toml");
bookmarks_file
}
/// ### get_config_paths
///
/// Returns paths for config client
/// Returns: path of config.toml and path for ssh keys
pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
// Prepare paths
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
bookmarks_file.push("config.toml");
let mut keys_dir: PathBuf = PathBuf::from(config_dir);
keys_dir.push(".ssh/"); // Path where keys are stored
(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
}
/// ### get_theme_path
///
/// Get paths for theme provider
/// Returns: path of theme.toml
pub fn get_theme_path(config_dir: &Path) -> PathBuf {
// Prepare paths
let mut theme_file: PathBuf = PathBuf::from(config_dir);
theme_file.push("theme.toml");
theme_file
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs::{File, OpenOptions};
use std::io::Write;
#[test]
fn test_system_environment_get_config_dir() {
// Create and get conf_dir
let conf_dir: PathBuf = init_config_dir().ok().unwrap().unwrap();
// Remove dir
assert!(std::fs::remove_dir_all(conf_dir.as_path()).is_ok());
}
#[test]
fn test_system_environment_get_config_dir_err() {
let mut conf_dir: PathBuf = std::env::temp_dir();
conf_dir.push("termscp");
// Create file
let mut f: File = OpenOptions::new()
.create(true)
.write(true)
.open(conf_dir.as_path())
.ok()
.unwrap();
// Write
assert!(writeln!(f, "Hello world!").is_ok());
// Drop file
drop(f);
// Get config dir (will fail)
assert!(init_config_dir().is_err());
// Remove file
assert!(std::fs::remove_file(conf_dir.as_path()).is_ok());
}
#[test]
fn test_system_environment_get_bookmarks_paths() {
assert_eq!(
get_bookmarks_paths(&Path::new("/home/omar/.config/termscp/")),
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
);
}
#[test]
fn test_system_environment_get_config_paths() {
assert_eq!(
get_config_paths(&Path::new("/home/omar/.config/termscp/")),
(
PathBuf::from("/home/omar/.config/termscp/config.toml"),
PathBuf::from("/home/omar/.config/termscp/.ssh/")
)
);
}
#[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"),
);
}
#[test]
fn test_system_environment_get_theme_path() {
assert_eq!(
get_theme_path(&Path::new("/home/omar/.config/termscp/")),
PathBuf::from("/home/omar/.config/termscp/theme.toml"),
);
}
}

View File

@@ -0,0 +1,167 @@
//! ## FileStorage
//!
//! `filestorage` provides an implementation of the `KeyStorage` trait using a file
/**
* 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.
*/
// Local
use super::{KeyStorage, KeyStorageError};
// Ext
use std::fs::{OpenOptions, Permissions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
/// ## FileStorage
///
/// File storage is an implementation o the `KeyStorage` which uses a file to store the key
pub struct FileStorage {
dir_path: PathBuf,
}
impl FileStorage {
/// ### new
///
/// Instantiates a new `FileStorage`
pub fn new(dir_path: &Path) -> Self {
FileStorage {
dir_path: PathBuf::from(dir_path),
}
}
/// ### make_file_path
///
/// Make file path for key file from `dir_path` and the application id
fn make_file_path(&self, storage_id: &str) -> PathBuf {
let mut p: PathBuf = self.dir_path.clone();
let file_name = format!(".{}.key", storage_id);
p.push(file_name);
p
}
}
impl KeyStorage for FileStorage {
/// ### get_key
///
/// Retrieve key from the key storage.
/// The key might be acccess through an identifier, which identifies
/// the key in the storage
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
let key_file: PathBuf = self.make_file_path(storage_id);
// Check if file exists
if !key_file.exists() {
return Err(KeyStorageError::NoSuchKey);
}
// Read key from file
match OpenOptions::new().read(true).open(key_file.as_path()) {
Ok(mut file) => {
let mut key: String = String::new();
match file.read_to_string(&mut key) {
Ok(_) => Ok(key),
Err(_) => Err(KeyStorageError::ProviderError),
}
}
Err(_) => Err(KeyStorageError::ProviderError),
}
}
/// ### set_key
///
/// Set the key into the key storage
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
let key_file: PathBuf = self.make_file_path(storage_id);
// Write key
match OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(key_file.as_path())
{
Ok(mut file) => {
// Write key to file
if file.write_all(key.as_bytes()).is_err() {
return Err(KeyStorageError::ProviderError);
}
// Set file to readonly
let mut permissions: Permissions = file.metadata().unwrap().permissions();
permissions.set_readonly(true);
let _ = file.set_permissions(permissions);
Ok(())
}
Err(_) => Err(KeyStorageError::ProviderError),
}
}
/// is_supported
///
/// Returns whether the key storage is supported on the host system
fn is_supported(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_system_keys_filestorage_make_dir() {
let storage: FileStorage = FileStorage::new(&Path::new("/tmp/"));
assert_eq!(
storage.make_file_path("bookmarks").as_path(),
Path::new("/tmp/.bookmarks.key")
);
}
#[test]
fn test_system_keys_filestorage_ok() {
let key_dir: tempfile::TempDir =
tempfile::TempDir::new().expect("Could not create tempdir");
let storage: FileStorage = FileStorage::new(key_dir.path());
// Supported
assert!(storage.is_supported());
let app_name: &str = "termscp";
let secret: &str = "Th15-15/My-Супер-Секрет";
// Secret should not exist
assert_eq!(
storage.get_key(app_name).err().unwrap(),
KeyStorageError::NoSuchKey
);
// Write secret
assert!(storage.set_key(app_name, secret).is_ok());
// Get secret
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
}
#[test]
fn test_system_keys_filestorage_err() {
let bad_dir: &Path = Path::new("/piro/poro/pero/");
let storage: FileStorage = FileStorage::new(bad_dir);
let app_name: &str = "termscp";
let secret: &str = "Th15-15/My-Супер-Секрет";
assert!(storage.set_key(app_name, secret).is_err());
}
}

View File

@@ -0,0 +1,137 @@
//! ## KeyringStorage
//!
//! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring
/**
* 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.
*/
// Local
use super::{KeyStorage, KeyStorageError};
// Ext
use keyring::{Keyring, KeyringError};
/// ## KeyringStorage
///
/// provides a `KeyStorage` implementation using the keyring crate
pub struct KeyringStorage {
username: String,
}
impl KeyringStorage {
/// ### new
///
/// Instantiates a new KeyringStorage
pub fn new(username: &str) -> Self {
KeyringStorage {
username: username.to_string(),
}
}
}
impl KeyStorage for KeyringStorage {
/// ### get_key
///
/// Retrieve key from the key storage.
/// The key might be acccess through an identifier, which identifies
/// the key in the storage
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
match storage.get_password() {
Ok(s) => Ok(s),
Err(e) => match e {
KeyringError::NoPasswordFound => Err(KeyStorageError::NoSuchKey),
#[cfg(target_os = "windows")]
KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey),
#[cfg(target_os = "macos")]
KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey),
#[cfg(target_os = "linux")]
KeyringError::SecretServiceError(_) => Err(KeyStorageError::ProviderError),
KeyringError::Parse(_) => Err(KeyStorageError::BadSytax),
_ => Err(KeyStorageError::ProviderError),
},
}
}
/// ### set_key
///
/// Set the key into the key storage
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
match storage.set_password(key) {
Ok(_) => Ok(()),
Err(_) => Err(KeyStorageError::ProviderError),
}
}
/// is_supported
///
/// Returns whether the key storage is supported on the host system
fn is_supported(&self) -> bool {
let dummy: String = String::from("dummy-service");
let storage: Keyring = Keyring::new(dummy.as_str(), self.username.as_str());
// Check what kind of error is returned
match storage.get_password() {
Ok(_) => true,
#[cfg(not(target_os = "linux"))]
Err(err) => !matches!(err, KeyringError::NoBackendFound),
#[cfg(target_os = "linux")]
Err(err) => !matches!(
err,
KeyringError::NoBackendFound | KeyringError::SecretServiceError(_)
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use whoami::username;
#[test]
fn test_system_keys_keyringstorage() {
let username: String = username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
assert!(storage.is_supported());
let app_name: &str = "termscp-test2";
let secret: &str = "Th15-15/My-Супер-Секрет";
let kring: Keyring = Keyring::new(app_name, username.as_str());
let _ = kring.delete_password();
drop(kring);
// Secret should not exist
assert_eq!(
storage.get_key(app_name).err().unwrap(),
KeyStorageError::NoSuchKey
);
// Write secret
assert!(storage.set_key(app_name, secret).is_ok());
// Get secret
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
// Delete the key manually...
let kring: Keyring = Keyring::new(app_name, username.as_str());
assert!(kring.delete_password().is_ok());
}
}

94
src/system/keys/mod.rs Normal file
View File

@@ -0,0 +1,94 @@
//! ## KeyStorage
//!
//! `keystorage` provides the trait to manipulate to a KeyStorage
/**
* 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.
*/
// Storages
pub mod filestorage;
#[cfg(feature = "with-keyring")]
pub mod keyringstorage;
// ext
use thiserror::Error;
/// ## KeyStorageError
///
/// defines the error type for the `KeyStorage`
#[derive(Debug, Error, PartialEq)]
pub enum KeyStorageError {
#[cfg(feature = "with-keyring")]
#[error("Key has a bad syntax")]
BadSytax,
#[error("Provider service error")]
ProviderError,
#[error("No such key")]
NoSuchKey,
}
/// ## KeyStorage
///
/// this traits provides the methods to communicate and interact with the key storage.
pub trait KeyStorage {
/// ### get_key
///
/// Retrieve key from the key storage.
/// The key might be acccess through an identifier, which identifies
/// the key in the storage
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError>;
/// ### set_key
///
/// Set the key into the key storage
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>;
/// is_supported
///
/// Returns whether the key storage is supported on the host system
fn is_supported(&self) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_system_keys_mod_errors() {
#[cfg(feature = "with-keyring")]
assert_eq!(
KeyStorageError::BadSytax.to_string(),
String::from("Key has a bad syntax")
);
assert_eq!(
KeyStorageError::ProviderError.to_string(),
String::from("Provider service error")
);
assert_eq!(
KeyStorageError::NoSuchKey.to_string(),
String::from("No such key")
);
}
}

72
src/system/logging.rs Normal file
View File

@@ -0,0 +1,72 @@
//! ## Logging
//!
//! `logging` is the module which initializes the logging system for termscp
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use crate::system::environment::{get_log_paths, init_config_dir};
use crate::utils::file::open_file;
// ext
use simplelog::{ConfigBuilder, LevelFilter, WriteLogger};
use std::fs::File;
use std::path::PathBuf;
/// ### init
///
/// Initialize logger
pub fn init() -> Result<(), String> {
// Init config dir
let config_dir: PathBuf = match init_config_dir() {
Ok(Some(p)) => p,
Ok(None) => {
return Err(String::from(
"This system doesn't seem to support CONFIG_DIR",
))
}
Err(err) => return Err(err),
};
let log_file_path: PathBuf = get_log_paths(config_dir.as_path());
// Open log file
let file: File = open_file(log_file_path.as_path(), true, true, false)
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
// Prepare log config
let config = ConfigBuilder::new()
.set_time_format_str("%Y-%m-%dT%H:%M:%S%z")
.build();
// Make logger
WriteLogger::init(LevelFilter::Trace, config, file)
.map_err(|e| format!("Failed to initialize logger: {}", e))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_system_logging_setup() {
assert!(init().is_ok());
}
}

35
src/system/mod.rs Normal file
View File

@@ -0,0 +1,35 @@
//! ## System
//!
//! `system` is the module which contains functions and data types related to current system
/**
* 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.
*/
// modules
pub mod bookmarks_client;
pub mod config_client;
pub mod environment;
pub(self) mod keys;
pub mod logging;
pub mod sshkey_storage;
pub mod theme_provider;

View File

@@ -0,0 +1,162 @@
//! ## SshKeyStorage
//!
//! `SshKeyStorage` is the module which behaves a storage for ssh keys
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::config_client::ConfigClient;
// Ext
use std::collections::HashMap;
use std::path::PathBuf;
pub struct SshKeyStorage {
hosts: HashMap<String, PathBuf>, // Association between {user}@{host} and RSA key path
}
impl SshKeyStorage {
/// ### storage_from_config
///
/// Create a `SshKeyStorage` starting from a `ConfigClient`
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
let mut hosts: HashMap<String, PathBuf> =
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
debug!("Setting up SSH key storage");
// Iterate over keys
for key in cfg_client.iter_ssh_keys() {
match cfg_client.get_ssh_key(key) {
Ok(host) => match host {
Some((addr, username, rsa_key_path)) => {
let key_name: String = Self::make_mapkey(&addr, &username);
hosts.insert(key_name, rsa_key_path);
}
None => continue,
},
Err(err) => {
error!("Failed to get SSH key for {}: {}", key, err);
continue;
}
}
info!("Got SSH key for {}", key);
}
// Return storage
SshKeyStorage { hosts }
}
/// ### empty
///
/// Create an empty ssh key storage; used in case `ConfigClient` is not available
#[cfg(test)]
pub fn empty() -> Self {
SshKeyStorage {
hosts: HashMap::new(),
}
}
/// ### resolve
///
/// Return RSA key path from host and username
pub fn resolve(&self, host: &str, username: &str) -> Option<&PathBuf> {
let key: String = Self::make_mapkey(host, username);
self.hosts.get(&key)
}
/// ### make_mapkey
///
/// Make mapkey from host and username
fn make_mapkey(host: &str, username: &str) -> String {
format!("{}@{}", username, host)
}
#[cfg(test)]
/// ### add_key
///
/// Add a key to storage
/// NOTE: available only for tests
pub fn add_key(&mut self, host: &str, username: &str, p: PathBuf) {
let key: String = Self::make_mapkey(host, username);
self.hosts.insert(key, p);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::config_client::ConfigClient;
use pretty_assertions::assert_eq;
use std::path::Path;
#[test]
fn test_system_sshkey_storage_new() {
let tmp_dir: tempfile::TempDir = tempfile::TempDir::new().ok().unwrap();
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();
// Add ssh key
assert!(client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok());
// Create ssh key storage
let storage: SshKeyStorage = SshKeyStorage::storage_from_config(&client);
// Verify key exists
let mut exp_key_path: PathBuf = key_path.clone();
exp_key_path.push("pi@192.168.1.31.key");
assert_eq!(
*storage.resolve("192.168.1.31", "pi").unwrap(),
exp_key_path
);
// Verify unexisting key
assert!(storage.resolve("deskichup", "veeso").is_none());
}
#[test]
fn test_system_sshkey_storage_empty() {
let storage: SshKeyStorage = SshKeyStorage::empty();
assert_eq!(storage.hosts.len(), 0);
}
#[test]
fn test_system_sshkey_storage_add() {
let mut storage: SshKeyStorage = SshKeyStorage::empty();
storage.add_key("deskichup", "veeso", PathBuf::from("/tmp/omar"));
assert_eq!(
*storage.resolve("deskichup", "veeso").unwrap(),
PathBuf::from("/tmp/omar")
);
}
/// ### get_paths
///
/// Get paths for configuration and keys directory
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let mut k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
k.push("ssh-keys/");
c.push("config.toml");
(c, k)
}
}

View File

@@ -0,0 +1,246 @@
//! ## ThemeProvider
//!
//! `theme_provider` is the module which provides an API between the theme configuration and the system
/**
* 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::config::{
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
themes::Theme,
};
// Ext
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::string::ToString;
/// ## ThemeProvider
///
/// ThemeProvider provides a high level API to communicate with the termscp theme
pub struct ThemeProvider {
theme: Theme, // Theme loaded
theme_path: PathBuf, // Theme TOML Path
degraded: bool, // Fallback mode; won't work with file system
}
impl ThemeProvider {
/// ### new
///
/// Instantiates a new `ThemeProvider`
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
let default_theme: Theme = Theme::default();
info!(
"Setting up theme provider with thene path {} ",
theme_path.display(),
);
// Create provider
let mut provider: ThemeProvider = ThemeProvider {
theme: default_theme,
theme_path: theme_path.to_path_buf(),
degraded: false,
};
// If Config file doesn't exist, create it
if !theme_path.exists() {
if let Err(err) = provider.save() {
error!("Couldn't write theme file: {}", err);
return Err(err);
}
debug!("Theme file didn't exist; created file");
} else {
// otherwise Load configuration from file
if let Err(err) = provider.load() {
error!("Couldn't read thene file: {}", err);
return Err(err);
}
debug!("Read theme file");
}
Ok(provider)
}
/// ### degraded
///
/// Create a new theme provider which won't work with file system.
/// This is done in order to prevent a lot of `unwrap_or` on Ui
pub fn degraded() -> Self {
Self {
theme: Theme::default(),
theme_path: PathBuf::default(),
degraded: true,
}
}
// -- getters
/// ### theme
///
/// Returns theme as reference
pub fn theme(&self) -> &Theme {
&self.theme
}
/// ### theme_mut
///
/// Returns a mutable reference to the theme
pub fn theme_mut(&mut self) -> &mut Theme {
&mut self.theme
}
// -- io
/// ### load
///
/// Load theme from file
pub fn load(&mut self) -> Result<(), SerializerError> {
if self.degraded {
warn!("Configuration won't be loaded, since degraded; reloading default...");
self.theme = Theme::default();
return Err(SerializerError::new_ex(
SerializerErrorKind::Generic,
String::from("Can't access theme file"),
));
}
// Open theme file for read
debug!("Loading theme from file...");
match OpenOptions::new()
.read(true)
.open(self.theme_path.as_path())
{
Ok(reader) => {
// Deserialize
match deserialize(Box::new(reader)) {
Ok(theme) => {
self.theme = theme;
Ok(())
}
Err(err) => Err(err),
}
}
Err(err) => {
error!("Failed to read theme: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
}
/// ### save
///
/// Save theme to file
pub fn save(&self) -> Result<(), SerializerError> {
if self.degraded {
warn!("Configuration won't be saved, since in degraded mode");
return Err(SerializerError::new_ex(
SerializerErrorKind::Generic,
String::from("Can't access theme file"),
));
}
// Open file
debug!("Writing theme");
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.theme_path.as_path())
{
Ok(writer) => serialize(self.theme(), Box::new(writer)),
Err(err) => {
error!("Failed to write theme: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::Io,
err.to_string(),
))
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tuirealm::tui::style::Color;
#[test]
fn test_system_theme_provider_new() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let theme_path: PathBuf = get_theme_path(tmp_dir.path());
// Initialize a new bookmarks client
let mut provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
// Verify client
assert_eq!(provider.theme().auth_address, Color::Yellow);
assert_eq!(provider.theme_path, theme_path);
assert_eq!(provider.degraded, false);
// Mutation
provider.theme_mut().auth_address = Color::Green;
assert_eq!(provider.theme().auth_address, Color::Green);
}
#[test]
fn test_system_theme_provider_load_and_save() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let theme_path: PathBuf = get_theme_path(tmp_dir.path());
// Initialize a new bookmarks client
let mut provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
// Write
provider.theme_mut().auth_address = Color::Green;
assert!(provider.save().is_ok());
provider.theme_mut().auth_address = Color::Blue;
// Reload
assert!(provider.load().is_ok());
// Unchanged
assert_eq!(provider.theme().auth_address, Color::Green);
// Instantiate a new provider
let provider: ThemeProvider = ThemeProvider::new(theme_path.as_path()).unwrap();
assert_eq!(provider.theme().auth_address, Color::Green); // Unchanged
}
#[test]
fn test_system_theme_provider_degraded() {
let mut provider: ThemeProvider = ThemeProvider::degraded();
assert_eq!(provider.theme().auth_address, Color::Yellow);
assert_eq!(provider.degraded, true);
provider.theme_mut().auth_address = Color::Green;
assert!(provider.load().is_err());
assert_eq!(provider.theme().auth_address, Color::Yellow);
assert!(provider.save().is_err());
}
#[test]
fn test_system_theme_provider_err() {
assert!(ThemeProvider::new(Path::new("/tmp/oifoif/omar")).is_err());
}
/// ### get_theme_path
///
/// Get paths for theme file
fn get_theme_path(dir: &Path) -> PathBuf {
let mut p: PathBuf = PathBuf::from(dir);
p.push("theme.toml");
p
}
}

View File

@@ -0,0 +1,273 @@
//! ## AuthActivity
//!
//! `auth_activity` is the module which implements the authentication activity
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::environment;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder};
use tuirealm::{Payload, PropsBuilder, Value};
impl AuthActivity {
/// ### del_bookmark
///
/// Delete bookmark
pub(super) fn del_bookmark(&mut self, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Iterate over kyes
let name: Option<&String> = self.bookmarks_list.get(idx);
if let Some(name) = name {
bookmarks_cli.del_bookmark(name);
// Write bookmarks
self.write_bookmarks();
}
// Delete element from vec
self.bookmarks_list.remove(idx);
}
}
/// ### load_bookmark
///
/// Load selected bookmark (at index) to input fields
pub(super) fn load_bookmark(&mut self, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() {
// Iterate over bookmarks
if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
// Load parameters into components
self.load_bookmark_into_gui(
bookmark.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4,
);
}
}
}
}
/// ### save_bookmark
///
/// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
let (address, port, protocol, username, password) = self.get_input();
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Check if password must be saved
let password: Option<String> = match save_password {
true => match self
.view
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
Some(Payload::One(Value::Usize(0))) => Some(password), // Yes
_ => None, // No such component / No
},
false => None,
};
bookmarks_cli.add_bookmark(name.clone(), address, port, protocol, username, password);
// Save bookmarks
self.write_bookmarks();
// Remove `name` from bookmarks if exists
self.bookmarks_list.retain(|b| b.as_str() != name.as_str());
// Push bookmark to list and sort
self.bookmarks_list.push(name);
self.sort_bookmarks();
}
}
/// ### del_recent
///
/// Delete recent
pub(super) fn del_recent(&mut self, idx: usize) {
if let Some(client) = self.bookmarks_client.as_mut() {
let name: Option<&String> = self.recents_list.get(idx);
if let Some(name) = name {
client.del_recent(name);
// Write bookmarks
self.write_bookmarks();
}
// Delete element from vec
self.recents_list.remove(idx);
}
}
/// ### load_recent
///
/// Load selected recent (at index) to input fields
pub(super) fn load_recent(&mut self, idx: usize) {
if let Some(client) = self.bookmarks_client.as_ref() {
// Iterate over bookmarks
if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) {
// Load parameters
self.load_bookmark_into_gui(
bookmark.0, bookmark.1, bookmark.2, bookmark.3, None,
);
}
}
}
}
/// ### save_recent
///
/// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) {
let (address, port, protocol, username, _password) = self.get_input();
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
bookmarks_cli.add_recent(address, port, protocol, username);
// Save bookmarks
self.write_bookmarks();
}
}
/// ### write_bookmarks
///
/// Write bookmarks to file
fn write_bookmarks(&mut self) {
if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() {
if let Err(err) = bookmarks_cli.write_bookmarks() {
self.mount_error(format!("Could not write bookmarks: {}", err).as_str());
}
}
}
/// ### init_bookmarks_client
///
/// Initialize bookmarks client
pub(super) fn init_bookmarks_client(&mut self) {
// Get config dir
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
match BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
) {
Ok(cli) => {
// Load bookmarks into list
let mut bookmarks_list: Vec<String> =
Vec::with_capacity(cli.iter_bookmarks().count());
for bookmark in cli.iter_bookmarks() {
bookmarks_list.push(bookmark.clone());
}
// Load recents into list
let mut recents_list: Vec<String> =
Vec::with_capacity(cli.iter_recents().count());
for recent in cli.iter_recents() {
recents_list.push(recent.clone());
}
self.bookmarks_client = Some(cli);
self.bookmarks_list = bookmarks_list;
self.recents_list = recents_list;
// Sort bookmark list
self.sort_bookmarks();
self.sort_recents();
}
Err(err) => {
self.mount_error(
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
err
)
.as_str(),
);
}
}
}
}
Err(err) => {
self.mount_error(
format!("Could not initialize configuration directory: {}", err).as_str(),
);
}
}
}
// -- privates
/// ### sort_bookmarks
///
/// Sort bookmarks in list
fn sort_bookmarks(&mut self) {
// Conver to lowercase when sorting
self.bookmarks_list
.sort_by(|a, b| a.to_lowercase().as_str().cmp(b.to_lowercase().as_str()));
}
/// ### sort_recents
///
/// Sort recents in list
fn sort_recents(&mut self) {
// Reverse order
self.recents_list.sort_by(|a, b| b.cmp(a));
}
/// ### load_bookmark_into_gui
///
/// Load bookmark data into the gui components
fn load_bookmark_into_gui(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) {
// Load parameters into components
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props).with_value(addr).build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props).with_value(username).build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(password) = password {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props).with_value(password).build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
}
}
}

View File

@@ -0,0 +1,116 @@
//! ## AuthActivity
//!
//! `auth_activity` is the module which implements the authentication activity
/**
* 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 super::{AuthActivity, FileTransferParams, FileTransferProtocol};
impl AuthActivity {
/// ### protocol_opt_to_enum
///
/// Convert radio index for protocol into a `FileTransferProtocol`
pub(super) fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
match protocol {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
_ => FileTransferProtocol::Sftp,
}
}
/// ### protocol_enum_to_opt
///
/// Convert `FileTransferProtocol` enum into radio group index
pub(super) fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
match protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
}
}
/// ### get_default_port_for_protocol
///
/// Get the default port for protocol
pub(super) fn get_default_port_for_protocol(protocol: FileTransferProtocol) -> u16 {
match protocol {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21,
}
}
/// ### is_port_standard
///
/// Returns whether the port is standard or not
pub(super) fn is_port_standard(port: u16) -> bool {
port < 1024
}
/// ### check_minimum_window_size
///
/// Check minimum window size window
pub(super) fn check_minimum_window_size(&mut self, height: u16) {
if height < 25 {
// Mount window error
self.mount_size_err();
} else {
self.umount_size_err();
}
}
/// ### collect_host_params
///
/// Get input values from fields or return an error if fields are invalid
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (address, port, protocol, username, password): (
String,
u16,
FileTransferProtocol,
String,
String,
) = self.get_input();
if address.is_empty() {
return Err("Invalid host");
}
if port == 0 {
return Err("Invalid port");
}
Ok(FileTransferParams {
address,
port,
protocol,
username: match username.is_empty() {
true => None,
false => Some(username),
},
password: match password.is_empty() {
true => None,
false => Some(password),
},
entry_directory: None,
})
}
}

View File

@@ -0,0 +1,263 @@
//! ## AuthActivity
//!
//! `auth_activity` is the module which implements the authentication activity
/**
* 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.
*/
// Sub modules
mod bookmarks;
mod misc;
mod update;
mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient;
use crate::utils::git;
// Includes
use crossterm::event::Event;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
// -- components
const COMPONENT_TEXT_H1: &str = "TEXT_H1";
const COMPONENT_TEXT_H2: &str = "TEXT_H2";
const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR";
const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS";
const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
const COMPONENT_RADIO_BOOKMARK_DEL_RECENT: &str = "RADIO_DELETE_RECENT";
const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD";
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
// Store keys
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
/// ### AuthActivity
///
/// AuthActivity is the data holder for the authentication activity
pub struct AuthActivity {
exit_reason: Option<ExitReason>,
context: Option<Context>,
view: View,
bookmarks_client: Option<BookmarksClient>,
redraw: bool, // Should ui actually be redrawned?
bookmarks_list: Vec<String>, // List of bookmarks
recents_list: Vec<String>, // list of recents
}
impl Default for AuthActivity {
fn default() -> Self {
Self::new()
}
}
impl AuthActivity {
/// ### new
///
/// Instantiates a new AuthActivity
pub fn new() -> AuthActivity {
AuthActivity {
exit_reason: None,
context: None,
view: View::init(),
bookmarks_client: None,
redraw: true, // True at startup
bookmarks_list: Vec::new(),
recents_list: Vec::new(),
}
}
/// ### on_create
///
/// If enabled in configuration, check for updates from Github
fn check_for_updates(&mut self) {
debug!("Check for updates...");
// Check version only if unset in the store
let ctx: &mut Context = self.context_mut();
if !ctx.store().isset(STORE_KEY_LATEST_VERSION) {
debug!("Version is not set in storage");
if ctx.config().get_check_for_updates() {
debug!("Check for updates is enabled");
// Send request
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
Ok(Some(git::GithubTag { tag_name, body })) => {
// If some, store version and release notes
info!("Latest version is: {}", tag_name);
ctx.store_mut()
.set_string(STORE_KEY_LATEST_VERSION, tag_name);
ctx.store_mut().set_string(STORE_KEY_RELEASE_NOTES, body);
}
Ok(None) => {
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
// Just set flag as check
ctx.store_mut().set(STORE_KEY_LATEST_VERSION);
}
Err(err) => {
// Report error
error!("Failed to get latest version: {}", err);
self.mount_error(
format!("Could not check for new updates: {}", err).as_str(),
);
}
}
} else {
info!("Check for updates is disabled");
}
}
}
/// ### context
///
/// Returns a reference to context
fn context(&self) -> &Context {
self.context.as_ref().unwrap()
}
/// ### context_mut
///
/// Returns a mutable reference to context
fn context_mut(&mut self) -> &mut Context {
self.context.as_mut().unwrap()
}
/// ### theme
///
/// Returns a reference to theme
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
}
impl Activity for AuthActivity {
/// ### on_create
///
/// `on_create` is the function which must be called to initialize the activity.
/// `on_create` must initialize all the data structures used by the activity
/// Context is taken from activity manager and will be released only when activity is destroyed
fn on_create(&mut self, mut context: Context) {
debug!("Initializing activity");
// Initialize file transfer params
context.set_ftparams(FileTransferParams::default());
// Set context
self.context = Some(context);
// Clear terminal
self.context_mut().clear_screen();
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// If check for updates is enabled, check for updates
self.check_for_updates();
// Initialize view
self.init();
// Init bookmarks client
if self.bookmarks_client.is_none() {
self.init_bookmarks_client();
// View bookarmsk
self.view_bookmarks();
self.view_recent_connections();
}
// Verify error state from context
if let Some(err) = self.context_mut().error() {
self.mount_error(err.as_str());
}
info!("Activity initialized");
}
/// ### on_draw
///
/// `on_draw` is the function which draws the graphical interface.
/// This function must be called at each tick to refresh the interface
fn on_draw(&mut self) {
// Context must be something
if self.context.is_none() {
return;
}
// Read one event
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Set redraw to true
self.redraw = true;
// Handle on resize
if let Event::Resize(_, h) = event {
self.check_minimum_window_size(h);
}
// Handle event on view and update
let msg = self.view.on(event);
self.update(msg);
}
// Redraw if necessary
if self.redraw {
// View
self.view();
// Set redraw to false
self.redraw = false;
}
}
/// ### will_umount
///
/// `will_umount` is the method which must be able to report to the activity manager, whether
/// the activity should be terminated or not.
/// If not, the call will return `None`, otherwise return`Some(ExitReason)`
fn will_umount(&self) -> Option<&ExitReason> {
self.exit_reason.as_ref()
}
/// ### on_destroy
///
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
/// This function must be called once before terminating the activity.
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
}
}
}

View File

@@ -0,0 +1,376 @@
//! ## AuthActivity
//!
//! `auth_activity` is the module which implements the authentication activity
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{
AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
COMPONENT_TEXT_HELP, COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR,
};
use crate::ui::keymap::*;
use tui_realm_stdlib::InputPropsBuilder;
use tuirealm::{Msg, Payload, PropsBuilder, Update, Value};
// -- update
impl Update for AuthActivity {
/// ### update
///
/// Update auth activity model based on msg
/// The function exits when returns None
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None, // Exit after None
Some(msg) => match msg {
// Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Focus ( UP )
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
// Protocol - On Change
(COMPONENT_RADIO_PROTOCOL, Msg::OnChange(Payload::One(Value::Usize(protocol)))) => {
// If port is standard, update the current port with default for selected protocol
let protocol: FileTransferProtocol = Self::protocol_opt_to_enum(*protocol);
// Get port
let port: u16 = self.get_input_port();
match Self::is_port_standard(port) {
false => None, // Return None
true => {
self.update_input_port(Self::get_default_port_for_protocol(protocol))
}
}
}
// Bookmarks commands
// <RIGHT> / <LEFT>
(COMPONENT_BOOKMARKS_LIST, key) if key == &MSG_KEY_RIGHT => {
// Give focus to recents
self.view.active(COMPONENT_RECENTS_LIST);
None
}
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_LEFT => {
// Give focus to bookmarks
self.view.active(COMPONENT_BOOKMARKS_LIST);
None
}
// <DEL | 'E'>
(COMPONENT_BOOKMARKS_LIST, key)
if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E =>
{
// Show delete popup
self.mount_bookmark_del_dialog();
None
}
(COMPONENT_RECENTS_LIST, key) if key == &MSG_KEY_DEL || key == &MSG_KEY_CHAR_E => {
// Show delete popup
self.mount_recent_del_dialog();
None
}
// Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
// Bookmark radio
// Del bookmarks
(
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Msg::OnSubmit(Payload::One(Value::Usize(index))),
) => {
// hide bookmark delete
self.umount_bookmark_del_dialog();
// Index must be 0 => YES
match *index {
0 => {
// Get selected bookmark
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
Some(Payload::One(Value::Usize(index))) => {
// Delete bookmark
self.del_bookmark(index);
// Update bookmarks
self.view_bookmarks()
}
_ => None,
}
}
_ => None,
}
}
(
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Msg::OnSubmit(Payload::One(Value::Usize(index))),
) => {
// hide bookmark delete
self.umount_recent_del_dialog();
// Index must be 0 => YES
match *index {
0 => {
// Get selected bookmark
match self.view.get_state(COMPONENT_RECENTS_LIST) {
Some(Payload::One(Value::Usize(index))) => {
// Delete recent
self.del_recent(index);
// Update bookmarks
self.view_recent_connections()
}
_ => None,
}
}
_ => None,
}
}
// <ESC> hide tab
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, key) if key == &MSG_KEY_ESC => {
self.umount_recent_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, _) => None,
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, key) if key == &MSG_KEY_ESC => {
self.umount_bookmark_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, _) => None,
// Error message
(COMPONENT_TEXT_ERROR, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Umount text error
self.umount_error();
None
}
(COMPONENT_TEXT_ERROR, _) => None,
(COMPONENT_TEXT_NEW_VERSION_NOTES, key)
if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER =>
{
// Umount release notes
self.umount_release_notes();
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None,
// Help
(_, key) if key == &MSG_KEY_CTRL_H => {
// Show help
self.mount_help();
None
}
// Release notes
(_, key) if key == &MSG_KEY_CTRL_R => {
// Show release notes
self.mount_release_notes();
None
}
(COMPONENT_TEXT_HELP, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Hide text help
self.umount_help();
None
}
(COMPONENT_TEXT_HELP, _) => None,
// Enter setup
(_, key) if key == &MSG_KEY_CTRL_C => {
self.exit_reason = Some(super::ExitReason::EnterSetup);
None
}
// Save bookmark; show popup
(_, key) if key == &MSG_KEY_CTRL_S => {
// Show popup
self.mount_bookmark_save_dialog();
// Give focus to bookmark name
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
None
}
(COMPONENT_INPUT_BOOKMARK_NAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to pwd
self.view.active(COMPONENT_RADIO_BOOKMARK_SAVE_PWD);
None
}
(COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key) if key == &MSG_KEY_UP => {
// Give focus to pwd
self.view.active(COMPONENT_INPUT_BOOKMARK_NAME);
None
}
// Save bookmark
(COMPONENT_INPUT_BOOKMARK_NAME, Msg::OnSubmit(_))
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
// Get values
let bookmark_name: String =
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
Some(Payload::One(Value::Str(s))) => s,
_ => String::new(),
};
let save_pwd: bool = matches!(
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
Some(Payload::One(Value::Usize(0)))
);
// Save bookmark
if !bookmark_name.is_empty() {
self.save_bookmark(bookmark_name, save_pwd);
}
// Umount popup
self.umount_bookmark_save_dialog();
// Reload bookmarks
self.view_bookmarks()
}
// Hide save bookmark
(COMPONENT_INPUT_BOOKMARK_NAME, key) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, key)
if key == &MSG_KEY_ESC =>
{
// Umount popup
self.umount_bookmark_save_dialog();
None
}
(COMPONENT_INPUT_BOOKMARK_NAME, _) | (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, _) => None,
// Quit dialog
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(choice)))) => {
// If choice is 0, quit termscp
if *choice == 0 {
self.exit_reason = Some(super::ExitReason::Quit);
}
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, key) if key == &MSG_KEY_ESC => {
self.umount_quit();
None
}
// -- text size error; block everything
(COMPONENT_TEXT_SIZE_ERR, _) => None,
// <TAB> bookmarks
(COMPONENT_BOOKMARKS_LIST, key) | (COMPONENT_RECENTS_LIST, key)
if key == &MSG_KEY_TAB =>
{
// Give focus to address
self.view.active(COMPONENT_INPUT_ADDR);
None
}
// Any <TAB>, go to bookmarks
(_, key) if key == &MSG_KEY_TAB => {
self.view.active(COMPONENT_BOOKMARKS_LIST);
None
}
// On submit on any unhandled (connect)
(_, Msg::OnSubmit(_)) => self.on_unhandled_submit(),
(_, key) if key == &MSG_KEY_ENTER => self.on_unhandled_submit(),
// <ESC> => Quit
(_, key) if key == &MSG_KEY_ESC => {
self.mount_quit();
None
}
(_, _) => None, // Ignore other events
},
}
}
}
impl AuthActivity {
fn update_input_port(&mut self, port: u16) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_INPUT_PORT) {
None => None,
Some(props) => {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.build();
self.view.update(COMPONENT_INPUT_PORT, props)
}
}
}
fn on_unhandled_submit(&mut self) -> Option<(String, Msg)> {
// Validate fields
match self.collect_host_params() {
Err(err) => {
// mount error
self.mount_error(err);
}
Ok(params) => {
self.save_recent();
// Set file transfer params to context
self.context_mut().set_ftparams(params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
}
}
// Return None
None
}
}

View File

@@ -0,0 +1,795 @@
//! ## AuthActivity
//!
//! `auth_activity` is the module which implements the authentication activity
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, Context, FileTransferProtocol};
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
use tui_realm_stdlib::{
input::{Input, InputPropsBuilder},
label::{Label, LabelPropsBuilder},
list::{List, ListPropsBuilder},
paragraph::{Paragraph, ParagraphPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
textarea::{Textarea, TextareaPropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan},
Msg, Payload, Value,
};
impl AuthActivity {
/// ### init
///
/// Initialize view, mounting all startup components inside the view
pub(super) fn init(&mut self) {
let key_color = self.theme().misc_keys;
let addr_color = self.theme().auth_address;
let protocol_color = self.theme().auth_protocol;
let port_color = self.theme().auth_port;
let username_color = self.theme().auth_username;
let password_color = self.theme().auth_password;
let bookmarks_color = self.theme().auth_bookmarks;
let recents_color = self.theme().auth_recents;
// Headers
self.view.mount(
super::COMPONENT_TEXT_H1,
Box::new(Label::new(
LabelPropsBuilder::default()
.bold()
.italic()
.with_text(String::from("$ termscp"))
.build(),
)),
);
self.view.mount(
super::COMPONENT_TEXT_H2,
Box::new(Label::new(
LabelPropsBuilder::default()
.bold()
.italic()
.with_text(format!("$ version {}", env!("CARGO_PKG_VERSION")))
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpan::new("Press ").bold(),
TextSpan::new("<CTRL+H>").bold().fg(key_color),
TextSpan::new(" to show keybindings; ").bold(),
TextSpan::new("<CTRL+C>").bold().fg(key_color),
TextSpan::new(" to enter setup").bold(),
])
.build(),
)),
);
// Get default protocol
let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol();
// Protocol
self.view.mount(
super::COMPONENT_RADIO_PROTOCOL,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(protocol_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
.with_title("Protocol", Alignment::Left)
.with_options(&["SFTP", "SCP", "FTP", "FTPS"])
.with_value(Self::protocol_enum_to_opt(default_protocol))
.rewind(true)
.build(),
)),
);
// Address
self.view.mount(
super::COMPONENT_INPUT_ADDR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(addr_color)
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
.with_label("Remote host", Alignment::Left)
.build(),
)),
);
// Port
self.view.mount(
super::COMPONENT_INPUT_PORT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(port_color)
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
.with_label("Port number", Alignment::Left)
.with_input(InputType::Number)
.with_input_len(5)
.with_value(Self::get_default_port_for_protocol(default_protocol).to_string())
.build(),
)),
);
// Username
self.view.mount(
super::COMPONENT_INPUT_USERNAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(username_color)
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
.with_label("Username", Alignment::Left)
.build(),
)),
);
// Password
self.view.mount(
super::COMPONENT_INPUT_PASSWORD,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(password_color)
.with_borders(Borders::ALL, BorderType::Rounded, password_color)
.with_label("Password", Alignment::Left)
.with_input(InputType::Password)
.build(),
)),
);
// Version notice
if let Some(version) = self
.context()
.store()
.get_string(super::STORE_KEY_LATEST_VERSION)
{
let version: String = version.to_string();
self.view.mount(
super::COMPONENT_TEXT_NEW_VERSION,
Box::new(Span::new(
SpanPropsBuilder::default()
.with_foreground(Color::Yellow)
.with_spans(vec![
TextSpan::from("termscp "),
TextSpan::new(version.as_str()).underlined().bold(),
TextSpan::from(" is NOW available! Get it from <https://veeso.github.io/termscp/>; view release notes with <CTRL+R>"),
])
.build(),
)),
);
}
// Bookmarks
self.view.mount(
super::COMPONENT_BOOKMARKS_LIST,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_background(bookmarks_color)
.with_foreground(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, bookmarks_color)
.with_title("Bookmarks", Alignment::Left)
.build(),
)),
);
// Recents
self.view.mount(
super::COMPONENT_RECENTS_LIST,
Box::new(BookmarkList::new(
BookmarkListPropsBuilder::default()
.with_background(recents_color)
.with_foreground(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, recents_color)
.with_title("Recent connections", Alignment::Left)
.build(),
)),
);
// Load bookmarks
let _ = self.view_bookmarks();
let _ = self.view_recent_connections();
// Active protocol
self.view.active(super::COMPONENT_RADIO_PROTOCOL);
}
/// ### view
///
/// Display view on canvas
pub(super) fn view(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal().draw(|f| {
// Check window size
let height: u16 = f.size().height;
self.check_minimum_window_size(height);
// Prepare chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(21), // Auth Form
Constraint::Min(3), // Bookmarks
]
.as_ref(),
)
.split(f.size());
// Create explorer chunks
let auth_chunks = Layout::default()
.constraints(
[
Constraint::Length(1), // h1
Constraint::Length(1), // h2
Constraint::Length(1), // Version
Constraint::Length(3), // protocol
Constraint::Length(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // footer
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(chunks[0]);
// Create bookmark chunks
let bookmark_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.split(chunks[1]);
// Render
// Auth chunks
self.view
.render(super::COMPONENT_TEXT_H1, f, auth_chunks[0]);
self.view
.render(super::COMPONENT_TEXT_H2, f, auth_chunks[1]);
self.view
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]);
self.view
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[6]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[7]);
self.view
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[8]);
// Bookmark chunks
self.view
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
self.view
.render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_SIZE_ERR) {
if props.visible {
let popup = draw_area_in(f.size(), 80, 20);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_SIZE_ERR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
{
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, f, popup);
}
}
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
{
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_NEW_VERSION_NOTES) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 90, 90);
f.render_widget(Clear, popup);
self.view
.render(super::COMPONENT_TEXT_NEW_VERSION_NOTES, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 20, 20);
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Input form
Constraint::Length(2), // Yes/No
]
.as_ref(),
)
.split(popup);
self.view
.render(super::COMPONENT_INPUT_BOOKMARK_NAME, f, popup_chunks[0]);
self.view
.render(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD, f, popup_chunks[1]);
}
}
});
self.context = Some(ctx);
}
// -- partials
/// ### view_bookmarks
///
/// Make text span from bookmarks
pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> {
let bookmarks: Vec<String> = self
.bookmarks_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String, _) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap();
format!(
"{} ({}://{}@{}:{})",
x,
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
)
})
.collect();
match self.view.get_props(super::COMPONENT_BOOKMARKS_LIST) {
None => None,
Some(props) => {
let msg = self.view.update(
super::COMPONENT_BOOKMARKS_LIST,
BookmarkListPropsBuilder::from(props)
.with_bookmarks(bookmarks)
.build(),
);
msg
}
}
}
/// ### view_recent_connections
///
/// View recent connections
pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> {
let bookmarks: Vec<String> = self
.recents_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap();
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
)
})
.collect();
match self.view.get_props(super::COMPONENT_RECENTS_LIST) {
None => None,
Some(props) => {
let msg = self.view.update(
super::COMPONENT_RECENTS_LIST,
BookmarkListPropsBuilder::from(props)
.with_bookmarks(bookmarks)
.build(),
);
msg
}
}
}
// -- mount
/// ### mount_error
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
let err_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(err_color)
.with_borders(Borders::ALL, BorderType::Thick, err_color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_ERROR);
}
/// ### umount_error
///
/// Umount error message
pub(super) fn umount_error(&mut self) {
self.view.umount(super::COMPONENT_TEXT_ERROR);
}
/// ### mount_size_err
///
/// Mount size error
pub(super) fn mount_size_err(&mut self) {
// Mount
let err_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_SIZE_ERR,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(err_color)
.with_borders(Borders::ALL, BorderType::Thick, err_color)
.bold()
.with_texts(vec![TextSpan::from(
"termscp requires at least 24 lines of height to run",
)])
.with_text_alignment(Alignment::Center)
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_SIZE_ERR);
}
/// ### umount_size_err
///
/// Umount error size error
pub(super) fn umount_size_err(&mut self) {
self.view.umount(super::COMPONENT_TEXT_SIZE_ERR);
}
/// ### mount_quit
///
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(quit_color)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_inverted_color(Color::Black)
.with_title("Quit termscp?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
);
self.view.active(super::COMPONENT_RADIO_QUIT);
}
/// ### umount_quit
///
/// Umount quit popup
pub(super) fn umount_quit(&mut self) {
self.view.umount(super::COMPONENT_RADIO_QUIT);
}
/// ### mount_bookmark_del_dialog
///
/// Mount bookmark delete dialog
pub(super) fn mount_bookmark_del_dialog(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.with_title("Delete bookmark?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(1)
.rewind(true)
.build(),
)),
);
// Active
self.view
.active(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK);
}
/// ### umount_bookmark_del_dialog
///
/// umount delete bookmark dialog
pub(super) fn umount_bookmark_del_dialog(&mut self) {
self.view
.umount(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK);
}
/// ### mount_bookmark_del_dialog
///
/// Mount recent delete dialog
pub(super) fn mount_recent_del_dialog(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.with_title("Delete bookmark?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(1)
.rewind(true)
.build(),
)),
);
// Active
self.view.active(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT);
}
/// ### umount_recent_del_dialog
///
/// umount delete recent dialog
pub(super) fn umount_recent_del_dialog(&mut self) {
self.view.umount(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT);
}
/// ### mount_bookmark_save_dialog
///
/// Mount bookmark save dialog
pub(super) fn mount_bookmark_save_dialog(&mut self) {
let save_color = self.theme().misc_save_dialog;
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
super::COMPONENT_INPUT_BOOKMARK_NAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(save_color)
.with_label("Save bookmark as…", Alignment::Center)
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
BorderType::Rounded,
Color::Reset,
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Rounded,
Color::Reset,
)
.with_title("Save password?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
);
// Give focus to input bookmark name
self.view.active(super::COMPONENT_INPUT_BOOKMARK_NAME);
}
/// ### umount_bookmark_save_dialog
///
/// Umount bookmark save dialog
pub(super) fn umount_bookmark_save_dialog(&mut self) {
self.view.umount(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD);
self.view.umount(super::COMPONENT_INPUT_BOOKMARK_NAME);
}
/// ### mount_help
///
/// Mount help
pub(super) fn mount_help(&mut self) {
let key_color = self.theme().misc_keys;
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(List::new(
ListPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.scrollable(true)
.bold()
.with_title("Help", Alignment::Center)
.with_rows(
TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
.add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(key_color))
.add_col(TextSpan::from(" Switch from form and bookmarks"))
.add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(key_color))
.add_col(TextSpan::from(" Switch bookmark tab"))
.add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(key_color))
.add_col(TextSpan::from(" Move up/down in current tab"))
.add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(key_color))
.add_col(TextSpan::from(" Connect/Load bookmark"))
.add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected bookmark"))
.add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Enter setup"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(key_color))
.add_col(TextSpan::from(" Save bookmark"))
.build(),
)
.build(),
)),
);
// Active help
self.view.active(super::COMPONENT_TEXT_HELP);
}
/// ### umount_help
///
/// Umount help
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
}
/// ### mount_release_notes
///
/// mount release notes text area
pub(super) fn mount_release_notes(&mut self) {
if let Some(ctx) = self.context.as_ref() {
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) {
// make spans
let spans: Vec<TextSpan> = release_notes.lines().map(TextSpan::from).collect();
self.view.mount(
super::COMPONENT_TEXT_NEW_VERSION_NOTES,
Box::new(Textarea::new(
TextareaPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_title("Release notes", Alignment::Center)
.with_texts(spans)
.build(),
)),
);
self.view.active(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
}
}
}
/// ### umount_release_notes
///
/// Umount release notes text area
pub(super) fn umount_release_notes(&mut self) {
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
}
/// ### get_input
///
/// Collect input values from view
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
let addr: String = self.get_input_addr();
let port: u16 = self.get_input_port();
let protocol: FileTransferProtocol = self.get_input_protocol();
let username: String = self.get_input_username();
let password: String = self.get_input_password();
(addr, port, protocol, username, password)
}
pub(super) fn get_input_addr(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_ADDR) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_port(&self) -> u16 {
match self.view.get_state(super::COMPONENT_INPUT_PORT) {
Some(Payload::One(Value::Usize(x))) => match x > 65535 {
true => 0,
false => x as u16,
},
_ => 0,
}
}
pub(super) fn get_input_protocol(&self) -> FileTransferProtocol {
match self.view.get_state(super::COMPONENT_RADIO_PROTOCOL) {
Some(Payload::One(Value::Usize(x))) => Self::protocol_opt_to_enum(x),
_ => FileTransferProtocol::Sftp,
}
}
pub(super) fn get_input_username(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_USERNAME) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_password(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_PASSWORD) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
}

View File

@@ -1,548 +0,0 @@
//! ## AuthActivity
//!
//! `auth_activity` is the module which implements the authentication activity
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Dependencies
extern crate crossterm;
extern crate tui;
extern crate unicode_width;
// locals
use super::{Activity, Context};
use crate::filetransfer::FileTransferProtocol;
// Includes
use crossterm::event::Event as InputEvent;
use crossterm::event::KeyCode;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, Clear, Paragraph, Tabs},
};
use unicode_width::UnicodeWidthStr;
/// ### InputField
///
/// InputField describes the current input field to edit
#[derive(std::cmp::PartialEq)]
enum InputField {
Address,
Port,
Protocol,
Username,
Password,
}
/// ### InputMode
///
/// InputMode describes the current input mode
/// Each input mode handle the input events in a different way
#[derive(std::cmp::PartialEq)]
enum InputMode {
Text,
Popup,
}
/// ### AuthActivity
///
/// AuthActivity is the data holder for the authentication activity
pub struct AuthActivity {
pub address: String,
pub port: String,
pub protocol: FileTransferProtocol,
pub username: String,
pub password: String,
pub submit: bool, // becomes true after user has submitted fields
pub quit: bool, // Becomes true if user has pressed esc
context: Option<Context>,
selected_field: InputField,
input_mode: InputMode,
popup_message: Option<String>,
password_placeholder: String,
redraw: bool, // Should ui actually be redrawned?
}
impl AuthActivity {
/// ### new
///
/// Instantiates a new AuthActivity
pub fn new() -> AuthActivity {
AuthActivity {
address: String::new(),
port: String::from("22"),
protocol: FileTransferProtocol::Sftp,
username: String::new(),
password: String::new(),
submit: false,
quit: false,
context: None,
selected_field: InputField::Address,
input_mode: InputMode::Text,
popup_message: None,
password_placeholder: String::new(),
redraw: true, // True at startup
}
}
/// ### set_input_mode
///
/// Update input mode based on current parameters
fn select_input_mode(&mut self) -> InputMode {
if self.popup_message.is_some() {
return InputMode::Popup;
}
// Default to text
InputMode::Text
}
/// ### handle_input_event
///
/// Handle input event, based on current input mode
fn handle_input_event(&mut self, ev: &InputEvent) {
match self.input_mode {
InputMode::Text => self.handle_input_event_mode_text(ev),
InputMode::Popup => self.handle_input_event_mode_popup(ev),
}
}
/// ### handle_input_event_mode_text
///
/// Handler for input event when in textmode
fn handle_input_event_mode_text(&mut self, ev: &InputEvent) {
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Esc => {
self.quit = true;
}
KeyCode::Enter => {
// Handle submit
// Check form
// Check address
if self.address.len() == 0 {
self.popup_message = Some(String::from("Invalid address"));
return;
}
// Check port
// Convert port to number
match self.port.parse::<usize>() {
Ok(val) => {
if val > 65535 {
self.popup_message = Some(String::from(
"Specified port must be in range 0-65535",
));
return;
}
}
Err(_) => {
self.popup_message =
Some(String::from("Specified port is not a number"));
return;
}
}
// Check username
//if self.username.len() == 0 {
// self.popup_message = Some(String::from("Invalid username"));
// return;
//}
// Everything OK, set enter
self.submit = true;
self.popup_message =
Some(format!("Connecting to {}:{}...", self.address, self.port));
}
KeyCode::Backspace => {
// Pop last char
match self.selected_field {
InputField::Address => {
let _ = self.address.pop();
}
InputField::Password => {
let _ = self.password.pop();
}
InputField::Username => {
let _ = self.username.pop();
}
InputField::Port => {
let _ = self.port.pop();
}
_ => { /* Nothing to do */ }
};
}
KeyCode::Up => {
// Move item up
self.selected_field = match self.selected_field {
InputField::Address => InputField::Password, // End of list (wrap)
InputField::Port => InputField::Address,
InputField::Protocol => InputField::Port,
InputField::Username => InputField::Protocol,
InputField::Password => InputField::Username,
}
}
KeyCode::Down | KeyCode::Tab => {
// Move item down
self.selected_field = match self.selected_field {
InputField::Address => InputField::Port,
InputField::Port => InputField::Protocol,
InputField::Protocol => InputField::Username,
InputField::Username => InputField::Password,
InputField::Password => InputField::Address, // End of list (wrap)
}
}
KeyCode::Char(ch) => {
match self.selected_field {
InputField::Address => self.address.push(ch),
InputField::Password => self.password.push(ch),
InputField::Username => self.username.push(ch),
InputField::Port => {
// Value must be numeric
if ch.is_numeric() {
self.port.push(ch);
}
}
_ => { /* Nothing to do */ }
}
}
KeyCode::Left => {
// If current field is Protocol handle event... (move element left)
if self.selected_field == InputField::Protocol {
self.protocol = match self.protocol {
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
FileTransferProtocol::Ftp(ftps) => match ftps {
false => FileTransferProtocol::Scp,
true => FileTransferProtocol::Ftp(false),
},
};
}
}
KeyCode::Right => {
// If current field is Protocol handle event... ( move element right )
if self.selected_field == InputField::Protocol {
self.protocol = match self.protocol {
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
FileTransferProtocol::Ftp(ftps) => match ftps {
false => FileTransferProtocol::Ftp(true),
true => FileTransferProtocol::Sftp, // End of list (wrap)
},
};
}
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_text
///
/// Handler for input event when in popup mode
fn handle_input_event_mode_popup(&mut self, ev: &InputEvent) {
// Only enter should be allowed here
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter => {
self.popup_message = None; // Hide popup
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### draw_remote_address
///
/// Draw remote address block
fn draw_remote_address(&self) -> Paragraph {
Paragraph::new(self.address.as_ref())
.style(match self.selected_field {
InputField::Address => Style::default().fg(Color::Yellow),
_ => Style::default(),
})
.block(
Block::default()
.borders(Borders::ALL)
.title("Remote address"),
)
}
/// ### draw_remote_port
///
/// Draw remote port block
fn draw_remote_port(&self) -> Paragraph {
Paragraph::new(self.port.as_ref())
.style(match self.selected_field {
InputField::Port => Style::default().fg(Color::Cyan),
_ => Style::default(),
})
.block(Block::default().borders(Borders::ALL).title("Remote port"))
}
/// ### draw_protocol_select
///
/// Draw protocol select
fn draw_protocol_select(&self) -> Tabs {
let protocols: Vec<Spans> = vec![
Spans::from("SFTP"),
Spans::from("SCP"),
Spans::from("FTP"),
Spans::from("FTPS"),
];
let index: usize = match self.protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(ftps) => match ftps {
false => 2,
true => 3,
},
};
Tabs::new(protocols)
.block(Block::default().borders(Borders::ALL).title("Protocol"))
.select(index)
.style(match self.selected_field {
InputField::Protocol => Style::default().fg(Color::Green),
_ => Style::default(),
})
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Green)
.fg(Color::Black),
)
}
/// ### draw_protocol_username
///
/// Draw username block
fn draw_protocol_username(&self) -> Paragraph {
Paragraph::new(self.username.as_ref())
.style(match self.selected_field {
InputField::Username => Style::default().fg(Color::Magenta),
_ => Style::default(),
})
.block(Block::default().borders(Borders::ALL).title("Username"))
}
/// ### draw_protocol_password
///
/// Draw password block
fn draw_protocol_password(&mut self) -> Paragraph {
// Create password secret
self.password_placeholder = (0..self.password.width()).map(|_| "*").collect::<String>();
Paragraph::new(self.password_placeholder.as_ref())
.style(match self.selected_field {
InputField::Password => Style::default().fg(Color::LightBlue),
_ => Style::default(),
})
.block(Block::default().borders(Borders::ALL).title("Password"))
}
/// ### draw_header
///
/// Draw header
fn draw_header(&self) -> Paragraph {
Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
}
/// ### draw_footer
///
/// Draw authentication page footer
fn draw_footer(&self) -> Paragraph {
// Write header
let (footer, h_style) = (
vec![
Span::raw("Press "),
Span::styled("<ESC>", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("<UP,DOWN>", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to change input field, "),
Span::styled("<ENTER>", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to submit form"),
],
Style::default().add_modifier(Modifier::BOLD),
);
let mut footer_text = Text::from(Spans::from(footer));
footer_text.patch_style(h_style);
Paragraph::new(footer_text)
}
/// ### draw_popup
///
/// Draw popup block
fn draw_popup(&self, r: Rect) -> (Paragraph, Rect) {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((80) / 2),
Constraint::Percentage(20),
Constraint::Percentage((80) / 2),
]
.as_ref(),
)
.split(r);
let area: Rect = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((80) / 2),
Constraint::Percentage(20),
Constraint::Percentage((80) / 2),
]
.as_ref(),
)
.split(popup_layout[1])[1];
let popup: Paragraph = Paragraph::new(self.popup_message.as_ref().unwrap().as_ref())
.style(Style::default().fg(Color::Red))
.block(Block::default().borders(Borders::ALL).title("Alert"));
(popup, area)
}
}
impl Activity for AuthActivity {
/// ### on_create
///
/// `on_create` is the function which must be called to initialize the activity.
/// `on_create` must initialize all the data structures used by the activity
/// Context is taken from activity manager and will be released only when activity is destroyed
fn on_create(&mut self, context: Context) {
// Set context
self.context = Some(context);
// Clear terminal
let _ = self.context.as_mut().unwrap().terminal.clear();
// Put raw mode on enabled
let _ = enable_raw_mode();
}
/// ### on_draw
///
/// `on_draw` is the function which draws the graphical interface.
/// This function must be called at each tick to refresh the interface
fn on_draw(&mut self) {
// Context must be something
if self.context.is_none() {
return;
}
// Start catching Input Events
if let Ok(input_events) = self.context.as_ref().unwrap().input_hnd.fetch_events() {
if input_events.len() > 0 {
self.redraw = true; // Set redraw to true if there is at least one event
}
// Iterate over input events
for event in input_events.iter() {
self.handle_input_event(event);
}
}
// Redraw if necessary
if self.redraw {
// Determine input mode
self.input_mode = self.select_input_mode();
// draw interface
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(5),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
]
.as_ref(),
)
.split(f.size());
// Draw header
f.render_widget(self.draw_header(), chunks[0]);
// Draw input fields
f.render_widget(self.draw_remote_address(), chunks[1]);
f.render_widget(self.draw_remote_port(), chunks[2]);
f.render_widget(self.draw_protocol_select(), chunks[3]);
f.render_widget(self.draw_protocol_username(), chunks[4]);
f.render_widget(self.draw_protocol_password(), chunks[5]);
// Draw footer
f.render_widget(self.draw_footer(), chunks[6]);
if self.popup_message.is_some() {
let (popup, popup_area): (Paragraph, Rect) = self.draw_popup(f.size());
f.render_widget(Clear, popup_area); //this clears out the background
f.render_widget(popup, popup_area);
}
// Set cursor
match self.selected_field {
InputField::Address => f.set_cursor(
chunks[1].x + self.address.width() as u16 + 1,
chunks[1].y + 1,
),
InputField::Port => {
f.set_cursor(chunks[2].x + self.port.width() as u16 + 1, chunks[2].y + 1)
}
InputField::Username => f.set_cursor(
chunks[4].x + self.username.width() as u16 + 1,
chunks[4].y + 1,
),
InputField::Password => f.set_cursor(
chunks[5].x + self.password_placeholder.width() as u16 + 1,
chunks[5].y + 1,
),
_ => {}
}
});
// Reset ctx
self.context = Some(ctx);
// Set redraw to false
self.redraw = false;
}
}
/// ### on_destroy
///
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
/// This function must be called once before terminating the activity.
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
let _ = disable_raw_mode();
if self.context.is_none() {
return None;
}
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
let _ = ctx.terminal.clear();
Some(ctx)
}
None => None,
}
}
}

View File

@@ -0,0 +1,184 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry};
use std::path::PathBuf;
impl FileTransferActivity {
/// ### action_enter_local_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(crate) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
}
}
/// ### action_enter_remote_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(crate) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
}
}
/// ### action_change_local_dir
///
/// Change local directory reading value from input
pub(crate) fn action_change_local_dir(&mut self, input: String, block_sync: bool) {
let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.local_changedir(dir_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(input, true);
}
}
/// ### action_change_remote_dir
///
/// Change remote directory reading value from input
pub(crate) fn action_change_remote_dir(&mut self, input: String, block_sync: bool) {
let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.remote_changedir(dir_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(input, true);
}
}
/// ### action_go_to_previous_local_dir
///
/// Go to previous directory from localhost
pub(crate) fn action_go_to_previous_local_dir(&mut self, block_sync: bool) {
if let Some(d) = self.local_mut().popd() {
self.local_changedir(d.as_path(), false);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_go_to_previous_remote_dir(true);
}
}
}
/// ### action_go_to_previous_remote_dir
///
/// Go to previous directory from remote host
pub(crate) fn action_go_to_previous_remote_dir(&mut self, block_sync: bool) {
if let Some(d) = self.remote_mut().popd() {
self.remote_changedir(d.as_path(), false);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_go_to_previous_local_dir(true);
}
}
}
/// ### action_go_to_local_upper_dir
///
/// Go to upper directory on local host
pub(crate) fn action_go_to_local_upper_dir(&mut self, block_sync: bool) {
// Get pwd
let path: PathBuf = self.local().wrkdir.clone();
// Go to parent directory
if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true);
// If sync is enabled update remote too
if self.browser.sync_browsing && !block_sync {
self.action_go_to_remote_upper_dir(true);
}
}
}
/// #### action_go_to_remote_upper_dir
///
/// Go to upper directory on remote host
pub(crate) fn action_go_to_remote_upper_dir(&mut self, block_sync: bool) {
// Get pwd
let path: PathBuf = self.remote().wrkdir.clone();
// Go to parent directory
if let Some(parent) = path.as_path().parent() {
self.remote_changedir(parent, true);
// If sync is enabled update local too
if self.browser.sync_browsing && !block_sync {
self.action_go_to_local_upper_dir(true);
}
}
}
}

View File

@@ -0,0 +1,263 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::filetransfer::FileTransferErrorType;
use crate::fs::FsFile;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
/// ### action_local_copy
///
/// Copy file on local
pub(crate) fn action_local_copy(&mut self, input: String) {
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.local_copy_file(&entry, dest_path.as_path());
// Reload entries
self.reload_local_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name());
self.local_copy_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
}
/// ### action_remote_copy
///
/// Copy file on remote
pub(crate) fn action_remote_copy(&mut self, input: String) {
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_copy_file(entry, dest_path.as_path());
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.into_iter() {
let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name());
self.remote_copy_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::None => {}
}
}
fn local_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
match self.host.copy(entry, dest) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
}
}
fn remote_copy_file(&mut self, entry: FsEntry, dest: &Path) {
match self.client.as_mut().copy(&entry, dest) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
);
}
Err(err) => match err.kind() {
FileTransferErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy
self.tricky_copy(entry, dest);
}
_ => self.log_and_alert(
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
},
}
}
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) {
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
self.umount_wait();
// match entry
match entry {
FsEntry::File(entry) => {
// Create tempfile
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err),
);
return;
}
};
// Download file
let name = entry.name.clone();
let entry_path = entry.abs_path.clone();
if let Err(err) =
self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name))
{
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err),
);
return;
}
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
Ok(e) => e.unwrap_file(),
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not stat \"{}\": {}",
tmpfile.path().display(),
err
),
);
return;
}
};
// Upload file to destination
let wrkdir = self.remote().wrkdir.clone();
if let Err(err) = self.filetransfer_send(
TransferPayload::File(tmpfile_entry),
wrkdir.as_path(),
Some(String::from(dest.to_string_lossy())),
) {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not write file {}: {}",
entry_path.display(),
err
),
);
return;
}
}
FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(d) => d,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err),
);
return;
}
};
// Get path of dest
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
tempdir_path.push(entry.get_name());
// Download file
if let Err(err) =
self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None)
{
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: failed to download file: {}", err),
);
return;
}
// Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
Ok(e) => e,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not stat \"{}\": {}",
tempdir.path().display(),
err
),
);
return;
}
};
// Upload to destination
let wrkdir: PathBuf = self.remote().wrkdir.clone();
if let Err(err) = self.filetransfer_send(
TransferPayload::Any(tempdir_entry),
wrkdir.as_path(),
Some(String::from(dest.to_string_lossy())),
) {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: failed to send file: {}", err),
);
return;
}
}
}
}
}

View File

@@ -0,0 +1,116 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
impl FileTransferActivity {
pub(crate) fn action_local_delete(&mut self) {
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
// Delete file
self.local_remove_file(&entry);
// Reload
self.reload_local_dir();
}
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// Delete file
self.local_remove_file(entry);
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
}
pub(crate) fn action_remote_delete(&mut self) {
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
// Delete file
self.remote_remove_file(&entry);
// Reload
self.reload_remote_dir();
}
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// Delete file
self.remote_remove_file(entry);
}
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::None => {}
}
}
pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) {
match self.host.remove(entry) {
Ok(_) => {
// Log
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", entry.get_abs_path().display()),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
entry.get_abs_path().display(),
err
),
);
}
}
}
pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) {
match self.client.remove(entry) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", entry.get_abs_path().display()),
);
}
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
entry.get_abs_path().display(),
err
),
);
}
}
}
}

View File

@@ -0,0 +1,232 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::fs::FsFile;
// ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::fs::OpenOptions;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
impl FileTransferActivity {
pub(crate) fn action_edit_local_file(&mut self) {
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
// Edit all entries
for entry in entries.iter() {
// Check if file
if entry.is_file() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"", entry.get_abs_path().display()),
);
// Edit file
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) {
self.log_and_alert(LogLevel::Error, err);
}
}
}
// Reload entries
self.reload_local_dir();
}
pub(crate) fn action_edit_remote_file(&mut self) {
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
// Edit all entries
for entry in entries.into_iter() {
// Check if file
if let FsEntry::File(file) = entry {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"", file.abs_path.display()),
);
// Edit file
if let Err(err) = self.edit_remote_file(file) {
self.log_and_alert(LogLevel::Error, err);
}
}
}
// Reload entries
self.reload_remote_dir();
}
/// ### edit_local_file
///
/// Edit a file on localhost
fn edit_local_file(&mut self, path: &Path) -> Result<(), String> {
// Read first 2048 bytes or less from file to check if it is textual
match OpenOptions::new().read(true).open(path) {
Ok(mut f) => {
// Read
let mut buff: [u8; 2048] = [0; 2048];
match f.read(&mut buff) {
Ok(size) => {
if content_inspector::inspect(&buff[0..size]).is_binary() {
return Err("Could not open file in editor: file is binary".to_string());
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
// Put input mode back to normal
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Open editor
match edit::edit_file(path) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Changes performed through editor saved to \"{}\"!",
path.display()
),
),
Err(err) => return Err(format!("Could not open editor: {}", err)),
}
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
// Enter alternate mode
ctx.enter_alternate_screen();
}
// Re-enable raw mode
let _ = enable_raw_mode();
Ok(())
}
/// ### edit_remote_file
///
/// Edit file on remote host
fn edit_remote_file(&mut self, file: FsFile) -> Result<(), String> {
// Create temp file
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
Ok(p) => p,
Err(err) => return Err(err),
};
// Download file
let file_name = file.name.clone();
let file_path = file.abs_path.clone();
if let Err(err) = self.filetransfer_recv(
TransferPayload::File(file),
tmpfile.as_path(),
Some(file_name.clone()),
) {
return Err(format!("Could not open file {}: {}", file_name, err));
}
// Get current file modification time
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.get_last_change_time(),
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Edit file
if let Err(err) = self.edit_local_file(tmpfile.as_path()) {
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e,
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Check if file has changed
match prev_mtime != tmpfile_entry.get_last_change_time() {
true => {
self.log(
LogLevel::Info,
format!(
"File \"{}\" has changed; writing changes to remote",
file_path.display()
),
);
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.unwrap_file(),
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Send file
let wrkdir = self.remote().wrkdir.clone();
if let Err(err) = self.filetransfer_send(
TransferPayload::File(tmpfile_entry),
wrkdir.as_path(),
Some(file_name),
) {
return Err(format!(
"Could not write file {}: {}",
file_path.display(),
err
));
}
}
false => {
self.log(
LogLevel::Info,
format!("File \"{}\" hasn't changed", file_path.display()),
);
}
}
Ok(())
}
}

View File

@@ -0,0 +1,66 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity {
pub(crate) fn action_local_exec(&mut self, input: String) {
match self.host.exec(input.as_str()) {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
// Reload entries
self.reload_local_dir();
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not execute command \"{}\": {}", input, err),
);
}
}
}
pub(crate) fn action_remote_exec(&mut self, input: String) {
match self.client.as_mut().exec(input.as_str()) {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
self.reload_remote_dir();
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not execute command \"{}\": {}", input, err),
);
}
}
}
}

View File

@@ -0,0 +1,221 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::super::browser::FileExplorerTab;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
match self.host.find(input.as_str()) {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {}", err)),
}
}
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
match self.client.as_mut().find(input.as_str()) {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {}", err)),
}
}
pub(crate) fn action_find_changedir(&mut self) {
// Match entry
if let SelectedEntry::One(entry) = self.get_found_selected_entries() {
// Get path: if a directory, use directory path; if it is a File, get parent path
let path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path,
FsEntry::File(file) => match file.abs_path.parent() {
None => PathBuf::from("."),
Some(p) => p.to_path_buf(),
},
};
// Change directory
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.local_changedir(path.as_path(), true)
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.remote_changedir(path.as_path(), true)
}
}
}
}
pub(crate) fn action_find_transfer(&mut self, save_as: Option<String>) {
let wrkdir: PathBuf = match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
};
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
},
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as {
dest_path.push(save_as);
}
// Iter files
let entries = entries.iter().map(|x| x.get_realfile()).collect();
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
}
}
SelectedEntry::None => {}
}
}
pub(crate) fn action_find_delete(&mut self) {
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => {
// Delete file
self.remove_found_file(&entry);
}
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// Delete file
self.remove_found_file(entry);
}
}
SelectedEntry::None => {}
}
}
fn remove_found_file(&mut self, entry: &FsEntry) {
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.local_remove_file(entry);
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.remote_remove_file(entry);
}
}
}
pub(crate) fn action_find_open(&mut self) {
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => {
// Open file
self.open_found_file(&entry, None);
}
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// Open file
self.open_found_file(entry, None);
}
}
SelectedEntry::None => {}
}
}
pub(crate) fn action_find_open_with(&mut self, with: &str) {
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => {
// Open file
self.open_found_file(&entry, Some(with));
}
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// Open file
self.open_found_file(entry, Some(with));
}
}
SelectedEntry::None => {}
}
}
fn open_found_file(&mut self, entry: &FsEntry, with: Option<&str>) {
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.action_open_local_file(entry, with);
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.action_open_remote_file(entry, with);
}
}
}
}

View File

@@ -0,0 +1,70 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_mkdir(&mut self, input: String) {
match self.host.mkdir(PathBuf::from(input.as_str()).as_path()) {
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
// Reload entries
self.reload_local_dir();
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not create directory \"{}\": {}", input, err),
);
}
}
}
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
match self
.client
.as_mut()
.mkdir(PathBuf::from(input.as_str()).as_path())
{
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
self.reload_remote_dir();
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not create directory \"{}\": {}", input, err),
);
}
}
}
}

View File

@@ -0,0 +1,149 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel, TransferPayload};
use tuirealm::{Payload, Value};
// actions
pub(crate) mod change_dir;
pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
pub(crate) mod exec;
pub(crate) mod find;
pub(crate) mod mkdir;
pub(crate) mod newfile;
pub(crate) mod open;
pub(crate) mod rename;
pub(crate) mod save;
pub(crate) mod submit;
#[derive(Debug)]
pub(crate) enum SelectedEntry {
One(FsEntry),
Many(Vec<FsEntry>),
None,
}
#[derive(Debug)]
enum SelectedEntryIndex {
One(usize),
Many(Vec<usize>),
None,
}
impl From<Option<&FsEntry>> for SelectedEntry {
fn from(opt: Option<&FsEntry>) -> Self {
match opt {
Some(e) => SelectedEntry::One(e.clone()),
None => SelectedEntry::None,
}
}
}
impl From<Vec<&FsEntry>> for SelectedEntry {
fn from(files: Vec<&FsEntry>) -> Self {
SelectedEntry::Many(files.into_iter().cloned().collect())
}
}
impl FileTransferActivity {
/// ### get_local_selected_entries
///
/// Get local file entry
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
.iter()
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry>
.flatten()
.collect();
SelectedEntry::from(files)
}
SelectedEntryIndex::None => SelectedEntry::None,
}
}
/// ### get_remote_selected_entries
///
/// Get remote file entry
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
.iter()
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry>
.flatten()
.collect();
SelectedEntry::from(files)
}
SelectedEntryIndex::None => SelectedEntry::None,
}
}
/// ### get_remote_selected_entries
///
/// Get remote file entry
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) {
SelectedEntryIndex::One(idx) => {
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
}
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
.iter()
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<FsEntry>
.flatten()
.collect();
SelectedEntry::from(files)
}
SelectedEntryIndex::None => SelectedEntry::None,
}
}
// -- private
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
match self.view.get_state(component) {
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
Some(Payload::Vec(files)) => {
let list: Vec<usize> = files
.iter()
.map(|x| match x {
Value::Usize(v) => *v,
_ => 0,
})
.collect();
SelectedEntryIndex::Many(list)
}
_ => SelectedEntryIndex::None,
}
}
}

View File

@@ -0,0 +1,128 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_newfile(&mut self, input: String) {
// Check if file exists
let mut file_exists: bool = false;
for file in self.local().iter_files_all() {
if input == file.get_name() {
file_exists = true;
}
}
if file_exists {
self.log_and_alert(
LogLevel::Warn,
format!("File \"{}\" already exists", input,),
);
return;
}
// Create file
let file_path: PathBuf = PathBuf::from(input.as_str());
if let Err(err) = self.host.open_file_write(file_path.as_path()) {
self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
// Reload files
self.reload_local_dir();
}
pub(crate) fn action_remote_newfile(&mut self, input: String) {
// Check if file exists
let mut file_exists: bool = false;
for file in self.remote().iter_files_all() {
if input == file.get_name() {
file_exists = true;
}
}
if file_exists {
self.log_and_alert(
LogLevel::Warn,
format!("File \"{}\" already exists", input,),
);
return;
}
// Get path on remote
let file_path: PathBuf = PathBuf::from(input.as_str());
// Create file (on local)
match tempfile::NamedTempFile::new() {
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create tempfile: {}", err),
),
Ok(tfile) => {
// Stat tempfile
let local_file: FsEntry = match self.host.stat(tfile.path()) {
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not stat tempfile: {}", err),
);
return;
}
Ok(f) => f,
};
if let FsEntry::File(local_file) = local_file {
// Create file
match self.client.send_file(&local_file, file_path.as_path()) {
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
),
Ok(writer) => {
// Finalize write
if let Err(err) = self.client.on_sent(writer) {
self.log_and_alert(
LogLevel::Warn,
format!("Could not finalize file: {}", err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
// Reload files
self.reload_remote_dir();
}
}
}
}
}
}
}

View File

@@ -0,0 +1,166 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
// ext
use std::path::{Path, PathBuf};
impl FileTransferActivity {
/// ### action_open_local
///
/// Open local file
pub(crate) fn action_open_local(&mut self) {
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
entries
.iter()
.for_each(|x| self.action_open_local_file(x, None));
}
/// ### action_open_remote
///
/// Open local file
pub(crate) fn action_open_remote(&mut self) {
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
entries
.iter()
.for_each(|x| self.action_open_remote_file(x, None));
}
/// ### action_open_local_file
///
/// Perform open lopcal file
pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
let entry: FsEntry = entry.get_realfile();
self.open_path_with(entry.get_abs_path().as_path(), open_with);
}
/// ### action_open_local
///
/// Open remote file. The file is first downloaded to a temporary directory on localhost
pub(crate) fn action_open_remote_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
let entry: FsEntry = entry.get_realfile();
// Download file
let tmpfile: String =
match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
return;
}
Some(p) => p,
};
let cache: PathBuf = match self.cache.as_ref() {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
return;
}
Some(p) => p.path().to_path_buf(),
};
match self.filetransfer_recv(
TransferPayload::Any(entry),
cache.as_path(),
Some(tmpfile.clone()),
) {
Ok(_) => {
// Make file and open if file exists
let mut tmp: PathBuf = cache;
tmp.push(tmpfile.as_str());
if tmp.exists() {
self.open_path_with(tmp.as_path(), open_with);
}
}
Err(err) => {
self.log(
LogLevel::Error,
format!("Failed to download remote entry: {}", err),
);
}
}
}
/// ### action_local_open_with
///
/// Open selected file with provided application
pub(crate) fn action_local_open_with(&mut self, with: &str) {
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
// Open all entries
entries
.iter()
.for_each(|x| self.action_open_local_file(x, Some(with)));
}
/// ### action_remote_open_with
///
/// Open selected file with provided application
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
// Open all entries
entries
.iter()
.for_each(|x| self.action_open_remote_file(x, Some(with)));
}
/// ### open_path_with
///
/// Common function which opens a path with default or specified program.
fn open_path_with(&mut self, p: &Path, with: Option<&str>) {
// Open file
let result = match with {
None => open::that(p),
Some(with) => open::with(p, with),
};
// Log result
match result {
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
Err(err) => self.log(
LogLevel::Error,
format!("Failed to open filoe `{}`: {}", p.display(), err),
),
}
// NOTE: clear screen in order to prevent crap on stderr
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
}
}
}

View File

@@ -0,0 +1,128 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use std::path::{Path, PathBuf};
impl FileTransferActivity {
pub(crate) fn action_local_rename(&mut self, input: String) {
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.local_rename_file(&entry, dest_path.as_path());
// Reload entries
self.reload_local_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name());
self.local_rename_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
}
pub(crate) fn action_remote_rename(&mut self, input: String) {
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_rename_file(&entry, dest_path.as_path());
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name());
self.remote_rename_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_remote_dir();
}
SelectedEntry::None => {}
}
}
fn local_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
match self.host.rename(entry, dest) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not move \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
}
}
fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
match self.client.as_mut().rename(entry, dest) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not move \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
}
}
}

View File

@@ -0,0 +1,136 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) {
self.action_local_send_file(Some(input));
}
pub(crate) fn action_remote_saveas(&mut self, input: String) {
self.action_remote_recv_file(Some(input));
}
pub(crate) fn action_local_send(&mut self) {
self.action_local_send_file(None);
}
pub(crate) fn action_remote_recv(&mut self) {
self.action_remote_recv_file(None);
}
fn action_local_send_file(&mut self, save_as: Option<String>) {
let wrkdir: PathBuf = self.remote().wrkdir.clone();
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
}
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as {
dest_path.push(save_as);
}
// Iter files
let entries = entries.iter().map(|x| x.get_realfile()).collect();
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
}
SelectedEntry::None => {}
}
}
fn action_remote_recv_file(&mut self, save_as: Option<String>) {
let wrkdir: PathBuf = self.local().wrkdir.clone();
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
}
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as {
dest_path.push(save_as);
}
// Iter files
let entries = entries.iter().map(|x| x.get_realfile()).collect();
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
}
SelectedEntry::None => {}
}
}
}

View File

@@ -0,0 +1,88 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry};
enum SubmitAction {
ChangeDir,
None,
}
impl FileTransferActivity {
/// ### action_submit_local
///
/// Decides which action to perform on submit for local explorer
/// Return true whether the directory changed
pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool {
let action: SubmitAction = match &entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
_ => SubmitAction::None,
}
}
None => SubmitAction::None,
}
}
};
match action {
SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false),
SubmitAction::None => false,
}
}
/// ### action_submit_remote
///
/// Decides which action to perform on submit for remote explorer
/// Return true whether the directory changed
pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool {
let action: SubmitAction = match &entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
_ => SubmitAction::None,
}
}
None => SubmitAction::None,
}
}
};
match action {
SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false),
SubmitAction::None => false,
}
}
}

View File

@@ -0,0 +1,164 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
use crate::fs::FsEntry;
use crate::system::config_client::ConfigClient;
/// ## FileExplorerTab
///
/// File explorer tab
#[derive(Clone, Copy)]
pub enum FileExplorerTab {
Local,
Remote,
FindLocal, // Find result tab
FindRemote, // Find result tab
}
/// ## Browser
///
/// Browser contains the browser options
pub struct Browser {
local: FileExplorer, // Local File explorer state
remote: FileExplorer, // Remote File explorer state
found: Option<FileExplorer>, // File explorer for find result
tab: FileExplorerTab, // Current selected tab
pub sync_browsing: bool,
}
impl Browser {
/// ### new
///
/// Build a new `Browser` struct
pub fn new(cli: &ConfigClient) -> Self {
Self {
local: Self::build_local_explorer(cli),
remote: Self::build_remote_explorer(cli),
found: None,
tab: FileExplorerTab::Local,
sync_browsing: false,
}
}
pub fn local(&self) -> &FileExplorer {
&self.local
}
pub fn local_mut(&mut self) -> &mut FileExplorer {
&mut self.local
}
pub fn remote(&self) -> &FileExplorer {
&self.remote
}
pub fn remote_mut(&mut self) -> &mut FileExplorer {
&mut self.remote
}
pub fn found(&self) -> Option<&FileExplorer> {
self.found.as_ref()
}
pub fn found_mut(&mut self) -> Option<&mut FileExplorer> {
self.found.as_mut()
}
pub fn set_found(&mut self, files: Vec<FsEntry>) {
let mut explorer = Self::build_found_explorer();
explorer.set_files(files);
self.found = Some(explorer);
}
pub fn del_found(&mut self) {
self.found = None;
}
pub fn tab(&self) -> FileExplorerTab {
self.tab
}
/// ### change_tab
///
/// Update tab value
pub fn change_tab(&mut self, tab: FileExplorerTab) {
self.tab = tab;
}
/// ### toggle_sync_browsing
///
/// Invert the current state for the sync browsing
pub fn toggle_sync_browsing(&mut self) {
self.sync_browsing = !self.sync_browsing;
}
/// ### build_local_explorer
///
/// Build a file explorer with local host setup
pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer {
let mut builder = Self::build_explorer(cli);
builder.with_formatter(cli.get_local_file_fmt().as_deref());
builder.build()
}
/// ### build_remote_explorer
///
/// Build a file explorer with remote host setup
pub fn build_remote_explorer(cli: &ConfigClient) -> FileExplorer {
let mut builder = Self::build_explorer(cli);
builder.with_formatter(cli.get_remote_file_fmt().as_deref());
builder.build()
}
/// ### build_explorer
///
/// Build explorer reading configuration from `ConfigClient`
fn build_explorer(cli: &ConfigClient) -> FileExplorerBuilder {
let mut builder: FileExplorerBuilder = FileExplorerBuilder::new();
// Set common keys
builder
.with_file_sorting(FileSorting::Name)
.with_stack_size(16)
.with_group_dirs(cli.get_group_dirs())
.with_hidden_files(cli.get_show_hidden_files());
builder
}
/// ### build_found_explorer
///
/// Build explorer reading from `ConfigClient`, for found result (has some differences)
fn build_found_explorer() -> FileExplorer {
FileExplorerBuilder::new()
.with_file_sorting(FileSorting::Name)
.with_group_dirs(Some(GroupDirs::First))
.with_hidden_files(true)
.with_stack_size(0)
.with_formatter(Some("{NAME:32} {SYMLINK}"))
.build()
}
}

View File

@@ -0,0 +1,29 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
pub(crate) mod browser;
pub(crate) mod transfer;

View File

@@ -0,0 +1,259 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use bytesize::ByteSize;
use std::fmt;
use std::time::Instant;
/// ### TransferStates
///
/// TransferStates contains the states related to the transfer process
pub struct TransferStates {
aborted: bool, // Describes whether the transfer process has been aborted
pub full: ProgressStates, // full transfer states
pub partial: ProgressStates, // Partial transfer states
}
/// ### ProgressStates
///
/// Progress states describes the states for the progress of a single transfer part
pub struct ProgressStates {
started: Instant,
total: usize,
written: usize,
}
impl Default for TransferStates {
fn default() -> Self {
Self::new()
}
}
impl TransferStates {
/// ### new
///
/// Instantiates a new transfer states
pub fn new() -> TransferStates {
TransferStates {
aborted: false,
full: ProgressStates::default(),
partial: ProgressStates::default(),
}
}
/// ### reset
///
/// Re-intiialize transfer states
pub fn reset(&mut self) {
self.aborted = false;
}
/// ### abort
///
/// Set aborted to true
pub fn abort(&mut self) {
self.aborted = true;
}
/// ### aborted
///
/// Returns whether transfer has been aborted
pub fn aborted(&self) -> bool {
self.aborted
}
}
impl Default for ProgressStates {
fn default() -> Self {
ProgressStates {
started: Instant::now(),
written: 0,
total: 0,
}
}
}
impl fmt::Display for ProgressStates {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let eta: String = match self.calc_eta() {
0 => String::from("--:--"),
seconds => format!(
"{:0width$}:{:0width$}",
(seconds / 60),
(seconds % 60),
width = 2
),
};
write!(
f,
"{:.2}% - ETA {} ({}/s)",
self.calc_progress_percentage(),
eta,
ByteSize(self.calc_bytes_per_second())
)
}
}
impl ProgressStates {
/// ### init
///
/// Initialize a new Progress State
pub fn init(&mut self, sz: usize) {
self.started = Instant::now();
self.total = sz;
self.written = 0;
}
/// ### update_progress
///
/// Update progress state
pub fn update_progress(&mut self, delta: usize) -> f64 {
self.written += delta;
self.calc_progress_percentage()
}
/// ### calc_progress
///
/// Calculate progress in a range between 0.0 to 1.0
pub fn calc_progress(&self) -> f64 {
let prog: f64 = (self.written as f64) / (self.total as f64);
match prog > 1.0 {
true => 1.0,
false => prog,
}
}
/// ### started
///
/// Get started
pub fn started(&self) -> Instant {
self.started
}
/// ### calc_progress_percentage
///
/// Calculate the current transfer progress as percentage
fn calc_progress_percentage(&self) -> f64 {
self.calc_progress() * 100.0
}
/// ### calc_bytes_per_second
///
/// Generic function to calculate bytes per second using elapsed time since transfer started and the bytes written
/// and the total amount of bytes to write
pub fn calc_bytes_per_second(&self) -> u64 {
// bytes_written : elapsed_secs = x : 1
let elapsed_secs: u64 = self.started.elapsed().as_secs();
match elapsed_secs {
0 => match self.written == self.total {
// NOTE: would divide by 0 :D
true => self.total as u64, // Download completed in less than 1 second
false => 0, // 0 B/S
},
_ => self.written as u64 / elapsed_secs,
}
}
/// ### calc_eta
///
/// Calculate ETA for current transfer as seconds
fn calc_eta(&self) -> u64 {
let elapsed_secs: u64 = self.started.elapsed().as_secs();
let prog: f64 = self.calc_progress_percentage();
match prog as u64 {
0 => 0,
_ => ((elapsed_secs * 100) / (prog as u64)) - elapsed_secs,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::time::Duration;
#[test]
fn test_ui_activities_filetransfer_lib_transfer_progress_states() {
let mut states: ProgressStates = ProgressStates::default();
assert_eq!(states.total, 0);
assert_eq!(states.written, 0);
assert!(states.started().elapsed().as_secs() < 5);
// Init new transfer
states.init(1024);
assert_eq!(states.total, 1024);
assert_eq!(states.written, 0);
assert_eq!(states.calc_bytes_per_second(), 0);
assert_eq!(states.calc_eta(), 0);
assert_eq!(states.calc_progress_percentage(), 0.0);
assert_eq!(states.calc_progress(), 0.0);
assert_eq!(states.to_string().as_str(), "0.00% - ETA --:-- (0 B/s)");
// Wait 4 second (virtually)
states.started = states.started.checked_sub(Duration::from_secs(4)).unwrap();
// Update state
states.update_progress(256);
assert_eq!(states.total, 1024);
assert_eq!(states.written, 256);
assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds
assert_eq!(states.calc_eta(), 12); // 16 total sub 4
assert_eq!(states.calc_progress_percentage(), 25.0);
assert_eq!(states.calc_progress(), 0.25);
assert_eq!(states.to_string().as_str(), "25.00% - ETA 00:12 (64 B/s)");
// 100%
states.started = states.started.checked_sub(Duration::from_secs(12)).unwrap();
states.update_progress(768);
assert_eq!(states.total, 1024);
assert_eq!(states.written, 1024);
assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds
assert_eq!(states.calc_eta(), 0); // 16 total sub 4
assert_eq!(states.calc_progress_percentage(), 100.0);
assert_eq!(states.calc_progress(), 1.0);
assert_eq!(states.to_string().as_str(), "100.00% - ETA --:-- (64 B/s)");
// Check if terminated at started
states.started = Instant::now();
assert_eq!(states.calc_bytes_per_second(), 1024);
}
#[test]
fn test_ui_activities_filetransfer_lib_transfer_states() {
let mut states: TransferStates = TransferStates::default();
assert_eq!(states.aborted, false);
assert_eq!(states.full.total, 0);
assert_eq!(states.full.written, 0);
assert!(states.full.started.elapsed().as_secs() < 5);
assert_eq!(states.partial.total, 0);
assert_eq!(states.partial.written, 0);
assert!(states.partial.started.elapsed().as_secs() < 5);
// Aborted
states.abort();
assert_eq!(states.aborted(), true);
states.reset();
assert_eq!(states.aborted(), false);
}
}

View File

@@ -0,0 +1,137 @@
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::path;
// Ext
use std::env;
use std::path::{Path, PathBuf};
use tuirealm::Update;
const LOG_CAPACITY: usize = 256;
impl FileTransferActivity {
/// ### log
///
/// Add message to log events
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
// Log to file
match level {
LogLevel::Error => error!("{}", msg),
LogLevel::Info => info!("{}", msg),
LogLevel::Warn => warn!("{}", msg),
}
// Create log record
let record: LogRecord = LogRecord::new(level, msg);
//Check if history overflows the size
if self.log_records.len() + 1 > LOG_CAPACITY {
self.log_records.pop_back(); // Start cleaning events from back
}
// Eventually push front the new record
self.log_records.push_front(record);
// Update log
let msg = self.update_logbox();
self.update(msg);
}
/// ### log_and_alert
///
/// Add message to log events and also display it as an alert
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
self.mount_error(msg.as_str());
self.log(level, msg);
// Update log
let msg = self.update_logbox();
self.update(msg);
}
/// ### init_config_client
///
/// Initialize configuration client if possible.
/// This function doesn't return errors.
pub(super) fn init_config_client() -> ConfigClient {
match environment::init_config_dir() {
Ok(termscp_dir) => match termscp_dir {
Some(termscp_dir) => {
// Make configuration file path and ssh keys path
let (config_path, ssh_keys_path): (PathBuf, PathBuf) =
environment::get_config_paths(termscp_dir.as_path());
match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) {
Ok(config_client) => config_client,
Err(_) => ConfigClient::degraded(),
}
}
None => ConfigClient::degraded(),
},
Err(_) => ConfigClient::degraded(),
}
}
/// ### make_ssh_storage
///
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
pub(super) fn make_ssh_storage(cli: &ConfigClient) -> SshKeyStorage {
SshKeyStorage::storage_from_config(cli)
}
/// ### setup_text_editor
///
/// Set text editor to use
pub(super) fn setup_text_editor(&self) {
env::set_var("EDITOR", self.config().get_text_editor());
}
/// ### read_input_event
///
/// Read one event.
/// Returns whether at least one event has been handled
pub(super) fn read_input_event(&mut self) -> bool {
if let Ok(Some(event)) = self.context().input_hnd().read_event() {
// Handle event
let msg = self.view.on(event);
self.update(msg);
// Return true
true
} else {
// Error
false
}
}
/// ### local_to_abs_path
///
/// Convert a path to absolute according to local explorer
pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.local().wrkdir.as_path(), path)
}
/// ### remote_to_abs_path
///
/// Convert a path to absolute according to remote explorer
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.remote().wrkdir.as_path(), path)
}
}

View File

@@ -0,0 +1,353 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// This module is split into files, cause it's just too big
pub(self) mod actions;
pub(self) mod lib;
pub(self) mod misc;
pub(self) mod session;
pub(self) mod update;
pub(self) mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
use crate::filetransfer::scp_transfer::ScpFileTransfer;
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
pub(self) use lib::browser;
use lib::browser::Browser;
use lib::transfer::TransferStates;
pub(self) use session::TransferPayload;
// Includes
use chrono::{DateTime, Local};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::collections::VecDeque;
use tempfile::TempDir;
use tuirealm::View;
// -- Storage keys
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
// -- components
const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL";
const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE";
const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND";
const COMPONENT_LOG_BOX: &str = "LOG_BOX";
const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL";
const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
const COMPONENT_INPUT_COPY: &str = "INPUT_COPY";
const COMPONENT_INPUT_EXEC: &str = "INPUT_EXEC";
const COMPONENT_INPUT_FIND: &str = "INPUT_FIND";
const COMPONENT_INPUT_GOTO: &str = "INPUT_GOTO";
const COMPONENT_INPUT_MKDIR: &str = "INPUT_MKDIR";
const COMPONENT_INPUT_NEWFILE: &str = "INPUT_NEWFILE";
const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
const COMPONENT_SPAN_STATUS_BAR_LOCAL: &str = "STATUS_BAR_LOCAL";
const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE";
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
/// ## LogLevel
///
/// Log level type
enum LogLevel {
Error,
Warn,
Info,
}
/// ## LogRecord
///
/// Log record entry
struct LogRecord {
pub time: DateTime<Local>,
pub level: LogLevel,
pub msg: String,
}
impl LogRecord {
/// ### new
///
/// Instantiates a new LogRecord
pub fn new(level: LogLevel, msg: String) -> LogRecord {
LogRecord {
time: Local::now(),
level,
msg,
}
}
}
/// ## FileTransferActivity
///
/// FileTransferActivity is the data holder for the file transfer activity
pub struct FileTransferActivity {
exit_reason: Option<ExitReason>, // Exit reason
context: Option<Context>, // Context holder
view: View, // View
host: Localhost, // Localhost
client: Box<dyn FileTransfer>, // File transfer client
browser: Browser, // Browser
log_records: VecDeque<LogRecord>, // Log records
transfer: TransferStates, // Transfer states
cache: Option<TempDir>, // Temporary directory where to store stuff
}
impl FileTransferActivity {
/// ### new
///
/// Instantiates a new FileTransferActivity
pub fn new(host: Localhost, protocol: FileTransferProtocol) -> FileTransferActivity {
// Get config client
let config_client: ConfigClient = Self::init_config_client();
FileTransferActivity {
exit_reason: None,
context: None,
view: View::init(),
host,
client: match protocol {
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
Self::make_ssh_storage(&config_client),
)),
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
FileTransferProtocol::Scp => {
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
}
},
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
transfer: TransferStates::default(),
cache: match TempDir::new() {
Ok(d) => Some(d),
Err(_) => None,
},
}
}
fn local(&self) -> &FileExplorer {
self.browser.local()
}
fn local_mut(&mut self) -> &mut FileExplorer {
self.browser.local_mut()
}
fn remote(&self) -> &FileExplorer {
self.browser.remote()
}
fn remote_mut(&mut self) -> &mut FileExplorer {
self.browser.remote_mut()
}
fn found(&self) -> Option<&FileExplorer> {
self.browser.found()
}
fn found_mut(&mut self) -> Option<&mut FileExplorer> {
self.browser.found_mut()
}
/// ### get_cache_tmp_name
///
/// Get file name for a file in cache
fn get_cache_tmp_name(&self, name: &str, file_type: Option<&str>) -> Option<String> {
self.cache.as_ref().map(|_| {
let base: String = format!(
"{}-{}",
name,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
match file_type {
None => base,
Some(file_type) => format!("{}.{}", base, file_type),
}
})
}
/// ### context
///
/// Returns a reference to context
fn context(&self) -> &Context {
self.context.as_ref().unwrap()
}
/// ### context_mut
///
/// Returns a mutable reference to context
fn context_mut(&mut self) -> &mut Context {
self.context.as_mut().unwrap()
}
/// ### config
///
/// Returns config client reference
fn config(&self) -> &ConfigClient {
self.context().config()
}
/// ### theme
///
/// Get a reference to `Theme`
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
}
/**
* Activity Trait
* Keep it clean :)
* Use methods instead!
*/
impl Activity for FileTransferActivity {
/// ### on_create
///
/// `on_create` is the function which must be called to initialize the activity.
/// `on_create` must initialize all the data structures used by the activity
fn on_create(&mut self, context: Context) {
debug!("Initializing activity...");
// Set context
self.context = Some(context);
// Clear terminal
self.context_mut().clear_screen();
// Put raw mode on enabled
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Get files at current pwd
self.reload_local_dir();
debug!("Read working directory");
// Configure text editor
self.setup_text_editor();
debug!("Setup text editor");
// init view
self.init();
debug!("Initialized view");
// Verify error state from context
if let Some(err) = self.context.as_mut().unwrap().error() {
error!("Fatal error on create: {}", err);
self.mount_fatal(&err);
}
info!("Created FileTransferActivity");
}
/// ### on_draw
///
/// `on_draw` is the function which draws the graphical interface.
/// This function must be called at each tick to refresh the interface
fn on_draw(&mut self) {
// Should ui actually be redrawned?
let mut redraw: bool = false;
// Context must be something
if self.context.is_none() {
return;
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context().ft_params().unwrap();
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
let msg: String = format!("Connecting to {}:{}", params.address, params.port);
// Set init state to connecting popup
self.mount_wait(msg.as_str());
// Force ui draw
self.view();
// Connect to remote
self.connect();
// Redraw
redraw = true;
}
// Handle input events (if false, becomes true; otherwise remains true)
redraw |= self.read_input_event();
// @! draw interface
if redraw {
self.view();
}
}
/// ### will_umount
///
/// `will_umount` is the method which must be able to report to the activity manager, whether
/// the activity should be terminated or not.
/// If not, the call will return `None`, otherwise return`Some(ExitReason)`
fn will_umount(&self) -> Option<&ExitReason> {
self.exit_reason.as_ref()
}
/// ### on_destroy
///
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
/// This function must be called once before terminating the activity.
fn on_destroy(&mut self) -> Option<Context> {
// Destroy cache
if let Some(cache) = self.cache.take() {
if let Err(err) = cache.close() {
error!("Failed to delete cache: {}", err);
}
}
// Disable raw mode
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Disconnect client
if self.client.is_connected() {
let _ = self.client.disconnect();
}
// Clear terminal and return
match self.context.take() {
Some(mut ctx) => {
ctx.clear_screen();
Some(ctx)
}
None => None,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,939 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH,
COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX,
COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE,
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder};
use crate::ui::keymap::*;
use crate::utils::fmt::fmt_path_elide_ex;
// externals
use tui_realm_stdlib::progress_bar::ProgressBarPropsBuilder;
use tuirealm::{
props::{Alignment, PropsBuilder, TableBuilder, TextSpan},
tui::style::Color,
Msg, Payload, Update, Value,
};
impl Update for FileTransferActivity {
// -- update
/// ### update
///
/// Update auth activity model based on msg
/// The function exits when returns None
fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg));
// Match msg
match ref_msg {
None => None, // Exit after None
Some(msg) => match msg {
// -- local tab
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_RIGHT => {
// Change tab
self.view.active(COMPONENT_EXPLORER_REMOTE);
self.browser.change_tab(FileExplorerTab::Remote);
None
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_BACKSPACE => {
// Go to previous directory
self.action_go_to_previous_local_dir(false);
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
// Match selected file
let mut entry: Option<FsEntry> = None;
if let Some(e) = self.local().get(*idx) {
entry = Some(e.clone());
}
if let Some(entry) = entry {
if self.action_submit_local(entry) {
// Update file list if sync
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
self.update_local_filelist()
} else {
None
}
} else {
None
}
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_SPACE => {
self.action_local_send();
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_A => {
// Toggle hidden files
self.local_mut().toggle_hidden_files();
// Update status bar
self.refresh_local_status_bar();
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_I => {
if let SelectedEntry::One(file) = self.get_local_selected_entries() {
self.mount_file_info(&file);
}
None
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_L => {
// Reload directory
self.reload_local_dir();
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_O => {
self.action_edit_local_file();
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, key) if key == &MSG_KEY_CHAR_U => {
self.action_go_to_local_upper_dir(false);
if self.browser.sync_browsing {
let _ = self.update_remote_filelist();
}
// Reload file list component
self.update_local_filelist()
}
// -- remote tab
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_LEFT => {
// Change tab
self.view.active(COMPONENT_EXPLORER_LOCAL);
self.browser.change_tab(FileExplorerTab::Local);
None
}
(COMPONENT_EXPLORER_REMOTE, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
// Match selected file
let mut entry: Option<FsEntry> = None;
if let Some(e) = self.remote().get(*idx) {
entry = Some(e.clone());
}
if let Some(entry) = entry {
if self.action_submit_remote(entry) {
// Update file list if sync
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
self.update_remote_filelist()
} else {
None
}
} else {
None
}
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_SPACE => {
self.action_remote_recv();
self.update_local_filelist()
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_BACKSPACE => {
// Go to previous directory
self.action_go_to_previous_remote_dir(false);
// If sync is enabled update local too
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_A => {
// Toggle hidden files
self.remote_mut().toggle_hidden_files();
// Update status bar
self.refresh_remote_status_bar();
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_I => {
if let SelectedEntry::One(file) = self.get_remote_selected_entries() {
self.mount_file_info(&file);
}
None
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_L => {
// Reload directory
self.reload_remote_dir();
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_O => {
// Edit file
self.action_edit_remote_file();
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, key) if key == &MSG_KEY_CHAR_U => {
self.action_go_to_remote_upper_dir(false);
if self.browser.sync_browsing {
let _ = self.update_local_filelist();
}
// Reload file list component
self.update_remote_filelist()
}
// -- common explorer keys
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_B =>
{
// Show sorting file
self.mount_file_sorting();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_C =>
{
self.mount_copy();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_D =>
{
self.mount_mkdir();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_F =>
{
self.mount_find_input();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_G =>
{
self.mount_goto();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_H =>
{
self.mount_help();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_N =>
{
self.mount_newfile();
None
}
(COMPONENT_EXPLORER_LOCAL, key)
| (COMPONENT_EXPLORER_REMOTE, key)
| (COMPONENT_LOG_BOX, key)
if key == &MSG_KEY_CHAR_Q =>
{
self.mount_quit();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_R =>
{
// Mount rename
self.mount_rename();
None
}
(COMPONENT_EXPLORER_LOCAL, key)
| (COMPONENT_EXPLORER_REMOTE, key)
| (COMPONENT_EXPLORER_FIND, key)
if key == &MSG_KEY_CHAR_S =>
{
// Mount save as
self.mount_saveas();
None
}
(COMPONENT_EXPLORER_LOCAL, key)
| (COMPONENT_EXPLORER_REMOTE, key)
| (COMPONENT_EXPLORER_FIND, key)
if key == &MSG_KEY_CHAR_V =>
{
// View
match self.browser.tab() {
FileExplorerTab::Local => self.action_open_local(),
FileExplorerTab::Remote => self.action_open_remote(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
self.action_find_open()
}
}
None
}
(COMPONENT_EXPLORER_LOCAL, key)
| (COMPONENT_EXPLORER_REMOTE, key)
| (COMPONENT_EXPLORER_FIND, key)
if key == &MSG_KEY_CHAR_W =>
{
// Open with
self.mount_openwith();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_X =>
{
// Mount exec
self.mount_exec();
None
}
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_CHAR_Y =>
{
// Toggle browser sync
self.browser.toggle_sync_browsing();
// Update status bar
self.refresh_remote_status_bar();
None
}
(COMPONENT_EXPLORER_LOCAL, key)
| (COMPONENT_EXPLORER_REMOTE, key)
| (COMPONENT_LOG_BOX, key)
if key == &MSG_KEY_ESC =>
{
self.mount_disconnect();
None
}
(COMPONENT_EXPLORER_LOCAL, key)
| (COMPONENT_EXPLORER_REMOTE, key)
| (COMPONENT_EXPLORER_FIND, key)
if key == &MSG_KEY_CHAR_E || key == &MSG_KEY_DEL =>
{
self.mount_radio_delete();
None
}
// -- find result explorer
(COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_ESC => {
// Umount find
self.umount_find();
// Finalize find
self.finalize_find();
None
}
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(_)) => {
// Find changedir
self.action_find_changedir();
// Umount find
self.umount_find();
// Finalize find
self.finalize_find();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => {
// Get entry
self.action_find_transfer(None);
// Reload files
match self.browser.tab() {
// NOTE: swapped by purpose
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
_ => None,
}
}
// -- switch to log
(COMPONENT_EXPLORER_LOCAL, key) | (COMPONENT_EXPLORER_REMOTE, key)
if key == &MSG_KEY_TAB =>
{
self.view.active(COMPONENT_LOG_BOX); // Active log box
None
}
// -- Log box
(COMPONENT_LOG_BOX, key) if key == &MSG_KEY_TAB => {
self.view.blur(); // Blur log box
None
}
// -- copy popup
(COMPONENT_INPUT_COPY, key) if key == &MSG_KEY_ESC => {
self.umount_copy();
None
}
(COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
// Copy file
self.umount_copy();
self.mount_blocking_wait("Copying file(s)…");
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_copy(input.to_string()),
FileExplorerTab::Remote => self.action_remote_copy(input.to_string()),
_ => panic!("Found tab doesn't support COPY"),
}
self.umount_wait();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_INPUT_COPY, _) => None,
// -- exec popup
(COMPONENT_INPUT_EXEC, key) if key == &MSG_KEY_ESC => {
self.umount_exec();
None
}
(COMPONENT_INPUT_EXEC, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
// Exex command
self.umount_exec();
self.mount_blocking_wait(format!("Executing '{}'…", input).as_str());
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_exec(input.to_string()),
FileExplorerTab::Remote => self.action_remote_exec(input.to_string()),
_ => panic!("Found tab doesn't support EXEC"),
}
self.umount_wait();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_INPUT_EXEC, _) => None,
// -- find popup
(COMPONENT_INPUT_FIND, key) if key == &MSG_KEY_ESC => {
self.umount_find_input();
None
}
(COMPONENT_INPUT_FIND, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
self.umount_find_input();
// Find
let res: Result<Vec<FsEntry>, String> = match self.browser.tab() {
FileExplorerTab::Local => self.action_local_find(input.to_string()),
FileExplorerTab::Remote => self.action_remote_find(input.to_string()),
_ => panic!("Trying to search for files, while already in a find result"),
};
// Match result
match res {
Err(err) => {
// Mount error
self.mount_error(err.as_str());
}
Ok(files) => {
// Create explorer and load files
self.browser.set_found(files);
// Mount result widget
self.mount_find(input);
self.update_find_list();
// Initialize tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::Local => FileExplorerTab::FindLocal,
FileExplorerTab::Remote => FileExplorerTab::FindRemote,
_ => FileExplorerTab::FindLocal,
});
}
}
None
}
// -- goto popup
(COMPONENT_INPUT_GOTO, key) if key == &MSG_KEY_ESC => {
self.umount_goto();
None
}
(COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => {
self.action_change_local_dir(input.to_string(), false)
}
FileExplorerTab::Remote => {
self.action_change_remote_dir(input.to_string(), false)
}
_ => panic!("Found tab doesn't support GOTO"),
}
// Umount
self.umount_goto();
// Reload files if sync
if self.browser.sync_browsing {
match self.browser.tab() {
FileExplorerTab::Remote => self.update_local_filelist(),
FileExplorerTab::Local => self.update_remote_filelist(),
_ => None,
};
}
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_INPUT_GOTO, _) => None,
// -- make directory
(COMPONENT_INPUT_MKDIR, key) if key == &MSG_KEY_ESC => {
self.umount_mkdir();
None
}
(COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_mkdir(input.to_string()),
FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()),
_ => panic!("Found tab doesn't support MKDIR"),
}
self.umount_mkdir();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_INPUT_MKDIR, _) => None,
// -- new file
(COMPONENT_INPUT_NEWFILE, key) if key == &MSG_KEY_ESC => {
self.umount_newfile();
None
}
(COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_newfile(input.to_string()),
FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()),
_ => panic!("Found tab doesn't support NEWFILE"),
}
self.umount_newfile();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_INPUT_NEWFILE, _) => None,
// -- open with
(COMPONENT_INPUT_OPEN_WITH, key) if key == &MSG_KEY_ESC => {
self.umount_openwith();
None
}
(COMPONENT_INPUT_OPEN_WITH, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_open_with(input),
FileExplorerTab::Remote => self.action_remote_open_with(input),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
self.action_find_open_with(input)
}
}
self.umount_openwith();
None
}
(COMPONENT_INPUT_OPEN_WITH, _) => None,
// -- rename
(COMPONENT_INPUT_RENAME, key) if key == &MSG_KEY_ESC => {
self.umount_rename();
None
}
(COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
self.umount_rename();
self.mount_blocking_wait("Moving file(s)…");
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_rename(input.to_string()),
FileExplorerTab::Remote => self.action_remote_rename(input.to_string()),
_ => panic!("Found tab doesn't support RENAME"),
}
self.umount_wait();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_INPUT_RENAME, _) => None,
// -- save as
(COMPONENT_INPUT_SAVEAS, key) if key == &MSG_KEY_ESC => {
self.umount_saveas();
None
}
(COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::One(Value::Str(input)))) => {
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_saveas(input.to_string()),
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
self.action_find_transfer(Some(input.to_string()));
}
}
self.umount_saveas();
// Reload files
match self.browser.tab() {
// NOTE: Swapped is intentional
FileExplorerTab::Local => self.update_remote_filelist(),
FileExplorerTab::Remote => self.update_local_filelist(),
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
}
}
(COMPONENT_INPUT_SAVEAS, _) => None,
// -- fileinfo
(COMPONENT_LIST_FILEINFO, key) | (COMPONENT_LIST_FILEINFO, key)
if key == &MSG_KEY_ENTER || key == &MSG_KEY_ESC =>
{
self.umount_file_info();
None
}
(COMPONENT_LIST_FILEINFO, _) => None,
// -- delete
(COMPONENT_RADIO_DELETE, key)
if key == &MSG_KEY_ESC
|| key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) =>
{
self.umount_radio_delete();
None
}
(COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Choice is 'YES'
self.umount_radio_delete();
self.mount_blocking_wait("Removing file(s)…");
match self.browser.tab() {
FileExplorerTab::Local => self.action_local_delete(),
FileExplorerTab::Remote => self.action_remote_delete(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
self.action_find_delete();
// Delete entries
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
Some(Payload::One(Value::Usize(idx))) => {
// Reload entries
self.found_mut().unwrap().del_entry(idx);
}
Some(Payload::Vec(values)) => {
values
.iter()
.map(|x| match x {
Value::Usize(v) => *v,
_ => 0,
})
.for_each(|x| self.found_mut().unwrap().del_entry(x));
}
_ => {}
}
self.update_find_list();
}
}
self.umount_wait();
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
FileExplorerTab::FindLocal => self.update_local_filelist(),
FileExplorerTab::FindRemote => self.update_remote_filelist(),
}
}
(COMPONENT_RADIO_DELETE, _) => None,
// -- disconnect
(COMPONENT_RADIO_DISCONNECT, key)
if key == &MSG_KEY_ESC
|| key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) =>
{
self.umount_disconnect();
None
}
(COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
self.disconnect();
self.umount_disconnect();
None
}
(COMPONENT_RADIO_DISCONNECT, _) => None,
// -- quit
(COMPONENT_RADIO_QUIT, key)
if key == &MSG_KEY_ESC
|| key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) =>
{
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
self.disconnect_and_quit();
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, _) => None,
// -- sorting
(COMPONENT_RADIO_SORTING, key) if key == &MSG_KEY_ESC => {
self.umount_file_sorting();
None
}
(COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => {
self.umount_file_sorting();
None
}
(COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::One(Value::Usize(mode)))) => {
// Get sorting mode
let sorting: FileSorting = match mode {
1 => FileSorting::ModifyTime,
2 => FileSorting::CreationTime,
3 => FileSorting::Size,
_ => FileSorting::Name,
};
match self.browser.tab() {
FileExplorerTab::Local => self.local_mut().sort_by(sorting),
FileExplorerTab::Remote => self.remote_mut().sort_by(sorting),
_ => panic!("Found result doesn't support SORTING"),
}
// Update status bar
match self.browser.tab() {
FileExplorerTab::Local => self.refresh_local_status_bar(),
FileExplorerTab::Remote => self.refresh_remote_status_bar(),
_ => panic!("Found result doesn't support SORTING"),
};
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
_ => None,
}
}
(COMPONENT_RADIO_SORTING, _) => None,
// -- error
(COMPONENT_TEXT_ERROR, key) | (COMPONENT_TEXT_ERROR, key)
if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER =>
{
self.umount_error();
None
}
(COMPONENT_TEXT_ERROR, _) => None,
// -- fatal
(COMPONENT_TEXT_FATAL, key) | (COMPONENT_TEXT_FATAL, key)
if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER =>
{
self.exit_reason = Some(super::ExitReason::Disconnect);
None
}
(COMPONENT_TEXT_FATAL, _) => None,
// -- help
(COMPONENT_TEXT_HELP, key) | (COMPONENT_TEXT_HELP, key)
if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER =>
{
self.umount_help();
None
}
(COMPONENT_TEXT_HELP, _) => None,
// -- progress bar
(COMPONENT_PROGRESS_BAR_PARTIAL, key) if key == &MSG_KEY_CTRL_C => {
// Set transfer aborted to True
self.transfer.abort();
None
}
(COMPONENT_PROGRESS_BAR_PARTIAL, _) => None,
// -- fallback
(_, _) => None, // Nothing to do
},
}
}
}
impl FileTransferActivity {
/// ### update_local_filelist
///
/// Update local file list
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_EXPLORER_LOCAL) {
Some(props) => {
// Get width
let width: usize = self
.context()
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let hostname: String = match hostname::get() {
Ok(h) => {
let hostname: String = h.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.get(0).unwrap_or(&"localhost"))
}
Err(_) => String::from("localhost"),
};
let hostname: String = format!(
"{}:{} ",
hostname,
fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/'
);
let files: Vec<String> = self
.local()
.iter_files()
.map(|x: &FsEntry| self.local().fmt_file(x))
.collect();
// Update
let props = FileListPropsBuilder::from(props)
.with_files(files)
.with_title(hostname, Alignment::Left)
.build();
// Update
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
}
None => None,
}
}
/// ### update_remote_filelist
///
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_EXPLORER_REMOTE) {
Some(props) => {
// Get width
let width: usize = self
.context()
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let params = self.context().ft_params().unwrap();
let hostname: String = format!(
"{}:{} ",
params.address,
fmt_path_elide_ex(
self.remote().wrkdir.as_path(),
width,
params.address.len() + 3 // 3 because of '/…/'
)
);
let files: Vec<String> = self
.remote()
.iter_files()
.map(|x: &FsEntry| self.remote().fmt_file(x))
.collect();
// Update
let props = FileListPropsBuilder::from(props)
.with_files(files)
.with_title(hostname, Alignment::Left)
.build();
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
}
None => None,
}
}
/// ### update_logbox
///
/// Update log box
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_LOG_BOX) {
Some(props) => {
// Make log entries
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in self.log_records.iter().enumerate() {
// Add row if not first row
if idx > 0 {
table.add_row();
}
let fg = match record.level {
LogLevel::Error => Color::Red,
LogLevel::Warn => Color::Yellow,
LogLevel::Info => Color::Green,
};
table
.add_col(TextSpan::from(format!(
"{}",
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
)))
.add_col(TextSpan::from(" ["))
.add_col(
TextSpan::new(
format!(
"{:5}",
match record.level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
}
)
.as_str(),
)
.fg(fg),
)
.add_col(TextSpan::from("]: "))
.add_col(TextSpan::from(record.msg.as_ref()));
}
let table = table.build();
let props = LogboxPropsBuilder::from(props).with_log(table).build();
self.view.update(super::COMPONENT_LOG_BOX, props)
}
None => None,
}
}
pub(super) fn update_progress_bar(&mut self, filename: String) -> Option<(String, Msg)> {
if let Some(props) = self.view.get_props(COMPONENT_PROGRESS_BAR_FULL) {
let props = ProgressBarPropsBuilder::from(props)
.with_label(self.transfer.full.to_string())
.with_progress(self.transfer.full.calc_progress())
.build();
let _ = self.view.update(COMPONENT_PROGRESS_BAR_FULL, props);
}
match self.view.get_props(COMPONENT_PROGRESS_BAR_PARTIAL) {
Some(props) => {
let props = ProgressBarPropsBuilder::from(props)
.with_title(filename, Alignment::Center)
.with_label(self.transfer.partial.to_string())
.with_progress(self.transfer.partial.calc_progress())
.build();
self.view.update(COMPONENT_PROGRESS_BAR_PARTIAL, props)
}
None => None,
}
}
/// ### finalize_find
///
/// Finalize find process
fn finalize_find(&mut self) {
// Set found to none
self.browser.del_found();
// Restore tab
self.browser.change_tab(match self.browser.tab() {
FileExplorerTab::FindLocal => FileExplorerTab::Local,
FileExplorerTab::FindRemote => FileExplorerTab::Remote,
_ => FileExplorerTab::Local,
});
}
fn update_find_list(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_EXPLORER_FIND) {
None => None,
Some(props) => {
// Prepare files
let files: Vec<String> = self
.found()
.unwrap()
.iter_files()
.map(|x: &FsEntry| self.found().unwrap().fmt_file(x))
.collect();
let props = FileListPropsBuilder::from(props).with_files(files).build();
self.view.update(COMPONENT_EXPLORER_FIND, props)
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,363 +0,0 @@
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
use super::{FileExplorerTab, FileTransferActivity, FsEntry, InputMode, LogLevel, PopupType};
use std::path::PathBuf;
use tui::style::Color;
impl FileTransferActivity {
/// ### callback_nothing_to_do
///
/// Self titled
pub(super) fn callback_nothing_to_do(&mut self) {}
/// ### callback_change_directory
///
/// Callback for GOTO command
pub(super) fn callback_change_directory(&mut self, input: String) {
let dir_path: PathBuf = PathBuf::from(input);
match self.tab {
FileExplorerTab::Local => {
// If path is relative, concat pwd
let abs_dir_path: PathBuf = match dir_path.is_relative() {
true => {
let mut d: PathBuf = self.context.as_ref().unwrap().local.pwd();
d.push(dir_path);
d
}
false => dir_path,
};
self.local_changedir(abs_dir_path.as_path(), true);
}
FileExplorerTab::Remote => {
// If path is relative, concat pwd
let abs_dir_path: PathBuf = match dir_path.is_relative() {
true => match self.client.pwd() {
Ok(mut wkrdir) => {
wkrdir.push(dir_path);
wkrdir
}
Err(err) => {
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not retrieve current directory: {}", err),
));
return;
}
},
false => dir_path,
};
self.remote_changedir(abs_dir_path.as_path(), true);
}
}
}
/// ### callback_mkdir
///
/// Callback for MKDIR command (supports both local and remote)
pub(super) fn callback_mkdir(&mut self, input: String) {
match self.tab {
FileExplorerTab::Local => {
match self
.context
.as_mut()
.unwrap()
.local
.mkdir(PathBuf::from(input.as_str()).as_path())
{
Ok(_) => {
// Reload files
self.log(
LogLevel::Info,
format!("Created directory \"{}\"", input).as_ref(),
);
let wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
self.local_scan(wrkdir.as_path());
}
Err(err) => {
// Report err
self.log(
LogLevel::Error,
format!("Could not create directory \"{}\": {}", input, err).as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not create directory \"{}\": {}", input, err),
));
}
}
}
FileExplorerTab::Remote => {
match self.client.mkdir(PathBuf::from(input.as_str()).as_path()) {
Ok(_) => {
// Reload files
self.log(
LogLevel::Info,
format!("Created directory \"{}\"", input).as_ref(),
);
self.reload_remote_dir();
}
Err(err) => {
// Report err
self.log(
LogLevel::Error,
format!("Could not create directory \"{}\": {}", input, err).as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not create directory \"{}\": {}", input, err),
));
}
}
}
}
}
/// ### callback_rename
///
/// Callback for RENAME command (supports borth local and remote)
pub(super) fn callback_rename(&mut self, input: String) {
match self.tab {
FileExplorerTab::Local => {
let mut dst_path: PathBuf = PathBuf::from(input);
// Check if path is relative
if dst_path.as_path().is_relative() {
let mut wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
wrkdir.push(dst_path);
dst_path = wrkdir;
}
// Check if file entry exists
if let Some(entry) = self.local.files.get(self.local.index) {
let full_path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => file.abs_path.clone(),
};
// Rename file or directory and report status as popup
match self
.context
.as_mut()
.unwrap()
.local
.rename(entry, dst_path.as_path())
{
Ok(_) => {
// Reload files
self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
// Log
self.log(
LogLevel::Info,
format!(
"Renamed file \"{}\" to \"{}\"",
full_path.display(),
dst_path.display()
)
.as_ref(),
);
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Could not rename file \"{}\": {}",
full_path.display(),
err
)
.as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not rename file: {}", err),
))
}
}
}
}
FileExplorerTab::Remote => {
// Check if file entry exists
if let Some(entry) = self.remote.files.get(self.remote.index) {
let full_path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => file.abs_path.clone(),
};
// Rename file or directory and report status as popup
let dst_path: PathBuf = PathBuf::from(input);
match self.client.rename(entry, dst_path.as_path()) {
Ok(_) => {
// Reload files
if let Ok(path) = self.client.pwd() {
self.remote_scan(path.as_path());
}
// Log
self.log(
LogLevel::Info,
format!(
"Renamed file \"{}\" to \"{}\"",
full_path.display(),
dst_path.display()
)
.as_ref(),
);
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Could not rename file \"{}\": {}",
full_path.display(),
err
)
.as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not rename file: {}", err),
))
}
}
}
}
}
}
/// ### callback_delete_fsentry
///
/// Delete current selected fsentry in the currently selected TAB
pub(super) fn callback_delete_fsentry(&mut self) {
// Match current selected tab
match self.tab {
FileExplorerTab::Local => {
// Check if file entry exists
if let Some(entry) = self.local.files.get(self.local.index) {
let full_path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => file.abs_path.clone(),
};
// Delete file or directory and report status as popup
match self.context.as_mut().unwrap().local.remove(entry) {
Ok(_) => {
// Reload files
self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
// Log
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", full_path.display()).as_ref(),
);
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
full_path.display(),
err
)
.as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not delete file: {}", err),
))
}
}
}
}
FileExplorerTab::Remote => {
// Check if file entry exists
if let Some(entry) = self.remote.files.get(self.remote.index) {
let full_path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => file.abs_path.clone(),
};
// Delete file
match self.client.remove(entry) {
Ok(_) => {
self.reload_remote_dir();
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", full_path.display()).as_ref(),
);
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
full_path.display(),
err
)
.as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not delete file: {}", err),
))
}
}
}
}
}
}
/// ### callback_save_as
///
/// Call file upload, but save with input as name
/// Handled both local and remote tab
pub(super) fn callback_save_as(&mut self, input: String) {
match self.tab {
FileExplorerTab::Local => {
// Get pwd
let wrkdir: PathBuf = match self.client.pwd() {
Ok(p) => p,
Err(err) => {
self.log(
LogLevel::Error,
format!("Could not get current remote path: {}", err).as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not get current remote path: {}", err),
));
return;
}
};
let files: Vec<FsEntry> = self.local.files.clone();
// Get file at index
if let Some(entry) = files.get(self.local.index) {
// Call send (upload)
self.filetransfer_send(entry, wrkdir.as_path(), Some(input));
}
}
FileExplorerTab::Remote => {
let files: Vec<FsEntry> = self.remote.files.clone();
// Get file at index
if let Some(entry) = files.get(self.remote.index) {
// Call receive (download)
self.filetransfer_recv(
entry,
self.context.as_ref().unwrap().local.pwd().as_path(),
Some(input),
);
}
}
}
}
}

View File

@@ -1,718 +0,0 @@
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
use super::{
DialogCallback, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputEvent,
InputField, InputMode, LogLevel, OnInputSubmitCallback, PopupType,
};
use crossterm::event::KeyCode;
use std::path::PathBuf;
use tui::style::Color;
impl FileTransferActivity {
/// ### handle_input_event
///
/// Handle input event based on current input mode
pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
// NOTE: this is necessary due to this <https://github.com/rust-lang/rust/issues/59159>
// NOTE: Do you want my opinion about that issue? It's a bs and doesn't make any sense.
let popup: Option<PopupType> = match &self.input_mode {
InputMode::Popup(ptype) => Some(ptype.clone()),
_ => None,
};
match &self.input_mode {
InputMode::Explorer => self.handle_input_event_mode_explorer(ev),
InputMode::Popup(_) => {
if let Some(popup) = popup {
self.handle_input_event_mode_popup(ev, popup);
}
}
}
}
/// ### handle_input_event_mode_explorer
///
/// Input event handler for explorer mode
pub(super) fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) {
// Match input field
match self.input_field {
InputField::Explorer => match self.tab {
// Match current selected tab
FileExplorerTab::Local => self.handle_input_event_mode_explorer_tab_local(ev),
FileExplorerTab::Remote => self.handle_input_event_mode_explorer_tab_remote(ev),
},
InputField::Logs => self.handle_input_event_mode_explorer_log(ev),
}
}
/// ### handle_input_event_mode_explorer_tab_local
///
/// Input event handler for explorer mode when localhost tab is selected
pub(super) fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) {
// Match events
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Esc => {
// Handle quit event
// Create quit prompt dialog
self.input_mode = self.create_disconnect_popup();
}
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
KeyCode::Right => self.tab = FileExplorerTab::Remote, // <RIGHT> switch to right tab
KeyCode::Up => {
// Move index up
if self.local.index > 0 {
self.local.index -= 1;
}
}
KeyCode::Down => {
// Move index down
if self.local.index + 1 < self.local.files.len() {
self.local.index += 1;
}
}
KeyCode::PageUp => {
// Move index up (fast)
if self.local.index > 8 {
self.local.index = self.local.index - 8; // Decrease by `8` if possible
} else {
self.local.index = 0; // Set to 0 otherwise
}
}
KeyCode::PageDown => {
// Move index down (fast)
if self.local.index + 8 >= self.local.files.len() {
// If overflows, set to size
self.local.index = self.local.files.len() - 1;
} else {
self.local.index = self.local.index + 8; // Increase by `8`
}
}
KeyCode::Enter => {
// Match selected file
let local_files: Vec<FsEntry> = self.local.files.clone();
if let Some(entry) = local_files.get(self.local.index) {
// If directory, enter directory, otherwise check if symlink
match entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true)
}
FsEntry::File(file) => {
// Check if symlink
if let Some(realpath) = &file.symlink {
// Stat realpath
match self
.context
.as_ref()
.unwrap()
.local
.stat(realpath.as_path())
{
Ok(real_file) => {
// If real file is a directory, enter directory
if let FsEntry::Directory(real_dir) = real_file {
self.local_changedir(
real_dir.abs_path.as_path(),
true,
)
}
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Failed to stat file \"{}\": {}",
realpath.display(),
err
)
.as_ref(),
);
self.input_mode =
InputMode::Popup(PopupType::Alert(
Color::Red,
format!(
"Failed to stat file \"{}\": {}",
realpath.display(),
err
),
));
}
}
}
}
}
}
}
KeyCode::Backspace => {
// Go to previous directory
if let Some(d) = self.local.popd() {
self.local_changedir(d.as_path(), false);
}
}
KeyCode::Delete => {
// Get file at index
if let Some(entry) = self.local.files.get(self.local.index) {
// Get file name
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Show delete prompt
self.input_mode = InputMode::Popup(PopupType::YesNo(
format!("Delete file \"{}\"", file_name),
FileTransferActivity::callback_delete_fsentry,
FileTransferActivity::callback_nothing_to_do,
))
}
}
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
'g' | 'G' => {
// Goto
// Show input popup
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Change working directory"),
FileTransferActivity::callback_change_directory,
));
}
'd' | 'D' => {
// Make directory
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert directory name"),
FileTransferActivity::callback_mkdir,
));
}
'h' | 'H' => {
// Show help
self.input_mode = InputMode::Popup(PopupType::Help);
}
'i' | 'I' => {
// Show file info
self.input_mode = InputMode::Popup(PopupType::FileInfo);
}
'r' | 'R' => {
// Rename
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert new name"),
FileTransferActivity::callback_rename,
));
}
's' | 'S' => {
// Save as...
// Ask for input
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Save as..."),
FileTransferActivity::callback_save_as,
));
}
'u' | 'U' => {
// Go to parent directory
// Get pwd
let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true);
}
}
' ' => {
// Get pwd
let wrkdir: PathBuf = match self.client.pwd() {
Ok(p) => p,
Err(err) => {
self.log(
LogLevel::Error,
format!("Could not get current remote path: {}", err)
.as_ref(),
);
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not get current remote path: {}", err),
));
return;
}
};
// Get files
let files: Vec<FsEntry> = self.local.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
// Get file at index
if let Some(entry) = files.get(self.local.index) {
// Call upload
self.filetransfer_send(entry, wrkdir.as_path(), None);
}
}
_ => { /* Nothing to do */ }
},
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_explorer_tab_local
///
/// Input event handler for explorer mode when remote tab is selected
pub(super) fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) {
// Match events
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Esc => {
// Handle quit event
// Create quit prompt dialog
self.input_mode = self.create_disconnect_popup();
}
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
KeyCode::Left => self.tab = FileExplorerTab::Local, // <LEFT> switch to local tab
KeyCode::Up => {
// Move index up
if self.remote.index > 0 {
self.remote.index -= 1;
}
}
KeyCode::Down => {
// Move index down
if self.remote.index + 1 < self.remote.files.len() {
self.remote.index += 1;
}
}
KeyCode::PageUp => {
// Move index up (fast)
if self.remote.index > 8 {
self.remote.index = self.remote.index - 8; // Decrease by `8` if possible
} else {
self.remote.index = 0; // Set to 0 otherwise
}
}
KeyCode::PageDown => {
// Move index down (fast)
if self.remote.index + 8 >= self.remote.files.len() {
// If overflows, set to size
self.remote.index = self.remote.files.len() - 1;
} else {
self.remote.index = self.remote.index + 8; // Increase by `8`
}
}
KeyCode::Enter => {
// Match selected file
let files: Vec<FsEntry> = self.remote.files.clone();
if let Some(entry) = files.get(self.remote.index) {
// If directory, enter directory; if file, check if is symlink
match entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true)
}
FsEntry::File(file) => {
// Check if symlink
if let Some(realpath) = &file.symlink {
// Stat realpath
match self.client.stat(realpath.as_path()) {
Ok(real_file) => {
// If real file is a directory, enter directory
if let FsEntry::Directory(real_dir) = real_file {
self.remote_changedir(
real_dir.abs_path.as_path(),
true,
)
}
}
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Failed to stat file \"{}\": {}",
realpath.display(),
err
)
.as_ref(),
);
self.input_mode =
InputMode::Popup(PopupType::Alert(
Color::Red,
format!(
"Failed to stat file \"{}\": {}",
realpath.display(),
err
),
));
}
}
}
}
}
}
}
KeyCode::Backspace => {
// Go to previous directory
if let Some(d) = self.remote.popd() {
self.remote_changedir(d.as_path(), false);
}
}
KeyCode::Delete => {
// Get file at index
if let Some(entry) = self.remote.files.get(self.remote.index) {
// Get file name
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Show delete prompt
self.input_mode = InputMode::Popup(PopupType::YesNo(
format!("Delete file \"{}\"", file_name),
FileTransferActivity::callback_delete_fsentry,
FileTransferActivity::callback_nothing_to_do,
))
}
}
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
'g' | 'G' => {
// Goto
// Show input popup
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Change working directory"),
FileTransferActivity::callback_change_directory,
));
}
'd' | 'D' => {
// Make directory
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert directory name"),
FileTransferActivity::callback_mkdir,
));
}
'h' | 'H' => {
// Show help
self.input_mode = InputMode::Popup(PopupType::Help);
}
'i' | 'I' => {
// Show file info
self.input_mode = InputMode::Popup(PopupType::FileInfo);
}
'r' | 'R' => {
// Rename
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert new name"),
FileTransferActivity::callback_rename,
));
}
's' | 'S' => {
// Save as...
// Ask for input
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Save as..."),
FileTransferActivity::callback_save_as,
));
}
'u' | 'U' => {
// Go to parent directory
// Get pwd
match self.client.pwd() {
Ok(path) => {
if let Some(parent) = path.as_path().parent() {
self.remote_changedir(parent, true);
}
}
Err(err) => {
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not change working directory: {}", err),
))
}
}
}
' ' => {
// Get files
let files: Vec<FsEntry> = self.remote.files.clone(); // Otherwise self is borrowed both as mutable and immutable...
// Get file at index
if let Some(entry) = files.get(self.remote.index) {
// Call upload
self.filetransfer_recv(
entry,
self.context.as_ref().unwrap().local.pwd().as_path(),
None,
);
}
}
_ => { /* Nothing to do */ }
},
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_explorer_log
///
/// Input even handler for explorer mode when log tab is selected
pub(super) fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) {
// Match event
let records_block: usize = 16;
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Esc => {
// Handle quit event
// Create quit prompt dialog
self.input_mode = self.create_disconnect_popup();
}
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
KeyCode::Down => {
// NOTE: Twisted logic
// Decrease log index
if self.log_index > 0 {
self.log_index = self.log_index - 1;
}
}
KeyCode::Up => {
// NOTE: Twisted logic
// Increase log index
if self.log_index + 1 < self.log_records.len() {
self.log_index = self.log_index + 1;
}
}
KeyCode::PageDown => {
// NOTE: Twisted logic
// Fast decreasing of log index
if self.log_index >= records_block {
self.log_index = self.log_index - records_block; // Decrease by `records_block` if possible
} else {
self.log_index = 0; // Set to 0 otherwise
}
}
KeyCode::PageUp => {
// NOTE: Twisted logic
// Fast increasing of log index
if self.log_index + records_block >= self.log_records.len() {
// If overflows, set to size
self.log_index = self.log_records.len() - 1;
} else {
self.log_index = self.log_index + records_block; // Increase by `records_block`
}
}
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
_ => { /* Nothing to do */ }
},
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_explorer
///
/// Input event handler for popup mode. Handler is then based on Popup type
pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) {
match popup {
PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
PopupType::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev),
PopupType::Help => self.handle_input_event_mode_popup_help(ev),
PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
PopupType::Progress(_) => self.handle_input_event_mode_popup_progress(ev),
PopupType::Wait(_) => self.handle_input_event_mode_popup_wait(ev),
PopupType::YesNo(_, yes_cb, no_cb) => {
self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
}
}
}
/// ### handle_input_event_mode_popup_alert
///
/// Input event handler for popup alert
pub(super) fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
// If enter, close popup
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter => {
// Set input mode back to explorer
self.input_mode = InputMode::Explorer;
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_popup_fileinfo
///
/// Input event handler for popup fileinfo
pub(super) fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) {
// If enter, close popup
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter | KeyCode::Esc => {
// Set input mode back to explorer
self.input_mode = InputMode::Explorer;
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_popup_help
///
/// Input event handler for popup help
pub(super) fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
// If enter, close popup
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter | KeyCode::Esc => {
// Set input mode back to explorer
self.input_mode = InputMode::Explorer;
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_popup_fatal
///
/// Input event handler for popup alert
pub(super) fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) {
// If enter, close popup
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter => {
// Set quit to true; since a fatal error happened
self.disconnect();
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_popup_input
///
/// Input event handler for input popup
pub(super) fn handle_input_event_mode_popup_input(
&mut self,
ev: &InputEvent,
cb: OnInputSubmitCallback,
) {
// If enter, close popup, otherwise push chars to input
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Esc => {
// Abort input
// Clear current input text
self.input_txt.clear();
// Set mode back to explorer
self.input_mode = InputMode::Explorer;
}
KeyCode::Enter => {
// Submit
let input_text: String = self.input_txt.clone();
// Clear current input text
self.input_txt.clear();
// Set mode back to explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
self.input_mode = InputMode::Explorer;
// Call cb
cb(self, input_text);
}
KeyCode::Char(ch) => self.input_txt.push(ch),
KeyCode::Backspace => {
let _ = self.input_txt.pop();
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_explorer_alert
///
/// Input event handler for popup alert
pub(super) fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) {
// There's nothing you can do here I guess... maybe ctrl+c in the future idk
match ev {
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_explorer_alert
///
/// Input event handler for popup alert
pub(super) fn handle_input_event_mode_popup_wait(&mut self, ev: &InputEvent) {
// There's nothing you can do here I guess... maybe ctrl+c in the future idk
match ev {
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_explorer_alert
///
/// Input event handler for popup alert
pub(super) fn handle_input_event_mode_popup_yesno(
&mut self,
ev: &InputEvent,
yes_cb: DialogCallback,
no_cb: DialogCallback,
) {
// If enter, close popup, otherwise move dialog option
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter => {
// @! Set input mode to Explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
self.input_mode = InputMode::Explorer;
// Check if user selected yes or not
match self.choice_opt {
DialogYesNoOption::No => no_cb(self),
DialogYesNoOption::Yes => yes_cb(self),
}
// Reset choice option to yes
self.choice_opt = DialogYesNoOption::Yes;
}
KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Set to NO
KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Set to YES
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
}

Some files were not shown because too many files have changed in this diff Show More