50 Commits

Author SHA1 Message Date
veeso
150a3cf346 Arch pkgs 2021-04-13 20:42:59 +02:00
veeso
b31de185c5 Merge branch '0.4.2' into main 2021-04-13 18:44:02 +02:00
Christian Visintin
477930bef9 Merge pull request #24 from veeso/imgbot
[ImgBot] Optimize images
2021-04-13 12:03:06 +02:00
ImgBotApp
03bbd6420e [ImgBot] Optimize images
*Total -- 3,783.77kb -> 3,022.10kb (20.13%)

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

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

  brew install veeso/termscp/termscp

is equivalent to the two commands

  brew tap veeso/termscp
  brew install termscp
2021-04-05 17:22:30 +02:00
veeso
6be9294e11 stable toolchain 2021-04-05 10:04:53 +02:00
veeso
7676e6e3a1 Improved coverage 2021-04-05 09:59:10 +02:00
veeso
9776ecbe60 Removed codecov path fix 2021-04-05 09:40:57 +02:00
veeso
a1632492ed Restored coverage with tarpaulin 2021-04-05 09:32:59 +02:00
veeso
8d74d4c4e5 FTP: added support for symlinks for Linux servers 2021-04-04 18:03:11 +02:00
veeso
e29ce3d0dd Merge branch '0.4.1' into issue-17-still-some-problems-with-symlinks 2021-04-04 17:33:00 +02:00
veeso
e6b952966c SCP: fixed symlink not properly detected 2021-04-04 17:29:17 +02:00
Christian Visintin
1ba139aed1 Merge pull request #19 from veeso/issue-18-every-ftp-transfer-is-made-in-ascii-mode
[BUG] every ftp transfer is made in ascii mode
2021-04-04 17:18:49 +02:00
veeso
f136057484 File transfer errors: to_string instead of format! 2021-04-04 16:32:54 +02:00
veeso
47cd112e69 FTP: transfer type set to binary 2021-04-04 16:32:39 +02:00
veeso
871a02c8b5 Updated contributing, issue templates and docs 2021-04-04 16:08:16 +02:00
veeso
44ba1111af Fixed test for edit 0.1.3 2021-04-03 22:20:20 +02:00
veeso
52b35f9232 Updated dependencies 2021-04-03 17:45:02 +02:00
veeso
f8a448f5e9 Use default color if Text span part is Reset 2021-04-03 17:27:43 +02:00
Christian Visintin
37da49f4f8 Merge pull request #16 from veeso/issue-13-could-not-start-activity-manager-no-such-file-or-directory
[BUG] Issue 13 could not start activity manager no such file or directory
2021-04-03 17:19:23 +02:00
veeso
c0ae922264 format 2021-04-03 16:48:37 +02:00
veeso
91081cb86a Use thiserror to format error messages 2021-04-03 16:33:18 +02:00
veeso
af678802bb Added path to HostError; scan_dir won't fail if it is not possible to stat an entry 2021-04-03 16:21:37 +02:00
veeso
66068ec73c Merge branch '0.4.1' into issue-13-could-not-start-activity-manager-no-such-file-or-directory 2021-04-03 15:47:31 +02:00
Christian Visintin
32e939c183 Merge pull request #15 from veeso/issue-9-some-problems-with-symlinks-hash-and-backslash-characters
[BUG] Issue 9 some problems with symlinks hash and backslash characters
2021-04-02 22:31:56 +02:00
veeso
b610da16a9 Fix remote paths for Windows 2021-04-02 22:09:58 +02:00
veeso
5dfcba3c51 Merge branch '0.4.1' into issue-13-could-not-start-activity-manager-no-such-file-or-directory 2021-04-01 22:20:50 +02:00
veeso
d48e05cd74 Merge branch '0.4.1' of github.com:veeso/termscp into 0.4.1 2021-04-01 22:20:29 +02:00
veeso
6f4cb46d94 Merge branch 'main' into 0.4.1 2021-04-01 22:19:19 +02:00
veeso
5886d90d16 Merge branch '0.4.1' into issue-13-could-not-start-activity-manager-no-such-file-or-directory 2021-04-01 22:18:45 +02:00
Christian Visintin
ade7160c20 Merge pull request #11 from veeso/issue-10-port-number-isnt-correctly-retrieved-from-the-bookmarks.toml
Issue 10 port number isnt correctly retrieved from the bookmarks.toml
2021-04-01 22:06:22 +02:00
veeso
0c22b322ae Fixed sha256sum 2021-03-30 22:07:23 +02:00
veeso
a5ba118393 changelog 2021-03-29 21:16:14 +02:00
veeso
7acf119c77 Fixed port not being loaded from bookmarks into gui 2021-03-29 21:14:35 +02:00
veeso
bd00ba7971 Working on 0.4.1 2021-03-29 20:59:55 +02:00
veeso
f94a811dd9 Missing 'S' key in keymap 2021-03-28 16:55:26 +02:00
49 changed files with 1191 additions and 732 deletions

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

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

View File

@@ -1,9 +1,9 @@
---
name: Bug report
about: Create a report to help me improving TermSCP
title: "[BUG]"
about: Create a report of the bug you've encountered
title: "[BUG] - ISSUE_TITLE"
labels: bug
assignees: ChristianVisintin
assignees: veeso
---
@@ -24,7 +24,7 @@ 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

View File

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

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

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

View File

@@ -1,4 +1,4 @@
# Pull Request Title
# ISSUE _NUMBER_ - PULL_REQUEST_TITLE
Fixes # (issue)
@@ -25,7 +25,10 @@ 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

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

@@ -0,0 +1,22 @@
name: coverage
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
coverage:
name: Generate coverage
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run cargo-tarpaulin
uses: actions-rs/tarpaulin@v0.1
with:
args: "--ignore-tests -- --test-threads 1"
- name: Upload to codecov.io
uses: codecov/codecov-action@v1

View File

@@ -13,24 +13,14 @@ jobs:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
toolchain: stable
override: true
components: rustfmt, clippy
- 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"
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy -- -Dwarnings
- 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 }}

View File

@@ -1,6 +1,8 @@
# Changelog
- [Changelog](#changelog)
- [0.4.2](#042)
- [0.4.1](#041)
- [0.4.0](#040)
- [0.3.3](#033)
- [0.3.2](#032)
@@ -14,10 +16,47 @@
---
## 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.

View File

@@ -4,14 +4,90 @@ 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)
- [The Context](#the-context)
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
- [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).
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 probability to encounter it and the difficult to fix 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.
---
@@ -23,217 +99,38 @@ At the moment, these kind of contributions are more appreciated and should be pr
- 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.
- A **logo** for the project: I'd really love to have a logo for termscp 💛
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 a PR with an **appropriate label** (e.g. bug, enhancement, ...).
2. Write a **properly documentation** compliant with **rustdoc** standard.
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. Report changes to the PR 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.
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. 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.
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
---
### Tests fails due to receivers
Yes. This happens quite often and is related to the fact that I'm using public SSH/SFTP/FTP server to test file receivers and sometimes this server go down for even a day or more. If your tests don't pass due to this, don't worry, submit the pull request and I'll take care of testing them by myself.
---
### 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).
---

364
Cargo.lock generated
View File

@@ -81,24 +81,14 @@ dependencies = [
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d70f2a8c3126a2aec089e0aebcd945607e1155bfb5b89682eddf43c3ce386718"
dependencies = [
"block-padding 0.1.5",
"byte-tools 0.2.0",
"generic-array 0.11.1",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.4",
"block-padding",
"generic-array",
]
[[package]]
@@ -107,19 +97,10 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0"
dependencies = [
"block-padding 0.2.1",
"block-padding",
"cipher",
]
[[package]]
name = "block-padding"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
dependencies = [
"byte-tools 0.3.1",
]
[[package]]
name = "block-padding"
version = "0.2.1"
@@ -128,27 +109,15 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
[[package]]
name = "bumpalo"
version = "3.5.0"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59"
[[package]]
name = "byte-tools"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40"
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
[[package]]
name = "byteorder"
version = "1.4.2"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytesize"
@@ -164,9 +133,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.66"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cfg-if"
@@ -205,7 +174,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
dependencies = [
"generic-array 0.14.4",
"generic-array",
]
[[package]]
@@ -281,9 +250,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.1"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
@@ -346,7 +315,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
dependencies = [
"generic-array 0.14.4",
"generic-array",
"subtle",
]
@@ -376,22 +345,13 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "digest"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90"
dependencies = [
"generic-array 0.9.0",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.4",
"generic-array",
]
[[package]]
@@ -416,14 +376,20 @@ dependencies = [
[[package]]
name = "edit"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323032447eba6f5aca88b46d6e7815151c16c53e4128569420c09d7840db3bfc"
checksum = "3cdd6936f8bd9782e28932eef853bfcd8548992ce5748bb3e7e88bad613d0ee0"
dependencies = [
"tempfile",
"which",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -461,24 +427,6 @@ dependencies = [
"regex",
]
[[package]]
name = "generic-array"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8107dafa78c80c848b71b60133954b4a58609a3a1a5f9af037ecc7f67280f369"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.4"
@@ -526,7 +474,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [
"digest 0.9.0",
"digest",
"hmac",
]
@@ -537,7 +485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [
"crypto-mac",
"digest 0.9.0",
"digest",
]
[[package]]
@@ -579,9 +527,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.46"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175"
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
dependencies = [
"wasm-bindgen",
]
@@ -606,15 +554,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.82"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
[[package]]
name = "libssh2-sys"
version = "0.2.20"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df40b13fe7ea1be9b9dffa365a51273816c345fc1811478b57ed7d964fbfc4ce"
checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee"
dependencies = [
"cc",
"libc",
@@ -647,38 +595,37 @@ dependencies = [
[[package]]
name = "lock_api"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312"
checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if 0.1.10",
"cfg-if 1.0.0",
]
[[package]]
name = "magic-crypt"
version = "3.1.6"
version = "3.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a01cf5086c27e3daff2a06886ab2fc44fe4fdec7d2df7a82e5329483011bfd7"
checksum = "6a7d8d3790b76ab76cc459a707e09009fcd8ef8da8999d7a99c9bb9b9bef8890"
dependencies = [
"aes-soft",
"base64",
"block-modes",
"crc-any",
"des",
"digest 0.7.6",
"digest 0.9.0",
"digest",
"md-5",
"sha2",
"tiger-digest",
"tiger",
]
[[package]]
@@ -699,8 +646,8 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
dependencies = [
"block-buffer 0.9.0",
"digest 0.9.0",
"block-buffer",
"digest",
"opaque-debug",
]
@@ -712,9 +659,9 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "mio"
version = "0.7.7"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
dependencies = [
"libc",
"log",
@@ -725,11 +672,10 @@ dependencies = [
[[package]]
name = "miow"
version = "0.3.6"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"socket2",
"winapi",
]
@@ -746,8 +692,8 @@ dependencies = [
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework 2.0.0",
"security-framework-sys 2.0.0",
"security-framework 2.2.0",
"security-framework-sys 2.2.0",
"tempfile",
]
@@ -776,9 +722,9 @@ dependencies = [
[[package]]
name = "num-bigint"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf"
checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba"
dependencies = [
"autocfg",
"num-integer",
@@ -838,9 +784,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.5.2"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "opaque-debug"
@@ -850,15 +796,15 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.32"
version = "0.10.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70"
checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"foreign-types",
"lazy_static",
"libc",
"once_cell",
"openssl-sys",
]
@@ -870,9 +816,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
[[package]]
name = "openssl-sys"
version = "0.9.60"
version = "0.9.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6"
checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f"
dependencies = [
"autocfg",
"cc",
@@ -898,8 +844,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
dependencies = [
"instant",
"lock_api 0.4.2",
"parking_lot_core 0.8.2",
"lock_api 0.4.3",
"parking_lot_core 0.8.3",
]
[[package]]
@@ -918,18 +864,24 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
dependencies = [
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall 0.1.57",
"redox_syscall 0.2.5",
"smallvec",
"winapi",
]
[[package]]
name = "path-slash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619"
[[package]]
name = "percent-encoding"
version = "2.1.0"
@@ -950,18 +902,18 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro2"
version = "1.0.24"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
@@ -981,13 +933,13 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18519b42a40024d661e1714153e9ad0c3de27cd495760ceb09710920f1098b1e"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
dependencies = [
"libc",
"rand_chacha 0.3.0",
"rand_core 0.6.1",
"rand_core 0.6.2",
"rand_hc 0.3.0",
]
@@ -1008,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
dependencies = [
"ppv-lite86",
"rand_core 0.6.1",
"rand_core 0.6.2",
]
[[package]]
@@ -1022,9 +974,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
dependencies = [
"getrandom 0.2.2",
]
@@ -1044,7 +996,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
dependencies = [
"rand_core 0.6.1",
"rand_core 0.6.2",
]
[[package]]
@@ -1055,9 +1007,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
dependencies = [
"bitflags",
]
@@ -1075,21 +1027,20 @@ dependencies = [
[[package]]
name = "regex"
version = "1.4.3"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.22"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
[[package]]
name = "remove_dir_all"
@@ -1213,15 +1164,15 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.0.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69"
checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84"
dependencies = [
"bitflags",
"core-foundation 0.9.1",
"core-foundation-sys 0.8.2",
"libc",
"security-framework-sys 2.0.0",
"security-framework-sys 2.2.0",
]
[[package]]
@@ -1236,9 +1187,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.0.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b"
checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339"
dependencies = [
"core-foundation-sys 0.8.2",
"libc",
@@ -1246,18 +1197,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.121"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6159e3c76cab06f6bc466244d43b35e77e9500cd685da87620addadc2a4c40b1"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.121"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3fcab8778dc651bc65cfab2e4eb64996f3c912b74002fb379c94517e1f27c46"
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
dependencies = [
"proc-macro2",
"quote",
@@ -1277,14 +1228,14 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8"
checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
dependencies = [
"block-buffer 0.9.0",
"block-buffer",
"cfg-if 1.0.0",
"cpuid-bool",
"digest 0.9.0",
"digest",
"opaque-debug",
]
@@ -1316,20 +1267,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "smawk"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1bc737c97d093feb72e67f4926d9b22d717ce8580cd25f0ce86d74e859c466d"
[[package]]
name = "socket2"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
dependencies = [
"cfg-if 1.0.0",
"libc",
"winapi",
]
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "spin"
@@ -1339,9 +1279,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "ssh2"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed024de0a5e6944fe3080a3745e7a5649c5517ea2a6cfb16333f94f89c248985"
checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7"
dependencies = [
"bitflags",
"libc",
@@ -1357,9 +1297,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
[[package]]
name = "syn"
version = "1.0.59"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07cb8b1b4ebf86a89ee88cbd201b022b94138c623644d035185c84d3f41b7e66"
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
dependencies = [
"proc-macro2",
"quote",
@@ -1374,15 +1314,15 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if 1.0.0",
"libc",
"rand 0.8.2",
"redox_syscall 0.2.4",
"rand 0.8.3",
"redox_syscall 0.2.5",
"remove_dir_all",
"winapi",
]
[[package]]
name = "termscp"
version = "0.4.0"
version = "0.4.2"
dependencies = [
"bitflags",
"bytesize",
@@ -1397,13 +1337,15 @@ dependencies = [
"keyring",
"lazy_static",
"magic-crypt",
"rand 0.8.2",
"path-slash",
"rand 0.8.3",
"regex",
"rpassword",
"serde",
"ssh2",
"tempfile",
"textwrap",
"thiserror",
"toml",
"tui",
"ureq",
@@ -1414,32 +1356,43 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.13.2"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cdcf6b66102d38821c33eea2bf1e8b7bd738072171cbf8a0683fbb46fcb8b0b"
checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835"
dependencies = [
"smawk",
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.1.1"
name = "thiserror"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301bdd13d23c49672926be451130892d274d3ba0b410c18e00daa7990ff38d99"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"once_cell",
"thiserror-impl",
]
[[package]]
name = "tiger-digest"
version = "0.1.1"
name = "thiserror-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68067e91b4b9bb2e1ce3dc55077c984bbe2fa2be65308264dab403c165257545"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
"block-buffer 0.5.1",
"byte-tools 0.2.0",
"digest 0.7.6",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tiger"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443e531cbcf9de83258cfef70bcd56c91188de5819ebd4b19c85f589e0617005"
dependencies = [
"block-buffer",
"byteorder",
"digest",
]
[[package]]
@@ -1492,9 +1445,9 @@ dependencies = [
[[package]]
name = "typenum"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "unicode-bidi"
@@ -1540,9 +1493,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.0.2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6585dcbf3483242f77b502864478ede62431baf3442b99367d3456ec20c1707b"
checksum = "6fbeb1aabb07378cf0e084971a74f24241273304653184f54cdce113c0d7df1b"
dependencies = [
"base64",
"chunked_transfer",
@@ -1586,9 +1539,9 @@ checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
[[package]]
name = "version_check"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasi"
@@ -1604,9 +1557,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.69"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e"
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
@@ -1614,9 +1567,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.69"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62"
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
dependencies = [
"bumpalo",
"lazy_static",
@@ -1629,9 +1582,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.69"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084"
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1639,9 +1592,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.69"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
dependencies = [
"proc-macro2",
"quote",
@@ -1652,15 +1605,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.69"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158"
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
[[package]]
name = "web-sys"
version = "0.3.46"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3"
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -1678,27 +1631,28 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376"
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
dependencies = [
"webpki",
]
[[package]]
name = "which"
version = "3.1.1"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
dependencies = [
"either",
"libc",
]
[[package]]
name = "whoami"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a921c0ad578a51c0b6c0bbb9b95f0ed11e90d61da506139e48a946edd11ee1e"
checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70"
dependencies = [
"wasm-bindgen",
"web-sys",
@@ -1706,9 +1660,9 @@ dependencies = [
[[package]]
name = "wildmatch"
version = "1.1.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
checksum = "07ae7ce410f81ba679081aac1d4874f3b1c328535b630209aa5b4cdaaf895e20"
[[package]]
name = "winapi"

View File

@@ -1,59 +1,17 @@
[package]
name = "termscp"
version = "0.4.0"
authors = ["Christian Visintin"]
edition = "2018"
license = "MIT"
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"
documentation = "https://docs.rs/termscp"
readme = "README.md"
edition = "2018"
homepage = "https://github.com/veeso/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.19.0"
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.2"
regex = "1.4.2"
rpassword = "5.0.1"
serde = { version = "1.0.121", features = ["derive"] }
ssh2 = "0.9.0"
tempfile = "3.1.0"
textwrap = "0.13.1"
toml = "0.5.8"
tui = { version = "0.14.0", features = ["crossterm"], default-features = false }
ureq = { version = "2.0.2", features = ["json"] }
whoami = "1.1.0"
wildmatch = "1.0.13"
[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"
# Features
[features]
githubActions = [] # used to run particular on github actions
[[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.4.2"
[package.metadata.rpm]
package = "termscp"
@@ -63,3 +21,62 @@ buildflags = ["--release"]
[package.metadata.rpm.targets]
termscp = { path = "/usr/bin/termscp" }
[[bin]]
name = "termscp"
path = "src/main.rs"
[dependencies]
bitflags = "1.2.1"
bytesize = "1.0.1"
chrono = "0.4.19"
content_inspector = "0.2.4"
crossterm = "0.19.0"
dirs = "3.0.1"
edit = "0.1.3"
getopts = "0.2.21"
hostname = "0.3.1"
lazy_static = "1.4.0"
magic-crypt = "3.1.7"
rand = "0.8.3"
regex = "1.4.5"
rpassword = "5.0.1"
ssh2 = "0.9.0"
tempfile = "3.1.0"
textwrap = "0.13.4"
thiserror = "^1.0.0"
toml = "0.5.8"
whoami = "1.1.1"
wildmatch = "2.0.0"
[dependencies.ftp4]
features = ["secure"]
version = "^4.0.2"
[dependencies.serde]
features = ["derive"]
version = "^1.0.0"
[dependencies.tui]
default-features = false
features = ["crossterm"]
version = "0.14.0"
[dependencies.ureq]
features = ["json"]
version = "2.1.0"
[features]
githubActions = []
[target."cfg(any(target_os = \"unix\", target_os = \"macos\", target_os = \"linux\"))"]
[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\"))"]
[target."cfg(any(target_os = \"windows\", target_os = \"macos\"))".dependencies]
keyring = "0.10.1"
[target."cfg(target_os = \"windows\")"]
[target."cfg(target_os = \"windows\")".dependencies]
path-slash = "0.1.4"

View File

@@ -1,12 +1,12 @@
# TermSCP
[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.4.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.4.2-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![codecov](https://codecov.io/gh/veeso/termscp/branch/main/graph/badge.svg?token=au67l7nQah)](https://codecov.io/gh/veeso/termscp)
[![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions)
~ Basically, WinSCP on a terminal ~
Developed by Christian Visintin
Current version: 0.4.0 (27/03/2021)
Current version: 0.4.2 (13/04/2021)
---
@@ -35,10 +35,11 @@ Current version: 0.4.0 (27/03/2021)
- [Documentation 📚](#documentation-)
- [Known issues 🧻](#known-issues-)
- [Upcoming Features 🧪](#upcoming-features-)
- [Contributions 🤝🏻](#contributions-)
- [Contributing and issues 🤝🏻](#contributing-and-issues-)
- [Changelog ⏳](#changelog-)
- [Powered by 🚀](#powered-by-)
- [Gallery 🎬](#gallery-)
- [Buy me a coffee ☕](#buy-me-a-coffee-)
- [License 📃](#license-)
---
@@ -99,8 +100,8 @@ Requirements:
### Deb package 📦
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.4.0_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.4.0_amd64.deb`
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.4.2_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.4.2_amd64.deb`
then install through dpkg:
@@ -112,8 +113,8 @@ gdebi termscp_*.deb
### RPM package 📦
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.4.0-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.4.0-1.x86_64.rpm`
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.4.2-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.4.2-1.x86_64.rpm`
then install through rpm:
@@ -139,7 +140,7 @@ Start PowerShell as administrator and run
choco install termscp
```
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.4.0.nupkg)
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.4.2.nupkg)
and then with PowerShell started with administrator previleges, run:
@@ -154,8 +155,7 @@ You can install TermSCP on MacOS using [brew](https://brew.sh/)
From your terminal run
```sh
brew tap veeso/termscp
brew install termscp
brew install veeso/termscp/termscp
```
---
@@ -360,6 +360,7 @@ If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
| `<Q>` | Quit TermSCP | Quit |
| `<R>` | Rename file | Rename |
| `<S>` | Save file as... | Save |
| `<U>` | Go to parent directory | Upper |
| `<X>` | Execute a command | eXecute |
| `<DEL>` | Delete file | |
@@ -381,8 +382,11 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
## Upcoming Features 🧪
- **Themes provider**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible
- **Themes provider 🎨**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible
to define the color schema for the entire application. I haven't planned this release yet
- **Local and remote file explorer format 🃏**: From 0.5.0 you will be able to customize the file format for both local and remote hosts.
- **Synchronized browsing of local and remote directories ⌚**: See [Issue 8](https://github.com/veeso/termscp/issues/8)
- **Group file select 🤩**: Possibility to select a group of files in explorers to operate on
No other new feature is planned at the moment. I actually think that termscp is getting mature and now I should focus upcoming updates more on bug fixing and
code/performance improvements than on new features.
@@ -394,11 +398,14 @@ Anyway there are some ideas which I'd like to implement. If you want to start wo
---
## 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 ⏳
@@ -443,6 +450,14 @@ TermSCP is powered by these aweseome projects:
---
## Buy me a coffee ☕
If you like termscp and you'd love to see the project to grow, please consider a little donation 🥳
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
---
## License 📃
termscp is licensed under the MIT license since version 0.4.0.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -5,5 +5,3 @@ ignore:
- src/ui/activities/
- src/ui/context.rs
- src/ui/input.rs
fixes:
- "/::"

View File

@@ -1,14 +1,14 @@
pkgbase = termscp
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.4.0
pkgver = 0.4.2
pkgrel = 1
url = https://github.com/veeso/termscp
arch = x86_64
license = GPL-3.0
license = MIT
provides = termscp
options = strip
source = https://github.com/veeso/termscp/releases/download/v0.4.0/termscp-0.4.0-x86_64.tar.gz
sha256sums = 7a8c70add8306a2cb3f2ee1d075a00fef143fc9aad4199797c7462bab1649296
source = https://github.com/veeso/termscp/releases/download/v0.4.2/termscp-0.4.2-x86_64.tar.gz
sha256sums = c72f78a4707402f7f970a883899f4f1583fd9eca6166cb7f7616be97cabf768a
pkgname = termscp

View File

@@ -1,6 +1,6 @@
# Maintainer: Christian Visintin
pkgname=termscp
pkgver=0.4.0
pkgver=0.4.2
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"
@@ -9,7 +9,7 @@ arch=("x86_64")
provides=("termscp")
options=("strip")
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
sha256sums=("7a8c70add8306a2cb3f2ee1d075a00fef143fc9aad4199797c7462bab1649296")
sha256sums=("c72f78a4707402f7f970a883899f4f1583fd9eca6166cb7f7616be97cabf768a")
package() {
install -Dm755 termscp -t "$pkgdir/usr/bin/"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

35
docs/deploy.md Normal file
View File

@@ -0,0 +1,35 @@
# Deploy checklist
Document audience: project maintainers
- [Deploy checklist](#deploy-checklist)
- [Description](#description)
- [Checklist](#checklist)
## Description
This document describes the checklist that must be fulfilled before releasing a new version of termscp.
## Checklist
- [ ] The latest build didn't report any error in the CI
- [ ] All commands when using SFTP work
- [ ] All commands when using SCP work
- [ ] All commands when using FTP work
- [ ] It is possible to load bookmarks
- [ ] Recent connections get saved
- [ ] Update versions and release date in readme, changelog and cargo.toml
- [ ] Build on MacOS
- [ ] Update sha256 and version on homebrew repository
- [ ] Build on Windows
- [ ] Update sha256 and version in chocolatey repository
- [ ] Create chocolatey package
- [ ] Build Linux version using docker from `dist/build/build.sh`
- [ ] Update sha256 and version in AUR files
- [ ] Create release and attach the following artifacts
- [ ] Deb package
- [ ] RPM package
- [ ] MacOs tar.gz
- [ ] Windows nupkg
- [ ] Windows zip
- [ ] AUR tar.gz

206
docs/developer.md Normal file
View File

@@ -0,0 +1,206 @@
# Developer Manual
Document audience: developers
- [Developer Manual](#developer-manual)
- [How TermSCP works](#how-termscp-works)
- [Activities](#activities)
- [The Context](#the-context)
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
- [Implementing File Transfers](#implementing-file-transfers)
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 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
---
## Tests fails due to receivers
Yes. This happens quite often and is related to the fact that I'm using public SSH/SFTP/FTP server to test file receivers and sometimes this server go down for even a day or more. If your tests don't pass due to this, don't worry, submit the pull request and I'll take care of testing them by myself.
---
## 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,
}
};
```

View File

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

View File

@@ -1 +0,0 @@
<mxfile host="Electron" modified="2021-03-02T08:14:29.978Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.6.2 Chrome/83.0.4103.122 Electron/9.2.0 Safari/537.36" etag="1sEcOODuVA3fk4xHo3nz" version="13.6.2" type="device"><diagram id="38W8qqbr2ERVeYZfRmVD" name="Page-1">7Vxbc5s4FP41ftyOQVwfEztpdnbTdCad7fZpRwHZqAHECPnWX7/CCHMRiXESLEKTyQM6SEJ83zk6F5RMwCzafqYwCW6Jj8KJPvW3EzCf6Lpm6Pok+536u1ziaFouWFLsi06l4B7/QkI4FdIV9lFa68gICRlO6kKPxDHyWE0GKSWbercFCetPTeASSYJ7D4ay9Dv2WSDeQrdL+Q3Cy6B4sma5+Z0IFp3Fm6QB9MmmIgJXEzCjhLD8KtrOUJiBV+CSj7t+4u5hYRTFrMsAJw1dfLf6Mv/vzvxye3fz7Xty/Ycp1sZ2xQsjn7+/aBLKArIkMQyvSuklJavYR9msU94q+/xNSMKFGhf+RIztBJlwxQgXBSwKxV20xezfbPgnU7R+VO7Mt2LmfWMnGgsSMzGhZvF2vu5ssU/CIUQpWVEPPYNBoVaQLhF7pp9+II1rOyIRYnTHx1EUQobX9XVAoXbLQ7+SGX4hyDmBKOuDqM5EAZVE2R9EdSbKUEmUmHcNw5V40v0uZSiS+KuzswkwQ/cJ3AOw4U6vzkT6iJgXiMYCh+GMhITuJwKeh8zFIuvEKHlElTvAAi7wnyRljShD2+dpkWEsBljCAQkPDKaivSn9mVY4qaDiy4pxb468LiH/F9rdM0IzX9wb+ouFx3/Ojn4dfMNWDT6QwL8k5DGC9DEdHfbW0MA3JPBnJF7g5diQB4NTe01X6pcrXrn00Wf3y05Hv6w9we55HLPuqA2h7O4xFGeA7qqjsvaP6s1y3L41IJIrSagCksFULcn6i0jWP0g+hWRHcncXHn8qZnxqK+Trvnyg/GqZXd3CmId/tMfo20cPzsO5PaFp1j2ho9wRqk1QT3CEB7vTakb3yVRndlrXzFYzVdqdptaD/i4cW6/keD/0glK4q3RICI5ZWpn5ayYoN5RDPVlsKJrWKPse6+9OG/qVr6DUtsOrvEIB3XengBWdU+zbuyvgAPWvH30CmlJ9OqGqOsRgsbNCqY0Wi2XWqyM89GIy+WGIkxQdDwhhmuRfBRd4m6lCI0JsYN5SO3HdZyPGN4gQgVs3qJZSidMSIDq9BYhyhTDgD9Gnsg3uuanjHeJlzK89/vI8mAeXGTTYg+GFuBFh38+tE6X4F3zYT5Xpv9h3+Lzm5cScZ3Nxg0wFO/0E54Z7NDg3W6DXe4Nerg9e4xB9ozBOF73mRkq+TJgNV2JYypMjU96CYLyGPZbG1UBvNJBXDrwlAf9nnKyYXCe4gTHfQEZnCw1CNOWmoBsqQ67i+jUh/CGlPH/EVTB1POJSelYBqP0ockoRdghkaUrPKwDFZ7Ve9llkKDlQZ5KBUpKLZbaWzMddGVcfhOgDOg057WZgwymb6qCjgTlK7UtObn+HGoPdDC+B4iKDLsf7JJ6jDAR5o3vvZQbLPb7TueesMxSn1yvo38UziiCTz9+9d/AN26yrfktm1ab6/YHvtqk+hZvRQW81d52W+pp9TuiBvPtH+48fAVyPT/PNaR1+oFrzgeKDXYPINoHRMU4CtspACcil6H8wkveod56E2HbDRpQfVAVyZHQz0s8vdnOD0mTwrXNuUIbSetgpNc8+Nyi74wZlKD0lbCitT7/wSMBQqmHdOVZbn5bzlBmJEhJnLzxyT2Qq/zMtICcq+UGACMbjS9LtRpLe9jH6rNGy0fIHiqwtQx9Tocp1Giy43VjorVBlyCnjV0oS+UDAmFho2oLVEpi9EQu8Wf4LhPy4YPmPJMDV/w==</diagram></mxfile>

View File

@@ -1 +0,0 @@
<mxfile host="Electron" modified="2021-03-04T08:07:06.708Z" 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="hf5xr88nEHbSVkpX1Y-k" version="13.3.1" type="device"><diagram id="tfe7Y5e8DOWIlyiIXn_Y" name="Page-1">7Vxbc6M2FP41nmkf2gGEMDwmTtLOdNN2Jp1205cMBhmrwYgKsbb3168wwlxEbOIEZOLNS5CQZOkcfd+5SPYEzFabX6gbL++Jj8KJofmbCbiZGIYOocb/ZTXbvMZ2YF4RUOyLRmXFA/6KRKXoF6TYR0mtISMkZDiuV3okipDHanUupWRdb7YgYf1TYzdAUsWD54Zy7T/YZ0uxCmNa1v+KcLAsPlm3nPzNyi0ai5UkS9cn60oVuJ2AGSWE5U+rzQyFmfAKueT97l54u58YRRHr0iFBd49//Wv8hmafV08PC+p8ur/5CeSjfHHDVCz4ymP4C2ZbMWm2LSTBR+NC54Xr9RIz9BC7XvZmzfXO65ZsFfKSzh89ssKeeF7gMJyRkNDdGMDz0dye8/qEUfKMKm+ABRzgZz1IxMQ20C1eFhNElKHNiyvX9/LkGxGRFWKUL0ATHQoNiC1oiuK61KdeNFlWdGmLOldsoWA/cCll/iAE/QqhF3u7Klyf7zpRJJQtSUAiN7wta68pSSMfZcNqvFS2+URILIT9H2JsK2TnpozU1YI2mH3Ouv8MRemx8uZmI0beFbZFIeLrrXTKio/Vd2W3Xano167EbJGHVchlQlLqoQPCE8BjLg0QO9DObt8SFIUu3+D1eby7hqcSrB74hNnpmHKTOGe3Bd5km6AOsoa0G6BDu7+DoHsHkIGpeQxl9qAgA0pBVoFYCbjRgMzuCDLdUIkyW0JZwI0p7xTFKf9vhXzq13PKn4LsCX3ZyeFO2hd0SVbzNHmlZTsMug6W7j1AZ1o10OnTFtsGLBl2utYb7qyxGLce8ZPjoguAoEoAFdOsIIhEP/woKZC7rnH2GFPioaQDUOau9xzslPpHykIcoRMA5CG4WPQNIN2u+4Z7e3TMObR6w48p6eSRB0BNlfAls4aLEOIg4s8eXzriorrOBIN5JHMlXqyw7+doQwn+6s53Q2VAiAmO2G4d8HoCb7KxOMASoZuefHJLrzOXY0qCN1vkbvQld2M0vHWG/oIOO/KdodRhKKbZGu423YWC7HgonwTnQYmD+BRNSjQ15ZQoh1P3LSp5mRJDtGBnT4iwLnZgyWKHgxIiVEqIJwVQNTos2VEBIXaOoN7qAO66XlHqbisNxAYuR/4zq6hgvJESc5q5wyPtTaA19lc+g3K37ZfyBtjL8V0a+y5DF+2h2o5qOi4+rNWKapxfw9e4q6Pg5kaYvSfiqg60QclZv3QdWIZyHThKDeS4Iwaja4bEUJrIB1OVOtYnpzhB49NxnqFUpeNimhUu5V5ATKJswVJYSNH/KaZZRqb5JndOPlaCWZ8aNdaFuvIEs3EZCbKm39dm7wZNkAG1B2ojN3dd40GgNEFmtBypIfbEo6o4ueioy2rJxgwbdYHL8/gbGTFbedQFzO8UeDIFgq7eoK7W45e9wXv3GfGaCK2zpWdceB5MqOQ4YNp2xWBQJjSVOiIjj8kKX/0oCk2ljoipNrdyKTpWy7SWxLTJkqSh/5SuuCbl25K9hNGDuJMQNA732q6ZDBtGA9nV/528woMcRxQNGoKHxTUPZVG0I4n9Dkc4Wcp893HuBkNo17Sga0De/4PeDjblaOojJpEsu37LytR0tdvfVJpPH3kEZXbOpztvtOsnXSqYWnWu5bbr8K0C6DRpod6hn2sFhRSr1wp2/saZJLgWC89znL4peao1ElzKL76acl5/Vh7DnGocD30Z7bjrN0BeX/m30Uz5UuTfOEt19CDyDnu7B5E3vW/lIofGdyt4uhXsepQCX9gVA2Uw5PiKr9VH9EzMzDCBrwnqJh4qNzPFIcLHjntNp0l5ih1/OJrT4x6pCxa/YHCeDrx5xB8/EwceyrF7zqzylRw3G+0cyHaQo5om2U6VH1pD2ad/3Tc3xsG2jcDXafnGzDt9dYMXy18MyQFV/u4KuP0G</diagram></mxfile>

View File

@@ -29,6 +29,7 @@ pub mod serializer;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserHosts
@@ -66,10 +67,13 @@ pub struct SerializerError {
/// ## SerializerErrorKind
///
/// Describes the kind of error for the serializer/deserializer
#[derive(std::fmt::Debug, PartialEq)]
#[derive(Error, Debug)]
pub enum SerializerErrorKind {
#[error("IO error")]
IoError,
#[error("Serialization error")]
SerializationError,
#[error("Syntax error")]
SyntaxError,
}
@@ -102,14 +106,9 @@ impl SerializerError {
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),
Some(msg) => write!(f, "{} ({})", self.kind, msg),
None => write!(f, "{}", self.kind),
}
}
}
@@ -172,12 +171,10 @@ mod tests {
#[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),

View File

@@ -38,6 +38,7 @@ use crate::filetransfer::FileTransferProtocol;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserConfig
@@ -117,10 +118,13 @@ pub struct SerializerError {
/// ## SerializerErrorKind
///
/// Describes the kind of error for the serializer/deserializer
#[derive(std::fmt::Debug, PartialEq)]
#[derive(Error, Debug)]
pub enum SerializerErrorKind {
#[error("IO error")]
IoError,
#[error("Serialization error")]
SerializationError,
#[error("Syntax error")]
SyntaxError,
}
@@ -144,14 +148,9 @@ impl SerializerError {
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),
Some(msg) => write!(f, "{} ({})", self.kind, msg),
None => write!(f, "{}", self.kind),
}
}
}
@@ -206,7 +205,17 @@ mod tests {
// 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"));
// Text editor
#[cfg(target_os = "windows")]
assert_eq!(
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
PathBuf::from("vim.EXE")
);
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
assert_eq!(
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
PathBuf::from("vim")
);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.remote.ssh_keys.len(), 0);
}
@@ -214,12 +223,10 @@ mod tests {
#[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),

View File

@@ -101,7 +101,6 @@ mod tests {
// 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
@@ -141,7 +140,6 @@ mod tests {
// 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

View File

@@ -28,6 +28,8 @@
// Dependencies
extern crate chrono;
extern crate ftp4;
#[cfg(os_target = "windows")]
extern crate path_slash;
extern crate regex;
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
@@ -36,7 +38,7 @@ use crate::utils::parser::{parse_datetime, parse_lstime};
// Includes
use ftp4::native_tls::TlsConnector;
use ftp4::FtpStream;
use ftp4::{types::FileType, FtpStream};
use regex::Regex;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
@@ -61,10 +63,24 @@ impl FtpFileTransfer {
FtpFileTransfer { stream: None, ftps }
}
/// ### resolve
///
/// Fix provided path; on Windows fixes the backslashes, converting them to slashes
/// While on POSIX does nothing
#[cfg(target_os = "windows")]
fn resolve(p: &Path) -> PathBuf {
PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str())
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn resolve(p: &Path) -> PathBuf {
p.to_path_buf()
}
/// ### parse_list_line
///
/// Parse a line of LIST command output and instantiates an FsEntry from it
fn parse_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
fn parse_list_line(&mut self, path: &Path, line: &str) -> Result<FsEntry, ()> {
// Try to parse using UNIX syntax
match self.parse_unix_list_line(path, line) {
Ok(entry) => Ok(entry),
@@ -83,7 +99,7 @@ impl FtpFileTransfer {
/// UNIX syntax has the following syntax:
/// {FILE_TYPE}{UNIX_PEX} {HARD_LINKS} {USER} {GROUP} {SIZE} {DATE} {FILENAME}
/// -rw-r--r-- 1 cvisintin staff 4968 27 Dic 10:46 CHANGELOG.md
fn parse_unix_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
fn parse_unix_list_line(&mut self, path: &Path, line: &str) -> Result<FsEntry, ()> {
// Prepare list regex
// NOTE: about this damn regex <https://stackoverflow.com/questions/32480890/is-there-a-regex-to-parse-the-values-from-an-ftp-directory-listing>
lazy_static! {
@@ -100,7 +116,8 @@ impl FtpFileTransfer {
}
// Collect metadata
// Get if is directory and if is symlink
let (is_dir, _is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() {
let (mut is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str()
{
"-" => (false, false),
"l" => (false, true),
"d" => (true, false),
@@ -158,13 +175,62 @@ impl FtpFileTransfer {
.as_str()
.parse::<usize>()
.unwrap_or(0);
let file_name: String = String::from(metadata.get(8).unwrap().as_str());
// Split filename if required
let (file_name, symlink_path): (String, Option<PathBuf>) = match is_symlink {
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
false => (String::from(metadata.get(8).unwrap().as_str()), None),
};
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
return Err(());
}
// Get symlink
let symlink: Option<Box<FsEntry>> = match symlink_path {
None => None,
Some(p) => Some(Box::new(match p.to_string_lossy().ends_with('/') {
true => {
// NOTE: is_dir becomes true
is_dir = true;
FsEntry::Directory(FsDirectory {
name: p
.file_name()
.unwrap_or(&std::ffi::OsStr::new(""))
.to_string_lossy()
.to_string(),
abs_path: p.clone(),
last_change_time: mtime,
last_access_time: mtime,
creation_time: mtime,
readonly: false,
symlink: None,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
})
}
false => FsEntry::File(FsFile {
name: p
.file_name()
.unwrap_or(&std::ffi::OsStr::new(""))
.to_string_lossy()
.to_string(),
abs_path: p.clone(),
last_change_time: mtime,
last_access_time: mtime,
creation_time: mtime,
readonly: false,
symlink: None,
size: filesize,
ftype: p.extension().map(|s| String::from(s.to_string_lossy())),
user: uid,
group: gid,
unix_pex: Some(unix_pex),
}),
})),
};
let mut abs_path: PathBuf = PathBuf::from(path);
abs_path.push(file_name.as_str());
let abs_path: PathBuf = Self::resolve(abs_path.as_path());
// get extension
let extension: Option<String> = abs_path
.as_path()
@@ -180,7 +246,7 @@ impl FtpFileTransfer {
last_access_time: mtime,
creation_time: mtime,
readonly: false,
symlink: None,
symlink,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
@@ -194,7 +260,7 @@ impl FtpFileTransfer {
size: filesize,
ftype: extension,
readonly: false,
symlink: None,
symlink,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
@@ -253,6 +319,7 @@ impl FtpFileTransfer {
// Get absolute path
let mut abs_path: PathBuf = PathBuf::from(path);
abs_path.push(file_name.as_str());
let abs_path: PathBuf = Self::resolve(abs_path.as_path());
// Get extension
let extension: Option<String> = abs_path
.as_path()
@@ -291,6 +358,16 @@ impl FtpFileTransfer {
None => Err(()), // Invalid syntax
}
}
/// ### get_name_and_link
///
/// Returns from a `ls -l` command output file name token, the name of the file and the symbolic link (if there is any)
fn get_name_and_link(&self, token: &str) -> (String, Option<PathBuf>) {
let tokens: Vec<&str> = token.split(" -> ").collect();
let filename: String = String::from(*tokens.get(0).unwrap());
let symlink: Option<PathBuf> = tokens.get(1).map(PathBuf::from);
(filename, symlink)
}
}
impl FileTransfer for FtpFileTransfer {
@@ -311,7 +388,7 @@ impl FileTransfer for FtpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -326,7 +403,7 @@ impl FileTransfer for FtpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::SslError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -335,7 +412,7 @@ impl FileTransfer for FtpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::SslError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -352,7 +429,14 @@ impl FileTransfer for FtpFileTransfer {
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("{}", err),
err.to_string(),
));
}
// Initialize file type
if let Err(err) = stream.transfer_type(FileType::Binary) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
err.to_string(),
));
}
// Set stream
@@ -371,7 +455,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -397,7 +481,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(path) => Ok(PathBuf::from(path.as_str())),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -411,12 +495,13 @@ impl FileTransfer for FtpFileTransfer {
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
let dir: PathBuf = Self::resolve(dir);
match &mut self.stream {
Some(stream) => match stream.cwd(&dir.to_string_lossy()) {
Ok(_) => Ok(PathBuf::from(dir)),
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
Ok(_) => Ok(dir),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -440,14 +525,15 @@ impl FileTransfer for FtpFileTransfer {
/// List directory entries
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
let dir: PathBuf = Self::resolve(path);
match &mut self.stream {
Some(stream) => match stream.list(Some(&path.to_string_lossy())) {
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
Ok(entries) => {
// Prepare result
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
// Iterate over entries
for entry in entries.iter() {
if let Ok(file) = self.parse_list_line(path, entry) {
if let Ok(file) = self.parse_list_line(dir.as_path(), entry) {
result.push(file);
}
}
@@ -455,7 +541,7 @@ impl FileTransfer for FtpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -468,12 +554,13 @@ impl FileTransfer for FtpFileTransfer {
///
/// Make directory
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
let dir: PathBuf = Self::resolve(dir);
match &mut self.stream {
Some(stream) => match stream.mkdir(&dir.to_string_lossy()) {
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -499,7 +586,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -512,7 +599,7 @@ impl FileTransfer for FtpFileTransfer {
if let Err(err) = self.remove(&file) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
format!("{}", err),
err.to_string(),
));
}
}
@@ -521,13 +608,13 @@ impl FileTransfer for FtpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
format!("{}", err),
err.to_string(),
)),
}
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -538,6 +625,7 @@ impl FileTransfer for FtpFileTransfer {
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
let dst: PathBuf = Self::resolve(dst);
match &mut self.stream {
Some(stream) => {
// Get name
@@ -559,7 +647,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -603,12 +691,13 @@ impl FileTransfer for FtpFileTransfer {
_local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError> {
let file_name: PathBuf = Self::resolve(file_name);
match &mut self.stream {
Some(stream) => match stream.put_with_stream(&file_name.to_string_lossy()) {
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -627,7 +716,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -649,7 +738,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -671,7 +760,7 @@ impl FileTransfer for FtpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
},
None => Err(FileTransferError::new(
@@ -701,7 +790,7 @@ mod tests {
#[test]
fn test_filetransfer_ftp_parse_list_line_unix() {
let ftp: FtpFileTransfer = FtpFileTransfer::new(false);
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Simple file
let fs_entry: FsEntry = ftp
.parse_list_line(
@@ -824,7 +913,7 @@ mod tests {
#[test]
fn test_filetransfer_ftp_parse_list_line_dos() {
let ftp: FtpFileTransfer = FtpFileTransfer::new(false);
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Simple file
let fs_entry: FsEntry = ftp
.parse_list_line(
@@ -1021,7 +1110,6 @@ mod tests {
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// List dir
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
// There should be at least 1 file
assert!(files.len() > 0);
@@ -1040,7 +1128,6 @@ mod tests {
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// List dir
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
// There should be at least 1 file
assert!(files.len() > 0);

View File

@@ -32,6 +32,7 @@ use crate::fs::{FsEntry, FsFile};
// ext
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use wildmatch::WildMatch;
// exports
pub mod ftp_transfer;
@@ -62,19 +63,31 @@ pub struct FileTransferError {
///
/// FileTransferErrorType defines the possible errors available for a file transfer
#[allow(dead_code)]
#[derive(std::fmt::Debug)]
#[derive(Error, Debug)]
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("Failed to create file")]
FileCreateDenied,
#[error("IO error: {0}")]
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,
}
@@ -98,25 +111,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),
}
}
}
@@ -278,7 +275,7 @@ pub trait FileTransfer {
match entry {
FsEntry::Directory(dir) => {
// If directory name, matches wildcard, push it to drained
if filter.is_match(dir.name.as_str()) {
if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone()));
}
match self.iter_search(dir.abs_path.as_path(), filter) {
@@ -287,7 +284,7 @@ pub trait FileTransfer {
}
}
FsEntry::File(file) => {
if filter.is_match(file.name.as_str()) {
if filter.matches(file.name.as_str()) {
drained.push(FsEntry::File(file.clone()));
}
}

View File

@@ -26,6 +26,8 @@
* SOFTWARE.
*/
// Dependencies
#[cfg(os_target = "windows")]
extern crate path_slash;
extern crate regex;
extern crate ssh2;
@@ -65,6 +67,20 @@ impl ScpFileTransfer {
}
}
/// ### resolve
///
/// Fix provided path; on Windows fixes the backslashes, converting them to slashes
/// While on POSIX does nothing
#[cfg(target_os = "windows")]
fn resolve(p: &Path) -> PathBuf {
PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str())
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn resolve(p: &Path) -> PathBuf {
p.to_path_buf()
}
/// ### parse_ls_output
///
/// Parse a line of `ls -l` output and tokenize the output into a `FsEntry`
@@ -149,9 +165,9 @@ impl ScpFileTransfer {
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
false => (String::from(metadata.get(8).unwrap().as_str()), None),
};
// Check if symlink points to a directory
if let Some(symlink_path) = symlink_path.as_ref() {
is_dir = symlink_path.is_dir();
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
return Err(());
}
// Get symlink; PATH mustn't be equal to filename
let symlink: Option<Box<FsEntry>> = match symlink_path {
@@ -163,17 +179,21 @@ impl ScpFileTransfer {
true => None,
false => match self.stat(p.as_path()) {
// If path match filename
Ok(e) => Some(Box::new(e)),
Ok(e) => {
// If e is a directory, set is_dir to true
if e.is_dir() {
is_dir = true;
}
Some(Box::new(e))
}
Err(_) => None, // Ignore errors
},
},
};
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
return Err(());
}
// Re-check if is directory
let mut abs_path: PathBuf = PathBuf::from(path);
abs_path.push(file_name.as_str());
let abs_path: PathBuf = Self::resolve(abs_path.as_path());
// Get extension
let extension: Option<String> = abs_path
.as_path()
@@ -298,7 +318,7 @@ impl FileTransfer for ScpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::BadAddress,
format!("{}", err),
err.to_string(),
))
}
};
@@ -329,7 +349,7 @@ impl FileTransfer for ScpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -339,7 +359,7 @@ impl FileTransfer for ScpFileTransfer {
if let Err(err) = session.handshake() {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
));
}
let username: String = match username {
@@ -361,7 +381,7 @@ impl FileTransfer for ScpFileTransfer {
) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("{}", err),
err.to_string(),
));
}
}
@@ -373,7 +393,7 @@ impl FileTransfer for ScpFileTransfer {
) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("{}", err),
err.to_string(),
));
}
}
@@ -405,7 +425,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -448,7 +468,7 @@ impl FileTransfer for ScpFileTransfer {
false => {
let mut p: PathBuf = PathBuf::from(".");
p.push(dir);
p
Self::resolve(p.as_path())
}
};
// Change directory
@@ -475,7 +495,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -491,6 +511,7 @@ impl FileTransfer for ScpFileTransfer {
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
match self.is_connected() {
true => {
let dst: PathBuf = Self::resolve(dst);
// Run `cp -rf`
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
@@ -516,7 +537,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -534,10 +555,11 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
// Send ls -l to path
let path: PathBuf = Self::resolve(path);
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
p.as_path(),
format!("unset LANG; ls -la \"{}\"", path.display()).as_str(),
format!("unset LANG; ls -la \"{}/\"", path.display()).as_str(),
) {
Ok(output) => {
// Split output by (\r)\n
@@ -546,7 +568,7 @@ impl FileTransfer for ScpFileTransfer {
for line in lines.iter() {
// First line must always be ignored
// Parse row, if ok push to entries
if let Ok(entry) = self.parse_ls_output(path, line) {
if let Ok(entry) = self.parse_ls_output(path.as_path(), line) {
entries.push(entry);
}
}
@@ -554,7 +576,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -571,6 +593,7 @@ impl FileTransfer for ScpFileTransfer {
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
match self.is_connected() {
true => {
let dir: PathBuf = Self::resolve(dir);
let p: PathBuf = self.wrkdir.clone();
// Mkdir dir && echo 0
match self.perform_shell_cmd_with_path(
@@ -590,7 +613,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -627,7 +650,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -644,6 +667,7 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
// Get path
let dst: PathBuf = Self::resolve(dst);
let path: PathBuf = file.get_abs_path();
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
@@ -668,7 +692,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -682,27 +706,23 @@ impl FileTransfer for ScpFileTransfer {
///
/// Stat file and return FsEntry
fn stat(&mut self, path: &Path) -> Result<FsEntry, FileTransferError> {
if path.is_dir() {
return Err(FileTransferError::new_ex(
FileTransferErrorType::UnsupportedFeature,
String::from("stat is not supported for directories"),
));
}
let path: PathBuf = match path.is_absolute() {
true => PathBuf::from(path),
false => {
let mut p: PathBuf = self.wrkdir.clone();
p.push(path);
p
Self::resolve(p.as_path())
}
};
match self.is_connected() {
true => {
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
p.as_path(),
format!("ls -l \"{}\"", path.display()).as_str(),
) {
// make command; Directories require `-d` option
let cmd: String = match path.to_string_lossy().ends_with('/') {
true => format!("ls -ld \"{}\"", path.display()),
false => format!("ls -l \"{}\"", path.display()),
};
match self.perform_shell_cmd_with_path(p.as_path(), cmd.as_str()) {
Ok(line) => {
// Parse ls line
let parent: PathBuf = match path.as_path().parent() {
@@ -723,7 +743,7 @@ impl FileTransfer for ScpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -744,7 +764,7 @@ impl FileTransfer for ScpFileTransfer {
Ok(output) => Ok(output),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -767,6 +787,7 @@ impl FileTransfer for ScpFileTransfer {
) -> Result<Box<dyn Write>, FileTransferError> {
match self.session.as_ref() {
Some(session) => {
let file_name: PathBuf = Self::resolve(file_name);
// Set blocking to true
session.set_blocking(true);
// Calculate file mode
@@ -798,11 +819,11 @@ impl FileTransfer for ScpFileTransfer {
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
};
// Send file
match session.scp_send(file_name, mode, file_size, Some(times)) {
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -825,7 +846,7 @@ impl FileTransfer for ScpFileTransfer {
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -1053,7 +1074,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
//#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn test_filetransfer_scp_find() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client

View File

@@ -76,12 +76,12 @@ impl SftpFileTransfer {
Ok(_) => Ok(p),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}", err),
err.to_string(),
)),
},
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -90,7 +90,7 @@ impl SftpFileTransfer {
Ok(_) => Ok(p),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}", err),
err.to_string(),
)),
},
Err(_) => Err(FileTransferError::new(
@@ -260,7 +260,7 @@ impl FileTransfer for SftpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::BadAddress,
format!("{}", err),
err.to_string(),
))
}
};
@@ -291,7 +291,7 @@ impl FileTransfer for SftpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -301,7 +301,7 @@ impl FileTransfer for SftpFileTransfer {
if let Err(err) = session.handshake() {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
));
}
let username: String = match username {
@@ -323,7 +323,7 @@ impl FileTransfer for SftpFileTransfer {
) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("{}", err),
err.to_string(),
));
}
}
@@ -335,7 +335,7 @@ impl FileTransfer for SftpFileTransfer {
) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("{}", err),
err.to_string(),
));
}
}
@@ -348,7 +348,7 @@ impl FileTransfer for SftpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -358,7 +358,7 @@ impl FileTransfer for SftpFileTransfer {
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
))
}
};
@@ -386,7 +386,7 @@ impl FileTransfer for SftpFileTransfer {
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -459,7 +459,7 @@ impl FileTransfer for SftpFileTransfer {
match sftp.readdir(dir.as_path()) {
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
format!("{}", err),
err.to_string(),
)),
Ok(files) => {
// Allocate vector
@@ -490,7 +490,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -517,7 +517,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -538,7 +538,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -562,7 +562,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -585,7 +585,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -604,7 +604,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(output) => Ok(output),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
err.to_string(),
)),
},
false => Err(FileTransferError::new(
@@ -643,7 +643,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(file) => Ok(Box::new(BufWriter::with_capacity(65536, file))),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("{}", err),
err.to_string(),
)),
}
}
@@ -669,7 +669,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}", err),
err.to_string(),
)),
}
}

View File

@@ -31,6 +31,7 @@ extern crate wildmatch;
use std::fs::{self, File, Metadata, OpenOptions};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use thiserror::Error;
use wildmatch::WildMatch;
// Metadata ext
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
@@ -44,15 +45,23 @@ use crate::fs::{FsDirectory, FsEntry, FsFile};
/// ## HostErrorType
///
/// HostErrorType provides an overview of the specific host error
#[derive(PartialEq, std::fmt::Debug)]
#[derive(Error, Debug)]
pub enum HostErrorType {
#[error("No such file or directory")]
NoSuchFileOrDirectory,
#[error("File is readonly")]
ReadonlyFile,
#[error("Could not access directory")]
DirNotAccessible,
#[error("Could not access file")]
FileNotAccessible,
#[error("File already exists")]
FileAlreadyExists,
#[error("Could not create file")]
CouldNotCreateFile,
#[error("Command execution failed")]
ExecutionFailed,
#[error("Could not delete file")]
DeleteFailed,
}
@@ -62,36 +71,42 @@ pub enum HostErrorType {
pub struct HostError {
pub error: HostErrorType,
pub ioerr: Option<std::io::Error>,
ioerr: Option<std::io::Error>,
path: Option<PathBuf>,
}
impl HostError {
/// ### new
///
/// Instantiates a new HostError
pub(crate) fn new(error: HostErrorType, errno: Option<std::io::Error>) -> HostError {
pub(crate) fn new(error: HostErrorType, errno: Option<std::io::Error>, p: &Path) -> Self {
HostError {
error,
ioerr: errno,
path: Some(p.to_path_buf()),
}
}
}
impl From<HostErrorType> for HostError {
fn from(error: HostErrorType) -> Self {
HostError {
error,
ioerr: None,
path: None,
}
}
}
impl std::fmt::Display for HostError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let code_str: &str = match self.error {
HostErrorType::NoSuchFileOrDirectory => "No such file or directory",
HostErrorType::ReadonlyFile => "File is readonly",
HostErrorType::DirNotAccessible => "Could not access directory",
HostErrorType::FileNotAccessible => "Could not access file",
HostErrorType::FileAlreadyExists => "File already exists",
HostErrorType::CouldNotCreateFile => "Could not create file",
HostErrorType::ExecutionFailed => "Could not run command",
HostErrorType::DeleteFailed => "Could not delete file",
let p_str: String = match self.path.as_ref() {
None => String::new(),
Some(p) => format!(" ({})", p.display().to_string()),
};
match &self.ioerr {
Some(err) => write!(f, "{}: {}", code_str, err),
None => write!(f, "{}", code_str),
Some(err) => write!(f, "{}: {}{}", self.error, err, p_str),
None => write!(f, "{}{}", self.error, p_str),
}
}
}
@@ -116,7 +131,11 @@ impl Localhost {
};
// Check if dir exists
if !host.file_exists(host.wrkdir.as_path()) {
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
host.wrkdir.as_path(),
));
}
// Retrieve files for provided path
host.files = match host.scan_dir(host.wrkdir.as_path()) {
@@ -148,11 +167,19 @@ impl Localhost {
let new_dir: PathBuf = self.to_abs_path(new_dir);
// Check whether directory exists
if !self.file_exists(new_dir.as_path()) {
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
new_dir.as_path(),
));
}
// Change directory
if std::env::set_current_dir(new_dir.as_path()).is_err() {
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
new_dir.as_path(),
));
}
let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location
// Update working directory
@@ -187,10 +214,16 @@ impl Localhost {
if dir_path.exists() {
match ignex {
true => return Ok(()),
false => return Err(HostError::new(HostErrorType::FileAlreadyExists, None)),
false => {
return Err(HostError::new(
HostErrorType::FileAlreadyExists,
None,
dir_path.as_path(),
))
}
}
}
match std::fs::create_dir(dir_path) {
match std::fs::create_dir(dir_path.as_path()) {
Ok(_) => {
// Update dir
if dir_name.is_relative() {
@@ -198,7 +231,11 @@ impl Localhost {
}
Ok(())
}
Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
dir_path.as_path(),
)),
}
}
@@ -210,7 +247,11 @@ impl Localhost {
FsEntry::Directory(dir) => {
// If file doesn't exist; return error
if !dir.abs_path.as_path().exists() {
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
dir.abs_path.as_path(),
));
}
// Remove
match std::fs::remove_dir_all(dir.abs_path.as_path()) {
@@ -219,13 +260,21 @@ impl Localhost {
self.files = self.scan_dir(self.wrkdir.as_path())?;
Ok(())
}
Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
dir.abs_path.as_path(),
)),
}
}
FsEntry::File(file) => {
// If file doesn't exist; return error
if !file.abs_path.as_path().exists() {
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
file.abs_path.as_path(),
));
}
// Remove
match std::fs::remove_file(file.abs_path.as_path()) {
@@ -234,7 +283,11 @@ impl Localhost {
self.files = self.scan_dir(self.wrkdir.as_path())?;
Ok(())
}
Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
file.abs_path.as_path(),
)),
}
}
}
@@ -251,7 +304,11 @@ impl Localhost {
self.files = self.scan_dir(self.wrkdir.as_path())?;
Ok(())
}
Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
abs_path.as_path(),
)),
}
}
@@ -276,7 +333,11 @@ impl Localhost {
};
// Copy entry path to dst path
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
return Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err)));
return Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
file.abs_path.as_path(),
));
}
}
FsEntry::Directory(dir) => {
@@ -328,7 +389,13 @@ impl Localhost {
let path: PathBuf = self.to_abs_path(path);
let attr: Metadata = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata,
Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
Err(err) => {
return Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
))
}
};
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
// Match dir / file
@@ -389,7 +456,13 @@ impl Localhost {
let path: PathBuf = self.to_abs_path(path);
let attr: Metadata = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata,
Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
Err(err) => {
return Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
))
}
};
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
// Match dir / file
@@ -455,7 +528,11 @@ impl Localhost {
Ok(s) => Ok(s.to_string()),
Err(_) => Ok(String::new()),
},
Err(err) => Err(HostError::new(HostErrorType::ExecutionFailed, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::ExecutionFailed,
Some(err),
self.wrkdir.as_path(),
)),
}
}
@@ -472,10 +549,18 @@ impl Localhost {
mpex.set_mode(self.mode_to_u32(pex));
match set_permissions(path.as_path(), mpex) {
Ok(_) => Ok(()),
Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
)),
}
}
Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
)),
}
}
@@ -485,7 +570,11 @@ impl Localhost {
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
let file: PathBuf = self.to_abs_path(file);
if !self.file_exists(file.as_path()) {
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
file.as_path(),
));
}
match OpenOptions::new()
.create(false)
@@ -494,7 +583,11 @@ impl Localhost {
.open(file.as_path())
{
Ok(f) => Ok(f),
Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
Err(err) => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
file.as_path(),
)),
}
}
@@ -511,8 +604,16 @@ impl Localhost {
{
Ok(f) => Ok(f),
Err(err) => match self.file_exists(file.as_path()) {
true => Err(HostError::new(HostErrorType::ReadonlyFile, Some(err))),
false => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
true => Err(HostError::new(
HostErrorType::ReadonlyFile,
Some(err),
file.as_path(),
)),
false => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
file.as_path(),
)),
},
}
}
@@ -526,20 +627,25 @@ impl Localhost {
/// ### scan_dir
///
/// Get content of the current directory as a list of fs entry (Windows)
/// Get content of the current directory as a list of fs entry
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(err) => return Err(HostError::new(HostErrorType::DirNotAccessible, Some(err))),
};
let mut fs_entries: Vec<FsEntry> = Vec::new();
for entry in entries.flatten() {
fs_entries.push(match self.stat(entry.path().as_path()) {
Ok(entry) => entry,
Err(err) => return Err(err),
});
match std::fs::read_dir(dir) {
Ok(e) => {
let mut fs_entries: Vec<FsEntry> = Vec::new();
for entry in e.flatten() {
// NOTE: 0.4.1, don't fail if stat for one file fails
if let Ok(entry) = self.stat(entry.path().as_path()) {
fs_entries.push(entry);
}
}
Ok(fs_entries)
}
Err(err) => Err(HostError::new(
HostErrorType::DirNotAccessible,
Some(err),
dir,
)),
}
Ok(fs_entries)
}
/// ### find
@@ -574,7 +680,7 @@ impl Localhost {
match entry {
FsEntry::Directory(dir) => {
// If directory matches; push directory to drained
if filter.is_match(dir.name.as_str()) {
if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone()));
}
match self.iter_search(dir.abs_path.as_path(), filter) {
@@ -583,7 +689,7 @@ impl Localhost {
}
}
FsEntry::File(file) => {
if filter.is_match(file.name.as_str()) {
if filter.matches(file.name.as_str()) {
drained.push(FsEntry::File(file.clone()));
}
}
@@ -641,9 +747,10 @@ mod tests {
#[test]
fn test_host_error_new() {
let error: HostError = HostError::new(HostErrorType::CouldNotCreateFile, None);
assert_eq!(error.error, HostErrorType::CouldNotCreateFile);
let error: HostError =
HostError::new(HostErrorType::CouldNotCreateFile, None, Path::new("/tmp"));
assert!(error.ioerr.is_none());
assert_eq!(error.path.as_ref().unwrap(), Path::new("/tmp"));
}
#[test]
@@ -791,7 +898,6 @@ mod tests {
// Get dir
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<FsEntry> = host.list_dir();
println!("Entries {:?}", files);
// Verify files
let file_0: &FsEntry = files.get(0).unwrap();
match file_0 {
@@ -1015,7 +1121,6 @@ mod tests {
// Verify dir_dest contains foo.txt
let mut test_file_path: PathBuf = dir_dest.clone();
test_file_path.push("foo.txt");
println!("{:?}", host.scan_dir(tmpdir.path()).ok().unwrap());
assert!(host.stat(test_file_path.as_path()).is_ok());
}
@@ -1068,40 +1173,38 @@ mod tests {
let err: HostError = HostError::new(
HostErrorType::CouldNotCreateFile,
Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
Path::new("/tmp"),
);
assert_eq!(
format!("{}", err),
String::from("Could not create file: address in use")
String::from("Could not create file: address in use (/tmp)"),
);
assert_eq!(
format!("{}", HostError::new(HostErrorType::DeleteFailed, None)),
format!("{}", HostError::from(HostErrorType::DeleteFailed)),
String::from("Could not delete file")
);
assert_eq!(
format!("{}", HostError::new(HostErrorType::ExecutionFailed, None)),
String::from("Could not run command")
format!("{}", HostError::from(HostErrorType::ExecutionFailed)),
String::from("Command execution failed"),
);
assert_eq!(
format!("{}", HostError::new(HostErrorType::DirNotAccessible, None)),
String::from("Could not access directory")
format!("{}", HostError::from(HostErrorType::DirNotAccessible)),
String::from("Could not access directory"),
);
assert_eq!(
format!(
"{}",
HostError::new(HostErrorType::NoSuchFileOrDirectory, None)
),
format!("{}", HostError::from(HostErrorType::NoSuchFileOrDirectory)),
String::from("No such file or directory")
);
assert_eq!(
format!("{}", HostError::new(HostErrorType::ReadonlyFile, None)),
format!("{}", HostError::from(HostErrorType::ReadonlyFile)),
String::from("File is readonly")
);
assert_eq!(
format!("{}", HostError::new(HostErrorType::FileNotAccessible, None)),
format!("{}", HostError::from(HostErrorType::FileNotAccessible)),
String::from("Could not access file")
);
assert_eq!(
format!("{}", HostError::new(HostErrorType::FileAlreadyExists, None)),
format!("{}", HostError::from(HostErrorType::FileAlreadyExists)),
String::from("File already exists")
);
}

View File

@@ -525,7 +525,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();

View File

@@ -39,6 +39,7 @@ pub struct KeyringStorage {
username: String,
}
#[cfg(not(tarpaulin_include))]
impl KeyringStorage {
/// ### new
///
@@ -50,6 +51,7 @@ impl KeyringStorage {
}
}
#[cfg(not(tarpaulin_include))]
impl KeyStorage for KeyringStorage {
/// ### get_key
///

View File

@@ -251,7 +251,7 @@ impl AuthActivity {
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = props.with_value(PropValue::Unsigned(port as usize)).build();
let props = props.with_value(PropValue::Str(port.to_string())).build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {

View File

@@ -61,7 +61,6 @@ impl AuthActivity {
super::COMPONENT_TEXT_FOOTER,
Box::new(Text::new(
PropsBuilder::default()
.with_foreground(Color::White)
.with_texts(TextParts::new(
None,
Some(vec![
@@ -158,7 +157,8 @@ impl AuthActivity {
super::COMPONENT_TEXT_NEW_VERSION,
Box::new(Text::new(
PropsBuilder::default()
.with_texts(TextParts::new(None, Some(vec![TextSpanBuilder::new(format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", version).as_str()).with_foreground(Color::Yellow).bold().build()])))
.with_foreground(Color::Yellow)
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", version))])))
.build()
))
);

View File

@@ -303,6 +303,10 @@ mod tests {
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);

View File

@@ -307,6 +307,10 @@ mod tests {
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);

View File

@@ -342,6 +342,7 @@ mod tests {
use super::*;
use crossterm::event::KeyEvent;
use tui::style::Color;
#[test]
fn test_ui_layout_components_input_text() {
@@ -362,6 +363,10 @@ mod tests {
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
// RenderData

View File

@@ -144,8 +144,7 @@ impl Component for LogBox {
None => Vec::new(),
Some(table) => table
.iter()
.enumerate()
.map(|(idx, row)| {
.map(|row| {
let mut columns: VecDeque<Span> = row
.iter()
.map(|col| {
@@ -161,15 +160,7 @@ impl Component for LogBox {
// Let's convert column spans into Spans rows NOTE: -4 because first line is always made by 5 columns; but there's always 1
let mut rows: Vec<Spans> = Vec::with_capacity(columns.len() - 4);
// Get first row
let mut first_row: Vec<Span> = vec![Span::styled(
match self.states.list_index == idx {
true => "> ",
false => " ",
},
Style::default()
.fg(self.props.foreground)
.bg(self.props.background),
)];
let mut first_row: Vec<Span> = Vec::with_capacity(5);
for _ in 0..5 {
if let Some(col) = columns.pop_front() {
first_row.push(col);
@@ -204,6 +195,7 @@ impl Component for LogBox {
.title(title),
)
.start_corner(Corner::BottomLeft)
.highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(self.props.get_modifiers()));
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
@@ -310,6 +302,7 @@ mod tests {
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_logbox() {
@@ -336,6 +329,10 @@ mod tests {
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);

View File

@@ -197,6 +197,12 @@ mod tests {
))
.build(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event

View File

@@ -155,6 +155,7 @@ mod tests {
use crate::ui::layout::props::{TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_progress_bar() {
@@ -168,6 +169,12 @@ mod tests {
);
// Get value
assert_eq!(component.get_value(), Payload::None);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Event
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),

View File

@@ -284,6 +284,10 @@ mod tests {
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::Unsigned(1));
// Handle events

View File

@@ -167,6 +167,7 @@ mod tests {
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_table() {
@@ -184,6 +185,12 @@ mod tests {
))
.build(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event

View File

@@ -30,7 +30,7 @@ use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use tui::{
layout::Rect,
style::Style,
style::{Color, Style},
text::{Span, Spans, Text as TuiText},
widgets::Paragraph,
};
@@ -64,12 +64,21 @@ impl Component for Text {
Some(rows) => rows
.iter()
.map(|x| {
// Keep line color, or use default
let fg: Color = match x.fg {
Color::Reset => self.props.foreground,
_ => x.fg,
};
let bg: Color = match x.bg {
Color::Reset => self.props.background,
_ => x.bg,
};
Span::styled(
x.content.clone(),
Style::default()
.add_modifier(x.get_modifiers())
.fg(x.fg)
.bg(x.bg),
.fg(fg)
.bg(bg),
)
})
.collect(),
@@ -169,6 +178,12 @@ mod tests {
))
.build(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event

View File

@@ -133,6 +133,7 @@ mod tests {
use crate::ui::layout::props::TextParts;
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_title() {
@@ -141,6 +142,12 @@ mod tests {
.with_texts(TextParts::new(Some(String::from("Title")), None))
.build(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event

View File

@@ -64,7 +64,6 @@ mod tests {
fn test_ui_layout_utils_draw_area_in() {
let area: Rect = Rect::new(0, 0, 1024, 512);
let child: Rect = draw_area_in(area, 75, 30);
println!("{:?}", child);
assert_eq!(child.x, 43);
assert_eq!(child.y, 63);
assert_eq!(child.width, 271);