Compare commits
444 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c1e4fef7a | ||
|
|
ddd4824567 | ||
|
|
22bb143820 | ||
|
|
78b918087e | ||
|
|
91ce709d7b | ||
|
|
214ec0c5a5 | ||
|
|
0cb9254e63 | ||
|
|
4bbb339844 | ||
|
|
92b081c076 | ||
|
|
32ab0267fb | ||
|
|
8a0f190474 | ||
|
|
a189a4c754 | ||
|
|
b7b765c16e | ||
|
|
7713c6c21d | ||
|
|
c49184dcb1 | ||
|
|
fc3803991a | ||
|
|
f6011543c0 | ||
|
|
7390bb58c5 | ||
|
|
81482b47f4 | ||
|
|
6cb1dcaa43 | ||
|
|
78e4a4899c | ||
|
|
d15997cca0 | ||
|
|
1558f4ffe4 | ||
|
|
764cca73d1 | ||
|
|
1757eb5bec | ||
|
|
6cfac57162 | ||
|
|
d36d330244 | ||
|
|
4203de6d3f | ||
|
|
098e3c709a | ||
|
|
1a8e151efb | ||
|
|
6c8d22d396 | ||
|
|
a0ee1d886a | ||
|
|
bf6f1625ec | ||
|
|
3271377b7e | ||
|
|
f36bb65b45 | ||
|
|
c722982c39 | ||
|
|
e1109fff15 | ||
|
|
59c6567ff3 | ||
|
|
61f6901767 | ||
|
|
8277c80860 | ||
|
|
4093ba169c | ||
|
|
e3a9d253f7 | ||
|
|
80c67c8aa8 | ||
|
|
421969c3da | ||
|
|
e15b79750b | ||
|
|
37abe596c7 | ||
|
|
e6b44e1461 | ||
|
|
b9cb961da6 | ||
|
|
55c4b777fb | ||
|
|
2231727adb | ||
|
|
0a7e29d92f | ||
|
|
a105a42519 | ||
|
|
d20c2c786b | ||
|
|
14abed44a5 | ||
|
|
65fee64132 | ||
|
|
348688b5ce | ||
|
|
0cb84fc531 | ||
|
|
8a36c459d5 | ||
|
|
c7414ab070 | ||
|
|
d16f9e7ad8 | ||
|
|
3cbd2ed013 | ||
|
|
398f518b2f | ||
|
|
97a62def11 | ||
|
|
7ed49126a4 | ||
|
|
d976a34705 | ||
|
|
8b01c71819 | ||
|
|
1ee5e368f6 | ||
|
|
8fc71aae66 | ||
|
|
0589cc2f0c | ||
|
|
931a66498f | ||
|
|
8ec0a0289a | ||
|
|
2d8ce475f8 | ||
|
|
f8766722d4 | ||
|
|
1319e0b17b | ||
|
|
5e17bf0736 | ||
|
|
ca50bddc56 | ||
|
|
c78fc583b0 | ||
|
|
04cafc4181 | ||
|
|
f8300fa587 | ||
|
|
407ca567f1 | ||
|
|
320fd5c2dd | ||
|
|
e4c616245a | ||
|
|
5986130dfa | ||
|
|
19610479bd | ||
|
|
e3d2151bad | ||
|
|
89d205e946 | ||
|
|
15d13af7d5 | ||
|
|
3df8ed13a4 | ||
|
|
5a4a364250 | ||
|
|
00bee04c2c | ||
|
|
4475e8c24c | ||
|
|
5a49987338 | ||
|
|
76f1f1dcf3 | ||
|
|
0175cfbfb6 | ||
|
|
48483a5c99 | ||
|
|
71e4cc9413 | ||
|
|
a00c0117a2 | ||
|
|
c1bc81c664 | ||
|
|
1b7ef30a50 | ||
|
|
efad2b96db | ||
|
|
56a200499e | ||
|
|
1c58f1d623 | ||
|
|
6cdd56e22f | ||
|
|
53acb847bc | ||
|
|
2b006457c7 | ||
|
|
fbc1672e3d | ||
|
|
542123ce04 | ||
|
|
59eb7f7935 | ||
|
|
74482d6e2c | ||
|
|
529ac2f778 | ||
|
|
b73c3228e4 | ||
|
|
26f7c1f9d1 | ||
|
|
f078232499 | ||
|
|
8cd91f6df3 | ||
|
|
5670c0f975 | ||
|
|
99d4177e89 | ||
|
|
d07b1c86be | ||
|
|
fe494c52b1 | ||
|
|
bc50328006 | ||
|
|
90afe204b1 | ||
|
|
d981b77ed7 | ||
|
|
541a9a55b5 | ||
|
|
cd3fc15bcb | ||
|
|
4e50038b41 | ||
|
|
a8354ee38f | ||
|
|
c4907c8ce5 | ||
|
|
3c3c680b00 | ||
|
|
3a32f45334 | ||
|
|
8e1843f90b | ||
|
|
dc01de98dc | ||
|
|
971061d231 | ||
|
|
d04dd831e4 | ||
|
|
8dcfda9787 | ||
|
|
c938021bef | ||
|
|
3fcf0fb72a | ||
|
|
e99406fbe4 | ||
|
|
60073be993 | ||
|
|
d3df4c7020 | ||
|
|
adce92e312 | ||
|
|
4973545fe1 | ||
|
|
8425c97102 | ||
|
|
1876b8bdc7 | ||
|
|
99ff3a8e45 | ||
|
|
cae251905c | ||
|
|
f738ce90e0 | ||
|
|
2f2bb20550 | ||
|
|
591ede0507 | ||
|
|
09665fead3 | ||
|
|
60ee2f1c2b | ||
|
|
d6d8197869 | ||
|
|
b1da42b543 | ||
|
|
e874550d29 | ||
|
|
6cd9657446 | ||
|
|
2790c613aa | ||
|
|
7b868bc60b | ||
|
|
c1b3511c06 | ||
|
|
1e3c859ae0 | ||
|
|
faab67800f | ||
|
|
7f9d92cbe9 | ||
|
|
4e287a0231 | ||
|
|
8c9c331d7e | ||
|
|
37ce54051e | ||
|
|
f31b047671 | ||
|
|
aaff616332 | ||
|
|
cfc1b73c74 | ||
|
|
0d2967423b | ||
|
|
cb3681ffdd | ||
|
|
4c343c6f34 | ||
|
|
ff158add1f | ||
|
|
2c64cd5cce | ||
|
|
c43ab28fb0 | ||
|
|
dd00e1f55a | ||
|
|
2b3acee97c | ||
|
|
d96558c3df | ||
|
|
b8db557ffe | ||
|
|
300256b196 | ||
|
|
7ba1b316f7 | ||
|
|
822d41e885 | ||
|
|
0253572975 | ||
|
|
0328c1de81 | ||
|
|
c6ee9dedb9 | ||
|
|
88136811b9 | ||
|
|
7c816d0822 | ||
|
|
a5fe62e502 | ||
|
|
892be42988 | ||
|
|
c0eb98c3f9 | ||
|
|
1ddbd0aa4e | ||
|
|
4f1e505a7a | ||
|
|
dcec804681 | ||
|
|
a127f46a06 | ||
|
|
c8d277c51d | ||
|
|
42477d6893 | ||
|
|
4c1fb826be | ||
|
|
a0c29d1174 | ||
|
|
2f0b340fe0 | ||
|
|
cbe242bb94 | ||
|
|
a088c6dbd3 | ||
|
|
036aba2420 | ||
|
|
60269c7777 | ||
|
|
e90f561584 | ||
|
|
4bd18c1386 | ||
|
|
fe51185f74 | ||
|
|
a35395bd51 | ||
|
|
f1225b1ff3 | ||
|
|
63d727d5c5 | ||
|
|
46864ee23a | ||
|
|
46728d8bc3 | ||
|
|
daa9a7c012 | ||
|
|
0f595b8cbf | ||
|
|
26b001a5fc | ||
|
|
3704d2520d | ||
|
|
41e89605f0 | ||
|
|
065ea59114 | ||
|
|
64bb2ed101 | ||
|
|
c48c73909d | ||
|
|
68d113f335 | ||
|
|
8bb66b53e0 | ||
|
|
84c36f1789 | ||
|
|
2214c1dee3 | ||
|
|
150a3cf346 | ||
|
|
b31de185c5 | ||
|
|
477930bef9 | ||
|
|
03bbd6420e | ||
|
|
b21607cd77 | ||
|
|
6c6dadc4e7 | ||
|
|
08b8946429 | ||
|
|
23882df474 | ||
|
|
d8547d8f21 | ||
|
|
c6de101808 | ||
|
|
d0688be5cb | ||
|
|
35f37cc2d3 | ||
|
|
81ae310e3d | ||
|
|
3be890c63a | ||
|
|
64a08e1440 | ||
|
|
fe5c35d789 | ||
|
|
df391dfb6f | ||
|
|
f5ac4207e8 | ||
|
|
b8c54b53d9 | ||
|
|
6be9294e11 | ||
|
|
7676e6e3a1 | ||
|
|
9776ecbe60 | ||
|
|
a1632492ed | ||
|
|
8d74d4c4e5 | ||
|
|
e29ce3d0dd | ||
|
|
e6b952966c | ||
|
|
1ba139aed1 | ||
|
|
f136057484 | ||
|
|
47cd112e69 | ||
|
|
871a02c8b5 | ||
|
|
44ba1111af | ||
|
|
52b35f9232 | ||
|
|
f8a448f5e9 | ||
|
|
37da49f4f8 | ||
|
|
c0ae922264 | ||
|
|
91081cb86a | ||
|
|
af678802bb | ||
|
|
66068ec73c | ||
|
|
32e939c183 | ||
|
|
b610da16a9 | ||
|
|
5dfcba3c51 | ||
|
|
d48e05cd74 | ||
|
|
6f4cb46d94 | ||
|
|
5886d90d16 | ||
|
|
ade7160c20 | ||
|
|
0c22b322ae | ||
|
|
a5ba118393 | ||
|
|
7acf119c77 | ||
|
|
bd00ba7971 | ||
|
|
f94a811dd9 | ||
|
|
deb46eaf41 | ||
|
|
7812d1c37b | ||
|
|
c2d401723f | ||
|
|
142169ee42 | ||
|
|
55e884889c | ||
|
|
67e36fa38f | ||
|
|
3dbe024029 | ||
|
|
96b7aff3b6 | ||
|
|
1ad75adf87 | ||
|
|
423f84353d | ||
|
|
de3983e53f | ||
|
|
15bea93da8 | ||
|
|
f5f84ac346 | ||
|
|
d44e2d8eb0 | ||
|
|
39bcd5e83b | ||
|
|
3dd6dbbb37 | ||
|
|
5a5d2fb162 | ||
|
|
08b855e779 | ||
|
|
8a22259eba | ||
|
|
24d727dec8 | ||
|
|
93c4d1c2e3 | ||
|
|
66f9ace7bd | ||
|
|
f3788ef61a | ||
|
|
b9d801e8bc | ||
|
|
3a1c6cac95 | ||
|
|
8ff7040a0a | ||
|
|
118467e079 | ||
|
|
51f0c56b84 | ||
|
|
88a014807f | ||
|
|
7a5861f32f | ||
|
|
63a627e4d0 | ||
|
|
f0d87ff8c4 | ||
|
|
30c2aa144b | ||
|
|
bf4f24ceec | ||
|
|
3520499289 | ||
|
|
977becd5c9 | ||
|
|
fd4c4a3772 | ||
|
|
7c6a22d3e1 | ||
|
|
41360bb2c5 | ||
|
|
9b00feb286 | ||
|
|
f2681ba0b9 | ||
|
|
7c9548c668 | ||
|
|
7caa4575bd | ||
|
|
16a8fc3ad8 | ||
|
|
cd31cc1fc9 | ||
|
|
2f3c1e7f7f | ||
|
|
61404cfbec | ||
|
|
12ec87235f | ||
|
|
b822e2131a | ||
|
|
3b99a5401f | ||
|
|
5156928bdc | ||
|
|
20e7a66ded | ||
|
|
411f734aef | ||
|
|
2c898a91da | ||
|
|
28f8c82ccf | ||
|
|
54342178e0 | ||
|
|
11af0666ea | ||
|
|
a72ecb39e0 | ||
|
|
47c23c6828 | ||
|
|
00e2a1db31 | ||
|
|
36cc6f445a | ||
|
|
ad9ed8facf | ||
|
|
4a7eb831b8 | ||
|
|
28d51fdcf6 | ||
|
|
4ebb8a3b51 | ||
|
|
e76dcd4638 | ||
|
|
eaada667b3 | ||
|
|
5bc46dd720 | ||
|
|
fe6e0eeab5 | ||
|
|
7e075c5b3d | ||
|
|
86dfc2bf97 | ||
|
|
bda69c661f | ||
|
|
8f3fe14843 | ||
|
|
5c952169b3 | ||
|
|
371ba5c399 | ||
|
|
0f4649ab8d | ||
|
|
2b6f7e4868 | ||
|
|
2e3dc7f7a5 | ||
|
|
2d1af97590 | ||
|
|
1e813b0d4d | ||
|
|
cdbfb3977b | ||
|
|
e9d3684f87 | ||
|
|
021bcf0c97 | ||
|
|
fba6da8120 | ||
|
|
9dbfbd0dc3 | ||
|
|
5980bc1fcb | ||
|
|
b2aaf5c57f | ||
|
|
e17224184e | ||
|
|
042007d9ed | ||
|
|
c832a6cb6f | ||
|
|
3aed691cb8 | ||
|
|
7b92bd22e7 | ||
|
|
581badd101 | ||
|
|
c4bc0af58a | ||
|
|
f75dd5d4e3 | ||
|
|
a4544e35f6 | ||
|
|
56d705e253 | ||
|
|
43c177e04d | ||
|
|
5a2d0b7b0b | ||
|
|
43298dee1c | ||
|
|
26014ecb58 | ||
|
|
57dd06d774 | ||
|
|
e21eb72705 | ||
|
|
00b1dbdffa | ||
|
|
55f74a8244 | ||
|
|
db0c54b781 | ||
|
|
5c9cb7eece | ||
|
|
b2d816d20c | ||
|
|
5b832cea8b | ||
|
|
c1780230e9 | ||
|
|
b90953f65e | ||
|
|
44041863ad | ||
|
|
2692041329 | ||
|
|
4a8ea185e6 | ||
|
|
98861a6bbd | ||
|
|
135d947c39 | ||
|
|
f3cbbb8d81 | ||
|
|
3ecf172fb5 | ||
|
|
c9871a0079 | ||
|
|
744e5a251a | ||
|
|
e89198d9bb | ||
|
|
e61e0c018c | ||
|
|
b57763e688 | ||
|
|
3ccbb325b3 | ||
|
|
3ea345ee8f | ||
|
|
ed2c50daac | ||
|
|
f27d5ea08f | ||
|
|
35ab9ae202 | ||
|
|
bd99665d1c | ||
|
|
93bab299ec | ||
|
|
eb33f93322 | ||
|
|
d165b699f0 | ||
|
|
7817295d8e | ||
|
|
a986e531d9 | ||
|
|
8a3b652dcd | ||
|
|
efbea63154 | ||
|
|
61045fa548 | ||
|
|
da5e1f315d | ||
|
|
85c57ce027 | ||
|
|
6682c07eb6 | ||
|
|
4e887c3429 | ||
|
|
6435271be8 | ||
|
|
c9a77fa65d | ||
|
|
cc5399d36e | ||
|
|
ca1aa5675a | ||
|
|
e21bfbbd14 | ||
|
|
e948d598b0 | ||
|
|
0173d67a3b | ||
|
|
025547a3dc | ||
|
|
af830d603d | ||
|
|
669fd23868 | ||
|
|
4ff7fc079c | ||
|
|
7f24d6db5c | ||
|
|
8c8d01c29c | ||
|
|
d23e6bb60c | ||
|
|
87aa900bc6 | ||
|
|
e8e4cd22a1 | ||
|
|
780cf592e4 | ||
|
|
45db58a0b3 | ||
|
|
f5218bc582 | ||
|
|
c5e2e02415 | ||
|
|
e088772685 | ||
|
|
859daa3107 | ||
|
|
56c580fc80 | ||
|
|
7a9ee697ff | ||
|
|
c16a2f6441 | ||
|
|
b3c4385617 | ||
|
|
e92370bd05 | ||
|
|
da0d5231bf | ||
|
|
c1f6308795 | ||
|
|
54ab24fc0c | ||
|
|
d99efb9de4 | ||
|
|
0c9ed38eb7 | ||
|
|
ec801f1555 | ||
|
|
e0d8879961 |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: ['https://www.buymeacoffee.com/veeso']
|
||||
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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
@@ -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
@@ -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.
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
@@ -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
|
||||
21
.github/workflows/aur-pub.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: aur-pub
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
aur-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Publish AUR package
|
||||
uses: KSXGitHub/github-actions-deploy-aur@v2.2.3
|
||||
with:
|
||||
pkgname: termscp
|
||||
pkgbuild: ./dist/pkgs/arch/PKGBUILD
|
||||
commit_username: ${{ secrets.AUR_USERNAME }}
|
||||
commit_email: ${{ secrets.AUR_EMAIL }}
|
||||
ssh_private_key: ${{ secrets.AUR_KEY }}
|
||||
commit_message: Update AUR package
|
||||
ssh_keyscan_types: rsa,dsa,ecdsa,ed25519
|
||||
37
.github/workflows/coverage.yml
vendored
Normal 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
@@ -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
|
||||
23
.github/workflows/linux.yml
vendored
@@ -11,24 +11,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- 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: nightly
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --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"
|
||||
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
|
||||
- name: Coverage with grcov
|
||||
uses: actions-rs/grcov@v0.1
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: cargo clippy -- -Dwarnings
|
||||
|
||||
4
.github/workflows/macos.yml
vendored
@@ -12,8 +12,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
run: cargo build
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
run: cargo test --verbose --lib --features github-actions -- --test-threads 1
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
4
.github/workflows/windows.yml
vendored
@@ -12,8 +12,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
run: cargo build
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
run: cargo test --verbose --lib --features github-actions -- --test-threads 1
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
250
CHANGELOG.md
@@ -1,6 +1,15 @@
|
||||
# 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)
|
||||
@@ -11,6 +20,247 @@
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
284
CONTRIBUTING.md
@@ -4,12 +4,91 @@ Before contributing to this repository, please first discuss the change you wish
|
||||
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,190 +96,43 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
|
||||
|
||||
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/veeso/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)
|
||||
- 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`) and (if a test server is not available) for file transfers.
|
||||
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. In changelog report changes under a chapter called `PR{PULL_REQUEST_NUMBER}` (e.g. PR12).
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
943
Cargo.lock
generated
106
Cargo.toml
@@ -1,54 +1,17 @@
|
||||
[package]
|
||||
name = "termscp"
|
||||
version = "0.3.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/veeso/termscp"
|
||||
repository = "https://github.com/veeso/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"
|
||||
edition = "2018"
|
||||
homepage = "https://veeso.github.io/termscp/"
|
||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.2.1"
|
||||
bytesize = "1.0.1"
|
||||
chrono = "0.4.19"
|
||||
content_inspector = "0.2.4"
|
||||
crossterm = "0.18.2"
|
||||
dirs = "3.0.1"
|
||||
edit = "0.1.2"
|
||||
ftp4 = { version = "^4.0.2", features = ["secure"] }
|
||||
getopts = "0.2.21"
|
||||
hostname = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
magic-crypt = "3.1.6"
|
||||
rand = "0.8.0"
|
||||
regex = "1.4.2"
|
||||
rpassword = "5.0.0"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
ssh2 = "0.9.0"
|
||||
tempfile = "3.1.0"
|
||||
textwrap = "0.13.1"
|
||||
toml = "0.5.8"
|
||||
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
|
||||
unicode-width = "0.1.7"
|
||||
whoami = "1.0.1"
|
||||
|
||||
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
keyring = "0.10.1"
|
||||
|
||||
[[bin]]
|
||||
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"
|
||||
@@ -58,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
@@ -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-2021 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-2021 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.
|
||||
|
||||
394
README.md
@@ -1,326 +1,106 @@
|
||||
# TermSCP
|
||||
# termscp
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://codecov.io/gh/veeso/termscp)
|
||||
<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.3.1 (18/01/2021)
|
||||
<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>
|
||||
|
||||
[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
|
||||
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](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-)
|
||||
- [AUR Package 🔼](#aur-package-)
|
||||
- [Chocolatey 🍫](#chocolatey-)
|
||||
- [Brew 🍻](#brew-)
|
||||
- [Usage ❓](#usage-)
|
||||
- [Address argument 🌎](#address-argument-)
|
||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||
- [Bookmarks ⭐](#bookmarks-)
|
||||
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
||||
- [Configuration ⚙️](#configuration-️)
|
||||
- [SSH Key Storage 🔐](#ssh-key-storage-)
|
||||
- [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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 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 is 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
|
||||
- Bookmarks and recent connections can be saved to access quickly to your favourite hosts
|
||||
- Supports text editors to view and edit text files
|
||||
- Supports both SFTP/SCP authentication through SSH keys and username/password
|
||||
- User customization directly from the user interface
|
||||
- Compatible with Windows, Linux, BSD and MacOS
|
||||
- Written in Rust
|
||||
- Easy to extend with new file transfers protocols
|
||||
- Developed keeping an eye on performance
|
||||
- 🖥 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
|
||||
```
|
||||
|
||||
Requirements:
|
||||
while if you're a Windows user, you can install termscp with [Chocolatey](https://chocolatey.org/).
|
||||
|
||||
- Linux
|
||||
- pkg-config
|
||||
- libssh2
|
||||
- openssl
|
||||
For more information or other platforms, please visit [veeso.github.io](https://veeso.github.io/termscp/#get-started) to view all installation methods.
|
||||
|
||||
### Deb package 📦
|
||||
### Requirements ❗
|
||||
|
||||
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.1_amd64.deb`
|
||||
- **Linux** users:
|
||||
- libssh
|
||||
- libdbus-1
|
||||
- **BSD** users:
|
||||
- libssh
|
||||
|
||||
then install through dpkg:
|
||||
### Optional Requirements ✔️
|
||||
|
||||
```sh
|
||||
dpkg -i termscp_*.deb
|
||||
# Or even better with gdebi
|
||||
gdebi termscp_*.deb
|
||||
```
|
||||
These requirements are not forcely required to run termscp, but to enjoy all of its features
|
||||
|
||||
### RPM package 📦
|
||||
|
||||
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.1-1.x86_64.rpm`
|
||||
|
||||
then install through rpm:
|
||||
|
||||
```sh
|
||||
rpm -U termscp_*.rpm
|
||||
```
|
||||
|
||||
### AUR Package 🔼
|
||||
|
||||
On Arch Linux based distribution, you can install termscp using for example [yay](https://github.com/Jguer/yay), which I recommend to install AUR packages.
|
||||
|
||||
```sh
|
||||
yay -S termscp
|
||||
```
|
||||
|
||||
### 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/veeso/termscp/releases/latest/download/termscp.0.3.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 veeso/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 (*defined in configuration*) 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 (*defined in configuration*) 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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
To create a bookmark, just fulfill the authentication form and then input `CTRL+S`; you'll then be asked to give a name to your bookmark, and tadah, the bookmark has been created.
|
||||
If you go to [gallery](#gallery-), there is a GIF showing how bookmarks work 💪.
|
||||
|
||||
### Are my passwords Safe 😈
|
||||
|
||||
Well, kinda.
|
||||
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? Well, depends on your operating system:
|
||||
|
||||
On Windows and MacOS the passwords are (if possible, but should be) in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system.
|
||||
|
||||
On Linux and BSD, on the other hand the key used to encrypt your passwords is stored on your drive. So it's 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 😉.
|
||||
Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but for different reasons I preferred not to make it available for this configuration. If you want to read more about my decision read [this issue](https://github.com/veeso/termscp/issues/2), while if you think this might have been implemented differently feel free to open an issue with your proposal.
|
||||
|
||||
---
|
||||
|
||||
## 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. [View more](#configuration-️)
|
||||
|
||||
---
|
||||
|
||||
## 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, 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:
|
||||
|
||||
- **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument.
|
||||
- **Text Editor**: the text editor to use. By default termscp will find the default editor for you; with this option you can force an editor to be used (e.g. `vim`). **Also GUI editors are supported**, unless they `nohup` from the parent process so if you ask: yes, you can use `notepad.exe`, and no: **Visual Studio Code doesn't work**.
|
||||
- **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway.
|
||||
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
|
||||
|
||||
### SSH Key Storage 🔐
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| `<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 | List |
|
||||
| `<N>` | Create new file with provided name | New |
|
||||
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
|
||||
| `<Q>` | Quit TermSCP | Quit |
|
||||
| `<R>` | Rename file | Rename |
|
||||
| `<U>` | Go to parent directory | Upper |
|
||||
| `<DEL>` | Delete file | |
|
||||
| `<CTRL+C>` | Abort file transfer process | |
|
||||
[](https://www.buymeacoffee.com/veeso)
|
||||
|
||||
---
|
||||
|
||||
@@ -332,43 +112,61 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
|
||||
|
||||
## Known issues 🧻
|
||||
|
||||
- `NoSuchFileOrDirectory` on connect (WSL): 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).
|
||||
- `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 🧪
|
||||
|
||||
- **Custom explorer format**: possibility to customize the file line in the explorer directly from configuration, with the possibility to choose with information to display.
|
||||
- **Find command in explorer**: possibility to search for files in explorers.
|
||||
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)
|
||||
- [rust-ftp](https://github.com/mattnenterprise/rust-ftp)
|
||||
- [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)
|
||||
|
||||
---
|
||||
|
||||
@@ -394,10 +192,6 @@ TermSCP is powered by these aweseome projects:
|
||||
|
||||
## 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
@@ -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.
|
||||
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 689 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 635 KiB After Width: | Height: | Size: 2.1 MiB |
BIN
assets/images/termscp-128.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/images/termscp-512.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/termscp-64.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/termscp-96.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
1
assets/images/termscp.svg
Normal 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 |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
assets/images/themes.gif
Normal file
|
After Width: | Height: | Size: 227 KiB |
@@ -1,7 +0,0 @@
|
||||
ignore:
|
||||
- src/main.rs
|
||||
- src/lib.rs
|
||||
- src/activity_manager.rs
|
||||
- src/ui/
|
||||
fixes:
|
||||
- "/::"
|
||||
30
dist/build/deploy.sh
vendored
@@ -7,32 +7,26 @@ fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
set -e # Don't fail
|
||||
|
||||
# Create pkgs directory
|
||||
cd ..
|
||||
PKGS_DIR=$(pwd)/pkgs
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/
|
||||
# Build x86_64
|
||||
cd x86_64/
|
||||
docker build --tag termscp-${VERSION}-x86_64 .
|
||||
# Create container and get deb, rpm
|
||||
# Build x86_64_deb
|
||||
cd x86_64_debian9/
|
||||
docker build --tag termscp-${VERSION}-x86_64_debian9 .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/deb/
|
||||
mkdir -p ${PKGS_DIR}/rpm/
|
||||
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64 termscp-${VERSION}-x86_64)
|
||||
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/
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.x86_64.rpm ${PKGS_DIR}/rpm/
|
||||
# Build x86_64_archlinux
|
||||
cd x86_64_archlinux/
|
||||
docker build --tag termscp-${VERSION}-x86_64_archlinux .
|
||||
# Create container and get AUR pkg
|
||||
# Build x86_64_centos7
|
||||
cd x86_64_centos7/
|
||||
docker build --tag termscp-${VERSION}-x86_64_centos7 .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/arch/
|
||||
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_archlinux termscp-${VERSION}-x86_64_archlinux)
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/termscp-${VERSION}-x86_64.tar.gz ${PKGS_DIR}/arch/
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/PKGBUILD ${PKGS_DIR}/arch/
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/.SRCINFO ${PKGS_DIR}/arch/
|
||||
# Replace termscp-bin with termscp in PKGBUILD
|
||||
sed -i 's/termscp-bin/termscp/g' ${PKGS_DIR}/arch/PKGBUILD
|
||||
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 $?
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
FROM archlinux/archlinux:latest as builder
|
||||
FROM centos:centos7 as builder
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN pacman -Syu --noconfirm \
|
||||
RUN yum -y install \
|
||||
git \
|
||||
gcc \
|
||||
openssl \
|
||||
pkg-config \
|
||||
sudo
|
||||
# Create build user
|
||||
RUN useradd build -m && \
|
||||
passwd -d build && \
|
||||
mkdir -p termscp && \
|
||||
chown -R build.build termscp/
|
||||
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 && \
|
||||
@@ -22,12 +18,9 @@ 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-aur
|
||||
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 && cargo aur
|
||||
# Create SRCINFO
|
||||
RUN chown -R build.build ../termscp/ && sudo -u build bash -c 'makepkg --printsrcinfo > .SRCINFO'
|
||||
|
||||
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
@@ -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
@@ -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"]
|
||||
14
dist/pkgs/arch/.SRCINFO
vendored
@@ -1,14 +0,0 @@
|
||||
pkgbase = termscp-bin
|
||||
pkgdesc = 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.
|
||||
pkgver = 0.3.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/veeso/termscp
|
||||
arch = x86_64
|
||||
license = GPL-3.0
|
||||
provides = termscp
|
||||
options = strip
|
||||
source = https://github.com/veeso/termscp/releases/download/v0.3.1/termscp-0.3.1-x86_64.tar.gz
|
||||
sha256sums = dd056531554737595cbe5ac9ff741fdabf5e386299bbc0c81ea9e0f00fbbe2d0
|
||||
|
||||
pkgname = termscp-bin
|
||||
|
||||
16
dist/pkgs/arch/PKGBUILD
vendored
@@ -1,16 +0,0 @@
|
||||
# Maintainer: Christian Visintin
|
||||
pkgname=termscp
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc="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."
|
||||
url="https://github.com/veeso/termscp"
|
||||
license=("GPL-3.0")
|
||||
arch=("x86_64")
|
||||
provides=("termscp")
|
||||
options=("strip")
|
||||
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
|
||||
sha256sums=("dd056531554737595cbe5ac9ff741fdabf5e386299bbc0c81ea9e0f00fbbe2d0")
|
||||
|
||||
package() {
|
||||
install -Dm755 termscp -t "$pkgdir/usr/bin/"
|
||||
}
|
||||
17
dist/pkgs/freebsd/manifest
vendored
Normal 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
@@ -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
|
||||
|
||||
---
|
||||
@@ -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
@@ -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>`.
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
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
@@ -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
|
||||
@@ -2,39 +2,43 @@
|
||||
//!
|
||||
//! `activity_manager` is the module which provides run methods and handling for activities
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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::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, setup_activity::SetupActivity, 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;
|
||||
|
||||
@@ -52,24 +56,30 @@ 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, HostError> {
|
||||
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(e) => return Err(e),
|
||||
};
|
||||
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,
|
||||
local_dir: local_dir.to_path_buf(),
|
||||
interval,
|
||||
})
|
||||
}
|
||||
@@ -77,21 +87,9 @@ impl ActivityManager {
|
||||
/// ### 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,
|
||||
port,
|
||||
protocol,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
pub fn set_filetransfer_params(&mut self, params: FileTransferParams) {
|
||||
// Put params into the context
|
||||
self.context.as_mut().unwrap().set_ftparams(params);
|
||||
}
|
||||
|
||||
/// ### run
|
||||
@@ -115,7 +113,7 @@ impl ActivityManager {
|
||||
drop(self.context.take());
|
||||
}
|
||||
|
||||
// Loops
|
||||
// -- Activity Loops
|
||||
|
||||
/// ### run_authentication
|
||||
///
|
||||
@@ -123,6 +121,7 @@ impl ActivityManager {
|
||||
/// Returns when activity terminates.
|
||||
/// Returns the next activity to run
|
||||
fn run_authentication(&mut self) -> Option<NextActivity> {
|
||||
info!("Starting AuthActivity...");
|
||||
// Prepare activity
|
||||
let mut activity: AuthActivity = AuthActivity::default();
|
||||
// Prepare result
|
||||
@@ -130,7 +129,10 @@ impl ActivityManager {
|
||||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None,
|
||||
None => {
|
||||
error!("Failed to start AuthActivity: context is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
@@ -138,40 +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.setup {
|
||||
// User requested activity
|
||||
result = Some(NextActivity::SetupActivity);
|
||||
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,
|
||||
});
|
||||
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
|
||||
}
|
||||
|
||||
@@ -181,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);
|
||||
@@ -229,7 +249,10 @@ impl ActivityManager {
|
||||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None,
|
||||
None => {
|
||||
error!("Failed to start SetupActivity: context is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
@@ -237,7 +260,8 @@ impl ActivityManager {
|
||||
// Draw activity
|
||||
activity.on_draw();
|
||||
// Check if activity has terminated
|
||||
if activity.quit {
|
||||
if let Some(ExitReason::Quit) = activity.will_umount() {
|
||||
info!("SetupActivity terminated due to 'Quit'");
|
||||
break;
|
||||
}
|
||||
// Sleep for ticks
|
||||
@@ -248,4 +272,63 @@ impl ActivityManager {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
//! ## Bookmarks
|
||||
//!
|
||||
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
pub mod serializer;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
/// ## 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(std::fmt::Debug, PartialEq)]
|
||||
pub enum SerializerErrorKind {
|
||||
IoError,
|
||||
SerializationError,
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
impl Default for UserHosts {
|
||||
fn default() -> Self {
|
||||
UserHosts {
|
||||
bookmarks: HashMap::new(),
|
||||
recents: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let err: String = match &self.kind {
|
||||
SerializerErrorKind::IoError => String::from("IO error"),
|
||||
SerializerErrorKind::SerializationError => String::from("Serialization error"),
|
||||
SerializerErrorKind::SyntaxError => String::from("Syntax error"),
|
||||
};
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", err, msg),
|
||||
None => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[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")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_bookmark_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
|
||||
assert_eq!(error.kind, SerializerErrorKind::SyntaxError);
|
||||
assert!(error.msg.is_none());
|
||||
assert_eq!(format!("{}", error), String::from("Syntax error"));
|
||||
let error: SerializerError =
|
||||
SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax"));
|
||||
assert_eq!(error.kind, SerializerErrorKind::SyntaxError);
|
||||
assert!(error.msg.is_some());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
String::from("Syntax error (bad syntax)")
|
||||
);
|
||||
// Fmt
|
||||
assert_eq!(
|
||||
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
|
||||
String::from("IO error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::SerializationError)
|
||||
),
|
||||
String::from("Serialization error")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for bookmarks
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{SerializerError, SerializerErrorKind, UserHosts};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub struct BookmarkSerializer {}
|
||||
|
||||
impl BookmarkSerializer {
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserHosts` into TOML and write content to writable
|
||||
pub fn serialize(
|
||||
&self,
|
||||
mut writable: Box<dyn Write>,
|
||||
hosts: &UserHosts,
|
||||
) -> Result<(), SerializerError> {
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(hosts) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SerializationError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deserialize
|
||||
///
|
||||
/// Read data from readable and deserialize its content as TOML
|
||||
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserHosts, SerializerError> {
|
||||
// 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::IoError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(hosts) => Ok(hosts),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::super::Bookmark;
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_serializer_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
let hosts = deserializer.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_bookmarks_serializer_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_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 deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
let hosts: UserHosts = UserHosts { bookmarks, recents };
|
||||
assert!(deserializer.serialize(Box::new(tmpfile), &hosts).is_ok());
|
||||
}
|
||||
|
||||
fn create_good_toml() -> 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() -> 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
|
||||
}
|
||||
}
|
||||
124
src/config/bookmarks.rs
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,230 +1,34 @@
|
||||
//! ## Config
|
||||
//!
|
||||
//! `config` is the module which provides access to termscp configuration
|
||||
//! `config` is the module which provides access to all the termscp configurations
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* 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::*;
|
||||
|
||||
// Modules
|
||||
pub mod serializer;
|
||||
|
||||
// Deps
|
||||
extern crate edit;
|
||||
|
||||
// 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 group_dirs: Option<String>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
group_dirs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RemoteConfig {
|
||||
fn default() -> Self {
|
||||
RemoteConfig {
|
||||
ssh_keys: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
/// ## 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(std::fmt::Debug, PartialEq)]
|
||||
pub enum SerializerErrorKind {
|
||||
IoError,
|
||||
SerializationError,
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
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 {
|
||||
let err: String = match &self.kind {
|
||||
SerializerErrorKind::IoError => String::from("IO error"),
|
||||
SerializerErrorKind::SerializationError => String::from("Serialization error"),
|
||||
SerializerErrorKind::SyntaxError => String::from("Syntax error"),
|
||||
};
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", err, msg),
|
||||
None => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::env;
|
||||
|
||||
#[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,
|
||||
group_dirs: Some(String::from("first")),
|
||||
};
|
||||
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.group_dirs, Some(String::from("first")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_new_default() {
|
||||
// Force vim editor
|
||||
env::set_var(String::from("EDITOR"), String::from("vim"));
|
||||
// Get default
|
||||
let cfg: UserConfig = UserConfig::default();
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.remote.ssh_keys.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
|
||||
assert_eq!(error.kind, SerializerErrorKind::SyntaxError);
|
||||
assert!(error.msg.is_none());
|
||||
assert_eq!(format!("{}", error), String::from("Syntax error"));
|
||||
let error: SerializerError =
|
||||
SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax"));
|
||||
assert_eq!(error.kind, SerializerErrorKind::SyntaxError);
|
||||
assert!(error.msg.is_some());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
String::from("Syntax error (bad syntax)")
|
||||
);
|
||||
// Fmt
|
||||
assert_eq!(
|
||||
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
|
||||
String::from("IO error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::SerializationError)
|
||||
),
|
||||
String::from("Serialization error")
|
||||
);
|
||||
}
|
||||
}
|
||||
pub mod bookmarks;
|
||||
pub mod params;
|
||||
pub mod serialization;
|
||||
pub mod themes;
|
||||
|
||||
155
src/config/params.rs
Normal 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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{SerializerError, SerializerErrorKind, UserConfig};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub struct ConfigSerializer {}
|
||||
|
||||
impl ConfigSerializer {
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserConfig` into TOML and write content to writable
|
||||
pub fn serialize(
|
||||
&self,
|
||||
mut writable: Box<dyn Write>,
|
||||
cfg: &UserConfig,
|
||||
) -> Result<(), SerializerError> {
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(cfg) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SerializationError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deserialize
|
||||
///
|
||||
/// Read data from readable and deserialize its content as TOML
|
||||
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserConfig, SerializerError> {
|
||||
// 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::IoError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(hosts) => Ok(hosts),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg = deserializer.deserialize(Box::new(toml_file));
|
||||
println!("{:?}", cfg);
|
||||
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, Some(String::from("last")));
|
||||
// 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_serializer_deserialize_ok_no_opts() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml_no_opts();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg = deserializer.deserialize(Box::new(toml_file));
|
||||
println!("{:?}", cfg);
|
||||
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);
|
||||
// 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_serializer_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_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 serializer: ConfigSerializer = ConfigSerializer {};
|
||||
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
|
||||
assert!(serializer.serialize(writer, &cfg).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!(serializer.deserialize(Box::new(toml_file)).is_ok());
|
||||
}
|
||||
|
||||
fn create_good_toml() -> 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
|
||||
group_dirs = "last"
|
||||
|
||||
[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_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() -> 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
|
||||
}
|
||||
}
|
||||
267
src/config/themes.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -2,42 +2,49 @@
|
||||
//!
|
||||
//! `filetransfer` is the module which provides the trait file transfers must implement and the different file transfers
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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(PartialEq, std::fmt::Debug, std::clone::Clone, Copy)]
|
||||
#[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,
|
||||
}
|
||||
|
||||
@@ -93,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,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
|
||||
@@ -189,6 +200,11 @@ 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
|
||||
@@ -224,6 +240,67 @@ 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
|
||||
@@ -242,14 +319,14 @@ impl std::string::ToString for FileTransferProtocol {
|
||||
}
|
||||
|
||||
impl std::str::FromStr for FileTransferProtocol {
|
||||
type Err = ();
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||
_ => Err(()),
|
||||
_ => Err(s.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,6 +338,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
@@ -325,13 +403,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_filetransfer_mod_error() {
|
||||
let err: FileTransferError = FileTransferError::new_ex(
|
||||
FileTransferErrorType::IoErr(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
|
||||
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("IO error: address in use (non va una mazza)")
|
||||
String::from("No such file or directory (non va una mazza)")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
@@ -410,5 +488,7 @@ mod tests {
|
||||
),
|
||||
String::from("Unsupported feature")
|
||||
);
|
||||
let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature);
|
||||
assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature);
|
||||
}
|
||||
}
|
||||
|
||||
137
src/filetransfer/params.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,31 @@
|
||||
//!
|
||||
//! `builder` is the module which provides a builder for FileExplorer
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -95,6 +98,18 @@ impl FileExplorerBuilder {
|
||||
}
|
||||
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)]
|
||||
@@ -102,12 +117,14 @@ 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::ByName); // Default
|
||||
assert_eq!(explorer.file_sorting, FileSorting::Name); // Default
|
||||
assert_eq!(explorer.group_dirs, None);
|
||||
assert_eq!(explorer.stack_size, 16);
|
||||
}
|
||||
@@ -115,14 +132,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_fs_explorer_builder_new_all() {
|
||||
let explorer: FileExplorer = FileExplorerBuilder::new()
|
||||
.with_file_sorting(FileSorting::ByModifyTime)
|
||||
.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::ByModifyTime); // Default
|
||||
assert_eq!(explorer.file_sorting, FileSorting::ModifyTime); // Default
|
||||
assert_eq!(explorer.group_dirs, Some(GroupDirs::First));
|
||||
assert_eq!(explorer.stack_size, 24);
|
||||
}
|
||||
|
||||
896
src/fs/explorer/formatter.rs
Normal 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(®ex_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(®ex_match[1]) {
|
||||
Some(regex_match) => {
|
||||
// Match group 0 (which is name)
|
||||
let callback: FmtCallback = match ®ex_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 ®ex_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)
|
||||
}
|
||||
}
|
||||
@@ -2,33 +2,35 @@
|
||||
//!
|
||||
//! `explorer` is the module which provides an Helper in handling Directory status through
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Deps
|
||||
extern crate bitflags;
|
||||
mod formatter;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
use formatter::Formatter;
|
||||
// Ext
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::VecDeque;
|
||||
@@ -50,10 +52,10 @@ bitflags! {
|
||||
/// FileSorting defines the criteria for sorting files
|
||||
#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
|
||||
pub enum FileSorting {
|
||||
ByName,
|
||||
ByModifyTime,
|
||||
ByCreationTime,
|
||||
BySize,
|
||||
Name,
|
||||
ModifyTime,
|
||||
CreationTime,
|
||||
Size,
|
||||
}
|
||||
|
||||
/// ## GroupDirs
|
||||
@@ -75,7 +77,7 @@ pub struct FileExplorer {
|
||||
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
|
||||
index: usize, // Selected file
|
||||
pub(crate) fmt: Formatter, // FsEntry formatter
|
||||
files: Vec<FsEntry>, // Files in directory
|
||||
}
|
||||
|
||||
@@ -85,10 +87,10 @@ impl Default for FileExplorer {
|
||||
wrkdir: PathBuf::from("/"),
|
||||
dirstack: VecDeque::with_capacity(16),
|
||||
stack_size: 16,
|
||||
file_sorting: FileSorting::ByName,
|
||||
file_sorting: FileSorting::Name,
|
||||
group_dirs: None,
|
||||
opts: ExplorerOpts::empty(),
|
||||
index: 0,
|
||||
fmt: Formatter::default(),
|
||||
files: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -123,16 +125,25 @@ impl FileExplorer {
|
||||
self.files = files;
|
||||
// Sort
|
||||
self.sort();
|
||||
// Reset index
|
||||
self.index_at_first();
|
||||
}
|
||||
|
||||
/// ### 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
|
||||
///
|
||||
@@ -159,11 +170,34 @@ impl FileExplorer {
|
||||
Box::new(self.files.iter())
|
||||
}
|
||||
|
||||
/// ### get_current_file
|
||||
/// ### get
|
||||
///
|
||||
/// Get file at index
|
||||
pub fn get_current_file(&self) -> Option<&FsEntry> {
|
||||
self.files.get(self.index)
|
||||
/// 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
|
||||
@@ -203,10 +237,10 @@ impl FileExplorer {
|
||||
fn sort(&mut self) {
|
||||
// Choose sorting method
|
||||
match &self.file_sorting {
|
||||
FileSorting::ByName => self.sort_files_by_name(),
|
||||
FileSorting::ByCreationTime => self.sort_files_by_creation_time(),
|
||||
FileSorting::ByModifyTime => self.sort_files_by_mtime(),
|
||||
FileSorting::BySize => self.sort_files_by_size(),
|
||||
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
|
||||
@@ -239,7 +273,8 @@ impl FileExplorer {
|
||||
///
|
||||
/// 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()));
|
||||
self.files
|
||||
.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
|
||||
}
|
||||
|
||||
/// ### sort_files_by_size
|
||||
@@ -263,196 +298,18 @@ impl FileExplorer {
|
||||
self.files.sort_by_key(|x: &FsEntry| x.is_dir());
|
||||
}
|
||||
|
||||
/// ### incr_index
|
||||
///
|
||||
/// Increment index to the first visible FsEntry.
|
||||
/// If index goes to `files.len() - 1`, the value will be seto to the minimum acceptable value
|
||||
pub fn incr_index(&mut self) {
|
||||
let sz: usize = self.files.len();
|
||||
// Increment or wrap
|
||||
if self.index + 1 >= sz {
|
||||
self.index = 0; // Wrap
|
||||
} else {
|
||||
self.index += 1; // Increment
|
||||
}
|
||||
// Validate
|
||||
match self.files.get(self.index) {
|
||||
Some(assoc_entry) => {
|
||||
if !self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
// Check if file is hidden, otherwise increment
|
||||
if assoc_entry.is_hidden() {
|
||||
// Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS)
|
||||
let hidden_files: usize =
|
||||
self.files.iter().filter(|x| x.is_hidden()).count();
|
||||
// Only if there are more files, than hidden files keep incrementing
|
||||
if sz > hidden_files {
|
||||
self.incr_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => self.index = 0, // Reset to 0, for safety reasons
|
||||
}
|
||||
}
|
||||
|
||||
/// ### incr_index_by
|
||||
///
|
||||
/// Increment index by up to n
|
||||
/// If index goes to `files.len() - 1`, the value will be seto to the minimum acceptable value
|
||||
pub fn incr_index_by(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
let prev_idx: usize = self.index;
|
||||
// Increment
|
||||
self.incr_index();
|
||||
// If prev index is > index and break
|
||||
if prev_idx > self.index {
|
||||
self.index = prev_idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_index
|
||||
///
|
||||
/// Decrement index to the first visible FsEntry.
|
||||
/// If index is 0, its value will be set to the maximum acceptable value
|
||||
pub fn decr_index(&mut self) {
|
||||
let sz: usize = self.files.len();
|
||||
// Increment or wrap
|
||||
if self.index > 0 {
|
||||
self.index -= 1; // Decrement
|
||||
} else {
|
||||
self.index = sz - 1; // Wrap
|
||||
}
|
||||
// Validate index
|
||||
match self.files.get(self.index) {
|
||||
Some(assoc_entry) => {
|
||||
if !self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
// Check if file is hidden, otherwise increment
|
||||
if assoc_entry.is_hidden() {
|
||||
// Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS)
|
||||
let hidden_files: usize =
|
||||
self.files.iter().filter(|x| x.is_hidden()).count();
|
||||
// Only if there are more files, than hidden files keep decrementing
|
||||
if sz > hidden_files {
|
||||
self.decr_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => self.index = 0, // Reset to 0, for safety reasons
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_index_by
|
||||
///
|
||||
/// Decrement index by up to n
|
||||
pub fn decr_index_by(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
let prev_idx: usize = self.index;
|
||||
// Increment
|
||||
self.decr_index();
|
||||
// If prev index is < index and break
|
||||
if prev_idx < self.index {
|
||||
self.index = prev_idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### index_at_first
|
||||
///
|
||||
/// Move index to first "visible" fs entry
|
||||
pub fn index_at_first(&mut self) {
|
||||
self.index = self.get_first_valid_index();
|
||||
}
|
||||
|
||||
/// ### get_first_valid_index
|
||||
///
|
||||
/// Return first valid index
|
||||
fn get_first_valid_index(&self) -> usize {
|
||||
match self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
true => 0,
|
||||
false => {
|
||||
// Look for first "non-hidden" entry
|
||||
for (i, f) in self.files.iter().enumerate() {
|
||||
if !f.is_hidden() {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// If all files are hidden, return 0
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_index
|
||||
///
|
||||
/// Return index
|
||||
pub fn get_index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// ### get_relative_index
|
||||
///
|
||||
/// Get relative index based on current options
|
||||
pub fn get_relative_index(&self) -> usize {
|
||||
match self.files.get(self.index) {
|
||||
Some(abs_entry) => {
|
||||
// Search abs entry in relative iterator
|
||||
for (i, f) in self.iter_files().enumerate() {
|
||||
if abs_entry.get_name() == f.get_name() {
|
||||
// If abs entry is f, return index
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Return 0 if not found
|
||||
0
|
||||
}
|
||||
None => 0, // Absolute entry doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_index
|
||||
///
|
||||
/// Set index to idx.
|
||||
/// If index exceeds size, is set to count() - 1; or 0
|
||||
pub fn set_index(&mut self, idx: usize) {
|
||||
let visible_sz: usize = self.iter_files().count();
|
||||
match idx >= visible_sz {
|
||||
true => match visible_sz {
|
||||
0 => self.index_at_first(),
|
||||
_ => self.index = visible_sz - 1,
|
||||
},
|
||||
false => match self.get_first_valid_index() > idx {
|
||||
true => self.index_at_first(),
|
||||
false => self.index = idx,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_abs_index
|
||||
///
|
||||
/// Set absolute index
|
||||
pub fn set_abs_index(&mut self, idx: usize) {
|
||||
self.index = match idx >= self.files.len() {
|
||||
true => match self.files.len() {
|
||||
0 => 0,
|
||||
_ => self.files.len() - 1,
|
||||
},
|
||||
false => idx,
|
||||
};
|
||||
}
|
||||
|
||||
/// ### toggle_hidden_files
|
||||
///
|
||||
/// Enable/disable hidden files
|
||||
pub fn toggle_hidden_files(&mut self) {
|
||||
self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Adjust index
|
||||
if self.index < self.get_first_valid_index() {
|
||||
self.index_at_first();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### hidden_files_visible
|
||||
///
|
||||
/// Returns whether hidden files are visible
|
||||
pub fn hidden_files_visible(&self) -> bool {
|
||||
self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,10 +318,10 @@ impl FileExplorer {
|
||||
impl ToString for FileSorting {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
FileSorting::ByCreationTime => "by_creation_time",
|
||||
FileSorting::ByModifyTime => "by_mtime",
|
||||
FileSorting::ByName => "by_name",
|
||||
FileSorting::BySize => "by_size",
|
||||
FileSorting::CreationTime => "by_creation_time",
|
||||
FileSorting::ModifyTime => "by_mtime",
|
||||
FileSorting::Name => "by_name",
|
||||
FileSorting::Size => "by_size",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -473,10 +330,10 @@ 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::ByCreationTime),
|
||||
"by_mtime" => Ok(FileSorting::ByModifyTime),
|
||||
"by_name" => Ok(FileSorting::ByName),
|
||||
"by_size" => Ok(FileSorting::BySize),
|
||||
"by_creation_time" => Ok(FileSorting::CreationTime),
|
||||
"by_mtime" => Ok(FileSorting::ModifyTime),
|
||||
"by_name" => Ok(FileSorting::Name),
|
||||
"by_size" => Ok(FileSorting::Size),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
@@ -506,8 +363,10 @@ impl FromStr for GroupDirs {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::fs::{FsDirectory, FsFile};
|
||||
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};
|
||||
|
||||
@@ -520,10 +379,9 @@ mod tests {
|
||||
assert_eq!(explorer.opts, ExplorerOpts::empty());
|
||||
assert_eq!(explorer.wrkdir, PathBuf::from("/"));
|
||||
assert_eq!(explorer.stack_size, 16);
|
||||
assert_eq!(explorer.index, 0);
|
||||
assert_eq!(explorer.group_dirs, None);
|
||||
assert_eq!(explorer.file_sorting, FileSorting::ByName);
|
||||
assert_eq!(explorer.get_file_sorting(), FileSorting::ByName);
|
||||
assert_eq!(explorer.file_sorting, FileSorting::Name);
|
||||
assert_eq!(explorer.get_file_sorting(), FileSorting::Name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -558,6 +416,7 @@ mod tests {
|
||||
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),
|
||||
@@ -567,7 +426,9 @@ mod tests {
|
||||
make_fs_entry("codecov.yml", false),
|
||||
make_fs_entry(".gitignore", false),
|
||||
]);
|
||||
assert_eq!(explorer.count(), 6);
|
||||
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(),
|
||||
@@ -579,124 +440,10 @@ mod tests {
|
||||
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_index() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Create files (files are then sorted by name DEFAULT)
|
||||
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("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),
|
||||
make_fs_entry(".gitignore", false),
|
||||
]);
|
||||
let sz: usize = explorer.count();
|
||||
// Get first index
|
||||
assert_eq!(explorer.get_first_valid_index(), 2);
|
||||
// Index should be 2 now; files hidden; this happens because `index_at_first` is called after loading files
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0); // Relative index should be 0
|
||||
assert_eq!(
|
||||
explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
false
|
||||
);
|
||||
// Increment index
|
||||
explorer.incr_index();
|
||||
// Index should now be 3 (was 0, + 2 + 1); first 2 files are hidden (.git, .gitignore)
|
||||
assert_eq!(explorer.get_index(), 3);
|
||||
// Relative index should be 1 instead
|
||||
assert_eq!(explorer.get_relative_index(), 1);
|
||||
// Increment by 2
|
||||
explorer.incr_index_by(2);
|
||||
// Index should now be 5, 3
|
||||
assert_eq!(explorer.get_index(), 5);
|
||||
assert_eq!(explorer.get_relative_index(), 3);
|
||||
// Increment by (exceed size)
|
||||
explorer.incr_index_by(20);
|
||||
// Index should be at last element
|
||||
assert_eq!(explorer.get_index(), sz - 1);
|
||||
assert_eq!(explorer.get_relative_index(), sz - 3);
|
||||
// Increment; should go to 2
|
||||
explorer.incr_index();
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Increment and then decrement
|
||||
explorer.incr_index();
|
||||
explorer.decr_index();
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Decrement (and wrap)
|
||||
explorer.decr_index();
|
||||
// Index should be at last element
|
||||
assert_eq!(explorer.get_index(), sz - 1);
|
||||
assert_eq!(explorer.get_relative_index(), sz - 3);
|
||||
// Set index to 5
|
||||
explorer.set_index(5);
|
||||
assert_eq!(explorer.get_index(), 5);
|
||||
assert_eq!(explorer.get_relative_index(), 3);
|
||||
// Decr by 2
|
||||
explorer.decr_index_by(2);
|
||||
assert_eq!(explorer.get_index(), 3);
|
||||
assert_eq!(explorer.get_relative_index(), 1);
|
||||
// Decr by 2
|
||||
explorer.decr_index_by(2);
|
||||
// Should decrement actually by 1 (since first two files are hidden)
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Toggle hidden files
|
||||
explorer.toggle_hidden_files();
|
||||
assert_eq!(
|
||||
explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
true
|
||||
);
|
||||
// Move index to 0
|
||||
explorer.set_index(0);
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
// Toggle hidden files
|
||||
explorer.toggle_hidden_files();
|
||||
// Index should now have been moved to 2
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
// Show hidden files
|
||||
explorer.toggle_hidden_files();
|
||||
// Set index to 5
|
||||
explorer.set_index(5);
|
||||
// Verify index
|
||||
assert_eq!(explorer.get_index(), 5);
|
||||
assert_eq!(explorer.get_relative_index(), 5); // Now relative matches
|
||||
// Decrement by 6, goes to 0
|
||||
explorer.decr_index_by(6);
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
assert_eq!(explorer.get_relative_index(), 0); // Now relative matches
|
||||
// Toggle; move at first
|
||||
explorer.toggle_hidden_files();
|
||||
assert_eq!(
|
||||
explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
false
|
||||
);
|
||||
explorer.index_at_first();
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Verify set index if exceeds
|
||||
let sz: usize = explorer.iter_files().count();
|
||||
explorer.set_index(sz);
|
||||
assert_eq!(explorer.get_index(), sz - 1); // Should be at last element
|
||||
// Empty files
|
||||
explorer.files.clear();
|
||||
explorer.index_at_first();
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_name() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
@@ -712,7 +459,7 @@ mod tests {
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::ByName);
|
||||
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/"
|
||||
@@ -728,7 +475,7 @@ mod tests {
|
||||
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::ByModifyTime);
|
||||
explorer.sort_by(FileSorting::ModifyTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(
|
||||
explorer.files.get(0).unwrap().get_name(),
|
||||
@@ -738,33 +485,6 @@ mod tests {
|
||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_set_abs_index() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Create files (files are then sorted by name DEFAULT)
|
||||
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("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),
|
||||
make_fs_entry(".gitignore", false),
|
||||
]);
|
||||
explorer.set_abs_index(3);
|
||||
assert_eq!(explorer.get_index(), 3);
|
||||
explorer.set_abs_index(12);
|
||||
assert_eq!(explorer.get_index(), 10);
|
||||
explorer.set_files(vec![]);
|
||||
explorer.set_abs_index(12);
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_creation_time() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
@@ -774,7 +494,7 @@ mod tests {
|
||||
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::ByCreationTime);
|
||||
explorer.sort_by(FileSorting::CreationTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(
|
||||
explorer.files.get(0).unwrap().get_name(),
|
||||
@@ -793,7 +513,7 @@ mod tests {
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::BySize);
|
||||
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");
|
||||
@@ -816,7 +536,7 @@ mod tests {
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::ByName);
|
||||
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/");
|
||||
@@ -843,7 +563,7 @@ mod tests {
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::ByName);
|
||||
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/");
|
||||
@@ -854,28 +574,64 @@ mod tests {
|
||||
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::ByCreationTime.to_string(), "by_creation_time");
|
||||
assert_eq!(FileSorting::ByModifyTime.to_string(), "by_mtime");
|
||||
assert_eq!(FileSorting::ByName.to_string(), "by_name");
|
||||
assert_eq!(FileSorting::BySize.to_string(), "by_size");
|
||||
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::ByCreationTime
|
||||
FileSorting::CreationTime
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_mtime").ok().unwrap(),
|
||||
FileSorting::ByModifyTime
|
||||
FileSorting::ModifyTime
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_name").ok().unwrap(),
|
||||
FileSorting::ByName
|
||||
FileSorting::Name
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_size").ok().unwrap(),
|
||||
FileSorting::BySize
|
||||
FileSorting::Size
|
||||
);
|
||||
assert!(FileSorting::from_str("omar").is_err());
|
||||
// Group dirs
|
||||
@@ -886,6 +642,23 @@ mod tests {
|
||||
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 {
|
||||
@@ -896,12 +669,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 64,
|
||||
ftype: None, // File type
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
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(),
|
||||
@@ -909,11 +681,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
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
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -928,12 +699,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: size,
|
||||
ftype: None, // File type
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
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(),
|
||||
@@ -941,11 +711,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
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
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
555
src/fs/mod.rs
@@ -2,42 +2,34 @@
|
||||
//!
|
||||
//! `fs` is the module which provides file system entities
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
// Locals
|
||||
use crate::utils::fmt::{fmt_pex, fmt_time};
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use users::get_user_by_uid;
|
||||
|
||||
/// ## FsEntry
|
||||
///
|
||||
@@ -60,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<Box<FsEntry>>, // 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
|
||||
@@ -79,12 +70,72 @@ 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<Box<FsEntry>>, // 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
|
||||
}
|
||||
|
||||
/// ## 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
|
||||
///
|
||||
/// Instantiates a new `UnixPex`
|
||||
pub fn new(read: bool, write: bool, execute: bool) -> Self {
|
||||
Self {
|
||||
read,
|
||||
write,
|
||||
execute,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### can_read
|
||||
///
|
||||
/// 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 {
|
||||
@@ -181,7 +232,7 @@ impl FsEntry {
|
||||
/// ### get_unix_pex
|
||||
///
|
||||
/// Get unix pex from `FsEntry`
|
||||
pub fn get_unix_pex(&self) -> Option<(u8, u8, u8)> {
|
||||
pub fn get_unix_pex(&self) -> Option<(UnixPex, UnixPex, UnixPex)> {
|
||||
match self {
|
||||
FsEntry::Directory(dir) => dir.unix_pex,
|
||||
FsEntry::File(file) => file.unix_pex,
|
||||
@@ -234,75 +285,26 @@ impl FsEntry {
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FsEntry {
|
||||
/// ### fmt_ls
|
||||
/// ### unwrap_file
|
||||
///
|
||||
/// Format File Entry as `ls` does
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// Create mode string
|
||||
let mut mode: String = String::with_capacity(10);
|
||||
let file_type: char = match self.is_symlink() {
|
||||
true => 'l',
|
||||
false => match self.is_dir() {
|
||||
true => 'd',
|
||||
false => '-',
|
||||
},
|
||||
};
|
||||
mode.push(file_type);
|
||||
match self.get_unix_pex() {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => mode.push_str(fmt_pex(owner, group, others).as_str()),
|
||||
/// Unwrap FsEntry as FsFile
|
||||
pub fn unwrap_file(self) -> FsFile {
|
||||
match self {
|
||||
FsEntry::File(file) => file,
|
||||
_ => panic!("unwrap_file: not a file"),
|
||||
}
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let username: String = match self.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 self.get_user() {
|
||||
Some(uid) => uid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Get group
|
||||
/*
|
||||
let group: String = match self.get_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(self.get_size() as u64);
|
||||
// Get date
|
||||
let datetime: String = fmt_time(self.get_last_change_time(), "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let name: &str = self.get_name();
|
||||
let last_idx: usize = match self.is_dir() {
|
||||
// NOTE: For directories is 19, since we push '/' to name
|
||||
true => 19,
|
||||
false => 20,
|
||||
};
|
||||
let mut name: String = match name.len() >= 24 {
|
||||
false => name.to_string(),
|
||||
true => format!("{}...", &name[0..last_idx]),
|
||||
};
|
||||
// If is directory, append '/'
|
||||
if self.is_dir() {
|
||||
name.push('/');
|
||||
}
|
||||
|
||||
#[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"),
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:10}\t{:17}",
|
||||
name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +312,7 @@ impl std::fmt::Display for FsEntry {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_fs_fsentry_dir() {
|
||||
@@ -320,11 +323,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
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"));
|
||||
@@ -338,7 +340,11 @@ mod tests {
|
||||
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((7, 5, 5)));
|
||||
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]
|
||||
@@ -351,12 +357,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
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"));
|
||||
@@ -367,10 +372,52 @@ mod tests {
|
||||
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((6, 4, 4)));
|
||||
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]
|
||||
@@ -383,12 +430,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
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 {
|
||||
@@ -398,12 +444,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
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 {
|
||||
@@ -412,11 +457,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
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);
|
||||
}
|
||||
@@ -432,12 +476,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
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!(
|
||||
@@ -451,11 +494,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
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"));
|
||||
}
|
||||
@@ -471,11 +513,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 7, 7)), // UNIX only
|
||||
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"),
|
||||
@@ -483,11 +524,10 @@ mod tests {
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: Some(Box::new(entry_target)),
|
||||
user: Some(0),
|
||||
group: Some(0),
|
||||
unix_pex: Some((7, 7, 7)),
|
||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
|
||||
});
|
||||
let entry_root: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("projects"),
|
||||
@@ -496,12 +536,11 @@ mod tests {
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8,
|
||||
readonly: false,
|
||||
ftype: None,
|
||||
symlink: Some(Box::new(entry_child)),
|
||||
user: Some(0),
|
||||
group: Some(0),
|
||||
unix_pex: Some((7, 7, 7)),
|
||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
|
||||
});
|
||||
assert_eq!(entry_root.is_symlink(), true);
|
||||
// get real file
|
||||
@@ -514,192 +553,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_fmt_file() {
|
||||
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,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-rw-r--r-- \troot \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-rw-r--r-- \t0 \t8.2 KB \t{}",
|
||||
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,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... \t-rw-r--r-- \troot \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... \t-rw-r--r-- \t0 \t8.2 KB \t{}",
|
||||
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,
|
||||
readonly: false,
|
||||
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(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \troot \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
|
||||
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,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"bar.txt \t-????????? \t0 \t8.2 KB \t{}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_fmt_dir() {
|
||||
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,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \tdrwxr-xr-x \troot \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \tdrwxr-xr-x \t0 \t4.1 KB \t{}",
|
||||
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,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \td????????? \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects/ \td????????? \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
657
src/host/mod.rs
80
src/lib.rs
@@ -1,37 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
#![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"
|
||||
)]
|
||||
|
||||
/**
|
||||
* 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 bookmarks;
|
||||
pub mod config;
|
||||
pub mod filetransfer;
|
||||
pub mod fs;
|
||||
pub mod host;
|
||||
pub mod support;
|
||||
pub mod system;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
321
src/main.rs
@@ -1,183 +1,274 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* 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: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
|
||||
// Crates
|
||||
extern crate getopts;
|
||||
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 bookmarks;
|
||||
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 = String::from("Usage: termscp [options]... [protocol://user@address:port]");
|
||||
print!("{}", opts.usage(&brief));
|
||||
println!("\nPlease, report issues to <https://github.com/veeso/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(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;
|
||||
if let Some(remote) = extra_args.get(0) {
|
||||
// @! Ordinary mode
|
||||
// Remote argument
|
||||
if let Some(remote) = args.positional.get(0) {
|
||||
// Parse address
|
||||
match utils::parser::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.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(err) => {
|
||||
eprintln!("Could not start activity manager: {}", err);
|
||||
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
@@ -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
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,38 @@
|
||||
//!
|
||||
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate whoami;
|
||||
/**
|
||||
* 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(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(feature = "with-keyring")]
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::bookmarks::serializer::BookmarkSerializer;
|
||||
use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts};
|
||||
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;
|
||||
@@ -65,10 +67,12 @@ impl BookmarksClient {
|
||||
recents_size: usize,
|
||||
) -> Result<BookmarksClient, SerializerError> {
|
||||
// Create default hosts
|
||||
let default_hosts: UserHosts = Default::default();
|
||||
// Make a key storage (windows / macos)
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
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
|
||||
@@ -77,29 +81,41 @@ impl BookmarksClient {
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => (Box::new(storage), app_name),
|
||||
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
|
||||
true => {
|
||||
debug!("Using KeyringStorage");
|
||||
(Box::new(storage), app_name)
|
||||
}
|
||||
false => {
|
||||
warn!("KeyringStorage is not supported; using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
||||
}
|
||||
}
|
||||
};
|
||||
// Make a key storage (linux / unix)
|
||||
#[cfg(any(target_os = "linux", target_os = "unix"))]
|
||||
// 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) => k,
|
||||
Ok(k) => {
|
||||
debug!("Key loaded with success");
|
||||
k
|
||||
}
|
||||
Err(e) => match e {
|
||||
KeyStorageError::NoSuchKey => {
|
||||
// If no such key, generate key and set it into the storage
|
||||
let key: String = Self::generate_key();
|
||||
debug!("Key doesn't exist yet or could not be loaded; generated a new key");
|
||||
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
|
||||
error!("Failed to set new key into storage: {}", e);
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
SerializerErrorKind::Io,
|
||||
format!("Could not write key to storage: {}", e),
|
||||
));
|
||||
}
|
||||
@@ -107,10 +123,11 @@ impl BookmarksClient {
|
||||
key
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to get key from storage: {}", e);
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
SerializerErrorKind::Io,
|
||||
format!("Could not get key from storage: {}", e),
|
||||
))
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -122,15 +139,19 @@ impl BookmarksClient {
|
||||
};
|
||||
// If bookmark file doesn't exist, initialize it
|
||||
if !bookmarks_file.exists() {
|
||||
info!("Bookmarks file doesn't exist yet; creating it...");
|
||||
if let Err(err) = client.write_bookmarks() {
|
||||
error!("Failed to create bookmarks file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
// Load bookmarks from file
|
||||
if let Err(err) = client.read_bookmarks() {
|
||||
error!("Failed to load bookmarks: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
info!("Bookmarks client initialized");
|
||||
// Load key
|
||||
Ok(client)
|
||||
}
|
||||
@@ -150,19 +171,29 @@ impl BookmarksClient {
|
||||
key: &str,
|
||||
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
||||
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
||||
debug!("Getting bookmark {}", key);
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||
err
|
||||
);
|
||||
FileTransferProtocol::Sftp // Default
|
||||
}
|
||||
},
|
||||
entry.username.clone(),
|
||||
match &entry.password {
|
||||
// Decrypted password if Some; if decryption fails return None
|
||||
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
||||
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
||||
Err(_) => None,
|
||||
Err(err) => {
|
||||
error!("Failed to decrypt password for bookmark: {}", err);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
@@ -182,9 +213,11 @@ impl BookmarksClient {
|
||||
password: Option<String>,
|
||||
) {
|
||||
if name.is_empty() {
|
||||
error!("Fatal error; bookmark name is empty");
|
||||
panic!("Bookmark name can't be empty");
|
||||
}
|
||||
// Make bookmark
|
||||
info!("Added bookmark {} with address {}", name, addr);
|
||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
||||
self.hosts.bookmarks.insert(name, host);
|
||||
}
|
||||
@@ -194,6 +227,7 @@ impl BookmarksClient {
|
||||
/// Delete entry from bookmarks
|
||||
pub fn del_bookmark(&mut self, name: &str) {
|
||||
let _ = self.hosts.bookmarks.remove(name);
|
||||
info!("Removed bookmark {}", name);
|
||||
}
|
||||
/// ### iter_recents
|
||||
///
|
||||
@@ -207,13 +241,20 @@ impl BookmarksClient {
|
||||
/// Get recent associated to key
|
||||
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
||||
// NOTE: password is not decrypted; recents will never have password
|
||||
info!("Getting bookmark {}", key);
|
||||
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||
err
|
||||
);
|
||||
FileTransferProtocol::Sftp // Default
|
||||
}
|
||||
},
|
||||
entry.username.clone(),
|
||||
))
|
||||
@@ -234,6 +275,7 @@ impl BookmarksClient {
|
||||
// Check if duplicated
|
||||
for recent_host in self.hosts.recents.values() {
|
||||
if *recent_host == host {
|
||||
debug!("Discarding recent since duplicated ({})", host.address);
|
||||
// Don't save duplicates
|
||||
return;
|
||||
}
|
||||
@@ -250,6 +292,7 @@ impl BookmarksClient {
|
||||
// Delete keys starting from the last one
|
||||
for key in keys.iter() {
|
||||
let _ = self.hosts.recents.remove(key);
|
||||
debug!("Removed recent bookmark {}", key);
|
||||
// If length is < self.recents_size; break
|
||||
if self.hosts.recents.len() < self.recents_size {
|
||||
break;
|
||||
@@ -257,6 +300,7 @@ impl BookmarksClient {
|
||||
}
|
||||
}
|
||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||
info!("Saved recent host {} ({})", name, host.address);
|
||||
self.hosts.recents.insert(name, host);
|
||||
}
|
||||
|
||||
@@ -265,6 +309,7 @@ impl BookmarksClient {
|
||||
/// Delete entry from recents
|
||||
pub fn del_recent(&mut self, name: &str) {
|
||||
let _ = self.hosts.recents.remove(name);
|
||||
info!("Removed recent host {}", name);
|
||||
}
|
||||
|
||||
/// ### write_bookmarks
|
||||
@@ -272,20 +317,21 @@ impl BookmarksClient {
|
||||
/// Write bookmarks to file
|
||||
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
||||
// Open file
|
||||
debug!("Writing bookmarks");
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.bookmarks_file.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.hosts)
|
||||
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(),
|
||||
))
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,14 +340,14 @@ impl BookmarksClient {
|
||||
/// Read bookmarks from file
|
||||
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
||||
// Open bookmarks file for read
|
||||
debug!("Reading bookmarks");
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.open(self.bookmarks_file.as_path())
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
match deserialize(Box::new(reader)) {
|
||||
Ok(hosts) => {
|
||||
self.hosts = hosts;
|
||||
Ok(())
|
||||
@@ -309,10 +355,13 @@ impl BookmarksClient {
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to read bookmarks: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::Io,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,10 +389,7 @@ impl BookmarksClient {
|
||||
port,
|
||||
username,
|
||||
protocol: protocol.to_string(),
|
||||
password: match password {
|
||||
Some(p) => Some(self.encrypt_str(p.as_str())), // Encrypt password if provided
|
||||
None => None,
|
||||
},
|
||||
password: password.map(|p| self.encrypt_str(p.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +407,7 @@ impl BookmarksClient {
|
||||
match crypto::aes128_b64_decrypt(self.key.as_str(), secret) {
|
||||
Ok(txt) => Ok(txt),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
SerializerErrorKind::Syntax,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
@@ -373,13 +419,16 @@ impl BookmarksClient {
|
||||
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 = create_tmp_dir();
|
||||
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 =
|
||||
@@ -393,7 +442,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "unix", target_os = "linux"))]
|
||||
#[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"),
|
||||
@@ -402,7 +456,7 @@ mod tests {
|
||||
)
|
||||
.is_err());
|
||||
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -412,7 +466,7 @@ mod tests {
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_new_from_existing() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -458,7 +512,7 @@ mod tests {
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_manipulate_bookmarks() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -504,7 +558,7 @@ mod tests {
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_bad_bookmark_name() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -523,7 +577,7 @@ mod tests {
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_manipulate_recents() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -558,7 +612,7 @@ mod tests {
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_dup_recent() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -583,7 +637,7 @@ mod tests {
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_recents_more_than_limit() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -629,9 +683,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_add_bookmark_empty() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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 =
|
||||
@@ -647,22 +700,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,34 @@
|
||||
//!
|
||||
//! `config_client` is the module which provides an API between the Config module and the system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate rand;
|
||||
/**
|
||||
* 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::serializer::ConfigSerializer;
|
||||
use crate::config::{SerializerError, SerializerErrorKind, UserConfig};
|
||||
use crate::config::{
|
||||
params::UserConfig,
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
// Ext
|
||||
@@ -47,26 +49,34 @@ 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<ConfigClient, SerializerError> {
|
||||
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::IoError,
|
||||
SerializerErrorKind::Io,
|
||||
format!(
|
||||
"Could not create SSH key directory \"{}\": {}",
|
||||
ssh_key_dir.display(),
|
||||
@@ -74,21 +84,40 @@ impl ConfigClient {
|
||||
),
|
||||
));
|
||||
}
|
||||
debug!("Created SSH key directory");
|
||||
}
|
||||
// If Config file doesn't exist, create it
|
||||
if !config_path.exists() {
|
||||
if let Err(err) = client.write_config() {
|
||||
error!("Couldn't create configuration file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Config file didn't exist; created file");
|
||||
} else {
|
||||
// otherwise Load configuration from file
|
||||
if let Err(err) = client.read_config() {
|
||||
error!("Couldn't read configuration file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Read configuration file");
|
||||
}
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// ### 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
|
||||
@@ -138,6 +167,20 @@ impl ConfigClient {
|
||||
self.config.user_interface.show_hidden_files = value;
|
||||
}
|
||||
|
||||
/// ### get_check_for_updates
|
||||
///
|
||||
/// Get value of `check_for_updates`
|
||||
pub fn get_check_for_updates(&self) -> bool {
|
||||
self.config.user_interface.check_for_updates.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// ### set_check_for_updates
|
||||
///
|
||||
/// Set new value for `check_for_updates`
|
||||
pub fn set_check_for_updates(&mut self, value: bool) {
|
||||
self.config.user_interface.check_for_updates = Some(value);
|
||||
}
|
||||
|
||||
/// ### get_group_dirs
|
||||
///
|
||||
/// Get GroupDirs value from configuration (will be converted from string)
|
||||
@@ -157,9 +200,40 @@ impl ConfigClient {
|
||||
/// 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 = match val {
|
||||
None => None,
|
||||
Some(val) => Some(val.to_string()),
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,6 +250,12 @@ impl ConfigClient {
|
||||
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 = {
|
||||
@@ -183,12 +263,18 @@ impl ConfigClient {
|
||||
p.push(format!("{}.key", host_name));
|
||||
p
|
||||
};
|
||||
info!(
|
||||
"Writing SSH file to {} for host {}",
|
||||
ssh_key_path.display(),
|
||||
host_name
|
||||
);
|
||||
// Write key to file
|
||||
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Self::make_io_err(err),
|
||||
};
|
||||
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
|
||||
error!("Failed to write SSH key to file: {}", err);
|
||||
return Self::make_io_err(err);
|
||||
}
|
||||
// Add host to keys
|
||||
@@ -203,7 +289,14 @@ impl ConfigClient {
|
||||
/// 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
|
||||
@@ -215,6 +308,7 @@ impl ConfigClient {
|
||||
};
|
||||
// Remove file
|
||||
if let Err(err) = remove_file(key_path.as_path()) {
|
||||
error!("Failed to remove key file {}: {}", key_path.display(), err);
|
||||
return Self::make_io_err(err);
|
||||
}
|
||||
// Commit changes to configuration
|
||||
@@ -227,6 +321,9 @@ impl ConfigClient {
|
||||
/// 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),
|
||||
@@ -252,6 +349,12 @@ impl ConfigClient {
|
||||
///
|
||||
/// 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)
|
||||
@@ -259,14 +362,14 @@ impl ConfigClient {
|
||||
.truncate(true)
|
||||
.open(self.config_path.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.config)
|
||||
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(),
|
||||
))
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +377,12 @@ impl ConfigClient {
|
||||
///
|
||||
/// 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)
|
||||
@@ -281,8 +390,7 @@ impl ConfigClient {
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
match deserialize(Box::new(reader)) {
|
||||
Ok(config) => {
|
||||
self.config = config;
|
||||
Ok(())
|
||||
@@ -290,10 +398,13 @@ impl ConfigClient {
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Failed to read configuration: {}", err);
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::Io,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +432,7 @@ impl ConfigClient {
|
||||
/// Make serializer error from `std::io::Error`
|
||||
fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> {
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
SerializerErrorKind::Io,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
@@ -334,17 +445,20 @@ mod tests {
|
||||
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: tempfile::TempDir = create_tmp_dir();
|
||||
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,
|
||||
@@ -358,20 +472,34 @@ mod tests {
|
||||
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: tempfile::TempDir = create_tmp_dir();
|
||||
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: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -404,7 +532,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_config_text_editor() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -415,7 +543,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_config_default_protocol() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -429,7 +557,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_config_show_hidden_files() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -438,9 +566,23 @@ mod tests {
|
||||
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: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -451,9 +593,42 @@ mod tests {
|
||||
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: tempfile::TempDir = create_tmp_dir();
|
||||
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()
|
||||
@@ -466,7 +641,6 @@ mod tests {
|
||||
// Iterate keys
|
||||
for key in client.iter_ssh_keys() {
|
||||
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
|
||||
println!("{:?}", host);
|
||||
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();
|
||||
@@ -518,13 +692,6 @@ mod tests {
|
||||
(c, k)
|
||||
}
|
||||
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
|
||||
fn get_sample_rsa_key() -> String {
|
||||
format!(
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n{}\n-----END OPENSSH PRIVATE KEY-----",
|
||||
|
||||
@@ -2,30 +2,29 @@
|
||||
//!
|
||||
//! `environment` is the module which provides Path and values for the system environment
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate dirs;
|
||||
|
||||
/**
|
||||
* 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};
|
||||
|
||||
@@ -35,9 +34,14 @@ use std::path::{Path, PathBuf};
|
||||
/// 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();
|
||||
@@ -80,11 +84,32 @@ pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
(bookmarks_file, keys_dir)
|
||||
}
|
||||
|
||||
/// ### get_log_paths
|
||||
///
|
||||
/// Returns the path for the supposed log file
|
||||
pub fn get_log_paths(config_dir: &Path) -> PathBuf {
|
||||
let mut log_file: PathBuf = PathBuf::from(config_dir);
|
||||
log_file.push("termscp.log");
|
||||
log_file
|
||||
}
|
||||
|
||||
/// ### 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;
|
||||
|
||||
@@ -98,7 +123,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_config_dir_err() {
|
||||
let mut conf_dir: PathBuf = dirs::config_dir().unwrap();
|
||||
let mut conf_dir: PathBuf = std::env::temp_dir();
|
||||
conf_dir.push("termscp");
|
||||
// Create file
|
||||
let mut f: File = OpenOptions::new()
|
||||
@@ -135,4 +160,20 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_log_paths() {
|
||||
assert_eq!(
|
||||
get_log_paths(&Path::new("/home/omar/.config/termscp/")),
|
||||
PathBuf::from("/home/omar/.config/termscp/termscp.log"),
|
||||
);
|
||||
}
|
||||
|
||||
#[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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
//!
|
||||
//! `filestorage` provides an implementation of the `KeyStorage` trait using a file
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -123,6 +125,8 @@ 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/"));
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
//!
|
||||
//! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate 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
|
||||
@@ -64,7 +64,10 @@ impl KeyStorage for KeyringStorage {
|
||||
KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey),
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey),
|
||||
_ => panic!("{}", e),
|
||||
#[cfg(target_os = "linux")]
|
||||
KeyringError::SecretServiceError(_) => Err(KeyStorageError::ProviderError),
|
||||
KeyringError::Parse(_) => Err(KeyStorageError::BadSytax),
|
||||
_ => Err(KeyStorageError::ProviderError),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -89,17 +92,22 @@ impl KeyStorage for KeyringStorage {
|
||||
// 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 {
|
||||
|
||||
extern crate whoami;
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,53 +2,50 @@
|
||||
//!
|
||||
//! `keystorage` provides the trait to manipulate to a KeyStorage
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(feature = "with-keyring")]
|
||||
pub mod keyringstorage;
|
||||
// ext
|
||||
use thiserror::Error;
|
||||
|
||||
/// ## KeyStorageError
|
||||
///
|
||||
/// defines the error type for the `KeyStorage`
|
||||
#[derive(PartialEq, std::fmt::Debug)]
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum KeyStorageError {
|
||||
//BadKey,
|
||||
#[cfg(feature = "with-keyring")]
|
||||
#[error("Key has a bad syntax")]
|
||||
BadSytax,
|
||||
#[error("Provider service error")]
|
||||
ProviderError,
|
||||
#[error("No such key")]
|
||||
NoSuchKey,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyStorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let err: String = String::from(match &self {
|
||||
//KeyStorageError::BadKey => "Bad key syntax",
|
||||
KeyStorageError::ProviderError => "Provider service error",
|
||||
KeyStorageError::NoSuchKey => "No such key",
|
||||
});
|
||||
write!(f, "{}", err)
|
||||
}
|
||||
}
|
||||
|
||||
/// ## KeyStorage
|
||||
///
|
||||
/// this traits provides the methods to communicate and interact with the key storage.
|
||||
@@ -76,14 +73,21 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::ProviderError),
|
||||
KeyStorageError::BadSytax.to_string(),
|
||||
String::from("Key has a bad syntax")
|
||||
);
|
||||
assert_eq!(
|
||||
KeyStorageError::ProviderError.to_string(),
|
||||
String::from("Provider service error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::NoSuchKey),
|
||||
KeyStorageError::NoSuchKey.to_string(),
|
||||
String::from("No such key")
|
||||
);
|
||||
}
|
||||
|
||||
72
src/system/logging.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! ## Logging
|
||||
//!
|
||||
//! `logging` is the module which initializes the logging system for termscp
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use crate::system::environment::{get_log_paths, init_config_dir};
|
||||
use crate::utils::file::open_file;
|
||||
// ext
|
||||
use simplelog::{ConfigBuilder, LevelFilter, WriteLogger};
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// ### init
|
||||
///
|
||||
/// Initialize logger
|
||||
pub fn init() -> Result<(), String> {
|
||||
// Init config dir
|
||||
let config_dir: PathBuf = match init_config_dir() {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => {
|
||||
return Err(String::from(
|
||||
"This system doesn't seem to support CONFIG_DIR",
|
||||
))
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let log_file_path: PathBuf = get_log_paths(config_dir.as_path());
|
||||
// Open log file
|
||||
let file: File = open_file(log_file_path.as_path(), true, true, false)
|
||||
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
|
||||
// Prepare log config
|
||||
let config = ConfigBuilder::new()
|
||||
.set_time_format_str("%Y-%m-%dT%H:%M:%S%z")
|
||||
.build();
|
||||
// Make logger
|
||||
WriteLogger::init(LevelFilter::Trace, config, file)
|
||||
.map_err(|e| format!("Failed to initialize logger: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_logging_setup() {
|
||||
assert!(init().is_ok());
|
||||
}
|
||||
}
|
||||
@@ -2,30 +2,34 @@
|
||||
//!
|
||||
//! `system` is the module which contains functions and data types related to current system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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(crate) mod keys;
|
||||
pub(self) mod keys;
|
||||
pub mod logging;
|
||||
pub mod sshkey_storage;
|
||||
pub mod theme_provider;
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
//!
|
||||
//! `SshKeyStorage` is the module which behaves a storage for ssh keys
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -40,6 +42,7 @@ impl SshKeyStorage {
|
||||
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
||||
let mut hosts: HashMap<String, PathBuf> =
|
||||
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
||||
debug!("Setting up SSH key storage");
|
||||
// Iterate over keys
|
||||
for key in cfg_client.iter_ssh_keys() {
|
||||
match cfg_client.get_ssh_key(key) {
|
||||
@@ -50,8 +53,12 @@ impl SshKeyStorage {
|
||||
}
|
||||
None => continue,
|
||||
},
|
||||
Err(_) => continue,
|
||||
Err(err) => {
|
||||
error!("Failed to get SSH key for {}: {}", key, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
info!("Got SSH key for {}", key);
|
||||
}
|
||||
// Return storage
|
||||
SshKeyStorage { hosts }
|
||||
@@ -60,6 +67,7 @@ impl SshKeyStorage {
|
||||
/// ### empty
|
||||
///
|
||||
/// Create an empty ssh key storage; used in case `ConfigClient` is not available
|
||||
#[cfg(test)]
|
||||
pub fn empty() -> Self {
|
||||
SshKeyStorage {
|
||||
hosts: HashMap::new(),
|
||||
@@ -80,19 +88,30 @@ impl SshKeyStorage {
|
||||
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 = create_tmp_dir();
|
||||
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()
|
||||
@@ -120,6 +139,16 @@ mod tests {
|
||||
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
|
||||
@@ -130,11 +159,4 @@ mod tests {
|
||||
c.push("config.toml");
|
||||
(c, k)
|
||||
}
|
||||
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
246
src/system/theme_provider.rs
Normal 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
|
||||
}
|
||||
}
|
||||
273
src/ui/activities/auth/bookmarks.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/ui/activities/auth/misc.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
263
src/ui/activities/auth/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
376
src/ui/activities/auth/update.rs
Normal 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
|
||||
}
|
||||
}
|
||||
795
src/ui/activities/auth/view.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate dirs;
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, Color, DialogYesNoOption, Popup};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::environment;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
|
||||
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 mut name: Option<String> = None;
|
||||
for (i, key) in bookmarks_cli.iter_bookmarks().enumerate() {
|
||||
if i == idx {
|
||||
name = Some(key.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(name) = name {
|
||||
bookmarks_cli.del_bookmark(&name);
|
||||
// Write bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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
|
||||
for (i, key) in bookmarks_cli.iter_bookmarks().enumerate() {
|
||||
if i == idx {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(&key) {
|
||||
// Load parameters
|
||||
self.address = bookmark.0;
|
||||
self.port = bookmark.1.to_string();
|
||||
self.protocol = bookmark.2;
|
||||
self.username = bookmark.3;
|
||||
if let Some(password) = bookmark.4 {
|
||||
self.password = password;
|
||||
}
|
||||
}
|
||||
// Break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_bookmark
|
||||
///
|
||||
/// Save current input fields as a bookmark
|
||||
pub(super) fn save_bookmark(&mut self, name: String) {
|
||||
// Check port
|
||||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
// Check if password must be saved
|
||||
let password: Option<String> = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => Some(self.password.clone()),
|
||||
DialogYesNoOption::No => None,
|
||||
};
|
||||
bookmarks_cli.add_bookmark(
|
||||
name,
|
||||
self.address.clone(),
|
||||
port,
|
||||
self.protocol,
|
||||
self.username.clone(),
|
||||
password,
|
||||
);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
}
|
||||
/// ### del_recent
|
||||
///
|
||||
/// Delete recent
|
||||
pub(super) fn del_recent(&mut self, idx: usize) {
|
||||
if let Some(client) = self.bookmarks_client.as_mut() {
|
||||
// Iterate over kyes
|
||||
let mut name: Option<String> = None;
|
||||
for (i, key) in client.iter_recents().enumerate() {
|
||||
if i == idx {
|
||||
name = Some(key.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(name) = name {
|
||||
client.del_recent(&name);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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
|
||||
for (i, key) in client.iter_recents().enumerate() {
|
||||
if i == idx {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// Load parameters
|
||||
self.address = bookmark.0;
|
||||
self.port = bookmark.1.to_string();
|
||||
self.protocol = bookmark.2;
|
||||
self.username = bookmark.3;
|
||||
// Break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_recent
|
||||
///
|
||||
/// Save current input fields as a "recent"
|
||||
pub(super) fn save_recent(&mut self) {
|
||||
// Check port
|
||||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
bookmarks_cli.add_recent(
|
||||
self.address.clone(),
|
||||
port,
|
||||
self.protocol,
|
||||
self.username.clone(),
|
||||
);
|
||||
// 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.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not write bookmarks: {}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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) => self.bookmarks_client = Some(cli),
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
bookmarks_file.display(),
|
||||
config_dir_path.display(),
|
||||
err
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize configuration directory: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{AuthActivity, InputForm};
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### callback_nothing_to_do
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_nothing_to_do(&mut self) {}
|
||||
|
||||
/// ### callback_quit
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_quit(&mut self) {
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// ### callback_del_bookmark
|
||||
///
|
||||
/// Callback which deletes recent or bookmark based on current form
|
||||
pub(super) fn callback_del_bookmark(&mut self) {
|
||||
match self.input_form {
|
||||
InputForm::Bookmarks => self.del_bookmark(self.bookmarks_idx),
|
||||
InputForm::Recents => self.del_recent(self.recents_idx),
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_save_bookmark
|
||||
///
|
||||
/// Callback used to save bookmark with name
|
||||
pub(super) fn callback_save_bookmark(&mut self, input: String) {
|
||||
if !input.is_empty() {
|
||||
self.save_bookmark(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{
|
||||
AuthActivity, DialogCallback, DialogYesNoOption, FileTransferProtocol, InputEvent, InputField,
|
||||
InputForm, Popup,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use tui::style::Color;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### handle_input_event
|
||||
///
|
||||
/// Handle input event, based on current input mode
|
||||
pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
let popup: Option<Popup> = match &self.popup {
|
||||
Some(ptype) => Some(ptype.clone()),
|
||||
_ => None,
|
||||
};
|
||||
match &self.popup {
|
||||
None => self.handle_input_event_mode_form(ev),
|
||||
Some(_) => {
|
||||
if let Some(ptype) = popup {
|
||||
self.handle_input_event_mode_popup(ev, ptype)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form
|
||||
///
|
||||
/// Handler for input event when in form mode
|
||||
fn handle_input_event_mode_form(&mut self, ev: &InputEvent) {
|
||||
match self.input_form {
|
||||
InputForm::AuthCredentials => self.handle_input_event_mode_form_auth(ev),
|
||||
InputForm::Bookmarks => self.handle_input_event_mode_form_bookmarks(ev),
|
||||
InputForm::Recents => self.handle_input_event_mode_form_recents(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_auth
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Auth
|
||||
fn handle_input_event_mode_form_auth(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::Bookmarks, // Move to bookmarks
|
||||
KeyCode::Enter => {
|
||||
// Handle submit
|
||||
// Check form
|
||||
// Check address
|
||||
if self.address.is_empty() {
|
||||
self.popup =
|
||||
Some(Popup::Alert(Color::Red, String::from("Invalid address")));
|
||||
return;
|
||||
}
|
||||
// Check port
|
||||
// Convert port to number
|
||||
match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save recent
|
||||
self.save_recent();
|
||||
// Everything OK, set enter
|
||||
self.submit = true;
|
||||
}
|
||||
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 => {
|
||||
// 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) => {
|
||||
// Check if Ctrl is enabled
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// If 'S', save bookmark as...
|
||||
match ch {
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'C' | 'c' => {
|
||||
// Show setup
|
||||
self.setup = true;
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
} else {
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_bookmarks
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Bookmarks
|
||||
fn handle_input_event_mode_form_bookmarks(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::AuthCredentials, // Move to Auth credentials
|
||||
KeyCode::Right => self.input_form = InputForm::Recents, // Move to recents
|
||||
KeyCode::Up => {
|
||||
// Move bookmarks index up
|
||||
if self.bookmarks_idx > 0 {
|
||||
self.bookmarks_idx -= 1;
|
||||
} else if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
// Put to last index (wrap)
|
||||
self.bookmarks_idx = bookmarks_cli.iter_bookmarks().count() - 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
let size: usize = bookmarks_cli.iter_bookmarks().count();
|
||||
// Check if can move down
|
||||
if self.bookmarks_idx + 1 >= size {
|
||||
// Move bookmarks index down
|
||||
self.bookmarks_idx = 0;
|
||||
} else {
|
||||
// Set index to first element (wrap)
|
||||
self.bookmarks_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected bookmark?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load bookmark
|
||||
self.load_bookmark(self.bookmarks_idx);
|
||||
// Set input form to Auth
|
||||
self.input_form = InputForm::AuthCredentials;
|
||||
// Set input field to password (very comfy)
|
||||
self.selected_field = InputField::Password;
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'C' | 'c' => {
|
||||
// Show setup
|
||||
self.setup = true;
|
||||
}
|
||||
'E' | 'e' => {
|
||||
// Ask if user wants to delete bookmark; NOTE: same as <DEL>
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected bookmark?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_recents
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Recents
|
||||
fn handle_input_event_mode_form_recents(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::AuthCredentials, // Move to Auth credentials
|
||||
KeyCode::Left => self.input_form = InputForm::Bookmarks, // Move to bookmarks
|
||||
KeyCode::Up => {
|
||||
// Move bookmarks index up
|
||||
if self.recents_idx > 0 {
|
||||
self.recents_idx -= 1;
|
||||
} else if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
// Put to last index (wrap)
|
||||
self.recents_idx = bookmarks_cli.iter_recents().count() - 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
let size: usize = bookmarks_cli.iter_recents().count();
|
||||
// Check if can move down
|
||||
if self.recents_idx + 1 >= size {
|
||||
// Move bookmarks index down
|
||||
self.recents_idx = 0;
|
||||
} else {
|
||||
// Set index to first element (wrap)
|
||||
self.recents_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected host?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load bookmark
|
||||
self.load_recent(self.recents_idx);
|
||||
// Set input form to Auth
|
||||
self.input_form = InputForm::AuthCredentials;
|
||||
// Set input field to password (very comfy)
|
||||
self.selected_field = InputField::Password;
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'C' | 'c' => {
|
||||
// Show setup
|
||||
self.setup = true;
|
||||
}
|
||||
'E' | 'e' => {
|
||||
// Ask if user wants to delete bookmark; NOTE: same as <DEL>
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected host?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* 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, ptype: Popup) {
|
||||
match ptype {
|
||||
Popup::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
Popup::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
Popup::SaveBookmark => self.handle_input_event_mode_popup_save_bookmark(ev),
|
||||
Popup::YesNo(_, yes_cb, no_cb) => {
|
||||
self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_alert
|
||||
///
|
||||
/// Handle input event when the input mode is popup, and popup type is alert
|
||||
fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
self.popup = None; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_help
|
||||
///
|
||||
/// Input event handler for popup help
|
||||
fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Set input mode back to form
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_save_bookmark
|
||||
///
|
||||
/// Input event handler for SaveBookmark popup
|
||||
fn handle_input_event_mode_popup_save_bookmark(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup, otherwise push chars to input
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Abort input
|
||||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form
|
||||
self.popup = None;
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Submit
|
||||
let input_text: String = self.input_txt.clone();
|
||||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.popup = None;
|
||||
// Call cb
|
||||
self.callback_save_bookmark(input_text);
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Move yes/no with arrows
|
||||
KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Move yes/no with arrows
|
||||
KeyCode::Char(ch) => self.input_txt.push(ch),
|
||||
KeyCode::Backspace => {
|
||||
let _ = self.input_txt.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_yesno
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
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
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
// @! Set input mode to Form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.popup = None;
|
||||
// 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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,638 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{
|
||||
AuthActivity, Context, DialogYesNoOption, FileTransferProtocol, InputField, InputForm, Popup,
|
||||
};
|
||||
use crate::utils::fmt::align_text_center;
|
||||
// Ext
|
||||
use std::string::ToString;
|
||||
use tui::{
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### draw
|
||||
///
|
||||
/// Draw UI
|
||||
pub(super) fn draw(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
// Prepare chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(70), // Auth Form
|
||||
Constraint::Percentage(30), // Bookmarks
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Create explorer chunks
|
||||
let auth_chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.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]);
|
||||
// Draw header
|
||||
f.render_widget(self.draw_header(), auth_chunks[0]);
|
||||
// Draw input fields
|
||||
f.render_widget(self.draw_remote_address(), auth_chunks[1]);
|
||||
f.render_widget(self.draw_remote_port(), auth_chunks[2]);
|
||||
f.render_widget(self.draw_protocol_select(), auth_chunks[3]);
|
||||
f.render_widget(self.draw_protocol_username(), auth_chunks[4]);
|
||||
f.render_widget(self.draw_protocol_password(), auth_chunks[5]);
|
||||
// Draw footer
|
||||
f.render_widget(self.draw_footer(), auth_chunks[6]);
|
||||
// Set cursor
|
||||
if let InputForm::AuthCredentials = self.input_form {
|
||||
match self.selected_field {
|
||||
InputField::Address => f.set_cursor(
|
||||
auth_chunks[1].x + self.address.width() as u16 + 1,
|
||||
auth_chunks[1].y + 1,
|
||||
),
|
||||
InputField::Port => f.set_cursor(
|
||||
auth_chunks[2].x + self.port.width() as u16 + 1,
|
||||
auth_chunks[2].y + 1,
|
||||
),
|
||||
InputField::Username => f.set_cursor(
|
||||
auth_chunks[4].x + self.username.width() as u16 + 1,
|
||||
auth_chunks[4].y + 1,
|
||||
),
|
||||
InputField::Password => f.set_cursor(
|
||||
auth_chunks[5].x + self.password_placeholder.width() as u16 + 1,
|
||||
auth_chunks[5].y + 1,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Draw bookmarks
|
||||
if let Some(tab) = self.draw_bookmarks_tab() {
|
||||
let mut bookmarks_state: ListState = ListState::default();
|
||||
bookmarks_state.select(Some(self.bookmarks_idx));
|
||||
f.render_stateful_widget(tab, bookmark_chunks[0], &mut bookmarks_state);
|
||||
}
|
||||
if let Some(tab) = self.draw_recents_tab() {
|
||||
let mut recents_state: ListState = ListState::default();
|
||||
recents_state.select(Some(self.recents_idx));
|
||||
f.render_stateful_widget(tab, bookmark_chunks[1], &mut recents_state);
|
||||
}
|
||||
// Draw popup
|
||||
if let Some(popup) = &self.popup {
|
||||
// Calculate popup size
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
Popup::Alert(_, _) => (50, 10),
|
||||
Popup::Help => (50, 70),
|
||||
Popup::SaveBookmark => (20, 20),
|
||||
Popup::YesNo(_, _, _) => (30, 10),
|
||||
};
|
||||
let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
match popup {
|
||||
Popup::Alert(color, txt) => f.render_widget(
|
||||
self.draw_popup_alert(*color, txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
Popup::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
Popup::SaveBookmark => {
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Input form
|
||||
Constraint::Length(2), // Yes/No
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_area);
|
||||
let (input, yes_no): (Paragraph, Tabs) = self.draw_popup_save_bookmark();
|
||||
// Render parts
|
||||
f.render_widget(input, popup_chunks[0]);
|
||||
f.render_widget(yes_no, popup_chunks[1]);
|
||||
// Set cursor
|
||||
f.set_cursor(
|
||||
popup_chunks[0].x + self.input_txt.width() as u16 + 1,
|
||||
popup_chunks[0].y + 1,
|
||||
)
|
||||
}
|
||||
Popup::YesNo(txt, _, _) => {
|
||||
f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
/// ### 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)
|
||||
.border_type(BorderType::Rounded)
|
||||
.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)
|
||||
.border_type(BorderType::Rounded)
|
||||
.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)
|
||||
.border_type(BorderType::Rounded)
|
||||
.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)
|
||||
.border_type(BorderType::Rounded)
|
||||
.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)
|
||||
.border_type(BorderType::Rounded)
|
||||
.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(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Cyan),
|
||||
),
|
||||
Span::raw(" to show keybindings; "),
|
||||
Span::styled(
|
||||
"<CTRL+C>",
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Cyan),
|
||||
),
|
||||
Span::raw(" to enter setup"),
|
||||
],
|
||||
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_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
fn draw_bookmarks_tab(&self) -> Option<List> {
|
||||
self.bookmarks_client.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter_bookmarks()
|
||||
.map(|key: &String| {
|
||||
let entry: (String, u16, FileTransferProtocol, String, _) = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_bookmark(key)
|
||||
.unwrap();
|
||||
ListItem::new(Span::from(format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
key,
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.input_form {
|
||||
InputForm::Bookmarks => (Color::Black, Color::LightGreen),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Bookmarks => Style::default().fg(Color::LightGreen),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title("Bookmarks"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
fn draw_recents_tab(&self) -> Option<List> {
|
||||
self.bookmarks_client.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter_recents()
|
||||
.map(|key: &String| {
|
||||
let entry: (String, u16, FileTransferProtocol, String) = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_recent(key)
|
||||
.unwrap();
|
||||
ListItem::new(Span::from(format!(
|
||||
"{}://{}@{}:{}",
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.input_form {
|
||||
InputForm::Recents => (Color::Black, Color::LightBlue),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Recents => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title("Recent connections"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_area
|
||||
///
|
||||
/// Draw popup area
|
||||
fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
Constraint::Percentage(height),
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
Constraint::Percentage(width),
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
/// ### draw_popup_alert
|
||||
///
|
||||
/// Draw alert popup
|
||||
fn draw_popup_alert(&self, color: Color, text: String, width: u16) -> List {
|
||||
// Wraps texts
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(color))
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Alert"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(Style::default().fg(color))
|
||||
}
|
||||
|
||||
/// ### draw_popup_input
|
||||
///
|
||||
/// Draw input popup
|
||||
fn draw_popup_save_bookmark(&self) -> (Paragraph, Tabs) {
|
||||
let input: Paragraph = Paragraph::new(self.input_txt.as_ref())
|
||||
.style(Style::default().fg(Color::White))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Save bookmark as..."),
|
||||
);
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => 0,
|
||||
DialogYesNoOption::No => 1,
|
||||
};
|
||||
let tabs: Tabs = Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Save password?"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::LightRed),
|
||||
);
|
||||
(input, tabs)
|
||||
}
|
||||
|
||||
/// ### draw_popup_yesno
|
||||
///
|
||||
/// Draw yes/no select popup
|
||||
fn draw_popup_yesno(&self, text: String) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => 0,
|
||||
DialogYesNoOption::No => 1,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(text),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Yellow),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_help
|
||||
///
|
||||
/// Draw authentication page help popup
|
||||
fn draw_popup_help(&self) -> List {
|
||||
// Write header
|
||||
let cmds: Vec<ListItem> = vec![
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ESC>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Quit TermSCP"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<TAB>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Switch input form and bookmarks"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<RIGHT/LEFT>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change bookmark tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<UP/DOWN>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Move up/down in current tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ENTER>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Submit"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<DEL>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete bookmark"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete selected bookmark"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+C>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Enter setup"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+S>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Save bookmark"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Help"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Sub modules
|
||||
mod bookmarks;
|
||||
mod callbacks;
|
||||
mod input;
|
||||
mod layout;
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::path::PathBuf;
|
||||
use tui::style::Color;
|
||||
|
||||
// Types
|
||||
type DialogCallback = fn(&mut AuthActivity);
|
||||
|
||||
/// ### InputField
|
||||
///
|
||||
/// InputField describes the current input field to edit
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputField {
|
||||
Address,
|
||||
Port,
|
||||
Protocol,
|
||||
Username,
|
||||
Password,
|
||||
}
|
||||
|
||||
/// ### DialogYesNoOption
|
||||
///
|
||||
/// Current yes/no dialog option
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum DialogYesNoOption {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// ### Popup
|
||||
///
|
||||
/// Popup describes the type of the popup displayed
|
||||
#[derive(Clone)]
|
||||
enum Popup {
|
||||
Alert(Color, String), // Show a message displaying text with the provided color
|
||||
Help, // Help page
|
||||
SaveBookmark,
|
||||
YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
|
||||
}
|
||||
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
/// ### InputForm
|
||||
///
|
||||
/// InputForm describes the selected input form
|
||||
enum InputForm {
|
||||
AuthCredentials,
|
||||
Bookmarks,
|
||||
Recents,
|
||||
}
|
||||
|
||||
/// ### 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
|
||||
pub setup: bool, // Becomes true if user has requested setup
|
||||
context: Option<Context>,
|
||||
bookmarks_client: Option<BookmarksClient>,
|
||||
config_client: Option<ConfigClient>,
|
||||
selected_field: InputField, // Selected field in AuthCredentials Form
|
||||
popup: Option<Popup>,
|
||||
input_form: InputForm,
|
||||
password_placeholder: String,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
input_txt: String, // Input text
|
||||
choice_opt: DialogYesNoOption, // Dialog popup selected option
|
||||
bookmarks_idx: usize, // Index of selected bookmark
|
||||
recents_idx: usize, // Index of selected recent
|
||||
}
|
||||
|
||||
impl Default for AuthActivity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
setup: false,
|
||||
context: None,
|
||||
bookmarks_client: None,
|
||||
config_client: None,
|
||||
selected_field: InputField::Address,
|
||||
popup: None,
|
||||
input_form: InputForm::AuthCredentials,
|
||||
password_placeholder: String::new(),
|
||||
redraw: true, // True at startup
|
||||
input_txt: String::new(),
|
||||
choice_opt: DialogYesNoOption::Yes,
|
||||
bookmarks_idx: 0,
|
||||
recents_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### init_config_client
|
||||
///
|
||||
/// Initialize config client
|
||||
fn init_config_client(&mut self) {
|
||||
// Get config dir
|
||||
match environment::init_config_dir() {
|
||||
Ok(config_dir) => {
|
||||
if let Some(config_dir) = 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) => {
|
||||
// Set default protocol
|
||||
self.protocol = cli.get_default_protocol();
|
||||
// Set client
|
||||
self.config_client = Some(cli);
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize user configuration: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize configuration directory: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
self.popup = None;
|
||||
// Init bookmarks client
|
||||
if self.bookmarks_client.is_none() {
|
||||
self.init_bookmarks_client();
|
||||
}
|
||||
// init config client
|
||||
if self.config_client.is_none() {
|
||||
self.init_config_client();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
if let Some(event) = event {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
}
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Draw
|
||||
self.draw();
|
||||
// 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();
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/ui/activities/filetransfer/actions/change_dir.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
263
src/ui/activities/filetransfer/actions/copy.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/ui/activities/filetransfer/actions/delete.rs
Normal 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
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
232
src/ui/activities/filetransfer/actions/edit.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
66
src/ui/activities/filetransfer/actions/exec.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
221
src/ui/activities/filetransfer/actions/find.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/ui/activities/filetransfer/actions/mkdir.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
src/ui/activities/filetransfer/actions/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/ui/activities/filetransfer/actions/newfile.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/ui/activities/filetransfer/actions/open.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/ui/activities/filetransfer/actions/rename.rs
Normal 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
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/ui/activities/filetransfer/actions/save.rs
Normal 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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/ui/activities/filetransfer/actions/submit.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/ui/activities/filetransfer/lib/browser.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
29
src/ui/activities/filetransfer/lib/mod.rs
Normal 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;
|
||||