mirror of
https://github.com/veeso/termscp.git
synced 2026-09-18 01:56:47 -07:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ad18ea5e7 | |||
| 2ed71c7ff8 | |||
| 49102985a4 | |||
| bc59df494b | |||
| 080c013fab | |||
| 6252df2959 | |||
| 9160c52cb0 | |||
| 38f1fccfd0 | |||
| 6ceaf048b5 | |||
| 534c16427e | |||
| 9b19240925 | |||
| 0ddbf3b104 | |||
| 126e58503f | |||
| 0be8b60c7d | |||
| 4ed6b118ca | |||
| a48b226d9b | |||
| e11af03fdd | |||
| f044e6ace2 | |||
| e056afbd28 | |||
| 9442e3b150 | |||
| 9c00b9a86c | |||
| 622cfe260a | |||
| 0c3ced012e | |||
| d60c5007f9 | |||
| 7e10b8e68b | |||
| 08509caf1b | |||
| 89de2d2dfa | |||
| abc80b93ad | |||
| 1b79a6fc91 | |||
| 5a3aa64fba | |||
| bc4b096077 | |||
| 0025b87b0f | |||
| bd0843d3da | |||
| 0ba43d5714 | |||
| 97e1ca789a | |||
| 2df13fac10 | |||
| 186313f57c | |||
| 8b0be49609 | |||
| e062ee0447 | |||
| c81cf39fe9 | |||
| 9cfef867a9 | |||
| 18314e6a2e | |||
| 9f3df1a79a | |||
| fe424107e9 | |||
| d97535894c | |||
| 4d65e48b3c | |||
| 0d24662b3d | |||
| bc14b049c0 | |||
| d32c588f15 | |||
| 60e86aff2a | |||
| 5f83a1468c | |||
| 5758a12c3b | |||
| 49a74d02ea | |||
| f6d35f1eea | |||
| 3fb61a76fe | |||
| ff4452c971 | |||
| fd1d8a0e26 | |||
| b0d6da8e23 | |||
| 4ca0865fe3 | |||
| b8115362f8 | |||
| 155f747563 | |||
| b04976bde3 | |||
| 5f7a0d8a46 | |||
| 694232564a | |||
| 54b674ad43 | |||
| c32822037e | |||
| abb5c212c5 | |||
| e9b54a227b | |||
| befc32198a | |||
| 7e5103ff7e | |||
| 2cb600083e | |||
| 47d23673e6 | |||
| a0b357cf8c | |||
| 75943f2b93 | |||
| 085ab721f9 | |||
| f4156a5059 | |||
| 4bebec369f | |||
| 05c8613279 | |||
| 205d2813ad | |||
| 86660a0cc9 | |||
| 05830db206 | |||
| 3c79e812eb | |||
| 0287e7706a | |||
| 67a14c2725 | |||
| df03c5c1bf | |||
| 3ce3ffee3d | |||
| c0b32a1847 | |||
| 81ae0035c3 | |||
| 783da22ca2 | |||
| 8715c2b6f9 | |||
| 98a748dccc | |||
| bef031a414 | |||
| ce0e953182 | |||
| 9a5caf75c3 | |||
| 446f4a3a32 | |||
| da75912d26 | |||
| 4ed23c2a18 | |||
| 23ae334bef | |||
| ec75ae1486 | |||
| 368570592f | |||
| 806793421e | |||
| 1f377b242d | |||
| a4906b129a | |||
| 5c6e8925ad | |||
| a18eff689d | |||
| 274742d6d9 | |||
| cdebcbd4dc | |||
| cdf303a847 | |||
| dd35fe825c | |||
| 5c4a971aca | |||
| 7522b4d0ff | |||
| b0f314837e | |||
| 8a9ba7745a | |||
| b7d75a2749 | |||
| fe0d9b0aa6 | |||
| 7dba691ccc | |||
| 099e2154ba | |||
| f2efb25ad7 | |||
| e45c3d5b4e | |||
| 69f821baef | |||
| 11559d0962 | |||
| 79c33095fc | |||
| 0ec6bcbcef | |||
| 3cde067339 | |||
| c05ef32270 | |||
| 7eb913ec7b | |||
| 4e63093d25 | |||
| e5d50698d2 | |||
| 79c31c2d84 | |||
| aab266a661 | |||
| a4c9acb49f |
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: veeso
|
||||
liberapay: veeso
|
||||
@@ -3,6 +3,9 @@ name: "Build artifacts"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TERMSCP_VERSION: "1.0.0"
|
||||
|
||||
jobs:
|
||||
build-binaries:
|
||||
name: Build - ${{ matrix.platform.release_for }}
|
||||
@@ -11,32 +14,159 @@ 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: Linux-aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
target: aarch64-unknown-linux-gnu
|
||||
debian_suffix: arm64
|
||||
|
||||
- release_for: Windows-x86_64
|
||||
os: windows-latest
|
||||
platform: windows
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
||||
- release_for: Windows-aarch64
|
||||
os: windows-11-arm
|
||||
platform: windows
|
||||
target: aarch64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: codeberg-mirror
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: "Mirror to Codeberg"
|
||||
uses: yesolutions/mirror-action@v0.7.0
|
||||
with:
|
||||
REMOTE: 'ssh://git@codeberg.org/veeso/termscp.git'
|
||||
GIT_SSH_PRIVATE_KEY: ${{ secrets.GIT_SSH_PRIVATE_KEY }}
|
||||
GIT_SSH_NO_VERIFY_HOST: "true"
|
||||
@@ -11,13 +11,17 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y curl wget libsmbclient
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install termscp from script
|
||||
run: |
|
||||
./install.sh -v=0.12.3 -f
|
||||
./install.sh -f
|
||||
which termscp || exit 1
|
||||
termscp --version
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
@@ -14,13 +15,19 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt, clippy
|
||||
- name: Format
|
||||
run: cargo +nightly fmt --all -- --check
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
@@ -30,7 +37,5 @@ jobs:
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features github-actions --no-fail-fast
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -Dwarnings
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
@@ -14,14 +15,21 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
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
|
||||
|
||||
@@ -21,3 +21,4 @@ jobs:
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
exempt-issue-labels: "backlog"
|
||||
exempt-all-milestones: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
@@ -14,11 +15,11 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-2019
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
@@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz
|
||||
|
||||
dist/pkgs/
|
||||
dist/build/macos/openssl/
|
||||
|
||||
.idea/
|
||||
.claude/
|
||||
|
||||
|
||||
+300
-35
@@ -1,42 +1,307 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.15.0](#0150)
|
||||
- [0.14.0](#0140)
|
||||
- [0.13.0](#0130)
|
||||
- [0.12.3](#0123)
|
||||
- [0.12.2](#0122)
|
||||
- [0.12.1](#0121)
|
||||
- [0.12.0](#0120)
|
||||
- [0.11.3](#0113)
|
||||
- [0.11.2](#0112)
|
||||
- [0.11.1](#0111)
|
||||
- [0.11.0](#0110)
|
||||
- [0.10.0](#0100)
|
||||
- [0.9.0](#090)
|
||||
- [0.8.2](#082)
|
||||
- [0.8.1](#081)
|
||||
- [0.8.0](#080)
|
||||
- [0.7.0](#070)
|
||||
- [0.6.1](#061)
|
||||
- [0.6.0](#060)
|
||||
- [0.5.1](#051)
|
||||
- [0.5.0](#050)
|
||||
- [0.4.2](#042)
|
||||
- [0.4.1](#041)
|
||||
- [0.4.0](#040)
|
||||
- [0.3.3](#033)
|
||||
- [0.3.2](#032)
|
||||
- [0.3.1](#031)
|
||||
- [0.3.0](#030)
|
||||
- [0.2.0](#020)
|
||||
- [0.1.3](#013)
|
||||
- [0.1.2](#012)
|
||||
- [0.1.1](#011)
|
||||
- [0.1.0](#010)
|
||||
|
||||
---
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Released on 2026-04-18
|
||||
|
||||
### Added
|
||||
|
||||
- rework TransferProgress to track bytes with lazy estimation
|
||||
- replace dual progress bar components with single TransferProgressBar
|
||||
- simplify progress bar layout to single component
|
||||
- update progress bar display for new unified data model
|
||||
- update transfer loop to use unified TransferProgress
|
||||
- consolidate theme progress bar fields into single transfer_progress_bar
|
||||
|
||||
### CI
|
||||
|
||||
- Codeberg mirroring
|
||||
- run test workflows once
|
||||
- check fmt with nightly toolchain
|
||||
- add linux and windows aarch64 build targets
|
||||
|
||||
### Changed
|
||||
|
||||
- FileTransferActivity pane-agnostic dispatch (#386)
|
||||
> Comprehensive design for incremental refactoring of the 13k-line
|
||||
> FileTransferActivity god-struct using a unified Pane abstraction.
|
||||
> Detailed step-by-step plan covering 6 phases: split monoliths,
|
||||
> error handling, Pane struct, action dedup, session split, view reorg.
|
||||
> Extract 26 popup components from the monolithic 1,868-line popups.rs
|
||||
> into 20 individual files under popups/. Each file contains one or two
|
||||
> related components with their own imports. The popups.rs module file
|
||||
> now contains only module declarations and re-exports.
|
||||
> Replace 8 panic!() calls with error!() logging and early returns/fallthrough.
|
||||
> These panics documented invariants (e.g. "this tab can't do X") but would crash
|
||||
> the app if somehow triggered. Error logging is safer and more resilient.
|
||||
> Replace raw FileExplorer fields in Browser with Pane structs that bundle
|
||||
> the explorer and connected state. Move host_bridge_connected and
|
||||
> remote_connected from FileTransferActivity into the panes. Add navigation
|
||||
> API (fs_pane, opposite_pane, is_find_tab) for future unification tasks.
|
||||
> Rename private get_selected_file to get_selected_file_by_id and add three
|
||||
> new unified methods (get_selected_entries, get_selected_file, is_selected_one)
|
||||
> that dispatch based on self.browser.tab(). Old per-tab methods are kept for
|
||||
> now until their callers are migrated in subsequent tasks.
|
||||
> Collapse _local_/_remote_ action method pairs (mkdir, delete, symlink,
|
||||
> chmod, rename, copy) into unified methods that branch internally on
|
||||
> is_local_tab(). This halves the number of action methods and simplifies
|
||||
> the update.rs dispatch logic. Also unifies ShowFileInfoPopup and
|
||||
> ShowChmodPopup dispatching to use get_selected_entries().
|
||||
> Move `host_bridge` and `client` filesystem fields from FileTransferActivity
|
||||
> into the Pane struct, enabling tab-agnostic dispatch via `fs_pane()`/
|
||||
> `fs_pane_mut()`. This eliminates most `is_local_tab()` branching across
|
||||
> 15+ action files.
|
||||
> Key changes:
|
||||
> - Add `fs: Box<dyn HostBridge>` to Pane, remove from FileTransferActivity
|
||||
> - Replace per-side method pairs with unified pane-dispatched methods
|
||||
> - Unify navigation (changedir, reload, scan, file_exists, has_file_changed)
|
||||
> - Replace 147-line popup if/else chain with data-driven priority table
|
||||
> - Replace assert!/panic!/unreachable! with proper error handling
|
||||
> - Fix typo "filetransfer_activiy" across ~29 files
|
||||
> - Add unit tests for Pane
|
||||
>
|
||||
> Net result: -473 lines, single code path for most file operations.
|
||||
- replace lazy_static with std::sync::LazyLock
|
||||
- migrate from mod.rs to named module files
|
||||
- split parser internals into focused modules
|
||||
- split auth form components by protocol
|
||||
- split auth update handlers by context
|
||||
- split auth view helpers by responsibility
|
||||
|
||||
### Documentation
|
||||
|
||||
- date
|
||||
- add core module and API documentation
|
||||
- document core host and ssh modules
|
||||
- document parser and file transfer params
|
||||
- complete remaining core module docs
|
||||
- document ssh key storage API
|
||||
|
||||
### Fixed
|
||||
|
||||
- replace panics reachable from user input with proper error handling
|
||||
- replace assert! calls in UI activities with graceful error handling
|
||||
- correct typos in BadSytax and theme_provider log messages
|
||||
- replace magic-crypt with aes-gcm for bookmark encryption
|
||||
> magic-crypt has known vulnerabilities. Replace it with aes-gcm for new
|
||||
> encryption (authenticated, with random nonces) while keeping a legacy
|
||||
> AES-128-CBC decryption path to transparently handle existing bookmarks.
|
||||
- replace recursive byte-counting with entry-based transfer progress (#395)
|
||||
> * fix: replace recursive byte-counting with entry-based transfer progress
|
||||
>
|
||||
> Replace the expensive recursive `get_total_transfer_size` pre-calculation
|
||||
> with a lightweight entry-based counter (`TransferProgress`) for the
|
||||
> overall progress bar. This avoids deep `list_dir` traversals before
|
||||
> transfers begin, which could cause FTP idle-timeout disconnections on
|
||||
> large directory trees.
|
||||
>
|
||||
> The per-file byte-level progress bar (`ProgressStates`) remains
|
||||
> unchanged. Bytes are still tracked via `TransferStates::add_bytes` for
|
||||
> notification threshold logic.
|
||||
- return after empty terminal prompt
|
||||
- resolve `.` and `..` in terminal `cd` and prevent panic in path elide
|
||||
> `absolutize` now lexically normalizes paths so `cd ..` navigates to the
|
||||
> parent directory instead of appending `..` literally. Also guard against
|
||||
> `file_name()` returning `None` in `fmt_path_elide_ex`, which caused a
|
||||
> panic on paths containing unresolved `..` components.
|
||||
- pass full command string to exec, not just the first word
|
||||
> The `Exec` arm of `Command::from_str` only captured the first
|
||||
> whitespace-delimited token, silently dropping all arguments.
|
||||
> Now passes the entire input string so e.g. `ls -la /tmp` works.
|
||||
- sync browsing when entering a directory from filtered/fuzzy view
|
||||
- stabilize core error handling
|
||||
> Remove production panic and unwrap paths from core modules.
|
||||
>
|
||||
> Propagate bookmark encryption failures, harden file watcher and temp mapped file handling, and clean up dead code in shared utilities.
|
||||
- normalize localhost relative path checks
|
||||
- use time-based redraw interval instead of progress-delta threshold
|
||||
> The old 1% progress threshold caused the UI to appear frozen on large
|
||||
> files (e.g. 1GB) because many read/write iterations passed between
|
||||
> redraws. Switching to a 100ms time-based interval ensures consistent
|
||||
> UI responsiveness regardless of file size.
|
||||
- render progress bar immediately after mounting
|
||||
> Call self.view() right after mount_progress_bar() at all 6 call sites
|
||||
> so the bar is visible on screen before the transfer loop begins.
|
||||
- filter self-references and dot entries from remote directory listings
|
||||
> Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
|
||||
> to the listed directory in the LIST response, causing the current folder
|
||||
> to appear as a duplicate entry in the explorer.
|
||||
|
||||
### Miscellaneous
|
||||
- Use apt to install termscp on debian base to prevent broken deps
|
||||
- Copilot is so dumb
|
||||
- Install with apt if installed
|
||||
- WTF CTRL SAVE DOESNT FUCKING WORK
|
||||
- funding
|
||||
- add centralized lint configuration to Cargo.toml
|
||||
- 1.0.0 version into manifests
|
||||
- format toml
|
||||
- add fmt+clippy convention to CLAUDE.md and apply nightly fmt
|
||||
- enable safe clippy pedantic lints
|
||||
|
||||
### Performance
|
||||
|
||||
- use sort_by_cached_key to avoid repeated lowercase allocations in file sorting
|
||||
|
||||
### Testing
|
||||
|
||||
- add parser and bookmark regression coverage
|
||||
- extend config and explorer regression coverage
|
||||
- extend system regression coverage
|
||||
|
||||
### Build
|
||||
|
||||
- removed `hostname`, use `whoami` instead
|
||||
> whoami provides `hostname` function, so we don't need the hostname dependency, since whoami is also being used for getting the username
|
||||
- replaced libssh with russh for remotefs-ssh
|
||||
- replace `version-compare` crate with `semver`
|
||||
- remotefs-ssh 0.8.1
|
||||
- remotefs-ssh 0.8.2
|
||||
> this version removes any usage of sh commands from the sftp backend and only uses pure protocol functions
|
||||
- migrate to tui-realm 4.0
|
||||
> Upgrade tuirealm (3.x -> 4.0.0), tui-realm-stdlib (3 -> 4), tui-term
|
||||
> (0.2 -> 0.3). Apply all breaking changes from the 4.0 migration guide
|
||||
> across the termscp UI.
|
||||
>
|
||||
> Key changes:
|
||||
>
|
||||
> - Root-level re-exports removed; imports moved to module-qualified
|
||||
> paths (`tuirealm::application`, `::component`, `::event`, `::props`,
|
||||
> `::state`, `::subscription`, `::listener`, `::ratatui`). Same for
|
||||
> stdlib component types (`tui_realm_stdlib::components::*`).
|
||||
> - `MockComponent` trait renamed to `Component`; old `Component` trait
|
||||
> renamed to `AppComponent`. `#[derive(MockComponent)]` is now
|
||||
> `#[derive(Component)]`. `Component::on` now takes `&Event<_>`.
|
||||
> - `TextSpan` replaced with `SpanStatic`/`LineStatic`/`TextStatic`
|
||||
> (ratatui-based); tuple `(String, Alignment)` titles replaced with
|
||||
> the new `Title` builder; `Alignment` split into
|
||||
> `HorizontalAlignment`/`VerticalAlignment`; stdlib components use
|
||||
> `.alignment_horizontal` instead of `.alignment`.
|
||||
> - `State::One`/`PropPayload::One` -> `Single`. `CmdResult::None`
|
||||
> -> `NoChange`. `Props::get_or` removed; `Props::get` now returns a
|
||||
> borrowed `Option<&AttrValue>` (call sites switched to
|
||||
> `.and_then(AttrValue::as_*)`). `Component::query` returns
|
||||
> `Option<QueryResult<'a>>`.
|
||||
> - `Attribute::HighlightedColor` -> `HighlightStyle` (a full `Style`).
|
||||
> `.highlighted_*` helpers renamed to `.highlight_*`.
|
||||
> - `PollStrategy::UpTo(n)` now requires a `Duration`; tick timeout moved
|
||||
> from `EventListenerCfg::poll_timeout` into `PollStrategy`.
|
||||
> - `TerminalBridge` removed; `Context` now holds
|
||||
> `CrosstermTerminalAdapter` directly and enables raw mode + alternate
|
||||
> screen explicitly. The `TerminalAdapter` trait is imported where its
|
||||
> methods are used.
|
||||
> - `Update` trait removed; activity `update` methods are plain inherent
|
||||
> functions.
|
||||
> - `ProgressBar` replaced by stdlib `Gauge`. Paragraph `.wrap` renamed
|
||||
> to `.wrap_trim`; `.text` now takes an `Into<Text>`. Stdlib `List` row
|
||||
> items are now individual lines (`Vec<Span>` per row) rather than a
|
||||
> `Table` of spans; custom `FileList`/`Log` convert between the two
|
||||
> models.
|
||||
> - Radio builders drop `.foreground(color)` so unselected items render
|
||||
> with the terminal default foreground, and set
|
||||
> `highlight_style(Style::default().fg(color).add_modifier(REVERSED))`
|
||||
> so the selected entry is visibly highlighted only with the theme
|
||||
> color.
|
||||
> - Custom `FileList` keeps the selected row highlighted with the full
|
||||
> highlight style when focused and falls back to a foreground-only
|
||||
> style when unfocused.
|
||||
> - Theme loading is now backwards compatible: `Theme` uses a custom
|
||||
> `Deserialize` through an intermediate `ThemeFile` with optional
|
||||
> fields, so missing keys, unknown values or legacy aliases
|
||||
> (`transfer_progress_bar_full`/`_partial`) fall back to defaults on a
|
||||
> per-field basis instead of failing the whole load.
|
||||
- upgrade remotefs-ssh to 0.8.3
|
||||
|
||||
## 0.19.1
|
||||
|
||||
Released on 2026-04-18
|
||||
|
||||
- [Issue 371](https://github.com/veeso/termscp/issues/371): Updated dependencies to allow build on NetBSD
|
||||
|
||||
## 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
|
||||
|
||||
- [**Multi Host support**](https://github.com/veeso/termscp/issues/285):
|
||||
- Now it is possible to work on two different remotes `remote A -> remote B` instead of just `localhost -> remote`
|
||||
- Cli arguments now accept an additional `remote-args` for the left panel.
|
||||
- For more details read this issue <https://github.com/veeso/termscp/issues/285>.
|
||||
- Change between auth forms with `<BACKTAB>`
|
||||
- Bookmarks are automatically loaded into the last auth form.
|
||||
- [Issue 289](https://github.com/veeso/termscp/issues/289): Use `uzers` instead of the dead package `users` which has several vulnerabilities
|
||||
- [Issue 290](https://github.com/veeso/termscp/issues/290): Password prompt was broken
|
||||
- [Issue 298](https://github.com/veeso/termscp/issues/298): tuirealm 2.x
|
||||
- Fixed some performance issues where sometimes the app froze for a couple of seconds, thanks to this <https://github.com/veeso/tui-realm/pull/78>.
|
||||
- [Issue 292](https://github.com/veeso/termscp/issues/292): New version alert was not displayed due to a semver regex issue.
|
||||
- [Issue 291](https://github.com/veeso/termscp/issues/291): Show `..` directory before all the others in the explorer. If you click on it you'll go the parent directory (same as pressing `<U>`). No, you can't select it for transfers and it's actually been implemented in the worse way possible, because this little change would require a huge refactoring of the explorer component. I promise I will do it one day, but I dunno when.
|
||||
- Logging: filter out messages not related to termscp or remotefs
|
||||
|
||||
## 0.15.0
|
||||
|
||||
Released on 03/10/2024
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
termscp is a terminal file transfer client with a TUI (Terminal User Interface), supporting SFTP, SCP, FTP/FTPS, Kube, S3, SMB, and WebDAV protocols. It features a dual-pane file explorer (local + remote), bookmarks, system keyring integration, file watching/sync, an embedded terminal, and customizable themes.
|
||||
|
||||
- **Language**: Rust (edition 2024, MSRV 1.89.0)
|
||||
- **UI Framework**: tuirealm v3 (built on crossterm)
|
||||
- **File Transfer**: remotefs ecosystem
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
cargo build --release
|
||||
cargo build --no-default-features # minimal build without SMB/keyring
|
||||
|
||||
# Test (CI-equivalent)
|
||||
cargo test --no-default-features --features github-actions --no-fail-fast
|
||||
|
||||
# Run a single test
|
||||
cargo test <test_name> -- --nocapture
|
||||
|
||||
# Run tests for a module
|
||||
cargo test --lib filetransfer::
|
||||
cargo test --lib config::params::tests
|
||||
|
||||
# Lint
|
||||
cargo clippy -- -Dwarnings
|
||||
|
||||
# Format
|
||||
cargo fmt --all -- --check # check only
|
||||
cargo fmt --all # fix
|
||||
```
|
||||
|
||||
### System Dependencies (for building)
|
||||
|
||||
- **Linux**: `libdbus-1-dev`, `libsmbclient-dev`
|
||||
- **macOS**: `pkg-config`, `samba` (brew, with force link)
|
||||
|
||||
## Feature Flags
|
||||
|
||||
- **`smb`** (default): SMB/Samba protocol support
|
||||
- **`keyring`** (default): System keyring integration for password storage
|
||||
- **`smb-vendored`**: Vendored SMB library (for static builds)
|
||||
- **`github-actions`**: CI flag — disables real keyring in tests, uses file-based storage
|
||||
- **`isolated-tests`**: For parallel test isolation
|
||||
|
||||
## Architecture
|
||||
|
||||
### Application Lifecycle
|
||||
|
||||
```
|
||||
main.rs → parse CLI args → ActivityManager::new() → ActivityManager::run()
|
||||
↓
|
||||
Activity loop (draw → poll → update)
|
||||
├── AuthActivity (login/bookmarks)
|
||||
├── FileTransferActivity (dual-pane explorer)
|
||||
└── SetupActivity (configuration)
|
||||
```
|
||||
|
||||
`ActivityManager` owns a `Context` that is passed between activities. Each activity takes ownership of the Context on `on_create()` and returns it on `on_destroy()`.
|
||||
|
||||
### Key Modules
|
||||
|
||||
| Module | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **activity_manager** | `src/activity_manager.rs` | Orchestrates activity lifecycle and transitions |
|
||||
| **ui/activities** | `src/ui/activities/{auth,filetransfer,setup}/` | Three main screens, each implementing the `Activity` trait |
|
||||
| **ui/context** | `src/ui/context.rs` | Shared `Context` struct (terminal, config, bookmarks, theme) |
|
||||
| **filetransfer** | `src/filetransfer/` | Protocol enum, `RemoteFsBuilder`, connection parameters |
|
||||
| **host** | `src/host/` | `HostBridge` trait — abstracts local (`Localhost`) and remote (`RemoteBridged`) file operations |
|
||||
| **explorer** | `src/explorer/` | `FileExplorer` — directory navigation, sorting, filtering, transfer queue |
|
||||
| **system** | `src/system/` | `BookmarksClient`, `ConfigClient`, `ThemeProvider`, `SshKeyStorage`, `KeyStorage` trait |
|
||||
| **config** | `src/config/` | TOML-based serialization for themes, bookmarks, user params |
|
||||
|
||||
### Core Traits
|
||||
|
||||
- **`Activity`** (`src/ui/activities/mod.rs`): `on_create`, `on_draw`, `will_umount`, `on_destroy` — UI screen lifecycle
|
||||
- **`HostBridge`** (`src/host/bridge.rs`): Unified file operations interface (connect, list_dir, open_file, mkdir, remove, rename, copy, etc.)
|
||||
- **`KeyStorage`** (`src/system/keys/mod.rs`): `get_key`/`set_key` — password storage abstraction (keyring or encrypted file fallback)
|
||||
|
||||
### Conditional Compilation
|
||||
|
||||
The `build.rs` defines cfg aliases via `cfg_aliases`:
|
||||
- `posix`, `macos`, `linux`, `win` — platform shortcuts
|
||||
- `smb`, `smb_unix`, `smb_windows` — feature + platform combinations
|
||||
|
||||
Platform-specific dependencies: SSH and FTP crates use different TLS backends on Unix vs Windows. SMB support is completely gated behind the `smb` feature flag.
|
||||
|
||||
### File Transfer Protocols
|
||||
|
||||
`FileTransferProtocol` enum maps to protocol-specific parameter types (`ProtocolParams` enum) and `RemoteFsBuilder` constructs the appropriate `RemoteFs` client. Each protocol has its own params struct (e.g., `GenericProtocolParams` for SSH-based, `AwsS3Params`, `KubeProtocolParams`, `SmbParams`, `WebDAVProtocolParams`).
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- **rustfmt**: `group_imports = "StdExternalCrate"`, `imports_granularity = "Module"`
|
||||
- **Error handling**: Custom error types with `thiserror`, module-level Result aliases (e.g., `HostResult<T>`)
|
||||
- **Builder pattern**: Used for `RemoteFsBuilder`, `HostBridgeBuilder`
|
||||
- **Client pattern**: System services wrapped as clients (`BookmarksClient`, `ConfigClient`)
|
||||
- **Tests**: Unit tests in `#[cfg(test)]` blocks within source files. Tests requiring serial execution use `#[serial]` from `serial_test`
|
||||
- **Encryption**: Bookmark passwords encrypted with `magic-crypt`; keys stored in system keyring or encrypted file
|
||||
|
||||
## Other conventions
|
||||
|
||||
- Always run `cargo +nightly fmt --all` and `cargo clippy --no-default-features -- -Dwarnings` after modifying Rust code
|
||||
- Always put plans to `./.claude/plans/`
|
||||
+104
-48
@@ -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
|
||||
|
||||
+8
-4
@@ -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.
|
||||
@@ -134,7 +138,7 @@ Let's make it simple and clear:
|
||||
In addition to the process described for the PRs, I've also decided to introduce a list of guidelines to follow when writing the code, that should be followed:
|
||||
|
||||
1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on termscp, try not to add useless dependencies.
|
||||
2. **No C-bindings**: personally I think that Rust still relies too much on C. And that's bad, really bad. Many libraries in Rust are just wrappers to C libraries, which is a huge problem, especially considering this is a multiplatform project. Everytime you add a C-binding to your project, you're forcing your users to install additional libraries to their systems. Sometimes these libraries are already installed on their systems (as happens for libssh2 or openssl in this case), but sometimes not. So if you really have to add a dependency to this project, please AVOID completely adding C-bounded libraries.
|
||||
2. **No C-bindings**: personally I think that Rust still relies too much on C. And that's bad, really bad. Many libraries in Rust are just wrappers to C libraries, which is a huge problem, especially considering this is a multiplatform project. Everytime you add a C-binding to your project, you're forcing your users to install additional libraries to their systems. Sometimes these libraries are already installed on their systems (as happens for openssl in this case), but sometimes not. So if you really have to add a dependency to this project, please AVOID completely adding C-bounded libraries.
|
||||
3. **Test units matter**: Whenever you implement something new to this project, always implement test units which cover the most cases as possible.
|
||||
4. **Comments are useful**: Many people say that the code should be that simple to talk by itself about what it does, and comments should then be useless. I personally don't agree. I'm not saying they're wrong, but I'm just saying that this approach has, in my personal opinion, many aspects which are underrated:
|
||||
1. What's obvious for me, might not be for the others.
|
||||
|
||||
Generated
+4178
-1254
File diff suppressed because it is too large
Load Diff
+107
-87
@@ -1,16 +1,17 @@
|
||||
[package]
|
||||
name = "termscp"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
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"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
categories = ["command-line-utilities"]
|
||||
homepage = "https://termscp.veeso.dev"
|
||||
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||
license = "MIT"
|
||||
name = "termscp"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
version = "0.15.0"
|
||||
rust-version = "1.89.0"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -23,94 +24,113 @@ 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"
|
||||
|
||||
[features]
|
||||
default = ["keyring", "smb"]
|
||||
github-actions = []
|
||||
isolated-tests = []
|
||||
keyring = []
|
||||
smb = ["dep:remotefs-smb"]
|
||||
smb-vendored = ["remotefs-smb/vendored"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8"
|
||||
aes-gcm = "0.10"
|
||||
argh = "0.1"
|
||||
base64 = "0.22"
|
||||
bitflags = "2"
|
||||
bytesize = "2"
|
||||
cbc = { version = "0.1", features = ["alloc"] }
|
||||
chrono = "0.4"
|
||||
content_inspector = "0.2"
|
||||
dirs = "6"
|
||||
edit = "0.1"
|
||||
filetime = "0.2"
|
||||
keyring = { version = "3", features = [
|
||||
"apple-native",
|
||||
"sync-secret-service",
|
||||
"vendored",
|
||||
"windows-native",
|
||||
] }
|
||||
lazy-regex = "3"
|
||||
log = "0.4"
|
||||
md-5 = "0.10"
|
||||
notify = "8"
|
||||
notify-rust = { version = "4", default-features = false, features = ["d"] }
|
||||
nucleo = "0.5"
|
||||
open = "5"
|
||||
rand = "0.9"
|
||||
regex = "1"
|
||||
remotefs = "0.3"
|
||||
remotefs-aws-s3 = "0.4"
|
||||
remotefs-kube = "0.4"
|
||||
remotefs-smb = { version = "0.3", optional = true }
|
||||
remotefs-ssh = { version = "0.8", default-features = false, features = ["russh"] }
|
||||
remotefs-webdav = "0.2"
|
||||
rpassword = "7"
|
||||
self_update = { version = "0.42", default-features = false, features = [
|
||||
"archive-tar",
|
||||
"archive-zip",
|
||||
"compression-flate2",
|
||||
"compression-zip-deflate",
|
||||
"rustls",
|
||||
] }
|
||||
semver = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
shellexpand = "3"
|
||||
simplelog = "0.12"
|
||||
ssh2-config = "0.7"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
toml = "1"
|
||||
tui-realm-stdlib = "4"
|
||||
tui-term = "0.3"
|
||||
tuirealm = "4"
|
||||
unicode-width = "0.2"
|
||||
whoami = "2"
|
||||
wildmatch = "2"
|
||||
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "0.4", features = [
|
||||
"native-tls",
|
||||
"native-tls-vendored",
|
||||
] }
|
||||
uzers = "0.12"
|
||||
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "0.4", features = ["native-tls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
serial_test = "3"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2"
|
||||
vergen-git2 = { version = "9", features = ["build", "cargo", "rustc", "si"] }
|
||||
|
||||
[[bin]]
|
||||
name = "termscp"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
argh = "^0.1"
|
||||
bitflags = "^2"
|
||||
bytesize = "^1"
|
||||
chrono = "^0.4"
|
||||
content_inspector = "^0.2"
|
||||
dirs = "^5.0"
|
||||
edit = "^0.1"
|
||||
filetime = "^0.2"
|
||||
hostname = "^0.4"
|
||||
keyring = { version = "^3", optional = true, features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"sync-secret-service",
|
||||
] }
|
||||
lazy-regex = "^3"
|
||||
lazy_static = "^1"
|
||||
log = "^0.4"
|
||||
magic-crypt = "^3"
|
||||
notify = "6"
|
||||
notify-rust = { version = "^4.5", default-features = false, features = ["d"] }
|
||||
nucleo = "0.5"
|
||||
open = "^5.0"
|
||||
rand = "^0.8.5"
|
||||
regex = "^1"
|
||||
remotefs = "^0.3"
|
||||
remotefs-aws-s3 = { version = "^0.3", default-features = false, features = [
|
||||
"find",
|
||||
"rustls",
|
||||
] }
|
||||
remotefs-kube = "0.4"
|
||||
remotefs-webdav = "^0.2"
|
||||
rpassword = "^7"
|
||||
self_update = { version = "^0.41", default-features = false, features = [
|
||||
"rustls",
|
||||
"archive-tar",
|
||||
"archive-zip",
|
||||
"compression-flate2",
|
||||
"compression-zip-deflate",
|
||||
] }
|
||||
serde = { version = "^1", features = ["derive"] }
|
||||
simplelog = "^0.12"
|
||||
ssh2-config = "^0.2"
|
||||
tempfile = "^3"
|
||||
thiserror = "^1"
|
||||
tokio = { version = "=1.38.1", features = ["rt"] }
|
||||
toml = "^0.8"
|
||||
tui-realm-stdlib = "^1.3"
|
||||
tuirealm = "^1.9"
|
||||
unicode-width = "^0.2"
|
||||
version-compare = "^0.2"
|
||||
whoami = "^1.5"
|
||||
wildmatch = "^2"
|
||||
[lints.rust]
|
||||
trivial_numeric_casts = "warn"
|
||||
unsafe_op_in_unsafe_fn = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "^1"
|
||||
serial_test = "^3"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["smb", "with-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"] }
|
||||
users = "0.11.0"
|
||||
[lints.clippy]
|
||||
complexity = { level = "warn", priority = -1 }
|
||||
correctness = { level = "warn", priority = -1 }
|
||||
cloned_instead_of_copied = "warn"
|
||||
implicit_clone = "warn"
|
||||
manual_string_new = "warn"
|
||||
perf = { level = "warn", priority = -1 }
|
||||
redundant_closure_for_method_calls = "warn"
|
||||
style = { level = "warn", priority = -1 }
|
||||
suspicious = { level = "warn", priority = -1 }
|
||||
unnested_or_patterns = "warn"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">Developed by <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Current version: 1.0.0 2026-04-18</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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
use cfg_aliases::cfg_aliases;
|
||||
use vergen_git2::{BuildBuilder, CargoBuilder, Emitter, Git2Builder, RustcBuilder, SysinfoBuilder};
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Setup cfg aliases
|
||||
cfg_aliases! {
|
||||
// Platforms
|
||||
macos: { target_os = "macos" },
|
||||
linux: { target_os = "linux" },
|
||||
unix: { target_family = "unix" },
|
||||
windows: { target_family = "windows" },
|
||||
posix: { target_family = "unix" },
|
||||
win: { target_family = "windows" },
|
||||
// exclusive features
|
||||
smb: { all(feature = "smb", not( macos )) },
|
||||
smb_unix: { all(unix, feature = "smb", not(macos)) },
|
||||
smb: { feature = "smb" },
|
||||
smb_unix: { all(unix, feature = "smb") },
|
||||
smb_windows: { all(windows, feature = "smb") }
|
||||
}
|
||||
|
||||
let build = BuildBuilder::all_build()?;
|
||||
let cargo = CargoBuilder::all_cargo()?;
|
||||
let git2 = Git2Builder::all_git()?;
|
||||
let rustc = RustcBuilder::all_rustc()?;
|
||||
let si = SysinfoBuilder::all_sysinfo()?;
|
||||
|
||||
Emitter::default()
|
||||
.add_instructions(&build)?
|
||||
.add_instructions(&cargo)?
|
||||
.add_instructions(&git2)?
|
||||
.add_instructions(&rustc)?
|
||||
.add_instructions(&si)?
|
||||
.emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
[changelog]
|
||||
body = """
|
||||
## {{ version | trim_start_matches(pat="v") }}
|
||||
|
||||
Released on {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- if commits | filter(attribute="breaking", value=true) | length > 0 %}
|
||||
|
||||
### ⚠ Breaking Changes
|
||||
|
||||
{%- for commit in commits | filter(attribute="breaking", value=true) %}
|
||||
- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | split(pat="\n") | first | trim }}
|
||||
{%- if commit.breaking_description %}
|
||||
> {{ commit.breaking_description }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{%- for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
### {{ group | upper_first }}
|
||||
|
||||
{%- for commit in commits %}
|
||||
- {% if commit.breaking %}💥 {% endif %}{% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | split(pat="\n") | first | trim }}
|
||||
{%- if commit.body %}
|
||||
> {{ commit.body | split(pat="\n") | join(sep="\n > ") }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
"""
|
||||
trim = false
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
split_commits = false
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Added" },
|
||||
{ message = "^fix", group = "Fixed" },
|
||||
{ message = "^refactor", group = "Changed" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^test", group = "Testing" },
|
||||
{ message = "^ci", group = "CI" },
|
||||
{ message = "^chore", group = "Miscellaneous" },
|
||||
]
|
||||
filter_commits = false
|
||||
tag_pattern = "v[0-9].*"
|
||||
sort_commits = "oldest"
|
||||
+26
-2
@@ -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 && \
|
||||
|
||||
Vendored
-1
@@ -43,7 +43,6 @@ www: \"https://termscp.veeso.dev/termscp/\"\n\
|
||||
maintainer: \"christian.visintin1997@gmail.com\"\n\
|
||||
prefix: \"/usr/local/bin\"\n\
|
||||
deps: {\n\
|
||||
libssh: {origin: security/libssh, version: 0.9.5}\n\
|
||||
}\n\
|
||||
files: {\n\
|
||||
/usr/local/bin/termscp: \"$HASH\"\n\
|
||||
|
||||
Vendored
+1
-1
@@ -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"
|
||||
|
||||
Vendored
+2
-2
@@ -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"
|
||||
|
||||
Vendored
+2
-2
@@ -81,7 +81,7 @@ fi
|
||||
# Build release (x86_64)
|
||||
X86_TARGET=""
|
||||
X86_TARGET_DIR=""
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
X86_TARGET="--target x86_64-apple-darwin"
|
||||
X86_TARGET_DIR="target/x86_64-apple-darwin/release/"
|
||||
fi
|
||||
@@ -92,7 +92,7 @@ RET_X86_64=$?
|
||||
|
||||
ARM64_TARGET=""
|
||||
ARM64_TARGET_DIR=""
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||
ARM64_TARGET="--target aarch64-apple-darwin"
|
||||
ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/"
|
||||
fi
|
||||
|
||||
-27
@@ -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
@@ -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"]
|
||||
+8
-10
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">Entwickelt von <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Aktuelle Version: 1.0.0 2026-04-18</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -186,19 +186,17 @@ 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` ⚠️
|
||||
|
||||
### Softwareanforderungen ❗
|
||||
|
||||
- **Linux** Benutzer:
|
||||
- libssh
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
- libsmbclient
|
||||
- **FreeBSD** Benutzer:
|
||||
- libssh
|
||||
- dbus
|
||||
- pkgconf
|
||||
- libsmbclient
|
||||
@@ -234,7 +232,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).
|
||||
|
||||
---
|
||||
|
||||
@@ -265,7 +263,7 @@ termscp wird von diesen großartigen Projekten unterstützt:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [russh](https://github.com/Eugeny/russh)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+50
-13
@@ -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)
|
||||
@@ -62,11 +67,11 @@
|
||||
|
||||
termscp kann mit den folgenden Optionen gestartet werden:
|
||||
|
||||
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
|
||||
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
|
||||
|
||||
ODER
|
||||
|
||||
`termscp [Optionen]... -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
|
||||
`termscp [Optionen]... -b [Lesezeichen-Name] -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
|
||||
|
||||
- `-P, --password <Passwort>` wenn Adresse angegeben wird, ist das Passwort dieses Argument
|
||||
- `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf
|
||||
@@ -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 ⏲️
|
||||
|
||||
|
||||
+8
-8
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">Desarrollado por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versión actual: 1.0.0 2026-04-18</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).
|
||||
|
||||
---
|
||||
|
||||
@@ -263,7 +263,7 @@ termscp funciona con estos increíbles proyectos:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [russh](https://github.com/Eugeny/russh)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+60
-21
@@ -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-)
|
||||
@@ -39,11 +44,11 @@
|
||||
|
||||
termscp se puede iniciar con las siguientes opciones:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
OR
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` si se proporciona la dirección, la contraseña será este argumento
|
||||
- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador
|
||||
@@ -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 ⏲️
|
||||
|
||||
|
||||
+8
-8
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">Développé par <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Version actuelle: 1.0.0 2026-04-18</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>
|
||||
|
||||
@@ -265,7 +265,7 @@ termscp est soutenu par ces projets impressionnants:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [russh](https://github.com/Eugeny/russh)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+58
-20
@@ -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-)
|
||||
@@ -37,11 +42,11 @@
|
||||
|
||||
termscp peut être démarré avec les options suivantes :
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
ou
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` si l'adresse est fournie, le mot de passe sera cet argument
|
||||
- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet
|
||||
@@ -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** .
|
||||
Lorsqu’on travaille avec des sélections, seules les fichiers sélectionnés seront affectés par les actions, tandis que l'élément actuellement surligné sera ignoré.
|
||||
|
||||
Il est également possible de travailler avec plusieurs fichiers depuis le panneau des résultats de recherche.
|
||||
|
||||
Toutes les actions sont disponibles avec des fichiers multiples, mais certaines peuvent se comporter différemment. Détails :
|
||||
|
||||
- *Copier* : lors de la copie, il vous sera demandé un nom de destination. Avec plusieurs fichiers, cela correspond au dossier de destination.
|
||||
- *Renommer* : identique à la copie, mais déplace les fichiers.
|
||||
- *Enregistrer sous* : identique à la copie, mais enregistre les fichiers à cet emplacement.
|
||||
|
||||
Si vous sélectionnez un fichier dans un dossier (ex. `/home`) puis changez de répertoire, il restera sélectionné et sera affiché dans la **file d’attente de transfert** en bas.
|
||||
Lorsqu’un fichier est sélectionné, le dossier *distant* courant lui est associé ; en cas de transfert, il sera envoyé vers ce dossier.
|
||||
|
||||
#### Exemple
|
||||
|
||||
Si on sélectionne `/home/a.txt` localement et que le panneau distant est sur `/tmp`, puis on passe à `/var`, on sélectionne `/var/b.txt` et que le panneau distant est sur `/home`, le transfert donnera :
|
||||
|
||||
- `/home/a.txt` transféré vers `/tmp/a.txt`
|
||||
- `/var/b.txt` transféré vers `/home/b.txt`
|
||||
|
||||
### Navigation synchronisée ⏲️
|
||||
|
||||
|
||||
+8
-8
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">Sviluppato da <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versione corrente: 1.0.0 2026-04-18</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).
|
||||
|
||||
---
|
||||
|
||||
@@ -263,7 +263,7 @@ se termscp esiste, è anche grazie a questi fantastici progetti:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [russh](https://github.com/Eugeny/russh)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+60
-22
@@ -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 l’ultima 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-)
|
||||
@@ -37,11 +42,11 @@
|
||||
|
||||
termscp può essere lanciato con questi argomenti:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
O
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi
|
||||
- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro
|
||||
@@ -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 l’ultima 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 all’elemento; quindi, se il file viene trasferito, verrà trasferito nella directory associata.
|
||||
|
||||
#### Esempio
|
||||
|
||||
Se selezioniamo un file locale `/home/a.txt`, siamo su `/tmp` nel pannello remoto, poi ci spostiamo su `/var`, selezioniamo `/var/b.txt`, e sul pannello remoto siamo su `/home`, eseguendo il trasferimento otterremo:
|
||||
|
||||
- `/home/a.txt` trasferito su `/tmp/a.txt`
|
||||
- `/var/b.txt` trasferito su `/home/b.txt`
|
||||
|
||||
### Synchronized browsing ⏲️
|
||||
|
||||
|
||||
+34
-5
@@ -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-)
|
||||
@@ -40,13 +42,15 @@
|
||||
|
||||
termscp can be started with the following options:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
OR
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument
|
||||
AND any combination of the two
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument. A password *can* be specified for each remote provided. The order must be the same of the address argument. The use of this parameter is discouraged.
|
||||
- `-b, --address-as-bookmark` resolve address argument as a bookmark name
|
||||
- `-q, --quiet` Disable logging
|
||||
- `-v, --version` Print version info
|
||||
@@ -163,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
|
||||
@@ -268,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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">Desenvolvido por <a href="https://veeso.me/" target="_blank">@veeso</a></p>
|
||||
<p align="center">Versão atual: 1.0.0 2026-04-18</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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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-)
|
||||
@@ -40,11 +42,11 @@
|
||||
|
||||
O termscp pode ser iniciado com as seguintes opções:
|
||||
|
||||
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
|
||||
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
|
||||
|
||||
OU
|
||||
|
||||
`termscp [opções]... -b [nome-do-favorito] [diretório-trabalho-local]`
|
||||
`termscp [opções]... -b [nome-do-favorito] -b [nome-do-favorito] [diretório-trabalho-local]`
|
||||
|
||||
- `-P, --password <senha>` se o endereço for fornecido, a senha será este argumento
|
||||
- `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito
|
||||
@@ -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 ⏲️
|
||||
|
||||
+8
-10
@@ -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>
|
||||
|
||||
<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.15.0 (03/10/2024)</p>
|
||||
<p align="center">由 <a href="https://veeso.me/" target="_blank">@veeso</a> 开发</p>
|
||||
<p align="center">当前版本: 1.0.0 2026-04-18</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/MIT"
|
||||
@@ -189,19 +189,17 @@ 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` ⚠️
|
||||
|
||||
### 依赖 ❗
|
||||
|
||||
- **Linux** 用户:
|
||||
- libssh
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
- libsmbclient
|
||||
- **FreeBSD** 用户:
|
||||
- libssh
|
||||
- dbus
|
||||
- pkgconf
|
||||
- libsmbclient
|
||||
@@ -238,7 +236,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)上找到。
|
||||
|
||||
---
|
||||
|
||||
@@ -270,7 +268,7 @@ termscp 由这些很棒的项目提供支持:
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [self_update](https://github.com/jaemk/self_update)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [russh](https://github.com/Eugeny/russh)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||
|
||||
+59
-18
@@ -8,11 +8,16 @@
|
||||
- [WebDAV 地址参数](#webdav-地址参数)
|
||||
- [SMB 地址参数](#smb-地址参数)
|
||||
- [如何输入密码](#如何输入密码)
|
||||
- [子命令](#子命令)
|
||||
- [导入主题](#导入主题)
|
||||
- [安装最新版本](#安装最新版本)
|
||||
- [导入 SSH 主机](#导入-ssh-主机)
|
||||
- [S3 连接参数](#s3-连接参数)
|
||||
- [Aws S3 凭证](#aws-s3-凭证)
|
||||
- [文件浏览](#文件浏览)
|
||||
- [快捷键](#快捷键)
|
||||
- [处理多个文件](#处理多个文件)
|
||||
- [操作多个文件 🥷](#操作多个文件-)
|
||||
- [示例](#示例)
|
||||
- [同步浏览](#同步浏览)
|
||||
- [打开/打开方式](#打开打开方式)
|
||||
- [书签](#书签)
|
||||
@@ -37,11 +42,11 @@
|
||||
|
||||
termscp启动时可以使用以下选项:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
或作为
|
||||
|
||||
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
|
||||
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` 登陆密码
|
||||
- `-b, --address-as-bookmark` 将地址参数解析为书签名称
|
||||
@@ -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`
|
||||
|
||||
### 同步浏览
|
||||
|
||||
|
||||
+53
-31
@@ -8,10 +8,10 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="0.15.0"
|
||||
TERMSCP_VERSION="1.0.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() {
|
||||
@@ -217,6 +217,49 @@ install_with_brew() {
|
||||
fi
|
||||
}
|
||||
|
||||
install_on_debian() {
|
||||
local pkg_manager
|
||||
if has apt; then
|
||||
pkg_manager="apt"
|
||||
elif has "apt-get"; then
|
||||
pkg_manager="apt-get"
|
||||
else
|
||||
pkg_manager="dpkg"
|
||||
fi
|
||||
|
||||
info "Detected $pkg_manager on your system"
|
||||
case "${ARCH}" in
|
||||
x86_64) DEB_URL="$DEB_URL_AMD64" ;;
|
||||
aarch64) DEB_URL="$DEB_URL_AARCH64" ;;
|
||||
*) try_with_cargo "we don't distribute packages for ${ARCH} at the moment" && return $? ;;
|
||||
esac
|
||||
info "Installing ${GREEN}termscp${NO_COLOR} via Debian package"
|
||||
archive=$(get_tmpfile "deb")
|
||||
download "${archive}" "${DEB_URL}"
|
||||
info "Downloaded debian package to ${archive}"
|
||||
if test_writeable "/usr/bin"; then
|
||||
sudo=""
|
||||
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
|
||||
else
|
||||
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}…"
|
||||
elevate_priv
|
||||
sudo="sudo"
|
||||
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
|
||||
fi
|
||||
info "$msg"
|
||||
|
||||
if [ "$pkg_manager" = "apt" ]; then
|
||||
$sudo apt install -y "${archive}"
|
||||
elif [ "$pkg_manager" = "apt-get" ]; then
|
||||
$sudo dpkg -i "${archive}"
|
||||
$sudo apt-get -f install
|
||||
else
|
||||
$sudo dpkg -i "${archive}"
|
||||
fi
|
||||
|
||||
rm -f ${archive}
|
||||
}
|
||||
|
||||
install_on_linux() {
|
||||
local msg
|
||||
local sudo
|
||||
@@ -236,28 +279,7 @@ install_on_linux() {
|
||||
elif has pikaur; then
|
||||
install_on_arch_linux pikaur
|
||||
elif has dpkg; then
|
||||
case "${ARCH}" in
|
||||
x86_64) DEB_URL="$DEB_URL_AMD64" ;;
|
||||
aarch64) DEB_URL="$DEB_URL_AARCH64" ;;
|
||||
*) try_with_cargo "we don't distribute packages for ${ARCH} at the moment" && return $? ;;
|
||||
esac
|
||||
info "Detected dpkg on your system"
|
||||
info "Installing ${GREEN}termscp${NO_COLOR} via Debian package"
|
||||
archive=$(get_tmpfile "deb")
|
||||
download "${archive}" "${DEB_URL}"
|
||||
info "Downloaded debian package to ${archive}"
|
||||
if test_writeable "/usr/bin"; then
|
||||
sudo=""
|
||||
msg="Installing ${GREEN}termscp${NO_COLOR}, please wait…"
|
||||
else
|
||||
warn "Root permissions are required to install ${GREEN}termscp${NO_COLOR}…"
|
||||
elevate_priv
|
||||
sudo="sudo"
|
||||
msg="Installing ${GREEN}termscp${NO_COLOR} as root, please wait…"
|
||||
fi
|
||||
info "$msg"
|
||||
$sudo dpkg -i "${archive}"
|
||||
rm -f ${archive}
|
||||
install_on_debian
|
||||
elif has brew; then
|
||||
install_with_brew
|
||||
else
|
||||
@@ -277,9 +299,9 @@ install_on_macos() {
|
||||
|
||||
install_bsd_cargo_deps() {
|
||||
set -e
|
||||
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
|
||||
confirm "${YELLOW}gcc${NO_COLOR} is required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
|
||||
sudo="$(elevate_priv_ex /usr/local/bin)"
|
||||
$sudo pkg install -y curl wget libssh gcc dbus pkgconf libsmbclient
|
||||
$sudo pkg install -y curl wget gcc dbus pkgconf libsmbclient
|
||||
info "Dependencies installed successfully"
|
||||
}
|
||||
|
||||
@@ -305,7 +327,7 @@ install_linux_cargo_deps() {
|
||||
exit 1
|
||||
fi
|
||||
set -e
|
||||
confirm "${YELLOW}libssh, gcc, openssl, pkg-config, libdbus${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}. The following command will be used to install the dependencies: '${BOLD}${YELLOW}${deps_cmd}${NO_COLOR}'. Would you like to proceed?"
|
||||
confirm "${YELLOW}gcc, openssl, pkg-config, libdbus${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}. The following command will be used to install the dependencies: '${BOLD}${YELLOW}${deps_cmd}${NO_COLOR}'. Would you like to proceed?"
|
||||
sudo="$(elevate_priv_ex /usr/local/bin)"
|
||||
$sudo $deps_cmd
|
||||
info "Dependencies installed successfully"
|
||||
@@ -451,7 +473,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"
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<p class="text-xs font-thin">
|
||||
<span>Christian Visintin © </span><span resolve-copyright></span>
|
||||
<span> | </span>
|
||||
<a class="text-xs font-thin" href="https://veeso.dev/en/privacy" target="_blank">Privacy policy</a>
|
||||
<a class="text-xs font-thin" href="https://veeso.me/en/privacy" target="_blank">Privacy policy</a>
|
||||
<span> | </span>
|
||||
<a class="text-xs font-thin" href="https://veeso.dev/en/cookie-policy" target="_blank">Cookie policy</a>
|
||||
<a class="text-xs font-thin" href="https://veeso.me/en/cookie-policy" target="_blank">Cookie policy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<span translate="getStarted.windows.moderation">Consider that Chocolatey moderation can take up to a few weeks
|
||||
since last release, so if the latest version is not available yet,
|
||||
you can install it downloading the ZIP file from</span>
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.0.15.0.nupkg"
|
||||
<a href="https://github.com/veeso/termscp/releases/latest/download/termscp.1.0.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.15.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_1.0.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>
|
||||
|
||||
+1
-1
@@ -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.15.0 is NOW out! Download it from</span>
|
||||
<span translate="intro.versionAlert">termscp 1.0.0 is NOW out! Download it from</span>
|
||||
<a href="/get-started.html" translate="intro.here">here!</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -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.15.0 is NOW out! Download it from",
|
||||
"versionAlert": "termscp 1.0.0 is NOW out! Download it from",
|
||||
"here": "here",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+1
-1
@@ -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.15.0 ya está disponible! Descárgalo desde",
|
||||
"versionAlert": "termscp 1.0.0 ya está disponible! Descárgalo desde",
|
||||
"here": "aquì",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+1
-1
@@ -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.15.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"versionAlert": "termscp 1.0.0 est maintenant sorti! Télécharge-le depuis",
|
||||
"here": "ici",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+1
-1
@@ -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.15.0 è ORA disponbile! Scaricalo da",
|
||||
"versionAlert": "termscp 1.0.0 è ORA disponbile! Scaricalo da",
|
||||
"here": "qui",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"intro": {
|
||||
"caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV",
|
||||
"getStarted": "开始 →",
|
||||
"versionAlert": "termscp 0.15.0 现已发布! 从下载",
|
||||
"versionAlert": "termscp 1.0.0 现已发布! 从下载",
|
||||
"here": "这里",
|
||||
"features": {
|
||||
"handy": {
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+205
-83
@@ -2,15 +2,17 @@
|
||||
//!
|
||||
//! `activity_manager` is the module which provides run methods and handling for activities
|
||||
|
||||
// Deps
|
||||
// Namespaces
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
||||
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::host::{HostError, Localhost};
|
||||
use crate::cli::{Remote, RemoteArgs};
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
||||
};
|
||||
use crate::host::HostError;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
@@ -30,6 +32,16 @@ pub enum NextActivity {
|
||||
SetupActivity,
|
||||
}
|
||||
|
||||
pub enum Host {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
pub enum HostParams {
|
||||
HostBridge(HostBridgeParams),
|
||||
Remote(FileTransferParams),
|
||||
}
|
||||
|
||||
/// The activity manager takes care of running activities and handling them until the application has ended
|
||||
pub struct ActivityManager {
|
||||
context: Option<Context>,
|
||||
@@ -38,7 +50,7 @@ pub struct ActivityManager {
|
||||
|
||||
impl ActivityManager {
|
||||
/// Initializes a new Activity Manager
|
||||
pub fn new(ticks: Duration) -> Result<ActivityManager, HostError> {
|
||||
pub fn new(ticks: Duration, keyring: bool) -> Result<ActivityManager, HostError> {
|
||||
// Prepare Context
|
||||
// Initialize configuration client
|
||||
let (config_client, error_config): (ConfigClient, Option<String>) =
|
||||
@@ -49,7 +61,7 @@ impl ActivityManager {
|
||||
(ConfigClient::degraded(), Some(err))
|
||||
}
|
||||
};
|
||||
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client() {
|
||||
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) {
|
||||
Ok(cli) => (cli, None),
|
||||
Err(err) => (None, Some(err)),
|
||||
};
|
||||
@@ -62,10 +74,106 @@ impl ActivityManager {
|
||||
})
|
||||
}
|
||||
|
||||
/// Configure remote args
|
||||
pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> {
|
||||
// Set for host bridge
|
||||
match remote_args.host_bridge {
|
||||
Remote::Bookmark(params) => self.resolve_bookmark_name(
|
||||
Host::HostBridge,
|
||||
¶ms.name,
|
||||
params.password.as_deref(),
|
||||
),
|
||||
Remote::Host(host_params) => self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(
|
||||
host_params.file_transfer_params.protocol,
|
||||
host_params.file_transfer_params.params,
|
||||
)),
|
||||
host_params.password.as_deref(),
|
||||
),
|
||||
Remote::None => {
|
||||
// local dir is remote_args.local_dir if set, otherwise current dir
|
||||
let local_dir = remote_args
|
||||
.local_dir
|
||||
.map(Ok)
|
||||
.unwrap_or_else(env::current_dir)
|
||||
.map_err(|err| format!("Could not resolve current directory: {err}"))?;
|
||||
debug!("host bridge is None, setting local dir to {:?}", local_dir,);
|
||||
|
||||
self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(local_dir)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}?;
|
||||
|
||||
// set remote
|
||||
match remote_args.remote {
|
||||
Remote::Bookmark(params) => {
|
||||
self.resolve_bookmark_name(Host::Remote, ¶ms.name, params.password.as_deref())
|
||||
}
|
||||
Remote::Host(host_params) => self.set_host_params(
|
||||
HostParams::Remote(host_params.file_transfer_params),
|
||||
host_params.password.as_deref(),
|
||||
),
|
||||
Remote::None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set file transfer params
|
||||
pub fn set_filetransfer_params(
|
||||
pub fn set_host_params(
|
||||
&mut self,
|
||||
mut params: FileTransferParams,
|
||||
host: HostParams,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let (remote_local_path, remote_remote_path) = match &host {
|
||||
HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let mut remote_params = match &host {
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => {
|
||||
Some((*protocol, protocol_params.clone()))
|
||||
}
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None,
|
||||
HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())),
|
||||
};
|
||||
|
||||
// Put params into the context
|
||||
if let Some((protocol, params)) = remote_params.as_mut() {
|
||||
self.resolve_password_for_protocol_params(*protocol, params, password)?;
|
||||
}
|
||||
|
||||
match host {
|
||||
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
|
||||
self.context_mut()?
|
||||
.set_host_bridge_params(HostBridgeParams::Localhost(path));
|
||||
}
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
|
||||
let (protocol, params) = remote_params.ok_or_else(|| {
|
||||
String::from("Missing remote parameters for host bridge configuration")
|
||||
})?;
|
||||
self.context_mut()?
|
||||
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
|
||||
}
|
||||
HostParams::Remote(_) => {
|
||||
let (protocol, params) = remote_params
|
||||
.ok_or_else(|| String::from("Missing remote parameters for remote host"))?;
|
||||
let params = FileTransferParams {
|
||||
local_path: remote_local_path,
|
||||
remote_path: remote_remote_path,
|
||||
protocol,
|
||||
params,
|
||||
};
|
||||
self.context_mut()?.set_remote_params(params);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_password_for_protocol_params(
|
||||
&mut self,
|
||||
protocol: FileTransferProtocol,
|
||||
params: &mut ProtocolParams,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
// Set password if provided
|
||||
@@ -73,28 +181,31 @@ impl ActivityManager {
|
||||
if let Some(password) = password {
|
||||
params.set_default_secret(password.to_string());
|
||||
} else if matches!(
|
||||
params.protocol,
|
||||
protocol,
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
|
||||
) && params.params.generic_params().is_some()
|
||||
) && params.generic_params().is_some()
|
||||
{
|
||||
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
||||
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
|
||||
let generic_params = params.params.generic_params().unwrap();
|
||||
let storage = SshKeyStorage::from(self.context_ref()?.config());
|
||||
let generic_params = params.generic_params().ok_or_else(|| {
|
||||
String::from("Missing generic parameters for SSH password resolution")
|
||||
})?;
|
||||
let username = generic_params
|
||||
.username
|
||||
.clone()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(whoami::username)
|
||||
.map_err(|err| format!("Could not get current username: {err}"))?;
|
||||
|
||||
if storage
|
||||
.resolve(
|
||||
&generic_params.address,
|
||||
&generic_params
|
||||
.username
|
||||
.clone()
|
||||
.unwrap_or(whoami::username()),
|
||||
)
|
||||
.resolve(&generic_params.address, &username)
|
||||
.is_none()
|
||||
{
|
||||
debug!(
|
||||
"storage could not find any suitable key for {}... prompting for password",
|
||||
generic_params.address
|
||||
);
|
||||
self.prompt_password(&mut params)?;
|
||||
self.prompt_password(params)?;
|
||||
} else {
|
||||
debug!(
|
||||
"a key is already set for {}; password is not required",
|
||||
@@ -102,17 +213,19 @@ impl ActivityManager {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.prompt_password(&mut params)?;
|
||||
self.prompt_password(params)?;
|
||||
}
|
||||
}
|
||||
// Put params into the context
|
||||
self.context.as_mut().unwrap().set_ftparams(params);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt user for password to set into params.
|
||||
fn prompt_password(&self, params: &mut FileTransferParams) -> Result<(), String> {
|
||||
match tty::read_secret_from_tty("Password: ") {
|
||||
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
||||
let ctx = self.context_mut()?;
|
||||
let prompt = format!("Password for {}: ", params.host_name());
|
||||
|
||||
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
|
||||
Err(err) => Err(format!("Could not read password: {err}")),
|
||||
Ok(Some(secret)) => {
|
||||
debug!(
|
||||
@@ -130,16 +243,28 @@ impl ActivityManager {
|
||||
/// Returns error if bookmark is not found
|
||||
pub fn resolve_bookmark_name(
|
||||
&mut self,
|
||||
host: Host,
|
||||
bookmark_name: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
|
||||
match bookmarks_client.get_bookmark(bookmark_name) {
|
||||
None => Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
)),
|
||||
Some(params) => self.set_filetransfer_params(params, password),
|
||||
}
|
||||
if let Some(bookmarks_client) = self.context_mut()?.bookmarks_client_mut() {
|
||||
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
||||
None => {
|
||||
return Err(format!(
|
||||
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
||||
));
|
||||
}
|
||||
Some(params) => params,
|
||||
};
|
||||
|
||||
let params = match host {
|
||||
Host::Remote => HostParams::Remote(params),
|
||||
Host::HostBridge => {
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params))
|
||||
}
|
||||
};
|
||||
|
||||
self.set_host_params(params, password)
|
||||
} else {
|
||||
Err(String::from(
|
||||
"Could not resolve bookmark name: bookmarks client not initialized",
|
||||
@@ -147,6 +272,18 @@ impl ActivityManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn context_mut(&mut self) -> Result<&mut Context, String> {
|
||||
self.context
|
||||
.as_mut()
|
||||
.ok_or_else(|| String::from("Activity manager context is not initialized"))
|
||||
}
|
||||
|
||||
fn context_ref(&self) -> Result<&Context, String> {
|
||||
self.context
|
||||
.as_ref()
|
||||
.ok_or_else(|| String::from("Activity manager context is not initialized"))
|
||||
}
|
||||
|
||||
///
|
||||
/// Loop for activity manager. You need to provide the activity to start with
|
||||
/// Returns the exitcode
|
||||
@@ -233,8 +370,17 @@ impl ActivityManager {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let host_bridge_params = match ctx.host_bridge_params() {
|
||||
Some(params) => params.clone(),
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: host bridge params is None");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// If ft params is None, return None
|
||||
let ft_params: &FileTransferParams = match ctx.ft_params() {
|
||||
let remote_params: &FileTransferParams = match ctx.remote_params() {
|
||||
Some(ft_params) => ft_params,
|
||||
None => {
|
||||
error!("Failed to start FileTransferActivity: file transfer params is None");
|
||||
@@ -242,28 +388,18 @@ impl ActivityManager {
|
||||
}
|
||||
};
|
||||
|
||||
// get local path:
|
||||
// - if set in file transfer params, get it from there
|
||||
// - otherwise is env current dir
|
||||
// - otherwise is /
|
||||
let local_wrkdir = ft_params
|
||||
.local_path
|
||||
.clone()
|
||||
.or(std::env::current_dir().ok())
|
||||
.unwrap_or(PathBuf::from("/"));
|
||||
|
||||
// Prepare activity
|
||||
let host: Localhost = match Localhost::new(local_wrkdir) {
|
||||
Ok(host) => host,
|
||||
Err(err) => {
|
||||
// Set error in context
|
||||
error!("Failed to initialize localhost: {}", err);
|
||||
ctx.set_error(format!("Could not initialize localhost: {err}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut activity: FileTransferActivity =
|
||||
FileTransferActivity::new(host, ft_params, self.ticks);
|
||||
// try to setup activity
|
||||
let mut activity =
|
||||
match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
|
||||
Ok(activity) => activity,
|
||||
Err(err) => {
|
||||
error!("Failed to start FileTransferActivity: {}", err);
|
||||
ctx.set_error(err);
|
||||
self.context = Some(ctx);
|
||||
// Return to authentication
|
||||
return Some(NextActivity::Authentication);
|
||||
}
|
||||
};
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Create activity
|
||||
@@ -327,31 +463,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
|
||||
@@ -390,19 +503,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,35 @@
|
||||
//!
|
||||
//! defines the types for main.rs types
|
||||
|
||||
mod remote;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use argh::FromArgs;
|
||||
pub use remote::{Remote, RemoteArgs};
|
||||
|
||||
use crate::activity_manager::NextActivity;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
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(FromArgs)]
|
||||
#[derive(Default, FromArgs)]
|
||||
#[argh(description = "
|
||||
where positional can be:
|
||||
- [address] [local-wrkdir]
|
||||
- [address_a] [address_b] [local-wrkdir]
|
||||
OR
|
||||
- [bookmark-Name] [local-wrkdir]
|
||||
- -b [bookmark-name_1] -b [bookmark-name_2] [local-wrkdir]
|
||||
|
||||
and any combination of the above
|
||||
|
||||
Address syntax can be:
|
||||
|
||||
@@ -37,14 +45,15 @@ pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub nested: Option<ArgsSubcommands>,
|
||||
/// resolve address argument as a bookmark name
|
||||
#[argh(switch, short = 'b')]
|
||||
pub address_as_bookmark: bool,
|
||||
#[argh(option, short = 'b')]
|
||||
pub bookmark: Vec<String>,
|
||||
/// enable TRACE log level
|
||||
#[argh(switch, short = 'D')]
|
||||
pub debug: bool,
|
||||
/// provide password from CLI
|
||||
/// provide password from CLI; if you need to provide multiple passwords, use multiple -P flags.
|
||||
/// In case just respect the order of the addresses
|
||||
#[argh(option, short = 'P')]
|
||||
pub password: Option<String>,
|
||||
pub password: Vec<String>,
|
||||
/// disable logging
|
||||
#[argh(switch, short = 'q')]
|
||||
pub quiet: bool,
|
||||
@@ -54,11 +63,11 @@ 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 = "protocol://user@address:port:wrkdir local-wrkdir"
|
||||
)]
|
||||
#[argh(positional, description = "address1 address2 local-wrkdir")]
|
||||
pub positional: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -66,7 +75,8 @@ pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub enum ArgsSubcommands {
|
||||
Config(ConfigArgs),
|
||||
LoadTheme(LoadThemeArgs),
|
||||
ImportSshHosts(ImportSshHostsArgs),
|
||||
ImportTheme(ImportThemeArgs),
|
||||
Update(UpdateArgs),
|
||||
}
|
||||
|
||||
@@ -80,17 +90,28 @@ 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,
|
||||
}
|
||||
|
||||
pub struct RunOpts {
|
||||
pub remote: Remote,
|
||||
pub remote: RemoteArgs,
|
||||
pub keyring: bool,
|
||||
pub ticks: Duration,
|
||||
pub log_level: LogLevel,
|
||||
pub task: Task,
|
||||
@@ -111,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),
|
||||
@@ -122,45 +151,11 @@ impl RunOpts {
|
||||
impl Default for RunOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
remote: Remote::None,
|
||||
remote: RemoteArgs::default(),
|
||||
ticks: Duration::from_millis(10),
|
||||
keyring: true,
|
||||
log_level: LogLevel::Info,
|
||||
task: Task::Activity(NextActivity::Authentication),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Remote {
|
||||
Bookmark(BookmarkParams),
|
||||
Host(HostParams),
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct BookmarkParams {
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
pub struct HostParams {
|
||||
pub params: FileTransferParams,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl BookmarkParams {
|
||||
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().to_string(),
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostParams {
|
||||
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
|
||||
Self {
|
||||
params,
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//! ## Remote CLI Arguments
|
||||
//!
|
||||
//! Parses positional and bookmark-based CLI arguments into the normalized remote
|
||||
//! connection parameters used by the application.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::Args;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils;
|
||||
|
||||
/// Address type
|
||||
enum AddrType {
|
||||
Address,
|
||||
Bookmark,
|
||||
}
|
||||
|
||||
/// Args for remote connection
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteArgs {
|
||||
/// Optional host bridge selected for the session.
|
||||
pub host_bridge: Remote,
|
||||
/// Target remote selected for the session.
|
||||
pub remote: Remote,
|
||||
/// Optional local working directory override.
|
||||
pub local_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for RemoteArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host_bridge: Remote::None,
|
||||
remote: Remote::None,
|
||||
local_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Args> for RemoteArgs {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(args: &Args) -> Result<Self, Self::Error> {
|
||||
let mut remote_args = RemoteArgs::default();
|
||||
// validate arguments
|
||||
match (args.bookmark.len(), args.positional.len()) {
|
||||
(0, positional) if positional < 4 => Ok(()),
|
||||
(1, positional) if positional < 3 => Ok(()),
|
||||
(2, positional) if positional < 2 => Ok(()),
|
||||
(_, _) => Err("Too many arguments".to_string()),
|
||||
}?;
|
||||
// parse bookmark first
|
||||
let last_item_index = (args.bookmark.len() + args.positional.len()).saturating_sub(1);
|
||||
|
||||
let mut hosts = vec![];
|
||||
|
||||
for (i, (addr_type, arg)) in args
|
||||
.bookmark
|
||||
.iter()
|
||||
.map(|x| (AddrType::Bookmark, x))
|
||||
.chain(args.positional.iter().map(|x| (AddrType::Address, x)))
|
||||
.enumerate()
|
||||
{
|
||||
// check if has password
|
||||
let password = args.password.get(i).cloned();
|
||||
|
||||
// check if is last item and so a possible local dir
|
||||
if i == last_item_index && Path::new(arg).exists() {
|
||||
remote_args.local_dir = Some(PathBuf::from(arg));
|
||||
continue;
|
||||
}
|
||||
|
||||
let remote = match addr_type {
|
||||
AddrType::Address => Self::parse_remote_address(arg)
|
||||
.map(|x| Remote::Host(HostParams::new(x, password)))?,
|
||||
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
|
||||
};
|
||||
|
||||
// set remote
|
||||
hosts.push(remote);
|
||||
}
|
||||
|
||||
// set args based on hosts len
|
||||
if hosts.len() == 1 {
|
||||
remote_args.remote = hosts
|
||||
.pop()
|
||||
.ok_or_else(|| String::from("Missing remote host configuration"))?;
|
||||
} else if hosts.len() == 2 {
|
||||
remote_args.host_bridge = hosts
|
||||
.pop()
|
||||
.ok_or_else(|| String::from("Missing host-bridge configuration"))?;
|
||||
remote_args.remote = hosts
|
||||
.pop()
|
||||
.ok_or_else(|| String::from("Missing remote host configuration"))?;
|
||||
}
|
||||
|
||||
Ok(remote_args)
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteArgs {
|
||||
/// Parse remote address
|
||||
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
|
||||
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remote argument type
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum Remote {
|
||||
/// Bookmark name argument
|
||||
Bookmark(BookmarkParams),
|
||||
/// Host argument
|
||||
Host(HostParams),
|
||||
/// Unspecified
|
||||
None,
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
/// Returns whether this CLI slot was left unspecified.
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bookmark parameters
|
||||
#[derive(Debug)]
|
||||
pub struct BookmarkParams {
|
||||
/// bookmark name
|
||||
pub name: String,
|
||||
/// bookmark password
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
/// Host parameters
|
||||
#[derive(Debug)]
|
||||
pub struct HostParams {
|
||||
/// file transfer parameters
|
||||
pub file_transfer_params: FileTransferParams,
|
||||
/// host password specified in arguments
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl BookmarkParams {
|
||||
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().to_string(),
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostParams {
|
||||
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
|
||||
Self {
|
||||
file_transfer_params: params,
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_one_remote() {
|
||||
let args = Args {
|
||||
positional: vec!["scp://host1".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::None));
|
||||
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_two_remotes() {
|
||||
let args = Args {
|
||||
positional: vec!["scp://host1".to_string(), "scp://host2".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn test_should_make_remote_args_from_two_remotes_and_local_dir() {
|
||||
let args = Args {
|
||||
positional: vec![
|
||||
"scp://host1".to_string(),
|
||||
"scp://host2".to_string(),
|
||||
"/home".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
||||
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_one_bookmarks() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::None));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_two_bookmarks() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string(), "bar".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn test_should_make_remote_args_from_two_bookmarks_and_local_dir() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string(), "bar".to_string()],
|
||||
positional: vec!["/home".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_one_bookmark_and_one_remote() {
|
||||
let args = Args {
|
||||
bookmark: vec!["foo".to_string()],
|
||||
positional: vec!["scp://host1".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn test_should_make_remote_args_from_one_bookmark_and_one_remote_with_local_dir() {
|
||||
let args = Args {
|
||||
positional: vec!["scp://host1".to_string(), "/home".to_string()],
|
||||
bookmark: vec!["foo".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
|
||||
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
||||
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
||||
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
||||
}
|
||||
}
|
||||
@@ -108,9 +108,9 @@ impl From<FileTransferParams> for Bookmark {
|
||||
smb: Some(SmbParams::from(params.clone())),
|
||||
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,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! ## Bookmark S3 Parameters
|
||||
//!
|
||||
//! Stores the bookmark-specific representation of AWS S3 connection settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::AwsS3Params;
|
||||
@@ -5,13 +9,20 @@ use crate::filetransfer::params::AwsS3Params;
|
||||
/// Connection parameters for Aws s3 protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct S3Params {
|
||||
/// Bucket name to open.
|
||||
pub bucket: String,
|
||||
/// AWS region used for the bucket.
|
||||
pub region: Option<String>,
|
||||
/// Custom endpoint URL for S3-compatible services.
|
||||
pub endpoint: Option<String>,
|
||||
/// Shared credentials profile name.
|
||||
pub profile: Option<String>,
|
||||
/// Static access key identifier.
|
||||
pub access_key: Option<String>,
|
||||
/// Static secret access key.
|
||||
pub secret_access_key: Option<String>,
|
||||
/// NOTE: there are no session token and security token since they are always temporary
|
||||
/// Whether to force path-style bucket addressing.
|
||||
pub new_path_style: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! ## Bookmark Kube Parameters
|
||||
//!
|
||||
//! Stores bookmark-specific Kubernetes connection settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::KubeProtocolParams;
|
||||
@@ -5,10 +9,15 @@ use crate::filetransfer::params::KubeProtocolParams;
|
||||
/// Extra Connection parameters for Kube protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct KubeParams {
|
||||
/// Optional default namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional cluster API URL.
|
||||
pub cluster_url: Option<String>,
|
||||
/// Optional Kubernetes username override.
|
||||
pub username: Option<String>,
|
||||
/// Optional client certificate path.
|
||||
pub client_cert: Option<String>,
|
||||
/// Optional client key path.
|
||||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! ## Bookmark SMB Parameters
|
||||
//!
|
||||
//! Stores bookmark-specific SMB share configuration.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||
@@ -5,11 +9,13 @@ use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||
/// Extra Connection parameters for SMB protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct SmbParams {
|
||||
/// SMB share name.
|
||||
pub share: String,
|
||||
/// Optional SMB workgroup used on POSIX platforms.
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
impl From<TransferSmbParams> for SmbParams {
|
||||
fn from(params: TransferSmbParams) -> Self {
|
||||
Self {
|
||||
@@ -19,7 +25,7 @@ impl From<TransferSmbParams> for SmbParams {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
impl From<TransferSmbParams> for SmbParams {
|
||||
fn from(params: TransferSmbParams) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Contains the error for serializer/deserializer
|
||||
@@ -63,7 +63,7 @@ where
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::Serialization,
|
||||
err.to_string(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
trace!("Serialized new bookmarks data: {}", data);
|
||||
@@ -112,7 +112,7 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use super::*;
|
||||
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -442,6 +442,34 @@ mod tests {
|
||||
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_deserialize_webdav_bookmark_protocol_alias() {
|
||||
let toml_file: tempfile::NamedTempFile = create_http_alias_toml_bookmarks();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().rewind().unwrap();
|
||||
|
||||
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
|
||||
let host = hosts.bookmarks.get("webdav").unwrap();
|
||||
|
||||
assert_eq!(host.protocol, FileTransferProtocol::WebDAV);
|
||||
assert_eq!(host.address.as_deref(), Some("https://myserver:4445"));
|
||||
assert_eq!(host.username.as_deref(), Some("omar"));
|
||||
assert_eq!(host.password.as_deref(), Some("mypassword"));
|
||||
assert_eq!(
|
||||
host.remote_path.as_deref(),
|
||||
Some(std::path::Path::new("/myshare/dir/subdir"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_fail_deserialize_bookmark_with_invalid_protocol() {
|
||||
let toml_file: tempfile::NamedTempFile = create_invalid_protocol_toml_bookmarks();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().rewind().unwrap();
|
||||
|
||||
assert!(deserialize::<UserHosts>(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_bookmarks_serializer_serialize() {
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
|
||||
@@ -583,10 +611,12 @@ mod tests {
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().rewind().unwrap();
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
|
||||
// Malformed theme files must still load successfully; unknown or invalid
|
||||
// fields fall back to defaults so user themes remain backwards compatible.
|
||||
let toml_file = create_bad_toml_theme();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().rewind().unwrap();
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err());
|
||||
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -693,6 +723,39 @@ mod tests {
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_http_alias_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
|
||||
[bookmarks.webdav]
|
||||
protocol = "HTTPS"
|
||||
address = "https://myserver:4445"
|
||||
username = "omar"
|
||||
password = "mypassword"
|
||||
directory = "/myshare/dir/subdir"
|
||||
|
||||
[recents]
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_invalid_protocol_toml_bookmarks() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
|
||||
[bookmarks.broken]
|
||||
protocol = "GOPHER"
|
||||
address = "gopher://myserver"
|
||||
|
||||
[recents]
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_good_toml_theme() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r##"auth_address = "Yellow"
|
||||
@@ -714,8 +777,7 @@ mod tests {
|
||||
transfer_local_explorer_highlighted = "Yellow"
|
||||
transfer_log_background = "255, 255, 255"
|
||||
transfer_log_window = "LightGreen"
|
||||
transfer_progress_bar_full = "forestgreen"
|
||||
transfer_progress_bar_partial = "Green"
|
||||
transfer_progress_bar = "forestgreen"
|
||||
transfer_remote_explorer_background = "#f0f0f0"
|
||||
transfer_remote_explorer_foreground = "rgb(40, 40, 40)"
|
||||
transfer_remote_explorer_highlighted = "LightBlue"
|
||||
|
||||
+209
-127
@@ -6,151 +6,68 @@
|
||||
// ext
|
||||
use serde::de::Error as DeError;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use crate::utils::fmt::fmt_color;
|
||||
use crate::utils::parser::parse_color;
|
||||
|
||||
/// Theme contains all the colors lookup table for termscp
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct Theme {
|
||||
// -- auth
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_address: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_bookmarks: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_password: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_port: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_protocol: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_recents: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub auth_username: Color,
|
||||
// -- misc
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_error_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_info_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_input_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_keys: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_quit_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_save_dialog: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub misc_warn_dialog: Color,
|
||||
// -- transfer
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_local_explorer_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_local_explorer_foreground: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_local_explorer_highlighted: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_log_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_log_window: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_progress_bar_full: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
pub transfer_progress_bar_partial: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_progress_bar: Color,
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_remote_explorer_background: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_remote_explorer_foreground: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_remote_explorer_highlighted: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_status_hidden: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_status_sorting: Color,
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color",
|
||||
serialize_with = "serialize_color"
|
||||
)]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub transfer_status_sync_browsing: Color,
|
||||
}
|
||||
|
||||
@@ -176,8 +93,7 @@ impl Default for Theme {
|
||||
transfer_local_explorer_highlighted: Color::Yellow,
|
||||
transfer_log_background: Color::Reset,
|
||||
transfer_log_window: Color::LightGreen,
|
||||
transfer_progress_bar_partial: Color::Green,
|
||||
transfer_progress_bar_full: Color::Green,
|
||||
transfer_progress_bar: Color::Green,
|
||||
transfer_remote_explorer_background: Color::Reset,
|
||||
transfer_remote_explorer_foreground: Color::Reset,
|
||||
transfer_remote_explorer_highlighted: Color::LightBlue,
|
||||
@@ -189,16 +105,124 @@ impl Default for Theme {
|
||||
}
|
||||
|
||||
// -- deserializer
|
||||
//
|
||||
// Custom deserialization: every field is optional and falls back to `Theme::default()`
|
||||
// when missing or when the supplied color string is invalid. This keeps user themes
|
||||
// backwards compatible even when fields are added, renamed, or contain typos.
|
||||
|
||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = Deserialize::deserialize(deserializer)?;
|
||||
// Parse color
|
||||
match parse_color(&s) {
|
||||
None => Err(DeError::custom("Invalid color")),
|
||||
Some(color) => Ok(color),
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
struct ThemeFile {
|
||||
auth_address: Option<String>,
|
||||
auth_bookmarks: Option<String>,
|
||||
auth_password: Option<String>,
|
||||
auth_port: Option<String>,
|
||||
auth_protocol: Option<String>,
|
||||
auth_recents: Option<String>,
|
||||
auth_username: Option<String>,
|
||||
misc_error_dialog: Option<String>,
|
||||
misc_info_dialog: Option<String>,
|
||||
misc_input_dialog: Option<String>,
|
||||
misc_keys: Option<String>,
|
||||
misc_quit_dialog: Option<String>,
|
||||
misc_save_dialog: Option<String>,
|
||||
misc_warn_dialog: Option<String>,
|
||||
transfer_local_explorer_background: Option<String>,
|
||||
transfer_local_explorer_foreground: Option<String>,
|
||||
transfer_local_explorer_highlighted: Option<String>,
|
||||
transfer_log_background: Option<String>,
|
||||
transfer_log_window: Option<String>,
|
||||
transfer_progress_bar: Option<String>,
|
||||
// Legacy aliases for the now-consolidated `transfer_progress_bar` field.
|
||||
transfer_progress_bar_full: Option<String>,
|
||||
transfer_progress_bar_partial: Option<String>,
|
||||
transfer_remote_explorer_background: Option<String>,
|
||||
transfer_remote_explorer_foreground: Option<String>,
|
||||
transfer_remote_explorer_highlighted: Option<String>,
|
||||
transfer_status_hidden: Option<String>,
|
||||
transfer_status_sorting: Option<String>,
|
||||
transfer_status_sync_browsing: Option<String>,
|
||||
}
|
||||
|
||||
impl ThemeFile {
|
||||
fn into_theme(self) -> Theme {
|
||||
let defaults = Theme::default();
|
||||
fn pick(value: Option<String>, fallback: Color) -> Color {
|
||||
value.as_deref().and_then(parse_color).unwrap_or(fallback)
|
||||
}
|
||||
Theme {
|
||||
auth_address: pick(self.auth_address, defaults.auth_address),
|
||||
auth_bookmarks: pick(self.auth_bookmarks, defaults.auth_bookmarks),
|
||||
auth_password: pick(self.auth_password, defaults.auth_password),
|
||||
auth_port: pick(self.auth_port, defaults.auth_port),
|
||||
auth_protocol: pick(self.auth_protocol, defaults.auth_protocol),
|
||||
auth_recents: pick(self.auth_recents, defaults.auth_recents),
|
||||
auth_username: pick(self.auth_username, defaults.auth_username),
|
||||
misc_error_dialog: pick(self.misc_error_dialog, defaults.misc_error_dialog),
|
||||
misc_info_dialog: pick(self.misc_info_dialog, defaults.misc_info_dialog),
|
||||
misc_input_dialog: pick(self.misc_input_dialog, defaults.misc_input_dialog),
|
||||
misc_keys: pick(self.misc_keys, defaults.misc_keys),
|
||||
misc_quit_dialog: pick(self.misc_quit_dialog, defaults.misc_quit_dialog),
|
||||
misc_save_dialog: pick(self.misc_save_dialog, defaults.misc_save_dialog),
|
||||
misc_warn_dialog: pick(self.misc_warn_dialog, defaults.misc_warn_dialog),
|
||||
transfer_local_explorer_background: pick(
|
||||
self.transfer_local_explorer_background,
|
||||
defaults.transfer_local_explorer_background,
|
||||
),
|
||||
transfer_local_explorer_foreground: pick(
|
||||
self.transfer_local_explorer_foreground,
|
||||
defaults.transfer_local_explorer_foreground,
|
||||
),
|
||||
transfer_local_explorer_highlighted: pick(
|
||||
self.transfer_local_explorer_highlighted,
|
||||
defaults.transfer_local_explorer_highlighted,
|
||||
),
|
||||
transfer_log_background: pick(
|
||||
self.transfer_log_background,
|
||||
defaults.transfer_log_background,
|
||||
),
|
||||
transfer_log_window: pick(self.transfer_log_window, defaults.transfer_log_window),
|
||||
transfer_progress_bar: pick(
|
||||
self.transfer_progress_bar
|
||||
.or(self.transfer_progress_bar_full)
|
||||
.or(self.transfer_progress_bar_partial),
|
||||
defaults.transfer_progress_bar,
|
||||
),
|
||||
transfer_remote_explorer_background: pick(
|
||||
self.transfer_remote_explorer_background,
|
||||
defaults.transfer_remote_explorer_background,
|
||||
),
|
||||
transfer_remote_explorer_foreground: pick(
|
||||
self.transfer_remote_explorer_foreground,
|
||||
defaults.transfer_remote_explorer_foreground,
|
||||
),
|
||||
transfer_remote_explorer_highlighted: pick(
|
||||
self.transfer_remote_explorer_highlighted,
|
||||
defaults.transfer_remote_explorer_highlighted,
|
||||
),
|
||||
transfer_status_hidden: pick(
|
||||
self.transfer_status_hidden,
|
||||
defaults.transfer_status_hidden,
|
||||
),
|
||||
transfer_status_sorting: pick(
|
||||
self.transfer_status_sorting,
|
||||
defaults.transfer_status_sorting,
|
||||
),
|
||||
transfer_status_sync_browsing: pick(
|
||||
self.transfer_status_sync_browsing,
|
||||
defaults.transfer_status_sync_browsing,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Theme {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let file = ThemeFile::deserialize(deserializer)?;
|
||||
Ok(file.into_theme())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,14 +235,29 @@ where
|
||||
serializer.serialize_str(s.as_str())
|
||||
}
|
||||
|
||||
// Kept for backwards compatibility with any external callers; no longer used directly
|
||||
// by serde derive because `Theme` now uses a custom `Deserialize` impl.
|
||||
#[allow(dead_code)]
|
||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = Deserialize::deserialize(deserializer)?;
|
||||
match parse_color(&s) {
|
||||
None => Err(DeError::custom("Invalid color")),
|
||||
Some(color) => Ok(color),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_themes_default() {
|
||||
fn should_get_default_theme() {
|
||||
let theme: Theme = Theme::default();
|
||||
assert_eq!(theme.auth_address, Color::Yellow);
|
||||
assert_eq!(theme.auth_bookmarks, Color::LightGreen);
|
||||
@@ -239,8 +278,7 @@ mod test {
|
||||
assert_eq!(theme.transfer_local_explorer_highlighted, Color::Yellow);
|
||||
assert_eq!(theme.transfer_log_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_log_window, Color::LightGreen);
|
||||
assert_eq!(theme.transfer_progress_bar_full, Color::Green);
|
||||
assert_eq!(theme.transfer_progress_bar_partial, Color::Green);
|
||||
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||
assert_eq!(theme.transfer_remote_explorer_background, Color::Reset);
|
||||
assert_eq!(theme.transfer_remote_explorer_foreground, Color::Reset);
|
||||
assert_eq!(theme.transfer_remote_explorer_highlighted, Color::LightBlue);
|
||||
@@ -248,4 +286,48 @@ mod test {
|
||||
assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
|
||||
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_accept_legacy_progress_bar_fields() {
|
||||
let toml = r#"
|
||||
auth_protocol = "Yellow"
|
||||
transfer_progress_bar_full = "Green"
|
||||
"#;
|
||||
let theme: Theme = toml::from_str(toml).expect("theme should load");
|
||||
assert_eq!(theme.auth_protocol, Color::Yellow);
|
||||
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_ignore_duplicated_legacy_progress_bar_fields() {
|
||||
let toml = r#"
|
||||
auth_protocol = "Yellow"
|
||||
transfer_progress_bar_full = "Green"
|
||||
transfer_progress_bar_partial = "Red"
|
||||
"#;
|
||||
let theme: Theme = toml::from_str(toml).expect("theme should load");
|
||||
assert_eq!(theme.auth_protocol, Color::Yellow);
|
||||
// `_full` wins because `transfer_progress_bar` and `_full` are checked first.
|
||||
assert_eq!(theme.transfer_progress_bar, Color::Green);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fall_back_to_defaults_on_invalid_values() {
|
||||
// Invalid color value should make *that* field fall back to default, without
|
||||
// breaking the rest of the theme.
|
||||
let toml = r##"
|
||||
auth_protocol = "not-a-color"
|
||||
auth_username = "#ca9ee6"
|
||||
"##;
|
||||
let theme: Theme = toml::from_str(toml).expect("theme should load");
|
||||
assert_eq!(theme.auth_protocol, Theme::default().auth_protocol);
|
||||
assert_eq!(theme.auth_username, Color::Rgb(202, 158, 230));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fall_back_to_defaults_on_missing_fields() {
|
||||
// Empty file should still produce a theme equal to default.
|
||||
let theme: Theme = toml::from_str("").expect("theme should load");
|
||||
assert_eq!(theme, Theme::default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -193,7 +245,8 @@ impl FileExplorer {
|
||||
|
||||
/// Sort explorer files by their name. All names are converted to lowercase
|
||||
fn sort_files_by_name(&mut self) {
|
||||
self.files.sort_by_key(|x: &File| x.name().to_lowercase());
|
||||
self.files
|
||||
.sort_by_cached_key(|x: &File| x.name().to_lowercase());
|
||||
}
|
||||
|
||||
/// Sort files by mtime; the newest comes first
|
||||
@@ -519,19 +572,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 +639,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 {
|
||||
@@ -24,7 +24,7 @@ impl FileExplorerBuilder {
|
||||
|
||||
/// Take FileExplorer out of builder
|
||||
pub fn build(&mut self) -> FileExplorer {
|
||||
self.explorer.take().unwrap()
|
||||
self.explorer.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Enable HIDDEN_FILES option
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+119
-74
@@ -11,8 +11,8 @@ use bytesize::ByteSize;
|
||||
use lazy_regex::{Lazy, Regex};
|
||||
use remotefs::File;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(unix)]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
#[cfg(posix)]
|
||||
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||
|
||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||
use crate::utils::path::diff_paths;
|
||||
@@ -108,7 +108,7 @@ impl CallChainBlock {
|
||||
None => {
|
||||
self.next_block = Some(Box::new(CallChainBlock::new(
|
||||
func, prefix, fmt_len, fmt_extra,
|
||||
)))
|
||||
)));
|
||||
}
|
||||
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
|
||||
}
|
||||
@@ -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,19 +364,31 @@ 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
|
||||
.metadata()
|
||||
.symlink
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.len() as u64,
|
||||
);
|
||||
format!("{cur_str}{prefix}{size:10}")
|
||||
match fsentry.metadata().symlink.as_ref() {
|
||||
Some(symlink) => {
|
||||
let size = ByteSize(symlink.to_string_lossy().len() as u64);
|
||||
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}")
|
||||
}
|
||||
None => format!("{cur_str}{prefix} "),
|
||||
}
|
||||
} else {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{cur_str}{prefix} ")
|
||||
@@ -420,7 +432,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 +440,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(),
|
||||
@@ -461,12 +473,14 @@ impl Formatter {
|
||||
let mut last_index: usize = 0;
|
||||
// Match fmt str against regex
|
||||
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
||||
// Get match index (unwrap is safe, since always exists)
|
||||
let index: usize = fmt_str.find(®ex_match[0]).unwrap();
|
||||
let Some(full_match) = regex_match.get(0) else {
|
||||
continue;
|
||||
};
|
||||
let index: usize = full_match.start();
|
||||
// Get prefix
|
||||
let prefix: String = String::from(&fmt_str[last_index..index]);
|
||||
// Increment last index (sum prefix lenght and the length of the key)
|
||||
last_index += prefix.len() + regex_match[0].len();
|
||||
last_index += prefix.len() + full_match.as_str().len();
|
||||
// Match attributes
|
||||
match FMT_ATTR_REGEX.captures(®ex_match[1]) {
|
||||
Some(regex_match) => {
|
||||
@@ -489,10 +503,7 @@ impl Formatter {
|
||||
};
|
||||
// Match format length: group 3
|
||||
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
||||
Some(len) => match len.as_str().parse::<usize>() {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => None,
|
||||
},
|
||||
Some(len) => len.as_str().parse::<usize>().ok(),
|
||||
None => None,
|
||||
};
|
||||
// Match format extra: group 2 + 1
|
||||
@@ -504,7 +515,7 @@ impl Formatter {
|
||||
match callchain.as_mut() {
|
||||
None => {
|
||||
callchain =
|
||||
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
|
||||
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra));
|
||||
}
|
||||
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
|
||||
}
|
||||
@@ -592,19 +603,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 +634,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 +665,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 +696,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 +734,7 @@ mod tests {
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -731,7 +742,7 @@ mod tests {
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -754,7 +765,7 @@ mod tests {
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -762,7 +773,7 @@ mod tests {
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -774,8 +785,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 +804,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 +828,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 +852,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 +876,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 +920,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn should_fmt_utf8_path() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
@@ -1002,6 +1026,27 @@ mod tests {
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_ignore_unknown_formatter_keys() {
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/foo.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: None,
|
||||
created: None,
|
||||
modified: None,
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let formatter: Formatter = Formatter::new("before {UNKNOWN:12} after {NAME:8}");
|
||||
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "before after foo.txt ");
|
||||
}
|
||||
|
||||
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
||||
fn dummy_fmt(
|
||||
_fmt: &Formatter,
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
//!
|
||||
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
|
||||
|
||||
mod builder;
|
||||
mod host_bridge_builder;
|
||||
pub mod params;
|
||||
mod remotefs_builder;
|
||||
|
||||
// -- export types
|
||||
pub use builder::Builder;
|
||||
pub use params::{FileTransferParams, ProtocolParams};
|
||||
pub use host_bridge_builder::HostBridgeBuilder;
|
||||
pub use params::{FileTransferParams, HostBridgeParams, ProtocolParams};
|
||||
pub use remotefs_builder::RemoteFsBuilder;
|
||||
|
||||
/// This enum defines the different transfer protocol available in termscp
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//! ## Host Bridge Builder
|
||||
//!
|
||||
//! Builds host bridge implementations from persisted host bridge parameters and
|
||||
//! the active configuration client.
|
||||
|
||||
use super::{HostBridgeParams, RemoteFsBuilder};
|
||||
use crate::host::{HostBridge, Localhost, RemoteBridged};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
/// Builds the host-side filesystem bridge used during file transfer sessions.
|
||||
pub struct HostBridgeBuilder;
|
||||
|
||||
impl HostBridgeBuilder {
|
||||
/// Builds a host bridge from serialized parameters.
|
||||
///
|
||||
/// Returns an error when the selected host protocol and parameters are inconsistent.
|
||||
pub fn build(
|
||||
params: HostBridgeParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<Box<dyn HostBridge>, String> {
|
||||
match params {
|
||||
HostBridgeParams::Localhost(path) => Localhost::new(path)
|
||||
.map(|host| Box::new(host) as Box<dyn HostBridge>)
|
||||
.map_err(|e| e.to_string()),
|
||||
HostBridgeParams::Remote(protocol, params) => {
|
||||
RemoteFsBuilder::build(protocol, params, config_client)
|
||||
.map(|host| Box::new(RemoteBridged::from(host)) as Box<dyn HostBridge>)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
-20
@@ -15,6 +15,34 @@ pub use self::smb::SmbParams;
|
||||
pub use self::webdav::WebDAVProtocolParams;
|
||||
use super::FileTransferProtocol;
|
||||
|
||||
/// Host bridge params
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HostBridgeParams {
|
||||
/// Localhost with starting working directory
|
||||
Localhost(PathBuf),
|
||||
/// Remote host with protocol and file transfer params
|
||||
Remote(FileTransferProtocol, ProtocolParams),
|
||||
}
|
||||
|
||||
impl HostBridgeParams {
|
||||
pub fn protocol_params(&self) -> Option<&ProtocolParams> {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => None,
|
||||
HostBridgeParams::Remote(_, params) => Some(params),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the host name for the bridge params
|
||||
pub fn username(&self) -> Option<String> {
|
||||
match self {
|
||||
HostBridgeParams::Localhost(_) => whoami::username().ok(),
|
||||
HostBridgeParams::Remote(_, params) => {
|
||||
params.generic_params().and_then(|p| p.username.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds connection parameters for file transfers
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTransferParams {
|
||||
@@ -24,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 {
|
||||
@@ -34,6 +71,43 @@ pub enum ProtocolParams {
|
||||
WebDAV(WebDAVProtocolParams),
|
||||
}
|
||||
|
||||
impl ProtocolParams {
|
||||
pub fn password_missing(&self) -> bool {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.password_missing(),
|
||||
ProtocolParams::Generic(params) => params.password_missing(),
|
||||
ProtocolParams::Kube(params) => params.password_missing(),
|
||||
ProtocolParams::Smb(params) => params.password_missing(),
|
||||
ProtocolParams::WebDAV(params) => params.password_missing(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the secret to ft params for the default secret field for this protocol
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Generic(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Kube(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Smb(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_name(&self) -> String {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||
ProtocolParams::Generic(params) => params.address.clone(),
|
||||
ProtocolParams::Kube(params) => params
|
||||
.namespace
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("default")),
|
||||
ProtocolParams::Smb(params) => params.address.clone(),
|
||||
ProtocolParams::WebDAV(params) => params.uri.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol params used by most common protocols
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericProtocolParams {
|
||||
@@ -68,25 +142,15 @@ impl FileTransferParams {
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
#[cfg(test)]
|
||||
pub fn password_missing(&self) -> bool {
|
||||
match &self.params {
|
||||
ProtocolParams::AwsS3(params) => params.password_missing(),
|
||||
ProtocolParams::Generic(params) => params.password_missing(),
|
||||
ProtocolParams::Kube(params) => params.password_missing(),
|
||||
ProtocolParams::Smb(params) => params.password_missing(),
|
||||
ProtocolParams::WebDAV(params) => params.password_missing(),
|
||||
}
|
||||
self.params.password_missing()
|
||||
}
|
||||
|
||||
/// Set the secret to ft params for the default secret field for this protocol
|
||||
#[cfg(test)]
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
match &mut self.params {
|
||||
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Generic(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Kube(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Smb(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
|
||||
}
|
||||
self.params.set_default_secret(secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +278,19 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_protocol_params_on_localhost_returns_none() {
|
||||
let params = HostBridgeParams::Localhost(PathBuf::from("/tmp"));
|
||||
assert!(params.protocol_params().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protocol_params_on_remote_returns_some() {
|
||||
let params =
|
||||
HostBridgeParams::Remote(FileTransferProtocol::Sftp, ProtocolParams::default());
|
||||
assert!(params.protocol_params().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_params() {
|
||||
let params: FileTransferParams =
|
||||
@@ -256,11 +333,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,
|
||||
@@ -317,7 +396,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn set_default_secret_smb() {
|
||||
let mut params = FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
//! ## AWS S3 Parameters
|
||||
//!
|
||||
//! Defines the runtime connection parameters used to build AWS S3 and
|
||||
//! S3-compatible remote filesystem clients.
|
||||
|
||||
/// Connection parameters for AWS S3 protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AwsS3Params {
|
||||
/// Target bucket name.
|
||||
pub bucket_name: String,
|
||||
/// Target region.
|
||||
pub region: Option<String>,
|
||||
/// Optional custom endpoint URL.
|
||||
pub endpoint: Option<String>,
|
||||
/// Optional shared credentials profile.
|
||||
pub profile: Option<String>,
|
||||
/// Optional static access key.
|
||||
pub access_key: Option<String>,
|
||||
/// Optional static secret access key.
|
||||
pub secret_access_key: Option<String>,
|
||||
/// Optional security token for temporary credentials.
|
||||
pub security_token: Option<String>,
|
||||
/// Optional session token for temporary credentials.
|
||||
pub session_token: Option<String>,
|
||||
/// Whether to force path-style bucket addressing.
|
||||
pub new_path_style: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
//! ## Kubernetes Parameters
|
||||
//!
|
||||
//! Defines the runtime parameters used to construct Kubernetes-backed file
|
||||
//! transfer clients.
|
||||
|
||||
use remotefs_kube::Config;
|
||||
|
||||
/// Protocol params used by WebDAV
|
||||
/// Protocol params used by Kubernetes connections.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KubeProtocolParams {
|
||||
/// Optional namespace for the default pod context.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional Kubernetes API URL.
|
||||
pub cluster_url: Option<String>,
|
||||
/// Optional username override.
|
||||
pub username: Option<String>,
|
||||
/// Optional client certificate path.
|
||||
pub client_cert: Option<String>,
|
||||
/// Optional client key path.
|
||||
pub client_key: Option<String>,
|
||||
}
|
||||
|
||||
impl KubeProtocolParams {
|
||||
/// Kubernetes connections do not use the shared password secret flow.
|
||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||
|
||||
/// Kubernetes params never require the generic password prompt.
|
||||
pub fn password_missing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Converts bookmark/runtime parameters into a `remotefs_kube` config.
|
||||
pub fn config(self) -> Option<Config> {
|
||||
if let Some(cluster_url) = self.cluster_url {
|
||||
let mut config = Config::new(cluster_url.parse().unwrap_or_default());
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
//! ## SMB Parameters
|
||||
//!
|
||||
//! Defines the runtime connection parameters used to build SMB remote
|
||||
//! filesystem clients.
|
||||
|
||||
/// Connection parameters for SMB protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmbParams {
|
||||
/// Hostname or address of the SMB server.
|
||||
pub address: String,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
/// SMB service port used on POSIX platforms.
|
||||
pub port: u16,
|
||||
/// Share name to mount.
|
||||
pub share: String,
|
||||
/// Optional username.
|
||||
pub username: Option<String>,
|
||||
/// Optional password.
|
||||
pub password: Option<String>,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
/// Optional workgroup used on POSIX platforms.
|
||||
pub workgroup: Option<String>,
|
||||
}
|
||||
|
||||
@@ -18,17 +29,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 +55,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 +68,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 +89,20 @@ mod test {
|
||||
let params = SmbParams::new("localhost", "temp");
|
||||
assert_eq!(¶ms.address, "localhost");
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
assert_eq!(params.port, 445);
|
||||
assert_eq!(¶ms.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 +119,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
fn should_init_smb_params_with_optionals() {
|
||||
let params = SmbParams::new("localhost", "temp")
|
||||
.username(Some("foo"))
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
//! ## WebDAV Parameters
|
||||
//!
|
||||
//! Defines the runtime connection parameters used to build WebDAV clients.
|
||||
|
||||
/// Protocol params used by WebDAV
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebDAVProtocolParams {
|
||||
/// Base WebDAV endpoint URI.
|
||||
pub uri: String,
|
||||
/// Username used for authentication.
|
||||
pub username: String,
|
||||
/// Password used for authentication.
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl WebDAVProtocolParams {
|
||||
/// Stores the shared secret as the active WebDAV password.
|
||||
pub fn set_default_secret(&mut self, secret: String) {
|
||||
self.password = secret;
|
||||
}
|
||||
|
||||
/// Returns whether the WebDAV password is currently missing.
|
||||
pub fn password_missing(&self) -> bool {
|
||||
self.password.is_empty()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||
use remotefs_smb::SmbOptions;
|
||||
#[cfg(smb)]
|
||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
||||
use remotefs_ssh::{ScpFs, SftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts};
|
||||
use remotefs_ssh::{
|
||||
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
|
||||
SshConfigParseRule, SshOpts,
|
||||
};
|
||||
use remotefs_webdav::WebDAVFs;
|
||||
|
||||
#[cfg(not(smb))]
|
||||
@@ -27,9 +30,9 @@ use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::ssh as ssh_utils;
|
||||
|
||||
/// Remotefs builder
|
||||
pub struct Builder;
|
||||
pub struct RemoteFsBuilder;
|
||||
|
||||
impl Builder {
|
||||
impl RemoteFsBuilder {
|
||||
/// Build RemoteFs client from protocol and params.
|
||||
///
|
||||
/// if protocol and parameters are inconsistent, the function will panic.
|
||||
@@ -37,40 +40,50 @@ impl Builder {
|
||||
protocol: FileTransferProtocol,
|
||||
params: ProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Box<dyn RemoteFs> {
|
||||
) -> Result<Box<dyn RemoteFs>, String> {
|
||||
match (protocol, params) {
|
||||
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
||||
Box::new(Self::aws_s3_client(params))
|
||||
Ok(Box::new(Self::aws_s3_client(params)?))
|
||||
}
|
||||
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::ftp_client(params, secure))
|
||||
Ok(Box::new(Self::ftp_client(params, secure)))
|
||||
}
|
||||
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||
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);
|
||||
fn aws_s3_client(params: AwsS3Params) -> Result<AwsS3Fs, String> {
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||
);
|
||||
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);
|
||||
}
|
||||
@@ -92,7 +105,7 @@ impl Builder {
|
||||
if let Some(session_token) = params.session_token {
|
||||
client = client.session_token(session_token);
|
||||
}
|
||||
client
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Build ftp client from parameters
|
||||
@@ -111,34 +124,38 @@ impl Builder {
|
||||
}
|
||||
|
||||
/// Build kube client
|
||||
fn kube_client(params: KubeProtocolParams) -> KubeFs {
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime"),
|
||||
);
|
||||
fn kube_client(params: KubeProtocolParams) -> Result<KubeFs, String> {
|
||||
let rt = Self::tokio_runtime()?;
|
||||
let kube_fs = KubeFs::new(&rt);
|
||||
if let Some(config) = params.config() {
|
||||
kube_fs.config(config)
|
||||
Ok(kube_fs.config(config))
|
||||
} else {
|
||||
kube_fs
|
||||
Ok(kube_fs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build scp client
|
||||
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
fn scp_client(
|
||||
params: GenericProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<ScpFs<SshSession<NoCheckServerKey>>, String> {
|
||||
let opts = Self::build_ssh_opts(params, config_client);
|
||||
let rt = Self::tokio_runtime()?;
|
||||
Ok(ScpFs::russh(opts, rt))
|
||||
}
|
||||
|
||||
/// Build sftp client
|
||||
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
fn sftp_client(
|
||||
params: GenericProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<SftpFs<SshSession<NoCheckServerKey>>, String> {
|
||||
let opts = Self::build_ssh_opts(params, config_client);
|
||||
let rt = Self::tokio_runtime()?;
|
||||
Ok(SftpFs::russh(opts, rt))
|
||||
}
|
||||
|
||||
#[cfg(smb_unix)]
|
||||
fn smb_client(params: SmbParams) -> SmbFs {
|
||||
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||
let mut credentials = SmbCredentials::default()
|
||||
.server(format!("smb://{}:{}", params.address, params.port))
|
||||
.share(params.share);
|
||||
@@ -153,22 +170,20 @@ impl Builder {
|
||||
credentials = credentials.workgroup(workgroup);
|
||||
}
|
||||
|
||||
match SmbFs::try_new(
|
||||
SmbFs::try_new(
|
||||
credentials,
|
||||
SmbOptions::default()
|
||||
.one_share_per_server(true)
|
||||
.case_sensitive(false),
|
||||
) {
|
||||
Ok(fs) => fs,
|
||||
Err(e) => {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
panic!("Invalid params for protocol SMB: {e}")
|
||||
}
|
||||
}
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
format!("Invalid params for protocol SMB: {e}")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(smb_windows)]
|
||||
fn smb_client(params: SmbParams) -> SmbFs {
|
||||
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||
let mut credentials = SmbCredentials::new(params.address, params.share);
|
||||
|
||||
if let Some(username) = params.username {
|
||||
@@ -178,7 +193,7 @@ impl Builder {
|
||||
credentials = credentials.password(password);
|
||||
}
|
||||
|
||||
SmbFs::new(credentials)
|
||||
Ok(SmbFs::new(credentials))
|
||||
}
|
||||
|
||||
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
|
||||
@@ -216,14 +231,19 @@ impl Builder {
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
if let Ok(username) = whoami::username() {
|
||||
opts = opts.username(username);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
} else if let Ok(username) = whoami::username() {
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(whoami::username());
|
||||
opts = opts.username(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() {
|
||||
@@ -239,6 +259,17 @@ impl Builder {
|
||||
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
|
||||
SshKeyStorage::from(config_client)
|
||||
}
|
||||
|
||||
/// Create tokio runtime to run async code for remotefs
|
||||
fn tokio_runtime() -> Result<Arc<tokio::runtime::Runtime>, String> {
|
||||
Ok(Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -262,7 +293,9 @@ mod test {
|
||||
.session_token(Some("gerry-scotti")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +308,9 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client).is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -288,7 +323,7 @@ mod test {
|
||||
client_key: Some("client_key".to_string()),
|
||||
});
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Kube, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -301,7 +336,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -314,7 +349,7 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -322,11 +357,10 @@ mod test {
|
||||
fn should_build_smb_fs() {
|
||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Smb, params, &config_client);
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn should_not_build_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
@@ -336,7 +370,9 @@ mod test {
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
assert!(
|
||||
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn get_config_client() -> ConfigClient {
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
//! ## Host
|
||||
//!
|
||||
//! `host` is the module which provides functionalities to host file system
|
||||
|
||||
mod bridge;
|
||||
mod localhost;
|
||||
mod remote_bridged;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
// Locals
|
||||
pub use self::bridge::HostBridge;
|
||||
pub use self::localhost::Localhost;
|
||||
pub use self::remote_bridged::RemoteBridged;
|
||||
|
||||
pub type HostResult<T> = Result<T, HostError>;
|
||||
|
||||
/// HostErrorType provides an overview of the specific host error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HostErrorType {
|
||||
#[error("No such file or directory")]
|
||||
NoSuchFileOrDirectory,
|
||||
#[error("File is readonly")]
|
||||
ReadonlyFile,
|
||||
#[error("Could not access directory")]
|
||||
DirNotAccessible,
|
||||
#[error("Could not access file")]
|
||||
FileNotAccessible,
|
||||
#[error("File already exists")]
|
||||
FileAlreadyExists,
|
||||
#[error("Could not create file")]
|
||||
CouldNotCreateFile,
|
||||
#[error("Command execution failed")]
|
||||
ExecutionFailed,
|
||||
#[error("Could not delete file")]
|
||||
DeleteFailed,
|
||||
#[cfg(win)]
|
||||
#[error("Not implemented")]
|
||||
NotImplemented,
|
||||
#[error("remote fs error: {0}")]
|
||||
RemoteFs(#[from] remotefs::RemoteError),
|
||||
}
|
||||
|
||||
/// HostError is a wrapper for the error type and the exact io error
|
||||
#[derive(Debug, Error)]
|
||||
pub struct HostError {
|
||||
pub error: HostErrorType,
|
||||
ioerr: Option<std::io::Error>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<remotefs::RemoteError> for HostError {
|
||||
fn from(value: remotefs::RemoteError) -> Self {
|
||||
HostError::from(HostErrorType::RemoteFs(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl HostError {
|
||||
/// Instantiates a new HostError
|
||||
pub(crate) fn new(error: HostErrorType, errno: Option<std::io::Error>, p: &Path) -> Self {
|
||||
HostError {
|
||||
error,
|
||||
ioerr: errno,
|
||||
path: Some(p.to_path_buf()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostErrorType> for HostError {
|
||||
fn from(error: HostErrorType) -> Self {
|
||||
HostError {
|
||||
error,
|
||||
ioerr: None,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HostError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let p_str: String = match self.path.as_ref() {
|
||||
None => String::new(),
|
||||
Some(p) => format!(" ({})", p.display()),
|
||||
};
|
||||
match &self.ioerr {
|
||||
Some(err) => write!(f, "{}: {}{}", self.error, err, p_str),
|
||||
None => write!(f, "{}{}", self.error, p_str),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_host_fmt_error() {
|
||||
let err: HostError = HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
|
||||
Path::new("/tmp"),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{err}"),
|
||||
String::from("Could not create file: address in use (/tmp)"),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::DeleteFailed)),
|
||||
String::from("Could not delete file")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::ExecutionFailed)),
|
||||
String::from("Command execution failed"),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::DirNotAccessible)),
|
||||
String::from("Could not access directory"),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::NoSuchFileOrDirectory)),
|
||||
String::from("No such file or directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::ReadonlyFile)),
|
||||
String::from("File is readonly")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::FileNotAccessible)),
|
||||
String::from("Could not access file")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::from(HostErrorType::FileAlreadyExists)),
|
||||
String::from("File already exists")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! ## Host Bridge
|
||||
//!
|
||||
//! Defines the host abstraction used to expose localhost and bridged remote
|
||||
//! filesystems through a shared interface.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::File;
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
|
||||
use super::HostResult;
|
||||
|
||||
/// Trait to bridge a remote filesystem to the host filesystem
|
||||
///
|
||||
/// In case of `Localhost` this should be effortless, while for remote hosts this should
|
||||
/// implement a real bridge when the resource is first loaded on the local
|
||||
/// filesystem and then processed on the remote.
|
||||
pub trait HostBridge {
|
||||
/// Connect to host
|
||||
fn connect(&mut self) -> HostResult<()>;
|
||||
|
||||
/// Disconnect from host
|
||||
fn disconnect(&mut self) -> HostResult<()>;
|
||||
|
||||
/// Returns whether the host is connected
|
||||
fn is_connected(&mut self) -> bool;
|
||||
|
||||
/// Returns whether the host is localhost
|
||||
fn is_localhost(&self) -> bool;
|
||||
|
||||
/// Print working directory
|
||||
fn pwd(&mut self) -> HostResult<PathBuf>;
|
||||
|
||||
/// Change working directory with the new provided directory
|
||||
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf>;
|
||||
|
||||
/// Make a directory at path and update the file list (only if relative)
|
||||
fn mkdir(&mut self, dir_name: &Path) -> HostResult<()> {
|
||||
self.mkdir_ex(dir_name, false)
|
||||
}
|
||||
|
||||
/// Extended option version of makedir.
|
||||
/// ignex: don't report error if directory already exists
|
||||
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()>;
|
||||
|
||||
/// Remove file entry
|
||||
fn remove(&mut self, entry: &File) -> HostResult<()>;
|
||||
|
||||
/// Rename file or directory to new name
|
||||
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()>;
|
||||
|
||||
/// Copy file to destination path
|
||||
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()>;
|
||||
|
||||
/// Stat file and create a File
|
||||
fn stat(&mut self, path: &Path) -> HostResult<File>;
|
||||
|
||||
/// Returns whether provided file path exists
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool>;
|
||||
|
||||
/// Get content of a directory
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>>;
|
||||
|
||||
/// Set file stat
|
||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()>;
|
||||
|
||||
/// Execute a command on localhost
|
||||
fn exec(&mut self, cmd: &str) -> HostResult<String>;
|
||||
|
||||
/// Create a symlink from src to dst
|
||||
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()>;
|
||||
|
||||
/// Change file mode to file, according to UNIX permissions
|
||||
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>;
|
||||
|
||||
/// Open file for reading
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>>;
|
||||
|
||||
/// Open file for writing
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>>;
|
||||
|
||||
/// Finalize write operation
|
||||
fn finalize_write(&mut self, writer: Box<dyn Write + Send>) -> HostResult<()>;
|
||||
}
|
||||
@@ -1,88 +1,22 @@
|
||||
//! ## Host
|
||||
//! ## Localhost Host Bridge
|
||||
//!
|
||||
//! `host` is the module which provides functionalities to host file system
|
||||
//! Implements the host bridge abstraction directly against the local
|
||||
//! filesystem.
|
||||
|
||||
// ext
|
||||
// Metadata ext
|
||||
#[cfg(unix)]
|
||||
use std::fs::set_permissions;
|
||||
use std::fs::{self, File as StdFile, OpenOptions};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
#[cfg(posix)]
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use filetime::{self, FileTime};
|
||||
#[cfg(unix)]
|
||||
use remotefs::fs::UnixPex;
|
||||
use remotefs::fs::{File, FileType, Metadata};
|
||||
use thiserror::Error;
|
||||
use filetime::FileTime;
|
||||
use remotefs::File;
|
||||
use remotefs::fs::{FileType, Metadata, UnixPex};
|
||||
|
||||
// Locals
|
||||
use super::{HostBridge, HostResult};
|
||||
use crate::host::{HostError, HostErrorType};
|
||||
use crate::utils::path;
|
||||
|
||||
/// HostErrorType provides an overview of the specific host error
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HostErrorType {
|
||||
#[error("No such file or directory")]
|
||||
NoSuchFileOrDirectory,
|
||||
#[error("File is readonly")]
|
||||
ReadonlyFile,
|
||||
#[error("Could not access directory")]
|
||||
DirNotAccessible,
|
||||
#[error("Could not access file")]
|
||||
FileNotAccessible,
|
||||
#[error("File already exists")]
|
||||
FileAlreadyExists,
|
||||
#[error("Could not create file")]
|
||||
CouldNotCreateFile,
|
||||
#[error("Command execution failed")]
|
||||
ExecutionFailed,
|
||||
#[error("Could not delete file")]
|
||||
DeleteFailed,
|
||||
}
|
||||
|
||||
/// HostError is a wrapper for the error type and the exact io error
|
||||
#[derive(Debug)]
|
||||
pub struct HostError {
|
||||
pub error: HostErrorType,
|
||||
ioerr: Option<std::io::Error>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl HostError {
|
||||
/// Instantiates a new HostError
|
||||
pub(crate) fn new(error: HostErrorType, errno: Option<std::io::Error>, p: &Path) -> Self {
|
||||
HostError {
|
||||
error,
|
||||
ioerr: errno,
|
||||
path: Some(p.to_path_buf()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostErrorType> for HostError {
|
||||
fn from(error: HostErrorType) -> Self {
|
||||
HostError {
|
||||
error,
|
||||
ioerr: None,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HostError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let p_str: String = match self.path.as_ref() {
|
||||
None => String::new(),
|
||||
Some(p) => format!(" ({})", p.display()),
|
||||
};
|
||||
match &self.ioerr {
|
||||
Some(err) => write!(f, "{}: {}{}", self.error, err, p_str),
|
||||
None => write!(f, "{}{}", self.error, p_str),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Localhost is the entity which holds the information about the current directory and host.
|
||||
/// It provides functions to navigate across the local host file system
|
||||
pub struct Localhost {
|
||||
@@ -92,14 +26,16 @@ pub struct Localhost {
|
||||
|
||||
impl Localhost {
|
||||
/// Instantiates a new Localhost struct
|
||||
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
|
||||
pub fn new(wrkdir: PathBuf) -> HostResult<Self> {
|
||||
debug!("Initializing localhost at {}", wrkdir.display());
|
||||
let mut host: Localhost = Localhost {
|
||||
wrkdir,
|
||||
files: Vec::new(),
|
||||
};
|
||||
// Check if dir exists
|
||||
if !host.file_exists(host.wrkdir.as_path()) {
|
||||
let pwd = host.pwd()?;
|
||||
|
||||
if !host.exists(&pwd)? {
|
||||
error!(
|
||||
"Failed to initialize localhost: {} doesn't exist",
|
||||
host.wrkdir.display()
|
||||
@@ -111,7 +47,7 @@ impl Localhost {
|
||||
));
|
||||
}
|
||||
// Retrieve files for provided path
|
||||
host.files = match host.list_dir(host.wrkdir.as_path()) {
|
||||
host.files = match host.list_dir(&pwd) {
|
||||
Ok(files) => files,
|
||||
Err(err) => {
|
||||
error!(
|
||||
@@ -125,17 +61,38 @@ impl Localhost {
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
/// Print working directory
|
||||
pub fn pwd(&self) -> PathBuf {
|
||||
self.wrkdir.clone()
|
||||
/// Convert path to absolute path
|
||||
fn to_path(&self, p: &Path) -> PathBuf {
|
||||
path::absolutize(self.wrkdir.as_path(), p)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostBridge for Localhost {
|
||||
fn connect(&mut self) -> HostResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change working directory with the new provided directory
|
||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||
fn disconnect(&mut self) -> HostResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_connected(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_localhost(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn pwd(&mut self) -> HostResult<PathBuf> {
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
|
||||
fn change_wrkdir(&mut self, new_dir: &std::path::Path) -> HostResult<PathBuf> {
|
||||
let new_dir: PathBuf = self.to_path(new_dir);
|
||||
info!("Changing localhost directory to {}...", new_dir.display());
|
||||
// Check whether directory exists
|
||||
if !self.file_exists(new_dir.as_path()) {
|
||||
if !self.exists(new_dir.as_path())? {
|
||||
error!("Could not change directory: No such file or directory");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
@@ -153,11 +110,12 @@ impl 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
|
||||
self.files = match self.list_dir(self.wrkdir.as_path()) {
|
||||
let pwd = self.pwd()?;
|
||||
self.files = match self.list_dir(&pwd) {
|
||||
Ok(files) => files,
|
||||
Err(err) => {
|
||||
error!("Could not scan new directory: {}", err);
|
||||
@@ -170,26 +128,19 @@ impl Localhost {
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
|
||||
/// Make a directory at path and update the file list (only if relative)
|
||||
pub fn mkdir(&mut self, dir_name: &Path) -> Result<(), HostError> {
|
||||
self.mkdir_ex(dir_name, false)
|
||||
}
|
||||
|
||||
/// Extended option version of makedir.
|
||||
/// ignex: don't report error if directory already exists
|
||||
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
||||
fn mkdir_ex(&mut self, dir_name: &std::path::Path, ignore_existing: bool) -> HostResult<()> {
|
||||
let dir_path: PathBuf = self.to_path(dir_name);
|
||||
info!("Making directory {}", dir_path.display());
|
||||
// If dir already exists, return Error
|
||||
if dir_path.exists() {
|
||||
match ignex {
|
||||
match ignore_existing {
|
||||
true => return Ok(()),
|
||||
false => {
|
||||
return Err(HostError::new(
|
||||
HostErrorType::FileAlreadyExists,
|
||||
None,
|
||||
dir_path.as_path(),
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +148,8 @@ impl Localhost {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
if dir_name.is_relative() {
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
let pwd = self.pwd()?;
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
}
|
||||
info!("Created directory {}", dir_path.display());
|
||||
Ok(())
|
||||
@@ -213,8 +165,7 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove file entry
|
||||
pub fn remove(&mut self, entry: &File) -> Result<(), HostError> {
|
||||
fn remove(&mut self, entry: &File) -> HostResult<()> {
|
||||
if entry.is_dir() {
|
||||
// If file doesn't exist; return error
|
||||
debug!("Removing directory {}", entry.path().display());
|
||||
@@ -230,7 +181,8 @@ impl Localhost {
|
||||
match std::fs::remove_dir_all(entry.path()) {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
let pwd = self.pwd()?;
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
info!("Removed directory {}", entry.path().display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -258,7 +210,8 @@ impl Localhost {
|
||||
match std::fs::remove_file(entry.path()) {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
let pwd = self.pwd()?;
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
info!("Removed file {}", entry.path().display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -274,12 +227,12 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename file or directory to new name
|
||||
pub fn rename(&mut self, entry: &File, dst_path: &Path) -> Result<(), HostError> {
|
||||
fn rename(&mut self, entry: &File, dst_path: &std::path::Path) -> HostResult<()> {
|
||||
match std::fs::rename(entry.path(), dst_path) {
|
||||
Ok(_) => {
|
||||
// Scan dir
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
let pwd = self.pwd()?;
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
debug!(
|
||||
"Moved file {} to {}",
|
||||
entry.path().display(),
|
||||
@@ -303,8 +256,7 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy file to destination path
|
||||
pub fn copy(&mut self, entry: &File, dst: &Path) -> Result<(), HostError> {
|
||||
fn copy(&mut self, entry: &File, dst: &std::path::Path) -> HostResult<()> {
|
||||
// Get absolute path of dest
|
||||
let dst: PathBuf = self.to_path(dst);
|
||||
info!(
|
||||
@@ -352,22 +304,23 @@ impl Localhost {
|
||||
info!("File copied");
|
||||
}
|
||||
// Reload directory if dst is pwd
|
||||
let pwd = self.pwd()?;
|
||||
match dst.is_dir() {
|
||||
true => {
|
||||
if dst == self.pwd().as_path() {
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
if dst == pwd {
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
} else if let Some(parent) = dst.parent() {
|
||||
// If parent is pwd, scan directory
|
||||
if parent == self.pwd().as_path() {
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
if parent == pwd {
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
false => {
|
||||
if let Some(parent) = dst.parent() {
|
||||
// If parent is pwd, scan directory
|
||||
if parent == self.pwd().as_path() {
|
||||
self.files = self.list_dir(self.wrkdir.as_path())?;
|
||||
if parent == pwd {
|
||||
self.files = self.list_dir(&pwd)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,8 +328,7 @@ impl Localhost {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stat file and create a File
|
||||
pub fn stat(&self, path: &Path) -> Result<File, HostError> {
|
||||
fn stat(&mut self, path: &std::path::Path) -> HostResult<File> {
|
||||
info!("Stating file {}", path.display());
|
||||
let path: PathBuf = self.to_path(path);
|
||||
let attr = match fs::metadata(path.as_path()) {
|
||||
@@ -399,8 +351,35 @@ impl Localhost {
|
||||
Ok(File { path, metadata })
|
||||
}
|
||||
|
||||
/// Set file stat
|
||||
pub fn setstat(&self, path: &Path, metadata: &Metadata) -> Result<(), HostError> {
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||
let path: PathBuf = self.to_path(path);
|
||||
|
||||
Ok(path.exists())
|
||||
}
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
||||
info!("Reading directory {}", path.display());
|
||||
match std::fs::read_dir(path) {
|
||||
Ok(e) => {
|
||||
let mut fs_entries: Vec<File> = Vec::new();
|
||||
for entry in e.flatten() {
|
||||
// NOTE: 0.4.1, don't fail if stat for one file fails
|
||||
match self.stat(entry.path().as_path()) {
|
||||
Ok(entry) => fs_entries.push(entry),
|
||||
Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
|
||||
}
|
||||
}
|
||||
Ok(fs_entries)
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::DirNotAccessible,
|
||||
Some(err),
|
||||
path,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
|
||||
debug!("Setting stat for file at {}", path.display());
|
||||
if let Some(mtime) = metadata.modified {
|
||||
let mtime = FileTime::from_system_time(mtime);
|
||||
@@ -413,18 +392,27 @@ impl 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)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a command on localhost
|
||||
pub fn exec(&self, cmd: &str) -> Result<String, HostError> {
|
||||
fn exec(&mut self, cmd: &str) -> HostResult<String> {
|
||||
// Make command
|
||||
let args: Vec<&str> = cmd.split(' ').collect();
|
||||
let cmd: &str = args.first().unwrap();
|
||||
let cmd: &str = match args.first() {
|
||||
Some(cmd) => cmd,
|
||||
None => {
|
||||
error!("Empty command provided to exec");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::ExecutionFailed,
|
||||
None,
|
||||
self.wrkdir.as_path(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let argv: &[&str] = &args[1..];
|
||||
info!("Executing command: {} {:?}", cmd, argv);
|
||||
match std::process::Command::new(cmd).args(argv).output() {
|
||||
@@ -446,16 +434,36 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Change file mode to file, according to UNIX permissions
|
||||
#[cfg(unix)]
|
||||
pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> {
|
||||
#[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| {
|
||||
error!(
|
||||
"Failed to create symlink at {} pointing at {}: {}",
|
||||
src.display(),
|
||||
dst.display(),
|
||||
e
|
||||
);
|
||||
HostError::new(HostErrorType::CouldNotCreateFile, Some(e), src.as_path())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(win)]
|
||||
fn symlink(&mut self, _src: &Path, _dst: &Path) -> HostResult<()> {
|
||||
warn!("Cannot create symlink on Windows");
|
||||
|
||||
Err(HostError::from(HostErrorType::NotImplemented))
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
fn chmod(&mut self, path: &std::path::Path, pex: UnixPex) -> HostResult<()> {
|
||||
let path: PathBuf = self.to_path(path);
|
||||
// Get metadta
|
||||
match fs::metadata(path.as_path()) {
|
||||
Ok(metadata) => {
|
||||
let mut mpex = metadata.permissions();
|
||||
mpex.set_mode(pex.into());
|
||||
match set_permissions(path.as_path(), mpex) {
|
||||
match fs::set_permissions(path.as_path(), mpex) {
|
||||
Ok(_) => {
|
||||
info!("Changed mode for {} to {:?}", path.display(), pex);
|
||||
Ok(())
|
||||
@@ -485,11 +493,17 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open file for read
|
||||
pub fn open_file_read(&self, file: &Path) -> Result<StdFile, HostError> {
|
||||
#[cfg(win)]
|
||||
fn chmod(&mut self, _path: &std::path::Path, _pex: UnixPex) -> HostResult<()> {
|
||||
warn!("Cannot set file mode on Windows");
|
||||
|
||||
Err(HostError::from(HostErrorType::NotImplemented))
|
||||
}
|
||||
|
||||
fn open_file(&mut self, file: &std::path::Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
let file: PathBuf = self.to_path(file);
|
||||
info!("Opening file {} for read", file.display());
|
||||
if !self.file_exists(file.as_path()) {
|
||||
if !self.exists(file.as_path())? {
|
||||
error!("File doesn't exist!");
|
||||
return Err(HostError::new(
|
||||
HostErrorType::NoSuchFileOrDirectory,
|
||||
@@ -503,7 +517,7 @@ impl Localhost {
|
||||
.write(false)
|
||||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(f),
|
||||
Ok(f) => Ok(Box::new(f)),
|
||||
Err(err) => {
|
||||
error!("Could not open file for read: {}", err);
|
||||
Err(HostError::new(
|
||||
@@ -515,8 +529,11 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open file for write
|
||||
pub fn open_file_write(&self, file: &Path) -> Result<StdFile, HostError> {
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
_metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>> {
|
||||
let file: PathBuf = self.to_path(file);
|
||||
info!("Opening file {} for write", file.display());
|
||||
match OpenOptions::new()
|
||||
@@ -525,10 +542,10 @@ impl Localhost {
|
||||
.truncate(true)
|
||||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(f),
|
||||
Ok(f) => Ok(Box::new(f)),
|
||||
Err(err) => {
|
||||
error!("Failed to open file: {}", err);
|
||||
match self.file_exists(file.as_path()) {
|
||||
match self.exists(file.as_path())? {
|
||||
true => Err(HostError::new(
|
||||
HostErrorType::ReadonlyFile,
|
||||
Some(err),
|
||||
@@ -544,71 +561,28 @@ impl Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether provided file path exists
|
||||
pub fn file_exists(&self, path: &Path) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
/// Get content of the current directory as a list of fs entry
|
||||
pub fn list_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
|
||||
info!("Reading directory {}", dir.display());
|
||||
match std::fs::read_dir(dir) {
|
||||
Ok(e) => {
|
||||
let mut fs_entries: Vec<File> = Vec::new();
|
||||
for entry in e.flatten() {
|
||||
// NOTE: 0.4.1, don't fail if stat for one file fails
|
||||
match self.stat(entry.path().as_path()) {
|
||||
Ok(entry) => fs_entries.push(entry),
|
||||
Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
|
||||
}
|
||||
}
|
||||
Ok(fs_entries)
|
||||
}
|
||||
Err(err) => Err(HostError::new(
|
||||
HostErrorType::DirNotAccessible,
|
||||
Some(err),
|
||||
dir,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a symlink at path pointing at target
|
||||
#[cfg(unix)]
|
||||
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> {
|
||||
let path = self.to_path(path);
|
||||
std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| {
|
||||
error!(
|
||||
"Failed to create symlink at {} pointing at {}: {}",
|
||||
path.display(),
|
||||
target.display(),
|
||||
e
|
||||
);
|
||||
HostError::new(HostErrorType::CouldNotCreateFile, Some(e), path.as_path())
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert path to absolute path
|
||||
fn to_path(&self, p: &Path) -> PathBuf {
|
||||
path::absolutize(self.wrkdir.as_path(), p)
|
||||
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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};
|
||||
|
||||
@@ -621,7 +595,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"));
|
||||
@@ -635,9 +610,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
#[cfg(win)]
|
||||
fn test_host_localhost_new() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap();
|
||||
assert_eq!(host.wrkdir, PathBuf::from("C:\\users"));
|
||||
// Scan dir
|
||||
let entries = std::fs::read_dir(PathBuf::from("C:\\users").as_path()).unwrap();
|
||||
@@ -657,14 +632,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn test_host_localhost_pwd() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
assert_eq!(host.pwd(), PathBuf::from("/dev"));
|
||||
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");
|
||||
@@ -680,7 +656,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();
|
||||
@@ -689,67 +665,86 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn test_host_localhost_open_read() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
// Create temp file
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
assert!(host.open_file_read(file.path()).is_ok());
|
||||
assert!(host.open_file(file.path()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
#[should_panic]
|
||||
fn test_host_localhost_open_read_err_no_such_file() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
assert!(host
|
||||
.open_file_read(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path())
|
||||
.is_ok());
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
fn test_host_localhost_open_read_err_not_accessible() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
//let mut perms = fs::metadata(file.path())?.permissions();
|
||||
fs::set_permissions(file.path(), PermissionsExt::from_mode(0o222)).unwrap();
|
||||
//fs::set_permissions(file.path(), perms)?;
|
||||
assert!(host.open_file_read(file.path()).is_err());
|
||||
assert!(host.open_file(file.path()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
fn test_host_localhost_open_write() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
// Create temp file
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
assert!(host.open_file_write(file.path()).is_ok());
|
||||
assert!(host.create_file(file.path(), &Metadata::default()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
fn test_host_localhost_open_write_err() {
|
||||
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
//let mut perms = fs::metadata(file.path())?.permissions();
|
||||
fs::set_permissions(file.path(), PermissionsExt::from_mode(0o444)).unwrap();
|
||||
//fs::set_permissions(file.path(), perms)?;
|
||||
assert!(host.open_file_write(file.path()).is_err());
|
||||
assert!(host.create_file(file.path(), &Metadata::default()).is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
#[cfg(posix)]
|
||||
fn should_resolve_relative_exists_and_stat_from_workdir() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let dir_path: &Path = tmpdir.path();
|
||||
assert!(make_file_at(dir_path, "nested.txt").is_ok());
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||
|
||||
assert!(host.exists(Path::new("nested.txt")).unwrap());
|
||||
assert!(!host.exists(Path::new("missing.txt")).unwrap());
|
||||
|
||||
let entry = host.stat(Path::new("nested.txt")).unwrap();
|
||||
assert_eq!(entry.path(), &dir_path.join("nested.txt"));
|
||||
assert!(entry.is_file());
|
||||
}
|
||||
|
||||
#[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();
|
||||
@@ -776,7 +771,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();
|
||||
@@ -785,23 +780,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
|
||||
@@ -809,28 +806,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
|
||||
@@ -844,25 +843,27 @@ 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]
|
||||
fn should_setstat() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
// stat
|
||||
let mut filemeta = host.stat(file.path()).unwrap();
|
||||
|
||||
@@ -883,26 +884,27 @@ 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();
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
// Chmod to file
|
||||
assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok());
|
||||
// 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();
|
||||
@@ -924,15 +926,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();
|
||||
@@ -954,7 +957,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();
|
||||
@@ -985,7 +988,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();
|
||||
@@ -1018,28 +1021,57 @@ mod tests {
|
||||
#[test]
|
||||
fn test_host_exec() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
// Execute
|
||||
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_host_exec_empty_command() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
// Execute empty command should return error
|
||||
assert!(host.exec("").is_err());
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
#[test]
|
||||
fn should_create_symlink() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let dir_path: &Path = tmpdir.path();
|
||||
// Make file
|
||||
assert!(make_file_at(dir_path, "pippo.txt").is_ok());
|
||||
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||
let mut p = dir_path.to_path_buf();
|
||||
p.push("pippo.txt");
|
||||
// Make symlink
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
#[test]
|
||||
fn should_stat_relative_symlink_as_symlink() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let dir_path: &Path = tmpdir.path();
|
||||
assert!(make_file_at(dir_path, "target.txt").is_ok());
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||
let target = dir_path.join("target.txt");
|
||||
|
||||
assert!(
|
||||
host.symlink(Path::new("link.txt"), target.as_path())
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let entry = host.stat(Path::new("link.txt")).unwrap();
|
||||
assert_eq!(entry.path(), &dir_path.join("link.txt"));
|
||||
assert!(entry.is_symlink());
|
||||
assert_eq!(entry.metadata().symlink.as_ref(), Some(&target));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -0,0 +1,315 @@
|
||||
//! ## Remote Bridged Host
|
||||
//!
|
||||
//! Bridges a `RemoteFs` implementation behind the local host interface used by
|
||||
//! the file transfer activity.
|
||||
|
||||
mod temp_mapped_file;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
|
||||
|
||||
use self::temp_mapped_file::TempMappedFile;
|
||||
use super::{HostBridge, HostError, HostResult};
|
||||
use crate::utils::path::normalize;
|
||||
|
||||
struct WriteStreamOp {
|
||||
path: PathBuf,
|
||||
metadata: Metadata,
|
||||
tempfile: TempMappedFile,
|
||||
}
|
||||
|
||||
/// A remote host bridged over the local host
|
||||
pub struct RemoteBridged {
|
||||
/// Remote fs client
|
||||
remote: Box<dyn RemoteFs>,
|
||||
/// Reminder used to finalize write stream
|
||||
write_stream_op: Option<WriteStreamOp>,
|
||||
}
|
||||
|
||||
impl RemoteBridged {
|
||||
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
let mut temp_file = TempMappedFile::new()?;
|
||||
|
||||
self.remote
|
||||
.open_file(file, Box::new(temp_file.clone()))
|
||||
.map_err(HostError::from)?;
|
||||
|
||||
// Sync changes
|
||||
temp_file.sync()?;
|
||||
|
||||
// now return as read
|
||||
Ok(Box::new(temp_file))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn RemoteFs>> for RemoteBridged {
|
||||
fn from(remote: Box<dyn RemoteFs>) -> Self {
|
||||
RemoteBridged {
|
||||
remote,
|
||||
write_stream_op: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostBridge for RemoteBridged {
|
||||
fn connect(&mut self) -> HostResult<()> {
|
||||
self.remote.connect().map(|_| ()).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> HostResult<()> {
|
||||
self.remote.disconnect().map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn is_connected(&mut self) -> bool {
|
||||
self.remote.is_connected()
|
||||
}
|
||||
|
||||
fn is_localhost(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn pwd(&mut self) -> HostResult<PathBuf> {
|
||||
debug!("Getting working directory");
|
||||
self.remote.pwd().map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {
|
||||
debug!("Changing working directory to {:?}", new_dir);
|
||||
self.remote.change_dir(new_dir).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> {
|
||||
debug!("Creating directory {:?}", dir_name);
|
||||
match self.remote.create_dir(dir_name, UnixPex::from(0o755)) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(remotefs::RemoteError {
|
||||
kind: RemoteErrorType::DirectoryAlreadyExists,
|
||||
..
|
||||
}) if ignore_existing => Ok(()),
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, entry: &File) -> HostResult<()> {
|
||||
debug!("Removing {:?}", entry.path());
|
||||
if entry.is_dir() {
|
||||
self.remote
|
||||
.remove_dir_all(entry.path())
|
||||
.map_err(HostError::from)
|
||||
} else {
|
||||
self.remote
|
||||
.remove_file(entry.path())
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> {
|
||||
debug!("Renaming {:?} to {:?}", entry.path(), dst_path);
|
||||
self.remote
|
||||
.mov(entry.path(), dst_path)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> {
|
||||
debug!("Copying {:?} to {:?}", entry.path(), dst);
|
||||
self.remote.copy(entry.path(), dst).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn stat(&mut self, path: &Path) -> HostResult<File> {
|
||||
debug!("Statting {:?}", path);
|
||||
self.remote.stat(path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||
debug!("Checking existence of {:?}", path);
|
||||
self.remote.exists(path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
||||
debug!("Listing directory {:?}", path);
|
||||
let entries = self.remote.list_dir(path).map_err(HostError::from)?;
|
||||
Ok(filter_self_refs(path, entries))
|
||||
}
|
||||
|
||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
|
||||
debug!("Setting metadata for {:?}", path);
|
||||
self.remote
|
||||
.setstat(path, metadata.clone())
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn exec(&mut self, cmd: &str) -> HostResult<String> {
|
||||
debug!("Executing command: {}", cmd);
|
||||
self.remote
|
||||
.exec(cmd)
|
||||
.map(|(_, stdout)| stdout)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
|
||||
debug!("Creating symlink from {:?} to {:?}", src, dst);
|
||||
self.remote.symlink(src, dst).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> {
|
||||
debug!("Changing permissions of {:?} to {:?}", path, pex);
|
||||
let stat = self.remote.stat(path).map_err(HostError::from)?;
|
||||
let mut metadata = stat.metadata.clone();
|
||||
metadata.mode = Some(pex);
|
||||
|
||||
self.setstat(path, &metadata)
|
||||
}
|
||||
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
// try to use stream, otherwise download to a temporary file and return a reader
|
||||
match self.remote.open(file) {
|
||||
Ok(stream) => Ok(Box::new(stream)),
|
||||
Err(RemoteError {
|
||||
kind: RemoteErrorType::UnsupportedFeature,
|
||||
..
|
||||
}) => self.open_file_from_temp(file),
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>> {
|
||||
// try to use stream, otherwise download to a temporary file and return a reader
|
||||
match self.remote.create(file, metadata) {
|
||||
Ok(stream) => Ok(Box::new(stream)),
|
||||
Err(RemoteError {
|
||||
kind: RemoteErrorType::UnsupportedFeature,
|
||||
..
|
||||
}) => {
|
||||
let tempfile = TempMappedFile::new()?;
|
||||
self.write_stream_op = Some(WriteStreamOp {
|
||||
path: file.to_path_buf(),
|
||||
metadata: metadata.clone(),
|
||||
tempfile: tempfile.clone(),
|
||||
});
|
||||
|
||||
Ok(Box::new(tempfile))
|
||||
}
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
|
||||
if let Some(WriteStreamOp {
|
||||
path,
|
||||
metadata,
|
||||
mut tempfile,
|
||||
}) = self.write_stream_op.take()
|
||||
{
|
||||
// sync
|
||||
tempfile.sync()?;
|
||||
// write file
|
||||
self.remote
|
||||
.create_file(&path, &metadata, Box::new(tempfile))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop entries that refer to the directory being listed.
|
||||
///
|
||||
/// Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
|
||||
/// to the listed directory in the LIST response, which would otherwise appear
|
||||
/// as a duplicate entry in the explorer.
|
||||
fn filter_self_refs(path: &Path, entries: Vec<File>) -> Vec<File> {
|
||||
let normalized = normalize(path);
|
||||
entries
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
let last = entry.path().components().next_back();
|
||||
let is_dot_ref = matches!(last, Some(Component::CurDir | Component::ParentDir));
|
||||
!is_dot_ref && normalize(entry.path()) != normalized
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use remotefs::fs::{FileType, Metadata};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn file(path: &str, file_type: FileType) -> File {
|
||||
File {
|
||||
path: PathBuf::from(path),
|
||||
metadata: Metadata {
|
||||
accessed: Some(SystemTime::UNIX_EPOCH),
|
||||
created: Some(SystemTime::UNIX_EPOCH),
|
||||
modified: Some(SystemTime::UNIX_EPOCH),
|
||||
file_type,
|
||||
gid: None,
|
||||
mode: None,
|
||||
size: 0,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_self_refs_drops_entry_matching_listed_dir() {
|
||||
let entries = vec![
|
||||
file("/wp-content/wp-content", FileType::Directory),
|
||||
file("/wp-content/index.php", FileType::File),
|
||||
];
|
||||
|
||||
let filtered = filter_self_refs(Path::new("/wp-content/wp-content"), entries);
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].path(), Path::new("/wp-content/index.php"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_self_refs_drops_dot_and_dotdot_entries() {
|
||||
let entries = vec![
|
||||
file("/foo/.", FileType::Directory),
|
||||
file("/foo/..", FileType::Directory),
|
||||
file("/foo/bar", FileType::File),
|
||||
];
|
||||
|
||||
let filtered = filter_self_refs(Path::new("/foo"), entries);
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].path(), Path::new("/foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_self_refs_normalizes_paths() {
|
||||
let entries = vec![
|
||||
file("/foo/./bar", FileType::File),
|
||||
file("/foo/baz/../", FileType::Directory),
|
||||
];
|
||||
|
||||
let filtered = filter_self_refs(Path::new("/foo"), entries);
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].path(), Path::new("/foo/./bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_self_refs_preserves_unrelated_entries() {
|
||||
let entries = vec![
|
||||
file("/home/user/notes.txt", FileType::File),
|
||||
file("/home/user/photos", FileType::Directory),
|
||||
];
|
||||
|
||||
let filtered = filter_self_refs(Path::new("/home/user"), entries.clone());
|
||||
|
||||
assert_eq!(filtered.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! ## Temp Mapped File
|
||||
//!
|
||||
//! Provides a temporary local file that mirrors a remote file while keeping a
|
||||
//! lazily opened read/write handle.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::host::{HostError, HostErrorType, HostResult};
|
||||
|
||||
/// A temporary file mapped to a remote file which has been transferred to local
|
||||
/// and which supports read/write operations
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TempMappedFile {
|
||||
tempfile: Arc<NamedTempFile>,
|
||||
handle: Arc<Mutex<Option<File>>>,
|
||||
}
|
||||
|
||||
impl Write for TempMappedFile {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let mut handle = self.write_hnd()?;
|
||||
handle.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
let mut handle = self.write_hnd()?;
|
||||
handle.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for TempMappedFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let mut handle = self.read_hnd()?;
|
||||
handle.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl TempMappedFile {
|
||||
/// Creates an empty temporary file container for a downloaded remote file.
|
||||
pub fn new() -> HostResult<Self> {
|
||||
NamedTempFile::new()
|
||||
.map(|tempfile| TempMappedFile {
|
||||
tempfile: Arc::new(tempfile),
|
||||
handle: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
.map_err(|e| {
|
||||
HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(e),
|
||||
std::path::Path::new(""),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Syncs the file to disk and frees the file handle.
|
||||
///
|
||||
/// Must be called
|
||||
pub fn sync(&mut self) -> HostResult<()> {
|
||||
{
|
||||
let mut lock = self.lock_handle().map_err(|e| {
|
||||
HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(e),
|
||||
self.tempfile.path(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(hnd) = lock.take() {
|
||||
hnd.sync_all().map_err(|e| {
|
||||
HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(e),
|
||||
self.tempfile.path(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||
let mut lock = self.lock_handle()?;
|
||||
if lock.is_none() {
|
||||
let hnd = File::create(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
|
||||
Ok(FileHandle::new(lock))
|
||||
}
|
||||
|
||||
fn read_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||
let mut lock = self.lock_handle()?;
|
||||
if lock.is_none() {
|
||||
let hnd = File::open(self.tempfile.path())?;
|
||||
lock.replace(hnd);
|
||||
}
|
||||
|
||||
Ok(FileHandle::new(lock))
|
||||
}
|
||||
|
||||
fn lock_handle(&self) -> io::Result<std::sync::MutexGuard<'_, Option<File>>> {
|
||||
self.handle
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("temporary file handle lock poisoned"))
|
||||
}
|
||||
}
|
||||
|
||||
struct FileHandle<'a> {
|
||||
guard: std::sync::MutexGuard<'a, Option<File>>,
|
||||
}
|
||||
|
||||
impl<'a> FileHandle<'a> {
|
||||
fn new(guard: std::sync::MutexGuard<'a, Option<File>>) -> Self {
|
||||
Self { guard }
|
||||
}
|
||||
|
||||
fn file_mut(&mut self) -> io::Result<&mut File> {
|
||||
self.guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| io::Error::other("temporary file handle is not initialized"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for FileHandle<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.file_mut()?.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.file_mut()?.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for FileHandle<'_> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.file_mut()?.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_write_and_read_file() {
|
||||
let mut file = TempMappedFile::new().unwrap();
|
||||
file.write_all(b"Hello, World!").unwrap();
|
||||
|
||||
file.sync().unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, b"Hello, World!");
|
||||
}
|
||||
}
|
||||
+110
-96
@@ -1,26 +1,10 @@
|
||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
//! ## termscp
|
||||
//!
|
||||
//! Binary entry point for argument parsing, logging setup, and activity
|
||||
//! manager startup.
|
||||
|
||||
// Crates
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate lazy_regex;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
||||
// External libs
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
// Include
|
||||
mod activity_manager;
|
||||
mod cli_opts;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod explorer;
|
||||
mod filetransfer;
|
||||
@@ -30,33 +14,57 @@ mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
// namespaces
|
||||
use activity_manager::{ActivityManager, NextActivity};
|
||||
use cli_opts::{Args, ArgsSubcommands, BookmarkParams, HostParams, Remote, RunOpts, Task};
|
||||
use filetransfer::FileTransferParams;
|
||||
use system::logging::{self, LogLevel};
|
||||
// Crates
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate lazy_regex;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
fn main() {
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use self::activity_manager::{ActivityManager, NextActivity};
|
||||
use self::cli::{Args, ArgsSubcommands, RemoteArgs, RunOpts, Task};
|
||||
use self::system::logging::{self, LogLevel};
|
||||
|
||||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const APP_BUILD_DATE: &str = env!("VERGEN_BUILD_TIMESTAMP");
|
||||
const APP_GIT_BRANCH: &str = env!("VERGEN_GIT_BRANCH");
|
||||
const APP_GIT_HASH: &str = env!("VERGEN_GIT_SHA");
|
||||
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
|
||||
type MainResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn git_hash() -> &'static str {
|
||||
APP_GIT_HASH[0..8].as_ref()
|
||||
}
|
||||
|
||||
fn main() -> MainResult<()> {
|
||||
let args: Args = argh::from_env();
|
||||
// Parse args
|
||||
let run_opts: RunOpts = match parse_args(args) {
|
||||
Ok(opts) => opts,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(255);
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
// Setup logging
|
||||
if let Err(err) = logging::init(run_opts.log_level) {
|
||||
eprintln!("Failed to initialize logging: {err}");
|
||||
}
|
||||
info!("termscp {} started!", TERMSCP_VERSION);
|
||||
info!(
|
||||
"{APP_NAME} v{TERMSCP_VERSION} ({APP_GIT_BRANCH}, {git_hash}, {APP_BUILD_DATE}) - Developed by {TERMSCP_AUTHORS}",
|
||||
git_hash = git_hash()
|
||||
);
|
||||
// Run
|
||||
info!("Starting activity manager...");
|
||||
let rc = run(run_opts);
|
||||
info!("termscp terminated with exitcode {}", rc);
|
||||
// Then return
|
||||
std::process::exit(rc);
|
||||
run(run_opts)
|
||||
}
|
||||
|
||||
/// Parse arguments
|
||||
@@ -65,15 +73,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!(
|
||||
"termscp - {TERMSCP_VERSION} - Developed by {TERMSCP_AUTHORS}",
|
||||
));
|
||||
run_opts.task = Task::Version;
|
||||
return Ok(run_opts);
|
||||
}
|
||||
// Logging
|
||||
if args.debug {
|
||||
@@ -81,27 +92,33 @@ 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
|
||||
match parse_address_arg(&args) {
|
||||
match RemoteArgs::try_from(&args) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(Remote::None) => {}
|
||||
Ok(remote) => {
|
||||
// Set params
|
||||
run_opts.remote = remote;
|
||||
// In this case the first activity will be FileTransfer
|
||||
run_opts.task = Task::Activity(NextActivity::FileTransfer);
|
||||
}
|
||||
}
|
||||
|
||||
// set activity based on remote state
|
||||
run_opts.task = if run_opts.remote.remote.is_none() {
|
||||
Task::Activity(NextActivity::Authentication)
|
||||
} else {
|
||||
Task::Activity(NextActivity::FileTransfer)
|
||||
};
|
||||
|
||||
// Local directory
|
||||
if let Some(localdir) = args.positional.get(1) {
|
||||
// Change working directory if local dir is set
|
||||
let localdir: PathBuf = PathBuf::from(localdir);
|
||||
if let Err(err) = env::set_current_dir(localdir.as_path()) {
|
||||
return Err(format!("Bad working directory argument: {err}"));
|
||||
}
|
||||
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
|
||||
@@ -111,90 +128,87 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
Ok(run_opts)
|
||||
}
|
||||
|
||||
/// Parse address argument from cli args
|
||||
fn parse_address_arg(args: &Args) -> Result<Remote, String> {
|
||||
if let Some(remote) = args.positional.first() {
|
||||
if args.address_as_bookmark {
|
||||
Ok(Remote::Bookmark(BookmarkParams::new(
|
||||
remote,
|
||||
args.password.as_ref(),
|
||||
)))
|
||||
} else {
|
||||
// Parse address
|
||||
parse_remote_address(remote.as_str())
|
||||
.map(|x| Remote::Host(HostParams::new(x, args.password.as_deref())))
|
||||
}
|
||||
} else {
|
||||
Ok(Remote::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse remote address
|
||||
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
|
||||
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
|
||||
}
|
||||
|
||||
/// Run task and return rc
|
||||
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!");
|
||||
0
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
1
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_install_update() -> i32 {
|
||||
fn run_install_update() -> MainResult<()> {
|
||||
match support::install_update() {
|
||||
Ok(msg) => {
|
||||
println!("{msg}");
|
||||
0
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Could not install update: {err}");
|
||||
1
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_activity(activity: NextActivity, ticks: Duration, remote: Remote) -> i32 {
|
||||
fn run_activity(
|
||||
activity: NextActivity,
|
||||
ticks: Duration,
|
||||
remote_args: RemoteArgs,
|
||||
keyring: bool,
|
||||
) -> MainResult<()> {
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks) {
|
||||
let mut manager: ActivityManager = match ActivityManager::new(ticks, keyring) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {err}");
|
||||
return 1;
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Set file transfer params if set
|
||||
match remote {
|
||||
Remote::Bookmark(BookmarkParams { name, password }) => {
|
||||
if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) {
|
||||
eprintln!("{err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Remote::Host(HostParams { params, password }) => {
|
||||
if let Err(err) = manager.set_filetransfer_params(params, password.as_deref()) {
|
||||
eprintln!("{err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Remote::None => {}
|
||||
if let Err(err) = manager.configure_remote_args(remote_args) {
|
||||
eprintln!("{err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
manager.run(activity);
|
||||
|
||||
0
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+38
-5
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
//! ## Import SSH Hosts
|
||||
//!
|
||||
//! Imports OpenSSH host entries into termscp bookmarks and optionally registers
|
||||
//! referenced private keys in the configured SSH key storage.
|
||||
|
||||
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:?}");
|
||||
if let Err(err) = bookmarks_client.add_bookmark(name, params, false) {
|
||||
error!("Could not add imported bookmark: {err}");
|
||||
}
|
||||
|
||||
// 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,
|
||||
_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: identity_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;
|
||||
|
||||
@@ -21,7 +23,9 @@ pub enum UpdateStatus {
|
||||
/// Info related to a github release
|
||||
#[derive(Debug)]
|
||||
pub struct Release {
|
||||
/// Release version string returned by GitHub.
|
||||
pub version: String,
|
||||
/// Release notes body returned by GitHub.
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
@@ -46,6 +50,7 @@ impl Update {
|
||||
self
|
||||
}
|
||||
|
||||
/// Installs the latest available release using the configured update options.
|
||||
pub fn upgrade(self) -> Result<UpdateStatus, UpdateError> {
|
||||
info!("Updating termscp...");
|
||||
GithubUpdater::configure()
|
||||
@@ -67,6 +72,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,8 +91,30 @@ 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);
|
||||
match parse_semver(r.version.as_str()) {
|
||||
Some(new_version) => {
|
||||
// Check if version is different
|
||||
@@ -105,8 +135,13 @@ impl Update {
|
||||
|
||||
/// Check wether new version is higher than new version
|
||||
fn is_new_version_higher(new_version: &str, current_version: &str) -> bool {
|
||||
version_compare::compare(new_version, current_version).unwrap_or(version_compare::Cmp::Lt)
|
||||
== version_compare::Cmp::Gt
|
||||
match (
|
||||
semver::Version::parse(new_version),
|
||||
semver::Version::parse(current_version),
|
||||
) {
|
||||
(Ok(new), Ok(current)) => new > current,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Status> for UpdateStatus {
|
||||
@@ -203,12 +238,66 @@ mod test {
|
||||
assert_eq!(release.version.as_str(), "0.7.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_default_release_body_when_missing() {
|
||||
let release: UpdRelease = UpdRelease {
|
||||
name: String::from("termscp 0.7.0"),
|
||||
version: String::from("0.7.0"),
|
||||
date: String::from("2021-09-12T00:00:00Z"),
|
||||
body: None,
|
||||
assets: vec![],
|
||||
};
|
||||
let release: Release = Release::from(release);
|
||||
|
||||
assert!(release.body.is_empty());
|
||||
assert_eq!(release.version.as_str(), "0.7.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_tell_that_version_is_higher() {
|
||||
assert!(Update::is_new_version_higher("0.10.0", "0.9.0"));
|
||||
assert!(Update::is_new_version_higher("0.20.0", "0.19.0"));
|
||||
assert!(Update::is_new_version_higher("0.20.0", "0.19.1"));
|
||||
assert!(Update::is_new_version_higher("1.0.0", "0.19.1"));
|
||||
assert!(!Update::is_new_version_higher("0.9.0", "0.10.0"));
|
||||
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 should_ignore_release_without_semver() {
|
||||
let release = Release {
|
||||
version: String::from("latest"),
|
||||
body: String::from("notes"),
|
||||
};
|
||||
|
||||
assert!(Update::check_version(release).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_ignore_release_when_version_is_not_newer() {
|
||||
let release = Release {
|
||||
version: cargo_crate_version!().to_string(),
|
||||
body: String::from("notes"),
|
||||
};
|
||||
|
||||
assert!(Update::check_version(release).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_accept_release_when_version_is_newer() {
|
||||
let release = Release {
|
||||
version: String::from("termscp-999.0.0"),
|
||||
body: String::from("notes"),
|
||||
};
|
||||
|
||||
let release = Update::check_version(release).unwrap();
|
||||
|
||||
assert_eq!(release.version.as_str(), "termscp-999.0.0");
|
||||
assert_eq!(release.body.as_str(), "notes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_check_whether_github_api_is_reachable() {
|
||||
assert!(Update::check_github_api_reachable().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
+299
-185
@@ -2,22 +2,16 @@
|
||||
//!
|
||||
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
||||
|
||||
// Crate
|
||||
// Ext
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
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},
|
||||
};
|
||||
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||
use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize};
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
@@ -39,42 +33,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 +95,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 = whoami::username().unwrap_or_default();
|
||||
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())
|
||||
@@ -191,15 +187,15 @@ impl BookmarksClient {
|
||||
name: S,
|
||||
params: FileTransferParams,
|
||||
save_password: bool,
|
||||
) {
|
||||
) -> Result<(), SerializerError> {
|
||||
let name: String = name.as_ref().to_string();
|
||||
if name.is_empty() {
|
||||
error!("Fatal error; bookmark name is empty");
|
||||
panic!("Bookmark name can't be empty");
|
||||
error!("Bookmark name is empty; ignoring add_bookmark request");
|
||||
return Ok(());
|
||||
}
|
||||
// Make bookmark
|
||||
info!("Added bookmark {}", name);
|
||||
let mut host: Bookmark = self.make_bookmark(params);
|
||||
let mut host: Bookmark = self.make_bookmark(params)?;
|
||||
// If not save_password, set secrets to `None`
|
||||
if !save_password {
|
||||
host.password = None;
|
||||
@@ -209,6 +205,7 @@ impl BookmarksClient {
|
||||
}
|
||||
}
|
||||
self.hosts.bookmarks.insert(name, host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete entry from bookmarks
|
||||
@@ -230,9 +227,9 @@ impl BookmarksClient {
|
||||
}
|
||||
|
||||
/// Add a new recent to bookmarks
|
||||
pub fn add_recent(&mut self, params: FileTransferParams) {
|
||||
pub fn add_recent(&mut self, params: FileTransferParams) -> Result<(), SerializerError> {
|
||||
// Make bookmark
|
||||
let mut host: Bookmark = self.make_bookmark(params);
|
||||
let mut host: Bookmark = self.make_bookmark(params)?;
|
||||
// Null password for recents
|
||||
host.password = None;
|
||||
if let Some(s3) = host.s3.as_mut() {
|
||||
@@ -244,7 +241,7 @@ impl BookmarksClient {
|
||||
if *value == host {
|
||||
debug!("Discarding recent since duplicated ({})", key);
|
||||
// Don't save duplicates
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// If hosts size is bigger than self.recents_size; pop last
|
||||
@@ -269,6 +266,7 @@ impl BookmarksClient {
|
||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||
info!("Saved recent host {}", name);
|
||||
self.hosts.recents.insert(name, host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete entry from recents
|
||||
@@ -333,27 +331,29 @@ impl BookmarksClient {
|
||||
}
|
||||
|
||||
/// Make bookmark from credentials
|
||||
fn make_bookmark(&self, params: FileTransferParams) -> Bookmark {
|
||||
fn make_bookmark(&self, params: FileTransferParams) -> Result<Bookmark, SerializerError> {
|
||||
let mut bookmark: Bookmark = Bookmark::from(params);
|
||||
// Encrypt password
|
||||
if let Some(pwd) = bookmark.password {
|
||||
bookmark.password = Some(self.encrypt_str(pwd.as_str()));
|
||||
bookmark.password = Some(self.encrypt_str(pwd.as_str())?);
|
||||
}
|
||||
// Encrypt aws s3 params
|
||||
if let Some(s3) = bookmark.s3.as_mut() {
|
||||
if let Some(access_key) = s3.access_key.as_mut() {
|
||||
*access_key = self.encrypt_str(access_key.as_str());
|
||||
*access_key = self.encrypt_str(access_key.as_str())?;
|
||||
}
|
||||
if let Some(secret_access_key) = s3.secret_access_key.as_mut() {
|
||||
*secret_access_key = self.encrypt_str(secret_access_key.as_str());
|
||||
*secret_access_key = self.encrypt_str(secret_access_key.as_str())?;
|
||||
}
|
||||
}
|
||||
bookmark
|
||||
Ok(bookmark)
|
||||
}
|
||||
|
||||
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
|
||||
fn encrypt_str(&self, txt: &str) -> String {
|
||||
crypto::aes128_b64_crypt(self.key.as_str(), txt)
|
||||
fn encrypt_str(&self, txt: &str) -> Result<String, SerializerError> {
|
||||
crypto::aes128_b64_crypt(self.key.as_str(), txt).map_err(|err| {
|
||||
SerializerError::new_ex(SerializerErrorKind::Serialization, err.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrypt provided string using AES-128
|
||||
@@ -389,7 +389,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,32 +405,40 @@ 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",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
let recent_key: String = String::from(client.iter_recents().next().unwrap());
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
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,9 +461,13 @@ 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);
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark("my-bucket", make_s3_ftparams(), true)
|
||||
.is_ok()
|
||||
);
|
||||
// Verify bookmark
|
||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||
@@ -473,9 +485,13 @@ 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);
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark("my-bucket", make_s3_ftparams(), false)
|
||||
.is_ok()
|
||||
);
|
||||
// Verify bookmark
|
||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||
@@ -494,9 +510,9 @@ 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());
|
||||
assert!(client.add_recent(make_s3_ftparams()).is_ok());
|
||||
// Verify bookmark
|
||||
let bookmark = client.iter_recents().next().unwrap();
|
||||
let bookmark = client.get_recent(bookmark).unwrap();
|
||||
@@ -517,29 +533,37 @@ 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",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
client.add_bookmark(
|
||||
"raspberry2",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword2"),
|
||||
),
|
||||
true,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry2",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword2"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_bookmarks().count(), 2);
|
||||
@@ -561,26 +585,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_bad_bookmark_name() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
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();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark with empty name should be silently ignored
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// No bookmark should have been added
|
||||
assert_eq!(client.iter_bookmarks().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -589,18 +617,22 @@ 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",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
false,
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
false,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
@@ -617,15 +649,19 @@ 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,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
let key: String = String::from(client.iter_recents().next().unwrap());
|
||||
@@ -653,22 +689,30 @@ 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,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
// There should be only one recent
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
}
|
||||
@@ -680,34 +724,46 @@ 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(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.1",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.1",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
sleep(Duration::from_secs(1));
|
||||
// 2
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.2",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.2",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
sleep(Duration::from_secs(1));
|
||||
// 3
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.3",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
assert!(
|
||||
client
|
||||
.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.3",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
// Limit is 2
|
||||
assert_eq!(client.iter_recents().count(), 2);
|
||||
// Check that 192.168.1.1 has been removed
|
||||
@@ -742,25 +798,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_system_bookmarks_add_bookmark_empty() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
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();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
// Add bookmark with empty name should be silently ignored
|
||||
assert!(
|
||||
client
|
||||
.add_bookmark(
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
// No bookmark should have been added
|
||||
assert_eq!(client.iter_bookmarks().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -769,7 +830,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(),
|
||||
@@ -778,6 +839,59 @@ mod tests {
|
||||
assert!(client.decrypt_str("bidoof").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_bookmark_when_password_decryption_fails() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
let mut bookmark = Bookmark::from(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
bookmark.password = Some(String::from("not-valid-base64"));
|
||||
client
|
||||
.hosts
|
||||
.bookmarks
|
||||
.insert(String::from("raspberry"), bookmark);
|
||||
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
|
||||
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(bookmark.4.as_deref(), Some("not-valid-base64"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_s3_bookmark_when_secret_decryption_fails() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
|
||||
let mut bookmark = Bookmark::from(make_s3_ftparams());
|
||||
let s3 = bookmark.s3.as_mut().unwrap();
|
||||
s3.access_key = Some(String::from("bad-access-key"));
|
||||
s3.secret_access_key = Some(String::from("bad-secret-key"));
|
||||
client
|
||||
.hosts
|
||||
.bookmarks
|
||||
.insert(String::from("my-bucket"), bookmark);
|
||||
|
||||
let bookmark = client.get_bookmark("my-bucket").unwrap();
|
||||
let params = bookmark.params.s3_params().unwrap();
|
||||
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||
assert_eq!(params.region.as_deref(), Some("eu-west-1"));
|
||||
assert_eq!(params.access_key.as_deref(), Some("bad-access-key"));
|
||||
assert_eq!(params.secret_access_key.as_deref(), Some("bad-secret-key"));
|
||||
}
|
||||
|
||||
/// Get paths for configuration and key for bookmarks
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let k: PathBuf = PathBuf::from(dir);
|
||||
|
||||
+41
-29
@@ -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);
|
||||
let (host, username) = Self::get_ssh_tokens(mkey)?;
|
||||
// Return key
|
||||
Ok(Some((host, username, PathBuf::from(key_path))))
|
||||
Some((host, username, PathBuf::from(key_path)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,12 +389,18 @@ impl ConfigClient {
|
||||
}
|
||||
|
||||
/// Get ssh tokens starting from ssh host key
|
||||
/// Panics if key has invalid syntax
|
||||
/// Returns: (host, username)
|
||||
fn get_ssh_tokens(host_key: &str) -> (String, String) {
|
||||
/// Returns: (host, username) or None if key has invalid syntax
|
||||
fn get_ssh_tokens(host_key: &str) -> Option<(String, String)> {
|
||||
let tokens: Vec<&str> = host_key.split('@').collect();
|
||||
assert!(tokens.len() >= 2);
|
||||
(String::from(tokens[1]), String::from(tokens[0]))
|
||||
if tokens.len() >= 2 {
|
||||
Some((String::from(tokens[1]), String::from(tokens[0])))
|
||||
} else {
|
||||
error!(
|
||||
"Invalid SSH host key format: '{}' (expected 'username@host')",
|
||||
host_key
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Make serializer error from `std::io::Error`
|
||||
@@ -454,7 +456,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 +482,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 +498,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 +682,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 +704,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());
|
||||
}
|
||||
@@ -711,10 +717,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
|
||||
(String::from("192.168.1.31"), String::from("pi"))
|
||||
Some((String::from("192.168.1.31"), String::from("pi")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_get_ssh_tokens_invalid() {
|
||||
assert!(ConfigClient::get_ssh_tokens("invalid").is_none());
|
||||
assert!(ConfigClient::get_ssh_tokens("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_make_io_err() {
|
||||
let err: SerializerError =
|
||||
|
||||
+11
-20
@@ -4,20 +4,21 @@
|
||||
|
||||
// Ext
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[cfg(not(test))]
|
||||
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::config_dir);
|
||||
#[cfg(test)]
|
||||
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| Some(std::env::temp_dir()));
|
||||
|
||||
#[cfg(not(test))]
|
||||
static CACHE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::cache_dir);
|
||||
#[cfg(test)]
|
||||
static CACHE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| Some(std::env::temp_dir()));
|
||||
|
||||
/// Get termscp config directory path and initialize it.
|
||||
/// Returns None if it's not possible to initialize it
|
||||
pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||
// Get file
|
||||
#[cfg(not(test))]
|
||||
lazy_static! {
|
||||
static ref CONF_DIR: Option<PathBuf> = dirs::config_dir();
|
||||
}
|
||||
#[cfg(test)]
|
||||
lazy_static! {
|
||||
static ref CONF_DIR: Option<PathBuf> = Some(std::env::temp_dir());
|
||||
}
|
||||
|
||||
if let Some(dir) = CONF_DIR.as_deref() {
|
||||
init_dir(dir).map(Option::Some)
|
||||
} else {
|
||||
@@ -28,16 +29,6 @@ pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||
/// Get termscp cache directory path and initialize it.
|
||||
/// Returns None if it's not possible to initialize it
|
||||
pub fn init_cache_dir() -> Result<Option<PathBuf>, String> {
|
||||
// Get file
|
||||
#[cfg(not(test))]
|
||||
lazy_static! {
|
||||
static ref CACHE_DIR: Option<PathBuf> = dirs::cache_dir();
|
||||
}
|
||||
#[cfg(test)]
|
||||
lazy_static! {
|
||||
static ref CACHE_DIR: Option<PathBuf> = Some(std::env::temp_dir());
|
||||
}
|
||||
|
||||
if let Some(dir) = CACHE_DIR.as_deref() {
|
||||
init_dir(dir).map(Option::Some)
|
||||
} else {
|
||||
|
||||
@@ -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,
|
||||
BadSyntax,
|
||||
#[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,9 +53,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
#[cfg(feature = "with-keyring")]
|
||||
assert_eq!(
|
||||
KeyStorageError::BadSytax.to_string(),
|
||||
KeyStorageError::BadSyntax.to_string(),
|
||||
String::from("Key has a bad syntax")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -71,7 +71,10 @@ impl KeyStorage for FileStorage {
|
||||
return Err(KeyStorageError::ProviderError);
|
||||
}
|
||||
// Set file to readonly
|
||||
let mut permissions: Permissions = file.metadata().unwrap().permissions();
|
||||
let mut permissions: Permissions = file
|
||||
.metadata()
|
||||
.map_err(|_| KeyStorageError::ProviderError)?
|
||||
.permissions();
|
||||
permissions.set_readonly(true);
|
||||
let _ = file.set_permissions(permissions);
|
||||
Ok(())
|
||||
|
||||
@@ -37,7 +37,7 @@ impl KeyStorage for KeyringStorage {
|
||||
| KeyringError::Invalid(_, _)
|
||||
| KeyringError::Ambiguous(_) => Err(KeyStorageError::ProviderError),
|
||||
KeyringError::BadEncoding(_) | KeyringError::TooLong(_, _) => {
|
||||
Err(KeyStorageError::BadSytax)
|
||||
Err(KeyStorageError::BadSyntax)
|
||||
}
|
||||
_ => Err(KeyStorageError::ProviderError),
|
||||
},
|
||||
@@ -76,16 +76,15 @@ impl KeyStorage for KeyringStorage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use whoami::username;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "isolated-tests"))]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
#[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))]
|
||||
fn test_system_keys_keyring_storage() {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
let username = whoami::username().expect("no username");
|
||||
let storage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
Ok(None) => {
|
||||
return Err(String::from(
|
||||
"This system doesn't seem to support CACHE_DIR",
|
||||
))
|
||||
));
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
@@ -25,7 +25,14 @@ pub fn init(level: LogLevel) -> Result<(), String> {
|
||||
let file = open_file(log_file_path.as_path(), true, true, false)
|
||||
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
|
||||
// Prepare log config
|
||||
let config = ConfigBuilder::new().set_time_format_rfc3339().build();
|
||||
let config = ConfigBuilder::new()
|
||||
.set_time_format_rfc3339()
|
||||
.add_filter_allow_str("termscp")
|
||||
.add_filter_allow_str("remotefs")
|
||||
.add_filter_allow_str("kube")
|
||||
.add_filter_allow_str("suppaftp")
|
||||
.add_filter_allow_str("pavao")
|
||||
.build();
|
||||
// Make logger
|
||||
WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}"))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ use ssh2_config::SshConfig;
|
||||
use super::config_client::ConfigClient;
|
||||
use crate::utils::ssh as ssh_utils;
|
||||
|
||||
/// Resolves SSH identity files from termscp config, SSH config, and standard
|
||||
/// OpenSSH default locations.
|
||||
#[derive(Default)]
|
||||
pub struct SshKeyStorage {
|
||||
/// Association between {user}@{host} and RSA key path
|
||||
@@ -38,21 +40,35 @@ impl SshKeyStorage {
|
||||
/// Resolve host via termscp ssh keys storage
|
||||
fn resolve_host_in_termscp_storage(&self, host: &str, username: &str) -> Option<&Path> {
|
||||
let key: String = Self::make_mapkey(host, username);
|
||||
self.hosts.get(&key).map(|x| x.as_path())
|
||||
self.hosts.get(&key).map(PathBuf::as_path)
|
||||
}
|
||||
|
||||
/// 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 +82,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 +105,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 +137,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 +151,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
|
||||
@@ -184,6 +208,43 @@ Host test
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_mapkey_from_username_and_host() {
|
||||
assert_eq!(
|
||||
SshKeyStorage::make_mapkey("example.org", "veeso"),
|
||||
"veeso@example.org"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_prefer_termscp_key_over_ssh_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 ssh_config_file = test_helpers::create_sample_file_with_content(format!(
|
||||
r#"
|
||||
Host test
|
||||
HostName 127.0.0.1
|
||||
User test
|
||||
IdentityFile {}
|
||||
"#,
|
||||
rsa_key.path().display()
|
||||
));
|
||||
let tmp_dir: tempfile::TempDir = tempfile::TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
client.set_ssh_config(Some(ssh_config_file.path().to_string_lossy().to_string()));
|
||||
assert!(client.add_ssh_key("test", "pi", "stored-key").is_ok());
|
||||
|
||||
let storage: SshKeyStorage = SshKeyStorage::from(&client);
|
||||
let resolved = storage.resolve("test", "pi").unwrap();
|
||||
|
||||
assert!(resolved.ends_with("pi@test.key"));
|
||||
assert_ne!(resolved.as_path(), rsa_key.path());
|
||||
}
|
||||
|
||||
/// Get paths for configuration and keys directory
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let mut k: PathBuf = PathBuf::from(dir);
|
||||
|
||||
@@ -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
|
||||
@@ -23,7 +23,7 @@ impl ThemeProvider {
|
||||
pub fn new(theme_path: &Path) -> Result<Self, SerializerError> {
|
||||
let default_theme: Theme = Theme::default();
|
||||
info!(
|
||||
"Setting up theme provider with thene path {} ",
|
||||
"Setting up theme provider with theme path {}",
|
||||
theme_path.display(),
|
||||
);
|
||||
// Create provider
|
||||
@@ -42,7 +42,7 @@ impl ThemeProvider {
|
||||
} else {
|
||||
// otherwise Load configuration from file
|
||||
if let Err(err) = provider.load() {
|
||||
error!("Couldn't read thene file: {}", err);
|
||||
error!("Couldn't read theme file: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Read theme file");
|
||||
@@ -143,7 +143,7 @@ impl ThemeProvider {
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tuirealm::tui::style::Color;
|
||||
use tuirealm::ratatui::style::Color;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -27,6 +27,8 @@ pub enum FsWatcherError {
|
||||
PathNotWatched,
|
||||
#[error("unable to watch path, since it's already watched")]
|
||||
PathAlreadyWatched,
|
||||
#[error("watcher event channel disconnected")]
|
||||
Disconnected,
|
||||
#[error("unknown event: {0}")]
|
||||
UnknownEvent(&'static str),
|
||||
#[error("worker error: {0}")]
|
||||
@@ -114,7 +116,7 @@ impl FsWatcher {
|
||||
let res = match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(res) => res,
|
||||
Err(RecvTimeoutError::Timeout) => return Ok(None),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
||||
Err(RecvTimeoutError::Disconnected) => return Err(FsWatcherError::Disconnected),
|
||||
};
|
||||
|
||||
// convert event to FsChange
|
||||
@@ -158,7 +160,7 @@ impl FsWatcher {
|
||||
|
||||
/// Returns the list of watched paths
|
||||
pub fn watched_paths(&self) -> Vec<&Path> {
|
||||
Vec::from_iter(self.paths.keys().map(|x| x.as_path()))
|
||||
Vec::from_iter(self.paths.keys().map(PathBuf::as_path))
|
||||
}
|
||||
|
||||
/// Unwatch provided path.
|
||||
@@ -245,9 +247,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 +265,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 +287,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 +303,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 +330,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 +350,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 +378,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 +403,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 +442,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 +457,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")]);
|
||||
@@ -136,7 +136,7 @@ impl FileToRemove {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileUpdate {
|
||||
/// Path to file which has changed
|
||||
local: PathBuf,
|
||||
host_bridge: PathBuf,
|
||||
/// Path to remote file to update
|
||||
remote: PathBuf,
|
||||
}
|
||||
@@ -152,13 +152,13 @@ impl FileUpdate {
|
||||
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path),
|
||||
local: changed_path,
|
||||
host_bridge: changed_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to local file to sync
|
||||
pub fn local(&self) -> &Path {
|
||||
self.local.as_path()
|
||||
pub fn host_bridge(&self) -> &Path {
|
||||
self.host_bridge.as_path()
|
||||
}
|
||||
|
||||
/// Get path to remote file to sync
|
||||
@@ -288,7 +288,7 @@ mod test {
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/bar.txt"),);
|
||||
assert_eq!(change.host_bridge(), Path::new("/tmp/bar.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
@@ -303,7 +303,7 @@ mod test {
|
||||
Path::new("/home/foo/temp"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/abc/foo.txt"),);
|
||||
assert_eq!(change.host_bridge(), Path::new("/tmp/abc/foo.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
|
||||
@@ -10,6 +10,8 @@ pub mod auth;
|
||||
pub mod filetransfer;
|
||||
pub mod setup;
|
||||
|
||||
const CROSSTERM_MAX_POLL: usize = 10;
|
||||
|
||||
// -- Exit reason
|
||||
|
||||
pub enum ExitReason {
|
||||
@@ -13,30 +13,41 @@ mod view;
|
||||
// Includes
|
||||
use std::time::Duration;
|
||||
|
||||
use tuirealm::application::PollStrategy;
|
||||
use tuirealm::application::{Application, PollStrategy};
|
||||
use tuirealm::event::NoUserEvent;
|
||||
use tuirealm::listener::EventListenerCfg;
|
||||
use tuirealm::{Application, NoUserEvent, Update};
|
||||
use tuirealm::terminal::TerminalAdapter;
|
||||
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
// radio
|
||||
const RADIO_PROTOCOL_SFTP: usize = 0;
|
||||
const RADIO_PROTOCOL_SCP: usize = 1;
|
||||
const RADIO_PROTOCOL_FTP: usize = 2;
|
||||
const RADIO_PROTOCOL_FTPS: usize = 3;
|
||||
const RADIO_PROTOCOL_S3: usize = 4;
|
||||
const RADIO_PROTOCOL_KUBE: usize = 5;
|
||||
const RADIO_PROTOCOL_WEBDAV: usize = 6;
|
||||
const RADIO_PROTOCOL_SMB: usize = 7;
|
||||
// host bridge protocol radio
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST: usize = 0;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SFTP: usize = 1;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 7;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last
|
||||
|
||||
// remote protocol radio
|
||||
const REMOTE_RADIO_PROTOCOL_SFTP: usize = 0;
|
||||
const REMOTE_RADIO_PROTOCOL_SCP: usize = 1;
|
||||
const REMOTE_RADIO_PROTOCOL_FTP: usize = 2;
|
||||
const REMOTE_RADIO_PROTOCOL_FTPS: usize = 3;
|
||||
const REMOTE_RADIO_PROTOCOL_S3: usize = 4;
|
||||
const REMOTE_RADIO_PROTOCOL_KUBE: usize = 5;
|
||||
const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 6;
|
||||
const REMOTE_RADIO_PROTOCOL_SMB: usize = 7; // Keep as last
|
||||
|
||||
// -- components
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Id {
|
||||
Address,
|
||||
BookmarkName,
|
||||
BookmarkSavePassword,
|
||||
BookmarksList,
|
||||
@@ -45,22 +56,33 @@ pub enum Id {
|
||||
ErrorPopup,
|
||||
GlobalListener,
|
||||
HelpFooter,
|
||||
HostBridge(AuthFormId),
|
||||
InfoPopup,
|
||||
InstallUpdatePopup,
|
||||
Keybindings,
|
||||
NewVersionChangelog,
|
||||
NewVersionDisclaimer,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
Remote(AuthFormId),
|
||||
Subtitle,
|
||||
Title,
|
||||
WaitPopup,
|
||||
WindowSizeError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum AuthFormId {
|
||||
Address,
|
||||
KubeNamespace,
|
||||
KubeClusterUrl,
|
||||
KubeUsername,
|
||||
KubeClientCert,
|
||||
KubeClientKey,
|
||||
LocalDirectory,
|
||||
NewVersionChangelog,
|
||||
NewVersionDisclaimer,
|
||||
Password,
|
||||
Port,
|
||||
Protocol,
|
||||
QuitPopup,
|
||||
RecentsList,
|
||||
RemoteDirectory,
|
||||
S3AccessKey,
|
||||
S3Bucket,
|
||||
@@ -72,25 +94,21 @@ pub enum Id {
|
||||
S3SecurityToken,
|
||||
S3SessionToken,
|
||||
SmbShare,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroup,
|
||||
Subtitle,
|
||||
Title,
|
||||
Username,
|
||||
WaitPopup,
|
||||
WebDAVUri,
|
||||
WindowSizeError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Msg {
|
||||
enum Msg {
|
||||
Form(FormMsg),
|
||||
Ui(UiMsg),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum FormMsg {
|
||||
enum FormMsg {
|
||||
Connect,
|
||||
DeleteBookmark,
|
||||
DeleteRecent,
|
||||
@@ -98,15 +116,14 @@ pub enum FormMsg {
|
||||
InstallUpdate,
|
||||
LoadBookmark(usize),
|
||||
LoadRecent(usize),
|
||||
ProtocolChanged(FileTransferProtocol),
|
||||
HostBridgeProtocolChanged(HostBridgeProtocol),
|
||||
RemoteProtocolChanged(FileTransferProtocol),
|
||||
Quit,
|
||||
SaveBookmark,
|
||||
SaveBookmark(FormTab),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum UiMsg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
BookmarksListBlur,
|
||||
BookmarksTabBlur,
|
||||
CloseDeleteBookmark,
|
||||
@@ -117,6 +134,25 @@ pub enum UiMsg {
|
||||
CloseKeybindingsPopup,
|
||||
CloseQuitPopup,
|
||||
CloseSaveBookmark,
|
||||
HostBridge(UiAuthFormMsg),
|
||||
RececentsListBlur,
|
||||
Remote(UiAuthFormMsg),
|
||||
BookmarkNameBlur,
|
||||
SaveBookmarkPasswordBlur,
|
||||
ShowDeleteBookmarkPopup,
|
||||
ShowDeleteRecentPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowQuitPopup,
|
||||
ShowReleaseNotes,
|
||||
ShowSaveBookmarkPopup,
|
||||
WindowResized,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum UiAuthFormMsg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
ChangeFormTab,
|
||||
KubeNamespaceBlurDown,
|
||||
KubeNamespaceBlurUp,
|
||||
KubeClusterUrlBlurDown,
|
||||
@@ -136,7 +172,6 @@ pub enum UiMsg {
|
||||
PortBlurUp,
|
||||
ProtocolBlurDown,
|
||||
ProtocolBlurUp,
|
||||
RececentsListBlur,
|
||||
RemoteDirectoryBlurDown,
|
||||
RemoteDirectoryBlurUp,
|
||||
S3AccessKeyBlurDown,
|
||||
@@ -159,23 +194,14 @@ pub enum UiMsg {
|
||||
S3SessionTokenBlurUp,
|
||||
SmbShareBlurDown,
|
||||
SmbShareBlurUp,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroupDown,
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroupUp,
|
||||
BookmarkNameBlur,
|
||||
SaveBookmarkPasswordBlur,
|
||||
ShowDeleteBookmarkPopup,
|
||||
ShowDeleteRecentPopup,
|
||||
ShowKeybindingsPopup,
|
||||
ShowQuitPopup,
|
||||
ShowReleaseNotes,
|
||||
ShowSaveBookmarkPopup,
|
||||
UsernameBlurDown,
|
||||
UsernameBlurUp,
|
||||
WebDAVUriBlurDown,
|
||||
WebDAVUriBlurUp,
|
||||
WindowResized,
|
||||
}
|
||||
|
||||
/// Auth form input mask
|
||||
@@ -184,10 +210,23 @@ enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
Kube,
|
||||
Localhost,
|
||||
Smb,
|
||||
WebDAV,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum HostBridgeProtocol {
|
||||
Localhost,
|
||||
Remote(FileTransferProtocol),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum FormTab {
|
||||
HostBridge,
|
||||
Remote,
|
||||
}
|
||||
|
||||
// Store keys
|
||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||
@@ -203,8 +242,11 @@ pub struct AuthActivity {
|
||||
exit_reason: Option<ExitReason>,
|
||||
/// Should redraw ui
|
||||
redraw: bool,
|
||||
/// Protocol
|
||||
protocol: FileTransferProtocol,
|
||||
/// Host bridge protocol
|
||||
host_bridge_protocol: HostBridgeProtocol,
|
||||
last_form_tab: FormTab,
|
||||
/// Remote file transfer protocol
|
||||
remote_protocol: FileTransferProtocol,
|
||||
context: Option<Context>,
|
||||
}
|
||||
|
||||
@@ -213,16 +255,16 @@ impl AuthActivity {
|
||||
pub fn new(ticks: Duration) -> AuthActivity {
|
||||
AuthActivity {
|
||||
app: Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(ticks)
|
||||
.poll_timeout(ticks),
|
||||
EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
|
||||
),
|
||||
context: None,
|
||||
bookmarks_list: Vec::new(),
|
||||
exit_reason: None,
|
||||
last_form_tab: FormTab::Remote,
|
||||
recents_list: Vec::new(),
|
||||
redraw: true,
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
||||
remote_protocol: FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +297,23 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Get current input mask to show
|
||||
fn input_mask(&self) -> InputMask {
|
||||
match self.protocol {
|
||||
fn remote_input_mask(&self) -> InputMask {
|
||||
Self::file_transfer_protocol_input_mask(self.remote_protocol)
|
||||
}
|
||||
|
||||
/// Get current input mask to show
|
||||
fn host_bridge_input_mask(&self) -> InputMask {
|
||||
match self.host_bridge_protocol {
|
||||
HostBridgeProtocol::Localhost => InputMask::Localhost,
|
||||
HostBridgeProtocol::Remote(protocol) => {
|
||||
Self::file_transfer_protocol_input_mask(protocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get input mask for protocol
|
||||
fn file_transfer_protocol_input_mask(protocol: FileTransferProtocol) -> InputMask {
|
||||
match protocol {
|
||||
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
@@ -275,7 +332,7 @@ impl Activity for AuthActivity {
|
||||
fn on_create(&mut self, mut context: Context) {
|
||||
debug!("Initializing activity");
|
||||
// Initialize file transfer params
|
||||
context.set_ftparams(FileTransferParams::default());
|
||||
context.set_remote_params(FileTransferParams::default());
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
@@ -311,7 +368,10 @@ impl Activity for AuthActivity {
|
||||
return;
|
||||
}
|
||||
// Tick
|
||||
match self.app.tick(PollStrategy::UpTo(3)) {
|
||||
match self
|
||||
.app
|
||||
.tick(PollStrategy::UpTo(3, std::time::Duration::from_millis(10)))
|
||||
{
|
||||
Ok(messages) => {
|
||||
for msg in messages.into_iter() {
|
||||
let mut msg = Some(msg);
|
||||
@@ -3,7 +3,8 @@
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, FileTransferParams};
|
||||
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||
WebDAVProtocolParams,
|
||||
@@ -26,29 +27,54 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Load selected bookmark (at index) to input fields
|
||||
pub(super) fn load_bookmark(&mut self, idx: usize) {
|
||||
pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
||||
// Load parameters into components
|
||||
self.load_bookmark_into_gui(bookmark);
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save current input fields as a bookmark
|
||||
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
|
||||
let params = match self.collect_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) {
|
||||
let params = match form_tab {
|
||||
FormTab::Remote => match self.collect_remote_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
FormTab::HostBridge => match self.collect_host_bridge_params() {
|
||||
Ok(HostBridgeParams::Remote(protocol, params)) => FileTransferParams {
|
||||
protocol,
|
||||
params,
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
},
|
||||
Ok(HostBridgeParams::Localhost(_)) => {
|
||||
self.mount_error("You cannot save a localhost bookmark");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||
if let Err(err) = bookmarks_cli.add_bookmark(name.clone(), params, save_password) {
|
||||
self.mount_error(format!("Could not save bookmark: {err}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
// Remove `name` from bookmarks if exists
|
||||
@@ -73,13 +99,16 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Load selected recent (at index) to input fields
|
||||
pub(super) fn load_recent(&mut self, idx: usize) {
|
||||
pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) {
|
||||
if let Some(client) = self.bookmarks_client() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.recents_list.get(idx) {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// Load parameters
|
||||
self.load_bookmark_into_gui(bookmark);
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +116,7 @@ impl AuthActivity {
|
||||
|
||||
/// Save current input fields as a "recent"
|
||||
pub(super) fn save_recent(&mut self) {
|
||||
let params = match self.collect_host_params() {
|
||||
let params = match self.collect_remote_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
@@ -95,7 +124,10 @@ impl AuthActivity {
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client_mut() {
|
||||
bookmarks_cli.add_recent(params);
|
||||
if let Err(err) = bookmarks_cli.add_recent(params) {
|
||||
self.mount_error(format!("Could not save recent host: {err}"));
|
||||
return;
|
||||
}
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
@@ -103,10 +135,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,73 +179,125 @@ impl AuthActivity {
|
||||
}
|
||||
|
||||
/// Load bookmark data into the gui components
|
||||
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
fn load_host_bridge_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
// Load parameters into components
|
||||
self.protocol = bookmark.protocol;
|
||||
self.mount_protocol(bookmark.protocol);
|
||||
self.host_bridge_protocol = HostBridgeProtocol::Remote(bookmark.protocol);
|
||||
self.mount_host_bridge_protocol(self.host_bridge_protocol);
|
||||
self.mount_remote_directory(
|
||||
FormTab::HostBridge,
|
||||
bookmark
|
||||
.remote_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
self.mount_local_directory(
|
||||
FormTab::HostBridge,
|
||||
bookmark
|
||||
.local_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
match bookmark.params {
|
||||
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
|
||||
ProtocolParams::Kube(params) => self.load_bookmark_kube_into_gui(params),
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
self.load_bookmark_s3_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
self.load_bookmark_kube_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
|
||||
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
|
||||
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(params),
|
||||
ProtocolParams::WebDAV(params) => self.load_bookmark_webdav_into_gui(params),
|
||||
ProtocolParams::Generic(params) => {
|
||||
self.load_bookmark_generic_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::Smb(params) => {
|
||||
self.load_bookmark_smb_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::WebDAV(params) => {
|
||||
self.load_bookmark_webdav_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
|
||||
self.mount_address(params.address.as_str());
|
||||
self.mount_port(params.port);
|
||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
||||
/// Load bookmark data into the gui components
|
||||
fn load_remote_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
// Load parameters into components
|
||||
self.remote_protocol = bookmark.protocol;
|
||||
self.mount_remote_protocol(bookmark.protocol);
|
||||
self.mount_remote_directory(
|
||||
FormTab::Remote,
|
||||
bookmark
|
||||
.remote_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
self.mount_local_directory(
|
||||
FormTab::Remote,
|
||||
bookmark
|
||||
.local_path
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
match bookmark.params {
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
self.load_bookmark_s3_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
self.load_bookmark_kube_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
|
||||
ProtocolParams::Generic(params) => {
|
||||
self.load_bookmark_generic_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(FormTab::Remote, params),
|
||||
ProtocolParams::WebDAV(params) => {
|
||||
self.load_bookmark_webdav_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
|
||||
self.mount_s3_bucket(params.bucket_name.as_str());
|
||||
self.mount_s3_region(params.region.as_deref().unwrap_or(""));
|
||||
self.mount_s3_endpoint(params.endpoint.as_deref().unwrap_or(""));
|
||||
self.mount_s3_profile(params.profile.as_deref().unwrap_or(""));
|
||||
self.mount_s3_access_key(params.access_key.as_deref().unwrap_or(""));
|
||||
self.mount_s3_secret_access_key(params.secret_access_key.as_deref().unwrap_or(""));
|
||||
self.mount_s3_security_token(params.security_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_session_token(params.session_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_new_path_style(params.new_path_style);
|
||||
fn load_bookmark_generic_into_gui(&mut self, form_tab: FormTab, params: GenericProtocolParams) {
|
||||
self.mount_address(form_tab, params.address.as_str());
|
||||
self.mount_port(form_tab, params.port);
|
||||
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) {
|
||||
self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or(""));
|
||||
self.mount_kube_namespace(params.namespace.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_key(params.client_key.as_deref().unwrap_or(""));
|
||||
self.mount_kube_username(params.username.as_deref().unwrap_or(""));
|
||||
fn load_bookmark_s3_into_gui(&mut self, form_tab: FormTab, params: AwsS3Params) {
|
||||
self.mount_s3_bucket(form_tab, params.bucket_name.as_str());
|
||||
self.mount_s3_region(form_tab, params.region.as_deref().unwrap_or(""));
|
||||
self.mount_s3_endpoint(form_tab, params.endpoint.as_deref().unwrap_or(""));
|
||||
self.mount_s3_profile(form_tab, params.profile.as_deref().unwrap_or(""));
|
||||
self.mount_s3_access_key(form_tab, params.access_key.as_deref().unwrap_or(""));
|
||||
self.mount_s3_secret_access_key(
|
||||
form_tab,
|
||||
params.secret_access_key.as_deref().unwrap_or(""),
|
||||
);
|
||||
self.mount_s3_security_token(form_tab, params.security_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_session_token(form_tab, params.session_token.as_deref().unwrap_or(""));
|
||||
self.mount_s3_new_path_style(form_tab, params.new_path_style);
|
||||
}
|
||||
|
||||
fn load_bookmark_smb_into_gui(&mut self, params: SmbParams) {
|
||||
self.mount_address(params.address.as_str());
|
||||
#[cfg(unix)]
|
||||
self.mount_port(params.port);
|
||||
self.mount_username(params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(params.password.as_deref().unwrap_or(""));
|
||||
self.mount_smb_share(¶ms.share);
|
||||
#[cfg(unix)]
|
||||
self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or(""));
|
||||
fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) {
|
||||
self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or(""));
|
||||
self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_cert(form_tab, params.client_cert.as_deref().unwrap_or(""));
|
||||
self.mount_kube_client_key(form_tab, params.client_key.as_deref().unwrap_or(""));
|
||||
self.mount_kube_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_webdav_into_gui(&mut self, params: WebDAVProtocolParams) {
|
||||
self.mount_webdav_uri(¶ms.uri);
|
||||
self.mount_username(¶ms.username);
|
||||
self.mount_password(¶ms.password);
|
||||
fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) {
|
||||
self.mount_address(form_tab, params.address.as_str());
|
||||
#[cfg(posix)]
|
||||
self.mount_port(form_tab, params.port);
|
||||
self.mount_username(form_tab, params.username.as_deref().unwrap_or(""));
|
||||
self.mount_password(form_tab, params.password.as_deref().unwrap_or(""));
|
||||
self.mount_smb_share(form_tab, ¶ms.share);
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
||||
}
|
||||
|
||||
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
||||
self.mount_webdav_uri(form_tab, ¶ms.uri);
|
||||
self.mount_username(form_tab, ¶ms.username);
|
||||
self.mount_password(form_tab, ¶ms.password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,33 +13,34 @@ pub use bookmarks::{
|
||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||
RecentsList,
|
||||
};
|
||||
#[cfg(unix)]
|
||||
#[cfg(posix)]
|
||||
pub use form::InputSmbWorkgroup;
|
||||
pub use form::{
|
||||
InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
|
||||
InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory,
|
||||
InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region,
|
||||
InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare,
|
||||
InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle,
|
||||
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
|
||||
InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
|
||||
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
||||
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
||||
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
|
||||
RemoteProtocolRadio,
|
||||
};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
WindowSizeError,
|
||||
};
|
||||
pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title};
|
||||
use tui_realm_stdlib::Phantom;
|
||||
use tui_realm_stdlib::components::Phantom;
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||
use tuirealm::{Component, MockComponent};
|
||||
|
||||
// -- global listener
|
||||
|
||||
#[derive(Default, MockComponent)]
|
||||
#[derive(Default, Component)]
|
||||
pub struct GlobalListener {
|
||||
component: Phantom,
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for GlobalListener {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Esc | Key::Function(10),
|
||||
@@ -2,17 +2,22 @@
|
||||
//!
|
||||
//! auth activity bookmarks components
|
||||
|
||||
use tui_realm_stdlib::{Input, List, Radio};
|
||||
use tui_realm_stdlib::components::{Input, List, Radio};
|
||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan};
|
||||
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
|
||||
use tuirealm::props::{
|
||||
BorderSides, BorderType, Borders, Color, HorizontalAlignment, InputType, SpanStatic, Style,
|
||||
TextModifiers, Title,
|
||||
};
|
||||
use tuirealm::state::{State, StateValue};
|
||||
|
||||
use super::{FormMsg, Msg, UiMsg};
|
||||
use crate::ui::activities::auth::FormTab;
|
||||
|
||||
// -- bookmark list
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct BookmarksList {
|
||||
component: List,
|
||||
}
|
||||
@@ -22,23 +27,23 @@ impl BookmarksList {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.highlighted_color(color)
|
||||
.highlight_style(Style::default().fg(color))
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.title("Bookmarks", Alignment::Left)
|
||||
.title(Title::from("Bookmarks").alignment(HorizontalAlignment::Left))
|
||||
.rows(
|
||||
bookmarks
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
||||
.collect(),
|
||||
.map(|x| vec![SpanStatic::from(x.clone())])
|
||||
.collect::<Vec<Vec<SpanStatic>>>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for BookmarksList {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
@@ -76,7 +81,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => {
|
||||
State::Single(StateValue::Usize(choice)) => {
|
||||
Some(Msg::Form(FormMsg::LoadBookmark(choice)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
@@ -97,7 +102,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
|
||||
|
||||
// -- recents list
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct RecentsList {
|
||||
component: List,
|
||||
}
|
||||
@@ -107,23 +112,23 @@ impl RecentsList {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(Borders::default().color(color).modifiers(BorderType::Plain))
|
||||
.highlighted_color(color)
|
||||
.highlight_style(Style::default().fg(color))
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.title("Recent connections", Alignment::Left)
|
||||
.title(Title::from("Recent connections").alignment(HorizontalAlignment::Left))
|
||||
.rows(
|
||||
bookmarks
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.as_str())])
|
||||
.collect(),
|
||||
.map(|x| vec![SpanStatic::from(x.clone())])
|
||||
.collect::<Vec<Vec<SpanStatic>>>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for RecentsList {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
@@ -161,7 +166,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => match self.state() {
|
||||
State::One(StateValue::Usize(choice)) => {
|
||||
State::Single(StateValue::Usize(choice)) => {
|
||||
Some(Msg::Form(FormMsg::LoadRecent(choice)))
|
||||
}
|
||||
_ => Some(Msg::None),
|
||||
@@ -182,7 +187,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
|
||||
|
||||
// -- delete bookmark
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct DeleteBookmarkPopup {
|
||||
component: Radio,
|
||||
}
|
||||
@@ -191,22 +196,28 @@ impl DeleteBookmarkPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(color)
|
||||
.add_modifier(TextModifiers::REVERSED),
|
||||
)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Delete selected bookmark?", Alignment::Center),
|
||||
.title(
|
||||
Title::from("Delete selected bookmark?").alignment(HorizontalAlignment::Center),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseDeleteBookmark))
|
||||
@@ -236,7 +247,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Form(FormMsg::DeleteBookmark))
|
||||
} else {
|
||||
@@ -250,7 +261,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
|
||||
|
||||
// -- delete recent
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct DeleteRecentPopup {
|
||||
component: Radio,
|
||||
}
|
||||
@@ -259,22 +270,29 @@ impl DeleteRecentPopup {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(color)
|
||||
.add_modifier(TextModifiers::REVERSED),
|
||||
)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(1)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Delete selected recent host?", Alignment::Center),
|
||||
.title(
|
||||
Title::from("Delete selected recent host?")
|
||||
.alignment(HorizontalAlignment::Center),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseDeleteRecent))
|
||||
@@ -304,7 +322,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
CmdResult::Submit(State::Single(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Form(FormMsg::DeleteRecent))
|
||||
} else {
|
||||
@@ -320,32 +338,38 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
|
||||
|
||||
// -- save password
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct BookmarkSavePassword {
|
||||
component: Radio,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl BookmarkSavePassword {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(color)
|
||||
.add_modifier(TextModifiers::REVERSED),
|
||||
)
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(Color::Reset)
|
||||
.sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(&["Yes", "No"])
|
||||
.choices(["Yes", "No"])
|
||||
.value(0)
|
||||
.rewind(true)
|
||||
.foreground(color)
|
||||
.title("Save secrets?", Alignment::Center),
|
||||
.title(Title::from("Save secrets?").alignment(HorizontalAlignment::Center)),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
||||
@@ -364,7 +388,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur))
|
||||
}
|
||||
@@ -375,13 +399,14 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
|
||||
|
||||
// -- new bookmark name
|
||||
|
||||
#[derive(MockComponent)]
|
||||
#[derive(Component)]
|
||||
pub struct BookmarkName {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl BookmarkName {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
@@ -391,14 +416,15 @@ impl BookmarkName {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title("Bookmark name", Alignment::Left)
|
||||
.title(Title::from("Bookmark name").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
impl AppComponent<Msg, NoUserEvent> for BookmarkName {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseSaveBookmark))
|
||||
@@ -442,12 +468,12 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
|
||||
code: Key::Char(ch),
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
self.perform(Cmd::Type(*ch));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark)),
|
||||
}) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::NoUserEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputAddress {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputAddress {
|
||||
pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"127.0.0.1",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Remote host").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(host),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputAddress {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputPort {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputPort {
|
||||
pub fn new(port: u16, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"22",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.input_type(InputType::UnsignedInteger)
|
||||
.input_len(5)
|
||||
.title(Title::from("Port number").alignment(HorizontalAlignment::Left))
|
||||
.value(port.to_string()),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputPort {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputUsername {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputUsername {
|
||||
pub fn new(username: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"root",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Username").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(username),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputUsername {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputPassword {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputPassword {
|
||||
pub fn new(password: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.title(Title::from("Password").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Password('*'))
|
||||
.value(password),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputPassword {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::NoUserEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputKubeNamespace {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputKubeNamespace {
|
||||
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"namespace",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Pod namespace (optional)").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputKubeNamespace {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputKubeClusterUrl {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputKubeClusterUrl {
|
||||
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"cluster url",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(
|
||||
Title::from("Kube cluster url (optional)").alignment(HorizontalAlignment::Left),
|
||||
)
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputKubeClusterUrl {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown)),
|
||||
FormTab::HostBridge => {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurDown))
|
||||
}
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputKubeUsername {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputKubeUsername {
|
||||
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"username",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Kube username (optional)").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputKubeUsername {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputKubeClientCert {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputKubeClientCert {
|
||||
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"/home/user/.kube/client.crt",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(
|
||||
Title::from("Kube client cert path (optional)")
|
||||
.alignment(HorizontalAlignment::Left),
|
||||
)
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputKubeClientCert {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown)),
|
||||
FormTab::HostBridge => {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurDown))
|
||||
}
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputKubeClientKey {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputKubeClientKey {
|
||||
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.placeholder(tuirealm::props::SpanStatic::styled(
|
||||
"/home/user/.kube/client.key",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(
|
||||
Title::from("Kube client key path (optional)")
|
||||
.alignment(HorizontalAlignment::Left),
|
||||
)
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputKubeClientKey {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let on_key_down = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown)),
|
||||
};
|
||||
let on_key_up = match self.form_tab {
|
||||
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurUp)),
|
||||
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurUp)),
|
||||
};
|
||||
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user