mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Compare commits
63 Commits
v0.14.0
...
8715c2b6f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
16
.github/workflows/build-artifacts.yml
vendored
16
.github/workflows/build-artifacts.yml
vendored
@@ -3,6 +3,9 @@ name: "Build artifacts"
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERMSCP_VERSION: "0.17.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-binaries:
|
build-binaries:
|
||||||
name: Build - ${{ matrix.platform.release_for }}
|
name: Build - ${{ matrix.platform.release_for }}
|
||||||
@@ -22,21 +25,22 @@ jobs:
|
|||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
targets: ${{ matrix.platform.target }}
|
||||||
target: ${{ matrix.platform.target }}
|
|
||||||
- name: Build release
|
- name: Build release
|
||||||
run: cargo build --release --target ${{ matrix.platform.target }}
|
run: cargo build --release --target ${{ matrix.platform.target }}
|
||||||
- name: Prepare artifact files
|
- name: Prepare artifact files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .artifact
|
mkdir -p .artifact
|
||||||
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
|
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"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
name: ${{ matrix.platform.release_for }}
|
name: termscp-${{ matrix.platform.target }}
|
||||||
path: .artifact/*
|
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
|
- uses: actions/checkout@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: actions-rs/cargo@v1
|
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
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo 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
|
days-before-pr-close: -1
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
exempt-issue-labels: "backlog"
|
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
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v2
|
uses: actions/configure-pages@v5
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: "./site/"
|
path: "./site/"
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build
|
run: cargo build
|
||||||
|
|||||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,6 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
- [Changelog](#changelog)
|
- [Changelog](#changelog)
|
||||||
|
- [0.17.0](#0170)
|
||||||
|
- [0.16.1](#0161)
|
||||||
|
- [0.16.0](#0160)
|
||||||
|
- [0.15.0](#0150)
|
||||||
- [0.14.0](#0140)
|
- [0.14.0](#0140)
|
||||||
- [0.13.0](#0130)
|
- [0.13.0](#0130)
|
||||||
- [0.12.3](#0123)
|
- [0.12.3](#0123)
|
||||||
@@ -36,6 +40,74 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## 0.14.0
|
||||||
|
|
||||||
Released on 17/07/2024
|
Released on 17/07/2024
|
||||||
|
|||||||
@@ -2,75 +2,131 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
contributors and maintainers pledge to making participation in our project and
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to a positive environment for our
|
||||||
include:
|
community include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Focusing on what is best for the community
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
* Showing empathy towards other community members
|
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
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
advances
|
any kind
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or email address,
|
||||||
address, without explicit permission
|
without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* 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
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
response to any instances of unacceptable behavior.
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
decisions when appropriate.
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
when an individual is representing the project or its community. Examples of
|
an individual is officially representing the community in public spaces.
|
||||||
representing a project or community include using an official project e-mail
|
Examples of representing our community include using an official email address,
|
||||||
address, posting via an official social media account, or acting as an appointed
|
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
|
representative at an online or offline event.
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at christian.visintin1997@gmail.com. All
|
reported to the community leaders responsible for enforcement at
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
[INSERT CONTACT METHOD].
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
reporter of any incident.
|
||||||
members of the project's leadership.
|
|
||||||
|
## 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
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
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
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
For answers to common questions about this code of conduct, see
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
<https://www.contributor-covenant.org/faq>
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change.
|
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)
|
- [Contributing](#contributing)
|
||||||
- [Project mission](#project-mission)
|
- [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
|
## 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.
|
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.
|
A lot of things have changed since them, both the features the project provides and my personal view of this project.
|
||||||
|
|||||||
4699
Cargo.lock
generated
4699
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
117
Cargo.toml
117
Cargo.toml
@@ -2,21 +2,16 @@
|
|||||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
homepage = "https://termscp.veeso.dev"
|
homepage = "https://termscp.veeso.dev"
|
||||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
keywords = [
|
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||||
"scp-client",
|
|
||||||
"sftp-client",
|
|
||||||
"ftp-client",
|
|
||||||
"winscp",
|
|
||||||
"command-line-utility",
|
|
||||||
]
|
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "termscp"
|
name = "termscp"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/veeso/termscp"
|
repository = "https://github.com/veeso/termscp"
|
||||||
version = "0.14.0"
|
version = "0.17.0"
|
||||||
|
rust-version = "1.85.0"
|
||||||
|
|
||||||
[package.metadata.rpm]
|
[package.metadata.rpm]
|
||||||
package = "termscp"
|
package = "termscp"
|
||||||
@@ -29,7 +24,7 @@ termscp = { path = "/usr/bin/termscp" }
|
|||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
|
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"
|
extended-description-file = "docs/misc/README.deb.txt"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@@ -38,33 +33,36 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argh = "^0.1"
|
argh = "^0.1"
|
||||||
bitflags = "^2.1"
|
bitflags = "^2"
|
||||||
bytesize = "^1.1"
|
bytesize = "^2"
|
||||||
chrono = "^0.4"
|
chrono = "^0.4"
|
||||||
content_inspector = "^0.2"
|
content_inspector = "^0.2"
|
||||||
dirs = "^5.0"
|
dirs = "^6"
|
||||||
edit = "^0.1"
|
edit = "^0.1"
|
||||||
filetime = "^0.2"
|
filetime = "^0.2"
|
||||||
hostname = "^0.4"
|
hostname = "^0.4"
|
||||||
keyring = { version = "^2.0", optional = true }
|
keyring = { version = "^3", features = [
|
||||||
lazy-regex = "^3.1"
|
"apple-native",
|
||||||
lazy_static = "^1.4"
|
"windows-native",
|
||||||
log = "^0.4"
|
"sync-secret-service",
|
||||||
magic-crypt = "^3.1"
|
"vendored",
|
||||||
notify = "=4.0.17"
|
|
||||||
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
|
||||||
open = "^5.0"
|
|
||||||
rand = "^0.8.5"
|
|
||||||
regex = "^1"
|
|
||||||
remotefs = "^0.2.0"
|
|
||||||
remotefs-aws-s3 = { version = "^0.2.4", default-features = false, features = [
|
|
||||||
"find",
|
|
||||||
"rustls",
|
|
||||||
] }
|
] }
|
||||||
remotefs-kube = "0.2"
|
lazy-regex = "^3"
|
||||||
remotefs-webdav = "^0.1.1"
|
lazy_static = "^1"
|
||||||
rpassword = "^7.0"
|
log = "^0.4"
|
||||||
self_update = { version = "^0.41", default-features = false, features = [
|
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",
|
"rustls",
|
||||||
"archive-tar",
|
"archive-tar",
|
||||||
"archive-zip",
|
"archive-zip",
|
||||||
@@ -73,44 +71,45 @@ self_update = { version = "^0.41", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
serde = { version = "^1", features = ["derive"] }
|
serde = { version = "^1", features = ["derive"] }
|
||||||
simplelog = "^0.12"
|
simplelog = "^0.12"
|
||||||
ssh2-config = "^0.2"
|
ssh2-config = "^0.4"
|
||||||
tempfile = "^3.4"
|
tempfile = "3"
|
||||||
thiserror = "^1"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1.44", features = ["rt"] }
|
||||||
toml = "^0.8"
|
toml = "^0.8"
|
||||||
tui-realm-stdlib = "^1.3.1"
|
tui-realm-stdlib = "2"
|
||||||
tuirealm = "^1.9.1"
|
tuirealm = "2"
|
||||||
unicode-width = "^0.1"
|
unicode-width = "^0.2"
|
||||||
version-compare = "^0.2"
|
version-compare = "^0.2"
|
||||||
whoami = "^1.4"
|
whoami = "^1.5"
|
||||||
wildmatch = "^2.1"
|
wildmatch = "^2"
|
||||||
|
|
||||||
|
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
||||||
|
remotefs-smb = { version = "^0.3", optional = true }
|
||||||
|
|
||||||
|
[target."cfg(target_family = \"unix\")".dependencies]
|
||||||
|
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]
|
[dev-dependencies]
|
||||||
pretty_assertions = "^1.3"
|
pretty_assertions = "^1"
|
||||||
serial_test = "^3"
|
serial_test = "^3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cfg_aliases = "0.2"
|
cfg_aliases = "0.2"
|
||||||
|
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smb", "with-keyring"]
|
default = ["smb", "keyring"]
|
||||||
github-actions = []
|
github-actions = []
|
||||||
with-keyring = ["keyring"]
|
isolated-tests = []
|
||||||
smb = ["remotefs-smb"]
|
keyring = []
|
||||||
|
smb = ["dep:remotefs-smb"]
|
||||||
[target."cfg(not(target_os = \"macos\"))".dependencies]
|
smb-vendored = ["remotefs-smb/vendored"]
|
||||||
remotefs-smb = { version = "^0.2", 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.3.1"
|
|
||||||
|
|
||||||
[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.3.1", features = ["ssh2-vendored"] }
|
|
||||||
users = "0.11.0"
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true
|
incremental = true
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/docs/ptbr/README.md"
|
href="/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Current version: 0.14.0 (17/07/2024)</p>
|
<p align="center">Current version: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -116,11 +116,6 @@
|
|||||||
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
|
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
|
||||||
alt="Windows CI"
|
alt="Windows CI"
|
||||||
/></a>
|
/></a>
|
||||||
<a href="https://coveralls.io/github/veeso/termscp"
|
|
||||||
><img
|
|
||||||
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
|
|
||||||
alt="Coveralls"
|
|
||||||
/></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -247,9 +242,7 @@ The user manual can be found on the [termscp's website](https://termscp.veeso.de
|
|||||||
|
|
||||||
## Upcoming Features 🧪
|
## Upcoming Features 🧪
|
||||||
|
|
||||||
For **2023** there will be two major updates during the year.
|
See [Milestones](https://github.com/veeso/termscp/milestones)
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
23
build.rs
23
build.rs
@@ -1,16 +1,33 @@
|
|||||||
use cfg_aliases::cfg_aliases;
|
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
|
// Setup cfg aliases
|
||||||
cfg_aliases! {
|
cfg_aliases! {
|
||||||
// Platforms
|
// Platforms
|
||||||
macos: { target_os = "macos" },
|
macos: { target_os = "macos" },
|
||||||
linux: { target_os = "linux" },
|
linux: { target_os = "linux" },
|
||||||
unix: { target_family = "unix" },
|
posix: { target_family = "unix" },
|
||||||
windows: { target_family = "windows" },
|
win: { target_family = "windows" },
|
||||||
// exclusive features
|
// exclusive features
|
||||||
smb: { all(feature = "smb", not( macos )) },
|
smb: { all(feature = "smb", not( macos )) },
|
||||||
smb_unix: { all(unix, feature = "smb", not(macos)) },
|
smb_unix: { all(unix, feature = "smb", not(macos)) },
|
||||||
smb_windows: { all(windows, feature = "smb") }
|
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 \
|
libdbus-1-dev \
|
||||||
build-essential \
|
build-essential \
|
||||||
libsmbclient-dev \
|
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 \
|
bash \
|
||||||
curl
|
curl \
|
||||||
|
cpanminus && \
|
||||||
|
cpanm Parse::Yapp::Driver;
|
||||||
|
|
||||||
# Install rust
|
# Install rust
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||||
|
|||||||
2
dist/build/linux-aarch64.sh
vendored
2
dist/build/linux-aarch64.sh
vendored
@@ -38,7 +38,7 @@ cd -
|
|||||||
mkdir -p ${PKGS_DIR}/deb/
|
mkdir -p ${PKGS_DIR}/deb/
|
||||||
mkdir -p ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
mkdir -p ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
||||||
docker run --name "$ARM64_DEB_NAME" -d "$ARM64_DEB_NAME" || docker start "$ARM64_DEB_NAME"
|
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 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/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 cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/aarch64-unknown-linux-gnu/
|
||||||
docker stop "$ARM64_DEB_NAME"
|
docker stop "$ARM64_DEB_NAME"
|
||||||
|
|||||||
4
dist/build/linux-x86_64.sh
vendored
4
dist/build/linux-x86_64.sh
vendored
@@ -30,13 +30,13 @@ PKGS_DIR=$(pwd)/pkgs
|
|||||||
cd -
|
cd -
|
||||||
mkdir -p ${PKGS_DIR}/
|
mkdir -p ${PKGS_DIR}/
|
||||||
# Build x86_64_deb
|
# Build x86_64_deb
|
||||||
cd x86_64_debian10/
|
cd x86_64_debian12/
|
||||||
docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_DEB_NAME" .
|
docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_DEB_NAME" .
|
||||||
cd -
|
cd -
|
||||||
mkdir -p ${PKGS_DIR}/deb/
|
mkdir -p ${PKGS_DIR}/deb/
|
||||||
mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/
|
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 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 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/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 cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/x86_64-unknown-linux-gnu/
|
||||||
docker stop "$X86_64_DEB_NAME"
|
docker stop "$X86_64_DEB_NAME"
|
||||||
|
|||||||
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"]
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Aktuelle Version: 0.14.0 (17/07/2024)</p>
|
<p align="center">Aktuelle Version: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
- [Dateiexplorer 📂](#dateiexplorer-)
|
- [Dateiexplorer 📂](#dateiexplorer-)
|
||||||
- [Tastenkombinationen ⌨](#tastenkombinationen-)
|
- [Tastenkombinationen ⌨](#tastenkombinationen-)
|
||||||
- [Mit mehreren Dateien arbeiten 🥷](#mit-mehreren-dateien-arbeiten-)
|
- [Mit mehreren Dateien arbeiten 🥷](#mit-mehreren-dateien-arbeiten-)
|
||||||
|
- [Beispiel](#beispiel)
|
||||||
- [Synchronisiertes Durchsuchen ⏲️](#synchronisiertes-durchsuchen-️)
|
- [Synchronisiertes Durchsuchen ⏲️](#synchronisiertes-durchsuchen-️)
|
||||||
- [Öffnen und Öffnen mit 🚪](#öffnen-und-öffnen-mit-)
|
- [Öffnen und Öffnen mit 🚪](#öffnen-und-öffnen-mit-)
|
||||||
- [Lesezeichen ⭐](#lesezeichen-)
|
- [Lesezeichen ⭐](#lesezeichen-)
|
||||||
@@ -62,11 +63,11 @@
|
|||||||
|
|
||||||
termscp kann mit den folgenden Optionen gestartet werden:
|
termscp kann mit den folgenden Optionen gestartet werden:
|
||||||
|
|
||||||
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
|
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
|
||||||
|
|
||||||
ODER
|
ODER
|
||||||
|
|
||||||
`termscp [Optionen]... -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
|
`termscp [Optionen]... -b [Lesezeichen-Name] -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
|
||||||
|
|
||||||
- `-P, --password <Passwort>` wenn Adresse angegeben wird, ist das Passwort dieses Argument
|
- `-P, --password <Passwort>` wenn Adresse angegeben wird, ist das Passwort dieses Argument
|
||||||
- `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf
|
- `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf
|
||||||
@@ -133,7 +134,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
Falls Sie eine Verbindung zu Kube herstellen möchten, verwenden Sie die folgende Syntax
|
Falls Sie eine Verbindung zu Kube herstellen möchten, verwenden Sie die folgende Syntax
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### SMB Adressargument
|
#### SMB Adressargument
|
||||||
@@ -297,19 +298,34 @@ Diese Panels sind im Wesentlichen 3 (ja, tatsächlich drei):
|
|||||||
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
|
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
|
||||||
| <CTRL+T> | Alle synchronisierten Pfade anzeigen | Track |
|
| <CTRL+T> | Alle synchronisierten Pfade anzeigen | Track |
|
||||||
|
|
||||||
### Mit mehreren Dateien arbeiten 🥷
|
### Mit mehreren Dateien arbeiten 🥷
|
||||||
|
|
||||||
Sie können mit mehreren Dateien arbeiten, indem Sie `<M>` drücken, um die aktuelle Datei auszuwählen, oder `<CTRL+A>`, um alle Dateien im Arbeitsverzeichnis auszuwählen.
|
Du kannst mit mehreren Dateien gleichzeitig arbeiten, mit diesen einfachen Tastenkombinationen:
|
||||||
Sobald eine Datei zur Auswahl markiert ist, wird sie mit einem `*` auf der linken Seite angezeigt.
|
|
||||||
Bei der Arbeit mit der Auswahl werden nur die ausgewählten Dateien für Aktionen verarbeitet, während der aktuell hervorgehobene Eintrag ignoriert wird.
|
|
||||||
Es ist auch möglich, mit mehreren Dateien im Suchergebnis-Panel zu arbeiten.
|
|
||||||
Alle Aktionen sind verfügbar, wenn Sie mit mehreren Dateien arbeiten, aber beachten Sie, dass einige Aktionen etwas anders funktionieren. Schauen wir uns das genauer an:
|
|
||||||
|
|
||||||
- _Kopieren_: Wann immer Sie eine Datei kopieren, werden Sie aufgefordert, den Zielnamen einzugeben. Bei der Arbeit mit mehreren Dateien bezieht sich dieser Name auf das Zielverzeichnis, in dem alle diese Dateien kopiert werden.
|
- `<M>`: Datei zur Auswahl markieren
|
||||||
|
- `<CTRL+A>`: alle Dateien im aktuellen Verzeichnis auswählenas
|
||||||
|
- `<ALT+A>`: Auswahl aller Dateien aufheben
|
||||||
|
|
||||||
- _Umbenennen_: Dasselbe wie Kopieren, aber die Dateien werden dorthin verschoben.
|
Markierte Dateien werden **mit hervorgehobenem Hintergrund** angezeigt.
|
||||||
|
Bei Auswahlaktionen werden nur die markierten Dateien verarbeitet, das aktuell hervorgehobene Element wird ignoriert.
|
||||||
|
|
||||||
- _Speichern unter_: Dasselbe wie Kopieren, aber die Dateien werden dorthin geschrieben.
|
Auch im Suchergebnis-Panel ist die Mehrfachauswahl möglich.
|
||||||
|
|
||||||
|
Alle Aktionen sind bei mehreren Dateien verfügbar, einige funktionieren jedoch leicht anders:
|
||||||
|
|
||||||
|
- *Kopieren*: du wirst nach einem Zielnamen gefragt. Bei mehreren Dateien ist das das Zielverzeichnis.
|
||||||
|
- *Umbenennen*: wie Kopieren, aber verschiebt die Dateien.
|
||||||
|
- *Speichern unter*: wie Kopieren, aber schreibt die Dateien dorthin.
|
||||||
|
|
||||||
|
Wenn du eine Datei in einem Verzeichnis (z. B. `/home`) auswählst und dann das Verzeichnis wechselst, bleibt sie ausgewählt und erscheint in der **Transfer-Warteschlange** im unteren Panel.
|
||||||
|
Beim Markieren einer Datei wird das aktuelle *Remote*-Verzeichnis gespeichert; bei einem Transfer wird sie in dieses Verzeichnis übertragen.
|
||||||
|
|
||||||
|
#### Beispiel
|
||||||
|
|
||||||
|
Wenn wir `/home/a.txt` lokal auswählen und im Remote-Panel in `/tmp` sind, dann zu `/var` wechseln, `/var/b.txt` auswählen und im Remote-Panel in `/home` sind, ergibt der Transfer:
|
||||||
|
|
||||||
|
- `/home/a.txt` → `/tmp/a.txt`
|
||||||
|
- `/var/b.txt` → `/home/b.txt`
|
||||||
|
|
||||||
### Synchronisiertes Durchsuchen ⏲️
|
### Synchronisiertes Durchsuchen ⏲️
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versión actual: 0.14.0 (17/07/2024)</p>
|
<p align="center">Versión actual: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
- [Credenciales de S3 🦊](#credenciales-de-s3-)
|
||||||
- [Explorador de archivos 📂](#explorador-de-archivos-)
|
- [Explorador de archivos 📂](#explorador-de-archivos-)
|
||||||
- [Keybindings ⌨](#keybindings-)
|
- [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-️)
|
- [Navegación sincronizada ⏲️](#navegación-sincronizada-️)
|
||||||
- [Abierta y abierta con 🚪](#abierta-y-abierta-con-)
|
- [Abierta y abierta con 🚪](#abierta-y-abierta-con-)
|
||||||
- [Marcadores ⭐](#marcadores-)
|
- [Marcadores ⭐](#marcadores-)
|
||||||
@@ -39,11 +40,11 @@
|
|||||||
|
|
||||||
termscp se puede iniciar con las siguientes opciones:
|
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
|
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
|
- `-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
|
- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador
|
||||||
@@ -110,7 +111,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
En caso de que quieras conectarte a Kube, utiliza la siguiente sintaxis
|
En caso de que quieras conectarte a Kube, utiliza la siguiente sintaxis
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Argumento de dirección de WebDAV
|
#### Argumento de dirección de WebDAV
|
||||||
@@ -259,17 +260,34 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
|
|||||||
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
|
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
|
||||||
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |
|
| `<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.
|
Puedes optar por trabajar con varios archivos, usando estos controles:
|
||||||
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:
|
|
||||||
|
|
||||||
- *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.
|
- `<M>`: marcar un archivo para selección
|
||||||
- *Rename*: igual que copiar, pero moverá archivos allí.
|
- `<CTRL+A>`: seleccionar todos los archivos del directorio actual
|
||||||
- *Save as*: igual que copiar, pero los escribirá allí.
|
- `<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 ⏲️
|
### Navegación sincronizada ⏲️
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Version actuelle: 0.14.0 (17/07/2024)</p>
|
<p align="center">Version actuelle: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
|
||||||
- [Raccourcis clavier ⌨](#raccourcis-clavier-)
|
- [Raccourcis clavier ⌨](#raccourcis-clavier-)
|
||||||
- [Travailler sur plusieurs fichiers 🥷](#travailler-sur-plusieurs-fichiers-)
|
- [Travailler sur plusieurs fichiers 🥷](#travailler-sur-plusieurs-fichiers-)
|
||||||
|
- [Exemple](#exemple)
|
||||||
- [Navigation synchronisée ⏲️](#navigation-synchronisée-️)
|
- [Navigation synchronisée ⏲️](#navigation-synchronisée-️)
|
||||||
- [Ouvrir et ouvrir avec 🚪](#ouvrir-et-ouvrir-avec-)
|
- [Ouvrir et ouvrir avec 🚪](#ouvrir-et-ouvrir-avec-)
|
||||||
- [Signets ⭐](#signets-)
|
- [Signets ⭐](#signets-)
|
||||||
@@ -37,11 +38,11 @@
|
|||||||
|
|
||||||
termscp peut être démarré avec les options suivantes :
|
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
|
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
|
- `-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
|
- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet
|
||||||
@@ -108,7 +109,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante
|
Si vous souhaitez vous connecter à Kube, utilisez la syntaxe suivante
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Argument d'adresse WebDAV
|
#### Argument d'adresse WebDAV
|
||||||
@@ -258,17 +259,34 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
|
|||||||
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
|
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
|
||||||
| `<CTRL+T>` | Afficher tous les chemins synchronisés | Track |
|
| `<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.
|
Vous pouvez choisir de travailler sur plusieurs fichiers avec ces simples commandes :
|
||||||
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:
|
|
||||||
|
|
||||||
- *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.
|
- `<M>` : marquer un fichier à sélectionner
|
||||||
- *Rename*: identique à la copie, mais y déplacera les fichiers.
|
- `<CTRL+A>` : sélectionner tous les fichiers du répertoire actuel
|
||||||
- *Save as*: identique à la copie, mais les y écrira.
|
- `<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 ⏲️
|
### Navigation synchronisée ⏲️
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versione corrente: 0.14.0 (17/07/2024)</p>
|
<p align="center">Versione corrente: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
- [Credenziali S3 🦊](#credenziali-s3-)
|
- [Credenziali S3 🦊](#credenziali-s3-)
|
||||||
- [File explorer 📂](#file-explorer-)
|
- [File explorer 📂](#file-explorer-)
|
||||||
- [Abbinamento tasti ⌨](#abbinamento-tasti-)
|
- [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-️)
|
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||||
- [Apri e apri con 🚪](#apri-e-apri-con-)
|
- [Apri e apri con 🚪](#apri-e-apri-con-)
|
||||||
- [Segnalibri ⭐](#segnalibri-)
|
- [Segnalibri ⭐](#segnalibri-)
|
||||||
@@ -37,11 +38,11 @@
|
|||||||
|
|
||||||
termscp può essere lanciato con questi argomenti:
|
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
|
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
|
- `-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
|
- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro
|
||||||
@@ -106,7 +107,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
Nel caso tu voglia connetterti a Kube usa la seguente sintassi
|
Nel caso tu voglia connetterti a Kube usa la seguente sintassi
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Argomento indirizzo per WebDAV
|
#### Argomento indirizzo per WebDAV
|
||||||
@@ -254,17 +255,34 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
|
|||||||
| `<CTRL+C>` | Annulla trasferimento file | |
|
| `<CTRL+C>` | Annulla trasferimento file | |
|
||||||
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
|
| `<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.
|
Puoi scegliere di lavorare con più file, usando questi semplici comandi:
|
||||||
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:
|
|
||||||
|
|
||||||
- *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.
|
- `<M>`: marca un file per la selezione
|
||||||
- *Rinomina*: Come il copia, ma li sposterà.
|
- `<CTRL+A>`: seleziona tutti i file nella directory corrente
|
||||||
- *Salva con nome*: Come il copia, ma li trasferirà.
|
- `<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 ⏲️
|
### Synchronized browsing ⏲️
|
||||||
|
|
||||||
|
|||||||
33
docs/man.md
33
docs/man.md
@@ -16,6 +16,7 @@
|
|||||||
- [File explorer 📂](#file-explorer-)
|
- [File explorer 📂](#file-explorer-)
|
||||||
- [Keybindings ⌨](#keybindings-)
|
- [Keybindings ⌨](#keybindings-)
|
||||||
- [Work on multiple files 🥷](#work-on-multiple-files-)
|
- [Work on multiple files 🥷](#work-on-multiple-files-)
|
||||||
|
- [Example](#example)
|
||||||
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||||
- [Open and Open With 🚪](#open-and-open-with-)
|
- [Open and Open With 🚪](#open-and-open-with-)
|
||||||
- [Bookmarks ⭐](#bookmarks-)
|
- [Bookmarks ⭐](#bookmarks-)
|
||||||
@@ -40,13 +41,15 @@
|
|||||||
|
|
||||||
termscp can be started with the following options:
|
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
|
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
|
- `-b, --address-as-bookmark` resolve address argument as a bookmark name
|
||||||
- `-q, --quiet` Disable logging
|
- `-q, --quiet` Disable logging
|
||||||
- `-v, --version` Print version info
|
- `-v, --version` Print version info
|
||||||
@@ -111,7 +114,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
In case you want to connect to Kube use the following syntax
|
In case you want to connect to Kube use the following syntax
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### WebDAV address argument
|
#### WebDAV address argument
|
||||||
@@ -272,16 +275,34 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
|||||||
|
|
||||||
### Work on multiple files 🥷
|
### 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.
|
You can opt to work on multiple files, with these simple controls:
|
||||||
Once a file is marked for selection, it will be displayed with a `*` on the left.
|
|
||||||
|
- `<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.
|
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.
|
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:
|
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.
|
- *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.
|
- *Rename*: same as copy, but will move files there.
|
||||||
- *Save as*: same as copy, but will write them 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 ⏲️
|
### Synchronized browsing ⏲️
|
||||||
|
|
||||||
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
|
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
|
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||||
<p align="center">Versão atual: 0.14.0 (17/07/2024)</p>
|
<p align="center">Versão atual: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
- [Credenciais do S3 🦊](#credenciais-do-s3-)
|
||||||
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
|
||||||
- [Atalhos de Teclado ⌨](#atalhos-de-teclado-)
|
- [Atalhos de Teclado ⌨](#atalhos-de-teclado-)
|
||||||
- [Trabalhar com Vários Arquivos 🥷](#trabalhar-com-vários-arquivos-)
|
- [Trabalhar com múltiplos arquivos 🥷](#trabalhar-com-múltiplos-arquivos-)
|
||||||
|
- [Exemplo](#exemplo)
|
||||||
- [Navegação Sincronizada ⏲️](#navegação-sincronizada-️)
|
- [Navegação Sincronizada ⏲️](#navegação-sincronizada-️)
|
||||||
- [Abrir e Abrir Com 🚪](#abrir-e-abrir-com-)
|
- [Abrir e Abrir Com 🚪](#abrir-e-abrir-com-)
|
||||||
- [Favoritos ⭐](#favoritos-)
|
- [Favoritos ⭐](#favoritos-)
|
||||||
@@ -40,11 +41,11 @@
|
|||||||
|
|
||||||
O termscp pode ser iniciado com as seguintes opções:
|
O termscp pode ser iniciado com as seguintes opções:
|
||||||
|
|
||||||
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
|
`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
|
OU
|
||||||
|
|
||||||
`termscp [opções]... -b [nome-do-favorito] [diretório-trabalho-local]`
|
`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
|
- `-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
|
- `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito
|
||||||
@@ -111,7 +112,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
Caso queira se conectar ao Kube, use a seguinte sintaxe
|
Caso queira se conectar ao Kube, use a seguinte sintaxe
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Argumento de Endereço do WebDAV
|
#### Argumento de Endereço do WebDAV
|
||||||
@@ -272,17 +273,34 @@ Para trocar de painel, você precisa pressionar `<LEFT>` para mover para o paine
|
|||||||
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
|
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
|
||||||
| `<CTRL+T>` | Mostrar todos os caminhos sincronizados | Track |
|
| `<CTRL+T>` | Mostrar todos os caminhos sincronizados | Track |
|
||||||
|
|
||||||
### Trabalhar com Vários Arquivos 🥷
|
### Trabalhar com múltiplos arquivos 🥷
|
||||||
|
|
||||||
Você pode optar por trabalhar com vários arquivos, selecionando-os pressionando `<M>`, para selecionar o arquivo atual, ou pressionando `<CTRL+A>`, que selecionará todos os arquivos no diretório de trabalho.
|
Você pode optar por trabalhar com vários arquivos, usando estes controles simples:
|
||||||
Uma vez que um arquivo esteja marcado para seleção, ele será exibido com um `*` à esquerda.
|
|
||||||
Ao trabalhar com seleção, apenas o arquivo selecionado será processado para ações, enquanto o item destacado atual será ignorado.
|
|
||||||
É possível trabalhar com vários arquivos também quando estiver no painel de resultados da busca.
|
|
||||||
Todas as ações estão disponíveis ao trabalhar com vários arquivos, mas tenha em mente que algumas ações funcionam de forma ligeiramente diferente. Vamos explicar algumas delas:
|
|
||||||
|
|
||||||
- *Copiar*: sempre que você copiar um arquivo, você será solicitado a inserir o nome de destino. Ao trabalhar com vários arquivos, esse nome refere-se ao diretório de destino onde todos esses arquivos serão copiados.
|
- `<M>`: marcar um arquivo para seleção
|
||||||
- *Renomear*: igual ao copiar, mas moverá os arquivos para lá.
|
- `<CTRL+A>`: selecionar todos os arquivos no diretório atual
|
||||||
- *Salvar como*: igual ao copiar, mas gravará lá.
|
- `<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 ⏲️
|
### Navegação Sincronizada ⏲️
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/veeso/termscp/blob/main/docs/ptbr/README.md"
|
href="https://github.com/veeso/termscp/blob/main/docs/pt-BR/README.md"
|
||||||
><img
|
><img
|
||||||
height="20"
|
height="20"
|
||||||
src="/assets/images/flags/br.png"
|
src="/assets/images/flags/br.png"
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
/></a>
|
/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">由 <a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</p>
|
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
||||||
<p align="center">当前版本: 0.14.0 (17/07/2024)</p>
|
<p align="center">当前版本: 0.17.0 24/03/2025</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/MIT"
|
<a href="https://opensource.org/licenses/MIT"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
- [Aws S3 凭证](#aws-s3-凭证)
|
- [Aws S3 凭证](#aws-s3-凭证)
|
||||||
- [文件浏览](#文件浏览)
|
- [文件浏览](#文件浏览)
|
||||||
- [快捷键](#快捷键)
|
- [快捷键](#快捷键)
|
||||||
- [处理多个文件](#处理多个文件)
|
- [操作多个文件 🥷](#操作多个文件-)
|
||||||
|
- [示例](#示例)
|
||||||
- [同步浏览](#同步浏览)
|
- [同步浏览](#同步浏览)
|
||||||
- [打开/打开方式](#打开打开方式)
|
- [打开/打开方式](#打开打开方式)
|
||||||
- [书签](#书签)
|
- [书签](#书签)
|
||||||
@@ -37,11 +38,11 @@
|
|||||||
|
|
||||||
termscp启动时可以使用以下选项:
|
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>` 登陆密码
|
- `-P, --password <password>` 登陆密码
|
||||||
- `-b, --address-as-bookmark` 将地址参数解析为书签名称
|
- `-b, --address-as-bookmark` 将地址参数解析为书签名称
|
||||||
@@ -108,7 +109,7 @@ s3://buckethead@eu-central-1:default:/assets
|
|||||||
如果您想连接到 Kube,请使用以下语法
|
如果您想连接到 Kube,请使用以下语法
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
kube://<container>@<pod></path>
|
kube://[namespace][@<cluster_url>][$</path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### WebDAV 地址参数
|
#### WebDAV 地址参数
|
||||||
@@ -254,14 +255,34 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
|||||||
| `<CTRL+C>` | 终止文件传输 | |
|
| `<CTRL+C>` | 终止文件传输 | |
|
||||||
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
| `<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`
|
||||||
|
|
||||||
### 同步浏览
|
### 同步浏览
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# -f, -y, --force, --yes
|
# -f, -y, --force, --yes
|
||||||
# Skip the confirmation prompt during installation
|
# Skip the confirmation prompt during installation
|
||||||
|
|
||||||
TERMSCP_VERSION="0.14.0"
|
TERMSCP_VERSION="0.17.0"
|
||||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${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_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
|
||||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
|
||||||
@@ -285,7 +285,7 @@ install_bsd_cargo_deps() {
|
|||||||
|
|
||||||
install_linux_cargo_deps() {
|
install_linux_cargo_deps() {
|
||||||
local debian_deps="gcc pkg-config libdbus-1-dev libsmbclient-dev"
|
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 arch_deps="gcc openssl pkg-config dbus smbclient"
|
||||||
local deps_cmd=""
|
local deps_cmd=""
|
||||||
# Get pkg manager
|
# Get pkg manager
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
<p class="text-xs font-thin">
|
<p class="text-xs font-thin">
|
||||||
<span>Christian Visintin © </span><span resolve-copyright></span>
|
<span>Christian Visintin © </span><span resolve-copyright></span>
|
||||||
<span> | </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>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
<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,
|
since last release, so if the latest version is not available yet,
|
||||||
you can install it downloading the ZIP file from</span>
|
you can install it downloading the ZIP file from</span>
|
||||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.14.0.nupkg"
|
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.17.0.nupkg"
|
||||||
target="_blank">Github</a>
|
target="_blank">Github</a>
|
||||||
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
On Debian based distros, you can install termscp using the Deb
|
On Debian based distros, you can install termscp using the Deb
|
||||||
package via:
|
package via:
|
||||||
</p>
|
</p>
|
||||||
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.14.0_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.17.0_amd64.deb</span>
|
||||||
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
@@ -157,7 +157,7 @@ sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</spa
|
|||||||
</p>
|
</p>
|
||||||
<pre><span class="function">cargo</span> install --locked --no-default-features --features smb <span class="string">termscp</span></pre>
|
<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>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
<span translate="intro.versionAlert">termscp 0.14.0 is NOW out! Download it from</span>
|
<span translate="intro.versionAlert">termscp 0.17.0 is NOW out! Download it from</span>
|
||||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Get started →",
|
"getStarted": "Get started →",
|
||||||
"versionAlert": "termscp 0.14.0 is NOW out! Download it from",
|
"versionAlert": "termscp 0.17.0 is NOW out! Download it from",
|
||||||
"here": "here",
|
"here": "here",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Para iniciar →",
|
"getStarted": "Para iniciar →",
|
||||||
"versionAlert": "termscp 0.14.0 ya está disponible! Descárgalo desde",
|
"versionAlert": "termscp 0.17.0 ya está disponible! Descárgalo desde",
|
||||||
"here": "aquì",
|
"here": "aquì",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "Pour commencer →",
|
"getStarted": "Pour commencer →",
|
||||||
"versionAlert": "termscp 0.14.0 est maintenant sorti! Télécharge-le depuis",
|
"versionAlert": "termscp 0.17.0 est maintenant sorti! Télécharge-le depuis",
|
||||||
"here": "ici",
|
"here": "ici",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"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"
|
"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": {
|
"intro": {
|
||||||
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
|
||||||
"getStarted": "Installa termscp →",
|
"getStarted": "Installa termscp →",
|
||||||
"versionAlert": "termscp 0.14.0 è ORA disponbile! Scaricalo da",
|
"versionAlert": "termscp 0.17.0 è ORA disponbile! Scaricalo da",
|
||||||
"here": "qui",
|
"here": "qui",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"intro": {
|
"intro": {
|
||||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||||
"getStarted": "开始 →",
|
"getStarted": "开始 →",
|
||||||
"versionAlert": "termscp 0.14.0 现已发布! 从下载",
|
"versionAlert": "termscp 0.17.0 现已发布! 从下载",
|
||||||
"here": "这里",
|
"here": "这里",
|
||||||
"features": {
|
"features": {
|
||||||
"handy": {
|
"handy": {
|
||||||
@@ -112,4 +112,4 @@
|
|||||||
"then": "启动后,系统将提示您是否安装更新。 确认安装和 ta-dah,新版本的termscp 现在应该可以在你的机器上使用了"
|
"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
|
//! `activity_manager` is the module which provides run methods and handling for activities
|
||||||
|
|
||||||
// Deps
|
use std::env;
|
||||||
// Namespaces
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
||||||
|
|
||||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
use crate::cli::{Remote, RemoteArgs};
|
||||||
use crate::host::{HostError, Localhost};
|
use crate::filetransfer::{
|
||||||
|
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
||||||
|
};
|
||||||
|
use crate::host::HostError;
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
@@ -30,6 +32,16 @@ pub enum NextActivity {
|
|||||||
SetupActivity,
|
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
|
/// The activity manager takes care of running activities and handling them until the application has ended
|
||||||
pub struct ActivityManager {
|
pub struct ActivityManager {
|
||||||
context: Option<Context>,
|
context: Option<Context>,
|
||||||
@@ -38,7 +50,7 @@ pub struct ActivityManager {
|
|||||||
|
|
||||||
impl ActivityManager {
|
impl ActivityManager {
|
||||||
/// Initializes a new Activity Manager
|
/// 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
|
// Prepare Context
|
||||||
// Initialize configuration client
|
// Initialize configuration client
|
||||||
let (config_client, error_config): (ConfigClient, Option<String>) =
|
let (config_client, error_config): (ConfigClient, Option<String>) =
|
||||||
@@ -49,7 +61,7 @@ impl ActivityManager {
|
|||||||
(ConfigClient::degraded(), Some(err))
|
(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),
|
Ok(cli) => (cli, None),
|
||||||
Err(err) => (None, Some(err)),
|
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
|
/// Set file transfer params
|
||||||
pub fn set_filetransfer_params(
|
pub fn set_host_params(
|
||||||
&mut self,
|
&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>,
|
password: Option<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Set password if provided
|
// Set password if provided
|
||||||
@@ -73,13 +180,13 @@ impl ActivityManager {
|
|||||||
if let Some(password) = password {
|
if let Some(password) = password {
|
||||||
params.set_default_secret(password.to_string());
|
params.set_default_secret(password.to_string());
|
||||||
} else if matches!(
|
} else if matches!(
|
||||||
params.protocol,
|
protocol,
|
||||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
|
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
|
// * 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 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
|
if storage
|
||||||
.resolve(
|
.resolve(
|
||||||
&generic_params.address,
|
&generic_params.address,
|
||||||
@@ -94,7 +201,7 @@ impl ActivityManager {
|
|||||||
"storage could not find any suitable key for {}... prompting for password",
|
"storage could not find any suitable key for {}... prompting for password",
|
||||||
generic_params.address
|
generic_params.address
|
||||||
);
|
);
|
||||||
self.prompt_password(&mut params)?;
|
self.prompt_password(params)?;
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"a key is already set for {}; password is not required",
|
"a key is already set for {}; password is not required",
|
||||||
@@ -102,17 +209,19 @@ impl ActivityManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.prompt_password(&mut params)?;
|
self.prompt_password(params)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Put params into the context
|
|
||||||
self.context.as_mut().unwrap().set_ftparams(params);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompt user for password to set into params.
|
/// Prompt user for password to set into params.
|
||||||
fn prompt_password(&self, params: &mut FileTransferParams) -> Result<(), String> {
|
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
||||||
match tty::read_secret_from_tty("Password: ") {
|
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}")),
|
Err(err) => Err(format!("Could not read password: {err}")),
|
||||||
Ok(Some(secret)) => {
|
Ok(Some(secret)) => {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -130,16 +239,28 @@ impl ActivityManager {
|
|||||||
/// Returns error if bookmark is not found
|
/// Returns error if bookmark is not found
|
||||||
pub fn resolve_bookmark_name(
|
pub fn resolve_bookmark_name(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
host: Host,
|
||||||
bookmark_name: &str,
|
bookmark_name: &str,
|
||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
|
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
|
||||||
match bookmarks_client.get_bookmark(bookmark_name) {
|
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
||||||
None => Err(format!(
|
None => {
|
||||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
return Err(format!(
|
||||||
)),
|
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||||
Some(params) => self.set_filetransfer_params(params, password),
|
));
|
||||||
}
|
}
|
||||||
|
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 {
|
} else {
|
||||||
Err(String::from(
|
Err(String::from(
|
||||||
"Could not resolve bookmark name: bookmarks client not initialized",
|
"Could not resolve bookmark name: bookmarks client not initialized",
|
||||||
@@ -233,8 +354,17 @@ impl ActivityManager {
|
|||||||
return None;
|
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
|
// 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,
|
Some(ft_params) => ft_params,
|
||||||
None => {
|
None => {
|
||||||
error!("Failed to start FileTransferActivity: file transfer params is None");
|
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||||
@@ -242,28 +372,18 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// get local path:
|
// try to setup activity
|
||||||
// - if set in file transfer params, get it from there
|
let mut activity =
|
||||||
// - otherwise is env current dir
|
match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
|
||||||
// - otherwise is /
|
Ok(activity) => activity,
|
||||||
let local_wrkdir = ft_params
|
Err(err) => {
|
||||||
.local_path
|
error!("Failed to start FileTransferActivity: {}", err);
|
||||||
.clone()
|
ctx.set_error(err);
|
||||||
.or(std::env::current_dir().ok())
|
self.context = Some(ctx);
|
||||||
.unwrap_or(PathBuf::from("/"));
|
// Return to authentication
|
||||||
|
return Some(NextActivity::Authentication);
|
||||||
// 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);
|
|
||||||
// Prepare result
|
// Prepare result
|
||||||
let result: Option<NextActivity>;
|
let result: Option<NextActivity>;
|
||||||
// Create activity
|
// Create activity
|
||||||
@@ -327,7 +447,7 @@ impl ActivityManager {
|
|||||||
|
|
||||||
// -- misc
|
// -- misc
|
||||||
|
|
||||||
fn init_bookmarks_client() -> Result<Option<BookmarksClient>, String> {
|
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
|
||||||
// Get config dir
|
// Get config dir
|
||||||
match environment::init_config_dir() {
|
match environment::init_config_dir() {
|
||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
@@ -336,16 +456,21 @@ impl ActivityManager {
|
|||||||
let bookmarks_file: PathBuf =
|
let bookmarks_file: PathBuf =
|
||||||
environment::get_bookmarks_paths(config_dir_path.as_path());
|
environment::get_bookmarks_paths(config_dir_path.as_path());
|
||||||
// Initialize client
|
// Initialize client
|
||||||
BookmarksClient::new(bookmarks_file.as_path(), config_dir_path.as_path(), 16)
|
BookmarksClient::new(
|
||||||
.map(Option::Some)
|
bookmarks_file.as_path(),
|
||||||
.map_err(|e| {
|
config_dir_path.as_path(),
|
||||||
format!(
|
16,
|
||||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
keyring,
|
||||||
bookmarks_file.display(),
|
)
|
||||||
config_dir_path.display(),
|
.map(Option::Some)
|
||||||
e
|
.map_err(|e| {
|
||||||
)
|
format!(
|
||||||
})
|
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||||
|
bookmarks_file.display(),
|
||||||
|
config_dir_path.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -390,19 +515,28 @@ impl ActivityManager {
|
|||||||
match ThemeProvider::new(theme_path.as_path()) {
|
match ThemeProvider::new(theme_path.as_path()) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(err) => {
|
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()
|
ThemeProvider::degraded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
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()
|
ThemeProvider::degraded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
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()
|
ThemeProvider::degraded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,32 @@
|
|||||||
//!
|
//!
|
||||||
//! defines the types for main.rs types
|
//! defines the types for main.rs types
|
||||||
|
|
||||||
|
mod remote;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
pub use remote::{Remote, RemoteArgs};
|
||||||
|
|
||||||
use crate::activity_manager::NextActivity;
|
use crate::activity_manager::NextActivity;
|
||||||
use crate::filetransfer::FileTransferParams;
|
|
||||||
use crate::system::logging::LogLevel;
|
use crate::system::logging::LogLevel;
|
||||||
|
|
||||||
pub enum Task {
|
pub enum Task {
|
||||||
Activity(NextActivity),
|
Activity(NextActivity),
|
||||||
ImportTheme(PathBuf),
|
ImportTheme(PathBuf),
|
||||||
InstallUpdate,
|
InstallUpdate,
|
||||||
|
Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromArgs)]
|
#[derive(Default, FromArgs)]
|
||||||
#[argh(description = "
|
#[argh(description = "
|
||||||
where positional can be:
|
where positional can be:
|
||||||
- [address] [local-wrkdir]
|
- [address_a] [address_b] [local-wrkdir]
|
||||||
OR
|
OR
|
||||||
- [bookmark-Name] [local-wrkdir]
|
- -b [bookmark-name_1] -b [bookmark-name_2] [local-wrkdir]
|
||||||
|
|
||||||
|
and any combination of the above
|
||||||
|
|
||||||
Address syntax can be:
|
Address syntax can be:
|
||||||
|
|
||||||
@@ -37,14 +42,15 @@ pub struct Args {
|
|||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub nested: Option<ArgsSubcommands>,
|
pub nested: Option<ArgsSubcommands>,
|
||||||
/// resolve address argument as a bookmark name
|
/// resolve address argument as a bookmark name
|
||||||
#[argh(switch, short = 'b')]
|
#[argh(option, short = 'b')]
|
||||||
pub address_as_bookmark: bool,
|
pub bookmark: Vec<String>,
|
||||||
/// enable TRACE log level
|
/// enable TRACE log level
|
||||||
#[argh(switch, short = 'D')]
|
#[argh(switch, short = 'D')]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
/// provide password from CLI
|
/// 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')]
|
#[argh(option, short = 'P')]
|
||||||
pub password: Option<String>,
|
pub password: Vec<String>,
|
||||||
/// disable logging
|
/// disable logging
|
||||||
#[argh(switch, short = 'q')]
|
#[argh(switch, short = 'q')]
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
@@ -54,11 +60,11 @@ pub struct Args {
|
|||||||
/// print version
|
/// print version
|
||||||
#[argh(switch, short = 'v')]
|
#[argh(switch, short = 'v')]
|
||||||
pub version: bool,
|
pub version: bool,
|
||||||
|
/// disable keyring support
|
||||||
|
#[argh(switch)]
|
||||||
|
pub wno_keyring: bool,
|
||||||
// -- positional
|
// -- positional
|
||||||
#[argh(
|
#[argh(positional, description = "address1 address2 local-wrkdir")]
|
||||||
positional,
|
|
||||||
description = "protocol://user@address:port:wrkdir local-wrkdir"
|
|
||||||
)]
|
|
||||||
pub positional: Vec<String>,
|
pub positional: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +96,8 @@ pub struct LoadThemeArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RunOpts {
|
pub struct RunOpts {
|
||||||
pub remote: Remote,
|
pub remote: RemoteArgs,
|
||||||
|
pub keyring: bool,
|
||||||
pub ticks: Duration,
|
pub ticks: Duration,
|
||||||
pub log_level: LogLevel,
|
pub log_level: LogLevel,
|
||||||
pub task: Task,
|
pub task: Task,
|
||||||
@@ -122,45 +129,11 @@ impl RunOpts {
|
|||||||
impl Default for RunOpts {
|
impl Default for RunOpts {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
remote: Remote::None,
|
remote: RemoteArgs::default(),
|
||||||
ticks: Duration::from_millis(10),
|
ticks: Duration::from_millis(10),
|
||||||
|
keyring: true,
|
||||||
log_level: LogLevel::Info,
|
log_level: LogLevel::Info,
|
||||||
task: Task::Activity(NextActivity::Authentication),
|
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,9 +108,9 @@ impl From<FileTransferParams> for Bookmark {
|
|||||||
smb: Some(SmbParams::from(params.clone())),
|
smb: Some(SmbParams::from(params.clone())),
|
||||||
protocol,
|
protocol,
|
||||||
address: Some(params.address),
|
address: Some(params.address),
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
port: Some(params.port),
|
port: Some(params.port),
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
port: None,
|
port: None,
|
||||||
username: params.username,
|
username: params.username,
|
||||||
password: params.password,
|
password: params.password,
|
||||||
@@ -159,7 +159,7 @@ impl From<Bookmark> for FileTransferParams {
|
|||||||
let params = KubeProtocolParams::from(params);
|
let params = KubeProtocolParams::from(params);
|
||||||
Self::new(bookmark.protocol, ProtocolParams::Kube(params))
|
Self::new(bookmark.protocol, ProtocolParams::Kube(params))
|
||||||
}
|
}
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
FileTransferProtocol::Smb => {
|
FileTransferProtocol::Smb => {
|
||||||
let params = TransferSmbParams::new(
|
let params = TransferSmbParams::new(
|
||||||
bookmark.address.unwrap_or_default(),
|
bookmark.address.unwrap_or_default(),
|
||||||
@@ -172,7 +172,7 @@ impl From<Bookmark> for FileTransferParams {
|
|||||||
|
|
||||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
FileTransferProtocol::Smb => {
|
FileTransferProtocol::Smb => {
|
||||||
let params = TransferSmbParams::new(
|
let params = TransferSmbParams::new(
|
||||||
bookmark.address.unwrap_or_default(),
|
bookmark.address.unwrap_or_default(),
|
||||||
@@ -351,8 +351,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn bookmark_from_kube_ftparams() {
|
fn bookmark_from_kube_ftparams() {
|
||||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||||
pod: "pod".to_string(),
|
|
||||||
container: "container".to_string(),
|
|
||||||
namespace: Some("default".to_string()),
|
namespace: Some("default".to_string()),
|
||||||
username: Some("root".to_string()),
|
username: Some("root".to_string()),
|
||||||
cluster_url: Some("https://localhost:6443".to_string()),
|
cluster_url: Some("https://localhost:6443".to_string()),
|
||||||
@@ -368,8 +366,6 @@ mod tests {
|
|||||||
assert!(bookmark.username.is_none());
|
assert!(bookmark.username.is_none());
|
||||||
assert!(bookmark.password.is_none());
|
assert!(bookmark.password.is_none());
|
||||||
let kube: &KubeParams = bookmark.kube.as_ref().unwrap();
|
let kube: &KubeParams = bookmark.kube.as_ref().unwrap();
|
||||||
assert_eq!(kube.pod_name.as_str(), "pod");
|
|
||||||
assert_eq!(kube.container.as_str(), "container");
|
|
||||||
assert_eq!(kube.namespace.as_deref().unwrap(), "default");
|
assert_eq!(kube.namespace.as_deref().unwrap(), "default");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
kube.cluster_url.as_deref().unwrap(),
|
kube.cluster_url.as_deref().unwrap(),
|
||||||
@@ -494,8 +490,6 @@ mod tests {
|
|||||||
remote_path: Some(PathBuf::from("/tmp")),
|
remote_path: Some(PathBuf::from("/tmp")),
|
||||||
local_path: Some(PathBuf::from("/usr")),
|
local_path: Some(PathBuf::from("/usr")),
|
||||||
kube: Some(KubeParams {
|
kube: Some(KubeParams {
|
||||||
pod_name: String::from("pod"),
|
|
||||||
container: String::from("container"),
|
|
||||||
namespace: Some(String::from("default")),
|
namespace: Some(String::from("default")),
|
||||||
cluster_url: Some(String::from("https://localhost:6443")),
|
cluster_url: Some(String::from("https://localhost:6443")),
|
||||||
username: Some(String::from("root")),
|
username: Some(String::from("root")),
|
||||||
@@ -516,7 +510,6 @@ mod tests {
|
|||||||
std::path::Path::new("/usr")
|
std::path::Path::new("/usr")
|
||||||
);
|
);
|
||||||
let gparams = params.params.kube_params().unwrap();
|
let gparams = params.params.kube_params().unwrap();
|
||||||
assert_eq!(gparams.pod.as_str(), "pod");
|
|
||||||
assert_eq!(gparams.namespace.as_deref().unwrap(), "default");
|
assert_eq!(gparams.namespace.as_deref().unwrap(), "default");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
gparams.cluster_url.as_deref().unwrap(),
|
gparams.cluster_url.as_deref().unwrap(),
|
||||||
@@ -528,7 +521,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
fn should_get_ftparams_from_smb_bookmark() {
|
fn should_get_ftparams_from_smb_bookmark() {
|
||||||
let bookmark: Bookmark = Bookmark {
|
let bookmark: Bookmark = Bookmark {
|
||||||
protocol: FileTransferProtocol::Smb,
|
protocol: FileTransferProtocol::Smb,
|
||||||
@@ -566,7 +559,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
fn should_get_ftparams_from_smb_bookmark() {
|
fn should_get_ftparams_from_smb_bookmark() {
|
||||||
let bookmark: Bookmark = Bookmark {
|
let bookmark: Bookmark = Bookmark {
|
||||||
protocol: FileTransferProtocol::Smb,
|
protocol: FileTransferProtocol::Smb,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ use crate::filetransfer::params::KubeProtocolParams;
|
|||||||
/// Extra Connection parameters for Kube protocol
|
/// Extra Connection parameters for Kube protocol
|
||||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||||
pub struct KubeParams {
|
pub struct KubeParams {
|
||||||
pub pod_name: String,
|
|
||||||
pub container: String,
|
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
pub cluster_url: Option<String>,
|
pub cluster_url: Option<String>,
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
@@ -17,8 +15,6 @@ pub struct KubeParams {
|
|||||||
impl From<KubeParams> for KubeProtocolParams {
|
impl From<KubeParams> for KubeProtocolParams {
|
||||||
fn from(value: KubeParams) -> Self {
|
fn from(value: KubeParams) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pod: value.pod_name,
|
|
||||||
container: value.container,
|
|
||||||
namespace: value.namespace,
|
namespace: value.namespace,
|
||||||
cluster_url: value.cluster_url,
|
cluster_url: value.cluster_url,
|
||||||
username: value.username,
|
username: value.username,
|
||||||
@@ -31,8 +27,6 @@ impl From<KubeParams> for KubeProtocolParams {
|
|||||||
impl From<KubeProtocolParams> for KubeParams {
|
impl From<KubeProtocolParams> for KubeParams {
|
||||||
fn from(value: KubeProtocolParams) -> Self {
|
fn from(value: KubeProtocolParams) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pod_name: value.pod,
|
|
||||||
container: value.container,
|
|
||||||
namespace: value.namespace,
|
namespace: value.namespace,
|
||||||
cluster_url: value.cluster_url,
|
cluster_url: value.cluster_url,
|
||||||
username: value.username,
|
username: value.username,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct SmbParams {
|
|||||||
pub workgroup: Option<String>,
|
pub workgroup: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
impl From<TransferSmbParams> for SmbParams {
|
impl From<TransferSmbParams> for SmbParams {
|
||||||
fn from(params: TransferSmbParams) -> Self {
|
fn from(params: TransferSmbParams) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -19,7 +19,7 @@ impl From<TransferSmbParams> for SmbParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
impl From<TransferSmbParams> for SmbParams {
|
impl From<TransferSmbParams> for SmbParams {
|
||||||
fn from(params: TransferSmbParams) -> Self {
|
fn from(params: TransferSmbParams) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Contains the error for serializer/deserializer
|
/// Contains the error for serializer/deserializer
|
||||||
@@ -63,7 +63,7 @@ where
|
|||||||
return Err(SerializerError::new_ex(
|
return Err(SerializerError::new_ex(
|
||||||
SerializerErrorKind::Serialization,
|
SerializerErrorKind::Serialization,
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
trace!("Serialized new bookmarks data: {}", data);
|
trace!("Serialized new bookmarks data: {}", data);
|
||||||
@@ -112,7 +112,7 @@ mod tests {
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tuirealm::tui::style::Color;
|
use tuirealm::ratatui::style::Color;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
||||||
@@ -412,8 +412,6 @@ mod tests {
|
|||||||
assert_eq!(host.password, None);
|
assert_eq!(host.password, None);
|
||||||
assert_eq!(host.protocol, FileTransferProtocol::Kube);
|
assert_eq!(host.protocol, FileTransferProtocol::Kube);
|
||||||
let kube = host.kube.as_ref().unwrap();
|
let kube = host.kube.as_ref().unwrap();
|
||||||
assert_eq!(kube.pod_name.as_str(), "my-pod");
|
|
||||||
assert_eq!(kube.container.as_str(), "my-container");
|
|
||||||
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
|
assert_eq!(kube.namespace.as_deref().unwrap(), "my-namespace");
|
||||||
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
|
assert_eq!(kube.cluster_url.as_deref().unwrap(), "https://my-cluster");
|
||||||
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
|
assert_eq!(kube.username.as_deref().unwrap(), "my-username");
|
||||||
@@ -424,14 +422,14 @@ mod tests {
|
|||||||
let host = hosts.bookmarks.get("smb").unwrap();
|
let host = hosts.bookmarks.get("smb").unwrap();
|
||||||
assert_eq!(host.address.as_deref().unwrap(), "localhost");
|
assert_eq!(host.address.as_deref().unwrap(), "localhost");
|
||||||
assert_eq!(host.port.unwrap(), 445);
|
assert_eq!(host.port.unwrap(), 445);
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(host.username.as_deref().unwrap(), "test");
|
assert_eq!(host.username.as_deref().unwrap(), "test");
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(host.password.as_deref().unwrap(), "test");
|
assert_eq!(host.password.as_deref().unwrap(), "test");
|
||||||
|
|
||||||
let smb = host.smb.as_ref().unwrap();
|
let smb = host.smb.as_ref().unwrap();
|
||||||
assert_eq!(smb.share.as_str(), "temp");
|
assert_eq!(smb.share.as_str(), "temp");
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
|
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,8 +513,6 @@ mod tests {
|
|||||||
s3: None,
|
s3: None,
|
||||||
smb: None,
|
smb: None,
|
||||||
kube: Some(KubeParams {
|
kube: Some(KubeParams {
|
||||||
pod_name: "my-pod".to_string(),
|
|
||||||
container: "my-container".to_string(),
|
|
||||||
namespace: Some("my-namespace".to_string()),
|
namespace: Some("my-namespace".to_string()),
|
||||||
cluster_url: Some("https://my-cluster".to_string()),
|
cluster_url: Some("https://my-cluster".to_string()),
|
||||||
username: Some("my-username".to_string()),
|
username: Some("my-username".to_string()),
|
||||||
@@ -593,6 +589,22 @@ mod tests {
|
|||||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
|
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 {
|
fn create_good_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||||
// Write
|
// Write
|
||||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
@@ -617,8 +629,6 @@ mod tests {
|
|||||||
[bookmarks.pod]
|
[bookmarks.pod]
|
||||||
protocol = "KUBE"
|
protocol = "KUBE"
|
||||||
[bookmarks.pod.kube]
|
[bookmarks.pod.kube]
|
||||||
pod_name = "my-pod"
|
|
||||||
container = "my-container"
|
|
||||||
namespace = "my-namespace"
|
namespace = "my-namespace"
|
||||||
cluster_url = "https://my-cluster"
|
cluster_url = "https://my-cluster"
|
||||||
username = "my-username"
|
username = "my-username"
|
||||||
@@ -644,6 +654,29 @@ mod tests {
|
|||||||
tmpfile
|
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 {
|
fn create_bad_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||||
// Write
|
// Write
|
||||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// ext
|
// ext
|
||||||
use serde::de::Error as DeError;
|
use serde::de::Error as DeError;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
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::fmt::fmt_color;
|
||||||
use crate::utils::parser::parse_color;
|
use crate::utils::parser::parse_color;
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use bytesize::ByteSize;
|
|||||||
use lazy_regex::{Lazy, Regex};
|
use lazy_regex::{Lazy, Regex};
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
use users::{get_group_by_gid, get_user_by_uid};
|
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||||
|
|
||||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||||
use crate::utils::path::diff_paths;
|
use crate::utils::path::diff_paths;
|
||||||
@@ -211,7 +211,7 @@ impl Formatter {
|
|||||||
_fmt_extra: Option<&String>,
|
_fmt_extra: Option<&String>,
|
||||||
) -> String {
|
) -> String {
|
||||||
// Get username
|
// Get username
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
let group: String = match fsentry.metadata().gid {
|
let group: String = match fsentry.metadata().gid {
|
||||||
Some(gid) => match get_group_by_gid(gid) {
|
Some(gid) => match get_group_by_gid(gid) {
|
||||||
Some(user) => user.name().to_string_lossy().to_string(),
|
Some(user) => user.name().to_string_lossy().to_string(),
|
||||||
@@ -219,7 +219,7 @@ impl Formatter {
|
|||||||
},
|
},
|
||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
let group: String = match fsentry.metadata().gid {
|
let group: String = match fsentry.metadata().gid {
|
||||||
Some(gid) => gid.to_string(),
|
Some(gid) => gid.to_string(),
|
||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
@@ -364,8 +364,16 @@ impl Formatter {
|
|||||||
if fsentry.is_file() {
|
if fsentry.is_file() {
|
||||||
// Get byte size
|
// Get byte size
|
||||||
let size: ByteSize = ByteSize(fsentry.metadata().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
|
// 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() {
|
} else if fsentry.metadata().symlink.is_some() {
|
||||||
let size = ByteSize(
|
let size = ByteSize(
|
||||||
fsentry
|
fsentry
|
||||||
@@ -376,7 +384,14 @@ impl Formatter {
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.len() as u64,
|
.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 {
|
} else {
|
||||||
// Add to cur str, prefix and the key value
|
// Add to cur str, prefix and the key value
|
||||||
format!("{cur_str}{prefix} ")
|
format!("{cur_str}{prefix} ")
|
||||||
@@ -420,7 +435,7 @@ impl Formatter {
|
|||||||
_fmt_extra: Option<&String>,
|
_fmt_extra: Option<&String>,
|
||||||
) -> String {
|
) -> String {
|
||||||
// Get username
|
// Get username
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
let username: String = match fsentry.metadata().uid {
|
let username: String = match fsentry.metadata().uid {
|
||||||
Some(uid) => match get_user_by_uid(uid) {
|
Some(uid) => match get_user_by_uid(uid) {
|
||||||
Some(user) => user.name().to_string_lossy().to_string(),
|
Some(user) => user.name().to_string_lossy().to_string(),
|
||||||
@@ -428,7 +443,7 @@ impl Formatter {
|
|||||||
},
|
},
|
||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
let username: String = match fsentry.metadata().uid {
|
let username: String = match fsentry.metadata().uid {
|
||||||
Some(uid) => uid.to_string(),
|
Some(uid) => uid.to_string(),
|
||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
@@ -592,19 +607,19 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o644)),
|
mode: Some(UnixPex::from(0o644)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
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")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
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")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -623,19 +638,19 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o644)),
|
mode: Some(UnixPex::from(0o644)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
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")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
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")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -654,19 +669,19 @@ mod tests {
|
|||||||
mode: None,
|
mode: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"bar.txt -????????? root 8.2 KB {}",
|
"bar.txt -????????? root 8.2 kB {}",
|
||||||
fmt_time(t, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"bar.txt -????????? 0 8.2 KB {}",
|
"bar.txt -????????? 0 8.2 kB {}",
|
||||||
fmt_time(t, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -685,19 +700,19 @@ mod tests {
|
|||||||
mode: None,
|
mode: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"bar.txt -????????? 0 8.2 KB {}",
|
"bar.txt -????????? 0 8.2 kB {}",
|
||||||
fmt_time(t, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"bar.txt -????????? 0 8.2 KB {}",
|
"bar.txt -????????? 0 8.2 kB {}",
|
||||||
fmt_time(t, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -723,7 +738,7 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o755)),
|
mode: Some(UnixPex::from(0o755)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
@@ -731,7 +746,7 @@ mod tests {
|
|||||||
fmt_time(t, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
@@ -754,7 +769,7 @@ mod tests {
|
|||||||
mode: None,
|
mode: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
@@ -762,7 +777,7 @@ mod tests {
|
|||||||
fmt_time(t, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
@@ -774,8 +789,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fs_explorer_formatter_all_together_now() {
|
fn test_fs_explorer_formatter_all_together_now() {
|
||||||
let formatter: Formatter =
|
let formatter: Formatter = Formatter::new(
|
||||||
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}");
|
"{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)
|
// Directory (with symlink)
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry = File {
|
let entry = File {
|
||||||
@@ -792,12 +808,15 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o755)),
|
mode: Some(UnixPex::from(0o755)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(
|
||||||
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
|
formatter.fmt(&entry),
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
format!(
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
"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"),
|
||||||
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
|
)
|
||||||
|
);
|
||||||
// Directory without symlink
|
// Directory without symlink
|
||||||
let entry = File {
|
let entry = File {
|
||||||
path: PathBuf::from("/home/cvisintin/projects"),
|
path: PathBuf::from("/home/cvisintin/projects"),
|
||||||
@@ -813,12 +832,15 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o755)),
|
mode: Some(UnixPex::from(0o755)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(
|
||||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
formatter.fmt(&entry),
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
format!(
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
"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"),
|
||||||
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
|
)
|
||||||
|
);
|
||||||
// File with symlink
|
// File with symlink
|
||||||
let entry = File {
|
let entry = File {
|
||||||
path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
@@ -834,12 +856,15 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o644)),
|
mode: Some(UnixPex::from(0o644)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(
|
||||||
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
|
formatter.fmt(&entry),
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
format!(
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
"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"),
|
||||||
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
|
)
|
||||||
|
);
|
||||||
// File without symlink
|
// File without symlink
|
||||||
let entry = File {
|
let entry = File {
|
||||||
path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
@@ -855,16 +880,19 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o644)),
|
mode: Some(UnixPex::from(0o644)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(
|
||||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
formatter.fmt(&entry),
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
format!(
|
||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
"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"),
|
||||||
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
fn should_fmt_path() {
|
fn should_fmt_path() {
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry = File {
|
let entry = File {
|
||||||
@@ -896,7 +924,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
fn should_fmt_utf8_path() {
|
fn should_fmt_utf8_path() {
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry = File {
|
let entry = File {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub(crate) mod builder;
|
|||||||
mod formatter;
|
mod formatter;
|
||||||
// Locals
|
// Locals
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::VecDeque;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ pub enum FileSorting {
|
|||||||
ModifyTime,
|
ModifyTime,
|
||||||
CreationTime,
|
CreationTime,
|
||||||
Size,
|
Size,
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GroupDirs defines how directories should be grouped in sorting files
|
/// GroupDirs defines how directories should be grouped in sorting files
|
||||||
@@ -41,14 +42,24 @@ pub enum GroupDirs {
|
|||||||
|
|
||||||
/// File explorer states
|
/// File explorer states
|
||||||
pub struct FileExplorer {
|
pub struct FileExplorer {
|
||||||
pub wrkdir: PathBuf, // Current directory
|
/// Current working directory
|
||||||
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
|
pub wrkdir: PathBuf,
|
||||||
pub(crate) stack_size: usize, // Directory stack size
|
/// Stack of visited directories
|
||||||
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
pub(crate) dirstack: VecDeque<PathBuf>,
|
||||||
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
/// Stack size
|
||||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
pub(crate) stack_size: usize,
|
||||||
pub(crate) fmt: Formatter, // File formatter
|
/// Criteria to sort file
|
||||||
files: Vec<File>, // Files in directory
|
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,
|
||||||
|
/// 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 {
|
impl Default for FileExplorer {
|
||||||
@@ -62,6 +73,7 @@ impl Default for FileExplorer {
|
|||||||
opts: ExplorerOpts::empty(),
|
opts: ExplorerOpts::empty(),
|
||||||
fmt: Formatter::default(),
|
fmt: Formatter::default(),
|
||||||
files: Vec::new(),
|
files: Vec::new(),
|
||||||
|
transfer_queue: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +150,35 @@ impl FileExplorer {
|
|||||||
filtered.get(idx).copied()
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
// Formatting
|
// Formatting
|
||||||
|
|
||||||
/// Format a file entry
|
/// Format a file entry
|
||||||
@@ -178,6 +219,7 @@ impl FileExplorer {
|
|||||||
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
||||||
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
||||||
FileSorting::Size => self.sort_files_by_size(),
|
FileSorting::Size => self.sort_files_by_size(),
|
||||||
|
FileSorting::None => {}
|
||||||
}
|
}
|
||||||
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
||||||
// Group directories if necessary
|
// Group directories if necessary
|
||||||
@@ -245,6 +287,7 @@ impl std::fmt::Display for FileSorting {
|
|||||||
FileSorting::ModifyTime => "by_mtime",
|
FileSorting::ModifyTime => "by_mtime",
|
||||||
FileSorting::Name => "by_name",
|
FileSorting::Name => "by_name",
|
||||||
FileSorting::Size => "by_size",
|
FileSorting::Size => "by_size",
|
||||||
|
FileSorting::None => "none",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -516,19 +559,19 @@ mod tests {
|
|||||||
mode: Some(UnixPex::from(0o644)),
|
mode: Some(UnixPex::from(0o644)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
explorer.fmt_file(&entry),
|
explorer.fmt_file(&entry),
|
||||||
format!(
|
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")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
explorer.fmt_file(&entry),
|
explorer.fmt_file(&entry),
|
||||||
format!(
|
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")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -583,6 +626,26 @@ mod tests {
|
|||||||
assert_eq!(explorer.files.len(), 3);
|
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 {
|
fn make_fs_entry(name: &str, is_dir: bool) -> File {
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let metadata = Metadata {
|
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
|
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
|
||||||
|
|
||||||
mod builder;
|
mod host_bridge_builder;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
|
mod remotefs_builder;
|
||||||
|
|
||||||
// -- export types
|
// -- export types
|
||||||
pub use builder::Builder;
|
pub use host_bridge_builder::HostBridgeBuilder;
|
||||||
pub use params::{FileTransferParams, ProtocolParams};
|
pub use params::{FileTransferParams, HostBridgeParams, ProtocolParams};
|
||||||
|
pub use remotefs_builder::RemoteFsBuilder;
|
||||||
|
|
||||||
/// This enum defines the different transfer protocol available in termscp
|
/// This enum defines the different transfer protocol available in termscp
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,24 @@ pub use self::smb::SmbParams;
|
|||||||
pub use self::webdav::WebDAVProtocolParams;
|
pub use self::webdav::WebDAVProtocolParams;
|
||||||
use super::FileTransferProtocol;
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Holds connection parameters for file transfers
|
/// Holds connection parameters for file transfers
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileTransferParams {
|
pub struct FileTransferParams {
|
||||||
@@ -34,6 +52,43 @@ pub enum ProtocolParams {
|
|||||||
WebDAV(WebDAVProtocolParams),
|
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
|
/// Protocol params used by most common protocols
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GenericProtocolParams {
|
pub struct GenericProtocolParams {
|
||||||
@@ -68,25 +123,15 @@ impl FileTransferParams {
|
|||||||
|
|
||||||
/// Returns whether a password is supposed to be required for this protocol params.
|
/// 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!!!
|
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||||
|
#[cfg(test)]
|
||||||
pub fn password_missing(&self) -> bool {
|
pub fn password_missing(&self) -> bool {
|
||||||
match &self.params {
|
self.params.password_missing()
|
||||||
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
|
/// 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) {
|
pub fn set_default_secret(&mut self, secret: String) {
|
||||||
match &mut self.params {
|
self.params.set_default_secret(secret);
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,11 +301,13 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn password_missing() {
|
fn password_missing() {
|
||||||
assert!(FileTransferParams::new(
|
assert!(
|
||||||
FileTransferProtocol::Scp,
|
FileTransferParams::new(
|
||||||
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
|
FileTransferProtocol::Scp,
|
||||||
)
|
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
|
||||||
.password_missing());
|
)
|
||||||
|
.password_missing()
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FileTransferParams::new(
|
FileTransferParams::new(
|
||||||
FileTransferProtocol::Scp,
|
FileTransferProtocol::Scp,
|
||||||
@@ -317,7 +364,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
fn set_default_secret_smb() {
|
fn set_default_secret_smb() {
|
||||||
let mut params = FileTransferParams::new(
|
let mut params = FileTransferParams::new(
|
||||||
FileTransferProtocol::Scp,
|
FileTransferProtocol::Scp,
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ use remotefs_kube::Config;
|
|||||||
/// Protocol params used by WebDAV
|
/// Protocol params used by WebDAV
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct KubeProtocolParams {
|
pub struct KubeProtocolParams {
|
||||||
pub pod: String,
|
|
||||||
pub container: String,
|
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
pub cluster_url: Option<String>,
|
pub cluster_url: Option<String>,
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SmbParams {
|
pub struct SmbParams {
|
||||||
pub address: String,
|
pub address: String,
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub share: String,
|
pub share: String,
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
pub workgroup: Option<String>,
|
pub workgroup: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,17 +18,17 @@ impl SmbParams {
|
|||||||
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
|
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
address: address.as_ref().to_string(),
|
address: address.as_ref().to_string(),
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
port: 445,
|
port: 445,
|
||||||
share: share.as_ref().to_string(),
|
share: share.as_ref().to_string(),
|
||||||
username: None,
|
username: None,
|
||||||
password: None,
|
password: None,
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
workgroup: None,
|
workgroup: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
pub fn port(mut self, port: u16) -> Self {
|
pub fn port(mut self, port: u16) -> Self {
|
||||||
self.port = port;
|
self.port = port;
|
||||||
self
|
self
|
||||||
@@ -44,7 +44,7 @@ impl SmbParams {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
|
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
|
||||||
self.workgroup = workgroup.map(|x| x.to_string());
|
self.workgroup = workgroup.map(|x| x.to_string());
|
||||||
self
|
self
|
||||||
@@ -57,12 +57,12 @@ impl SmbParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set password
|
/// Set password
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
pub fn set_default_secret(&mut self, secret: String) {
|
pub fn set_default_secret(&mut self, secret: String) {
|
||||||
self.password = Some(secret);
|
self.password = Some(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,20 +78,20 @@ mod test {
|
|||||||
let params = SmbParams::new("localhost", "temp");
|
let params = SmbParams::new("localhost", "temp");
|
||||||
assert_eq!(¶ms.address, "localhost");
|
assert_eq!(¶ms.address, "localhost");
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert_eq!(params.port, 445);
|
assert_eq!(params.port, 445);
|
||||||
assert_eq!(¶ms.share, "temp");
|
assert_eq!(¶ms.share, "temp");
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert!(params.username.is_none());
|
assert!(params.username.is_none());
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert!(params.password.is_none());
|
assert!(params.password.is_none());
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
assert!(params.workgroup.is_none());
|
assert!(params.workgroup.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
fn should_init_smb_params_with_optionals() {
|
fn should_init_smb_params_with_optionals() {
|
||||||
let params = SmbParams::new("localhost", "temp")
|
let params = SmbParams::new("localhost", "temp")
|
||||||
.port(3456)
|
.port(3456)
|
||||||
@@ -108,7 +108,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(windows)]
|
#[cfg(win)]
|
||||||
fn should_init_smb_params_with_optionals() {
|
fn should_init_smb_params_with_optionals() {
|
||||||
let params = SmbParams::new("localhost", "temp")
|
let params = SmbParams::new("localhost", "temp")
|
||||||
.username(Some("foo"))
|
.username(Some("foo"))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
|||||||
use remotefs::RemoteFs;
|
use remotefs::RemoteFs;
|
||||||
use remotefs_aws_s3::AwsS3Fs;
|
use remotefs_aws_s3::AwsS3Fs;
|
||||||
use remotefs_ftp::FtpFs;
|
use remotefs_ftp::FtpFs;
|
||||||
use remotefs_kube::KubeFs;
|
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||||
#[cfg(smb_unix)]
|
#[cfg(smb_unix)]
|
||||||
use remotefs_smb::SmbOptions;
|
use remotefs_smb::SmbOptions;
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
@@ -27,9 +27,9 @@ use crate::system::sshkey_storage::SshKeyStorage;
|
|||||||
use crate::utils::ssh as ssh_utils;
|
use crate::utils::ssh as ssh_utils;
|
||||||
|
|
||||||
/// Remotefs builder
|
/// Remotefs builder
|
||||||
pub struct Builder;
|
pub struct RemoteFsBuilder;
|
||||||
|
|
||||||
impl Builder {
|
impl RemoteFsBuilder {
|
||||||
/// Build RemoteFs client from protocol and params.
|
/// Build RemoteFs client from protocol and params.
|
||||||
///
|
///
|
||||||
/// if protocol and parameters are inconsistent, the function will panic.
|
/// if protocol and parameters are inconsistent, the function will panic.
|
||||||
@@ -37,40 +37,50 @@ impl Builder {
|
|||||||
protocol: FileTransferProtocol,
|
protocol: FileTransferProtocol,
|
||||||
params: ProtocolParams,
|
params: ProtocolParams,
|
||||||
config_client: &ConfigClient,
|
config_client: &ConfigClient,
|
||||||
) -> Box<dyn RemoteFs> {
|
) -> Result<Box<dyn RemoteFs>, String> {
|
||||||
match (protocol, params) {
|
match (protocol, params) {
|
||||||
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(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)) => {
|
(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)) => {
|
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||||
Box::new(Self::kube_client(params))
|
Ok(Box::new(Self::kube_client(params)))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::Scp, ProtocolParams::Generic(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)) => {
|
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
||||||
Box::new(Self::sftp_client(params, config_client))
|
Ok(Box::new(Self::sftp_client(params, config_client)))
|
||||||
}
|
}
|
||||||
#[cfg(smb)]
|
#[cfg(smb)]
|
||||||
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
|
||||||
Box::new(Self::smb_client(params))
|
Ok(Box::new(Self::smb_client(params)))
|
||||||
}
|
}
|
||||||
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
||||||
Box::new(Self::webdav_client(params))
|
Ok(Box::new(Self::webdav_client(params)))
|
||||||
}
|
}
|
||||||
(protocol, params) => {
|
(protocol, params) => {
|
||||||
error!("Invalid params for protocol '{:?}'", protocol);
|
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
|
/// Build aws s3 client from parameters
|
||||||
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
|
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 {
|
if let Some(region) = params.region {
|
||||||
client = client.region(region);
|
client = client.region(region);
|
||||||
}
|
}
|
||||||
@@ -113,13 +123,13 @@ impl Builder {
|
|||||||
/// Build kube client
|
/// Build kube client
|
||||||
fn kube_client(params: KubeProtocolParams) -> KubeFs {
|
fn kube_client(params: KubeProtocolParams) -> KubeFs {
|
||||||
let rt = Arc::new(
|
let rt = Arc::new(
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.worker_threads(1)
|
.worker_threads(1)
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.expect("Unable to create tokio runtime"),
|
.expect("Unable to create tokio runtime"),
|
||||||
);
|
);
|
||||||
let kube_fs = KubeFs::new(¶ms.pod, ¶ms.container, &rt);
|
let kube_fs = KubeFs::new(&rt);
|
||||||
if let Some(config) = params.config() {
|
if let Some(config) = params.config() {
|
||||||
kube_fs.config(config)
|
kube_fs.config(config)
|
||||||
} else {
|
} else {
|
||||||
@@ -262,7 +272,9 @@ mod test {
|
|||||||
.session_token(Some("gerry-scotti")),
|
.session_token(Some("gerry-scotti")),
|
||||||
);
|
);
|
||||||
let config_client = get_config_client();
|
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]
|
#[test]
|
||||||
@@ -275,14 +287,14 @@ mod test {
|
|||||||
.password(Some("qwerty123")),
|
.password(Some("qwerty123")),
|
||||||
);
|
);
|
||||||
let config_client = get_config_client();
|
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]
|
#[test]
|
||||||
fn test_should_build_kube_fs() {
|
fn test_should_build_kube_fs() {
|
||||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||||
pod: "pod".to_string(),
|
|
||||||
container: "container".to_string(),
|
|
||||||
namespace: Some("namespace".to_string()),
|
namespace: Some("namespace".to_string()),
|
||||||
cluster_url: Some("cluster_url".to_string()),
|
cluster_url: Some("cluster_url".to_string()),
|
||||||
username: Some("username".to_string()),
|
username: Some("username".to_string()),
|
||||||
@@ -290,7 +302,7 @@ mod test {
|
|||||||
client_key: Some("client_key".to_string()),
|
client_key: Some("client_key".to_string()),
|
||||||
});
|
});
|
||||||
let config_client = get_config_client();
|
let config_client = get_config_client();
|
||||||
let _ = Builder::build(FileTransferProtocol::Kube, params, &config_client);
|
assert!(RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -303,7 +315,7 @@ mod test {
|
|||||||
.password(Some("qwerty123")),
|
.password(Some("qwerty123")),
|
||||||
);
|
);
|
||||||
let config_client = get_config_client();
|
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]
|
#[test]
|
||||||
@@ -316,7 +328,7 @@ mod test {
|
|||||||
.password(Some("qwerty123")),
|
.password(Some("qwerty123")),
|
||||||
);
|
);
|
||||||
let config_client = get_config_client();
|
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]
|
#[test]
|
||||||
@@ -324,11 +336,10 @@ mod test {
|
|||||||
fn should_build_smb_fs() {
|
fn should_build_smb_fs() {
|
||||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||||
let config_client = get_config_client();
|
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]
|
#[test]
|
||||||
#[should_panic]
|
|
||||||
fn should_not_build_fs() {
|
fn should_not_build_fs() {
|
||||||
let params = ProtocolParams::Generic(
|
let params = ProtocolParams::Generic(
|
||||||
GenericProtocolParams::default()
|
GenericProtocolParams::default()
|
||||||
@@ -338,7 +349,9 @@ mod test {
|
|||||||
.password(Some("qwerty123")),
|
.password(Some("qwerty123")),
|
||||||
);
|
);
|
||||||
let config_client = get_config_client();
|
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 {
|
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
1082
src/host/mod.rs
1082
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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/main.rs
172
src/main.rs
@@ -1,5 +1,13 @@
|
|||||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
mod activity_manager;
|
||||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
mod cli;
|
||||||
|
mod config;
|
||||||
|
mod explorer;
|
||||||
|
mod filetransfer;
|
||||||
|
mod host;
|
||||||
|
mod support;
|
||||||
|
mod system;
|
||||||
|
mod ui;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
// Crates
|
// Crates
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -13,50 +21,49 @@ extern crate log;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate magic_crypt;
|
extern crate magic_crypt;
|
||||||
|
|
||||||
// External libs
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// Include
|
use self::activity_manager::{ActivityManager, NextActivity};
|
||||||
mod activity_manager;
|
use self::cli::{Args, ArgsSubcommands, RemoteArgs, RunOpts, Task};
|
||||||
mod cli_opts;
|
use self::system::logging::{self, LogLevel};
|
||||||
mod config;
|
|
||||||
mod explorer;
|
|
||||||
mod filetransfer;
|
|
||||||
mod host;
|
|
||||||
mod support;
|
|
||||||
mod system;
|
|
||||||
mod ui;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
// namespaces
|
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
use activity_manager::{ActivityManager, NextActivity};
|
const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
|
||||||
use cli_opts::{Args, ArgsSubcommands, BookmarkParams, HostParams, Remote, RunOpts, Task};
|
const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
|
||||||
use filetransfer::FileTransferParams;
|
const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
|
||||||
use system::logging::{self, LogLevel};
|
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();
|
let args: Args = argh::from_env();
|
||||||
// Parse args
|
// Parse args
|
||||||
let run_opts: RunOpts = match parse_args(args) {
|
let run_opts: RunOpts = match parse_args(args) {
|
||||||
Ok(opts) => opts,
|
Ok(opts) => opts,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
std::process::exit(255);
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Setup logging
|
// Setup logging
|
||||||
if let Err(err) = logging::init(run_opts.log_level) {
|
if let Err(err) = logging::init(run_opts.log_level) {
|
||||||
eprintln!("Failed to initialize logging: {err}");
|
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
|
// Run
|
||||||
info!("Starting activity manager...");
|
info!("Starting activity manager...");
|
||||||
let rc = run(run_opts);
|
run(run_opts)
|
||||||
info!("termscp terminated with exitcode {}", rc);
|
|
||||||
// Then return
|
|
||||||
std::process::exit(rc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse arguments
|
/// Parse arguments
|
||||||
@@ -69,11 +76,11 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||||||
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
|
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
|
||||||
None => {
|
None => {
|
||||||
let mut run_opts: RunOpts = RunOpts::default();
|
let mut run_opts: RunOpts = RunOpts::default();
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
if args.version {
|
if args.version {
|
||||||
return Err(format!(
|
run_opts.task = Task::Version;
|
||||||
"termscp - {TERMSCP_VERSION} - Developed by {TERMSCP_AUTHORS}",
|
return Ok(run_opts);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
// Logging
|
// Logging
|
||||||
if args.debug {
|
if args.debug {
|
||||||
@@ -81,25 +88,31 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||||||
} else if args.quiet {
|
} else if args.quiet {
|
||||||
run_opts.log_level = LogLevel::Off;
|
run_opts.log_level = LogLevel::Off;
|
||||||
}
|
}
|
||||||
|
// set keyring
|
||||||
|
if args.wno_keyring {
|
||||||
|
run_opts.keyring = false;
|
||||||
|
}
|
||||||
// Match ticks
|
// Match ticks
|
||||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||||
// Remote argument
|
// Remote argument
|
||||||
match parse_address_arg(&args) {
|
match RemoteArgs::try_from(&args) {
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
Ok(Remote::None) => {}
|
|
||||||
Ok(remote) => {
|
Ok(remote) => {
|
||||||
// Set params
|
// Set params
|
||||||
run_opts.remote = remote;
|
run_opts.remote = remote;
|
||||||
// In this case the first activity will be FileTransfer
|
|
||||||
run_opts.task = Task::Activity(NextActivity::FileTransfer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Local directory
|
||||||
if let Some(localdir) = args.positional.get(1) {
|
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
|
||||||
// Change working directory if local dir is set
|
if let Err(err) = env::set_current_dir(localdir) {
|
||||||
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}"));
|
return Err(format!("Bad working directory argument: {err}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,90 +124,75 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||||||
Ok(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.first() {
|
|
||||||
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
|
/// Run task and return rc
|
||||||
fn run(run_opts: RunOpts) -> i32 {
|
fn run(run_opts: RunOpts) -> MainResult<()> {
|
||||||
match run_opts.task {
|
match run_opts.task {
|
||||||
Task::ImportTheme(theme) => run_import_theme(&theme),
|
Task::ImportTheme(theme) => run_import_theme(&theme),
|
||||||
Task::InstallUpdate => run_install_update(),
|
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) {
|
match support::import_theme(theme) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Theme has been successfully imported!");
|
println!("Theme has been successfully imported!");
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
1
|
Err(err.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_install_update() -> i32 {
|
fn run_install_update() -> MainResult<()> {
|
||||||
match support::install_update() {
|
match support::install_update() {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
println!("{msg}");
|
println!("{msg}");
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Could not install update: {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)
|
// 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,
|
Ok(m) => m,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Could not start activity manager: {err}");
|
eprintln!("Could not start activity manager: {err}");
|
||||||
return 1;
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set file transfer params if set
|
// Set file transfer params if set
|
||||||
match remote {
|
if let Err(err) = manager.configure_remote_args(remote_args) {
|
||||||
Remote::Bookmark(BookmarkParams { name, password }) => {
|
eprintln!("{err}");
|
||||||
if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) {
|
return Err(err.into());
|
||||||
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 => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.run(activity);
|
manager.run(activity);
|
||||||
|
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
use self_update::backends::github::Update as GithubUpdater;
|
use self_update::backends::github::Update as GithubUpdater;
|
||||||
pub use self_update::errors::Error as UpdateError;
|
pub use self_update::errors::Error as UpdateError;
|
||||||
use self_update::update::Release as UpdRelease;
|
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;
|
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
|
/// In case received version is newer than current one, version as Some is returned; otherwise None
|
||||||
fn check_version(r: Release) -> Option<Release> {
|
fn check_version(r: Release) -> Option<Release> {
|
||||||
|
debug!("got version from GitHub: {}", r.version);
|
||||||
match parse_semver(r.version.as_str()) {
|
match parse_semver(r.version.as_str()) {
|
||||||
Some(new_version) => {
|
Some(new_version) => {
|
||||||
// Check if version is different
|
// Check if version is different
|
||||||
@@ -145,10 +146,13 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(not(all(
|
#[cfg(all(
|
||||||
any(target_os = "macos", target_os = "freebsd"),
|
not(all(
|
||||||
feature = "github-actions"
|
any(target_os = "macos", target_os = "freebsd"),
|
||||||
)))]
|
feature = "github-actions"
|
||||||
|
)),
|
||||||
|
not(feature = "isolated-tests")
|
||||||
|
))]
|
||||||
fn auto_update() {
|
fn auto_update() {
|
||||||
// Wno version
|
// Wno version
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -162,10 +166,13 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(not(all(
|
#[cfg(all(
|
||||||
any(target_os = "macos", target_os = "freebsd"),
|
not(all(
|
||||||
feature = "github-actions"
|
any(target_os = "macos", target_os = "freebsd"),
|
||||||
)))]
|
feature = "github-actions"
|
||||||
|
)),
|
||||||
|
not(feature = "isolated-tests")
|
||||||
|
))]
|
||||||
fn check_for_updates() {
|
fn check_for_updates() {
|
||||||
println!("{:?}", Update::is_new_version_available());
|
println!("{:?}", Update::is_new_version_available());
|
||||||
assert!(Update::is_new_version_available().is_ok());
|
assert!(Update::is_new_version_available().is_ok());
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ use std::string::ToString;
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use super::keys::filestorage::FileStorage;
|
use super::keys::filestorage::FileStorage;
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
use super::keys::keyringstorage::KeyringStorage;
|
use super::keys::keyringstorage::KeyringStorage;
|
||||||
use super::keys::{KeyStorage, KeyStorageError};
|
use super::keys::{KeyStorage, KeyStorageError};
|
||||||
// Local
|
// Local
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
bookmarks::{Bookmark, UserHosts},
|
bookmarks::{Bookmark, UserHosts},
|
||||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
|
||||||
};
|
};
|
||||||
use crate::filetransfer::FileTransferParams;
|
use crate::filetransfer::FileTransferParams;
|
||||||
use crate::utils::crypto;
|
use crate::utils::crypto;
|
||||||
@@ -39,42 +38,13 @@ impl BookmarksClient {
|
|||||||
bookmarks_file: &Path,
|
bookmarks_file: &Path,
|
||||||
storage_path: &Path,
|
storage_path: &Path,
|
||||||
recents_size: usize,
|
recents_size: usize,
|
||||||
|
keyring: bool,
|
||||||
) -> Result<BookmarksClient, SerializerError> {
|
) -> Result<BookmarksClient, SerializerError> {
|
||||||
// Create default hosts
|
// Create default hosts
|
||||||
let default_hosts: UserHosts = UserHosts::default();
|
let default_hosts: UserHosts = UserHosts::default();
|
||||||
debug!("Setting up bookmarks client...");
|
debug!("Setting up bookmarks client...");
|
||||||
// Make a key storage (with-keyring)
|
// Get key storage
|
||||||
#[cfg(feature = "with-keyring")]
|
let (key_storage, service_id) = Self::keyring(storage_path, keyring);
|
||||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
|
||||||
debug!("Setting up KeyStorage");
|
|
||||||
let username: String = whoami::username();
|
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
|
||||||
// Check if keyring storage is supported
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let app_name: &str = "termscp";
|
|
||||||
#[cfg(test)] // NOTE: when running test, add -test
|
|
||||||
let app_name: &str = "termscp-test";
|
|
||||||
match storage.is_supported() {
|
|
||||||
true => {
|
|
||||||
debug!("Using KeyringStorage");
|
|
||||||
(Box::new(storage), app_name)
|
|
||||||
}
|
|
||||||
false => {
|
|
||||||
warn!("KeyringStorage is not supported; using FileStorage");
|
|
||||||
(Box::new(FileStorage::new(storage_path)), "bookmarks")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Make a key storage (wno-keyring)
|
|
||||||
#[cfg(not(feature = "with-keyring"))]
|
|
||||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let app_name: &str = "bookmarks";
|
|
||||||
#[cfg(test)] // NOTE: when running test, add -test
|
|
||||||
let app_name: &str = "bookmarks-test";
|
|
||||||
debug!("Using FileStorage");
|
|
||||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
|
||||||
};
|
|
||||||
// Load key
|
// Load key
|
||||||
let key: String = match key_storage.get_key(service_id) {
|
let key: String = match key_storage.get_key(service_id) {
|
||||||
Ok(k) => {
|
Ok(k) => {
|
||||||
@@ -130,6 +100,37 @@ impl BookmarksClient {
|
|||||||
Ok(client)
|
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
|
/// Iterate over bookmarks keys
|
||||||
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
|
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
|
||||||
Box::new(self.hosts.bookmarks.keys())
|
Box::new(self.hosts.bookmarks.keys())
|
||||||
@@ -389,7 +390,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let client: BookmarksClient =
|
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
|
// Verify client
|
||||||
assert_eq!(client.hosts.bookmarks.len(), 0);
|
assert_eq!(client.hosts.bookmarks.len(), 0);
|
||||||
assert_eq!(client.hosts.recents.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());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add some bookmarks
|
||||||
client.add_bookmark(
|
client.add_bookmark(
|
||||||
"raspberry",
|
"raspberry",
|
||||||
@@ -430,7 +431,7 @@ mod tests {
|
|||||||
let key: String = client.key.clone();
|
let key: String = client.key.clone();
|
||||||
// Re-initialize a client
|
// Re-initialize a client
|
||||||
let client: BookmarksClient =
|
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
|
// Verify it loaded parameters correctly
|
||||||
assert_eq!(client.key, key);
|
assert_eq!(client.key, key);
|
||||||
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
|
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());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add s3 bookmark
|
||||||
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
|
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
|
||||||
// Verify bookmark
|
// Verify bookmark
|
||||||
@@ -473,7 +474,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add s3 bookmark
|
||||||
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
|
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
|
||||||
// Verify bookmark
|
// Verify bookmark
|
||||||
@@ -494,7 +495,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add s3 bookmark
|
||||||
client.add_recent(make_s3_ftparams());
|
client.add_recent(make_s3_ftparams());
|
||||||
// Verify bookmark
|
// Verify bookmark
|
||||||
@@ -517,7 +518,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add bookmark
|
||||||
client.add_bookmark(
|
client.add_bookmark(
|
||||||
"raspberry",
|
"raspberry",
|
||||||
@@ -568,7 +569,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add bookmark
|
||||||
client.add_bookmark(
|
client.add_bookmark(
|
||||||
"",
|
"",
|
||||||
@@ -589,7 +590,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add bookmark
|
||||||
client.add_bookmark(
|
client.add_bookmark(
|
||||||
"raspberry",
|
"raspberry",
|
||||||
@@ -617,7 +618,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add bookmark
|
||||||
client.add_recent(make_generic_ftparams(
|
client.add_recent(make_generic_ftparams(
|
||||||
FileTransferProtocol::Sftp,
|
FileTransferProtocol::Sftp,
|
||||||
@@ -653,7 +654,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add bookmark
|
||||||
client.add_recent(make_generic_ftparams(
|
client.add_recent(make_generic_ftparams(
|
||||||
FileTransferProtocol::Sftp,
|
FileTransferProtocol::Sftp,
|
||||||
@@ -680,7 +681,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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)
|
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||||
// 1
|
// 1
|
||||||
client.add_recent(make_generic_ftparams(
|
client.add_recent(make_generic_ftparams(
|
||||||
@@ -748,7 +749,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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
|
// Add bookmark
|
||||||
client.add_bookmark(
|
client.add_bookmark(
|
||||||
"",
|
"",
|
||||||
@@ -769,7 +770,7 @@ mod tests {
|
|||||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
// Initialize a new bookmarks client
|
// Initialize a new bookmarks client
|
||||||
let mut client: BookmarksClient =
|
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();
|
client.key = "MYSUPERSECRETKEY".to_string();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
|
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
// Ext
|
// 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::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
use crate::config::params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD};
|
use crate::config::params::{DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD, UserConfig};
|
||||||
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
|
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||||
use crate::explorer::GroupDirs;
|
use crate::explorer::GroupDirs;
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
|
|
||||||
@@ -480,9 +480,11 @@ mod tests {
|
|||||||
// Change some stuff
|
// Change some stuff
|
||||||
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
|
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
|
||||||
client.set_default_protocol(FileTransferProtocol::Scp);
|
client.set_default_protocol(FileTransferProtocol::Scp);
|
||||||
assert!(client
|
assert!(
|
||||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
client
|
||||||
.is_ok());
|
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
assert!(client.write_config().is_ok());
|
assert!(client.write_config().is_ok());
|
||||||
// Istantiate a new client
|
// Istantiate a new client
|
||||||
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||||
@@ -678,9 +680,11 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
// Add a new key
|
// Add a new key
|
||||||
let rsa_key: String = get_sample_rsa_key();
|
let rsa_key: String = get_sample_rsa_key();
|
||||||
assert!(client
|
assert!(
|
||||||
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
client
|
||||||
.is_ok());
|
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// Iterate keys
|
// Iterate keys
|
||||||
for key in client.iter_ssh_keys() {
|
for key in client.iter_ssh_keys() {
|
||||||
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
|
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
|
||||||
|
|||||||
@@ -76,30 +76,31 @@ impl KeyStorage for KeyringStorage {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
use whoami::username;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||||
fn test_system_keys_keyringstorage() {
|
fn test_system_keys_keyringstorage() {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use whoami::username;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
let username: String = username();
|
let username: String = username();
|
||||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||||
assert!(storage.is_supported());
|
assert!(storage.is_supported());
|
||||||
let app_name: &str = "termscp-test2";
|
let app_name: &str = "termscp-test2";
|
||||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||||
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
||||||
let _ = kring.delete_password();
|
let _ = kring.delete_credential();
|
||||||
drop(kring);
|
drop(kring);
|
||||||
// Secret should not exist
|
// Secret should not exist
|
||||||
assert!(storage.get_key(app_name).is_err());
|
assert!(storage.get_key(app_name).is_err());
|
||||||
// Write secret
|
// Write secret
|
||||||
assert!(storage.set_key(app_name, secret).is_ok());
|
assert!(storage.set_key(app_name, secret).is_ok());
|
||||||
// Get secret
|
// 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...
|
// Delete the key manually...
|
||||||
let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap();
|
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
|
// Storages
|
||||||
pub mod filestorage;
|
pub mod filestorage;
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
pub mod keyringstorage;
|
pub mod keyringstorage;
|
||||||
// ext
|
// ext
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
use keyring::Error as KeyringError;
|
use keyring::Error as KeyringError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// defines the error type for the `KeyStorage`
|
/// defines the error type for the `KeyStorage`
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum KeyStorageError {
|
pub enum KeyStorageError {
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
#[error("Key has a bad syntax")]
|
#[error("Key has a bad syntax")]
|
||||||
BadSytax,
|
BadSytax,
|
||||||
#[error("Provider service error")]
|
#[error("Provider service error")]
|
||||||
ProviderError,
|
ProviderError,
|
||||||
#[error("No such key")]
|
#[error("No such key")]
|
||||||
NoSuchKey,
|
NoSuchKey,
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
#[error("keyring error: {0}")]
|
#[error("keyring error: {0}")]
|
||||||
KeyringError(KeyringError),
|
KeyringError(KeyringError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
impl From<KeyringError> for KeyStorageError {
|
impl From<KeyringError> for KeyStorageError {
|
||||||
fn from(e: KeyringError) -> Self {
|
fn from(e: KeyringError) -> Self {
|
||||||
Self::KeyringError(e)
|
Self::KeyringError(e)
|
||||||
@@ -58,7 +53,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_system_keys_mod_errors() {
|
fn test_system_keys_mod_errors() {
|
||||||
#[cfg(feature = "with-keyring")]
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeyStorageError::BadSytax.to_string(),
|
KeyStorageError::BadSytax.to_string(),
|
||||||
String::from("Key has a bad syntax")
|
String::from("Key has a bad syntax")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
|||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
return Err(String::from(
|
return Err(String::from(
|
||||||
"This system doesn't seem to support CACHE_DIR",
|
"This system doesn't seem to support CACHE_DIR",
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
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)
|
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))?;
|
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
|
||||||
// Prepare log config
|
// 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
|
// Make logger
|
||||||
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
|
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,9 +123,11 @@ mod tests {
|
|||||||
.ok()
|
.ok()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Add ssh key
|
// Add ssh key
|
||||||
assert!(client
|
assert!(
|
||||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
client
|
||||||
.is_ok());
|
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// Create ssh key storage
|
// Create ssh key storage
|
||||||
let storage: SshKeyStorage = SshKeyStorage::from(&client);
|
let storage: SshKeyStorage = SshKeyStorage::from(&client);
|
||||||
// Verify key exists
|
// Verify key exists
|
||||||
@@ -141,7 +143,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sould_resolve_key_from_ssh2_config() {
|
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!(
|
let ssh_config_file = test_helpers::create_sample_file_with_content(format!(
|
||||||
r#"
|
r#"
|
||||||
Host test
|
Host test
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::fs::OpenOptions;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::string::ToString;
|
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;
|
use crate::config::themes::Theme;
|
||||||
|
|
||||||
/// ThemeProvider provides a high level API to communicate with the termscp theme
|
/// ThemeProvider provides a high level API to communicate with the termscp theme
|
||||||
@@ -143,7 +143,7 @@ impl ThemeProvider {
|
|||||||
mod test {
|
mod test {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tuirealm::tui::style::Color;
|
use tuirealm::ratatui::style::Color;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ impl FileToRemove {
|
|||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct FileUpdate {
|
pub struct FileUpdate {
|
||||||
/// Path to file which has changed
|
/// Path to file which has changed
|
||||||
local: PathBuf,
|
host_bridge: PathBuf,
|
||||||
/// Path to remote file to update
|
/// Path to remote file to update
|
||||||
remote: PathBuf,
|
remote: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -152,13 +152,13 @@ impl FileUpdate {
|
|||||||
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||||
Self {
|
Self {
|
||||||
remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path),
|
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
|
/// Get path to local file to sync
|
||||||
pub fn local(&self) -> &Path {
|
pub fn host_bridge(&self) -> &Path {
|
||||||
self.local.as_path()
|
self.host_bridge.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get path to remote file to sync
|
/// Get path to remote file to sync
|
||||||
@@ -288,7 +288,7 @@ mod test {
|
|||||||
Path::new("/home/foo/bar.txt"),
|
Path::new("/home/foo/bar.txt"),
|
||||||
);
|
);
|
||||||
if let FsChange::Update(change) = change {
|
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"));
|
assert_eq!(change.remote(), Path::new("/home/foo/bar.txt"));
|
||||||
} else {
|
} else {
|
||||||
panic!("not an update");
|
panic!("not an update");
|
||||||
@@ -303,7 +303,7 @@ mod test {
|
|||||||
Path::new("/home/foo/temp"),
|
Path::new("/home/foo/temp"),
|
||||||
);
|
);
|
||||||
if let FsChange::Update(change) = change {
|
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"));
|
assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt"));
|
||||||
} else {
|
} else {
|
||||||
panic!("not an update");
|
panic!("not an update");
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ mod change;
|
|||||||
// -- export
|
// -- export
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
|
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub use change::FsChange;
|
pub use change::FsChange;
|
||||||
use notify::{
|
use notify::{
|
||||||
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher,
|
Config, Error as WatcherError, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
|
|||||||
PathNotWatched,
|
PathNotWatched,
|
||||||
#[error("unable to watch path, since it's already watched")]
|
#[error("unable to watch path, since it's already watched")]
|
||||||
PathAlreadyWatched,
|
PathAlreadyWatched,
|
||||||
|
#[error("unknown event: {0}")]
|
||||||
|
UnknownEvent(&'static str),
|
||||||
#[error("worker error: {0}")]
|
#[error("worker error: {0}")]
|
||||||
WorkerError(WatcherError),
|
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
|
/// File system watcher
|
||||||
pub struct FsWatcher {
|
pub struct FsWatcher {
|
||||||
paths: HashMap<PathBuf, PathBuf>,
|
paths: HashMap<PathBuf, PathBuf>,
|
||||||
receiver: Receiver<DebouncedEvent>,
|
receiver: Receiver<notify::Result<Event>>,
|
||||||
watcher: RecommendedWatcher,
|
watcher: RecommendedWatcher,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,29 +105,32 @@ impl FsWatcher {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
paths: HashMap::default(),
|
paths: HashMap::default(),
|
||||||
receiver,
|
receiver,
|
||||||
watcher: watcher(tx, delay)?,
|
watcher: RecommendedWatcher::new(tx, Config::default().with_poll_interval(delay))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll searching for the first available disk change
|
/// Poll searching for the first available disk change
|
||||||
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
|
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
|
||||||
match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||||
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)),
|
Ok(res) => res,
|
||||||
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)),
|
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
||||||
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),
|
|
||||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
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() {
|
fn should_watch_path() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// check if in paths
|
// check if in paths
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
watcher.paths.get(tempdir.path()).unwrap(),
|
watcher.paths.get(tempdir.path()).unwrap(),
|
||||||
@@ -205,16 +263,20 @@ mod test {
|
|||||||
fn should_not_watch_path_if_subdir_of_watched_path() {
|
fn should_not_watch_path_if_subdir_of_watched_path() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// watch subdir
|
// watch subdir
|
||||||
let mut subdir = tempdir.path().to_path_buf();
|
let mut subdir = tempdir.path().to_path_buf();
|
||||||
subdir.push("abc/def");
|
subdir.push("abc/def");
|
||||||
// should return already watched
|
// should return already watched
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
watcher
|
||||||
.is_err());
|
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
// close tempdir
|
// close tempdir
|
||||||
assert!(tempdir.close().is_ok());
|
assert!(tempdir.close().is_ok());
|
||||||
}
|
}
|
||||||
@@ -223,9 +285,11 @@ mod test {
|
|||||||
fn should_unwatch_path() {
|
fn should_unwatch_path() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// unwatch
|
// unwatch
|
||||||
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
||||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||||
@@ -237,9 +301,11 @@ mod test {
|
|||||||
fn should_unwatch_path_when_subdir() {
|
fn should_unwatch_path_when_subdir() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// unwatch
|
// unwatch
|
||||||
let mut subdir = tempdir.path().to_path_buf();
|
let mut subdir = tempdir.path().to_path_buf();
|
||||||
subdir.push("abc/def");
|
subdir.push("abc/def");
|
||||||
@@ -262,9 +328,11 @@ mod test {
|
|||||||
fn should_tell_whether_path_is_watched() {
|
fn should_tell_whether_path_is_watched() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
assert_eq!(watcher.watched(tempdir.path()), true);
|
assert_eq!(watcher.watched(tempdir.path()), true);
|
||||||
let mut subdir = tempdir.path().to_path_buf();
|
let mut subdir = tempdir.path().to_path_buf();
|
||||||
subdir.push("abc/def");
|
subdir.push("abc/def");
|
||||||
@@ -280,9 +348,11 @@ mod test {
|
|||||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// create file
|
// create file
|
||||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||||
// wait
|
// wait
|
||||||
@@ -306,9 +376,11 @@ mod test {
|
|||||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
// create file
|
// create file
|
||||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||||
std::thread::sleep(Duration::from_millis(500));
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
@@ -329,7 +401,7 @@ mod test {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
fn should_poll_file_moved() {
|
fn should_poll_file_moved() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
@@ -368,9 +440,11 @@ mod test {
|
|||||||
fn should_poll_nothing() {
|
fn should_poll_nothing() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||||
let tempdir = TempDir::new().unwrap();
|
let tempdir = TempDir::new().unwrap();
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
watcher
|
||||||
.is_ok());
|
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
assert!(watcher.poll().ok().unwrap().is_none());
|
assert!(watcher.poll().ok().unwrap().is_none());
|
||||||
// close tempdir
|
// close tempdir
|
||||||
assert!(tempdir.close().is_ok());
|
assert!(tempdir.close().is_ok());
|
||||||
@@ -381,9 +455,11 @@ mod test {
|
|||||||
fn should_get_watched_paths() {
|
fn should_get_watched_paths() {
|
||||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
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("/tmp"), Path::new("/tmp")).is_ok());
|
||||||
assert!(watcher
|
assert!(
|
||||||
.watch(Path::new("/home"), Path::new("/home"))
|
watcher
|
||||||
.is_ok());
|
.watch(Path::new("/home"), Path::new("/home"))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
let mut watched_paths = watcher.watched_paths();
|
let mut watched_paths = watcher.watched_paths();
|
||||||
watched_paths.sort();
|
watched_paths.sort();
|
||||||
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);
|
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
//! `auth_activity` is the module which implements the authentication activity
|
//! `auth_activity` is the module which implements the authentication activity
|
||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
use super::{AuthActivity, FileTransferParams};
|
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
|
||||||
|
use crate::filetransfer::HostBridgeParams;
|
||||||
use crate::filetransfer::params::{
|
use crate::filetransfer::params::{
|
||||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||||
WebDAVProtocolParams,
|
WebDAVProtocolParams,
|
||||||
@@ -26,27 +27,49 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load selected bookmark (at index) to input fields
|
/// 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() {
|
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
||||||
// Iterate over bookmarks
|
// Iterate over bookmarks
|
||||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
||||||
// Load parameters into components
|
// 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
|
/// Save current input fields as a bookmark
|
||||||
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
|
pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
|
||||||
let params = match self.collect_host_params() {
|
let params = match form_tab {
|
||||||
Ok(p) => p,
|
FormTab::Remote => match self.collect_remote_host_params() {
|
||||||
Err(e) => {
|
Ok(p) => p,
|
||||||
self.mount_error(e);
|
Err(e) => {
|
||||||
return;
|
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() {
|
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||||
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
||||||
// Save bookmarks
|
// Save bookmarks
|
||||||
@@ -73,13 +96,16 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load selected recent (at index) to input fields
|
/// 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() {
|
if let Some(client) = self.bookmarks_client() {
|
||||||
// Iterate over bookmarks
|
// Iterate over bookmarks
|
||||||
if let Some(key) = self.recents_list.get(idx) {
|
if let Some(key) = self.recents_list.get(idx) {
|
||||||
if let Some(bookmark) = client.get_recent(key) {
|
if let Some(bookmark) = client.get_recent(key) {
|
||||||
// Load parameters
|
// 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +113,7 @@ impl AuthActivity {
|
|||||||
|
|
||||||
/// Save current input fields as a "recent"
|
/// Save current input fields as a "recent"
|
||||||
pub(super) fn save_recent(&mut self) {
|
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,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.mount_error(e);
|
self.mount_error(e);
|
||||||
@@ -147,75 +173,125 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load bookmark data into the gui components
|
/// 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
|
// Load parameters into components
|
||||||
self.protocol = bookmark.protocol;
|
self.host_bridge_protocol = HostBridgeProtocol::Remote(bookmark.protocol);
|
||||||
self.mount_protocol(bookmark.protocol);
|
self.mount_host_bridge_protocol(self.host_bridge_protocol);
|
||||||
self.mount_remote_directory(
|
self.mount_remote_directory(
|
||||||
|
FormTab::HostBridge,
|
||||||
bookmark
|
bookmark
|
||||||
.remote_path
|
.remote_path
|
||||||
.map(|x| x.to_string_lossy().to_string())
|
.map(|x| x.to_string_lossy().to_string())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
self.mount_local_directory(
|
self.mount_local_directory(
|
||||||
|
FormTab::HostBridge,
|
||||||
bookmark
|
bookmark
|
||||||
.local_path
|
.local_path
|
||||||
.map(|x| x.to_string_lossy().to_string())
|
.map(|x| x.to_string_lossy().to_string())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
match bookmark.params {
|
match bookmark.params {
|
||||||
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
|
ProtocolParams::AwsS3(params) => {
|
||||||
ProtocolParams::Kube(params) => self.load_bookmark_kube_into_gui(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(params),
|
ProtocolParams::Generic(params) => {
|
||||||
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(params),
|
self.load_bookmark_generic_into_gui(FormTab::HostBridge, params)
|
||||||
ProtocolParams::WebDAV(params) => self.load_bookmark_webdav_into_gui(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) {
|
/// Load bookmark data into the gui components
|
||||||
self.mount_address(params.address.as_str());
|
fn load_remote_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||||
self.mount_port(params.port);
|
// Load parameters into components
|
||||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
self.remote_protocol = bookmark.protocol;
|
||||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
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) {
|
fn load_bookmark_generic_into_gui(&mut self, form_tab: FormTab, params: GenericProtocolParams) {
|
||||||
self.mount_s3_bucket(params.bucket_name.as_str());
|
self.mount_address(form_tab, params.address.as_str());
|
||||||
self.mount_s3_region(params.region.as_deref().unwrap_or(""));
|
self.mount_port(form_tab, params.port);
|
||||||
self.mount_s3_endpoint(params.endpoint.as_deref().unwrap_or(""));
|
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||||
self.mount_s3_profile(params.profile.as_deref().unwrap_or(""));
|
self.mount_password(form_tab, params.password.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_kube_into_gui(&mut self, params: KubeProtocolParams) {
|
fn load_bookmark_s3_into_gui(&mut self, form_tab: FormTab, params: AwsS3Params) {
|
||||||
self.mount_kube_pod_name(params.pod.as_str());
|
self.mount_s3_bucket(form_tab, params.bucket_name.as_str());
|
||||||
self.mount_kube_container(¶ms.container);
|
self.mount_s3_region(form_tab, params.region.as_deref().unwrap_or(""));
|
||||||
self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or(""));
|
self.mount_s3_endpoint(form_tab, params.endpoint.as_deref().unwrap_or(""));
|
||||||
self.mount_kube_namespace(params.namespace.as_deref().unwrap_or(""));
|
self.mount_s3_profile(form_tab, params.profile.as_deref().unwrap_or(""));
|
||||||
self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or(""));
|
self.mount_s3_access_key(form_tab, params.access_key.as_deref().unwrap_or(""));
|
||||||
self.mount_kube_client_key(params.client_key.as_deref().unwrap_or(""));
|
self.mount_s3_secret_access_key(
|
||||||
self.mount_kube_username(params.username.as_deref().unwrap_or(""));
|
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_smb_into_gui(&mut self, params: SmbParams) {
|
fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) {
|
||||||
self.mount_address(params.address.as_str());
|
self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or(""));
|
||||||
#[cfg(unix)]
|
self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or(""));
|
||||||
self.mount_port(params.port);
|
self.mount_kube_client_cert(form_tab, params.client_cert.as_deref().unwrap_or(""));
|
||||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
self.mount_kube_client_key(form_tab, params.client_key.as_deref().unwrap_or(""));
|
||||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
self.mount_kube_username(form_tab, params.username.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_webdav_into_gui(&mut self, params: WebDAVProtocolParams) {
|
fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) {
|
||||||
self.mount_webdav_uri(¶ms.uri);
|
self.mount_address(form_tab, params.address.as_str());
|
||||||
self.mount_username(¶ms.username);
|
#[cfg(posix)]
|
||||||
self.mount_password(¶ms.password);
|
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 tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||||
|
|
||||||
use super::{FormMsg, Msg, UiMsg};
|
use super::{FormMsg, Msg, UiMsg};
|
||||||
|
use crate::ui::activities::auth::FormTab;
|
||||||
|
|
||||||
// -- bookmark list
|
// -- bookmark list
|
||||||
|
|
||||||
@@ -323,10 +324,11 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
|||||||
#[derive(MockComponent)]
|
#[derive(MockComponent)]
|
||||||
pub struct BookmarkSavePassword {
|
pub struct BookmarkSavePassword {
|
||||||
component: Radio,
|
component: Radio,
|
||||||
|
form_tab: FormTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BookmarkSavePassword {
|
impl BookmarkSavePassword {
|
||||||
pub fn new(color: Color) -> Self {
|
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Radio::default()
|
component: Radio::default()
|
||||||
.borders(
|
.borders(
|
||||||
@@ -340,6 +342,7 @@ impl BookmarkSavePassword {
|
|||||||
.rewind(true)
|
.rewind(true)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Save secrets?", Alignment::Center),
|
.title("Save secrets?", Alignment::Center),
|
||||||
|
form_tab,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,7 +367,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
|||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Enter, ..
|
code: Key::Enter, ..
|
||||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||||
Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur))
|
Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur))
|
||||||
}
|
}
|
||||||
@@ -378,10 +381,11 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
|||||||
#[derive(MockComponent)]
|
#[derive(MockComponent)]
|
||||||
pub struct BookmarkName {
|
pub struct BookmarkName {
|
||||||
component: Input,
|
component: Input,
|
||||||
|
form_tab: FormTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BookmarkName {
|
impl BookmarkName {
|
||||||
pub fn new(color: Color) -> Self {
|
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Input::default()
|
component: Input::default()
|
||||||
.borders(
|
.borders(
|
||||||
@@ -393,6 +397,7 @@ impl BookmarkName {
|
|||||||
.foreground(color)
|
.foreground(color)
|
||||||
.title("Bookmark name", Alignment::Left)
|
.title("Bookmark name", Alignment::Left)
|
||||||
.input_type(InputType::Text),
|
.input_type(InputType::Text),
|
||||||
|
form_tab,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,7 +452,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
|
|||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Enter, ..
|
code: Key::Enter, ..
|
||||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Down, ..
|
code: Key::Down, ..
|
||||||
}) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)),
|
}) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,15 +13,15 @@ pub use bookmarks::{
|
|||||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||||
RecentsList,
|
RecentsList,
|
||||||
};
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
pub use form::InputSmbWorkgroup;
|
pub use form::InputSmbWorkgroup;
|
||||||
pub use form::{
|
pub use form::{
|
||||||
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeContainer,
|
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
|
||||||
InputKubeNamespace, InputKubePodName, InputKubeUsername, InputLocalDirectory, InputPassword,
|
InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
|
||||||
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
||||||
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
||||||
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, ProtocolRadio,
|
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
|
||||||
RadioS3NewPathStyle,
|
RemoteProtocolRadio,
|
||||||
};
|
};
|
||||||
pub use popup::{
|
pub use popup::{
|
||||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ impl HelpFooter {
|
|||||||
TextSpan::from(" Change field "),
|
TextSpan::from(" Change field "),
|
||||||
TextSpan::from("<TAB>").bold().fg(key_color),
|
TextSpan::from("<TAB>").bold().fg(key_color),
|
||||||
TextSpan::from(" Switch tab "),
|
TextSpan::from(" Switch tab "),
|
||||||
|
TextSpan::from("<BACKTAB>").bold().fg(key_color),
|
||||||
|
TextSpan::from(" Switch form "),
|
||||||
TextSpan::from("<ENTER>").bold().fg(key_color),
|
TextSpan::from("<ENTER>").bold().fg(key_color),
|
||||||
TextSpan::from(" Submit form "),
|
TextSpan::from(" Submit form "),
|
||||||
TextSpan::from("<F10|ESC>").bold().fg(key_color),
|
TextSpan::from("<F10|ESC>").bold().fg(key_color),
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! `auth_activity` is the module which implements the authentication activity
|
//! `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::filetransfer::params::ProtocolParams;
|
||||||
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
use crate::system::auto_update::{Release, Update, UpdateStatus};
|
||||||
use crate::system::notifications::Notification;
|
use crate::system::notifications::Notification;
|
||||||
@@ -36,24 +39,72 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Collect host params as `FileTransferParams`
|
/// Collect host params as `FileTransferParams`
|
||||||
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
pub(super) fn collect_host_bridge_params(&self) -> Result<HostBridgeParams, &'static str> {
|
||||||
match self.protocol {
|
match self.host_bridge_protocol {
|
||||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
|
HostBridgeProtocol::Localhost => self.collect_localhost_host_params(),
|
||||||
FileTransferProtocol::Kube => self.collect_kube_host_params(),
|
HostBridgeProtocol::Remote(remote) => {
|
||||||
FileTransferProtocol::Smb => self.collect_smb_host_params(),
|
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::Ftp(_)
|
||||||
| FileTransferProtocol::Scp
|
| FileTransferProtocol::Scp
|
||||||
| FileTransferProtocol::Sftp => self.collect_generic_host_params(self.protocol),
|
| FileTransferProtocol::Sftp => {
|
||||||
FileTransferProtocol::WebDAV => self.collect_webdav_host_params(),
|
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
|
/// Get input values from fields or return an error if fields are invalid to work as generic
|
||||||
pub(super) fn collect_generic_host_params(
|
pub(super) fn collect_generic_host_params(
|
||||||
&self,
|
&self,
|
||||||
protocol: FileTransferProtocol,
|
protocol: FileTransferProtocol,
|
||||||
|
form_tab: FormTab,
|
||||||
) -> Result<FileTransferParams, &'static str> {
|
) -> 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() {
|
if params.address.is_empty() {
|
||||||
return Err("Invalid host");
|
return Err("Invalid host");
|
||||||
}
|
}
|
||||||
@@ -63,45 +114,52 @@ impl AuthActivity {
|
|||||||
Ok(FileTransferParams {
|
Ok(FileTransferParams {
|
||||||
protocol,
|
protocol,
|
||||||
params: ProtocolParams::Generic(params),
|
params: ProtocolParams::Generic(params),
|
||||||
local_path: self.get_input_local_directory(),
|
local_path: self.get_input_local_directory(form_tab),
|
||||||
remote_path: self.get_input_remote_directory(),
|
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
|
/// 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> {
|
pub(super) fn collect_s3_host_params(
|
||||||
let params = self.get_s3_params_input();
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Result<FileTransferParams, &'static str> {
|
||||||
|
let params = self.get_s3_params_input(form_tab);
|
||||||
if params.bucket_name.is_empty() {
|
if params.bucket_name.is_empty() {
|
||||||
return Err("Invalid bucket");
|
return Err("Invalid bucket");
|
||||||
}
|
}
|
||||||
Ok(FileTransferParams {
|
Ok(FileTransferParams {
|
||||||
protocol: FileTransferProtocol::AwsS3,
|
protocol: FileTransferProtocol::AwsS3,
|
||||||
params: ProtocolParams::AwsS3(params),
|
params: ProtocolParams::AwsS3(params),
|
||||||
local_path: self.get_input_local_directory(),
|
local_path: self.get_input_local_directory(form_tab),
|
||||||
remote_path: self.get_input_remote_directory(),
|
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
|
/// 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) -> Result<FileTransferParams, &'static str> {
|
pub(super) fn collect_kube_host_params(
|
||||||
let params = self.get_kube_params_input();
|
&self,
|
||||||
if params.pod.is_empty() {
|
form_tab: FormTab,
|
||||||
return Err("Invalid pod name");
|
) -> Result<FileTransferParams, &'static str> {
|
||||||
}
|
let params = self.get_kube_params_input(form_tab);
|
||||||
|
|
||||||
Ok(FileTransferParams {
|
Ok(FileTransferParams {
|
||||||
protocol: FileTransferProtocol::Kube,
|
protocol: FileTransferProtocol::Kube,
|
||||||
params: ProtocolParams::Kube(params),
|
params: ProtocolParams::Kube(params),
|
||||||
local_path: self.get_input_local_directory(),
|
local_path: self.get_input_local_directory(form_tab),
|
||||||
remote_path: self.get_input_remote_directory(),
|
remote_path: self.get_input_remote_directory(form_tab),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn collect_smb_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
pub(super) fn collect_smb_host_params(
|
||||||
let params = self.get_smb_params_input();
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Result<FileTransferParams, &'static str> {
|
||||||
|
let params = self.get_smb_params_input(form_tab);
|
||||||
if params.address.is_empty() {
|
if params.address.is_empty() {
|
||||||
return Err("Invalid address");
|
return Err("Invalid address");
|
||||||
}
|
}
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
if params.port == 0 {
|
if params.port == 0 {
|
||||||
return Err("Invalid port");
|
return Err("Invalid port");
|
||||||
}
|
}
|
||||||
@@ -111,21 +169,24 @@ impl AuthActivity {
|
|||||||
Ok(FileTransferParams {
|
Ok(FileTransferParams {
|
||||||
protocol: FileTransferProtocol::Smb,
|
protocol: FileTransferProtocol::Smb,
|
||||||
params: ProtocolParams::Smb(params),
|
params: ProtocolParams::Smb(params),
|
||||||
local_path: self.get_input_local_directory(),
|
local_path: self.get_input_local_directory(form_tab),
|
||||||
remote_path: self.get_input_remote_directory(),
|
remote_path: self.get_input_remote_directory(form_tab),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn collect_webdav_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
pub(super) fn collect_webdav_host_params(
|
||||||
let params = self.get_webdav_params_input();
|
&self,
|
||||||
|
form_tab: FormTab,
|
||||||
|
) -> Result<FileTransferParams, &'static str> {
|
||||||
|
let params = self.get_webdav_params_input(form_tab);
|
||||||
if params.uri.is_empty() {
|
if params.uri.is_empty() {
|
||||||
return Err("Invalid URI");
|
return Err("Invalid URI");
|
||||||
}
|
}
|
||||||
Ok(FileTransferParams {
|
Ok(FileTransferParams {
|
||||||
protocol: FileTransferProtocol::WebDAV,
|
protocol: FileTransferProtocol::WebDAV,
|
||||||
params: ProtocolParams::WebDAV(params),
|
params: ProtocolParams::WebDAV(params),
|
||||||
local_path: self.get_input_local_directory(),
|
local_path: self.get_input_local_directory(form_tab),
|
||||||
remote_path: self.get_input_remote_directory(),
|
remote_path: self.get_input_remote_directory(form_tab),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,26 +17,36 @@ use tuirealm::application::PollStrategy;
|
|||||||
use tuirealm::listener::EventListenerCfg;
|
use tuirealm::listener::EventListenerCfg;
|
||||||
use tuirealm::{Application, NoUserEvent, Update};
|
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::config::themes::Theme;
|
||||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||||
use crate::system::bookmarks_client::BookmarksClient;
|
use crate::system::bookmarks_client::BookmarksClient;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
// radio
|
// host bridge protocol radio
|
||||||
const RADIO_PROTOCOL_SFTP: usize = 0;
|
const HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST: usize = 0;
|
||||||
const RADIO_PROTOCOL_SCP: usize = 1;
|
const HOST_BRIDGE_RADIO_PROTOCOL_SFTP: usize = 1;
|
||||||
const RADIO_PROTOCOL_FTP: usize = 2;
|
const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2;
|
||||||
const RADIO_PROTOCOL_FTPS: usize = 3;
|
const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3;
|
||||||
const RADIO_PROTOCOL_S3: usize = 4;
|
const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4;
|
||||||
const RADIO_PROTOCOL_KUBE: usize = 5;
|
const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5;
|
||||||
const RADIO_PROTOCOL_WEBDAV: usize = 6;
|
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6;
|
||||||
const RADIO_PROTOCOL_SMB: usize = 7;
|
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
|
// -- components
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum Id {
|
pub enum Id {
|
||||||
Address,
|
|
||||||
BookmarkName,
|
BookmarkName,
|
||||||
BookmarkSavePassword,
|
BookmarkSavePassword,
|
||||||
BookmarksList,
|
BookmarksList,
|
||||||
@@ -45,24 +55,33 @@ pub enum Id {
|
|||||||
ErrorPopup,
|
ErrorPopup,
|
||||||
GlobalListener,
|
GlobalListener,
|
||||||
HelpFooter,
|
HelpFooter,
|
||||||
|
HostBridge(AuthFormId),
|
||||||
InfoPopup,
|
InfoPopup,
|
||||||
InstallUpdatePopup,
|
InstallUpdatePopup,
|
||||||
Keybindings,
|
Keybindings,
|
||||||
KubePodName,
|
NewVersionChangelog,
|
||||||
KubeContainer,
|
NewVersionDisclaimer,
|
||||||
|
QuitPopup,
|
||||||
|
RecentsList,
|
||||||
|
Remote(AuthFormId),
|
||||||
|
Subtitle,
|
||||||
|
Title,
|
||||||
|
WaitPopup,
|
||||||
|
WindowSizeError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
|
pub enum AuthFormId {
|
||||||
|
Address,
|
||||||
KubeNamespace,
|
KubeNamespace,
|
||||||
KubeClusterUrl,
|
KubeClusterUrl,
|
||||||
KubeUsername,
|
KubeUsername,
|
||||||
KubeClientCert,
|
KubeClientCert,
|
||||||
KubeClientKey,
|
KubeClientKey,
|
||||||
LocalDirectory,
|
LocalDirectory,
|
||||||
NewVersionChangelog,
|
|
||||||
NewVersionDisclaimer,
|
|
||||||
Password,
|
Password,
|
||||||
Port,
|
Port,
|
||||||
Protocol,
|
Protocol,
|
||||||
QuitPopup,
|
|
||||||
RecentsList,
|
|
||||||
RemoteDirectory,
|
RemoteDirectory,
|
||||||
S3AccessKey,
|
S3AccessKey,
|
||||||
S3Bucket,
|
S3Bucket,
|
||||||
@@ -74,25 +93,21 @@ pub enum Id {
|
|||||||
S3SecurityToken,
|
S3SecurityToken,
|
||||||
S3SessionToken,
|
S3SessionToken,
|
||||||
SmbShare,
|
SmbShare,
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
SmbWorkgroup,
|
SmbWorkgroup,
|
||||||
Subtitle,
|
|
||||||
Title,
|
|
||||||
Username,
|
Username,
|
||||||
WaitPopup,
|
|
||||||
WebDAVUri,
|
WebDAVUri,
|
||||||
WindowSizeError,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Msg {
|
enum Msg {
|
||||||
Form(FormMsg),
|
Form(FormMsg),
|
||||||
Ui(UiMsg),
|
Ui(UiMsg),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum FormMsg {
|
enum FormMsg {
|
||||||
Connect,
|
Connect,
|
||||||
DeleteBookmark,
|
DeleteBookmark,
|
||||||
DeleteRecent,
|
DeleteRecent,
|
||||||
@@ -100,15 +115,14 @@ pub enum FormMsg {
|
|||||||
InstallUpdate,
|
InstallUpdate,
|
||||||
LoadBookmark(usize),
|
LoadBookmark(usize),
|
||||||
LoadRecent(usize),
|
LoadRecent(usize),
|
||||||
ProtocolChanged(FileTransferProtocol),
|
HostBridgeProtocolChanged(HostBridgeProtocol),
|
||||||
|
RemoteProtocolChanged(FileTransferProtocol),
|
||||||
Quit,
|
Quit,
|
||||||
SaveBookmark,
|
SaveBookmark(FormTab),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum UiMsg {
|
pub enum UiMsg {
|
||||||
AddressBlurDown,
|
|
||||||
AddressBlurUp,
|
|
||||||
BookmarksListBlur,
|
BookmarksListBlur,
|
||||||
BookmarksTabBlur,
|
BookmarksTabBlur,
|
||||||
CloseDeleteBookmark,
|
CloseDeleteBookmark,
|
||||||
@@ -119,10 +133,25 @@ pub enum UiMsg {
|
|||||||
CloseKeybindingsPopup,
|
CloseKeybindingsPopup,
|
||||||
CloseQuitPopup,
|
CloseQuitPopup,
|
||||||
CloseSaveBookmark,
|
CloseSaveBookmark,
|
||||||
KubePodNameBlurDown,
|
HostBridge(UiAuthFormMsg),
|
||||||
KubePodNameBlurUp,
|
RececentsListBlur,
|
||||||
KubeContainerBlurDown,
|
Remote(UiAuthFormMsg),
|
||||||
KubeContainerBlurUp,
|
BookmarkNameBlur,
|
||||||
|
SaveBookmarkPasswordBlur,
|
||||||
|
ShowDeleteBookmarkPopup,
|
||||||
|
ShowDeleteRecentPopup,
|
||||||
|
ShowKeybindingsPopup,
|
||||||
|
ShowQuitPopup,
|
||||||
|
ShowReleaseNotes,
|
||||||
|
ShowSaveBookmarkPopup,
|
||||||
|
WindowResized,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum UiAuthFormMsg {
|
||||||
|
AddressBlurDown,
|
||||||
|
AddressBlurUp,
|
||||||
|
ChangeFormTab,
|
||||||
KubeNamespaceBlurDown,
|
KubeNamespaceBlurDown,
|
||||||
KubeNamespaceBlurUp,
|
KubeNamespaceBlurUp,
|
||||||
KubeClusterUrlBlurDown,
|
KubeClusterUrlBlurDown,
|
||||||
@@ -142,7 +171,6 @@ pub enum UiMsg {
|
|||||||
PortBlurUp,
|
PortBlurUp,
|
||||||
ProtocolBlurDown,
|
ProtocolBlurDown,
|
||||||
ProtocolBlurUp,
|
ProtocolBlurUp,
|
||||||
RececentsListBlur,
|
|
||||||
RemoteDirectoryBlurDown,
|
RemoteDirectoryBlurDown,
|
||||||
RemoteDirectoryBlurUp,
|
RemoteDirectoryBlurUp,
|
||||||
S3AccessKeyBlurDown,
|
S3AccessKeyBlurDown,
|
||||||
@@ -165,23 +193,14 @@ pub enum UiMsg {
|
|||||||
S3SessionTokenBlurUp,
|
S3SessionTokenBlurUp,
|
||||||
SmbShareBlurDown,
|
SmbShareBlurDown,
|
||||||
SmbShareBlurUp,
|
SmbShareBlurUp,
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
SmbWorkgroupDown,
|
SmbWorkgroupDown,
|
||||||
#[cfg(unix)]
|
#[cfg(posix)]
|
||||||
SmbWorkgroupUp,
|
SmbWorkgroupUp,
|
||||||
BookmarkNameBlur,
|
|
||||||
SaveBookmarkPasswordBlur,
|
|
||||||
ShowDeleteBookmarkPopup,
|
|
||||||
ShowDeleteRecentPopup,
|
|
||||||
ShowKeybindingsPopup,
|
|
||||||
ShowQuitPopup,
|
|
||||||
ShowReleaseNotes,
|
|
||||||
ShowSaveBookmarkPopup,
|
|
||||||
UsernameBlurDown,
|
UsernameBlurDown,
|
||||||
UsernameBlurUp,
|
UsernameBlurUp,
|
||||||
WebDAVUriBlurDown,
|
WebDAVUriBlurDown,
|
||||||
WebDAVUriBlurUp,
|
WebDAVUriBlurUp,
|
||||||
WindowResized,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auth form input mask
|
/// Auth form input mask
|
||||||
@@ -190,10 +209,23 @@ enum InputMask {
|
|||||||
Generic,
|
Generic,
|
||||||
AwsS3,
|
AwsS3,
|
||||||
Kube,
|
Kube,
|
||||||
|
Localhost,
|
||||||
Smb,
|
Smb,
|
||||||
WebDAV,
|
WebDAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum HostBridgeProtocol {
|
||||||
|
Localhost,
|
||||||
|
Remote(FileTransferProtocol),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum FormTab {
|
||||||
|
HostBridge,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
// Store keys
|
// Store keys
|
||||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||||
@@ -209,8 +241,11 @@ pub struct AuthActivity {
|
|||||||
exit_reason: Option<ExitReason>,
|
exit_reason: Option<ExitReason>,
|
||||||
/// Should redraw ui
|
/// Should redraw ui
|
||||||
redraw: bool,
|
redraw: bool,
|
||||||
/// Protocol
|
/// Host bridge protocol
|
||||||
protocol: FileTransferProtocol,
|
host_bridge_protocol: HostBridgeProtocol,
|
||||||
|
last_form_tab: FormTab,
|
||||||
|
/// Remote file transfer protocol
|
||||||
|
remote_protocol: FileTransferProtocol,
|
||||||
context: Option<Context>,
|
context: Option<Context>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,15 +255,17 @@ impl AuthActivity {
|
|||||||
AuthActivity {
|
AuthActivity {
|
||||||
app: Application::init(
|
app: Application::init(
|
||||||
EventListenerCfg::default()
|
EventListenerCfg::default()
|
||||||
.default_input_listener(ticks)
|
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
|
||||||
.poll_timeout(ticks),
|
.poll_timeout(ticks),
|
||||||
),
|
),
|
||||||
context: None,
|
context: None,
|
||||||
bookmarks_list: Vec::new(),
|
bookmarks_list: Vec::new(),
|
||||||
exit_reason: None,
|
exit_reason: None,
|
||||||
|
last_form_tab: FormTab::Remote,
|
||||||
recents_list: Vec::new(),
|
recents_list: Vec::new(),
|
||||||
redraw: true,
|
redraw: true,
|
||||||
protocol: FileTransferProtocol::Sftp,
|
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
||||||
|
remote_protocol: FileTransferProtocol::Sftp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,8 +298,23 @@ impl AuthActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get current input mask to show
|
/// Get current input mask to show
|
||||||
fn input_mask(&self) -> InputMask {
|
fn remote_input_mask(&self) -> InputMask {
|
||||||
match self.protocol {
|
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::AwsS3 => InputMask::AwsS3,
|
||||||
FileTransferProtocol::Ftp(_)
|
FileTransferProtocol::Ftp(_)
|
||||||
| FileTransferProtocol::Scp
|
| FileTransferProtocol::Scp
|
||||||
@@ -281,7 +333,7 @@ impl Activity for AuthActivity {
|
|||||||
fn on_create(&mut self, mut context: Context) {
|
fn on_create(&mut self, mut context: Context) {
|
||||||
debug!("Initializing activity");
|
debug!("Initializing activity");
|
||||||
// Initialize file transfer params
|
// Initialize file transfer params
|
||||||
context.set_ftparams(FileTransferParams::default());
|
context.set_remote_params(FileTransferParams::default());
|
||||||
// Set context
|
// Set context
|
||||||
self.context = Some(context);
|
self.context = Some(context);
|
||||||
// Clear terminal
|
// 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 {
|
impl FileTransferActivity {
|
||||||
/// Enter a directory on local host from entry
|
/// Enter a directory on local host from entry
|
||||||
pub(crate) fn action_enter_local_dir(&mut self, dir: File) {
|
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() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name()));
|
||||||
}
|
}
|
||||||
@@ -35,8 +35,9 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// Change local directory reading value from input
|
/// Change local directory reading value from input
|
||||||
pub(crate) fn action_change_local_dir(&mut self, input: String) {
|
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());
|
let dir_path: PathBuf =
|
||||||
self.local_changedir(dir_path.as_path(), true);
|
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
|
// Check whether to sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
self.synchronize_browsing(SyncBrowsingDestination::Path(input));
|
||||||
@@ -55,8 +56,8 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// Go to previous directory from localhost
|
/// Go to previous directory from localhost
|
||||||
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
pub(crate) fn action_go_to_previous_local_dir(&mut self) {
|
||||||
if let Some(d) = self.local_mut().popd() {
|
if let Some(d) = self.host_bridge_mut().popd() {
|
||||||
self.local_changedir(d.as_path(), false);
|
self.host_bridge_changedir(d.as_path(), false);
|
||||||
// Check whether to sync
|
// Check whether to sync
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
self.synchronize_browsing(SyncBrowsingDestination::PreviousDir);
|
||||||
@@ -78,10 +79,10 @@ impl FileTransferActivity {
|
|||||||
/// Go to upper directory on local host
|
/// Go to upper directory on local host
|
||||||
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
pub(crate) fn action_go_to_local_upper_dir(&mut self) {
|
||||||
// Get pwd
|
// Get pwd
|
||||||
let path: PathBuf = self.local().wrkdir.clone();
|
let path: PathBuf = self.host_bridge().wrkdir.clone();
|
||||||
// Go to parent directory
|
// Go to parent directory
|
||||||
if let Some(parent) = path.as_path().parent() {
|
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 sync is enabled update remote too
|
||||||
if self.browser.sync_browsing && self.browser.found().is_none() {
|
if self.browser.sync_browsing && self.browser.found().is_none() {
|
||||||
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
self.synchronize_browsing(SyncBrowsingDestination::ParentDir);
|
||||||
@@ -118,7 +119,7 @@ impl FileTransferActivity {
|
|||||||
trace!("Synchronizing browsing to path {}", path.display());
|
trace!("Synchronizing browsing to path {}", path.display());
|
||||||
// Check whether destination exists on host
|
// Check whether destination exists on host
|
||||||
let exists = match self.browser.tab() {
|
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,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -129,7 +130,17 @@ impl FileTransferActivity {
|
|||||||
return;
|
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,
|
_ => return,
|
||||||
};
|
};
|
||||||
let name = path
|
let name = path
|
||||||
@@ -150,13 +161,15 @@ impl FileTransferActivity {
|
|||||||
trace!("User wants to create the unexisting directory");
|
trace!("User wants to create the unexisting directory");
|
||||||
// Make directory
|
// Make directory
|
||||||
match self.browser.tab() {
|
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()),
|
FileExplorerTab::Remote => self.action_local_mkdir(name.clone()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Do not synchronize, disable sync browsing and return
|
// 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(
|
self.log(
|
||||||
LogLevel::Warn,
|
LogLevel::Warn,
|
||||||
format!("Refused to create '{name}'; synchronized browsing disabled"),
|
format!("Refused to create '{name}'; synchronized browsing disabled"),
|
||||||
@@ -173,18 +186,18 @@ impl FileTransferActivity {
|
|||||||
// Enter directory
|
// Enter directory
|
||||||
match destination {
|
match destination {
|
||||||
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
SyncBrowsingDestination::ParentDir => match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true),
|
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true),
|
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
SyncBrowsingDestination::Path(_) => match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), true),
|
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true),
|
||||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), true),
|
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
SyncBrowsingDestination::PreviousDir => match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.remote_changedir(path.as_path(), false),
|
FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false),
|
||||||
FileExplorerTab::Remote => self.local_changedir(path.as_path(), false),
|
FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -197,13 +210,13 @@ impl FileTransferActivity {
|
|||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
match (destination, self.browser.tab()) {
|
match (destination, self.browser.tab()) {
|
||||||
// NOTE: tab and methods are switched on purpose
|
// 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())
|
self.remote().wrkdir.parent().map(|x| x.to_path_buf())
|
||||||
}
|
}
|
||||||
(SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => {
|
(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() {
|
if let Some(p) = self.remote_mut().popd() {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
@@ -212,7 +225,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
(SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => {
|
||||||
if let Some(p) = self.local_mut().popd() {
|
if let Some(p) = self.host_bridge_mut().popd() {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
warn!("Cannot synchronize browsing: local has no previous directory in stack");
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ use remotefs::fs::UnixPex;
|
|||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
pub fn action_local_chmod(&mut self, mode: UnixPex) {
|
||||||
let files = self.get_local_selected_entries().get_files();
|
let files = self.get_local_selected_entries().get_files();
|
||||||
|
|
||||||
for file in 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(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
@@ -51,12 +50,11 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
|
||||||
let files = self.get_found_selected_entries().get_files();
|
let files = self.get_found_selected_entries().get_files();
|
||||||
|
|
||||||
for file in 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(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ impl FileTransferActivity {
|
|||||||
self.local_copy_file(&entry, dest_path.as_path());
|
self.local_copy_file(&entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Try to copy each file to Input/{FILE_NAME}
|
|
||||||
let base_path: PathBuf = PathBuf::from(input);
|
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
|
||||||
dest_path.push(entry.name());
|
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 => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
@@ -39,21 +40,22 @@ impl FileTransferActivity {
|
|||||||
self.remote_copy_file(entry, dest_path.as_path());
|
self.remote_copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Try to copy each file to Input/{FILE_NAME}
|
|
||||||
let base_path: PathBuf = PathBuf::from(input);
|
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.into_iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
|
||||||
dest_path.push(entry.name());
|
dest_path.push(entry.name());
|
||||||
self.remote_copy_file(entry, dest_path.as_path());
|
self.remote_copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
fn local_copy_file(&mut self, entry: &File, dest: &Path) {
|
||||||
match self.host.copy(entry, dest) {
|
match self.host_bridge.copy(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -136,7 +138,7 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Stat dir
|
// 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,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
@@ -189,7 +191,7 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Get local fs entry
|
// 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(e) if e.is_file() => e,
|
||||||
Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
|
Ok(_) => panic!("{} is not a file", tmpfile.path().display()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
// Delete file
|
||||||
self.local_remove_file(entry);
|
self.local_remove_file(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.host_bridge_mut().clear_queue();
|
||||||
|
self.reload_host_bridge_filelist();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
@@ -33,17 +37,21 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
// Delete file
|
||||||
self.remote_remove_file(entry);
|
self.remote_remove_file(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
pub(crate) fn local_remove_file(&mut self, entry: &File) {
|
||||||
match self.host.remove(entry) {
|
match self.host_bridge.remove(entry) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Log
|
// Log
|
||||||
self.log(
|
self.log(
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
// locals
|
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
// ext
|
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
use remotefs::fs::Metadata;
|
||||||
|
|
||||||
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ impl FileTransferActivity {
|
|||||||
pub(crate) fn action_edit_local_file(&mut self) {
|
pub(crate) fn action_edit_local_file(&mut self) {
|
||||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
let entries: Vec<File> = match self.get_local_selected_entries() {
|
||||||
SelectedFile::One(entry) => vec![entry],
|
SelectedFile::One(entry) => vec![entry],
|
||||||
SelectedFile::Many(entries) => entries,
|
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||||
SelectedFile::None => vec![],
|
SelectedFile::None => vec![],
|
||||||
};
|
};
|
||||||
// Edit all entries
|
// Edit all entries
|
||||||
@@ -29,17 +28,26 @@ impl FileTransferActivity {
|
|||||||
format!("Opening file \"{}\"…", entry.path().display()),
|
format!("Opening file \"{}\"…", entry.path().display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
if let Err(err) = self.edit_local_file(entry.path()) {
|
let res = match self.host_bridge.is_localhost() {
|
||||||
|
true => self.edit_local_file(entry.path()).map(|_| ()),
|
||||||
|
false => self.edit_bridged_local_file(entry),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
self.log_and_alert(LogLevel::Error, err);
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.host_bridge_mut().clear_queue();
|
||||||
|
self.reload_host_bridge_filelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_edit_remote_file(&mut self) {
|
pub(crate) fn action_edit_remote_file(&mut self) {
|
||||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
||||||
SelectedFile::One(entry) => vec![entry],
|
SelectedFile::One(entry) => vec![entry],
|
||||||
SelectedFile::Many(entries) => entries,
|
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||||
SelectedFile::None => vec![],
|
SelectedFile::None => vec![],
|
||||||
};
|
};
|
||||||
// Edit all entries
|
// Edit all entries
|
||||||
@@ -56,10 +64,90 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edit a file on localhost
|
/// Edit a file on localhost
|
||||||
fn edit_local_file(&mut self, path: &Path) -> Result<(), String> {
|
fn edit_bridged_local_file(&mut self, entry: &File) -> Result<(), String> {
|
||||||
|
// Download file
|
||||||
|
let tmpfile: String =
|
||||||
|
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
|
||||||
|
None => {
|
||||||
|
return Err("Could not create tempdir".to_string());
|
||||||
|
}
|
||||||
|
Some(p) => p,
|
||||||
|
};
|
||||||
|
let cache: PathBuf = match self.cache.as_ref() {
|
||||||
|
None => {
|
||||||
|
return Err("Could not create tempdir".to_string());
|
||||||
|
}
|
||||||
|
Some(p) => p.path().to_path_buf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// open from host bridge
|
||||||
|
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
||||||
|
Ok(reader) => reader,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(format!("Failed to open bridged entry: {err}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tempfile = cache.join(tmpfile);
|
||||||
|
|
||||||
|
// write to file
|
||||||
|
let mut writer = match std::fs::File::create(tempfile.as_path()) {
|
||||||
|
Ok(writer) => writer,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(format!("Failed to write file: {err}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_file_size = match std::io::copy(&mut reader, &mut writer) {
|
||||||
|
Err(err) => return Err(format!("Could not write file: {err}")),
|
||||||
|
Ok(size) => size,
|
||||||
|
};
|
||||||
|
|
||||||
|
// edit file
|
||||||
|
|
||||||
|
let has_changed = self.edit_local_file(tempfile.as_path())?;
|
||||||
|
|
||||||
|
if has_changed {
|
||||||
|
// report changes to remote
|
||||||
|
let mut reader = match std::fs::File::open(tempfile.as_path()) {
|
||||||
|
Ok(reader) => reader,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(format!("Could not open file: {err}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut writer = match self.host_bridge.create_file(
|
||||||
|
entry.path(),
|
||||||
|
&Metadata {
|
||||||
|
size: new_file_size,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(writer) => writer,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(format!("Could not write file: {err}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
|
||||||
|
return Err(format!("Could not write file: {err}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.host_bridge
|
||||||
|
.finalize_write(writer)
|
||||||
|
.map_err(|err| format!("Could not write file: {err}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_local_file(&mut self, path: &Path) -> Result<bool, String> {
|
||||||
// Read first 2048 bytes or less from file to check if it is textual
|
// Read first 2048 bytes or less from file to check if it is textual
|
||||||
match OpenOptions::new().read(true).open(path) {
|
match OpenOptions::new().read(true).open(path) {
|
||||||
Ok(mut f) => {
|
Ok(mut f) => {
|
||||||
@@ -90,6 +178,8 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// Lock ports
|
// Lock ports
|
||||||
assert!(self.app.lock_ports().is_ok());
|
assert!(self.app.lock_ports().is_ok());
|
||||||
|
// Get current file modification time
|
||||||
|
let prev_mtime = self.get_localhost_mtime(path)?;
|
||||||
// Open editor
|
// Open editor
|
||||||
match edit::edit_file(path) {
|
match edit::edit_file(path) {
|
||||||
Ok(_) => self.log(
|
Ok(_) => self.log(
|
||||||
@@ -117,16 +207,29 @@ impl FileTransferActivity {
|
|||||||
// Unlock ports
|
// Unlock ports
|
||||||
assert!(self.app.unlock_ports().is_ok());
|
assert!(self.app.unlock_ports().is_ok());
|
||||||
}
|
}
|
||||||
Ok(())
|
let after_mtime = self.get_localhost_mtime(path)?;
|
||||||
|
|
||||||
|
// return if file has changed
|
||||||
|
Ok(prev_mtime != after_mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_localhost_mtime(&self, p: &Path) -> Result<SystemTime, String> {
|
||||||
|
let attr = match std::fs::metadata(p) {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(format!("Could not read file metadata: {}", err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Metadata::from(attr)
|
||||||
|
.modified
|
||||||
|
.unwrap_or(std::time::UNIX_EPOCH))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edit file on remote host
|
/// Edit file on remote host
|
||||||
fn edit_remote_file(&mut self, file: File) -> Result<(), String> {
|
fn edit_remote_file(&mut self, file: File) -> Result<(), String> {
|
||||||
// Create temp file
|
// Create temp file
|
||||||
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
|
let tmpfile = self.download_file_as_temp(&file)?;
|
||||||
Ok(p) => p,
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
|
||||||
// Download file
|
// Download file
|
||||||
let file_name = file.name();
|
let file_name = file.name();
|
||||||
let file_path = file.path().to_path_buf();
|
let file_path = file.path().to_path_buf();
|
||||||
@@ -138,27 +241,27 @@ impl FileTransferActivity {
|
|||||||
return Err(format!("Could not open file {file_name}: {err}"));
|
return Err(format!("Could not open file {file_name}: {err}"));
|
||||||
}
|
}
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
|
let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not stat \"{}\": {}",
|
"Could not stat \"{}\": {}",
|
||||||
tmpfile.as_path().display(),
|
tmpfile.as_path().display(),
|
||||||
err
|
err
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Edit file
|
// Edit file
|
||||||
self.edit_local_file(tmpfile.as_path())?;
|
self.edit_local_file(tmpfile.as_path())?;
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry: File = match self.host.stat(tmpfile.as_path()) {
|
let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not stat \"{}\": {}",
|
"Could not stat \"{}\": {}",
|
||||||
tmpfile.as_path().display(),
|
tmpfile.as_path().display(),
|
||||||
err
|
err
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Check if file has changed
|
// Check if file has changed
|
||||||
@@ -177,14 +280,14 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry = match self.host.stat(tmpfile.as_path()) {
|
let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not stat \"{}\": {}",
|
"Could not stat \"{}\": {}",
|
||||||
tmpfile.as_path().display(),
|
tmpfile.as_path().display(),
|
||||||
err
|
err
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Send file
|
// Send file
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use super::{FileTransferActivity, LogLevel};
|
|||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||||
match self.host.exec(input.as_str()) {
|
match self.host_bridge.exec(input.as_str()) {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
// Reload files
|
// Reload files
|
||||||
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
|
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use regex::Regex;
|
|||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
use wildmatch::WildMatch;
|
use wildmatch::WildMatch;
|
||||||
|
|
||||||
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
|
||||||
use crate::ui::activities::filetransfer::FileTransferActivity;
|
use crate::ui::activities::filetransfer::FileTransferActivity;
|
||||||
|
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Filter {
|
pub enum Filter {
|
||||||
@@ -40,7 +40,7 @@ impl FileTransferActivity {
|
|||||||
let filter = Filter::from_str(filter).unwrap();
|
let filter = Filter::from_str(filter).unwrap();
|
||||||
|
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.browser.local().iter_files(),
|
FileExplorerTab::HostBridge => self.browser.host_bridge().iter_files(),
|
||||||
FileExplorerTab::Remote => self.browser.remote().iter_files(),
|
FileExplorerTab::Remote => self.browser.remote().iter_files(),
|
||||||
_ => return vec![],
|
_ => return vec![],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,10 @@ use super::super::browser::FileExplorerTab;
|
|||||||
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<File>, String> {
|
|
||||||
match self.host.find(input.as_str()) {
|
|
||||||
Ok(entries) => Ok(entries),
|
|
||||||
Err(err) => Err(format!("Could not search for files: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<File>, String> {
|
|
||||||
match self.client.as_mut().find(input.as_str()) {
|
|
||||||
Ok(entries) => Ok(entries),
|
|
||||||
Err(err) => Err(format!("Could not search for files: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn action_find_changedir(&mut self) {
|
pub(crate) fn action_find_changedir(&mut self) {
|
||||||
// Match entry
|
// Match entry
|
||||||
if let SelectedFile::One(entry) = self.get_found_selected_entries() {
|
if let Some(entry) = self.get_found_selected_file() {
|
||||||
|
debug!("Changedir to: {}", entry.name());
|
||||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||||
let path = if entry.is_dir() {
|
let path = if entry.is_dir() {
|
||||||
entry.path().to_path_buf()
|
entry.path().to_path_buf()
|
||||||
@@ -37,8 +24,8 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
// Change directory
|
// Change directory
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
self.local_changedir(path.as_path(), true)
|
self.host_bridge_changedir(path.as_path(), true)
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
self.remote_changedir(path.as_path(), true)
|
self.remote_changedir(path.as_path(), true)
|
||||||
@@ -49,12 +36,16 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
|
||||||
let wrkdir: PathBuf = match self.browser.tab() {
|
let wrkdir: PathBuf = match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
|
self.remote().wrkdir.clone()
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
self.host_bridge().wrkdir.clone()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match self.get_found_selected_entries() {
|
match self.get_found_selected_entries() {
|
||||||
SelectedFile::One(entry) => match self.browser.tab() {
|
SelectedFile::One(entry) => match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.remote_file_exists(file_to_check.as_path())
|
&& self.remote_file_exists(file_to_check.as_path())
|
||||||
@@ -79,7 +70,7 @@ impl FileTransferActivity {
|
|||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.local_file_exists(file_to_check.as_path())
|
&& self.host_bridge_file_exists(file_to_check.as_path())
|
||||||
&& !self.should_replace_file(
|
&& !self.should_replace_file(
|
||||||
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
opts.save_as.clone().unwrap_or_else(|| entry.name()),
|
||||||
)
|
)
|
||||||
@@ -107,16 +98,17 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
if self.config().get_prompt_on_file_replace() {
|
if self.config().get_prompt_on_file_replace() {
|
||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&File> = entries
|
let existing_files: Vec<&File> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|(x, dest_path)| {
|
||||||
self.remote_file_exists(
|
self.remote_file_exists(
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.map(|(x, _)| x)
|
||||||
.collect();
|
.collect();
|
||||||
// Check whether to replace files
|
// Check whether to replace files
|
||||||
if !existing_files.is_empty()
|
if !existing_files.is_empty()
|
||||||
@@ -126,7 +118,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::Many(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
@@ -143,11 +135,12 @@ impl FileTransferActivity {
|
|||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&File> = entries
|
let existing_files: Vec<&File> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|(x, dest_path)| {
|
||||||
self.local_file_exists(
|
self.host_bridge_file_exists(
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.map(|(x, _)| x)
|
||||||
.collect();
|
.collect();
|
||||||
// Check whether to replace files
|
// Check whether to replace files
|
||||||
if !existing_files.is_empty()
|
if !existing_files.is_empty()
|
||||||
@@ -157,7 +150,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::Many(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
@@ -166,6 +159,12 @@ impl FileTransferActivity {
|
|||||||
format!("Could not download file: {err}"),
|
format!("Could not download file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
if let Some(f) = self.found_mut() {
|
||||||
|
f.clear_queue();
|
||||||
|
self.update_find_list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +180,16 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Delete file
|
// Delete file
|
||||||
self.remove_found_file(entry);
|
self.remove_found_file(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
if let Some(f) = self.found_mut() {
|
||||||
|
f.clear_queue();
|
||||||
|
self.update_find_list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
@@ -192,7 +197,7 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn remove_found_file(&mut self, entry: &File) {
|
fn remove_found_file(&mut self, entry: &File) {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
self.local_remove_file(entry);
|
self.local_remove_file(entry);
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
@@ -209,10 +214,15 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Open file
|
// Open file
|
||||||
self.open_found_file(entry, None);
|
self.open_found_file(entry, None);
|
||||||
}
|
}
|
||||||
|
// clear selection
|
||||||
|
if let Some(f) = self.found_mut() {
|
||||||
|
f.clear_queue();
|
||||||
|
self.update_find_list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
@@ -226,10 +236,15 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, _) in entries.iter() {
|
||||||
// Open file
|
// Open file
|
||||||
self.open_found_file(entry, Some(with));
|
self.open_found_file(entry, Some(with));
|
||||||
}
|
}
|
||||||
|
// clear selection
|
||||||
|
if let Some(f) = self.found_mut() {
|
||||||
|
f.clear_queue();
|
||||||
|
self.update_find_list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
@@ -237,7 +252,7 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
fn open_found_file(&mut self, entry: &File, with: Option<&str>) {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
self.action_open_local_file(entry, with);
|
self.action_open_local_file(entry, with);
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
|||||||
19
src/ui/activities/filetransfer/actions/mark.rs
Normal file
19
src/ui/activities/filetransfer/actions/mark.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! ## FileTransferActivity
|
||||||
|
//!
|
||||||
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
|
use super::FileTransferActivity;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_mark_file(&mut self, index: usize) {
|
||||||
|
self.enqueue_file(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_mark_all(&mut self) {
|
||||||
|
self.enqueue_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_mark_clear(&mut self) {
|
||||||
|
self.clear_queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,10 @@ use super::{FileTransferActivity, LogLevel};
|
|||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
pub(crate) fn action_local_mkdir(&mut self, input: String) {
|
||||||
match self.host.mkdir(PathBuf::from(input.as_str()).as_path()) {
|
match self
|
||||||
|
.host_bridge
|
||||||
|
.mkdir(PathBuf::from(input.as_str()).as_path())
|
||||||
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Reload files
|
// Reload files
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
self.log(LogLevel::Info, format!("Created directory \"{input}\""));
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
//!
|
//!
|
||||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||||
|
|
||||||
use remotefs::fs::UnixPex;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use remotefs::File;
|
use remotefs::File;
|
||||||
|
use remotefs::fs::UnixPex;
|
||||||
use tuirealm::{State, StateValue};
|
use tuirealm::{State, StateValue};
|
||||||
|
|
||||||
use super::browser::FileExplorerTab;
|
use super::browser::FileExplorerTab;
|
||||||
|
use super::lib::browser::FoundExplorerTab;
|
||||||
use super::{
|
use super::{
|
||||||
FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, TransferMsg, TransferOpts,
|
FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, TransferMsg, TransferOpts,
|
||||||
TransferPayload, UiMsg,
|
TransferPayload, UiMsg,
|
||||||
};
|
};
|
||||||
|
use crate::explorer::FileExplorer;
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
pub(crate) mod change_dir;
|
pub(crate) mod change_dir;
|
||||||
@@ -21,20 +25,24 @@ pub(crate) mod edit;
|
|||||||
pub(crate) mod exec;
|
pub(crate) mod exec;
|
||||||
pub(crate) mod filter;
|
pub(crate) mod filter;
|
||||||
pub(crate) mod find;
|
pub(crate) mod find;
|
||||||
|
pub(crate) mod mark;
|
||||||
pub(crate) mod mkdir;
|
pub(crate) mod mkdir;
|
||||||
pub(crate) mod newfile;
|
pub(crate) mod newfile;
|
||||||
pub(crate) mod open;
|
pub(crate) mod open;
|
||||||
mod pending;
|
mod pending;
|
||||||
pub(crate) mod rename;
|
pub(crate) mod rename;
|
||||||
pub(crate) mod save;
|
pub(crate) mod save;
|
||||||
|
pub(crate) mod scan;
|
||||||
pub(crate) mod submit;
|
pub(crate) mod submit;
|
||||||
pub(crate) mod symlink;
|
pub(crate) mod symlink;
|
||||||
|
pub(crate) mod walkdir;
|
||||||
pub(crate) mod watcher;
|
pub(crate) mod watcher;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum SelectedFile {
|
pub(crate) enum SelectedFile {
|
||||||
One(File),
|
One(File),
|
||||||
Many(Vec<File>),
|
/// List of file with their destination path
|
||||||
|
Many(Vec<(File, PathBuf)>),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +51,10 @@ impl SelectedFile {
|
|||||||
/// In case is `Many` the first item mode is returned
|
/// In case is `Many` the first item mode is returned
|
||||||
pub fn unix_pex(&self) -> Option<UnixPex> {
|
pub fn unix_pex(&self) -> Option<UnixPex> {
|
||||||
match self {
|
match self {
|
||||||
Self::Many(files) => files.iter().next().and_then(|file| file.metadata().mode),
|
Self::Many(files) => files
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.and_then(|(file, _)| file.metadata().mode),
|
||||||
Self::One(file) => file.metadata().mode,
|
Self::One(file) => file.metadata().mode,
|
||||||
Self::None => None,
|
Self::None => None,
|
||||||
}
|
}
|
||||||
@@ -53,7 +64,7 @@ impl SelectedFile {
|
|||||||
pub fn get_files(self) -> Vec<File> {
|
pub fn get_files(self) -> Vec<File> {
|
||||||
match self {
|
match self {
|
||||||
Self::One(file) => vec![file],
|
Self::One(file) => vec![file],
|
||||||
Self::Many(files) => files,
|
Self::Many(files) => files.into_iter().map(|(f, _)| f).collect(),
|
||||||
Self::None => vec![],
|
Self::None => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +73,6 @@ impl SelectedFile {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum SelectedFileIndex {
|
enum SelectedFileIndex {
|
||||||
One(usize),
|
One(usize),
|
||||||
Many(Vec<usize>),
|
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,68 +85,42 @@ impl From<Option<&File>> for SelectedFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<&File>> for SelectedFile {
|
|
||||||
fn from(files: Vec<&File>) -> Self {
|
|
||||||
SelectedFile::Many(files.into_iter().cloned().collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// Get local file entry
|
/// Get local file entry
|
||||||
pub(crate) fn get_local_selected_entries(&self) -> SelectedFile {
|
pub(crate) fn get_local_selected_entries(&mut self) -> SelectedFile {
|
||||||
match self.get_selected_index(&Id::ExplorerLocal) {
|
self.get_selected_files(&Id::ExplorerHostBridge)
|
||||||
SelectedFileIndex::One(idx) => SelectedFile::from(self.local().get(idx)),
|
}
|
||||||
SelectedFileIndex::Many(files) => {
|
|
||||||
let files: Vec<&File> = files
|
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
|
||||||
.iter()
|
self.get_selected_file(&Id::ExplorerHostBridge)
|
||||||
.filter_map(|x| self.local().get(*x)) // Usize to Option<File>
|
|
||||||
.collect();
|
|
||||||
SelectedFile::from(files)
|
|
||||||
}
|
|
||||||
SelectedFileIndex::None => SelectedFile::None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_remote_selected_entries(&self) -> SelectedFile {
|
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
|
||||||
match self.get_selected_index(&Id::ExplorerRemote) {
|
self.get_selected_files(&Id::ExplorerRemote)
|
||||||
SelectedFileIndex::One(idx) => SelectedFile::from(self.remote().get(idx)),
|
}
|
||||||
SelectedFileIndex::Many(files) => {
|
|
||||||
let files: Vec<&File> = files
|
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
|
||||||
.iter()
|
self.get_selected_file(&Id::ExplorerRemote)
|
||||||
.filter_map(|x| self.remote().get(*x)) // Usize to Option<File>
|
|
||||||
.collect();
|
|
||||||
SelectedFile::from(files)
|
|
||||||
}
|
|
||||||
SelectedFileIndex::None => SelectedFile::None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether only one entry is selected on local host
|
/// Returns whether only one entry is selected on local host
|
||||||
pub(crate) fn is_local_selected_one(&self) -> bool {
|
pub(crate) fn is_local_selected_one(&mut self) -> bool {
|
||||||
matches!(self.get_local_selected_entries(), SelectedFile::One(_))
|
matches!(self.get_local_selected_entries(), SelectedFile::One(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether only one entry is selected on remote host
|
/// Returns whether only one entry is selected on remote host
|
||||||
pub(crate) fn is_remote_selected_one(&self) -> bool {
|
pub(crate) fn is_remote_selected_one(&mut self) -> bool {
|
||||||
matches!(self.get_remote_selected_entries(), SelectedFile::One(_))
|
matches!(self.get_remote_selected_entries(), SelectedFile::One(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_found_selected_entries(&self) -> SelectedFile {
|
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
|
||||||
match self.get_selected_index(&Id::ExplorerFind) {
|
self.get_selected_files(&Id::ExplorerFind)
|
||||||
SelectedFileIndex::One(idx) => {
|
}
|
||||||
SelectedFile::from(self.found().as_ref().unwrap().get(idx))
|
|
||||||
}
|
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
|
||||||
SelectedFileIndex::Many(files) => {
|
self.get_selected_file(&Id::ExplorerFind)
|
||||||
let files: Vec<&File> = files
|
|
||||||
.iter()
|
|
||||||
.filter_map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<File>
|
|
||||||
.collect();
|
|
||||||
SelectedFile::from(files)
|
|
||||||
}
|
|
||||||
SelectedFileIndex::None => SelectedFile::None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- private
|
// -- private
|
||||||
@@ -144,17 +128,69 @@ impl FileTransferActivity {
|
|||||||
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
|
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
|
||||||
match self.app.state(id) {
|
match self.app.state(id) {
|
||||||
Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
|
Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
|
||||||
Ok(State::Vec(files)) => {
|
|
||||||
let list: Vec<usize> = files
|
|
||||||
.iter()
|
|
||||||
.map(|x| match x {
|
|
||||||
StateValue::Usize(v) => *v,
|
|
||||||
_ => 0,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
SelectedFileIndex::Many(list)
|
|
||||||
}
|
|
||||||
_ => SelectedFileIndex::None,
|
_ => SelectedFileIndex::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_selected_files(&mut self, id: &Id) -> SelectedFile {
|
||||||
|
let browser = self.browser_by_id(id);
|
||||||
|
// if transfer queue is not empty, return that
|
||||||
|
let transfer_queue = browser.enqueued().clone();
|
||||||
|
if !transfer_queue.is_empty() {
|
||||||
|
return SelectedFile::Many(
|
||||||
|
transfer_queue
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(src, dest)| {
|
||||||
|
let src_file = self.get_file_from_path(id, src)?;
|
||||||
|
Some((src_file, dest.clone()))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let browser = self.browser_by_id(id);
|
||||||
|
// if no transfer queue, return selected files
|
||||||
|
match self.get_selected_index(id) {
|
||||||
|
SelectedFileIndex::One(idx) => {
|
||||||
|
let Some(f) = browser.get(idx) else {
|
||||||
|
return SelectedFile::None;
|
||||||
|
};
|
||||||
|
SelectedFile::One(f.clone())
|
||||||
|
}
|
||||||
|
SelectedFileIndex::None => SelectedFile::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option<File> {
|
||||||
|
match *id {
|
||||||
|
Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(),
|
||||||
|
Id::ExplorerRemote => self.client.stat(path).ok(),
|
||||||
|
Id::ExplorerFind => {
|
||||||
|
let found = self.browser.found_tab().unwrap();
|
||||||
|
match found {
|
||||||
|
FoundExplorerTab::Local => self.host_bridge.stat(path).ok(),
|
||||||
|
FoundExplorerTab::Remote => self.client.stat(path).ok(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browser_by_id(&self, id: &Id) -> &FileExplorer {
|
||||||
|
match *id {
|
||||||
|
Id::ExplorerHostBridge => self.host_bridge(),
|
||||||
|
Id::ExplorerRemote => self.remote(),
|
||||||
|
Id::ExplorerFind => self.found().as_ref().unwrap(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_selected_file(&self, id: &Id) -> Option<File> {
|
||||||
|
let browser = self.browser_by_id(id);
|
||||||
|
// if no transfer queue, return selected files
|
||||||
|
match self.get_selected_index(id) {
|
||||||
|
SelectedFileIndex::One(idx) => browser.get(idx).cloned(),
|
||||||
|
SelectedFileIndex::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
use std::fs::File as StdFile;
|
use std::fs::File as StdFile;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use remotefs::fs::Metadata;
|
||||||
|
|
||||||
use super::{File, FileTransferActivity, LogLevel};
|
use super::{File, FileTransferActivity, LogLevel};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_newfile(&mut self, input: String) {
|
pub(crate) fn action_local_newfile(&mut self, input: String) {
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
let mut file_exists: bool = false;
|
let mut file_exists: bool = false;
|
||||||
for file in self.local().iter_files_all() {
|
for file in self.host_bridge().iter_files_all() {
|
||||||
if input == file.name() {
|
if input == file.name() {
|
||||||
file_exists = true;
|
file_exists = true;
|
||||||
}
|
}
|
||||||
@@ -21,19 +23,35 @@ impl FileTransferActivity {
|
|||||||
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
|
self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file
|
// Create file
|
||||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||||
if let Err(err) = self.host.open_file_write(file_path.as_path()) {
|
let writer = match self
|
||||||
|
.host_bridge
|
||||||
|
.create_file(file_path.as_path(), &Metadata::default())
|
||||||
|
{
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// finalize write
|
||||||
|
if let Err(err) = self.host_bridge.finalize_write(writer) {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
format!("Could not write file \"{}\": {}", file_path.display(), err),
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Created file \"{}\"", file_path.display()),
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Created file \"{}\"", file_path.display()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
||||||
@@ -57,7 +75,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
Ok(tfile) => {
|
Ok(tfile) => {
|
||||||
// Stat tempfile
|
// Stat tempfile
|
||||||
let local_file: File = match self.host.stat(tfile.path()) {
|
let local_file: File = match self.host_bridge.stat(tfile.path()) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
|
|||||||
@@ -13,29 +13,41 @@ impl FileTransferActivity {
|
|||||||
pub(crate) fn action_open_local(&mut self) {
|
pub(crate) fn action_open_local(&mut self) {
|
||||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
let entries: Vec<File> = match self.get_local_selected_entries() {
|
||||||
SelectedFile::One(entry) => vec![entry],
|
SelectedFile::One(entry) => vec![entry],
|
||||||
SelectedFile::Many(entries) => entries,
|
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||||
SelectedFile::None => vec![],
|
SelectedFile::None => vec![],
|
||||||
};
|
};
|
||||||
entries
|
entries
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| self.action_open_local_file(x, None));
|
.for_each(|x| self.action_open_local_file(x, None));
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.host_bridge_mut().clear_queue();
|
||||||
|
self.reload_host_bridge_filelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open local file
|
/// Open local file
|
||||||
pub(crate) fn action_open_remote(&mut self) {
|
pub(crate) fn action_open_remote(&mut self) {
|
||||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
||||||
SelectedFile::One(entry) => vec![entry],
|
SelectedFile::One(entry) => vec![entry],
|
||||||
SelectedFile::Many(entries) => entries,
|
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||||
SelectedFile::None => vec![],
|
SelectedFile::None => vec![],
|
||||||
};
|
};
|
||||||
entries
|
entries
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| self.action_open_remote_file(x, None));
|
.for_each(|x| self.action_open_remote_file(x, None));
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform open lopcal file
|
/// Perform open lopcal file
|
||||||
pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) {
|
pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||||
self.open_path_with(entry.path(), open_with);
|
if self.host_bridge.is_localhost() {
|
||||||
|
self.open_path_with(entry.path(), open_with);
|
||||||
|
} else {
|
||||||
|
self.open_bridged_file(entry, open_with);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
||||||
@@ -82,26 +94,84 @@ impl FileTransferActivity {
|
|||||||
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
||||||
let entries: Vec<File> = match self.get_local_selected_entries() {
|
let entries: Vec<File> = match self.get_local_selected_entries() {
|
||||||
SelectedFile::One(entry) => vec![entry],
|
SelectedFile::One(entry) => vec![entry],
|
||||||
SelectedFile::Many(entries) => entries,
|
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||||
SelectedFile::None => vec![],
|
SelectedFile::None => vec![],
|
||||||
};
|
};
|
||||||
// Open all entries
|
// Open all entries
|
||||||
entries
|
entries
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| self.action_open_local_file(x, Some(with)));
|
.for_each(|x| self.action_open_local_file(x, Some(with)));
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.host_bridge_mut().clear_queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open selected file with provided application
|
/// Open selected file with provided application
|
||||||
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
||||||
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
let entries: Vec<File> = match self.get_remote_selected_entries() {
|
||||||
SelectedFile::One(entry) => vec![entry],
|
SelectedFile::One(entry) => vec![entry],
|
||||||
SelectedFile::Many(entries) => entries,
|
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
|
||||||
SelectedFile::None => vec![],
|
SelectedFile::None => vec![],
|
||||||
};
|
};
|
||||||
// Open all entries
|
// Open all entries
|
||||||
entries
|
entries
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| self.action_open_remote_file(x, Some(with)));
|
.for_each(|x| self.action_open_remote_file(x, Some(with)));
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
self.reload_remote_filelist();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) {
|
||||||
|
// Download file
|
||||||
|
let tmpfile: String =
|
||||||
|
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
|
||||||
|
None => {
|
||||||
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(p) => p,
|
||||||
|
};
|
||||||
|
let cache: PathBuf = match self.cache.as_ref() {
|
||||||
|
None => {
|
||||||
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(p) => p.path().to_path_buf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tmpfile = cache.join(tmpfile);
|
||||||
|
|
||||||
|
// open from host bridge
|
||||||
|
let mut reader = match self.host_bridge.open_file(entry.path()) {
|
||||||
|
Ok(reader) => reader,
|
||||||
|
Err(err) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!("Failed to open bridged entry: {err}"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// write to file
|
||||||
|
let mut writer = match std::fs::File::create(tmpfile.as_path()) {
|
||||||
|
Ok(writer) => writer,
|
||||||
|
Err(err) => {
|
||||||
|
self.log(LogLevel::Error, format!("Failed to create file: {err}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
|
||||||
|
self.log(LogLevel::Error, format!("Failed to write file: {err}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpfile.exists() {
|
||||||
|
self.open_path_with(tmpfile.as_path(), open_with);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Common function which opens a path with default or specified program.
|
/// Common function which opens a path with default or specified program.
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Try to copy each file to Input/{FILE_NAME}
|
// Try to copy each file to Input/{FILE_NAME}
|
||||||
let base_path: PathBuf = PathBuf::from(input);
|
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
|
||||||
dest_path.push(entry.name());
|
dest_path.push(entry.name());
|
||||||
self.local_rename_file(entry, dest_path.as_path());
|
self.local_rename_file(&entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.host_bridge_mut().clear_queue();
|
||||||
|
self.reload_host_bridge_filelist();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
@@ -38,20 +40,23 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
SelectedFile::Many(entries) => {
|
SelectedFile::Many(entries) => {
|
||||||
// Try to copy each file to Input/{FILE_NAME}
|
// Try to copy each file to Input/{FILE_NAME}
|
||||||
let base_path: PathBuf = PathBuf::from(input);
|
|
||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for (entry, mut dest_path) in entries.into_iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
|
||||||
dest_path.push(entry.name());
|
dest_path.push(entry.name());
|
||||||
self.remote_rename_file(entry, dest_path.as_path());
|
self.remote_rename_file(&entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
// reload remote
|
||||||
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_rename_file(&mut self, entry: &File, dest: &Path) {
|
fn local_rename_file(&mut self, entry: &File, dest: &Path) {
|
||||||
match self.host.rename(entry, dest) {
|
match self.host_bridge.rename(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
|
|||||||
@@ -64,11 +64,12 @@ impl FileTransferActivity {
|
|||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&File> = entries
|
let existing_files: Vec<&File> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|(x, dest_path)| {
|
||||||
self.remote_file_exists(
|
self.remote_file_exists(
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.map(|(x, _)| x)
|
||||||
.collect();
|
.collect();
|
||||||
// Check whether to replace files
|
// Check whether to replace files
|
||||||
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
||||||
@@ -76,7 +77,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::Many(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
@@ -86,6 +87,10 @@ impl FileTransferActivity {
|
|||||||
format!("Could not upload file: {err}"),
|
format!("Could not upload file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// clear selection
|
||||||
|
self.host_bridge_mut().clear_queue();
|
||||||
|
self.reload_host_bridge_filelist();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
@@ -93,12 +98,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remote_recv_file(&mut self, opts: TransferOpts) {
|
fn remote_recv_file(&mut self, opts: TransferOpts) {
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
let wrkdir: PathBuf = self.host_bridge().wrkdir.clone();
|
||||||
match self.get_remote_selected_entries() {
|
match self.get_remote_selected_entries() {
|
||||||
SelectedFile::One(entry) => {
|
SelectedFile::One(entry) => {
|
||||||
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
|
||||||
if self.config().get_prompt_on_file_replace()
|
if self.config().get_prompt_on_file_replace()
|
||||||
&& self.local_file_exists(file_to_check.as_path())
|
&& self.host_bridge_file_exists(file_to_check.as_path())
|
||||||
&& !self
|
&& !self
|
||||||
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
.should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name()))
|
||||||
{
|
{
|
||||||
@@ -128,11 +133,12 @@ impl FileTransferActivity {
|
|||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&File> = entries
|
let existing_files: Vec<&File> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|(x, dest_path)| {
|
||||||
self.local_file_exists(
|
self.host_bridge_file_exists(
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.map(|(x, _)| x)
|
||||||
.collect();
|
.collect();
|
||||||
// Check whether to replace files
|
// Check whether to replace files
|
||||||
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
||||||
@@ -140,7 +146,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::Many(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
@@ -150,6 +156,11 @@ impl FileTransferActivity {
|
|||||||
format!("Could not download file: {err}"),
|
format!("Could not download file: {err}"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// clear selection
|
||||||
|
self.remote_mut().clear_queue();
|
||||||
|
// reload remote
|
||||||
|
self.reload_remote_filelist();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectedFile::None => {}
|
SelectedFile::None => {}
|
||||||
|
|||||||
19
src/ui/activities/filetransfer/actions/scan.rs
Normal file
19
src/ui/activities/filetransfer/actions/scan.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::{File, FileTransferActivity};
|
||||||
|
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
|
||||||
|
|
||||||
|
impl FileTransferActivity {
|
||||||
|
pub(crate) fn action_scan(&mut self, p: &Path) -> Result<Vec<File>, String> {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self
|
||||||
|
.host_bridge
|
||||||
|
.list_dir(p)
|
||||||
|
.map_err(|e| format!("Failed to list directory: {}", e)),
|
||||||
|
FileExplorerTab::Remote | FileExplorerTab::FindRemote => self
|
||||||
|
.client
|
||||||
|
.list_dir(p)
|
||||||
|
.map_err(|e| format!("Failed to list directory: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ impl FileTransferActivity {
|
|||||||
} else if entry.metadata().symlink.is_some() {
|
} else if entry.metadata().symlink.is_some() {
|
||||||
// Stat file
|
// Stat file
|
||||||
let symlink = entry.metadata().symlink.as_ref().unwrap();
|
let symlink = entry.metadata().symlink.as_ref().unwrap();
|
||||||
let stat_file = match self.host.stat(symlink.as_path()) {
|
let stat_file = match self.host_bridge.stat(symlink.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user