58 Commits

Author SHA1 Message Date
veeso
5f7a0d8a46 fix: install.sh deb name
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-12-02 14:27:34 +01:00
veeso
694232564a fix: install.sh deb name 2025-12-02 14:25:55 +01:00
veeso
54b674ad43 ci: windows artifact name
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
Windows / build (push) Has been cancelled
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-11-11 12:34:36 +01:00
veeso
c32822037e ci: deploy site 2025-11-11 12:21:05 +01:00
veeso
abb5c212c5 feat: Merge branch '0.19.0' 2025-11-11 12:19:21 +01:00
veeso
e9b54a227b chore: CHANGELOG date 2025-11-11 09:42:21 +01:00
veeso
befc32198a ci: debian fix
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2025-11-10 17:25:29 +01:00
veeso
7e5103ff7e ci: Debian 2025-11-10 17:06:44 +01:00
veeso
2cb600083e docs: Release date 2025-11-10 16:44:24 +01:00
Christian Visintin
47d23673e6 ci: Build artifacts for Windows x86_64 and Ubuntu x86_64 (#368) 2025-11-10 16:43:25 +01:00
Christian Visintin
a0b357cf8c feat: Added <CTRL+S> keybinding to get the total size of selected paths. (#367)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
* feat: Added `<CTRL+S>` keybinding to get the total size of selected paths.

closes #297
2025-11-09 21:14:42 +01:00
Christian Visintin
75943f2b93 feat: Changed file overwrite behaviour (#366)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.

closes #335
2025-11-09 19:00:17 +01:00
veeso
085ab721f9 build: remotefs-ssh 0.7.1
This version fixes compatibility with hosts which don't use bash/sh as the default shell.

closes #365
2025-11-09 17:38:50 +01:00
Christian Visintin
f4156a5059 feat: Import bookmarks from ssh config with a CLI command (#364)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
* feat: Import bookmarks from ssh config with a CLI command

Use import-ssh-hosts to import all the possible hosts by the configured ssh config or the default one on your machine

closes #331
2025-11-08 15:32:52 +01:00
Christian Visintin
4bebec369f fix: Issues with update checks (#363)
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Removed error popup message if failed to check for updates.
Prevent long timeouts when checking for updates if the network is down or the DNS is not working.

closes #354
2025-10-02 21:27:51 +02:00
Christian Visintin
05c8613279 fix: Report a message while calculating total size of files to transfer. (#362)
* fix: Report a message while calculating total size of files to transfer.

Currently, in case of huge transfers the app may look frozen while calculating the transfer size. We should at least report to the user we are actually doing something.

closes #361

* ci: windows runner
2025-10-02 20:58:26 +02:00
veeso
205d2813ad perf: Migrated to libssh.org on Linux and MacOS for better ssh agent support.
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
closes #337
2025-09-20 18:13:20 +02:00
veeso
86660a0cc9 fix: SMB support for MacOS with vendored build of libsmbclient.
closes #334
2025-09-20 18:07:26 +02:00
veeso
05830db206 docs: User manual and get started links
Some checks failed
Install.sh / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-09-16 10:36:17 +02:00
veeso
3c79e812eb build: 0.19 deps 2025-09-06 16:40:01 +02:00
moshyfawn
0287e7706a fix: typo in file open error message (#349)
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-06-13 22:50:06 +02:00
veeso
67a14c2725 fix: lock
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2025-06-10 21:20:04 +02:00
veeso
df03c5c1bf feat: 0.18
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2025-06-10 14:29:02 +02:00
veeso
3ce3ffee3d fix: larger file info popup 2025-06-10 14:26:36 +02:00
Christian Visintin
c0b32a1847 feat: Replaced the Exec popup with a fully functional terminal emulator (#348)
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
* feat: Replaced the `Exec` popup with a fully functional terminal emulator

closes #340

* fix: colors and fmt for terminal

* feat: Handle exit and cd on terminal

* fix: Fmt pah
2025-06-10 13:17:20 +02:00
Christian Visintin
81ae0035c3 Fix SSH auth with id keys (#347)
I fixed the id_rsa/id_ed25519 SSH auth issue on Mac and now termscp should respect the key-based authentication, just like the regular ssh user@hostname, without the need for any ssh agents.

---

Co-authored-by: Lucas Czekaj <lukasz@czekaj.us>
2025-06-08 18:34:59 +02:00
veeso
783da22ca2 feat: **Updated dependencies** and updated the Rust edition to 2024 2025-06-08 18:00:42 +02:00
veeso
8715c2b6f9 chore: CODE_OF_CONDUCT update
Some checks failed
Install.sh / build (push) Has been cancelled
2025-05-10 19:23:06 +02:00
veeso
98a748dccc style: catppuccin themes
Some checks failed
Install.sh / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Linux / build (push) Has been cancelled
2025-04-15 12:24:08 +02:00
Christian Visintin
bef031a414 Update website.yml
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2025-03-23 22:01:18 +01:00
veeso
ce0e953182 chore: site 2025-03-23 18:13:05 +01:00
veeso
9a5caf75c3 build: so apparently native-tls vendored tries to build openssl on windows, wtf guys?
Some checks failed
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2025-03-23 18:07:20 +01:00
veeso
446f4a3a32 build: build docker for x86 2025-03-23 16:43:51 +01:00
veeso
da75912d26 docs: version
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
2025-03-23 15:59:50 +01:00
veeso
4ed23c2a18 build: aws-s3 0.4.2 2025-03-23 15:58:25 +01:00
Christian Visintin
23ae334bef ci(build): build vendored smb and refactor platform deps (#333) 2025-03-23 15:57:54 +01:00
Christian Visintin
ec75ae1486 feat: 132 queuing transfers (#332)
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` 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`.

closes #132
2025-03-23 14:36:13 +01:00
veeso
368570592f feat(cli): added --wno-keyring flag to disable keyring
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
closes #308
2025-03-22 13:44:02 +01:00
veeso
806793421e fix(bookmarks): Local directory path is not switching to what's specified in the bookmark
closes #316
2025-03-22 13:25:29 +01:00
veeso
1f377b242d fix(log): add suppaftp/pavao/kube to allowed logs
Some checks failed
Windows / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
fix #330
2025-03-17 18:59:47 +01:00
veeso
a4906b129a docs: veeso.me instead of veeso.dev
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
2025-03-17 09:31:44 +01:00
veeso
5c6e8925ad test(remotefs_builder): check result, build doesn't panic anymore 2025-03-17 09:30:33 +01:00
veeso
a18eff689d fix(aws-s3): updated remotefs-aws-s3 to 0.4.1
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
should fix #329
2025-03-16 20:06:35 +01:00
veeso
274742d6d9 build: bump to ssh2-config 0.4 and remotefs 0.6 to have support for Include in config files
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
fix #309
2025-03-15 20:04:21 +01:00
veeso
cdebcbd4dc fix: the return value of --version should be 0
fix #317
2025-03-15 16:54:36 +01:00
veeso
cdf303a847 fix: fixed a crash when the local directory specified in the auth form does not exist
fix #319
2025-03-15 16:46:57 +01:00
veeso
dd35fe825c fix(ui): fixed input mask on host bridge on local dir up
if you go up on local dir when localhost is selected it panics

fix #327
2025-03-15 14:24:24 +01:00
veeso
5c4a971aca docs(CONTRIBUTING): docs(CONTRIBUTING): mistakes
fix #318
2025-03-15 14:19:18 +01:00
veeso
7522b4d0ff chore(changelog): changelog 2025-03-15 14:16:18 +01:00
veeso
b0f314837e build(deps): updated dependencies and edition to 2024 2025-03-15 14:15:45 +01:00
Christian Visintin
8a9ba7745a Merge pull request #325 from eggplants/fix-clippy-error
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
fix: clippy error
2025-01-06 12:53:13 +01:00
haruna
b7d75a2749 fix: clippy error 2025-01-05 22:52:45 +09:00
Christian Visintin
fe0d9b0aa6 Update stale.yml
Some checks failed
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
2024-12-28 13:22:03 +01:00
veeso
7dba691ccc fix: isolated-tests for localhost
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2024-11-13 17:06:34 +01:00
veeso
099e2154ba fix: unused import isolated tests 2024-11-13 15:57:49 +01:00
veeso
f2efb25ad7 fix: 0.16.1
Some checks are pending
Install.sh / build (push) Waiting to run
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
2024-11-12 12:34:26 +01:00
veeso
e45c3d5b4e fix: gg rust 1.82 for introducing a nice breaking change in config which was not mentioned in changelog
Some checks failed
Install.sh / build (push) Has been cancelled
Linux / build (push) Has been cancelled
MacOS / build (push) Has been cancelled
Windows / build (push) Has been cancelled
2024-10-21 11:09:50 +02:00
veeso
69f821baef fix: cfg unix forbidden in rust .82
Some checks are pending
Install.sh / build (push) Waiting to run
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run
2024-10-21 10:32:50 +02:00
135 changed files with 8877 additions and 4715 deletions

View File

@@ -3,6 +3,9 @@ name: "Build artifacts"
on:
workflow_dispatch:
env:
TERMSCP_VERSION: "0.19.0"
jobs:
build-binaries:
name: Build - ${{ matrix.platform.release_for }}
@@ -11,13 +14,24 @@ jobs:
platform:
- release_for: MacOS-x86_64
os: macos-latest
platform: macos
target: x86_64-apple-darwin
script: macos.sh
- release_for: MacOS-M1
- release_for: MacOS-aarch64
os: macos-latest
platform: macos
target: aarch64-apple-darwin
script: macos.sh
- release_for: Linux-x86_64
os: ubuntu-latest
platform: linux
target: x86_64-unknown-linux-gnu
debian_suffix: amd64
- release_for: Windows-x86_64
os: windows-latest
platform: windows
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform.os }}
steps:
@@ -26,17 +40,122 @@ jobs:
with:
toolchain: stable
targets: ${{ matrix.platform.target }}
- name: Build release
run: cargo build --release --target ${{ matrix.platform.target }}
- name: Prepare artifact files
- name: Install dependencies (Linux)
if: matrix.platform.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y \
make \
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 \
libdbus-1-dev \
cpanminus;
sudo cpanm Parse::Yapp::Driver
- name: Install dependencies (MacOS)
if: matrix.platform.platform == 'macos'
run: |
brew update
brew install \
bison \
cpanminus \
cups \
flex \
gettext \
gmp \
gnutls \
icu4c \
jansson \
libarchive \
libbsd \
libunistring \
libgit2 \
libtirpc \
openldap \
pkg-config \
zlib
brew link --force bison
brew link --force cups
brew link --force flex
brew link --force gettext
brew link --force gmp
brew link --force gnutls
brew link --force icu4c
brew link --force jansson
brew link --force libarchive
brew link --force libbsd
brew link --force libgit2
brew link --force libtirpc
brew link --force libunistring
brew link --force openldap
brew link --force zlib
cpanm Parse::Yapp::Driver
- name: Build release (MacOS Intel)
if: matrix.platform.target == 'x86_64-apple-darwin'
run: cargo build --release --no-default-features --features keyring --target ${{ matrix.platform.target }}
- name: Build release (others)
if: matrix.platform.target != 'x86_64-apple-darwin'
run: cargo build --release --features smb-vendored --target ${{ matrix.platform.target }}
- name: Build deb
if: matrix.platform.platform == 'linux'
run: |
cargo install cargo-deb
cargo deb --target ${{ matrix.platform.target }} --features smb-vendored
- name: Prepare artifact files (Posix)
if: matrix.platform.platform != 'windows'
run: |
mkdir -p .artifact
mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp
tar -czf .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz -C .artifact termscp
ls -l .artifact/
- name: "Upload artifact"
- name: Upload artifact (Posix)
if: matrix.platform.platform != 'windows'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: ${{ matrix.platform.release_for }}
path: .artifact/termscp
name: termscp-${{ matrix.platform.target }}
path: .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz
- name: Upload artifact (Windows)
if: matrix.platform.platform == 'windows'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}
path: target/${{ matrix.platform.target }}/release/termscp.exe
- name: Upload artifact (Deb)
if: matrix.platform.platform == 'linux'
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
retention-days: 1
name: termscp-${{ matrix.platform.target }}-deb
path: target/debian/termscp_${{ env.TERMSCP_VERSION }}-1_${{ matrix.platform.debian_suffix }}.deb

View File

@@ -22,6 +22,13 @@ jobs:
with:
toolchain: stable
components: rustfmt, clippy
- name: Install dependencies
run: |
brew update
brew install \
pkg-config \
samba
brew link --force samba
- name: Build
run: cargo build
- name: Run tests

View File

@@ -21,3 +21,4 @@ jobs:
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
exempt-issue-labels: "backlog"
exempt-all-milestones: true

View File

@@ -6,7 +6,8 @@ on:
push:
branches: ["main"]
paths:
- "./site/**/*"
- ".github/workflows/website.yml"
- "site/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -33,11 +34,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v2
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: "./site/"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4

View File

@@ -15,7 +15,7 @@ env:
jobs:
build:
runs-on: windows-2019
runs-on: windows-latest
steps:
- uses: actions/checkout@v2

View File

@@ -1,6 +1,10 @@
# Changelog
- [Changelog](#changelog)
- [0.19.0](#0190)
- [0.18.0](#0180)
- [0.17.0](#0170)
- [0.16.1](#0161)
- [0.16.0](#0160)
- [0.15.0](#0150)
- [0.14.0](#0140)
@@ -38,6 +42,70 @@
---
## 0.19.0
Released on 11/11/2025
- [Issue 297](https://github.com/veeso/termscp/issues/297): Added `<CTRL+S>` keybinding to get the total size of selected paths.
- [Issue 331](https://github.com/veeso/termscp/issues/331): Added new `import-ssh-hosts` CLI subcommand to import all the hosts from the ssh config as bookmarks.
- [Issue 335](https://github.com/veeso/termscp/issues/335): Changed file overwrite behaviour
- Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.
- [Issue 354](https://github.com/veeso/termscp/issues/354):
- Removed error popup message if failed to check for updates.
- Prevent long timeouts when checking for updates if the network is down or the DNS is not working.
- [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found.
- [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient.
- [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support.
- [Issue 361](https://github.com/veeso/termscp/issues/361): Report a message while calculating total size of files to transfer.
## 0.18.0
Released on 11/11/2025
- 🐚 An **Embedded shell for termscp**:
- [Issue 340](https://github.com/veeso/termscp/issues/340): Replaced the `Exec` popup with a **fully functional terminal emulator** embedded thanks to [A-Kenji's tui-term](https://github.com/a-kenji/tui-term).
- Command History
- Support for `cd` and `exit` commands as well.
- Exit just closes the terminal emulator.
- [Issue 345](https://github.com/veeso/termscp/issues/345): Default keys are used from `~/.ssh` directory if no keys are resolved for the host.
- **Updated dependencies** and updated the Rust edition to `2024`
## 0.17.0
Released on 24/03/2025
- **Queuing transfers**:
- the logic of selecting files has been extended!
- From now on selecting file will put the files into a **transfer queue**, which is shown on the bottom panel.
- When a file is selected the file is added to the queue with a destination path, which is the **current other explorer path at the moment of selection.**
- It is possible to navigate to the transfer queue by using `P` and pressing `ENTER` or `DELETE` on a file will remove it from the transfer queue.
- Other commands will work as well on the transfer queue, like `COPY`, `MOVE`, `DELETE`, `RENAME`.
- [issue 308](https://github.com/veeso/termscp/issues/308): added `--wno-keyring` flag to disable keyring
- [issue 316](https://github.com/veeso/termscp/issues/316): Local directory path is not switching to what's specified in the bookmark. Now the local directory path is correctly set following this hierarchy:
1. Local directory path specified for the host bridge
2. Local directory path specified in the bookmark
3. Working directory
- [issue 317](https://github.com/veeso/termscp/issues/317): the return value of `--version` should be `0`
- [issue 319](https://github.com/veeso/termscp/issues/319): fixed a crash when the local directory specified in the auth form does not exist
- [issue 327](https://github.com/veeso/termscp/issues/327): fixed a panic when trying to go up from local directory on localhost in the auth form
- [issue 330](https://github.com/veeso/termscp/issues/330): add suppaftp/pavao/kube to allowed logs
- Dependencies:
- `argh` to `0.1.13`
- `bytesize` to `2`
- `dirs` to `6`
- `magic-crypt` to `4`
- `notify` to `8`
- `ssh2-config` to `0.4`
- `remotefs-ssh` to `0.6`
- `rust` edition to `2024`
## 0.16.1
Released on 12/11/2024
- Just fixed this: e45c3d5b4ef64653e5b6cc4f3703e3b67514306d
- `fix: gg rust 1.82 for introducing a nice breaking change in config which was not mentioned in changelog`
## 0.16.0
Released on 14/10/2024

View File

@@ -2,75 +2,131 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
Examples of behavior that contributes to a positive environment for our
community include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior by participants include:
Examples of unacceptable behavior include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
professional setting
## Our Responsibilities
## Enforcement Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at christian.visintin1997@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,7 +1,7 @@
# Contributing
Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change.
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
Please note we have a [code of conduct](CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project.
- [Contributing](#contributing)
- [Project mission](#project-mission)
@@ -20,9 +20,13 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
## Project mission
termscp was born because, as a terminal lover and Linux user, I wanted something like WinSCP on Linux and on terminal. I my previous job I used SFTP/SCP pratically everyday and that made me to desire an application like termscp so much, that eventually I started to work on it in the spare time. I saw there was a very cool library to create terminal user interface (`tui-rs`), so I started to code it. I wrote termscp as an experiment, I designed kinda nothing at the time. I just said
termscp was born because, as a terminal lover and Linux user, I wanted something like WinSCP on Linux and on terminal. At my previous job, I used SFTP/SCP practically everyday and that made me desire an application like termscp so much that eventually I started to work on it in my spare time.
> Ok, there must be a `FileTransfer` trait somehow, I'll have more views, so I'll use something like Android activities, and there must be a module to interact with the local host".
I saw there was a very cool library to create terminal user interfaces (`tui-rs`, now `ratatui`), so I started to code it.
I wrote termscp as an experiment. I didn't design anything at the time. I just said,
> "Ok, there must be a FileTransfer trait somehow. I'll have more views, so I'll use something like Android activities, and there must be a module to interact with the local host."
And so in december 2020 I had the first version of termscp running and it worked, but was very simple, raw and minimal.
A lot of things have changed since them, both the features the project provides and my personal view of this project.

3599
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
categories = ["command-line-utilities"]
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
edition = "2021"
edition = "2024"
homepage = "https://termscp.veeso.dev"
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
@@ -10,7 +10,8 @@ license = "MIT"
name = "termscp"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
version = "0.16.0"
version = "0.19.0"
rust-version = "1.88.0"
[package.metadata.rpm]
package = "termscp"
@@ -23,7 +24,7 @@ termscp = { path = "/usr/bin/termscp" }
[package.metadata.deb]
maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
copyright = "2022, Christian Visintin <christian.visintin@veeso.dev>"
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
extended-description-file = "docs/misc/README.deb.txt"
[[bin]]
@@ -33,37 +34,36 @@ path = "src/main.rs"
[dependencies]
argh = "^0.1"
bitflags = "^2"
bytesize = "^1"
bytesize = "^2"
chrono = "^0.4"
content_inspector = "^0.2"
dirs = "^5.0"
dirs = "^6"
edit = "^0.1"
filetime = "^0.2"
hostname = "^0.4"
keyring = { version = "^3", optional = true, features = [
keyring = { version = "^3", features = [
"apple-native",
"windows-native",
"sync-secret-service",
"vendored",
] }
lazy-regex = "^3"
lazy_static = "^1"
log = "^0.4"
magic-crypt = "^3"
notify = "6"
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
magic-crypt = "4"
notify = "8"
notify-rust = { version = "^4", default-features = false, features = ["d"] }
nucleo = "0.5"
open = "^5.0"
rand = "^0.8.5"
rand = "^0.9"
regex = "^1"
remotefs = "^0.3"
remotefs-aws-s3 = { version = "^0.3", default-features = false, features = [
"find",
"rustls",
] }
remotefs-aws-s3 = "0.4"
remotefs-kube = "0.4"
remotefs-smb = { version = "^0.3", optional = true }
remotefs-webdav = "^0.2"
rpassword = "^7"
self_update = { version = "^0.41", default-features = false, features = [
self_update = { version = "^0.42", default-features = false, features = [
"rustls",
"archive-tar",
"archive-zip",
@@ -71,19 +71,36 @@ self_update = { version = "^0.41", default-features = false, features = [
"compression-zip-deflate",
] }
serde = { version = "^1", features = ["derive"] }
shellexpand = "3"
simplelog = "^0.12"
ssh2-config = "^0.2"
tempfile = "^3"
thiserror = "^1"
tokio = { version = "=1.38.1", features = ["rt"] }
toml = "^0.8"
tui-realm-stdlib = "2"
tuirealm = "2"
ssh2-config = "^0.6"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["rt"] }
toml = "^0.9"
tui-realm-stdlib = "3"
tuirealm = "3"
tui-term = "0.2"
unicode-width = "^0.2"
version-compare = "^0.2"
whoami = "^1.5"
whoami = "^1.6"
wildmatch = "^2"
[target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "^0.3", features = [
"native-tls-vendored",
"native-tls",
] }
remotefs-ssh = { version = "^0.7", default-features = false, features = [
"find",
"libssh-vendored",
] }
uzers = "0.12"
[target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "^0.3", features = ["native-tls"] }
remotefs-ssh = { version = "^0.7" }
[dev-dependencies]
pretty_assertions = "^1"
serial_test = "^3"
@@ -92,27 +109,13 @@ serial_test = "^3"
cfg_aliases = "0.2"
vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] }
[features]
default = ["smb", "with-keyring"]
default = ["smb", "keyring"]
github-actions = []
isolated-tests = []
smb = ["remotefs-smb"]
with-keyring = ["keyring"]
[target."cfg(not(target_os = \"macos\"))".dependencies]
remotefs-smb = { version = "^0.3", optional = true }
[target."cfg(target_family = \"windows\")"]
[target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "^0.2", features = ["native-tls"] }
remotefs-ssh = "^0.4"
[target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies]
remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] }
remotefs-ssh = { version = "^0.4", features = ["ssh2-vendored"] }
uzers = "0.12"
keyring = []
smb = ["dep:remotefs-smb"]
smb-vendored = ["remotefs-smb/vendored"]
[profile.dev]
incremental = true

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installation</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installation</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">User manual</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">User manual</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<a
href="/docs/ptbr/README.md"
href="/docs/pt-BR/README.md"
><img
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center">Developed by <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Current version: 0.16.0 (14/10/2024)</p>
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Current version: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -144,6 +144,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
- 📝 View and edit files with your favourite applications
- 💁 SFTP/SCP authentication with SSH keys and username/password
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
- 🐚 Embedded terminal for executing commands on the system.
- 🎨 Make it yours!
- Themes
- Custom file explorer format
@@ -190,7 +191,7 @@ Arch Linux users can install termscp from the official repositories.
pacman -S termscp
```
For more information or other platforms, please visit [termscp.veeso.dev](https://termscp.veeso.dev/#get-started) to view all installation methods.
For more information or other platforms, please visit [termscp.veeso.dev](https://termscp.veeso.dev/get-started.html) to view all installation methods.
⚠️ If you're looking on how to update termscp just run termscp from CLI with: `(sudo) termscp --update` ⚠️
@@ -236,7 +237,7 @@ You can make a donation with one of these platforms:
## User manual 📚
The user manual can be found on the [termscp's website](https://termscp.veeso.dev/#user-manual) or on [Github](docs/man.md).
The user manual can be found on the [termscp's website](https://termscp.veeso.dev/user-manual.html) or on [Github](docs/man.md).
---

View File

@@ -7,11 +7,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Platforms
macos: { target_os = "macos" },
linux: { target_os = "linux" },
unix: { target_family = "unix" },
windows: { target_family = "windows" },
posix: { target_family = "unix" },
win: { target_family = "windows" },
// exclusive features
smb: { all(feature = "smb", not( macos )) },
smb_unix: { all(unix, feature = "smb", not(macos)) },
smb: { feature = "smb" },
smb_unix: { all(unix, feature = "smb") },
smb_windows: { all(windows, feature = "smb") }
}

View File

@@ -9,9 +9,33 @@ RUN apt update && apt install -y \
libdbus-1-dev \
build-essential \
libsmbclient-dev \
libsmbclient \
libgit2-dev \
build-essential \
pkg-config \
libbsd-dev \
libcap-dev \
libcups2-dev \
libgnutls28-dev \
libicu-dev \
libjansson-dev \
libkeyutils-dev \
libldap2-dev \
zlib1g-dev \
libpam0g-dev \
libacl1-dev \
libarchive-dev \
flex \
bison \
libntirpc-dev \
libtracker-sparql-3.0-dev \
libglib2.0-dev \
libdbus-1-dev \
libsasl2-dev \
libunistring-dev \
bash \
curl
curl \
cpanminus && \
cpanm Parse::Yapp::Driver;
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \

View File

@@ -38,7 +38,7 @@ cd -
mkdir -p ${PKGS_DIR}/deb/
mkdir -p ${PKGS_DIR}/aarch64-unknown-linux-gnu/
docker run --name "$ARM64_DEB_NAME" -d "$ARM64_DEB_NAME" || docker start "$ARM64_DEB_NAME"
docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release && cargo deb"
docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb"
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_arm64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_arm64.deb
docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/aarch64-unknown-linux-gnu/
docker stop "$ARM64_DEB_NAME"

View File

@@ -30,13 +30,13 @@ PKGS_DIR=$(pwd)/pkgs
cd -
mkdir -p ${PKGS_DIR}/
# Build x86_64_deb
cd x86_64_debian10/
cd x86_64_debian12/
docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_DEB_NAME" .
cd -
mkdir -p ${PKGS_DIR}/deb/
mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/
docker run --name "$X86_64_DEB_NAME" -d "$X86_64_DEB_NAME" || docker start "$X86_64_DEB_NAME"
docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release && cargo deb"
docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb"
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_amd64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_amd64.deb
docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/x86_64-unknown-linux-gnu/
docker stop "$X86_64_DEB_NAME"

4
dist/build/macos.sh vendored
View File

@@ -81,7 +81,7 @@ fi
# Build release (x86_64)
X86_TARGET=""
X86_TARGET_DIR=""
if [ "$ARCH" = "aarch64" ]; then
if [ "$ARCH" = "x86_64" ]; then
X86_TARGET="--target x86_64-apple-darwin"
X86_TARGET_DIR="target/x86_64-apple-darwin/release/"
fi
@@ -92,7 +92,7 @@ RET_X86_64=$?
ARM64_TARGET=""
ARM64_TARGET_DIR=""
if [ "$ARCH" = "aarch64" ]; then
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
ARM64_TARGET="--target aarch64-apple-darwin"
ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/"
fi

View File

@@ -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
View 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"]

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Webseite</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installation</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installation</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Benutzerhandbuch</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Benutzerhandbuch</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<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
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center">Entwickelt von <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Aktuelle Version: 0.16.0 (14/10/2024)</p>
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Aktuelle Version: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -186,7 +186,7 @@ Wenn Sie ein Windows-Benutzer sind, können Sie termscp mit [Chocolatey](https:/
choco install termscp
```
Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started), um alle Installationsmethoden anzuzeigen.
Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html), um alle Installationsmethoden anzuzeigen.
⚠️ Wenn Sie wissen möchten, wie Sie termscp aktualisieren können, führen Sie einfach termscp über die CLI aus mit: `(sudo) termscp --update` ⚠️
@@ -234,7 +234,7 @@ Sie können mit einer dieser Plattformen spenden:
## User manual 📚
Das Benutzerhandbuch finden Sie auf der [termscp-Website](https://termscp.veeso.dev/termscp/#user-manual) oder auf [Github](man.md).
Das Benutzerhandbuch finden Sie auf der [termscp-Website](https://termscp.veeso.dev/termscp/user-manual.html) oder auf [Github](man.md).
---

View File

@@ -10,11 +10,16 @@
- [Unterbefehle](#unterbefehle)
- [Ein Thema importieren](#ein-thema-importieren)
- [Neueste Version installieren](#neueste-version-installieren)
- [Unterbefehle](#unterbefehle-1)
- [Ein Theme importieren](#ein-theme-importieren)
- [Neueste Version installieren](#neueste-version-installieren-1)
- [SSH-Hosts importieren](#ssh-hosts-importieren)
- [S3-Verbindungsparameter](#s3-verbindungsparameter)
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen-)
- [Dateiexplorer 📂](#dateiexplorer-)
- [Tastenkombinationen ⌨](#tastenkombinationen-)
- [Mit mehreren Dateien arbeiten 🥷](#mit-mehreren-dateien-arbeiten-)
- [Beispiel](#beispiel)
- [Synchronisiertes Durchsuchen ⏲️](#synchronisiertes-durchsuchen-)
- [Öffnen und Öffnen mit 🚪](#öffnen-und-öffnen-mit-)
- [Lesezeichen ⭐](#lesezeichen-)
@@ -28,9 +33,9 @@
- [AWS S3 Adressargument](#aws-s3-adressargument-1)
- [SMB Adressargument](#smb-adressargument-1)
- [Wie das Passwort bereitgestellt werden kann 🔐](#wie-das-passwort-bereitgestellt-werden-kann--1)
- [Unterbefehle](#unterbefehle-1)
- [Unterbefehle](#unterbefehle-2)
- [Ein Thema importieren](#ein-thema-importieren-1)
- [Neueste Version installieren](#neueste-version-installieren-1)
- [Neueste Version installieren](#neueste-version-installieren-2)
- [S3-Verbindungsparameter](#s3-verbindungsparameter-1)
- [S3-Anmeldeinformationen 🦊](#s3-anmeldeinformationen--1)
- [Dateiexplorer 📂](#dateiexplorer--1)
@@ -172,6 +177,22 @@ Führen Sie termscp als `termscp theme <thema-datei>` aus
Führen Sie termscp als `termscp update` aus
### Unterbefehle
#### Ein Theme importieren
Führen Sie termscp mit `termscp theme <theme-datei>` aus.
#### Neueste Version installieren
Führen Sie termscp mit `termscp update` aus.
#### SSH-Hosts importieren
Führen Sie termscp mit `termscp import-ssh-hosts [ssh-config-datei]` aus.
Importieren Sie alle Hosts aus der angegebenen SSH-Konfigurationsdatei (wenn keine angegeben ist, wird `~/.ssh/config` verwendet) als Lesezeichen in termscp. Identitätsdateien werden ebenfalls als SSH-Schlüssel in termscp importiert.
---
## S3-Verbindungsparameter
@@ -295,21 +316,37 @@ Diese Panels sind im Wesentlichen 3 (ja, tatsächlich drei):
| <CTRL+A> | Alle Dateien auswählen | |
| <ALT+A> | Alle Dateien abwählen | |
| <CTRL+C> | Dateiübertragungsvorgang abbrechen | |
| `<CTRL+S>` | Gesamte Größe des ausgewählten Pfads abrufen | Size |
| <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.
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:
Du kannst mit mehreren Dateien gleichzeitig arbeiten, mit diesen einfachen Tastenkombinationen:
- _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 ⏲️

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Sitio Web</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Instalación</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Instalación</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manual de usuario</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manual de usuario</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<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
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center">Desarrollado por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Versión actual: 0.16.0 (14/10/2024)</p>
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Versión actual: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -186,7 +186,7 @@ mientras que si eres un usuario de Windows, puedes instalar termscp con [Chocola
choco install termscp
```
Para obtener más información u otras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started) para ver todos los métodos de instalación.
Para obtener más información u otras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html) para ver todos los métodos de instalación.
⚠️ Si estás buscando cómo actualizar termscp, simplemente ejecute termscp desde CLI con:: `(sudo) termscp --update` ⚠️
@@ -232,7 +232,7 @@ Puedes hacer una donación con una de estas plataformas:
## Manual de usuario y documentación 📚
El manual del usuario se puede encontrar en el [sitio web de termscp](https://termscp.veeso.dev/termscp/#user-manual) o en [Github](man.md).
El manual del usuario se puede encontrar en el [sitio web de termscp](https://termscp.veeso.dev/termscp/user-manual.html) o en [Github](man.md).
---

View File

@@ -8,11 +8,16 @@
- [Argumento de dirección de WebDAV](#argumento-de-dirección-de-webdav)
- [Argumento dirección por SMB](#argumento-dirección-por-smb)
- [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-)
- [Subcomandos](#subcomandos)
- [Importar un tema](#importar-un-tema)
- [Instalar la versión más reciente](#instalar-la-versión-más-reciente)
- [Importar hosts SSH](#importar-hosts-ssh)
- [S3 parámetros de conexión](#s3-parámetros-de-conexión)
- [Credenciales de S3 🦊](#credenciales-de-s3-)
- [Explorador de archivos 📂](#explorador-de-archivos-)
- [Keybindings ⌨](#keybindings-)
- [Trabaja en varios archivos 🥷](#trabaja-en-varios-archivos-)
- [Trabajar con múltiples archivos 🥷](#trabajar-con-múltiples-archivos-)
- [Ejemplo](#ejemplo)
- [Navegación sincronizada ⏲️](#navegación-sincronizada-)
- [Abierta y abierta con 🚪](#abierta-y-abierta-con-)
- [Marcadores ⭐](#marcadores-)
@@ -152,6 +157,22 @@ La contraseña se puede proporcionar básicamente a través de 3 formas cuando s
- Con `sshpass`: puede proporcionar la contraseña a través de `sshpass`, p. ej. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Se te pedirá que ingreses: si no utilizas ninguno de los métodos anteriores, se te pedirá la contraseña, como ocurre con las herramientas más clásicas como `scp`, `ssh`, etc.
### Subcomandos
#### Importar un tema
Ejecute termscp como `termscp theme <archivo-tema>`
#### Instalar la versión más reciente
Ejecute termscp como `termscp update`
#### Importar hosts SSH
Ejecute termscp como `termscp import-ssh-hosts [archivo-config-ssh]`
Importa todos los hosts del archivo de configuración SSH especificado (si no se proporciona, se usará `~/.ssh/config`) como marcadores en termscp. Los archivos de identidad también se importarán como claves SSH en termscp.
---
## S3 parámetros de conexión
@@ -230,25 +251,25 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
| `<BACKTAB>` | Cambiar entre la pestaña de registro y el explorador | |
| `<A>` | Alternar archivos ocultos | All |
| `<B>` | Ordenar archivos por | Bubblesort? |
| `<C|F5>` | Copiar archivo / directorio | Copy |
| `<D|F7>` | Hacer directorio | Directory |
| `<E|F8|DEL>` | Eliminar archivo | Erase |
| `<C\|F5>` | Copiar archivo / directorio | Copy |
| `<D\|F7>` | Hacer directorio | Directory |
| `<E\|F8\|DEL>` | Eliminar archivo | Erase |
| `<F>` | Búsqueda de archivos | Find |
| `<G>` | Ir a la ruta proporcionada | Go to |
| `<H|F1>` | Mostrar ayuda | Help |
| `<H\|F1>` | Mostrar ayuda | Help |
| `<I>` | Mostrar información sobre el archivo | Info |
| `<K>` | Crear un enlace simbólico que apunte a la entrada seleccionada actualmente | symlinK |
| `<L>` | Recargar contenido del directorio / Borrar selección | List |
| `<M>` | Seleccione un archivo | Mark |
| `<N>` | Crear un nuevo archivo con el nombre proporcionado | New |
| `<O|F4>` | Editar archivo | Open |
| `<O\|F4>` | Editar archivo | Open |
| `<P>` | Open log panel | Panel |
| `<Q|F10>` | Salir de termscp | Quit |
| `<R|F6>` | Renombrar archivo | Rename |
| `<S|F2>` | Guardar archivo como... | Save |
| `<Q\|F10>` | Salir de termscp | Quit |
| `<R\|F6>` | Renombrar archivo | Rename |
| `<S\|F2>` | Guardar archivo como... | Save |
| `<T>` | Sincronizar los cambios en la ruta seleccionada con el control remoto | Track |
| `<U>` | Ir al directorio principal | Upper |
| `<V|F3>` | Abrir archivo con el programa predeterminado | View |
| `<V\|F3>` | Abrir archivo con el programa predeterminado | View |
| `<W>` | Abrir archivo con el programa proporcionado | With |
| `<X>` | Ejecutar un comando | eXecute |
| `<Y>` | Alternar navegación sincronizada | sYnc |
@@ -257,19 +278,37 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
| `<CTRL+A>` | Seleccionar todos los archivos | |
| `<ALT+A>` | Deseleccionar todos los archivos | |
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
| `<CTRL+S>` | Obtener el tamaño total de la ruta seleccionada | Size |
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |
### Trabaja en varios archivos 🥷
### Trabajar con múltiples archivos 🥷
Puede optar por trabajar en varios archivos, seleccionándolos presionando `<M>`, para seleccionar el archivo actual, o presionando `<CTRL + A>`, que seleccionará todos los archivos en el directorio de trabajo.
Una vez que un archivo está marcado para su selección, se mostrará con un `*` a la izquierda.
Al trabajar en la selección, solo se procesará el archivo seleccionado para las acciones, mientras que el elemento resaltado actual se ignorará.
También es posible trabajar en varios archivos desde el panel de resultados de búsqueda.
Todas las acciones están disponibles cuando se trabaja con varios archivos, pero tenga en cuenta que algunas acciones funcionan de forma ligeramente diferente. Vamos a sumergirnos en:
Puedes optar por trabajar con varios archivos, usando estos controles:
- *Copy*: cada vez que copie un archivo, se le pedirá que inserte el nombre de destino. Cuando se trabaja con varios archivos, este nombre se refiere al directorio de destino donde se copiarán todos estos archivos.
- *Rename*: igual que copiar, pero moverá archivos allí.
- *Save as*: igual que copiar, pero los escribirá allí.
- `<M>`: marcar un archivo para selección
- `<CTRL+A>`: seleccionar todos los archivos del directorio actual
- `<ALT+A>`: deseleccionar todos los archivos
Una vez marcado, el archivo será **mostrado con un fondo resaltado** .
Cuando se trabaja con una selección, solo los archivos seleccionados serán procesados; el archivo resaltado actual será ignorado.
También se puede trabajar con múltiples archivos desde el panel de resultados de búsqueda.
Todas las acciones están disponibles con archivos múltiples, pero algunas funcionan de forma algo distinta. Veamos:
- *Copiar*: al copiar, se pedirá el nombre de destino. Para varios archivos, es el directorio donde se copiarán.
- *Renombrar*: igual que copiar, pero mueve los archivos.
- *Guardar como*: igual que copiar, pero escribe los archivos allí.
Si seleccionas un archivo en un directorio (ej. `/home`) y cambias de directorio, seguirá seleccionado y se mostrará en la **cola de transferencia** en el panel inferior.
Cuando se selecciona un archivo, se asocia la carpeta *remota* actual con él; si se transfiere, será a esa carpeta.
#### Ejemplo
Si seleccionamos `/home/a.txt` localmente y estamos en `/tmp` en remoto, luego cambiamos a `/var`, seleccionamos `/var/b.txt` y estamos en `/home` en el panel remoto, el resultado de transferir será:
- `/home/a.txt` transferido a `/tmp/a.txt`
- `/var/b.txt` transferido a `/home/b.txt`
### Navegación sincronizada ⏲️

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Site internet</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installation</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installation</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manuel de l'Utilisateur</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manuel de l'Utilisateur</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<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
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center">Développé par <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Version actuelle: 0.16.0 (14/10/2024)</p>
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Version actuelle: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -186,7 +186,7 @@ tandis que si tu es un utilisateur Windows, tu peux installer termscp avec [Choc
choco install termscp
```
Pour plus d'informations sur les autres méthodes d'installation, veuillez visiter [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started).
Pour plus d'informations sur les autres méthodes d'installation, veuillez visiter [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html).
⚠️ Si tu cherche comme de mettre à jour termscp, tu dois exécuter cette commande dans le terminal: `(sudo) termscp --update` ⚠️
@@ -232,7 +232,7 @@ Tu peux faire un don avec l'une de ces plateformes:
## Manuel d'utilisateur et Documentation 📚
Le manuel d'utilisateur peut être trouvé sur le [site de termscp](https://termscp.veeso.dev/termscp/#user-manual) ou sur [Github](man.md).
Le manuel d'utilisateur peut être trouvé sur le [site de termscp](https://termscp.veeso.dev/termscp/user-manual.html) ou sur [Github](man.md).
La documentation peut être trouvé sur Rust Docs <https://docs.rs/termscp>

View File

@@ -8,11 +8,16 @@
- [Argument d'adresse WebDAV](#argument-dadresse-webdav)
- [Argument d'adresse SMB](#argument-dadresse-smb)
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
- [Sous-commandes](#sous-commandes)
- [Importer un thème](#importer-un-thème)
- [Installer la dernière version](#installer-la-dernière-version)
- [Importer des hôtes SSH](#importer-des-hôtes-ssh)
- [S3 paramètres de connexion](#s3-paramètres-de-connexion)
- [Identifiants S3 🦊](#identifiants-s3-)
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
- [Raccourcis clavier ⌨](#raccourcis-clavier-)
- [Travailler sur plusieurs fichiers 🥷](#travailler-sur-plusieurs-fichiers-)
- [Exemple](#exemple)
- [Navigation synchronisée ⏲️](#navigation-synchronisée-)
- [Ouvrir et ouvrir avec 🚪](#ouvrir-et-ouvrir-avec-)
- [Signets ⭐](#signets-)
@@ -141,7 +146,6 @@ syntaxe **Other systems**:
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Comment le mot de passe peut être fourni 🔐
Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe.
@@ -151,6 +155,22 @@ Le mot de passe peut être fourni de 3 manières lorsque l'argument d'adresse es
- Avec `sshpass`: vous pouvez fournir un mot de passe via `sshpass`, par ex. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Il vous sera demandé : si vous n'utilisez aucune des méthodes précédentes, le mot de passe vous sera demandé, comme c'est le cas avec les outils plus classiques tels que `scp`, `ssh`, etc.
### Sous-commandes
#### Importer un thème
Exécutez termscp avec `termscp theme <fichier-thème>`
#### Installer la dernière version
Exécutez termscp avec `termscp update`
#### Importer des hôtes SSH
Exécutez termscp avec `termscp import-ssh-hosts [fichier-config-ssh]`
Importez tous les hôtes du fichier de configuration SSH spécifié (si non fourni, `~/.ssh/config` sera utilisé) comme favoris dans termscp. Les fichiers d'identité seront également importés comme clés SSH dans termscp.
---
## S3 paramètres de connexion
@@ -229,25 +249,25 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
| `<BACKTAB>` | Basculer entre l'onglet journal et l'explorateur | |
| `<A>` | Basculer les fichiers cachés | All |
| `<B>` | Trier les fichiers par | Bubblesort? |
| `<C|F5>` | Copier le fichier/répertoire | Copy |
| `<D|F7>` | Créer un dossier | Directory |
| `<E|F8|DEL>` | Supprimer le fichier (Identique à `DEL`) | Erase |
| `<C\|F5>` | Copier le fichier/répertoire | Copy |
| `<D\|F7>` | Créer un dossier | Directory |
| `<E\|F8\|DEL>` | Supprimer le fichier (Identique à `DEL`) | Erase |
| `<F>` | Rechercher des fichiers | Find |
| `<G>` | Aller au chemin fourni | Go to |
| `<H|F1>` | Afficher l'aide | Help |
| `<H\|F1>` | Afficher l'aide | Help |
| `<I>` | Afficher les informations sur le fichier ou le dossier sélectionné | Info |
| `<K>` | Créer un lien symbolique pointant vers l'entrée actuellement sélectionnée | symlinK |
| `<L>` | Recharger le contenu du répertoire actuel / Effacer la sélection | List |
| `<M>` | Sélectionner un fichier | Mark |
| `<N>` | Créer un nouveau fichier avec le nom fourni | New |
| `<O|F4>` | Modifier le fichier | Open |
| `<O\|F4>` | Modifier le fichier | Open |
| `<P>` | Ouvre le panel de journals | Panel |
| `<Q|F10>` | Quitter termscp | Quit |
| `<R|F6>` | Renommer le fichier | Rename |
| `<S|F2>` | Enregistrer le fichier sous... | Save |
| `<Q\|F10>` | Quitter termscp | Quit |
| `<R\|F6>` | Renommer le fichier | Rename |
| `<S\|F2>` | Enregistrer le fichier sous... | Save |
| `<T>` | Synchroniser les modifications apportées au chemin sélectionné | Track |
| `<U>` | Aller dans le répertoire parent | Upper |
| `<V|F3>` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View |
| `<V\|F3>` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View |
| `<W>` | Ouvrir le fichier avec le programme spécifié | With |
| `<X>` | Exécuter une commande | eXecute |
| `<Y>` | Basculer la navigation synchronisée | sYnc |
@@ -256,19 +276,37 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
| `<CTRL+A>` | Sélectionner tous les fichiers | |
| `<ALT+A>` | Desélectionner tous les fichiers | |
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
| `<CTRL+S>` | Obtenir la taille totale du chemin sélectionné | Size |
| `<CTRL+T>` | Afficher tous les chemins synchronisés | Track |
### Travailler sur plusieurs fichiers 🥷
Vous pouvez choisir de travailler sur plusieurs fichiers, en les sélectionnant en appuyant sur `<M>`, afin de sélectionner le fichier actuel, ou en appuyant sur `<CTRL+A>`, ce qui sélectionnera tous les fichiers dans le répertoire de travail.
Une fois qu'un fichier est marqué pour la sélection, il sera affiché avec un `*` sur la gauche.
Lorsque vous travaillez sur la sélection, seul le fichier sélectionné sera traité pour les actions, tandis que l'élément en surbrillance actuel sera ignoré.
Il est également possible de travailler sur plusieurs fichiers dans le panneau des résultats de recherche.
Toutes les actions sont disponibles lorsque vous travaillez avec plusieurs fichiers, mais sachez que certaines actions fonctionnent de manière légèrement différente. Plongeons dans:
Vous pouvez choisir de travailler sur plusieurs fichiers avec ces simples commandes :
- *Copy*: chaque fois que vous copiez un fichier, vous serez invité à insérer le nom de destination. Lorsque vous travaillez avec plusieurs fichiers, ce nom fait référence au répertoire de destination où tous ces fichiers seront copiés.
- *Rename*: identique à la copie, mais y déplacera les fichiers.
- *Save as*: identique à la copie, mais les y écrira.
- `<M>` : marquer un fichier à sélectionner
- `<CTRL+A>` : sélectionner tous les fichiers du répertoire actuel
- `<ALT+A>` : désélectionner tous les fichiers
Une fois sélectionné, un fichier sera **affiché avec un fond en surbrillance** .
Lorsquon 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 dattente de transfert** en bas.
Lorsquun 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 ⏲️

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Sito</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Installazione</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Installazione</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manuale utente</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manuale utente</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<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
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center">Sviluppato da <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Versione corrente: 0.16.0 (14/10/2024)</p>
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Versione corrente: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -186,7 +186,7 @@ mentre se sei un utente Windows, puoi installare termscp con [Chocolatey](https:
choco install termscp
```
Per ulteriori informazioni sui metodi di installazione su altre piattaforme, visita [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started).
Per ulteriori informazioni sui metodi di installazione su altre piattaforme, visita [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html).
⚠️ Se stavi cercando come aggiornare la tua versione di termscp, puoi semplicemente lanciare termscp con questi argomenti: `(sudo) termscp --update` ⚠️
@@ -232,7 +232,7 @@ Puoi fare una donazione tramite una di queste piattaforme:
## Manuale utente 📚
Il manuale utente lo puoi trovare sul [sito di termscp](https://termscp.veeso.dev/termscp/#user-manual) o su [Github](man.md).
Il manuale utente lo puoi trovare sul [sito di termscp](https://termscp.veeso.dev/termscp/user-manual.html) o su [Github](man.md).
---

View File

@@ -8,11 +8,16 @@
- [Argomento indirizzo per WebDAV](#argomento-indirizzo-per-webdav)
- [Indirizzo SMB](#indirizzo-smb)
- [Come fornire la password 🔐](#come-fornire-la-password-)
- [Sottocomandi](#sottocomandi)
- [Importare un tema](#importare-un-tema)
- [Installare lultima versione](#installare-lultima-versione)
- [Importare host SSH](#importare-host-ssh)
- [Parametri di connessione S3](#parametri-di-connessione-s3)
- [Credenziali S3 🦊](#credenziali-s3-)
- [File explorer 📂](#file-explorer-)
- [Abbinamento tasti ⌨](#abbinamento-tasti-)
- [Lavora su più file 🥷](#lavora-su-più-file-)
- [Lavora con più file 🥷](#lavora-con-più-file-)
- [Esempio](#esempio)
- [Synchronized browsing ⏲️](#synchronized-browsing-)
- [Apri e apri con 🚪](#apri-e-apri-con-)
- [Segnalibri ⭐](#segnalibri-)
@@ -139,7 +144,6 @@ SMB ha una sintassi differente rispetto agli altri protocolli e cambia in base a
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Come fornire la password 🔐
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:
@@ -148,6 +152,22 @@ Quando si usa l'argomento indirizzo non è possibile fornire la password diretta
- Tramite `sshpass`: puoi fornire la password tramite l'applicazione GNU/Linux sshpass `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Forniscila quando richiesta: se non la fornisci tramite nessun metodo precedente, alla connessione ti verrà richiesto di fornirla in un prompt che la oscurerà (come avviene con sudo tipo).
### Sottocomandi
#### Importare un tema
Esegui termscp come `termscp theme <file-tema>`
#### Installare lultima versione
Esegui termscp come `termscp update`
#### Importare host SSH
Esegui termscp come `termscp import-ssh-hosts [file-config-ssh]`
Importa tutti gli host dal file di configurazione SSH specificato (se non fornito, verrà usato `~/.ssh/config`) come segnalibri in termscp. I file di identità verranno importati come chiavi SSH in termscp.
---
## Parametri di connessione S3
@@ -225,25 +245,25 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
| `<BACKTAB>` | Cambia tra explorer e pannello di log | |
| `<A>` | Mostra/nascondi file nascosti | All |
| `<B>` | Ordina file per | Bubblesort? |
| `<C|F5>` | Copia file/directory | Copy |
| `<D|F7>` | Crea directory | Directory |
| `<E|F8|DEL>` | Elimina file | Erase |
| `<C\|F5>` | Copia file/directory | Copy |
| `<D\|F7>` | Crea directory | Directory |
| `<E\|F8\|DEL>` | Elimina file | Erase |
| `<F>` | Cerca file (wild match supportato) | Find |
| `<G>` | Vai al percorso indicato | Go to |
| `<H|F1>` | Mostra help | Help |
| `<H\|F1>` | Mostra help | Help |
| `<I>` | Mostra informazioni per il file selezionato | Info |
| `<K>` | Crea un link simbolico che punta al file selezionato | symlinK |
| `<L>` | Ricarica posizione corrente / pulisci selezione file | List |
| `<M>` | Seleziona file | Mark |
| `<N>` | Crea nuovo file con il nome fornito | New |
| `<O|F4>` | Modifica file; Vedi text editor | Open |
| `<O\|F4>` | Modifica file; Vedi text editor | Open |
| `<P>` | Apri pannello log | Panel |
| `<Q|F10>` | Termina termscp | Quit |
| `<R|F6>` | Rinomina file | Rename |
| `<S|F2>` | Salva file con nome | Save |
| `<Q\|F10>` | Termina termscp | Quit |
| `<R\|F6>` | Rinomina file | Rename |
| `<S\|F2>` | Salva file con nome | Save |
| `<T>` | Sincronizza il percorso locale con l'host remoto | Track |
| `<U>` | Vai alla directory padre | Upper |
| `<V|F3>` | Apri il file con il programma definito dal sistema | View |
| `<V\|F3>` | Apri il file con il programma definito dal sistema | View |
| `<W>` | Apri il file con il programma specificato | With |
| `<X>` | Esegui comando shell | eXecute |
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
@@ -252,19 +272,37 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
| `<CTRL+A>` | Seleziona tutti i file | |
| `<ALT+A>` | Deseleziona tutti i file | |
| `<CTRL+C>` | Annulla trasferimento file | |
| `<CTRL+S>` | Ottieni la dimensione totale del percorso selezionato | Size |
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
### Lavora su più file 🥷
### Lavora con più file 🥷
Puoi lavorare su una selezione di file, marcandoli come selezionati tramite `<M>`, per selezionare il file corrente o con `<CTRL+A` per selezionarli tutti.
Una volta che un file è marcato, sarà visualizzato con un `*` prima del nome.
Quando lavori con una selezioni, solo i file selezionati saranno presi in considerazione (l'eventuale file evidenziato sarà ignorato).
È possibile operare su più file anche nel pannello di ricerca.
Tutte le azioni sono disponibili quando si lavora sulle selezioni, ma occhio, che alcune azioni si comporteranno in maniera leggermente differente. Vediamo quali e come:
Puoi scegliere di lavorare con più file, usando questi semplici comandi:
- *Copia*: Se copi un file, ti verrà richiesto di inserire il nome delle destinazione, ma quando lavori con la selezione, il nome si riferisce alla directory di destinazione, mentre il nome del file rimarrà inviariato.
- *Rinomina*: Come il copia, ma li sposterà.
- *Salva con nome*: Come il copia, ma li trasferirà.
- `<M>`: marca un file per la selezione
- `<CTRL+A>`: seleziona tutti i file nella directory corrente
- `<ALT+A>`: deseleziona tutti i file
Una volta che un file è stato selezionato, verrà **evidenziato con uno sfondo colorato** .
Quando lavori su una selezione, solo i file selezionati verranno processati per le azioni, mentre l'elemento attualmente evidenziato sarà ignorato.
È possibile lavorare con più file anche dal pannello dei risultati di ricerca.
Tutte le azioni sono disponibili anche quando si lavora con più file, ma alcune funzionano in modo leggermente diverso. Ecco i dettagli:
- *Copia*: quando copi un file, ti verrà chiesto di inserire il nome di destinazione. Con più file selezionati, questo nome rappresenta la cartella di destinazione dove verranno copiati.
- *Rinomina*: come la copia, ma i file verranno spostati lì.
- *Salva come*: come la copia, ma i file verranno salvati lì.
Se selezioni un file in una directory (es. `/home`) e poi cambi directory, il file rimarrà selezionato e sarà visibile nella **coda di trasferimento** nel pannello inferiore.
Quando un file viene selezionato, la directory *remota* corrente viene associata allelemento; 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 ⏲️

View File

@@ -11,11 +11,13 @@
- [Subcommands](#subcommands)
- [Import a theme](#import-a-theme)
- [Install latest version](#install-latest-version)
- [Import ssh hosts](#import-ssh-hosts)
- [S3 connection parameters](#s3-connection-parameters)
- [S3 credentials 🦊](#s3-credentials-)
- [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-)
- [Work on multiple files 🥷](#work-on-multiple-files-)
- [Example](#example)
- [Synchronized browsing ⏲️](#synchronized-browsing-)
- [Open and Open With 🚪](#open-and-open-with-)
- [Bookmarks ⭐](#bookmarks-)
@@ -165,6 +167,12 @@ Run termscp as `termscp theme <theme-file>`
Run termscp as `termscp update`
#### Import ssh hosts
Run termscp as `termscp import-ssh-hosts [ssh-config-file]`
Import all the hosts from the specified ssh config file (if not provided, `~/.ssh/config` will be used) as bookmarks in termscp. Identity files will be imported as ssh keys in termscp too.
---
## S3 connection parameters
@@ -270,20 +278,39 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
| `<CTRL+A>` | Select all files | |
| `<ALT+A>` | Deselect all files | |
| `<CTRL+C>` | Abort file transfer process | |
| `<CTRL+S>` | Get total size of the selected path | Size |
| `<CTRL+T>` | Show all synchronized paths | Track |
### Work on multiple files 🥷
You can opt to work on multiple files, selecting them pressing `<M>`, in order to select the current file, or pressing `<CTRL+A>`, which will select all the files in the working directory.
Once a file is marked for selection, it will be displayed with a `*` on the left.
You can opt to work on multiple files, with these simple controls:
- `<M>`: mark a file for selection
- `<CTRL+A>`: select all files in the current directory
- `<ALT+A>`: deselect all files
Once a file is marked for selection, it will be **displayed with an highlighted background**.
When working on selection, only selected file will be processed for actions, while the current highlighted item will be ignored.
It is possible to work on multiple files also when in the find result panel.
All the actions are available when working with multiple files, but be aware that some actions work in a slightly different way. Let's dive in:
- *Copy*: whenever you copy a file, you'll be prompted to insert the destination name. When working with multiple file, this name refers to the destination directory where all these files will be copied.
- *Rename*: same as copy, but will move files there.
- *Save as*: same as copy, but will write them there.
If you select a file in a directory (e.g. `/home`) and then you change directory the file will be kept selected and it will be displayed in the **transfer queue** in the bottom panel.
When a file gets selected the current *remote* directory is associated to its entry; so in case the file gets transferred it will be transferred to the directory associated to the file.
#### Example
If we select a file on local `/home/a.txt` and we're currently at `/tmp` on remote and then we move to `/var` and we select `/var/b.txt` and on the remote panel we're at `/home` and we perform a transfer the result will be:
- `/home/a.txt` transferred to `/tmp/a.txt`
- `/var/b.txt` transferred to `/home/b.txt`
### Synchronized browsing ⏲️
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">Website</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">Instalação</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">Instalação</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">Manual do usuário</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">Manual do usuário</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<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
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center">Desenvolvido por <a href="https://veeso.dev/" target="_blank">@veeso</a></p>
<p align="center">Versão atual: 0.16.0 (14/10/2024)</p>
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
<p align="center">Versão atual: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -195,7 +195,7 @@ Usuários do Arch Linux podem instalar o termscp pelos repositórios oficiais.
pacman -S termscp
```
Para mais informações ou outras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/#get-started) para ver todos os métodos de instalação.
Para mais informações ou outras plataformas, visite [termscp.veeso.dev](https://termscp.veeso.dev/get-started.html) para ver todos os métodos de instalação.
⚠️ Se você quer saber como atualizar o termscp, basta executar o termscp a partir do CLI com: `(sudo) termscp --update` ⚠️
@@ -241,7 +241,7 @@ Você pode fazer uma doação por meio de uma dessas plataformas:
## Manual do Usuário 📚
O manual do usuário pode ser encontrado no [site do termscp](https://termscp.veeso.dev/#user-manual) ou no [Github](docs/man.md).
O manual do usuário pode ser encontrado no [site do termscp](https://termscp.veeso.dev/user-manual.html) ou no [Github](docs/man.md).
---

View File

@@ -11,11 +11,13 @@
- [Subcomandos](#subcomandos)
- [Importar um Tema](#importar-um-tema)
- [Instalar a Última Versão](#instalar-a-última-versão)
- [Importar hosts SSH](#importar-hosts-ssh)
- [Parâmetros de Conexão do S3](#parâmetros-de-conexão-do-s3)
- [Credenciais do S3 🦊](#credenciais-do-s3-)
- [Explorador de Arquivos 📂](#explorador-de-arquivos-)
- [Atalhos de Teclado ⌨](#atalhos-de-teclado-)
- [Trabalhar com 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-)
- [Abrir e Abrir Com 🚪](#abrir-e-abrir-com-)
- [Favoritos ⭐](#favoritos-)
@@ -163,6 +165,12 @@ Execute o termscp como `termscp theme <theme-file>`
Execute o termscp como `termscp update`
#### Importar hosts SSH
Execute o termscp como `termscp import-ssh-hosts [arquivo-config-ssh]`
Importe todos os hosts do arquivo de configuração SSH especificado (se não for fornecido, `~/.ssh/config` será usado) como favoritos no termscp. Os arquivos de identidade também serão importados como chaves SSH no termscp.
---
## Parâmetros de Conexão do S3
@@ -270,19 +278,37 @@ Para trocar de painel, você precisa pressionar `<LEFT>` para mover para o paine
| `<CTRL+A>` | Selecionar todos os arquivos | |
| `<ALT+A>` | Deselecionar todos os arquivos | |
| `<CTRL+C>` | Abortir processo de transferência de arquivo | |
| `<CTRL+S>` | Obter o tamanho total do caminho selecionado | | Size |
| `<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.
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:
Você pode optar por trabalhar com vários arquivos, usando estes controles simples:
- *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.
- *Renomear*: igual ao copiar, mas moverá os arquivos para lá.
- *Salvar como*: igual ao copiar, mas gravará lá.
- `<M>`: marcar um arquivo para seleção
- `<CTRL+A>`: selecionar todos os arquivos no diretório atual
- `<ALT+A>`: desselecionar todos os arquivos
Uma vez marcado, o arquivo será **exibido com fundo destacado** .
Ao trabalhar com seleção, apenas os arquivos selecionados serão processados, enquanto o item atualmente destacado será ignorado.
É possível trabalhar com múltiplos arquivos também no painel de resultados de busca.
Todas as ações estão disponíveis ao trabalhar com múltiplos arquivos, mas algumas funcionam de forma ligeiramente diferente. Vamos ver:
- *Copiar*: ao copiar, será solicitado o nome de destino. Com múltiplos arquivos, esse nome será o diretório de destino para todos eles.
- *Renomear*: igual a copiar, mas moverá os arquivos.
- *Salvar como*: igual a copiar, mas escreverá os arquivos nesse local.
Se você selecionar um arquivo num diretório (ex: `/home`) e mudar de diretório, ele continuará selecionado e aparecerá na **fila de transferência** no painel inferior.
Ao selecionar um arquivo, o diretório *remoto* atual é associado a ele; então, se for transferido, será enviado para esse diretório associado.
#### Exemplo
Se selecionarmos `/home/a.txt` localmente e estivermos em `/tmp` no painel remoto, depois mudarmos para `/var` e selecionarmos `/var/b.txt`, e estivermos em `/home` no painel remoto, ao transferir teremos:
- `/home/a.txt` transferido para `/tmp/a.txt`
- `/var/b.txt` transferido para `/home/b.txt`
### Navegação Sincronizada ⏲️

View File

@@ -8,9 +8,9 @@
<p align="center">
<a href="https://termscp.veeso.dev" target="_blank">网站</a>
·
<a href="https://termscp.veeso.dev/#get-started" target="_blank">安装</a>
<a href="https://termscp.veeso.dev/get-started.html" target="_blank">安装</a>
·
<a href="https://termscp.veeso.dev/#user-manual" target="_blank">用户手册</a>
<a href="https://termscp.veeso.dev/user-manual.html" target="_blank">用户手册</a>
</p>
<p align="center">
@@ -22,7 +22,7 @@
/></a>
&nbsp;
<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
height="20"
src="/assets/images/flags/br.png"
@@ -70,8 +70,8 @@
/></a>
</p>
<p align="center"><a href="https://veeso.dev/" target="_blank">@veeso</a> 开发</p>
<p align="center">当前版本: 0.16.0 (14/10/2024)</p>
<p align="center"><a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
<p align="center">当前版本: 0.19.0 11/11/2025</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
@@ -189,7 +189,7 @@ curl -sSLf http://get-termscp.veeso.dev | sh
choco install termscp
```
如需更多信息或其他的平台支持,请访问 [termscp.veeso.dev](https://termscp.veeso.dev/termscp/#get-started) 查看所有安装方法。
如需更多信息或其他的平台支持,请访问 [termscp.veeso.dev](https://termscp.veeso.dev/termscp/get-started.html) 查看所有安装方法。
⚠️ 如果您正在寻找如何更新 termscp 只需从 CLI 运行 termscp `(sudo) termscp --update` ⚠️
@@ -238,7 +238,7 @@ choco install termscp
## 用户手册和文档 📚
用户手册可以在[termscp的网站](https://termscp.veeso.dev/termscp/#user-manual)或者在[Github](man.md)上找到。
用户手册可以在[termscp的网站](https://termscp.veeso.dev/termscp/user-manual.html)或者在[Github](man.md)上找到。
---

View File

@@ -8,11 +8,16 @@
- [WebDAV 地址参数](#webdav-地址参数)
- [SMB 地址参数](#smb-地址参数)
- [如何输入密码](#如何输入密码)
- [子命令](#子命令)
- [导入主题](#导入主题)
- [安装最新版本](#安装最新版本)
- [导入 SSH 主机](#导入-ssh-主机)
- [S3 连接参数](#s3-连接参数)
- [Aws S3 凭证](#aws-s3-凭证)
- [文件浏览](#文件浏览)
- [快捷键](#快捷键)
- [处理多个文件](#处理多个文件)
- [操作多个文件 🥷](#操作多个文件-)
- [示例](#示例)
- [同步浏览](#同步浏览)
- [打开/打开方式](#打开打开方式)
- [书签](#书签)
@@ -148,6 +153,21 @@ smb://[username@]<server-name>[:port]/<share>[/path/.../]
- 通过 `sshpass`: 你可以通过 `sshpass` 传入密码, 例如: `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- 提示输入密码:如果你不使用前面的任何方法,你会被提示输入密码,就像 `scp`、`ssh` 等比较经典的工具上一样。
### 子命令
#### 导入主题
以 termscp theme <theme-file> 的方式运行 termscp。
#### 安装最新版本
以 termscp update 的方式运行 termscp。
#### 导入 SSH 主机
以 `termscp import-ssh-hosts [ssh-config-file]` 的方式运行 termscp。
从指定的 SSH 配置文件中导入所有主机(如果未提供,则使用 `~/.ssh/config`)作为 termscp 中的书签。身份文件也会作为 SSH 密钥导入到 termscp 中。
---
## S3 连接参数
@@ -225,25 +245,25 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
| `<BACKTAB>` | 在日志面板和管理器面板之间切换 | |
| `<A>` | 是否显示隐藏文件 | All |
| `<B>` | 按..排序 | Bubblesort? |
| `<C|F5>` | 复制文件(夹) | Copy |
| `<D|F7>` | 创建文件夹 | Directory |
| `<E|F8|DEL>` | 删除文件 | Erase |
| `<C\|F5>` | 复制文件(夹) | Copy |
| `<D\|F7>` | 创建文件夹 | Directory |
| `<E\|F8\|DEL>` | 删除文件 | Erase |
| `<F>` | 文件搜索 (支持通配符) | Find |
| `<G>` | 跳转到指定路径 | Go to |
| `<H|F1>` | 显示帮助 | Help |
| `<H\|F1>` | 显示帮助 | Help |
| `<I>` | 显示选中文件(夹)信息 | Info |
| `<K>` | 创建指向当前选定条目的符号链接 | symlinK |
| `<L>` | 刷新当前目录列表 / 清除选中状态 | List |
| `<M>` | 选中文件 | Mark |
| `<N>` | 使用键入的名称新建文件 | New |
| `<O|F4>` | 编辑文件;参考文本编辑器文档 | Open |
| `<O\|F4>` | 编辑文件;参考文本编辑器文档 | Open |
| `<P>` | 打开日志面板 | Panel |
| `<Q|F10>` | 退出termscp | Quit |
| `<R|F7>` | 重命名文件 | Rename |
| `<S|F2>` | 另存为... | Save |
| `<Q\|F10>` | 退出termscp | Quit |
| `<R\|F7>` | 重命名文件 | Rename |
| `<S\|F2>` | 另存为... | Save |
| `<T>` | 显示所有同步路径 | Track |
| `<U>` | 进入上层目录 | Upper |
| `<V|F3>` | 使用默认方式打开文件 | View |
| `<V\|F3>` | 使用默认方式打开文件 | View |
| `<W>` | 使用指定程序打开文件 | With |
| `<X>` | 运行命令 | eXecute |
| `<Y>` | 是否开启同步浏览 | sYnc |
@@ -252,16 +272,37 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
| `<CTRL+A>` | 选中所有文件 | |
| `<ALT+A>` | 取消选择所有文件 | |
| `<CTRL+C>` | 终止文件传输 | |
| `<CTRL+S>` | 获取所选路径的总大小 | Size |
| `<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`
### 同步浏览

View File

@@ -8,10 +8,10 @@
# -f, -y, --force, --yes
# Skip the confirmation prompt during installation
TERMSCP_VERSION="0.16.0"
TERMSCP_VERSION="0.19.0"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
PATH="$PATH:/usr/sbin"
@@ -33,8 +33,8 @@ NO_COLOR="$(tput sgr0 2>/dev/null || printf '')"
set_termscp_version() {
TERMSCP_VERSION="$1"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_arm64.deb"
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
}
info() {
@@ -451,7 +451,7 @@ case $PLATFORM in
esac
completed "Congratulations! Termscp has successfully been installed on your system!"
info "If you're a new user, you might be interested in reading the user manual <https://termscp.veeso.dev/#user-manual>"
info "If you're a new user, you might be interested in reading the user manual <https://termscp.veeso.dev/user-manual.html>"
info "While if you've just updated your termscp version, you can find the changelog at this link <https://termscp.veeso.dev/#changelog>"
info "Remember that if you encounter any issue, you can report them on Github <https://github.com/veeso/termscp/issues/new>"
info "Feel free to open an issue also if you have an idea which could improve the project"

View File

@@ -30,9 +30,9 @@
<p class="text-xs font-thin">
<span>Christian Visintin © </span><span resolve-copyright></span>
<span>&nbsp;|&nbsp;</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>&nbsp;|&nbsp;</span>
<a class="text-xs font-thin" href="https://veeso.dev/en/cookie-policy" target="_blank">Cookie policy</a>
<a class="text-xs font-thin" href="https://veeso.me/en/cookie-policy" target="_blank">Cookie policy</a>
</p>
</div>
</div>

View File

@@ -35,7 +35,7 @@
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
since last release, so if the latest version is not available yet,
you can install it downloading the ZIP file from</span>
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.16.0.nupkg"
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.19.0.nupkg"
target="_blank">Github</a>
<span translate="getStarted.windows.then">and then, from the ZIP directory, install it via</span>
</p>
@@ -74,7 +74,7 @@
On Debian based distros, you can install termscp using the Deb
package via:
</p>
<pre><span class="function">wget</span> -O termscp.deb <span class="string">https://github.com/veeso/termscp/releases/latest/download/termscp_0.16.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.19.0_amd64.deb</span>
sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</span></pre>
</div>
<h3>
@@ -157,7 +157,7 @@ sudo <span class="function">dpkg</span> -i <span class="string">termscp.deb</spa
</p>
<pre><span class="function">cargo</span> install --locked --no-default-features --features smb <span class="string">termscp</span></pre>
<p translate="getStarted.cargo.noSMB" class="pt-4 pb-2"></p>
<pre><span class="function">cargo</span> install --locked --no-default-features --features with-keyring <span class="string">termscp</span></pre>
<pre><span class="function">cargo</span> install --locked --no-default-features --features keyring <span class="string">termscp</span></pre>
</div>
</section>
</section>

View File

@@ -12,7 +12,7 @@
</button>
<div class="p-4 my-4 text-sm text-green-800 rounded-lg bg-green-50">
<p class="text-lg">
<span translate="intro.versionAlert">termscp 0.16.0 is NOW out! Download it from</span>&nbsp;
<span translate="intro.versionAlert">termscp 0.19.0 is NOW out! Download it from</span>&nbsp;
<a href="/get-started.html" translate="intro.here">here!</a>
</p>
</div>

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Get started →",
"versionAlert": "termscp 0.16.0 is NOW out! Download it from",
"versionAlert": "termscp 0.19.0 is NOW out! Download it from",
"here": "here",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "Un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Para iniciar →",
"versionAlert": "termscp 0.16.0 ya está disponible! Descárgalo desde",
"versionAlert": "termscp 0.19.0 ya está disponible! Descárgalo desde",
"here": "aquì",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "Un file transfer et navigateur de terminal riche en fonctionnalités avec support pour SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "Pour commencer →",
"versionAlert": "termscp 0.16.0 est maintenant sorti! Télécharge-le depuis",
"versionAlert": "termscp 0.19.0 est maintenant sorti! Télécharge-le depuis",
"here": "ici",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "Un file transfer ed explorer ricco di funzionalità con supporto per SFTP/SCP/FTP/S3",
"getStarted": "Installa termscp →",
"versionAlert": "termscp 0.16.0 è ORA disponbile! Scaricalo da",
"versionAlert": "termscp 0.19.0 è ORA disponbile! Scaricalo da",
"here": "qui",
"features": {
"handy": {

View File

@@ -12,7 +12,7 @@
"intro": {
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
"getStarted": "开始 →",
"versionAlert": "termscp 0.16.0 现已发布! 从下载",
"versionAlert": "termscp 0.19.0 现已发布! 从下载",
"here": "这里",
"features": {
"handy": {

File diff suppressed because one or more lines are too long

View File

@@ -50,7 +50,7 @@ pub struct ActivityManager {
impl ActivityManager {
/// Initializes a new Activity Manager
pub fn new(ticks: Duration) -> Result<ActivityManager, HostError> {
pub fn new(ticks: Duration, keyring: bool) -> Result<ActivityManager, HostError> {
// Prepare Context
// Initialize configuration client
let (config_client, error_config): (ConfigClient, Option<String>) =
@@ -61,7 +61,7 @@ impl ActivityManager {
(ConfigClient::degraded(), Some(err))
}
};
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client() {
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) {
Ok(cli) => (cli, None),
Err(err) => (None, Some(err)),
};
@@ -90,13 +90,18 @@ impl ActivityManager {
)),
host_params.password.as_deref(),
),
Remote::None => self.set_host_params(
HostParams::HostBridge(HostBridgeParams::Localhost(
env::current_dir()
.map_err(|e| format!("Could not get current directory: {e}"))?,
)),
None,
),
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
@@ -243,7 +248,7 @@ impl ActivityManager {
None => {
return Err(format!(
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
))
));
}
Some(params) => params,
};
@@ -342,7 +347,7 @@ impl ActivityManager {
fn run_filetransfer(&mut self) -> Option<NextActivity> {
info!("Starting FileTransferActivity");
// Get context
let ctx: Context = match self.context.take() {
let mut ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => {
error!("Failed to start FileTransferActivity: context is None");
@@ -367,8 +372,18 @@ impl ActivityManager {
}
};
let mut activity: FileTransferActivity =
FileTransferActivity::new(host_bridge_params, remote_params, self.ticks);
// try to setup activity
let mut activity =
match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
Ok(activity) => activity,
Err(err) => {
error!("Failed to start FileTransferActivity: {}", err);
ctx.set_error(err);
self.context = Some(ctx);
// Return to authentication
return Some(NextActivity::Authentication);
}
};
// Prepare result
let result: Option<NextActivity>;
// Create activity
@@ -432,31 +447,8 @@ impl ActivityManager {
// -- misc
fn init_bookmarks_client() -> Result<Option<BookmarksClient>, String> {
// Get config dir
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
BookmarksClient::new(bookmarks_file.as_path(), config_dir_path.as_path(), 16)
.map(Option::Some)
.map_err(|e| {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
e
)
})
} else {
Ok(None)
}
}
Err(err) => Err(err),
}
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
crate::support::bookmarks_client(keyring)
}
/// Initialize configuration client
@@ -495,19 +487,28 @@ impl ActivityManager {
match ThemeProvider::new(theme_path.as_path()) {
Ok(provider) => provider,
Err(err) => {
error!("Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err);
error!(
"Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode",
theme_path.display(),
err
);
ThemeProvider::degraded()
}
}
}
None => {
error!("This system doesn't provide a configuration directory; using theme provider in degraded mode");
error!(
"This system doesn't provide a configuration directory; using theme provider in degraded mode"
);
ThemeProvider::degraded()
}
}
}
Err(err) => {
error!("Could not initialize configuration directory: {}; using theme provider in degraded mode", err);
error!(
"Could not initialize configuration directory: {}; using theme provider in degraded mode",
err
);
ThemeProvider::degraded()
}
}

View File

@@ -15,8 +15,12 @@ use crate::system::logging::LogLevel;
pub enum Task {
Activity(NextActivity),
/// Import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
ImportSshHosts(Option<PathBuf>),
ImportTheme(PathBuf),
InstallUpdate,
Version,
}
#[derive(Default, FromArgs)]
@@ -59,6 +63,9 @@ pub struct Args {
/// print version
#[argh(switch, short = 'v')]
pub version: bool,
/// disable keyring support
#[argh(switch)]
pub wno_keyring: bool,
// -- positional
#[argh(positional, description = "address1 address2 local-wrkdir")]
pub positional: Vec<String>,
@@ -68,7 +75,8 @@ pub struct Args {
#[argh(subcommand)]
pub enum ArgsSubcommands {
Config(ConfigArgs),
LoadTheme(LoadThemeArgs),
ImportSshHosts(ImportSshHostsArgs),
ImportTheme(ImportThemeArgs),
Update(UpdateArgs),
}
@@ -82,10 +90,20 @@ pub struct ConfigArgs {}
#[argh(subcommand, name = "update")]
pub struct UpdateArgs {}
#[derive(FromArgs)]
/// import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
#[argh(subcommand, name = "import-ssh-hosts")]
pub struct ImportSshHostsArgs {
#[argh(positional)]
/// optional ssh config file; if not specified, the default location will be used
pub ssh_config: Option<PathBuf>,
}
#[derive(FromArgs)]
/// import the specified theme
#[argh(subcommand, name = "theme")]
pub struct LoadThemeArgs {
pub struct ImportThemeArgs {
#[argh(positional)]
/// theme file
pub theme: PathBuf,
@@ -93,6 +111,7 @@ pub struct LoadThemeArgs {
pub struct RunOpts {
pub remote: RemoteArgs,
pub keyring: bool,
pub ticks: Duration,
pub log_level: LogLevel,
pub task: Task,
@@ -113,6 +132,14 @@ impl RunOpts {
}
}
pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Self {
Self {
task: Task::ImportSshHosts(ssh_config),
keyring,
..Default::default()
}
}
pub fn import_theme(theme: PathBuf) -> Self {
Self {
task: Task::ImportTheme(theme),
@@ -126,6 +153,7 @@ impl Default for RunOpts {
Self {
remote: RemoteArgs::default(),
ticks: Duration::from_millis(10),
keyring: true,
log_level: LogLevel::Info,
task: Task::Activity(NextActivity::Authentication),
}

View File

@@ -180,7 +180,7 @@ mod test {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_should_make_remote_args_from_two_remotes_and_local_dir() {
let args = Args {
positional: vec![
@@ -224,7 +224,7 @@ mod test {
}
#[test]
#[cfg(unix)]
#[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()],
@@ -254,7 +254,7 @@ mod test {
}
#[test]
#[cfg(unix)]
#[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()],

View File

@@ -108,9 +108,9 @@ impl From<FileTransferParams> for Bookmark {
smb: Some(SmbParams::from(params.clone())),
protocol,
address: Some(params.address),
#[cfg(unix)]
#[cfg(posix)]
port: Some(params.port),
#[cfg(windows)]
#[cfg(win)]
port: None,
username: params.username,
password: params.password,
@@ -159,7 +159,7 @@ impl From<Bookmark> for FileTransferParams {
let params = KubeProtocolParams::from(params);
Self::new(bookmark.protocol, ProtocolParams::Kube(params))
}
#[cfg(unix)]
#[cfg(posix)]
FileTransferProtocol::Smb => {
let params = TransferSmbParams::new(
bookmark.address.unwrap_or_default(),
@@ -172,7 +172,7 @@ impl From<Bookmark> for FileTransferParams {
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
}
#[cfg(windows)]
#[cfg(win)]
FileTransferProtocol::Smb => {
let params = TransferSmbParams::new(
bookmark.address.unwrap_or_default(),
@@ -521,7 +521,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn should_get_ftparams_from_smb_bookmark() {
let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::Smb,
@@ -559,7 +559,7 @@ mod tests {
}
#[test]
#[cfg(windows)]
#[cfg(win)]
fn should_get_ftparams_from_smb_bookmark() {
let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::Smb,

View File

@@ -9,7 +9,7 @@ pub struct SmbParams {
pub workgroup: Option<String>,
}
#[cfg(unix)]
#[cfg(posix)]
impl From<TransferSmbParams> for SmbParams {
fn from(params: TransferSmbParams) -> Self {
Self {
@@ -19,7 +19,7 @@ impl From<TransferSmbParams> for SmbParams {
}
}
#[cfg(windows)]
#[cfg(win)]
impl From<TransferSmbParams> for SmbParams {
fn from(params: TransferSmbParams) -> Self {
Self {

View File

@@ -4,8 +4,8 @@
use std::io::{Read, Write};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde::de::DeserializeOwned;
use thiserror::Error;
/// Contains the error for serializer/deserializer
@@ -63,7 +63,7 @@ where
return Err(SerializerError::new_ex(
SerializerErrorKind::Serialization,
err.to_string(),
))
));
}
};
trace!("Serialized new bookmarks data: {}", data);
@@ -422,14 +422,14 @@ mod tests {
let host = hosts.bookmarks.get("smb").unwrap();
assert_eq!(host.address.as_deref().unwrap(), "localhost");
assert_eq!(host.port.unwrap(), 445);
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(host.username.as_deref().unwrap(), "test");
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(host.password.as_deref().unwrap(), "test");
let smb = host.smb.as_ref().unwrap();
assert_eq!(smb.share.as_str(), "temp");
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
}

View File

@@ -65,10 +65,10 @@ impl FileExplorerBuilder {
/// Set formatter for FileExplorer
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
if let Some(e) = self.explorer.as_mut() {
if let Some(fmt_str) = fmt_str {
e.fmt = Formatter::new(fmt_str);
}
if let Some(e) = self.explorer.as_mut()
&& let Some(fmt_str) = fmt_str
{
e.fmt = Formatter::new(fmt_str);
}
self
}

View File

@@ -11,7 +11,7 @@ use bytesize::ByteSize;
use lazy_regex::{Lazy, Regex};
use remotefs::File;
use unicode_width::UnicodeWidthStr;
#[cfg(unix)]
#[cfg(posix)]
use uzers::{get_group_by_gid, get_user_by_uid};
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
@@ -211,7 +211,7 @@ impl Formatter {
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(unix)]
#[cfg(posix)]
let group: String = match fsentry.metadata().gid {
Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(),
@@ -219,7 +219,7 @@ impl Formatter {
},
None => 0.to_string(),
};
#[cfg(windows)]
#[cfg(win)]
let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(),
None => 0.to_string(),
@@ -364,8 +364,16 @@ impl Formatter {
if fsentry.is_file() {
// Get byte size
let size: ByteSize = ByteSize(fsentry.metadata().size);
let mut fmt = size.display().si().to_string();
// pad with up to len 10
let pad = 10usize.saturating_sub(fmt.len());
for _ in 0..pad {
fmt.push(' ');
}
format!("{cur_str}{prefix}{fmt}")
// Add to cur str, prefix and the key value
format!("{cur_str}{prefix}{size:10}")
//format!("{cur_str}{prefix}{size:10}", size = size.display().si())
} else if fsentry.metadata().symlink.is_some() {
let size = ByteSize(
fsentry
@@ -376,7 +384,14 @@ impl Formatter {
.to_string_lossy()
.len() as u64,
);
format!("{cur_str}{prefix}{size:10}")
let mut fmt = size.display().si().to_string();
// pad with up to len 10
let pad = 10usize.saturating_sub(fmt.len());
for _ in 0..pad {
fmt.push(' ');
}
format!("{cur_str}{prefix}{fmt}")
} else {
// Add to cur str, prefix and the key value
format!("{cur_str}{prefix} ")
@@ -420,7 +435,7 @@ impl Formatter {
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(unix)]
#[cfg(posix)]
let username: String = match fsentry.metadata().uid {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
@@ -428,7 +443,7 @@ impl Formatter {
},
None => 0.to_string(),
};
#[cfg(windows)]
#[cfg(win)]
let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(),
None => 0.to_string(),
@@ -489,10 +504,7 @@ impl Formatter {
};
// Match format length: group 3
let fmt_len: Option<usize> = match &regex_match.get(3) {
Some(len) => match len.as_str().parse::<usize>() {
Ok(len) => Some(len),
Err(_) => None,
},
Some(len) => len.as_str().parse::<usize>().ok(),
None => None,
};
// Match format extra: group 2 + 1
@@ -592,19 +604,19 @@ mod tests {
mode: Some(UnixPex::from(0o644)),
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
"bar.txt -rw-r--r-- root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
"bar.txt -rw-r--r-- 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
@@ -623,19 +635,19 @@ mod tests {
mode: Some(UnixPex::from(0o644)),
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
"piroparoporoperoperupup… -rw-r--r-- root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
@@ -654,19 +666,19 @@ mod tests {
mode: None,
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? root 8.2 KB {}",
"bar.txt -????????? root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
"bar.txt -????????? 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
@@ -685,19 +697,19 @@ mod tests {
mode: None,
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
"bar.txt -????????? 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 KB {}",
"bar.txt -????????? 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
@@ -723,7 +735,7 @@ mod tests {
mode: Some(UnixPex::from(0o755)),
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -731,7 +743,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -754,7 +766,7 @@ mod tests {
mode: None,
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -762,7 +774,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
@@ -774,8 +786,9 @@ mod tests {
#[test]
fn test_fs_explorer_formatter_all_together_now() {
let formatter: Formatter =
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
let formatter: Formatter = Formatter::new(
"{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}",
);
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let entry = File {
@@ -792,12 +805,15 @@ mod tests {
mode: Some(UnixPex::from(0o755)),
},
};
assert_eq!(formatter.fmt(&entry), format!(
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
assert_eq!(
formatter.fmt(&entry),
format!(
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
// Directory without symlink
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
@@ -813,12 +829,15 @@ mod tests {
mode: Some(UnixPex::from(0o755)),
},
};
assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
// File with symlink
let entry = File {
path: PathBuf::from("/bar.txt"),
@@ -834,12 +853,15 @@ mod tests {
mode: Some(UnixPex::from(0o644)),
},
};
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
// File without symlink
let entry = File {
path: PathBuf::from("/bar.txt"),
@@ -855,16 +877,19 @@ mod tests {
mode: Some(UnixPex::from(0o644)),
},
};
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt 0 0 -rw-r--r-- 8.2 kB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn should_fmt_path() {
let t: SystemTime = SystemTime::now();
let entry = File {
@@ -896,7 +921,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn should_fmt_utf8_path() {
let t: SystemTime = SystemTime::now();
let entry = File {

View File

@@ -7,7 +7,7 @@ pub(crate) mod builder;
mod formatter;
// Locals
use std::cmp::Reverse;
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -42,14 +42,26 @@ pub enum GroupDirs {
/// File explorer states
pub struct FileExplorer {
pub wrkdir: PathBuf, // Current directory
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
pub(crate) stack_size: usize, // Directory stack size
pub(crate) file_sorting: FileSorting, // File sorting criteria
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
pub(crate) opts: ExplorerOpts, // Explorer options
pub(crate) fmt: Formatter, // File formatter
files: Vec<File>, // Files in directory
/// Current working directory
pub wrkdir: PathBuf,
/// Stack of visited directories
pub(crate) dirstack: VecDeque<PathBuf>,
/// Stack size
pub(crate) stack_size: usize,
/// Criteria to sort file
pub(crate) file_sorting: FileSorting,
/// defines how to group directories in the explorer
pub(crate) group_dirs: Option<GroupDirs>,
/// Explorer options
pub(crate) opts: ExplorerOpts,
/// Formatter for file entries
pub(crate) fmt: Formatter,
/// Is terminal open for this explorer?
terminal: bool,
/// Files in directory
files: Vec<File>,
/// files enqueued for transfer. Map between source and destination
transfer_queue: HashMap<PathBuf, PathBuf>, // transfer queue
}
impl Default for FileExplorer {
@@ -63,6 +75,8 @@ impl Default for FileExplorer {
opts: ExplorerOpts::empty(),
fmt: Formatter::default(),
files: Vec::new(),
terminal: false,
transfer_queue: HashMap::new(),
}
}
}
@@ -139,6 +153,44 @@ impl FileExplorer {
filtered.get(idx).copied()
}
/// Enqueue a file for transfer
pub fn enqueue(&mut self, src: &Path, dst: &Path) {
self.transfer_queue
.insert(PathBuf::from(src), PathBuf::from(dst));
}
/// Enqueue all files for transfer
pub fn enqueue_all(&mut self, dst: &Path) {
let files: Vec<_> = self.iter_files().map(|f| f.path.clone()).collect();
for file in files {
self.enqueue(&file, dst);
}
}
/// Get enqueued files
pub fn enqueued(&self) -> &HashMap<PathBuf, PathBuf> {
&self.transfer_queue
}
/// Dequeue a file
pub fn dequeue(&mut self, src: &Path) {
self.transfer_queue.remove(src);
}
/// Clear transfer queue
pub fn clear_queue(&mut self) {
self.transfer_queue.clear();
}
/// Toggle terminal state
pub fn toggle_terminal(&mut self, terminal: bool) {
self.terminal = terminal;
}
pub fn terminal_open(&self) -> bool {
self.terminal
}
// Formatting
/// Format a file entry
@@ -519,19 +571,19 @@ mod tests {
mode: Some(UnixPex::from(0o644)),
},
};
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 KB {}",
"bar.txt -rw-r--r-- root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(windows)]
#[cfg(win)]
assert_eq!(
explorer.fmt_file(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 KB {}",
"bar.txt -rw-r--r-- 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
@@ -586,6 +638,26 @@ mod tests {
assert_eq!(explorer.files.len(), 3);
}
#[test]
fn test_should_enqueue_and_dequeue_files() {
let mut explorer: FileExplorer = FileExplorer::default();
// Create files (files are then sorted by name)
explorer.set_files(vec![
make_fs_entry("CONTRIBUTING.md", false),
make_fs_entry("docs", true),
make_fs_entry("src", true),
make_fs_entry("README.md", false),
]);
// Enqueue
explorer.enqueue(Path::new("CONTRIBUTING.md"), Path::new("CONTRIBUTING.md"));
explorer.enqueue(Path::new("docs"), Path::new("docs"));
// Dequeue
explorer.dequeue(Path::new("CONTRIBUTING.md"));
assert_eq!(explorer.enqueued().len(), 1);
explorer.dequeue(Path::new("docs"));
assert_eq!(explorer.enqueued().len(), 0);
}
fn make_fs_entry(name: &str, is_dir: bool) -> File {
let t: SystemTime = SystemTime::now();
let metadata = Metadata {

View File

@@ -7,15 +7,19 @@ pub struct HostBridgeBuilder;
impl HostBridgeBuilder {
/// Build Host Bridge from parms
///
/// if protocol and parameters are inconsistent, the function will panic.
pub fn build(params: HostBridgeParams, config_client: &ConfigClient) -> Box<dyn HostBridge> {
/// 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) => {
Box::new(Localhost::new(path).expect("Failed to create Localhost"))
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>)
}
HostBridgeParams::Remote(protocol, params) => Box::new(RemoteBridged::from(
RemoteFsBuilder::build(protocol, params, config_client),
)),
}
}
}

View File

@@ -31,6 +31,16 @@ impl HostBridgeParams {
HostBridgeParams::Remote(_, params) => params,
}
}
/// Returns the host name for the bridge params
pub fn username(&self) -> Option<String> {
match self {
HostBridgeParams::Localhost(_) => Some(whoami::username()),
HostBridgeParams::Remote(_, params) => {
params.generic_params().and_then(|p| p.username.clone())
}
}
}
}
/// Holds connection parameters for file transfers
@@ -42,6 +52,15 @@ pub struct FileTransferParams {
pub local_path: Option<PathBuf>,
}
impl FileTransferParams {
/// Returns the remote path if set, otherwise returns the local path
pub fn username(&self) -> Option<String> {
self.params
.generic_params()
.and_then(|p| p.username.clone())
}
}
/// Container for protocol params
#[derive(Debug, Clone)]
pub enum ProtocolParams {
@@ -301,11 +320,13 @@ mod test {
#[test]
fn password_missing() {
assert!(FileTransferParams::new(
FileTransferProtocol::Scp,
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
)
.password_missing());
assert!(
FileTransferParams::new(
FileTransferProtocol::Scp,
ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test")))
)
.password_missing()
);
assert_eq!(
FileTransferParams::new(
FileTransferProtocol::Scp,
@@ -362,7 +383,7 @@ mod test {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn set_default_secret_smb() {
let mut params = FileTransferParams::new(
FileTransferProtocol::Scp,

View File

@@ -2,12 +2,12 @@
#[derive(Debug, Clone)]
pub struct SmbParams {
pub address: String,
#[cfg(unix)]
#[cfg(posix)]
pub port: u16,
pub share: String,
pub username: Option<String>,
pub password: Option<String>,
#[cfg(unix)]
#[cfg(posix)]
pub workgroup: Option<String>,
}
@@ -18,17 +18,17 @@ impl SmbParams {
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
Self {
address: address.as_ref().to_string(),
#[cfg(unix)]
#[cfg(posix)]
port: 445,
share: share.as_ref().to_string(),
username: None,
password: None,
#[cfg(unix)]
#[cfg(posix)]
workgroup: None,
}
}
#[cfg(unix)]
#[cfg(posix)]
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
@@ -44,7 +44,7 @@ impl SmbParams {
self
}
#[cfg(unix)]
#[cfg(posix)]
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
self.workgroup = workgroup.map(|x| x.to_string());
self
@@ -57,12 +57,12 @@ impl SmbParams {
}
/// Set password
#[cfg(unix)]
#[cfg(posix)]
pub fn set_default_secret(&mut self, secret: String) {
self.password = Some(secret);
}
#[cfg(windows)]
#[cfg(win)]
pub fn set_default_secret(&mut self, _secret: String) {}
}
@@ -78,20 +78,20 @@ mod test {
let params = SmbParams::new("localhost", "temp");
assert_eq!(&params.address, "localhost");
#[cfg(unix)]
#[cfg(posix)]
assert_eq!(params.port, 445);
assert_eq!(&params.share, "temp");
#[cfg(unix)]
#[cfg(posix)]
assert!(params.username.is_none());
#[cfg(unix)]
#[cfg(posix)]
assert!(params.password.is_none());
#[cfg(unix)]
#[cfg(posix)]
assert!(params.workgroup.is_none());
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn should_init_smb_params_with_optionals() {
let params = SmbParams::new("localhost", "temp")
.port(3456)
@@ -108,7 +108,7 @@ mod test {
}
#[test]
#[cfg(windows)]
#[cfg(win)]
fn should_init_smb_params_with_optionals() {
let params = SmbParams::new("localhost", "temp")
.username(Some("foo"))

View File

@@ -13,6 +13,10 @@ use remotefs_kube::KubeMultiPodFs as KubeFs;
use remotefs_smb::SmbOptions;
#[cfg(smb)]
use remotefs_smb::{SmbCredentials, SmbFs};
#[cfg(windows)]
use remotefs_ssh::LibSsh2Session as SshSession;
#[cfg(unix)]
use remotefs_ssh::LibSshSession as SshSession;
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
use remotefs_webdav::WebDAVFs;
@@ -37,40 +41,50 @@ impl RemoteFsBuilder {
protocol: FileTransferProtocol,
params: ProtocolParams,
config_client: &ConfigClient,
) -> Box<dyn RemoteFs> {
) -> Result<Box<dyn RemoteFs>, String> {
match (protocol, params) {
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
Box::new(Self::aws_s3_client(params))
Ok(Box::new(Self::aws_s3_client(params)))
}
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
Box::new(Self::ftp_client(params, secure))
Ok(Box::new(Self::ftp_client(params, secure)))
}
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
Box::new(Self::kube_client(params))
Ok(Box::new(Self::kube_client(params)))
}
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
Box::new(Self::scp_client(params, config_client))
Ok(Box::new(Self::scp_client(params, config_client)))
}
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
Box::new(Self::sftp_client(params, config_client))
Ok(Box::new(Self::sftp_client(params, config_client)))
}
#[cfg(smb)]
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
Box::new(Self::smb_client(params))
Ok(Box::new(Self::smb_client(params)))
}
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
Box::new(Self::webdav_client(params))
Ok(Box::new(Self::webdav_client(params)))
}
(protocol, params) => {
error!("Invalid params for protocol '{:?}'", protocol);
panic!("Invalid protocol '{protocol:?}' with parameters of type {params:?}")
Err(format!(
"Invalid protocol '{protocol:?}' with parameters of type {params:?}",
))
}
}
}
/// Build aws s3 client from parameters
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
let mut client = AwsS3Fs::new(params.bucket_name).new_path_style(params.new_path_style);
let rt = Arc::new(
tokio::runtime::Builder::new_current_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("Unable to create tokio runtime"),
);
let mut client =
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
if let Some(region) = params.region {
client = client.region(region);
}
@@ -128,12 +142,18 @@ impl RemoteFsBuilder {
}
/// Build scp client
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
fn scp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> ScpFs<SshSession> {
Self::build_ssh_opts(params, config_client).into()
}
/// Build sftp client
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
fn sftp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> SftpFs<SshSession> {
Self::build_ssh_opts(params, config_client).into()
}
@@ -223,7 +243,11 @@ impl RemoteFsBuilder {
debug!("no username was provided, using current username");
opts = opts.username(whoami::username());
}
if let Some(password) = params.password {
// For SSH protocols, only set password if explicitly provided and non-empty.
// This allows the SSH library to prioritize key-based and agent authentication.
if let Some(password) = params.password
&& !password.is_empty()
{
opts = opts.password(password);
}
if let Some(config_path) = config_client.get_ssh_config() {
@@ -262,7 +286,9 @@ mod test {
.session_token(Some("gerry-scotti")),
);
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
assert!(
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_ok()
);
}
#[test]
@@ -275,7 +301,9 @@ mod test {
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client);
assert!(
RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client).is_ok()
);
}
#[test]
@@ -288,7 +316,7 @@ mod test {
client_key: Some("client_key".to_string()),
});
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client);
assert!(RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client).is_ok());
}
#[test]
@@ -301,7 +329,7 @@ mod test {
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client);
assert!(RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client).is_ok());
}
#[test]
@@ -314,7 +342,7 @@ mod test {
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client);
assert!(RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client).is_ok());
}
#[test]
@@ -322,11 +350,10 @@ mod test {
fn should_build_smb_fs() {
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client);
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
}
#[test]
#[should_panic]
fn should_not_build_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
@@ -336,7 +363,9 @@ mod test {
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client);
assert!(
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_err()
);
}
fn get_config_client() -> ConfigClient {

View File

@@ -1,8 +1,8 @@
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use remotefs::fs::{Metadata, UnixPex};
use remotefs::File;
use remotefs::fs::{Metadata, UnixPex};
use super::HostResult;

View File

@@ -1,12 +1,12 @@
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
#[cfg(unix)]
#[cfg(posix)]
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use filetime::FileTime;
use remotefs::fs::{FileType, Metadata, UnixPex};
use remotefs::File;
use remotefs::fs::{FileType, Metadata, UnixPex};
use super::{HostBridge, HostResult};
use crate::host::{HostError, HostErrorType};
@@ -105,8 +105,8 @@ impl HostBridge for Localhost {
));
}
let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location
// Update working directory
// Change dir
// Update working directory
// Change dir
self.wrkdir = new_dir;
// Scan new directory
let pwd = self.pwd()?;
@@ -135,7 +135,7 @@ impl HostBridge for Localhost {
HostErrorType::FileAlreadyExists,
None,
dir_path.as_path(),
))
));
}
}
}
@@ -385,7 +385,7 @@ impl HostBridge for Localhost {
filetime::set_file_atime(path, atime)
.map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?;
}
#[cfg(unix)]
#[cfg(posix)]
if let Some(mode) = metadata.mode {
self.chmod(path, mode)?;
}
@@ -417,7 +417,7 @@ impl HostBridge for Localhost {
}
}
#[cfg(unix)]
#[cfg(posix)]
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
let src = self.to_path(src);
std::os::unix::fs::symlink(dst, src.as_path()).map_err(|e| {
@@ -431,14 +431,14 @@ impl HostBridge for Localhost {
})
}
#[cfg(windows)]
#[cfg(win)]
fn symlink(&mut self, _src: &Path, _dst: &Path) -> HostResult<()> {
warn!("Cannot create symlink on Windows");
Err(HostError::from(HostErrorType::NotImplemented))
}
#[cfg(unix)]
#[cfg(posix)]
fn chmod(&mut self, path: &std::path::Path, pex: UnixPex) -> HostResult<()> {
let path: PathBuf = self.to_path(path);
// Get metadta
@@ -476,7 +476,7 @@ impl HostBridge for Localhost {
}
}
#[cfg(windows)]
#[cfg(win)]
fn chmod(&mut self, _path: &std::path::Path, _pex: UnixPex) -> HostResult<()> {
warn!("Cannot set file mode on Windows");
@@ -553,19 +553,19 @@ impl HostBridge for Localhost {
#[cfg(test)]
mod tests {
#[cfg(unix)]
#[cfg(posix)]
use std::fs::File as StdFile;
#[cfg(unix)]
#[cfg(posix)]
use std::io::Write;
use std::ops::AddAssign;
#[cfg(unix)]
use std::os::unix::fs::{symlink, PermissionsExt};
#[cfg(posix)]
use std::os::unix::fs::{PermissionsExt, symlink};
use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::*;
#[cfg(unix)]
#[cfg(posix)]
use crate::utils::test_helpers::make_fsentry;
use crate::utils::test_helpers::{create_sample_file, make_file_at};
@@ -578,7 +578,8 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
#[cfg(not(feature = "isolated-tests"))]
fn test_host_localhost_new() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert_eq!(host.wrkdir, PathBuf::from("/dev"));
@@ -592,7 +593,7 @@ mod tests {
}
#[test]
#[cfg(windows)]
#[cfg(win)]
fn test_host_localhost_new() {
let mut host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap();
assert_eq!(host.wrkdir, PathBuf::from("C:\\users"));
@@ -614,14 +615,15 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_host_localhost_pwd() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert_eq!(host.pwd().unwrap(), PathBuf::from("/dev"));
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
#[cfg(not(feature = "isolated-tests"))]
fn test_host_localhost_change_dir() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
let new_dir: PathBuf = PathBuf::from("/dev");
@@ -637,7 +639,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
#[should_panic]
fn test_host_localhost_change_dir_failed() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
@@ -646,7 +648,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_host_localhost_open_read() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Create temp file
@@ -655,13 +657,14 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
#[should_panic]
fn test_host_localhost_open_read_err_no_such_file() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert!(host
.open_file(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path())
.is_ok());
assert!(
host.open_file(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path())
.is_ok()
);
}
#[test]
@@ -676,7 +679,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_host_localhost_open_write() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Create temp file
@@ -695,18 +698,20 @@ mod tests {
assert!(host.create_file(file.path(), &Metadata::default()).is_err());
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn test_host_localhost_symlinks() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
// Create symlink
assert!(symlink(
format!("{}/foo.txt", tmpdir.path().display()),
format!("{}/bar.txt", tmpdir.path().display())
)
.is_ok());
assert!(
symlink(
format!("{}/foo.txt", tmpdir.path().display()),
format!("{}/bar.txt", tmpdir.path().display())
)
.is_ok()
);
// Get dir
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.files.clone();
@@ -733,7 +738,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
@@ -742,23 +747,25 @@ mod tests {
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
// Try to re-create directory
// Try to re-create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
// Try abs path
assert!(host
.mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true)
.is_ok());
assert!(
host.mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true)
.is_ok()
);
// Fail
assert!(host
.mkdir_ex(
assert!(
host.mkdir_ex(
PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(),
true
)
.is_err());
.is_err()
);
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_host_localhost_remove() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file
@@ -766,28 +773,30 @@ mod tests {
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
// Remove file
// Remove file
assert!(host.remove(files.get(0).unwrap()).is_ok());
// There should be 0 files now
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 0); // There should be 0 files now
// Create directory
// Create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
// Delete directory
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
assert!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory
assert!(host
.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
.is_err());
assert!(host
.remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false))
.is_err());
assert!(
host.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
.is_err()
);
assert!(
host.remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false))
.is_err()
);
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_host_localhost_rename() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file
@@ -801,18 +810,20 @@ mod tests {
// Rename file
let dst_path: PathBuf =
PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
assert!(host
.rename(files.get(0).unwrap(), dst_path.as_path())
.is_ok());
assert!(
host.rename(files.get(0).unwrap(), dst_path.as_path())
.is_ok()
);
// There should be still 1 file now, but named bar.txt
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 0 files now
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
// Fail
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
assert!(host
.rename(files.get(0).unwrap(), bad_path.as_path())
.is_err());
assert!(
host.rename(files.get(0).unwrap(), bad_path.as_path())
.is_err()
);
}
#[test]
@@ -840,7 +851,7 @@ mod tests {
assert_eq!(new_metadata.metadata().modified, Some(new_mtime));
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn test_host_chmod() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -851,15 +862,16 @@ mod tests {
// Chmod to dir
assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok());
// Error
assert!(host
.chmod(
assert!(
host.chmod(
Path::new("/tmp/krgiogoiegj/kwrgnoerig"),
UnixPex::from(0o777)
)
.is_err());
.is_err()
);
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn test_host_copy_file_absolute() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -881,15 +893,16 @@ mod tests {
// Verify host has two files
assert_eq!(host.files.len(), 2);
// Fail copy
assert!(host
.copy(
assert!(
host.copy(
&make_fsentry(PathBuf::from("/a/a7/a/a7a"), false),
PathBuf::from("571k422i").as_path()
)
.is_err());
.is_err()
);
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn test_host_copy_file_relative() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -911,7 +924,7 @@ mod tests {
assert_eq!(host.files.len(), 2);
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn test_host_copy_directory_absolute() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -942,7 +955,7 @@ mod tests {
assert!(host.stat(test_file_path.as_path()).is_ok());
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn test_host_copy_directory_relative() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -980,7 +993,7 @@ mod tests {
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
}
#[cfg(unix)]
#[cfg(posix)]
#[test]
fn should_create_symlink() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -994,9 +1007,10 @@ mod tests {
assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok());
// Fail symlink
assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err());
assert!(host
.symlink(Path::new("/tmp/oooo/aaaa"), p.as_path())
.is_err());
assert!(
host.symlink(Path::new("/tmp/oooo/aaaa"), p.as_path())
.is_err()
);
}
#[test]

View File

@@ -44,7 +44,7 @@ pub enum HostErrorType {
}
/// HostError is a wrapper for the error type and the exact io error
#[derive(Debug)]
#[derive(Debug, Error)]
pub struct HostError {
pub error: HostErrorType,
ioerr: Option<std::io::Error>,

View File

@@ -22,7 +22,7 @@ extern crate log;
extern crate magic_crypt;
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::time::Duration;
use self::activity_manager::{ActivityManager, NextActivity};
@@ -33,24 +33,24 @@ const APP_NAME: &str = env!("CARGO_PKG_NAME");
const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
const EXIT_CODE_SUCCESS: i32 = 0;
const EXIT_CODE_ERROR: i32 = 1;
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
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() {
fn main() -> MainResult<()> {
let args: Args = argh::from_env();
// Parse args
let run_opts: RunOpts = match parse_args(args) {
Ok(opts) => opts,
Err(err) => {
eprintln!("{err}");
std::process::exit(255);
return Err(err.into());
}
};
// Setup logging
@@ -63,10 +63,7 @@ fn main() {
);
// Run
info!("Starting activity manager...");
let rc = run(run_opts);
info!("termscp terminated with exitcode {}", rc);
// Then return
std::process::exit(rc);
run(run_opts)
}
/// Parse arguments
@@ -75,16 +72,18 @@ fn main() {
fn parse_args(args: Args) -> Result<RunOpts, String> {
let run_opts = match args.nested {
Some(ArgsSubcommands::Update(_)) => RunOpts::update(),
Some(ArgsSubcommands::LoadTheme(args)) => RunOpts::import_theme(args.theme),
Some(ArgsSubcommands::ImportSshHosts(subargs)) => {
RunOpts::import_ssh_hosts(subargs.ssh_config, !args.wno_keyring)
}
Some(ArgsSubcommands::ImportTheme(args)) => RunOpts::import_theme(args.theme),
Some(ArgsSubcommands::Config(_)) => RunOpts::config(),
None => {
let mut run_opts: RunOpts = RunOpts::default();
// Version
if args.version {
return Err(format!(
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
git_hash = git_hash()
));
run_opts.task = Task::Version;
return Ok(run_opts);
}
// Logging
if args.debug {
@@ -92,6 +91,10 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
} else if args.quiet {
run_opts.log_level = LogLevel::Off;
}
// set keyring
if args.wno_keyring {
run_opts.keyring = false;
}
// Match ticks
run_opts.ticks = Duration::from_millis(args.ticks);
// Remote argument
@@ -111,10 +114,10 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
};
// Local directory
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
if let Err(err) = env::set_current_dir(localdir) {
return Err(format!("Bad working directory argument: {err}"));
}
if let Some(localdir) = run_opts.remote.local_dir.as_deref()
&& let Err(err) = env::set_current_dir(localdir)
{
return Err(format!("Bad working directory argument: {err}"));
}
run_opts
@@ -125,57 +128,86 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
}
/// Run task and return rc
fn run(run_opts: RunOpts) -> i32 {
fn run(run_opts: RunOpts) -> MainResult<()> {
match run_opts.task {
Task::ImportSshHosts(ssh_config) => run_import_ssh_hosts(ssh_config, run_opts.keyring),
Task::ImportTheme(theme) => run_import_theme(&theme),
Task::InstallUpdate => run_install_update(),
Task::Activity(activity) => run_activity(activity, run_opts.ticks, run_opts.remote),
Task::Activity(activity) => {
run_activity(activity, run_opts.ticks, run_opts.remote, run_opts.keyring)
}
Task::Version => print_version(),
}
}
fn run_import_theme(theme: &Path) -> i32 {
fn print_version() -> MainResult<()> {
println!(
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
git_hash = git_hash()
);
Ok(())
}
fn run_import_ssh_hosts(ssh_config_path: Option<PathBuf>, keyring: bool) -> MainResult<()> {
support::import_ssh_hosts(ssh_config_path, keyring)
.map(|_| {
println!("SSH hosts have been successfully imported!");
})
.map_err(|err| {
eprintln!("{err}");
err.into()
})
}
fn run_import_theme(theme: &Path) -> MainResult<()> {
match support::import_theme(theme) {
Ok(_) => {
println!("Theme has been successfully imported!");
EXIT_CODE_ERROR
Ok(())
}
Err(err) => {
eprintln!("{err}");
EXIT_CODE_ERROR
Err(err.into())
}
}
}
fn run_install_update() -> i32 {
fn run_install_update() -> MainResult<()> {
match support::install_update() {
Ok(msg) => {
println!("{msg}");
EXIT_CODE_SUCCESS
Ok(())
}
Err(err) => {
eprintln!("Could not install update: {err}");
EXIT_CODE_ERROR
Err(err.into())
}
}
}
fn run_activity(activity: NextActivity, ticks: Duration, remote_args: RemoteArgs) -> i32 {
fn run_activity(
activity: NextActivity,
ticks: Duration,
remote_args: RemoteArgs,
keyring: bool,
) -> MainResult<()> {
// Create activity manager (and context too)
let mut manager: ActivityManager = match ActivityManager::new(ticks) {
let mut manager: ActivityManager = match ActivityManager::new(ticks, keyring) {
Ok(m) => m,
Err(err) => {
eprintln!("Could not start activity manager: {err}");
return EXIT_CODE_ERROR;
return Err(err.into());
}
};
// Set file transfer params if set
if let Err(err) = manager.configure_remote_args(remote_args) {
eprintln!("{err}");
return EXIT_CODE_ERROR;
return Err(err.into());
}
manager.run(activity);
EXIT_CODE_SUCCESS
Ok(())
}

View File

@@ -2,11 +2,14 @@
//!
//! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes
// mod
mod import_ssh_hosts;
use std::fs;
use std::path::{Path, PathBuf};
pub use self::import_ssh_hosts::import_ssh_hosts;
use crate::system::auto_update::{Update, UpdateStatus};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::system::notifications::Notification;
@@ -79,10 +82,40 @@ fn get_config_client() -> Option<ConfigClient> {
Err(_) => None,
Ok(dir) => {
let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path());
match ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()) {
Err(_) => None,
Ok(c) => Some(c),
}
ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()).ok()
}
}
}
/// Init [`BookmarksClient`].
pub fn bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
// Get config dir
match environment::init_config_dir() {
Ok(path) => {
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
if let Some(config_dir_path) = path {
let bookmarks_file: PathBuf =
environment::get_bookmarks_paths(config_dir_path.as_path());
// Initialize client
BookmarksClient::new(
bookmarks_file.as_path(),
config_dir_path.as_path(),
16,
keyring,
)
.map(Option::Some)
.map_err(|e| {
format!(
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
bookmarks_file.display(),
config_dir_path.display(),
e
)
})
} else {
Ok(None)
}
}
Err(err) => Err(err),
}
}

View File

@@ -0,0 +1,326 @@
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use ssh2_config::{Host, HostClause, ParseRule, SshConfig};
use crate::filetransfer::params::GenericProtocolParams;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol, ProtocolParams};
/// Parameters required to add an ssh key for a host.
struct SshKeyParams {
host: String,
ssh_key: String,
username: String,
}
/// Import ssh hosts from the specified ssh config file, or from the default location
/// and save them as bookmarks.
pub fn import_ssh_hosts(ssh_config: Option<PathBuf>, keyring: bool) -> Result<(), String> {
// get config client
let mut cfg_client = super::get_config_client()
.ok_or_else(|| String::from("Could not import ssh hosts: could not load configuration"))?;
// resolve ssh_config
let ssh_config = ssh_config.or_else(|| cfg_client.get_ssh_config().map(PathBuf::from));
// load bookmarks client
let mut bookmarks_client = super::bookmarks_client(keyring)?
.ok_or_else(|| String::from("Could not import ssh hosts: could not load bookmarks"))?;
// load ssh config
let ssh_config = match ssh_config {
Some(p) => {
debug!("Importing ssh hosts from file: {}", p.display());
let mut reader = BufReader::new(
File::open(&p)
.map_err(|e| format!("Could not open ssh config file {}: {e}", p.display()))?,
);
SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
}
None => {
debug!("Importing ssh hosts from default location");
SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)
}
}
.map_err(|e| format!("Could not parse ssh config file: {e}"))?;
// iter hosts and add bookmarks
ssh_config
.get_hosts()
.iter()
.flat_map(host_to_params)
.for_each(|(name, params, identity_file_params)| {
debug!("Adding bookmark for host: {name} with params: {params:?}");
bookmarks_client.add_bookmark(name, params, false);
// add ssh key if any
if let Some(identity_file_params) = identity_file_params {
debug!(
"Host {host} has identity file, will add ssh key for it",
host = identity_file_params.host
);
if let Err(err) = cfg_client.add_ssh_key(
&identity_file_params.host,
&identity_file_params.username,
&identity_file_params.ssh_key,
) {
error!(
"Could not add ssh key for host {host}: {err}",
host = identity_file_params.host
);
}
}
});
// save bookmarks
if let Err(err) = bookmarks_client.write_bookmarks() {
return Err(format!(
"Could not save imported ssh hosts as bookmarks: {err}"
));
}
println!("Imported ssh hosts");
Ok(())
}
/// Tries to derive [`FileTransferParams`] from the specified ssh host.
fn host_to_params(
host: &Host,
) -> impl Iterator<Item = (String, FileTransferParams, Option<SshKeyParams>)> {
host.pattern
.iter()
.filter_map(|pattern| host_pattern_to_params(host, pattern))
}
/// Tries to derive [`FileTransferParams`] from the specified ssh host and pattern.
///
/// If `IdentityFile` is specified in the host parameters, it will be included in the returned tuple.
fn host_pattern_to_params(
host: &Host,
pattern: &HostClause,
) -> Option<(String, FileTransferParams, Option<SshKeyParams>)> {
debug!("Processing host with pattern: {pattern:?}",);
if pattern.negated || pattern.pattern.contains('*') || pattern.pattern.contains('?') {
debug!("Skipping host with pattern: {pattern}",);
return None;
}
let address = host
.params
.host_name
.as_deref()
.unwrap_or(pattern.pattern.as_str())
.to_string();
debug!("Resolved address for pattern {pattern}: {address}");
let port = host.params.port.unwrap_or(22);
debug!("Resolved port for pattern {pattern}: {port}");
let username = host.params.user.clone();
debug!("Resolved username for pattern {pattern}: {username:?}");
let identity_file_params = resolve_identity_file_path(host, pattern, &address);
Some((
pattern.to_string(),
FileTransferParams::new(
FileTransferProtocol::Sftp,
ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(username),
),
),
identity_file_params,
))
}
fn resolve_identity_file_path(
host: &Host,
pattern: &HostClause,
resolved_address: &str,
) -> Option<SshKeyParams> {
let (Some(username), Some(identity_file)) = (
host.params.user.as_ref(),
host.params.identity_file.as_ref().and_then(|v| v.first()),
) else {
debug!(
"No identity file specified for host {host}, skipping ssh key import",
host = pattern.pattern
);
return None;
};
// expand tilde
let identity_filepath = shellexpand::tilde(&identity_file.display().to_string()).to_string();
debug!("Resolved identity file for pattern {pattern}: {identity_filepath}",);
let Ok(mut ssh_file) = File::open(identity_file) else {
error!(
"Could not open identity file {identity_filepath} for host {host}",
host = pattern.pattern
);
return None;
};
let mut ssh_key = String::new();
use std::io::Read as _;
if let Err(err) = ssh_file.read_to_string(&mut ssh_key) {
error!(
"Could not read identity file {identity_filepath} for host {host}: {err}",
host = pattern.pattern
);
return None;
}
Some(SshKeyParams {
host: resolved_address.to_string(),
username: username.clone(),
ssh_key,
})
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use tempfile::NamedTempFile;
use super::*;
use crate::system::bookmarks_client::BookmarksClient;
#[test]
fn test_should_import_ssh_hosts() {
let ssh_test_config = ssh_test_config();
// import ssh hosts
let result = import_ssh_hosts(Some(ssh_test_config.config.path().to_path_buf()), false);
assert!(result.is_ok());
// verify imported hosts
let config_client = super::super::get_config_client()
.ok_or_else(|| String::from("Could not import ssh hosts: could not load configuration"))
.expect("failed to load config client");
// load bookmarks client
let bookmarks_client = super::super::bookmarks_client(false)
.expect("failed to load bookmarks client")
.expect("bookmarks client is none");
// verify bookmarks
check_bookmark(&bookmarks_client, "test1", "test1.example.com", 2200, None);
check_bookmark(
&bookmarks_client,
"test2",
"test2.example.com",
22,
Some("test2user"),
);
check_bookmark(
&bookmarks_client,
"test3",
"test3.example.com",
2222,
Some("test3user"),
);
// verify ssh keys
let (host, username, _key) = config_client
.get_ssh_key("test3user@test3.example.com")
.expect("ssh key is missing for test3user@test3.example.com");
assert_eq!(host, "test3.example.com");
assert_eq!(username, "test3user");
}
fn check_bookmark(
bookmarks_client: &BookmarksClient,
name: &str,
expected_address: &str,
expected_port: u16,
expected_username: Option<&str>,
) {
// verify bookmarks
let bookmark = bookmarks_client
.get_bookmark(name)
.expect("failed to get bookmark");
let params1 = bookmark
.params
.generic_params()
.expect("should have generic params");
assert_eq!(params1.address, expected_address);
assert_eq!(params1.port, expected_port);
assert_eq!(params1.username.as_deref(), expected_username);
assert!(params1.password.is_none());
}
struct SshTestConfig {
config: NamedTempFile,
#[allow(dead_code)]
identity_file: NamedTempFile,
}
fn ssh_test_config() -> SshTestConfig {
use std::io::Write as _;
let mut identity_file = NamedTempFile::new().expect("failed to create tempfile");
writeln!(
identity_file,
r"-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK
9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww
5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I
oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N
nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm
HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC
m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO
H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe
SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv
B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys
FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm
MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd
SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq
6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1
GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK
5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0
w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f
4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd
tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o
Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ
ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb
3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e
TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59
RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
-----END OPENSSH PRIVATE KEY-----"
)
.expect("failed to write identity file");
let mut file = NamedTempFile::new().expect("failed to create tempfile");
// let's declare a couple of hosts
writeln!(
file,
r#"
Host test1
HostName test1.example.com
Port 2200
Host test2
HostName test2.example.com
User test2user
Host test3
HostName test3.example.com
User test3user
Port 2222
IdentityFile {identity_path}
"#,
identity_path = identity_file.path().display()
)
.expect("failed to write ssh config");
SshTestConfig {
config: file,
identity_file,
}
}
}

View File

@@ -2,10 +2,12 @@
//!
//! Automatic update module. This module is used to upgrade the current version of termscp to the latest available on Github
use std::net::ToSocketAddrs as _;
use self_update::backends::github::Update as GithubUpdater;
pub use self_update::errors::Error as UpdateError;
use self_update::update::Release as UpdRelease;
use self_update::{cargo_crate_version, Status};
use self_update::{Status, cargo_crate_version};
use crate::utils::parser::parse_semver;
@@ -67,6 +69,9 @@ impl Update {
/// otherwise if no version is available, return None
/// In case of error returns Error with the error description
pub fn is_new_version_available() -> Result<Option<Release>, UpdateError> {
// check if api.github.com is reachable before doing anything
Self::check_github_api_reachable()?;
info!("Checking whether a new version is available...");
GithubUpdater::configure()
// Set default options
@@ -83,6 +88,27 @@ impl Update {
.map(Self::check_version)
}
/// Check if api.github.com is reachable
/// This is useful to avoid long timeouts when the network is down
/// or the DNS is not working
fn check_github_api_reachable() -> Result<(), UpdateError> {
let Some(socket_addr) = ("api.github.com", 443)
.to_socket_addrs()
.ok()
.and_then(|mut i| i.next())
else {
error!("Could not resolve api.github.com");
return Err(UpdateError::Network(
"Could not resolve api.github.com".into(),
));
};
// just try to open a connection to api.github.com with a timeout of 5 seconds with tcp
std::net::TcpStream::connect_timeout(&socket_addr, std::time::Duration::from_secs(5))
.map(|_| ())
.map_err(|e| UpdateError::Network(format!("Could not reach api.github.com: {e}")))
}
/// In case received version is newer than current one, version as Some is returned; otherwise None
fn check_version(r: Release) -> Option<Release> {
debug!("got version from GitHub: {}", r.version);
@@ -212,4 +238,9 @@ mod test {
assert!(!Update::is_new_version_higher("0.9.9", "0.10.1"));
assert!(!Update::is_new_version_higher("0.10.9", "0.11.0"));
}
#[test]
fn test_should_check_whether_github_api_is_reachable() {
assert!(Update::check_github_api_reachable().is_ok());
}
}

View File

@@ -10,13 +10,12 @@ use std::string::ToString;
use std::time::SystemTime;
use super::keys::filestorage::FileStorage;
#[cfg(feature = "with-keyring")]
use super::keys::keyringstorage::KeyringStorage;
use super::keys::{KeyStorage, KeyStorageError};
// Local
use crate::config::{
bookmarks::{Bookmark, UserHosts},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
serialization::{SerializerError, SerializerErrorKind, deserialize, serialize},
};
use crate::filetransfer::FileTransferParams;
use crate::utils::crypto;
@@ -39,42 +38,13 @@ impl BookmarksClient {
bookmarks_file: &Path,
storage_path: &Path,
recents_size: usize,
keyring: bool,
) -> Result<BookmarksClient, SerializerError> {
// Create default hosts
let default_hosts: UserHosts = UserHosts::default();
debug!("Setting up bookmarks client...");
// Make a key storage (with-keyring)
#[cfg(feature = "with-keyring")]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
debug!("Setting up KeyStorage");
let username: String = whoami::username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
// Check if keyring storage is supported
#[cfg(not(test))]
let app_name: &str = "termscp";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "termscp-test";
match storage.is_supported() {
true => {
debug!("Using KeyringStorage");
(Box::new(storage), app_name)
}
false => {
warn!("KeyringStorage is not supported; using FileStorage");
(Box::new(FileStorage::new(storage_path)), "bookmarks")
}
}
};
// Make a key storage (wno-keyring)
#[cfg(not(feature = "with-keyring"))]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
#[cfg(not(test))]
let app_name: &str = "bookmarks";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "bookmarks-test";
debug!("Using FileStorage");
(Box::new(FileStorage::new(storage_path)), app_name)
};
// Get key storage
let (key_storage, service_id) = Self::keyring(storage_path, keyring);
// Load key
let key: String = match key_storage.get_key(service_id) {
Ok(k) => {
@@ -130,6 +100,37 @@ impl BookmarksClient {
Ok(client)
}
/// Get the key storage
fn keyring(storage_path: &Path, keyring: bool) -> (Box<dyn KeyStorage>, &'static str) {
if keyring && cfg!(feature = "keyring") {
debug!("Setting up KeyStorage");
let username: String = whoami::username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
// Check if keyring storage is supported
#[cfg(not(test))]
let app_name: &str = "termscp";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "termscp-test";
match storage.is_supported() {
true => {
debug!("Using KeyringStorage");
(Box::new(storage), app_name)
}
false => {
warn!("KeyringStorage is not supported; using FileStorage");
(Box::new(FileStorage::new(storage_path)), "bookmarks")
}
}
} else {
#[cfg(not(test))]
let app_name: &str = "bookmarks";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "bookmarks-test";
debug!("Using FileStorage");
(Box::new(FileStorage::new(storage_path)), app_name)
}
}
/// Iterate over bookmarks keys
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.hosts.bookmarks.keys())
@@ -389,7 +390,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Verify client
assert_eq!(client.hosts.bookmarks.len(), 0);
assert_eq!(client.hosts.recents.len(), 0);
@@ -405,7 +406,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add some bookmarks
client.add_bookmark(
"raspberry",
@@ -430,7 +431,7 @@ mod tests {
let key: String = client.key.clone();
// Re-initialize a client
let client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Verify it loaded parameters correctly
assert_eq!(client.key, key);
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
@@ -453,7 +454,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add s3 bookmark
client.add_bookmark("my-bucket", make_s3_ftparams(), true);
// Verify bookmark
@@ -473,7 +474,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add s3 bookmark
client.add_bookmark("my-bucket", make_s3_ftparams(), false);
// Verify bookmark
@@ -494,7 +495,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add s3 bookmark
client.add_recent(make_s3_ftparams());
// Verify bookmark
@@ -517,7 +518,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add bookmark
client.add_bookmark(
"raspberry",
@@ -568,7 +569,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add bookmark
client.add_bookmark(
"",
@@ -589,7 +590,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add bookmark
client.add_bookmark(
"raspberry",
@@ -617,7 +618,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add bookmark
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
@@ -653,7 +654,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add bookmark
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
@@ -680,7 +681,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2, true).unwrap();
// Add recent, wait 1 second for each one (cause the name depends on time)
// 1
client.add_recent(make_generic_ftparams(
@@ -748,7 +749,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
// Add bookmark
client.add_bookmark(
"",
@@ -769,7 +770,7 @@ mod tests {
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
client.key = "MYSUPERSECRETKEY".to_string();
assert_eq!(
client.decrypt_str("z4Z6LpcpYqBW4+bkIok+5A==").ok().unwrap(),

View File

@@ -4,14 +4,14 @@
// Locals
// Ext
use std::fs::{create_dir, remove_file, File, OpenOptions};
use std::fs::{File, OpenOptions, create_dir, remove_file};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use crate::config::params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD};
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
use crate::config::params::{DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD, UserConfig};
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol;
@@ -153,10 +153,7 @@ impl ConfigClient {
// Convert string to `GroupDirs`
match &self.config.user_interface.group_dirs {
None => None,
Some(val) => match GroupDirs::from_str(val.as_str()) {
Ok(val) => Some(val),
Err(_) => None,
},
Some(val) => GroupDirs::from_str(val.as_str()).ok(),
}
}
@@ -303,19 +300,18 @@ impl ConfigClient {
/// Get ssh key from host.
/// None is returned if key doesn't exist
/// `std::io::Error` is returned in case it was not possible to read the key file
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
pub fn get_ssh_key(&self, mkey: &str) -> Option<SshHost> {
if self.degraded {
return Ok(None);
return None;
}
// Check if Key exists
match self.config.remote.ssh_keys.get(mkey) {
None => Ok(None),
None => None,
Some(key_path) => {
// Get host and username
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
// Return key
Ok(Some((host, username, PathBuf::from(key_path))))
Some((host, username, PathBuf::from(key_path)))
}
}
}
@@ -454,7 +450,7 @@ mod tests {
// I/O
assert!(client.add_ssh_key("Omar", "omar", "omar").is_err());
assert!(client.del_ssh_key("omar", "omar").is_err());
assert!(client.get_ssh_key("omar").ok().unwrap().is_none());
assert!(client.get_ssh_key("omar").is_none());
assert!(client.write_config().is_err());
assert!(client.read_config().is_err());
}
@@ -480,9 +476,11 @@ mod tests {
// Change some stuff
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
client.set_default_protocol(FileTransferProtocol::Scp);
assert!(client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok());
assert!(
client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok()
);
assert!(client.write_config().is_ok());
// Istantiate a new client
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
@@ -494,7 +492,7 @@ mod tests {
let mut expected_key_path: PathBuf = key_path;
expected_key_path.push("pi@192.168.1.31.key");
assert_eq!(
client.get_ssh_key("pi@192.168.1.31").unwrap().unwrap(),
client.get_ssh_key("pi@192.168.1.31").unwrap(),
(
String::from("192.168.1.31"),
String::from("pi"),
@@ -678,12 +676,14 @@ mod tests {
.unwrap();
// Add a new key
let rsa_key: String = get_sample_rsa_key();
assert!(client
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
.is_ok());
assert!(
client
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
.is_ok()
);
// Iterate keys
for key in client.iter_ssh_keys() {
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
let host: SshHost = client.get_ssh_key(key).unwrap();
assert_eq!(host.0, String::from("192.168.1.31"));
assert_eq!(host.1, String::from("pi"));
let mut expected_key_path: PathBuf = key_path.clone();
@@ -698,7 +698,7 @@ mod tests {
assert_eq!(key, rsa_key);
}
// Unexisting key
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
assert!(client.get_ssh_key("test").is_none());
// Delete key
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
}

View File

@@ -76,14 +76,14 @@ impl KeyStorage for KeyringStorage {
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use whoami::username;
use super::*;
#[test]
#[cfg(not(feature = "isolated-tests"))]
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
fn test_system_keys_keyringstorage() {
use pretty_assertions::assert_eq;
use whoami::username;
use super::*;
let username: String = username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
assert!(storage.is_supported());

View File

@@ -4,29 +4,24 @@
// Storages
pub mod filestorage;
#[cfg(feature = "with-keyring")]
pub mod keyringstorage;
// ext
#[cfg(feature = "with-keyring")]
use keyring::Error as KeyringError;
use thiserror::Error;
/// defines the error type for the `KeyStorage`
#[derive(Debug, Error)]
pub enum KeyStorageError {
#[cfg(feature = "with-keyring")]
#[error("Key has a bad syntax")]
BadSytax,
#[error("Provider service error")]
ProviderError,
#[error("No such key")]
NoSuchKey,
#[cfg(feature = "with-keyring")]
#[error("keyring error: {0}")]
KeyringError(KeyringError),
}
#[cfg(feature = "with-keyring")]
impl From<KeyringError> for KeyStorageError {
fn from(e: KeyringError) -> Self {
Self::KeyringError(e)
@@ -58,7 +53,6 @@ mod tests {
#[test]
fn test_system_keys_mod_errors() {
#[cfg(feature = "with-keyring")]
assert_eq!(
KeyStorageError::BadSytax.to_string(),
String::from("Key has a bad syntax")

View File

@@ -16,7 +16,7 @@ pub fn init(level: LogLevel) -> Result<(), String> {
Ok(None) => {
return Err(String::from(
"This system doesn't seem to support CACHE_DIR",
))
));
}
Err(err) => return Err(err),
};
@@ -29,6 +29,9 @@ pub fn init(level: LogLevel) -> Result<(), String> {
.set_time_format_rfc3339()
.add_filter_allow_str("termscp")
.add_filter_allow_str("remotefs")
.add_filter_allow_str("kube")
.add_filter_allow_str("suppaftp")
.add_filter_allow_str("pavao")
.build();
// Make logger
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))

View File

@@ -44,15 +44,29 @@ impl SshKeyStorage {
/// Resolve host via ssh2 configuration
fn resolve_host_in_ssh2_configuration(&self, host: &str) -> Option<PathBuf> {
self.ssh_config.as_ref().and_then(|x| {
let key = x
.query(host)
x.query(host)
.identity_file
.as_ref()
.and_then(|x| x.first().cloned());
key
.and_then(|x| x.first().cloned())
})
}
/// Get default SSH identity files that SSH would normally try
/// This mirrors the behavior of OpenSSH client
fn get_default_identity_files(&self) -> Vec<PathBuf> {
let Some(home_dir) = dirs::home_dir() else {
return Vec::new();
};
let ssh_dir = home_dir.join(".ssh");
// Standard SSH identity files in order of preference (matches OpenSSH)
["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"]
.iter()
.map(|key_name| ssh_dir.join(key_name))
.filter(|key_path| key_path.exists())
.collect()
}
}
impl SshKeyStorageTrait for SshKeyStorage {
@@ -66,9 +80,13 @@ impl SshKeyStorageTrait for SshKeyStorage {
username, host
);
// otherwise search in configuration
let key = self.resolve_host_in_ssh2_configuration(host)?;
debug!("Found key in SSH config for {host}: {}", key.display());
Some(key)
if let Some(key) = self.resolve_host_in_ssh2_configuration(host) {
debug!("Found key in SSH config for {host}: {}", key.display());
return Some(key);
}
// As a final fallback, try default SSH identity files (like regular ssh does)
self.get_default_identity_files().into_iter().next()
}
}
@@ -85,17 +103,11 @@ impl From<&ConfigClient> for SshKeyStorage {
// Iterate over keys in storage
for key in cfg_client.iter_ssh_keys() {
match cfg_client.get_ssh_key(key) {
Ok(host) => match host {
Some((addr, username, rsa_key_path)) => {
let key_name: String = Self::make_mapkey(&addr, &username);
hosts.insert(key_name, rsa_key_path);
}
None => continue,
},
Err(err) => {
error!("Failed to get SSH key for {}: {}", key, err);
continue;
Some((addr, username, rsa_key_path)) => {
let key_name: String = Self::make_mapkey(&addr, &username);
hosts.insert(key_name, rsa_key_path);
}
None => continue,
}
info!("Got SSH key for {}", key);
}
@@ -123,9 +135,11 @@ mod tests {
.ok()
.unwrap();
// Add ssh key
assert!(client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok());
assert!(
client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok()
);
// Create ssh key storage
let storage: SshKeyStorage = SshKeyStorage::from(&client);
// Verify key exists
@@ -135,13 +149,21 @@ mod tests {
*storage.resolve("192.168.1.31", "pi").unwrap(),
exp_key_path
);
// Verify unexisting key
assert!(storage.resolve("deskichup", "veeso").is_none());
// Verify key is a default key or none
let default_keys: Vec<PathBuf> = storage.get_default_identity_files().into_iter().collect();
if let Some(key) = storage.resolve("deskichup", "veeso") {
assert!(default_keys.contains(&key));
} else {
assert!(default_keys.is_empty());
}
}
#[test]
fn sould_resolve_key_from_ssh2_config() {
let rsa_key = test_helpers::create_sample_file_with_content("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a");
let rsa_key = test_helpers::create_sample_file_with_content(
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a",
);
let ssh_config_file = test_helpers::create_sample_file_with_content(format!(
r#"
Host test

View File

@@ -8,7 +8,7 @@ use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::string::ToString;
use crate::config::serialization::{deserialize, serialize, SerializerError, SerializerErrorKind};
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
use crate::config::themes::Theme;
/// ThemeProvider provides a high level API to communicate with the termscp theme

View File

@@ -7,7 +7,7 @@ mod change;
// -- export
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
use std::time::Duration;
pub use change::FsChange;
@@ -245,9 +245,11 @@ mod test {
fn should_watch_path() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
let tempdir = TempDir::new().unwrap();
assert!(watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok()
);
// check if in paths
assert_eq!(
watcher.paths.get(tempdir.path()).unwrap(),
@@ -261,16 +263,20 @@ mod test {
fn should_not_watch_path_if_subdir_of_watched_path() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
let tempdir = TempDir::new().unwrap();
assert!(watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok()
);
// watch subdir
let mut subdir = tempdir.path().to_path_buf();
subdir.push("abc/def");
// should return already watched
assert!(watcher
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
.is_err());
assert!(
watcher
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
.is_err()
);
// close tempdir
assert!(tempdir.close().is_ok());
}
@@ -279,9 +285,11 @@ mod test {
fn should_unwatch_path() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
let tempdir = TempDir::new().unwrap();
assert!(watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok()
);
// unwatch
assert!(watcher.unwatch(tempdir.path()).is_ok());
assert!(watcher.paths.get(tempdir.path()).is_none());
@@ -293,9 +301,11 @@ mod test {
fn should_unwatch_path_when_subdir() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
let tempdir = TempDir::new().unwrap();
assert!(watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok()
);
// unwatch
let mut subdir = tempdir.path().to_path_buf();
subdir.push("abc/def");
@@ -318,9 +328,11 @@ mod test {
fn should_tell_whether_path_is_watched() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
let tempdir = TempDir::new().unwrap();
assert!(watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok()
);
assert_eq!(watcher.watched(tempdir.path()), true);
let mut subdir = tempdir.path().to_path_buf();
subdir.push("abc/def");
@@ -336,9 +348,11 @@ mod test {
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
let tempdir = TempDir::new().unwrap();
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
assert!(watcher
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
.is_ok()
);
// create file
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
// wait
@@ -362,9 +376,11 @@ mod test {
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
let tempdir = TempDir::new().unwrap();
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
assert!(watcher
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
.is_ok()
);
// create file
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
std::thread::sleep(Duration::from_millis(500));
@@ -385,7 +401,7 @@ mod test {
/*
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn should_poll_file_moved() {
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
let tempdir = TempDir::new().unwrap();
@@ -424,9 +440,11 @@ mod test {
fn should_poll_nothing() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
let tempdir = TempDir::new().unwrap();
assert!(watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok());
assert!(
watcher
.watch(tempdir.path(), Path::new("/tmp/test"))
.is_ok()
);
assert!(watcher.poll().ok().unwrap().is_none());
// close tempdir
assert!(tempdir.close().is_ok());
@@ -437,9 +455,11 @@ mod test {
fn should_get_watched_paths() {
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
assert!(watcher.watch(Path::new("/tmp"), Path::new("/tmp")).is_ok());
assert!(watcher
.watch(Path::new("/home"), Path::new("/home"))
.is_ok());
assert!(
watcher
.watch(Path::new("/home"), Path::new("/home"))
.is_ok()
);
let mut watched_paths = watcher.watched_paths();
watched_paths.sort();
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);

View File

@@ -4,11 +4,11 @@
// Locals
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
use crate::filetransfer::HostBridgeParams;
use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
WebDAVProtocolParams,
};
use crate::filetransfer::HostBridgeParams;
impl AuthActivity {
/// Delete bookmark
@@ -30,13 +30,13 @@ impl AuthActivity {
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client() {
// Iterate over bookmarks
if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
// Load parameters into components
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
if let Some(key) = self.bookmarks_list.get(idx)
&& let Some(bookmark) = bookmarks_cli.get_bookmark(key)
{
// Load parameters into components
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
}
}
@@ -99,13 +99,13 @@ impl AuthActivity {
pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
if let Some(client) = self.bookmarks_client() {
// Iterate over bookmarks
if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) {
// Load parameters
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
if let Some(key) = self.recents_list.get(idx)
&& let Some(bookmark) = client.get_recent(key)
{
// Load parameters
match form_tab {
FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark),
FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark),
}
}
}
@@ -129,10 +129,10 @@ impl AuthActivity {
/// Write bookmarks to file
fn write_bookmarks(&mut self) {
if let Some(bookmarks_cli) = self.bookmarks_client() {
if let Err(err) = bookmarks_cli.write_bookmarks() {
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
}
if let Some(bookmarks_cli) = self.bookmarks_client()
&& let Err(err) = bookmarks_cli.write_bookmarks()
{
self.mount_error(format!("Could not write bookmarks: {err}").as_str());
}
}
@@ -280,12 +280,12 @@ impl AuthActivity {
fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) {
self.mount_address(form_tab, params.address.as_str());
#[cfg(unix)]
#[cfg(posix)]
self.mount_port(form_tab, params.port);
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
self.mount_smb_share(form_tab, &params.share);
#[cfg(unix)]
#[cfg(posix)]
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
}

View File

@@ -197,7 +197,7 @@ impl DeleteBookmarkPopup {
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.value(1)
.rewind(true)
.foreground(color)
@@ -265,7 +265,7 @@ impl DeleteRecentPopup {
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.value(1)
.rewind(true)
.foreground(color)
@@ -337,7 +337,7 @@ impl BookmarkSavePassword {
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.value(0)
.rewind(true)
.foreground(color)

View File

@@ -10,14 +10,13 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
use super::{FileTransferProtocol, FormMsg, Msg, UiMsg};
use crate::ui::activities::auth::{
FormTab, HostBridgeProtocol, UiAuthFormMsg, HOST_BRIDGE_RADIO_PROTOCOL_FTP,
HOST_BRIDGE_RADIO_PROTOCOL_FTPS, HOST_BRIDGE_RADIO_PROTOCOL_KUBE,
HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST, HOST_BRIDGE_RADIO_PROTOCOL_S3,
HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, REMOTE_RADIO_PROTOCOL_FTP,
REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE, REMOTE_RADIO_PROTOCOL_S3,
REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP, REMOTE_RADIO_PROTOCOL_SMB,
REMOTE_RADIO_PROTOCOL_WEBDAV,
FormTab, HOST_BRIDGE_RADIO_PROTOCOL_FTP, HOST_BRIDGE_RADIO_PROTOCOL_FTPS,
HOST_BRIDGE_RADIO_PROTOCOL_KUBE, HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST,
HOST_BRIDGE_RADIO_PROTOCOL_S3, HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, HostBridgeProtocol,
REMOTE_RADIO_PROTOCOL_FTP, REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE,
REMOTE_RADIO_PROTOCOL_S3, REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP,
REMOTE_RADIO_PROTOCOL_SMB, REMOTE_RADIO_PROTOCOL_WEBDAV, UiAuthFormMsg,
};
// -- protocol
@@ -37,9 +36,9 @@ impl RemoteProtocolRadio {
.modifiers(BorderType::Rounded),
)
.choices(if cfg!(smb) {
&["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"]
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"].into_iter()
} else {
&["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"]
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
})
.foreground(color)
.rewind(true)
@@ -93,10 +92,10 @@ impl Component<Msg, NoUserEvent> for RemoteProtocolRadio {
code: Key::Down, ..
}) => return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurDown))),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp)))
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp)));
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur)))
return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur)));
}
Event::Keyboard(KeyEvent {
code: Key::BackTab, ..
@@ -127,7 +126,7 @@ impl HostBridgeProtocolRadio {
.modifiers(BorderType::Rounded),
)
.choices(if cfg!(smb) {
&[
vec![
"Localhost",
"SFTP",
"SCP",
@@ -138,8 +137,9 @@ impl HostBridgeProtocolRadio {
"WebDAV",
"SMB",
]
.into_iter()
} else {
&[
vec![
"Localhost",
"SFTP",
"SCP",
@@ -149,6 +149,7 @@ impl HostBridgeProtocolRadio {
"Kube",
"WebDAV",
]
.into_iter()
})
.foreground(color)
.rewind(true)
@@ -228,10 +229,10 @@ impl Component<Msg, NoUserEvent> for HostBridgeProtocolRadio {
code: Key::Down, ..
}) => return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurDown))),
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp)))
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp)));
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur)))
return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur)));
}
Event::Keyboard(KeyEvent {
code: Key::BackTab, ..
@@ -650,7 +651,7 @@ impl RadioS3NewPathStyle {
.color(color)
.modifiers(BorderType::Rounded),
)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.foreground(color)
.rewind(true)
.title("New path style", Alignment::Left)
@@ -952,14 +953,14 @@ impl Component<Msg, NoUserEvent> for InputSmbShare {
}
}
#[cfg(unix)]
#[cfg(posix)]
#[derive(MockComponent)]
pub struct InputSmbWorkgroup {
component: Input,
form_tab: FormTab,
}
#[cfg(unix)]
#[cfg(posix)]
impl InputSmbWorkgroup {
pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self {
Self {
@@ -978,7 +979,7 @@ impl InputSmbWorkgroup {
}
}
#[cfg(unix)]
#[cfg(posix)]
impl Component<Msg, NoUserEvent> for InputSmbWorkgroup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab {

View File

@@ -13,7 +13,7 @@ pub use bookmarks::{
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
RecentsList,
};
#[cfg(unix)]
#[cfg(posix)]
pub use form::InputSmbWorkgroup;
pub use form::{
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,

View File

@@ -28,7 +28,7 @@ impl ErrorPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.text([TextSpan::from(text.as_ref())])
.wrap(true),
}
}
@@ -64,7 +64,7 @@ impl InfoPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.text([TextSpan::from(text.as_ref())])
.wrap(true),
}
}
@@ -100,7 +100,7 @@ impl WaitPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.text([TextSpan::from(text.as_ref())])
.wrap(true),
}
}
@@ -130,7 +130,7 @@ impl WindowSizeError {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(
.text([TextSpan::from(
"termscp requires at least 24 lines of height to run",
)])
.wrap(true),
@@ -163,7 +163,7 @@ impl QuitPopup {
.foreground(color)
.title("Quit termscp?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
.choices(["Yes", "No"]),
}
}
}
@@ -230,7 +230,7 @@ impl InstallUpdatePopup {
.foreground(color)
.title("Install update?", Alignment::Center)
.rewind(true)
.choices(&["Yes", "No"]),
.choices(["Yes", "No"]),
}
}
}
@@ -296,13 +296,7 @@ impl ReleaseNotes {
)
.foreground(color)
.title("Release notes", Alignment::Center)
.text_rows(
notes
.lines()
.map(TextSpan::from)
.collect::<Vec<TextSpan>>()
.as_slice(),
),
.text_rows(notes.lines().map(TextSpan::from)),
}
}
}

View File

@@ -64,7 +64,7 @@ pub struct NewVersionDisclaimer {
impl NewVersionDisclaimer {
pub fn new(new_version: &str, color: Color) -> Self {
Self {
component: Span::default().foreground(color).spans(&[
component: Span::default().foreground(color).spans([
TextSpan::from("termscp "),
TextSpan::new(new_version).underlined().bold(),
TextSpan::from(
@@ -91,7 +91,7 @@ pub struct HelpFooter {
impl HelpFooter {
pub fn new(key_color: Color) -> Self {
Self {
component: Span::default().spans(&[
component: Span::default().spans([
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color),
TextSpan::from(" Help "),
TextSpan::from("<CTRL+C>").bold().fg(key_color),

View File

@@ -5,8 +5,8 @@
use std::env;
use super::{AuthActivity, FileTransferParams, FileTransferProtocol, FormTab, HostBridgeProtocol};
use crate::filetransfer::params::ProtocolParams;
use crate::filetransfer::HostBridgeParams;
use crate::filetransfer::params::ProtocolParams;
use crate::system::auto_update::{Release, Update, UpdateStatus};
use crate::system::notifications::Notification;
@@ -83,8 +83,16 @@ impl AuthActivity {
}
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))
@@ -151,7 +159,7 @@ impl AuthActivity {
if params.address.is_empty() {
return Err("Invalid address");
}
#[cfg(unix)]
#[cfg(posix)]
if params.port == 0 {
return Err("Invalid port");
}
@@ -215,10 +223,7 @@ impl AuthActivity {
}
Err(err) => {
// Report error
error!("Failed to get latest version: {}", err);
self.mount_error(
format!("Could not check for new updates: {err}").as_str(),
);
error!("Failed to get latest version: {err}",);
}
}
} else {

View File

@@ -17,7 +17,7 @@ use tuirealm::application::PollStrategy;
use tuirealm::listener::EventListenerCfg;
use tuirealm::{Application, NoUserEvent, Update};
use super::{Activity, Context, ExitReason, CROSSTERM_MAX_POLL};
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient;
@@ -93,7 +93,7 @@ pub enum AuthFormId {
S3SecurityToken,
S3SessionToken,
SmbShare,
#[cfg(unix)]
#[cfg(posix)]
SmbWorkgroup,
Username,
WebDAVUri,
@@ -193,9 +193,9 @@ pub enum UiAuthFormMsg {
S3SessionTokenBlurUp,
SmbShareBlurDown,
SmbShareBlurUp,
#[cfg(unix)]
#[cfg(posix)]
SmbWorkgroupDown,
#[cfg(unix)]
#[cfg(posix)]
SmbWorkgroupUp,
UsernameBlurDown,
UsernameBlurUp,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -167,7 +167,9 @@ impl FileTransferActivity {
}
} else {
// Do not synchronize, disable sync browsing and return
trace!("The user doesn't want to create the directory; disabling synchronized browsing");
trace!(
"The user doesn't want to create the directory; disabling synchronized browsing"
);
self.log(
LogLevel::Warn,
format!("Refused to create '{name}'; synchronized browsing disabled"),

View File

@@ -18,14 +18,15 @@ impl FileTransferActivity {
self.local_copy_file(&entry, dest_path.as_path());
}
SelectedFile::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone();
for (entry, mut dest_path) in entries.into_iter() {
dest_path.push(entry.name());
self.local_copy_file(entry, dest_path.as_path());
self.local_copy_file(&entry, dest_path.as_path());
}
// clear selection
self.host_bridge_mut().clear_queue();
self.reload_host_bridge_filelist();
}
SelectedFile::None => {}
}
@@ -39,14 +40,15 @@ impl FileTransferActivity {
self.remote_copy_file(entry, dest_path.as_path());
}
SelectedFile::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.into_iter() {
let mut dest_path: PathBuf = base_path.clone();
for (entry, mut dest_path) in entries.into_iter() {
dest_path.push(entry.name());
self.remote_copy_file(entry, dest_path.as_path());
}
// clear selection
self.remote_mut().clear_queue();
self.reload_remote_filelist();
}
SelectedFile::None => {}
}

View File

@@ -16,10 +16,14 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Iter files
for entry in entries.iter() {
for (entry, _) in entries.iter() {
// Delete file
self.local_remove_file(entry);
}
// clear selection
self.host_bridge_mut().clear_queue();
self.reload_host_bridge_filelist();
}
SelectedFile::None => {}
}
@@ -33,10 +37,14 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Iter files
for entry in entries.iter() {
for (entry, _) in entries.iter() {
// Delete file
self.remote_remove_file(entry);
}
// clear selection
self.remote_mut().clear_queue();
self.reload_remote_filelist();
}
SelectedFile::None => {}
}

View File

@@ -7,8 +7,8 @@ use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use remotefs::fs::Metadata;
use remotefs::File;
use remotefs::fs::Metadata;
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
@@ -16,7 +16,7 @@ impl FileTransferActivity {
pub(crate) fn action_edit_local_file(&mut self) {
let entries: Vec<File> = match self.get_local_selected_entries() {
SelectedFile::One(entry) => vec![entry],
SelectedFile::Many(entries) => entries,
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
SelectedFile::None => vec![],
};
// Edit all entries
@@ -38,12 +38,16 @@ impl FileTransferActivity {
}
}
}
// clear selection
self.host_bridge_mut().clear_queue();
self.reload_host_bridge_filelist();
}
pub(crate) fn action_edit_remote_file(&mut self) {
let entries: Vec<File> = match self.get_remote_selected_entries() {
SelectedFile::One(entry) => vec![entry],
SelectedFile::Many(entries) => entries,
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
SelectedFile::None => vec![],
};
// Edit all entries
@@ -60,6 +64,10 @@ impl FileTransferActivity {
}
}
}
// clear selection
self.remote_mut().clear_queue();
self.reload_remote_filelist();
}
/// Edit a file on localhost
@@ -221,10 +229,7 @@ impl FileTransferActivity {
/// Edit file on remote host
fn edit_remote_file(&mut self, file: File) -> Result<(), String> {
// Create temp file
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
Ok(p) => p,
Err(err) => return Err(err),
};
let tmpfile = self.download_file_as_temp(&file)?;
// Download file
let file_name = file.name();
let file_path = file.path().to_path_buf();
@@ -243,7 +248,7 @@ impl FileTransferActivity {
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
));
}
};
// Edit file
@@ -256,7 +261,7 @@ impl FileTransferActivity {
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
));
}
};
// Check if file has changed
@@ -282,7 +287,7 @@ impl FileTransferActivity {
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
));
}
};
// Send file

View File

@@ -2,41 +2,127 @@
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
use std::path::PathBuf;
use std::str::FromStr;
// locals
use super::{FileTransferActivity, LogLevel};
/// Terminal command
#[derive(Debug, Clone, PartialEq, Eq)]
enum Command {
Cd(String),
Exec(String),
Exit,
}
impl FromStr for Command {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split_whitespace();
match parts.next() {
Some("cd") => {
if let Some(path) = parts.next() {
Ok(Command::Cd(path.to_string()))
} else {
Err("cd command requires a path".to_string())
}
}
Some("exit") | Some("logout") => Ok(Command::Exit),
Some(cmd) => Ok(Command::Exec(cmd.to_string())),
None => Err("".to_string()),
}
}
}
impl FileTransferActivity {
pub(crate) fn action_local_exec(&mut self, input: String) {
match self.host_bridge.exec(input.as_str()) {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{input}\": {output}"));
}
self.action_exec(false, input);
}
pub(crate) fn action_remote_exec(&mut self, input: String) {
self.action_exec(true, input);
}
fn action_exec(&mut self, remote: bool, cmd: String) {
if cmd.is_empty() {
self.print_terminal("".to_string());
}
let cmd = match Command::from_str(&cmd) {
Ok(cmd) => cmd,
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not execute command \"{input}\": {err}"),
);
self.log(LogLevel::Error, format!("Invalid command: {err}"));
self.print_terminal(err);
return;
}
};
match cmd {
Command::Cd(path) => {
self.action_exec_cd(remote, path);
}
Command::Exec(executable) => {
self.action_exec_executable(remote, executable);
}
Command::Exit => {
self.action_exec_exit();
}
}
}
pub(crate) fn action_remote_exec(&mut self, input: String) {
match self.client.as_mut().exec(input.as_str()) {
Ok((rc, output)) => {
// Reload files
self.log(
LogLevel::Info,
format!("\"{input}\" (exitcode: {rc}): {output}"),
);
fn action_exec_exit(&mut self) {
self.browser.toggle_terminal(false);
self.umount_exec();
}
fn action_exec_cd(&mut self, remote: bool, input: String) {
let new_dir = if remote {
let dir_path: PathBuf =
self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.remote_changedir(dir_path.as_path(), true);
dir_path
} else {
let dir_path: PathBuf =
self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path());
self.host_bridge_changedir(dir_path.as_path(), true);
dir_path
};
self.update_browser_file_list();
// update prompt and print the new directory
self.update_terminal_prompt();
self.print_terminal(new_dir.display().to_string());
}
/// Execute a [`Command::Exec`] command
fn action_exec_executable(&mut self, remote: bool, cmd: String) {
let res = if remote {
self.client
.as_mut()
.exec(cmd.as_str())
.map(|(_, output)| output)
.map_err(|e| e.to_string())
} else {
self.host_bridge
.exec(cmd.as_str())
.map_err(|e| e.to_string())
};
match res {
Ok(output) => {
self.print_terminal(output);
}
Err(err) => {
// Report err
self.log_and_alert(
self.log(
LogLevel::Error,
format!("Could not execute command \"{input}\": {err}"),
format!("Could not execute command \"{cmd}\": {err}"),
);
self.print_terminal(err);
}
}
}

View File

@@ -0,0 +1,94 @@
use remotefs::File;
use super::{FileTransferActivity, LogLevel};
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
#[derive(Debug, Copy, Clone)]
enum Host {
HostBridge,
Remote,
}
impl FileTransferActivity {
pub(crate) fn action_get_file_size(&mut self) {
// Get selected file
self.mount_blocking_wait("Getting total path size...");
let total_size = match self.browser.tab() {
FileExplorerTab::HostBridge => {
let files = self.get_local_selected_entries().get_files();
self.get_files_size(files, Host::HostBridge)
}
FileExplorerTab::Remote => {
let files = self.get_remote_selected_entries().get_files();
self.get_files_size(files, Host::Remote)
}
FileExplorerTab::FindHostBridge => {
let files = self.get_found_selected_entries().get_files();
self.get_files_size(files, Host::HostBridge)
}
FileExplorerTab::FindRemote => {
let files = self.get_found_selected_entries().get_files();
self.get_files_size(files, Host::Remote)
}
};
self.umount_wait();
self.mount_info(format!(
"Total file size: {size}",
size = bytesize::ByteSize::b(total_size)
));
}
fn get_files_size(&mut self, files: Vec<File>, host: Host) -> u64 {
files.into_iter().map(|f| self.get_file_size(f, host)).sum()
}
fn get_file_size(&mut self, file: File, host: Host) -> u64 {
if let Some(symlink) = &file.metadata().symlink {
// stat
let stat_res = match host {
Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()),
Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()),
};
match stat_res {
Ok(stat) => stat.metadata().size,
Err(err_msg) => {
self.log(
LogLevel::Error,
format!(
"Failed to stat symlink target {path}: {err_msg}",
path = symlink.display(),
),
);
0
}
}
} else if file.is_dir() {
// list and sum
let list_res = match host {
Host::HostBridge => self
.host_bridge
.list_dir(&file.path)
.map_err(|e| e.to_string()),
Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()),
};
match list_res {
Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(),
Err(err_msg) => {
self.log(
LogLevel::Error,
format!(
"Failed to list directory {path}: {err_msg}",
path = file.path.display(),
),
);
0
}
}
} else {
file.metadata().size
}
}
}

View File

@@ -4,8 +4,8 @@ use regex::Regex;
use remotefs::File;
use wildmatch::WildMatch;
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
use crate::ui::activities::filetransfer::FileTransferActivity;
use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab;
#[derive(Clone, Debug)]
pub enum Filter {

View File

@@ -11,7 +11,7 @@ use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, Tr
impl FileTransferActivity {
pub(crate) fn action_find_changedir(&mut self) {
// 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
let path = if entry.is_dir() {
@@ -99,25 +99,18 @@ impl FileTransferActivity {
// Iter files
match self.browser.tab() {
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|x| {
self.remote_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Check whether to replace files
if !existing_files.is_empty()
&& !self.should_replace_files(existing_files)
{
return;
}
}
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
entries,
) = self.get_files_to_transfer_with_overwrites(
entries,
super::save::CheckFileExists::Remote,
)
else {
debug!("User cancelled file transfer due to overwrites");
return;
};
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
None,
) {
@@ -130,25 +123,18 @@ impl FileTransferActivity {
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|x| {
self.host_bridge_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Check whether to replace files
if !existing_files.is_empty()
&& !self.should_replace_files(existing_files)
{
return;
}
}
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
entries,
) = self.get_files_to_transfer_with_overwrites(
entries,
super::save::CheckFileExists::HostBridge,
)
else {
debug!("User cancelled file transfer due to overwrites");
return;
};
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
None,
) {
@@ -157,6 +143,12 @@ impl FileTransferActivity {
format!("Could not download file: {err}"),
);
}
// clear selection
if let Some(f) = self.found_mut() {
f.clear_queue();
self.update_find_list();
}
}
}
}
@@ -172,10 +164,16 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Iter files
for entry in entries.iter() {
for (entry, _) in entries.iter() {
// Delete file
self.remove_found_file(entry);
}
// clear selection
if let Some(f) = self.found_mut() {
f.clear_queue();
self.update_find_list();
}
}
SelectedFile::None => {}
}
@@ -200,10 +198,15 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Iter files
for entry in entries.iter() {
for (entry, _) in entries.iter() {
// Open file
self.open_found_file(entry, None);
}
// clear selection
if let Some(f) = self.found_mut() {
f.clear_queue();
self.update_find_list();
}
}
SelectedFile::None => {}
}
@@ -217,10 +220,15 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Iter files
for entry in entries.iter() {
for (entry, _) in entries.iter() {
// Open file
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 => {}
}

View 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();
}
}

View File

@@ -2,15 +2,19 @@
//!
//! `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::fs::UnixPex;
use tuirealm::{State, StateValue};
use super::browser::FileExplorerTab;
use super::lib::browser::FoundExplorerTab;
use super::{
FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg, TransferMsg, TransferOpts,
TransferPayload, UiMsg,
};
use crate::explorer::FileExplorer;
// actions
pub(crate) mod change_dir;
@@ -19,8 +23,10 @@ pub(crate) mod copy;
pub(crate) mod delete;
pub(crate) mod edit;
pub(crate) mod exec;
pub(crate) mod file_size;
pub(crate) mod filter;
pub(crate) mod find;
pub(crate) mod mark;
pub(crate) mod mkdir;
pub(crate) mod newfile;
pub(crate) mod open;
@@ -36,7 +42,8 @@ pub(crate) mod watcher;
#[derive(Debug)]
pub(crate) enum SelectedFile {
One(File),
Many(Vec<File>),
/// List of file with their destination path
Many(Vec<(File, PathBuf)>),
None,
}
@@ -45,7 +52,10 @@ impl SelectedFile {
/// In case is `Many` the first item mode is returned
pub fn unix_pex(&self) -> Option<UnixPex> {
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::None => None,
}
@@ -55,7 +65,7 @@ impl SelectedFile {
pub fn get_files(self) -> Vec<File> {
match self {
Self::One(file) => vec![file],
Self::Many(files) => files,
Self::Many(files) => files.into_iter().map(|(f, _)| f).collect(),
Self::None => vec![],
}
}
@@ -64,7 +74,6 @@ impl SelectedFile {
#[derive(Debug)]
enum SelectedFileIndex {
One(usize),
Many(Vec<usize>),
None,
}
@@ -77,68 +86,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 {
/// Get local file entry
pub(crate) fn get_local_selected_entries(&self) -> SelectedFile {
match self.get_selected_index(&Id::ExplorerHostBridge) {
SelectedFileIndex::One(idx) => SelectedFile::from(self.host_bridge().get(idx)),
SelectedFileIndex::Many(files) => {
let files: Vec<&File> = files
.iter()
.filter_map(|x| self.host_bridge().get(*x)) // Usize to Option<File>
.collect();
SelectedFile::from(files)
}
SelectedFileIndex::None => SelectedFile::None,
}
pub(crate) fn get_local_selected_entries(&mut self) -> SelectedFile {
self.get_selected_files(&Id::ExplorerHostBridge)
}
pub(crate) fn get_local_selected_file(&self) -> Option<File> {
self.get_selected_file(&Id::ExplorerHostBridge)
}
/// Get remote file entry
pub(crate) fn get_remote_selected_entries(&self) -> SelectedFile {
match self.get_selected_index(&Id::ExplorerRemote) {
SelectedFileIndex::One(idx) => SelectedFile::from(self.remote().get(idx)),
SelectedFileIndex::Many(files) => {
let files: Vec<&File> = files
.iter()
.filter_map(|x| self.remote().get(*x)) // Usize to Option<File>
.collect();
SelectedFile::from(files)
}
SelectedFileIndex::None => SelectedFile::None,
}
pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile {
self.get_selected_files(&Id::ExplorerRemote)
}
pub(crate) fn get_remote_selected_file(&self) -> Option<File> {
self.get_selected_file(&Id::ExplorerRemote)
}
/// 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(_))
}
/// 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(_))
}
/// Get remote file entry
pub(crate) fn get_found_selected_entries(&self) -> SelectedFile {
match self.get_selected_index(&Id::ExplorerFind) {
SelectedFileIndex::One(idx) => {
SelectedFile::from(self.found().as_ref().unwrap().get(idx))
}
SelectedFileIndex::Many(files) => {
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,
}
pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile {
self.get_selected_files(&Id::ExplorerFind)
}
pub(crate) fn get_found_selected_file(&self) -> Option<File> {
self.get_selected_file(&Id::ExplorerFind)
}
// -- private
@@ -146,17 +129,69 @@ impl FileTransferActivity {
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
match self.app.state(id) {
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,
}
}
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,
}
}
}

View File

@@ -13,24 +13,32 @@ impl FileTransferActivity {
pub(crate) fn action_open_local(&mut self) {
let entries: Vec<File> = match self.get_local_selected_entries() {
SelectedFile::One(entry) => vec![entry],
SelectedFile::Many(entries) => entries,
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
SelectedFile::None => vec![],
};
entries
.iter()
.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
pub(crate) fn action_open_remote(&mut self) {
let entries: Vec<File> = match self.get_remote_selected_entries() {
SelectedFile::One(entry) => vec![entry],
SelectedFile::Many(entries) => entries,
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
SelectedFile::None => vec![],
};
entries
.iter()
.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
@@ -86,26 +94,33 @@ impl FileTransferActivity {
pub(crate) fn action_local_open_with(&mut self, with: &str) {
let entries: Vec<File> = match self.get_local_selected_entries() {
SelectedFile::One(entry) => vec![entry],
SelectedFile::Many(entries) => entries,
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
SelectedFile::None => vec![],
};
// Open all entries
entries
.iter()
.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
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
let entries: Vec<File> = match self.get_remote_selected_entries() {
SelectedFile::One(entry) => vec![entry],
SelectedFile::Many(entries) => entries,
SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(),
SelectedFile::None => vec![],
};
// Open all entries
entries
.iter()
.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>) {
@@ -171,7 +186,7 @@ impl FileTransferActivity {
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
Err(err) => self.log(
LogLevel::Error,
format!("Failed to open filoe `{}`: {}", p.display(), err),
format!("Failed to open file `{}`: {}", p.display(), err),
),
}
// NOTE: clear screen in order to prevent crap on stderr

View File

@@ -18,13 +18,15 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone();
for (entry, mut dest_path) in entries.into_iter() {
dest_path.push(entry.name());
self.local_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 => {}
}
@@ -38,13 +40,16 @@ impl FileTransferActivity {
}
SelectedFile::Many(entries) => {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone();
for (entry, mut dest_path) in entries.into_iter() {
dest_path.push(entry.name());
self.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 => {}
}

View File

@@ -10,6 +10,37 @@ use super::{
TransferPayload,
};
enum GetFileToReplaceResult {
Replace(Vec<(File, PathBuf)>),
Cancel,
}
/// Result of getting files to transfer with overwrites.
///
/// - FilesToTransfer: files to transfer.
/// - Cancel: user cancelled the operation.
pub(crate) enum TransferFilesWithOverwritesResult {
FilesToTransfer(Vec<(File, PathBuf)>),
Cancel,
}
/// Decides whether to check file existence on host bridge or remote side.
pub(crate) enum CheckFileExists {
HostBridge,
Remote,
}
/// Options for all files replacement.
///
/// - ReplaceAll: user wants to replace all files.
/// - SkipAll: user wants to skip all files.
/// - Unset: no option set yet.
enum AllOpts {
ReplaceAll,
SkipAll,
Unset,
}
impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) {
self.local_send_file(TransferOpts::default().save_as(Some(input)));
@@ -60,23 +91,14 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// Iter files
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|x| {
self.remote_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Check whether to replace files
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
return;
}
}
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
self.get_files_to_transfer_with_overwrites(entries, CheckFileExists::Remote)
else {
debug!("User cancelled file transfer due to overwrites");
return;
};
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
None,
) {
@@ -86,6 +108,10 @@ impl FileTransferActivity {
format!("Could not upload file: {err}"),
);
}
} else {
// clear selection
self.host_bridge_mut().clear_queue();
self.reload_host_bridge_filelist();
}
}
SelectedFile::None => {}
@@ -123,24 +149,15 @@ impl FileTransferActivity {
if let Some(save_as) = opts.save_as {
dest_path.push(save_as);
}
// Iter files
if self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&File> = entries
.iter()
.filter(|x| {
self.host_bridge_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Check whether to replace files
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
return;
}
}
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) = self
.get_files_to_transfer_with_overwrites(entries, CheckFileExists::HostBridge)
else {
debug!("User cancelled file transfer due to overwrites");
return;
};
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
TransferPayload::TransferQueue(entries),
dest_path.as_path(),
None,
) {
@@ -150,6 +167,11 @@ impl FileTransferActivity {
format!("Could not download file: {err}"),
);
}
} else {
// clear selection
self.remote_mut().clear_queue();
// reload remote
self.reload_remote_filelist();
}
}
SelectedFile::None => {}
@@ -161,11 +183,17 @@ impl FileTransferActivity {
self.mount_radio_replace(&file_name);
// Wait for answer
trace!("Asking user whether he wants to replace file {}", file_name);
if self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
{
if matches!(
self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite),
Msg::PendingAction(PendingActionMsg::ReplaceSkip),
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll),
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll),
]),
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)
| Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)
) {
trace!("User wants to replace file");
self.umount_radio_replace();
true
@@ -176,28 +204,76 @@ impl FileTransferActivity {
}
}
/// Set pending transfer for many files into storage and mount radio
pub(crate) fn should_replace_files(&mut self, files: Vec<&File>) -> bool {
let file_names: Vec<String> = files.iter().map(|x| x.name()).collect();
self.mount_radio_replace_many(file_names.as_slice());
// Wait for answer
trace!(
"Asking user whether he wants to replace files {:?}",
file_names
);
if self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
{
trace!("User wants to replace files");
/// Get files to replace
fn get_files_to_replace(&mut self, files: Vec<(File, PathBuf)>) -> GetFileToReplaceResult {
// keep only files the user want to replace
let mut files_to_replace = vec![];
let mut all_opts = AllOpts::Unset;
for (file, p) in files {
// Check for all opts
match all_opts {
AllOpts::ReplaceAll => {
trace!(
"User wants to replace all files, including file {}",
file.name()
);
files_to_replace.push((file, p));
continue;
}
AllOpts::SkipAll => {
trace!(
"User wants to skip all files, including file {}",
file.name()
);
continue;
}
AllOpts::Unset => {}
}
let file_name = file.name();
self.mount_radio_replace(&file_name);
// Wait for answer
match self.wait_for_pending_msg(&[
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite),
Msg::PendingAction(PendingActionMsg::ReplaceSkip),
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll),
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll),
]) {
Msg::PendingAction(PendingActionMsg::ReplaceCancel) => {
trace!("The user cancelled the replace operation");
self.umount_radio_replace();
return GetFileToReplaceResult::Cancel;
}
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite) => {
trace!("User wants to replace file {}", file_name);
files_to_replace.push((file, p));
}
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll) => {
trace!(
"User wants to replace all files from now on, including file {}",
file_name
);
files_to_replace.push((file, p));
all_opts = AllOpts::ReplaceAll;
}
Msg::PendingAction(PendingActionMsg::ReplaceSkip) => {
trace!("The user skipped file {}", file_name);
}
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll) => {
trace!(
"The user skipped all files from now on, including file {}",
file_name
);
all_opts = AllOpts::SkipAll;
}
_ => {}
}
self.umount_radio_replace();
true
} else {
trace!("The user doesn't want replace file");
self.umount_radio_replace();
false
}
GetFileToReplaceResult::Replace(files_to_replace)
}
/// Get file to check for path
@@ -213,4 +289,40 @@ impl FileTransferActivity {
p.push(e.name());
p
}
/// Get the files to transfer with overwrites.
///
/// Existing and unexisting files are splitted, and only existing files are prompted for replacement.
pub(crate) fn get_files_to_transfer_with_overwrites(
&mut self,
files: Vec<(File, PathBuf)>,
file_exists: CheckFileExists,
) -> TransferFilesWithOverwritesResult {
if !self.config().get_prompt_on_file_replace() {
return TransferFilesWithOverwritesResult::FilesToTransfer(files);
}
// unzip between existing and non-existing files
let (existing_files, new_files): (Vec<_>, Vec<_>) =
files.into_iter().partition(|(x, dest_path)| {
let p = Self::file_to_check_many(x, dest_path);
match file_exists {
CheckFileExists::Remote => self.remote_file_exists(p.as_path()),
CheckFileExists::HostBridge => self.host_bridge_file_exists(p.as_path()),
}
});
// filter only files to replace
let existing_files = match self.get_files_to_replace(existing_files) {
GetFileToReplaceResult::Replace(files) => files,
GetFileToReplaceResult::Cancel => {
return TransferFilesWithOverwritesResult::Cancel;
}
};
// merge back
TransferFilesWithOverwritesResult::FilesToTransfer(
existing_files.into_iter().chain(new_files).collect(),
)
}
}

View File

@@ -5,12 +5,12 @@
// locals
use std::path::PathBuf;
use super::{FileTransferActivity, LogLevel, SelectedFile};
use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity {
/// Create symlink on localhost
pub(crate) fn action_local_symlink(&mut self, name: String) {
if let SelectedFile::One(entry) = self.get_local_selected_entries() {
if let Some(entry) = self.get_local_selected_file() {
match self
.host_bridge
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())
@@ -34,7 +34,7 @@ impl FileTransferActivity {
/// Copy file on remote
pub(crate) fn action_remote_symlink(&mut self, name: String) {
if let SelectedFile::One(entry) = self.get_remote_selected_entries() {
if let Some(entry) = self.get_remote_selected_file() {
match self
.client
.symlink(PathBuf::from(name.as_str()).as_path(), entry.path())

View File

@@ -53,12 +53,20 @@ impl MockComponent for Log {
.unwrap()
.unwrap_table()
.iter()
.map(|row| ListItem::new(tui_realm_stdlib::utils::wrap_spans(row, width, &self.props)))
.map(|row| {
let row_refs = row.iter().collect::<Vec<_>>();
ListItem::new(tui_realm_stdlib::utils::wrap_spans(
row_refs.as_slice(),
width,
&self.props,
))
})
.collect();
let title = ("Log".to_string(), Alignment::Left);
let w = TuiList::new(list_items)
.block(tui_realm_stdlib::utils::get_block(
borders,
Some(("Log".to_string(), Alignment::Left)),
Some(&title),
focus,
None,
))
@@ -166,6 +174,12 @@ impl Component<Msg, NoUserEvent> for Log {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => Some(Msg::Ui(UiMsg::BottomPanelRight)),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => Some(Msg::Ui(UiMsg::BottomPanelLeft)),
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {

View File

@@ -16,7 +16,7 @@ pub struct FooterBar {
impl FooterBar {
pub fn new(key_color: Color) -> Self {
Self {
component: Span::default().spans(&[
component: Span::default().spans([
TextSpan::from("<F1|H>").bold().fg(key_color),
TextSpan::from(" Help "),
TextSpan::from("<TAB>").bold().fg(key_color),

View File

@@ -12,20 +12,23 @@ use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
mod log;
mod misc;
mod popups;
mod selected_files;
mod terminal;
mod transfer;
pub use misc::FooterBar;
pub use popups::{
ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, FatalPopup,
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
WatcherPopup, ATTR_FILES,
SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, SymlinkPopup,
SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList, WatcherPopup,
};
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
pub use self::log::Log;
pub use self::selected_files::SelectedFilesList;
pub use self::terminal::Terminal;
#[derive(Default, MockComponent)]
pub struct GlobalListener {

View File

@@ -16,11 +16,11 @@ use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan,
};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[cfg(unix)]
#[cfg(posix)]
use uzers::{get_group_by_gid, get_user_by_uid};
pub use self::chmod::ChmodPopup;
pub use self::goto::{GotoPopup, ATTR_FILES};
pub use self::goto::{ATTR_FILES, GotoPopup};
use super::super::Browser;
use super::{Msg, PendingActionMsg, TransferMsg, UiMsg};
use crate::explorer::FileSorting;
@@ -214,7 +214,7 @@ impl DeletePopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.value(1)
.title("Delete file(s)?", Alignment::Center),
}
@@ -279,7 +279,7 @@ impl DisconnectPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.title("Are you sure you want to disconnect?", Alignment::Center),
}
}
@@ -344,7 +344,7 @@ impl ErrorPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.text([TextSpan::from(text.as_ref())])
.wrap(true),
}
}
@@ -362,89 +362,6 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
}
}
#[derive(MockComponent)]
pub struct ExecPopup {
component: Input,
}
impl ExecPopup {
pub fn new(color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.input_type(InputType::Text)
.placeholder("ps a", Style::default().fg(Color::Rgb(128, 128, 128)))
.title("Execute command", Alignment::Center),
}
}
}
impl Component<Msg, NoUserEvent> for ExecPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
self.perform(Cmd::Move(Direction::Left));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => {
self.perform(Cmd::Move(Direction::Right));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Delete, ..
}) => {
self.perform(Cmd::Cancel);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Backspace,
..
}) => {
self.perform(Cmd::Delete);
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
..
}) => {
self.perform(Cmd::Type(ch));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => match self.state() {
State::One(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::ExecuteCmd(i)))
}
_ => Some(Msg::None),
},
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseExecPopup))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct FatalPopup {
component: Paragraph,
@@ -461,7 +378,7 @@ impl FatalPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.text([TextSpan::from(text.as_ref())])
.wrap(true),
}
}
@@ -497,6 +414,10 @@ impl FileInfoPopup {
texts
.add_col(TextSpan::from("Path: "))
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
texts
.add_row()
.add_col(TextSpan::from("Name: "))
.add_col(TextSpan::new(file.name()).fg(Color::Yellow));
if let Some(filetype) = file.extension() {
texts
.add_row()
@@ -533,7 +454,7 @@ impl FileInfoPopup {
.add_col(TextSpan::from("Last access time: "))
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
// User
#[cfg(unix)]
#[cfg(posix)]
let username: String = match file.metadata().uid {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
@@ -541,10 +462,10 @@ impl FileInfoPopup {
},
None => String::from("0"),
};
#[cfg(windows)]
#[cfg(win)]
let username: String = format!("{}", file.metadata().uid.unwrap_or(0));
// Group
#[cfg(unix)]
#[cfg(posix)]
let group: String = match file.metadata().gid {
Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(),
@@ -552,7 +473,7 @@ impl FileInfoPopup {
},
None => String::from("0"),
};
#[cfg(windows)]
#[cfg(win)]
let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
texts
.add_row()
@@ -670,7 +591,7 @@ impl KeybindingsPopup {
))
.add_row()
.add_col(TextSpan::new("<P>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle log panel"))
.add_col(TextSpan::from(" Toggle bottom panel"))
.add_row()
.add_col(TextSpan::new("<Q|F10>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp"))
@@ -723,6 +644,11 @@ impl KeybindingsPopup {
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Interrupt file transfer"))
.add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(key_color))
.add_col(TextSpan::from(
" Get total path size of selected files",
))
.add_row()
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color))
.add_col(TextSpan::from(" Show watched paths"))
.build(),
@@ -1121,7 +1047,7 @@ impl QuitPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.title("Are you sure you want to quit termscp?", Alignment::Center),
}
}
@@ -1275,7 +1201,7 @@ impl ReplacePopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"])
.title(text, Alignment::Center),
}
}
@@ -1284,9 +1210,6 @@ impl ReplacePopup {
impl Component<Msg, NoUserEvent> for ReplacePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
}
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => {
@@ -1300,102 +1223,36 @@ impl Component<Msg, NoUserEvent> for ReplacePopup {
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
}
Event::Keyboard(KeyEvent {
code: Key::Char('y'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile)),
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)),
Event::Keyboard(KeyEvent {
code: Key::Char('n'),
modifiers: KeyModifiers::NONE,
}) => Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)),
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)),
Event::Keyboard(KeyEvent {
code: Key::Enter, ..
}) => {
if matches!(
self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0)))
) {
Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile))
} else {
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
}) => match self.perform(Cmd::Submit) {
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite))
}
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct ReplacingFilesListPopup {
component: List,
}
impl ReplacingFilesListPopup {
pub fn new(files: &[String], color: Color) -> Self {
Self {
component: List::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.scroll(true)
.step(4)
.highlighted_color(color)
.highlighted_str("")
.title(
"The following files are going to be replaced",
Alignment::Center,
)
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
}
}
}
impl Component<Msg, NoUserEvent> for ReplacingFilesListPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
}
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
}
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
}
CmdResult::Submit(State::One(StateValue::Usize(2))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll))
}
CmdResult::Submit(State::One(StateValue::Usize(3))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll))
}
CmdResult::Submit(State::One(StateValue::Usize(4))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
}
_ => Some(Msg::None),
},
_ => None,
}
}
@@ -1502,7 +1359,7 @@ impl SortingPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Name", "Modify time", "Creation time", "Size"])
.choices(["Name", "Modify time", "Creation time", "Size"])
.title("Sort files by…", Alignment::Center)
.value(match value {
FileSorting::CreationTime => 2,
@@ -1554,7 +1411,7 @@ impl StatusBarLocal {
let file_sorting = file_sorting_label(browser.host_bridge().file_sorting);
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
Self {
component: Span::default().spans(&[
component: Span::default().spans([
TextSpan::new("File sorting: ").fg(sorting_color),
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
TextSpan::new(" Hidden files: ").fg(hidden_color),
@@ -1589,7 +1446,7 @@ impl StatusBarRemote {
false => "OFF",
};
Self {
component: Span::default().spans(&[
component: Span::default().spans([
TextSpan::new("File sorting: ").fg(sorting_color),
TextSpan::new(file_sorting).fg(sorting_color).reversed(),
TextSpan::new(" Hidden files: ").fg(hidden_color),
@@ -1728,7 +1585,7 @@ impl SyncBrowsingMkdirPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.title(
format!(
r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"#
@@ -1802,7 +1659,7 @@ impl WaitPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[TextSpan::from(text.as_ref())])
.text([TextSpan::from(text.as_ref())])
.wrap(true),
}
}
@@ -1830,7 +1687,7 @@ impl WalkdirWaitPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[
.text([
TextSpan::from(text.as_ref()),
TextSpan::from("Press 'CTRL+C' to abort"),
])
@@ -1961,7 +1818,7 @@ impl WatcherPopup {
.modifiers(BorderType::Rounded),
)
.foreground(color)
.choices(&["Yes", "No"])
.choices(["Yes", "No"])
.title(text, Alignment::Center),
}
}

View File

@@ -59,21 +59,21 @@ impl ChmodPopup {
},
user: Checkbox::default()
.foreground(color)
.choices(&["Read", "Write", "Execute"])
.choices(["Read", "Write", "Execute"])
.title("User", Alignment::Left)
.borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.user()))
.rewind(true),
group: Checkbox::default()
.foreground(color)
.choices(&["Read", "Write", "Execute"])
.choices(["Read", "Write", "Execute"])
.title("Group", Alignment::Left)
.borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.group()))
.rewind(true),
others: Checkbox::default()
.foreground(color)
.choices(&["Read", "Write", "Execute"])
.choices(["Read", "Write", "Execute"])
.title("Others", Alignment::Left)
.borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.others()))
@@ -208,9 +208,11 @@ impl MockComponent for ChmodPopup {
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
let div_title = (self.title.clone(), Alignment::Center);
let div = tui_realm_stdlib::utils::get_block(
Borders::default().color(self.color),
Some((self.title.clone(), Alignment::Center)),
Some(&div_title),
focus,
None,
);

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tui_realm_stdlib::Input;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
@@ -158,7 +158,7 @@ impl OwnStates {
.unwrap_or_else(|| PathBuf::from("/"));
// if path is `.`, then return None
if parent == PathBuf::from(".") {
if parent == Path::new(".") {
return Suggestion::None;
}
@@ -365,7 +365,7 @@ mod test {
}
#[test]
#[cfg(unix)]
#[cfg(posix)]
fn test_should_suggest_absolute_path() {
let mut states = OwnStates {
files: vec![

View File

@@ -0,0 +1,126 @@
use std::path::{Path, PathBuf};
use tui_realm_stdlib::List;
use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
use crate::ui::activities::filetransfer::{MarkQueue, Msg, UiMsg};
#[derive(MockComponent)]
pub struct SelectedFilesList {
component: List,
paths: Vec<PathBuf>,
queue: MarkQueue,
}
impl SelectedFilesList {
pub fn new(
paths: &[(PathBuf, PathBuf)],
queue: MarkQueue,
color: Color,
title: &'static str,
) -> Self {
let enqueued_paths = paths
.iter()
.map(|(src, _)| src.clone())
.collect::<Vec<PathBuf>>();
Self {
queue,
paths: enqueued_paths,
component: List::default()
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
.rewind(true)
.scroll(true)
.step(4)
.highlighted_color(color)
.highlighted_str("")
.title(title, Alignment::Left)
.rows(
paths
.iter()
.map(|(src, dest)| {
vec![
TextSpan::from(Self::filename(src)),
TextSpan::from(" -> "),
TextSpan::from(Self::filename(dest)),
]
})
.collect(),
),
}
}
fn filename(p: &Path) -> String {
p.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
}
}
impl Component<Msg, NoUserEvent> for SelectedFilesList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent {
code: Key::Down, ..
}) => {
self.perform(Cmd::Move(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
..
}) => {
self.perform(Cmd::Scroll(Direction::Down));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::PageUp, ..
}) => {
self.perform(Cmd::Scroll(Direction::Up));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Home, ..
}) => {
self.perform(Cmd::GoTo(Position::Begin));
Some(Msg::None)
}
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
self.perform(Cmd::GoTo(Position::End));
Some(Msg::None)
}
Event::Keyboard(KeyEvent {
code: Key::Right, ..
}) => Some(Msg::Ui(UiMsg::BottomPanelRight)),
Event::Keyboard(KeyEvent {
code: Key::Left, ..
}) => Some(Msg::Ui(UiMsg::BottomPanelLeft)),
Event::Keyboard(KeyEvent {
code: Key::BackTab | Key::Tab | Key::Char('p'),
..
}) => Some(Msg::Ui(UiMsg::LogBackTabbed)),
Event::Keyboard(KeyEvent {
code: Key::Enter | Key::Delete,
..
}) => {
// unmark the selected file
let State::One(StateValue::Usize(idx)) = self.state() else {
return None;
};
let path = self.paths.get(idx)?;
Some(Msg::Ui(UiMsg::MarkRemove(self.queue, path.clone())))
}
_ => None,
}
}
}

Some files were not shown because too many files have changed in this diff Show More