mirror of
https://github.com/veeso/termscp.git
synced 2026-09-19 02:25:54 -07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4120311bf6 | |||
| 74c91c747c | |||
| f54c50f6ef | |||
| 325c8ceee7 | |||
| ebfaeb956c | |||
| 133ec898d8 | |||
| 1587d3d625 |
@@ -1,154 +0,0 @@
|
||||
name: Linux musl probe
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "test/440-musl-release"
|
||||
paths:
|
||||
- ".github/workflows/musl.yml"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
- "src/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build-${{ matrix.target }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-musl
|
||||
runner: ubuntu-24.04
|
||||
- target: aarch64-unknown-linux-musl
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and audit static binary
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--env TARGET \
|
||||
--volume "$GITHUB_WORKSPACE:/work" \
|
||||
--workdir /work \
|
||||
rust:1.98-alpine3.22 \
|
||||
sh -euxc '
|
||||
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"
|
||||
|
||||
mkdir -p /tmp/native
|
||||
wget -q https://ftp.gnu.org/gnu/nettle/nettle-3.10.1.tar.gz \
|
||||
-O /tmp/native/nettle.tar.gz
|
||||
tar -xzf /tmp/native/nettle.tar.gz -C /tmp/native
|
||||
cd /tmp/native/nettle-3.10.1
|
||||
./configure \
|
||||
--prefix=/tmp/native/nettle \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
--disable-documentation \
|
||||
--enable-mini-gmp
|
||||
make -j$(getconf _NPROCESSORS_ONLN)
|
||||
make install
|
||||
|
||||
wget -q https://www.gnupg.org/ftp/gcrypt/gnutls/v3.8/gnutls-3.8.13.tar.xz \
|
||||
-O /tmp/native/gnutls.tar.xz
|
||||
tar -xf /tmp/native/gnutls.tar.xz -C /tmp/native
|
||||
cd /tmp/native/gnutls-3.8.13
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
cd /work
|
||||
PAVAO_SRC=$(find "${CARGO_HOME:-/usr/local/cargo}/registry/src" \
|
||||
-type d -name "pavao-src-4.24.6" -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@4.24.6
|
||||
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
|
||||
file "target/$TARGET/release/termscp"
|
||||
readelf -l "target/$TARGET/release/termscp" | \
|
||||
tee /tmp/program-headers.txt
|
||||
readelf -d "target/$TARGET/release/termscp" | \
|
||||
tee /tmp/dynamic-section.txt
|
||||
! grep -q INTERP /tmp/program-headers.txt
|
||||
! grep -q NEEDED /tmp/dynamic-section.txt
|
||||
'
|
||||
|
||||
- name: Upload verified binary
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: target/${{ matrix.target }}/release/termscp
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ inputs.version }}
|
||||
prepared_ref: ${{ steps.prepared-ref.outputs.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -44,9 +45,9 @@ jobs:
|
||||
git config user.email "christian.visintin@veeso.dev"
|
||||
|
||||
- name: Install git-cliff
|
||||
uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
|
||||
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
|
||||
@@ -66,6 +67,9 @@ jobs:
|
||||
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
@@ -84,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 }}
|
||||
@@ -95,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
|
||||
@@ -125,58 +139,44 @@ jobs:
|
||||
steps:
|
||||
- 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
|
||||
- name: Prepare release version
|
||||
shell: bash
|
||||
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 Rust target
|
||||
if: matrix.kind != 'windows'
|
||||
if: matrix.kind == 'macos'
|
||||
run: rustup target add "$TARGET"
|
||||
- name: Install Rust target
|
||||
if: matrix.kind == 'windows'
|
||||
run: rustup target add "$env:TARGET"
|
||||
|
||||
# ---- Linux: native per-arch build (x86_64 on ubuntu-latest, aarch64 on ubuntu-24.04-arm) ----
|
||||
- name: Install dependencies (Linux)
|
||||
# ---- Linux: static musl build in a pinned Alpine container ----
|
||||
- name: Install cargo-deb (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
make \
|
||||
libgit2-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libbsd-dev \
|
||||
libcap-dev \
|
||||
libcups2-dev \
|
||||
libgnutls28-dev \
|
||||
libicu-dev \
|
||||
libjansson-dev \
|
||||
libkeyutils-dev \
|
||||
libldap2-dev \
|
||||
zlib1g-dev \
|
||||
libpam0g-dev \
|
||||
libacl1-dev \
|
||||
libarchive-dev \
|
||||
flex \
|
||||
bison \
|
||||
libntirpc-dev \
|
||||
libtracker-sparql-3.0-dev \
|
||||
libglib2.0-dev \
|
||||
libdbus-1-dev \
|
||||
libsasl2-dev \
|
||||
libunistring-dev \
|
||||
cpanminus
|
||||
sudo cpanm Parse::Yapp::Driver
|
||||
cargo install cargo-deb
|
||||
run: cargo install cargo-deb --locked
|
||||
- name: Build (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: just build_release "$TARGET" "--features smb-vendored"
|
||||
run: just build_musl "$TARGET"
|
||||
- name: Build deb (Linux)
|
||||
if: matrix.kind == 'linux'
|
||||
run: just package_deb "$TARGET"
|
||||
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)
|
||||
@@ -229,7 +229,7 @@ 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 }}
|
||||
@@ -255,8 +255,8 @@ 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
|
||||
@@ -293,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
|
||||
@@ -336,9 +334,15 @@ jobs:
|
||||
- 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
@@ -399,10 +403,15 @@ jobs:
|
||||
steps:
|
||||
- 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
|
||||
- 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
|
||||
|
||||
|
||||
+158
@@ -1,5 +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
|
||||
|
||||
Generated
+622
-805
File diff suppressed because it is too large
Load Diff
+13
-12
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "termscp"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
authors = ["Christian Visintin <christian.visintin@veeso.dev>"]
|
||||
categories = ["command-line-utilities"]
|
||||
edition = "2024"
|
||||
@@ -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]
|
||||
@@ -45,7 +46,7 @@ bytesize = "2"
|
||||
cbc = { version = "0.2", features = ["alloc"] }
|
||||
chrono = "0.4"
|
||||
content_inspector = "0.2"
|
||||
dirs = "6"
|
||||
dirs = "7"
|
||||
edit = "0.1"
|
||||
filetime = "0.2"
|
||||
keyring-core = "1"
|
||||
@@ -58,13 +59,13 @@ nucleo = "0.5"
|
||||
open = "5"
|
||||
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.5", default-features = false, optional = true, features = ["find", "pavao"] }
|
||||
remotefs-ssh = { version = "0.9", default-features = false, features = ["russh"] }
|
||||
remotefs-webdav = "0.2"
|
||||
remotefs = "1"
|
||||
remotefs-aws-s3 = { version = "1", features = ["tokio"] }
|
||||
remotefs-gcs = { version = "1", features = ["tokio"] }
|
||||
remotefs-kube = { version = "1", features = ["tokio"] }
|
||||
remotefs-smb = { version = "1", default-features = false, optional = true, features = ["find", "pavao"] }
|
||||
remotefs-ssh = { version = "1", default-features = false, features = ["russh"] }
|
||||
remotefs-webdav = { version = "1", features = ["tokio"] }
|
||||
rpassword = "7"
|
||||
self_update = { version = "0.42", default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] }
|
||||
semver = "1"
|
||||
@@ -75,7 +76,7 @@ simplelog = "0.12"
|
||||
ssh2-config = "0.8"
|
||||
tempfile = "3"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
toml = "1"
|
||||
tui-realm-stdlib = "4"
|
||||
tui-term = "0.3"
|
||||
@@ -88,11 +89,11 @@ wildmatch = "2"
|
||||
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 = "1", features = ["native-tls", "native-tls-vendored"] }
|
||||
uzers = "0.12"
|
||||
|
||||
[target."cfg(target_family = \"windows\")".dependencies]
|
||||
remotefs-ftp = { version = "0.4", features = ["native-tls"] }
|
||||
remotefs-ftp = { version = "1", features = ["native-tls"] }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
apple-native-keyring-store = { version = "1", features = ["keychain"] }
|
||||
|
||||
@@ -117,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
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -117,6 +117,10 @@ pacman -S termscp
|
||||
|
||||
### 依赖 ❗
|
||||
|
||||
官方 Linux 二进制文件和 `.deb` 包静态链接了 musl,**没有任何运行时依赖**:可在任意发行版、任意 glibc 版本上运行。
|
||||
|
||||
以下依赖仅在从源码构建 termscp 时需要:
|
||||
|
||||
- **Linux** 用户:
|
||||
- libdbus-1
|
||||
- pkg-config
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -17,6 +17,11 @@ build_crates_release:
|
||||
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:
|
||||
|
||||
@@ -42,3 +42,4 @@ changelog version:
|
||||
rm -f "$secfile"
|
||||
mv "$tmp" CHANGELOG.md
|
||||
echo "CHANGELOG.md updated for $tag"
|
||||
just fmt CHANGELOG.md
|
||||
|
||||
@@ -35,7 +35,9 @@ setup_githooks:
|
||||
[group('code_check')]
|
||||
check_install_scripts:
|
||||
sh -n install.sh
|
||||
shellcheck 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 \
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Publish the termscp crate
|
||||
[group('publish')]
|
||||
publish_crate args="":
|
||||
cargo publish --locked --features smb-vendored {{ args }}
|
||||
cargo publish --locked --features smb {{ args }}
|
||||
|
||||
+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
|
||||
>.
|
||||
|
||||
+34
-46
@@ -565,20 +565,18 @@ mod tests {
|
||||
let explorer: FileExplorer = FileExplorer::default();
|
||||
// Create fs entry
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = File::new(
|
||||
"/bar.txt",
|
||||
Metadata::default()
|
||||
.accessed(t)
|
||||
.created(t)
|
||||
.modified(t)
|
||||
.file_type(FileType::File)
|
||||
.uid(0)
|
||||
.gid(0)
|
||||
.mode(UnixPex::from(0o644))
|
||||
.size(8192),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
@@ -690,47 +688,37 @@ mod tests {
|
||||
|
||||
fn make_fs_entry(name: &str, is_dir: bool) -> File {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let metadata = Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: if is_dir {
|
||||
let metadata = Metadata::default()
|
||||
.accessed(t)
|
||||
.created(t)
|
||||
.modified(t)
|
||||
.file_type(if is_dir {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
},
|
||||
symlink: None,
|
||||
gid: Some(0),
|
||||
uid: Some(0),
|
||||
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||
size: 64,
|
||||
};
|
||||
File {
|
||||
path: PathBuf::from(name),
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
.gid(0)
|
||||
.uid(0)
|
||||
.mode(UnixPex::from(if is_dir { 0o755 } else { 0o644 }))
|
||||
.size(64);
|
||||
File::new(name, metadata)
|
||||
}
|
||||
|
||||
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> File {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let metadata = Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: if is_dir {
|
||||
let metadata = Metadata::default()
|
||||
.accessed(t)
|
||||
.created(t)
|
||||
.modified(t)
|
||||
.file_type(if is_dir {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
},
|
||||
symlink: None,
|
||||
gid: Some(0),
|
||||
uid: Some(0),
|
||||
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||
size: size as u64,
|
||||
};
|
||||
File {
|
||||
path: PathBuf::from(name),
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
.gid(0)
|
||||
.uid(0)
|
||||
.mode(UnixPex::from(if is_dir { 0o755 } else { 0o644 }))
|
||||
.size(size as u64);
|
||||
File::new(name, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
+231
-255
@@ -363,8 +363,10 @@ impl Formatter {
|
||||
) -> String {
|
||||
if fsentry.is_file() {
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(fsentry.metadata().size);
|
||||
let mut fmt = size.display().si().to_string();
|
||||
let mut fmt = fsentry.metadata().size.map_or_else(
|
||||
|| String::from("?"),
|
||||
|size| ByteSize(size).display().si().to_string(),
|
||||
);
|
||||
// pad with up to len 10
|
||||
let pad = 10usize.saturating_sub(fmt.len());
|
||||
for _ in 0..pad {
|
||||
@@ -534,7 +536,6 @@ impl Formatter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -542,26 +543,61 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "The fixture parameters mirror the optional remotefs metadata fields."
|
||||
)]
|
||||
fn fixture(
|
||||
path: &str,
|
||||
file_type: FileType,
|
||||
size: Option<u64>,
|
||||
symlink: Option<&str>,
|
||||
uid: Option<u32>,
|
||||
gid: Option<u32>,
|
||||
mode: Option<UnixPex>,
|
||||
timestamp: Option<SystemTime>,
|
||||
) -> File {
|
||||
let mut metadata = Metadata::default().file_type(file_type);
|
||||
if let Some(size) = size {
|
||||
metadata = metadata.size(size);
|
||||
}
|
||||
if let Some(symlink) = symlink {
|
||||
metadata = metadata.symlink(symlink);
|
||||
}
|
||||
if let Some(uid) = uid {
|
||||
metadata = metadata.uid(uid);
|
||||
}
|
||||
if let Some(gid) = gid {
|
||||
metadata = metadata.gid(gid);
|
||||
}
|
||||
if let Some(mode) = mode {
|
||||
metadata = metadata.mode(mode);
|
||||
}
|
||||
if let Some(timestamp) = timestamp {
|
||||
metadata = metadata
|
||||
.accessed(timestamp)
|
||||
.created(timestamp)
|
||||
.modified(timestamp);
|
||||
}
|
||||
File::new(path, metadata)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_callchain() {
|
||||
// Make a dummy formatter
|
||||
let dummy_formatter: Formatter = Formatter::new("");
|
||||
// Make a dummy entry
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let dummy_entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let dummy_entry = fixture(
|
||||
"/bar.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
let prefix: String = String::from("h");
|
||||
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
|
||||
assert!(callchain.next_block.is_none());
|
||||
@@ -589,20 +625,16 @@ mod tests {
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/bar.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
@@ -620,20 +652,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// Elide name
|
||||
let entry = File {
|
||||
path: PathBuf::from("/piroparoporoperoperupupu.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/piroparoporoperoperupupu.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
@@ -651,20 +679,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// No pex
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/bar.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
Some(0),
|
||||
Some(0),
|
||||
None,
|
||||
Some(t),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
@@ -682,20 +706,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// No user
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: Some(0),
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/bar.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
Some(0),
|
||||
None,
|
||||
Some(t),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
@@ -720,20 +740,16 @@ mod tests {
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Directory,
|
||||
size: 4096,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/home/cvisintin/projects",
|
||||
FileType::Directory,
|
||||
Some(4096),
|
||||
None,
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(UnixPex::from(0o755)),
|
||||
Some(t),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
@@ -751,20 +767,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// No pex, no user
|
||||
let entry = File {
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Directory,
|
||||
size: 4096,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: Some(0),
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/home/cvisintin/projects",
|
||||
FileType::Directory,
|
||||
Some(4096),
|
||||
None,
|
||||
None,
|
||||
Some(0),
|
||||
None,
|
||||
Some(t),
|
||||
);
|
||||
#[cfg(posix)]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
@@ -790,20 +802,16 @@ mod tests {
|
||||
);
|
||||
// Directory (with symlink)
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Symlink,
|
||||
size: 4096,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/home/cvisintin/projects",
|
||||
FileType::Symlink,
|
||||
Some(4096),
|
||||
Some("project.info"),
|
||||
None,
|
||||
None,
|
||||
Some(UnixPex::from(0o755)),
|
||||
Some(t),
|
||||
);
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -814,20 +822,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// Directory without symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Directory,
|
||||
size: 4096,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/home/cvisintin/projects",
|
||||
FileType::Directory,
|
||||
Some(4096),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(UnixPex::from(0o755)),
|
||||
Some(t),
|
||||
);
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -838,20 +842,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// File with symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Symlink,
|
||||
size: 8192,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/bar.txt",
|
||||
FileType::Symlink,
|
||||
Some(8192),
|
||||
Some("project.info"),
|
||||
None,
|
||||
None,
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -862,20 +862,16 @@ mod tests {
|
||||
)
|
||||
);
|
||||
// File without symlink
|
||||
let entry = File {
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/bar.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
@@ -891,20 +887,16 @@ mod tests {
|
||||
#[cfg(posix)]
|
||||
fn should_fmt_path() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Symlink,
|
||||
size: 8192,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/a/b/c/bar.txt",
|
||||
FileType::Symlink,
|
||||
Some(8192),
|
||||
Some("project.info"),
|
||||
None,
|
||||
None,
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("File path: {PATH}");
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry).as_str(),
|
||||
@@ -923,20 +915,16 @@ mod tests {
|
||||
#[cfg(posix)]
|
||||
fn should_fmt_utf8_path() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/a/b/c/россия"),
|
||||
metadata: Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: FileType::Symlink,
|
||||
size: 8192,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/a/b/c/россия",
|
||||
FileType::Symlink,
|
||||
Some(8192),
|
||||
Some("project.info"),
|
||||
None,
|
||||
None,
|
||||
Some(UnixPex::from(0o644)),
|
||||
Some(t),
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("File path: {PATH}");
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry).as_str(),
|
||||
@@ -948,100 +936,88 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_fmt_short_ascii_name() {
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/foo.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: None,
|
||||
created: None,
|
||||
modified: None,
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/foo.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("{NAME:8}");
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "foo.txt ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fmt_exceeding_length_ascii_name() {
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/christian-visintin.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: None,
|
||||
created: None,
|
||||
modified: None,
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/christian-visintin.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("{NAME:8}");
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "christi…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fmt_short_utf8_name() {
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/россия"),
|
||||
metadata: Metadata {
|
||||
accessed: None,
|
||||
created: None,
|
||||
modified: None,
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/россия",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("{NAME:8}");
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "россия ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fmt_long_utf8_name() {
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵"),
|
||||
metadata: Metadata {
|
||||
accessed: None,
|
||||
created: None,
|
||||
modified: None,
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("{NAME:8}");
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fmt_unknown_file_size() {
|
||||
let entry = File::new("/tmp/unknown", Metadata::default());
|
||||
let formatter = Formatter::new("{SIZE}");
|
||||
|
||||
assert_eq!(formatter.fmt(&entry), "? ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_ignore_unknown_formatter_keys() {
|
||||
let entry = File {
|
||||
path: PathBuf::from("/tmp/foo.txt"),
|
||||
metadata: Metadata {
|
||||
accessed: None,
|
||||
created: None,
|
||||
modified: None,
|
||||
file_type: FileType::File,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: None,
|
||||
},
|
||||
};
|
||||
let entry = fixture(
|
||||
"/tmp/foo.txt",
|
||||
FileType::File,
|
||||
Some(8192),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("before {UNKNOWN:12} after {NAME:8}");
|
||||
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "before after foo.txt ");
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mod host_bridge_builder;
|
||||
pub mod params;
|
||||
mod remotefs_builder;
|
||||
mod wrapper;
|
||||
|
||||
// -- export types
|
||||
pub use host_bridge_builder::HostBridgeBuilder;
|
||||
|
||||
@@ -3,23 +3,22 @@
|
||||
//! Remotefs client builder
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use remotefs::RemoteFs;
|
||||
use remotefs_aws_s3::AwsS3Fs;
|
||||
use remotefs_ftp::FtpFs;
|
||||
use remotefs_gcs::credentials::service_account;
|
||||
use remotefs_gcs::{GoogleCloudStorageCredentials, GoogleCloudStorageFs};
|
||||
use remotefs_kube::KubeMultiPodFs as KubeFs;
|
||||
#[cfg(smb)]
|
||||
use remotefs_smb::{PavaoSmbCredentials as SmbCredentials, PavaoSmbFs as SmbFs};
|
||||
use remotefs_kube::KubeMultiPodFs;
|
||||
#[cfg(smb_unix)]
|
||||
use remotefs_smb::{PavaoSmbOptions as SmbOptions, SmbDialect as RemoteSmbDialect};
|
||||
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,
|
||||
NoCheckServerKey, RusshScpFs, RusshSftpFs, SshAgentIdentity, SshConfigParseRule, SshOpts,
|
||||
};
|
||||
use remotefs_webdav::WebDAVFs;
|
||||
|
||||
@@ -30,6 +29,7 @@ use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams
|
||||
#[cfg(smb)]
|
||||
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, SmbParams};
|
||||
use super::params::{KubeProtocolParams, WebDAVProtocolParams};
|
||||
use super::wrapper::RuntimeRemoteFs;
|
||||
use super::{FileTransferProtocol, ProtocolParams};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
@@ -71,7 +71,7 @@ impl RemoteFsBuilder {
|
||||
Ok(Box::new(Self::smb_client(params)?))
|
||||
}
|
||||
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
|
||||
Ok(Box::new(Self::webdav_client(params)))
|
||||
Ok(Box::new(Self::webdav_client(params)?))
|
||||
}
|
||||
(protocol, params) => {
|
||||
error!("Invalid params for protocol '{:?}'", protocol);
|
||||
@@ -83,16 +83,11 @@ impl RemoteFsBuilder {
|
||||
}
|
||||
|
||||
/// Build aws s3 client from parameters
|
||||
fn aws_s3_client(params: AwsS3Params) -> Result<AwsS3Fs, String> {
|
||||
let rt = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||
);
|
||||
let mut client =
|
||||
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
|
||||
fn aws_s3_client(params: AwsS3Params) -> Result<RuntimeRemoteFs, String> {
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
|
||||
let mut client = AwsS3Fs::new(params.bucket_name).new_path_style(params.new_path_style);
|
||||
|
||||
if let Some(region) = params.region {
|
||||
client = client.region(region);
|
||||
}
|
||||
@@ -114,14 +109,15 @@ impl RemoteFsBuilder {
|
||||
if let Some(session_token) = params.session_token {
|
||||
client = client.session_token(session_token);
|
||||
}
|
||||
Ok(client)
|
||||
let client = client.into_blocking(runtime.handle().clone());
|
||||
Ok(RuntimeRemoteFs::new(client, runtime))
|
||||
}
|
||||
|
||||
/// Build a Google Cloud Storage client from parameters.
|
||||
fn gcs_client(params: GoogleCloudStorageParams) -> Result<GoogleCloudStorageFs, String> {
|
||||
fn gcs_client(params: GoogleCloudStorageParams) -> Result<RuntimeRemoteFs, String> {
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let mut client = match params.service_account_key {
|
||||
None => GoogleCloudStorageFs::new(params.bucket_name, &runtime),
|
||||
None => GoogleCloudStorageFs::new(params.bucket_name),
|
||||
Some(path) => {
|
||||
let raw = std::fs::read_to_string(&path).map_err(|error| {
|
||||
format!("Unable to read GCS service-account file '{path}': {error}")
|
||||
@@ -139,12 +135,12 @@ impl RemoteFsBuilder {
|
||||
GoogleCloudStorageFs::with_credentials(
|
||||
params.bucket_name,
|
||||
GoogleCloudStorageCredentials::custom(credentials),
|
||||
&runtime,
|
||||
)
|
||||
}
|
||||
};
|
||||
client = client.endpoint(params.endpoint);
|
||||
Ok(client)
|
||||
let client = client.into_blocking(runtime.handle().clone());
|
||||
Ok(RuntimeRemoteFs::new(client, runtime))
|
||||
}
|
||||
|
||||
/// Build ftp client from parameters
|
||||
@@ -163,34 +159,42 @@ impl RemoteFsBuilder {
|
||||
}
|
||||
|
||||
/// Build kube client
|
||||
fn kube_client(params: KubeProtocolParams) -> Result<KubeFs, String> {
|
||||
let rt = Self::tokio_runtime()?;
|
||||
let kube_fs = KubeFs::new(&rt);
|
||||
if let Some(config) = params.config() {
|
||||
Ok(kube_fs.config(config))
|
||||
fn kube_client(params: KubeProtocolParams) -> Result<RuntimeRemoteFs, String> {
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let kube_fs = if let Some(config) = params.config() {
|
||||
KubeMultiPodFs::new().config(config)
|
||||
} else {
|
||||
Ok(kube_fs)
|
||||
}
|
||||
KubeMultiPodFs::new()
|
||||
};
|
||||
|
||||
let client = kube_fs.into_blocking(runtime.handle().clone());
|
||||
Ok(RuntimeRemoteFs::new(client, runtime))
|
||||
}
|
||||
|
||||
/// Build scp client
|
||||
fn scp_client(
|
||||
params: GenericProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<ScpFs<SshSession<NoCheckServerKey>>, String> {
|
||||
) -> Result<RuntimeRemoteFs, String> {
|
||||
let opts = Self::build_ssh_opts(params, config_client);
|
||||
let rt = Self::tokio_runtime()?;
|
||||
Ok(ScpFs::russh(opts, rt))
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let client =
|
||||
RusshScpFs::<NoCheckServerKey>::new(opts).into_blocking(runtime.handle().clone());
|
||||
|
||||
Ok(RuntimeRemoteFs::new(client, runtime))
|
||||
}
|
||||
|
||||
/// Build sftp client
|
||||
fn sftp_client(
|
||||
params: GenericProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Result<SftpFs<SshSession<NoCheckServerKey>>, String> {
|
||||
) -> Result<RuntimeRemoteFs, String> {
|
||||
let opts = Self::build_ssh_opts(params, config_client);
|
||||
let rt = Self::tokio_runtime()?;
|
||||
Ok(SftpFs::russh(opts, rt))
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let client =
|
||||
RusshSftpFs::<NoCheckServerKey>::new(opts).into_blocking(runtime.handle().clone());
|
||||
|
||||
Ok(RuntimeRemoteFs::new(client, runtime))
|
||||
}
|
||||
|
||||
/// Maps the user-facing SMB family to inclusive remotefs dialect bounds.
|
||||
@@ -250,8 +254,16 @@ impl RemoteFsBuilder {
|
||||
Ok(SmbFs::new(credentials))
|
||||
}
|
||||
|
||||
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
|
||||
WebDAVFs::new(¶ms.username, ¶ms.password, ¶ms.uri)
|
||||
fn webdav_client(params: WebDAVProtocolParams) -> Result<RuntimeRemoteFs, String> {
|
||||
let runtime = Self::tokio_runtime()?;
|
||||
let client = WebDAVFs::new(
|
||||
¶ms.uri,
|
||||
remotefs_webdav::Auth::basic(params.username, params.password),
|
||||
)
|
||||
.map_err(|e| format!("failed to create WebDAV client: {e}"))?;
|
||||
|
||||
let client = client.into_blocking(runtime.handle().clone());
|
||||
Ok(RuntimeRemoteFs::new(client, runtime))
|
||||
}
|
||||
|
||||
/// Build ssh options from generic protocol params and client configuration
|
||||
@@ -287,14 +299,12 @@ impl RemoteFsBuilder {
|
||||
}
|
||||
|
||||
/// Create tokio runtime to run async code for remotefs
|
||||
fn tokio_runtime() -> Result<Arc<tokio::runtime::Runtime>, String> {
|
||||
Ok(Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
|
||||
))
|
||||
fn tokio_runtime() -> Result<tokio::runtime::Runtime, String> {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to create tokio runtime: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Runtime-owning wrapper for asynchronous remote filesystem adapters.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use remotefs::fs::{
|
||||
Capabilities, ExecOutput, ReadOptions, ReadStream, SetMetadata, UnixPex, WriteOptions,
|
||||
WriteStream,
|
||||
};
|
||||
use remotefs::{File, RemoteFs, RemoteResult};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
pub(super) struct RuntimeRemoteFs {
|
||||
// Fields are dropped in declaration order, so the client cannot outlive its runtime.
|
||||
remote: Box<dyn RemoteFs>,
|
||||
_runtime: Runtime,
|
||||
}
|
||||
|
||||
impl RuntimeRemoteFs {
|
||||
pub(super) fn new(remote: impl RemoteFs + 'static, runtime: Runtime) -> Self {
|
||||
Self {
|
||||
remote: Box::new(remote),
|
||||
_runtime: runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteFs for RuntimeRemoteFs {
|
||||
fn connect(&mut self) -> RemoteResult<()> {
|
||||
self.remote.connect()
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> RemoteResult<()> {
|
||||
self.remote.disconnect()
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.remote.is_connected()
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Capabilities {
|
||||
self.remote.capabilities()
|
||||
}
|
||||
|
||||
fn list_dir(&self, path: &Path) -> RemoteResult<Vec<File>> {
|
||||
self.remote.list_dir(path)
|
||||
}
|
||||
|
||||
fn stat(&self, path: &Path) -> RemoteResult<File> {
|
||||
self.remote.stat(path)
|
||||
}
|
||||
|
||||
fn exists(&self, path: &Path) -> RemoteResult<bool> {
|
||||
self.remote.exists(path)
|
||||
}
|
||||
|
||||
fn set_metadata(&self, path: &Path, metadata: &SetMetadata) -> RemoteResult<()> {
|
||||
self.remote.set_metadata(path, metadata)
|
||||
}
|
||||
|
||||
fn create_dir(&self, path: &Path, mode: Option<UnixPex>) -> RemoteResult<()> {
|
||||
self.remote.create_dir(path, mode)
|
||||
}
|
||||
|
||||
fn remove_file(&self, path: &Path) -> RemoteResult<()> {
|
||||
self.remote.remove_file(path)
|
||||
}
|
||||
|
||||
fn remove_dir(&self, path: &Path) -> RemoteResult<()> {
|
||||
self.remote.remove_dir(path)
|
||||
}
|
||||
|
||||
fn remove_dir_all(&self, path: &Path) -> RemoteResult<()> {
|
||||
self.remote.remove_dir_all(path)
|
||||
}
|
||||
|
||||
fn rename(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
|
||||
self.remote.rename(src, dest)
|
||||
}
|
||||
|
||||
fn copy(&self, src: &Path, dest: &Path) -> RemoteResult<()> {
|
||||
self.remote.copy(src, dest)
|
||||
}
|
||||
|
||||
fn symlink(&self, path: &Path, target: &Path) -> RemoteResult<()> {
|
||||
self.remote.symlink(path, target)
|
||||
}
|
||||
|
||||
fn open(&self, path: &Path, opts: &ReadOptions) -> RemoteResult<ReadStream> {
|
||||
self.remote.open(path, opts)
|
||||
}
|
||||
|
||||
fn create(&self, path: &Path, opts: &WriteOptions) -> RemoteResult<WriteStream> {
|
||||
self.remote.create(path, opts)
|
||||
}
|
||||
|
||||
fn append(&self, path: &Path, opts: &WriteOptions) -> RemoteResult<WriteStream> {
|
||||
self.remote.append(path, opts)
|
||||
}
|
||||
|
||||
fn read_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
opts: &ReadOptions,
|
||||
dest: &mut (dyn Write + Send),
|
||||
) -> RemoteResult<u64> {
|
||||
self.remote.read_file(path, opts, dest)
|
||||
}
|
||||
|
||||
fn write_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
opts: &WriteOptions,
|
||||
src: &mut (dyn Read + Send),
|
||||
) -> RemoteResult<u64> {
|
||||
self.remote.write_file(path, opts, src)
|
||||
}
|
||||
|
||||
fn append_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
opts: &WriteOptions,
|
||||
src: &mut (dyn Read + Send),
|
||||
) -> RemoteResult<u64> {
|
||||
self.remote.append_file(path, opts, src)
|
||||
}
|
||||
|
||||
fn exec(&self, cmd: &str) -> RemoteResult<ExecOutput> {
|
||||
self.remote.exec(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use remotefs_ftp::FtpFs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_remote_fs_keeps_runtime_alive() {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let handle = runtime.handle().clone();
|
||||
let remote = RuntimeRemoteFs::new(FtpFs::new("127.0.0.1", 21), runtime);
|
||||
|
||||
let task = handle.spawn(async { 42 });
|
||||
|
||||
assert_eq!(handle.block_on(task).unwrap(), 42);
|
||||
drop(remote);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -11,7 +11,8 @@ use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
// Locals
|
||||
pub use self::bridge::HostBridge;
|
||||
#[doc(inline)]
|
||||
pub use self::bridge::{HostBridge, HostReader, HostWriter};
|
||||
pub use self::localhost::Localhost;
|
||||
pub use self::remote_bridged::RemoteBridged;
|
||||
|
||||
@@ -48,6 +49,7 @@ pub enum HostErrorType {
|
||||
/// HostError is a wrapper for the error type and the exact io error
|
||||
#[derive(Debug, Error)]
|
||||
pub struct HostError {
|
||||
#[source]
|
||||
pub error: HostErrorType,
|
||||
ioerr: Option<std::io::Error>,
|
||||
path: Option<PathBuf>,
|
||||
|
||||
+213
-8
@@ -3,7 +3,8 @@
|
||||
//! Defines the host abstraction used to expose localhost and bridged remote
|
||||
//! filesystems through a shared interface.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::fmt;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use remotefs::File;
|
||||
@@ -11,6 +12,111 @@ use remotefs::fs::{Metadata, UnixPex};
|
||||
|
||||
use super::HostResult;
|
||||
|
||||
enum HostReaderInner {
|
||||
Io(Box<dyn Read + Send>),
|
||||
Remote(remotefs::fs::ReadStream),
|
||||
}
|
||||
|
||||
/// An owned host reader that optionally retains a remote transfer finalizer.
|
||||
pub struct HostReader(HostReaderInner);
|
||||
|
||||
impl HostReader {
|
||||
pub(crate) fn io<T>(reader: T) -> Self
|
||||
where
|
||||
T: Read + Send + 'static,
|
||||
{
|
||||
Self(HostReaderInner::Io(Box::new(reader)))
|
||||
}
|
||||
|
||||
pub(crate) fn remote(reader: remotefs::fs::ReadStream) -> Self {
|
||||
Self(HostReaderInner::Remote(reader))
|
||||
}
|
||||
|
||||
/// Completes the remote read and consumes this reader.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the remote stream finalization error, if the reader is backed
|
||||
/// by a remote stream.
|
||||
pub fn finish(self) -> super::HostResult<()> {
|
||||
match self.0 {
|
||||
HostReaderInner::Io(_) => Ok(()),
|
||||
HostReaderInner::Remote(reader) => reader.finish().map_err(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HostReader {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("HostReader").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for HostReader {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
match &mut self.0 {
|
||||
HostReaderInner::Io(reader) => reader.read(buffer),
|
||||
HostReaderInner::Remote(reader) => reader.read(buffer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum HostWriterInner {
|
||||
Io(Box<dyn Write + Send>),
|
||||
Remote(remotefs::fs::WriteStream),
|
||||
}
|
||||
|
||||
/// An owned host writer that optionally retains a remote transfer finalizer.
|
||||
pub struct HostWriter(HostWriterInner);
|
||||
|
||||
impl HostWriter {
|
||||
pub(crate) fn io<T>(writer: T) -> Self
|
||||
where
|
||||
T: Write + Send + 'static,
|
||||
{
|
||||
Self(HostWriterInner::Io(Box::new(writer)))
|
||||
}
|
||||
|
||||
pub(crate) fn remote(writer: remotefs::fs::WriteStream) -> Self {
|
||||
Self(HostWriterInner::Remote(writer))
|
||||
}
|
||||
|
||||
/// Completes the remote write and consumes this writer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the remote stream finalization error, if the writer is backed
|
||||
/// by a remote stream.
|
||||
pub fn finish(self) -> super::HostResult<()> {
|
||||
match self.0 {
|
||||
HostWriterInner::Io(_) => Ok(()),
|
||||
HostWriterInner::Remote(writer) => writer.finish().map_err(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HostWriter {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("HostWriter").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for HostWriter {
|
||||
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
|
||||
match &mut self.0 {
|
||||
HostWriterInner::Io(writer) => writer.write(buffer),
|
||||
HostWriterInner::Remote(writer) => writer.write(buffer),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match &mut self.0 {
|
||||
HostWriterInner::Io(writer) => writer.flush(),
|
||||
HostWriterInner::Remote(writer) => writer.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to bridge a remote filesystem to the host filesystem
|
||||
///
|
||||
/// In case of `Localhost` this should be effortless, while for remote hosts this should
|
||||
@@ -75,15 +181,114 @@ pub trait HostBridge {
|
||||
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>;
|
||||
|
||||
/// Open file for reading
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>>;
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<HostReader>;
|
||||
|
||||
/// Open file for writing
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>>;
|
||||
fn create_file(&mut self, file: &Path, metadata: &Metadata) -> HostResult<HostWriter>;
|
||||
|
||||
/// Finalize write operation
|
||||
fn finalize_write(&mut self, writer: Box<dyn Write + Send>) -> HostResult<()>;
|
||||
fn finalize_write(&mut self, writer: HostWriter) -> HostResult<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use remotefs::fs::{ReadStream, RemoteRead, RemoteWrite, WriteStream};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct TrackedReader {
|
||||
reader: Cursor<Vec<u8>>,
|
||||
finishes: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Read for TrackedReader {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.reader.read(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteRead for TrackedReader {
|
||||
fn finish(self: Box<Self>) -> remotefs::RemoteResult<()> {
|
||||
self.finishes.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct TrackedWriter {
|
||||
writer: Cursor<Vec<u8>>,
|
||||
finishes: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Write for TrackedWriter {
|
||||
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
|
||||
self.writer.write(buffer)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteWrite for TrackedWriter {
|
||||
fn finish(self: Box<Self>) -> remotefs::RemoteResult<()> {
|
||||
self.finishes.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_reader_finishes_exactly_once_when_consumed() {
|
||||
let finishes = Arc::new(AtomicUsize::new(0));
|
||||
let reader = HostReader::remote(ReadStream::new(TrackedReader {
|
||||
reader: Cursor::new(Vec::new()),
|
||||
finishes: finishes.clone(),
|
||||
}));
|
||||
|
||||
reader.finish().unwrap();
|
||||
|
||||
assert_eq!(finishes.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_writer_finishes_exactly_once_when_consumed() {
|
||||
let finishes = Arc::new(AtomicUsize::new(0));
|
||||
let writer = HostWriter::remote(WriteStream::new(TrackedWriter {
|
||||
writer: Cursor::new(Vec::new()),
|
||||
finishes: finishes.clone(),
|
||||
}));
|
||||
|
||||
writer.finish().unwrap();
|
||||
|
||||
assert_eq!(finishes.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfinished_remote_reader_is_dropped_without_finalizing() {
|
||||
let finishes = Arc::new(AtomicUsize::new(0));
|
||||
let reader = HostReader::remote(ReadStream::new(TrackedReader {
|
||||
reader: Cursor::new(Vec::new()),
|
||||
finishes: finishes.clone(),
|
||||
}));
|
||||
|
||||
drop(reader);
|
||||
|
||||
assert_eq!(finishes.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfinished_remote_writer_is_dropped_without_finalizing() {
|
||||
let finishes = Arc::new(AtomicUsize::new(0));
|
||||
let writer = HostWriter::remote(WriteStream::new(TrackedWriter {
|
||||
writer: Cursor::new(Vec::new()),
|
||||
finishes: finishes.clone(),
|
||||
}));
|
||||
|
||||
drop(writer);
|
||||
|
||||
assert_eq!(finishes.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-14
@@ -4,7 +4,7 @@
|
||||
//! filesystem.
|
||||
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Write;
|
||||
#[cfg(posix)]
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -13,7 +13,7 @@ use filetime::FileTime;
|
||||
use remotefs::File;
|
||||
use remotefs::fs::{FileType, Metadata, UnixPex};
|
||||
|
||||
use super::{HostBridge, HostResult};
|
||||
use super::{HostBridge, HostReader, HostResult, HostWriter};
|
||||
use crate::host::{HostError, HostErrorType};
|
||||
use crate::utils::path;
|
||||
|
||||
@@ -368,7 +368,7 @@ impl HostBridge for Localhost {
|
||||
metadata.file_type = FileType::Symlink;
|
||||
}
|
||||
// Match dir / file
|
||||
Ok(File { path, metadata })
|
||||
Ok(File::new(path, metadata))
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||
@@ -520,7 +520,7 @@ impl HostBridge for Localhost {
|
||||
Err(HostError::from(HostErrorType::NotImplemented))
|
||||
}
|
||||
|
||||
fn open_file(&mut self, file: &std::path::Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
fn open_file(&mut self, file: &std::path::Path) -> HostResult<HostReader> {
|
||||
let file: PathBuf = self.to_path(file);
|
||||
info!("Opening file {} for read", file.display());
|
||||
if !self.exists(file.as_path())? {
|
||||
@@ -537,7 +537,7 @@ impl HostBridge for Localhost {
|
||||
.write(false)
|
||||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(Box::new(f)),
|
||||
Ok(f) => Ok(HostReader::io(f)),
|
||||
Err(err) => {
|
||||
error!("Could not open file for read: {}", err);
|
||||
Err(HostError::new(
|
||||
@@ -549,11 +549,7 @@ impl HostBridge for Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
_metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>> {
|
||||
fn create_file(&mut self, file: &Path, _metadata: &Metadata) -> HostResult<HostWriter> {
|
||||
let file: PathBuf = self.to_path(file);
|
||||
info!("Opening file {} for write", file.display());
|
||||
match OpenOptions::new()
|
||||
@@ -562,7 +558,7 @@ impl HostBridge for Localhost {
|
||||
.truncate(true)
|
||||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(Box::new(f)),
|
||||
Ok(f) => Ok(HostWriter::io(f)),
|
||||
Err(err) => {
|
||||
error!("Failed to open file: {}", err);
|
||||
match self.exists(file.as_path())? {
|
||||
@@ -581,9 +577,11 @@ impl HostBridge for Localhost {
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
|
||||
// no-op
|
||||
Ok(())
|
||||
fn finalize_write(&mut self, mut writer: HostWriter) -> HostResult<()> {
|
||||
writer.flush().map_err(|error| {
|
||||
HostError::new(HostErrorType::FileNotAccessible, Some(error), Path::new(""))
|
||||
})?;
|
||||
writer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+238
-89
@@ -5,19 +5,19 @@
|
||||
|
||||
mod temp_mapped_file;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use remotefs::fs::{Metadata, UnixPex};
|
||||
use remotefs::fs::{Capabilities, Metadata, ReadOptions, SetMetadata, UnixPex, WriteOptions};
|
||||
use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs};
|
||||
|
||||
use self::temp_mapped_file::TempMappedFile;
|
||||
use super::{HostBridge, HostError, HostResult};
|
||||
use super::{HostBridge, HostError, HostErrorType, HostReader, HostResult, HostWriter};
|
||||
use crate::utils::path::normalize;
|
||||
|
||||
struct WriteStreamOp {
|
||||
path: PathBuf,
|
||||
metadata: Metadata,
|
||||
options: WriteOptions,
|
||||
tempfile: TempMappedFile,
|
||||
}
|
||||
|
||||
@@ -25,30 +25,81 @@ struct WriteStreamOp {
|
||||
pub struct RemoteBridged {
|
||||
/// Remote fs client
|
||||
remote: Box<dyn RemoteFs>,
|
||||
/// Consumer-owned remote working directory used to resolve relative paths
|
||||
wrkdir: PathBuf,
|
||||
/// Reminder used to finalize write stream
|
||||
write_stream_op: Option<WriteStreamOp>,
|
||||
}
|
||||
|
||||
impl RemoteBridged {
|
||||
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
fn open_file_from_temp(&mut self, file: &Path) -> HostResult<HostReader> {
|
||||
let mut temp_file = TempMappedFile::new()?;
|
||||
|
||||
self.remote
|
||||
.open_file(file, Box::new(temp_file.clone()))
|
||||
.read_file(file, &ReadOptions::default(), &mut temp_file)
|
||||
.map_err(HostError::from)?;
|
||||
|
||||
// Sync changes
|
||||
temp_file.sync()?;
|
||||
|
||||
// now return as read
|
||||
Ok(Box::new(temp_file))
|
||||
Ok(HostReader::io(temp_file))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_remote_path(wrkdir: &Path, target: &Path) -> HostResult<PathBuf> {
|
||||
let resolved = if remotefs::path::ensure_absolute(target).is_ok() {
|
||||
target.to_path_buf()
|
||||
} else {
|
||||
wrkdir.join(target)
|
||||
};
|
||||
remotefs::path::ensure_absolute(&resolved).map_err(HostError::from)?;
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn write_options(metadata: &Metadata) -> WriteOptions {
|
||||
let mut options = WriteOptions::default();
|
||||
if let Some(size) = metadata.size {
|
||||
options = options.size_hint(size);
|
||||
}
|
||||
if let Some(mode) = metadata.mode {
|
||||
options = options.mode(mode);
|
||||
}
|
||||
if let Some(modified) = metadata.modified {
|
||||
options = options.modified(modified);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn set_metadata_options(metadata: &Metadata) -> SetMetadata {
|
||||
let mut options = SetMetadata::default();
|
||||
if let Some(mode) = metadata.mode {
|
||||
options = options.mode(mode);
|
||||
}
|
||||
if let Some(uid) = metadata.uid {
|
||||
options = options.uid(uid);
|
||||
}
|
||||
if let Some(gid) = metadata.gid {
|
||||
options = options.gid(gid);
|
||||
}
|
||||
if let Some(accessed) = metadata.accessed {
|
||||
options = options.accessed(accessed);
|
||||
}
|
||||
if let Some(modified) = metadata.modified {
|
||||
options = options.modified(modified);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn map_io_error(error: std::io::Error) -> HostError {
|
||||
HostError::new(HostErrorType::FileNotAccessible, Some(error), Path::new(""))
|
||||
}
|
||||
|
||||
impl From<Box<dyn RemoteFs>> for RemoteBridged {
|
||||
fn from(remote: Box<dyn RemoteFs>) -> Self {
|
||||
RemoteBridged {
|
||||
remote,
|
||||
wrkdir: PathBuf::from("/"),
|
||||
write_stream_op: None,
|
||||
}
|
||||
}
|
||||
@@ -56,7 +107,7 @@ impl From<Box<dyn RemoteFs>> for RemoteBridged {
|
||||
|
||||
impl HostBridge for RemoteBridged {
|
||||
fn connect(&mut self) -> HostResult<()> {
|
||||
self.remote.connect().map(|_| ()).map_err(HostError::from)
|
||||
self.remote.connect().map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> HostResult<()> {
|
||||
@@ -73,71 +124,85 @@ impl HostBridge for RemoteBridged {
|
||||
|
||||
fn pwd(&mut self) -> HostResult<PathBuf> {
|
||||
debug!("Getting working directory");
|
||||
self.remote.pwd().map_err(HostError::from)
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
|
||||
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {
|
||||
debug!("Changing working directory to {:?}", new_dir);
|
||||
self.remote.change_dir(new_dir).map_err(HostError::from)
|
||||
let new_dir = resolve_remote_path(&self.wrkdir, new_dir)?;
|
||||
let entry = self.remote.stat(&new_dir).map_err(HostError::from)?;
|
||||
if !entry.is_dir() {
|
||||
return Err(HostError::from(RemoteError::new(RemoteErrorType::BadFile)));
|
||||
}
|
||||
self.wrkdir = new_dir;
|
||||
Ok(self.wrkdir.clone())
|
||||
}
|
||||
|
||||
fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> {
|
||||
debug!("Creating directory {:?}", dir_name);
|
||||
match self.remote.create_dir(dir_name, UnixPex::from(0o755)) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(remotefs::RemoteError {
|
||||
kind: RemoteErrorType::DirectoryAlreadyExists,
|
||||
..
|
||||
}) if ignore_existing => Ok(()),
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
let path = resolve_remote_path(&self.wrkdir, dir_name)?;
|
||||
match self.remote.create_dir(&path, Some(UnixPex::from(0o755))) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if ignore_existing && error.kind() == RemoteErrorType::AlreadyExists => {
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => Err(HostError::from(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, entry: &File) -> HostResult<()> {
|
||||
debug!("Removing {:?}", entry.path());
|
||||
let path = resolve_remote_path(&self.wrkdir, entry.path())?;
|
||||
if entry.is_dir() {
|
||||
self.remote
|
||||
.remove_dir_all(entry.path())
|
||||
.map_err(HostError::from)
|
||||
self.remote.remove_dir_all(&path).map_err(HostError::from)
|
||||
} else {
|
||||
self.remote
|
||||
.remove_file(entry.path())
|
||||
.map_err(HostError::from)
|
||||
self.remote.remove_file(&path).map_err(HostError::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> {
|
||||
debug!("Renaming {:?} to {:?}", entry.path(), dst_path);
|
||||
let source = resolve_remote_path(&self.wrkdir, entry.path())?;
|
||||
let destination = resolve_remote_path(&self.wrkdir, dst_path)?;
|
||||
self.remote
|
||||
.mov(entry.path(), dst_path)
|
||||
.rename(&source, &destination)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> {
|
||||
debug!("Copying {:?} to {:?}", entry.path(), dst);
|
||||
self.remote.copy(entry.path(), dst).map_err(HostError::from)
|
||||
let source = resolve_remote_path(&self.wrkdir, entry.path())?;
|
||||
let destination = resolve_remote_path(&self.wrkdir, dst)?;
|
||||
self.remote
|
||||
.copy(&source, &destination)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn stat(&mut self, path: &Path) -> HostResult<File> {
|
||||
debug!("Statting {:?}", path);
|
||||
self.remote.stat(path).map_err(HostError::from)
|
||||
let path = resolve_remote_path(&self.wrkdir, path)?;
|
||||
self.remote.stat(&path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> HostResult<bool> {
|
||||
debug!("Checking existence of {:?}", path);
|
||||
self.remote.exists(path).map_err(HostError::from)
|
||||
let path = resolve_remote_path(&self.wrkdir, path)?;
|
||||
self.remote.exists(&path).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> HostResult<Vec<File>> {
|
||||
debug!("Listing directory {:?}", path);
|
||||
let entries = self.remote.list_dir(path).map_err(HostError::from)?;
|
||||
Ok(filter_self_refs(path, entries))
|
||||
let path = resolve_remote_path(&self.wrkdir, path)?;
|
||||
let entries = self.remote.list_dir(&path).map_err(HostError::from)?;
|
||||
Ok(filter_self_refs(&path, entries))
|
||||
}
|
||||
|
||||
fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> {
|
||||
debug!("Setting metadata for {:?}", path);
|
||||
let path = resolve_remote_path(&self.wrkdir, path)?;
|
||||
let options = set_metadata_options(metadata);
|
||||
self.remote
|
||||
.setstat(path, metadata.clone())
|
||||
.set_metadata(&path, &options)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
@@ -145,78 +210,108 @@ impl HostBridge for RemoteBridged {
|
||||
debug!("Executing command: {}", cmd);
|
||||
self.remote
|
||||
.exec(cmd)
|
||||
.map(|(_, stdout)| stdout)
|
||||
.map(|output| output.stdout)
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> {
|
||||
debug!("Creating symlink from {:?} to {:?}", src, dst);
|
||||
self.remote.symlink(src, dst).map_err(HostError::from)
|
||||
let path = resolve_remote_path(&self.wrkdir, src)?;
|
||||
let target = resolve_remote_path(&self.wrkdir, dst)?;
|
||||
self.remote.symlink(&path, &target).map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> {
|
||||
debug!("Changing permissions of {:?} to {:?}", path, pex);
|
||||
let stat = self.remote.stat(path).map_err(HostError::from)?;
|
||||
let mut metadata = stat.metadata.clone();
|
||||
metadata.mode = Some(pex);
|
||||
|
||||
self.setstat(path, &metadata)
|
||||
let path = resolve_remote_path(&self.wrkdir, path)?;
|
||||
self.remote
|
||||
.set_metadata(&path, &SetMetadata::default().mode(pex))
|
||||
.map_err(HostError::from)
|
||||
}
|
||||
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<Box<dyn Read + Send>> {
|
||||
// try to use stream, otherwise download to a temporary file and return a reader
|
||||
match self.remote.open(file) {
|
||||
Ok(stream) => Ok(Box::new(stream)),
|
||||
Err(RemoteError {
|
||||
kind: RemoteErrorType::UnsupportedFeature,
|
||||
..
|
||||
}) => self.open_file_from_temp(file),
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file(
|
||||
&mut self,
|
||||
file: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> HostResult<Box<dyn Write + Send>> {
|
||||
// try to use stream, otherwise download to a temporary file and return a reader
|
||||
match self.remote.create(file, metadata) {
|
||||
Ok(stream) => Ok(Box::new(stream)),
|
||||
Err(RemoteError {
|
||||
kind: RemoteErrorType::UnsupportedFeature,
|
||||
..
|
||||
}) => {
|
||||
let tempfile = TempMappedFile::new()?;
|
||||
self.write_stream_op = Some(WriteStreamOp {
|
||||
path: file.to_path_buf(),
|
||||
metadata: metadata.clone(),
|
||||
tempfile: tempfile.clone(),
|
||||
});
|
||||
|
||||
Ok(Box::new(tempfile))
|
||||
fn open_file(&mut self, file: &Path) -> HostResult<HostReader> {
|
||||
let path = resolve_remote_path(&self.wrkdir, file)?;
|
||||
if self
|
||||
.remote
|
||||
.capabilities()
|
||||
.contains(Capabilities::STREAM_READ)
|
||||
{
|
||||
match self.remote.open(&path, &ReadOptions::default()) {
|
||||
Ok(stream) => Ok(HostReader::remote(stream)),
|
||||
Err(error) if error.kind() == RemoteErrorType::UnsupportedFeature => {
|
||||
self.open_file_from_temp(&path)
|
||||
}
|
||||
Err(error) => Err(HostError::from(error)),
|
||||
}
|
||||
Err(e) => Err(HostError::from(e)),
|
||||
} else {
|
||||
self.open_file_from_temp(&path)
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_write(&mut self, _writer: Box<dyn Write + Send>) -> HostResult<()> {
|
||||
fn create_file(&mut self, file: &Path, metadata: &Metadata) -> HostResult<HostWriter> {
|
||||
let path = resolve_remote_path(&self.wrkdir, file)?;
|
||||
let options = write_options(metadata);
|
||||
self.write_stream_op = None;
|
||||
if self
|
||||
.remote
|
||||
.capabilities()
|
||||
.contains(Capabilities::STREAM_WRITE)
|
||||
{
|
||||
match self.remote.create(&path, &options) {
|
||||
Ok(stream) => Ok(HostWriter::remote(stream)),
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
RemoteErrorType::SizeRequired | RemoteErrorType::UnsupportedFeature
|
||||
) =>
|
||||
{
|
||||
self.create_file_from_temp(path, options)
|
||||
}
|
||||
Err(error) => Err(HostError::from(error)),
|
||||
}
|
||||
} else {
|
||||
self.create_file_from_temp(path, options)
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_write(&mut self, mut writer: HostWriter) -> HostResult<()> {
|
||||
writer.flush().map_err(map_io_error)?;
|
||||
writer.finish()?;
|
||||
if let Some(WriteStreamOp {
|
||||
path,
|
||||
metadata,
|
||||
mut options,
|
||||
mut tempfile,
|
||||
}) = self.write_stream_op.take()
|
||||
{
|
||||
// sync
|
||||
tempfile.sync()?;
|
||||
options = options.size_hint(tempfile.len()?);
|
||||
// write file
|
||||
self.remote
|
||||
.create_file(&path, &metadata, Box::new(tempfile))?;
|
||||
.write_file(&path, &options, &mut tempfile)
|
||||
.map_err(HostError::from)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteBridged {
|
||||
fn create_file_from_temp(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
options: WriteOptions,
|
||||
) -> HostResult<HostWriter> {
|
||||
let tempfile = TempMappedFile::new()?;
|
||||
self.write_stream_op = Some(WriteStreamOp {
|
||||
path,
|
||||
options,
|
||||
tempfile: tempfile.clone(),
|
||||
});
|
||||
|
||||
Ok(HostWriter::io(tempfile))
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop entries that refer to the directory being listed.
|
||||
///
|
||||
/// Some non-compliant FTP servers (e.g. LiteSpeed) include a self-reference
|
||||
@@ -236,31 +331,85 @@ fn filter_self_refs(path: &Path, entries: Vec<File>) -> Vec<File> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use remotefs::fs::{FileType, Metadata};
|
||||
use remotefs::fs::{FileType, Metadata, UnixPex};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn file(path: &str, file_type: FileType) -> File {
|
||||
File {
|
||||
path: PathBuf::from(path),
|
||||
metadata: Metadata {
|
||||
accessed: Some(SystemTime::UNIX_EPOCH),
|
||||
created: Some(SystemTime::UNIX_EPOCH),
|
||||
modified: Some(SystemTime::UNIX_EPOCH),
|
||||
file_type,
|
||||
gid: None,
|
||||
mode: None,
|
||||
size: 0,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
},
|
||||
File::new(
|
||||
path,
|
||||
Metadata::default()
|
||||
.accessed(UNIX_EPOCH)
|
||||
.created(UNIX_EPOCH)
|
||||
.modified(UNIX_EPOCH)
|
||||
.file_type(file_type)
|
||||
.size(0),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_relative_paths_from_consumer_working_directory() {
|
||||
assert_eq!(
|
||||
resolve_remote_path(Path::new("/home/user"), Path::new("docs/report.txt")).unwrap(),
|
||||
Path::new("/home/user/docs/report.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_absolute_remote_paths() {
|
||||
for path in ["/srv/data", r"C:\data\file", r"\\server\share\file"] {
|
||||
assert_eq!(
|
||||
resolve_remote_path(Path::new("/ignored"), Path::new(path)).unwrap(),
|
||||
Path::new(path)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_metadata_to_write_options() {
|
||||
let modified = SystemTime::UNIX_EPOCH;
|
||||
let mode = UnixPex::from(0o640);
|
||||
let options = write_options(&Metadata::default().size(42).mode(mode).modified(modified));
|
||||
|
||||
assert_eq!(options.size_hint, Some(42));
|
||||
assert_eq!(options.mode, Some(mode));
|
||||
assert_eq!(options.modified, Some(modified));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_settable_metadata_fields() {
|
||||
let accessed = SystemTime::UNIX_EPOCH;
|
||||
let modified = accessed + std::time::Duration::from_secs(1);
|
||||
let mode = UnixPex::from(0o640);
|
||||
let options = set_metadata_options(
|
||||
&Metadata::default()
|
||||
.accessed(accessed)
|
||||
.gid(20)
|
||||
.mode(mode)
|
||||
.modified(modified)
|
||||
.uid(10),
|
||||
);
|
||||
|
||||
assert_eq!(options.accessed, Some(accessed));
|
||||
assert_eq!(options.gid, Some(20));
|
||||
assert_eq!(options.mode, Some(mode));
|
||||
assert_eq!(options.modified, Some(modified));
|
||||
assert_eq!(options.uid, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_metadata_maps_to_empty_options() {
|
||||
assert_eq!(write_options(&Metadata::default()), Default::default());
|
||||
assert_eq!(
|
||||
set_metadata_options(&Metadata::default()),
|
||||
Default::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_self_refs_drops_entry_matching_listed_dir() {
|
||||
let entries = vec![
|
||||
|
||||
@@ -82,6 +82,19 @@ impl TempMappedFile {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the number of bytes currently stored in the temporary file.
|
||||
pub fn len(&self) -> HostResult<u64> {
|
||||
std::fs::metadata(self.tempfile.path())
|
||||
.map(|metadata| metadata.len())
|
||||
.map_err(|error| {
|
||||
HostError::new(
|
||||
HostErrorType::FileNotAccessible,
|
||||
Some(error),
|
||||
self.tempfile.path(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn write_hnd(&mut self) -> io::Result<FileHandle<'_>> {
|
||||
let mut lock = self.lock_handle()?;
|
||||
if lock.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)
|
||||
@@ -180,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(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -107,10 +107,12 @@ impl FileTransferActivity {
|
||||
}
|
||||
};
|
||||
|
||||
let new_file_size = match std::io::copy(&mut reader, &mut writer) {
|
||||
Err(err) => return Err(format!("Could not write file: {err}")),
|
||||
Ok(size) => size,
|
||||
};
|
||||
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
|
||||
return Err(format!("Could not write file: {err}"));
|
||||
}
|
||||
reader
|
||||
.finish()
|
||||
.map_err(|err| format!("Could not finish file read: {err}"))?;
|
||||
|
||||
// edit file
|
||||
|
||||
@@ -126,10 +128,11 @@ impl FileTransferActivity {
|
||||
};
|
||||
let mut writer = match self.browser.local_pane_mut().fs.create_file(
|
||||
entry.path(),
|
||||
&Metadata {
|
||||
size: new_file_size,
|
||||
..Default::default()
|
||||
},
|
||||
&Metadata::default().size(
|
||||
std::fs::metadata(tempfile.as_path())
|
||||
.map_err(|err| format!("Could not stat edited file: {err}"))?
|
||||
.len(),
|
||||
),
|
||||
) {
|
||||
Ok(writer) => writer,
|
||||
Err(err) => {
|
||||
@@ -141,6 +144,10 @@ impl FileTransferActivity {
|
||||
return Err(format!("Could not write file: {err}"));
|
||||
}
|
||||
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|err| format!("Could not write file: {err}"))?;
|
||||
|
||||
self.browser
|
||||
.local_pane_mut()
|
||||
.fs
|
||||
|
||||
@@ -2,6 +2,12 @@ use remotefs::File;
|
||||
|
||||
use super::{FileTransferActivity, LogLevel};
|
||||
|
||||
fn aggregate_sizes(sizes: impl IntoIterator<Item = Option<u64>>) -> Option<u64> {
|
||||
sizes
|
||||
.into_iter()
|
||||
.try_fold(0_u64, |total, size| total.checked_add(size?))
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// Calculate and display the total size of the selected file(s) via the active tab's pane.
|
||||
pub(crate) fn action_get_file_size(&mut self) {
|
||||
@@ -11,17 +17,23 @@ impl FileTransferActivity {
|
||||
let total_size = self.get_files_size(files);
|
||||
|
||||
self.umount_wait();
|
||||
self.mount_info(format!(
|
||||
"Total file size: {size}",
|
||||
size = bytesize::ByteSize::b(total_size)
|
||||
));
|
||||
let message = total_size.map_or_else(
|
||||
|| String::from("Total file size: Unknown"),
|
||||
|size| {
|
||||
format!(
|
||||
"Total file size: {size}",
|
||||
size = bytesize::ByteSize::b(size)
|
||||
)
|
||||
},
|
||||
);
|
||||
self.mount_info(message);
|
||||
}
|
||||
|
||||
fn get_files_size(&mut self, files: Vec<File>) -> u64 {
|
||||
files.into_iter().map(|f| self.get_file_size(f)).sum()
|
||||
fn get_files_size(&mut self, files: Vec<File>) -> Option<u64> {
|
||||
aggregate_sizes(files.into_iter().map(|file| self.get_file_size(file)))
|
||||
}
|
||||
|
||||
fn get_file_size(&mut self, file: File) -> u64 {
|
||||
fn get_file_size(&mut self, file: File) -> Option<u64> {
|
||||
if let Some(symlink) = &file.metadata().symlink {
|
||||
match self.browser.fs_pane_mut().fs.stat(symlink) {
|
||||
Ok(stat) => stat.metadata().size,
|
||||
@@ -33,12 +45,12 @@ impl FileTransferActivity {
|
||||
path = symlink.display(),
|
||||
),
|
||||
);
|
||||
0
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if file.is_dir() {
|
||||
match self.browser.fs_pane_mut().fs.list_dir(&file.path) {
|
||||
Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(),
|
||||
Ok(list) => self.get_files_size(list),
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
@@ -47,7 +59,7 @@ impl FileTransferActivity {
|
||||
path = file.path.display(),
|
||||
),
|
||||
);
|
||||
0
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -55,3 +67,14 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::aggregate_sizes;
|
||||
|
||||
#[test]
|
||||
fn aggregates_only_known_sizes() {
|
||||
assert_eq!(aggregate_sizes([Some(2), Some(3)]), Some(5));
|
||||
assert_eq!(aggregate_sizes([Some(2), None, Some(3)]), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl FileTransferActivity {
|
||||
.browser
|
||||
.fs_pane_mut()
|
||||
.fs
|
||||
.create_file(file_path.as_path(), &Metadata::default())
|
||||
.create_file(file_path.as_path(), &Metadata::default().size(0))
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
|
||||
@@ -131,6 +131,14 @@ impl FileTransferActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = reader.finish() {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("Failed to finish bridged file read: {err}"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if tmpfile.exists() {
|
||||
self.open_path_with(tmpfile.as_path(), open_with);
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ impl FileInfoPopup {
|
||||
.add_col(SpanStatic::from("File type: "))
|
||||
.add_col(SpanStatic::raw(filetype.clone()).fg(Color::LightGreen));
|
||||
}
|
||||
let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size);
|
||||
texts
|
||||
.add_row()
|
||||
.add_col(SpanStatic::from("Size: "))
|
||||
.add_col(SpanStatic::raw(format!("{bsize} ({size})")).fg(Color::Cyan));
|
||||
.add_col(SpanStatic::raw(format_file_size(file)).fg(Color::Cyan));
|
||||
let atime: String = fmt_time(
|
||||
file.metadata().accessed.unwrap_or(UNIX_EPOCH),
|
||||
"%b %d %Y %H:%M:%S",
|
||||
@@ -117,6 +116,13 @@ impl FileInfoPopup {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_size(file: &File) -> String {
|
||||
match file.metadata().size {
|
||||
Some(size) => format!("{bsize} ({size})", bsize = ByteSize(size), size = size),
|
||||
None => String::from("Unknown"),
|
||||
}
|
||||
}
|
||||
|
||||
impl AppComponent<Msg, NoUserEvent> for FileInfoPopup {
|
||||
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
@@ -128,3 +134,17 @@ impl AppComponent<Msg, NoUserEvent> for FileInfoPopup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use remotefs::fs::Metadata;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn formats_unknown_file_size_as_unknown() {
|
||||
let file = File::new("/tmp/unknown", Metadata::default());
|
||||
|
||||
assert_eq!(format_file_size(&file), "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use bytesize::ByteSize;
|
||||
pub struct TransferProgress {
|
||||
files_total: usize,
|
||||
files_completed: usize,
|
||||
cur_file_size: usize,
|
||||
cur_file_size: Option<usize>,
|
||||
cur_file_written: usize,
|
||||
total_bytes_written: usize,
|
||||
pub(crate) started: Instant,
|
||||
@@ -27,7 +27,7 @@ impl Default for TransferProgress {
|
||||
Self {
|
||||
files_total: 0,
|
||||
files_completed: 0,
|
||||
cur_file_size: 0,
|
||||
cur_file_size: None,
|
||||
cur_file_written: 0,
|
||||
total_bytes_written: 0,
|
||||
started: Instant::now(),
|
||||
@@ -41,13 +41,15 @@ impl fmt::Display for TransferProgress {
|
||||
0 => String::from("--:--"),
|
||||
seconds => format!("{:02}:{:02}", seconds / 60, seconds % 60),
|
||||
};
|
||||
let total = self.cur_file_size.map_or_else(
|
||||
|| String::from("?"),
|
||||
|size| ByteSize(size as u64).to_string(),
|
||||
);
|
||||
write!(
|
||||
f,
|
||||
"{} / {} — {:.1}% — ETA {} ({}/s)",
|
||||
"{} / {total} — {:.1}% — ETA {eta} ({}/s)",
|
||||
ByteSize(self.cur_file_written as u64),
|
||||
ByteSize(self.cur_file_size as u64),
|
||||
self.calc_partial_progress() * 100.0,
|
||||
eta,
|
||||
ByteSize(self.calc_bytes_per_second()),
|
||||
)
|
||||
}
|
||||
@@ -58,15 +60,15 @@ impl TransferProgress {
|
||||
pub fn init(&mut self, files_total: usize) {
|
||||
self.files_total = files_total;
|
||||
self.files_completed = 0;
|
||||
self.cur_file_size = 0;
|
||||
self.cur_file_size = None;
|
||||
self.cur_file_written = 0;
|
||||
self.total_bytes_written = 0;
|
||||
self.started = Instant::now();
|
||||
}
|
||||
|
||||
/// Begin a new file with a known size.
|
||||
pub fn start_file(&mut self, size: usize) {
|
||||
self.cur_file_size = size;
|
||||
/// Begin a new file with an optional advertised size.
|
||||
pub fn start_file(&mut self, size: Option<u64>) {
|
||||
self.cur_file_size = size.and_then(|size| usize::try_from(size).ok());
|
||||
self.cur_file_written = 0;
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ impl TransferProgress {
|
||||
/// fraction in [`Self::calc_full_progress`].
|
||||
pub fn finish_file(&mut self) {
|
||||
self.files_completed += 1;
|
||||
self.cur_file_size = 0;
|
||||
self.cur_file_size = None;
|
||||
self.cur_file_written = 0;
|
||||
}
|
||||
|
||||
@@ -92,25 +94,26 @@ impl TransferProgress {
|
||||
self.files_completed += 1;
|
||||
}
|
||||
|
||||
/// Fraction of the current file written (0.0..=1.0). Zero-byte file => 1.0.
|
||||
/// Fraction of the current file written (0.0..=1.0).
|
||||
pub fn calc_partial_progress(&self) -> f64 {
|
||||
if self.cur_file_size == 0 {
|
||||
return 1.0;
|
||||
match self.cur_file_size {
|
||||
None => 0.0,
|
||||
Some(0) => 1.0,
|
||||
Some(size) => (self.cur_file_written as f64 / size as f64).min(1.0),
|
||||
}
|
||||
(self.cur_file_written as f64 / self.cur_file_size as f64).min(1.0)
|
||||
}
|
||||
|
||||
/// Overall progress (0.0..=1.0): file-weighted with intra-file interpolation.
|
||||
///
|
||||
/// The current file only contributes a fraction while it is genuinely in
|
||||
/// progress (`cur_file_size > 0` and not all files completed). A finished
|
||||
/// file clears `cur_file_size` (see [`Self::finish_file`]) so it is counted
|
||||
/// progress (a known size and not all files completed). A finished file
|
||||
/// clears `cur_file_size` (see [`Self::finish_file`]) so it is counted
|
||||
/// exactly once via `files_completed`.
|
||||
pub fn calc_full_progress(&self) -> f64 {
|
||||
if self.files_total == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let cur_fraction = if self.cur_file_size == 0 || self.files_completed >= self.files_total {
|
||||
let cur_fraction = if self.files_completed >= self.files_total {
|
||||
0.0
|
||||
} else {
|
||||
self.calc_partial_progress()
|
||||
@@ -234,7 +237,7 @@ mod test {
|
||||
assert!(progress.is_single_file());
|
||||
assert_eq!(progress.calc_full_progress(), 0.0);
|
||||
|
||||
progress.start_file(1024);
|
||||
progress.start_file(Some(1024));
|
||||
assert_eq!(progress.calc_partial_progress(), 0.0);
|
||||
assert_eq!(progress.calc_full_progress(), 0.0);
|
||||
|
||||
@@ -258,13 +261,13 @@ mod test {
|
||||
assert!(!progress.is_single_file());
|
||||
|
||||
// File 1 fully transferred => full ≈ 0.25
|
||||
progress.start_file(1000);
|
||||
progress.start_file(Some(1000));
|
||||
progress.add_bytes(1000);
|
||||
progress.finish_file();
|
||||
assert!((progress.calc_full_progress() - 0.25).abs() < 1e-9);
|
||||
|
||||
// File 2 half transferred => partial ≈ 0.5, full ≈ 0.375
|
||||
progress.start_file(1000);
|
||||
progress.start_file(Some(1000));
|
||||
progress.add_bytes(500);
|
||||
assert!((progress.calc_partial_progress() - 0.5).abs() < 1e-9);
|
||||
assert!((progress.calc_full_progress() - 0.375).abs() < 1e-9);
|
||||
@@ -276,7 +279,7 @@ mod test {
|
||||
progress.init(2);
|
||||
|
||||
// One file actually transferred.
|
||||
progress.start_file(100);
|
||||
progress.start_file(Some(100));
|
||||
progress.add_bytes(100);
|
||||
progress.finish_file();
|
||||
|
||||
@@ -294,15 +297,34 @@ mod test {
|
||||
let mut progress = TransferProgress::default();
|
||||
progress.init(1);
|
||||
|
||||
progress.start_file(0);
|
||||
progress.start_file(Some(0));
|
||||
assert!((progress.calc_partial_progress() - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_progress_unknown_size() {
|
||||
let mut progress = TransferProgress::default();
|
||||
progress.init(1);
|
||||
progress.start_file(None);
|
||||
progress.add_bytes(256);
|
||||
|
||||
assert_eq!(progress.calc_partial_progress(), 0.0);
|
||||
assert_eq!(progress.calc_full_progress(), 0.0);
|
||||
assert_eq!(progress.calc_eta(), 0);
|
||||
assert_eq!(progress.total_bytes_written(), 256);
|
||||
assert!(progress.to_string().contains("?"));
|
||||
assert!(!progress.to_string().contains("0 B"));
|
||||
|
||||
progress.finish_file();
|
||||
assert_eq!(progress.files_completed(), 1);
|
||||
assert_eq!(progress.calc_full_progress(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_progress_timing() {
|
||||
let mut progress = TransferProgress::default();
|
||||
progress.init(1);
|
||||
progress.start_file(1024);
|
||||
progress.start_file(Some(1024));
|
||||
|
||||
progress.started = progress
|
||||
.started
|
||||
|
||||
@@ -10,7 +10,7 @@ use bytesize::ByteSize;
|
||||
use remotefs::fs::File;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::host::HostError;
|
||||
use crate::host::{HostError, HostReader, HostWriter};
|
||||
use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel};
|
||||
use crate::utils::fmt::fmt_millis;
|
||||
|
||||
@@ -457,8 +457,8 @@ impl FileTransferActivity {
|
||||
host: &File,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
mut reader: Box<dyn Read + Send>,
|
||||
mut writer: Box<dyn Write + Send>,
|
||||
mut reader: HostReader,
|
||||
mut writer: HostWriter,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Write file
|
||||
let file_size = self
|
||||
@@ -467,18 +467,16 @@ impl FileTransferActivity {
|
||||
.fs
|
||||
.stat(host.path())
|
||||
.map_err(TransferErrorReason::HostError)
|
||||
.map(|x| x.metadata().size as usize)?;
|
||||
.map(|x| x.metadata().size)?;
|
||||
// Init transfer
|
||||
self.transfer.progress.start_file(file_size);
|
||||
let file_started = Instant::now();
|
||||
|
||||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
let mut last_redraw: Instant = Instant::now();
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely written,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < file_size && !self.transfer.aborted() {
|
||||
// Read until EOF or until the transfer is aborted.
|
||||
while !self.transfer.aborted() {
|
||||
// Handle input events (each 500ms) or if never fetched before
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
@@ -494,33 +492,17 @@ impl FileTransferActivity {
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE];
|
||||
let delta: usize = match reader.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
if bytes_read == 0 {
|
||||
continue;
|
||||
} else {
|
||||
let mut delta: usize = 0;
|
||||
while delta < bytes_read {
|
||||
// Write bytes
|
||||
match writer.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => {
|
||||
delta += bytes;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::RemoteIoError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
delta
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::HostIoError(err));
|
||||
}
|
||||
};
|
||||
let bytes_read = reader
|
||||
.read(&mut buffer)
|
||||
.map_err(TransferErrorReason::HostIoError)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
writer
|
||||
.write_all(&buffer[..bytes_read])
|
||||
.map_err(TransferErrorReason::RemoteIoError)?;
|
||||
// Increase progress
|
||||
self.transfer.progress.add_bytes(delta);
|
||||
self.transfer.progress.add_bytes(bytes_read);
|
||||
// Redraw at most every 100ms to keep UI responsive for large files
|
||||
if last_redraw.elapsed().as_millis() >= 100 {
|
||||
self.update_progress_bar(format!("Uploading \"{file_name}\"…"));
|
||||
@@ -528,12 +510,14 @@ impl FileTransferActivity {
|
||||
last_redraw = Instant::now();
|
||||
}
|
||||
}
|
||||
// Finalize stream
|
||||
handle_remote_finalize_result(self.browser.remote_pane_mut().fs.finalize_write(writer))?;
|
||||
// if upload was abrupted, return error
|
||||
if self.transfer.aborted() {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
writer.flush().map_err(TransferErrorReason::RemoteIoError)?;
|
||||
reader.finish().map_err(TransferErrorReason::HostError)?;
|
||||
// Finalize stream
|
||||
handle_remote_finalize_result(self.browser.remote_pane_mut().fs.finalize_write(writer))?;
|
||||
// set stat
|
||||
if let Err(err) = self
|
||||
.browser
|
||||
@@ -812,21 +796,17 @@ impl FileTransferActivity {
|
||||
host_bridge: &Path,
|
||||
remote: &File,
|
||||
file_name: String,
|
||||
mut reader: Box<dyn Read + Send>,
|
||||
mut writer: Box<dyn Write + Send>,
|
||||
mut reader: HostReader,
|
||||
mut writer: HostWriter,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Init transfer
|
||||
self.transfer
|
||||
.progress
|
||||
.start_file(remote.metadata.size as usize);
|
||||
self.transfer.progress.start_file(remote.metadata.size);
|
||||
let file_started = Instant::now();
|
||||
// Write host_bridge file
|
||||
let mut last_redraw: Instant = Instant::now();
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely read,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() {
|
||||
// Read until EOF or until the transfer is aborted.
|
||||
while !self.transfer.aborted() {
|
||||
// Handle input events (each 500 ms) or is None
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
@@ -842,31 +822,17 @@ impl FileTransferActivity {
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE];
|
||||
let delta: usize = match reader.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
if bytes_read == 0 {
|
||||
continue;
|
||||
} else {
|
||||
let mut delta: usize = 0;
|
||||
while delta < bytes_read {
|
||||
// Write bytes
|
||||
match writer.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => delta += bytes,
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::HostIoError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
delta
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::RemoteIoError(err));
|
||||
}
|
||||
};
|
||||
let bytes_read = reader
|
||||
.read(&mut buffer)
|
||||
.map_err(TransferErrorReason::RemoteIoError)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
writer
|
||||
.write_all(&buffer[..bytes_read])
|
||||
.map_err(TransferErrorReason::HostIoError)?;
|
||||
// Set progress
|
||||
self.transfer.progress.add_bytes(delta);
|
||||
self.transfer.progress.add_bytes(bytes_read);
|
||||
// Redraw at most every 100ms to keep UI responsive for large files
|
||||
if last_redraw.elapsed().as_millis() >= 100 {
|
||||
self.update_progress_bar(format!("Downloading \"{file_name}\""));
|
||||
@@ -879,6 +845,10 @@ impl FileTransferActivity {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
|
||||
writer.flush().map_err(TransferErrorReason::HostIoError)?;
|
||||
reader
|
||||
.finish()
|
||||
.map_err(TransferErrorReason::RemoteHostError)?;
|
||||
// Finalize write
|
||||
self.browser
|
||||
.local_pane_mut()
|
||||
@@ -953,25 +923,20 @@ mod worklist_test {
|
||||
// equal (`File` derives `PartialEq` over its full metadata, timestamps
|
||||
// included), allowing direct equality assertions against the worklist.
|
||||
let t = SystemTime::UNIX_EPOCH;
|
||||
let metadata = Metadata {
|
||||
accessed: Some(t),
|
||||
created: Some(t),
|
||||
modified: Some(t),
|
||||
file_type: if is_dir {
|
||||
let metadata = Metadata::default()
|
||||
.accessed(t)
|
||||
.created(t)
|
||||
.modified(t)
|
||||
.file_type(if is_dir {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
},
|
||||
symlink: None,
|
||||
gid: Some(0),
|
||||
uid: Some(0),
|
||||
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||
size: 64,
|
||||
};
|
||||
File {
|
||||
path: PathBuf::from(path),
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
.gid(0)
|
||||
.uid(0)
|
||||
.mode(UnixPex::from(if is_dir { 0o755 } else { 0o644 }))
|
||||
.size(64);
|
||||
File::new(path, metadata)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,10 +14,7 @@ pub fn create_sample_file_entry() -> (File, NamedTempFile) {
|
||||
// Write
|
||||
let tmpfile = create_sample_file();
|
||||
(
|
||||
File {
|
||||
path: tmpfile.path().to_path_buf(),
|
||||
metadata: Metadata::default(),
|
||||
},
|
||||
File::new(tmpfile.path().to_path_buf(), Metadata::default()),
|
||||
tmpfile,
|
||||
)
|
||||
}
|
||||
@@ -58,14 +55,14 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
|
||||
/// Create a File at specified path
|
||||
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> File {
|
||||
let path: PathBuf = path.as_ref().to_path_buf();
|
||||
File {
|
||||
File::new(
|
||||
path,
|
||||
metadata: Metadata::default().file_type(if is_dir {
|
||||
Metadata::default().file_type(if is_dir {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
}),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Open a file with two handlers, the first is to read, the second is to write
|
||||
|
||||
Reference in New Issue
Block a user