Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
150a3cf346 | ||
|
|
b31de185c5 | ||
|
|
477930bef9 | ||
|
|
03bbd6420e | ||
|
|
b21607cd77 | ||
|
|
6c6dadc4e7 | ||
|
|
08b8946429 | ||
|
|
23882df474 | ||
|
|
d8547d8f21 | ||
|
|
c6de101808 | ||
|
|
d0688be5cb | ||
|
|
35f37cc2d3 | ||
|
|
81ae310e3d | ||
|
|
3be890c63a | ||
|
|
64a08e1440 | ||
|
|
fe5c35d789 | ||
|
|
df391dfb6f | ||
|
|
f5ac4207e8 | ||
|
|
b8c54b53d9 | ||
|
|
6be9294e11 | ||
|
|
7676e6e3a1 | ||
|
|
9776ecbe60 | ||
|
|
a1632492ed | ||
|
|
8d74d4c4e5 | ||
|
|
e29ce3d0dd | ||
|
|
e6b952966c | ||
|
|
1ba139aed1 | ||
|
|
f136057484 | ||
|
|
47cd112e69 | ||
|
|
871a02c8b5 | ||
|
|
44ba1111af | ||
|
|
52b35f9232 | ||
|
|
f8a448f5e9 | ||
|
|
37da49f4f8 | ||
|
|
c0ae922264 | ||
|
|
91081cb86a | ||
|
|
af678802bb | ||
|
|
66068ec73c | ||
|
|
32e939c183 | ||
|
|
b610da16a9 | ||
|
|
5dfcba3c51 | ||
|
|
d48e05cd74 | ||
|
|
6f4cb46d94 | ||
|
|
5886d90d16 | ||
|
|
ade7160c20 | ||
|
|
0c22b322ae | ||
|
|
a5ba118393 | ||
|
|
7acf119c77 | ||
|
|
bd00ba7971 | ||
|
|
f94a811dd9 |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: ['https://www.buymeacoffee.com/veeso']
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help me improving TermSCP
|
||||
title: "[BUG]"
|
||||
about: Create a report of the bug you've encountered
|
||||
title: "[BUG] - ISSUE_TITLE"
|
||||
labels: bug
|
||||
assignees: ChristianVisintin
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
@@ -24,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
|
||||
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,12 +1,23 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for TermSCP
|
||||
title: "[Feature Request]"
|
||||
labels: enhancement
|
||||
assignees: ChristianVisintin
|
||||
about: Suggest an idea to improve termscp
|
||||
title: "[Feature Request] - FEATURE_TITLE"
|
||||
labels: "new feature"
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Describe the feature you'd like to be added
|
||||
Put here a brief introduction to your suggestion.
|
||||
|
||||
### Changes
|
||||
|
||||
The following changes to the application are expected
|
||||
|
||||
- ...
|
||||
|
||||
## Implementation
|
||||
|
||||
Provide any kind of suggestion you propose on how to implement the feature.
|
||||
If you have none, delete this section.
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask what you want about the project
|
||||
title: "[QUESTION] - TITLE"
|
||||
labels: question
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
@@ -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
|
||||
12
.github/workflows/linux.yml
vendored
@@ -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 }}
|
||||
|
||||
39
CHANGELOG.md
@@ -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.
|
||||
|
||||
307
CONTRIBUTING.md
@@ -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
@@ -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"
|
||||
|
||||
115
Cargo.toml
@@ -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"
|
||||
|
||||
45
README.md
@@ -1,12 +1,12 @@
|
||||
# TermSCP
|
||||
|
||||
[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
|
||||
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://codecov.io/gh/veeso/termscp)
|
||||
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](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 🥳
|
||||
|
||||
[](https://www.buymeacoffee.com/veeso)
|
||||
|
||||
---
|
||||
|
||||
## License 📃
|
||||
|
||||
termscp is licensed under the MIT license since version 0.4.0.
|
||||
|
||||
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 689 KiB After Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 635 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -5,5 +5,3 @@ ignore:
|
||||
- src/ui/activities/
|
||||
- src/ui/context.rs
|
||||
- src/ui/input.rs
|
||||
fixes:
|
||||
- "/::"
|
||||
|
||||
8
dist/pkgs/arch/.SRCINFO
vendored
@@ -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
|
||||
|
||||
|
||||
4
dist/pkgs/arch/PKGBUILD
vendored
@@ -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/"
|
||||
|
||||
BIN
docs/bl.png
|
Before Width: | Height: | Size: 271 KiB |
35
docs/deploy.md
Normal 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
@@ -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,
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
235
src/host/mod.rs
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
))
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||