mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a14c2725 | ||
|
|
df03c5c1bf | ||
|
|
3ce3ffee3d | ||
|
|
c0b32a1847 | ||
|
|
81ae0035c3 | ||
|
|
783da22ca2 | ||
|
|
8715c2b6f9 | ||
|
|
98a748dccc | ||
|
|
bef031a414 | ||
|
|
ce0e953182 | ||
|
|
9a5caf75c3 | ||
|
|
446f4a3a32 | ||
|
|
da75912d26 | ||
|
|
4ed23c2a18 | ||
|
|
23ae334bef | ||
|
|
ec75ae1486 | ||
|
|
368570592f | ||
|
|
806793421e | ||
|
|
1f377b242d | ||
|
|
a4906b129a | ||
|
|
5c6e8925ad | ||
|
|
a18eff689d | ||
|
|
274742d6d9 | ||
|
|
cdebcbd4dc | ||
|
|
cdf303a847 | ||
|
|
dd35fe825c | ||
|
|
5c4a971aca | ||
|
|
7522b4d0ff | ||
|
|
b0f314837e | ||
|
|
8a9ba7745a | ||
|
|
b7d75a2749 | ||
|
|
fe0d9b0aa6 | ||
|
|
7dba691ccc | ||
|
|
099e2154ba | ||
|
|
f2efb25ad7 | ||
|
|
e45c3d5b4e | ||
|
|
69f821baef | ||
|
|
11559d0962 | ||
|
|
79c33095fc | ||
|
|
0ec6bcbcef | ||
|
|
3cde067339 | ||
|
|
c05ef32270 | ||
|
|
7eb913ec7b | ||
|
|
4e63093d25 | ||
|
|
e5d50698d2 | ||
|
|
79c31c2d84 | ||
|
|
aab266a661 | ||
|
|
a4c9acb49f | ||
|
|
6205c7f3c5 | ||
|
|
e4af014013 | ||
|
|
c9f649f40f | ||
|
|
1f27ca2607 | ||
|
|
a1288c7480 | ||
|
|
4bfec52ba5 | ||
|
|
3f01be3baa | ||
|
|
8e2ffeabce | ||
|
|
fc68e2621b | ||
|
|
b2a8a3041c | ||
|
|
c507d54700 | ||
|
|
31c5ad7534 | ||
|
|
14ac10547c | ||
|
|
ae1638ee17 | ||
|
|
c5f76ec51c | ||
|
|
d319a2fae4 | ||
|
|
72a5703a08 | ||
|
|
91b4d4e463 | ||
|
|
17719ea370 | ||
|
|
707a3faa54 | ||
|
|
dfe58e6147 | ||
|
|
c49dab3888 | ||
|
|
47ff07e496 | ||
|
|
61a8fb95e4 | ||
|
|
f757336d75 | ||
|
|
cf529c1678 | ||
|
|
631f09b9a8 | ||
|
|
65aed76605 | ||
|
|
9036d83635 | ||
|
|
179c4de4ed | ||
|
|
f3b84c97e1 | ||
|
|
b5b3aeb645 | ||
|
|
e5172d4207 | ||
|
|
50e7f5f5d0 | ||
|
|
88cdae79a8 | ||
|
|
4f3b97b198 | ||
|
|
54a8317d0e | ||
|
|
3a46aa74f9 | ||
|
|
aca5b37898 | ||
|
|
0394a12c9f | ||
|
|
e61d04aa1b | ||
|
|
c0c9f7c0dd | ||
|
|
905fe5fc9f | ||
|
|
89ab53a71b | ||
|
|
7dccac6105 | ||
|
|
44051ec718 | ||
|
|
c7469b8594 | ||
|
|
5dfee2cbd9 | ||
|
|
679a829744 | ||
|
|
2a51ab984c | ||
|
|
2b2ebadd6a | ||
|
|
2b15a23fe8 | ||
|
|
d30c3dfadd |
16
.github/workflows/build-artifacts.yml
vendored
16
.github/workflows/build-artifacts.yml
vendored
@@ -3,6 +3,9 @@ name: "Build artifacts"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TERMSCP_VERSION: "0.18.0"
|
||||
|
||||
jobs:
|
||||
build-binaries:
|
||||
name: Build - ${{ matrix.platform.release_for }}
|
||||
@@ -22,21 +25,22 @@ jobs:
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
target: ${{ matrix.platform.target }}
|
||||
targets: ${{ matrix.platform.target }}
|
||||
- name: Build release
|
||||
run: cargo build --release --target ${{ matrix.platform.target }}
|
||||
- name: Prepare artifact files
|
||||
run: |
|
||||
mkdir -p .artifact
|
||||
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
|
||||
tar -czf .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz -C .artifact termscp
|
||||
ls -l .artifact/
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
name: ${{ matrix.platform.release_for }}
|
||||
path: .artifact/*
|
||||
name: termscp-${{ matrix.platform.target }}
|
||||
path: .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz
|
||||
|
||||
45
.github/workflows/coverage.yml
vendored
45
.github/workflows/coverage.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Coverage
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev libsmbclient
|
||||
- name: Setup nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- name: Run tests (nightly)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features github-actions --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||
- name: Coverage with grcov
|
||||
id: coverage
|
||||
uses: actions-rs/grcov@v0.1
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ${{ steps.coverage.outputs.report }}
|
||||
3
.github/workflows/linux.yml
vendored
3
.github/workflows/linux.yml
vendored
@@ -21,10 +21,9 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
3
.github/workflows/macos.yml
vendored
3
.github/workflows/macos.yml
vendored
@@ -18,10 +18,9 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Build
|
||||
run: cargo build
|
||||
|
||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -21,3 +21,4 @@ jobs:
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
exempt-issue-labels: "backlog"
|
||||
exempt-all-milestones: true
|
||||
|
||||
6
.github/workflows/website.yml
vendored
6
.github/workflows/website.yml
vendored
@@ -33,11 +33,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "./site/"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
3
.github/workflows/windows.yml
vendored
3
.github/workflows/windows.yml
vendored
@@ -19,10 +19,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Build
|
||||
run: cargo build
|
||||
|
||||
113
CHANGELOG.md
113
CHANGELOG.md
@@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.18.0](#0180)
|
||||
- [0.17.0](#0170)
|
||||
- [0.16.1](#0161)
|
||||
- [0.16.0](#0160)
|
||||
- [0.15.0](#0150)
|
||||
- [0.14.0](#0140)
|
||||
- [0.13.0](#0130)
|
||||
- [0.12.3](#0123)
|
||||
- [0.12.2](#0122)
|
||||
- [0.12.1](#0121)
|
||||
@@ -34,6 +41,112 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.18.0
|
||||
|
||||
Released on 10/06/2025
|
||||
|
||||
- 🐚 An **Embedded shell for termscp**:
|
||||
- [Issue 340](https://github.com/veeso/termscp/issues/340): Replaced the `Exec` popup with a **fully functional terminal emulator** embedded thanks to [A-Kenji's tui-term](https://github.com/a-kenji/tui-term).
|
||||
- Command History
|
||||
- Support for `cd` and `exit` commands as well.
|
||||
- Exit just closes the terminal emulator.
|
||||
- [Issue 345](https://github.com/veeso/termscp/issues/345): Default keys are used from `~/.ssh` directory if no keys are resolved for the host.
|
||||
- **Updated dependencies** and updated the Rust edition to `2024`
|
||||
|
||||
## 0.17.0
|
||||
|
||||
Released on 24/03/2025
|
||||
|
||||
- **Queuing transfers**:
|
||||
- the logic of selecting files has been extended!
|
||||
- From now on selecting file will put the files into a **transfer queue**, which is shown on the bottom panel.
|
||||
- When a file is selected the file is added to the queue with a destination path, which is the **current other explorer path at the moment of selection.**
|
||||
- It is possible to navigate to the transfer queue by using `P` and pressing `ENTER` or `DELETE` on a file will remove it from the transfer queue.
|
||||
- Other commands will work as well on the transfer queue, like `COPY`, `MOVE`, `DELETE`, `RENAME`.
|
||||
- [issue 308](https://github.com/veeso/termscp/issues/308): added `--wno-keyring` flag to disable keyring
|
||||
- [issue 316](https://github.com/veeso/termscp/issues/316): Local directory path is not switching to what's specified in the bookmark. Now the local directory path is correctly set following this hierarchy:
|
||||
1. Local directory path specified for the host bridge
|
||||
2. Local directory path specified in the bookmark
|
||||
3. Working directory
|
||||
- [issue 317](https://github.com/veeso/termscp/issues/317): the return value of `--version` should be `0`
|
||||
- [issue 319](https://github.com/veeso/termscp/issues/319): fixed a crash when the local directory specified in the auth form does not exist
|
||||
- [issue 327](https://github.com/veeso/termscp/issues/327): fixed a panic when trying to go up from local directory on localhost in the auth form
|
||||
- [issue 330](https://github.com/veeso/termscp/issues/330): add suppaftp/pavao/kube to allowed logs
|
||||
- Dependencies:
|
||||
- `argh` to `0.1.13`
|
||||
- `bytesize` to `2`
|
||||
- `dirs` to `6`
|
||||
- `magic-crypt` to `4`
|
||||
- `notify` to `8`
|
||||
- `ssh2-config` to `0.4`
|
||||
- `remotefs-ssh` to `0.6`
|
||||
- `rust` edition to `2024`
|
||||
|
||||
## 0.16.1
|
||||
|
||||
Released on 12/11/2024
|
||||
|
||||
- Just fixed this: e45c3d5b4ef64653e5b6cc4f3703e3b67514306d
|
||||
- `fix: gg rust 1.82 for introducing a nice breaking change in config which was not mentioned in changelog`
|
||||
|
||||
## 0.16.0
|
||||
|
||||
Released on 14/10/2024
|
||||
|
||||
- [**Multi Host support**](https://github.com/veeso/termscp/issues/285):
|
||||
- Now it is possible to work on two different remotes `remote A -> remote B` instead of just `localhost -> remote`
|
||||
- Cli arguments now accept an additional `remote-args` for the left panel.
|
||||
- For more details read this issue <https://github.com/veeso/termscp/issues/285>.
|
||||
- Change between auth forms with `<BACKTAB>`
|
||||
- Bookmarks are automatically loaded into the last auth form.
|
||||
- [Issue 289](https://github.com/veeso/termscp/issues/289): Use `uzers` instead of the dead package `users` which has several vulnerabilities
|
||||
- [Issue 290](https://github.com/veeso/termscp/issues/290): Password prompt was broken
|
||||
- [Issue 298](https://github.com/veeso/termscp/issues/298): tuirealm 2.x
|
||||
- Fixed some performance issues where sometimes the app froze for a couple of seconds, thanks to this <https://github.com/veeso/tui-realm/pull/78>.
|
||||
- [Issue 292](https://github.com/veeso/termscp/issues/292): New version alert was not displayed due to a semver regex issue.
|
||||
- [Issue 291](https://github.com/veeso/termscp/issues/291): Show `..` directory before all the others in the explorer. If you click on it you'll go the parent directory (same as pressing `<U>`). No, you can't select it for transfers and it's actually been implemented in the worse way possible, because this little change would require a huge refactoring of the explorer component. I promise I will do it one day, but I dunno when.
|
||||
- Logging: filter out messages not related to termscp or remotefs
|
||||
|
||||
## 0.15.0
|
||||
|
||||
Released on 03/10/2024
|
||||
|
||||
- [Issue 249](https://github.com/veeso/termscp/issues/249): The old *find* command has been replaced with a brand new explorer with support to 🪄 **Fuzzy search** 🪄. The command is still `<F>`.
|
||||
- [Issue 283](https://github.com/veeso/termscp/issues/283): **Find command can now be cancelled** by pressing `<CTRL+C>`. While scanning the directory it will also display the current progress.
|
||||
- [Issue 268](https://github.com/veeso/termscp/issues/268): 📦 **Pods and container explorer** 🐳 for Kube protocol.
|
||||
- BREAKING ‼️ Kube address argument has changed to `namespace[@<cluster_url>][$<path>]`
|
||||
- Pod and container argumets have been removed; from now on you will connect with the following syntax to the provided namespace: `/pod-name/container-name/path/to/file`
|
||||
- [Issue 279](https://github.com/veeso/termscp/issues/279): do not clear screen
|
||||
- [Issue 277](https://github.com/veeso/termscp/issues/277): Fix a bug in the configuration page, which caused being stuck if the added SSH key was empty
|
||||
- [Issue 272](https://github.com/veeso/termscp/issues/272): `isolated-tests` feature to run tests for releasing on distributions which run in isolated environments
|
||||
- [Issue 280](https://github.com/veeso/termscp/issues/280): Autocompletion when pressing `<TAB>` on the `Go to` popup.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
Released on 17/07/2024
|
||||
|
||||
- [Issue 226](https://github.com/veeso/termscp/issues/226): Use ssh-agent
|
||||
- [Issue 241](https://github.com/veeso/termscp/issues/241): Jump to next entry after select
|
||||
- [Issue 242](https://github.com/veeso/termscp/issues/242): Added `Kube` protocol support
|
||||
- [Issue 255](https://github.com/veeso/termscp/issues/255): New keybindings `Alt + A` to deselect all files
|
||||
- [Issue 256](https://github.com/veeso/termscp/issues/256): Filter files in current folder. You can now filter files by pressing `/`. Both wildmatch and regex are accepted to filter files.
|
||||
- [Issue 257](https://github.com/veeso/termscp/issues/257): CLI remote args cannot handle '@' in the username
|
||||
|
||||
## 0.13.0
|
||||
|
||||
Released on 03/03/2024
|
||||
|
||||
- Added CLI subcommands
|
||||
- Changed `-t` to `theme`
|
||||
- Changed `-u` to `update`
|
||||
- Changed `-c` to `config`
|
||||
- Introduced support for [WebDAV](https://www.rfc-editor.org/rfc/rfc4918)
|
||||
- It is now possible also to connect directly to WebDAV server with the syntax `http(s)://username:password@google.com`
|
||||
- Bugfix:
|
||||
- [Issue 232](https://github.com/veeso/termscp/issues/232): AWS S3 wasn't working anymore due to rust-s3 outdate
|
||||
- Dependencies:
|
||||
- Added `remotefs-webdav 0.1.1`
|
||||
|
||||
## 0.12.3
|
||||
|
||||
Released on 06/10/2023
|
||||
|
||||
@@ -2,75 +2,131 @@
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at christian.visintin1997@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
<https://www.contributor-covenant.org/faq>
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Contributing
|
||||
|
||||
Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change.
|
||||
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
|
||||
Please note we have a [code of conduct](CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project.
|
||||
|
||||
- [Contributing](#contributing)
|
||||
- [Project mission](#project-mission)
|
||||
@@ -20,9 +20,13 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
|
||||
|
||||
## 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
|
||||
termscp was born because, as a terminal lover and Linux user, I wanted something like WinSCP on Linux and on terminal. At my previous job, I used SFTP/SCP practically everyday and that made me desire an application like termscp so much that eventually I started to work on it in my spare time.
|
||||
|
||||
> 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".
|
||||
I saw there was a very cool library to create terminal user interfaces (`tui-rs`, now `ratatui`), so I started to code it.
|
||||
|
||||
I wrote termscp as an experiment. I didn't design anything 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.
|
||||
|
||||
5416
Cargo.lock
generated
5416
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
132
Cargo.toml
132
Cargo.toml
@@ -1,22 +1,17 @@
|
||||
[package]
|
||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||
categories = ["command-line-utilities"]
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3"
|
||||
edition = "2021"
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
||||
edition = "2024"
|
||||
homepage = "https://termscp.veeso.dev"
|
||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
keywords = [
|
||||
"scp-client",
|
||||
"sftp-client",
|
||||
"ftp-client",
|
||||
"winscp",
|
||||
"command-line-utility",
|
||||
]
|
||||
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||
license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.12.3"
|
||||
version = "0.18.0"
|
||||
rust-version = "1.85.1"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -29,7 +24,7 @@ termscp = { path = "/usr/bin/termscp" }
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
|
||||
copyright = "2022, Christian Visintin <christian.visintin@veeso.dev>"
|
||||
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
|
||||
extended-description-file = "docs/misc/README.deb.txt"
|
||||
|
||||
[[bin]]
|
||||
@@ -38,30 +33,36 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
argh = "^0.1"
|
||||
bitflags = "^2.1"
|
||||
bytesize = "^1.1"
|
||||
bitflags = "^2"
|
||||
bytesize = "^2"
|
||||
chrono = "^0.4"
|
||||
content_inspector = "^0.2"
|
||||
dirs = "^5.0"
|
||||
dirs = "^6"
|
||||
edit = "^0.1"
|
||||
filetime = "^0.2"
|
||||
hostname = "^0.3"
|
||||
keyring = { version = "^2.0", optional = true }
|
||||
lazy-regex = "^2.5"
|
||||
lazy_static = "^1.4"
|
||||
log = "^0.4"
|
||||
magic-crypt = "^3.1"
|
||||
notify = "=4.0.17"
|
||||
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
||||
open = "^5.0"
|
||||
rand = "^0.8.5"
|
||||
remotefs = "^0.2.0"
|
||||
remotefs-aws-s3 = { version = "^0.2.1", default-features = false, features = [
|
||||
"find",
|
||||
"rustls",
|
||||
hostname = "^0.4"
|
||||
keyring = { version = "^3", features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"sync-secret-service",
|
||||
"vendored",
|
||||
] }
|
||||
rpassword = "^7.0"
|
||||
self_update = { version = "^0.37", default-features = false, features = [
|
||||
lazy-regex = "^3"
|
||||
lazy_static = "^1"
|
||||
log = "^0.4"
|
||||
magic-crypt = "4"
|
||||
notify = "8"
|
||||
notify-rust = { version = "^4", default-features = false, features = ["d"] }
|
||||
nucleo = "0.5"
|
||||
open = "^5.0"
|
||||
rand = "^0.9"
|
||||
regex = "^1"
|
||||
remotefs = "^0.3"
|
||||
remotefs-aws-s3 = "0.4"
|
||||
remotefs-kube = "0.4"
|
||||
remotefs-webdav = "^0.2"
|
||||
rpassword = "^7"
|
||||
self_update = { version = "^0.42", default-features = false, features = [
|
||||
"rustls",
|
||||
"archive-tar",
|
||||
"archive-zip",
|
||||
@@ -70,43 +71,46 @@ self_update = { version = "^0.37", default-features = false, features = [
|
||||
] }
|
||||
serde = { version = "^1", features = ["derive"] }
|
||||
simplelog = "^0.12"
|
||||
ssh2-config = "^0.2"
|
||||
tempfile = "^3.4"
|
||||
thiserror = "^1"
|
||||
toml = "^0.7"
|
||||
tui-realm-stdlib = "^1.2"
|
||||
tuirealm = "^1.8.0"
|
||||
unicode-width = "^0.1"
|
||||
version-compare = "^0.1"
|
||||
whoami = "^1.4"
|
||||
wildmatch = "^2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "^1.3"
|
||||
serial_test = "^2.0"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["smb", "with-keyring"]
|
||||
github-actions = []
|
||||
with-keyring = ["keyring"]
|
||||
smb = ["remotefs-smb"]
|
||||
ssh2-config = "^0.5"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1.44", features = ["rt"] }
|
||||
toml = "^0.8"
|
||||
tui-realm-stdlib = "3"
|
||||
tuirealm = "3"
|
||||
tui-term = "0.2"
|
||||
unicode-width = "^0.2"
|
||||
version-compare = "^0.2"
|
||||
whoami = "^1.6"
|
||||
wildmatch = "^2"
|
||||
|
||||
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
||||
remotefs-smb = { version = "^0.2", optional = true }
|
||||
remotefs-smb = { version = "^0.3", optional = true }
|
||||
|
||||
[target."cfg(target_family = \"windows\")"]
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.1.2", features = ["native-tls"] }
|
||||
remotefs-ssh = "^0.2"
|
||||
|
||||
[target."cfg(target_family = \"unix\")"]
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.1.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.2", features = ["ssh2-vendored"] }
|
||||
users = "0.11.0"
|
||||
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
|
||||
remotefs-ssh = { version = "^0.6", features = ["ssh2-vendored"] }
|
||||
uzers = "0.12"
|
||||
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
|
||||
remotefs-ssh = { version = "^0.6" }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "^1"
|
||||
serial_test = "^3"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2"
|
||||
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
||||
|
||||
[features]
|
||||
default = ["smb", "keyring"]
|
||||
github-actions = []
|
||||
isolated-tests = []
|
||||
keyring = []
|
||||
smb = ["dep:remotefs-smb"]
|
||||
smb-vendored = ["remotefs-smb/vendored"]
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
42
README.md
42
README.md
@@ -1,7 +1,7 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
<img src="/assets/images/termscp.svg" alt="termscp logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ A feature rich terminal file transfer ~</p>
|
||||
@@ -21,6 +21,14 @@
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="/docs/de/README.md"
|
||||
><img
|
||||
@@ -62,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 0.12.3 (06/10/2023)</p>
|
||||
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -73,7 +81,7 @@
|
||||
/></a>
|
||||
<a href="https://github.com/veeso/termscp/stargazers"
|
||||
><img
|
||||
src="https://img.shields.io/github/stars/veeso/termscp.svg"
|
||||
src="https://img.shields.io/github/stars/veeso/termscp?style=flat"
|
||||
alt="Repo stars"
|
||||
/></a>
|
||||
<a href="https://crates.io/crates/termscp"
|
||||
@@ -108,18 +116,13 @@
|
||||
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
|
||||
alt="Windows CI"
|
||||
/></a>
|
||||
<a href="https://coveralls.io/github/veeso/termscp"
|
||||
><img
|
||||
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
|
||||
alt="Coveralls"
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## About termscp 🖥
|
||||
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD**, **NetBSD** and **Windows** compatible.
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD**, **NetBSD** and **Windows** compatible.
|
||||
|
||||

|
||||
|
||||
@@ -131,14 +134,17 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** and **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
|
||||
- Create, remove, rename, search, view and edit files
|
||||
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
|
||||
- 📝 View and edit files with your favourite applications
|
||||
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
||||
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
||||
- 🐚 Embedded terminal for executing commands on the system.
|
||||
- 🎨 Make it yours!
|
||||
- Themes
|
||||
- Custom file explorer format
|
||||
@@ -162,7 +168,7 @@ If you want to contribute to this project, don't forget to check out our [contri
|
||||
If you are a Linux, a FreeBSD or a MacOS user this simple shell script will install termscp on your system with a single command:
|
||||
|
||||
```sh
|
||||
curl -sSLf http://get-termscp.veeso.dev | sh
|
||||
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
|
||||
```
|
||||
|
||||
> ❗ MacOs installation requires [Homebrew](https://brew.sh/), otherwise the Rust compiler will be installed
|
||||
@@ -179,6 +185,12 @@ NetBSD users can install termscp from the official repositories.
|
||||
pkgin install termscp
|
||||
```
|
||||
|
||||
Arch Linux users can install termscp from the official repositories.
|
||||
|
||||
```sh
|
||||
pacman -S termscp
|
||||
```
|
||||
|
||||
For more information or other platforms, please visit [termscp.veeso.dev](https://termscp.veeso.dev/#get-started) to view all installation methods.
|
||||
|
||||
⚠️ If you're looking on how to update termscp just run termscp from CLI with: `(sudo) termscp --update` ⚠️
|
||||
@@ -220,7 +232,6 @@ You can make a donation with one of these platforms:
|
||||
|
||||
[](https://ko-fi.com/veeso)
|
||||
[](https://www.paypal.me/chrisintin)
|
||||
[](https://btc.com/bc1qvlmykjn7htz0vuprmjrlkwtv9m9pan6kylsr8w)
|
||||
|
||||
---
|
||||
|
||||
@@ -232,9 +243,7 @@ The user manual can be found on the [termscp's website](https://termscp.veeso.de
|
||||
|
||||
## Upcoming Features 🧪
|
||||
|
||||
For **2023** there will be two major updates during the year.
|
||||
|
||||
Along to new features, termscp developments is now focused on UX and performance improvements, so if you have any suggestion, feel free to open an issue.
|
||||
See [Milestones](https://github.com/veeso/termscp/milestones)
|
||||
|
||||
---
|
||||
|
||||
@@ -263,12 +272,13 @@ termscp is powered by these awesome projects:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [edit](https://github.com/milkey-mouse/edit)
|
||||
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
||||
- [kube](https://github.com/kube-rs/kube)
|
||||
- [open-rs](https://github.com/Byron/open-rs)
|
||||
- [pavao](https://github.com/veeso/pavao)
|
||||
- [remotefs](https://github.com/veeso/remotefs-rs)
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
23
build.rs
23
build.rs
@@ -1,16 +1,33 @@
|
||||
use cfg_aliases::cfg_aliases;
|
||||
use vergen_git2::{BuildBuilder, CargoBuilder, Emitter, Git2Builder, RustcBuilder, SysinfoBuilder};
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Setup cfg aliases
|
||||
cfg_aliases! {
|
||||
// Platforms
|
||||
macos: { target_os = "macos" },
|
||||
linux: { target_os = "linux" },
|
||||
unix: { target_family = "unix" },
|
||||
windows: { target_family = "windows" },
|
||||
posix: { target_family = "unix" },
|
||||
win: { target_family = "windows" },
|
||||
// exclusive features
|
||||
smb: { all(feature = "smb", not( macos )) },
|
||||
smb_unix: { all(unix, feature = "smb", not(macos)) },
|
||||
smb_windows: { all(windows, feature = "smb") }
|
||||
}
|
||||
|
||||
let build = BuildBuilder::all_build()?;
|
||||
let cargo = CargoBuilder::all_cargo()?;
|
||||
let git2 = Git2Builder::all_git()?;
|
||||
let rustc = RustcBuilder::all_rustc()?;
|
||||
let si = SysinfoBuilder::all_sysinfo()?;
|
||||
|
||||
Emitter::default()
|
||||
.add_instructions(&build)?
|
||||
.add_instructions(&cargo)?
|
||||
.add_instructions(&git2)?
|
||||
.add_instructions(&rustc)?
|
||||
.add_instructions(&si)?
|
||||
.emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
28
dist/build/aarch64_debian10/Dockerfile
vendored
28
dist/build/aarch64_debian10/Dockerfile
vendored
@@ -9,9 +9,33 @@ RUN apt update && apt install -y \
|
||||
libdbus-1-dev \
|
||||
build-essential \
|
||||
libsmbclient-dev \
|
||||
libsmbclient \
|
||||
libgit2-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libbsd-dev \
|
||||
libcap-dev \
|
||||
libcups2-dev \
|
||||
libgnutls28-dev \
|
||||
libicu-dev \
|
||||
libjansson-dev \
|
||||
libkeyutils-dev \
|
||||
libldap2-dev \
|
||||
zlib1g-dev \
|
||||
libpam0g-dev \
|
||||
libacl1-dev \
|
||||
libarchive-dev \
|
||||
flex \
|
||||
bison \
|
||||
libntirpc-dev \
|
||||
libtracker-sparql-3.0-dev \
|
||||
libglib2.0-dev \
|
||||
libdbus-1-dev \
|
||||
libsasl2-dev \
|
||||
libunistring-dev \
|
||||
bash \
|
||||
curl
|
||||
curl \
|
||||
cpanminus && \
|
||||
cpanm Parse::Yapp::Driver;
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
|
||||
4
dist/build/freebsd.sh
vendored
4
dist/build/freebsd.sh
vendored
@@ -34,9 +34,9 @@ rm manifest
|
||||
echo -e "name: \"termscp\"" > manifest
|
||||
echo -e "version: $VERSION" >> manifest
|
||||
echo -e "origin: veeso/termscp" >> manifest
|
||||
echo -e "comment: \"A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3\"" >> manifest
|
||||
echo -e "comment: \"A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV\"" >> manifest
|
||||
echo -e "desc: <<EOD\n\
|
||||
A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3\n\
|
||||
A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV\n\
|
||||
EOD\n\
|
||||
arch: \"amd64\"\n\
|
||||
www: \"https://termscp.veeso.dev/termscp/\"\n\
|
||||
|
||||
14
dist/build/linux-aarch64.sh
vendored
14
dist/build/linux-aarch64.sh
vendored
@@ -21,7 +21,6 @@ fi
|
||||
|
||||
# names
|
||||
ARM64_DEB_NAME="termscp-arm64_deb"
|
||||
ARM64_RPM_NAME="termscp-arm64_rpm"
|
||||
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
@@ -39,8 +38,8 @@ cd -
|
||||
mkdir -p ${PKGS_DIR}/deb/
|
||||
mkdir -p ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
||||
docker run --name "$ARM64_DEB_NAME" -d "$ARM64_DEB_NAME" || docker start "$ARM64_DEB_NAME"
|
||||
docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release && cargo deb"
|
||||
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}_arm64.deb ${PKGS_DIR}/deb/
|
||||
docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb"
|
||||
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_arm64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_arm64.deb
|
||||
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
||||
docker stop "$ARM64_DEB_NAME"
|
||||
# Make tar.gz
|
||||
@@ -49,14 +48,5 @@ tar cvzf termscp-v${VERSION}-aarch64-unknown-linux-gnu.tar.gz termscp
|
||||
echo "Sha256 (homebrew aarch64): $(sha256sum termscp-v${VERSION}-aarch64-unknown-linux-gnu.tar.gz)"
|
||||
rm termscp
|
||||
cd -
|
||||
# Build aarch64_centos7
|
||||
cd aarch64_centos7/
|
||||
docker buildx build --platform linux/arm64 $CACHE --build-arg branch=${BRANCH} --tag $ARM64_RPM_NAME .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/rpm/
|
||||
docker run --name "$ARM64_RPM_NAME" -d "$ARM64_RPM_NAME" || docker start "$ARM64_RPM_NAME"
|
||||
docker exec -it "$ARM64_RPM_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH; cargo rpm init; cargo rpm build"
|
||||
docker cp ${ARM64_RPM_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/aarch64/termscp-${VERSION}-1.el7.aarch64.rpm ${PKGS_DIR}/rpm/termscp-${VERSION}-1.aarch64.rpm
|
||||
docker stop "$ARM64_RPM_NAME"
|
||||
|
||||
exit $?
|
||||
|
||||
16
dist/build/linux-x86_64.sh
vendored
16
dist/build/linux-x86_64.sh
vendored
@@ -21,7 +21,6 @@ fi
|
||||
|
||||
# names
|
||||
X86_64_DEB_NAME="termscp-x86_64_deb"
|
||||
X86_64_RPM_NAME="termscp-x86_64_rpm"
|
||||
|
||||
set -e # Don't fail
|
||||
|
||||
@@ -31,14 +30,14 @@ PKGS_DIR=$(pwd)/pkgs
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/
|
||||
# Build x86_64_deb
|
||||
cd x86_64_debian10/
|
||||
cd x86_64_debian12/
|
||||
docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_DEB_NAME" .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/deb/
|
||||
mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/
|
||||
docker run --name "$X86_64_DEB_NAME" -d "$X86_64_DEB_NAME" || docker start "$X86_64_DEB_NAME"
|
||||
docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release && cargo deb"
|
||||
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}_amd64.deb ${PKGS_DIR}/deb/
|
||||
docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb"
|
||||
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_amd64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_amd64.deb
|
||||
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/x86_64-unknown-linux-gnu/
|
||||
docker stop "$X86_64_DEB_NAME"
|
||||
# Make tar.gz
|
||||
@@ -47,14 +46,5 @@ tar cvzf termscp-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz termscp
|
||||
echo "Sha256 x86_64 (homebrew): $(sha256sum termscp-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz)"
|
||||
rm termscp
|
||||
cd -
|
||||
# Build x86_64_centos7
|
||||
cd x86_64_centos7/
|
||||
docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_RPM_NAME" .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/rpm/
|
||||
docker run --name "$X86_64_RPM_NAME" -d "$X86_64_RPM_NAME" || docker start "$X86_64_RPM_NAME"
|
||||
docker exec -it "$X86_64_RPM_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH; cargo rpm init; cargo rpm build"
|
||||
docker cp ${X86_64_RPM_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.el7.x86_64.rpm ${PKGS_DIR}/rpm/termscp-${VERSION}-1.x86_64.rpm
|
||||
docker stop "$X86_64_RPM_NAME"
|
||||
|
||||
exit $?
|
||||
|
||||
4
dist/build/macos.sh
vendored
4
dist/build/macos.sh
vendored
@@ -81,7 +81,7 @@ fi
|
||||
# Build release (x86_64)
|
||||
X86_TARGET=""
|
||||
X86_TARGET_DIR=""
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
X86_TARGET="--target x86_64-apple-darwin"
|
||||
X86_TARGET_DIR="target/x86_64-apple-darwin/release/"
|
||||
fi
|
||||
@@ -92,7 +92,7 @@ RET_X86_64=$?
|
||||
|
||||
ARM64_TARGET=""
|
||||
ARM64_TARGET_DIR=""
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||
ARM64_TARGET="--target aarch64-apple-darwin"
|
||||
ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/"
|
||||
fi
|
||||
|
||||
27
dist/build/x86_64_debian10/Dockerfile
vendored
27
dist/build/x86_64_debian10/Dockerfile
vendored
@@ -1,27 +0,0 @@
|
||||
FROM debian:buster
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libdbus-1-dev \
|
||||
build-essential \
|
||||
libsmbclient-dev \
|
||||
libsmbclient \
|
||||
bash \
|
||||
curl
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo deb
|
||||
RUN . $HOME/.cargo/env && cargo install cargo-deb
|
||||
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
54
dist/build/x86_64_debian12/Dockerfile
vendored
Normal file
54
dist/build/x86_64_debian12/Dockerfile
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
FROM debian:bookworm
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libdbus-1-dev \
|
||||
build-essential \
|
||||
libsmbclient-dev \
|
||||
libgit2-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libbsd-dev \
|
||||
libcap-dev \
|
||||
libcups2-dev \
|
||||
libgnutls28-dev \
|
||||
libgnutls30 \
|
||||
libicu-dev \
|
||||
libjansson-dev \
|
||||
libkeyutils-dev \
|
||||
libldap2-dev \
|
||||
zlib1g-dev \
|
||||
libpam0g-dev \
|
||||
libacl1-dev \
|
||||
libarchive-dev \
|
||||
libssl-dev \
|
||||
flex \
|
||||
bison \
|
||||
libntirpc-dev \
|
||||
libglib2.0-dev \
|
||||
libdbus-1-dev \
|
||||
libsasl2-dev \
|
||||
libunistring-dev \
|
||||
bash \
|
||||
curl \
|
||||
cpanminus && \
|
||||
cpanm Parse::Yapp::Driver;
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y && \
|
||||
. $HOME/.cargo/env && \
|
||||
cargo version
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo deb
|
||||
RUN . $HOME/.cargo/env && cargo install cargo-deb
|
||||
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
@@ -1,7 +1,7 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
<img src="/assets/images/termscp.svg" alt="logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ Eine funktionsreiche Terminal-Dateiübertragung ~</p>
|
||||
@@ -14,13 +14,21 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/veeso/termscp"
|
||||
<a href="https://github.com/veeso/termscp"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/gb.png"
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="/docs/de/README.md"
|
||||
><img
|
||||
@@ -62,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 0.12.3 (06/10/2023)</p>
|
||||
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -124,7 +132,7 @@
|
||||
|
||||
## Über termscp 🖥
|
||||
|
||||
Termscp ist ein funktionsreicher Terminal-Dateitransfer und Explorer mit Unterstützung für SCP/SFTP/FTP/S3. Im Grunde handelt es sich also um ein Terminal-Dienstprogramm mit einer TUI, um eine Verbindung zu einem Remote-Server herzustellen, um Dateien abzurufen und hochzuladen und mit dem lokalen Dateisystem zu interagieren. Es ist **Linux**, **MacOS**, **FreeBSD** und **Windows** kompatibel.
|
||||
Termscp ist ein funktionsreicher Terminal-Dateitransfer und Explorer mit Unterstützung für SCP/SFTP/FTP/Kube/S3/WebDAV. Im Grunde handelt es sich also um ein Terminal-Dienstprogramm mit einer TUI, um eine Verbindung zu einem Remote-Server herzustellen, um Dateien abzurufen und hochzuladen und mit dem lokalen Dateisystem zu interagieren. Es ist **Linux**, **MacOS**, **FreeBSD** und **Windows** kompatibel.
|
||||
|
||||

|
||||
|
||||
@@ -136,8 +144,10 @@ Termscp ist ein funktionsreicher Terminal-Dateitransfer und Explorer mit Unterst
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** und **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Erkunden und bedienen Sie das Dateisystem der Fernbedienung und des lokalen Computers mit einer praktischen Benutzeroberfläche
|
||||
- Erstellen, Entfernen, Umbenennen, Suchen, Anzeigen und Bearbeiten von Dateien
|
||||
- ⭐ Verbinden Sie sich über integrierte Lesezeichen und aktuelle Verbindungen mit Ihren Lieblingshosts
|
||||
@@ -257,7 +267,7 @@ termscp wird von diesen großartigen Projekten unterstützt:
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
1347
docs/de/man.md
1347
docs/de/man.md
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
<img src="/assets/images/termscp.svg" alt="logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ Una transferencia de archivos de terminal rica en funciones ~</p>
|
||||
@@ -21,6 +21,14 @@
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="/docs/de/README.md"
|
||||
><img
|
||||
@@ -62,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 0.12.3 (06/10/2023)</p>
|
||||
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -124,7 +132,7 @@
|
||||
|
||||
## Sobre termscp 🖥
|
||||
|
||||
Termscp es un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/S3. Básicamente, es una utilidad de terminal con una TUI para conectarse a un servidor remoto para recuperar y cargar archivos e interactuar con el sistema de archivos local. Es compatible con **Linux**, **MacOS**, **FreeBSD** y **Windows**.
|
||||
Termscp es un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV. Básicamente, es una utilidad de terminal con una TUI para conectarse a un servidor remoto para recuperar y cargar archivos e interactuar con el sistema de archivos local. Es compatible con **Linux**, **MacOS**, **FreeBSD** y **Windows**.
|
||||
|
||||

|
||||
|
||||
@@ -136,8 +144,10 @@ Termscp es un explorador y transferencia de archivos de terminal rico en funcion
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** y **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Explore y opere en el sistema de archivos de la máquina local y remota con una interfaz de usuario práctica
|
||||
- Cree, elimine, cambie el nombre, busque, vea y edite archivos
|
||||
- ⭐ Conéctese a sus hosts favoritos y conexiones recientes
|
||||
@@ -255,7 +265,7 @@ termscp funciona con estos increíbles proyectos:
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
- [Uso ❓](#uso-)
|
||||
- [Argumento dirección 🌎](#argumento-dirección-)
|
||||
- [Argumento dirección por AWS S3](#argumento-dirección-por-aws-s3)
|
||||
- [Argumento de dirección Kube](#argumento-de-dirección-kube)
|
||||
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
|
||||
- [Argumento dirección por SMB](#argumento-dirección-por-smb)
|
||||
- [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-)
|
||||
- [S3 parámetros de conexión](#s3-parámetros-de-conexión)
|
||||
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
||||
- [Explorador de archivos 📂](#explorador-de-archivos-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Trabaja en varios archivos 🥷](#trabaja-en-varios-archivos-)
|
||||
- [Trabajar con múltiples archivos 🥷](#trabajar-con-múltiples-archivos-)
|
||||
- [Ejemplo](#ejemplo)
|
||||
- [Navegación sincronizada ⏲️](#navegación-sincronizada-️)
|
||||
- [Abierta y abierta con 🚪](#abierta-y-abierta-con-)
|
||||
- [Marcadores ⭐](#marcadores-)
|
||||
@@ -37,18 +40,15 @@
|
||||
|
||||
termscp se puede iniciar con las siguientes opciones:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
OR
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` si se proporciona la dirección, la contraseña será este argumento
|
||||
- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador
|
||||
- `-c, --config` Abrir termscp comenzando desde la página de configuración
|
||||
- `-q, --quiet` Deshabilitar el registro
|
||||
- `-t, --theme <path>` Importar tema especificado
|
||||
- `-u, --update` Actualizar termscp a la última versión
|
||||
- `-v, --version` Imprimir información de la versión
|
||||
- `-h, --help` Imprimir página de ayuda
|
||||
|
||||
@@ -106,6 +106,28 @@ por ejemplo
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### Argumento de dirección Kube
|
||||
|
||||
En caso de que quieras conectarte a Kube, utiliza la siguiente sintaxis
|
||||
|
||||
```txt
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argumento de dirección de WebDAV
|
||||
|
||||
En caso de que quieras conectarte a WebDAV utiliza la siguiente sintaxis
|
||||
|
||||
```txt
|
||||
http://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
o en caso de que quieras usar https
|
||||
|
||||
```txt
|
||||
https://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
#### Argumento dirección por SMB
|
||||
|
||||
SMB tiene una sintaxis diferente para el argumento de la dirección CLI, que es diferente si está en Windows u otros sistemas:
|
||||
@@ -232,21 +254,40 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
|
||||
| `<X>` | Ejecutar un comando | eXecute |
|
||||
| `<Y>` | Alternar navegación sincronizada | sYnc |
|
||||
| `<Z>` | Cambiar ppermisos de archivo | |
|
||||
| `</>` | Filtrar archivos (se admite tanto regex como coincidencias con comodines) | |
|
||||
| `<CTRL+A>` | Seleccionar todos los archivos | |
|
||||
| `<ALT+A>` | Deseleccionar todos los archivos | |
|
||||
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
|
||||
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |
|
||||
|
||||
### Trabaja en varios archivos 🥷
|
||||
### Trabajar con múltiples archivos 🥷
|
||||
|
||||
Puede optar por trabajar en varios archivos, seleccionándolos presionando `<M>`, para seleccionar el archivo actual, o presionando `<CTRL + A>`, que seleccionará todos los archivos en el directorio de trabajo.
|
||||
Una vez que un archivo está marcado para su selección, se mostrará con un `*` a la izquierda.
|
||||
Al trabajar en la selección, solo se procesará el archivo seleccionado para las acciones, mientras que el elemento resaltado actual se ignorará.
|
||||
También es posible trabajar en varios archivos desde el panel de resultados de búsqueda.
|
||||
Todas las acciones están disponibles cuando se trabaja con varios archivos, pero tenga en cuenta que algunas acciones funcionan de forma ligeramente diferente. Vamos a sumergirnos en:
|
||||
Puedes optar por trabajar con varios archivos, usando estos controles:
|
||||
|
||||
- *Copy*: cada vez que copie un archivo, se le pedirá que inserte el nombre de destino. Cuando se trabaja con varios archivos, este nombre se refiere al directorio de destino donde se copiarán todos estos archivos.
|
||||
- *Rename*: igual que copiar, pero moverá archivos allí.
|
||||
- *Save as*: igual que copiar, pero los escribirá allí.
|
||||
- `<M>`: marcar un archivo para selección
|
||||
- `<CTRL+A>`: seleccionar todos los archivos del directorio actual
|
||||
- `<ALT+A>`: deseleccionar todos los archivos
|
||||
|
||||
Una vez marcado, el archivo será **mostrado con un fondo resaltado** .
|
||||
Cuando se trabaja con una selección, solo los archivos seleccionados serán procesados; el archivo resaltado actual será ignorado.
|
||||
|
||||
También se puede trabajar con múltiples archivos desde el panel de resultados de búsqueda.
|
||||
|
||||
Todas las acciones están disponibles con archivos múltiples, pero algunas funcionan de forma algo distinta. Veamos:
|
||||
|
||||
- *Copiar*: al copiar, se pedirá el nombre de destino. Para varios archivos, es el directorio donde se copiarán.
|
||||
- *Renombrar*: igual que copiar, pero mueve los archivos.
|
||||
- *Guardar como*: igual que copiar, pero escribe los archivos allí.
|
||||
|
||||
Si seleccionas un archivo en un directorio (ej. `/home`) y cambias de directorio, seguirá seleccionado y se mostrará en la **cola de transferencia** en el panel inferior.
|
||||
Cuando se selecciona un archivo, se asocia la carpeta *remota* actual con él; si se transfiere, será a esa carpeta.
|
||||
|
||||
#### Ejemplo
|
||||
|
||||
Si seleccionamos `/home/a.txt` localmente y estamos en `/tmp` en remoto, luego cambiamos a `/var`, seleccionamos `/var/b.txt` y estamos en `/home` en el panel remoto, el resultado de transferir será:
|
||||
|
||||
- `/home/a.txt` transferido a `/tmp/a.txt`
|
||||
- `/var/b.txt` transferido a `/home/b.txt`
|
||||
|
||||
### Navegación sincronizada ⏲️
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
<img src="/assets/images/termscp.svg" alt="logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ Un file transfer de terminal riche en fonctionnalités ~</p>
|
||||
@@ -21,6 +21,14 @@
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="/docs/de/README.md"
|
||||
><img
|
||||
@@ -62,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 0.12.3 (06/10/2023)</p>
|
||||
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -124,7 +132,7 @@
|
||||
|
||||
## À propos des termscp 🖥
|
||||
|
||||
Termscp est un file transfer et explorateur de fichiers de terminal riche en fonctionnalités, avec support pour SCP/SFTP/FTP/S3. Essentiellement c'est une utilitaire terminal avec une TUI pour se connecter à un serveur distant pour télécharger de fichiers et interagir avec le système de fichiers local. Il est compatible avec **Linux**, **MacOS**, **FreeBSD** et **Windows**.
|
||||
Termscp est un file transfer et explorateur de fichiers de terminal riche en fonctionnalités, avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV. Essentiellement c'est une utilitaire terminal avec une TUI pour se connecter à un serveur distant pour télécharger de fichiers et interagir avec le système de fichiers local. Il est compatible avec **Linux**, **MacOS**, **FreeBSD** et **Windows**.
|
||||
|
||||

|
||||
|
||||
@@ -136,8 +144,10 @@ Termscp est un file transfer et explorateur de fichiers de terminal riche en fon
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** et **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Explorer et opérer sur le système de fichiers distant et local avec une interface utilisateur pratique.
|
||||
- Créer, supprimer, renommer, rechercher, afficher et modifier des fichiers
|
||||
- ⭐ Connectez-vous à vos hôtes préférés via des signets et des connexions récentes.
|
||||
@@ -257,7 +267,7 @@ termscp est soutenu par ces projets impressionnants:
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
- [Usage ❓](#usage-)
|
||||
- [Argument d'adresse 🌎](#argument-dadresse-)
|
||||
- [Argument d'adresse AWS S3](#argument-dadresse-aws-s3)
|
||||
- [Argument d'adresse Kube](#argument-dadresse-kube)
|
||||
- [Argument d'adresse WebDAV](#argument-dadresse-webdav)
|
||||
- [Argument d'adresse SMB](#argument-dadresse-smb)
|
||||
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
|
||||
- [S3 paramètres de connexion](#s3-paramètres-de-connexion)
|
||||
@@ -11,6 +13,7 @@
|
||||
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
||||
- [Raccourcis clavier ⌨](#raccourcis-clavier-)
|
||||
- [Travailler sur plusieurs fichiers 🥷](#travailler-sur-plusieurs-fichiers-)
|
||||
- [Exemple](#exemple)
|
||||
- [Navigation synchronisée ⏲️](#navigation-synchronisée-️)
|
||||
- [Ouvrir et ouvrir avec 🚪](#ouvrir-et-ouvrir-avec-)
|
||||
- [Signets ⭐](#signets-)
|
||||
@@ -35,18 +38,15 @@
|
||||
|
||||
termscp peut être démarré avec les options suivantes :
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
ou
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` si l'adresse est fournie, le mot de passe sera cet argument
|
||||
- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet
|
||||
- `-c, --config` Ouvrir termscp à partir de la page de configuration
|
||||
- `-q, --quiet` Désactiver la journalisation
|
||||
- `-t, --theme <path>` Importer le thème spécifié
|
||||
- `-u, --update` Mettre à jour termscp vers la dernière version
|
||||
- `-v, --version` Imprimer les informations sur la version
|
||||
- `-h, --help` Imprimer la page d'aide
|
||||
|
||||
@@ -104,6 +104,28 @@ e.g.
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### Argument d'adresse Kube
|
||||
|
||||
Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante
|
||||
|
||||
```txt
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argument d'adresse WebDAV
|
||||
|
||||
Dans le cas où vous souhaitez vous connecter à WebDAV, utilisez la syntaxe suivante
|
||||
|
||||
```txt
|
||||
http://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
ou dans le cas où vous souhaitez utiliser https
|
||||
|
||||
```txt
|
||||
https://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
#### Argument d'adresse SMB
|
||||
|
||||
SMB a une syntaxe différente pour l'argument d'adresse CLI, qui est différente que vous soyez sur Windows ou sur d'autres systèmes :
|
||||
@@ -231,21 +253,40 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
|
||||
| `<X>` | Exécuter une commande | eXecute |
|
||||
| `<Y>` | Basculer la navigation synchronisée | sYnc |
|
||||
| `<Z>` | Changer permissions de fichier | |
|
||||
| `</>` | Filtrer les fichiers (les expressions régulières et les correspondances génériques sont prises en charge) | |
|
||||
| `<CTRL+A>` | Sélectionner tous les fichiers | |
|
||||
| `<ALT+A>` | Desélectionner tous les fichiers | |
|
||||
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
|
||||
| `<CTRL+T>` | Afficher tous les chemins synchronisés | Track |
|
||||
|
||||
### Travailler sur plusieurs fichiers 🥷
|
||||
### Travailler sur plusieurs fichiers 🥷
|
||||
|
||||
Vous pouvez choisir de travailler sur plusieurs fichiers, en les sélectionnant en appuyant sur `<M>`, afin de sélectionner le fichier actuel, ou en appuyant sur `<CTRL+A>`, ce qui sélectionnera tous les fichiers dans le répertoire de travail.
|
||||
Une fois qu'un fichier est marqué pour la sélection, il sera affiché avec un `*` sur la gauche.
|
||||
Lorsque vous travaillez sur la sélection, seul le fichier sélectionné sera traité pour les actions, tandis que l'élément en surbrillance actuel sera ignoré.
|
||||
Il est également possible de travailler sur plusieurs fichiers dans le panneau des résultats de recherche.
|
||||
Toutes les actions sont disponibles lorsque vous travaillez avec plusieurs fichiers, mais sachez que certaines actions fonctionnent de manière légèrement différente. Plongeons dans:
|
||||
Vous pouvez choisir de travailler sur plusieurs fichiers avec ces simples commandes :
|
||||
|
||||
- *Copy*: chaque fois que vous copiez un fichier, vous serez invité à insérer le nom de destination. Lorsque vous travaillez avec plusieurs fichiers, ce nom fait référence au répertoire de destination où tous ces fichiers seront copiés.
|
||||
- *Rename*: identique à la copie, mais y déplacera les fichiers.
|
||||
- *Save as*: identique à la copie, mais les y écrira.
|
||||
- `<M>` : marquer un fichier à sélectionner
|
||||
- `<CTRL+A>` : sélectionner tous les fichiers du répertoire actuel
|
||||
- `<ALT+A>` : désélectionner tous les fichiers
|
||||
|
||||
Une fois sélectionné, un fichier sera **affiché avec un fond en surbrillance** .
|
||||
Lorsqu’on travaille avec des sélections, seules les fichiers sélectionnés seront affectés par les actions, tandis que l'élément actuellement surligné sera ignoré.
|
||||
|
||||
Il est également possible de travailler avec plusieurs fichiers depuis le panneau des résultats de recherche.
|
||||
|
||||
Toutes les actions sont disponibles avec des fichiers multiples, mais certaines peuvent se comporter différemment. Détails :
|
||||
|
||||
- *Copier* : lors de la copie, il vous sera demandé un nom de destination. Avec plusieurs fichiers, cela correspond au dossier de destination.
|
||||
- *Renommer* : identique à la copie, mais déplace les fichiers.
|
||||
- *Enregistrer sous* : identique à la copie, mais enregistre les fichiers à cet emplacement.
|
||||
|
||||
Si vous sélectionnez un fichier dans un dossier (ex. `/home`) puis changez de répertoire, il restera sélectionné et sera affiché dans la **file d’attente de transfert** en bas.
|
||||
Lorsqu’un fichier est sélectionné, le dossier *distant* courant lui est associé ; en cas de transfert, il sera envoyé vers ce dossier.
|
||||
|
||||
#### Exemple
|
||||
|
||||
Si on sélectionne `/home/a.txt` localement et que le panneau distant est sur `/tmp`, puis on passe à `/var`, on sélectionne `/var/b.txt` et que le panneau distant est sur `/home`, le transfert donnera :
|
||||
|
||||
- `/home/a.txt` transféré vers `/tmp/a.txt`
|
||||
- `/var/b.txt` transféré vers `/home/b.txt`
|
||||
|
||||
### Navigation synchronisée ⏲️
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
<img src="/assets/images/termscp.svg" alt="logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ Un file transfer ricco di funzionalità ~</p>
|
||||
@@ -21,6 +21,14 @@
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/de/README.md"
|
||||
><img
|
||||
@@ -62,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 0.12.3 (06/10/2023)</p>
|
||||
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -124,7 +132,7 @@
|
||||
|
||||
## Riguardo a termscp 🖥
|
||||
|
||||
Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a SCP/SFTP/FTP/S3. In pratica è un utility su terminale con una terminal user-interface per connettersi a server remoti per scambiare file ed interagire con il file system sia locale che remoto. È compatibile con **Linux**, **MacOS**, **FreeBSD** e **Windows**.
|
||||
Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a SCP/SFTP/FTP/Kube/S3/WebDAV. In pratica è un utility su terminale con una terminal user-interface per connettersi a server remoti per scambiare file ed interagire con il file system sia locale che remoto. È compatibile con **Linux**, **MacOS**, **FreeBSD** e **Windows**.
|
||||
|
||||

|
||||
|
||||
@@ -136,8 +144,10 @@ Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a S
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** and **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Esplora e opera sia sul file system locale che su quello remoto con una UI di facile utilizzo.
|
||||
- Crea, rimuove, rinomina, cerca, visualizza e modifica file
|
||||
- ⭐ Connettiti ai tuoi host preferiti tramite la funzionalità integrata dei segnalibri e delle connessioni recenti.
|
||||
@@ -255,7 +265,7 @@ se termscp esiste, è anche grazie a questi fantastici progetti:
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
- [Argomenti da linea di comando ❓](#argomenti-da-linea-di-comando-)
|
||||
- [Argomento indirizzo 🌎](#argomento-indirizzo-)
|
||||
- [Argomento indirizzo per AWS S3](#argomento-indirizzo-per-aws-s3)
|
||||
- [Argomento indirizzo Kube](#argomento-indirizzo-kube)
|
||||
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
|
||||
- [Indirizzo SMB](#indirizzo-smb)
|
||||
- [Come fornire la password 🔐](#come-fornire-la-password-)
|
||||
- [Parametri di connessione S3](#parametri-di-connessione-s3)
|
||||
- [Credenziali S3 🦊](#credenziali-s3-)
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
- [Abbinamento tasti ⌨](#abbinamento-tasti-)
|
||||
- [Lavora su più file 🥷](#lavora-su-più-file-)
|
||||
- [Lavora con più file 🥷](#lavora-con-più-file-)
|
||||
- [Esempio](#esempio)
|
||||
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||
- [Apri e apri con 🚪](#apri-e-apri-con-)
|
||||
- [Segnalibri ⭐](#segnalibri-)
|
||||
@@ -35,18 +38,15 @@
|
||||
|
||||
termscp può essere lanciato con questi argomenti:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
O
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi
|
||||
- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro
|
||||
- `-c, --config` Apri la configurazione di termscp
|
||||
- `-q, --quiet` Disabilita i log
|
||||
- `-t, --theme <path>` Importa il tema al percorso fornito
|
||||
- `-u, --update` Aggiorna termscp all'ultima versione
|
||||
- `-v, --version` Mostra a video le informazioni sulla versione attualmente installata
|
||||
- `-h, --help` Mostra la pagina di aiuto.
|
||||
|
||||
@@ -102,6 +102,28 @@ e.g.
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### Argomento indirizzo Kube
|
||||
|
||||
Nel caso tu voglia connetterti a Kube usa la seguente sintassi
|
||||
|
||||
```txt
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argomento indirizzo per WebDAV
|
||||
|
||||
Nel caso in cui si desideri connettersi a WebDAV utilizzare la seguente sintassi
|
||||
|
||||
```txt
|
||||
http://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
oppure nel caso in cui si desideri utilizzare https
|
||||
|
||||
```txt
|
||||
https://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
#### Indirizzo SMB
|
||||
|
||||
SMB ha una sintassi differente rispetto agli altri protocolli e cambia in base al sistema operativo:
|
||||
@@ -227,21 +249,40 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
|
||||
| `<X>` | Esegui comando shell | eXecute |
|
||||
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
|
||||
| `<Z>` | Modifica permessi file | |
|
||||
| `</>` | Filtra i file (supporta sia regex che wildmatch ) | |
|
||||
| `<CTRL+A>` | Seleziona tutti i file | |
|
||||
| `<ALT+A>` | Deseleziona tutti i file | |
|
||||
| `<CTRL+C>` | Annulla trasferimento file | |
|
||||
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
|
||||
|
||||
### Lavora su più file 🥷
|
||||
### Lavora con più file 🥷
|
||||
|
||||
Puoi lavorare su una selezione di file, marcandoli come selezionati tramite `<M>`, per selezionare il file corrente o con `<CTRL+A` per selezionarli tutti.
|
||||
Una volta che un file è marcato, sarà visualizzato con un `*` prima del nome.
|
||||
Quando lavori con una selezioni, solo i file selezionati saranno presi in considerazione (l'eventuale file evidenziato sarà ignorato).
|
||||
È possibile operare su più file anche nel pannello di ricerca.
|
||||
Tutte le azioni sono disponibili quando si lavora sulle selezioni, ma occhio, che alcune azioni si comporteranno in maniera leggermente differente. Vediamo quali e come:
|
||||
Puoi scegliere di lavorare con più file, usando questi semplici comandi:
|
||||
|
||||
- *Copia*: Se copi un file, ti verrà richiesto di inserire il nome delle destinazione, ma quando lavori con la selezione, il nome si riferisce alla directory di destinazione, mentre il nome del file rimarrà inviariato.
|
||||
- *Rinomina*: Come il copia, ma li sposterà.
|
||||
- *Salva con nome*: Come il copia, ma li trasferirà.
|
||||
- `<M>`: marca un file per la selezione
|
||||
- `<CTRL+A>`: seleziona tutti i file nella directory corrente
|
||||
- `<ALT+A>`: deseleziona tutti i file
|
||||
|
||||
Una volta che un file è stato selezionato, verrà **evidenziato con uno sfondo colorato** .
|
||||
Quando lavori su una selezione, solo i file selezionati verranno processati per le azioni, mentre l'elemento attualmente evidenziato sarà ignorato.
|
||||
|
||||
È possibile lavorare con più file anche dal pannello dei risultati di ricerca.
|
||||
|
||||
Tutte le azioni sono disponibili anche quando si lavora con più file, ma alcune funzionano in modo leggermente diverso. Ecco i dettagli:
|
||||
|
||||
- *Copia*: quando copi un file, ti verrà chiesto di inserire il nome di destinazione. Con più file selezionati, questo nome rappresenta la cartella di destinazione dove verranno copiati.
|
||||
- *Rinomina*: come la copia, ma i file verranno spostati lì.
|
||||
- *Salva come*: come la copia, ma i file verranno salvati lì.
|
||||
|
||||
Se selezioni un file in una directory (es. `/home`) e poi cambi directory, il file rimarrà selezionato e sarà visibile nella **coda di trasferimento** nel pannello inferiore.
|
||||
Quando un file viene selezionato, la directory *remota* corrente viene associata all’elemento; quindi, se il file viene trasferito, verrà trasferito nella directory associata.
|
||||
|
||||
#### Esempio
|
||||
|
||||
Se selezioniamo un file locale `/home/a.txt`, siamo su `/tmp` nel pannello remoto, poi ci spostiamo su `/var`, selezioniamo `/var/b.txt`, e sul pannello remoto siamo su `/home`, eseguendo il trasferimento otterremo:
|
||||
|
||||
- `/home/a.txt` trasferito su `/tmp/a.txt`
|
||||
- `/var/b.txt` trasferito su `/home/b.txt`
|
||||
|
||||
### Synchronized browsing ⏲️
|
||||
|
||||
|
||||
93
docs/man.md
93
docs/man.md
@@ -4,13 +4,19 @@
|
||||
- [Usage ❓](#usage-)
|
||||
- [Address argument 🌎](#address-argument-)
|
||||
- [AWS S3 address argument](#aws-s3-address-argument)
|
||||
- [Kube address argument](#kube-address-argument)
|
||||
- [WebDAV address argument](#webdav-address-argument)
|
||||
- [SMB address argument](#smb-address-argument)
|
||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||
- [Subcommands](#subcommands)
|
||||
- [Import a theme](#import-a-theme)
|
||||
- [Install latest version](#install-latest-version)
|
||||
- [S3 connection parameters](#s3-connection-parameters)
|
||||
- [S3 credentials 🦊](#s3-credentials-)
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Work on multiple files 🥷](#work-on-multiple-files-)
|
||||
- [Example](#example)
|
||||
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||
- [Open and Open With 🚪](#open-and-open-with-)
|
||||
- [Bookmarks ⭐](#bookmarks-)
|
||||
@@ -35,18 +41,17 @@
|
||||
|
||||
termscp can be started with the following options:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
OR
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument
|
||||
AND any combination of the two
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument. A password *can* be specified for each remote provided. The order must be the same of the address argument. The use of this parameter is discouraged.
|
||||
- `-b, --address-as-bookmark` resolve address argument as a bookmark name
|
||||
- `-c, --config` Open termscp starting from the configuration page
|
||||
- `-q, --quiet` Disable logging
|
||||
- `-t, --theme <path>` Import specified theme
|
||||
- `-u, --update` Update termscp to latest version
|
||||
- `-v, --version` Print version info
|
||||
- `-h, --help` Print help page
|
||||
|
||||
@@ -104,6 +109,28 @@ e.g.
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### Kube address argument
|
||||
|
||||
In case you want to connect to Kube use the following syntax
|
||||
|
||||
```txt
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### WebDAV address argument
|
||||
|
||||
In case you want to connect to webDAV use the following syntax
|
||||
|
||||
```txt
|
||||
http://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
or in case you want to use https
|
||||
|
||||
```txt
|
||||
https://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
#### SMB address argument
|
||||
|
||||
SMB has a different syntax for CLI address argument, which is different whether you're on Windows or other systems:
|
||||
@@ -129,6 +156,16 @@ Password can be basically provided through 3 ways when address argument is provi
|
||||
- Via `sshpass`: you can provide password via `sshpass`, e.g. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
|
||||
- You will be prompted for it: if you don't use any of the previous methods, you will be prompted for the password, as happens with the more classics tools such as `scp`, `ssh`, etc.
|
||||
|
||||
### Subcommands
|
||||
|
||||
#### Import a theme
|
||||
|
||||
Run termscp as `termscp theme <theme-file>`
|
||||
|
||||
#### Install latest version
|
||||
|
||||
Run termscp as `termscp update`
|
||||
|
||||
---
|
||||
|
||||
## S3 connection parameters
|
||||
@@ -207,45 +244,65 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
| `<BACKTAB>` | Switch between log tab and explorer | |
|
||||
| `<A>` | Toggle hidden files | All |
|
||||
| `<B>` | Sort files by | Bubblesort? |
|
||||
| `<C|F5>` | Copy file/directory | Copy |
|
||||
| `<D|F7>` | Make directory | Directory |
|
||||
| `<E|F8|DEL>` | Delete file | Erase |
|
||||
| `<C\|F5>` | Copy file/directory | Copy |
|
||||
| `<D\|F7>` | Make directory | Directory |
|
||||
| `<E\|F8\|DEL>`| Delete file | Erase |
|
||||
| `<F>` | Search for files (wild match is supported) | Find |
|
||||
| `<G>` | Go to supplied path | Go to |
|
||||
| `<H|F1>` | Show help | Help |
|
||||
| `<H\|F1>` | Show help | Help |
|
||||
| `<I>` | Show info about selected file or directory | Info |
|
||||
| `<K>` | Create symlink pointing to the currently selected entry | symlinK |
|
||||
| `<L>` | Reload current directory's content / Clear selection | List |
|
||||
| `<M>` | Select a file | Mark |
|
||||
| `<N>` | Create new file with provided name | New |
|
||||
| `<O|F4>` | Edit file; see Text editor | Open |
|
||||
| `<O\|F4>` | Edit file; see Text editor | Open |
|
||||
| `<P>` | Open log panel | Panel |
|
||||
| `<Q|F10>` | Quit termscp | Quit |
|
||||
| `<R|F6>` | Rename file | Rename |
|
||||
| `<S|F2>` | Save file as... | Save |
|
||||
| `<Q\|F10>` | Quit termscp | Quit |
|
||||
| `<R\|F6>` | Rename file | Rename |
|
||||
| `<S\|F2>` | Save file as... | Save |
|
||||
| `<T>` | Synchronize changes to selected path to remote | Track |
|
||||
| `<U>` | Go to parent directory | Up |
|
||||
| `<V|F3>` | Open file with default program for filetype | View |
|
||||
| `<V\|F3>` | Open file with default program for filetype | View |
|
||||
| `<W>` | Open file with provided program | With |
|
||||
| `<X>` | Execute a command | eXecute |
|
||||
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||
| `<Z>` | Change file mode | |
|
||||
| `<Z>` | Change file mode | |
|
||||
| `</>` | Filter files (both regex and wildmatch is supported) | |
|
||||
| `<CTRL+A>` | Select all files | |
|
||||
| `<ALT+A>` | Deselect all files | |
|
||||
| `<CTRL+C>` | Abort file transfer process | |
|
||||
| `<CTRL+T>` | Show all synchronized paths | Track |
|
||||
|
||||
### Work on multiple files 🥷
|
||||
|
||||
You can opt to work on multiple files, selecting them pressing `<M>`, in order to select the current file, or pressing `<CTRL+A>`, which will select all the files in the working directory.
|
||||
Once a file is marked for selection, it will be displayed with a `*` on the left.
|
||||
You can opt to work on multiple files, with these simple controls:
|
||||
|
||||
- `<M>`: mark a file for selection
|
||||
- `<CTRL+A>`: select all files in the current directory
|
||||
- `<ALT+A>`: deselect all files
|
||||
|
||||
Once a file is marked for selection, it will be **displayed with an highlighted background**.
|
||||
|
||||
When working on selection, only selected file will be processed for actions, while the current highlighted item will be ignored.
|
||||
It is possible to work on multiple files also when in the find result panel.
|
||||
|
||||
All the actions are available when working with multiple files, but be aware that some actions work in a slightly different way. Let's dive in:
|
||||
|
||||
- *Copy*: whenever you copy a file, you'll be prompted to insert the destination name. When working with multiple file, this name refers to the destination directory where all these files will be copied.
|
||||
- *Rename*: same as copy, but will move files there.
|
||||
- *Save as*: same as copy, but will write them there.
|
||||
|
||||
If you select a file in a directory (e.g. `/home`) and then you change directory the file will be kept selected and it will be displayed in the **transfer queue** in the bottom panel.
|
||||
|
||||
When a file gets selected the current *remote* directory is associated to its entry; so in case the file gets transferred it will be transferred to the directory associated to the file.
|
||||
|
||||
#### Example
|
||||
|
||||
If we select a file on local `/home/a.txt` and we're currently at `/tmp` on remote and then we move to `/var` and we select `/var/b.txt` and on the remote panel we're at `/home` and we perform a transfer the result will be:
|
||||
|
||||
- `/home/a.txt` transferred to `/tmp/a.txt`
|
||||
- `/var/b.txt` transferred to `/home/b.txt`
|
||||
|
||||
### Synchronized browsing ⏲️
|
||||
|
||||
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3.
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV.
|
||||
Basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and
|
||||
to interact with the local file system.
|
||||
|
||||
|
||||
317
docs/pt-BR/README.md
Normal file
317
docs/pt-BR/README.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" alt="termscp logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ Uma transferência de arquivos de terminal rica em recursos ~</p>
|
||||
<p align="center">
|
||||
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
|
||||
·
|
||||
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Instalação</a>
|
||||
·
|
||||
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manual do usuário</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/veeso/termscp"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/gb.png"
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/de/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/de.png"
|
||||
alt="Deutsch"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/es/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/es.png"
|
||||
alt="Español"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/fr/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/fr.png"
|
||||
alt="Français"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/it/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/it.png"
|
||||
alt="Italiano"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/zh-CN/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/cn.png"
|
||||
alt="简体中文"
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versão atual: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
><img
|
||||
src="https://img.shields.io/badge/License-MIT-teal.svg"
|
||||
alt="License-MIT"
|
||||
/></a>
|
||||
<a href="https://github.com/veeso/termscp/stargazers"
|
||||
><img
|
||||
src="https://img.shields.io/github/stars/veeso/termscp?style=flat"
|
||||
alt="Repo stars"
|
||||
/></a>
|
||||
<a href="https://crates.io/crates/termscp"
|
||||
><img
|
||||
src="https://img.shields.io/crates/d/termscp.svg"
|
||||
alt="Downloads counter"
|
||||
/></a>
|
||||
<a href="https://crates.io/crates/termscp"
|
||||
><img
|
||||
src="https://img.shields.io/crates/v/termscp.svg"
|
||||
alt="Latest version"
|
||||
/></a>
|
||||
<a href="https://ko-fi.com/veeso">
|
||||
<img
|
||||
src="https://img.shields.io/badge/donate-ko--fi-red"
|
||||
alt="Ko-fi"
|
||||
/></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/veeso/termscp/actions"
|
||||
><img
|
||||
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
|
||||
alt="Linux CI"
|
||||
/></a>
|
||||
<a href="https://github.com/veeso/termscp/actions"
|
||||
><img
|
||||
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
|
||||
alt="MacOS CI"
|
||||
/></a>
|
||||
<a href="https://github.com/veeso/termscp/actions"
|
||||
><img
|
||||
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
|
||||
alt="Windows CI"
|
||||
/></a>
|
||||
<a href="https://coveralls.io/github/veeso/termscp"
|
||||
><img
|
||||
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
|
||||
alt="Coveralls"
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Sobre o termscp 🖥
|
||||
|
||||
Termscp é um explorador e utilitário de transferência de arquivos com uma interface de terminal, com suporte para SCP/SFTP/FTP/Kube/S3/WebDAV. Basicamente, é uma ferramenta de terminal com uma interface de usuário para conectar-se a um servidor remoto para baixar e enviar arquivos e interagir com o sistema de arquivos local. Ele é compatível com **Linux**, **MacOS**, **FreeBSD**, **NetBSD** e **Windows**.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Recursos 🎁
|
||||
|
||||
- 📁 Diferentes protocolos de comunicação
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** e **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Explore e opere no sistema de arquivos remoto e local com uma interface amigável
|
||||
- Crie, remova, renomeie, pesquise, visualize e edite arquivos
|
||||
- ⭐ Conecte-se aos seus hosts favoritos por meio de marcadores integrados e conexões recentes
|
||||
- 📝 Veja e edite arquivos com suas aplicações favoritas
|
||||
- 💁 Autenticação SFTP/SCP com chaves SSH e nome de usuário/senha
|
||||
- 🐧 Compatível com Windows, Linux, FreeBSD, NetBSD e MacOS
|
||||
- 🎨 Personalize do seu jeito!
|
||||
- Temas
|
||||
- Formato de explorador de arquivos customizável
|
||||
- Editor de texto personalizável
|
||||
- Ordenação de arquivos customizável
|
||||
- e muitos outros parâmetros...
|
||||
- 📫 Receba notificações no Desktop quando um arquivo grande for transferido
|
||||
- 🔭 Mantenha as alterações de arquivos sincronizadas com o host remoto
|
||||
- 🔐 Salve sua senha no cofre de senhas do sistema operacional
|
||||
- 🦀 Feito em Rust
|
||||
- 👀 Desenvolvido com foco em desempenho
|
||||
- 🦄 Atualizações frequentes e incríveis
|
||||
|
||||
---
|
||||
|
||||
## Como começar 🚀
|
||||
|
||||
Se você está pensando em instalar o termscp, eu quero te agradecer 💜 ! Espero que você goste do termscp!
|
||||
Se você quiser contribuir para este projeto, não se esqueça de verificar nosso [guia de contribuição](CONTRIBUTING.md).
|
||||
|
||||
Se você é um usuário de Linux, FreeBSD ou MacOS, este simples script de shell instalará o termscp no seu sistema com um único comando:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
|
||||
```
|
||||
|
||||
> ❗ A instalação no MacOS requer [Homebrew](https://brew.sh/), caso contrário, o compilador Rust será instalado.
|
||||
|
||||
Se você é um usuário de Windows, pode instalar o termscp com [Chocolatey](https://chocolatey.org/):
|
||||
|
||||
```ps
|
||||
choco install termscp
|
||||
```
|
||||
|
||||
Usuários do NetBSD podem instalar o termscp pelos repositórios oficiais.
|
||||
|
||||
```sh
|
||||
pkgin install termscp
|
||||
```
|
||||
|
||||
Usuários do Arch Linux podem instalar o termscp pelos repositórios oficiais.
|
||||
|
||||
```sh
|
||||
pacman -S termscp
|
||||
```
|
||||
|
||||
Para mais informações ou outras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/#get-started) para ver todos os métodos de instalação.
|
||||
|
||||
⚠️ Se você quer saber como atualizar o termscp, basta executar o termscp a partir do CLI com: `(sudo) termscp --update` ⚠️
|
||||
|
||||
### Requisitos ❗
|
||||
|
||||
- Para usuários de **Linux**:
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
- libsmbclient
|
||||
- Para usuários de **FreeBSD** ou **NetBSD**:
|
||||
- dbus
|
||||
- pkgconf
|
||||
- libsmbclient
|
||||
|
||||
### Requisitos Opcionais ✔️
|
||||
|
||||
Estes requisitos não são obrigatórios para rodar o termscp, mas para aproveitar todos os seus recursos.
|
||||
|
||||
- Para usuários de **Linux/FreeBSD**:
|
||||
- Para **abrir** arquivos via `V` (pelo menos um dos seguintes)
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- Para usuários de **Linux**:
|
||||
- Um gerenciador de chaves: leia mais no [Manual do Usuário](docs/man.md#linux-keyring)
|
||||
- Para usuários do **WSL**
|
||||
- Para **abrir** arquivos via `V` (pelo menos um dos seguintes)
|
||||
- [wslu](https://github.com/wslutilities/wslu)
|
||||
|
||||
---
|
||||
|
||||
## Apoie o desenvolvedor ☕
|
||||
|
||||
Se você gosta do termscp e está grato pelo trabalho que fiz, considere uma pequena doação 🥳
|
||||
|
||||
Você pode fazer uma doação por meio de uma dessas plataformas:
|
||||
|
||||
[](https://ko-fi.com/veeso)
|
||||
[](https://www.paypal.me/chrisintin)
|
||||
|
||||
---
|
||||
|
||||
## Manual do Usuário 📚
|
||||
|
||||
O manual do usuário pode ser encontrado no [site do termscp](https://termscp.veeso.dev/#user-manual) ou no [Github](docs/man.md).
|
||||
|
||||
---
|
||||
|
||||
## Próximos Recursos 🧪
|
||||
|
||||
Para **2023**, haverá duas grandes atualizações durante o ano.
|
||||
|
||||
Além de novos recursos, o desenvolvimento do termscp agora está focado em melhorias de UX e desempenho, então, se você tiver alguma sugestão, sinta-se à vontade para abrir um problema.
|
||||
|
||||
---
|
||||
|
||||
## Contribuições e problemas 🤝🏻
|
||||
|
||||
Contribuições, relatos de bugs, novos recursos e perguntas são bem-vindos! 😉
|
||||
Se você tiver alguma pergunta ou preocupação, ou se quiser sugerir um novo recurso, ou apenas melhorar o termscp, sinta-se à vontade para abrir um problema ou um PR.
|
||||
|
||||
Uma contribuição **apreciada** seria a tradução do manual do usuário e do README para **outros idiomas**.
|
||||
|
||||
Por favor, siga [nosso guia de contribuição](CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
## Mudanças ⏳
|
||||
|
||||
Veja o changelog do termscp [AQUI](CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
## Impulsionado por 💪
|
||||
|
||||
O termscp é impulsionado por esses projetos incríveis:
|
||||
|
||||
- [bytesize](https://github.com/hyunsik/bytesize)
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [edit](https://github.com/milkey-mouse/edit)
|
||||
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
||||
- [open-rs](https://github.com/Byron/open-rs)
|
||||
- [pavao](https://github.com/veeso/pavao)
|
||||
- [remotefs](https://github.com/veeso/remotefs-rs)
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
---
|
||||
|
||||
## Galeria 🎬
|
||||
|
||||
> Termscp Home
|
||||
|
||||

|
||||
|
||||
> Marcadores
|
||||
|
||||

|
||||
|
||||
> Configuração
|
||||
|
||||

|
||||
|
||||
> Editor de Texto
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Licença 📃
|
||||
|
||||
O termscp é licenciado sob a licença MIT.
|
||||
|
||||
Você pode ler a licença completa [AQUI](LICENSE).
|
||||
613
docs/pt-BR/man.md
Normal file
613
docs/pt-BR/man.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# Manual do Usuário 🎓
|
||||
|
||||
- [Manual do Usuário 🎓](#manual-do-usuário-)
|
||||
- [Uso ❓](#uso-)
|
||||
- [Argumento de Endereço 🌎](#argumento-de-endereço-)
|
||||
- [Argumento de Endereço do AWS S3](#argumento-de-endereço-do-aws-s3)
|
||||
- [Argumento de endereço Kube](#argumento-de-endereço-kube)
|
||||
- [Argumento de Endereço do WebDAV](#argumento-de-endereço-do-webdav)
|
||||
- [Argumento de Endereço do SMB](#argumento-de-endereço-do-smb)
|
||||
- [Como a Senha Pode Ser Fornecida 🔐](#como-a-senha-pode-ser-fornecida-)
|
||||
- [Subcomandos](#subcomandos)
|
||||
- [Importar um Tema](#importar-um-tema)
|
||||
- [Instalar a Última Versão](#instalar-a-última-versão)
|
||||
- [Parâmetros de Conexão do S3](#parâmetros-de-conexão-do-s3)
|
||||
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
||||
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
||||
- [Atalhos de Teclado ⌨](#atalhos-de-teclado-)
|
||||
- [Trabalhar com múltiplos arquivos 🥷](#trabalhar-com-múltiplos-arquivos-)
|
||||
- [Exemplo](#exemplo)
|
||||
- [Navegação Sincronizada ⏲️](#navegação-sincronizada-️)
|
||||
- [Abrir e Abrir Com 🚪](#abrir-e-abrir-com-)
|
||||
- [Favoritos ⭐](#favoritos-)
|
||||
- [Minhas Senhas São Seguras? 😈](#minhas-senhas-são-seguras-)
|
||||
- [Keyring do Linux](#keyring-do-linux)
|
||||
- [Configuração do KeepassXC para o termscp](#configuração-do-keepassxc-para-o-termscp)
|
||||
- [Configuração ⚙️](#configuração-️)
|
||||
- [Armazenamento de Chave SSH 🔐](#armazenamento-de-chave-ssh-)
|
||||
- [Formato do Explorador de Arquivos](#formato-do-explorador-de-arquivos)
|
||||
- [Temas 🎨](#temas-)
|
||||
- [Meu Tema Não Carrega 😱](#meu-tema-não-carrega-)
|
||||
- [Estilos 💈](#estilos-)
|
||||
- [Página de Autenticação](#página-de-autenticação)
|
||||
- [Página de Transferência](#página-de-transferência)
|
||||
- [Diversos](#diversos)
|
||||
- [Editor de Texto ✏](#editor-de-texto-)
|
||||
- [Registro de Logs 🩺](#registro-de-logs-)
|
||||
- [Notificações 📫](#notificações-)
|
||||
- [Observador de Arquivos 🔭](#observador-de-arquivos-)
|
||||
|
||||
## Uso ❓
|
||||
|
||||
O termscp pode ser iniciado com as seguintes opções:
|
||||
|
||||
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
|
||||
|
||||
OU
|
||||
|
||||
`termscp [opções]... -b [nome-do-favorito] -b [nome-do-favorito] [diretório-trabalho-local]`
|
||||
|
||||
- `-P, --password <senha>` se o endereço for fornecido, a senha será este argumento
|
||||
- `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito
|
||||
- `-q, --quiet` Desabilita o registro de logs
|
||||
- `-v, --version` Exibe informações da versão
|
||||
- `-h, --help` Exibe a página de ajuda
|
||||
|
||||
O termscp pode ser iniciado em três modos diferentes, se nenhum argumento adicional for fornecido, ele exibirá o formulário de autenticação, onde o usuário poderá fornecer os parâmetros necessários para se conectar ao peer remoto.
|
||||
|
||||
Alternativamente, o usuário pode fornecer um endereço como argumento para pular o formulário de autenticação e iniciar diretamente a conexão com o servidor remoto.
|
||||
|
||||
Se um argumento de endereço ou nome de favorito for fornecido, você também pode definir o diretório de trabalho para o host local.
|
||||
|
||||
### Argumento de Endereço 🌎
|
||||
|
||||
O argumento de endereço tem a seguinte sintaxe:
|
||||
|
||||
```txt
|
||||
[protocol://][username@]<address>[:port][:wrkdir]
|
||||
```
|
||||
|
||||
Vamos ver alguns exemplos dessa sintaxe particular, pois ela é bem conveniente e você provavelmente a usará com mais frequência do que a outra...
|
||||
|
||||
- Conectar usando o protocolo padrão (*definido na configuração*) a 192.168.1.31; a porta, se não for fornecida, será a padrão para o protocolo selecionado (dependerá da sua configuração); o nome de usuário será o do usuário atual
|
||||
|
||||
```sh
|
||||
termscp 192.168.1.31
|
||||
```
|
||||
|
||||
- Conectar usando o protocolo padrão (*definido na configuração*) a 192.168.1.31; o nome de usuário é `root`
|
||||
|
||||
```sh
|
||||
termscp root@192.168.1.31
|
||||
```
|
||||
|
||||
- Conectar usando scp a 192.168.1.31, a porta é 4022; o nome de usuário é `omar`
|
||||
|
||||
```sh
|
||||
termscp scp://omar@192.168.1.31:4022
|
||||
```
|
||||
|
||||
- Conectar usando scp a 192.168.1.31, a porta é 4022; o nome de usuário é `omar`. Você começará no diretório `/tmp`
|
||||
|
||||
```sh
|
||||
termscp scp://omar@192.168.1.31:4022:/tmp
|
||||
```
|
||||
|
||||
#### Argumento de Endereço do AWS S3
|
||||
|
||||
O AWS S3 tem uma sintaxe diferente para o argumento de endereço CLI, por razões óbvias, mas tentei mantê-la o mais próxima possível do argumento de endereço genérico:
|
||||
|
||||
```txt
|
||||
s3://<bucket-name>@<region>[:profile][:/wrkdir]
|
||||
```
|
||||
|
||||
Exemplo:
|
||||
|
||||
```txt
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### Argumento de endereço Kube
|
||||
|
||||
Caso queira se conectar ao Kube, use a seguinte sintaxe
|
||||
|
||||
```txt
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### Argumento de Endereço do WebDAV
|
||||
|
||||
Caso você queira se conectar ao WebDAV, use a seguinte sintaxe:
|
||||
|
||||
```txt
|
||||
http://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
ou, se preferir usar https:
|
||||
|
||||
```txt
|
||||
https://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
#### Argumento de Endereço do SMB
|
||||
|
||||
O SMB tem uma sintaxe diferente para o argumento de endereço CLI, que varia se você estiver no Windows ou em outros sistemas:
|
||||
|
||||
**Sintaxe do Windows:**
|
||||
|
||||
```txt
|
||||
\\[username@]<server-name>\<share>[\path\...]
|
||||
```
|
||||
|
||||
**Sintaxe de outros sistemas:**
|
||||
|
||||
```txt
|
||||
smb://[username@]<server-name>[:port]/<share>[/path/.../]
|
||||
```
|
||||
|
||||
#### Como a Senha Pode Ser Fornecida 🔐
|
||||
|
||||
Você provavelmente notou que, ao fornecer o argumento de endereço, não há como fornecer a senha.
|
||||
A senha pode ser fornecida basicamente de três maneiras quando o argumento de endereço é fornecido:
|
||||
|
||||
- Opção `-P, --password`: apenas use essa opção CLI fornecendo a senha. Eu desaconselho fortemente esse método, pois é muito inseguro (você pode manter a senha no histórico do shell).
|
||||
- Via `sshpass`: você pode fornecer a senha via `sshpass`, por exemplo, `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`.
|
||||
- Você será solicitado a fornecer a senha: se você não usar nenhum dos métodos anteriores, será solicitado a fornecer a senha, como acontece com ferramentas mais clássicas como `scp`, `ssh`, etc.
|
||||
|
||||
### Subcomandos
|
||||
|
||||
#### Importar um Tema
|
||||
|
||||
Execute o termscp como `termscp theme <theme-file>`
|
||||
|
||||
#### Instalar a Última Versão
|
||||
|
||||
Execute o termscp como `termscp update`
|
||||
|
||||
---
|
||||
|
||||
## Parâmetros de Conexão do S3
|
||||
|
||||
Esses parâmetros são necessários para se conectar ao AWS S3 e a outros servidores compatíveis com S3:
|
||||
|
||||
- AWS S3:
|
||||
- **Nome do balde**
|
||||
- **Região**
|
||||
- *Perfil* (se não fornecido: "default")
|
||||
- *Chave de acesso* (a menos que seja público)
|
||||
- *Chave de acesso secreta* (a menos que seja público)
|
||||
- *Token de segurança* (se necessário)
|
||||
- *Token de sessão* (se necessário)
|
||||
- Novo estilo de caminho: **NÃO**
|
||||
- Outros endpoints S3:
|
||||
- **Nome do balde**
|
||||
- **Endpoint**
|
||||
- *Chave de acesso* (a menos que seja público)
|
||||
- *Chave de acesso secreta* (a menos que seja público)
|
||||
- Novo estilo de caminho: **SIM**
|
||||
|
||||
### Credenciais do S3 🦊
|
||||
|
||||
Para se conectar a um balde do AWS S3, você obviamente precisa fornecer algumas credenciais.
|
||||
Existem basicamente três maneiras de fazer isso:
|
||||
Estes são os métodos para fornecer credenciais para o S3:
|
||||
|
||||
1. Formulário de autenticação:
|
||||
|
||||
|
||||
1. Você pode fornecer a `access_key` (deve ser obrigatória), a `secret_access_key` (deve ser obrigatória), o `security_token` e o `session_token`.
|
||||
2. Se você salvar a conexão S3 como um favorito, essas credenciais serão salvas como uma string criptografada AES-256/BASE64 no seu arquivo de favoritos (exceto para o token de segurança e o token de sessão, que são credenciais temporárias).
|
||||
2. Use seu arquivo de credenciais: basta configurar a CLI da AWS via `aws configure` e suas credenciais já devem estar localizadas em `~/.aws/credentials`. Caso você esteja usando um perfil diferente de `default`, apenas forneça-o no campo de perfil no formulário de autenticação.
|
||||
3. **Variáveis de ambiente**: você sempre pode fornecer suas credenciais como variáveis de ambiente. Lembre-se de que essas credenciais **sempre substituirão** as credenciais localizadas no arquivo de `credentials`. Veja como configurar o ambiente abaixo:
|
||||
|
||||
Estas devem sempre ser obrigatórias:
|
||||
|
||||
- `AWS_ACCESS_KEY_ID`: ID da chave de acesso da AWS (geralmente começa com `AKIA...`)
|
||||
- `AWS_SECRET_ACCESS_KEY`: a chave de acesso secreta
|
||||
|
||||
Caso você tenha configurado uma segurança mais rigorosa, você *pode* precisar destes também:
|
||||
|
||||
- `AWS_SECURITY_TOKEN`: token de segurança
|
||||
- `AWS_SESSION_TOKEN`: token de sessão
|
||||
|
||||
⚠️ Suas credenciais estão seguras: o termscp não manipula esses valores diretamente! Suas credenciais são consumidas diretamente pelo crate **s3**.
|
||||
Se você tiver alguma preocupação com a segurança, entre em contato com o autor da biblioteca no [Github](https://github.com/durch/rust-s3) ⚠️
|
||||
|
||||
---
|
||||
|
||||
## Explorador de Arquivos 📂
|
||||
|
||||
Quando nos referimos a exploradores de arquivos no termscp, estamos falando dos painéis que você pode ver após estabelecer uma conexão com o remoto.
|
||||
Esses painéis são basicamente três (sim, três na verdade):
|
||||
|
||||
- Painel do explorador local: ele é exibido à esquerda da sua tela e mostra as entradas do diretório atual do localhost.
|
||||
- Painel do explorador remoto: ele é exibido à direita da sua tela e mostra as entradas do diretório atual do host remoto.
|
||||
- Painel de resultados de busca: dependendo de onde você está buscando arquivos (local/remoto), ele substituirá o painel local ou o painel do explorador. Este painel mostra as entradas que correspondem à consulta de busca que você realizou.
|
||||
|
||||
Para trocar de painel, você precisa pressionar `<LEFT>` para mover para o painel do explorador remoto e `<RIGHT>` para voltar para o painel do explorador local. Sempre que estiver no painel de resultados da busca, você precisa pressionar `<ESC>` para sair do painel e voltar ao painel anterior.
|
||||
|
||||
### Atalhos de Teclado ⌨
|
||||
|
||||
| Tecla | Comando | Lembrete |
|
||||
|----------------|----------------------------------------------------------|-------------|
|
||||
| `<ESC>` | Desconectar do remoto; retornar à página de autenticação | |
|
||||
| `<BACKSPACE>` | Voltar ao diretório anterior na pilha | |
|
||||
| `<TAB>` | Alternar aba do explorador | |
|
||||
| `<RIGHT>` | Mover para a aba do explorador remoto | |
|
||||
| `<LEFT>` | Mover para a aba do explorador local | |
|
||||
| `<UP>` | Mover para cima na lista selecionada | |
|
||||
| `<DOWN>` | Mover para baixo na lista selecionada | |
|
||||
| `<PGUP>` | Mover para cima na lista selecionada por 8 linhas | |
|
||||
| `<PGDOWN>` | Mover para baixo na lista selecionada por 8 linhas | |
|
||||
| `<ENTER>` | Entrar no diretório | |
|
||||
| `<ESPAÇO>` | Fazer upload/download do arquivo selecionado | |
|
||||
| `<BACKTAB>` | Alternar entre aba de logs e explorador | |
|
||||
| `<A>` | Alternar arquivos ocultos | Todos |
|
||||
| `<B>` | Ordenar arquivos por | Bubblesort?|
|
||||
| `<C\|F5>` | Copiar arquivo/diretório | Copiar |
|
||||
| `<D\|F7>` | Criar diretório | Diretório |
|
||||
| `<E\|F8\|DEL>`| Deletar arquivo | Apagar |
|
||||
| `<F>` | Buscar arquivos (suporta pesquisa com coringas) | Buscar |
|
||||
| `<G>` | Ir para caminho especificado | Ir para |
|
||||
| `<H\|F1>` | Mostrar ajuda | Ajuda |
|
||||
| `<I>` | Mostrar informações sobre arquivo ou diretório selecionado | Informação |
|
||||
| `<K>` | Criar link simbólico apontando para a entrada selecionada | Symlink |
|
||||
| `<L>` | Recarregar conteúdo do diretório atual / Limpar seleção | Lista |
|
||||
| `<M>` | Selecionar um arquivo | Marcar |
|
||||
| `<N>` | Criar novo arquivo com o nome fornecido | Novo |
|
||||
| `<O\|F4>` | Editar arquivo; veja Editor de Texto | Abrir |
|
||||
| `<P>` | Abrir painel de logs | Painel |
|
||||
| `<Q\|F10>` | Sair do termscp | Sair |
|
||||
| `<R\|F6>` | Renomear arquivo | Renomear |
|
||||
| `<S\|F2>` | Salvar arquivo como... | Salvar |
|
||||
| `<T>` | Sincronizar alterações para caminho selecionado para remoto | Track |
|
||||
| `<U>` | Ir para o diretório pai | Subir |
|
||||
| `<V\|F3>` | Abrir arquivo com o programa padrão para o tipo de arquivo | Visualizar |
|
||||
| `<W>` | Abrir arquivo com o programa fornecido | Com |
|
||||
| `<X>` | Executar um comando | Executar |
|
||||
| `<Y>` | Alternar navegação sincronizada | Sincronizar |
|
||||
| `<Z>` | Alterar modo de arquivo | |
|
||||
| `</>` | Filtrar arquivos (suporte tanto para regex quanto para coringa) | |
|
||||
| `<CTRL+A>` | Selecionar todos os arquivos | |
|
||||
| `<ALT+A>` | Deselecionar todos os arquivos | |
|
||||
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
|
||||
| `<CTRL+T>` | Mostrar todos os caminhos sincronizados | Track |
|
||||
|
||||
### Trabalhar com múltiplos arquivos 🥷
|
||||
|
||||
Você pode optar por trabalhar com vários arquivos, usando estes controles simples:
|
||||
|
||||
- `<M>`: marcar um arquivo para seleção
|
||||
- `<CTRL+A>`: selecionar todos os arquivos no diretório atual
|
||||
- `<ALT+A>`: desselecionar todos os arquivos
|
||||
|
||||
Uma vez marcado, o arquivo será **exibido com fundo destacado** .
|
||||
Ao trabalhar com seleção, apenas os arquivos selecionados serão processados, enquanto o item atualmente destacado será ignorado.
|
||||
|
||||
É possível trabalhar com múltiplos arquivos também no painel de resultados de busca.
|
||||
|
||||
Todas as ações estão disponíveis ao trabalhar com múltiplos arquivos, mas algumas funcionam de forma ligeiramente diferente. Vamos ver:
|
||||
|
||||
- *Copiar*: ao copiar, será solicitado o nome de destino. Com múltiplos arquivos, esse nome será o diretório de destino para todos eles.
|
||||
- *Renomear*: igual a copiar, mas moverá os arquivos.
|
||||
- *Salvar como*: igual a copiar, mas escreverá os arquivos nesse local.
|
||||
|
||||
Se você selecionar um arquivo num diretório (ex: `/home`) e mudar de diretório, ele continuará selecionado e aparecerá na **fila de transferência** no painel inferior.
|
||||
Ao selecionar um arquivo, o diretório *remoto* atual é associado a ele; então, se for transferido, será enviado para esse diretório associado.
|
||||
|
||||
#### Exemplo
|
||||
|
||||
Se selecionarmos `/home/a.txt` localmente e estivermos em `/tmp` no painel remoto, depois mudarmos para `/var` e selecionarmos `/var/b.txt`, e estivermos em `/home` no painel remoto, ao transferir teremos:
|
||||
|
||||
- `/home/a.txt` transferido para `/tmp/a.txt`
|
||||
- `/var/b.txt` transferido para `/home/b.txt`
|
||||
|
||||
### Navegação Sincronizada ⏲️
|
||||
|
||||
Quando ativada, a navegação sincronizada permitirá sincronizar a navegação entre os dois painéis.
|
||||
Isso significa que, sempre que você mudar o diretório de trabalho em um painel, a mesma ação será reproduzida no outro painel. Se quiser ativar a navegação sincronizada, basta pressionar `<Y>`; pressione duas vezes para desativar. Enquanto estiver ativada, o estado da navegação sincronizada será exibido na barra de status como `ON` (Ligado).
|
||||
|
||||
### Abrir e Abrir Com 🚪
|
||||
|
||||
Os comandos para abrir e abrir com são alimentados pelo [open-rs](https://docs.rs/crate/open/1.7.0).
|
||||
Ao abrir arquivos com o comando Visualizar (`<V>`), será usado o aplicativo padrão do sistema para o tipo de arquivo. Para isso, será usado o serviço padrão do sistema operacional, então certifique-se de ter pelo menos um destes instalados no seu sistema:
|
||||
|
||||
- Para usuários do **Windows**: você não precisa se preocupar, pois o crate usará o comando `start`.
|
||||
- Para usuários do **MacOS**: também não é necessário se preocupar, pois o crate usará `open`, que já está instalado no seu sistema.
|
||||
- Para usuários do **Linux**: um dos seguintes deve estar instalado:
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- Para usuários do **WSL**: *wslview* é necessário, você deve instalar [wslu](https://github.com/wslutilities/wslu).
|
||||
|
||||
> Pergunta: Posso editar arquivos remotos usando o comando de visualização?
|
||||
> Resposta: Não, pelo menos não diretamente do "painel remoto". Você deve baixá-lo para um diretório local primeiro, porque quando você abre um arquivo remoto, ele é baixado para um diretório temporário, mas não há como criar um observador para o arquivo para verificar quando o programa que você usou para abri-lo foi fechado, então o termscp não pode saber quando você terminou de editar o arquivo.
|
||||
|
||||
---
|
||||
|
||||
## Favoritos ⭐
|
||||
|
||||
No termscp é possível salvar hosts favoritos, que podem ser carregados rapidamente a partir do layout principal do termscp.
|
||||
O termscp também salvará os últimos 16 hosts aos quais você se conectou.
|
||||
Esse recurso permite que você carregue todos os parâmetros necessários para se conectar a um determinado host remoto, simplesmente selecionando o favorito na aba abaixo do formulário de autenticação.
|
||||
|
||||
Os favoritos serão salvos, se possível, em:
|
||||
|
||||
- `$HOME/.config/termscp/` no Linux/BSD
|
||||
- `$HOME/Library/Application Support/termscp` no MacOS
|
||||
- `FOLDERID_RoamingAppData\termscp\` no Windows
|
||||
|
||||
Para os favoritos apenas (isso não se aplica aos hosts recentes), também é possível salvar a senha usada para autenticar. A senha não é salva por padrão e deve ser especificada no prompt ao salvar um novo favorito.
|
||||
Se você estiver preocupado com a segurança da senha salva para seus favoritos, por favor, leia o [capítulo abaixo 👀](#minhas-senhas-são-seguras-).
|
||||
|
||||
Para criar um novo favorito, siga estas etapas:
|
||||
|
||||
1. Digite no formulário de autenticação os parâmetros para se conectar ao seu servidor remoto
|
||||
2. Pressione `<CTRL+S>`
|
||||
3. Digite o nome que deseja dar ao favorito
|
||||
4. Escolha se deseja lembrar da senha ou não
|
||||
5. Pressione `<ENTER>` para enviar
|
||||
|
||||
Sempre que quiser usar a conexão salva anteriormente, basta pressionar `<TAB>` para navegar para a lista de favoritos e carregar os parâmetros do favorito no formulário pressionando `<ENTER>`.
|
||||
|
||||

|
||||
|
||||
### Minhas Senhas São Seguras? 😈
|
||||
|
||||
Claro 😉.
|
||||
Como já mencionado, os favoritos são salvos no diretório de configuração juntamente com as senhas. As senhas, obviamente, não são texto simples, elas são criptografadas com **AES-128**. Isso as torna seguras? Absolutamente! (exceto para usuários de BSD e WSL 😢)
|
||||
|
||||
No **Windows**, **Linux** e **MacOS**, a chave usada para criptografar senhas é armazenada, se possível (e deve ser), respectivamente no *Windows Vault*, no *sistema keyring* e no *Keychain*. Isso é realmente super seguro e é gerenciado diretamente pelo seu sistema operacional.
|
||||
|
||||
❗ Por favor, note que se você é um usuário de Linux, seria melhor ler o [capítulo abaixo 👀](#keyring-do-linux), porque o keyring pode não estar habilitado ou suportado no seu sistema!
|
||||
|
||||
Por outro lado, no *BSD* e no *WSL*, a chave usada para criptografar suas senhas é armazenada em seu disco (em `$HOME/.config/termscp`). Ainda é possível recuperar a chave para descriptografar as senhas. Felizmente, a localização da chave garante que ela não possa ser lida por outros usuários diferentes de você, mas sim, eu ainda não salvaria a senha para um servidor exposto na internet 😉.
|
||||
|
||||
#### Keyring do Linux
|
||||
|
||||
Todos nós amamos o Linux por causa da liberdade que ele oferece aos usuários. Você pode basicamente fazer o que quiser como usuário de Linux, mas isso também tem alguns contras, como o fato de muitas vezes não haver aplicativos padrão em diferentes distribuições. E isso também envolve o keyring.
|
||||
Isso significa que no Linux pode não haver um keyring instalado no seu sistema. Infelizmente, a biblioteca que usamos para trabalhar com o armazenamento de chaves requer um serviço que expõe `org.freedesktop.secrets` no D-BUS, e o pior é que há apenas dois serviços que o expõem.
|
||||
|
||||
- ❗ Se você usa GNOME como ambiente de desktop (por exemplo, usuários do Ubuntu), já deve estar bem, pois o keyring já é fornecido pelo `gnome-keyring` e tudo deve estar funcionando.
|
||||
- ❗ Para usuários de outros ambientes de desktop, há um programa legal que você pode usar para obter um keyring, que é o [KeepassXC](https://keepassxc.org/), que eu uso na minha instalação Manjaro (com KDE) e funciona bem. O único problema é que você precisa configurá-lo para ser usado junto com o termscp (mas é bastante simples). Para começar com KeepassXC, leia mais [aqui](#configuração-do-keepassxc-para-o-termscp).
|
||||
- ❗ E se você não quiser instalar nenhum desses serviços? Bem, não tem problema! **termscp continuará funcionando normalmente**, mas salvará a chave em um arquivo, como normalmente faz para BSD e WSL.
|
||||
|
||||
##### Configuração do KeepassXC para o termscp
|
||||
|
||||
Siga estas etapas para configurar o KeepassXC para o termscp:
|
||||
|
||||
1. Instale o KeepassXC
|
||||
2. Vá para "ferramentas" > "configurações" na barra de ferramentas
|
||||
3. Selecione "Integração do Serviço Secreto" e ative "Habilitar Integração do Serviço Secreto do KeepassXC"
|
||||
4. Crie um banco de dados, se você ainda não tiver um: na barra de ferramentas "Banco de dados" > "Novo banco de dados"
|
||||
5. Na barra de ferramentas: "Banco de dados" > "Configurações do banco de dados"
|
||||
6. Selecione "Integração do Serviço Secreto" e ative "Expor entradas sob este grupo"
|
||||
7. Selecione o grupo na lista onde deseja que o segredo do termscp seja mantido. Lembre-se de que esse grupo pode ser usado por qualquer outro aplicativo para armazenar segredos via DBUS.
|
||||
|
||||
---
|
||||
|
||||
## Configuração ⚙️
|
||||
|
||||
O termscp suporta alguns parâmetros definidos pelo usuário, que podem ser definidos na configuração.
|
||||
Por baixo dos panos, o termscp tem um arquivo TOML e alguns outros diretórios onde todos os parâmetros serão salvos, mas não se preocupe, você não precisará tocar em nenhum desses arquivos manualmente, pois fiz com que fosse possível configurar o termscp completamente a partir de sua interface de usuário.
|
||||
|
||||
Assim como para os favoritos, o termscp só requer que esses caminhos estejam acessíveis:
|
||||
|
||||
- `$HOME/.config/termscp/` no Linux/BSD
|
||||
- `$HOME/Library/Application Support/termscp` no MacOs
|
||||
- `FOLDERID_RoamingAppData\termscp\` no Windows
|
||||
|
||||
Para acessar a configuração, basta pressionar `<CTRL+C>` a partir da tela inicial do termscp.
|
||||
|
||||
Estes parâmetros podem ser alterados:
|
||||
|
||||
- **Editor de Texto**: o editor de texto a ser usado. Por padrão, o termscp encontrará o editor padrão para você; com essa opção, você pode forçar um editor a ser usado (por exemplo, `vim`). **Também são suportados editores GUI**, a menos que eles `nohup` do processo pai.
|
||||
- **Protocolo Padrão**: o protocolo padrão é o valor padrão para o protocolo de transferência de arquivos a ser usado no termscp. Isso se aplica à página de login e ao argumento CLI do endereço.
|
||||
- **Exibir Arquivos Ocultos**: selecione se os arquivos ocultos devem ser exibidos por padrão. Você ainda poderá decidir se deseja exibir ou não arquivos ocultos em tempo de execução pressionando `A`.
|
||||
- **Verificar atualizações**: se definido como `yes`, o termscp buscará a API do Github para verificar se há uma nova versão do termscp disponível.
|
||||
- **Prompt ao substituir arquivos existentes?**: Se definido como `yes`, o termscp pedirá confirmação sempre que uma transferência de arquivos causaria a substituição de um arquivo existente no host de destino.
|
||||
- **Agrupar Diretórios**: selecione se os diretórios devem ser agrupados ou não nos exploradores de arquivos. Se `Display first` for selecionado, os diretórios serão ordenados usando o método configurado, mas exibidos antes dos arquivos; se `Display last` for selecionado, eles serão exibidos depois.
|
||||
- **Sintaxe do formatador de arquivos remotos**: sintaxe para exibir informações de arquivo para cada arquivo no explorador remoto. Veja [Formato do Explorador de Arquivos](#formato-do-explorador-de-arquivos)
|
||||
- **Sintaxe do formatador de arquivos locais**: sintaxe para exibir informações de arquivo para cada arquivo no explorador local. Veja [Formato do Explorador de Arquivos](#formato-do-explorador-de-arquivos)
|
||||
- **Habilitar notificações?**: Se definido como `Yes`, as notificações serão exibidas.
|
||||
- **Notificações: tamanho mínimo para transferência**: se o tamanho da transferência for maior ou igual ao valor especificado, as notificações para a transferência serão exibidas. Os valores aceitos estão no formato `{UNSIGNED} B/KB/MB/GB/TB/PB`.
|
||||
- **Caminho da configuração SSH**: define o arquivo de configuração SSH a ser usado ao se conectar a um servidor SCP/SFTP. Se não definido (vazio), nenhum arquivo será usado. Você pode especificar um caminho começando com `~` para indicar o caminho inicial (por exemplo, `~/.ssh/config`). Os parâmetros suportados pelo termscp estão especificados [AQUI](https://github.com/veeso/ssh2-config#exposed-attributes).
|
||||
|
||||
### Armazenamento de Chave SSH 🔐
|
||||
|
||||
Além da configuração, o termscp também oferece um recurso **essencial** para **clientes SFTP/SCP**: o armazenamento de chave SSH.
|
||||
|
||||
Você pode acessar o armazenamento de chaves SSH na configuração, indo para a aba `Chaves SSH`. Uma vez lá, você pode:
|
||||
|
||||
- **Adicionar uma nova chave**: basta pressionar `<CTRL+N>` e você será solicitado a criar uma nova chave. Forneça o nome do host/endereço IP e o nome de usuário associado à chave e, finalmente, um editor de texto será aberto: cole a **chave SSH PRIVADA** no editor de texto, salve e saia.
|
||||
- **Remover uma chave existente**: apenas pressione `<DEL>` ou `<CTRL+E>` na chave que você deseja remover para deletar a chave do termscp permanentemente.
|
||||
- **Editar uma chave existente**: basta pressionar `<ENTER>` na chave que você deseja editar para alterar a chave privada.
|
||||
|
||||
> Pergunta: Espere, minha chave privada está protegida com senha, posso usá-la?
|
||||
> Resposta: Claro que sim. A senha fornecida para autenticação no termscp é válida tanto para autenticação por nome de usuário/senha quanto para autenticação por chave RSA.
|
||||
|
||||
### Formato do Explorador de Arquivos
|
||||
|
||||
É possível, através da configuração, definir um formato personalizado para o explorador de arquivos. Isso é possível tanto para o host local quanto para o remoto, para que você possa ter duas sintaxes diferentes em uso. Esses campos, com nome `File formatter syntax (local)` e `File formatter syntax (remote)`, definirão como as entradas de arquivos serão exibidas no explorador de arquivos.
|
||||
A sintaxe para o formatador é a seguinte `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
|
||||
Cada chave entre colchetes será substituída pelo atributo relacionado, enquanto tudo fora dos colchetes permanecerá inalterado.
|
||||
|
||||
- O nome da chave é obrigatório e deve ser uma das chaves abaixo.
|
||||
- O comprimento descreve o espaço reservado para exibir o campo. Atributos estáticos não suportam esse recurso (GRUPO, PEX, TAMANHO, USUÁRIO).
|
||||
- O Extra é suportado apenas por alguns parâmetros e é uma opção adicional. Veja as chaves para verificar se o extra é suportado.
|
||||
|
||||
Estas são as chaves suportadas pelo formatador:
|
||||
|
||||
- `ATIME`: Última vez de acesso (com sintaxe padrão `%b %d %Y %H:%M`); O Extra pode ser fornecido como a sintaxe de tempo (por exemplo, `{ATIME:8:%H:%M}`).
|
||||
- `CTIME`: Tempo de criação (com sintaxe `%b %d %Y %H:%M`); O Extra pode ser fornecido como a sintaxe de tempo (por exemplo, `{CTIME:8:%H:%M}`).
|
||||
- `GROUP`: Grupo do proprietário.
|
||||
- `MTIME`: Última modificação (com sintaxe `%b %d %Y %H:%M`); O Extra pode ser fornecido como a sintaxe de tempo (por exemplo, `{MTIME:8:%H:%M}`).
|
||||
- `NAME`: Nome do arquivo (pastas entre a raiz e os primeiros ancestrais são omitidas se forem maiores que o comprimento).
|
||||
- `PATH`: Caminho absoluto do arquivo (pastas entre a raiz e os primeiros ancestrais são omitidas se forem maiores que o comprimento).
|
||||
- `PEX`: Permissões do arquivo (formato UNIX).
|
||||
- `SIZE`: Tamanho do arquivo (omitido para diretórios).
|
||||
- `SYMLINK`: Link simbólico (se houver `-> {FILE_PATH}`).
|
||||
- `USER`: Nome do proprietário.
|
||||
|
||||
Se deixado vazio, será usada a sintaxe padrão do formatador: `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`.
|
||||
|
||||
---
|
||||
|
||||
## Temas 🎨
|
||||
|
||||
O termscp oferece a você um recurso incrível: a possibilidade de definir as cores para vários componentes no aplicativo.
|
||||
Se você deseja personalizar o termscp, há duas maneiras disponíveis para fazer isso:
|
||||
|
||||
- A partir do **menu de configuração**
|
||||
- Importando um **arquivo de tema**
|
||||
|
||||
Para criar sua própria personalização no termscp, tudo o que você precisa fazer é entrar na configuração a partir da atividade de autenticação, pressionar `<CTRL+C>` e depois `<TAB>` duas vezes. Agora você deve ter se movido para o painel de `themes`.
|
||||
|
||||
Aqui você pode se mover com `<UP>` e `<DOWN>` para alterar o estilo que deseja alterar, como mostrado no gif abaixo:
|
||||
|
||||

|
||||
|
||||
O termscp suporta tanto a sintaxe tradicional de hexadecimal explícito (`#rrggbb`) quanto rgb `rgb(r, g, b)` para fornecer cores, mas também **[cores CSS](https://www.w3schools.com/cssref/css_colors.asp)** (como `crimson`) são aceitas 😉. Há também uma palavra-chave especial, que é `Default`. Default significa que a cor usada será a cor padrão de primeiro plano ou plano de fundo, dependendo da situação (primeiro plano para textos e linhas, plano de fundo para, bem, adivinhe).
|
||||
|
||||
Como mencionado antes, você também pode importar arquivos de temas. Você pode se inspirar ou usar diretamente um dos temas fornecidos junto com o termscp, localizado no diretório `themes/` deste repositório, e importá-los executando o termscp como `termscp -t <arquivo-do-tema>`. Se tudo correu bem, ele deve informar que o tema foi importado com sucesso.
|
||||
|
||||
### Meu Tema Não Carrega 😱
|
||||
|
||||
Isso provavelmente se deve a uma atualização recente que quebrou o tema. Sempre que eu adiciono uma nova chave aos temas, o tema salvo não será carregado. Para corrigir esse problema, existem duas soluções rápidas:
|
||||
|
||||
1. Recarregar o tema: sempre que eu lançar uma atualização, também corrigirei os "temas oficiais", então você só precisará baixá-lo novamente do repositório e reimportar o tema usando a opção `-t`.
|
||||
|
||||
```sh
|
||||
termscp -t <theme.toml>
|
||||
```
|
||||
|
||||
2. Corrigir seu tema: se você estiver usando um tema personalizado, você pode editá-lo via `vim` e adicionar a chave que está faltando. O tema está localizado em `$CONFIG_DIR/termscp/theme.toml`, onde `$CONFIG_DIR` é:
|
||||
|
||||
- FreeBSD/GNU-Linux: `$HOME/.config/`
|
||||
- MacOs: `$HOME/Library/Application Support`
|
||||
- Windows: `%appdata%`
|
||||
|
||||
❗ As chaves que faltam são relatadas no CHANGELOG sob `BREAKING CHANGES` para a versão que você acabou de instalar.
|
||||
|
||||
### Estilos 💈
|
||||
|
||||
Você pode encontrar na tabela abaixo a descrição para cada campo de estilo.
|
||||
Por favor, note que **estilos não se aplicam à página de configuração**, para torná-la sempre acessível no caso de você bagunçar tudo.
|
||||
|
||||
#### Página de Autenticação
|
||||
|
||||
| Chave | Descrição |
|
||||
|-----------------|----------------------------------------------|
|
||||
| auth_address | Cor do campo de entrada para endereço IP |
|
||||
| auth_bookmarks | Cor do painel de favoritos |
|
||||
| auth_password | Cor do campo de entrada para senha |
|
||||
| auth_port | Cor do campo de entrada para número da porta |
|
||||
| auth_protocol | Cor do grupo de rádio para protocolo |
|
||||
| auth_recents | Cor do painel de recentes |
|
||||
| auth_username | Cor do campo de entrada para nome de usuário |
|
||||
|
||||
#### Página de Transferência
|
||||
|
||||
| Chave | Descrição |
|
||||
|--------------------------------------|---------------------------------------------------------------------------------|
|
||||
| transfer_local_explorer_background | Cor de fundo do explorador do localhost |
|
||||
| transfer_local_explorer_foreground | Cor de primeiro plano do explorador do localhost |
|
||||
| transfer_local_explorer_highlighted | Cor da borda e realce do explorador do localhost |
|
||||
| transfer_remote_explorer_background | Cor de fundo do explorador remoto |
|
||||
| transfer_remote_explorer_foreground | Cor de primeiro plano do explorador remoto |
|
||||
| transfer_remote_explorer_highlighted | Cor da borda e realce do explorador remoto |
|
||||
| transfer_log_background | Cor de fundo do painel de logs |
|
||||
| transfer_log_window | Cor da janela para o painel de logs |
|
||||
| transfer_progress_bar_partial | Cor parcial da barra de progresso |
|
||||
| transfer_progress_bar_total | Cor total da barra de progresso |
|
||||
| transfer_status_hidden | Cor para a etiqueta "oculto" na barra de status |
|
||||
| transfer_status_sorting | Cor para a etiqueta "ordenando" na barra de status; aplica-se também ao diálogo de ordenação de arquivos |
|
||||
| transfer_status_sync_browsing | Cor para a etiqueta "navegação sincronizada" na barra de status |
|
||||
|
||||
#### Diversos
|
||||
|
||||
Estes estilos se aplicam a diferentes partes do aplicativo.
|
||||
|
||||
| Chave | Descrição |
|
||||
|-----------------------------|------------------------------------------------|
|
||||
| misc_error_dialog | Cor para mensagens de erro |
|
||||
| misc_info_dialog | Cor para diálogos de informações |
|
||||
| misc_input_dialog | Cor para diálogos de entrada (como copiar arquivo) |
|
||||
| misc_keys | Cor do texto para teclas de atalho |
|
||||
| misc_quit_dialog | Cor para diálogos de saída |
|
||||
| misc_save_dialog | Cor para diálogos de salvar |
|
||||
| misc_warn_dialog | Cor para diálogos de aviso |
|
||||
|
||||
---
|
||||
|
||||
## Editor de Texto ✏
|
||||
|
||||
O termscp possui, como você deve ter notado, muitos recursos, um deles é a possibilidade de visualizar e editar arquivos de texto. Não importa se o arquivo está localizado no host local ou no host remoto, o termscp oferece a possibilidade de abrir um arquivo no seu editor de texto favorito.
|
||||
Caso o arquivo esteja localizado no host remoto, ele será primeiro baixado para o seu diretório temporário e, **somente** se alterações forem feitas no arquivo, ele será re-enviado para o host remoto. O termscp verifica se você fez alterações no arquivo verificando o último tempo de modificação do arquivo.
|
||||
|
||||
> ❗ Apenas um lembrete: **você só pode editar arquivos de texto**; arquivos binários não são suportados.
|
||||
|
||||
---
|
||||
|
||||
## Registro de Logs 🩺
|
||||
|
||||
O termscp escreve um arquivo de log para cada sessão, que é gravado em:
|
||||
|
||||
- `$HOME/.cache/termscp/termscp.log` no Linux/BSD
|
||||
- `$HOME/Library/Caches/termscp/termscp.log` no MacOs
|
||||
- `FOLDERID_LocalAppData\termscp\termscp.log` no Windows
|
||||
|
||||
o log não será rotacionado, mas será truncado após cada execução do termscp, então se você quiser relatar um problema e anexar seu arquivo de log, lembre-se de salvar o arquivo de log em um local seguro antes de usar o termscp novamente. O registro por padrão é feito no nível *INFO*, então não é muito detalhado.
|
||||
|
||||
Se você quiser enviar um problema, por favor, se puder, reproduza o problema com o nível definido como `TRACE`, para isso, inicie o termscp com a opção CLI `-D`.
|
||||
|
||||
Sei que você pode ter algumas perguntas sobre arquivos de log, então fiz um tipo de perguntas e respostas:
|
||||
|
||||
> Não quero registros, posso desativá-los?
|
||||
|
||||
Sim, você pode. Basta iniciar o termscp com a opção `-q ou --quiet`. Você pode aliasar o termscp para tornar isso persistente. Lembre-se de que os registros são usados para diagnosticar problemas, então, como atrás de todo projeto de código aberto deve sempre haver esse tipo de ajuda mútua, manter os arquivos de log pode ser sua maneira de apoiar o projeto 😉. Não quero que você se sinta culpado, mas só estou dizendo.
|
||||
|
||||
> O registro é seguro?
|
||||
|
||||
Se você estiver preocupado com a segurança, o arquivo de log não contém nenhuma senha em texto simples, então não se preocupe e expõe as mesmas informações que o arquivo irmão `bookmarks` relata.
|
||||
|
||||
## Notificações 📫
|
||||
|
||||
O termscp enviará notificações da área de trabalho para estes tipos de eventos:
|
||||
|
||||
- Em **Transferência concluída**: A notificação será enviada quando uma transferência for concluída com sucesso.
|
||||
- ❗ A notificação será exibida apenas se o tamanho total da transferência for pelo menos o especificado em `Notifications: minimum transfer size` na configuração.
|
||||
- Em **Transferência falhou**: A notificação será enviada quando uma transferência falhar devido a um erro.
|
||||
- ❗ A notificação será exibida apenas se o tamanho total da transferência for pelo menos o especificado em `Notifications: minimum transfer size` na configuração.
|
||||
- Em **Atualização disponível**: Sempre que uma nova versão do termscp estiver disponível, uma notificação será exibida.
|
||||
- Em **Atualização instalada**: Sempre que uma nova versão do termscp for instalada, uma notificação será exibida.
|
||||
- Em **Falha na atualização**: Sempre que a instalação da atualização falhar, uma notificação será exibida.
|
||||
|
||||
❗ Se você prefere manter as notificações desativadas, basta entrar na configuração e definir `Enable notifications?` para `No` 😉.
|
||||
❗ Se quiser alterar o tamanho mínimo para exibir notificações, você pode mudar o valor na configuração com a chave `Notifications: minimum transfer size` e ajustá-lo ao que for melhor para você 🙂.
|
||||
|
||||
---
|
||||
|
||||
## Observador de Arquivos 🔭
|
||||
|
||||
O observador de arquivos permite que você configure uma lista de caminhos para sincronizar com os hosts remotos.
|
||||
Isso significa que, sempre que uma alteração no sistema de arquivos local for detectada no caminho sincronizado, a alteração será automaticamente relatada para o caminho do host remoto configurado, dentro de 5 segundos.
|
||||
|
||||
Você pode definir quantos caminhos desejar para sincronizar:
|
||||
|
||||
1. Coloque o cursor no explorador local no diretório/arquivo que deseja manter sincronizado.
|
||||
2. Vá para o diretório para o qual deseja que as alterações sejam relatadas no host remoto.
|
||||
3. Pressione `<T>`.
|
||||
4. Responda `<YES>` na janela pop-up.
|
||||
|
||||
Para desfazer a observação, basta pressionar `<T>` no caminho local sincronizado (ou em qualquer um de seus subdiretórios)
|
||||
OU você pode simplesmente pressionar `<CTRL+T>` e pressionar `<ENTER>` no caminho sincronizado que deseja desfazer a observação.
|
||||
|
||||
Estas alterações serão relatadas para o host remoto:
|
||||
|
||||
- Novos arquivos, alterações em arquivos.
|
||||
- Arquivo movido/renomeado.
|
||||
- Arquivo removido/desvinculado.
|
||||
|
||||
> ❗ O observador funciona apenas em uma direção (local > remoto). Não é possível sincronizar automaticamente as alterações do host remoto para o local.
|
||||
@@ -1,7 +1,7 @@
|
||||
# termscp
|
||||
|
||||
<p align="center">
|
||||
<img src="/assets/images/termscp.svg" width="256" height="256" />
|
||||
<img src="/assets/images/termscp.svg" alt="logo" width="256" height="256" />
|
||||
</p>
|
||||
|
||||
<p align="center">~ 功能丰富的终端文件传输工具 ~</p>
|
||||
@@ -21,6 +21,14 @@
|
||||
alt="English"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||
><img
|
||||
height="20"
|
||||
src="/assets/images/flags/br.png"
|
||||
alt="Brazilian Portuguese"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
href="https://github.com/veeso/termscp/blob/main/docs/de/README.md"
|
||||
><img
|
||||
@@ -62,8 +70,8 @@
|
||||
/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">由 <a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</p>
|
||||
<p align="center">当前版本: 0.12.3 (06/10/2023)</p>
|
||||
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
||||
<p align="center">当前版本: 0.18.0 10/06/2025</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -124,7 +132,7 @@
|
||||
|
||||
## 关于 termscp 🖥
|
||||
|
||||
termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/SFTP/FTP/S3。 作为一个带有 TUI 的命令行工具,它可以连接到远程服务器进行文件检索和上传,并能够与本地文件系统进行交互。
|
||||
termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/SFTP/FTP/Kube/S3/WebDAV。 作为一个带有 TUI 的命令行工具,它可以连接到远程服务器进行文件检索和上传,并能够与本地文件系统进行交互。
|
||||
|
||||
兼容 **Linux**、**MacOS**、**FreeBSD** 和 **Windows** 操作系统。
|
||||
|
||||
@@ -138,8 +146,10 @@ termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/S
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** and **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作
|
||||
- 创建、删除、重命名、搜索、查看和编辑文件
|
||||
- ⭐ 通过“内置书签”和“最近连接”快速连接到您的主机
|
||||
@@ -262,7 +272,7 @@ termscp 由这些很棒的项目提供支持:
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
- [whoami](https://github.com/libcala/whoami)
|
||||
- [wildmatch](https://github.com/becheran/wildmatch)
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
- [用法](#用法)
|
||||
- [地址参数](#地址参数)
|
||||
- [AWS S3 地址参数](#aws-s3-地址参数)
|
||||
- [Kube 地址参数](#kube-地址参数)
|
||||
- [WebDAV 地址参数](#webdav-地址参数)
|
||||
- [SMB 地址参数](#smb-地址参数)
|
||||
- [如何输入密码](#如何输入密码)
|
||||
- [S3 连接参数](#s3-连接参数)
|
||||
- [Aws S3 凭证](#aws-s3-凭证)
|
||||
- [文件浏览](#文件浏览)
|
||||
- [快捷键](#快捷键)
|
||||
- [处理多个文件](#处理多个文件)
|
||||
- [操作多个文件 🥷](#操作多个文件-)
|
||||
- [示例](#示例)
|
||||
- [同步浏览](#同步浏览)
|
||||
- [打开/打开方式](#打开打开方式)
|
||||
- [书签](#书签)
|
||||
@@ -35,17 +38,15 @@
|
||||
|
||||
termscp启动时可以使用以下选项:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
或作为
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` 登陆密码
|
||||
- `-b, --address-as-bookmark` 将地址参数解析为书签名称
|
||||
- `-c, --config` 打开termscp时打开配置页面
|
||||
- `-q, --quiet` 禁用日志
|
||||
- `-t, --theme <path>` 导入自定义主题
|
||||
- `-v, --version` 打印版本信息
|
||||
- `-h, --help` 打开帮助
|
||||
|
||||
@@ -103,6 +104,27 @@ s3://<bucket-name>@<region>[:profile][:/wrkdir]
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### Kube 地址参数
|
||||
|
||||
如果您想连接到 Kube,请使用以下语法
|
||||
|
||||
```txt
|
||||
kube://[namespace][@<cluster_url>][$</path>]
|
||||
```
|
||||
|
||||
#### WebDAV 地址参数
|
||||
|
||||
如果您想要连接到 WebDAV,请使用以下语法
|
||||
|
||||
```txt
|
||||
http://<username>:<password>@<url></path>
|
||||
或者如果您想要使用 https
|
||||
```
|
||||
|
||||
```txt
|
||||
https://<username>:<password>@<url></path>
|
||||
```
|
||||
|
||||
#### SMB 地址参数
|
||||
|
||||
SMB 对 CLI 地址参数有不同的语法,无论您是在 Windows 还是其他系统上,这都是不同的:
|
||||
@@ -227,18 +249,40 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
||||
| `<X>` | 运行命令 | eXecute |
|
||||
| `<Y>` | 是否开启同步浏览 | sYnc |
|
||||
| `<Z>` | 更改文件权限 | |
|
||||
| `</>` | 过滤文件(支持正则表达式和通配符匹配) | |
|
||||
| `<CTRL+A>` | 选中所有文件 | |
|
||||
| `<ALT+A>` | 取消选择所有文件 | |
|
||||
| `<CTRL+C>` | 终止文件传输 | |
|
||||
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
||||
|
||||
### 处理多个文件
|
||||
### 操作多个文件 🥷
|
||||
|
||||
你可以同时操作多个文件,按`<M>`选定它们,或者按`<CTRL+A>` 全选当前工作目录中的所有文件。一旦一个文件被标记为选择,它将在左边显示一个 "*"。在这种模式下,只有选定的文件会被处理,而当前光标高亮显示的项目会被忽略。在查找结果面板中,也可以对多个文件进行处理。
|
||||
在处理多个文件时,所有的操作都是可用的,但请注意,有些操作的工作方式略有不同。让我们深入了解一下:
|
||||
你可以通过以下简单的控制操作多个文件:
|
||||
|
||||
- *复制*: 当你复制一个文件时,你会被提示输入完整目标路径名。当处理多个文件时,这个名称指的是所有这些文件将被复制到的目标目录。
|
||||
- *重命名*: 和复制操作类似, 但是会移动文件到目标路径。
|
||||
- *保存为*: 和复制操作类似, 但是会写入文件到目标路径。
|
||||
- `<M>`:标记文件以进行选择
|
||||
- `<CTRL+A>`:选择当前目录下的所有文件
|
||||
- `<ALT+A>`:取消选择所有文件
|
||||
|
||||
被标记的文件将会以**高亮背景** 显示。
|
||||
当进行选择操作时,只有被选中的文件会执行操作,而当前高亮显示的项目会被忽略。
|
||||
|
||||
即使是在查找结果面板中,也可以操作多个文件。
|
||||
|
||||
在操作多个文件时,所有功能都可用,但某些功能会有些许不同。具体如下:
|
||||
|
||||
- *复制*:复制时会提示你输入目标名称。操作多个文件时,该名称是目标目录,所有文件将被复制到此目录中。
|
||||
- *重命名*:与复制相同,但文件将被移动到该目录。
|
||||
- *另存为*:与复制相同,但文件将被写入该目录。
|
||||
|
||||
如果你在某个目录(如 `/home`)中选择了文件,然后切换目录,文件仍会保持被选中状态,并在底部面板的**传输队列** 中显示。
|
||||
文件被选中时,会将当前*远程*目录与该文件关联;如果文件被传输,它将被传输到与之关联的目录中。
|
||||
|
||||
#### 示例
|
||||
|
||||
如果我们在本地选择 `/home/a.txt`,此时远程目录是 `/tmp`,然后我们切换到 `/var`,选择 `/var/b.txt`,而此时远程目录为 `/home`,执行传输后的结果为:
|
||||
|
||||
- `/home/a.txt` 传输到 `/tmp/a.txt`
|
||||
- `/var/b.txt` 传输到 `/home/b.txt`
|
||||
|
||||
### 同步浏览
|
||||
|
||||
|
||||
35
install.sh
35
install.sh
@@ -8,12 +8,10 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="0.12.3"
|
||||
TERMSCP_VERSION="0.18.0"
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
RPM_URL_AMD64="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
|
||||
RPM_URL_AARCH64="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.aarch64.rpm"
|
||||
|
||||
PATH="$PATH:/usr/sbin"
|
||||
|
||||
@@ -37,8 +35,6 @@ set_termscp_version() {
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||
RPM_URL_AMD64="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
|
||||
RPM_URL_AARCH64="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.aarch64.rpm"
|
||||
}
|
||||
|
||||
info() {
|
||||
@@ -225,7 +221,9 @@ install_on_linux() {
|
||||
local msg
|
||||
local sudo
|
||||
local archive
|
||||
if has yay; then
|
||||
if has pacman; then
|
||||
install_on_arch_linux pacman
|
||||
elif has yay; then
|
||||
install_on_arch_linux yay
|
||||
elif has pakku; then
|
||||
install_on_arch_linux pakku
|
||||
@@ -260,29 +258,6 @@ install_on_linux() {
|
||||
info "$msg"
|
||||
$sudo dpkg -i "${archive}"
|
||||
rm -f ${archive}
|
||||
elif has rpm; then
|
||||
case "${ARCH}" in
|
||||
x86_64) RPM_URL="$RPM_URL_AMD64" ;;
|
||||
aarch64) RPM_URL="$RPM_URL_AARCH64" ;;
|
||||
*) try_with_cargo "we don't distribute packages for ${ARCH} at the moment" && return $? ;;
|
||||
esac
|
||||
info "Detected rpm on your system"
|
||||
info "Installing ${GREEN}termscp${NO_COLOR} via RPM package"
|
||||
archive=$(get_tmpfile "rpm")
|
||||
download "${archive}" "${RPM_URL}"
|
||||
info "Downloaded rpm package to ${archive}"
|
||||
if test_writeable "/usr/bin"; then
|
||||
sudo=""
|
||||
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
|
||||
else
|
||||
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}…"
|
||||
elevate_priv
|
||||
sudo="sudo"
|
||||
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
|
||||
fi
|
||||
info "$msg"
|
||||
$sudo rpm -U "${archive}"
|
||||
rm -f ${archive}
|
||||
elif has brew; then
|
||||
install_with_brew
|
||||
else
|
||||
@@ -310,7 +285,7 @@ install_bsd_cargo_deps() {
|
||||
|
||||
install_linux_cargo_deps() {
|
||||
local debian_deps="gcc pkg-config libdbus-1-dev libsmbclient-dev"
|
||||
local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel libsmbclient-devel"
|
||||
local rpm_deps="gcc openssl pkgconfig dbus-devel openssl-devel libsmbclient-devel"
|
||||
local arch_deps="gcc openssl pkg-config dbus smbclient"
|
||||
local deps_cmd=""
|
||||
# Get pkg manager
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
<head>
|
||||
<title>
|
||||
termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/S3/SMB | termscp
|
||||
termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV | termscp
|
||||
</title>
|
||||
<meta property="og:description"
|
||||
content="termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3. It is Linux, MacOS, FreeBSD, NetBSD and Windows compatible" />
|
||||
content="termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV. It is Linux, MacOS, FreeBSD, NetBSD and Windows compatible" />
|
||||
<meta name="description"
|
||||
content="termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3. It is Linux, MacOS, FreeBSD, NetBSD and Windows compatible" />
|
||||
<meta property="og:title" content="termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/S3 | termscp" />
|
||||
content="termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV. It is Linux, MacOS, FreeBSD, NetBSD and Windows compatible" />
|
||||
<meta property="og:title" content="termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/Kube/S3/WebDAV | termscp" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<p class="text-xs font-thin">
|
||||
<span>Christian Visintin © </span><span resolve-copyright></span>
|
||||
<span> | </span>
|
||||
<a class="text-xs font-thin" href="https://veeso.dev/en/privacy" target="_blank">Privacy policy</a>
|
||||
<a class="text-xs font-thin" href="https://veeso.me/en/privacy" target="_blank">Privacy policy</a>
|
||||
<span> | </span>
|
||||
<a class="text-xs font-thin" href="https://veeso.dev/en/cookie-policy" target="_blank">Cookie policy</a>
|
||||
<a class="text-xs font-thin" href="https://veeso.me/en/cookie-policy" target="_blank">Cookie policy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||
since last release, so if the latest version is not available yet,
|
||||
you can install it downloading the ZIP file from</span>
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.12.3.nupkg"
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.18.0.nupkg"
|
||||
target="_blank">Github</a>
|
||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||
</p>
|
||||
@@ -59,12 +59,11 @@
|
||||
</h3>
|
||||
<div class="installation">
|
||||
<p>
|
||||
<span translate="getStarted.arch.intro">On Arch Linux based distros, you can install termscp using an AUR
|
||||
package manager such as</span>
|
||||
<a href="https://github.com/Jguer/yay" target="_blank">yay</a>
|
||||
<span translate="getStarted.arch.intro">On Arch Linux based distros, you can install termscp using</span>
|
||||
<a href="https://wiki.archlinux.org/title/pacman" target="_blank">pacman</a>
|
||||
<span translate="getStarted.arch.then">then run:</span>
|
||||
</p>
|
||||
<pre><span class="function">yay</span> -S <span class="string">termscp</span></pre>
|
||||
<pre><span class="function">pacman</span> -S <span class="string">termscp</span></pre>
|
||||
</div>
|
||||
<h3>
|
||||
<i class="devicon-debian-plain"></i> <span translate="getStarted.debian.title">Debian derived
|
||||
@@ -75,21 +74,13 @@
|
||||
On Debian based distros, you can install termscp using the Deb
|
||||
package via:
|
||||
</p>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.12.3_amd64.deb</span>
|
||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.18.0_amd64.deb</span>
|
||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||
</div>
|
||||
<h3>
|
||||
<i class="devicon-redhat-plain"></i> <span translate="getStarted.redhat.title">Redhat derived
|
||||
users</span>
|
||||
</h3>
|
||||
<div class="installation">
|
||||
<p translate="getStarted.redhat.body">
|
||||
On RedHat based distros, you can install termscp using the RPM
|
||||
package via:
|
||||
</p>
|
||||
<pre><span class="function">wget</span> -O termscp.rpm <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp-0.12.3-1.x86_64.rpm</span>
|
||||
sudo <span class="function">rpm</span> -U <span class="string">termscp.rpm</span></pre>
|
||||
</div>
|
||||
<h3>
|
||||
<span>Brew</span>
|
||||
</h3>
|
||||
@@ -166,7 +157,7 @@ sudo <span class="function">rpm</span> -U <span class="string">termscp.rpm</span
|
||||
</p>
|
||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features smb <span class="string">termscp</span></pre>
|
||||
<p translate="getStarted.cargo.noSMB" class="pt-4 pb-2"></p>
|
||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features with-keyring <span class="string">termscp</span></pre>
|
||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features keyring <span class="string">termscp</span></pre>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<section id="intro" class="flex flex-col mx-auto items-center justify-center w-full px-4 dark:bg-brand dark:text-gray-100">
|
||||
<section id="intro"
|
||||
class="flex flex-col mx-auto items-center justify-center w-full px-4 dark:bg-brand dark:text-gray-100">
|
||||
<h1 class="text-3xl text-center font-thin">termscp</h1>
|
||||
<img class="w-[256px] h-auto m-auto" alt="logo" src="assets/images/termscp.webp" />
|
||||
<h2 class="text-xl font-thin text-center py-6" translate="intro.caption">
|
||||
A feature rich terminal UI file transfer and explorer with support for
|
||||
SCP/SFTP/FTP/S3/SMB
|
||||
SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV
|
||||
</h2>
|
||||
<button class="bg-brand hover:bg-gray-800 text-white font-thin text-xl py-2 px-4 rounded-xl max-w-fit">
|
||||
<a href="/get-started.html" class="no-underline" translate="intro.getStarted">Get started →</a>
|
||||
</button>
|
||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||
<p class="text-lg">
|
||||
<span translate="intro.versionAlert">termscp 0.12.3 is NOW out! Download it from</span>
|
||||
<span translate="intro.versionAlert">termscp 0.18.0 is NOW out! Download it from</span>
|
||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -66,7 +67,8 @@
|
||||
<div class="flex flex-row sm:flex-col justify-around gap-12">
|
||||
<div class="hook">
|
||||
<h3 class="text-center text-gray-500 dark:text-gray-100 font-light text-xl">
|
||||
<a href="/get-started.html" class="no-underline hover:underline" translate="intro.footer.getStarted">Get started</a>
|
||||
<a href="/get-started.html" class="no-underline hover:underline" translate="intro.footer.getStarted">Get
|
||||
started</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="hook">
|
||||
@@ -76,7 +78,8 @@
|
||||
</div>
|
||||
<div class="hook">
|
||||
<h3 class="text-center text-gray-500 dark:text-gray-100 font-light text-xl">
|
||||
<a href="/updates.html" class="no-underline hover:underline" translate="intro.footer.updates">Install updates</a>
|
||||
<a href="/updates.html" class="no-underline hover:underline" translate="intro.footer.updates">Install
|
||||
updates</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="css/updates.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="updates" class="flex flex-col mx-auto items-center justify-center w-full px-12 gap-8 dark:text-gray-100">
|
||||
<h1 translate="updates.title" class="text-3xl font-thin">Keeping termscp up to date</h1>
|
||||
@@ -8,7 +9,7 @@
|
||||
<section>
|
||||
<h2 class="text-2xl font-thin">
|
||||
<i class="fa fa-question-circle"></i> <span translate="updates.reasons.title">Why should you install
|
||||
updates</span>
|
||||
updates</span>
|
||||
</h2>
|
||||
<div class="wall-of-text">
|
||||
<p translate="updates.reasons.wallOfText" class="text-gray-700 dark:text-gray-300">
|
||||
@@ -41,7 +42,8 @@
|
||||
</section>
|
||||
<!-- Gui method -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-thin"><i class="fa fa-desktop"></i> <span translate="updates.gui.title">GUI method</span></h2>
|
||||
<h2 class="text-2xl font-thin"><i class="fa fa-desktop"></i> <span translate="updates.gui.title">GUI
|
||||
method</span></h2>
|
||||
<div class="installation">
|
||||
<p translate="updates.gui.body" class="text-gray-700 dark:text-gray-300">
|
||||
The GUI method just consists in starting termscp with no options, you
|
||||
@@ -64,8 +66,8 @@
|
||||
<p>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span translate="updates.gui.pex">
|
||||
If you have previously installed termscp via Deb/RPM package, you
|
||||
may need to use the CLI method running termscp with sudo
|
||||
If you have previously installed termscp via Deb package, you
|
||||
may need to use the CLI method running termscp with sudo
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -73,7 +75,8 @@
|
||||
</section>
|
||||
<!-- CLI method -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-thin"><i class="fa fa-glasses"></i> <span translate="updates.cli.title">CLI method</span></h2>
|
||||
<h2 class="text-2xl font-thin"><i class="fa fa-glasses"></i> <span translate="updates.cli.title">CLI
|
||||
method</span></h2>
|
||||
<div class="installation">
|
||||
<p translate="updates.cli.body" class="text-gray-700 dark:text-gray-300">
|
||||
If you prefer, you can install a new update just using the dedicated
|
||||
@@ -94,4 +97,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
</body>
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
<head>
|
||||
<title>
|
||||
termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/S3/SMB | termscp
|
||||
termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV | termscp
|
||||
</title>
|
||||
<meta property="og:description"
|
||||
content="a WinSCP alternative for Linux and MacOS with support for SCP/SFTP/FTP/S3/SMB. Command line file transfer with user interface compatible with all the operating systems." />
|
||||
content="a WinSCP alternative for Linux and MacOS with support for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV. Command line file transfer with user interface compatible with all the operating systems." />
|
||||
<meta name="description"
|
||||
content="a WinSCP alternative for Linux and MacOS with support for SCP/SFTP/FTP/S3/SMB. Command line file transfer with user interface compatible with all the operating systems." />
|
||||
<meta property="og:title" content="termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/S3/SMB | termscp" />
|
||||
content="a WinSCP alternative for Linux and MacOS with support for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV. Command line file transfer with user interface compatible with all the operating systems." />
|
||||
<meta property="og:title"
|
||||
content="termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV | termscp" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
<body>
|
||||
<div id="layout" class="dark:bg-brand dark:text-gray-100">
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<header id="menu"></header>
|
||||
<main>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"support": "Support me"
|
||||
},
|
||||
"intro": {
|
||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3",
|
||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Get started →",
|
||||
"versionAlert": "termscp 0.12.3 is NOW out! Download it from",
|
||||
"versionAlert": "termscp 0.18.0 is NOW out! Download it from",
|
||||
"here": "here",
|
||||
"features": {
|
||||
"handy": {
|
||||
@@ -62,7 +62,7 @@
|
||||
"noBinary": "Opt for this method instead if binaries for your platform are not available or you want to select features",
|
||||
"arch": {
|
||||
"title": "Arch derived users",
|
||||
"intro": "On Arch Linux based distros, you can install termscp using an AUR package manager such as",
|
||||
"intro": "On Arch Linux based distros, you can install termscp using",
|
||||
"then": "then run"
|
||||
},
|
||||
"debian": {
|
||||
@@ -112,4 +112,4 @@
|
||||
"then": "Once started, you will be prompted whether to install or not the update. Confirm the installation and ta-dah, the new version of termscp should now be available on your machine"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
"support": "Apoyame"
|
||||
},
|
||||
"intro": {
|
||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/S3",
|
||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Para iniciar →",
|
||||
"versionAlert": "termscp 0.12.3 ya está disponible! Descárgalo desde",
|
||||
"versionAlert": "termscp 0.18.0 ya está disponible! Descárgalo desde",
|
||||
"here": "aquì",
|
||||
"features": {
|
||||
"handy": {
|
||||
@@ -112,4 +112,4 @@
|
||||
"then": "Una vez iniciado, se le preguntará si desea instalar o no la actualización. Confirme la instalación y ta-dah, la nueva versión de termscp ahora debería estar disponible en su máquina"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
"support": "Me soutenir"
|
||||
},
|
||||
"intro": {
|
||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/S3",
|
||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "Pour commencer →",
|
||||
"versionAlert": "termscp 0.12.3 est maintenant sorti! Télécharge-le depuis",
|
||||
"versionAlert": "termscp 0.18.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"here": "ici",
|
||||
"features": {
|
||||
"handy": {
|
||||
@@ -112,4 +112,4 @@
|
||||
"then": "Une fois démarré, vous serez invité à installer ou non la mise à jour. Confirmez l'installation et ta-dah, la nouvelle version de termscp devrait maintenant être disponible sur votre machine"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||
"getStarted": "Installa termscp →",
|
||||
"versionAlert": "termscp 0.12.3 è ORA disponbile! Scaricalo da",
|
||||
"versionAlert": "termscp 0.18.0 è ORA disponbile! Scaricalo da",
|
||||
"here": "qui",
|
||||
"features": {
|
||||
"handy": {
|
||||
@@ -112,4 +112,4 @@
|
||||
"then": "Una volta lanciato, se c'è un aggiornamento disponibile ti chiederà se procedere. Conferma e a questo punto dovrebbe installarlo. Se tutto è andato a buon fine, riavviando termscp dovrebbe essere l'ultima versione."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
"support": "支持我"
|
||||
},
|
||||
"intro": {
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/S3",
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "开始 →",
|
||||
"versionAlert": "termscp 0.12.3 现已发布! 从下载",
|
||||
"versionAlert": "termscp 0.18.0 现已发布! 从下载",
|
||||
"here": "这里",
|
||||
"features": {
|
||||
"handy": {
|
||||
@@ -112,4 +112,4 @@
|
||||
"then": "启动后,系统将提示您是否安装更新。 确认安装和 ta-dah,新版本的termscp 现在应该可以在你的机器上使用了"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -2,15 +2,17 @@
|
||||
//!
|
||||
//! `activity_manager` is the module which provides run methods and handling for activities
|
||||
|
||||
// Deps
|
||||
// Namespaces
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
||||
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::host::{HostError, Localhost};
|
||||
use crate::cli::{Remote, RemoteArgs};
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
||||
};
|
||||
use crate::host::HostError;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
@@ -30,6 +32,16 @@ pub enum NextActivity {
|
||||
SetupActivity,
|
||||
}
|
||||
|
||||
pub enum Host {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
pub enum HostParams {
|
||||
HostBridge(HostBridgeParams),
|
||||
Remote(FileTransferParams),
|
||||
}
|
||||
|
||||
/// The activity manager takes care of running activities and handling them until the application has ended
|
||||
pub struct ActivityManager {
|
||||
context: Option<Context>,
|
||||
@@ -38,7 +50,7 @@ pub struct ActivityManager {
|
||||
|
||||
impl ActivityManager {
|
||||
/// Initializes a new Activity Manager
|
||||
pub fn new(ticks: Duration) -> Result<ActivityManager, HostError> {
|
||||
pub fn new(ticks: Duration, keyring: bool) -> Result<ActivityManager, HostError> {
|
||||
// Prepare Context
|
||||
// Initialize configuration client
|
||||
let (config_client, error_config): (ConfigClient, Option<String>) =
|
||||
@@ -49,7 +61,7 @@ impl ActivityManager {
|
||||
(ConfigClient::degraded(), Some(err))
|
||||
}
|
||||
};
|
||||
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client() {
|
||||
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) {
|
||||
Ok(cli) => (cli, None),
|
||||
Err(err) => (None, Some(err)),
|
||||
};
|
||||
@@ -62,10 +74,105 @@ impl ActivityManager {
|
||||
})
|
||||
}
|
||||
|
||||
/// Configure remote args
|
||||
pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> {
|
||||
// Set for host bridge
|
||||
match remote_args.host_bridge {
|
||||
Remote::Bookmark(params) => self.resolve_bookmark_name(
|
||||
Host::HostBridge,
|
||||
¶ms.name,
|
||||
params.password.as_deref(),
|
||||
),
|
||||
Remote::Host(host_params) => self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(
|
||||
host_params.file_transfer_params.protocol,
|
||||
host_params.file_transfer_params.params,
|
||||
)),
|
||||
host_params.password.as_deref(),
|
||||
),
|
||||
Remote::None => {
|
||||
// local dir is remote_args.local_dir if set, otherwise current dir
|
||||
let local_dir = remote_args
|
||||
.local_dir
|
||||
.unwrap_or_else(|| env::current_dir().unwrap());
|
||||
debug!("host bridge is None, setting local dir to {:?}", local_dir,);
|
||||
|
||||
self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(local_dir)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}?;
|
||||
|
||||
// set remote
|
||||
match remote_args.remote {
|
||||
Remote::Bookmark(params) => {
|
||||
self.resolve_bookmark_name(Host::Remote, ¶ms.name, params.password.as_deref())
|
||||
}
|
||||
Remote::Host(host_params) => self.set_host_params(
|
||||
HostParams::Remote(host_params.file_transfer_params),
|
||||
host_params.password.as_deref(),
|
||||
),
|
||||
Remote::None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set file transfer params
|
||||
pub fn set_filetransfer_params(
|
||||
pub fn set_host_params(
|
||||
&mut self,
|
||||
mut params: FileTransferParams,
|
||||
host: HostParams,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let (remote_local_path, remote_remote_path) = match &host {
|
||||
HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let mut remote_params = match &host {
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => {
|
||||
Some((*protocol, protocol_params.clone()))
|
||||
}
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None,
|
||||
HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())),
|
||||
};
|
||||
|
||||
// Put params into the context
|
||||
if let Some((protocol, params)) = remote_params.as_mut() {
|
||||
self.resolve_password_for_protocol_params(*protocol, params, password)?;
|
||||
}
|
||||
|
||||
match host {
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
|
||||
self.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_host_bridge_params(HostBridgeParams::Localhost(path));
|
||||
}
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
|
||||
let (protocol, params) = remote_params.unwrap();
|
||||
self.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
|
||||
}
|
||||
HostParams::Remote(_) => {
|
||||
let (protocol, params) = remote_params.unwrap();
|
||||
let params = FileTransferParams {
|
||||
local_path: remote_local_path,
|
||||
remote_path: remote_remote_path,
|
||||
protocol,
|
||||
params,
|
||||
};
|
||||
self.context.as_mut().unwrap().set_remote_params(params);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_password_for_protocol_params(
|
||||
&mut self,
|
||||
protocol: FileTransferProtocol,
|
||||
params: &mut ProtocolParams,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
// Set password if provided
|
||||
@@ -73,13 +180,13 @@ impl ActivityManager {
|
||||
if let Some(password) = password {
|
||||
params.set_default_secret(password.to_string());
|
||||
} else if matches!(
|
||||
params.protocol,
|
||||
protocol,
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
|
||||
) && params.params.generic_params().is_some()
|
||||
) && params.generic_params().is_some()
|
||||
{
|
||||
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
||||
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
||||
let generic_params = params.params.generic_params().unwrap();
|
||||
let generic_params = params.generic_params().unwrap();
|
||||
if storage
|
||||
.resolve(
|
||||
&generic_params.address,
|
||||
@@ -94,7 +201,7 @@ impl ActivityManager {
|
||||
"storage could not find any suitable key for {}... prompting for password",
|
||||
generic_params.address
|
||||
);
|
||||
self.prompt_password(&mut params)?;
|
||||
self.prompt_password(params)?;
|
||||
} else {
|
||||
debug!(
|
||||
"a key is already set for {}; password is not required",
|
||||
@@ -102,17 +209,19 @@ impl ActivityManager {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.prompt_password(&mut params)?;
|
||||
self.prompt_password(params)?;
|
||||
}
|
||||
}
|
||||
// Put params into the context
|
||||
self.context.as_mut().unwrap().set_ftparams(params);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt user for password to set into params.
|
||||
fn prompt_password(&self, params: &mut FileTransferParams) -> Result<(), String> {
|
||||
match tty::read_secret_from_tty("Password: ") {
|
||||
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
||||
let ctx = self.context.as_mut().unwrap();
|
||||
let prompt = format!("Password for {}: ", params.host_name());
|
||||
|
||||
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
|
||||
Err(err) => Err(format!("Could not read password: {err}")),
|
||||
Ok(Some(secret)) => {
|
||||
debug!(
|
||||
@@ -130,16 +239,28 @@ impl ActivityManager {
|
||||
/// Returns error if bookmark is not found
|
||||
pub fn resolve_bookmark_name(
|
||||
&mut self,
|
||||
host: Host,
|
||||
bookmark_name: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
|
||||
match bookmarks_client.get_bookmark(bookmark_name) {
|
||||
None => Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
)),
|
||||
Some(params) => self.set_filetransfer_params(params, password),
|
||||
}
|
||||
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
||||
None => {
|
||||
return Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
));
|
||||
}
|
||||
Some(params) => params,
|
||||
};
|
||||
|
||||
let params = match host {
|
||||
Host::Remote => HostParams::Remote(params),
|
||||
Host::HostBridge => {
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params))
|
||||
}
|
||||
};
|
||||
|
||||
self.set_host_params(params, password)
|
||||
} else {
|
||||
Err(String::from(
|
||||
"Could not resolve bookmark name: bookmarks client not initialized",
|
||||
@@ -233,8 +354,17 @@ impl ActivityManager {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let host_bridge_params = match ctx.host_bridge_params() {
|
||||
Some(params) => params.clone(),
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: host bridge params is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// If ft params is None, return None
|
||||
let ft_params: &FileTransferParams = match ctx.ft_params() {
|
||||
let remote_params: &FileTransferParams = match ctx.remote_params() {
|
||||
Some(ft_params) => ft_params,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||
@@ -242,28 +372,18 @@ impl ActivityManager {
|
||||
}
|
||||
};
|
||||
|
||||
// get local path:
|
||||
// - if set in file transfer params, get it from there
|
||||
// - otherwise is env current dir
|
||||
// - otherwise is /
|
||||
let local_wrkdir = ft_params
|
||||
.local_path
|
||||
.clone()
|
||||
.or(std::env::current_dir().ok())
|
||||
.unwrap_or(PathBuf::from("/"));
|
||||
|
||||
// Prepare activity
|
||||
let host: Localhost = match Localhost::new(local_wrkdir) {
|
||||
Ok(host) => host,
|
||||
Err(err) => {
|
||||
// Set error in context
|
||||
error!("Failed to initialize localhost: {}", err);
|
||||
ctx.set_error(format!("Could not initialize localhost: {err}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut activity: FileTransferActivity =
|
||||
FileTransferActivity::new(host, ft_params, self.ticks);
|
||||
// try to setup activity
|
||||
let mut activity =
|
||||
match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
|
||||
Ok(activity) => activity,
|
||||
Err(err) => {
|
||||
error!("Failed to start FileTransferActivity: {}", err);
|
||||
ctx.set_error(err);
|
||||
self.context = Some(ctx);
|
||||
// Return to authentication
|
||||
return Some(NextActivity::Authentication);
|
||||
}
|
||||
};
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Create activity
|
||||
@@ -327,7 +447,7 @@ impl ActivityManager {
|
||||
|
||||
// -- misc
|
||||
|
||||
fn init_bookmarks_client() -> Result<Option<BookmarksClient>, String> {
|
||||
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
|
||||
// Get config dir
|
||||
match environment::init_config_dir() {
|
||||
Ok(path) => {
|
||||
@@ -336,16 +456,21 @@ impl ActivityManager {
|
||||
let bookmarks_file: PathBuf =
|
||||
environment::get_bookmarks_paths(config_dir_path.as_path());
|
||||
// Initialize client
|
||||
BookmarksClient::new(bookmarks_file.as_path(), config_dir_path.as_path(), 16)
|
||||
.map(Option::Some)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
bookmarks_file.display(),
|
||||
config_dir_path.display(),
|
||||
e
|
||||
)
|
||||
})
|
||||
BookmarksClient::new(
|
||||
bookmarks_file.as_path(),
|
||||
config_dir_path.as_path(),
|
||||
16,
|
||||
keyring,
|
||||
)
|
||||
.map(Option::Some)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
bookmarks_file.display(),
|
||||
config_dir_path.display(),
|
||||
e
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -390,19 +515,28 @@ impl ActivityManager {
|
||||
match ThemeProvider::new(theme_path.as_path()) {
|
||||
Ok(provider) => provider,
|
||||
Err(err) => {
|
||||
error!("Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err);
|
||||
error!(
|
||||
"Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode",
|
||||
theme_path.display(),
|
||||
err
|
||||
);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("This system doesn't provide a configuration directory; using theme provider in degraded mode");
|
||||
error!(
|
||||
"This system doesn't provide a configuration directory; using theme provider in degraded mode"
|
||||
);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not initialize configuration directory: {}; using theme provider in degraded mode", err);
|
||||
error!(
|
||||
"Could not initialize configuration directory: {}; using theme provider in degraded mode",
|
||||
err
|
||||
);
|
||||
ThemeProvider::degraded()
|
||||
}
|
||||
}
|
||||
|
||||
139
src/cli.rs
Normal file
139
src/cli.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
//! ## CLI opts
|
||||
//!
|
||||
//! defines the types for main.rs types
|
||||
|
||||
mod remote;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use argh::FromArgs;
|
||||
pub use remote::{Remote, RemoteArgs};
|
||||
|
||||
use crate::activity_manager::NextActivity;
|
||||
use crate::system::logging::LogLevel;
|
||||
|
||||
pub enum Task {
|
||||
Activity(NextActivity),
|
||||
ImportTheme(PathBuf),
|
||||
InstallUpdate,
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(Default, FromArgs)]
|
||||
#[argh(description = "
|
||||
where positional can be:
|
||||
- [address_a] [address_b] [local-wrkdir]
|
||||
OR
|
||||
- -b [bookmark-name_1] -b [bookmark-name_2] [local-wrkdir]
|
||||
|
||||
and any combination of the above
|
||||
|
||||
Address syntax can be:
|
||||
|
||||
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
|
||||
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
|
||||
- `\\\\<server>[:port]\\<share>[\\path]` for SMB (on Windows)
|
||||
- `smb://[user@]<server>[:port]</share>[/path]` for SMB (on other systems)
|
||||
|
||||
Please, report issues to <https://github.com/veeso/termscp>
|
||||
Please, consider supporting the author <https://ko-fi.com/veeso>")]
|
||||
pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub nested: Option<ArgsSubcommands>,
|
||||
/// resolve address argument as a bookmark name
|
||||
#[argh(option, short = 'b')]
|
||||
pub bookmark: Vec<String>,
|
||||
/// enable TRACE log level
|
||||
#[argh(switch, short = 'D')]
|
||||
pub debug: bool,
|
||||
/// provide password from CLI; if you need to provide multiple passwords, use multiple -P flags.
|
||||
/// In case just respect the order of the addresses
|
||||
#[argh(option, short = 'P')]
|
||||
pub password: Vec<String>,
|
||||
/// disable logging
|
||||
#[argh(switch, short = 'q')]
|
||||
pub quiet: bool,
|
||||
/// set UI ticks; default 10ms
|
||||
#[argh(option, short = 'T', default = "10")]
|
||||
pub ticks: u64,
|
||||
/// print version
|
||||
#[argh(switch, short = 'v')]
|
||||
pub version: bool,
|
||||
/// disable keyring support
|
||||
#[argh(switch)]
|
||||
pub wno_keyring: bool,
|
||||
// -- positional
|
||||
#[argh(positional, description = "address1 address2 local-wrkdir")]
|
||||
pub positional: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand)]
|
||||
pub enum ArgsSubcommands {
|
||||
Config(ConfigArgs),
|
||||
LoadTheme(LoadThemeArgs),
|
||||
Update(UpdateArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// open termscp configuration
|
||||
#[argh(subcommand, name = "config")]
|
||||
pub struct ConfigArgs {}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// update termscp to the latest version
|
||||
#[argh(subcommand, name = "update")]
|
||||
pub struct UpdateArgs {}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// import the specified theme
|
||||
#[argh(subcommand, name = "theme")]
|
||||
pub struct LoadThemeArgs {
|
||||
#[argh(positional)]
|
||||
/// theme file
|
||||
pub theme: PathBuf,
|
||||
}
|
||||
|
||||
pub struct RunOpts {
|
||||
pub remote: RemoteArgs,
|
||||
pub keyring: bool,
|
||||
pub ticks: Duration,
|
||||
pub log_level: LogLevel,
|
||||
pub task: Task,
|
||||
}
|
||||
|
||||
impl RunOpts {
|
||||
pub fn config() -> Self {
|
||||
Self {
|
||||
task: Task::Activity(NextActivity::SetupActivity),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update() -> Self {
|
||||
Self {
|
||||
task: Task::InstallUpdate,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_theme(theme: PathBuf) -> Self {
|
||||
Self {
|
||||
task: Task::ImportTheme(theme),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RunOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
remote: RemoteArgs::default(),
|
||||
ticks: Duration::from_millis(10),
|
||||
keyring: true,
|
||||
log_level: LogLevel::Info,
|
||||
task: Task::Activity(NextActivity::Authentication),
|
||||
}
|
||||
}
|
||||
}
|
||||
271
src/cli/remote.rs
Normal file
271
src/cli/remote.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::Args;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils;
|
||||
|
||||
/// Address type
|
||||
enum AddrType {
|
||||
Address,
|
||||
Bookmark,
|
||||
}
|
||||
|
||||
/// Args for remote connection
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteArgs {
|
||||
pub host_bridge: Remote,
|
||||
pub remote: Remote,
|
||||
pub local_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for RemoteArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host_bridge: Remote::None,
|
||||
remote: Remote::None,
|
||||
local_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Args> for RemoteArgs {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(args: &Args) -> Result<Self, Self::Error> {
|
||||
let mut remote_args = RemoteArgs::default();
|
||||
// validate arguments
|
||||
match (args.bookmark.len(), args.positional.len()) {
|
||||
(0, positional) if positional < 4 => Ok(()),
|
||||
(1, positional) if positional < 3 => Ok(()),
|
||||
(2, positional) if positional < 2 => Ok(()),
|
||||
(_, _) => Err("Too many arguments".to_string()),
|
||||
}?;
|
||||
// parse bookmark first
|
||||
let last_item_index = (args.bookmark.len() + args.positional.len())
|
||||
.checked_sub(1)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut hosts = vec![];
|
||||
|
||||
for (i, (addr_type, arg)) in args
|
||||
.bookmark
|
||||
.iter()
|
||||
.map(|x| (AddrType::Bookmark, x))
|
||||
.chain(args.positional.iter().map(|x| (AddrType::Address, x)))
|
||||
.enumerate()
|
||||
{
|
||||
// check if has password
|
||||
let password = args.password.get(i).cloned();
|
||||
|
||||
// check if is last item and so a possible local dir
|
||||
if i == last_item_index && Path::new(arg).exists() {
|
||||
remote_args.local_dir = Some(PathBuf::from(arg));
|
||||
continue;
|
||||
}
|
||||
|
||||
let remote = match addr_type {
|
||||
AddrType::Address => Self::parse_remote_address(arg)
|
||||
.map(|x| Remote::Host(HostParams::new(x, password)))?,
|
||||
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
|
||||
};
|
||||
|
||||
// set remote
|
||||
hosts.push(remote);
|
||||
}
|
||||
|
||||
// set args based on hosts len
|
||||
if hosts.len() == 1 {
|
||||
remote_args.remote = hosts.pop().unwrap();
|
||||
} else if hosts.len() == 2 {
|
||||
remote_args.host_bridge = hosts.pop().unwrap();
|
||||
remote_args.remote = hosts.pop().unwrap();
|
||||
}
|
||||
|
||||
Ok(remote_args)
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteArgs {
|
||||
/// Parse remote address
|
||||
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
|
||||
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remote argument type
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum Remote {
|
||||
/// Bookmark name argument
|
||||
Bookmark(BookmarkParams),
|
||||
/// Host argument
|
||||
Host(HostParams),
|
||||
/// Unspecified
|
||||
None,
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bookmark parameters
|
||||
#[derive(Debug)]
|
||||
pub struct BookmarkParams {
|
||||
/// bookmark name
|
||||
pub name: String,
|
||||
/// bookmark password
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
/// Host parameters
|
||||
#[derive(Debug)]
|
||||
pub struct HostParams {
|
||||
/// file transfer parameters
|
||||
pub file_transfer_params: FileTransferParams,
|
||||
/// host password specified in arguments
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl BookmarkParams {
|
||||
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().to_string(),
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostParams {
|
||||
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
|
||||
Self {
|
||||
file_transfer_params: params,
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_one_remote() {
|
||||
let args = Args {
|
||||
positional: vec!["scp://host1".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::None));
|
||||
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_two_remotes() {
|
||||
let args = Args {
|
||||
positional: vec!["scp://host1".to_string(), "scp://host2".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn test_should_make_remote_args_from_two_remotes_and_local_dir() {
|
||||
let args = Args {
|
||||
positional: vec![
|
||||
"scp://host1".to_string(),
|
||||
"scp://host2".to_string(),
|
||||
"/home".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
||||
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_one_bookmarks() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::None));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_two_bookmarks() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string(), "bar".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn test_should_make_remote_args_from_two_bookmarks_and_local_dir() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string(), "bar".to_string()],
|
||||
positional: vec!["/home".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_one_bookmark_and_one_remote() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string()],
|
||||
positional: vec!["scp://host1".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn test_should_make_remote_args_from_one_bookmark_and_one_remote_with_local_dir() {
|
||||
let args = Args {
|
||||
positional: vec!["scp://host1".to_string(), "/home".to_string()],
|
||||
bookmark: vec!["foo".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
||||
}
|
||||
}
|
||||
127
src/cli_opts.rs
127
src/cli_opts.rs
@@ -1,127 +0,0 @@
|
||||
//! ## CLI opts
|
||||
//!
|
||||
//! defines the types for main.rs types
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use argh::FromArgs;
|
||||
|
||||
use crate::activity_manager::NextActivity;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::system::logging::LogLevel;
|
||||
|
||||
pub enum Task {
|
||||
Activity(NextActivity),
|
||||
ImportTheme(PathBuf),
|
||||
InstallUpdate,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(description = "
|
||||
where positional can be:
|
||||
- [address] [local-wrkdir]
|
||||
OR
|
||||
- [bookmark-Name] [local-wrkdir]
|
||||
|
||||
Address syntax can be:
|
||||
|
||||
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
|
||||
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
|
||||
- `\\\\<server>[:port]\\<share>[\\path]` for SMB (on Windows)
|
||||
- `smb://[user@]<server>[:port]</share>[/path]` for SMB (on other systems)
|
||||
|
||||
Please, report issues to <https://github.com/veeso/termscp>
|
||||
Please, consider supporting the author <https://ko-fi.com/veeso>")]
|
||||
pub struct Args {
|
||||
#[argh(
|
||||
switch,
|
||||
short = 'b',
|
||||
description = "resolve address argument as a bookmark name"
|
||||
)]
|
||||
pub address_as_bookmark: bool,
|
||||
#[argh(switch, short = 'c', description = "open termscp configuration")]
|
||||
pub config: bool,
|
||||
#[argh(switch, short = 'D', description = "enable TRACE log level")]
|
||||
pub debug: bool,
|
||||
#[argh(option, short = 'P', description = "provide password from CLI")]
|
||||
pub password: Option<String>,
|
||||
#[argh(switch, short = 'q', description = "disable logging")]
|
||||
pub quiet: bool,
|
||||
#[argh(option, short = 't', description = "import specified theme")]
|
||||
pub theme: Option<String>,
|
||||
#[argh(
|
||||
switch,
|
||||
short = 'u',
|
||||
description = "update termscp to the latest version"
|
||||
)]
|
||||
pub update: bool,
|
||||
#[argh(
|
||||
option,
|
||||
short = 'T',
|
||||
default = "10",
|
||||
description = "set UI ticks; default 10ms"
|
||||
)]
|
||||
pub ticks: u64,
|
||||
#[argh(switch, short = 'v', description = "print version")]
|
||||
pub version: bool,
|
||||
// -- positional
|
||||
#[argh(
|
||||
positional,
|
||||
description = "protocol://user@address:port:wrkdir local-wrkdir"
|
||||
)]
|
||||
pub positional: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct RunOpts {
|
||||
pub remote: Remote,
|
||||
pub ticks: Duration,
|
||||
pub log_level: LogLevel,
|
||||
pub task: Task,
|
||||
}
|
||||
|
||||
impl Default for RunOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
remote: Remote::None,
|
||||
ticks: Duration::from_millis(10),
|
||||
log_level: LogLevel::Info,
|
||||
task: Task::Activity(NextActivity::Authentication),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Remote {
|
||||
Bookmark(BookmarkParams),
|
||||
Host(HostParams),
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct BookmarkParams {
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
pub struct HostParams {
|
||||
pub params: FileTransferParams,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl BookmarkParams {
|
||||
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().to_string(),
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostParams {
|
||||
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
|
||||
Self {
|
||||
params,
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
//!
|
||||
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
|
||||
|
||||
mod aws_s3;
|
||||
mod kube;
|
||||
mod smb;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
@@ -9,8 +13,12 @@ use std::str::FromStr;
|
||||
use serde::de::Error as DeError;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub use self::aws_s3::S3Params;
|
||||
pub use self::kube::KubeParams;
|
||||
pub use self::smb::SmbParams;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, ProtocolParams, SmbParams as TransferSmbParams,
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams,
|
||||
SmbParams as TransferSmbParams, WebDAVProtocolParams,
|
||||
};
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
|
||||
@@ -43,32 +51,14 @@ pub struct Bookmark {
|
||||
pub remote_path: Option<PathBuf>,
|
||||
/// local folder to open at startup
|
||||
pub local_path: Option<PathBuf>,
|
||||
/// Kube params; optional. When used other fields are empty for sure
|
||||
pub kube: Option<KubeParams>,
|
||||
/// S3 params; optional. When used other fields are empty for sure
|
||||
pub s3: Option<S3Params>,
|
||||
/// SMB params; optional. Extra params required for SMB protocol
|
||||
pub smb: Option<SmbParams>,
|
||||
}
|
||||
|
||||
/// Connection parameters for Aws s3 protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct S3Params {
|
||||
pub bucket: String,
|
||||
pub region: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub profile: Option<String>,
|
||||
pub access_key: Option<String>,
|
||||
pub secret_access_key: Option<String>,
|
||||
/// NOTE: there are no session token and security token since they are always temporary
|
||||
pub new_path_style: Option<bool>,
|
||||
}
|
||||
|
||||
/// Extra Connection parameters for SMB protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct SmbParams {
|
||||
pub share: String,
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
// -- impls
|
||||
|
||||
impl From<FileTransferParams> for Bookmark {
|
||||
@@ -86,6 +76,7 @@ impl From<FileTransferParams> for Bookmark {
|
||||
password: params.password,
|
||||
remote_path,
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
},
|
||||
@@ -97,23 +88,49 @@ impl From<FileTransferParams> for Bookmark {
|
||||
password: None,
|
||||
remote_path,
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: Some(S3Params::from(params)),
|
||||
smb: None,
|
||||
},
|
||||
ProtocolParams::Kube(params) => Self {
|
||||
protocol,
|
||||
address: None,
|
||||
port: None,
|
||||
username: None,
|
||||
password: None,
|
||||
remote_path,
|
||||
local_path,
|
||||
kube: Some(KubeParams::from(params)),
|
||||
s3: None,
|
||||
smb: None,
|
||||
},
|
||||
ProtocolParams::Smb(params) => Self {
|
||||
smb: Some(SmbParams::from(params.clone())),
|
||||
protocol,
|
||||
address: Some(params.address),
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
port: Some(params.port),
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
port: None,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
remote_path,
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
},
|
||||
ProtocolParams::WebDAV(parms) => Self {
|
||||
protocol,
|
||||
address: Some(parms.uri),
|
||||
port: None,
|
||||
username: Some(parms.username),
|
||||
password: Some(parms.password),
|
||||
remote_path,
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +154,12 @@ impl From<Bookmark> for FileTransferParams {
|
||||
.password(bookmark.password);
|
||||
Self::new(bookmark.protocol, ProtocolParams::Generic(params))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
FileTransferProtocol::Kube => {
|
||||
let params = bookmark.kube.unwrap_or_default();
|
||||
let params = KubeProtocolParams::from(params);
|
||||
Self::new(bookmark.protocol, ProtocolParams::Kube(params))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
FileTransferProtocol::Smb => {
|
||||
let params = TransferSmbParams::new(
|
||||
bookmark.address.unwrap_or_default(),
|
||||
@@ -150,7 +172,7 @@ impl From<Bookmark> for FileTransferParams {
|
||||
|
||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||
}
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
FileTransferProtocol::Smb => {
|
||||
let params = TransferSmbParams::new(
|
||||
bookmark.address.unwrap_or_default(),
|
||||
@@ -161,56 +183,20 @@ impl From<Bookmark> for FileTransferParams {
|
||||
|
||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||
}
|
||||
FileTransferProtocol::WebDAV => Self::new(
|
||||
FileTransferProtocol::WebDAV,
|
||||
ProtocolParams::WebDAV(WebDAVProtocolParams {
|
||||
uri: bookmark.address.unwrap_or_default(),
|
||||
username: bookmark.username.unwrap_or_default(),
|
||||
password: bookmark.password.unwrap_or_default(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
.remote_path(bookmark.remote_path) // Set entry remote_path
|
||||
.local_path(bookmark.local_path) // Set entry local path
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AwsS3Params> for S3Params {
|
||||
fn from(params: AwsS3Params) -> Self {
|
||||
S3Params {
|
||||
bucket: params.bucket_name,
|
||||
region: params.region,
|
||||
endpoint: params.endpoint,
|
||||
profile: params.profile,
|
||||
access_key: params.access_key,
|
||||
secret_access_key: params.secret_access_key,
|
||||
new_path_style: Some(params.new_path_style),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<S3Params> for AwsS3Params {
|
||||
fn from(params: S3Params) -> Self {
|
||||
AwsS3Params::new(params.bucket, params.region, params.profile)
|
||||
.endpoint(params.endpoint)
|
||||
.access_key(params.access_key)
|
||||
.secret_access_key(params.secret_access_key)
|
||||
.new_path_style(params.new_path_style.unwrap_or(false))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl From<TransferSmbParams> for SmbParams {
|
||||
fn from(params: TransferSmbParams) -> Self {
|
||||
Self {
|
||||
share: params.share,
|
||||
workgroup: params.workgroup,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl From<TransferSmbParams> for SmbParams {
|
||||
fn from(params: TransferSmbParams) -> Self {
|
||||
Self {
|
||||
share: params.share,
|
||||
workgroup: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_protocol<'de, D>(deserializer: D) -> Result<FileTransferProtocol, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -256,6 +242,7 @@ mod tests {
|
||||
password: Some(String::from("password")),
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
};
|
||||
@@ -267,6 +254,7 @@ mod tests {
|
||||
password: Some(String::from("password")),
|
||||
remote_path: Some(PathBuf::from("/home")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
};
|
||||
@@ -360,6 +348,34 @@ mod tests {
|
||||
assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookmark_from_kube_ftparams() {
|
||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||
namespace: Some("default".to_string()),
|
||||
username: Some("root".to_string()),
|
||||
cluster_url: Some("https://localhost:6443".to_string()),
|
||||
client_cert: Some("cert".to_string()),
|
||||
client_key: Some("key".to_string()),
|
||||
});
|
||||
let params: FileTransferParams =
|
||||
FileTransferParams::new(FileTransferProtocol::Kube, params);
|
||||
let bookmark = Bookmark::from(params);
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::Kube);
|
||||
assert!(bookmark.address.is_none());
|
||||
assert!(bookmark.port.is_none());
|
||||
assert!(bookmark.username.is_none());
|
||||
assert!(bookmark.password.is_none());
|
||||
let kube: &KubeParams = bookmark.kube.as_ref().unwrap();
|
||||
assert_eq!(kube.namespace.as_deref().unwrap(), "default");
|
||||
assert_eq!(
|
||||
kube.cluster_url.as_deref().unwrap(),
|
||||
"https://localhost:6443"
|
||||
);
|
||||
assert_eq!(kube.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(kube.client_cert.as_deref().unwrap(), "cert");
|
||||
assert_eq!(kube.client_key.as_deref().unwrap(), "key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftparams_from_generic_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
@@ -370,6 +386,7 @@ mod tests {
|
||||
password: Some(String::from("password")),
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
};
|
||||
@@ -390,6 +407,36 @@ mod tests {
|
||||
assert_eq!(gparams.password.as_deref().unwrap(), "password");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftparams_from_webdav() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
address: Some(String::from("192.168.1.1")),
|
||||
port: None,
|
||||
protocol: FileTransferProtocol::WebDAV,
|
||||
username: Some(String::from("root")),
|
||||
password: Some(String::from("password")),
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::WebDAV);
|
||||
assert_eq!(
|
||||
params.remote_path.as_deref().unwrap(),
|
||||
std::path::Path::new("/tmp")
|
||||
);
|
||||
assert_eq!(
|
||||
params.local_path.as_deref().unwrap(),
|
||||
std::path::Path::new("/usr")
|
||||
);
|
||||
let gparams = params.params.webdav_params().unwrap();
|
||||
assert_eq!(gparams.uri.as_str(), "192.168.1.1");
|
||||
assert_eq!(gparams.username, "root");
|
||||
assert_eq!(gparams.password, "password");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftparams_from_s3_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
@@ -400,6 +447,7 @@ mod tests {
|
||||
password: None,
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: Some(S3Params {
|
||||
bucket: String::from("veeso"),
|
||||
region: Some(String::from("eu-west-1")),
|
||||
@@ -432,7 +480,48 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn ftparams_from_kube_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
protocol: FileTransferProtocol::Kube,
|
||||
address: None,
|
||||
port: None,
|
||||
username: None,
|
||||
password: None,
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: Some(KubeParams {
|
||||
namespace: Some(String::from("default")),
|
||||
cluster_url: Some(String::from("https://localhost:6443")),
|
||||
username: Some(String::from("root")),
|
||||
client_cert: Some(String::from("cert")),
|
||||
client_key: Some(String::from("key")),
|
||||
}),
|
||||
s3: None,
|
||||
smb: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Kube);
|
||||
assert_eq!(
|
||||
params.remote_path.as_deref().unwrap(),
|
||||
std::path::Path::new("/tmp")
|
||||
);
|
||||
assert_eq!(
|
||||
params.local_path.as_deref().unwrap(),
|
||||
std::path::Path::new("/usr")
|
||||
);
|
||||
let gparams = params.params.kube_params().unwrap();
|
||||
assert_eq!(gparams.namespace.as_deref().unwrap(), "default");
|
||||
assert_eq!(
|
||||
gparams.cluster_url.as_deref().unwrap(),
|
||||
"https://localhost:6443"
|
||||
);
|
||||
assert_eq!(gparams.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(gparams.client_cert.as_deref().unwrap(), "cert");
|
||||
assert_eq!(gparams.client_key.as_deref().unwrap(), "key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn should_get_ftparams_from_smb_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
protocol: FileTransferProtocol::Smb,
|
||||
@@ -442,6 +531,7 @@ mod tests {
|
||||
password: Some("bar".to_string()),
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
@@ -469,7 +559,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
fn should_get_ftparams_from_smb_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
protocol: FileTransferProtocol::Smb,
|
||||
@@ -480,6 +570,7 @@ mod tests {
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
s3: None,
|
||||
kube: None,
|
||||
smb: Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
workgroup: None,
|
||||
|
||||
40
src/config/bookmarks/aws_s3.rs
Normal file
40
src/config/bookmarks/aws_s3.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::AwsS3Params;
|
||||
|
||||
/// Connection parameters for Aws s3 protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct S3Params {
|
||||
pub bucket: String,
|
||||
pub region: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub profile: Option<String>,
|
||||
pub access_key: Option<String>,
|
||||
pub secret_access_key: Option<String>,
|
||||
/// NOTE: there are no session token and security token since they are always temporary
|
||||
pub new_path_style: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<AwsS3Params> for S3Params {
|
||||
fn from(params: AwsS3Params) -> Self {
|
||||
S3Params {
|
||||
bucket: params.bucket_name,
|
||||
region: params.region,
|
||||
endpoint: params.endpoint,
|
||||
profile: params.profile,
|
||||
access_key: params.access_key,
|
||||
secret_access_key: params.secret_access_key,
|
||||
new_path_style: Some(params.new_path_style),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<S3Params> for AwsS3Params {
|
||||
fn from(params: S3Params) -> Self {
|
||||
AwsS3Params::new(params.bucket, params.region, params.profile)
|
||||
.endpoint(params.endpoint)
|
||||
.access_key(params.access_key)
|
||||
.secret_access_key(params.secret_access_key)
|
||||
.new_path_style(params.new_path_style.unwrap_or(false))
|
||||
}
|
||||
}
|
||||
37
src/config/bookmarks/kube.rs
Normal file
37
src/config/bookmarks/kube.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::KubeProtocolParams;
|
||||
|
||||
/// Extra Connection parameters for Kube protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct KubeParams {
|
||||
pub namespace: Option<String>,
|
||||
pub cluster_url: Option<String>,
|
||||
pub username: Option<String>,
|
||||
pub client_cert: Option<String>,
|
||||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
impl From<KubeParams> for KubeProtocolParams {
|
||||
fn from(value: KubeParams) -> Self {
|
||||
Self {
|
||||
namespace: value.namespace,
|
||||
cluster_url: value.cluster_url,
|
||||
username: value.username,
|
||||
client_cert: value.client_cert,
|
||||
client_key: value.client_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KubeProtocolParams> for KubeParams {
|
||||
fn from(value: KubeProtocolParams) -> Self {
|
||||
Self {
|
||||
namespace: value.namespace,
|
||||
cluster_url: value.cluster_url,
|
||||
username: value.username,
|
||||
client_cert: value.client_cert,
|
||||
client_key: value.client_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/config/bookmarks/smb.rs
Normal file
30
src/config/bookmarks/smb.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||
|
||||
/// Extra Connection parameters for SMB protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct SmbParams {
|
||||
pub share: String,
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl From<TransferSmbParams> for SmbParams {
|
||||
fn from(params: TransferSmbParams) -> Self {
|
||||
Self {
|
||||
share: params.share,
|
||||
workgroup: params.workgroup,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(win)]
|
||||
impl From<TransferSmbParams> for SmbParams {
|
||||
fn from(params: TransferSmbParams) -> Self {
|
||||
Self {
|
||||
share: params.share,
|
||||
workgroup: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
//! `config` is the module which provides access to all the termscp configurations
|
||||
|
||||
// export
|
||||
pub use params::*;
|
||||
|
||||
pub mod bookmarks;
|
||||
pub mod params;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Contains the error for serializer/deserializer
|
||||
@@ -63,7 +63,7 @@ where
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::Serialization,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
trace!("Serialized new bookmarks data: {}", data);
|
||||
@@ -112,10 +112,10 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use super::*;
|
||||
use crate::config::bookmarks::{Bookmark, S3Params, SmbParams, UserHosts};
|
||||
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
||||
use crate::config::params::UserConfig;
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
@@ -366,7 +366,7 @@ mod tests {
|
||||
assert_eq!(host.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(host.password, None);
|
||||
// Verify bookmarks
|
||||
assert_eq!(hosts.bookmarks.len(), 5);
|
||||
assert_eq!(hosts.bookmarks.len(), 6);
|
||||
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
|
||||
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
|
||||
assert_eq!(host.port.unwrap(), 22);
|
||||
@@ -404,19 +404,32 @@ mod tests {
|
||||
assert_eq!(s3.access_key.as_deref().unwrap(), "pippo");
|
||||
assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto");
|
||||
assert_eq!(s3.new_path_style.unwrap(), true);
|
||||
// Kube pod
|
||||
let host: &Bookmark = hosts.bookmarks.get("pod").unwrap();
|
||||
assert_eq!(host.address, None);
|
||||
assert_eq!(host.port, None);
|
||||
assert_eq!(host.username, None);
|
||||
assert_eq!(host.password, None);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::Kube);
|
||||
let kube = host.kube.as_ref().unwrap();
|
||||
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
|
||||
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
|
||||
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
|
||||
assert_eq!(kube.client_cert.as_deref().unwrap(), "my-cert");
|
||||
assert_eq!(kube.client_key.as_deref().unwrap(), "my-key");
|
||||
|
||||
// smb
|
||||
let host = hosts.bookmarks.get("smb").unwrap();
|
||||
assert_eq!(host.address.as_deref().unwrap(), "localhost");
|
||||
assert_eq!(host.port.unwrap(), 445);
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(host.username.as_deref().unwrap(), "test");
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(host.password.as_deref().unwrap(), "test");
|
||||
|
||||
let smb = host.smb.as_ref().unwrap();
|
||||
assert_eq!(smb.share.as_str(), "temp");
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
|
||||
}
|
||||
|
||||
@@ -443,6 +456,7 @@ mod tests {
|
||||
password: None,
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
},
|
||||
@@ -457,6 +471,7 @@ mod tests {
|
||||
password: Some(String::from("password")),
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
},
|
||||
@@ -480,9 +495,33 @@ mod tests {
|
||||
secret_access_key: None,
|
||||
new_path_style: None,
|
||||
}),
|
||||
kube: None,
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
// push kube pod
|
||||
bookmarks.insert(
|
||||
String::from("pod"),
|
||||
Bookmark {
|
||||
address: None,
|
||||
port: None,
|
||||
protocol: FileTransferProtocol::Kube,
|
||||
username: None,
|
||||
password: None,
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
s3: None,
|
||||
smb: None,
|
||||
kube: Some(KubeParams {
|
||||
namespace: Some("my-namespace".to_string()),
|
||||
cluster_url: Some("https://my-cluster".to_string()),
|
||||
username: Some("my-username".to_string()),
|
||||
client_cert: Some("my-cert".to_string()),
|
||||
client_key: Some("my-key".to_string()),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
let smb_params: Option<SmbParams> = Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
workgroup: None,
|
||||
@@ -498,6 +537,7 @@ mod tests {
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
s3: None,
|
||||
kube: None,
|
||||
smb: smb_params,
|
||||
},
|
||||
);
|
||||
@@ -513,6 +553,7 @@ mod tests {
|
||||
remote_path: Some(PathBuf::from("/tmp")),
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
s3: None,
|
||||
kube: None,
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
@@ -548,6 +589,22 @@ mod tests {
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_deserialize_v14_pod_bookmark() {
|
||||
let toml = create_v14_pod_bookmark();
|
||||
toml.as_file().sync_all().unwrap();
|
||||
toml.as_file().rewind().unwrap();
|
||||
let deserialized: UserHosts = deserialize(Box::new(toml)).unwrap();
|
||||
let kube = deserialized.bookmarks.get("pod").unwrap();
|
||||
assert_eq!(kube.protocol, FileTransferProtocol::Kube);
|
||||
let kube = kube.kube.as_ref().unwrap();
|
||||
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
|
||||
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
|
||||
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
|
||||
assert_eq!(kube.client_cert.as_deref().unwrap(), "my-cert");
|
||||
assert_eq!(kube.client_key.as_deref().unwrap(), "my-key");
|
||||
}
|
||||
|
||||
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
@@ -569,6 +626,15 @@ mod tests {
|
||||
secret_access_key = "pluto"
|
||||
new_path_style = true
|
||||
|
||||
[bookmarks.pod]
|
||||
protocol = "KUBE"
|
||||
[bookmarks.pod.kube]
|
||||
namespace = "my-namespace"
|
||||
cluster_url = "https://my-cluster"
|
||||
username = "my-username"
|
||||
client_cert = "my-cert"
|
||||
client_key = "my-key"
|
||||
|
||||
[bookmarks.smb]
|
||||
protocol = "SMB"
|
||||
address = "localhost"
|
||||
@@ -588,6 +654,29 @@ mod tests {
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
|
||||
[bookmarks.pod]
|
||||
protocol = "KUBE"
|
||||
[bookmarks.pod.kube]
|
||||
pod_name = "my-pod"
|
||||
container = "my-container"
|
||||
namespace = "my-namespace"
|
||||
cluster_url = "https://my-cluster"
|
||||
username = "my-username"
|
||||
client_cert = "my-cert"
|
||||
client_key = "my-key"
|
||||
|
||||
[recents]
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// ext
|
||||
use serde::de::Error as DeError;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use crate::utils::fmt::fmt_color;
|
||||
use crate::utils::parser::parse_color;
|
||||
|
||||
@@ -11,8 +11,8 @@ use bytesize::ByteSize;
|
||||
use lazy_regex::{Lazy, Regex};
|
||||
use remotefs::File;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(unix)]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
#[cfg(posix)]
|
||||
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||
|
||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||
use crate::utils::path::diff_paths;
|
||||
@@ -211,7 +211,7 @@ impl Formatter {
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get username
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
let group: String = match fsentry.metadata().gid {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
@@ -219,7 +219,7 @@ impl Formatter {
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
let group: String = match fsentry.metadata().gid {
|
||||
Some(gid) => gid.to_string(),
|
||||
None => 0.to_string(),
|
||||
@@ -364,8 +364,16 @@ impl Formatter {
|
||||
if fsentry.is_file() {
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(fsentry.metadata().size);
|
||||
let mut fmt = size.display().si().to_string();
|
||||
// pad with up to len 10
|
||||
let pad = 10usize.saturating_sub(fmt.len());
|
||||
for _ in 0..pad {
|
||||
fmt.push(' ');
|
||||
}
|
||||
|
||||
format!("{cur_str}{prefix}{fmt}")
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{cur_str}{prefix}{size:10}")
|
||||
//format!("{cur_str}{prefix}{size:10}", size = size.display().si())
|
||||
} else if fsentry.metadata().symlink.is_some() {
|
||||
let size = ByteSize(
|
||||
fsentry
|
||||
@@ -376,7 +384,14 @@ impl Formatter {
|
||||
.to_string_lossy()
|
||||
.len() as u64,
|
||||
);
|
||||
format!("{cur_str}{prefix}{size:10}")
|
||||
let mut fmt = size.display().si().to_string();
|
||||
// pad with up to len 10
|
||||
let pad = 10usize.saturating_sub(fmt.len());
|
||||
for _ in 0..pad {
|
||||
fmt.push(' ');
|
||||
}
|
||||
|
||||
format!("{cur_str}{prefix}{fmt}")
|
||||
} else {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{cur_str}{prefix} ")
|
||||
@@ -420,7 +435,7 @@ impl Formatter {
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get username
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
let username: String = match fsentry.metadata().uid {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
@@ -428,7 +443,7 @@ impl Formatter {
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
let username: String = match fsentry.metadata().uid {
|
||||
Some(uid) => uid.to_string(),
|
||||
None => 0.to_string(),
|
||||
@@ -489,10 +504,7 @@ impl Formatter {
|
||||
};
|
||||
// Match format length: group 3
|
||||
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
||||
Some(len) => match len.as_str().parse::<usize>() {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => None,
|
||||
},
|
||||
Some(len) => len.as_str().parse::<usize>().ok(),
|
||||
None => None,
|
||||
};
|
||||
// Match format extra: group 2 + 1
|
||||
@@ -592,19 +604,19 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -623,19 +635,19 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
|
||||
"piroparoporoperoperupup… -rw-r--r-- root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
|
||||
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -654,19 +666,19 @@ mod tests {
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? root 8.2 KB {}",
|
||||
"bar.txt -????????? root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
"bar.txt -????????? 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -685,19 +697,19 @@ mod tests {
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
"bar.txt -????????? 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
"bar.txt -????????? 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -723,7 +735,7 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -731,7 +743,7 @@ mod tests {
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -754,7 +766,7 @@ mod tests {
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -762,7 +774,7 @@ mod tests {
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -774,8 +786,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_all_together_now() {
|
||||
let formatter: Formatter =
|
||||
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
|
||||
let formatter: Formatter = Formatter::new(
|
||||
"{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}",
|
||||
);
|
||||
// Directory (with symlink)
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
@@ -792,12 +805,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
// Directory without symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
@@ -813,12 +829,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
// File with symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
@@ -834,12 +853,15 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
// File without symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
@@ -855,16 +877,19 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 kB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn should_fmt_path() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
@@ -896,7 +921,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn should_fmt_utf8_path() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
|
||||
@@ -7,10 +7,9 @@ pub(crate) mod builder;
|
||||
mod formatter;
|
||||
// Locals
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use formatter::Formatter;
|
||||
// Ext
|
||||
@@ -31,6 +30,7 @@ pub enum FileSorting {
|
||||
ModifyTime,
|
||||
CreationTime,
|
||||
Size,
|
||||
None,
|
||||
}
|
||||
|
||||
/// GroupDirs defines how directories should be grouped in sorting files
|
||||
@@ -42,14 +42,26 @@ pub enum GroupDirs {
|
||||
|
||||
/// File explorer states
|
||||
pub struct FileExplorer {
|
||||
pub wrkdir: PathBuf, // Current directory
|
||||
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
|
||||
pub(crate) stack_size: usize, // Directory stack size
|
||||
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
||||
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
||||
pub(crate) fmt: Formatter, // File formatter
|
||||
files: Vec<File>, // Files in directory
|
||||
/// Current working directory
|
||||
pub wrkdir: PathBuf,
|
||||
/// Stack of visited directories
|
||||
pub(crate) dirstack: VecDeque<PathBuf>,
|
||||
/// Stack size
|
||||
pub(crate) stack_size: usize,
|
||||
/// Criteria to sort file
|
||||
pub(crate) file_sorting: FileSorting,
|
||||
/// defines how to group directories in the explorer
|
||||
pub(crate) group_dirs: Option<GroupDirs>,
|
||||
/// Explorer options
|
||||
pub(crate) opts: ExplorerOpts,
|
||||
/// Formatter for file entries
|
||||
pub(crate) fmt: Formatter,
|
||||
/// Is terminal open for this explorer?
|
||||
terminal: bool,
|
||||
/// Files in directory
|
||||
files: Vec<File>,
|
||||
/// files enqueued for transfer. Map between source and destination
|
||||
transfer_queue: HashMap<PathBuf, PathBuf>, // transfer queue
|
||||
}
|
||||
|
||||
impl Default for FileExplorer {
|
||||
@@ -63,6 +75,8 @@ impl Default for FileExplorer {
|
||||
opts: ExplorerOpts::empty(),
|
||||
fmt: Formatter::default(),
|
||||
files: Vec::new(),
|
||||
terminal: false,
|
||||
transfer_queue: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,13 +113,6 @@ impl FileExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// Return amount of files
|
||||
pub fn count(&self) -> usize {
|
||||
self.files.len()
|
||||
}
|
||||
*/
|
||||
|
||||
/// Iterate over files
|
||||
/// Filters are applied based on current options (e.g. hidden files not returned)
|
||||
pub fn iter_files(&self) -> impl Iterator<Item = &File> + '_ {
|
||||
@@ -146,6 +153,44 @@ impl FileExplorer {
|
||||
filtered.get(idx).copied()
|
||||
}
|
||||
|
||||
/// Enqueue a file for transfer
|
||||
pub fn enqueue(&mut self, src: &Path, dst: &Path) {
|
||||
self.transfer_queue
|
||||
.insert(PathBuf::from(src), PathBuf::from(dst));
|
||||
}
|
||||
|
||||
/// Enqueue all files for transfer
|
||||
pub fn enqueue_all(&mut self, dst: &Path) {
|
||||
let files: Vec<_> = self.iter_files().map(|f| f.path.clone()).collect();
|
||||
for file in files {
|
||||
self.enqueue(&file, dst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get enqueued files
|
||||
pub fn enqueued(&self) -> &HashMap<PathBuf, PathBuf> {
|
||||
&self.transfer_queue
|
||||
}
|
||||
|
||||
/// Dequeue a file
|
||||
pub fn dequeue(&mut self, src: &Path) {
|
||||
self.transfer_queue.remove(src);
|
||||
}
|
||||
|
||||
/// Clear transfer queue
|
||||
pub fn clear_queue(&mut self) {
|
||||
self.transfer_queue.clear();
|
||||
}
|
||||
|
||||
/// Toggle terminal state
|
||||
pub fn toggle_terminal(&mut self, terminal: bool) {
|
||||
self.terminal = terminal;
|
||||
}
|
||||
|
||||
pub fn terminal_open(&self) -> bool {
|
||||
self.terminal
|
||||
}
|
||||
|
||||
// Formatting
|
||||
|
||||
/// Format a file entry
|
||||
@@ -186,6 +231,7 @@ impl FileExplorer {
|
||||
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
||||
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
||||
FileSorting::Size => self.sort_files_by_size(),
|
||||
FileSorting::None => {}
|
||||
}
|
||||
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
||||
// Group directories if necessary
|
||||
@@ -243,14 +289,19 @@ impl FileExplorer {
|
||||
|
||||
// Traits
|
||||
|
||||
impl ToString for FileSorting {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
FileSorting::CreationTime => "by_creation_time",
|
||||
FileSorting::ModifyTime => "by_mtime",
|
||||
FileSorting::Name => "by_name",
|
||||
FileSorting::Size => "by_size",
|
||||
})
|
||||
impl std::fmt::Display for FileSorting {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
FileSorting::CreationTime => "by_creation_time",
|
||||
FileSorting::ModifyTime => "by_mtime",
|
||||
FileSorting::Name => "by_name",
|
||||
FileSorting::Size => "by_size",
|
||||
FileSorting::None => "none",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,12 +318,16 @@ impl FromStr for FileSorting {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for GroupDirs {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
GroupDirs::First => "first",
|
||||
GroupDirs::Last => "last",
|
||||
})
|
||||
impl std::fmt::Display for GroupDirs {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
GroupDirs::First => "first",
|
||||
GroupDirs::Last => "last",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,19 +571,19 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- root 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
"bar.txt -rw-r--r-- 0 8.2 kB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
@@ -583,6 +638,26 @@ mod tests {
|
||||
assert_eq!(explorer.files.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_enqueue_and_dequeue_files() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("docs", true),
|
||||
make_fs_entry("src", true),
|
||||
make_fs_entry("README.md", false),
|
||||
]);
|
||||
// Enqueue
|
||||
explorer.enqueue(Path::new("CONTRIBUTING.md"), Path::new("CONTRIBUTING.md"));
|
||||
explorer.enqueue(Path::new("docs"), Path::new("docs"));
|
||||
// Dequeue
|
||||
explorer.dequeue(Path::new("CONTRIBUTING.md"));
|
||||
assert_eq!(explorer.enqueued().len(), 1);
|
||||
explorer.dequeue(Path::new("docs"));
|
||||
assert_eq!(explorer.enqueued().len(), 0);
|
||||
}
|
||||
|
||||
fn make_fs_entry(name: &str, is_dir: bool) -> File {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let metadata = Metadata {
|
||||
|
||||
25
src/filetransfer/host_bridge_builder.rs
Normal file
25
src/filetransfer/host_bridge_builder.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use super::{HostBridgeParams, RemoteFsBuilder};
|
||||
use crate::host::{HostBridge, Localhost, RemoteBridged};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
pub struct HostBridgeBuilder;
|
||||
|
||||
impl HostBridgeBuilder {
|
||||
/// Build Host Bridge from parms
|
||||
///
|
||||
/// if protocol and parameters are inconsistent, the function will return an error.
|
||||
pub fn build(
|
||||
params: HostBridgeParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<Box<dyn HostBridge>, String> {
|
||||
match params {
|
||||
HostBridgeParams::Localhost(path) => Localhost::new(path)
|
||||
.map(|host| Box::new(host) as Box<dyn HostBridge>)
|
||||
.map_err(|e| e.to_string()),
|
||||
HostBridgeParams::Remote(protocol, params) => {
|
||||
RemoteFsBuilder::build(protocol, params, config_client)
|
||||
.map(|host| Box::new(RemoteBridged::from(host)) as Box<dyn HostBridge>)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
//!
|
||||
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
|
||||
|
||||
mod builder;
|
||||
mod host_bridge_builder;
|
||||
pub mod params;
|
||||
mod remotefs_builder;
|
||||
|
||||
// -- export types
|
||||
pub use builder::Builder;
|
||||
pub use params::{FileTransferParams, ProtocolParams};
|
||||
pub use host_bridge_builder::HostBridgeBuilder;
|
||||
pub use params::{FileTransferParams, HostBridgeParams, ProtocolParams};
|
||||
pub use remotefs_builder::RemoteFsBuilder;
|
||||
|
||||
/// This enum defines the different transfer protocol available in termscp
|
||||
|
||||
@@ -15,25 +17,33 @@ pub use params::{FileTransferParams, ProtocolParams};
|
||||
pub enum FileTransferProtocol {
|
||||
AwsS3,
|
||||
Ftp(bool), // Bool is for secure (true => ftps)
|
||||
Kube,
|
||||
Scp,
|
||||
Sftp,
|
||||
Smb,
|
||||
WebDAV,
|
||||
}
|
||||
|
||||
// Traits
|
||||
|
||||
impl std::string::ToString for FileTransferProtocol {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
FileTransferProtocol::AwsS3 => "S3",
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => "FTPS",
|
||||
false => "FTP",
|
||||
},
|
||||
FileTransferProtocol::Scp => "SCP",
|
||||
FileTransferProtocol::Sftp => "SFTP",
|
||||
FileTransferProtocol::Smb => "SMB",
|
||||
})
|
||||
impl std::fmt::Display for FileTransferProtocol {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
FileTransferProtocol::AwsS3 => "S3",
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => "FTPS",
|
||||
false => "FTP",
|
||||
},
|
||||
FileTransferProtocol::Kube => "KUBE",
|
||||
FileTransferProtocol::Scp => "SCP",
|
||||
FileTransferProtocol::Sftp => "SFTP",
|
||||
FileTransferProtocol::Smb => "SMB",
|
||||
FileTransferProtocol::WebDAV => "WEBDAV",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +53,12 @@ impl std::str::FromStr for FileTransferProtocol {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||
"KUBE" => Ok(FileTransferProtocol::Kube),
|
||||
"S3" => Ok(FileTransferProtocol::AwsS3),
|
||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||
"SMB" => Ok(FileTransferProtocol::Smb),
|
||||
"WEBDAV" | "HTTP" | "HTTPS" => Ok(FileTransferProtocol::WebDAV),
|
||||
_ => Err(s.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -107,6 +119,14 @@ mod tests {
|
||||
FileTransferProtocol::from_str("scp").ok().unwrap(),
|
||||
FileTransferProtocol::Scp
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("kube").ok().unwrap(),
|
||||
FileTransferProtocol::Kube
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("KUBE").ok().unwrap(),
|
||||
FileTransferProtocol::Kube
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("SMB").ok().unwrap(),
|
||||
FileTransferProtocol::Smb
|
||||
@@ -134,9 +154,18 @@ mod tests {
|
||||
FileTransferProtocol::Ftp(false).to_string(),
|
||||
String::from("FTP")
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::WebDAV.to_string(),
|
||||
String::from("WEBDAV")
|
||||
);
|
||||
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
|
||||
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
||||
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
|
||||
assert_eq!(FileTransferProtocol::Smb.to_string(), String::from("SMB"));
|
||||
assert_eq!(
|
||||
FileTransferProtocol::WebDAV.to_string(),
|
||||
String::from("WEBDAV")
|
||||
);
|
||||
assert_eq!(FileTransferProtocol::Kube.to_string(), String::from("KUBE"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,47 @@
|
||||
//!
|
||||
//! file transfer parameters
|
||||
|
||||
mod aws_s3;
|
||||
mod kube;
|
||||
mod smb;
|
||||
mod webdav;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use self::aws_s3::AwsS3Params;
|
||||
pub use self::kube::KubeProtocolParams;
|
||||
pub use self::smb::SmbParams;
|
||||
pub use self::webdav::WebDAVProtocolParams;
|
||||
use super::FileTransferProtocol;
|
||||
|
||||
/// Host bridge params
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HostBridgeParams {
|
||||
/// Localhost with starting working directory
|
||||
Localhost(PathBuf),
|
||||
/// Remote host with protocol and file transfer params
|
||||
Remote(FileTransferProtocol, ProtocolParams),
|
||||
}
|
||||
|
||||
impl HostBridgeParams {
|
||||
pub fn unwrap_protocol_params(&self) -> &ProtocolParams {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => panic!("Localhost has no protocol params"),
|
||||
HostBridgeParams::Remote(_, params) => params,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the host name for the bridge params
|
||||
pub fn username(&self) -> Option<String> {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => Some(whoami::username()),
|
||||
HostBridgeParams::Remote(_, params) => {
|
||||
params.generic_params().and_then(|p| p.username.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds connection parameters for file transfers
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTransferParams {
|
||||
@@ -15,12 +52,60 @@ pub struct FileTransferParams {
|
||||
pub local_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl FileTransferParams {
|
||||
/// Returns the remote path if set, otherwise returns the local path
|
||||
pub fn username(&self) -> Option<String> {
|
||||
self.params
|
||||
.generic_params()
|
||||
.and_then(|p| p.username.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Container for protocol params
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProtocolParams {
|
||||
Generic(GenericProtocolParams),
|
||||
AwsS3(AwsS3Params),
|
||||
Kube(KubeProtocolParams),
|
||||
Smb(SmbParams),
|
||||
WebDAV(WebDAVProtocolParams),
|
||||
}
|
||||
|
||||
impl ProtocolParams {
|
||||
pub fn password_missing(&self) -> bool {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.password_missing(),
|
||||
ProtocolParams::Generic(params) => params.password_missing(),
|
||||
ProtocolParams::Kube(params) => params.password_missing(),
|
||||
ProtocolParams::Smb(params) => params.password_missing(),
|
||||
ProtocolParams::WebDAV(params) => params.password_missing(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the secret to ft params for the default secret field for this protocol
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Generic(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Kube(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Smb(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_name(&self) -> String {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||
ProtocolParams::Generic(params) => params.address.clone(),
|
||||
ProtocolParams::Kube(params) => params
|
||||
.namespace
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("default")),
|
||||
ProtocolParams::Smb(params) => params.address.clone(),
|
||||
ProtocolParams::WebDAV(params) => params.uri.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol params used by most common protocols
|
||||
@@ -32,33 +117,6 @@ pub struct GenericProtocolParams {
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
/// Connection parameters for AWS S3 protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AwsS3Params {
|
||||
pub bucket_name: String,
|
||||
pub region: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub profile: Option<String>,
|
||||
pub access_key: Option<String>,
|
||||
pub secret_access_key: Option<String>,
|
||||
pub security_token: Option<String>,
|
||||
pub session_token: Option<String>,
|
||||
pub new_path_style: bool,
|
||||
}
|
||||
|
||||
/// Connection parameters for SMB protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmbParams {
|
||||
pub address: String,
|
||||
#[cfg(unix)]
|
||||
pub port: u16,
|
||||
pub share: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
#[cfg(unix)]
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
impl FileTransferParams {
|
||||
/// Instantiates a new `FileTransferParams`
|
||||
pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self {
|
||||
@@ -84,21 +142,15 @@ impl FileTransferParams {
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
#[cfg(test)]
|
||||
pub fn password_missing(&self) -> bool {
|
||||
match &self.params {
|
||||
ProtocolParams::AwsS3(params) => params.password_missing(),
|
||||
ProtocolParams::Generic(params) => params.password_missing(),
|
||||
ProtocolParams::Smb(params) => params.password_missing(),
|
||||
}
|
||||
self.params.password_missing()
|
||||
}
|
||||
|
||||
/// Set the secret to ft params for the default secret field for this protocol
|
||||
#[cfg(test)]
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
match &mut self.params {
|
||||
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Generic(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Smb(params) => params.set_default_secret(secret),
|
||||
}
|
||||
self.params.set_default_secret(secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +193,15 @@ impl ProtocolParams {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Retrieve Kube params parameters if any
|
||||
pub fn kube_params(&self) -> Option<&KubeProtocolParams> {
|
||||
match self {
|
||||
ProtocolParams::Kube(params) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Retrieve SMB parameters if any
|
||||
pub fn smb_params(&self) -> Option<&SmbParams> {
|
||||
@@ -149,6 +210,15 @@ impl ProtocolParams {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Retrieve WebDAV parameters if any
|
||||
pub fn webdav_params(&self) -> Option<&WebDAVProtocolParams> {
|
||||
match self {
|
||||
ProtocolParams::WebDAV(params) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Generic protocol params
|
||||
@@ -201,127 +271,6 @@ impl GenericProtocolParams {
|
||||
}
|
||||
}
|
||||
|
||||
// -- S3 params
|
||||
|
||||
impl AwsS3Params {
|
||||
/// Instantiates a new `AwsS3Params` struct
|
||||
pub fn new<S: AsRef<str>>(bucket: S, region: Option<S>, profile: Option<S>) -> Self {
|
||||
Self {
|
||||
bucket_name: bucket.as_ref().to_string(),
|
||||
region: region.map(|x| x.as_ref().to_string()),
|
||||
profile: profile.map(|x| x.as_ref().to_string()),
|
||||
endpoint: None,
|
||||
access_key: None,
|
||||
secret_access_key: None,
|
||||
security_token: None,
|
||||
session_token: None,
|
||||
new_path_style: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with specified endpoint
|
||||
pub fn endpoint<S: AsRef<str>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.endpoint = endpoint.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided access key
|
||||
pub fn access_key<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.access_key = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided secret_access_key
|
||||
pub fn secret_access_key<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.secret_access_key = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided security_token
|
||||
pub fn security_token<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.security_token = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided session_token
|
||||
pub fn session_token<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.session_token = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify new path style when constructing aws s3 params
|
||||
pub fn new_path_style(mut self, new_path_style: bool) -> Self {
|
||||
self.new_path_style = new_path_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.secret_access_key.is_none() && self.security_token.is_none()
|
||||
}
|
||||
|
||||
/// Set password
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.secret_access_key = Some(secret);
|
||||
}
|
||||
}
|
||||
|
||||
// -- SMB params
|
||||
|
||||
impl SmbParams {
|
||||
/// Instantiates a new `AwsS3Params` struct
|
||||
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
|
||||
Self {
|
||||
address: address.as_ref().to_string(),
|
||||
#[cfg(unix)]
|
||||
port: 445,
|
||||
share: share.as_ref().to_string(),
|
||||
username: None,
|
||||
password: None,
|
||||
#[cfg(unix)]
|
||||
workgroup: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn username(mut self, username: Option<impl ToString>) -> Self {
|
||||
self.username = username.map(|x| x.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn password(mut self, password: Option<impl ToString>) -> Self {
|
||||
self.password = password.map(|x| x.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
|
||||
self.workgroup = workgroup.map(|x| x.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.password.is_none()
|
||||
}
|
||||
|
||||
/// Set password
|
||||
#[cfg(unix)]
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.password = Some(secret);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
@@ -356,87 +305,6 @@ mod test {
|
||||
assert!(params.password.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_init_aws_s3_params() {
|
||||
let params: AwsS3Params = AwsS3Params::new("omar", Some("eu-west-1"), Some("test"));
|
||||
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||
assert_eq!(params.region.as_deref().unwrap(), "eu-west-1");
|
||||
assert_eq!(params.profile.as_deref().unwrap(), "test");
|
||||
assert!(params.endpoint.is_none());
|
||||
assert!(params.access_key.is_none());
|
||||
assert!(params.secret_access_key.is_none());
|
||||
assert!(params.security_token.is_none());
|
||||
assert!(params.session_token.is_none());
|
||||
assert_eq!(params.new_path_style, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_init_aws_s3_params_with_optionals() {
|
||||
let params: AwsS3Params = AwsS3Params::new("omar", Some("eu-west-1"), Some("test"))
|
||||
.endpoint(Some("http://omar.it"))
|
||||
.access_key(Some("pippo"))
|
||||
.secret_access_key(Some("pluto"))
|
||||
.security_token(Some("omar"))
|
||||
.session_token(Some("gerry-scotti"))
|
||||
.new_path_style(true);
|
||||
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||
assert_eq!(params.region.as_deref().unwrap(), "eu-west-1");
|
||||
assert_eq!(params.profile.as_deref().unwrap(), "test");
|
||||
assert_eq!(params.endpoint.as_deref().unwrap(), "http://omar.it");
|
||||
assert_eq!(params.access_key.as_deref().unwrap(), "pippo");
|
||||
assert_eq!(params.secret_access_key.as_deref().unwrap(), "pluto");
|
||||
assert_eq!(params.security_token.as_deref().unwrap(), "omar");
|
||||
assert_eq!(params.session_token.as_deref().unwrap(), "gerry-scotti");
|
||||
assert_eq!(params.new_path_style, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_init_smb_params() {
|
||||
let params = SmbParams::new("localhost", "temp");
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
|
||||
#[cfg(unix)]
|
||||
assert_eq!(params.port, 445);
|
||||
assert_eq!(¶ms.share, "temp");
|
||||
|
||||
#[cfg(unix)]
|
||||
assert!(params.username.is_none());
|
||||
#[cfg(unix)]
|
||||
assert!(params.password.is_none());
|
||||
#[cfg(unix)]
|
||||
assert!(params.workgroup.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn should_init_smb_params_with_optionals() {
|
||||
let params = SmbParams::new("localhost", "temp")
|
||||
.port(3456)
|
||||
.username(Some("foo"))
|
||||
.password(Some("bar"))
|
||||
.workgroup(Some("baz"));
|
||||
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
assert_eq!(params.port, 3456);
|
||||
assert_eq!(¶ms.share, "temp");
|
||||
assert_eq!(params.username.as_deref().unwrap(), "foo");
|
||||
assert_eq!(params.password.as_deref().unwrap(), "bar");
|
||||
assert_eq!(params.workgroup.as_deref().unwrap(), "baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn should_init_smb_params_with_optionals() {
|
||||
let params = SmbParams::new("localhost", "temp")
|
||||
.username(Some("foo"))
|
||||
.password(Some("bar"));
|
||||
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
assert_eq!(¶ms.share, "temp");
|
||||
assert_eq!(params.username.as_deref().unwrap(), "foo");
|
||||
assert_eq!(params.password.as_deref().unwrap(), "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references() {
|
||||
let mut params =
|
||||
@@ -452,11 +320,13 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn password_missing() {
|
||||
assert!(FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
|
||||
)
|
||||
.password_missing());
|
||||
assert!(
|
||||
FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
|
||||
)
|
||||
.password_missing()
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
@@ -512,6 +382,40 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn set_default_secret_smb() {
|
||||
let mut params = FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::Smb(SmbParams::new("localhost", "temp")),
|
||||
);
|
||||
params.set_default_secret(String::from("secret"));
|
||||
assert_eq!(
|
||||
params
|
||||
.params
|
||||
.smb_params()
|
||||
.unwrap()
|
||||
.password
|
||||
.as_deref()
|
||||
.unwrap(),
|
||||
"secret"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_default_secret_webdav() {
|
||||
let mut params = FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::WebDAV(WebDAVProtocolParams {
|
||||
uri: "http://localhost".to_string(),
|
||||
username: "user".to_string(),
|
||||
password: "pass".to_string(),
|
||||
}),
|
||||
);
|
||||
params.set_default_secret(String::from("secret"));
|
||||
assert_eq!(params.params.webdav_params().unwrap().password, "secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_default_secret_generic() {
|
||||
let mut params =
|
||||
|
||||
121
src/filetransfer/params/aws_s3.rs
Normal file
121
src/filetransfer/params/aws_s3.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
/// Connection parameters for AWS S3 protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AwsS3Params {
|
||||
pub bucket_name: String,
|
||||
pub region: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub profile: Option<String>,
|
||||
pub access_key: Option<String>,
|
||||
pub secret_access_key: Option<String>,
|
||||
pub security_token: Option<String>,
|
||||
pub session_token: Option<String>,
|
||||
pub new_path_style: bool,
|
||||
}
|
||||
|
||||
// -- S3 params
|
||||
|
||||
impl AwsS3Params {
|
||||
/// Instantiates a new `AwsS3Params` struct
|
||||
pub fn new<S: AsRef<str>>(bucket: S, region: Option<S>, profile: Option<S>) -> Self {
|
||||
Self {
|
||||
bucket_name: bucket.as_ref().to_string(),
|
||||
region: region.map(|x| x.as_ref().to_string()),
|
||||
profile: profile.map(|x| x.as_ref().to_string()),
|
||||
endpoint: None,
|
||||
access_key: None,
|
||||
secret_access_key: None,
|
||||
security_token: None,
|
||||
session_token: None,
|
||||
new_path_style: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with specified endpoint
|
||||
pub fn endpoint<S: AsRef<str>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.endpoint = endpoint.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided access key
|
||||
pub fn access_key<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.access_key = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided secret_access_key
|
||||
pub fn secret_access_key<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.secret_access_key = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided security_token
|
||||
pub fn security_token<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.security_token = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct aws s3 params with provided session_token
|
||||
pub fn session_token<S: AsRef<str>>(mut self, key: Option<S>) -> Self {
|
||||
self.session_token = key.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify new path style when constructing aws s3 params
|
||||
pub fn new_path_style(mut self, new_path_style: bool) -> Self {
|
||||
self.new_path_style = new_path_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.secret_access_key.is_none() && self.security_token.is_none()
|
||||
}
|
||||
|
||||
/// Set password
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.secret_access_key = Some(secret);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_init_aws_s3_params() {
|
||||
let params: AwsS3Params = AwsS3Params::new("omar", Some("eu-west-1"), Some("test"));
|
||||
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||
assert_eq!(params.region.as_deref().unwrap(), "eu-west-1");
|
||||
assert_eq!(params.profile.as_deref().unwrap(), "test");
|
||||
assert!(params.endpoint.is_none());
|
||||
assert!(params.access_key.is_none());
|
||||
assert!(params.secret_access_key.is_none());
|
||||
assert!(params.security_token.is_none());
|
||||
assert!(params.session_token.is_none());
|
||||
assert_eq!(params.new_path_style, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_init_aws_s3_params_with_optionals() {
|
||||
let params: AwsS3Params = AwsS3Params::new("omar", Some("eu-west-1"), Some("test"))
|
||||
.endpoint(Some("http://omar.it"))
|
||||
.access_key(Some("pippo"))
|
||||
.secret_access_key(Some("pluto"))
|
||||
.security_token(Some("omar"))
|
||||
.session_token(Some("gerry-scotti"))
|
||||
.new_path_style(true);
|
||||
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||
assert_eq!(params.region.as_deref().unwrap(), "eu-west-1");
|
||||
assert_eq!(params.profile.as_deref().unwrap(), "test");
|
||||
assert_eq!(params.endpoint.as_deref().unwrap(), "http://omar.it");
|
||||
assert_eq!(params.access_key.as_deref().unwrap(), "pippo");
|
||||
assert_eq!(params.secret_access_key.as_deref().unwrap(), "pluto");
|
||||
assert_eq!(params.security_token.as_deref().unwrap(), "omar");
|
||||
assert_eq!(params.session_token.as_deref().unwrap(), "gerry-scotti");
|
||||
assert_eq!(params.new_path_style, true);
|
||||
}
|
||||
}
|
||||
35
src/filetransfer/params/kube.rs
Normal file
35
src/filetransfer/params/kube.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use remotefs_kube::Config;
|
||||
|
||||
/// Protocol params used by WebDAV
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KubeProtocolParams {
|
||||
pub namespace: Option<String>,
|
||||
pub cluster_url: Option<String>,
|
||||
pub username: Option<String>,
|
||||
pub client_cert: Option<String>,
|
||||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
impl KubeProtocolParams {
|
||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||
|
||||
pub fn password_missing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn config(self) -> Option<Config> {
|
||||
if let Some(cluster_url) = self.cluster_url {
|
||||
let mut config = Config::new(cluster_url.parse().unwrap_or_default());
|
||||
config.auth_info.username = self.username;
|
||||
config.auth_info.client_certificate = self.client_cert;
|
||||
config.auth_info.client_key = self.client_key;
|
||||
if let Some(namespace) = self.namespace {
|
||||
config.default_namespace = namespace;
|
||||
}
|
||||
|
||||
Some(config)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/filetransfer/params/smb.rs
Normal file
122
src/filetransfer/params/smb.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
/// Connection parameters for SMB protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmbParams {
|
||||
pub address: String,
|
||||
#[cfg(posix)]
|
||||
pub port: u16,
|
||||
pub share: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
#[cfg(posix)]
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
// -- SMB params
|
||||
|
||||
impl SmbParams {
|
||||
/// Instantiates a new `AwsS3Params` struct
|
||||
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
|
||||
Self {
|
||||
address: address.as_ref().to_string(),
|
||||
#[cfg(posix)]
|
||||
port: 445,
|
||||
share: share.as_ref().to_string(),
|
||||
username: None,
|
||||
password: None,
|
||||
#[cfg(posix)]
|
||||
workgroup: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn username(mut self, username: Option<impl ToString>) -> Self {
|
||||
self.username = username.map(|x| x.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn password(mut self, password: Option<impl ToString>) -> Self {
|
||||
self.password = password.map(|x| x.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
|
||||
self.workgroup = workgroup.map(|x| x.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.password.is_none()
|
||||
}
|
||||
|
||||
/// Set password
|
||||
#[cfg(posix)]
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.password = Some(secret);
|
||||
}
|
||||
|
||||
#[cfg(win)]
|
||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_init_smb_params() {
|
||||
let params = SmbParams::new("localhost", "temp");
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
|
||||
#[cfg(posix)]
|
||||
assert_eq!(params.port, 445);
|
||||
assert_eq!(¶ms.share, "temp");
|
||||
|
||||
#[cfg(posix)]
|
||||
assert!(params.username.is_none());
|
||||
#[cfg(posix)]
|
||||
assert!(params.password.is_none());
|
||||
#[cfg(posix)]
|
||||
assert!(params.workgroup.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn should_init_smb_params_with_optionals() {
|
||||
let params = SmbParams::new("localhost", "temp")
|
||||
.port(3456)
|
||||
.username(Some("foo"))
|
||||
.password(Some("bar"))
|
||||
.workgroup(Some("baz"));
|
||||
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
assert_eq!(params.port, 3456);
|
||||
assert_eq!(¶ms.share, "temp");
|
||||
assert_eq!(params.username.as_deref().unwrap(), "foo");
|
||||
assert_eq!(params.password.as_deref().unwrap(), "bar");
|
||||
assert_eq!(params.workgroup.as_deref().unwrap(), "baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(win)]
|
||||
fn should_init_smb_params_with_optionals() {
|
||||
let params = SmbParams::new("localhost", "temp")
|
||||
.username(Some("foo"))
|
||||
.password(Some("bar"));
|
||||
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
assert_eq!(¶ms.share, "temp");
|
||||
assert_eq!(params.username.as_deref().unwrap(), "foo");
|
||||
assert_eq!(params.password.as_deref().unwrap(), "bar");
|
||||
}
|
||||
}
|
||||
17
src/filetransfer/params/webdav.rs
Normal file
17
src/filetransfer/params/webdav.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
/// Protocol params used by WebDAV
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebDAVProtocolParams {
|
||||
pub uri: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl WebDAVProtocolParams {
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.password = secret;
|
||||
}
|
||||
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.password.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,33 @@
|
||||
//! Remotefs client builder
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use remotefs::RemoteFs;
|
||||
use remotefs_aws_s3::AwsS3Fs;
|
||||
use remotefs_ftp::FtpFs;
|
||||
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||
#[cfg(smb_unix)]
|
||||
use remotefs_smb::SmbOptions;
|
||||
#[cfg(smb)]
|
||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
||||
use remotefs_ssh::{ScpFs, SftpFs, SshConfigParseRule, SshOpts};
|
||||
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
|
||||
use remotefs_webdav::WebDAVFs;
|
||||
|
||||
#[cfg(not(smb))]
|
||||
use super::params::{AwsS3Params, GenericProtocolParams};
|
||||
#[cfg(smb)]
|
||||
use super::params::{AwsS3Params, GenericProtocolParams, SmbParams};
|
||||
use super::params::{KubeProtocolParams, WebDAVProtocolParams};
|
||||
use super::{FileTransferProtocol, ProtocolParams};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::ssh as ssh_utils;
|
||||
|
||||
/// Remotefs builder
|
||||
pub struct Builder;
|
||||
pub struct RemoteFsBuilder;
|
||||
|
||||
impl Builder {
|
||||
impl RemoteFsBuilder {
|
||||
/// Build RemoteFs client from protocol and params.
|
||||
///
|
||||
/// if protocol and parameters are inconsistent, the function will panic.
|
||||
@@ -33,34 +37,50 @@ impl Builder {
|
||||
protocol: FileTransferProtocol,
|
||||
params: ProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Box<dyn RemoteFs> {
|
||||
) -> Result<Box<dyn RemoteFs>, String> {
|
||||
match (protocol, params) {
|
||||
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
||||
Box::new(Self::aws_s3_client(params))
|
||||
Ok(Box::new(Self::aws_s3_client(params)))
|
||||
}
|
||||
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::ftp_client(params, secure))
|
||||
Ok(Box::new(Self::ftp_client(params, secure)))
|
||||
}
|
||||
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||
Ok(Box::new(Self::kube_client(params)))
|
||||
}
|
||||
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::scp_client(params, config_client))
|
||||
Ok(Box::new(Self::scp_client(params, config_client)))
|
||||
}
|
||||
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::sftp_client(params, config_client))
|
||||
Ok(Box::new(Self::sftp_client(params, config_client)))
|
||||
}
|
||||
#[cfg(smb)]
|
||||
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
||||
Box::new(Self::smb_client(params))
|
||||
Ok(Box::new(Self::smb_client(params)))
|
||||
}
|
||||
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
||||
Ok(Box::new(Self::webdav_client(params)))
|
||||
}
|
||||
(protocol, params) => {
|
||||
error!("Invalid params for protocol '{:?}'", protocol);
|
||||
panic!("Invalid protocol '{protocol:?}' with parameters of type {params:?}")
|
||||
Err(format!(
|
||||
"Invalid protocol '{protocol:?}' with parameters of type {params:?}",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build aws s3 client from parameters
|
||||
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
|
||||
let mut client = AwsS3Fs::new(params.bucket_name).new_path_style(params.new_path_style);
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
);
|
||||
let mut client =
|
||||
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
|
||||
if let Some(region) = params.region {
|
||||
client = client.region(region);
|
||||
}
|
||||
@@ -100,6 +120,23 @@ impl Builder {
|
||||
client
|
||||
}
|
||||
|
||||
/// Build kube client
|
||||
fn kube_client(params: KubeProtocolParams) -> KubeFs {
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
);
|
||||
let kube_fs = KubeFs::new(&rt);
|
||||
if let Some(config) = params.config() {
|
||||
kube_fs.config(config)
|
||||
} else {
|
||||
kube_fs
|
||||
}
|
||||
}
|
||||
|
||||
/// Build scp client
|
||||
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
@@ -154,10 +191,15 @@ impl Builder {
|
||||
SmbFs::new(credentials)
|
||||
}
|
||||
|
||||
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
|
||||
WebDAVFs::new(¶ms.username, ¶ms.password, ¶ms.uri)
|
||||
}
|
||||
|
||||
/// Build ssh options from generic protocol params and client configuration
|
||||
fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts {
|
||||
let mut opts = SshOpts::new(params.address.clone())
|
||||
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
|
||||
.ssh_agent_identity(Some(SshAgentIdentity::All))
|
||||
.port(params.port);
|
||||
// get ssh config
|
||||
let ssh_config = config_client
|
||||
@@ -191,8 +233,12 @@ impl Builder {
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
}
|
||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||
// This allows the SSH library to prioritize key-based and agent authentication.
|
||||
if let Some(password) = params.password {
|
||||
opts = opts.password(password);
|
||||
if !password.is_empty() {
|
||||
opts = opts.password(password);
|
||||
}
|
||||
}
|
||||
if let Some(config_path) = config_client.get_ssh_config() {
|
||||
opts = opts.config_file(
|
||||
@@ -230,7 +276,9 @@ mod test {
|
||||
.session_token(Some("gerry-scotti")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -243,7 +291,22 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_build_kube_fs() {
|
||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||
namespace: Some("namespace".to_string()),
|
||||
cluster_url: Some("cluster_url".to_string()),
|
||||
username: Some("username".to_string()),
|
||||
client_cert: Some("client_cert".to_string()),
|
||||
client_key: Some("client_key".to_string()),
|
||||
});
|
||||
let config_client = get_config_client();
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -256,7 +319,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -269,7 +332,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -277,11 +340,10 @@ mod test {
|
||||
fn should_build_smb_fs() {
|
||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Smb, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn should_not_build_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
@@ -291,7 +353,9 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn get_config_client() -> ConfigClient {
|
||||
84
src/host/bridge.rs
Normal file
84
src/host/bridge.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::File;
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
|
||||
use super::HostResult;
|
||||
|
||||
/// Trait to bridge a remote filesystem to the host filesystem
|
||||
///
|
||||
/// In case of `Localhost` this should be effortless, while for remote hosts this should
|
||||
/// implement a real bridge when the resource is first loaded on the local
|
||||
/// filesystem and then processed on the remote.
|
||||
pub trait HostBridge {
|
||||
/// Connect to host
|
||||
fn connect(&mut self) -> HostResult<()>;
|
||||
|
||||
/// Disconnect from host
|
||||
fn disconnect(&mut self) -> HostResult<()>;
|
||||
|
||||
/// Returns whether the host is connected
|
||||
fn is_connected(&mut self) -> bool;
|
||||
|
||||
/// Returns whether the host is localhost
|
||||
fn is_localhost(&self) -> bool;
|
||||
|
||||
/// Print working directory
|
||||
fn pwd(&mut self) -> HostResult<PathBuf>;
|
||||
|
||||
/// Change working directory with the new provided directory
|
||||
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf>;
|
||||
|
||||
/// Make a directory at path and update the file list (only if relative)
|
||||
fn mkdir(&mut self, dir_name: &Path) -> HostResult<()> {
|
||||
self.mkdir_ex(dir_name, false)
|
||||
}
|
||||
|
||||
/// Extended option version of makedir.
|
||||
/// ignex: don't report error if directory already exists
|
||||
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()>;
|
||||
|
||||
/// Remove file entry
|
||||
fn remove(&mut self, entry: &File) -> HostResult<()>;
|
||||
|
||||
/// Rename file or directory to new name
|
||||
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()>;
|
||||
|
||||
/// Copy file to destination path
|
||||
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()>;
|
||||
|
||||
/// Stat file and create a File
|
||||
fn stat(&mut self, path: &Path) -> HostResult<File>;
|
||||
|
||||
/// Returns whether provided file path exists
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool>;
|
||||
|
||||
/// Get content of a directory
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>>;
|
||||
|
||||
/// Set file stat
|
||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()>;
|
||||
|
||||
/// Execute a command on localhost
|
||||
fn exec(&mut self, cmd: &str) -> HostResult<String>;
|
||||
|
||||
/// Create a symlink from src to dst
|
||||
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()>;
|
||||
|
||||
/// Change file mode to file, according to UNIX permissions
|
||||
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>;
|
||||
|
||||
/// Open file for reading
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>>;
|
||||
|
||||
/// Open file for writing
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>>;
|
||||
|
||||
/// Finalize write operation
|
||||
fn finalize_write(&mut self, writer: Box<dyn Write + Send>) -> HostResult<()>;
|
||||
}
|
||||
1056
src/host/localhost.rs
Normal file
1056
src/host/localhost.rs
Normal file
File diff suppressed because it is too large
Load Diff
1085
src/host/mod.rs
1085
src/host/mod.rs
File diff suppressed because it is too large
Load Diff
211
src/host/remote_bridged.rs
Normal file
211
src/host/remote_bridged.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
mod temp_mapped_file;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
|
||||
|
||||
use self::temp_mapped_file::TempMappedFile;
|
||||
use super::{HostBridge, HostError, HostResult};
|
||||
|
||||
struct WriteStreamOp {
|
||||
path: PathBuf,
|
||||
metadata: Metadata,
|
||||
tempfile: TempMappedFile,
|
||||
}
|
||||
|
||||
/// A remote host bridged over the local host
|
||||
pub struct RemoteBridged {
|
||||
/// Remote fs client
|
||||
remote: Box<dyn RemoteFs>,
|
||||
/// Reminder used to finalize write stream
|
||||
write_stream_op: Option<WriteStreamOp>,
|
||||
}
|
||||
|
||||
impl RemoteBridged {
|
||||
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
let mut temp_file = TempMappedFile::new()?;
|
||||
|
||||
self.remote
|
||||
.open_file(file, Box::new(temp_file.clone()))
|
||||
.map_err(HostError::from)?;
|
||||
|
||||
// Sync changes
|
||||
temp_file.sync()?;
|
||||
|
||||
// now return as read
|
||||
Ok(Box::new(temp_file))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn RemoteFs>> for RemoteBridged {
|
||||
fn from(remote: Box<dyn RemoteFs>) -> Self {
|
||||
RemoteBridged {
|
||||
remote,
|
||||
write_stream_op: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostBridge for RemoteBridged {
|
||||
fn connect(&mut self) -> HostResult<()> {
|
||||
self.remote.connect().map(|_| ()).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> HostResult<()> {
|
||||
self.remote.disconnect().map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn is_connected(&mut self) -> bool {
|
||||
self.remote.is_connected()
|
||||
}
|
||||
|
||||
fn is_localhost(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn pwd(&mut self) -> HostResult<PathBuf> {
|
||||
debug!("Getting working directory");
|
||||
self.remote.pwd().map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {
|
||||
debug!("Changing working directory to {:?}", new_dir);
|
||||
self.remote.change_dir(new_dir).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> {
|
||||
debug!("Creating directory {:?}", dir_name);
|
||||
match self.remote.create_dir(dir_name, UnixPex::from(0o755)) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(remotefs::RemoteError {
|
||||
kind: RemoteErrorType::DirectoryAlreadyExists,
|
||||
..
|
||||
}) if ignore_existing => Ok(()),
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, entry: &File) -> HostResult<()> {
|
||||
debug!("Removing {:?}", entry.path());
|
||||
if entry.is_dir() {
|
||||
self.remote
|
||||
.remove_dir_all(entry.path())
|
||||
.map_err(HostError::from)
|
||||
} else {
|
||||
self.remote
|
||||
.remove_file(entry.path())
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> {
|
||||
debug!("Renaming {:?} to {:?}", entry.path(), dst_path);
|
||||
self.remote
|
||||
.mov(entry.path(), dst_path)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> {
|
||||
debug!("Copying {:?} to {:?}", entry.path(), dst);
|
||||
self.remote.copy(entry.path(), dst).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn stat(&mut self, path: &Path) -> HostResult<File> {
|
||||
debug!("Statting {:?}", path);
|
||||
self.remote.stat(path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||
debug!("Checking existence of {:?}", path);
|
||||
self.remote.exists(path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
||||
debug!("Listing directory {:?}", path);
|
||||
self.remote.list_dir(path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
|
||||
debug!("Setting metadata for {:?}", path);
|
||||
self.remote
|
||||
.setstat(path, metadata.clone())
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn exec(&mut self, cmd: &str) -> HostResult<String> {
|
||||
debug!("Executing command: {}", cmd);
|
||||
self.remote
|
||||
.exec(cmd)
|
||||
.map(|(_, stdout)| stdout)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
|
||||
debug!("Creating symlink from {:?} to {:?}", src, dst);
|
||||
self.remote.symlink(src, dst).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> {
|
||||
debug!("Changing permissions of {:?} to {:?}", path, pex);
|
||||
let stat = self.remote.stat(path).map_err(HostError::from)?;
|
||||
let mut metadata = stat.metadata.clone();
|
||||
metadata.mode = Some(pex);
|
||||
|
||||
self.setstat(path, &metadata)
|
||||
}
|
||||
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
// try to use stream, otherwise download to a temporary file and return a reader
|
||||
match self.remote.open(file) {
|
||||
Ok(stream) => Ok(Box::new(stream)),
|
||||
Err(RemoteError {
|
||||
kind: RemoteErrorType::UnsupportedFeature,
|
||||
..
|
||||
}) => self.open_file_from_temp(file),
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>> {
|
||||
// try to use stream, otherwise download to a temporary file and return a reader
|
||||
match self.remote.create(file, metadata) {
|
||||
Ok(stream) => Ok(Box::new(stream)),
|
||||
Err(RemoteError {
|
||||
kind: RemoteErrorType::UnsupportedFeature,
|
||||
..
|
||||
}) => {
|
||||
let tempfile = TempMappedFile::new()?;
|
||||
self.write_stream_op = Some(WriteStreamOp {
|
||||
path: file.to_path_buf(),
|
||||
metadata: metadata.clone(),
|
||||
tempfile: tempfile.clone(),
|
||||
});
|
||||
|
||||
Ok(Box::new(tempfile))
|
||||
}
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
|
||||
if let Some(WriteStreamOp {
|
||||
path,
|
||||
metadata,
|
||||
mut tempfile,
|
||||
}) = self.write_stream_op.take()
|
||||
{
|
||||
// sync
|
||||
tempfile.sync()?;
|
||||
// write file
|
||||
self.remote
|
||||
.create_file(&path, &metadata, Box::new(tempfile))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
120
src/host/remote_bridged/temp_mapped_file.rs
Normal file
120
src/host/remote_bridged/temp_mapped_file.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::host::{HostError, HostErrorType, HostResult};
|
||||
|
||||
/// A temporary file mapped to a remote file which has been transferred to local
|
||||
/// and which supports read/write operations
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TempMappedFile {
|
||||
tempfile: Arc<NamedTempFile>,
|
||||
handle: Arc<Mutex<Option<File>>>,
|
||||
}
|
||||
|
||||
impl Write for TempMappedFile {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let rc = self.write_hnd()?;
|
||||
let mut ref_mut = rc.lock().unwrap();
|
||||
ref_mut.as_mut().unwrap().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
let rc = self.write_hnd()?;
|
||||
let mut ref_mut = rc.lock().unwrap();
|
||||
ref_mut.as_mut().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for TempMappedFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let rc = self.read_hnd()?;
|
||||
let mut ref_mut = rc.lock().unwrap();
|
||||
ref_mut.as_mut().unwrap().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl TempMappedFile {
|
||||
pub fn new() -> HostResult<Self> {
|
||||
NamedTempFile::new()
|
||||
.map(|tempfile| TempMappedFile {
|
||||
tempfile: Arc::new(tempfile),
|
||||
handle: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
.map_err(|e| {
|
||||
HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(e),
|
||||
std::path::Path::new(""),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Syncs the file to disk and frees the file handle.
|
||||
///
|
||||
/// Must be called
|
||||
pub fn sync(&mut self) -> HostResult<()> {
|
||||
{
|
||||
let mut lock = self.handle.lock().unwrap();
|
||||
|
||||
if let Some(hnd) = lock.take() {
|
||||
hnd.sync_all().map_err(|e| {
|
||||
HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(e),
|
||||
self.tempfile.path(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
|
||||
{
|
||||
let mut lock = self.handle.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
let hnd = File::create(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.handle.clone())
|
||||
}
|
||||
|
||||
fn read_hnd(&mut self) -> io::Result<Arc<Mutex<Option<File>>>> {
|
||||
{
|
||||
let mut lock = self.handle.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
let hnd = File::open(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_write_and_read_file() {
|
||||
let mut file = TempMappedFile::new().unwrap();
|
||||
file.write_all(b"Hello, World!").unwrap();
|
||||
|
||||
file.sync().unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, b"Hello, World!");
|
||||
}
|
||||
}
|
||||
239
src/main.rs
239
src/main.rs
@@ -1,5 +1,13 @@
|
||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
mod activity_manager;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod explorer;
|
||||
mod filetransfer;
|
||||
mod host;
|
||||
mod support;
|
||||
mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
// Crates
|
||||
#[macro_use]
|
||||
@@ -13,189 +21,178 @@ extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
||||
// External libs
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
// Include
|
||||
mod activity_manager;
|
||||
mod cli_opts;
|
||||
mod config;
|
||||
mod explorer;
|
||||
mod filetransfer;
|
||||
mod host;
|
||||
mod support;
|
||||
mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
use self::activity_manager::{ActivityManager, NextActivity};
|
||||
use self::cli::{Args, ArgsSubcommands, RemoteArgs, RunOpts, Task};
|
||||
use self::system::logging::{self, LogLevel};
|
||||
|
||||
// namespaces
|
||||
use activity_manager::{ActivityManager, NextActivity};
|
||||
use cli_opts::{Args, BookmarkParams, HostParams, Remote, RunOpts, Task};
|
||||
use filetransfer::FileTransferParams;
|
||||
use system::logging::{self, LogLevel};
|
||||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
|
||||
const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
|
||||
const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
|
||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
|
||||
fn main() {
|
||||
type MainResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn git_hash() -> &'static str {
|
||||
APP_GIT_HASH[0..8].as_ref()
|
||||
}
|
||||
|
||||
fn main() -> MainResult<()> {
|
||||
let args: Args = argh::from_env();
|
||||
// Parse args
|
||||
let run_opts: RunOpts = match parse_args(args) {
|
||||
Ok(opts) => opts,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(255);
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
// Setup logging
|
||||
if let Err(err) = logging::init(run_opts.log_level) {
|
||||
eprintln!("Failed to initialize logging: {err}");
|
||||
}
|
||||
info!("termscp {} started!", TERMSCP_VERSION);
|
||||
info!(
|
||||
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
|
||||
git_hash = git_hash()
|
||||
);
|
||||
// Run
|
||||
info!("Starting activity manager...");
|
||||
let rc = run(run_opts);
|
||||
info!("termscp terminated with exitcode {}", rc);
|
||||
// Then return
|
||||
std::process::exit(rc);
|
||||
run(run_opts)
|
||||
}
|
||||
|
||||
/// Parse arguments
|
||||
/// In case of success returns `RunOpts`
|
||||
/// in case something is wrong returns the error message
|
||||
fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
let mut run_opts: RunOpts = RunOpts::default();
|
||||
// Version
|
||||
if args.version {
|
||||
return Err(format!(
|
||||
"termscp - {TERMSCP_VERSION} - Developed by {TERMSCP_AUTHORS}",
|
||||
));
|
||||
}
|
||||
// Setup activity?
|
||||
if args.config {
|
||||
run_opts.task = Task::Activity(NextActivity::SetupActivity);
|
||||
}
|
||||
// Logging
|
||||
if args.debug {
|
||||
run_opts.log_level = LogLevel::Trace;
|
||||
} else if args.quiet {
|
||||
run_opts.log_level = LogLevel::Off;
|
||||
}
|
||||
// Match ticks
|
||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||
// @! extra modes
|
||||
if let Some(theme) = args.theme.as_deref() {
|
||||
run_opts.task = Task::ImportTheme(PathBuf::from(theme));
|
||||
}
|
||||
if args.update {
|
||||
run_opts.task = Task::InstallUpdate;
|
||||
}
|
||||
// @! Ordinary mode
|
||||
// Remote argument
|
||||
match parse_address_arg(&args) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(Remote::None) => {}
|
||||
Ok(remote) => {
|
||||
// Set params
|
||||
run_opts.remote = remote;
|
||||
// In this case the first activity will be FileTransfer
|
||||
run_opts.task = Task::Activity(NextActivity::FileTransfer);
|
||||
}
|
||||
}
|
||||
let run_opts = match args.nested {
|
||||
Some(ArgsSubcommands::Update(_)) => RunOpts::update(),
|
||||
Some(ArgsSubcommands::LoadTheme(args)) => RunOpts::import_theme(args.theme),
|
||||
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
|
||||
None => {
|
||||
let mut run_opts: RunOpts = RunOpts::default();
|
||||
|
||||
// Local directory
|
||||
if let Some(localdir) = args.positional.get(1) {
|
||||
// Change working directory if local dir is set
|
||||
let localdir: PathBuf = PathBuf::from(localdir);
|
||||
if let Err(err) = env::set_current_dir(localdir.as_path()) {
|
||||
return Err(format!("Bad working directory argument: {err}"));
|
||||
// Version
|
||||
if args.version {
|
||||
run_opts.task = Task::Version;
|
||||
return Ok(run_opts);
|
||||
}
|
||||
// Logging
|
||||
if args.debug {
|
||||
run_opts.log_level = LogLevel::Trace;
|
||||
} else if args.quiet {
|
||||
run_opts.log_level = LogLevel::Off;
|
||||
}
|
||||
// set keyring
|
||||
if args.wno_keyring {
|
||||
run_opts.keyring = false;
|
||||
}
|
||||
// Match ticks
|
||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||
// Remote argument
|
||||
match RemoteArgs::try_from(&args) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(remote) => {
|
||||
// Set params
|
||||
run_opts.remote = remote;
|
||||
}
|
||||
}
|
||||
|
||||
// set activity based on remote state
|
||||
run_opts.task = if run_opts.remote.remote.is_none() {
|
||||
Task::Activity(NextActivity::Authentication)
|
||||
} else {
|
||||
Task::Activity(NextActivity::FileTransfer)
|
||||
};
|
||||
|
||||
// Local directory
|
||||
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
|
||||
if let Err(err) = env::set_current_dir(localdir) {
|
||||
return Err(format!("Bad working directory argument: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
run_opts
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(run_opts)
|
||||
}
|
||||
|
||||
/// Parse address argument from cli args
|
||||
fn parse_address_arg(args: &Args) -> Result<Remote, String> {
|
||||
if let Some(remote) = args.positional.get(0) {
|
||||
if args.address_as_bookmark {
|
||||
Ok(Remote::Bookmark(BookmarkParams::new(
|
||||
remote,
|
||||
args.password.as_ref(),
|
||||
)))
|
||||
} else {
|
||||
// Parse address
|
||||
parse_remote_address(remote.as_str())
|
||||
.map(|x| Remote::Host(HostParams::new(x, args.password.as_deref())))
|
||||
}
|
||||
} else {
|
||||
Ok(Remote::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse remote address
|
||||
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
|
||||
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
|
||||
}
|
||||
|
||||
/// Run task and return rc
|
||||
fn run(run_opts: RunOpts) -> i32 {
|
||||
fn run(run_opts: RunOpts) -> MainResult<()> {
|
||||
match run_opts.task {
|
||||
Task::ImportTheme(theme) => run_import_theme(&theme),
|
||||
Task::InstallUpdate => run_install_update(),
|
||||
Task::Activity(activity) => run_activity(activity, run_opts.ticks, run_opts.remote),
|
||||
Task::Activity(activity) => {
|
||||
run_activity(activity, run_opts.ticks, run_opts.remote, run_opts.keyring)
|
||||
}
|
||||
Task::Version => print_version(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_import_theme(theme: &Path) -> i32 {
|
||||
fn print_version() -> MainResult<()> {
|
||||
println!(
|
||||
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
|
||||
git_hash = git_hash()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_import_theme(theme: &Path) -> MainResult<()> {
|
||||
match support::import_theme(theme) {
|
||||
Ok(_) => {
|
||||
println!("Theme has been successfully imported!");
|
||||
0
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
1
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_install_update() -> i32 {
|
||||
fn run_install_update() -> MainResult<()> {
|
||||
match support::install_update() {
|
||||
Ok(msg) => {
|
||||
println!("{msg}");
|
||||
0
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Could not install update: {err}");
|
||||
1
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_activity(activity: NextActivity, ticks: Duration, remote: Remote) -> i32 {
|
||||
fn run_activity(
|
||||
activity: NextActivity,
|
||||
ticks: Duration,
|
||||
remote_args: RemoteArgs,
|
||||
keyring: bool,
|
||||
) -> MainResult<()> {
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks) {
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks, keyring) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {err}");
|
||||
return 1;
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Set file transfer params if set
|
||||
match remote {
|
||||
Remote::Bookmark(BookmarkParams { name, password }) => {
|
||||
if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) {
|
||||
eprintln!("{err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Remote::Host(HostParams { params, password }) => {
|
||||
if let Err(err) = manager.set_filetransfer_params(params, password.as_deref()) {
|
||||
eprintln!("{err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Remote::None => {}
|
||||
if let Err(err) = manager.configure_remote_args(remote_args) {
|
||||
eprintln!("{err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
manager.run(activity);
|
||||
0
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -79,10 +79,7 @@ fn get_config_client() -> Option<ConfigClient> {
|
||||
Err(_) => None,
|
||||
Ok(dir) => {
|
||||
let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path());
|
||||
match ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()) {
|
||||
Err(_) => None,
|
||||
Ok(c) => Some(c),
|
||||
}
|
||||
ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use self_update::backends::github::Update as GithubUpdater;
|
||||
pub use self_update::errors::Error as UpdateError;
|
||||
use self_update::update::Release as UpdRelease;
|
||||
use self_update::{cargo_crate_version, Status};
|
||||
use self_update::{Status, cargo_crate_version};
|
||||
|
||||
use crate::utils::parser::parse_semver;
|
||||
|
||||
@@ -85,6 +85,7 @@ impl Update {
|
||||
|
||||
/// In case received version is newer than current one, version as Some is returned; otherwise None
|
||||
fn check_version(r: Release) -> Option<Release> {
|
||||
debug!("got version from GitHub: {}", r.version);
|
||||
match parse_semver(r.version.as_str()) {
|
||||
Some(new_version) => {
|
||||
// Check if version is different
|
||||
@@ -145,10 +146,13 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)))]
|
||||
#[cfg(all(
|
||||
not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)),
|
||||
not(feature = "isolated-tests")
|
||||
))]
|
||||
fn auto_update() {
|
||||
// Wno version
|
||||
assert_eq!(
|
||||
@@ -162,10 +166,13 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)))]
|
||||
#[cfg(all(
|
||||
not(all(
|
||||
any(target_os = "macos", target_os = "freebsd"),
|
||||
feature = "github-actions"
|
||||
)),
|
||||
not(feature = "isolated-tests")
|
||||
))]
|
||||
fn check_for_updates() {
|
||||
println!("{:?}", Update::is_new_version_available());
|
||||
assert!(Update::is_new_version_available().is_ok());
|
||||
|
||||
@@ -10,13 +10,12 @@ use std::string::ToString;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::keys::filestorage::FileStorage;
|
||||
#[cfg(feature = "with-keyring")]
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::config::{
|
||||
bookmarks::{Bookmark, UserHosts},
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
||||
};
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils::crypto;
|
||||
@@ -39,42 +38,13 @@ impl BookmarksClient {
|
||||
bookmarks_file: &Path,
|
||||
storage_path: &Path,
|
||||
recents_size: usize,
|
||||
keyring: bool,
|
||||
) -> Result<BookmarksClient, SerializerError> {
|
||||
// Create default hosts
|
||||
let default_hosts: UserHosts = UserHosts::default();
|
||||
debug!("Setting up bookmarks client...");
|
||||
// Make a key storage (with-keyring)
|
||||
#[cfg(feature = "with-keyring")]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "termscp";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => {
|
||||
debug!("Using KeyringStorage");
|
||||
(Box::new(storage), app_name)
|
||||
}
|
||||
false => {
|
||||
warn!("KeyringStorage is not supported; using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
||||
}
|
||||
}
|
||||
};
|
||||
// Make a key storage (wno-keyring)
|
||||
#[cfg(not(feature = "with-keyring"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
debug!("Using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
};
|
||||
// Get key storage
|
||||
let (key_storage, service_id) = Self::keyring(storage_path, keyring);
|
||||
// Load key
|
||||
let key: String = match key_storage.get_key(service_id) {
|
||||
Ok(k) => {
|
||||
@@ -130,6 +100,37 @@ impl BookmarksClient {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Get the key storage
|
||||
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
|
||||
if keyring && cfg!(feature = "keyring") {
|
||||
debug!("Setting up KeyStorage");
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "termscp";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => {
|
||||
debug!("Using KeyringStorage");
|
||||
(Box::new(storage), app_name)
|
||||
}
|
||||
false => {
|
||||
warn!("KeyringStorage is not supported; using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
debug!("Using FileStorage");
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over bookmarks keys
|
||||
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.hosts.bookmarks.keys())
|
||||
@@ -389,7 +390,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Verify client
|
||||
assert_eq!(client.hosts.bookmarks.len(), 0);
|
||||
assert_eq!(client.hosts.recents.len(), 0);
|
||||
@@ -405,7 +406,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add some bookmarks
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
@@ -430,7 +431,7 @@ mod tests {
|
||||
let key: String = client.key.clone();
|
||||
// Re-initialize a client
|
||||
let client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Verify it loaded parameters correctly
|
||||
assert_eq!(client.key, key);
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
|
||||
@@ -453,7 +454,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
|
||||
// Verify bookmark
|
||||
@@ -473,7 +474,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
|
||||
// Verify bookmark
|
||||
@@ -494,7 +495,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add s3 bookmark
|
||||
client.add_recent(make_s3_ftparams());
|
||||
// Verify bookmark
|
||||
@@ -517,7 +518,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
@@ -568,7 +569,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
@@ -589,7 +590,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
@@ -617,7 +618,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
@@ -653,7 +654,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
@@ -680,7 +681,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2, true).unwrap();
|
||||
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||
// 1
|
||||
client.add_recent(make_generic_ftparams(
|
||||
@@ -748,7 +749,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
@@ -769,7 +770,7 @@ mod tests {
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
client.key = "MYSUPERSECRETKEY".to_string();
|
||||
assert_eq!(
|
||||
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
// Locals
|
||||
// Ext
|
||||
use std::fs::{create_dir, remove_file, File, OpenOptions};
|
||||
use std::fs::{File, OpenOptions, create_dir, remove_file};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::config::params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD};
|
||||
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
|
||||
use crate::config::params::{DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD, UserConfig};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::explorer::GroupDirs;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
@@ -153,10 +153,7 @@ impl ConfigClient {
|
||||
// Convert string to `GroupDirs`
|
||||
match &self.config.user_interface.group_dirs {
|
||||
None => None,
|
||||
Some(val) => match GroupDirs::from_str(val.as_str()) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
},
|
||||
Some(val) => GroupDirs::from_str(val.as_str()).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +416,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::config::UserConfig;
|
||||
use crate::config::params::UserConfig;
|
||||
use crate::utils::random::random_alphanumeric_with_len;
|
||||
|
||||
#[test]
|
||||
@@ -480,9 +477,11 @@ mod tests {
|
||||
// Change some stuff
|
||||
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
|
||||
client.set_default_protocol(FileTransferProtocol::Scp);
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok());
|
||||
assert!(
|
||||
client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok()
|
||||
);
|
||||
assert!(client.write_config().is_ok());
|
||||
// Istantiate a new client
|
||||
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
@@ -678,9 +677,11 @@ mod tests {
|
||||
.unwrap();
|
||||
// Add a new key
|
||||
let rsa_key: String = get_sample_rsa_key();
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
||||
.is_ok());
|
||||
assert!(
|
||||
client
|
||||
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
||||
.is_ok()
|
||||
);
|
||||
// Iterate keys
|
||||
for key in client.iter_ssh_keys() {
|
||||
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
|
||||
|
||||
@@ -76,30 +76,31 @@ impl KeyStorage for KeyringStorage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
||||
let _ = kring.delete_password();
|
||||
let _ = kring.delete_credential();
|
||||
drop(kring);
|
||||
// Secret should not exist
|
||||
assert!(storage.get_key(app_name).is_err());
|
||||
// Write secret
|
||||
assert!(storage.set_key(app_name, secret).is_ok());
|
||||
// Get secret
|
||||
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
|
||||
assert_eq!(storage.get_key(app_name).unwrap().as_str(), secret);
|
||||
|
||||
// Delete the key manually...
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
||||
assert!(kring.delete_password().is_ok());
|
||||
assert!(kring.delete_credential().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,24 @@
|
||||
|
||||
// Storages
|
||||
pub mod filestorage;
|
||||
#[cfg(feature = "with-keyring")]
|
||||
pub mod keyringstorage;
|
||||
// ext
|
||||
#[cfg(feature = "with-keyring")]
|
||||
use keyring::Error as KeyringError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// defines the error type for the `KeyStorage`
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyStorageError {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
#[error("Key has a bad syntax")]
|
||||
BadSytax,
|
||||
#[error("Provider service error")]
|
||||
ProviderError,
|
||||
#[error("No such key")]
|
||||
NoSuchKey,
|
||||
#[cfg(feature = "with-keyring")]
|
||||
#[error("keyring error: {0}")]
|
||||
KeyringError(KeyringError),
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-keyring")]
|
||||
impl From<KeyringError> for KeyStorageError {
|
||||
fn from(e: KeyringError) -> Self {
|
||||
Self::KeyringError(e)
|
||||
@@ -58,7 +53,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
assert_eq!(
|
||||
KeyStorageError::BadSytax.to_string(),
|
||||
String::from("Key has a bad syntax")
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
Ok(None) => {
|
||||
return Err(String::from(
|
||||
"This system doesn't seem to support CACHE_DIR",
|
||||
))
|
||||
));
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
@@ -25,7 +25,14 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
let file = open_file(log_file_path.as_path(), true, true, false)
|
||||
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
|
||||
// Prepare log config
|
||||
let config = ConfigBuilder::new().set_time_format_rfc3339().build();
|
||||
let config = ConfigBuilder::new()
|
||||
.set_time_format_rfc3339()
|
||||
.add_filter_allow_str("termscp")
|
||||
.add_filter_allow_str("remotefs")
|
||||
.add_filter_allow_str("kube")
|
||||
.add_filter_allow_str("suppaftp")
|
||||
.add_filter_allow_str("pavao")
|
||||
.build();
|
||||
// Make logger
|
||||
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
|
||||
}
|
||||
|
||||
@@ -44,15 +44,29 @@ impl SshKeyStorage {
|
||||
/// Resolve host via ssh2 configuration
|
||||
fn resolve_host_in_ssh2_configuration(&self, host: &str) -> Option<PathBuf> {
|
||||
self.ssh_config.as_ref().and_then(|x| {
|
||||
let key = x
|
||||
.query(host)
|
||||
x.query(host)
|
||||
.identity_file
|
||||
.as_ref()
|
||||
.and_then(|x| x.get(0).cloned());
|
||||
|
||||
key
|
||||
.and_then(|x| x.first().cloned())
|
||||
})
|
||||
}
|
||||
|
||||
/// Get default SSH identity files that SSH would normally try
|
||||
/// This mirrors the behavior of OpenSSH client
|
||||
fn get_default_identity_files(&self) -> Vec<PathBuf> {
|
||||
let Some(home_dir) = dirs::home_dir() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let ssh_dir = home_dir.join(".ssh");
|
||||
|
||||
// Standard SSH identity files in order of preference (matches OpenSSH)
|
||||
["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"]
|
||||
.iter()
|
||||
.map(|key_name| ssh_dir.join(key_name))
|
||||
.filter(|key_path| key_path.exists())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshKeyStorageTrait for SshKeyStorage {
|
||||
@@ -66,9 +80,13 @@ impl SshKeyStorageTrait for SshKeyStorage {
|
||||
username, host
|
||||
);
|
||||
// otherwise search in configuration
|
||||
let key = self.resolve_host_in_ssh2_configuration(host)?;
|
||||
debug!("Found key in SSH config for {host}: {}", key.display());
|
||||
Some(key)
|
||||
if let Some(key) = self.resolve_host_in_ssh2_configuration(host) {
|
||||
debug!("Found key in SSH config for {host}: {}", key.display());
|
||||
return Some(key);
|
||||
}
|
||||
|
||||
// As a final fallback, try default SSH identity files (like regular ssh does)
|
||||
self.get_default_identity_files().into_iter().next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +141,11 @@ mod tests {
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Add ssh key
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok());
|
||||
assert!(
|
||||
client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok()
|
||||
);
|
||||
// Create ssh key storage
|
||||
let storage: SshKeyStorage = SshKeyStorage::from(&client);
|
||||
// Verify key exists
|
||||
@@ -135,13 +155,21 @@ mod tests {
|
||||
*storage.resolve("192.168.1.31", "pi").unwrap(),
|
||||
exp_key_path
|
||||
);
|
||||
// Verify unexisting key
|
||||
assert!(storage.resolve("deskichup", "veeso").is_none());
|
||||
// Verify key is a default key or none
|
||||
let default_keys: Vec<PathBuf> = storage.get_default_identity_files().into_iter().collect();
|
||||
|
||||
if let Some(key) = storage.resolve("deskichup", "veeso") {
|
||||
assert!(default_keys.contains(&key));
|
||||
} else {
|
||||
assert!(default_keys.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sould_resolve_key_from_ssh2_config() {
|
||||
let rsa_key = test_helpers::create_sample_file_with_content("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a");
|
||||
let rsa_key = test_helpers::create_sample_file_with_content(
|
||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a",
|
||||
);
|
||||
let ssh_config_file = test_helpers::create_sample_file_with_content(format!(
|
||||
r#"
|
||||
Host test
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::config::themes::Theme;
|
||||
|
||||
/// ThemeProvider provides a high level API to communicate with the termscp theme
|
||||
@@ -143,7 +143,7 @@ impl ThemeProvider {
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ impl FileToRemove {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileUpdate {
|
||||
/// Path to file which has changed
|
||||
local: PathBuf,
|
||||
host_bridge: PathBuf,
|
||||
/// Path to remote file to update
|
||||
remote: PathBuf,
|
||||
}
|
||||
@@ -152,13 +152,13 @@ impl FileUpdate {
|
||||
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path),
|
||||
local: changed_path,
|
||||
host_bridge: changed_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to local file to sync
|
||||
pub fn local(&self) -> &Path {
|
||||
self.local.as_path()
|
||||
pub fn host_bridge(&self) -> &Path {
|
||||
self.host_bridge.as_path()
|
||||
}
|
||||
|
||||
/// Get path to remote file to sync
|
||||
@@ -288,7 +288,7 @@ mod test {
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/bar.txt"),);
|
||||
assert_eq!(change.host_bridge(), Path::new("/tmp/bar.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
@@ -303,7 +303,7 @@ mod test {
|
||||
Path::new("/home/foo/temp"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/abc/foo.txt"),);
|
||||
assert_eq!(change.host_bridge(), Path::new("/tmp/abc/foo.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
|
||||
@@ -7,12 +7,12 @@ mod change;
|
||||
// -- export
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
|
||||
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use change::FsChange;
|
||||
use notify::{
|
||||
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
Config, Error as WatcherError, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
|
||||
PathNotWatched,
|
||||
#[error("unable to watch path, since it's already watched")]
|
||||
PathAlreadyWatched,
|
||||
#[error("unknown event: {0}")]
|
||||
UnknownEvent(&'static str),
|
||||
#[error("worker error: {0}")]
|
||||
WorkerError(WatcherError),
|
||||
}
|
||||
@@ -37,10 +39,61 @@ impl From<WatcherError> for FsWatcherError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes an event that can be received from the `FsWatcher`
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum FsWatcherEvent {
|
||||
Rename { source: PathBuf, dest: PathBuf },
|
||||
Remove(PathBuf),
|
||||
Create(PathBuf),
|
||||
Modify(PathBuf),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl TryFrom<Event> for FsWatcherEvent {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(ev: Event) -> Result<Self, Self::Error> {
|
||||
match ev.kind {
|
||||
EventKind::Any | EventKind::Access(_) | EventKind::Other => Ok(Self::Other),
|
||||
EventKind::Create(_) => {
|
||||
if ev.paths.len() == 2 {
|
||||
Ok(Self::Rename {
|
||||
source: ev.paths[0].clone(),
|
||||
dest: ev.paths[1].clone(),
|
||||
})
|
||||
} else if let Some(p) = ev.paths.first() {
|
||||
Ok(Self::Create(p.clone()))
|
||||
} else {
|
||||
Err("No path found")
|
||||
}
|
||||
}
|
||||
EventKind::Modify(_) => {
|
||||
if ev.paths.len() == 2 {
|
||||
Ok(Self::Rename {
|
||||
source: ev.paths[0].clone(),
|
||||
dest: ev.paths[1].clone(),
|
||||
})
|
||||
} else if let Some(p) = ev.paths.first() {
|
||||
Ok(Self::Modify(p.clone()))
|
||||
} else {
|
||||
Err("No path found")
|
||||
}
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
if let Some(p) = ev.paths.first() {
|
||||
Ok(Self::Remove(p.clone()))
|
||||
} else {
|
||||
Err("No path found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// File system watcher
|
||||
pub struct FsWatcher {
|
||||
paths: HashMap<PathBuf, PathBuf>,
|
||||
receiver: Receiver<DebouncedEvent>,
|
||||
receiver: Receiver<notify::Result<Event>>,
|
||||
watcher: RecommendedWatcher,
|
||||
}
|
||||
|
||||
@@ -52,29 +105,32 @@ impl FsWatcher {
|
||||
Ok(Self {
|
||||
paths: HashMap::default(),
|
||||
receiver,
|
||||
watcher: watcher(tx, delay)?,
|
||||
watcher: RecommendedWatcher::new(tx, Config::default().with_poll_interval(delay))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll searching for the first available disk change
|
||||
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
|
||||
match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)),
|
||||
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)),
|
||||
Ok(DebouncedEvent::Chmod(p) | DebouncedEvent::Create(p) | DebouncedEvent::Write(p)) => {
|
||||
Ok(self.build_fs_update(p))
|
||||
}
|
||||
Ok(
|
||||
DebouncedEvent::Rescan
|
||||
| DebouncedEvent::NoticeRemove(_)
|
||||
| DebouncedEvent::NoticeWrite(_),
|
||||
) => Ok(None),
|
||||
Ok(DebouncedEvent::Error(e, _)) => {
|
||||
error!("FsWatcher reported error: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => Ok(None),
|
||||
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(res) => res,
|
||||
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
||||
};
|
||||
|
||||
// convert event to FsChange
|
||||
let event = res
|
||||
.map(FsWatcherEvent::try_from)
|
||||
.map_err(FsWatcherError::from)?
|
||||
.map_err(FsWatcherError::UnknownEvent)?;
|
||||
|
||||
match event {
|
||||
FsWatcherEvent::Rename { source, dest } => Ok(self.build_fs_move(source, dest)),
|
||||
FsWatcherEvent::Remove(p) => Ok(self.build_fs_remove(p)),
|
||||
FsWatcherEvent::Modify(p) | FsWatcherEvent::Create(p) => Ok(self.build_fs_update(p)),
|
||||
FsWatcherEvent::Other => {
|
||||
debug!("unknown event");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +245,11 @@ mod test {
|
||||
fn should_watch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// check if in paths
|
||||
assert_eq!(
|
||||
watcher.paths.get(tempdir.path()).unwrap(),
|
||||
@@ -205,16 +263,20 @@ mod test {
|
||||
fn should_not_watch_path_if_subdir_of_watched_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// watch subdir
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
// should return already watched
|
||||
assert!(watcher
|
||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||
.is_err());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||
.is_err()
|
||||
);
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
@@ -223,9 +285,11 @@ mod test {
|
||||
fn should_unwatch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// unwatch
|
||||
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
@@ -237,9 +301,11 @@ mod test {
|
||||
fn should_unwatch_path_when_subdir() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// unwatch
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
@@ -262,9 +328,11 @@ mod test {
|
||||
fn should_tell_whether_path_is_watched() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(watcher.watched(tempdir.path()), true);
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
@@ -280,9 +348,11 @@ mod test {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
// wait
|
||||
@@ -306,9 +376,11 @@ mod test {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
@@ -329,7 +401,7 @@ mod test {
|
||||
|
||||
/*
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn should_poll_file_moved() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
@@ -368,9 +440,11 @@ mod test {
|
||||
fn should_poll_nothing() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(watcher.poll().ok().unwrap().is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
@@ -381,9 +455,11 @@ mod test {
|
||||
fn should_get_watched_paths() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.watch(Path::new("/tmp"), Path::new("/tmp")).is_ok());
|
||||
assert!(watcher
|
||||
.watch(Path::new("/home"), Path::new("/home"))
|
||||
.is_ok());
|
||||
assert!(
|
||||
watcher
|
||||
.watch(Path::new("/home"), Path::new("/home"))
|
||||
.is_ok()
|
||||
);
|
||||
let mut watched_paths = watcher.watched_paths();
|
||||
watched_paths.sort();
|
||||
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, FileTransferParams};
|
||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams, SmbParams};
|
||||
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||
WebDAVProtocolParams,
|
||||
};
|
||||
|
||||
impl AuthActivity {
|
||||
/// Delete bookmark
|
||||
@@ -23,27 +27,49 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Load selected bookmark (at index) to input fields
|
||||
pub(super) fn load_bookmark(&mut self, idx: usize) {
|
||||
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
||||
// Load parameters into components
|
||||
self.load_bookmark_into_gui(bookmark);
|
||||
match form_tab {
|
||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save current input fields as a bookmark
|
||||
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
|
||||
let params = match self.collect_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
|
||||
let params = match form_tab {
|
||||
FormTab::Remote => match self.collect_remote_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
FormTab::HostBridge => match self.collect_host_bridge_params() {
|
||||
Ok(HostBridgeParams::Remote(protocol, params)) => FileTransferParams {
|
||||
protocol,
|
||||
params,
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
},
|
||||
Ok(HostBridgeParams::Localhost(_)) => {
|
||||
self.mount_error("You cannot save a localhost bookmark");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
||||
// Save bookmarks
|
||||
@@ -70,13 +96,16 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Load selected recent (at index) to input fields
|
||||
pub(super) fn load_recent(&mut self, idx: usize) {
|
||||
pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
|
||||
if let Some(client) = self.bookmarks_client() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.recents_list.get(idx) {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// Load parameters
|
||||
self.load_bookmark_into_gui(bookmark);
|
||||
match form_tab {
|
||||
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
|
||||
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +113,7 @@ impl AuthActivity {
|
||||
|
||||
/// Save current input fields as a "recent"
|
||||
pub(super) fn save_recent(&mut self) {
|
||||
let params = match self.collect_host_params() {
|
||||
let params = match self.collect_remote_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
@@ -144,56 +173,125 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Load bookmark data into the gui components
|
||||
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
fn load_host_bridge_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
// Load parameters into components
|
||||
self.protocol = bookmark.protocol;
|
||||
self.mount_protocol(bookmark.protocol);
|
||||
self.host_bridge_protocol = HostBridgeProtocol::Remote(bookmark.protocol);
|
||||
self.mount_host_bridge_protocol(self.host_bridge_protocol);
|
||||
self.mount_remote_directory(
|
||||
FormTab::HostBridge,
|
||||
bookmark
|
||||
.remote_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
self.mount_local_directory(
|
||||
FormTab::HostBridge,
|
||||
bookmark
|
||||
.local_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
match bookmark.params {
|
||||
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
|
||||
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
|
||||
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(params),
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
self.load_bookmark_s3_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
self.load_bookmark_kube_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
|
||||
ProtocolParams::Generic(params) => {
|
||||
self.load_bookmark_generic_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::Smb(params) => {
|
||||
self.load_bookmark_smb_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::WebDAV(params) => {
|
||||
self.load_bookmark_webdav_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
|
||||
self.mount_address(params.address.as_str());
|
||||
self.mount_port(params.port);
|
||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
||||
/// Load bookmark data into the gui components
|
||||
fn load_remote_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
// Load parameters into components
|
||||
self.remote_protocol = bookmark.protocol;
|
||||
self.mount_remote_protocol(bookmark.protocol);
|
||||
self.mount_remote_directory(
|
||||
FormTab::Remote,
|
||||
bookmark
|
||||
.remote_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
self.mount_local_directory(
|
||||
FormTab::Remote,
|
||||
bookmark
|
||||
.local_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
match bookmark.params {
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
self.load_bookmark_s3_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
self.load_bookmark_kube_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
|
||||
ProtocolParams::Generic(params) => {
|
||||
self.load_bookmark_generic_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(FormTab::Remote, params),
|
||||
ProtocolParams::WebDAV(params) => {
|
||||
self.load_bookmark_webdav_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
|
||||
self.mount_s3_bucket(params.bucket_name.as_str());
|
||||
self.mount_s3_region(params.region.as_deref().unwrap_or(""));
|
||||
self.mount_s3_endpoint(params.endpoint.as_deref().unwrap_or(""));
|
||||
self.mount_s3_profile(params.profile.as_deref().unwrap_or(""));
|
||||
self.mount_s3_access_key(params.access_key.as_deref().unwrap_or(""));
|
||||
self.mount_s3_secret_access_key(params.secret_access_key.as_deref().unwrap_or(""));
|
||||
self.mount_s3_security_token(params.security_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_session_token(params.session_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_new_path_style(params.new_path_style);
|
||||
fn load_bookmark_generic_into_gui(&mut self, form_tab: FormTab, params: GenericProtocolParams) {
|
||||
self.mount_address(form_tab, params.address.as_str());
|
||||
self.mount_port(form_tab, params.port);
|
||||
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_smb_into_gui(&mut self, params: SmbParams) {
|
||||
self.mount_address(params.address.as_str());
|
||||
#[cfg(unix)]
|
||||
self.mount_port(params.port);
|
||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
||||
self.mount_smb_share(¶ms.share);
|
||||
#[cfg(unix)]
|
||||
self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or(""));
|
||||
fn load_bookmark_s3_into_gui(&mut self, form_tab: FormTab, params: AwsS3Params) {
|
||||
self.mount_s3_bucket(form_tab, params.bucket_name.as_str());
|
||||
self.mount_s3_region(form_tab, params.region.as_deref().unwrap_or(""));
|
||||
self.mount_s3_endpoint(form_tab, params.endpoint.as_deref().unwrap_or(""));
|
||||
self.mount_s3_profile(form_tab, params.profile.as_deref().unwrap_or(""));
|
||||
self.mount_s3_access_key(form_tab, params.access_key.as_deref().unwrap_or(""));
|
||||
self.mount_s3_secret_access_key(
|
||||
form_tab,
|
||||
params.secret_access_key.as_deref().unwrap_or(""),
|
||||
);
|
||||
self.mount_s3_security_token(form_tab, params.security_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_session_token(form_tab, params.session_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_new_path_style(form_tab, params.new_path_style);
|
||||
}
|
||||
|
||||
fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) {
|
||||
self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or(""));
|
||||
self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_cert(form_tab, params.client_cert.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_key(form_tab, params.client_key.as_deref().unwrap_or(""));
|
||||
self.mount_kube_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) {
|
||||
self.mount_address(form_tab, params.address.as_str());
|
||||
#[cfg(posix)]
|
||||
self.mount_port(form_tab, params.port);
|
||||
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
|
||||
self.mount_smb_share(form_tab, ¶ms.share);
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
||||
self.mount_webdav_uri(form_tab, ¶ms.uri);
|
||||
self.mount_username(form_tab, ¶ms.username);
|
||||
self.mount_password(form_tab, ¶ms.password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputT
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
|
||||
use super::{FormMsg, Msg, UiMsg};
|
||||
use crate::ui::activities::auth::FormTab;
|
||||
|
||||
// -- bookmark list
|
||||
|
||||
@@ -196,7 +197,7 @@ impl DeleteBookmarkPopup {
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
@@ -264,7 +265,7 @@ impl DeleteRecentPopup {
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
@@ -323,10 +324,11 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarkSavePassword {
|
||||
component: Radio,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl BookmarkSavePassword {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
@@ -335,11 +337,12 @@ impl BookmarkSavePassword {
|
||||
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(0)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Save secrets?", Alignment::Center),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,7 +367,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur))
|
||||
}
|
||||
@@ -378,10 +381,11 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
#[derive(MockComponent)]
|
||||
pub struct BookmarkName {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl BookmarkName {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
@@ -393,6 +397,7 @@ impl BookmarkName {
|
||||
.foreground(color)
|
||||
.title("Bookmark name", Alignment::Left)
|
||||
.input_type(InputType::Text),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,7 +452,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,15 @@ pub use bookmarks::{
|
||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||
RecentsList,
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
pub use form::InputSmbWorkgroup;
|
||||
pub use form::{
|
||||
InputAddress, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory,
|
||||
InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region,
|
||||
InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare,
|
||||
InputUsername, ProtocolRadio, RadioS3NewPathStyle,
|
||||
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
|
||||
InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
|
||||
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
||||
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
||||
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
|
||||
RemoteProtocolRadio,
|
||||
};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
|
||||
@@ -28,7 +28,7 @@ impl ErrorPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ impl InfoPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ impl WaitPopup {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(text.as_ref())])
|
||||
.text([TextSpan::from(text.as_ref())])
|
||||
.wrap(true),
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ impl WindowSizeError {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.text(&[TextSpan::from(
|
||||
.text([TextSpan::from(
|
||||
"termscp requires at least 24 lines of height to run",
|
||||
)])
|
||||
.wrap(true),
|
||||
@@ -163,7 +163,7 @@ impl QuitPopup {
|
||||
.foreground(color)
|
||||
.title("Quit termscp?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
.choices(["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ impl InstallUpdatePopup {
|
||||
.foreground(color)
|
||||
.title("Install update?", Alignment::Center)
|
||||
.rewind(true)
|
||||
.choices(&["Yes", "No"]),
|
||||
.choices(["Yes", "No"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,13 +296,7 @@ impl ReleaseNotes {
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Release notes", Alignment::Center)
|
||||
.text_rows(
|
||||
notes
|
||||
.lines()
|
||||
.map(TextSpan::from)
|
||||
.collect::<Vec<TextSpan>>()
|
||||
.as_slice(),
|
||||
),
|
||||
.text_rows(notes.lines().map(TextSpan::from)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ pub struct NewVersionDisclaimer {
|
||||
impl NewVersionDisclaimer {
|
||||
pub fn new(new_version: &str, color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().foreground(color).spans(&[
|
||||
component: Span::default().foreground(color).spans([
|
||||
TextSpan::from("termscp "),
|
||||
TextSpan::new(new_version).underlined().bold(),
|
||||
TextSpan::from(
|
||||
@@ -91,7 +91,7 @@ pub struct HelpFooter {
|
||||
impl HelpFooter {
|
||||
pub fn new(key_color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().spans(&[
|
||||
component: Span::default().spans([
|
||||
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
|
||||
TextSpan::from(" Help "),
|
||||
TextSpan::from("<CTRL+C>").bold().fg(key_color),
|
||||
@@ -100,6 +100,8 @@ impl HelpFooter {
|
||||
TextSpan::from(" Change field "),
|
||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
||||
TextSpan::from(" Switch tab "),
|
||||
TextSpan::from("<BACKTAB>").bold().fg(key_color),
|
||||
TextSpan::from(" Switch form "),
|
||||
TextSpan::from("<ENTER>").bold().fg(key_color),
|
||||
TextSpan::from(" Submit form "),
|
||||
TextSpan::from("<F10|ESC>").bold().fg(key_color),
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
|
||||
use std::env;
|
||||
|
||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
use crate::filetransfer::params::ProtocolParams;
|
||||
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||
use crate::system::notifications::Notification;
|
||||
@@ -14,7 +17,9 @@ impl AuthActivity {
|
||||
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
|
||||
FileTransferProtocol::Ftp(_) => 21,
|
||||
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
|
||||
FileTransferProtocol::Kube => 22, // Doesn't matter, since not used
|
||||
FileTransferProtocol::Smb => 445,
|
||||
FileTransferProtocol::WebDAV => 80, // Doesn't matter, since not used
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,22 +39,72 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Collect host params as `FileTransferParams`
|
||||
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
match self.protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
|
||||
FileTransferProtocol::Smb => self.collect_smb_host_params(),
|
||||
pub(super) fn collect_host_bridge_params(&self) -> Result<HostBridgeParams, &'static str> {
|
||||
match self.host_bridge_protocol {
|
||||
HostBridgeProtocol::Localhost => self.collect_localhost_host_params(),
|
||||
HostBridgeProtocol::Remote(remote) => {
|
||||
let transfer_params = match remote {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::HostBridge),
|
||||
FileTransferProtocol::Kube => {
|
||||
self.collect_kube_host_params(FormTab::HostBridge)
|
||||
}
|
||||
FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::HostBridge),
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
| FileTransferProtocol::Sftp => {
|
||||
self.collect_generic_host_params(remote, FormTab::HostBridge)
|
||||
}
|
||||
FileTransferProtocol::WebDAV => {
|
||||
self.collect_webdav_host_params(FormTab::HostBridge)
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(HostBridgeParams::Remote(
|
||||
transfer_params.protocol,
|
||||
transfer_params.params,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect host params as `FileTransferParams`
|
||||
pub(super) fn collect_remote_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
match self.remote_protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::Remote),
|
||||
FileTransferProtocol::Kube => self.collect_kube_host_params(FormTab::Remote),
|
||||
FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::Remote),
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
| FileTransferProtocol::Sftp => self.collect_generic_host_params(self.protocol),
|
||||
| FileTransferProtocol::Sftp => {
|
||||
self.collect_generic_host_params(self.remote_protocol, FormTab::Remote)
|
||||
}
|
||||
FileTransferProtocol::WebDAV => self.collect_webdav_host_params(FormTab::Remote),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_localhost_host_params(&self) -> Result<HostBridgeParams, &'static str> {
|
||||
// get remote local path
|
||||
let remote_local_path = self.get_input_local_directory(FormTab::Remote);
|
||||
|
||||
// Local path is:
|
||||
// - the input local path if set
|
||||
// - the remote local path if set
|
||||
// - the current directory if neither is set
|
||||
let path = self
|
||||
.get_input_local_directory(FormTab::HostBridge)
|
||||
.or(remote_local_path)
|
||||
.unwrap_or_else(|| env::current_dir().unwrap_or_default());
|
||||
|
||||
Ok(HostBridgeParams::Localhost(path))
|
||||
}
|
||||
|
||||
/// Get input values from fields or return an error if fields are invalid to work as generic
|
||||
pub(super) fn collect_generic_host_params(
|
||||
&self,
|
||||
protocol: FileTransferProtocol,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_generic_params_input();
|
||||
let params = self.get_generic_params_input(form_tab);
|
||||
if params.address.is_empty() {
|
||||
return Err("Invalid host");
|
||||
}
|
||||
@@ -59,31 +114,52 @@ impl AuthActivity {
|
||||
Ok(FileTransferParams {
|
||||
protocol,
|
||||
params: ProtocolParams::Generic(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get input values from fields or return an error if fields are invalid to work as aws s3
|
||||
pub(super) fn collect_s3_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_s3_params_input();
|
||||
pub(super) fn collect_s3_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_s3_params_input(form_tab);
|
||||
if params.bucket_name.is_empty() {
|
||||
return Err("Invalid bucket");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::AwsS3,
|
||||
params: ProtocolParams::AwsS3(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn collect_smb_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_smb_params_input();
|
||||
/// Get input values from fields or return an error if fields are invalid to work as aws s3
|
||||
pub(super) fn collect_kube_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_kube_params_input(form_tab);
|
||||
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::Kube,
|
||||
params: ProtocolParams::Kube(params),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn collect_smb_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_smb_params_input(form_tab);
|
||||
if params.address.is_empty() {
|
||||
return Err("Invalid address");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
if params.port == 0 {
|
||||
return Err("Invalid port");
|
||||
}
|
||||
@@ -93,8 +169,24 @@ impl AuthActivity {
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::Smb,
|
||||
params: ProtocolParams::Smb(params),
|
||||
local_path: self.get_input_local_directory(),
|
||||
remote_path: self.get_input_remote_directory(),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn collect_webdav_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_webdav_params_input(form_tab);
|
||||
if params.uri.is_empty() {
|
||||
return Err("Invalid URI");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::WebDAV,
|
||||
params: ProtocolParams::WebDAV(params),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,24 +17,36 @@ use tuirealm::application::PollStrategy;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::{Application, NoUserEvent, Update};
|
||||
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
// radio
|
||||
const RADIO_PROTOCOL_SFTP: usize = 0;
|
||||
const RADIO_PROTOCOL_SCP: usize = 1;
|
||||
const RADIO_PROTOCOL_FTP: usize = 2;
|
||||
const RADIO_PROTOCOL_FTPS: usize = 3;
|
||||
const RADIO_PROTOCOL_S3: usize = 4;
|
||||
const RADIO_PROTOCOL_SMB: usize = 5;
|
||||
// host bridge protocol radio
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST: usize = 0;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SFTP: usize = 1;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 7;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last
|
||||
|
||||
// remote protocol radio
|
||||
const REMOTE_RADIO_PROTOCOL_SFTP: usize = 0;
|
||||
const REMOTE_RADIO_PROTOCOL_SCP: usize = 1;
|
||||
const REMOTE_RADIO_PROTOCOL_FTP: usize = 2;
|
||||
const REMOTE_RADIO_PROTOCOL_FTPS: usize = 3;
|
||||
const REMOTE_RADIO_PROTOCOL_S3: usize = 4;
|
||||
const REMOTE_RADIO_PROTOCOL_KUBE: usize = 5;
|
||||
const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 6;
|
||||
const REMOTE_RADIO_PROTOCOL_SMB: usize = 7; // Keep as last
|
||||
|
||||
// -- components
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Id {
|
||||
Address,
|
||||
BookmarkName,
|
||||
BookmarkSavePassword,
|
||||
BookmarksList,
|
||||
@@ -43,17 +55,33 @@ pub enum Id {
|
||||
ErrorPopup,
|
||||
GlobalListener,
|
||||
HelpFooter,
|
||||
HostBridge(AuthFormId),
|
||||
InfoPopup,
|
||||
InstallUpdatePopup,
|
||||
Keybindings,
|
||||
LocalDirectory,
|
||||
NewVersionChangelog,
|
||||
NewVersionDisclaimer,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
Remote(AuthFormId),
|
||||
Subtitle,
|
||||
Title,
|
||||
WaitPopup,
|
||||
WindowSizeError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum AuthFormId {
|
||||
Address,
|
||||
KubeNamespace,
|
||||
KubeClusterUrl,
|
||||
KubeUsername,
|
||||
KubeClientCert,
|
||||
KubeClientKey,
|
||||
LocalDirectory,
|
||||
Password,
|
||||
Port,
|
||||
Protocol,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
RemoteDirectory,
|
||||
S3AccessKey,
|
||||
S3Bucket,
|
||||
@@ -65,24 +93,21 @@ pub enum Id {
|
||||
S3SecurityToken,
|
||||
S3SessionToken,
|
||||
SmbShare,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroup,
|
||||
Subtitle,
|
||||
Title,
|
||||
Username,
|
||||
WaitPopup,
|
||||
WindowSizeError,
|
||||
WebDAVUri,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Msg {
|
||||
enum Msg {
|
||||
Form(FormMsg),
|
||||
Ui(UiMsg),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum FormMsg {
|
||||
enum FormMsg {
|
||||
Connect,
|
||||
DeleteBookmark,
|
||||
DeleteRecent,
|
||||
@@ -90,15 +115,14 @@ pub enum FormMsg {
|
||||
InstallUpdate,
|
||||
LoadBookmark(usize),
|
||||
LoadRecent(usize),
|
||||
ProtocolChanged(FileTransferProtocol),
|
||||
HostBridgeProtocolChanged(HostBridgeProtocol),
|
||||
RemoteProtocolChanged(FileTransferProtocol),
|
||||
Quit,
|
||||
SaveBookmark,
|
||||
SaveBookmark(FormTab),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum UiMsg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
BookmarksListBlur,
|
||||
BookmarksTabBlur,
|
||||
CloseDeleteBookmark,
|
||||
@@ -109,6 +133,35 @@ pub enum UiMsg {
|
||||
CloseKeybindingsPopup,
|
||||
CloseQuitPopup,
|
||||
CloseSaveBookmark,
|
||||
HostBridge(UiAuthFormMsg),
|
||||
RececentsListBlur,
|
||||
Remote(UiAuthFormMsg),
|
||||
BookmarkNameBlur,
|
||||
SaveBookmarkPasswordBlur,
|
||||
ShowDeleteBookmarkPopup,
|
||||
ShowDeleteRecentPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowQuitPopup,
|
||||
ShowReleaseNotes,
|
||||
ShowSaveBookmarkPopup,
|
||||
WindowResized,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum UiAuthFormMsg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
ChangeFormTab,
|
||||
KubeNamespaceBlurDown,
|
||||
KubeNamespaceBlurUp,
|
||||
KubeClusterUrlBlurDown,
|
||||
KubeClusterUrlBlurUp,
|
||||
KubeUsernameBlurDown,
|
||||
KubeUsernameBlurUp,
|
||||
KubeClientCertBlurDown,
|
||||
KubeClientCertBlurUp,
|
||||
KubeClientKeyBlurDown,
|
||||
KubeClientKeyBlurUp,
|
||||
LocalDirectoryBlurDown,
|
||||
LocalDirectoryBlurUp,
|
||||
ParamsFormBlur,
|
||||
@@ -118,7 +171,6 @@ pub enum UiMsg {
|
||||
PortBlurUp,
|
||||
ProtocolBlurDown,
|
||||
ProtocolBlurUp,
|
||||
RececentsListBlur,
|
||||
RemoteDirectoryBlurDown,
|
||||
RemoteDirectoryBlurUp,
|
||||
S3AccessKeyBlurDown,
|
||||
@@ -141,21 +193,14 @@ pub enum UiMsg {
|
||||
S3SessionTokenBlurUp,
|
||||
SmbShareBlurDown,
|
||||
SmbShareBlurUp,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroupDown,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroupUp,
|
||||
BookmarkNameBlur,
|
||||
SaveBookmarkPasswordBlur,
|
||||
ShowDeleteBookmarkPopup,
|
||||
ShowDeleteRecentPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowQuitPopup,
|
||||
ShowReleaseNotes,
|
||||
ShowSaveBookmarkPopup,
|
||||
UsernameBlurDown,
|
||||
UsernameBlurUp,
|
||||
WindowResized,
|
||||
WebDAVUriBlurDown,
|
||||
WebDAVUriBlurUp,
|
||||
}
|
||||
|
||||
/// Auth form input mask
|
||||
@@ -163,7 +208,22 @@ pub enum UiMsg {
|
||||
enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
Kube,
|
||||
Localhost,
|
||||
Smb,
|
||||
WebDAV,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum HostBridgeProtocol {
|
||||
Localhost,
|
||||
Remote(FileTransferProtocol),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum FormTab {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
// Store keys
|
||||
@@ -181,8 +241,11 @@ pub struct AuthActivity {
|
||||
exit_reason: Option<ExitReason>,
|
||||
/// Should redraw ui
|
||||
redraw: bool,
|
||||
/// Protocol
|
||||
protocol: FileTransferProtocol,
|
||||
/// Host bridge protocol
|
||||
host_bridge_protocol: HostBridgeProtocol,
|
||||
last_form_tab: FormTab,
|
||||
/// Remote file transfer protocol
|
||||
remote_protocol: FileTransferProtocol,
|
||||
context: Option<Context>,
|
||||
}
|
||||
|
||||
@@ -192,15 +255,17 @@ impl AuthActivity {
|
||||
AuthActivity {
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(ticks)
|
||||
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
|
||||
.poll_timeout(ticks),
|
||||
),
|
||||
context: None,
|
||||
bookmarks_list: Vec::new(),
|
||||
exit_reason: None,
|
||||
last_form_tab: FormTab::Remote,
|
||||
recents_list: Vec::new(),
|
||||
redraw: true,
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
||||
remote_protocol: FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,13 +298,30 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Get current input mask to show
|
||||
fn input_mask(&self) -> InputMask {
|
||||
match self.protocol {
|
||||
fn remote_input_mask(&self) -> InputMask {
|
||||
Self::file_transfer_protocol_input_mask(self.remote_protocol)
|
||||
}
|
||||
|
||||
/// Get current input mask to show
|
||||
fn host_bridge_input_mask(&self) -> InputMask {
|
||||
match self.host_bridge_protocol {
|
||||
HostBridgeProtocol::Localhost => InputMask::Localhost,
|
||||
HostBridgeProtocol::Remote(protocol) => {
|
||||
Self::file_transfer_protocol_input_mask(protocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get input mask for protocol
|
||||
fn file_transfer_protocol_input_mask(protocol: FileTransferProtocol) -> InputMask {
|
||||
match protocol {
|
||||
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
| FileTransferProtocol::Sftp => InputMask::Generic,
|
||||
FileTransferProtocol::Kube => InputMask::Kube,
|
||||
FileTransferProtocol::Smb => InputMask::Smb,
|
||||
FileTransferProtocol::WebDAV => InputMask::WebDAV,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,7 +333,7 @@ impl Activity for AuthActivity {
|
||||
fn on_create(&mut self, mut context: Context) {
|
||||
debug!("Initializing activity");
|
||||
// Initialize file transfer params
|
||||
context.set_ftparams(FileTransferParams::default());
|
||||
context.set_remote_params(FileTransferParams::default());
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ enum SyncBrowsingDestination {
|
||||
impl FileTransferActivity {
|
||||
/// Enter a directory on local host from entry
|
||||
pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
|
||||
self.local_changedir(dir.path(), true);
|
||||
self.host_bridge_changedir(dir.path(), true);
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||
}
|
||||
@@ -35,8 +35,9 @@ impl FileTransferActivity {
|
||||
|
||||
/// Change local directory reading value from input
|
||||
pub(crate) fn action_change_local_dir(&mut self, input: String) {
|
||||
let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.local_changedir(dir_path.as_path(), true);
|
||||
let dir_path: PathBuf =
|
||||
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
|
||||
self.host_bridge_changedir(dir_path.as_path(), true);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||
@@ -55,8 +56,8 @@ impl FileTransferActivity {
|
||||
|
||||
/// Go to previous directory from localhost
|
||||
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
||||
if let Some(d) = self.local_mut().popd() {
|
||||
self.local_changedir(d.as_path(), false);
|
||||
if let Some(d) = self.host_bridge_mut().popd() {
|
||||
self.host_bridge_changedir(d.as_path(), false);
|
||||
// Check whether to sync
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||
@@ -78,10 +79,10 @@ impl FileTransferActivity {
|
||||
/// Go to upper directory on local host
|
||||
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
||||
// Get pwd
|
||||
let path: PathBuf = self.local().wrkdir.clone();
|
||||
let path: PathBuf = self.host_bridge().wrkdir.clone();
|
||||
// Go to parent directory
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.local_changedir(parent, true);
|
||||
self.host_bridge_changedir(parent, true);
|
||||
// If sync is enabled update remote too
|
||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||
@@ -118,7 +119,7 @@ impl FileTransferActivity {
|
||||
trace!("Synchronizing browsing to path {}", path.display());
|
||||
// Check whether destination exists on host
|
||||
let exists = match self.browser.tab() {
|
||||
FileExplorerTab::Local => match self.client.exists(path.as_path()) {
|
||||
FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -129,7 +130,17 @@ impl FileTransferActivity {
|
||||
return;
|
||||
}
|
||||
},
|
||||
FileExplorerTab::Remote => self.host.file_exists(path.as_path()),
|
||||
FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to check whether {} exists on host: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
let name = path
|
||||
@@ -150,13 +161,15 @@ impl FileTransferActivity {
|
||||
trace!("User wants to create the unexisting directory");
|
||||
// Make directory
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_remote_mkdir(name.clone()),
|
||||
FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()),
|
||||
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Do not synchronize, disable sync browsing and return
|
||||
trace!("The user doesn't want to create the directory; disabling synchronized browsing");
|
||||
trace!(
|
||||
"The user doesn't want to create the directory; disabling synchronized browsing"
|
||||
);
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
format!("Refused to create '{name}'; synchronized browsing disabled"),
|
||||
@@ -173,18 +186,18 @@ impl FileTransferActivity {
|
||||
// Enter directory
|
||||
match destination {
|
||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true),
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true),
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||
_ => {}
|
||||
},
|
||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), false),
|
||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), false),
|
||||
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
||||
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
@@ -197,13 +210,13 @@ impl FileTransferActivity {
|
||||
) -> Option<PathBuf> {
|
||||
match (destination, self.browser.tab()) {
|
||||
// NOTE: tab and methods are switched on purpose
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Local) => {
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => {
|
||||
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
}
|
||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
|
||||
self.local().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf())
|
||||
}
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Local) => {
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => {
|
||||
if let Some(p) = self.remote_mut().popd() {
|
||||
Some(p)
|
||||
} else {
|
||||
@@ -212,7 +225,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
||||
if let Some(p) = self.local_mut().popd() {
|
||||
if let Some(p) = self.host_bridge_mut().popd() {
|
||||
Some(p)
|
||||
} else {
|
||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||
|
||||
@@ -3,12 +3,11 @@ use remotefs::fs::UnixPex;
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
impl FileTransferActivity {
|
||||
#[cfg(unix)]
|
||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_local_selected_entries().get_files();
|
||||
|
||||
for file in files {
|
||||
if let Err(err) = self.host.chmod(file.path(), mode) {
|
||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
@@ -51,12 +50,11 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
||||
let files = self.get_found_selected_entries().get_files();
|
||||
|
||||
for file in files {
|
||||
if let Err(err) = self.host.chmod(file.path(), mode) {
|
||||
if let Err(err) = self.host_bridge.chmod(file.path(), mode) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
|
||||
@@ -18,14 +18,15 @@ impl FileTransferActivity {
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Try to copy each file to Input/{FILE_NAME}
|
||||
let base_path: PathBuf = PathBuf::from(input);
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
let mut dest_path: PathBuf = base_path.clone();
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.local_copy_file(entry, dest_path.as_path());
|
||||
self.local_copy_file(&entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -39,21 +40,22 @@ impl FileTransferActivity {
|
||||
self.remote_copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Try to copy each file to Input/{FILE_NAME}
|
||||
let base_path: PathBuf = PathBuf::from(input);
|
||||
// Iter files
|
||||
for entry in entries.into_iter() {
|
||||
let mut dest_path: PathBuf = base_path.clone();
|
||||
for (entry, mut dest_path) in entries.into_iter() {
|
||||
dest_path.push(entry.name());
|
||||
self.remote_copy_file(entry, dest_path.as_path());
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
||||
match self.host.copy(entry, dest) {
|
||||
match self.host_bridge.copy(entry, dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -136,7 +138,7 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// Stat dir
|
||||
let tempdir_entry = match self.host.stat(tempdir_path.as_path()) {
|
||||
let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
@@ -189,7 +191,7 @@ impl FileTransferActivity {
|
||||
return Err(err);
|
||||
}
|
||||
// Get local fs entry
|
||||
let tmpfile_entry = match self.host.stat(tmpfile.path()) {
|
||||
let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) {
|
||||
Ok(e) if e.is_file() => e,
|
||||
Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
|
||||
Err(err) => {
|
||||
|
||||
@@ -16,10 +16,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.local_remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.host_bridge_mut().clear_queue();
|
||||
self.reload_host_bridge_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
@@ -33,17 +37,21 @@ impl FileTransferActivity {
|
||||
}
|
||||
SelectedFile::Many(entries) => {
|
||||
// Iter files
|
||||
for entry in entries.iter() {
|
||||
for (entry, _) in entries.iter() {
|
||||
// Delete file
|
||||
self.remote_remove_file(entry);
|
||||
}
|
||||
|
||||
// clear selection
|
||||
self.remote_mut().clear_queue();
|
||||
self.reload_remote_filelist();
|
||||
}
|
||||
SelectedFile::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
||||
match self.host.remove(entry) {
|
||||
match self.host_bridge.remove(entry) {
|
||||
Ok(_) => {
|
||||
// Log
|
||||
self.log(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user