mirror of
https://github.com/veeso/termscp.git
synced 2026-09-18 01:56:47 -07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f54c50f6ef | |||
| 325c8ceee7 | |||
| ebfaeb956c | |||
| 133ec898d8 | |||
| 1587d3d625 | |||
| 751f68f6d4 | |||
| e966a83220 | |||
| d99c76b43c | |||
| 7434c51063 | |||
| 9fbd617f88 | |||
| 2bbe560ec5 | |||
| 7472ca98a0 | |||
| 974b6d6917 | |||
| 98a1ce42dc | |||
| afbc74113f | |||
| 08c51a32cc | |||
| 739517f7e4 | |||
| 77281ed926 | |||
| 4b6325ebe3 | |||
| 6933a98cda |
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# termscp pre-commit hook.
|
||||
#
|
||||
# Runs two gates before a commit is recorded:
|
||||
# 1. dprint -- check formatting (Markdown, TOML, YAML, Rust)
|
||||
# 2. cargo-deny -- advisories, licenses, bans, and sources
|
||||
#
|
||||
# Install with `just setup_githooks` (sets core.hooksPath to .githooks).
|
||||
# Bypass in an emergency with `git commit --no-verify`.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "pre-commit: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v just >/dev/null 2>&1 || fail "just not found; install it to run the pre-commit checks"
|
||||
|
||||
if ! git diff --cached --quiet --diff-filter=ACMR --; then
|
||||
# Check the exact tree that will be committed, not possibly different
|
||||
# working-tree contents. The trailing slash is required by checkout-index.
|
||||
index_tree="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf -- "$index_tree"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
git checkout-index --all --prefix="$index_tree/"
|
||||
|
||||
echo "pre-commit: checking staged-tree formatting"
|
||||
(
|
||||
cd "$index_tree"
|
||||
just fmt_check
|
||||
)
|
||||
|
||||
echo "pre-commit: checking staged-tree dependencies"
|
||||
(
|
||||
cd "$index_tree"
|
||||
just deny
|
||||
)
|
||||
fi
|
||||
|
||||
echo "pre-commit: all checks passed"
|
||||
@@ -4,7 +4,6 @@ about: Create a report of the bug you've encountered
|
||||
title: "[BUG] - ISSUE_TITLE"
|
||||
labels: bug
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
@@ -4,7 +4,6 @@ about: Report a typo/error in a repository document
|
||||
title: "[COPY] - ISSUE_TITLE"
|
||||
labels: documentation
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
## Report
|
||||
|
||||
@@ -4,7 +4,6 @@ about: Suggest an idea to improve termscp
|
||||
title: "[Feature Request] - FEATURE_TITLE"
|
||||
labels: "new feature"
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
@@ -4,5 +4,4 @@ about: Ask what you want about the project
|
||||
title: "[QUESTION] - TITLE"
|
||||
labels: question
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
@@ -4,7 +4,6 @@ about: Create a report of a security vulnerability
|
||||
title: "[SECURITY] - ISSUE_TITLE"
|
||||
labels: security
|
||||
assignees: veeso
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
# ISSUE _NUMBER_ - PULL_REQUEST_TITLE
|
||||
# Description
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
List here your changes
|
||||
|
||||
- I made this...
|
||||
- I made also that...
|
||||
|
||||
## Type of change
|
||||
|
||||
Please select relevant options.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
<!--
|
||||
Provide a brief description of the changes you made in this pull request. If your changes are related to a specific issue, please mention the issue number (e.g., "Fixes #123").
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the contribution guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I formatted the code with `cargo fmt`
|
||||
- [ ] I checked my code using `cargo clippy` and reports no warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] I have introduced no new *C-bindings*
|
||||
- [ ] The changes I've made are Windows, MacOS, UNIX, Linux compatible (or I've handled them using `cfg target_os`)
|
||||
- [ ] I increased or maintained the code coverage for the project, compared to the previous commit
|
||||
- [ ] I have read the [AI Policy](https://github.com/veeso/termscp/blob/main/AI_POLICY.md) and the [contributing guidelines](https://github.com/veeso/termscp/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Acceptance tests
|
||||
## AI Disclosure
|
||||
|
||||
wait for a *project maintainer* to fulfill this section...
|
||||
|
||||
- [ ] regression test: ...
|
||||
<!--
|
||||
Describe how you used AI tools in this contribution. If you did not use any AI tools, write "N/A".
|
||||
-->
|
||||
|
||||
+97
-27
@@ -5,12 +5,12 @@ on:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
- "site/**"
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "./site/**/*"
|
||||
- "site/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -19,50 +19,120 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build-(${{ matrix.os }})
|
||||
toolchain:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
channel: ${{ steps.extract.outputs.result }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Extract toolchain channel from rust-toolchain.toml
|
||||
id: extract
|
||||
uses: mikefarah/yq@c14f446382944492701b16c1ddb48bb9dbe683e3 # v4.53.6
|
||||
with:
|
||||
cmd: yq '.toolchain.channel' rust-toolchain.toml
|
||||
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Rust (nightly)
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- name: Check formatting
|
||||
uses: dprint/check@9cb3a2b17a8e606d37aae341e49df3654933fc23 # v2.3
|
||||
|
||||
install-scripts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
- name: Check install scripts
|
||||
run: just check_install_scripts
|
||||
|
||||
crates:
|
||||
needs: toolchain
|
||||
name: crates-${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
|
||||
run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev libsmbclient-dev
|
||||
- name: Install macOS dependencies
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew update
|
||||
brew install \
|
||||
pkg-config \
|
||||
samba
|
||||
pkg-config \
|
||||
samba
|
||||
brew link --force samba
|
||||
- name: Install nightly toolchain
|
||||
if: runner.os == 'Linux'
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- name: Format
|
||||
if: runner.os == 'Linux'
|
||||
run: cargo +nightly fmt --all -- --check
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: ${{ needs.toolchain.outputs.channel }}
|
||||
components: clippy
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
- name: Build
|
||||
if: runner.os != 'Linux'
|
||||
run: cargo build
|
||||
- name: Run tests (Linux)
|
||||
run: just build_crates
|
||||
- name: Test (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: cargo test --no-default-features --features github-actions --no-fail-fast
|
||||
- name: Run tests
|
||||
run: just test "--no-default-features --features github-actions --no-fail-fast"
|
||||
- name: Test
|
||||
if: runner.os != 'Linux'
|
||||
run: cargo test --verbose --features github-actions
|
||||
run: just test "--verbose --features github-actions"
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -Dwarnings
|
||||
run: just clippy "-- -D warnings"
|
||||
|
||||
doc:
|
||||
needs: toolchain
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Linux dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev libsmbclient-dev
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: ${{ needs.toolchain.outputs.channel }}
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
- name: Build documentation
|
||||
run: just doc
|
||||
|
||||
deny:
|
||||
needs: toolchain
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: ${{ needs.toolchain.outputs.channel }}
|
||||
- name: Install cargo-deny
|
||||
uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
|
||||
with:
|
||||
tool: cargo-deny
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
- name: Check dependencies
|
||||
run: just deny
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
name: codeberg-mirror
|
||||
on:
|
||||
push:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: "Mirror to Codeberg"
|
||||
uses: yesolutions/mirror-action@1708f16cdb28634fd3ba10c5c79abc91f5578a14 # 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"
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install termscp from script
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install mdBook
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
<a href="./en-US/">termscp documentation</a>
|
||||
HTML
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: site_out
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
|
||||
+101
-74
@@ -21,23 +21,36 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ inputs.version }}
|
||||
prepared_ref: ${{ steps.prepared-ref.outputs.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
persist-credentials: true
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Validate version
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "invalid release version: $VERSION (expected MAJOR.MINOR.PATCH)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config user.name "veeso"
|
||||
git config user.email "christian.visintin@veeso.dev"
|
||||
|
||||
- name: Install git-cliff
|
||||
uses: taiki-e/install-action@56545b37b57562edd73171cb6c62cc509db4c34e # v2
|
||||
uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4
|
||||
with:
|
||||
tool: git-cliff
|
||||
tool: git-cliff,dprint
|
||||
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
|
||||
- name: Bump version
|
||||
env:
|
||||
@@ -47,15 +60,18 @@ jobs:
|
||||
- name: Generate CHANGELOG
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: git-cliff --tag "v$VERSION" -o CHANGELOG.md
|
||||
run: just changelog "$VERSION"
|
||||
|
||||
- name: Generate release notes
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: git-cliff --unreleased --tag "v$VERSION" --strip header -o RELEASE_NOTES.md
|
||||
|
||||
- name: Verify Cargo.lock is unchanged
|
||||
run: git diff --exit-code -- Cargo.lock
|
||||
|
||||
- name: Upload release notes
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-notes
|
||||
path: RELEASE_NOTES.md
|
||||
@@ -72,10 +88,22 @@ jobs:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
rm -f RELEASE_NOTES.md
|
||||
git add -A
|
||||
git add -A -- . ':!Cargo.lock'
|
||||
git diff --cached --exit-code -- Cargo.lock
|
||||
git commit -m "chore: release v$VERSION"
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Export prepared ref
|
||||
id: prepared-ref
|
||||
env:
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
name: build-${{ matrix.target }}
|
||||
@@ -83,14 +111,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-24.04
|
||||
kind: linux
|
||||
deb_suffix: amd64
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-24.04-arm
|
||||
kind: linux
|
||||
deb_suffix: arm64
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
kind: macos
|
||||
@@ -111,55 +137,46 @@ jobs:
|
||||
TARGET: ${{ matrix.target }}
|
||||
FEATURES: ${{ matrix.features }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.dry_run && github.sha || 'main' }}
|
||||
ref: ${{ needs.prepare.outputs.prepared_ref }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
# ---- Linux: native per-arch build (x86_64 on ubuntu-latest, aarch64 on ubuntu-24.04-arm) ----
|
||||
- name: Install dependencies (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
- name: Prepare release version
|
||||
shell: bash
|
||||
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 \
|
||||
cpanminus
|
||||
sudo cpanm Parse::Yapp::Driver
|
||||
cargo install cargo-deb
|
||||
dist/release/bump_version.sh "$VERSION" "$(date +%F)"
|
||||
cargo update --package termscp --precise "$VERSION"
|
||||
cargo metadata --locked --no-deps --format-version 1 > /dev/null
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
- name: Install Rust target
|
||||
if: matrix.kind == 'macos'
|
||||
run: rustup target add "$TARGET"
|
||||
- name: Install Rust target
|
||||
if: matrix.kind == 'windows'
|
||||
run: rustup target add "$env:TARGET"
|
||||
|
||||
# ---- Linux: static musl build in a pinned Alpine container ----
|
||||
- name: Install cargo-deb (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: cargo install cargo-deb --locked
|
||||
- name: Build (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: cargo build --release --features smb-vendored --target "$TARGET"
|
||||
run: just build_musl "$TARGET"
|
||||
- name: Build deb (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: cargo deb --no-build --target "$TARGET" --features smb-vendored
|
||||
run: cargo deb --locked --no-build --target "$TARGET" --features smb-vendored
|
||||
- name: Verify deb declares no runtime dependencies (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: |
|
||||
deb=$(ls target/"$TARGET"/debian/*.deb)
|
||||
depends=$(dpkg-deb -f "$deb" Depends)
|
||||
if [ -n "$depends" ]; then
|
||||
echo "static deb must have no Depends, got: $depends" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- macOS ----
|
||||
- name: Install deps (macOS)
|
||||
@@ -173,12 +190,12 @@ jobs:
|
||||
cpanm Parse::Yapp::Driver
|
||||
- name: Build (macOS)
|
||||
if: matrix.kind == 'macos'
|
||||
run: cargo build --release $FEATURES --target "$TARGET"
|
||||
run: just build_release "$TARGET" "$FEATURES"
|
||||
|
||||
# ---- Windows ----
|
||||
- name: Build (Windows)
|
||||
if: matrix.kind == 'windows'
|
||||
run: cargo build --release --features smb-vendored --target "$env:TARGET"
|
||||
run: just build_release "$env:TARGET" "--features smb-vendored"
|
||||
|
||||
# ---- Package posix (tar.gz) ----
|
||||
- name: Package (posix)
|
||||
@@ -204,7 +221,7 @@ jobs:
|
||||
run: cp target/"$TARGET"/debian/*.deb artifact/
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-${{ matrix.target }}
|
||||
path: artifact/*
|
||||
@@ -212,20 +229,20 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
publish-homebrew:
|
||||
needs: [prepare, build]
|
||||
needs: [prepare, build, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: build-*
|
||||
path: dl
|
||||
merge-multiple: true
|
||||
|
||||
- name: Checkout homebrew tap
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: veeso/homebrew-termscp
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
@@ -238,12 +255,12 @@ jobs:
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
SHA_MAC_ARM=$(cat dl/aarch64-apple-darwin.sha256)
|
||||
SHA_MAC_X64=$(cat dl/x86_64-apple-darwin.sha256)
|
||||
SHA_LIN_ARM=$(cat dl/aarch64-unknown-linux-gnu.sha256)
|
||||
SHA_LIN_X64=$(cat dl/x86_64-unknown-linux-gnu.sha256)
|
||||
SHA_LIN_ARM=$(cat dl/aarch64-unknown-linux-musl.sha256)
|
||||
SHA_LIN_X64=$(cat dl/x86_64-unknown-linux-musl.sha256)
|
||||
BASE="https://github.com/veeso/termscp/releases/latest/download"
|
||||
cat > tap/Formula/termscp.rb <<EOF
|
||||
class Termscp < Formula
|
||||
desc "A feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3/Kube/SMB/WebDAV"
|
||||
desc "A feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3/GCS/Kube/SMB/WebDAV"
|
||||
homepage "https://termscp.rs/"
|
||||
license "MIT"
|
||||
version "$VERSION"
|
||||
@@ -276,14 +293,12 @@ jobs:
|
||||
end
|
||||
|
||||
on_linux do
|
||||
depends_on "dbus"
|
||||
|
||||
on_arm do
|
||||
url "$BASE/termscp-v$VERSION-aarch64-unknown-linux-gnu.tar.gz"
|
||||
url "$BASE/termscp-v$VERSION-aarch64-unknown-linux-musl.tar.gz"
|
||||
sha256 "$SHA_LIN_ARM"
|
||||
end
|
||||
on_intel do
|
||||
url "$BASE/termscp-v$VERSION-x86_64-unknown-linux-gnu.tar.gz"
|
||||
url "$BASE/termscp-v$VERSION-x86_64-unknown-linux-musl.tar.gz"
|
||||
sha256 "$SHA_LIN_X64"
|
||||
end
|
||||
end
|
||||
@@ -316,21 +331,27 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
ref: ${{ inputs.dry_run && github.sha || 'main' }}
|
||||
ref: ${{ needs.prepare.outputs.prepared_ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Prepare release version
|
||||
run: |
|
||||
dist/release/bump_version.sh "$VERSION" "$(date +%F)"
|
||||
cargo update --package termscp --precise "$VERSION"
|
||||
cargo metadata --locked --no-deps --format-version 1 > /dev/null
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: build-*
|
||||
path: dl
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download release notes
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-notes
|
||||
path: notes
|
||||
@@ -355,7 +376,7 @@ jobs:
|
||||
|
||||
- name: Upload assets artifact (dry run)
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-assets-dryrun
|
||||
path: out/*
|
||||
@@ -380,13 +401,19 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: main
|
||||
ref: ${{ needs.prepare.outputs.prepared_ref }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
- name: Prepare release version
|
||||
run: |
|
||||
dist/release/bump_version.sh "$VERSION" "$(date +%F)"
|
||||
cargo update --package termscp --precise "$VERSION"
|
||||
cargo metadata --locked --no-deps --format-version 1 > /dev/null
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
|
||||
- name: Install dependencies (Linux)
|
||||
run: |
|
||||
@@ -421,12 +448,12 @@ jobs:
|
||||
|
||||
- name: Authenticate to crates.io
|
||||
id: auth
|
||||
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
|
||||
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
|
||||
|
||||
- name: Publish to crates.io
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
run: cargo publish --features smb-vendored
|
||||
run: just publish_crate
|
||||
|
||||
publish-choco:
|
||||
needs: [prepare, release]
|
||||
|
||||
@@ -9,30 +9,28 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: site
|
||||
|
||||
jobs:
|
||||
build-site:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: just site_install
|
||||
- name: Format
|
||||
run: npm run format:check
|
||||
run: just site_fmt_check
|
||||
- name: Lint
|
||||
run: npm run check
|
||||
run: just site_check
|
||||
- name: Test
|
||||
run: npm test --if-present
|
||||
run: just site_test
|
||||
- name: Build
|
||||
run: npm run build
|
||||
run: just site_build
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@a20b814fb01b71def3bd6f56e7494d667ddf28da # v4.1.1
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 7
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# AI Policy
|
||||
|
||||
The maintainers of this project don't want to interact directly with bots.
|
||||
Contributions must come from a human who understands the submitted code or text,
|
||||
can explain it, and is responsible for its quality as if they wrote it
|
||||
themselves. Using AI as a tool to help you write or reason about a contribution
|
||||
is fine; submitting AI output you don't understand, or relaying messages
|
||||
between a maintainer and an AI, is not.
|
||||
|
||||
If you wish to include context from an interaction with AI in your comments, it
|
||||
must be in a quote block (e.g., using `>`) and disclosed as such. It must be
|
||||
accompanied by human commentary explaining the relevance and implications of
|
||||
the context.
|
||||
|
||||
If you plan to contribute to termscp using AI tools, you **must** read,
|
||||
understand and follow this policy.
|
||||
|
||||
## Disclosure
|
||||
|
||||
You **must** disclose the use of AI tools in your contributions. You
|
||||
**must** perform this disclosure through the pull request template; filing
|
||||
PRs without using the pull request template is a violation of this policy.
|
||||
|
||||
This applies to any AI tool that generated or substantially shaped the content
|
||||
you submit — code, prose or commit messages — including agents, chat assistants
|
||||
and AI-assisted editors. Mechanical editor autocompletion of a few characters
|
||||
does not need to be disclosed; if in doubt, disclose.
|
||||
|
||||
## Translations
|
||||
|
||||
AI is useful when communicating as a non-native English speaker. If you are
|
||||
using AI to edit your comments for this purpose, please take the time to ensure
|
||||
it reflects your own voice and ideas.
|
||||
|
||||
## Provenance and licensing
|
||||
|
||||
You are responsible for the provenance of anything you submit. AI tools can
|
||||
reproduce code or text from their training data verbatim, which may carry a
|
||||
license incompatible with this project. By contributing, you confirm the work
|
||||
is yours to submit under the project's license and does not infringe a third
|
||||
party's rights. The same standard that applies to code you write by hand.
|
||||
|
||||
## Consequences
|
||||
|
||||
Violating this policy may get your contribution rejected and, for repeated or
|
||||
egregious violations, get you blocked from the project without further warning.
|
||||
|
||||
## Examples
|
||||
|
||||
Examples of things that are not allowed:
|
||||
|
||||
- Point an agent to a GitHub issue, ask it to solve the issue and open a PR,
|
||||
without understanding the issue, the PR, or testing the solution yourself.
|
||||
- Copy responses from the AI when replying to questions from maintainers,
|
||||
without understanding the question or the response.
|
||||
In other words, you should not "play telephone" between your AI-generated code,
|
||||
the reviewer, and the AI tool.
|
||||
|
||||
## Inspiration
|
||||
|
||||
This AI policy was inspired by:
|
||||
|
||||
- [release-plz](https://github.com/release-plz/.github/blob/main/AI_POLICY.md)
|
||||
+224
-34
@@ -1,3 +1,163 @@
|
||||
# Changelog
|
||||
|
||||
## 1.2.0
|
||||
|
||||
Released on 2026-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- **site:** add privacy policy page (#435)
|
||||
> Add a GDPR privacy policy covering Vercel hosting/server logs and
|
||||
> EU-hosted Umami analytics, and link it from the footer.
|
||||
- **gcs:** add Google Cloud Storage support (#443)
|
||||
> - feat(gcs): add Google Cloud Storage support
|
||||
- add support for all ssh2 config parameters.
|
||||
> Achieved by bumping `remotefs-ssh` to `0.9`.
|
||||
>
|
||||
> Added support for these parameters:
|
||||
>
|
||||
> - Compression
|
||||
> - Host key certificates
|
||||
> - CA signature algorithms
|
||||
> - keys to agents
|
||||
> - ProxyJump
|
||||
> - Server alive intervals
|
||||
> - Agent forwarding
|
||||
> - Remote forwarding
|
||||
> - Bind address
|
||||
> - Bind interface
|
||||
> - Connection attempts
|
||||
> - TCP Keepalive
|
||||
> - Accepted public key algos
|
||||
> - Certificate files
|
||||
- **ssh:** auto-fill ssh config parameters in auth form
|
||||
> Resolve SSH host parameters in auth forms and CLI connections while preserving explicit user, bookmark, and parsed alias values. Continue forwarding SSH config files for HostName and other SSH options, and document the precedence in English and Chinese.
|
||||
- **smb:** add SMB dialect selection and persistence (#445)
|
||||
> Support Auto, SMB1, SMB2, and SMB3 selection on Unix, bound negotiation to the selected dialect family, preserve legacy bookmarks as Auto, and document the new option. Windows keeps operating-system-managed negotiation.
|
||||
|
||||
### CI
|
||||
|
||||
- remove Codeberg mirror workflow. I do not support Codeberg mission anymore
|
||||
- migrate project automation to Just (#442)
|
||||
> - ci: migrate project automation to Just
|
||||
>
|
||||
> Centralize build, test, release, dependency, hook, and website commands in Just recipes. Pin workflow tooling, use the repository toolchain, and run the complete validation set in CI.
|
||||
>
|
||||
> - ci: codex being codex
|
||||
> - docs: update CLAUDE.md for just task runner migration
|
||||
>
|
||||
> Reflect the switch to just recipes for build/test/clippy/fmt, note
|
||||
> dprint replacing raw rustfmt, and add a cross-platform code requirement.
|
||||
>
|
||||
> - fix: resolve clippy warnings breaking CI on ubuntu and windows
|
||||
>
|
||||
> Use clone() instead of implicit to_string() on already-owned String
|
||||
> values, gate the windows-only unused make_file_at import behind
|
||||
> cfg(posix), and fix unused mut / manual assign-op in the windows-only
|
||||
> localhost test.
|
||||
>
|
||||
> - fix: fmt
|
||||
- **release:** build Linux artifacts as static musl binaries (#447)
|
||||
> Build Linux release artifacts as statically linked musl binaries for x86_64 and aarch64, update packaging and updater handling, and document the reduced runtime requirements.
|
||||
>
|
||||
> Keep release version preparation locked without refreshing dependencies or committing Cargo.lock.
|
||||
- **release:** add dprint
|
||||
|
||||
### Fixed
|
||||
|
||||
- **publish:** anchor include globs to crate root
|
||||
> Unanchored include patterns (LICENSE, README.md) matched at any depth
|
||||
> via gitignore-glob semantics, pulling 437 site/node_modules files into
|
||||
> the package and breaking cargo publish's dirty check.
|
||||
- print actual reason for failed host params collecting
|
||||
> previously, we didn't show any reason for failed collecting of host params in the auth form, but just a generic message
|
||||
|
||||
### Build
|
||||
|
||||
- **deps:** update aes-gcm to 0.11
|
||||
> Migrate nonce generation and parsing to the aes-gcm 0.11 API while preserving the encrypted payload format.
|
||||
- bump remotefs-smb 0.5.0 (#446)
|
||||
> - build: bump remotefs-smb 0.5.0
|
||||
> - fix(smb): windows build
|
||||
|
||||
## 1.2.0
|
||||
|
||||
Released on 2026-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- **site:** add privacy policy page (#435)
|
||||
> Add a GDPR privacy policy covering Vercel hosting/server logs and
|
||||
> EU-hosted Umami analytics, and link it from the footer.
|
||||
- **gcs:** add Google Cloud Storage support (#443)
|
||||
> - feat(gcs): add Google Cloud Storage support
|
||||
- add support for all ssh2 config parameters.
|
||||
> Achieved by bumping `remotefs-ssh` to `0.9`.
|
||||
>
|
||||
> Added support for these parameters:
|
||||
>
|
||||
> - Compression
|
||||
> - Host key certificates
|
||||
> - CA signature algorithms
|
||||
> - keys to agents
|
||||
> - ProxyJump
|
||||
> - Server alive intervals
|
||||
> - Agent forwarding
|
||||
> - Remote forwarding
|
||||
> - Bind address
|
||||
> - Bind interface
|
||||
> - Connection attempts
|
||||
> - TCP Keepalive
|
||||
> - Accepted public key algos
|
||||
> - Certificate files
|
||||
- **ssh:** auto-fill ssh config parameters in auth form
|
||||
> Resolve SSH host parameters in auth forms and CLI connections while preserving explicit user, bookmark, and parsed alias values. Continue forwarding SSH config files for HostName and other SSH options, and document the precedence in English and Chinese.
|
||||
- **smb:** add SMB dialect selection and persistence (#445)
|
||||
> Support Auto, SMB1, SMB2, and SMB3 selection on Unix, bound negotiation to the selected dialect family, preserve legacy bookmarks as Auto, and document the new option. Windows keeps operating-system-managed negotiation.
|
||||
|
||||
### CI
|
||||
|
||||
- migrate project automation to Just (#442)
|
||||
> - ci: migrate project automation to Just
|
||||
>
|
||||
> Centralize build, test, release, dependency, hook, and website commands in Just recipes. Pin workflow tooling, use the repository toolchain, and run the complete validation set in CI.
|
||||
>
|
||||
> - ci: codex being codex
|
||||
> - docs: update CLAUDE.md for just task runner migration
|
||||
>
|
||||
> Reflect the switch to just recipes for build/test/clippy/fmt, note
|
||||
> dprint replacing raw rustfmt, and add a cross-platform code requirement.
|
||||
>
|
||||
> - fix: resolve clippy warnings breaking CI on ubuntu and windows
|
||||
>
|
||||
> Use clone() instead of implicit to_string() on already-owned String
|
||||
> values, gate the windows-only unused make_file_at import behind
|
||||
> cfg(posix), and fix unused mut / manual assign-op in the windows-only
|
||||
> localhost test.
|
||||
>
|
||||
> - fix: fmt
|
||||
- **release:** build Linux artifacts as static musl binaries (#447)
|
||||
> Build Linux release artifacts as statically linked musl binaries for x86_64 and aarch64, update packaging and updater handling, and document the reduced runtime requirements.
|
||||
>
|
||||
> Keep release version preparation locked without refreshing dependencies or committing Cargo.lock.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **publish:** anchor include globs to crate root
|
||||
> Unanchored include patterns (LICENSE, README.md) matched at any depth
|
||||
> via gitignore-glob semantics, pulling 437 site/node_modules files into
|
||||
> the package and breaking cargo publish's dirty check.
|
||||
- print actual reason for failed host params collecting
|
||||
> previously, we didn't show any reason for failed collecting of host params in the auth form, but just a generic message
|
||||
|
||||
### Build
|
||||
|
||||
- **deps:** update aes-gcm to 0.11
|
||||
> Migrate nonce generation and parsing to the aes-gcm 0.11 API while preserving the encrypted payload format.
|
||||
- bump remotefs-smb 0.5.0 (#446)
|
||||
> - build: bump remotefs-smb 0.5.0
|
||||
> - fix(smb): windows build
|
||||
|
||||
## 1.1.1
|
||||
|
||||
Released on 2026-06-08
|
||||
@@ -10,6 +170,7 @@ Released on 2026-06-08
|
||||
> crates.io versions are immutable (no overwrite) and Chocolatey's
|
||||
> moderation queue blocks a fast re-push, so a clean 1.1.1 without vergen
|
||||
> is the only fix.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
Released on 2026-06-08
|
||||
@@ -29,7 +190,7 @@ Released on 2026-06-08
|
||||
- **install:** add Windows PowerShell installer and copy buttons on site
|
||||
> Add install.ps1 mirroring install.sh for Windows: arch detection,
|
||||
> release zip download, binary extraction, user PATH update.
|
||||
>
|
||||
>
|
||||
> - copy install.ps1 to site public/ at build time (copy-install.mjs)
|
||||
> - serve /install.ps1 with text/plain Content-Type (vercel.json)
|
||||
> - add PowerShell one-liner to install page and README
|
||||
@@ -38,11 +199,11 @@ Released on 2026-06-08
|
||||
- **config:** move config dir to ~/.config/termscp on macOS and %USERPROFILE%\.termscp on Windows
|
||||
> Resolve the config directory through a single per-platform config_dir()
|
||||
> function instead of relying on dirs::config_dir everywhere:
|
||||
>
|
||||
>
|
||||
> - macOS: ~/.config/termscp (was ~/Library/Application Support/termscp)
|
||||
> - Windows: %USERPROFILE%\.termscp (was roaming %APPDATA%\termscp)
|
||||
> - Linux/other: /termscp (unchanged)
|
||||
>
|
||||
>
|
||||
> Existing users are migrated automatically on first run: when the new
|
||||
> directory is absent and the legacy location exists, the whole config
|
||||
> directory is moved to the new path. The cache directory stays at the
|
||||
@@ -54,7 +215,7 @@ Released on 2026-06-08
|
||||
> Single workflow_dispatch (version, dry_run) that bumps versions, regenerates
|
||||
> CHANGELOG via git-cliff, rebuilds site CSS, builds all targets, creates the
|
||||
> GitHub release, updates the Homebrew tap and publishes Chocolatey.
|
||||
>
|
||||
>
|
||||
> - dist/release/bump_version.sh: version replacer across all tracked locations (+tests)
|
||||
> - .github/workflows/release.yml: prepare -> build matrix -> homebrew/release -> choco
|
||||
> - retire build-artifacts.yml (merged into release.yml)
|
||||
@@ -66,7 +227,7 @@ Released on 2026-06-08
|
||||
- fix release notes generation in release workflow
|
||||
> prepare job failed: git-cliff --latest crashed with 'trim_start_matches on
|
||||
> null' because the checkout was shallow (no tags/history) so no release existed.
|
||||
>
|
||||
>
|
||||
> - checkout prepare with fetch-depth: 0 + fetch-tags so git-cliff sees full
|
||||
> history and tags (also fixes an otherwise-truncated CHANGELOG)
|
||||
> - generate release notes with --unreleased --tag v$VERSION instead of --latest:
|
||||
@@ -143,7 +304,7 @@ Released on 2026-06-08
|
||||
> Migrate the transfer progress UI to tuirealm 4, where the stdlib
|
||||
> `ProgressBar` widget was dropped, by rebuilding the dual-bar panel on
|
||||
> top of `Gauge`.
|
||||
>
|
||||
>
|
||||
> - Restore the unified two-bar look: the full bar (top) and partial bar
|
||||
> (bottom) draw joined borders so they read as a single panel; a single
|
||||
> file shows one fully-bordered bar.
|
||||
@@ -159,19 +320,19 @@ Released on 2026-06-08
|
||||
> its top border is dropped to join the seam with the full bar. That left
|
||||
> the partial bar with one inner row while the full bar kept two, so the
|
||||
> two gauges rendered at unequal heights.
|
||||
>
|
||||
>
|
||||
> - Move the filename from the partial bar's title into its gauge label.
|
||||
> - Skip setting an empty title so no phantom top-positioned title triggers
|
||||
> the inset.
|
||||
> - Put the panel title on the top (full) bar for multi-file transfers.
|
||||
> - Bump the two-bar popup height to fit the joined panel.
|
||||
>
|
||||
>
|
||||
> Also bump Cargo.lock and adapt the embedded terminal to the new vt100
|
||||
> `screen_mut()` API.
|
||||
- **copy:** prevent emptying file when copy destination is empty (#421)
|
||||
> An empty copy destination resolved to the source file's own path, so
|
||||
> std::fs::copy truncated the original file to 0 bytes.
|
||||
>
|
||||
>
|
||||
> - localhost::copy now refuses to copy a file onto itself, returning an
|
||||
> error instead of truncating it (root cause).
|
||||
> - action_copy treats an empty/whitespace destination as a cancel.
|
||||
@@ -180,7 +341,7 @@ Released on 2026-06-08
|
||||
> path. Downstream upload logic treats the queued destination as the full
|
||||
> file path and passes it straight to create_file, so transfers failed
|
||||
> with a Failure error when the remote target resolved to a directory.
|
||||
>
|
||||
>
|
||||
> Append each entry's file name to the destination directory at enqueue
|
||||
> time in both enqueue_file and enqueue_all, matching the single-file
|
||||
> transfer path which already builds the full target path.
|
||||
@@ -191,6 +352,7 @@ Released on 2026-06-08
|
||||
> The old manual dist/build/* scripts and dist/{deb,rpm}.sh are superseded by the
|
||||
> automated release workflow. Add the chocolatey package consumed by release.yml.
|
||||
- **site:** copy install.sh from repo root at build time (single source)
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Released on 2026-04-18
|
||||
@@ -243,6 +405,7 @@ Released on 2026-04-18
|
||||
> `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)
|
||||
@@ -250,7 +413,7 @@ Released on 2026-04-18
|
||||
> - 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
|
||||
@@ -278,14 +441,14 @@ Released on 2026-04-18
|
||||
> 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
|
||||
>
|
||||
> - 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.
|
||||
@@ -302,7 +465,7 @@ Released on 2026-04-18
|
||||
- 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
|
||||
@@ -341,9 +504,9 @@ Released on 2026-04-18
|
||||
> 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
|
||||
@@ -394,6 +557,7 @@ Released on 2026-04-18
|
||||
### Style
|
||||
|
||||
- linter
|
||||
|
||||
## 0.19.1
|
||||
|
||||
Released on 2025-12-20
|
||||
@@ -407,6 +571,7 @@ Released on 2025-12-20
|
||||
- install.sh deb name
|
||||
- install.sh deb name
|
||||
- Updated dependencies to allow build on NetBSD
|
||||
|
||||
## 0.19.0
|
||||
|
||||
Released on 2025-11-11
|
||||
@@ -414,13 +579,13 @@ Released on 2025-11-11
|
||||
### Added
|
||||
|
||||
- Import bookmarks from ssh config with a CLI command (#364)
|
||||
> * feat: Import bookmarks from ssh config with a CLI command
|
||||
>
|
||||
> - feat: Import bookmarks from ssh config with a CLI command
|
||||
>
|
||||
> Use import-ssh-hosts to import all the possible hosts by the configured ssh config or the default one on your machine
|
||||
- Changed file overwrite behaviour (#366)
|
||||
> Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.
|
||||
- Added `<CTRL+S>` keybinding to get the total size of selected paths. (#367)
|
||||
> * feat: Added `<CTRL+S>` keybinding to get the total size of selected paths.
|
||||
> - feat: Added `<CTRL+S>` keybinding to get the total size of selected paths.
|
||||
- Merge branch '0.19.0'
|
||||
|
||||
### CI
|
||||
@@ -440,8 +605,8 @@ Released on 2025-11-11
|
||||
- typo in file open error message (#349)
|
||||
- SMB support for MacOS with vendored build of libsmbclient.
|
||||
- Report a message while calculating total size of files to transfer. (#362)
|
||||
> * fix: Report a message while calculating total size of files to transfer.
|
||||
>
|
||||
> - fix: Report a message while calculating total size of files to transfer.
|
||||
>
|
||||
> Currently, in case of huge transfers the app may look frozen while calculating the transfer size. We should at least report to the user we are actually doing something.
|
||||
- Issues with update checks (#363)
|
||||
> Removed error popup message if failed to check for updates.
|
||||
@@ -456,6 +621,7 @@ Released on 2025-11-11
|
||||
- 0.19 deps
|
||||
- remotefs-ssh 0.7.1
|
||||
> This version fixes compatibility with hosts which don't use bash/sh as the default shell.
|
||||
|
||||
## 0.18.0
|
||||
|
||||
Released on 2025-06-10
|
||||
@@ -464,7 +630,7 @@ Released on 2025-06-10
|
||||
|
||||
- **Updated dependencies** and updated the Rust edition to `2024`
|
||||
- Replaced the `Exec` popup with a fully functional terminal emulator (#348)
|
||||
> * feat: Replaced the `Exec` popup with a fully functional terminal emulator
|
||||
> - feat: Replaced the `Exec` popup with a fully functional terminal emulator
|
||||
- 0.18
|
||||
|
||||
### Fixed
|
||||
@@ -475,6 +641,7 @@ Released on 2025-06-10
|
||||
### Style
|
||||
|
||||
- catppuccin themes
|
||||
|
||||
## 0.17.0
|
||||
|
||||
Released on 2025-03-23
|
||||
@@ -523,6 +690,7 @@ Released on 2025-03-23
|
||||
- aws-s3 0.4.2
|
||||
- build docker for x86
|
||||
- so apparently native-tls vendored tries to build openssl on windows, wtf guys?
|
||||
|
||||
## 0.16.1
|
||||
|
||||
Released on 2024-11-12
|
||||
@@ -532,6 +700,7 @@ Released on 2024-11-12
|
||||
- cfg unix forbidden in rust .82
|
||||
- gg rust 1.82 for introducing a nice breaking change in config which was not mentioned in changelog
|
||||
- 0.16.1
|
||||
|
||||
## 0.16.0
|
||||
|
||||
Released on 2024-10-14
|
||||
@@ -550,6 +719,7 @@ Released on 2024-10-14
|
||||
- issue 292 New version alert was not displayed due to a semver regex issue. (#300)
|
||||
- 0.16
|
||||
- tiny ui issue
|
||||
|
||||
## 0.15.0
|
||||
|
||||
Released on 2024-10-03
|
||||
@@ -572,14 +742,14 @@ Released on 2024-10-03
|
||||
- issue 277 Fix a bug in the configuration page, which caused being stuck if the added SSH key was empty
|
||||
- popup texts
|
||||
- `isolated-tests` feature to run tests for releasing on distributions which run in isolated environments (#286)
|
||||
> * fix: `isolated-tests` feature to run tests for releasing on distributions which run in isolated environments
|
||||
>
|
||||
> * fix: cond
|
||||
> - fix: `isolated-tests` feature to run tests for releasing on distributions which run in isolated environments
|
||||
> - fix: cond
|
||||
- set date
|
||||
- github ci is stable and reliable (one worker broken each 2 weeks)
|
||||
- ci
|
||||
- readme
|
||||
- include build.rs
|
||||
|
||||
## 0.14.0
|
||||
|
||||
Released on 2024-07-17
|
||||
@@ -608,6 +778,7 @@ Released on 2024-07-17
|
||||
- german manual
|
||||
- removed support for RPM
|
||||
- changelog
|
||||
|
||||
## 0.13.0
|
||||
|
||||
Released on 2024-03-02
|
||||
@@ -624,6 +795,7 @@ Released on 2024-03-02
|
||||
- debian script
|
||||
- debian script
|
||||
- lint???
|
||||
|
||||
## 0.12.2
|
||||
|
||||
Released on 2023-10-01
|
||||
@@ -636,6 +808,7 @@ Released on 2023-10-01
|
||||
|
||||
- fmt
|
||||
- panic if the terminal screen is too small
|
||||
|
||||
## 0.12.1
|
||||
|
||||
Released on 2023-07-06
|
||||
@@ -659,6 +832,7 @@ Released on 2023-07-06
|
||||
- don't run CI on site/.md change
|
||||
- rustup target
|
||||
- don't update path breadcrumb if enter/scan dir failed (#203)
|
||||
|
||||
## 0.12.0
|
||||
|
||||
Released on 2023-05-16
|
||||
@@ -677,6 +851,7 @@ Released on 2023-05-16
|
||||
- pavao 0.2.3
|
||||
- macos script
|
||||
- release date
|
||||
|
||||
## 0.11.3
|
||||
|
||||
Released on 2023-04-19
|
||||
@@ -688,6 +863,7 @@ Released on 2023-04-19
|
||||
### Fixed
|
||||
|
||||
- relative paths windows (#167)
|
||||
|
||||
## 0.11.2
|
||||
|
||||
Released on 2023-04-18
|
||||
@@ -696,6 +872,7 @@ Released on 2023-04-18
|
||||
|
||||
- dependencies up-to-date
|
||||
- site 0.11.2
|
||||
|
||||
## 0.8.1
|
||||
|
||||
Released on 2022-03-22
|
||||
@@ -703,6 +880,7 @@ Released on 2022-03-22
|
||||
### Fixed
|
||||
|
||||
- footer listed "Delete" shortcut as "Make Dir"
|
||||
|
||||
## 0.8.0
|
||||
|
||||
Released on 2022-01-06
|
||||
@@ -710,6 +888,7 @@ Released on 2022-01-06
|
||||
### Arch
|
||||
|
||||
- install rust only if not found on local system
|
||||
|
||||
## 0.7.0
|
||||
|
||||
Released on 2021-10-12
|
||||
@@ -717,6 +896,7 @@ Released on 2021-10-12
|
||||
### Option
|
||||
|
||||
- prompt user when about to replace an existing file caused by a file transfer
|
||||
|
||||
## 0.6.1
|
||||
|
||||
Released on 2021-08-30
|
||||
@@ -724,6 +904,7 @@ Released on 2021-08-30
|
||||
### Fixed
|
||||
|
||||
- When copying files with tricky copy, the upper progress bar shows no text
|
||||
|
||||
## 0.5.1
|
||||
|
||||
Released on 2021-06-21
|
||||
@@ -731,6 +912,7 @@ Released on 2021-06-21
|
||||
### Fix
|
||||
|
||||
- target_family unix means also macos and linux; use BSD target_os
|
||||
|
||||
## 0.5.0
|
||||
|
||||
Released on 2021-05-23
|
||||
@@ -743,6 +925,7 @@ Released on 2021-05-23
|
||||
### Grcov
|
||||
|
||||
- exclude activities
|
||||
|
||||
## 0.4.1
|
||||
|
||||
Released on 2021-04-06
|
||||
@@ -755,18 +938,19 @@ Released on 2021-04-06
|
||||
### Readme
|
||||
|
||||
- one-liner for Homebrew
|
||||
> The one-liner command
|
||||
>
|
||||
> brew install veeso/termscp/termscp
|
||||
>
|
||||
> is equivalent to the two commands
|
||||
>
|
||||
> brew tap veeso/termscp
|
||||
> brew install termscp
|
||||
> The one-liner command
|
||||
>
|
||||
> brew install veeso/termscp/termscp
|
||||
>
|
||||
> is equivalent to the two commands
|
||||
>
|
||||
> brew tap veeso/termscp
|
||||
> brew install termscp
|
||||
|
||||
### SCP
|
||||
|
||||
- fixed symlink not properly detected
|
||||
|
||||
## 0.4.0
|
||||
|
||||
Released on 2021-03-27
|
||||
@@ -786,6 +970,7 @@ Released on 2021-03-27
|
||||
### View
|
||||
|
||||
- return String instead of id
|
||||
|
||||
## 0.3.3
|
||||
|
||||
Released on 2021-02-28
|
||||
@@ -793,6 +978,7 @@ Released on 2021-02-28
|
||||
### Git
|
||||
|
||||
- check for new updates (utils)
|
||||
|
||||
## 0.3.2
|
||||
|
||||
Released on 2021-01-24
|
||||
@@ -800,6 +986,7 @@ Released on 2021-01-24
|
||||
### Testing
|
||||
|
||||
- don't run on windows
|
||||
|
||||
## 0.3.0
|
||||
|
||||
Released on 2021-01-10
|
||||
@@ -834,6 +1021,7 @@ Released on 2021-01-10
|
||||
### SetupActivity
|
||||
|
||||
- <CTRL+E> as <DEL>
|
||||
|
||||
## 0.2.0
|
||||
|
||||
Released on 2020-12-21
|
||||
@@ -845,6 +1033,7 @@ Released on 2020-12-21
|
||||
### Scp
|
||||
|
||||
- when username was not provided, it didn't fallback to current username
|
||||
|
||||
## 0.1.2
|
||||
|
||||
Released on 2020-12-13
|
||||
@@ -852,6 +1041,7 @@ Released on 2020-12-13
|
||||
### FsEntry
|
||||
|
||||
- :*::symlink is now a Option<Box<FsEntry>>; this improved symlinks, which gave errors some times
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Released on 2020-12-06
|
||||
|
||||
@@ -4,36 +4,39 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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.
|
||||
termscp is a terminal file transfer client with a TUI (Terminal User Interface), supporting SFTP, SCP, FTP/FTPS, Kube, S3, GCS, 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)
|
||||
- **Language**: Rust (edition 2024, MSRV 1.98.0)
|
||||
- **UI Framework**: tuirealm v3 (built on crossterm)
|
||||
- **File Transfer**: remotefs ecosystem
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
Task runner is `just` (modular recipes under `just/*.just`, imported by root `justfile`). Run `just --list` for the full set.
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
cargo build --release
|
||||
cargo build --no-default-features # minimal build without SMB/keyring
|
||||
just build_crates # cargo build --workspace
|
||||
just build_crates "--release"
|
||||
cargo build --no-default-features # minimal build without SMB/keyring (no just recipe)
|
||||
|
||||
# Test (CI-equivalent)
|
||||
cargo test --no-default-features --features github-actions --no-fail-fast
|
||||
just test "--no-default-features --features github-actions --no-fail-fast"
|
||||
|
||||
# Run a single test
|
||||
# Run a single test / a module (use cargo directly, just recipes don't take test names)
|
||||
cargo test <test_name> -- --nocapture
|
||||
|
||||
# Run tests for a module
|
||||
cargo test --lib filetransfer::
|
||||
cargo test --lib config::params::tests
|
||||
|
||||
# Lint
|
||||
cargo clippy -- -Dwarnings
|
||||
just clippy "-- -D warnings"
|
||||
|
||||
# Format
|
||||
cargo fmt --all -- --check # check only
|
||||
cargo fmt --all # fix
|
||||
# Format (dprint: Markdown, TOML, YAML, and Rust via nightly rustfmt)
|
||||
just fmt_check # check only
|
||||
just fmt # fix
|
||||
|
||||
# All code checks at once (fmt_check, clippy -D warnings, doc, deny, install script lint)
|
||||
just check_code
|
||||
```
|
||||
|
||||
### System Dependencies (for building)
|
||||
@@ -66,16 +69,16 @@ main.rs → parse CLI args → ActivityManager::new() → ActivityManager::run()
|
||||
|
||||
### 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 |
|
||||
| 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
|
||||
|
||||
@@ -86,6 +89,7 @@ main.rs → parse CLI args → ActivityManager::new() → ActivityManager::run()
|
||||
### 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
|
||||
|
||||
@@ -93,7 +97,7 @@ Platform-specific dependencies: SSH and FTP crates use different TLS backends on
|
||||
|
||||
### 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`).
|
||||
`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`, `GoogleCloudStorageParams`, `KubeProtocolParams`, `SmbParams`, `WebDAVProtocolParams`).
|
||||
|
||||
## Code Conventions
|
||||
|
||||
@@ -106,6 +110,7 @@ Platform-specific dependencies: SSH and FTP crates use different TLS backends on
|
||||
|
||||
## Other conventions
|
||||
|
||||
- Always run `cargo +nightly fmt --all` and `cargo clippy --no-default-features -- -Dwarnings` after modifying Rust code
|
||||
- Always run `just fmt` and `just clippy "-- -D warnings"` after modifying Rust code
|
||||
- Always put plans to `./.claude/plans/`
|
||||
- When changing behavior that is documented under `docs/` (paths, config keys, commands, flags, etc.), update BOTH `docs/en-US/` and `docs/zh-CN/` to keep the translations in sync
|
||||
- All code must be cross-platform compatible (Windows, macOS, Linux) — avoid POSIX-only APIs, hardcoded path separators, or shell-specific behavior unless gated behind the `posix`/`win` cfg aliases
|
||||
|
||||
+10
-10
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* 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,
|
||||
- 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
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
- 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 email address,
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- 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
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
+1
-1
@@ -150,5 +150,5 @@ You can view the developer guide [here](https://docs.termscp.rs/en-US/developer/
|
||||
|
||||
---
|
||||
|
||||
Thank you for any contribution!
|
||||
Thank you for any contribution!\
|
||||
Christian Visintin
|
||||
|
||||
Generated
+1721
-1689
File diff suppressed because it is too large
Load Diff
+19
-30
@@ -1,17 +1,17 @@
|
||||
[package]
|
||||
name = "termscp"
|
||||
version = "1.1.1"
|
||||
edition = "2024"
|
||||
version = "1.2.0"
|
||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
categories = ["command-line-utilities"]
|
||||
edition = "2024"
|
||||
homepage = "https://termscp.rs"
|
||||
include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
include = ["/src/**/*", "/build.rs", "/LICENSE", "/README.md", "/CHANGELOG.md"]
|
||||
keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
rust-version = "1.89.0"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
rust-version = "1.98.0"
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV"
|
||||
|
||||
[package.metadata.rpm]
|
||||
package = "termscp"
|
||||
@@ -25,6 +25,7 @@ termscp = { path = "/usr/bin/termscp" }
|
||||
[package.metadata.deb]
|
||||
maintainer = "Christian Visintin <christian.visintin@veeso.dev>"
|
||||
copyright = "2025, Christian Visintin <christian.visintin@veeso.dev>"
|
||||
depends = ""
|
||||
extended-description-file = "docs/misc/README.deb.txt"
|
||||
|
||||
[features]
|
||||
@@ -37,9 +38,9 @@ smb-vendored = ["remotefs-smb/vendored"]
|
||||
|
||||
[dependencies]
|
||||
aes = "0.9"
|
||||
aes-gcm = "0.10"
|
||||
aes-gcm = "0.11"
|
||||
argh = "0.1"
|
||||
base64 = "0.22"
|
||||
base64 = "0.23"
|
||||
bitflags = "2"
|
||||
bytesize = "2"
|
||||
cbc = { version = "0.2", features = ["alloc"] }
|
||||
@@ -60,25 +61,19 @@ rand = "0.10"
|
||||
regex = "1"
|
||||
remotefs = "0.3"
|
||||
remotefs-aws-s3 = "0.4"
|
||||
remotefs-gcs = "0.1"
|
||||
remotefs-kube = "0.4"
|
||||
remotefs-smb = { version = "0.3", optional = true }
|
||||
remotefs-ssh = { version = "0.8", default-features = false, features = [
|
||||
"russh",
|
||||
] }
|
||||
remotefs-smb = { version = "0.5", default-features = false, optional = true, features = ["find", "pavao"] }
|
||||
remotefs-ssh = { version = "0.9", 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",
|
||||
] }
|
||||
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"] }
|
||||
serde_json = "1"
|
||||
shellexpand = "3"
|
||||
simplelog = "0.12"
|
||||
ssh2-config = "0.7"
|
||||
ssh2-config = "0.8"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
@@ -91,16 +86,10 @@ whoami = "2"
|
||||
wildmatch = "2"
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"freebsd\"))".dependencies]
|
||||
dbus-secret-service-keyring-store = { version = "1", features = [
|
||||
"crypto-rust",
|
||||
"vendored",
|
||||
] }
|
||||
dbus-secret-service-keyring-store = { version = "1", features = ["crypto-rust", "vendored"] }
|
||||
|
||||
[target."cfg(target_family = \"unix\")".dependencies]
|
||||
remotefs-ftp = { version = "0.4", features = [
|
||||
"native-tls",
|
||||
"native-tls-vendored",
|
||||
] }
|
||||
remotefs-ftp = { version = "0.4", features = ["native-tls", "native-tls-vendored"] }
|
||||
uzers = "0.12"
|
||||
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
@@ -114,7 +103,7 @@ windows-native-keyring-store = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
serial_test = "3"
|
||||
serial_test = "4"
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import "./just/build.just"
|
||||
import "./just/changelog.just"
|
||||
import "./just/code_check.just"
|
||||
import "./just/publish.just"
|
||||
import "./just/run.just"
|
||||
import "./just/site.just"
|
||||
import "./just/test.just"
|
||||
|
||||
# Lists all the available commands
|
||||
default:
|
||||
@just --list
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
## About termscp 🖥
|
||||
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD**, **NetBSD** and **Windows** compatible.
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/Google Cloud Storage (GCS)/WebDAV. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD**, **NetBSD** and **Windows** compatible.
|
||||
|
||||

|
||||
|
||||
@@ -43,39 +43,40 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
||||
|
||||
## Features 🎁
|
||||
|
||||
- 📁 Different communication protocols
|
||||
- 📁 Different communication protocols
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** and **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **Google Cloud Storage (GCS)**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
|
||||
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
|
||||
- Create, remove, rename, search, view and edit files
|
||||
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
|
||||
- 📝 View and edit files with your favourite applications
|
||||
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
||||
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
||||
- 🐚 Embedded terminal for executing commands on the system.
|
||||
- 🎨 Make it yours!
|
||||
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
|
||||
- 📝 View and edit files with your favourite applications
|
||||
- 💁 SFTP/SCP authentication with SSH keys and username/password
|
||||
- 🐧 Compatible with Windows, Linux, FreeBSD, NetBSD and MacOS
|
||||
- 🐚 Embedded terminal for executing commands on the system.
|
||||
- 🎨 Make it yours!
|
||||
- Themes
|
||||
- Custom file explorer format
|
||||
- Customizable text editor
|
||||
- Customizable file sorting
|
||||
- and many other parameters...
|
||||
- 📫 Get notified via Desktop Notifications when a large file has been transferred
|
||||
- 🔭 Keep file changes synchronized with the remote host
|
||||
- 🔐 Save your password in your operating system key vault
|
||||
- 🦀 Rust-powered
|
||||
- 👀 Developed keeping an eye on performance
|
||||
- 🦄 Frequent awesome updates
|
||||
- 📫 Get notified via Desktop Notifications when a large file has been transferred
|
||||
- 🔭 Keep file changes synchronized with the remote host
|
||||
- 🔐 Save your password in your operating system key vault
|
||||
- 🦀 Rust-powered
|
||||
- 👀 Developed keeping an eye on performance
|
||||
- 🦄 Frequent awesome updates
|
||||
|
||||
---
|
||||
|
||||
## Get started 🚀
|
||||
|
||||
If you're considering to install termscp I want to thank you 💜 ! I hope you will enjoy termscp!
|
||||
If you're considering to install termscp I want to thank you 💜 ! I hope you will enjoy termscp!\
|
||||
If you want to contribute to this project, don't forget to check out our [contribute guide](CONTRIBUTING.md).
|
||||
|
||||
If you are a Linux, a FreeBSD or a MacOS user this simple shell script will install termscp on your system with a single command:
|
||||
@@ -116,6 +117,12 @@ For more information or other platforms, please visit [termscp.rs](https://terms
|
||||
|
||||
### Requirements ❗
|
||||
|
||||
The official Linux binaries and `.deb` packages are statically linked against
|
||||
musl and have **no runtime requirements**: they run on any distribution and
|
||||
any glibc version.
|
||||
|
||||
These are only required to build termscp from source:
|
||||
|
||||
- **Linux** users:
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
@@ -131,10 +138,10 @@ These requirements are not forced required to run termscp, but to enjoy all of i
|
||||
|
||||
- **Linux/FreeBSD** users:
|
||||
- To **open** files via `V` (at least one of these)
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- _xdg-open_
|
||||
- _gio_
|
||||
- _gnome-open_
|
||||
- _kde-open_
|
||||
- **Linux** users:
|
||||
- A keyring manager: read more in the [User manual](https://docs.termscp.rs/en-US/configuration/password-security.html#linux-keyring)
|
||||
- **WSL** users
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# cargo-deny configuration for termscp.
|
||||
#
|
||||
# Run locally with `just deny`. The same command runs in CI and enforces
|
||||
# advisories, licenses, bans, and dependency sources.
|
||||
#
|
||||
# Docs: https://embarkstudios.github.io/cargo-deny/
|
||||
|
||||
[graph]
|
||||
all-features = true
|
||||
|
||||
[output]
|
||||
feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
yanked = "deny"
|
||||
unmaintained = "all"
|
||||
# These are temporary, reachable transitive-risk acceptances owned by the
|
||||
# termscp maintainers. Review or remove every exception by 2026-12-31. The
|
||||
# upgrade target is a remotefs/self_update release line that accepts each fixed
|
||||
# dependency version; until then, connections to untrusted endpoints remain an
|
||||
# acknowledged denial-of-service and certificate-validation risk.
|
||||
ignore = [
|
||||
# h2 0.3 is retained transitively by the reqwest 0.11/AWS HTTP stack; its
|
||||
# dependency constraints cannot select the fixed h2 0.4 release.
|
||||
{ id = "RUSTSEC-2026-0258", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade reqwest/AWS stack to fixed h2" },
|
||||
# number_prefix is retained by self_update through indicatif. It is
|
||||
# unmaintained, but has no reported vulnerability or compatible replacement.
|
||||
{ id = "RUSTSEC-2025-0119", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade self_update/indicatif" },
|
||||
# Both affected quick-xml lines are constrained by remotefs-webdav and
|
||||
# self_update to versions older than the fixed 0.41 release.
|
||||
{ id = "RUSTSEC-2026-0194", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade remotefs-webdav/self_update to quick-xml >=0.41" },
|
||||
{ id = "RUSTSEC-2026-0195", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade remotefs-webdav/self_update to quick-xml >=0.41" },
|
||||
# The SSH stack requires RSA, for which RustSec reports no safe upgrade.
|
||||
{ id = "RUSTSEC-2023-0071", reason = "owner: termscp maintainers; review by 2026-12-31; replace RSA dependency when upstream fix exists" },
|
||||
# rustls-pemfile is retained by the WebDAV and Kubernetes clients. Both
|
||||
# versions are unmaintained but have no reported vulnerability.
|
||||
{ id = "RUSTSEC-2025-0134", reason = "owner: termscp maintainers; review by 2026-12-31; upgrade WebDAV/Kubernetes clients away from rustls-pemfile" },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-1-Clause",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
]
|
||||
confidence-threshold = 0.8
|
||||
exceptions = [
|
||||
# The WebDAV backend currently depends on rustydav, which declares GPL-3.0.
|
||||
# Keep this exception crate-scoped so no other GPL dependency is admitted.
|
||||
{ allow = ["GPL-3.0"], crate = "rustydav" },
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
ignore = false
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
wildcards = "deny"
|
||||
allow-wildcard-paths = true
|
||||
highlight = "all"
|
||||
workspace-default-features = "allow"
|
||||
external-default-features = "allow"
|
||||
allow = []
|
||||
deny = []
|
||||
skip = []
|
||||
skip-tree = []
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = []
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>termscp</id>
|
||||
<version>1.1.1</version>
|
||||
<version>1.2.0</version>
|
||||
<title>termSCP</title>
|
||||
<authors>Christian Visintin</authors>
|
||||
<owners>Christian Visintin</owners>
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@ $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
|
||||
$is_arm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64'
|
||||
|
||||
if ($is_arm64) {
|
||||
$url = 'https://github.com/veeso/termscp/releases/download/v1.1.1/termscp-v1.1.1-msvc.zip'
|
||||
$url = 'https://github.com/veeso/termscp/releases/download/v1.2.0/termscp-v1.2.0-msvc.zip'
|
||||
$checksum = 'f6ad6c62f1578562f9af4bcee93bd4cc429cb52219c3636359b008db8789587e'
|
||||
} else {
|
||||
$url = 'https://github.com/veeso/termscp/releases/download/v1.1.1/termscp-v1.1.1-x86_64-pc-windows-msvc.zip'
|
||||
$url = 'https://github.com/veeso/termscp/releases/download/v1.2.0/termscp-v1.2.0-x86_64-pc-windows-msvc.zip'
|
||||
$checksum = 'd7796081b6f67b82acfa94557aa6852d12a33daab3cca6490660b20e42752005'
|
||||
}
|
||||
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env sh
|
||||
# Builds a static musl termscp release binary for the given target triple
|
||||
# inside a pinned Alpine container.
|
||||
#
|
||||
# Usage: dist/release/build_musl.sh <target-triple>
|
||||
set -eu
|
||||
|
||||
IMAGE="rust:1.98-alpine3.22"
|
||||
|
||||
TARGET="${1:-}"
|
||||
if [ -z "$TARGET" ]; then
|
||||
echo "usage: $0 <target-triple>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "$TARGET" in
|
||||
x86_64-unknown-linux-musl) PLATFORM="linux/amd64" ;;
|
||||
aarch64-unknown-linux-musl) PLATFORM="linux/arm64" ;;
|
||||
*)
|
||||
echo "unsupported target: $TARGET" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
WORKSPACE="$(CDPATH='' cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
|
||||
# The container appends a [patch.crates-io] section to Cargo.toml and updates
|
||||
# Cargo.lock; keep byte-exact copies so the workspace is clean afterwards.
|
||||
BACKUP_DIR="$(mktemp -d)"
|
||||
cp -p "$WORKSPACE/Cargo.toml" "$BACKUP_DIR/Cargo.toml"
|
||||
cp -p "$WORKSPACE/Cargo.lock" "$BACKUP_DIR/Cargo.lock"
|
||||
|
||||
restore_manifests() {
|
||||
cp -p "$BACKUP_DIR/Cargo.toml" "$WORKSPACE/Cargo.toml"
|
||||
cp -p "$BACKUP_DIR/Cargo.lock" "$WORKSPACE/Cargo.lock"
|
||||
rm -rf "$BACKUP_DIR"
|
||||
}
|
||||
trap restore_manifests EXIT
|
||||
|
||||
HOST_UID="$(id -u)"
|
||||
HOST_GID="$(id -g)"
|
||||
export TARGET HOST_UID HOST_GID
|
||||
|
||||
docker run --rm \
|
||||
--platform "$PLATFORM" \
|
||||
--env TARGET \
|
||||
--env HOST_UID \
|
||||
--env HOST_GID \
|
||||
--volume "$WORKSPACE:/work" \
|
||||
--workdir /work \
|
||||
"$IMAGE" \
|
||||
sh /work/dist/release/build_musl_container.sh
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env sh
|
||||
# Builds a static musl termscp binary. Runs INSIDE the Alpine container
|
||||
# started by dist/release/build_musl.sh; /work is the mounted workspace.
|
||||
#
|
||||
# Required environment: TARGET, HOST_UID, HOST_GID.
|
||||
set -eux
|
||||
|
||||
NETTLE_VERSION="3.10.1"
|
||||
GNUTLS_VERSION="3.8.13"
|
||||
PAVAO_SRC_VERSION="4.24.6"
|
||||
|
||||
cleanup() {
|
||||
chown -R "$HOST_UID:$HOST_GID" /work
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
apk add --no-cache \
|
||||
bison \
|
||||
binutils \
|
||||
build-base \
|
||||
file \
|
||||
flex \
|
||||
git \
|
||||
gnutls-dev \
|
||||
libgit2-dev \
|
||||
libgit2-static \
|
||||
libunistring-dev \
|
||||
libunistring-static \
|
||||
linux-headers \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
perl \
|
||||
perl-parse-yapp \
|
||||
pkgconf \
|
||||
python3 \
|
||||
wget \
|
||||
xz \
|
||||
zlib-dev \
|
||||
zlib-static
|
||||
|
||||
rustup target add "$TARGET"
|
||||
cargo fetch --locked
|
||||
|
||||
NATIVE_CFLAGS="-O2 -fPIC"
|
||||
if [ "$TARGET" = "aarch64-unknown-linux-musl" ]; then
|
||||
NATIVE_CFLAGS="$NATIVE_CFLAGS -mno-outline-atomics"
|
||||
fi
|
||||
export CFLAGS="$NATIVE_CFLAGS"
|
||||
|
||||
# -- static nettle (GnuTLS crypto backend); mini-gmp avoids a GMP dependency
|
||||
mkdir -p /tmp/native
|
||||
wget -q "https://ftp.gnu.org/gnu/nettle/nettle-$NETTLE_VERSION.tar.gz" \
|
||||
-O /tmp/native/nettle.tar.gz
|
||||
tar -xzf /tmp/native/nettle.tar.gz -C /tmp/native
|
||||
cd "/tmp/native/nettle-$NETTLE_VERSION"
|
||||
./configure \
|
||||
--prefix=/tmp/native/nettle \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
--disable-documentation \
|
||||
--enable-mini-gmp
|
||||
make -j"$(getconf _NPROCESSORS_ONLN)"
|
||||
make install
|
||||
|
||||
# -- static GnuTLS; every optional backend is disabled so nothing links
|
||||
# against a shared library
|
||||
wget -q "https://www.gnupg.org/ftp/gcrypt/gnutls/v3.8/gnutls-$GNUTLS_VERSION.tar.xz" \
|
||||
-O /tmp/native/gnutls.tar.xz
|
||||
tar -xf /tmp/native/gnutls.tar.xz -C /tmp/native
|
||||
cd "/tmp/native/gnutls-$GNUTLS_VERSION"
|
||||
PKG_CONFIG_PATH=/tmp/native/nettle/lib/pkgconfig \
|
||||
./configure \
|
||||
--prefix=/tmp/native/gnutls \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
--disable-doc \
|
||||
--disable-tests \
|
||||
--disable-nls \
|
||||
--disable-hardware-acceleration \
|
||||
--with-nettle-mini \
|
||||
--with-included-libtasn1 \
|
||||
--with-included-unistring \
|
||||
--without-idn \
|
||||
--without-p11-kit \
|
||||
--without-brotli \
|
||||
--without-zstd \
|
||||
--without-zlib
|
||||
make -j"$(getconf _NPROCESSORS_ONLN)"
|
||||
make install
|
||||
|
||||
# -- flatten gnutls.pc: pkg-config must hand the linker the static archives
|
||||
# directly, with no Requires.private chain to resolve
|
||||
mkdir -p /tmp/native/pkgconfig
|
||||
sed \
|
||||
-e "s#^Libs:.*#Libs: -L/tmp/native/gnutls/lib -lgnutls -latomic -L/tmp/native/nettle/lib -lhogweed -lnettle#" \
|
||||
-e "/^Requires.private:/d" \
|
||||
-e "s#^Cflags:.*#Cflags: -I/tmp/native/gnutls/include -I/tmp/native/nettle/include#" \
|
||||
/tmp/native/gnutls/lib/pkgconfig/gnutls.pc \
|
||||
> /tmp/native/pkgconfig/gnutls.pc
|
||||
|
||||
# -- pavao-src: Samba's replacement library omits two sources that musl needs
|
||||
cd /work
|
||||
PAVAO_SRC=$(find "${CARGO_HOME:-/usr/local/cargo}/registry/src" \
|
||||
-type d -name "pavao-src-$PAVAO_SRC_VERSION" -print -quit)
|
||||
test -n "$PAVAO_SRC"
|
||||
cp -R "$PAVAO_SRC" /tmp/pavao-src
|
||||
perl -0pi -e "s#( \\\"lib/replace/replace\\.c\\\",\\n)#\$1 \\\"lib/replace/closefrom.c\\\",\\n \\\"lib/replace/strptime.c\\\",\\n#" \
|
||||
/tmp/pavao-src/src/lib.rs
|
||||
|
||||
cat >> Cargo.toml <<EOF
|
||||
|
||||
[patch.crates-io]
|
||||
pavao-src = { path = "/tmp/pavao-src" }
|
||||
EOF
|
||||
cargo update -p "pavao-src@$PAVAO_SRC_VERSION"
|
||||
|
||||
export PKG_CONFIG_ALL_STATIC=1
|
||||
export PKG_CONFIG_PATH=/tmp/native/pkgconfig:/tmp/native/nettle/lib/pkgconfig:/usr/lib/pkgconfig
|
||||
export RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static"
|
||||
cargo build --locked --release --target "$TARGET" --features smb-vendored
|
||||
|
||||
# -- prove the binary is static: no interpreter, no shared libraries
|
||||
file "target/$TARGET/release/termscp"
|
||||
if readelf -l "target/$TARGET/release/termscp" > /tmp/program-headers.txt; then
|
||||
cat /tmp/program-headers.txt
|
||||
else
|
||||
status=$?
|
||||
cat /tmp/program-headers.txt
|
||||
exit "$status"
|
||||
fi
|
||||
if readelf -d "target/$TARGET/release/termscp" > /tmp/dynamic-section.txt; then
|
||||
cat /tmp/dynamic-section.txt
|
||||
else
|
||||
status=$?
|
||||
cat /tmp/dynamic-section.txt
|
||||
exit "$status"
|
||||
fi
|
||||
if grep -q INTERP /tmp/program-headers.txt; then
|
||||
exit 1
|
||||
fi
|
||||
if grep -q NEEDED /tmp/dynamic-section.txt; then
|
||||
exit 1
|
||||
fi
|
||||
Vendored
+4
@@ -4,6 +4,10 @@
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?usage: bump_version.sh <version> [date] [root]}"
|
||||
if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "invalid release version: $VERSION (expected MAJOR.MINOR.PATCH)" >&2
|
||||
exit 2
|
||||
fi
|
||||
DATE="${2:-$(date +%F)}"
|
||||
ROOT="${3:-$(git rev-parse --show-toplevel)}"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[book]
|
||||
title = "termscp"
|
||||
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV"
|
||||
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV"
|
||||
authors = ["Christian Visintin"]
|
||||
language = "en"
|
||||
src = "."
|
||||
|
||||
@@ -48,3 +48,15 @@ The following parameters can be configured:
|
||||
attributes supported by termscp are listed at
|
||||
[the ssh2-config exposed attributes](https://github.com/veeso/ssh2-config#exposed-attributes).
|
||||
See also [SSH key storage](ssh-keys.md).
|
||||
|
||||
## SSH configuration behavior
|
||||
|
||||
In either SFTP or SCP authentication pane, after you change and leave the
|
||||
`Host` field, termscp keeps the entered alias visible and fills the visible
|
||||
`Port` and `Username` fields from the matching SSH configuration entry. If the
|
||||
new Host does not match an entry, it resets Port to `22` and Username to empty.
|
||||
`HostName` never replaces the alias in the form.
|
||||
|
||||
Bookmarks retain their saved Port and User values; the SSH configuration does
|
||||
not silently override them. The configured file still provides `HostName` and
|
||||
other supported SSH options when termscp connects.
|
||||
|
||||
@@ -47,20 +47,20 @@ are two quick fixes:
|
||||
1. Re-import the official theme. After each release the official themes are
|
||||
patched, so download the updated theme from the repository and re-import it:
|
||||
|
||||
```sh
|
||||
termscp theme <theme.toml>
|
||||
```
|
||||
```sh
|
||||
termscp theme <theme.toml>
|
||||
```
|
||||
|
||||
2. Edit your theme by hand. If you use a custom theme, edit the file and add the
|
||||
missing key. The theme is located at `$CONFIG_DIR/theme.toml`, where
|
||||
`$CONFIG_DIR` is:
|
||||
|
||||
- FreeBSD/Linux: `$HOME/.config/termscp`
|
||||
- macOS: `$HOME/.config/termscp`
|
||||
- Windows: `%USERPROFILE%\.termscp`
|
||||
- FreeBSD/Linux: `$HOME/.config/termscp`
|
||||
- macOS: `$HOME/.config/termscp`
|
||||
- Windows: `%USERPROFILE%\.termscp`
|
||||
|
||||
Missing keys are reported in the CHANGELOG under `BREAKING CHANGES` for the
|
||||
version you have just installed.
|
||||
Missing keys are reported in the CHANGELOG under `BREAKING CHANGES` for the
|
||||
version you have just installed.
|
||||
|
||||
## Styles
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ documentation for termscp modules, which can instead be found on Rust Docs at
|
||||
guidelines to implement features such as file transfers and additions to the
|
||||
user interface.
|
||||
|
||||
termscp is written in Rust (edition 2024, MSRV 1.89.0). The user interface is
|
||||
termscp is written in Rust (edition 2024, MSRV 1.98.0). The user interface is
|
||||
built with [tuirealm](https://github.com/veeso/tui-realm) v3, which runs on top
|
||||
of [crossterm](https://github.com/crossterm-rs/crossterm).
|
||||
|
||||
@@ -37,8 +37,8 @@ In addition to the 3 core modules, others have been added over time:
|
||||
storage and the bookmarks.
|
||||
- **utils**: contains the utilities used by pretty much all of the project.
|
||||
|
||||
termscp supports the following protocols: SFTP, SCP, FTP/FTPS, Kube, S3, SMB and
|
||||
WebDAV.
|
||||
termscp supports the following protocols: SFTP, SCP, FTP/FTPS, Kube, S3, GCS,
|
||||
SMB and WebDAV.
|
||||
|
||||
## Activities
|
||||
|
||||
@@ -63,8 +63,8 @@ works best from different frameworks:
|
||||
more, read <https://github.com/veeso/tui-realm>.
|
||||
- **Components**: components are built around tui in order to reuse widgets. This
|
||||
is achieved through the `Component` trait, inspired by
|
||||
[React](https://reactjs.org/). Each component has its *Properties* and can have
|
||||
its *States*. Each component must handle input events, accept new properties,
|
||||
[React](https://reactjs.org/). Each component has its _Properties_ and can have
|
||||
its _States_. Each component must handle input events, accept new properties,
|
||||
and provide a method to **render** itself. This logic now lives in
|
||||
[tui-realm](https://github.com/veeso/tui-realm).
|
||||
- **Messages: an Elm-based approach**: input events are handled with an approach
|
||||
|
||||
@@ -18,6 +18,18 @@ When termscp starts without an address, it shows the authentication form. Fill
|
||||
in the protocol, address, port, username, and password, then connect. termscp
|
||||
will open the dual-pane explorer once the connection succeeds.
|
||||
|
||||
## SSH configuration precedence
|
||||
|
||||
For SFTP and SCP connections, termscp resolves CLI Username and Port values in
|
||||
this order: explicit `Username`/`Port`, SSH configuration `User`/`Port`, then
|
||||
the current OS user and Port `22`. Username and Port are resolved independently,
|
||||
so an explicit value for one does not prevent SSH configuration from supplying
|
||||
the other.
|
||||
|
||||
For example, `termscp myhost` uses the configured Port and User for `myhost`.
|
||||
`termscp alice@myhost:22` uses `alice` and `22`, regardless of the SSH
|
||||
configuration.
|
||||
|
||||
## Address argument syntax
|
||||
|
||||
The generic address argument has the following syntax:
|
||||
@@ -30,8 +42,10 @@ This syntax is convenient, and you will probably use it instead of the
|
||||
interactive form. Here are some examples.
|
||||
|
||||
Connect using the default protocol (defined in your configuration) to
|
||||
`192.168.1.31`. If the port is not provided, the default port for the selected
|
||||
protocol is used. The username is the current user's name.
|
||||
`192.168.1.31`. For SFTP and SCP, an omitted Port or Username is taken from a
|
||||
matching SSH configuration entry. If no value is configured, it falls back to
|
||||
the protocol default port or the current OS user. Other protocols use their
|
||||
default port and the current OS user.
|
||||
|
||||
```sh
|
||||
termscp 192.168.1.31
|
||||
@@ -56,7 +70,7 @@ directory `/tmp`:
|
||||
termscp scp://omar@192.168.1.31:4022:/tmp
|
||||
```
|
||||
|
||||
For protocol-specific address syntax (S3, Kube, WebDAV, and SMB), see
|
||||
For protocol-specific address syntax (S3, GCS, Kube, WebDAV, and SMB), see
|
||||
[Connection parameters](connection-parameters.md).
|
||||
|
||||
## How the password is provided
|
||||
|
||||
@@ -128,6 +128,37 @@ ways to do this.
|
||||
Your credentials are safe: termscp does not manipulate these values directly.
|
||||
They are consumed directly by the `s3` crate.
|
||||
|
||||
## Google Cloud Storage
|
||||
|
||||
termscp supports Google Cloud Storage (GCS) buckets through the Google Cloud
|
||||
Storage JSON API.
|
||||
|
||||
Authentication-form fields:
|
||||
|
||||
- Bucket name (required)
|
||||
- Endpoint (defaults to `https://storage.googleapis.com`)
|
||||
- Optional service-account JSON path
|
||||
|
||||
Leave the service-account JSON path empty to use Application Default
|
||||
Credentials (ADC). ADC can obtain credentials from
|
||||
`GOOGLE_APPLICATION_CREDENTIALS`, local gcloud ADC credentials, or the Google
|
||||
Cloud metadata service when termscp runs on Google Cloud infrastructure.
|
||||
|
||||
When a service-account JSON path is supplied, termscp reads that file when it
|
||||
connects. Bookmarks store only the path and never copy the service-account JSON
|
||||
or its private key.
|
||||
|
||||
The dedicated CLI syntax is:
|
||||
|
||||
```txt
|
||||
gcs://<bucket>[:/working/directory]
|
||||
```
|
||||
|
||||
CLI connections use ADC and the default endpoint. Use the authentication form
|
||||
or a bookmark when you need a custom endpoint or a service-account JSON path.
|
||||
The selected ADC identity or service account must have IAM permissions for the
|
||||
storage operations you want to perform.
|
||||
|
||||
## SMB
|
||||
|
||||
Authentication-form fields:
|
||||
@@ -138,8 +169,28 @@ Authentication-form fields:
|
||||
- Password
|
||||
- Port (other systems only; default `445`)
|
||||
- Workgroup (other systems only)
|
||||
- SMB version (other systems only; default `Auto`)
|
||||
|
||||
On Windows the port and workgroup fields are not used.
|
||||
On Windows the port, workgroup and SMB version fields are not used: the
|
||||
operating system manages SMB protocol negotiation.
|
||||
|
||||
The SMB version field bounds the dialects offered during negotiation:
|
||||
|
||||
| Selection | Dialects negotiated |
|
||||
| --------- | ----------------------- |
|
||||
| Auto | SMB 2.0.2 through 3.1.1 |
|
||||
| SMB1 | NT1 (CIFS) only |
|
||||
| SMB2 | SMB 2.0.2 through 2.1 |
|
||||
| SMB3 | SMB 3.0 through 3.1.1 |
|
||||
|
||||
`Auto` never negotiates SMB1. SMB1 is deprecated and insecure: select it only
|
||||
for isolated legacy devices that support nothing newer. A warning is shown in
|
||||
the form while SMB1 is selected.
|
||||
|
||||
Bookmarks store the selection under the `dialect` key of the SMB table
|
||||
(`auto`, `smb1`, `smb2` or `smb3`). Bookmarks saved before this option existed
|
||||
have no `dialect` key and behave as `Auto`. The address syntax below does not
|
||||
carry a version; connections started from the command line use `Auto`.
|
||||
|
||||
Windows address syntax:
|
||||
|
||||
|
||||
@@ -44,9 +44,28 @@ Install termscp from the official repositories:
|
||||
pacman -S termscp
|
||||
```
|
||||
|
||||
## Official binaries
|
||||
|
||||
Official release binaries are published for these targets:
|
||||
|
||||
- GNU/Linux:
|
||||
- `x86_64-unknown-linux-musl`
|
||||
- `aarch64-unknown-linux-musl`
|
||||
- macOS:
|
||||
- `x86_64-apple-darwin`
|
||||
- `aarch64-apple-darwin`
|
||||
- Windows:
|
||||
- `x86_64-pc-windows-msvc`
|
||||
- `aarch64-pc-windows-msvc`
|
||||
|
||||
The Linux binaries, and the `.deb` package built from them, are statically
|
||||
linked against musl. They have no runtime dependencies: they run on any Linux
|
||||
distribution and any glibc version, with no system packages to install.
|
||||
|
||||
## Requirements
|
||||
|
||||
The following system dependencies are required to run termscp.
|
||||
The official binaries do not require these dependencies. They are needed only
|
||||
to build termscp from source, for example with `cargo install termscp`:
|
||||
|
||||
- Linux users:
|
||||
- libdbus-1
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ same time. termscp runs on Linux, macOS, FreeBSD, NetBSD, and Windows.
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple transfer protocols: SFTP, SCP, FTP and FTPS, Kube, S3, SMB,
|
||||
- Multiple transfer protocols: SFTP, SCP, FTP and FTPS, Kube, S3, GCS, SMB,
|
||||
and WebDAV.
|
||||
- Dual-pane explorer to browse and operate on both the remote and the local
|
||||
file system: create, remove, rename, search, view, and edit files.
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/shared/termscp.svg">
|
||||
|
||||
<meta property="og:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
|
||||
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
|
||||
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
|
||||
<meta property="og:image" content="https://docs.termscp.rs/og_preview.jpg">
|
||||
<meta property="og:url" content="https://docs.termscp.rs/">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
|
||||
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
|
||||
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
|
||||
<meta name="twitter:image" content="https://docs.termscp.rs/og_preview.jpg">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV.
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV.
|
||||
Basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and
|
||||
to interact with the local file system.
|
||||
|
||||
@@ -9,6 +9,9 @@ Features:
|
||||
- SCP
|
||||
- FTP and FTPS
|
||||
- S3
|
||||
- GCS
|
||||
- SMB
|
||||
- WebDAV
|
||||
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
|
||||
- Create, remove, rename, search, view and edit files
|
||||
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
|
||||
|
||||
+25
-20
@@ -35,7 +35,7 @@
|
||||
|
||||
## 关于 termscp 🖥
|
||||
|
||||
termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/SFTP/FTP/Kube/S3/WebDAV。 简而言之,它是一个带有 TUI 的终端工具,可以连接到远程服务器进行文件的检索和上传,并能够与本地文件系统进行交互。 它兼容 **Linux**、**MacOS**、**FreeBSD**、**NetBSD** 和 **Windows** 操作系统。
|
||||
termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/SFTP/FTP/Kube/S3/Google Cloud Storage (GCS)/WebDAV。 简而言之,它是一个带有 TUI 的终端工具,可以连接到远程服务器进行文件的检索和上传,并能够与本地文件系统进行交互。 它兼容 **Linux**、**MacOS**、**FreeBSD**、**NetBSD** 和 **Windows** 操作系统。
|
||||
|
||||

|
||||
|
||||
@@ -43,39 +43,40 @@ termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/S
|
||||
|
||||
## 特性 🎁
|
||||
|
||||
- 📁 支持多种通信协议
|
||||
- 📁 支持多种通信协议
|
||||
- **SFTP**
|
||||
- **SCP**
|
||||
- **FTP** 和 **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **Google Cloud Storage (GCS)**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作
|
||||
- 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作
|
||||
- 创建、删除、重命名、搜索、查看和编辑文件
|
||||
- ⭐ 通过“内置书签”和“最近连接”快速连接到您喜爱的主机
|
||||
- 📝 使用您喜欢的应用程序查看和编辑文件
|
||||
- 💁 使用 SSH 密钥和用户名/密码进行 SFTP/SCP 身份验证
|
||||
- 🐧 兼容 Windows、Linux、FreeBSD、NetBSD 和 MacOS 操作系统
|
||||
- 🐚 内置终端,可在系统上执行命令。
|
||||
- 🎨 丰富的个性化设置!
|
||||
- ⭐ 通过“内置书签”和“最近连接”快速连接到您喜爱的主机
|
||||
- 📝 使用您喜欢的应用程序查看和编辑文件
|
||||
- 💁 使用 SSH 密钥和用户名/密码进行 SFTP/SCP 身份验证
|
||||
- 🐧 兼容 Windows、Linux、FreeBSD、NetBSD 和 MacOS 操作系统
|
||||
- 🐚 内置终端,可在系统上执行命令。
|
||||
- 🎨 丰富的个性化设置!
|
||||
- 主题
|
||||
- 自定义文件浏览器格式
|
||||
- 可自定义的文本编辑器
|
||||
- 可自定义的文件排序
|
||||
- 以及许多其他参数...
|
||||
- 📫 传输大文件时通过桌面通知获得提醒
|
||||
- 🔭 与远程主机文件更改保持同步
|
||||
- 🔐 将密码保存在操作系统密钥保管库中
|
||||
- 🦀 由 Rust 提供强力支持
|
||||
- 👀 开发时更注重性能
|
||||
- 🦄 频繁的精彩更新
|
||||
- 📫 传输大文件时通过桌面通知获得提醒
|
||||
- 🔭 与远程主机文件更改保持同步
|
||||
- 🔐 将密码保存在操作系统密钥保管库中
|
||||
- 🦀 由 Rust 提供强力支持
|
||||
- 👀 开发时更注重性能
|
||||
- 🦄 频繁的精彩更新
|
||||
|
||||
---
|
||||
|
||||
## 开始 🚀
|
||||
|
||||
如果您正在考虑安装 termscp,我想对您表示感谢 💜 ! 希望您会喜欢 termscp!
|
||||
如果您正在考虑安装 termscp,我想对您表示感谢 💜 ! 希望您会喜欢 termscp!\
|
||||
如果您想为此项目做出贡献,请不要忘记查看我们的[贡献指南](CONTRIBUTING.md)。
|
||||
|
||||
如果您是 Linux、FreeBSD 或 MacOS 用户,使用以下简单的 shell 脚本即可通过单行指令在您的系统上安装 termscp:
|
||||
@@ -116,6 +117,10 @@ pacman -S termscp
|
||||
|
||||
### 依赖 ❗
|
||||
|
||||
官方 Linux 二进制文件和 `.deb` 包静态链接了 musl,**没有任何运行时依赖**:可在任意发行版、任意 glibc 版本上运行。
|
||||
|
||||
以下依赖仅在从源码构建 termscp 时需要:
|
||||
|
||||
- **Linux** 用户:
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
@@ -131,10 +136,10 @@ pacman -S termscp
|
||||
|
||||
- **Linux/FreeBSD** 用户:
|
||||
- 用 `V` **打开**文件(至少其中之一)
|
||||
- *xdg-open*
|
||||
- *gio*
|
||||
- *gnome-open*
|
||||
- *kde-open*
|
||||
- _xdg-open_
|
||||
- _gio_
|
||||
- _gnome-open_
|
||||
- _kde-open_
|
||||
- **Linux** 用户:
|
||||
- 密钥环管理器:在[用户手册](https://docs.termscp.rs/zh-CN/configuration/password-security.html#linux-密钥环)中阅读更多内容
|
||||
- **WSL** 用户
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[book]
|
||||
title = "termscp"
|
||||
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV"
|
||||
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV"
|
||||
authors = ["Christian Visintin"]
|
||||
language = "zh"
|
||||
src = "."
|
||||
|
||||
+10
-10
@@ -18,16 +18,16 @@ termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]
|
||||
|
||||
## 选项
|
||||
|
||||
| Key | 说明 |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `-b <bookmark-name>` | 将位置地址参数解析为书签名称。重复使用该标志可以打开多个书签。 |
|
||||
| `-D` | 启用 `TRACE` 日志级别(调试/详细日志)。 |
|
||||
| `-P <password>` | 从命令行提供密码。重复使用该标志可为多个远程主机提供密码;其顺序必须与地址参数一致。不推荐使用。 |
|
||||
| `-q` | 禁用日志记录。 |
|
||||
| `-T <ticks>` | 设置 UI 的 tick 间隔(以毫秒为单位)。默认值为 `10`。 |
|
||||
| `--wno-keyring` | 禁用系统 keyring 支持。 |
|
||||
| `-v` | 打印版本信息。 |
|
||||
| `--help` | 打印帮助页面。 |
|
||||
| Key | 说明 |
|
||||
| -------------------- | ------------------------------------------------ |
|
||||
| `-b <bookmark-name>` | 将位置地址参数解析为书签名称。重复使用该标志可以打开多个书签。 |
|
||||
| `-D` | 启用 `TRACE` 日志级别(调试/详细日志)。 |
|
||||
| `-P <password>` | 从命令行提供密码。重复使用该标志可为多个远程主机提供密码;其顺序必须与地址参数一致。不推荐使用。 |
|
||||
| `-q` | 禁用日志记录。 |
|
||||
| `-T <ticks>` | 设置 UI 的 tick 间隔(以毫秒为单位)。默认值为 `10`。 |
|
||||
| `--wno-keyring` | 禁用系统 keyring 支持。 |
|
||||
| `-v` | 打印版本信息。 |
|
||||
| `--help` | 打印帮助页面。 |
|
||||
|
||||
不推荐使用 `-P` 选项,因为密码可能会保留在 shell 历史记录中。请参阅书签和密码安全章节,了解更安全的凭据提供方式。
|
||||
|
||||
|
||||
@@ -25,3 +25,9 @@ termscp 要求以下路径可访问:
|
||||
- **启用通知**:如果设置为 `Yes`,则会显示桌面通知。参见 [通知](notifications.md)。
|
||||
- **通知:最小传输大小**:如果传输大小大于或等于指定值,则显示传输通知。可接受的格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB`。
|
||||
- **SSH 配置路径**:连接到 SCP/SFTP 服务器时使用的 SSH 配置文件。如果留空,则不使用任何文件。你可以指定以 `~` 开头的路径来表示主目录(例如 `~/.ssh/config`)。termscp 支持的属性列于 [ssh2-config 公开的属性](https://github.com/veeso/ssh2-config#exposed-attributes)。另请参见 [SSH 密钥存储](ssh-keys.md)。
|
||||
|
||||
## SSH 配置行为
|
||||
|
||||
在 SFTP 或 SCP 的任一认证面板中,更改并离开 `Host` 字段后,termscp 会保持输入的别名可见,并使用匹配的 SSH 配置条目填充可见的 `Port` 和 `Username` 字段。如果新的 Host 不匹配任何条目,Port 会重置为 `22`,Username 会重置为空。`HostName` 永远不会替换表单中的别名。
|
||||
|
||||
书签会保留其保存的 Port 和 User 值;SSH 配置不会在幕后覆盖它们。连接时,已配置的文件仍会提供 `HostName` 和其他受支持的 SSH 选项。
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
|
||||
以下是格式化器支持的键:
|
||||
|
||||
| 键 | 说明 |
|
||||
| --------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `ATIME` | 最后访问时间(默认 `%b %d %Y %H:%M`);`EXTRA` 为时间格式(例如 `{ATIME:8:%H:%M}`) |
|
||||
| `CTIME` | 创建时间(默认 `%b %d %Y %H:%M`);`EXTRA` 为时间格式(例如 `{CTIME:8:%H:%M}`) |
|
||||
| `GROUP` | 所属组 |
|
||||
| `MTIME` | 最后修改时间(默认 `%b %d %Y %H:%M`);`EXTRA` 为时间格式(例如 `{MTIME:8:%H:%M}`) |
|
||||
| `NAME` | 文件名(如果根目录与首个祖先目录之间的文件夹长度超过 `LENGTH`,则会被省略) |
|
||||
| `PATH` | 文件绝对路径(如果根目录与首个祖先目录之间的文件夹长度超过 `LENGTH`,则会被省略) |
|
||||
| `PEX` | 文件权限(UNIX 格式) |
|
||||
| `SIZE` | 文件大小(目录省略) |
|
||||
| `SYMLINK` | 符号链接目标(如果有,`-> {FILE_PATH}`) |
|
||||
| `USER` | 所属用户 |
|
||||
| 键 | 说明 |
|
||||
| --------- | --------------------------------------------------------------- |
|
||||
| `ATIME` | 最后访问时间(默认 `%b %d %Y %H:%M`);`EXTRA` 为时间格式(例如 `{ATIME:8:%H:%M}`) |
|
||||
| `CTIME` | 创建时间(默认 `%b %d %Y %H:%M`);`EXTRA` 为时间格式(例如 `{CTIME:8:%H:%M}`) |
|
||||
| `GROUP` | 所属组 |
|
||||
| `MTIME` | 最后修改时间(默认 `%b %d %Y %H:%M`);`EXTRA` 为时间格式(例如 `{MTIME:8:%H:%M}`) |
|
||||
| `NAME` | 文件名(如果根目录与首个祖先目录之间的文件夹长度超过 `LENGTH`,则会被省略) |
|
||||
| `PATH` | 文件绝对路径(如果根目录与首个祖先目录之间的文件夹长度超过 `LENGTH`,则会被省略) |
|
||||
| `PEX` | 文件权限(UNIX 格式) |
|
||||
| `SIZE` | 文件大小(目录省略) |
|
||||
| `SYMLINK` | 符号链接目标(如果有,`-> {FILE_PATH}`) |
|
||||
| `USER` | 所属用户 |
|
||||
|
||||
## 默认格式
|
||||
|
||||
|
||||
@@ -36,17 +36,17 @@ termscp 接受以下颜色格式:
|
||||
|
||||
1. 重新导入官方主题。每次发布后,官方主题都会被修补,因此从仓库下载更新后的主题并重新导入:
|
||||
|
||||
```sh
|
||||
termscp theme <theme.toml>
|
||||
```
|
||||
```sh
|
||||
termscp theme <theme.toml>
|
||||
```
|
||||
|
||||
2. 手动编辑你的主题。如果你使用自定义主题,请编辑该文件并添加缺失的键。主题位于 `$CONFIG_DIR/theme.toml`,其中 `$CONFIG_DIR` 为:
|
||||
|
||||
- FreeBSD/Linux:`$HOME/.config/termscp`
|
||||
- macOS:`$HOME/.config/termscp`
|
||||
- Windows:`%USERPROFILE%\.termscp`
|
||||
- FreeBSD/Linux:`$HOME/.config/termscp`
|
||||
- macOS:`$HOME/.config/termscp`
|
||||
- Windows:`%USERPROFILE%\.termscp`
|
||||
|
||||
缺失的键会在你刚安装的版本的 CHANGELOG 中 `BREAKING CHANGES` 部分列出。
|
||||
缺失的键会在你刚安装的版本的 CHANGELOG 中 `BREAKING CHANGES` 部分列出。
|
||||
|
||||
## 样式
|
||||
|
||||
@@ -54,44 +54,44 @@ termscp 接受以下颜色格式:
|
||||
|
||||
### 认证页面
|
||||
|
||||
| 键 | 说明 |
|
||||
| ---------------- | -------------------------- |
|
||||
| `auth_address` | IP 地址输入框的颜色 |
|
||||
| `auth_bookmarks` | 书签面板的颜色 |
|
||||
| `auth_password` | 密码输入框的颜色 |
|
||||
| `auth_port` | 端口号输入框的颜色 |
|
||||
| `auth_protocol` | 协议单选框组的颜色 |
|
||||
| `auth_recents` | 最近记录面板的颜色 |
|
||||
| `auth_username` | 用户名输入框的颜色 |
|
||||
| 键 | 说明 |
|
||||
| ---------------- | ----------- |
|
||||
| `auth_address` | IP 地址输入框的颜色 |
|
||||
| `auth_bookmarks` | 书签面板的颜色 |
|
||||
| `auth_password` | 密码输入框的颜色 |
|
||||
| `auth_port` | 端口号输入框的颜色 |
|
||||
| `auth_protocol` | 协议单选框组的颜色 |
|
||||
| `auth_recents` | 最近记录面板的颜色 |
|
||||
| `auth_username` | 用户名输入框的颜色 |
|
||||
|
||||
### 传输页面
|
||||
|
||||
| 键 | 说明 |
|
||||
| -------------------------------------- | -------------------------------------------------- |
|
||||
| `transfer_local_explorer_background` | 本地主机浏览器的背景色 |
|
||||
| `transfer_local_explorer_foreground` | 本地主机浏览器的前景色 |
|
||||
| `transfer_local_explorer_highlighted` | 本地主机浏览器的边框及高亮颜色 |
|
||||
| `transfer_remote_explorer_background` | 远程浏览器的背景色 |
|
||||
| `transfer_remote_explorer_foreground` | 远程浏览器的前景色 |
|
||||
| `transfer_remote_explorer_highlighted` | 远程浏览器的边框及高亮颜色 |
|
||||
| `transfer_log_background` | 日志面板的背景色 |
|
||||
| `transfer_log_window` | 日志面板的窗口颜色 |
|
||||
| `transfer_progress_bar_partial` | 部分进度条的颜色 |
|
||||
| `transfer_progress_bar_total` | 总进度条的颜色 |
|
||||
| `transfer_status_hidden` | 状态栏 "hidden" 标签的颜色 |
|
||||
| 键 | 说明 |
|
||||
| -------------------------------------- | ------------------------------- |
|
||||
| `transfer_local_explorer_background` | 本地主机浏览器的背景色 |
|
||||
| `transfer_local_explorer_foreground` | 本地主机浏览器的前景色 |
|
||||
| `transfer_local_explorer_highlighted` | 本地主机浏览器的边框及高亮颜色 |
|
||||
| `transfer_remote_explorer_background` | 远程浏览器的背景色 |
|
||||
| `transfer_remote_explorer_foreground` | 远程浏览器的前景色 |
|
||||
| `transfer_remote_explorer_highlighted` | 远程浏览器的边框及高亮颜色 |
|
||||
| `transfer_log_background` | 日志面板的背景色 |
|
||||
| `transfer_log_window` | 日志面板的窗口颜色 |
|
||||
| `transfer_progress_bar_partial` | 部分进度条的颜色 |
|
||||
| `transfer_progress_bar_total` | 总进度条的颜色 |
|
||||
| `transfer_status_hidden` | 状态栏 "hidden" 标签的颜色 |
|
||||
| `transfer_status_sorting` | 状态栏 "sorting" 标签的颜色;也适用于文件排序对话框 |
|
||||
| `transfer_status_sync_browsing` | 状态栏 "sync browsing" 标签的颜色 |
|
||||
| `transfer_status_sync_browsing` | 状态栏 "sync browsing" 标签的颜色 |
|
||||
|
||||
### 杂项
|
||||
|
||||
这些样式适用于应用程序的不同部分。
|
||||
|
||||
| 键 | 说明 |
|
||||
| ------------------- | -------------------------------- |
|
||||
| `misc_error_dialog` | 错误消息的颜色 |
|
||||
| `misc_info_dialog` | 信息对话框的颜色 |
|
||||
| 键 | 说明 |
|
||||
| ------------------- | ---------------- |
|
||||
| `misc_error_dialog` | 错误消息的颜色 |
|
||||
| `misc_info_dialog` | 信息对话框的颜色 |
|
||||
| `misc_input_dialog` | 输入对话框的颜色(例如复制文件) |
|
||||
| `misc_keys` | 按键文本的颜色 |
|
||||
| `misc_quit_dialog` | 退出对话框的颜色 |
|
||||
| `misc_save_dialog` | 保存对话框的颜色 |
|
||||
| `misc_warn_dialog` | 警告对话框的颜色 |
|
||||
| `misc_keys` | 按键文本的颜色 |
|
||||
| `misc_quit_dialog` | 退出对话框的颜色 |
|
||||
| `misc_save_dialog` | 保存对话框的颜色 |
|
||||
| `misc_warn_dialog` | 警告对话框的颜色 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
欢迎阅读 termscp 的开发者手册。本章不包含 termscp 各模块的文档,相关文档可以在 Rust Docs 上找到:<https://docs.rs/termscp>。本章描述 termscp 的工作原理,以及实现诸如文件传输和用户界面扩展等功能的指南。
|
||||
|
||||
termscp 使用 Rust 编写(edition 2024,MSRV 1.89.0)。用户界面使用 [tuirealm](https://github.com/veeso/tui-realm) v3 构建,它运行在 [crossterm](https://github.com/crossterm-rs/crossterm) 之上。
|
||||
termscp 使用 Rust 编写(edition 2024,MSRV 1.98.0)。用户界面使用 [tuirealm](https://github.com/veeso/tui-realm) v3 构建,它运行在 [crossterm](https://github.com/crossterm-rs/crossterm) 之上。
|
||||
|
||||
## termscp 的工作原理
|
||||
|
||||
@@ -20,7 +20,7 @@ termscp 基本上由 3 个核心模块组成:
|
||||
- **system**:提供与配置、ssh 密钥存储和书签交互的方式。
|
||||
- **utils**:包含几乎整个项目都会使用的工具。
|
||||
|
||||
termscp 支持以下协议:SFTP、SCP、FTP/FTPS、Kube、S3、SMB 和 WebDAV。
|
||||
termscp 支持以下协议:SFTP、SCP、FTP/FTPS、Kube、S3、GCS、SMB 和 WebDAV。
|
||||
|
||||
## Activities
|
||||
|
||||
@@ -30,7 +30,7 @@ termscp 支持以下协议:SFTP、SCP、FTP/FTPS、Kube、S3、SMB 和 WebDAV
|
||||
|
||||
- **顶层的 Activities**:每个“视图”都是一个 Activity,并由 `Activity Manager` 来处理它们。这种方法受 Android 启发。它适用于具有不同视图的 ui,每个视图都有自己的组件和逻辑。Activities 与 `Context` 协作,`Context` 是用于在 activities 之间共享数据的数据持有者。
|
||||
- **Activities 显示 Applications**:每个 activity 可以显示不同的 **Applications**。一个 application 包含一个 **View**,它基本上是一个 **components** 列表,每个组件都有其属性。view 是组件的门面,同时也处理焦点,即当前处于活动状态的组件。你不能拥有多个活动组件,因此必须对此进行处理;与此同时,如果当前组件被销毁,焦点必须交还给之前处于活动状态的组件。**Application** 负责处理所有这些工作。要了解更多信息,请阅读 <https://github.com/veeso/tui-realm>。
|
||||
- **Components**:components 是围绕 tui 构建的,以便复用控件。这是通过 `Component` trait 实现的,该 trait 受 [React](https://reactjs.org/) 启发。每个组件都有其 *Properties*,并且可以拥有其 *States*。每个组件必须处理输入事件、接受新的属性,并提供一个用于**渲染**自身的方法。这一逻辑现在位于 [tui-realm](https://github.com/veeso/tui-realm) 中。
|
||||
- **Components**:components 是围绕 tui 构建的,以便复用控件。这是通过 `Component` trait 实现的,该 trait 受 [React](https://reactjs.org/) 启发。每个组件都有其 _Properties_,并且可以拥有其 _States_。每个组件必须处理输入事件、接受新的属性,并提供一个用于**渲染**自身的方法。这一逻辑现在位于 [tui-realm](https://github.com/veeso/tui-realm) 中。
|
||||
- **Messages:基于 Elm 的方法**:输入事件采用受 [Elm](https://elm-lang.org/) 启发的方法来处理。在 Elm 中,你使用三个基本函数来实现 ui:**update**、**view** 和 **init**。termscp 将 Elm update 函数的等价实现编写为一个递归函数内部的大型 match 分支,你可以在每个 activity 的 `update.rs` 文件中找到它。这个 match 分支处理组件为响应传入的输入事件而产生的消息,并促使 activity 改变其状态。
|
||||
|
||||
termscp 实现了一个名为 `Activity` 的 trait,它是 Android activity 的一个大幅精简版本。该 trait 提供以下方法:
|
||||
|
||||
@@ -12,6 +12,12 @@ termscp 可以根据你传入的参数以三种不同的方式启动。
|
||||
|
||||
当 termscp 在不带地址的情况下启动时,会显示认证表单。填写协议、地址、端口、用户名和密码,然后进行连接。连接成功后,termscp 将打开双面板浏览器。
|
||||
|
||||
## SSH 配置优先级
|
||||
|
||||
对于 SFTP 和 SCP 连接,termscp 会按以下顺序解析 CLI 的 Username 和 Port 值:显式指定的 `Username`/`Port`、SSH 配置中的 `User`/`Port`,然后是当前 OS 用户和 Port `22`。Username 和 Port 会独立解析,因此其中一个值被显式指定不会阻止 SSH 配置提供另一个值。
|
||||
|
||||
例如,`termscp myhost` 会使用为 `myhost` 配置的 Port 和 User。无论 SSH 配置为何,`termscp alice@myhost:22` 都会使用 `alice` 和 `22`。
|
||||
|
||||
## 地址参数语法
|
||||
|
||||
通用地址参数采用以下语法:
|
||||
@@ -22,7 +28,7 @@ termscp 可以根据你传入的参数以三种不同的方式启动。
|
||||
|
||||
这种语法很方便,你很可能会用它来代替交互式表单。下面是一些示例。
|
||||
|
||||
使用默认协议(在你的配置中定义)连接到 `192.168.1.31`。如果未提供端口,则使用所选协议的默认端口。用户名为当前用户的名称。
|
||||
使用默认协议(在你的配置中定义)连接到 `192.168.1.31`。对于 SFTP 和 SCP,未提供的 Port 或 Username 会从匹配的 SSH 配置条目中获取。如果没有配置相应的值,则会回退到协议默认 Port 或当前 OS 用户。其他协议会使用其默认 Port 和当前 OS 用户。
|
||||
|
||||
```sh
|
||||
termscp 192.168.1.31
|
||||
@@ -46,7 +52,7 @@ termscp scp://omar@192.168.1.31:4022
|
||||
termscp scp://omar@192.168.1.31:4022:/tmp
|
||||
```
|
||||
|
||||
有关各协议专属的地址语法(S3、Kube、WebDAV 和 SMB),请参阅[连接参数](connection-parameters.md)。
|
||||
有关各协议专属的地址语法(S3、GCS、Kube、WebDAV 和 SMB),请参阅[连接参数](connection-parameters.md)。
|
||||
|
||||
## 密码的提供方式
|
||||
|
||||
|
||||
@@ -114,6 +114,33 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
|
||||
你的凭据是安全的:termscp 不会直接操作这些值。它们由 `s3` crate 直接使用。
|
||||
|
||||
## Google Cloud Storage
|
||||
|
||||
termscp 通过 Google Cloud Storage JSON API 支持 Google Cloud Storage(GCS)存储桶。
|
||||
|
||||
认证表单字段:
|
||||
|
||||
- 存储桶名称(必填)
|
||||
- 端点(默认为 `https://storage.googleapis.com`)
|
||||
- 可选的服务账号 JSON 路径
|
||||
|
||||
将服务账号 JSON 路径留空即可使用应用默认凭据(ADC)。当 termscp 在 Google
|
||||
Cloud 基础设施上运行时,ADC 可以从 `GOOGLE_APPLICATION_CREDENTIALS`、本地
|
||||
gcloud ADC 凭据或 Google Cloud 元数据服务中获取凭据。
|
||||
|
||||
提供服务账号 JSON 路径后,termscp 会在连接时读取该文件。书签只保存该路径,
|
||||
不会复制服务账号 JSON 或其中的私钥。
|
||||
|
||||
专用的 CLI 语法如下:
|
||||
|
||||
```txt
|
||||
gcs://<bucket>[:/working/directory]
|
||||
```
|
||||
|
||||
CLI 连接使用 ADC 和默认端点。如果需要自定义端点或服务账号 JSON 路径,请使用
|
||||
认证表单或书签。所选 ADC 身份或服务账号必须拥有你想执行的存储操作所需的 IAM
|
||||
权限。
|
||||
|
||||
## SMB
|
||||
|
||||
认证表单字段:
|
||||
@@ -124,8 +151,22 @@ s3://buckethead@eu-central-1:default:/assets
|
||||
- 密码
|
||||
- 端口(仅其他系统;默认 `445`)
|
||||
- 工作组(仅其他系统)
|
||||
- SMB 版本(仅其他系统;默认 `Auto`)
|
||||
|
||||
在 Windows 上,端口和工作组字段不会被使用。
|
||||
在 Windows 上,端口、工作组和 SMB 版本字段不会被使用:SMB 协议协商由操作系统管理。
|
||||
|
||||
SMB 版本字段限定协商时可用的方言:
|
||||
|
||||
| 选项 | 协商的方言 |
|
||||
| ---- | ------------------ |
|
||||
| Auto | SMB 2.0.2 至 3.1.1 |
|
||||
| SMB1 | 仅 NT1(CIFS) |
|
||||
| SMB2 | SMB 2.0.2 至 2.1 |
|
||||
| SMB3 | SMB 3.0 至 3.1.1 |
|
||||
|
||||
`Auto` 永远不会协商 SMB1。SMB1 已弃用且不安全:仅在无法支持更新协议的隔离旧设备上选择它。选择 SMB1 时,表单中会显示警告。
|
||||
|
||||
书签使用 SMB 表中的 `dialect` 键保存所选项(`auto`、`smb1`、`smb2` 或 `smb3`)。在此选项出现之前保存的书签没有 `dialect` 键,其行为等同于 `Auto`。下方的地址语法不包含版本;从命令行发起的连接使用 `Auto`。
|
||||
|
||||
Windows 地址语法:
|
||||
|
||||
|
||||
@@ -42,9 +42,25 @@ pkgin install termscp
|
||||
pacman -S termscp
|
||||
```
|
||||
|
||||
## 官方二进制文件
|
||||
|
||||
官方发布的二进制文件支持以下目标:
|
||||
|
||||
- GNU/Linux:
|
||||
- `x86_64-unknown-linux-musl`
|
||||
- `aarch64-unknown-linux-musl`
|
||||
- macOS:
|
||||
- `x86_64-apple-darwin`
|
||||
- `aarch64-apple-darwin`
|
||||
- Windows:
|
||||
- `x86_64-pc-windows-msvc`
|
||||
- `aarch64-pc-windows-msvc`
|
||||
|
||||
Linux 二进制文件以及由其构建的 `.deb` 包都静态链接了 musl。它们没有任何运行时依赖:可在任意 Linux 发行版、任意 glibc 版本上运行,无需安装任何系统软件包。
|
||||
|
||||
## 系统要求
|
||||
|
||||
运行 termscp 需要以下系统依赖。
|
||||
官方二进制文件不需要以下依赖。它们仅在从源码构建 termscp 时才需要,例如使用 `cargo install termscp`:
|
||||
|
||||
- Linux 用户:
|
||||
- libdbus-1
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ termscp 是一款功能丰富、带有 TUI(终端用户界面)的终端文
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 支持多种传输协议:SFTP、SCP、FTP 和 FTPS、Kube、S3、SMB 以及 WebDAV。
|
||||
- 支持多种传输协议:SFTP、SCP、FTP 和 FTPS、Kube、S3、GCS、SMB 以及 WebDAV。
|
||||
- 双面板浏览器,可同时浏览并操作远程和本地文件系统:创建、删除、重命名、搜索、查看和编辑文件。
|
||||
- 书签和最近连接记录,帮助你快速重新连接到常用的主机。
|
||||
- 使用你喜爱的编辑器查看和编辑文件。
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/shared/termscp.svg">
|
||||
|
||||
<meta property="og:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
|
||||
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
|
||||
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
|
||||
<meta property="og:image" content="https://docs.termscp.rs/og_preview.jpg">
|
||||
<meta property="og:url" content="https://docs.termscp.rs/">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
|
||||
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
|
||||
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
|
||||
<meta name="twitter:image" content="https://docs.termscp.rs/og_preview.jpg">
|
||||
|
||||
@@ -2,48 +2,48 @@
|
||||
|
||||
以下按键可在文件浏览器中使用。随时按 `<H|F1>` 可打开应用内帮助。
|
||||
|
||||
| 按键 | 操作 |
|
||||
| -------------- | ---------------------------------------------------------------------- |
|
||||
| `<ESC>` | 断开与远程的连接并返回认证页面 |
|
||||
| `<BACKSPACE>` | 返回导航栈中的上一个目录 |
|
||||
| `<TAB>` | 切换当前活动的浏览器选项卡 |
|
||||
| `<RIGHT>` | 移动到远程浏览器选项卡 |
|
||||
| `<LEFT>` | 移动到本地浏览器选项卡 |
|
||||
| `<UP>` | 在所选列表中向上移动 |
|
||||
| `<DOWN>` | 在所选列表中向下移动 |
|
||||
| `<PGUP>` | 在所选列表中向上移动 8 行 |
|
||||
| `<PGDOWN>` | 在所选列表中向下移动 8 行 |
|
||||
| `<ENTER>` | 进入所选目录 |
|
||||
| `<SPACE>` | 上传或下载所选文件 |
|
||||
| `<BACKTAB>` | 在日志选项卡与浏览器之间切换 |
|
||||
| `<A>` | 切换是否显示隐藏文件 |
|
||||
| `<B>` | 选择文件的排序方式 |
|
||||
| `<C\|F5>` | 复制所选文件或目录 |
|
||||
| `<D\|F7>` | 新建目录 |
|
||||
| `<E\|F8\|DEL>` | 删除所选文件 |
|
||||
| `<F>` | 搜索文件(支持通配符匹配) |
|
||||
| `<G>` | 跳转到指定路径 |
|
||||
| `<H\|F1>` | 显示帮助 |
|
||||
| `<I>` | 显示所选文件或目录的信息 |
|
||||
| `<K>` | 创建指向当前所选条目的符号链接 |
|
||||
| `<L>` | 重新加载当前目录的内容,或清除当前选择 |
|
||||
| `<M>` | 选择一个文件 |
|
||||
| `<N>` | 使用提供的名称创建新文件 |
|
||||
| `<O\|F4>` | 在文本编辑器中编辑所选文件 |
|
||||
| `<P>` | 打开日志面板 |
|
||||
| `<Q\|F10>` | 退出 termscp |
|
||||
| `<R\|F6>` | 重命名所选文件 |
|
||||
| `<S\|F2>` | 将所选文件另存为新名称 |
|
||||
| `<T>` | 将所选路径上的更改同步到远程 |
|
||||
| `<U>` | 进入上级目录 |
|
||||
| `<V\|F3>` | 使用该文件类型的默认程序打开所选文件 |
|
||||
| `<W>` | 使用你指定的程序打开所选文件 |
|
||||
| `<X>` | 执行命令 |
|
||||
| `<Y>` | 切换同步浏览 |
|
||||
| `<Z>` | 更改文件模式 |
|
||||
| `</>` | 过滤文件(同时支持正则表达式和通配符匹配) |
|
||||
| `<CTRL+A>` | 选择所有文件 |
|
||||
| `<ALT+A>` | 取消选择所有文件 |
|
||||
| `<CTRL+C>` | 中止文件传输过程 |
|
||||
| `<CTRL+S>` | 获取所选路径的总大小 |
|
||||
| `<CTRL+T>` | 显示所有已同步的路径 |
|
||||
| 按键 | 操作 |
|
||||
| -------------- | --------------------- |
|
||||
| `<ESC>` | 断开与远程的连接并返回认证页面 |
|
||||
| `<BACKSPACE>` | 返回导航栈中的上一个目录 |
|
||||
| `<TAB>` | 切换当前活动的浏览器选项卡 |
|
||||
| `<RIGHT>` | 移动到远程浏览器选项卡 |
|
||||
| `<LEFT>` | 移动到本地浏览器选项卡 |
|
||||
| `<UP>` | 在所选列表中向上移动 |
|
||||
| `<DOWN>` | 在所选列表中向下移动 |
|
||||
| `<PGUP>` | 在所选列表中向上移动 8 行 |
|
||||
| `<PGDOWN>` | 在所选列表中向下移动 8 行 |
|
||||
| `<ENTER>` | 进入所选目录 |
|
||||
| `<SPACE>` | 上传或下载所选文件 |
|
||||
| `<BACKTAB>` | 在日志选项卡与浏览器之间切换 |
|
||||
| `<A>` | 切换是否显示隐藏文件 |
|
||||
| `<B>` | 选择文件的排序方式 |
|
||||
| `<C\|F5>` | 复制所选文件或目录 |
|
||||
| `<D\|F7>` | 新建目录 |
|
||||
| `<E\|F8\|DEL>` | 删除所选文件 |
|
||||
| `<F>` | 搜索文件(支持通配符匹配) |
|
||||
| `<G>` | 跳转到指定路径 |
|
||||
| `<H\|F1>` | 显示帮助 |
|
||||
| `<I>` | 显示所选文件或目录的信息 |
|
||||
| `<K>` | 创建指向当前所选条目的符号链接 |
|
||||
| `<L>` | 重新加载当前目录的内容,或清除当前选择 |
|
||||
| `<M>` | 选择一个文件 |
|
||||
| `<N>` | 使用提供的名称创建新文件 |
|
||||
| `<O\|F4>` | 在文本编辑器中编辑所选文件 |
|
||||
| `<P>` | 打开日志面板 |
|
||||
| `<Q\|F10>` | 退出 termscp |
|
||||
| `<R\|F6>` | 重命名所选文件 |
|
||||
| `<S\|F2>` | 将所选文件另存为新名称 |
|
||||
| `<T>` | 将所选路径上的更改同步到远程 |
|
||||
| `<U>` | 进入上级目录 |
|
||||
| `<V\|F3>` | 使用该文件类型的默认程序打开所选文件 |
|
||||
| `<W>` | 使用你指定的程序打开所选文件 |
|
||||
| `<X>` | 执行命令 |
|
||||
| `<Y>` | 切换同步浏览 |
|
||||
| `<Z>` | 更改文件模式 |
|
||||
| `</>` | 过滤文件(同时支持正则表达式和通配符匹配) |
|
||||
| `<CTRL+A>` | 选择所有文件 |
|
||||
| `<ALT+A>` | 取消选择所有文件 |
|
||||
| `<CTRL+C>` | 中止文件传输过程 |
|
||||
| `<CTRL+S>` | 获取所选路径的总大小 |
|
||||
| `<CTRL+T>` | 显示所有已同步的路径 |
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://dprint.dev/schemas/v0.json",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80,
|
||||
"newLineKind": "lf",
|
||||
"markdown": {
|
||||
"textWrap": "maintain"
|
||||
},
|
||||
"toml": {},
|
||||
"yaml": {},
|
||||
"exec": {
|
||||
"cwd": "${configDir}",
|
||||
"commands": [
|
||||
{
|
||||
"command": "rustup run nightly rustfmt --edition 2024",
|
||||
"exts": ["rs"],
|
||||
"cacheKeyFiles": ["rustfmt.toml", "rust-toolchain.toml"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"excludes": [
|
||||
"**/target",
|
||||
"**/node_modules",
|
||||
"**/*-lock.json",
|
||||
"Cargo.lock",
|
||||
"docs/book",
|
||||
"docs/zh-CN/cli/cli.md",
|
||||
"docs/zh-CN/configuration/explorer-format.md",
|
||||
"docs/zh-CN/configuration/themes.md",
|
||||
"docs/zh-CN/usage/keyboard-shortcuts.md",
|
||||
"**/tests/fixtures"
|
||||
],
|
||||
"plugins": [
|
||||
"https://plugins.dprint.dev/markdown-0.22.1.wasm@4906fbb038977732aae0e216e4b0b957e2722f8d79282660ccb36d27dd051f17",
|
||||
"https://plugins.dprint.dev/toml-0.7.0.wasm@0126c8112691542d30b52a639076ecc83e07bace877638cee7c6915fd36b8629",
|
||||
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm@40a2fdda7040317eb1b23520f3a00769a5571eedb049c4ca9175c1b9eeba01ae",
|
||||
"https://plugins.dprint.dev/dprint/exec-0.6.2.json@df98f54ffd3092b8a841aedd6d098a2651f16d0a796a40535774f1a8b4b9d463"
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version = "1.1.1",
|
||||
[string]$Version = "1.2.0",
|
||||
[string]$InstallDir = "$env:LOCALAPPDATA\Programs\termscp",
|
||||
[Alias("Yes")]
|
||||
[switch]$Force
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
# -f, -y, --force, --yes
|
||||
# Skip the confirmation prompt during installation
|
||||
|
||||
TERMSCP_VERSION="1.1.1"
|
||||
TERMSCP_VERSION="1.2.0"
|
||||
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
|
||||
DEB_URL_AMD64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_amd64.deb"
|
||||
DEB_URL_AARCH64="${GITHUB_URL}/termscp_${TERMSCP_VERSION}-1_arm64.deb"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Build everything
|
||||
[group('build')]
|
||||
build_all: build_crates
|
||||
|
||||
# Build the Rust crate
|
||||
[group('build')]
|
||||
build_crates args="":
|
||||
cargo build --workspace {{ args }}
|
||||
|
||||
# Build all Rust crates in release mode
|
||||
[group('build')]
|
||||
build_crates_release:
|
||||
just build_crates "--release"
|
||||
|
||||
# Build a release binary for a target triple
|
||||
[group('build')]
|
||||
build_release target features="":
|
||||
cargo build --locked --release --target {{ target }} {{ features }}
|
||||
|
||||
# Build a static musl Linux release binary in a pinned Alpine container
|
||||
[group('build')]
|
||||
build_musl target:
|
||||
dist/release/build_musl.sh {{ target }}
|
||||
|
||||
# Package an already-built Linux release as a Debian package
|
||||
[group('build')]
|
||||
package_deb target:
|
||||
cargo deb --no-build --target {{ target }} --features smb-vendored
|
||||
|
||||
# Update Cargo.lock; pass cargo update arguments to scope the update.
|
||||
[group('build')]
|
||||
update_lock args="":
|
||||
cargo update {{ args }}
|
||||
|
||||
# Clean build artifacts
|
||||
[group('build')]
|
||||
[confirm("Are you sure you want to clean the build artifacts?")]
|
||||
clean:
|
||||
cargo clean
|
||||
@@ -0,0 +1,45 @@
|
||||
set positional-arguments
|
||||
|
||||
# Print the unreleased changelog section to stdout (preview only), e.g. `just changelog_preview 8.1.0`
|
||||
[group('changelog')]
|
||||
changelog_preview version:
|
||||
git-cliff --config cliff.toml --unreleased --tag "v$1" --strip all
|
||||
|
||||
# Add a new entry to CHANGELOG.md from the unreleased conventional commits, e.g. `just changelog 8.1.0`
|
||||
[group('changelog')]
|
||||
changelog version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
version="$1"
|
||||
if [[ ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "invalid release version: $version (expected MAJOR.MINOR.PATCH)" >&2
|
||||
exit 2
|
||||
fi
|
||||
tag="v$version"
|
||||
section="$(git-cliff --config cliff.toml --unreleased --tag "$tag" --strip all)"
|
||||
if [ -z "$section" ]; then
|
||||
echo "No unreleased conventional commits found; nothing to add." >&2
|
||||
exit 0
|
||||
fi
|
||||
anchor="$(printf '%s' "$version" | tr -d '.')"
|
||||
secfile="$(mktemp)"
|
||||
tmp="$(mktemp)"
|
||||
printf '%s\n' "$section" > "$secfile"
|
||||
# Insert a TOC entry before the first existing version entry and the rendered
|
||||
# section before the first existing version heading, keeping the title + TOC.
|
||||
awk -v secfile="$secfile" -v ver="$version" -v anc="$anchor" '
|
||||
!toc_done && /^[[:space:]]*- \[[0-9]/ {
|
||||
print " - [" ver "](#" anc ")"
|
||||
toc_done = 1
|
||||
}
|
||||
!body_done && /^## [0-9]/ {
|
||||
while ((getline line < secfile) > 0) print line
|
||||
print ""
|
||||
body_done = 1
|
||||
}
|
||||
{ print }
|
||||
' CHANGELOG.md > "$tmp"
|
||||
rm -f "$secfile"
|
||||
mv "$tmp" CHANGELOG.md
|
||||
echo "CHANGELOG.md updated for $tag"
|
||||
just fmt CHANGELOG.md
|
||||
@@ -0,0 +1,54 @@
|
||||
alias lint := clippy
|
||||
|
||||
# Format all sources (Markdown, TOML, YAML and Rust via nightly rustfmt) with dprint
|
||||
[group('code_check')]
|
||||
fmt args="":
|
||||
dprint fmt {{ args }}
|
||||
|
||||
# Check formatting of all sources with dprint (no writes)
|
||||
[group('code_check')]
|
||||
fmt_check args="":
|
||||
dprint check {{ args }}
|
||||
|
||||
# Run clippy on all targets
|
||||
[group('code_check')]
|
||||
clippy args="":
|
||||
cargo clippy --workspace --all-targets {{ args }}
|
||||
|
||||
# Build the crate documentation, denying warnings
|
||||
[group('code_check')]
|
||||
doc args="":
|
||||
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps {{ args }}
|
||||
|
||||
# Check dependencies for advisories, licenses, bans and sources (cargo-deny)
|
||||
[group('code_check')]
|
||||
deny args="":
|
||||
cargo deny check {{ args }}
|
||||
|
||||
# Point git at the tracked .githooks directory (installs the pre-commit hook)
|
||||
[group('code_check')]
|
||||
setup_githooks:
|
||||
git config core.hooksPath .githooks
|
||||
@echo "git hooks installed: core.hooksPath = .githooks"
|
||||
|
||||
# Lint the install scripts (shellcheck; PowerShell parse when pwsh is available)
|
||||
[group('code_check')]
|
||||
check_install_scripts:
|
||||
sh -n install.sh
|
||||
sh -n dist/release/build_musl.sh
|
||||
sh -n dist/release/build_musl_container.sh
|
||||
shellcheck install.sh dist/release/build_musl.sh dist/release/build_musl_container.sh
|
||||
@if command -v pwsh >/dev/null 2>&1; then \
|
||||
pwsh -NoProfile -Command '$t = $null; $e = $null; $null = [System.Management.Automation.Language.Parser]::ParseFile("install.ps1", [ref]$t, [ref]$e); if ($e) { $e; exit 1 }'; \
|
||||
else \
|
||||
echo "pwsh not found: skipping install.ps1 parse check"; \
|
||||
fi
|
||||
|
||||
# Run all code checks. Fails if any check fails
|
||||
[group('code_check')]
|
||||
check_code:
|
||||
just fmt_check
|
||||
just clippy "-- -D warnings"
|
||||
just doc
|
||||
just deny
|
||||
just check_install_scripts
|
||||
@@ -0,0 +1,4 @@
|
||||
# Publish the termscp crate
|
||||
[group('publish')]
|
||||
publish_crate args="":
|
||||
cargo publish --locked --features smb-vendored {{ args }}
|
||||
@@ -0,0 +1,13 @@
|
||||
[group('run')]
|
||||
run *args:
|
||||
@set -m; cargo run --bin termscp -- {{ args }}
|
||||
|
||||
# Build, sign, and run the macOS CLI binary with a stable development identity
|
||||
# Job control gives smista its own foreground process group, so Ctrl-C reaches it
|
||||
# without also interrupting just.
|
||||
[group('run')]
|
||||
run_signed *args:
|
||||
@test "$(uname -s)" = "Darwin" || (echo "run_signed is only supported on macOS." >&2; exit 1)
|
||||
cargo build --bin termscp
|
||||
codesign --force --sign "${TERMSCP_CODESIGN_IDENTITY:-Apple Development}" target/debug/termscp
|
||||
@set -m; target/debug/termscp {{ args }}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Install website dependencies from the lockfile
|
||||
[group('site')]
|
||||
site_install:
|
||||
cd site && npm ci
|
||||
|
||||
# Check website formatting
|
||||
[group('site')]
|
||||
site_fmt_check:
|
||||
cd site && npm run format:check
|
||||
|
||||
# Run Astro and TypeScript checks
|
||||
[group('site')]
|
||||
site_check:
|
||||
cd site && npm run check
|
||||
|
||||
# Run website tests when the package defines them
|
||||
[group('site')]
|
||||
site_test:
|
||||
cd site && npm test --if-present
|
||||
|
||||
# Build the website
|
||||
[group('site')]
|
||||
site_build:
|
||||
cd site && npm run build
|
||||
|
||||
# Run every website validation step
|
||||
[group('site')]
|
||||
site_ci: site_fmt_check site_check site_test site_build
|
||||
@@ -0,0 +1,13 @@
|
||||
# Run all tests
|
||||
[group('test')]
|
||||
test_all: test
|
||||
|
||||
# Run the Rust test suite
|
||||
[group('test')]
|
||||
test args="":
|
||||
cargo test --workspace {{ args }}
|
||||
|
||||
# Generate an LCOV code coverage report for the Rust workspace (requires cargo-llvm-cov)
|
||||
[group('test')]
|
||||
coverage output="lcov.info":
|
||||
cargo llvm-cov --workspace --lcov --output-path {{ output }}
|
||||
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.98.0"
|
||||
components = ["clippy", "rustfmt"]
|
||||
@@ -26,6 +26,12 @@ const year = new Date().getFullYear();
|
||||
data-umami-event="docs"
|
||||
data-umami-event-location="footer">User manual</a
|
||||
>
|
||||
<a
|
||||
href="/privacy"
|
||||
class="hover:text-text"
|
||||
data-umami-event="privacy"
|
||||
data-umami-event-location="footer">Privacy</a
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
termscp v{VERSION} · © {year} Christian Visintin · Released under the MIT license.
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export const DOCS_URL = "https://docs.termscp.rs";
|
||||
export const GITHUB_URL = "https://github.com/veeso/termscp";
|
||||
export const VERSION = "1.1.1";
|
||||
export const VERSION = "1.2.0";
|
||||
|
||||
@@ -72,9 +72,12 @@ const methods = [
|
||||
Update anytime with <span class="text-green">termscp --update</span>.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-overlay">
|
||||
Linux build deps: <span class="text-text">libdbus-1</span>, <span
|
||||
Official Linux binaries and .deb packages are statically linked against <span
|
||||
class="text-text">musl</span
|
||||
>: no runtime dependencies, any distribution, any glibc version. Building
|
||||
from source still needs <span class="text-text">libdbus-1</span>, <span
|
||||
class="text-text">pkg-config</span
|
||||
>, <span class="text-text">libsmbclient</span>. More details in the <a
|
||||
> and <span class="text-text">libsmbclient</span>. More details in the <a
|
||||
href={DOCS_URL}
|
||||
class="text-blue hover:underline">docs</a
|
||||
>.
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
import Base from "../layouts/Base.astro";
|
||||
import Nav from "../components/Nav.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
|
||||
const EMAIL = "info@veeso.dev";
|
||||
---
|
||||
|
||||
<Base
|
||||
title="Privacy Policy — termscp"
|
||||
description="How termscp.rs handles your data: cookieless, privacy-first analytics with Umami, EU-hosted, and no tracking cookies."
|
||||
path="/privacy"
|
||||
>
|
||||
<Nav />
|
||||
<main class="mx-auto w-full max-w-[760px] flex-1 px-6 py-16">
|
||||
<p class="font-mono text-xs uppercase tracking-[0.12em] text-overlay">
|
||||
termscp
|
||||
</p>
|
||||
<h1 class="mt-3 text-3xl font-bold tracking-tight text-text">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p class="mt-3 font-mono text-sm text-overlay">
|
||||
Last updated: 19 June 2026
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="mt-10 flex flex-col gap-8 text-base leading-relaxed text-subtext"
|
||||
>
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">Overview</h2>
|
||||
<p>
|
||||
This website is the project page for termscp, an open-source terminal
|
||||
file transfer client released under the MIT license. We keep data
|
||||
collection to an absolute minimum: no tracking cookies, no advertising
|
||||
networks, and we never sell or share personal data. We do, however,
|
||||
process a limited amount of data that is technically unavoidable when
|
||||
you visit any website — such as your IP address and server logs
|
||||
handled by our hosting provider — together with anonymous, aggregated
|
||||
usage analytics. This policy explains what we process, why, on what
|
||||
legal basis, and the rights you have under the EU General Data
|
||||
Protection Regulation (GDPR) and Italian data protection law (D.Lgs.
|
||||
196/2003 as amended by D.Lgs. 101/2018).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">
|
||||
Data controller & contact
|
||||
</h2>
|
||||
<p>
|
||||
The data controller is <strong class="text-text"
|
||||
>veeso.dev di Christian Visintin</strong
|
||||
>, VAT no. IT03104140300, Via Antonio Marangoni 33, 33100 Udine (UD),
|
||||
Italy. For any privacy-related question or to exercise your rights,
|
||||
you can reach out at{" "}
|
||||
<a
|
||||
href={`mailto:${EMAIL}`}
|
||||
class="font-medium text-blue underline-offset-2 hover:underline"
|
||||
>{EMAIL}</a
|
||||
>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">Cookies</h2>
|
||||
<p>
|
||||
We do not use any cookies to track user behaviour on this website. No
|
||||
consent banner is shown because there is nothing to consent to: no
|
||||
profiling cookies, no third-party advertising cookies. Our analytics
|
||||
provider is cookieless (see below), so no consent is required under
|
||||
the Italian Garante's guidelines on cookies and tracking tools.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">
|
||||
Hosting & server logs
|
||||
</h2>
|
||||
<p>
|
||||
This website is hosted by Vercel Inc. (340 S Lemon Ave #4133, Walnut,
|
||||
CA 91789, USA), which acts as a data processor on our behalf. As with
|
||||
any web server, Vercel automatically processes technical data needed
|
||||
to deliver the site and keep it secure: your IP address, browser
|
||||
user-agent, requested URLs, referrer, and timestamps, recorded in
|
||||
server logs.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
<strong class="text-text">Legal basis:</strong> our legitimate interest
|
||||
(Art. 6(1)(f) GDPR) in operating the website, ensuring its security, and
|
||||
preventing abuse. These logs are kept only for as long as necessary for
|
||||
those purposes (typically up to 30 days) and are not used to profile you
|
||||
or build advertising audiences.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">Analytics with Umami</h2>
|
||||
<p>
|
||||
We use Umami to collect anonymous usage data so we can understand how
|
||||
visitors use the website and improve its design and functionality. We
|
||||
use the EU-hosted Umami Cloud service (<span class="font-mono"
|
||||
>cloud.umami.is</span
|
||||
>), where analytics data is stored on servers located in the European
|
||||
Union (Germany). The service is operated by Umami Software, Inc.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
Umami is cookieless and privacy-focused: it does not set cookies and
|
||||
does not store your IP address or any data that can directly identify
|
||||
you. It derives only aggregated, anonymous metrics (such as country,
|
||||
browser, and page views). We also track a small number of anonymous
|
||||
interaction events — for example clicks on the GitHub, crates.io, and
|
||||
documentation links — to measure interest in the project; none of
|
||||
these events contain personal data.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
<strong class="text-text">Legal basis:</strong> our legitimate interest
|
||||
(Art. 6(1)(f) GDPR) in measuring and improving the website. Because the
|
||||
data is anonymous and no cookies or device identifiers are used, no consent
|
||||
is required.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">
|
||||
International data transfers
|
||||
</h2>
|
||||
<p>
|
||||
Our analytics data is stored within the European Union. Some of our
|
||||
providers are US-based companies (Vercel Inc. and Umami Software,
|
||||
Inc.), so limited technical data may be processed outside the European
|
||||
Economic Area. Where this happens, transfers are protected by
|
||||
appropriate safeguards under Chapter V GDPR — namely the EU–US Data
|
||||
Privacy Framework and/or the European Commission's Standard
|
||||
Contractual Clauses, together with the relevant data processing
|
||||
agreements.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">Your rights</h2>
|
||||
<p>
|
||||
Under the GDPR you have the right to access your personal data, and to
|
||||
request its rectification, erasure, or restriction, as well as the
|
||||
right to object to processing and the right to data portability. Note
|
||||
that the analytics data we hold is anonymous and cannot be linked back
|
||||
to you, so for that data we may be unable to identify you in order to
|
||||
act on a request.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
To exercise any right, contact us at{" "}
|
||||
<a
|
||||
href={`mailto:${EMAIL}`}
|
||||
class="font-medium text-blue underline-offset-2 hover:underline"
|
||||
>{EMAIL}</a
|
||||
>. You also have the right to lodge a complaint with the Italian
|
||||
supervisory authority, the{" "}
|
||||
<a
|
||||
href="https://www.garanteprivacy.it"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="font-medium text-blue underline-offset-2 hover:underline"
|
||||
>Garante per la protezione dei dati personali</a
|
||||
>, or with the data protection authority of your country of residence.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">External links</h2>
|
||||
<p>
|
||||
This website links to external services such as GitHub and crates.io.
|
||||
Once you leave this site, the privacy policy of the destination
|
||||
service applies. We are not responsible for the content or privacy
|
||||
practices of external websites.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-xl font-bold text-text">Changes to this policy</h2>
|
||||
<p>
|
||||
We may update this privacy policy from time to time. Any changes will
|
||||
be published on this page with an updated "Last updated" date.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</Base>
|
||||
+170
-11
@@ -7,12 +7,13 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
||||
use ssh2_config::SshConfig;
|
||||
|
||||
use crate::cli::{Remote, RemoteArgs};
|
||||
use crate::filetransfer::{
|
||||
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
||||
};
|
||||
use crate::host::HostError;
|
||||
use crate::host::{HostError, HostErrorType};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
@@ -23,7 +24,7 @@ use crate::ui::activities::filetransfer::FileTransferActivity;
|
||||
use crate::ui::activities::setup::SetupActivity;
|
||||
use crate::ui::activities::{Activity, ExitReason};
|
||||
use crate::ui::context::Context;
|
||||
use crate::utils::{fmt, tty};
|
||||
use crate::utils::{fmt, ssh as ssh_utils, tty};
|
||||
|
||||
/// NextActivity identifies the next identity to run once the current has ended
|
||||
pub enum NextActivity {
|
||||
@@ -67,7 +68,19 @@ impl ActivityManager {
|
||||
};
|
||||
let error = error_config.or(error_bookmark);
|
||||
let theme_provider: ThemeProvider = Self::init_theme_provider();
|
||||
let ctx: Context = Context::new(bookmarks_client, config_client, theme_provider, error);
|
||||
let ssh_config = config_client
|
||||
.get_ssh_config()
|
||||
.map(ssh_utils::parse_ssh2_config)
|
||||
.transpose()
|
||||
.map_err(|err| HostError::from(HostErrorType::InvalidSshConfig(err)))?;
|
||||
|
||||
let ctx: Context = Context::new(
|
||||
bookmarks_client,
|
||||
config_client,
|
||||
theme_provider,
|
||||
ssh_config,
|
||||
error,
|
||||
);
|
||||
Ok(ActivityManager {
|
||||
context: Some(ctx),
|
||||
ticks,
|
||||
@@ -83,13 +96,20 @@ impl ActivityManager {
|
||||
¶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::Host(host_params) => {
|
||||
let params = apply_ssh_config_to_omitted_cli_parameters(
|
||||
host_params.file_transfer_params,
|
||||
host_params.port_explicit,
|
||||
self.context_ref()?.ssh_config(),
|
||||
);
|
||||
self.set_host_params(
|
||||
HostParams::HostBridge(HostBridgeParams::Remote(
|
||||
params.protocol,
|
||||
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
|
||||
@@ -112,7 +132,11 @@ impl ActivityManager {
|
||||
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),
|
||||
HostParams::Remote(apply_ssh_config_to_omitted_cli_parameters(
|
||||
host_params.file_transfer_params,
|
||||
host_params.port_explicit,
|
||||
self.context_ref()?.ssh_config(),
|
||||
)),
|
||||
host_params.password.as_deref(),
|
||||
),
|
||||
Remote::None => Ok(()),
|
||||
@@ -530,3 +554,138 @@ impl ActivityManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies SSH configuration values only to CLI parameters omitted by the user.
|
||||
fn apply_ssh_config_to_omitted_cli_parameters(
|
||||
mut file_transfer_params: FileTransferParams,
|
||||
port_explicit: bool,
|
||||
ssh_config: Option<&SshConfig>,
|
||||
) -> FileTransferParams {
|
||||
if !matches!(
|
||||
file_transfer_params.protocol,
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
|
||||
) {
|
||||
return file_transfer_params;
|
||||
}
|
||||
|
||||
if let ProtocolParams::Generic(params) = &mut file_transfer_params.params {
|
||||
let resolved = ssh_utils::resolve_ssh_host_params(ssh_config, params.address.as_str());
|
||||
if !port_explicit {
|
||||
params.port = resolved.port;
|
||||
}
|
||||
if params.username.is_none() {
|
||||
params.username = resolved.username;
|
||||
}
|
||||
}
|
||||
|
||||
file_transfer_params
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::apply_ssh_config_to_omitted_cli_parameters;
|
||||
use crate::filetransfer::params::GenericProtocolParams;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol, ProtocolParams};
|
||||
use crate::utils::ssh::parse_ssh2_config;
|
||||
use crate::utils::test_helpers;
|
||||
|
||||
#[test]
|
||||
fn should_apply_ssh_config_to_omitted_cli_port_and_username() {
|
||||
let config = ssh_config();
|
||||
let params = ssh_params(22, None);
|
||||
|
||||
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&config));
|
||||
let resolved = resolved.params.generic_params().unwrap();
|
||||
|
||||
assert_eq!(resolved.port, 2222);
|
||||
assert_eq!(resolved.username.as_deref(), Some("configured-user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_preserve_explicit_cli_port_over_ssh_config() {
|
||||
let config = ssh_config();
|
||||
let params = ssh_params(22, None);
|
||||
|
||||
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, true, Some(&config));
|
||||
let resolved = resolved.params.generic_params().unwrap();
|
||||
|
||||
assert_eq!(resolved.port, 22);
|
||||
assert_eq!(resolved.username.as_deref(), Some("configured-user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_preserve_explicit_cli_username_over_ssh_config() {
|
||||
let config = ssh_config();
|
||||
let params = ssh_params(22, Some("cli-user"));
|
||||
|
||||
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&config));
|
||||
let resolved = resolved.params.generic_params().unwrap();
|
||||
|
||||
assert_eq!(resolved.port, 2222);
|
||||
assert_eq!(resolved.username.as_deref(), Some("cli-user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_apply_only_omitted_cli_ssh_parameters() {
|
||||
let config = ssh_config();
|
||||
let params = ssh_params(2200, Some("cli-user"));
|
||||
|
||||
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, true, Some(&config));
|
||||
let resolved = resolved.params.generic_params().unwrap();
|
||||
|
||||
assert_eq!(resolved.port, 2200);
|
||||
assert_eq!(resolved.username.as_deref(), Some("cli-user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_default_omitted_cli_ssh_port_without_configuration() {
|
||||
let params = ssh_params(22, None);
|
||||
|
||||
let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, None);
|
||||
|
||||
assert_eq!(resolved.params.generic_params().unwrap().port, 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_non_ssh_cli_parameters_unchanged() {
|
||||
let params = FileTransferParams::new(
|
||||
FileTransferProtocol::Ftp(false),
|
||||
ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("configured-host")
|
||||
.port(21)
|
||||
.username(Some("ftp-user")),
|
||||
),
|
||||
);
|
||||
|
||||
let resolved =
|
||||
apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&ssh_config()));
|
||||
let resolved = resolved.params.generic_params().unwrap();
|
||||
|
||||
assert_eq!(resolved.port, 21);
|
||||
assert_eq!(resolved.username.as_deref(), Some("ftp-user"));
|
||||
}
|
||||
|
||||
fn ssh_params(port: u16, username: Option<&str>) -> FileTransferParams {
|
||||
FileTransferParams::new(
|
||||
FileTransferProtocol::Scp,
|
||||
ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("configured-host")
|
||||
.port(port)
|
||||
.username(username),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn ssh_config() -> ssh2_config::SshConfig {
|
||||
let config_file = test_helpers::create_sample_file_with_content(
|
||||
"Host configured-host\n Port 2222\n User configured-user\n",
|
||||
);
|
||||
|
||||
parse_ssh2_config(&config_file.path().to_string_lossy())
|
||||
.expect("test SSH configuration should parse")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ Address syntax can be:
|
||||
|
||||
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
|
||||
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
|
||||
- `gcs://<bucket>[:/working/directory]` for Google Cloud Storage (ADC)
|
||||
- `\\\\<server>[:port]\\<share>[\\path]` for SMB (on Windows)
|
||||
- `smb://[user@]<server>[:port]</share>[/path]` for SMB (on other systems)
|
||||
|
||||
|
||||
+35
-5
@@ -70,8 +70,13 @@ impl TryFrom<&Args> for RemoteArgs {
|
||||
}
|
||||
|
||||
let remote = match addr_type {
|
||||
AddrType::Address => Self::parse_remote_address(arg)
|
||||
.map(|x| Remote::Host(HostParams::new(x, password)))?,
|
||||
AddrType::Address => Self::parse_remote_address(arg).map(|parsed| {
|
||||
Remote::Host(HostParams::new(
|
||||
parsed.file_transfer_params,
|
||||
parsed.port_explicit,
|
||||
password,
|
||||
))
|
||||
})?,
|
||||
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
|
||||
};
|
||||
|
||||
@@ -99,8 +104,9 @@ impl TryFrom<&Args> for RemoteArgs {
|
||||
|
||||
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}"))
|
||||
fn parse_remote_address(remote: &str) -> Result<utils::parser::ParsedRemote, String> {
|
||||
utils::parser::parse_remote_opt_with_metadata(remote)
|
||||
.map_err(|e| format!("Bad address option: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +143,8 @@ pub struct BookmarkParams {
|
||||
pub struct HostParams {
|
||||
/// file transfer parameters
|
||||
pub file_transfer_params: FileTransferParams,
|
||||
/// Whether the address explicitly provided a port.
|
||||
pub port_explicit: bool,
|
||||
/// host password specified in arguments
|
||||
pub password: Option<String>,
|
||||
}
|
||||
@@ -151,9 +159,14 @@ impl BookmarkParams {
|
||||
}
|
||||
|
||||
impl HostParams {
|
||||
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
|
||||
pub fn new<S: AsRef<str>>(
|
||||
params: FileTransferParams,
|
||||
port_explicit: bool,
|
||||
password: Option<S>,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_transfer_params: params,
|
||||
port_explicit,
|
||||
password: password.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
@@ -179,6 +192,23 @@ mod test {
|
||||
assert_eq!(remote_args.local_dir, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_preserve_explicit_port_from_positional_remote() {
|
||||
for (remote, port_explicit) in [("scp://host", false), ("scp://host:22", true)] {
|
||||
let args = Args {
|
||||
positional: vec![remote.to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
||||
let Remote::Host(params) = remote_args.remote else {
|
||||
panic!("expected positional remote to be a host");
|
||||
};
|
||||
|
||||
assert_eq!(params.port_explicit, port_explicit, "{remote}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_make_remote_args_from_args_two_remotes() {
|
||||
let args = Args {
|
||||
|
||||
+116
-16
@@ -3,6 +3,7 @@
|
||||
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
|
||||
|
||||
mod aws_s3;
|
||||
mod gcs;
|
||||
mod kube;
|
||||
mod smb;
|
||||
|
||||
@@ -14,11 +15,12 @@ use serde::de::Error as DeError;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub use self::aws_s3::S3Params;
|
||||
pub use self::gcs::GcsParams;
|
||||
pub use self::kube::KubeParams;
|
||||
pub use self::smb::SmbParams;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams,
|
||||
SmbParams as TransferSmbParams, WebDAVProtocolParams,
|
||||
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
|
||||
ProtocolParams, SmbParams as TransferSmbParams, WebDAVProtocolParams,
|
||||
};
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
|
||||
@@ -55,6 +57,8 @@ pub struct Bookmark {
|
||||
pub kube: Option<KubeParams>,
|
||||
/// S3 params; optional. When used other fields are empty for sure
|
||||
pub s3: Option<S3Params>,
|
||||
/// Google Cloud Storage params; optional. When used other fields are empty for sure
|
||||
pub gcs: Option<GcsParams>,
|
||||
/// SMB params; optional. Extra params required for SMB protocol
|
||||
pub smb: Option<SmbParams>,
|
||||
}
|
||||
@@ -78,6 +82,7 @@ impl From<FileTransferParams> for Bookmark {
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
ProtocolParams::AwsS3(params) => Self {
|
||||
@@ -90,6 +95,7 @@ impl From<FileTransferParams> for Bookmark {
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: Some(S3Params::from(params)),
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
ProtocolParams::Kube(params) => Self {
|
||||
@@ -102,6 +108,7 @@ impl From<FileTransferParams> for Bookmark {
|
||||
local_path,
|
||||
kube: Some(KubeParams::from(params)),
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
ProtocolParams::Smb(params) => Self {
|
||||
@@ -118,6 +125,7 @@ impl From<FileTransferParams> for Bookmark {
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
},
|
||||
ProtocolParams::WebDAV(parms) => Self {
|
||||
protocol,
|
||||
@@ -129,6 +137,20 @@ impl From<FileTransferParams> for Bookmark {
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
ProtocolParams::GoogleCloudStorage(params) => Self {
|
||||
protocol,
|
||||
address: None,
|
||||
port: None,
|
||||
username: None,
|
||||
password: None,
|
||||
remote_path,
|
||||
local_path,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: Some(GcsParams::from(params)),
|
||||
smb: None,
|
||||
},
|
||||
}
|
||||
@@ -144,6 +166,14 @@ impl From<Bookmark> for FileTransferParams {
|
||||
let params = AwsS3Params::from(params);
|
||||
Self::new(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params))
|
||||
}
|
||||
FileTransferProtocol::GoogleCloudStorage => {
|
||||
let params = bookmark.gcs.unwrap_or_default();
|
||||
let params = GoogleCloudStorageParams::from(params);
|
||||
Self::new(
|
||||
FileTransferProtocol::GoogleCloudStorage,
|
||||
ProtocolParams::GoogleCloudStorage(params),
|
||||
)
|
||||
}
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
| FileTransferProtocol::Sftp => {
|
||||
@@ -161,25 +191,25 @@ impl From<Bookmark> for FileTransferParams {
|
||||
}
|
||||
#[cfg(posix)]
|
||||
FileTransferProtocol::Smb => {
|
||||
let params = TransferSmbParams::new(
|
||||
bookmark.address.unwrap_or_default(),
|
||||
bookmark.smb.clone().map(|x| x.share).unwrap_or_default(),
|
||||
)
|
||||
.port(bookmark.port.unwrap_or(445))
|
||||
.username(bookmark.username)
|
||||
.password(bookmark.password)
|
||||
.workgroup(bookmark.smb.and_then(|x| x.workgroup));
|
||||
let smb = bookmark.smb.unwrap_or_default();
|
||||
let params =
|
||||
TransferSmbParams::new(bookmark.address.unwrap_or_default(), smb.share)
|
||||
.port(bookmark.port.unwrap_or(445))
|
||||
.username(bookmark.username)
|
||||
.password(bookmark.password)
|
||||
.workgroup(smb.workgroup)
|
||||
.dialect(smb.dialect.unwrap_or_default());
|
||||
|
||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||
}
|
||||
#[cfg(win)]
|
||||
FileTransferProtocol::Smb => {
|
||||
let params = TransferSmbParams::new(
|
||||
bookmark.address.unwrap_or_default(),
|
||||
bookmark.smb.clone().map(|x| x.share).unwrap_or_default(),
|
||||
)
|
||||
.username(bookmark.username)
|
||||
.password(bookmark.password);
|
||||
let smb = bookmark.smb.unwrap_or_default();
|
||||
let params =
|
||||
TransferSmbParams::new(bookmark.address.unwrap_or_default(), smb.share)
|
||||
.username(bookmark.username)
|
||||
.password(bookmark.password)
|
||||
.dialect(smb.dialect.unwrap_or_default());
|
||||
|
||||
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
|
||||
}
|
||||
@@ -224,6 +254,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::{DEFAULT_GCS_ENDPOINT, SmbDialect};
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_default() {
|
||||
@@ -244,6 +275,7 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
};
|
||||
let recent: Bookmark = Bookmark {
|
||||
@@ -256,6 +288,7 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
};
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
@@ -348,6 +381,37 @@ mod tests {
|
||||
assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_convert_gcs_params_to_bookmark_and_back() {
|
||||
let transfer = FileTransferParams::new(
|
||||
FileTransferProtocol::GoogleCloudStorage,
|
||||
ProtocolParams::GoogleCloudStorage(
|
||||
GoogleCloudStorageParams::new("archive-bucket")
|
||||
.service_account_key(Some("/keys/archive.json")),
|
||||
),
|
||||
)
|
||||
.remote_path(Some("/backups"));
|
||||
|
||||
let bookmark = Bookmark::from(transfer);
|
||||
let gcs = bookmark.gcs.as_ref().unwrap();
|
||||
assert_eq!(gcs.bucket, "archive-bucket");
|
||||
assert_eq!(gcs.endpoint, DEFAULT_GCS_ENDPOINT);
|
||||
assert_eq!(
|
||||
gcs.service_account_key.as_deref(),
|
||||
Some("/keys/archive.json")
|
||||
);
|
||||
assert_eq!(bookmark.password, None);
|
||||
|
||||
let restored = FileTransferParams::from(bookmark);
|
||||
let params = restored.params.gcs_params().unwrap();
|
||||
assert_eq!(params.bucket_name, "archive-bucket");
|
||||
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
|
||||
assert_eq!(
|
||||
params.service_account_key.as_deref(),
|
||||
Some("/keys/archive.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookmark_from_kube_ftparams() {
|
||||
let params = ProtocolParams::Kube(KubeProtocolParams {
|
||||
@@ -388,6 +452,7 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
@@ -419,6 +484,7 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
@@ -457,6 +523,7 @@ mod tests {
|
||||
secret_access_key: Some(String::from("pluto")),
|
||||
new_path_style: Some(true),
|
||||
}),
|
||||
gcs: None,
|
||||
smb: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
@@ -497,6 +564,7 @@ mod tests {
|
||||
client_key: Some(String::from("key")),
|
||||
}),
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
@@ -533,9 +601,11 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
workgroup: Some("testone".to_string()),
|
||||
dialect: Some(SmbDialect::Smb2),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -556,6 +626,7 @@ mod tests {
|
||||
assert_eq!(smb_params.password.as_deref().unwrap(), "bar");
|
||||
assert_eq!(smb_params.username.as_deref().unwrap(), "foo");
|
||||
assert_eq!(smb_params.workgroup.as_deref().unwrap(), "testone");
|
||||
assert_eq!(smb_params.dialect, SmbDialect::Smb2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -571,9 +642,11 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
s3: None,
|
||||
kube: None,
|
||||
gcs: None,
|
||||
smb: Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
workgroup: None,
|
||||
dialect: Some(SmbDialect::Smb2),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -590,5 +663,32 @@ mod tests {
|
||||
let smb_params = params.params.smb_params().unwrap();
|
||||
assert_eq!(smb_params.address.as_str(), "localhost");
|
||||
assert_eq!(smb_params.share.as_str(), "test");
|
||||
assert_eq!(smb_params.dialect, SmbDialect::Smb2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_default_dialect_when_bookmark_has_none() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
protocol: FileTransferProtocol::Smb,
|
||||
address: Some("localhost".to_string()),
|
||||
port: Some(445),
|
||||
username: None,
|
||||
password: None,
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
workgroup: None,
|
||||
dialect: None,
|
||||
}),
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
assert_eq!(
|
||||
params.params.smb_params().unwrap().dialect,
|
||||
SmbDialect::Auto
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! ## Bookmark Google Cloud Storage Parameters
|
||||
//!
|
||||
//! Stores bookmark-specific Google Cloud Storage connection settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::{DEFAULT_GCS_ENDPOINT, GoogleCloudStorageParams};
|
||||
|
||||
fn default_gcs_endpoint() -> String {
|
||||
DEFAULT_GCS_ENDPOINT.to_string()
|
||||
}
|
||||
|
||||
/// Google Cloud Storage connection parameters stored in a bookmark.
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
pub struct GcsParams {
|
||||
/// Bucket name to open.
|
||||
pub bucket: String,
|
||||
/// Google Cloud Storage endpoint URL.
|
||||
#[serde(default = "default_gcs_endpoint")]
|
||||
pub endpoint: String,
|
||||
/// Optional path to a service-account JSON file.
|
||||
pub service_account_key: Option<String>,
|
||||
}
|
||||
|
||||
impl From<GoogleCloudStorageParams> for GcsParams {
|
||||
fn from(params: GoogleCloudStorageParams) -> Self {
|
||||
Self {
|
||||
bucket: params.bucket_name,
|
||||
endpoint: if params.endpoint.is_empty() {
|
||||
default_gcs_endpoint()
|
||||
} else {
|
||||
params.endpoint
|
||||
},
|
||||
service_account_key: params.service_account_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GcsParams> for GoogleCloudStorageParams {
|
||||
fn from(params: GcsParams) -> Self {
|
||||
GoogleCloudStorageParams::new(params.bucket)
|
||||
.endpoint(if params.endpoint.is_empty() {
|
||||
default_gcs_endpoint()
|
||||
} else {
|
||||
params.endpoint
|
||||
})
|
||||
.service_account_key(params.service_account_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_normalize_empty_endpoint() {
|
||||
let params = GoogleCloudStorageParams::from(GcsParams {
|
||||
bucket: String::from("archive-bucket"),
|
||||
endpoint: String::new(),
|
||||
service_account_key: None,
|
||||
});
|
||||
|
||||
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::filetransfer::params::SmbParams as TransferSmbParams;
|
||||
use crate::filetransfer::params::{SmbDialect, SmbParams as TransferSmbParams};
|
||||
|
||||
/// Extra Connection parameters for SMB protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
|
||||
@@ -13,6 +13,9 @@ pub struct SmbParams {
|
||||
pub share: String,
|
||||
/// Optional SMB workgroup used on POSIX platforms.
|
||||
pub workgroup: Option<String>,
|
||||
/// Requested SMB protocol family. `None` (older bookmarks) means `Auto`.
|
||||
#[serde(default)]
|
||||
pub dialect: Option<SmbDialect>,
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
@@ -21,6 +24,7 @@ impl From<TransferSmbParams> for SmbParams {
|
||||
Self {
|
||||
share: params.share,
|
||||
workgroup: params.workgroup,
|
||||
dialect: Some(params.dialect),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +35,46 @@ impl From<TransferSmbParams> for SmbParams {
|
||||
Self {
|
||||
share: params.share,
|
||||
workgroup: None,
|
||||
dialect: Some(params.dialect),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_deserialize_missing_dialect_as_none() {
|
||||
let params: SmbParams = toml::from_str("share = \"temp\"").unwrap();
|
||||
assert_eq!(params.share.as_str(), "temp");
|
||||
assert_eq!(params.dialect, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_deserialize_dialect() {
|
||||
let params: SmbParams = toml::from_str("share = \"temp\"\ndialect = \"smb1\"").unwrap();
|
||||
assert_eq!(params.dialect, Some(SmbDialect::Smb1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_round_trip_dialect() {
|
||||
let params = SmbParams {
|
||||
share: "temp".to_string(),
|
||||
workgroup: None,
|
||||
dialect: Some(SmbDialect::Smb2),
|
||||
};
|
||||
let toml_str = toml::to_string(¶ms).unwrap();
|
||||
let decoded: SmbParams = toml::from_str(&toml_str).unwrap();
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_convert_transfer_params_with_dialect() {
|
||||
let transfer = TransferSmbParams::new("localhost", "temp").dialect(SmbDialect::Smb3);
|
||||
let params = SmbParams::from(transfer);
|
||||
assert_eq!(params.dialect, Some(SmbDialect::Smb3));
|
||||
}
|
||||
}
|
||||
|
||||
+193
-4
@@ -118,7 +118,8 @@ mod tests {
|
||||
use crate::config::bookmarks::{Bookmark, KubeParams, S3Params, SmbParams, UserHosts};
|
||||
use crate::config::params::UserConfig;
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::utils::test_helpers::create_file_ioers;
|
||||
|
||||
#[test]
|
||||
@@ -200,7 +201,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
assert!(!cfg.remote.ssh_keys.contains_key("1.1.1.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -240,7 +241,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
assert!(!cfg.remote.ssh_keys.contains_key("1.1.1.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -366,7 +367,7 @@ mod tests {
|
||||
assert_eq!(host.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(host.password, None);
|
||||
// Verify bookmarks
|
||||
assert_eq!(hosts.bookmarks.len(), 6);
|
||||
assert_eq!(hosts.bookmarks.len(), 7);
|
||||
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
|
||||
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
|
||||
assert_eq!(host.port.unwrap(), 22);
|
||||
@@ -404,6 +405,20 @@ mod tests {
|
||||
assert_eq!(s3.access_key.as_deref().unwrap(), "pippo");
|
||||
assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto");
|
||||
assert_eq!(s3.new_path_style.unwrap(), true);
|
||||
// Google Cloud Storage bucket
|
||||
let host: &Bookmark = hosts.bookmarks.get("gcs-bucket").unwrap();
|
||||
assert_eq!(host.address, None);
|
||||
assert_eq!(host.port, None);
|
||||
assert_eq!(host.username, None);
|
||||
assert_eq!(host.password, None);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::GoogleCloudStorage);
|
||||
let gcs = host.gcs.as_ref().unwrap();
|
||||
assert_eq!(gcs.bucket, "archive-bucket");
|
||||
assert_eq!(gcs.endpoint, "https://storage.googleapis.com");
|
||||
assert_eq!(
|
||||
gcs.service_account_key.as_deref(),
|
||||
Some("/keys/archive.json")
|
||||
);
|
||||
// Kube pod
|
||||
let host: &Bookmark = hosts.bookmarks.get("pod").unwrap();
|
||||
assert_eq!(host.address, None);
|
||||
@@ -431,6 +446,7 @@ mod tests {
|
||||
assert_eq!(smb.share.as_str(), "temp");
|
||||
#[cfg(posix)]
|
||||
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
|
||||
assert_eq!(smb.dialect, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -461,6 +477,105 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_deserialize_gcs_bookmark_without_service_account_key() {
|
||||
let toml_file = create_gcs_adc_toml_bookmark();
|
||||
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("gcs-adc").unwrap();
|
||||
|
||||
assert_eq!(host.protocol, FileTransferProtocol::GoogleCloudStorage);
|
||||
let gcs = host.gcs.as_ref().unwrap();
|
||||
assert_eq!(gcs.bucket, "adc-bucket");
|
||||
assert_eq!(gcs.endpoint, "https://storage.googleapis.com");
|
||||
assert_eq!(gcs.service_account_key, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_deserialize_legacy_smb_bookmark_without_dialect() {
|
||||
let toml_file = create_good_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("smb").unwrap();
|
||||
let smb = host.smb.as_ref().unwrap();
|
||||
assert_eq!(smb.share.as_str(), "temp");
|
||||
assert_eq!(smb.dialect, None);
|
||||
|
||||
// Legacy bookmarks resolve to secure Auto at runtime.
|
||||
let params = FileTransferParams::from(host.clone());
|
||||
assert_eq!(
|
||||
params.params.smb_params().unwrap().dialect,
|
||||
SmbDialect::Auto
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_deserialize_smb_bookmark_with_dialect() {
|
||||
let toml_file = create_smb_dialect_toml_bookmark();
|
||||
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("smb-dialect").unwrap();
|
||||
let smb = host.smb.as_ref().unwrap();
|
||||
assert_eq!(smb.dialect, Some(SmbDialect::Smb2));
|
||||
|
||||
let params = FileTransferParams::from(host.clone());
|
||||
assert_eq!(
|
||||
params.params.smb_params().unwrap().dialect,
|
||||
SmbDialect::Smb2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_reserialize_legacy_smb_bookmark_and_reload() {
|
||||
let toml_file = create_good_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 output_file = tempfile::NamedTempFile::new().unwrap();
|
||||
let output_path = output_file.path().to_path_buf();
|
||||
serialize(
|
||||
&hosts,
|
||||
Box::new(std::fs::File::create(&output_path).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let reloaded: UserHosts =
|
||||
deserialize(Box::new(std::fs::File::open(&output_path).unwrap())).unwrap();
|
||||
let smb = reloaded.bookmarks.get("smb").unwrap().smb.as_ref().unwrap();
|
||||
assert_eq!(smb.share.as_str(), "temp");
|
||||
assert_eq!(smb.dialect, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_serialize_gcs_bookmark_fields() {
|
||||
let toml_file = create_good_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 output_file = tempfile::NamedTempFile::new().unwrap();
|
||||
let output_path = output_file.path().to_path_buf();
|
||||
serialize(
|
||||
&hosts,
|
||||
Box::new(std::fs::File::create(&output_path).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
let output = std::fs::read_to_string(output_path).unwrap();
|
||||
|
||||
assert!(output.contains("protocol = \"GCS\""));
|
||||
assert!(output.contains("bucket = \"archive-bucket\""));
|
||||
assert!(output.contains("endpoint = \"https://storage.googleapis.com\""));
|
||||
assert!(output.contains("service_account_key = \"/keys/archive.json\""));
|
||||
assert!(output.contains("directory = \"/backups\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_fail_deserialize_bookmark_with_invalid_protocol() {
|
||||
let toml_file: tempfile::NamedTempFile = create_invalid_protocol_toml_bookmarks();
|
||||
@@ -486,6 +601,7 @@ mod tests {
|
||||
local_path: None,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
@@ -501,6 +617,7 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
@@ -524,6 +641,27 @@ mod tests {
|
||||
new_path_style: None,
|
||||
}),
|
||||
kube: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("gcs-bucket"),
|
||||
Bookmark {
|
||||
address: None,
|
||||
port: None,
|
||||
protocol: FileTransferProtocol::GoogleCloudStorage,
|
||||
username: None,
|
||||
password: None,
|
||||
remote_path: Some(PathBuf::from("/backups")),
|
||||
local_path: None,
|
||||
kube: None,
|
||||
s3: None,
|
||||
gcs: Some(crate::config::bookmarks::GcsParams {
|
||||
bucket: "archive-bucket".to_string(),
|
||||
endpoint: "https://storage.googleapis.com".to_string(),
|
||||
service_account_key: Some("/keys/archive.json".to_string()),
|
||||
}),
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
@@ -539,6 +677,7 @@ mod tests {
|
||||
remote_path: None,
|
||||
local_path: None,
|
||||
s3: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
kube: Some(KubeParams {
|
||||
namespace: Some("my-namespace".to_string()),
|
||||
@@ -553,6 +692,7 @@ mod tests {
|
||||
let smb_params: Option<SmbParams> = Some(SmbParams {
|
||||
share: "test".to_string(),
|
||||
workgroup: None,
|
||||
dialect: None,
|
||||
});
|
||||
bookmarks.insert(
|
||||
String::from("smb"),
|
||||
@@ -566,6 +706,7 @@ mod tests {
|
||||
local_path: None,
|
||||
s3: None,
|
||||
kube: None,
|
||||
gcs: None,
|
||||
smb: smb_params,
|
||||
},
|
||||
);
|
||||
@@ -582,6 +723,7 @@ mod tests {
|
||||
local_path: Some(PathBuf::from("/usr")),
|
||||
s3: None,
|
||||
kube: None,
|
||||
gcs: None,
|
||||
smb: None,
|
||||
},
|
||||
);
|
||||
@@ -656,6 +798,15 @@ mod tests {
|
||||
secret_access_key = "pluto"
|
||||
new_path_style = true
|
||||
|
||||
[bookmarks.gcs-bucket]
|
||||
protocol = "GCS"
|
||||
directory = "/backups"
|
||||
|
||||
[bookmarks.gcs-bucket.gcs]
|
||||
bucket = "archive-bucket"
|
||||
endpoint = "https://storage.googleapis.com"
|
||||
service_account_key = "/keys/archive.json"
|
||||
|
||||
[bookmarks.pod]
|
||||
protocol = "KUBE"
|
||||
[bookmarks.pod.kube]
|
||||
@@ -684,6 +835,44 @@ mod tests {
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_gcs_adc_toml_bookmark() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
|
||||
[bookmarks.gcs-adc]
|
||||
protocol = "GCS"
|
||||
|
||||
[bookmarks.gcs-adc.gcs]
|
||||
bucket = "adc-bucket"
|
||||
|
||||
[recents]
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_smb_dialect_toml_bookmark() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks.smb-dialect]
|
||||
protocol = "SMB"
|
||||
address = "localhost"
|
||||
port = 445
|
||||
username = "test"
|
||||
password = "test"
|
||||
|
||||
[bookmarks.smb-dialect.smb]
|
||||
share = "temp"
|
||||
workgroup = "test"
|
||||
dialect = "smb2"
|
||||
|
||||
[recents]
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
|
||||
+8
-8
@@ -401,7 +401,7 @@ mod tests {
|
||||
assert_eq!(explorer.dirstack.len(), 2);
|
||||
assert_eq!(*explorer.dirstack.get(1).unwrap(), PathBuf::from("/dev"));
|
||||
assert_eq!(
|
||||
*explorer.dirstack.get(0).unwrap(),
|
||||
*explorer.dirstack.front().unwrap(),
|
||||
PathBuf::from("/home/omar")
|
||||
);
|
||||
}
|
||||
@@ -425,7 +425,7 @@ mod tests {
|
||||
assert!(explorer.get(100).is_none());
|
||||
//assert_eq!(explorer.count(), 6);
|
||||
// Verify (files are sorted by name)
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), ".git");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), ".git");
|
||||
// Iter files (all)
|
||||
assert_eq!(explorer.iter_files_all().count(), 6);
|
||||
// Iter files (hidden excluded) (.git, .gitignore are hidden)
|
||||
@@ -453,7 +453,7 @@ mod tests {
|
||||
]);
|
||||
explorer.sort_by(FileSorting::Name);
|
||||
// First entry should be "Cargo.lock"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), "Cargo.lock");
|
||||
// Last should be "src"
|
||||
assert_eq!(explorer.files.get(8).unwrap().name(), "src");
|
||||
}
|
||||
@@ -469,7 +469,7 @@ mod tests {
|
||||
explorer.set_files(vec![entry1, entry2]);
|
||||
explorer.sort_by(FileSorting::ModifyTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||
// Last should be "src"
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||
}
|
||||
@@ -485,7 +485,7 @@ mod tests {
|
||||
explorer.set_files(vec![entry1, entry2]);
|
||||
explorer.sort_by(FileSorting::CreationTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||
// Last should be "src"
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||
}
|
||||
@@ -501,7 +501,7 @@ mod tests {
|
||||
]);
|
||||
explorer.sort_by(FileSorting::Size);
|
||||
// Directory has size 4096
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "src");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), "src");
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||
assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md");
|
||||
}
|
||||
@@ -525,7 +525,7 @@ mod tests {
|
||||
explorer.sort_by(FileSorting::Name);
|
||||
explorer.group_dirs_by(Some(GroupDirs::First));
|
||||
// First entry should be "docs"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "docs");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), "docs");
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "src");
|
||||
// 3rd is file first for alphabetical order
|
||||
assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock");
|
||||
@@ -555,7 +555,7 @@ mod tests {
|
||||
assert_eq!(explorer.files.get(8).unwrap().name(), "docs");
|
||||
assert_eq!(explorer.files.get(9).unwrap().name(), "src");
|
||||
// first is file for alphabetical order
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
|
||||
assert_eq!(explorer.files.first().unwrap().name(), "Cargo.lock");
|
||||
// Last in files should be "README.md" (last file for alphabetical ordening)
|
||||
assert_eq!(explorer.files.get(7).unwrap().name(), "README.md");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ pub use remotefs_builder::RemoteFsBuilder;
|
||||
pub enum FileTransferProtocol {
|
||||
AwsS3,
|
||||
Ftp(bool), // Bool is for secure (true => ftps)
|
||||
GoogleCloudStorage,
|
||||
Kube,
|
||||
Scp,
|
||||
Sftp,
|
||||
@@ -37,6 +38,7 @@ impl std::fmt::Display for FileTransferProtocol {
|
||||
true => "FTPS",
|
||||
false => "FTP",
|
||||
},
|
||||
FileTransferProtocol::GoogleCloudStorage => "GCS",
|
||||
FileTransferProtocol::Kube => "KUBE",
|
||||
FileTransferProtocol::Scp => "SCP",
|
||||
FileTransferProtocol::Sftp => "SFTP",
|
||||
@@ -53,6 +55,7 @@ impl std::str::FromStr for FileTransferProtocol {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||
"GCS" => Ok(FileTransferProtocol::GoogleCloudStorage),
|
||||
"KUBE" => Ok(FileTransferProtocol::Kube),
|
||||
"S3" => Ok(FileTransferProtocol::AwsS3),
|
||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||
@@ -143,6 +146,14 @@ mod tests {
|
||||
FileTransferProtocol::from_str("s3").ok().unwrap(),
|
||||
FileTransferProtocol::AwsS3
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("GCS").ok().unwrap(),
|
||||
FileTransferProtocol::GoogleCloudStorage
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("gcs").ok().unwrap(),
|
||||
FileTransferProtocol::GoogleCloudStorage
|
||||
);
|
||||
// Error
|
||||
assert!(FileTransferProtocol::from_str("dummy").is_err());
|
||||
// To String
|
||||
@@ -161,6 +172,10 @@ mod tests {
|
||||
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
|
||||
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
||||
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
|
||||
assert_eq!(
|
||||
FileTransferProtocol::GoogleCloudStorage.to_string(),
|
||||
String::from("GCS")
|
||||
);
|
||||
assert_eq!(FileTransferProtocol::Smb.to_string(), String::from("SMB"));
|
||||
assert_eq!(
|
||||
FileTransferProtocol::WebDAV.to_string(),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! file transfer parameters
|
||||
|
||||
mod aws_s3;
|
||||
mod google_cloud_storage;
|
||||
mod kube;
|
||||
mod smb;
|
||||
mod webdav;
|
||||
@@ -10,8 +11,9 @@ mod webdav;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use self::aws_s3::AwsS3Params;
|
||||
pub use self::google_cloud_storage::{DEFAULT_GCS_ENDPOINT, GoogleCloudStorageParams};
|
||||
pub use self::kube::KubeProtocolParams;
|
||||
pub use self::smb::SmbParams;
|
||||
pub use self::smb::{SmbDialect, SmbParams};
|
||||
pub use self::webdav::WebDAVProtocolParams;
|
||||
use super::FileTransferProtocol;
|
||||
|
||||
@@ -66,6 +68,7 @@ impl FileTransferParams {
|
||||
pub enum ProtocolParams {
|
||||
Generic(GenericProtocolParams),
|
||||
AwsS3(AwsS3Params),
|
||||
GoogleCloudStorage(GoogleCloudStorageParams),
|
||||
Kube(KubeProtocolParams),
|
||||
Smb(SmbParams),
|
||||
WebDAV(WebDAVProtocolParams),
|
||||
@@ -76,6 +79,7 @@ impl ProtocolParams {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.password_missing(),
|
||||
ProtocolParams::Generic(params) => params.password_missing(),
|
||||
ProtocolParams::GoogleCloudStorage(params) => params.password_missing(),
|
||||
ProtocolParams::Kube(params) => params.password_missing(),
|
||||
ProtocolParams::Smb(params) => params.password_missing(),
|
||||
ProtocolParams::WebDAV(params) => params.password_missing(),
|
||||
@@ -87,6 +91,7 @@ impl ProtocolParams {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::Generic(params) => params.set_default_secret(secret),
|
||||
ProtocolParams::GoogleCloudStorage(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),
|
||||
@@ -97,6 +102,7 @@ impl ProtocolParams {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||
ProtocolParams::Generic(params) => params.address.clone(),
|
||||
ProtocolParams::GoogleCloudStorage(params) => params.bucket_name.clone(),
|
||||
ProtocolParams::Kube(params) => params
|
||||
.namespace
|
||||
.as_ref()
|
||||
@@ -193,6 +199,15 @@ impl ProtocolParams {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Retrieve Google Cloud Storage parameters if any.
|
||||
pub fn gcs_params(&self) -> Option<&GoogleCloudStorageParams> {
|
||||
match self {
|
||||
ProtocolParams::GoogleCloudStorage(params) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Retrieve Kube params parameters if any
|
||||
pub fn kube_params(&self) -> Option<&KubeProtocolParams> {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! ## Google Cloud Storage Parameters
|
||||
//!
|
||||
//! Defines the runtime connection parameters used to build Google Cloud
|
||||
//! Storage clients.
|
||||
|
||||
/// Google Cloud Storage's default JSON API endpoint.
|
||||
pub const DEFAULT_GCS_ENDPOINT: &str = "https://storage.googleapis.com";
|
||||
|
||||
/// Connection parameters for Google Cloud Storage.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GoogleCloudStorageParams {
|
||||
/// Target bucket name.
|
||||
pub bucket_name: String,
|
||||
/// Google Cloud Storage endpoint URL.
|
||||
pub endpoint: String,
|
||||
/// Optional path to a service-account JSON file.
|
||||
pub service_account_key: Option<String>,
|
||||
}
|
||||
|
||||
impl GoogleCloudStorageParams {
|
||||
/// Creates Google Cloud Storage parameters using the default endpoint.
|
||||
pub fn new<S: Into<String>>(bucket_name: S) -> Self {
|
||||
Self {
|
||||
bucket_name: bucket_name.into(),
|
||||
endpoint: DEFAULT_GCS_ENDPOINT.to_string(),
|
||||
service_account_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the Google Cloud Storage endpoint.
|
||||
pub fn endpoint<S: Into<String>>(mut self, endpoint: S) -> Self {
|
||||
self.endpoint = endpoint.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the optional service-account JSON file path.
|
||||
pub fn service_account_key<S: Into<String>>(mut self, path: Option<S>) -> Self {
|
||||
self.service_account_key = path.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Reports whether the protocol's default secret is missing.
|
||||
pub fn password_missing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Ignores generic password secrets because GCS uses ADC or a credential file.
|
||||
pub fn set_default_secret(&mut self, _secret: String) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_use_google_storage_default_endpoint() {
|
||||
let params = GoogleCloudStorageParams::new("my-bucket");
|
||||
|
||||
assert_eq!(params.bucket_name, "my-bucket");
|
||||
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
|
||||
assert_eq!(params.service_account_key, None);
|
||||
assert!(!params.password_missing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_override_endpoint_and_credentials_path() {
|
||||
let params = GoogleCloudStorageParams::new("my-bucket")
|
||||
.endpoint("http://127.0.0.1:4443")
|
||||
.service_account_key(Some("credentials.json"));
|
||||
|
||||
assert_eq!(params.endpoint, "http://127.0.0.1:4443");
|
||||
assert_eq!(
|
||||
params.service_account_key.as_deref(),
|
||||
Some("credentials.json")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,26 @@
|
||||
//! Defines the runtime connection parameters used to build SMB remote
|
||||
//! filesystem clients.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// SMB protocol family requested for a connection.
|
||||
///
|
||||
/// Each family maps to inclusive dialect bounds when the Unix client is built.
|
||||
/// `Auto` negotiates SMB2 or SMB3 and never falls back to SMB1.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SmbDialect {
|
||||
/// Negotiate SMB 2.0.2 through SMB 3.1.1.
|
||||
#[default]
|
||||
Auto,
|
||||
/// Force the deprecated NT1 (CIFS) dialect.
|
||||
Smb1,
|
||||
/// Negotiate SMB 2.0.2 through SMB 2.1.
|
||||
Smb2,
|
||||
/// Negotiate SMB 3.0 through SMB 3.1.1.
|
||||
Smb3,
|
||||
}
|
||||
|
||||
/// Connection parameters for SMB protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmbParams {
|
||||
@@ -20,6 +40,8 @@ pub struct SmbParams {
|
||||
#[cfg(posix)]
|
||||
/// Optional workgroup used on POSIX platforms.
|
||||
pub workgroup: Option<String>,
|
||||
/// Requested SMB protocol family. Enforced on POSIX platforms only.
|
||||
pub dialect: SmbDialect,
|
||||
}
|
||||
|
||||
// -- SMB params
|
||||
@@ -36,6 +58,7 @@ impl SmbParams {
|
||||
password: None,
|
||||
#[cfg(posix)]
|
||||
workgroup: None,
|
||||
dialect: SmbDialect::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +84,12 @@ impl SmbParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the SMB protocol family to request.
|
||||
pub fn dialect(mut self, dialect: SmbDialect) -> Self {
|
||||
self.dialect = dialect;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether a password is supposed to be required for this protocol params.
|
||||
/// The result true is returned ONLY if the supposed secret is MISSING!!!
|
||||
pub fn password_missing(&self) -> bool {
|
||||
@@ -82,7 +111,8 @@ mod test {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use super::SmbParams;
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
#[test]
|
||||
fn should_init_smb_params() {
|
||||
@@ -118,6 +148,67 @@ mod test {
|
||||
assert_eq!(params.workgroup.as_deref().unwrap(), "baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_default_dialect_to_auto() {
|
||||
assert_eq!(SmbDialect::default(), SmbDialect::Auto);
|
||||
let params = SmbParams::new("localhost", "temp");
|
||||
assert_eq!(params.dialect, SmbDialect::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_dialect() {
|
||||
let params = SmbParams::new("localhost", "temp").dialect(SmbDialect::Smb1);
|
||||
assert_eq!(params.dialect, SmbDialect::Smb1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_serialize_dialect_lowercase() {
|
||||
assert_eq!(
|
||||
toml::to_string(&Wrapper {
|
||||
dialect: SmbDialect::Auto,
|
||||
})
|
||||
.unwrap()
|
||||
.trim(),
|
||||
"dialect = \"auto\""
|
||||
);
|
||||
assert_eq!(
|
||||
toml::to_string(&Wrapper {
|
||||
dialect: SmbDialect::Smb1,
|
||||
})
|
||||
.unwrap()
|
||||
.trim(),
|
||||
"dialect = \"smb1\""
|
||||
);
|
||||
assert_eq!(
|
||||
toml::to_string(&Wrapper {
|
||||
dialect: SmbDialect::Smb2,
|
||||
})
|
||||
.unwrap()
|
||||
.trim(),
|
||||
"dialect = \"smb2\""
|
||||
);
|
||||
assert_eq!(
|
||||
toml::to_string(&Wrapper {
|
||||
dialect: SmbDialect::Smb3,
|
||||
})
|
||||
.unwrap()
|
||||
.trim(),
|
||||
"dialect = \"smb3\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_deserialize_dialect_lowercase() {
|
||||
let w: Wrapper = toml::from_str("dialect = \"smb3\"").unwrap();
|
||||
assert_eq!(w.dialect, SmbDialect::Smb3);
|
||||
assert!(toml::from_str::<Wrapper>("dialect = \"SMB3\"").is_err());
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct Wrapper {
|
||||
dialect: SmbDialect,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(win)]
|
||||
fn should_init_smb_params_with_optionals() {
|
||||
|
||||
@@ -8,26 +8,32 @@ use std::sync::Arc;
|
||||
use remotefs::RemoteFs;
|
||||
use remotefs_aws_s3::AwsS3Fs;
|
||||
use remotefs_ftp::FtpFs;
|
||||
use remotefs_gcs::credentials::service_account;
|
||||
use remotefs_gcs::{GoogleCloudStorageCredentials, GoogleCloudStorageFs};
|
||||
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||
#[cfg(smb_unix)]
|
||||
use remotefs_smb::SmbOptions;
|
||||
#[cfg(smb)]
|
||||
use remotefs_smb::{SmbCredentials, SmbFs};
|
||||
use remotefs_smb::{
|
||||
PavaoSmbCredentials as SmbCredentials, PavaoSmbFs as SmbFs, PavaoSmbOptions as SmbOptions,
|
||||
SmbDialect as RemoteSmbDialect,
|
||||
};
|
||||
#[cfg(smb_windows)]
|
||||
use remotefs_smb::{WNetSmbCredentials as SmbCredentials, WNetSmbFs as SmbFs};
|
||||
use remotefs_ssh::{
|
||||
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
|
||||
SshConfigParseRule, SshOpts,
|
||||
};
|
||||
use remotefs_webdav::WebDAVFs;
|
||||
|
||||
#[cfg(smb_unix)]
|
||||
use super::params::SmbDialect;
|
||||
#[cfg(not(smb))]
|
||||
use super::params::{AwsS3Params, GenericProtocolParams};
|
||||
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams};
|
||||
#[cfg(smb)]
|
||||
use super::params::{AwsS3Params, GenericProtocolParams, SmbParams};
|
||||
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, SmbParams};
|
||||
use super::params::{KubeProtocolParams, WebDAVProtocolParams};
|
||||
use super::{FileTransferProtocol, ProtocolParams};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::ssh as ssh_utils;
|
||||
|
||||
/// Remotefs builder
|
||||
pub struct RemoteFsBuilder;
|
||||
@@ -48,6 +54,10 @@ impl RemoteFsBuilder {
|
||||
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||
Ok(Box::new(Self::ftp_client(params, secure)))
|
||||
}
|
||||
(
|
||||
FileTransferProtocol::GoogleCloudStorage,
|
||||
ProtocolParams::GoogleCloudStorage(params),
|
||||
) => Ok(Box::new(Self::gcs_client(params)?)),
|
||||
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
|
||||
Ok(Box::new(Self::kube_client(params)?))
|
||||
}
|
||||
@@ -108,6 +118,36 @@ impl RemoteFsBuilder {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Build a Google Cloud Storage client from parameters.
|
||||
fn gcs_client(params: GoogleCloudStorageParams) -> Result<GoogleCloudStorageFs, String> {
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let mut client = match params.service_account_key {
|
||||
None => GoogleCloudStorageFs::new(params.bucket_name, &runtime),
|
||||
Some(path) => {
|
||||
let raw = std::fs::read_to_string(&path).map_err(|error| {
|
||||
format!("Unable to read GCS service-account file '{path}': {error}")
|
||||
})?;
|
||||
let key = serde_json::from_str(&raw).map_err(|error| {
|
||||
format!("Invalid GCS service-account JSON in '{path}': {error}")
|
||||
})?;
|
||||
let credentials = {
|
||||
let _guard = runtime.enter();
|
||||
service_account::Builder::new(key).build()
|
||||
}
|
||||
.map_err(|error| {
|
||||
format!("Invalid GCS service-account credentials in '{path}': {error}")
|
||||
})?;
|
||||
GoogleCloudStorageFs::with_credentials(
|
||||
params.bucket_name,
|
||||
GoogleCloudStorageCredentials::custom(credentials),
|
||||
&runtime,
|
||||
)
|
||||
}
|
||||
};
|
||||
client = client.endpoint(params.endpoint);
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Build ftp client from parameters
|
||||
fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs {
|
||||
let mut client = FtpFs::new(params.address, params.port).passive_mode();
|
||||
@@ -154,6 +194,17 @@ impl RemoteFsBuilder {
|
||||
Ok(SftpFs::russh(opts, rt))
|
||||
}
|
||||
|
||||
/// Maps the user-facing SMB family to inclusive remotefs dialect bounds.
|
||||
#[cfg(smb_unix)]
|
||||
fn smb_dialect_bounds(dialect: SmbDialect) -> (RemoteSmbDialect, RemoteSmbDialect) {
|
||||
match dialect {
|
||||
SmbDialect::Auto => (RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb311),
|
||||
SmbDialect::Smb1 => (RemoteSmbDialect::Nt1, RemoteSmbDialect::Nt1),
|
||||
SmbDialect::Smb2 => (RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb210),
|
||||
SmbDialect::Smb3 => (RemoteSmbDialect::Smb300, RemoteSmbDialect::Smb311),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(smb_unix)]
|
||||
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
|
||||
let mut credentials = SmbCredentials::default()
|
||||
@@ -170,11 +221,14 @@ impl RemoteFsBuilder {
|
||||
credentials = credentials.workgroup(workgroup);
|
||||
}
|
||||
|
||||
SmbFs::try_new(
|
||||
let (min_dialect, max_dialect) = Self::smb_dialect_bounds(params.dialect);
|
||||
SmbFs::try_new_with_dialect(
|
||||
credentials,
|
||||
SmbOptions::default()
|
||||
.one_share_per_server(true)
|
||||
.case_sensitive(false),
|
||||
min_dialect,
|
||||
max_dialect,
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("Invalid params for protocol SMB: {e}");
|
||||
@@ -193,6 +247,7 @@ impl RemoteFsBuilder {
|
||||
credentials = credentials.password(password);
|
||||
}
|
||||
|
||||
// Dialect is OS-managed on Windows.
|
||||
Ok(SmbFs::new(credentials))
|
||||
}
|
||||
|
||||
@@ -206,37 +261,9 @@ impl RemoteFsBuilder {
|
||||
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
|
||||
.ssh_agent_identity(Some(SshAgentIdentity::All))
|
||||
.port(params.port);
|
||||
// get ssh config
|
||||
let ssh_config = config_client
|
||||
.get_ssh_config()
|
||||
.and_then(|path| {
|
||||
debug!("reading ssh config at {}", path);
|
||||
ssh_utils::parse_ssh2_config(path).ok()
|
||||
})
|
||||
.map(|config| config.query(¶ms.address));
|
||||
|
||||
//* override port
|
||||
if let Some(port) = ssh_config.as_ref().and_then(|config| config.port) {
|
||||
opts = opts.port(port);
|
||||
}
|
||||
|
||||
//* get username. Case 1 provided in params
|
||||
if let Some(username) = params.username {
|
||||
opts = opts.username(username);
|
||||
} else if let Some(ssh_config) = &ssh_config {
|
||||
debug!("no username was provided, checking whether a user is set for this host");
|
||||
if let Some(username) = &ssh_config.user {
|
||||
debug!("found username from config: {username}");
|
||||
opts = opts.username(username);
|
||||
} else {
|
||||
//* case 3: use system username; can't be None
|
||||
debug!("no username was provided, using current username");
|
||||
if let Ok(username) = whoami::username() {
|
||||
opts = opts.username(username);
|
||||
}
|
||||
}
|
||||
} else if let Ok(username) = whoami::username() {
|
||||
debug!("no username was provided, using current username");
|
||||
opts = opts.username(username);
|
||||
}
|
||||
// For SSH protocols, only set password if explicitly provided and non-empty.
|
||||
@@ -277,6 +304,8 @@ mod test {
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(smb)]
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
@@ -298,6 +327,62 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_build_gcs_fs_with_application_default_credentials() {
|
||||
let params = ProtocolParams::GoogleCloudStorage(GoogleCloudStorageParams::new("my-bucket"));
|
||||
let config_client = get_config_client();
|
||||
|
||||
assert!(
|
||||
RemoteFsBuilder::build(
|
||||
FileTransferProtocol::GoogleCloudStorage,
|
||||
params,
|
||||
&config_client,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_reject_missing_gcs_service_account_file() {
|
||||
let directory = tempfile::TempDir::new().unwrap();
|
||||
let missing = directory.path().join("missing.json");
|
||||
let params = GoogleCloudStorageParams::new("my-bucket")
|
||||
.service_account_key(Some(missing.to_string_lossy().into_owned()));
|
||||
|
||||
assert!(RemoteFsBuilder::gcs_client(params).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_reject_malformed_gcs_service_account_json() {
|
||||
let file = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(file.path(), "not-json").unwrap();
|
||||
let params = GoogleCloudStorageParams::new("my-bucket")
|
||||
.service_account_key(Some(file.path().to_string_lossy().into_owned()));
|
||||
|
||||
assert!(RemoteFsBuilder::gcs_client(params).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_build_gcs_fs_with_service_account_file() {
|
||||
let file = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(
|
||||
file.path(),
|
||||
r#"{
|
||||
"type": "service_account",
|
||||
"client_email": "termscp@example.iam.gserviceaccount.com",
|
||||
"private_key_id": "test-key",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\ninvalid-test-key\n-----END PRIVATE KEY-----\n",
|
||||
"project_id": "termscp-test",
|
||||
"universe_domain": "googleapis.com"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let params = GoogleCloudStorageParams::new("my-bucket")
|
||||
.service_account_key(Some(file.path().to_string_lossy().into_owned()));
|
||||
|
||||
assert!(RemoteFsBuilder::gcs_client(params).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_build_ftp_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
@@ -354,12 +439,50 @@ mod test {
|
||||
|
||||
#[test]
|
||||
#[cfg(smb)]
|
||||
#[serial]
|
||||
fn should_build_smb_fs() {
|
||||
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
|
||||
let config_client = get_config_client();
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(smb_unix)]
|
||||
fn should_map_smb_dialect_to_bounds() {
|
||||
use remotefs_smb::SmbDialect as RemoteSmbDialect;
|
||||
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
assert_eq!(
|
||||
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Auto),
|
||||
(RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb311)
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb1),
|
||||
(RemoteSmbDialect::Nt1, RemoteSmbDialect::Nt1)
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb2),
|
||||
(RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb210)
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb3),
|
||||
(RemoteSmbDialect::Smb300, RemoteSmbDialect::Smb311)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(smb)]
|
||||
#[serial]
|
||||
fn should_build_smb_fs_with_dialect() {
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
let params =
|
||||
ProtocolParams::Smb(SmbParams::new("localhost", "share").dialect(SmbDialect::Smb1));
|
||||
let config_client = get_config_client();
|
||||
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_build_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
|
||||
@@ -36,6 +36,8 @@ pub enum HostErrorType {
|
||||
ExecutionFailed,
|
||||
#[error("Could not delete file")]
|
||||
DeleteFailed,
|
||||
#[error("Invalid SSH configuration: {0}")]
|
||||
InvalidSshConfig(String),
|
||||
#[cfg(win)]
|
||||
#[error("Not implemented")]
|
||||
NotImplemented,
|
||||
|
||||
+17
-15
@@ -602,9 +602,11 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::test_helpers::create_sample_file;
|
||||
#[cfg(posix)]
|
||||
use crate::utils::test_helpers::make_file_at;
|
||||
#[cfg(posix)]
|
||||
use crate::utils::test_helpers::make_fsentry;
|
||||
use crate::utils::test_helpers::{create_sample_file, make_file_at};
|
||||
|
||||
#[test]
|
||||
fn test_host_error_new() {
|
||||
@@ -632,13 +634,13 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(win)]
|
||||
fn test_host_localhost_new() {
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap();
|
||||
let 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();
|
||||
let mut counter: usize = 0;
|
||||
for _ in entries {
|
||||
counter = counter + 1;
|
||||
counter += 1;
|
||||
}
|
||||
assert_eq!(host.files.len(), counter);
|
||||
}
|
||||
@@ -769,7 +771,7 @@ mod tests {
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let files: Vec<File> = host.files.clone();
|
||||
// Verify files
|
||||
let file_0: &File = files.get(0).unwrap();
|
||||
let file_0: &File = files.first().unwrap();
|
||||
if file_0.name() == *"foo.txt" {
|
||||
assert!(file_0.metadata.symlink.is_none());
|
||||
} else {
|
||||
@@ -827,7 +829,7 @@ mod tests {
|
||||
let files: Vec<File> = host.files.clone();
|
||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||
// Remove file
|
||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||
assert!(host.remove(files.first().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
|
||||
@@ -836,7 +838,7 @@ mod tests {
|
||||
// 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());
|
||||
assert!(host.remove(files.first().unwrap()).is_ok());
|
||||
// Remove unexisting directory
|
||||
assert!(
|
||||
host.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
|
||||
@@ -859,22 +861,22 @@ 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
|
||||
assert_eq!(files.get(0).unwrap().name(), "foo.txt");
|
||||
assert_eq!(files.first().unwrap().name(), "foo.txt");
|
||||
// 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())
|
||||
host.rename(files.first().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");
|
||||
assert_eq!(files.first().unwrap().name(), "bar.txt");
|
||||
// Fail
|
||||
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
|
||||
assert!(
|
||||
host.rename(files.get(0).unwrap(), bad_path.as_path())
|
||||
host.rename(files.first().unwrap(), bad_path.as_path())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
@@ -939,7 +941,7 @@ mod tests {
|
||||
file2_path.push("bar.txt");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let file1_entry: File = host.files.get(0).unwrap().clone();
|
||||
let file1_entry: File = host.files.first().unwrap().clone();
|
||||
assert_eq!(file1_entry.name(), String::from("foo.txt"));
|
||||
// Copy
|
||||
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
||||
@@ -969,7 +971,7 @@ mod tests {
|
||||
let file2_path: PathBuf = PathBuf::from("bar.txt");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let file1_entry: File = host.files.get(0).unwrap().clone();
|
||||
let file1_entry: File = host.files.first().unwrap().clone();
|
||||
assert_eq!(file1_entry.name(), String::from("foo.txt"));
|
||||
// Copy
|
||||
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
||||
@@ -989,7 +991,7 @@ mod tests {
|
||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let file1_entry: File = host.files.get(0).unwrap().clone();
|
||||
let file1_entry: File = host.files.first().unwrap().clone();
|
||||
assert_eq!(file1_entry.name(), String::from("foo.txt"));
|
||||
// Copy with empty destination -> must fail and leave file untouched
|
||||
assert!(
|
||||
@@ -1022,7 +1024,7 @@ mod tests {
|
||||
dir_dest.push("test_dest_dir/");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let dir_src_entry: File = host.files.get(0).unwrap().clone();
|
||||
let dir_src_entry: File = host.files.first().unwrap().clone();
|
||||
assert_eq!(dir_src_entry.name(), String::from("test_dir"));
|
||||
// Copy
|
||||
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
||||
@@ -1052,7 +1054,7 @@ mod tests {
|
||||
let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let dir_src_entry: File = host.files.get(0).unwrap().clone();
|
||||
let dir_src_entry: File = host.files.first().unwrap().clone();
|
||||
assert_eq!(dir_src_entry.name(), String::from("test_dir"));
|
||||
// Copy
|
||||
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
||||
|
||||
+1
-7
@@ -88,13 +88,7 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
||||
// Match ticks
|
||||
run_opts.ticks = Duration::from_millis(args.ticks);
|
||||
// Remote argument
|
||||
match RemoteArgs::try_from(&args) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(remote) => {
|
||||
// Set params
|
||||
run_opts.remote = remote;
|
||||
}
|
||||
}
|
||||
run_opts.remote = RemoteArgs::try_from(&args)?;
|
||||
|
||||
// set activity based on remote state
|
||||
run_opts.task = if run_opts.remote.remote.is_none() {
|
||||
|
||||
@@ -50,6 +50,20 @@ impl Update {
|
||||
self
|
||||
}
|
||||
|
||||
/// Maps a build target triple onto the target triple used to name the
|
||||
/// official release assets.
|
||||
///
|
||||
/// Official Linux binaries are statically linked against musl, so a
|
||||
/// termscp built against glibc (for example via `cargo install`) must
|
||||
/// still download the `-musl` asset.
|
||||
fn map_release_target(target: &str) -> String {
|
||||
if target.contains("-linux-") {
|
||||
target.replace("-gnu", "-musl")
|
||||
} else {
|
||||
target.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the latest available release using the configured update options.
|
||||
pub fn upgrade(self) -> Result<UpdateStatus, UpdateError> {
|
||||
info!("Updating termscp...");
|
||||
@@ -58,6 +72,7 @@ impl Update {
|
||||
.repo_owner("veeso")
|
||||
.repo_name("termscp")
|
||||
.bin_name("termscp")
|
||||
.target(&Self::map_release_target(self_update::get_target()))
|
||||
.current_version(cargo_crate_version!())
|
||||
.no_confirm(!self.ask_confirm)
|
||||
.show_download_progress(self.progress)
|
||||
@@ -68,7 +83,8 @@ impl Update {
|
||||
}
|
||||
|
||||
/// Returns whether a new version of termscp is available
|
||||
/// In case of success returns Ok(Option<Release>), where the Option is Some(new_version);
|
||||
/// In case of success returns `Ok(Option<Release>)`, where the option is
|
||||
/// `Some(new_version)`;
|
||||
/// 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> {
|
||||
@@ -179,6 +195,38 @@ mod test {
|
||||
assert_eq!(upd.progress, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_map_linux_release_target_to_musl() {
|
||||
assert_eq!(
|
||||
Update::map_release_target("x86_64-unknown-linux-gnu"),
|
||||
"x86_64-unknown-linux-musl".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Update::map_release_target("aarch64-unknown-linux-gnu"),
|
||||
"aarch64-unknown-linux-musl".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_non_gnu_linux_release_target_unchanged() {
|
||||
assert_eq!(
|
||||
Update::map_release_target("x86_64-unknown-linux-musl"),
|
||||
"x86_64-unknown-linux-musl".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_other_platform_release_targets_unchanged() {
|
||||
assert_eq!(
|
||||
Update::map_release_target("aarch64-apple-darwin"),
|
||||
"aarch64-apple-darwin".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Update::map_release_target("x86_64-pc-windows-msvc"),
|
||||
"x86_64-pc-windows-msvc".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(
|
||||
not(all(
|
||||
|
||||
@@ -379,7 +379,9 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams};
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, DEFAULT_GCS_ENDPOINT, GenericProtocolParams, GoogleCloudStorageParams,
|
||||
};
|
||||
use crate::filetransfer::{FileTransferProtocol, ProtocolParams};
|
||||
|
||||
#[test]
|
||||
@@ -526,6 +528,58 @@ mod tests {
|
||||
assert_eq!(params.secret_access_key, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_preserve_gcs_service_account_path_for_saved_bookmarks() {
|
||||
for save_password in [true, false] {
|
||||
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();
|
||||
|
||||
client
|
||||
.add_bookmark(
|
||||
"gcs-bucket",
|
||||
make_gcs_ftparams(Some("/keys/archive.json")),
|
||||
save_password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bookmark = client.get_bookmark("gcs-bucket").unwrap();
|
||||
let params = bookmark.params.gcs_params().unwrap();
|
||||
assert_eq!(params.bucket_name, "archive-bucket");
|
||||
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
|
||||
assert_eq!(
|
||||
params.service_account_key.as_deref(),
|
||||
Some("/keys/archive.json")
|
||||
);
|
||||
assert_eq!(bookmark.password_missing(), false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_gcs_recent_without_password() {
|
||||
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();
|
||||
|
||||
client
|
||||
.add_recent(make_gcs_ftparams(Some("/keys/archive.json")).remote_path(Some("/backups")))
|
||||
.unwrap();
|
||||
|
||||
let recent_key = client.iter_recents().next().unwrap().clone();
|
||||
let recent = client.get_recent(&recent_key).unwrap();
|
||||
let params = recent.params.gcs_params().unwrap();
|
||||
assert_eq!(params.bucket_name, "archive-bucket");
|
||||
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
|
||||
assert_eq!(
|
||||
params.service_account_key.as_deref(),
|
||||
Some("/keys/archive.json")
|
||||
);
|
||||
assert_eq!(recent.password_missing(), false);
|
||||
assert_eq!(recent.remote_path.as_deref(), Some(Path::new("/backups")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_manipulate_bookmarks() {
|
||||
@@ -767,7 +821,7 @@ mod tests {
|
||||
// Limit is 2
|
||||
assert_eq!(client.iter_recents().count(), 2);
|
||||
// Check that 192.168.1.1 has been removed
|
||||
let key: String = client.iter_recents().next().unwrap().to_string();
|
||||
let key: String = client.iter_recents().next().unwrap().clone();
|
||||
assert!(matches!(
|
||||
client
|
||||
.hosts
|
||||
@@ -781,7 +835,7 @@ mod tests {
|
||||
.as_str(),
|
||||
"192.168.1.2" | "192.168.1.3"
|
||||
));
|
||||
let key: String = client.iter_recents().nth(1).unwrap().to_string();
|
||||
let key: String = client.iter_recents().nth(1).unwrap().clone();
|
||||
assert!(matches!(
|
||||
client
|
||||
.hosts
|
||||
@@ -932,13 +986,23 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn make_gcs_ftparams(service_account_key: Option<&str>) -> FileTransferParams {
|
||||
FileTransferParams::new(
|
||||
FileTransferProtocol::GoogleCloudStorage,
|
||||
ProtocolParams::GoogleCloudStorage(
|
||||
GoogleCloudStorageParams::new("archive-bucket")
|
||||
.service_account_key(service_account_key),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn ftparams_to_tup(
|
||||
params: FileTransferParams,
|
||||
) -> (String, u16, FileTransferProtocol, String, Option<String>) {
|
||||
let protocol = params.protocol;
|
||||
let p = params.params.generic_params().unwrap();
|
||||
(
|
||||
p.address.to_string(),
|
||||
p.address.clone(),
|
||||
p.port,
|
||||
protocol,
|
||||
p.username.as_ref().cloned().unwrap_or_default(),
|
||||
|
||||
@@ -532,6 +532,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_round_trip_gcs_as_default_protocol() {
|
||||
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
client.set_default_protocol(FileTransferProtocol::GoogleCloudStorage);
|
||||
|
||||
assert_eq!(
|
||||
client.get_default_protocol(),
|
||||
FileTransferProtocol::GoogleCloudStorage
|
||||
);
|
||||
assert_eq!(client.config.user_interface.default_protocol, "GCS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_show_hidden_files() {
|
||||
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||
@@ -595,7 +612,7 @@ mod tests {
|
||||
client.set_local_file_fmt(String::from("{NAME}"));
|
||||
assert_eq!(client.get_local_file_fmt().unwrap(), String::from("{NAME}"));
|
||||
// Delete
|
||||
client.set_local_file_fmt(String::from(""));
|
||||
client.set_local_file_fmt(String::new());
|
||||
assert_eq!(client.get_local_file_fmt(), None);
|
||||
}
|
||||
|
||||
@@ -613,7 +630,7 @@ mod tests {
|
||||
String::from("{NAME}")
|
||||
);
|
||||
// Delete
|
||||
client.set_remote_file_fmt(String::from(""));
|
||||
client.set_remote_file_fmt(String::new());
|
||||
assert_eq!(client.get_remote_file_fmt(), None);
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ mod tests {
|
||||
let mut f: File = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(conf_dir.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
@@ -294,7 +294,7 @@ mod test {
|
||||
);
|
||||
// unwatch
|
||||
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
assert!(!watcher.paths.contains_key(tempdir.path()));
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
@@ -315,7 +315,7 @@ mod test {
|
||||
watcher.unwatch(subdir.as_path()).unwrap().as_path(),
|
||||
Path::new(tempdir.path())
|
||||
);
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
assert!(!watcher.paths.contains_key(tempdir.path()));
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
+137
-6
@@ -31,9 +31,10 @@ 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
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_GCS: usize = 6;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 7;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 8;
|
||||
const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 9; // Keep as last
|
||||
|
||||
// remote protocol radio
|
||||
const REMOTE_RADIO_PROTOCOL_SFTP: usize = 0;
|
||||
@@ -41,9 +42,10 @@ 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
|
||||
const REMOTE_RADIO_PROTOCOL_GCS: usize = 5;
|
||||
const REMOTE_RADIO_PROTOCOL_KUBE: usize = 6;
|
||||
const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 7;
|
||||
const REMOTE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last
|
||||
|
||||
// -- components
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
@@ -74,6 +76,9 @@ pub enum Id {
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum AuthFormId {
|
||||
Address,
|
||||
GcsBucket,
|
||||
GcsEndpoint,
|
||||
GcsServiceAccountKey,
|
||||
KubeNamespace,
|
||||
KubeClusterUrl,
|
||||
KubeUsername,
|
||||
@@ -96,6 +101,10 @@ pub enum AuthFormId {
|
||||
SmbShare,
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroup,
|
||||
#[cfg(posix)]
|
||||
SmbDialect,
|
||||
#[cfg(posix)]
|
||||
SmbDialectWarning,
|
||||
Username,
|
||||
WebDAVUri,
|
||||
}
|
||||
@@ -153,6 +162,12 @@ pub enum UiAuthFormMsg {
|
||||
AddressBlurDown,
|
||||
AddressBlurUp,
|
||||
ChangeFormTab,
|
||||
GcsBucketBlurDown,
|
||||
GcsBucketBlurUp,
|
||||
GcsEndpointBlurDown,
|
||||
GcsEndpointBlurUp,
|
||||
GcsServiceAccountKeyBlurDown,
|
||||
GcsServiceAccountKeyBlurUp,
|
||||
KubeNamespaceBlurDown,
|
||||
KubeNamespaceBlurUp,
|
||||
KubeClusterUrlBlurDown,
|
||||
@@ -198,6 +213,10 @@ pub enum UiAuthFormMsg {
|
||||
SmbWorkgroupDown,
|
||||
#[cfg(posix)]
|
||||
SmbWorkgroupUp,
|
||||
#[cfg(posix)]
|
||||
SmbDialectBlurDown,
|
||||
#[cfg(posix)]
|
||||
SmbDialectBlurUp,
|
||||
UsernameBlurDown,
|
||||
UsernameBlurUp,
|
||||
WebDAVUriBlurDown,
|
||||
@@ -209,6 +228,7 @@ pub enum UiAuthFormMsg {
|
||||
enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
Gcs,
|
||||
Kube,
|
||||
Localhost,
|
||||
Smb,
|
||||
@@ -231,6 +251,18 @@ enum FormTab {
|
||||
const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION";
|
||||
const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES";
|
||||
|
||||
fn should_resolve_ssh_host_params(
|
||||
protocol: FileTransferProtocol,
|
||||
mounted_address: &str,
|
||||
address: &str,
|
||||
force: bool,
|
||||
) -> bool {
|
||||
matches!(
|
||||
protocol,
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp
|
||||
) && (force || mounted_address != address)
|
||||
}
|
||||
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
app: Application<Id, Msg, NoUserEvent>,
|
||||
@@ -244,7 +276,11 @@ pub struct AuthActivity {
|
||||
redraw: bool,
|
||||
/// Host bridge protocol
|
||||
host_bridge_protocol: HostBridgeProtocol,
|
||||
/// Last Host address applied to the Host Bridge form.
|
||||
last_host_bridge_address: String,
|
||||
last_form_tab: FormTab,
|
||||
/// Last Host address applied to the Remote form.
|
||||
last_remote_address: String,
|
||||
/// Remote file transfer protocol
|
||||
remote_protocol: FileTransferProtocol,
|
||||
context: Option<Context>,
|
||||
@@ -261,6 +297,8 @@ impl AuthActivity {
|
||||
bookmarks_list: Vec::new(),
|
||||
exit_reason: None,
|
||||
last_form_tab: FormTab::Remote,
|
||||
last_host_bridge_address: String::new(),
|
||||
last_remote_address: String::new(),
|
||||
recents_list: Vec::new(),
|
||||
redraw: true,
|
||||
host_bridge_protocol: HostBridgeProtocol::Localhost,
|
||||
@@ -301,6 +339,24 @@ impl AuthActivity {
|
||||
Self::file_transfer_protocol_input_mask(self.remote_protocol)
|
||||
}
|
||||
|
||||
fn set_remote_protocol(&mut self, protocol: FileTransferProtocol) {
|
||||
self.remote_protocol = protocol;
|
||||
}
|
||||
|
||||
fn last_mounted_address(&self, form_tab: FormTab) -> &str {
|
||||
match form_tab {
|
||||
FormTab::HostBridge => self.last_host_bridge_address.as_str(),
|
||||
FormTab::Remote => self.last_remote_address.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_last_mounted_address(&mut self, form_tab: FormTab, address: &str) {
|
||||
match form_tab {
|
||||
FormTab::HostBridge => self.last_host_bridge_address = address.to_string(),
|
||||
FormTab::Remote => self.last_remote_address = address.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current input mask to show
|
||||
fn host_bridge_input_mask(&self) -> InputMask {
|
||||
match self.host_bridge_protocol {
|
||||
@@ -315,6 +371,7 @@ impl AuthActivity {
|
||||
fn file_transfer_protocol_input_mask(protocol: FileTransferProtocol) -> InputMask {
|
||||
match protocol {
|
||||
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
|
||||
FileTransferProtocol::GoogleCloudStorage => InputMask::Gcs,
|
||||
FileTransferProtocol::Ftp(_)
|
||||
| FileTransferProtocol::Scp
|
||||
| FileTransferProtocol::Sftp => InputMask::Generic,
|
||||
@@ -411,3 +468,77 @@ impl Activity for AuthActivity {
|
||||
self.context.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_set_configured_remote_protocol() {
|
||||
let mut activity = AuthActivity::new(Duration::ZERO);
|
||||
|
||||
activity.set_remote_protocol(FileTransferProtocol::GoogleCloudStorage);
|
||||
|
||||
assert_eq!(
|
||||
activity.remote_protocol,
|
||||
FileTransferProtocol::GoogleCloudStorage
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_resolve_ssh_params_only_after_host_change_or_forced_ssh_transition() {
|
||||
assert!(should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
"saved-host",
|
||||
"edited-host",
|
||||
false
|
||||
));
|
||||
assert!(!should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
"saved-host",
|
||||
"saved-host",
|
||||
false
|
||||
));
|
||||
assert!(should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Scp,
|
||||
"saved-host",
|
||||
"saved-host",
|
||||
true
|
||||
));
|
||||
assert!(!should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Ftp(false),
|
||||
"saved-host",
|
||||
"edited-host",
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_track_host_bridge_and_remote_addresses_independently() {
|
||||
let mut activity = AuthActivity::new(Duration::ZERO);
|
||||
|
||||
activity.set_last_mounted_address(FormTab::HostBridge, "bookmark-host");
|
||||
activity.set_last_mounted_address(FormTab::Remote, "recent-host");
|
||||
|
||||
assert_eq!(
|
||||
activity.last_mounted_address(FormTab::HostBridge),
|
||||
"bookmark-host"
|
||||
);
|
||||
assert_eq!(
|
||||
activity.last_mounted_address(FormTab::Remote),
|
||||
"recent-host"
|
||||
);
|
||||
assert!(!should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
activity.last_mounted_address(FormTab::Remote),
|
||||
"recent-host",
|
||||
false
|
||||
));
|
||||
assert!(should_resolve_ssh_host_params(
|
||||
FileTransferProtocol::Sftp,
|
||||
activity.last_mounted_address(FormTab::HostBridge),
|
||||
"edited-host",
|
||||
false
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
|
||||
use crate::filetransfer::HostBridgeParams;
|
||||
use crate::filetransfer::params::{
|
||||
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
|
||||
WebDAVProtocolParams,
|
||||
AwsS3Params, DEFAULT_GCS_ENDPOINT, GenericProtocolParams, GoogleCloudStorageParams,
|
||||
KubeProtocolParams, ProtocolParams, SmbParams, WebDAVProtocolParams,
|
||||
};
|
||||
|
||||
impl AuthActivity {
|
||||
@@ -201,6 +201,9 @@ impl AuthActivity {
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
self.load_bookmark_s3_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::GoogleCloudStorage(params) => {
|
||||
self.load_bookmark_gcs_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
self.load_bookmark_kube_into_gui(FormTab::HostBridge, params)
|
||||
}
|
||||
@@ -240,6 +243,9 @@ impl AuthActivity {
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
self.load_bookmark_s3_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
ProtocolParams::GoogleCloudStorage(params) => {
|
||||
self.load_bookmark_gcs_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
ProtocolParams::Kube(params) => {
|
||||
self.load_bookmark_kube_into_gui(FormTab::Remote, params)
|
||||
}
|
||||
@@ -276,6 +282,22 @@ impl AuthActivity {
|
||||
self.mount_s3_new_path_style(form_tab, params.new_path_style);
|
||||
}
|
||||
|
||||
fn load_bookmark_gcs_into_gui(&mut self, form_tab: FormTab, params: GoogleCloudStorageParams) {
|
||||
self.mount_gcs_bucket(form_tab, ¶ms.bucket_name);
|
||||
self.mount_gcs_endpoint(
|
||||
form_tab,
|
||||
if params.endpoint.is_empty() {
|
||||
DEFAULT_GCS_ENDPOINT
|
||||
} else {
|
||||
¶ms.endpoint
|
||||
},
|
||||
);
|
||||
self.mount_gcs_service_account_key(
|
||||
form_tab,
|
||||
params.service_account_key.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(""));
|
||||
@@ -293,6 +315,8 @@ impl AuthActivity {
|
||||
self.mount_smb_share(form_tab, ¶ms.share);
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or(""));
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect(form_tab, params.dialect);
|
||||
}
|
||||
|
||||
fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) {
|
||||
|
||||
@@ -13,16 +13,16 @@ pub use bookmarks::{
|
||||
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
|
||||
RecentsList,
|
||||
};
|
||||
#[cfg(posix)]
|
||||
pub use form::InputSmbWorkgroup;
|
||||
pub use form::{
|
||||
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
|
||||
InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
|
||||
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
|
||||
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
|
||||
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
|
||||
RemoteProtocolRadio,
|
||||
HostBridgeProtocolRadio, InputAddress, InputGcsBucket, InputGcsEndpoint,
|
||||
InputGcsServiceAccountKey, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl,
|
||||
InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword, InputPort,
|
||||
InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile,
|
||||
InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken,
|
||||
InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, RemoteProtocolRadio,
|
||||
};
|
||||
#[cfg(posix)]
|
||||
pub use form::{InputSmbWorkgroup, RadioSmbDialect, SmbDialectWarning};
|
||||
pub use popup::{
|
||||
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
|
||||
WindowSizeError,
|
||||
|
||||
@@ -13,14 +13,18 @@ use tuirealm::props::{
|
||||
use super::{FileTransferProtocol, FormMsg, Msg, UiMsg};
|
||||
use crate::ui::activities::auth::{
|
||||
FormTab, HOST_BRIDGE_RADIO_PROTOCOL_FTP, HOST_BRIDGE_RADIO_PROTOCOL_FTPS,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_KUBE, HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_S3, HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_GCS, HOST_BRIDGE_RADIO_PROTOCOL_KUBE,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST, HOST_BRIDGE_RADIO_PROTOCOL_S3,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, HostBridgeProtocol,
|
||||
REMOTE_RADIO_PROTOCOL_FTP, REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE,
|
||||
REMOTE_RADIO_PROTOCOL_S3, REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP,
|
||||
REMOTE_RADIO_PROTOCOL_SMB, REMOTE_RADIO_PROTOCOL_WEBDAV, UiAuthFormMsg,
|
||||
REMOTE_RADIO_PROTOCOL_FTP, REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_GCS,
|
||||
REMOTE_RADIO_PROTOCOL_KUBE, REMOTE_RADIO_PROTOCOL_S3, REMOTE_RADIO_PROTOCOL_SCP,
|
||||
REMOTE_RADIO_PROTOCOL_SFTP, REMOTE_RADIO_PROTOCOL_SMB, REMOTE_RADIO_PROTOCOL_WEBDAV,
|
||||
UiAuthFormMsg,
|
||||
};
|
||||
|
||||
#[path = "form/gcs.rs"]
|
||||
mod gcs;
|
||||
#[path = "form/generic.rs"]
|
||||
mod generic;
|
||||
#[path = "form/kube.rs"]
|
||||
@@ -36,6 +40,7 @@ mod smb;
|
||||
#[path = "form/webdav.rs"]
|
||||
mod webdav;
|
||||
|
||||
pub use gcs::{InputGcsBucket, InputGcsEndpoint, InputGcsServiceAccountKey};
|
||||
pub use generic::{InputAddress, InputPassword, InputPort, InputUsername};
|
||||
pub use kube::{
|
||||
InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
|
||||
@@ -49,7 +54,7 @@ pub use s3::{
|
||||
};
|
||||
pub use smb::InputSmbShare;
|
||||
#[cfg(posix)]
|
||||
pub use smb::InputSmbWorkgroup;
|
||||
pub use smb::{InputSmbWorkgroup, RadioSmbDialect, SmbDialectWarning};
|
||||
pub use webdav::InputWebDAVUri;
|
||||
|
||||
fn handle_input_ev(
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
//! ## Google Cloud Storage Form
|
||||
//!
|
||||
//! Input components for Google Cloud Storage authentication parameters.
|
||||
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputGcsBucket {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputGcsBucket {
|
||||
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(
|
||||
"my-bucket",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Bucket").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(bucket),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputGcsBucket {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let (on_key_down, on_key_up) = match self.form_tab {
|
||||
FormTab::Remote => (
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsBucketBlurDown)),
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsBucketBlurUp)),
|
||||
),
|
||||
FormTab::HostBridge => (
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsBucketBlurDown)),
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsBucketBlurUp)),
|
||||
),
|
||||
};
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputGcsEndpoint {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputGcsEndpoint {
|
||||
pub fn new(endpoint: &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(
|
||||
"https://storage.googleapis.com",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Endpoint").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(endpoint),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputGcsEndpoint {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let (on_key_down, on_key_up) = match self.form_tab {
|
||||
FormTab::Remote => (
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsEndpointBlurDown)),
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsEndpointBlurUp)),
|
||||
),
|
||||
FormTab::HostBridge => (
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsEndpointBlurDown)),
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsEndpointBlurUp)),
|
||||
),
|
||||
};
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputGcsServiceAccountKey {
|
||||
component: Input,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
impl InputGcsServiceAccountKey {
|
||||
pub fn new(path: &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(
|
||||
"Optional service-account JSON path",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
))
|
||||
.title(Title::from("Service account JSON").alignment(HorizontalAlignment::Left))
|
||||
.input_type(InputType::Text)
|
||||
.value(path),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for InputGcsServiceAccountKey {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
let (on_key_down, on_key_up) = match self.form_tab {
|
||||
FormTab::Remote => (
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsServiceAccountKeyBlurDown)),
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsServiceAccountKeyBlurUp)),
|
||||
),
|
||||
FormTab::HostBridge => (
|
||||
Msg::Ui(UiMsg::HostBridge(
|
||||
UiAuthFormMsg::GcsServiceAccountKeyBlurDown,
|
||||
)),
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsServiceAccountKeyBlurUp)),
|
||||
),
|
||||
};
|
||||
let form_tab = self.form_tab;
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,12 @@ impl RemoteProtocolRadio {
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(if cfg!(smb) {
|
||||
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"].into_iter()
|
||||
vec![
|
||||
"SFTP", "SCP", "FTP", "FTPS", "S3", "GCS", "Kube", "WebDAV", "SMB",
|
||||
]
|
||||
.into_iter()
|
||||
} else {
|
||||
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
|
||||
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "GCS", "Kube", "WebDAV"].into_iter()
|
||||
})
|
||||
.rewind(true)
|
||||
.title(Title::from("Protocol").alignment(HorizontalAlignment::Left))
|
||||
@@ -44,6 +47,7 @@ impl RemoteProtocolRadio {
|
||||
REMOTE_RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false),
|
||||
REMOTE_RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true),
|
||||
REMOTE_RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3,
|
||||
REMOTE_RADIO_PROTOCOL_GCS => FileTransferProtocol::GoogleCloudStorage,
|
||||
REMOTE_RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb,
|
||||
REMOTE_RADIO_PROTOCOL_KUBE => FileTransferProtocol::Kube,
|
||||
REMOTE_RADIO_PROTOCOL_WEBDAV => FileTransferProtocol::WebDAV,
|
||||
@@ -58,6 +62,7 @@ impl RemoteProtocolRadio {
|
||||
FileTransferProtocol::Ftp(false) => REMOTE_RADIO_PROTOCOL_FTP,
|
||||
FileTransferProtocol::Ftp(true) => REMOTE_RADIO_PROTOCOL_FTPS,
|
||||
FileTransferProtocol::AwsS3 => REMOTE_RADIO_PROTOCOL_S3,
|
||||
FileTransferProtocol::GoogleCloudStorage => REMOTE_RADIO_PROTOCOL_GCS,
|
||||
FileTransferProtocol::Kube => REMOTE_RADIO_PROTOCOL_KUBE,
|
||||
FileTransferProtocol::Smb => REMOTE_RADIO_PROTOCOL_SMB,
|
||||
FileTransferProtocol::WebDAV => REMOTE_RADIO_PROTOCOL_WEBDAV,
|
||||
@@ -128,6 +133,7 @@ impl HostBridgeProtocolRadio {
|
||||
"FTP",
|
||||
"FTPS",
|
||||
"S3",
|
||||
"GCS",
|
||||
"Kube",
|
||||
"WebDAV",
|
||||
"SMB",
|
||||
@@ -141,6 +147,7 @@ impl HostBridgeProtocolRadio {
|
||||
"FTP",
|
||||
"FTPS",
|
||||
"S3",
|
||||
"GCS",
|
||||
"Kube",
|
||||
"WebDAV",
|
||||
]
|
||||
@@ -168,6 +175,9 @@ impl HostBridgeProtocolRadio {
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3) => {
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_S3
|
||||
}
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::GoogleCloudStorage) => {
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_GCS
|
||||
}
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::Smb) => HOST_BRIDGE_RADIO_PROTOCOL_SMB,
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::Kube) => {
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_KUBE
|
||||
@@ -194,6 +204,9 @@ impl HostBridgeProtocolRadio {
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_S3 => {
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3)
|
||||
}
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_GCS => {
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::GoogleCloudStorage)
|
||||
}
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_SMB => HostBridgeProtocol::Remote(FileTransferProtocol::Smb),
|
||||
HOST_BRIDGE_RADIO_PROTOCOL_KUBE => {
|
||||
HostBridgeProtocol::Remote(FileTransferProtocol::Kube)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#[cfg(posix)]
|
||||
use tui_realm_stdlib::components::Span;
|
||||
use tuirealm::component::{AppComponent, Component};
|
||||
use tuirealm::event::NoUserEvent;
|
||||
#[cfg(posix)]
|
||||
use tuirealm::props::SpanStatic;
|
||||
|
||||
use super::*;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InputSmbShare {
|
||||
@@ -85,3 +91,163 @@ impl AppComponent<Msg, NoUserEvent> for InputSmbWorkgroup {
|
||||
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, posix))]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
|
||||
#[test]
|
||||
fn should_map_radio_options_to_dialect() {
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(0), SmbDialect::Auto);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(1), SmbDialect::Smb1);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(2), SmbDialect::Smb2);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(3), SmbDialect::Smb3);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(99), SmbDialect::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_map_dialect_to_radio_options() {
|
||||
for dialect in [
|
||||
SmbDialect::Auto,
|
||||
SmbDialect::Smb1,
|
||||
SmbDialect::Smb2,
|
||||
SmbDialect::Smb3,
|
||||
] {
|
||||
let opt = RadioSmbDialect::dialect_to_opt(dialect);
|
||||
assert_eq!(RadioSmbDialect::opt_to_dialect(opt), dialect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_AUTO: usize = 0;
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_SMB1: usize = 1;
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_SMB2: usize = 2;
|
||||
#[cfg(posix)]
|
||||
const RADIO_SMB_DIALECT_SMB3: usize = 3;
|
||||
|
||||
/// Radio to select the SMB protocol family.
|
||||
#[cfg(posix)]
|
||||
#[derive(Component)]
|
||||
pub struct RadioSmbDialect {
|
||||
component: Radio,
|
||||
form_tab: FormTab,
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl RadioSmbDialect {
|
||||
pub fn new(dialect: SmbDialect, 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)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.choices(["Auto", "SMB1 (insecure)", "SMB2", "SMB3"])
|
||||
.rewind(true)
|
||||
.title(Title::from("SMB version").alignment(HorizontalAlignment::Left))
|
||||
.value(Self::dialect_to_opt(dialect)),
|
||||
form_tab,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the radio choice index to a dialect. Unknown indexes map to `Auto`.
|
||||
pub fn opt_to_dialect(opt: usize) -> SmbDialect {
|
||||
match opt {
|
||||
RADIO_SMB_DIALECT_SMB1 => SmbDialect::Smb1,
|
||||
RADIO_SMB_DIALECT_SMB2 => SmbDialect::Smb2,
|
||||
RADIO_SMB_DIALECT_SMB3 => SmbDialect::Smb3,
|
||||
_ => SmbDialect::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn dialect_to_opt(dialect: SmbDialect) -> usize {
|
||||
match dialect {
|
||||
SmbDialect::Auto => RADIO_SMB_DIALECT_AUTO,
|
||||
SmbDialect::Smb1 => RADIO_SMB_DIALECT_SMB1,
|
||||
SmbDialect::Smb2 => RADIO_SMB_DIALECT_SMB2,
|
||||
SmbDialect::Smb3 => RADIO_SMB_DIALECT_SMB3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl AppComponent<Msg, NoUserEvent> for RadioSmbDialect {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => Some(Msg::Form(FormMsg::Connect)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => Some(if self.form_tab == FormTab::Remote {
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbDialectBlurDown))
|
||||
} else {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbDialectBlurDown))
|
||||
}),
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
Some(if self.form_tab == FormTab::Remote {
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbDialectBlurUp))
|
||||
} else {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbDialectBlurUp))
|
||||
})
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
Some(if self.form_tab == FormTab::Remote {
|
||||
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur))
|
||||
} else {
|
||||
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur))
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One-line warning shown above the dialect radio while SMB1 is selected.
|
||||
#[cfg(posix)]
|
||||
#[derive(Component)]
|
||||
pub struct SmbDialectWarning {
|
||||
component: Span,
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl SmbDialectWarning {
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
component: Span::default().foreground(color).spans([SpanStatic::from(
|
||||
"Warning: SMB1 is deprecated and insecure. Use it only for isolated legacy devices.",
|
||||
)]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
impl AppComponent<Msg, NoUserEvent> for SmbDialectWarning {
|
||||
fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ impl AuthActivity {
|
||||
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
|
||||
FileTransferProtocol::Ftp(_) => 21,
|
||||
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
|
||||
FileTransferProtocol::GoogleCloudStorage => 22, // Doesn't matter, since not used
|
||||
FileTransferProtocol::Kube => 22, // Doesn't matter, since not used
|
||||
FileTransferProtocol::Smb => 445,
|
||||
FileTransferProtocol::WebDAV => 80, // Doesn't matter, since not used
|
||||
@@ -45,6 +46,9 @@ impl AuthActivity {
|
||||
HostBridgeProtocol::Remote(remote) => {
|
||||
let transfer_params = match remote {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::HostBridge),
|
||||
FileTransferProtocol::GoogleCloudStorage => {
|
||||
self.collect_gcs_host_params(FormTab::HostBridge)
|
||||
}
|
||||
FileTransferProtocol::Kube => {
|
||||
self.collect_kube_host_params(FormTab::HostBridge)
|
||||
}
|
||||
@@ -71,6 +75,9 @@ impl AuthActivity {
|
||||
pub(super) fn collect_remote_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
match self.remote_protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::Remote),
|
||||
FileTransferProtocol::GoogleCloudStorage => {
|
||||
self.collect_gcs_host_params(FormTab::Remote)
|
||||
}
|
||||
FileTransferProtocol::Kube => self.collect_kube_host_params(FormTab::Remote),
|
||||
FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::Remote),
|
||||
FileTransferProtocol::Ftp(_)
|
||||
@@ -136,6 +143,26 @@ impl AuthActivity {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get input values from fields or return an error if fields are invalid to work as GCS.
|
||||
pub(super) fn collect_gcs_host_params(
|
||||
&self,
|
||||
form_tab: FormTab,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let params = self.get_gcs_params_input(form_tab);
|
||||
if params.bucket_name.is_empty() {
|
||||
return Err("Invalid bucket");
|
||||
}
|
||||
if params.endpoint.is_empty() {
|
||||
return Err("Invalid endpoint");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol: FileTransferProtocol::GoogleCloudStorage,
|
||||
params: ProtocolParams::GoogleCloudStorage(params),
|
||||
local_path: self.get_input_local_directory(form_tab),
|
||||
remote_path: self.get_input_remote_directory(form_tab),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get input values from fields or return an error if fields are invalid to work as aws s3
|
||||
pub(super) fn collect_kube_host_params(
|
||||
&self,
|
||||
|
||||
@@ -6,8 +6,10 @@ use tuirealm::state::{State, StateValue};
|
||||
|
||||
use super::{
|
||||
AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg,
|
||||
UiAuthFormMsg, UiMsg,
|
||||
UiAuthFormMsg, UiMsg, should_resolve_ssh_host_params,
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::utils::ssh::resolve_ssh_host_params;
|
||||
|
||||
impl AuthActivity {
|
||||
pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||
@@ -24,19 +26,26 @@ impl AuthActivity {
|
||||
fn update_form(&mut self, msg: FormMsg) -> Option<Msg> {
|
||||
match msg {
|
||||
FormMsg::Connect => {
|
||||
let Ok(remote_params) = self.collect_remote_host_params() else {
|
||||
// mount error
|
||||
self.mount_error("Invalid remote params parameters");
|
||||
return None;
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
let remote_params = match self.collect_remote_host_params() {
|
||||
Ok(remote_params) => remote_params,
|
||||
Err(err) => {
|
||||
// mount error
|
||||
self.mount_error(format!("Invalid remote host parameters: {err}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(host_bridge_params) = self.collect_host_bridge_params() else {
|
||||
// mount error
|
||||
self.mount_error("Invalid host bridge params parameters");
|
||||
return None;
|
||||
};
|
||||
|
||||
debug!("Remote params: {:?}", remote_params);
|
||||
|
||||
let host_bridge_params = match self.collect_host_bridge_params() {
|
||||
Ok(host_bridge_params) => host_bridge_params,
|
||||
Err(err) => {
|
||||
// mount error
|
||||
self.mount_error(format!("Invalid host bridge parameters: {err}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
debug!("Host bridge params: {:?}", host_bridge_params);
|
||||
|
||||
self.save_recent();
|
||||
@@ -86,6 +95,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::Smb => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
|
||||
InputMask::Gcs => &Id::Remote(AuthFormId::GcsBucket),
|
||||
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
|
||||
},
|
||||
@@ -94,6 +104,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
|
||||
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
|
||||
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
|
||||
InputMask::Gcs => &Id::HostBridge(AuthFormId::GcsBucket),
|
||||
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
|
||||
},
|
||||
@@ -112,6 +123,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::Smb => &Id::Remote(AuthFormId::Password),
|
||||
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
|
||||
InputMask::Gcs => &Id::Remote(AuthFormId::GcsBucket),
|
||||
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
|
||||
},
|
||||
@@ -120,6 +132,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
|
||||
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
|
||||
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
|
||||
InputMask::Gcs => &Id::HostBridge(AuthFormId::GcsBucket),
|
||||
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
|
||||
},
|
||||
@@ -133,24 +146,36 @@ impl AuthActivity {
|
||||
self.host_bridge_protocol = protocol;
|
||||
// Update port
|
||||
let port: u16 = self.get_input_port(FormTab::HostBridge);
|
||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol
|
||||
&& Self::is_port_standard(port)
|
||||
{
|
||||
self.mount_port(
|
||||
FormTab::HostBridge,
|
||||
Self::get_default_port_for_protocol(remote_protocol),
|
||||
);
|
||||
if let HostBridgeProtocol::Remote(remote_protocol) = protocol {
|
||||
match remote_protocol {
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, true);
|
||||
}
|
||||
_ if Self::is_port_standard(port) => {
|
||||
self.mount_port(
|
||||
FormTab::HostBridge,
|
||||
Self::get_default_port_for_protocol(remote_protocol),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
FormMsg::RemoteProtocolChanged(protocol) => {
|
||||
self.remote_protocol = protocol;
|
||||
// Update port
|
||||
let port: u16 = self.get_input_port(FormTab::Remote);
|
||||
if Self::is_port_standard(port) {
|
||||
self.mount_port(
|
||||
FormTab::Remote,
|
||||
Self::get_default_port_for_protocol(protocol),
|
||||
);
|
||||
match protocol {
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, true);
|
||||
}
|
||||
_ if Self::is_port_standard(port) => {
|
||||
self.mount_port(
|
||||
FormTab::Remote,
|
||||
Self::get_default_port_for_protocol(protocol),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
FormMsg::Quit => {
|
||||
@@ -244,6 +269,7 @@ impl AuthActivity {
|
||||
fn update_host_bridge_ui(&mut self, msg: UiAuthFormMsg) {
|
||||
match msg {
|
||||
UiAuthFormMsg::AddressBlurDown => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb {
|
||||
Id::HostBridge(AuthFormId::SmbShare)
|
||||
} else {
|
||||
@@ -252,9 +278,11 @@ impl AuthActivity {
|
||||
self.activate_component(id);
|
||||
}
|
||||
UiAuthFormMsg::AddressBlurUp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
||||
}
|
||||
UiAuthFormMsg::ChangeFormTab => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
self.last_form_tab = FormTab::Remote;
|
||||
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
||||
}
|
||||
@@ -269,6 +297,7 @@ impl AuthActivity {
|
||||
self.activate_component(id);
|
||||
}
|
||||
UiAuthFormMsg::ParamsFormBlur => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::HostBridge, false);
|
||||
self.activate_component(Id::BookmarksList);
|
||||
}
|
||||
UiAuthFormMsg::PasswordBlurDown => {
|
||||
@@ -280,6 +309,7 @@ impl AuthActivity {
|
||||
#[cfg(win)]
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||
InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"),
|
||||
InputMask::Gcs => unreachable!("this shouldn't happen (password on gcs)"),
|
||||
InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"),
|
||||
InputMask::WebDAV => Id::HostBridge(AuthFormId::RemoteDirectory),
|
||||
};
|
||||
@@ -294,6 +324,7 @@ impl AuthActivity {
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbShare),
|
||||
InputMask::Localhost
|
||||
| InputMask::AwsS3
|
||||
| InputMask::Gcs
|
||||
| InputMask::Kube
|
||||
| InputMask::WebDAV => {
|
||||
unreachable!("this shouldn't happen (port on s3/kube/webdav)")
|
||||
@@ -310,6 +341,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => Id::HostBridge(AuthFormId::Address),
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::Address),
|
||||
InputMask::AwsS3 => Id::HostBridge(AuthFormId::S3Bucket),
|
||||
InputMask::Gcs => Id::HostBridge(AuthFormId::GcsBucket),
|
||||
InputMask::Kube => Id::HostBridge(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => Id::HostBridge(AuthFormId::WebDAVUri),
|
||||
};
|
||||
@@ -326,11 +358,12 @@ impl AuthActivity {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => Id::HostBridge(AuthFormId::Password),
|
||||
#[cfg(posix)]
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbWorkgroup),
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbDialect),
|
||||
#[cfg(win)]
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::Password),
|
||||
InputMask::Kube => Id::HostBridge(AuthFormId::KubeClientKey),
|
||||
InputMask::AwsS3 => Id::HostBridge(AuthFormId::S3NewPathStyle),
|
||||
InputMask::Gcs => Id::HostBridge(AuthFormId::GcsServiceAccountKey),
|
||||
InputMask::WebDAV => Id::HostBridge(AuthFormId::Password),
|
||||
};
|
||||
self.activate_component(id);
|
||||
@@ -389,6 +422,24 @@ impl AuthActivity {
|
||||
UiAuthFormMsg::S3NewPathStyleBlurUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::S3SessionToken))
|
||||
}
|
||||
UiAuthFormMsg::GcsBucketBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::GcsEndpoint))
|
||||
}
|
||||
UiAuthFormMsg::GcsBucketBlurUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol))
|
||||
}
|
||||
UiAuthFormMsg::GcsEndpointBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::GcsServiceAccountKey))
|
||||
}
|
||||
UiAuthFormMsg::GcsEndpointBlurUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::GcsBucket))
|
||||
}
|
||||
UiAuthFormMsg::GcsServiceAccountKeyBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||
}
|
||||
UiAuthFormMsg::GcsServiceAccountKeyBlurUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::GcsEndpoint))
|
||||
}
|
||||
UiAuthFormMsg::KubeClientCertBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::KubeClientKey))
|
||||
}
|
||||
@@ -432,12 +483,20 @@ impl AuthActivity {
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||
self.activate_component(Id::HostBridge(AuthFormId::SmbDialect))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurUp => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::SmbWorkgroup))
|
||||
}
|
||||
UiAuthFormMsg::UsernameBlurDown => {
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Password))
|
||||
}
|
||||
@@ -448,6 +507,7 @@ impl AuthActivity {
|
||||
InputMask::Smb => Id::HostBridge(AuthFormId::SmbShare),
|
||||
InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"),
|
||||
InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"),
|
||||
InputMask::Gcs => unreachable!("this shouldn't happen (username on gcs)"),
|
||||
InputMask::WebDAV => Id::HostBridge(AuthFormId::WebDAVUri),
|
||||
};
|
||||
self.activate_component(id);
|
||||
@@ -464,6 +524,7 @@ impl AuthActivity {
|
||||
fn update_remote_ui(&mut self, msg: UiAuthFormMsg) {
|
||||
match msg {
|
||||
UiAuthFormMsg::AddressBlurDown => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb {
|
||||
Id::Remote(AuthFormId::SmbShare)
|
||||
} else {
|
||||
@@ -472,9 +533,11 @@ impl AuthActivity {
|
||||
self.activate_component(id);
|
||||
}
|
||||
UiAuthFormMsg::AddressBlurUp => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.activate_component(Id::Remote(AuthFormId::Protocol));
|
||||
}
|
||||
UiAuthFormMsg::ChangeFormTab => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.last_form_tab = FormTab::HostBridge;
|
||||
self.activate_component(Id::HostBridge(AuthFormId::Protocol));
|
||||
}
|
||||
@@ -485,6 +548,7 @@ impl AuthActivity {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory));
|
||||
}
|
||||
UiAuthFormMsg::ParamsFormBlur => {
|
||||
self.resolve_ssh_host_params_if_needed(FormTab::Remote, false);
|
||||
self.activate_component(Id::BookmarksList);
|
||||
}
|
||||
UiAuthFormMsg::PasswordBlurDown => {
|
||||
@@ -496,6 +560,7 @@ impl AuthActivity {
|
||||
#[cfg(win)]
|
||||
InputMask::Smb => Id::Remote(AuthFormId::RemoteDirectory),
|
||||
InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"),
|
||||
InputMask::Gcs => unreachable!("this shouldn't happen (password on gcs)"),
|
||||
InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"),
|
||||
InputMask::WebDAV => Id::Remote(AuthFormId::RemoteDirectory),
|
||||
};
|
||||
@@ -510,6 +575,7 @@ impl AuthActivity {
|
||||
InputMask::Smb => Id::Remote(AuthFormId::SmbShare),
|
||||
InputMask::Localhost
|
||||
| InputMask::AwsS3
|
||||
| InputMask::Gcs
|
||||
| InputMask::Kube
|
||||
| InputMask::WebDAV => {
|
||||
unreachable!("this shouldn't happen (port on s3/kube/webdav)")
|
||||
@@ -526,6 +592,7 @@ impl AuthActivity {
|
||||
InputMask::Generic => Id::Remote(AuthFormId::Address),
|
||||
InputMask::Smb => Id::Remote(AuthFormId::Address),
|
||||
InputMask::AwsS3 => Id::Remote(AuthFormId::S3Bucket),
|
||||
InputMask::Gcs => Id::Remote(AuthFormId::GcsBucket),
|
||||
InputMask::Kube => Id::Remote(AuthFormId::KubeNamespace),
|
||||
InputMask::WebDAV => Id::Remote(AuthFormId::WebDAVUri),
|
||||
};
|
||||
@@ -542,11 +609,12 @@ impl AuthActivity {
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Generic => Id::Remote(AuthFormId::Password),
|
||||
#[cfg(posix)]
|
||||
InputMask::Smb => Id::Remote(AuthFormId::SmbWorkgroup),
|
||||
InputMask::Smb => Id::Remote(AuthFormId::SmbDialect),
|
||||
#[cfg(win)]
|
||||
InputMask::Smb => Id::Remote(AuthFormId::Password),
|
||||
InputMask::Kube => Id::Remote(AuthFormId::KubeClientKey),
|
||||
InputMask::AwsS3 => Id::Remote(AuthFormId::S3NewPathStyle),
|
||||
InputMask::Gcs => Id::Remote(AuthFormId::GcsServiceAccountKey),
|
||||
InputMask::WebDAV => Id::Remote(AuthFormId::Password),
|
||||
};
|
||||
self.activate_component(id);
|
||||
@@ -605,6 +673,24 @@ impl AuthActivity {
|
||||
UiAuthFormMsg::S3NewPathStyleBlurUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::S3SessionToken))
|
||||
}
|
||||
UiAuthFormMsg::GcsBucketBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::GcsEndpoint))
|
||||
}
|
||||
UiAuthFormMsg::GcsBucketBlurUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::Protocol))
|
||||
}
|
||||
UiAuthFormMsg::GcsEndpointBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::GcsServiceAccountKey))
|
||||
}
|
||||
UiAuthFormMsg::GcsEndpointBlurUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::GcsBucket))
|
||||
}
|
||||
UiAuthFormMsg::GcsServiceAccountKeyBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
||||
}
|
||||
UiAuthFormMsg::GcsServiceAccountKeyBlurUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::GcsEndpoint))
|
||||
}
|
||||
UiAuthFormMsg::KubeClientCertBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::KubeClientKey))
|
||||
}
|
||||
@@ -648,12 +734,20 @@ impl AuthActivity {
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
||||
self.activate_component(Id::Remote(AuthFormId::SmbDialect))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbWorkgroupUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::Password))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
|
||||
}
|
||||
#[cfg(posix)]
|
||||
UiAuthFormMsg::SmbDialectBlurUp => {
|
||||
self.activate_component(Id::Remote(AuthFormId::SmbWorkgroup))
|
||||
}
|
||||
UiAuthFormMsg::UsernameBlurDown => {
|
||||
self.activate_component(Id::Remote(AuthFormId::Password))
|
||||
}
|
||||
@@ -664,6 +758,7 @@ impl AuthActivity {
|
||||
InputMask::Smb => Id::Remote(AuthFormId::SmbShare),
|
||||
InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"),
|
||||
InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"),
|
||||
InputMask::Gcs => unreachable!("this shouldn't happen (username on gcs)"),
|
||||
InputMask::WebDAV => Id::Remote(AuthFormId::WebDAVUri),
|
||||
};
|
||||
self.activate_component(id);
|
||||
@@ -677,6 +772,35 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_ssh_host_params_if_needed(&mut self, form_tab: FormTab, force: bool) {
|
||||
let protocol = match form_tab {
|
||||
FormTab::HostBridge => match self.host_bridge_protocol {
|
||||
HostBridgeProtocol::Localhost => return,
|
||||
HostBridgeProtocol::Remote(protocol) => protocol,
|
||||
},
|
||||
FormTab::Remote => self.remote_protocol,
|
||||
};
|
||||
let address = self.get_input_addr(form_tab);
|
||||
let should_resolve = should_resolve_ssh_host_params(
|
||||
protocol,
|
||||
self.last_mounted_address(form_tab),
|
||||
address.as_str(),
|
||||
force,
|
||||
);
|
||||
if !should_resolve {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = resolve_ssh_host_params(self.context().ssh_config(), address.as_str());
|
||||
self.mount_port(form_tab, 22);
|
||||
self.mount_username(form_tab, "");
|
||||
self.mount_port(form_tab, params.port);
|
||||
if let Some(username) = params.username {
|
||||
self.mount_username(form_tab, username.as_str());
|
||||
}
|
||||
self.set_last_mounted_address(form_tab, address.as_str());
|
||||
}
|
||||
|
||||
fn activate_component(&mut self, id: Id) {
|
||||
if let Err(err) = self.app.active(&id) {
|
||||
error!("Failed to activate component: {err}");
|
||||
|
||||
+164
-56
@@ -3,7 +3,7 @@
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
use tuirealm::props::Color;
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
||||
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tuirealm::ratatui::widgets::Clear;
|
||||
use tuirealm::terminal::TerminalAdapter;
|
||||
|
||||
@@ -11,6 +11,9 @@ use super::{
|
||||
AuthActivity, AuthFormId, Context, FileTransferProtocol, FormTab, HostBridgeProtocol, Id,
|
||||
InputMask, components,
|
||||
};
|
||||
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
use crate::utils::ui::{Popup, Size};
|
||||
|
||||
#[path = "view/mounting.rs"]
|
||||
@@ -52,6 +55,9 @@ impl AuthActivity {
|
||||
self.mount_port(FormTab::HostBridge, 22);
|
||||
self.mount_username(FormTab::HostBridge, "");
|
||||
self.mount_password(FormTab::HostBridge, "");
|
||||
self.mount_gcs_bucket(FormTab::HostBridge, "");
|
||||
self.mount_gcs_endpoint(FormTab::HostBridge, DEFAULT_GCS_ENDPOINT);
|
||||
self.mount_gcs_service_account_key(FormTab::HostBridge, "");
|
||||
self.mount_s3_bucket(FormTab::HostBridge, "");
|
||||
self.mount_s3_profile(FormTab::HostBridge, "");
|
||||
self.mount_s3_region(FormTab::HostBridge, "");
|
||||
@@ -69,9 +75,14 @@ impl AuthActivity {
|
||||
self.mount_smb_share(FormTab::HostBridge, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(FormTab::HostBridge, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect(FormTab::HostBridge, SmbDialect::default());
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect_warning(FormTab::HostBridge);
|
||||
self.mount_webdav_uri(FormTab::HostBridge, "");
|
||||
|
||||
let remote_default_protocol = self.context().config().get_default_protocol();
|
||||
self.set_remote_protocol(remote_default_protocol);
|
||||
self.mount_remote_protocol(remote_default_protocol);
|
||||
self.mount_remote_directory(FormTab::Remote, "");
|
||||
self.mount_local_directory(FormTab::Remote, "");
|
||||
@@ -82,6 +93,9 @@ impl AuthActivity {
|
||||
);
|
||||
self.mount_username(FormTab::Remote, "");
|
||||
self.mount_password(FormTab::Remote, "");
|
||||
self.mount_gcs_bucket(FormTab::Remote, "");
|
||||
self.mount_gcs_endpoint(FormTab::Remote, DEFAULT_GCS_ENDPOINT);
|
||||
self.mount_gcs_service_account_key(FormTab::Remote, "");
|
||||
self.mount_s3_bucket(FormTab::Remote, "");
|
||||
self.mount_s3_profile(FormTab::Remote, "");
|
||||
self.mount_s3_region(FormTab::Remote, "");
|
||||
@@ -99,6 +113,10 @@ impl AuthActivity {
|
||||
self.mount_smb_share(FormTab::Remote, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_workgroup(FormTab::Remote, "");
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect(FormTab::Remote, SmbDialect::default());
|
||||
#[cfg(posix)]
|
||||
self.mount_smb_dialect_warning(FormTab::Remote);
|
||||
self.mount_webdav_uri(FormTab::Remote, "");
|
||||
|
||||
if let Some(version) = self
|
||||
@@ -239,8 +257,10 @@ impl AuthActivity {
|
||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||
area: tuirealm::ratatui::layout::Rect,
|
||||
) {
|
||||
let input_mask_size = Self::input_mask_size(self.host_bridge_input_mask());
|
||||
let input_mask = self.host_bridge_input_mask();
|
||||
let protocol_and_mask_chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Length(12)].as_ref())
|
||||
.constraints([Constraint::Length(3), Constraint::Length(input_mask_size)].as_ref())
|
||||
.direction(Direction::Vertical)
|
||||
.split(area);
|
||||
|
||||
@@ -250,35 +270,25 @@ impl AuthActivity {
|
||||
protocol_and_mask_chunks[0],
|
||||
);
|
||||
|
||||
let input_mask = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(protocol_and_mask_chunks[1]);
|
||||
match self.host_bridge_input_mask() {
|
||||
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_host_bridge_s3_view()),
|
||||
InputMask::Generic => {
|
||||
self.render_view_ids(f, input_mask, self.get_host_bridge_generic_params_view())
|
||||
}
|
||||
InputMask::Kube => {
|
||||
self.render_view_ids(f, input_mask, self.get_host_bridge_kube_view())
|
||||
}
|
||||
let view_ids = match input_mask {
|
||||
InputMask::AwsS3 => self.get_host_bridge_s3_view(),
|
||||
InputMask::Gcs => self.get_host_bridge_gcs_view(),
|
||||
InputMask::Generic => self.get_host_bridge_generic_params_view(),
|
||||
InputMask::Kube => self.get_host_bridge_kube_view(),
|
||||
InputMask::Localhost => {
|
||||
let view_ids = self.get_host_bridge_localhost_view();
|
||||
self.app.view(&view_ids[0], f, input_mask[0]);
|
||||
self.app.view(&view_ids[0], f, protocol_and_mask_chunks[1]);
|
||||
return;
|
||||
}
|
||||
InputMask::Smb => self.render_view_ids(f, input_mask, self.get_host_bridge_smb_view()),
|
||||
InputMask::WebDAV => {
|
||||
self.render_view_ids(f, input_mask, self.get_host_bridge_webdav_view())
|
||||
}
|
||||
}
|
||||
InputMask::Smb => self.get_host_bridge_smb_view(),
|
||||
InputMask::WebDAV => self.get_host_bridge_webdav_view(),
|
||||
};
|
||||
self.render_form_rows(
|
||||
f,
|
||||
protocol_and_mask_chunks[1],
|
||||
FormTab::HostBridge,
|
||||
view_ids,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_remote_input_mask(
|
||||
@@ -286,8 +296,10 @@ impl AuthActivity {
|
||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||
area: tuirealm::ratatui::layout::Rect,
|
||||
) {
|
||||
let input_mask_size = Self::input_mask_size(self.remote_input_mask());
|
||||
let input_mask = self.remote_input_mask();
|
||||
let protocol_and_mask_chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Length(12)].as_ref())
|
||||
.constraints([Constraint::Length(3), Constraint::Length(input_mask_size)].as_ref())
|
||||
.direction(Direction::Vertical)
|
||||
.split(area);
|
||||
|
||||
@@ -297,39 +309,135 @@ impl AuthActivity {
|
||||
protocol_and_mask_chunks[0],
|
||||
);
|
||||
|
||||
let input_mask = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(protocol_and_mask_chunks[1]);
|
||||
match self.remote_input_mask() {
|
||||
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_remote_s3_view()),
|
||||
InputMask::Generic => {
|
||||
self.render_view_ids(f, input_mask, self.get_remote_generic_params_view())
|
||||
}
|
||||
InputMask::Kube => self.render_view_ids(f, input_mask, self.get_remote_kube_view()),
|
||||
let view_ids = match input_mask {
|
||||
InputMask::AwsS3 => self.get_remote_s3_view(),
|
||||
InputMask::Gcs => self.get_remote_gcs_view(),
|
||||
InputMask::Generic => self.get_remote_generic_params_view(),
|
||||
InputMask::Kube => self.get_remote_kube_view(),
|
||||
InputMask::Localhost => unreachable!(),
|
||||
InputMask::Smb => self.render_view_ids(f, input_mask, self.get_remote_smb_view()),
|
||||
InputMask::WebDAV => self.render_view_ids(f, input_mask, self.get_remote_webdav_view()),
|
||||
}
|
||||
InputMask::Smb => self.get_remote_smb_view(),
|
||||
InputMask::WebDAV => self.get_remote_webdav_view(),
|
||||
};
|
||||
self.render_form_rows(f, protocol_and_mask_chunks[1], FormTab::Remote, view_ids);
|
||||
}
|
||||
|
||||
fn render_view_ids(
|
||||
/// Splits `area` into four 3-line form rows. When `warning_row` is
|
||||
/// `Some(index)`, a 1-line row is inserted directly above row `index` and
|
||||
/// returned as the second tuple element.
|
||||
fn split_input_mask(area: Rect, warning_row: Option<usize>) -> ([Rect; 4], Option<Rect>) {
|
||||
let mut constraints = Vec::with_capacity(6);
|
||||
for row in 0..4 {
|
||||
if warning_row == Some(row) {
|
||||
constraints.push(Constraint::Length(1));
|
||||
}
|
||||
constraints.push(Constraint::Length(3));
|
||||
}
|
||||
constraints.push(Constraint::Min(0));
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Vertical)
|
||||
.split(area);
|
||||
|
||||
let mut rows = [Rect::default(); 4];
|
||||
let mut warning = None;
|
||||
let mut chunk = 0;
|
||||
for (row, slot) in rows.iter_mut().enumerate() {
|
||||
if warning_row == Some(row) {
|
||||
warning = Some(chunks[chunk]);
|
||||
chunk += 1;
|
||||
}
|
||||
*slot = chunks[chunk];
|
||||
chunk += 1;
|
||||
}
|
||||
(rows, warning)
|
||||
}
|
||||
|
||||
/// Returns the visible row index of the SMB dialect radio when the form
|
||||
/// shows SMB and SMB1 is selected; `None` otherwise.
|
||||
#[cfg(posix)]
|
||||
fn smb_dialect_warning_row(&self, form_tab: FormTab, view_ids: &[Id; 4]) -> Option<usize> {
|
||||
let input_mask = match form_tab {
|
||||
FormTab::HostBridge => self.host_bridge_input_mask(),
|
||||
FormTab::Remote => self.remote_input_mask(),
|
||||
};
|
||||
if input_mask != InputMask::Smb || self.get_input_smb_dialect(form_tab) != SmbDialect::Smb1
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let dialect_id = Self::form_tab_id(form_tab, AuthFormId::SmbDialect);
|
||||
view_ids.iter().position(|id| *id == dialect_id)
|
||||
}
|
||||
|
||||
#[cfg(win)]
|
||||
fn smb_dialect_warning_row(&self, _form_tab: FormTab, _view_ids: &[Id; 4]) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_form_rows(
|
||||
&mut self,
|
||||
f: &mut tuirealm::ratatui::Frame<'_>,
|
||||
input_mask: std::rc::Rc<[tuirealm::ratatui::layout::Rect]>,
|
||||
area: Rect,
|
||||
form_tab: FormTab,
|
||||
view_ids: [Id; 4],
|
||||
) {
|
||||
self.app.view(&view_ids[0], f, input_mask[0]);
|
||||
self.app.view(&view_ids[1], f, input_mask[1]);
|
||||
self.app.view(&view_ids[2], f, input_mask[2]);
|
||||
self.app.view(&view_ids[3], f, input_mask[3]);
|
||||
let warning_row = self.smb_dialect_warning_row(form_tab, &view_ids);
|
||||
let (rows, warning) = Self::split_input_mask(area, warning_row);
|
||||
#[cfg(posix)]
|
||||
if let Some(rect) = warning {
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialectWarning);
|
||||
self.app.view(&id, f, rect);
|
||||
}
|
||||
#[cfg(win)]
|
||||
let _ = warning;
|
||||
for (id, rect) in view_ids.iter().zip(rows) {
|
||||
self.app.view(id, f, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use tuirealm::ratatui::layout::Rect;
|
||||
|
||||
use super::AuthActivity;
|
||||
|
||||
#[test]
|
||||
fn should_split_input_mask_without_warning() {
|
||||
let area = Rect::new(0, 0, 40, 13);
|
||||
|
||||
let (rows, warning) = AuthActivity::split_input_mask(area, None);
|
||||
|
||||
assert_eq!(warning, None);
|
||||
assert_eq!(rows[0], Rect::new(0, 0, 40, 3));
|
||||
assert_eq!(rows[1], Rect::new(0, 3, 40, 3));
|
||||
assert_eq!(rows[2], Rect::new(0, 6, 40, 3));
|
||||
assert_eq!(rows[3], Rect::new(0, 9, 40, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_split_input_mask_with_middle_warning() {
|
||||
let area = Rect::new(0, 0, 40, 13);
|
||||
|
||||
let (rows, warning) = AuthActivity::split_input_mask(area, Some(2));
|
||||
|
||||
assert_eq!(warning, Some(Rect::new(0, 6, 40, 1)));
|
||||
assert_eq!(rows[0], Rect::new(0, 0, 40, 3));
|
||||
assert_eq!(rows[1], Rect::new(0, 3, 40, 3));
|
||||
assert_eq!(rows[2], Rect::new(0, 7, 40, 3));
|
||||
assert_eq!(rows[3], Rect::new(0, 10, 40, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_split_input_mask_with_first_row_warning() {
|
||||
let area = Rect::new(0, 0, 40, 13);
|
||||
|
||||
let (rows, warning) = AuthActivity::split_input_mask(area, Some(0));
|
||||
|
||||
assert_eq!(warning, Some(Rect::new(0, 0, 40, 1)));
|
||||
assert_eq!(rows[0], Rect::new(0, 1, 40, 3));
|
||||
assert_eq!(rows[1], Rect::new(0, 4, 40, 3));
|
||||
assert_eq!(rows[2], Rect::new(0, 7, 40, 3));
|
||||
assert_eq!(rows[3], Rect::new(0, 10, 40, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
#[cfg(posix)]
|
||||
use crate::filetransfer::params::SmbDialect;
|
||||
use crate::ui::activities::auth::STORE_KEY_RELEASE_NOTES;
|
||||
|
||||
impl AuthActivity {
|
||||
@@ -320,6 +322,7 @@ impl AuthActivity {
|
||||
form_tab: FormTab,
|
||||
address: &str,
|
||||
) {
|
||||
self.set_last_mounted_address(form_tab, address);
|
||||
let addr_color = self.theme().auth_address;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::Address);
|
||||
if let Err(err) = self.app.remount(
|
||||
@@ -383,6 +386,56 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn mount_gcs_bucket(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
bucket: &str,
|
||||
) {
|
||||
let color = self.theme().auth_address;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::GcsBucket);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::InputGcsBucket::new(bucket, form_tab, color)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn mount_gcs_endpoint(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
endpoint: &str,
|
||||
) {
|
||||
let color = self.theme().auth_username;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::GcsEndpoint);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::InputGcsEndpoint::new(endpoint, form_tab, color)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn mount_gcs_service_account_key(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
path: &str,
|
||||
) {
|
||||
let color = self.theme().auth_password;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::GcsServiceAccountKey);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::InputGcsServiceAccountKey::new(
|
||||
path, form_tab, color,
|
||||
)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn mount_s3_bucket(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
@@ -654,6 +707,36 @@ impl AuthActivity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub(in crate::ui::activities::auth) fn mount_smb_dialect(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
dialect: SmbDialect,
|
||||
) {
|
||||
let color = self.theme().auth_protocol;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialect);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::RadioSmbDialect::new(dialect, form_tab, color)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(posix)]
|
||||
pub(in crate::ui::activities::auth) fn mount_smb_dialect_warning(&mut self, form_tab: FormTab) {
|
||||
let color = self.theme().misc_warn_dialog;
|
||||
let id = Self::form_tab_id(form_tab, AuthFormId::SmbDialectWarning);
|
||||
if let Err(err) = self.app.remount(
|
||||
id,
|
||||
Box::new(components::SmbDialectWarning::new(color)),
|
||||
vec![],
|
||||
) {
|
||||
error!("Failed to remount component: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::ui::activities::auth) fn mount_webdav_uri(
|
||||
&mut self,
|
||||
form_tab: FormTab,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user