SMB support (#184)

* feat: smb client

* fix: smb connection

* fix: smbclient deps

* feat: SMB mentions to user manual

* feat: changelog

* dlib macos

* fix: removed smb support from macos :(

* fix: restored libsmbclient build

* fix: strange lint message

* fix: macos build smb

* fix: macos build smb

* fix: macos tests

* fix: macos lint

* feat: SMB windows support

* fix: windows tests
This commit is contained in:
Christian Visintin
2023-05-13 15:00:16 +02:00
committed by GitHub
parent a13663e5e9
commit b7369162d2
54 changed files with 1256 additions and 154 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev libsmbclient
- name: Setup nightly toolchain - name: Setup nightly toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
-1
View File
@@ -8,7 +8,6 @@ env:
jobs: jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
+2
View File
@@ -37,6 +37,8 @@ Released on ??
- **Change file permissions**: you can now change file permissions easily with the permissions popup pressing `Z` in the explorer. - **Change file permissions**: you can now change file permissions easily with the permissions popup pressing `Z` in the explorer.
- [Issue 172](https://github.com/veeso/termscp/issues/172) - [Issue 172](https://github.com/veeso/termscp/issues/172)
- **SMB protocol**: Support for SMB protocol has been added thanks to the [remotefs-smb](https://github.com/veeso/remotefs-rs-smb) library and the [pavao](https://github.com/veeso/pavao) project. You may notice that the interface is quiet different between Windows and Linux/MacOs/BSD due to the fact that SMB is natively supported on Windows systems.
- [Issue 182](https://github.com/veeso/termscp/issues/182)
- [Issue 153](https://github.com/veeso/termscp/issues/153): show a loading message when loading directory's content - [Issue 153](https://github.com/veeso/termscp/issues/153): show a loading message when loading directory's content
- [Issue 176](https://github.com/veeso/termscp/issues/176): debug log is now written to CACHE_DIR - [Issue 176](https://github.com/veeso/termscp/issues/176): debug log is now written to CACHE_DIR
- [Issue 173](https://github.com/veeso/termscp/issues/173): allow unknown fields in ssh2 configuration file - [Issue 173](https://github.com/veeso/termscp/issues/173): allow unknown fields in ssh2 configuration file
Generated
+35
View File
@@ -375,6 +375,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.24" version = "0.4.24"
@@ -2042,6 +2048,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pavao"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f789d21b9a0ae0168b6325cee036811e746e635ef90896161e1df2ee3d3b60d"
dependencies = [
"lazy_static",
"libc",
"log",
"pkg-config",
"thiserror",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.2.0" version = "2.2.0"
@@ -2259,6 +2278,20 @@ dependencies = [
"users", "users",
] ]
[[package]]
name = "remotefs-smb"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c4fb523b04b6bcd5dae95a33cbaee73cf7d6607862039b6d2bff310db6ed9f"
dependencies = [
"filetime",
"libc",
"log",
"pavao",
"remotefs",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "remotefs-ssh" name = "remotefs-ssh"
version = "0.2.0" version = "0.2.0"
@@ -2897,6 +2930,7 @@ dependencies = [
"argh", "argh",
"bitflags 2.2.0", "bitflags 2.2.0",
"bytesize", "bytesize",
"cfg_aliases",
"chrono", "chrono",
"content_inspector", "content_inspector",
"dirs 5.0.0", "dirs 5.0.0",
@@ -2916,6 +2950,7 @@ dependencies = [
"remotefs", "remotefs",
"remotefs-aws-s3", "remotefs-aws-s3",
"remotefs-ftp", "remotefs-ftp",
"remotefs-smb",
"remotefs-ssh", "remotefs-ssh",
"rpassword", "rpassword",
"self_update", "self_update",
+7
View File
@@ -70,11 +70,18 @@ wildmatch = "^2.1"
pretty_assertions = "^1.3" pretty_assertions = "^1.3"
serial_test = "^2.0" serial_test = "^2.0"
[build-dependencies]
cfg_aliases = "0.1"
[features] [features]
default = [ "with-keyring" ] default = [ "with-keyring" ]
github-actions = [ ] github-actions = [ ]
with-keyring = [ "keyring" ] with-keyring = [ "keyring" ]
[target."cfg(not(target_os = \"macos\"))"]
[target."cfg(not(target_os = \"macos\"))".dependencies]
remotefs-smb = "^0.2"
[target."cfg(target_family = \"windows\")"] [target."cfg(target_family = \"windows\")"]
[target."cfg(target_family = \"windows\")".dependencies] [target."cfg(target_family = \"windows\")".dependencies]
remotefs-ftp = { version = "^0.1.2", features = [ "native-tls" ] } remotefs-ftp = { version = "^0.1.2", features = [ "native-tls" ] }
+4
View File
@@ -132,6 +132,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
- **SCP** - **SCP**
- **FTP** and **FTPS** - **FTP** and **FTPS**
- **S3** - **S3**
- **SMB**
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI - 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
- Create, remove, rename, search, view and edit files - Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections - ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
@@ -187,9 +188,11 @@ For more information or other platforms, please visit [termscp.veeso.dev](https:
- **Linux** users: - **Linux** users:
- libdbus-1 - libdbus-1
- pkg-config - pkg-config
- libsmbclient
- **FreeBSD** or, **NetBSD** users: - **FreeBSD** or, **NetBSD** users:
- dbus - dbus
- pkgconf - pkgconf
- libsmbclient
### Optional Requirements ✔️ ### Optional Requirements ✔️
@@ -261,6 +264,7 @@ termscp is powered by these awesome projects:
- [edit](https://github.com/milkey-mouse/edit) - [edit](https://github.com/milkey-mouse/edit)
- [keyring-rs](https://github.com/hwchen/keyring-rs) - [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs) - [open-rs](https://github.com/Byron/open-rs)
- [pavao](https://github.com/veeso/pavao)
- [remotefs](https://github.com/veeso/remotefs-rs) - [remotefs](https://github.com/veeso/remotefs-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword) - [rpassword](https://github.com/conradkleinespel/rpassword)
- [self_update](https://github.com/jaemk/self_update) - [self_update](https://github.com/jaemk/self_update)
+15
View File
@@ -0,0 +1,15 @@
use cfg_aliases::cfg_aliases;
fn main() {
// Setup cfg aliases
cfg_aliases! {
// Platforms
macos: { target_os = "macos" },
linux: { target_os = "linux" },
unix: { target_family = "unix" },
windows: { target_family = "windows" },
// exclusive features
smb: { not( macos ) },
smb_unix: { all(unix, not(macos)) }
}
}
+1
View File
@@ -9,6 +9,7 @@ RUN yum -y install \
gcc \ gcc \
make \ make \
dbus-devel \ dbus-devel \
libsmbclient-devel \
bash \ bash \
rpm-build rpm-build
# Install rust # Install rust
+2
View File
@@ -8,6 +8,8 @@ RUN apt update && apt install -y \
pkg-config \ pkg-config \
libdbus-1-dev \ libdbus-1-dev \
build-essential \ build-essential \
libsmbclient-dev \
libsmbclient \
bash \ bash \
curl curl
+1
View File
@@ -9,6 +9,7 @@ RUN yum -y install \
gcc \ gcc \
make \ make \
dbus-devel \ dbus-devel \
libsmbclient-devel \
bash \ bash \
rpm-build rpm-build
# Install rust # Install rust
+2
View File
@@ -8,6 +8,8 @@ RUN apt update && apt install -y \
pkg-config \ pkg-config \
libdbus-1-dev \ libdbus-1-dev \
build-essential \ build-essential \
libsmbclient-dev \
libsmbclient \
bash \ bash \
curl curl
+3
View File
@@ -137,6 +137,7 @@ Termscp ist ein funktionsreicher Terminal-Dateitransfer und Explorer mit Unterst
- **SCP** - **SCP**
- **FTP** und **FTPS** - **FTP** und **FTPS**
- **S3** - **S3**
- **SMB**
- 🖥 Erkunden und bedienen Sie das Dateisystem der Fernbedienung und des lokalen Computers mit einer praktischen Benutzeroberfläche - 🖥 Erkunden und bedienen Sie das Dateisystem der Fernbedienung und des lokalen Computers mit einer praktischen Benutzeroberfläche
- Erstellen, Entfernen, Umbenennen, Suchen, Anzeigen und Bearbeiten von Dateien - Erstellen, Entfernen, Umbenennen, Suchen, Anzeigen und Bearbeiten von Dateien
- ⭐ Verbinden Sie sich über integrierte Lesezeichen und aktuelle Verbindungen mit Ihren Lieblingshosts - ⭐ Verbinden Sie sich über integrierte Lesezeichen und aktuelle Verbindungen mit Ihren Lieblingshosts
@@ -185,10 +186,12 @@ Für weitere Informationen oder andere Plattformen besuchen Sie bitte [termscp.v
- libssh - libssh
- libdbus-1 - libdbus-1
- pkg-config - pkg-config
- libsmbclient
- **FreeBSD** Benutzer: - **FreeBSD** Benutzer:
- libssh - libssh
- dbus - dbus
- pkgconf - pkgconf
- libsmbclient
### Optionale Softwareanforderungen ✔️ ### Optionale Softwareanforderungen ✔️
+17
View File
@@ -4,6 +4,7 @@
- [Usage ❓](#usage-) - [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-) - [Address argument 🌎](#address-argument-)
- [AWS S3 address argument](#aws-s3-address-argument) - [AWS S3 address argument](#aws-s3-address-argument)
- [SMB address argument](#smb-address-argument)
- [How Password can be provided 🔐](#how-password-can-be-provided-) - [How Password can be provided 🔐](#how-password-can-be-provided-)
- [S3 connection parameters](#s3-connection-parameters) - [S3 connection parameters](#s3-connection-parameters)
- [S3 credentials 🦊](#s3-credentials-) - [S3 credentials 🦊](#s3-credentials-)
@@ -105,6 +106,22 @@ e.g.
s3://buckethead@eu-central-1:default:/assets s3://buckethead@eu-central-1:default:/assets
``` ```
#### SMB address argument
SMB has a different syntax for CLI address argument, which is different whether you're on Windows or other systems:
**Windows** syntax:
```txt
\\[username@]<server-name>\<share>[\path\...]
```
**Other systems** syntax:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### How Password can be provided 🔐 #### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password. You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
+5 -4
View File
@@ -137,6 +137,7 @@ Termscp es un explorador y transferencia de archivos de terminal rico en funcion
- **SCP** - **SCP**
- **FTP** y **FTPS** - **FTP** y **FTPS**
- **S3** - **S3**
- **SMB**
- 🖥 Explore y opere en el sistema de archivos de la máquina local y remota con una interfaz de usuario práctica - 🖥 Explore y opere en el sistema de archivos de la máquina local y remota con una interfaz de usuario práctica
- Cree, elimine, cambie el nombre, busque, vea y edite archivos - Cree, elimine, cambie el nombre, busque, vea y edite archivos
- ⭐ Conéctese a sus hosts favoritos y conexiones recientes - ⭐ Conéctese a sus hosts favoritos y conexiones recientes
@@ -181,14 +182,14 @@ Para obtener más información u otras plataformas, visite [termscp.veeso.dev](h
### Requisitos ❗ ### Requisitos ❗
- Usuarios **Linux**: - **Linux** users:
- libssh
- libdbus-1 - libdbus-1
- pkg-config - pkg-config
- Usuarios **FreeBSD**: - libsmbclient
- libssh - **FreeBSD** or, **NetBSD** users:
- dbus - dbus
- pkgconf - pkgconf
- libsmbclient
### Requisitos opcionales ✔️ ### Requisitos opcionales ✔️
+17
View File
@@ -4,6 +4,7 @@
- [Uso ❓](#uso-) - [Uso ❓](#uso-)
- [Argumento dirección 🌎](#argumento-dirección-) - [Argumento dirección 🌎](#argumento-dirección-)
- [Argumento dirección por AWS S3](#argumento-dirección-por-aws-s3) - [Argumento dirección por AWS S3](#argumento-dirección-por-aws-s3)
- [Argumento dirección por SMB](#argumento-dirección-por-smb)
- [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-) - [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-)
- [S3 parámetros de conexión](#s3-parámetros-de-conexión) - [S3 parámetros de conexión](#s3-parámetros-de-conexión)
- [Credenciales de S3 🦊](#credenciales-de-s3-) - [Credenciales de S3 🦊](#credenciales-de-s3-)
@@ -105,6 +106,22 @@ por ejemplo
s3://buckethead@eu-central-1:default:/assets s3://buckethead@eu-central-1:default:/assets
``` ```
#### Argumento dirección por SMB
SMB tiene una sintaxis diferente para el argumento de la dirección CLI, que es diferente si está en Windows u otros sistemas:
**Windows** sintaxis:
```txt
\\[username@]<server-name>\<share>[\path\...]
```
**Other systems** sintaxis:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Cómo se puede proporcionar la contraseña 🔐 #### Cómo se puede proporcionar la contraseña 🔐
Probablemente haya notado que, al proporcionar la dirección como argumento, no hay forma de proporcionar la contraseña. Probablemente haya notado que, al proporcionar la dirección como argumento, no hay forma de proporcionar la contraseña.
+5 -4
View File
@@ -137,6 +137,7 @@ Termscp est un file transfer et explorateur de fichiers de terminal riche en fon
- **SCP** - **SCP**
- **FTP** et **FTPS** - **FTP** et **FTPS**
- **S3** - **S3**
- **SMB**
- 🖥 Explorer et opérer sur le système de fichiers distant et local avec une interface utilisateur pratique. - 🖥 Explorer et opérer sur le système de fichiers distant et local avec une interface utilisateur pratique.
- Créer, supprimer, renommer, rechercher, afficher et modifier des fichiers - Créer, supprimer, renommer, rechercher, afficher et modifier des fichiers
- ⭐ Connectez-vous à vos hôtes préférés via des signets et des connexions récentes. - ⭐ Connectez-vous à vos hôtes préférés via des signets et des connexions récentes.
@@ -181,14 +182,14 @@ Pour plus d'informations sur les autres méthodes d'installation, veuillez visit
### Requis ❗ ### Requis ❗
- utilisateurs **Linux**: - **Linux** users:
- libssh
- libdbus-1 - libdbus-1
- pkg-config - pkg-config
- utilisateurs **FreeBSD**: - libsmbclient
- libssh - **FreeBSD** or, **NetBSD** users:
- dbus - dbus
- pkgconf - pkgconf
- libsmbclient
### Requis facultatives ✔️ ### Requis facultatives ✔️
+18
View File
@@ -4,6 +4,7 @@
- [Usage ❓](#usage-) - [Usage ❓](#usage-)
- [Argument d'adresse 🌎](#argument-dadresse-) - [Argument d'adresse 🌎](#argument-dadresse-)
- [Argument d'adresse AWS S3](#argument-dadresse-aws-s3) - [Argument d'adresse AWS S3](#argument-dadresse-aws-s3)
- [Argument d'adresse SMB](#argument-dadresse-smb)
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-) - [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
- [S3 paramètres de connexion](#s3-paramètres-de-connexion) - [S3 paramètres de connexion](#s3-paramètres-de-connexion)
- [Identifiants S3 🦊](#identifiants-s3-) - [Identifiants S3 🦊](#identifiants-s3-)
@@ -103,6 +104,23 @@ e.g.
s3://buckethead@eu-central-1:default:/assets s3://buckethead@eu-central-1:default:/assets
``` ```
#### Argument d'adresse SMB
SMB a une syntaxe différente pour l'argument d'adresse CLI, qui est différente que vous soyez sur Windows ou sur d'autres systèmes :
syntaxe **Windows**:
```txt
\\[username@]<server-name>\<share>[\path\...]
```
syntaxe **Other systems**:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Comment le mot de passe peut être fourni 🔐 #### Comment le mot de passe peut être fourni 🔐
Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe. Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe.
+5 -4
View File
@@ -137,6 +137,7 @@ Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a S
- **SCP** - **SCP**
- **FTP** and **FTPS** - **FTP** and **FTPS**
- **S3** - **S3**
- **SMB**
- 🖥 Esplora e opera sia sul file system locale che su quello remoto con una UI di facile utilizzo. - 🖥 Esplora e opera sia sul file system locale che su quello remoto con una UI di facile utilizzo.
- Crea, rimuove, rinomina, cerca, visualizza e modifica file - Crea, rimuove, rinomina, cerca, visualizza e modifica file
- ⭐ Connettiti ai tuoi host preferiti tramite la funzionalità integrata dei segnalibri e delle connessioni recenti. - ⭐ Connettiti ai tuoi host preferiti tramite la funzionalità integrata dei segnalibri e delle connessioni recenti.
@@ -181,14 +182,14 @@ Per ulteriori informazioni sui metodi di installazione su altre piattaforme, vis
### Requisiti ❗ ### Requisiti ❗
- Utenti **Linux**: - **Linux** users:
- libssh
- libdbus-1 - libdbus-1
- pkg-config - pkg-config
- Utenti **FreeBSD**: - libsmbclient
- libssh - **FreeBSD** or, **NetBSD** users:
- dbus - dbus
- pkgconf - pkgconf
- libsmbclient
### Requisiti opzionali ✔️ ### Requisiti opzionali ✔️
+18
View File
@@ -4,6 +4,7 @@
- [Argomenti da linea di comando ❓](#argomenti-da-linea-di-comando-) - [Argomenti da linea di comando ❓](#argomenti-da-linea-di-comando-)
- [Argomento indirizzo 🌎](#argomento-indirizzo-) - [Argomento indirizzo 🌎](#argomento-indirizzo-)
- [Argomento indirizzo per AWS S3](#argomento-indirizzo-per-aws-s3) - [Argomento indirizzo per AWS S3](#argomento-indirizzo-per-aws-s3)
- [Indirizzo SMB](#indirizzo-smb)
- [Come fornire la password 🔐](#come-fornire-la-password-) - [Come fornire la password 🔐](#come-fornire-la-password-)
- [Parametri di connessione S3](#parametri-di-connessione-s3) - [Parametri di connessione S3](#parametri-di-connessione-s3)
- [Credenziali S3 🦊](#credenziali-s3-) - [Credenziali S3 🦊](#credenziali-s3-)
@@ -101,6 +102,23 @@ e.g.
s3://buckethead@eu-central-1:default:/assets s3://buckethead@eu-central-1:default:/assets
``` ```
#### Indirizzo SMB
SMB ha una sintassi differente rispetto agli altri protocolli e cambia in base al sistema operativo:
**Windows**:
```txt
\\[username@]<server-name>\<share>[\path\...]
```
**Altri sistemi**:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Come fornire la password 🔐 #### Come fornire la password 🔐
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo: Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:
+17
View File
@@ -4,6 +4,7 @@
- [Usage ❓](#usage-) - [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-) - [Address argument 🌎](#address-argument-)
- [AWS S3 address argument](#aws-s3-address-argument) - [AWS S3 address argument](#aws-s3-address-argument)
- [SMB address argument](#smb-address-argument)
- [How Password can be provided 🔐](#how-password-can-be-provided-) - [How Password can be provided 🔐](#how-password-can-be-provided-)
- [S3 connection parameters](#s3-connection-parameters) - [S3 connection parameters](#s3-connection-parameters)
- [S3 credentials 🦊](#s3-credentials-) - [S3 credentials 🦊](#s3-credentials-)
@@ -103,6 +104,22 @@ e.g.
s3://buckethead@eu-central-1:default:/assets s3://buckethead@eu-central-1:default:/assets
``` ```
#### SMB address argument
SMB has a different syntax for CLI address argument, which is different whether you're on Windows or other systems:
**Windows** syntax:
```txt
\\[username@]<server-name>\<share>[\path\...]
```
**Other systems** syntax:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### How Password can be provided 🔐 #### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password. You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
+3
View File
@@ -139,6 +139,7 @@ termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/S
- **SCP** - **SCP**
- **FTP** and **FTPS** - **FTP** and **FTPS**
- **S3** - **S3**
- **SMB**
- 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作 - 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作
- 创建、删除、重命名、搜索、查看和编辑文件 - 创建、删除、重命名、搜索、查看和编辑文件
- ⭐ 通过“内置书签”和“最近连接”快速连接到您的主机 - ⭐ 通过“内置书签”和“最近连接”快速连接到您的主机
@@ -188,10 +189,12 @@ choco install termscp
- libssh - libssh
- libdbus-1 - libdbus-1
- pkg-config - pkg-config
- libsmbclient
- **FreeBSD** 用户: - **FreeBSD** 用户:
- libssh - libssh
- dbus - dbus
- pkgconf - pkgconf
- libsmbclient
### 可选项 ✔️ ### 可选项 ✔️
+17
View File
@@ -4,6 +4,7 @@
- [用法](#用法) - [用法](#用法)
- [地址参数](#地址参数) - [地址参数](#地址参数)
- [AWS S3 地址参数](#aws-s3-地址参数) - [AWS S3 地址参数](#aws-s3-地址参数)
- [SMB 地址参数](#smb-地址参数)
- [如何输入密码](#如何输入密码) - [如何输入密码](#如何输入密码)
- [S3 连接参数](#s3-连接参数) - [S3 连接参数](#s3-连接参数)
- [Aws S3 凭证](#aws-s3-凭证) - [Aws S3 凭证](#aws-s3-凭证)
@@ -102,6 +103,22 @@ s3://<bucket-name>@<region>[:profile][:/wrkdir]
s3://buckethead@eu-central-1:default:/assets s3://buckethead@eu-central-1:default:/assets
``` ```
#### SMB 地址参数
SMB 对 CLI 地址参数有不同的语法,无论您是在 Windows 还是其他系统上,这都是不同的:
**Windows** 句法:
```txt
\\[username@]<server-name>\<share>[\path\...]
```
**其他系统** 句法:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### 如何输入密码 #### 如何输入密码
你可能已经注意到,url参数中没有办法直接附加密码,你可以通过以下三种方式提供密码: 你可能已经注意到,url参数中没有办法直接附加密码,你可以通过以下三种方式提供密码:
+4 -4
View File
@@ -304,14 +304,14 @@ install_bsd_cargo_deps() {
set -e set -e
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?" confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
sudo="$(elevate_priv_ex /usr/local/bin)" sudo="$(elevate_priv_ex /usr/local/bin)"
$sudo pkg install -y curl wget libssh gcc dbus pkgconf $sudo pkg install -y curl wget libssh gcc dbus pkgconf libsmbclient
info "Dependencies installed successfully" info "Dependencies installed successfully"
} }
install_linux_cargo_deps() { install_linux_cargo_deps() {
local debian_deps="gcc pkg-config libdbus-1-dev" local debian_deps="gcc pkg-config libdbus-1-dev libsmbclient-dev"
local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel" local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel libsmbclient-devel"
local arch_deps="gcc openssl pkg-config dbus" local arch_deps="gcc openssl pkg-config dbus smbclient"
local deps_cmd="" local deps_cmd=""
# Get pkg manager # Get pkg manager
if has apt; then if has apt; then
+2
View File
@@ -28,6 +28,8 @@ Address syntax can be:
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp - `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol - `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
- `\\\\<server>[:port]\\<share>[\\path]` for SMB (on Windows)
- `smb://[user@]<server>[:port]</share>[/path]` for SMB (on other systems)
Please, report issues to <https://github.com/veeso/termscp> Please, report issues to <https://github.com/veeso/termscp>
Please, consider supporting the author <https://ko-fi.com/veeso>")] Please, consider supporting the author <https://ko-fi.com/veeso>")]
+135 -1
View File
@@ -9,7 +9,9 @@ use std::str::FromStr;
use serde::de::Error as DeError; use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, ProtocolParams, SmbParams as TransferSmbParams,
};
use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
/// UserHosts contains all the hosts saved by the user in the data storage /// UserHosts contains all the hosts saved by the user in the data storage
@@ -40,6 +42,8 @@ pub struct Bookmark {
pub directory: Option<PathBuf>, pub directory: Option<PathBuf>,
/// S3 params; optional. When used other fields are empty for sure /// S3 params; optional. When used other fields are empty for sure
pub s3: Option<S3Params>, pub s3: Option<S3Params>,
/// SMB params; optional. Extra params required for SMB protocol
pub smb: Option<SmbParams>,
} }
/// Connection parameters for Aws s3 protocol /// Connection parameters for Aws s3 protocol
@@ -55,6 +59,13 @@ pub struct S3Params {
pub new_path_style: Option<bool>, pub new_path_style: Option<bool>,
} }
/// Extra Connection parameters for SMB protocol
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
pub struct SmbParams {
pub share: String,
pub workgroup: Option<String>,
}
// -- impls // -- impls
impl From<FileTransferParams> for Bookmark { impl From<FileTransferParams> for Bookmark {
@@ -71,6 +82,7 @@ impl From<FileTransferParams> for Bookmark {
password: params.password, password: params.password,
directory, directory,
s3: None, s3: None,
smb: None,
}, },
ProtocolParams::AwsS3(params) => Self { ProtocolParams::AwsS3(params) => Self {
protocol, protocol,
@@ -80,6 +92,20 @@ impl From<FileTransferParams> for Bookmark {
password: None, password: None,
directory, directory,
s3: Some(S3Params::from(params)), s3: Some(S3Params::from(params)),
smb: None,
},
ProtocolParams::Smb(params) => Self {
smb: Some(SmbParams::from(params.clone())),
protocol,
address: Some(params.address),
#[cfg(unix)]
port: Some(params.port),
#[cfg(windows)]
port: None,
username: params.username,
password: params.password,
directory,
s3: None,
}, },
} }
} }
@@ -104,6 +130,30 @@ impl From<Bookmark> for FileTransferParams {
.password(bookmark.password); .password(bookmark.password);
Self::new(bookmark.protocol, ProtocolParams::Generic(params)) Self::new(bookmark.protocol, ProtocolParams::Generic(params))
} }
#[cfg(unix)]
FileTransferProtocol::Smb => {
let params = TransferSmbParams::new(
bookmark.address.unwrap_or_default(),
bookmark.smb.clone().map(|x| x.share).unwrap_or_default(),
)
.port(bookmark.port.unwrap_or(445))
.username(bookmark.username)
.password(bookmark.password)
.workgroup(bookmark.smb.and_then(|x| x.workgroup));
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
}
#[cfg(windows)]
FileTransferProtocol::Smb => {
let params = TransferSmbParams::new(
bookmark.address.unwrap_or_default(),
bookmark.smb.clone().map(|x| x.share).unwrap_or_default(),
)
.username(bookmark.username)
.password(bookmark.password);
Self::new(bookmark.protocol, ProtocolParams::Smb(params))
}
} }
.entry_directory(bookmark.directory) // Set entry directory .entry_directory(bookmark.directory) // Set entry directory
} }
@@ -133,6 +183,26 @@ impl From<S3Params> for AwsS3Params {
} }
} }
#[cfg(unix)]
impl From<TransferSmbParams> for SmbParams {
fn from(params: TransferSmbParams) -> Self {
Self {
share: params.share,
workgroup: params.workgroup,
}
}
}
#[cfg(windows)]
impl From<TransferSmbParams> for SmbParams {
fn from(params: TransferSmbParams) -> Self {
Self {
share: params.share,
workgroup: None,
}
}
}
fn deserialize_protocol<'de, D>(deserializer: D) -> Result<FileTransferProtocol, D::Error> fn deserialize_protocol<'de, D>(deserializer: D) -> Result<FileTransferProtocol, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@@ -178,6 +248,7 @@ mod tests {
password: Some(String::from("password")), password: Some(String::from("password")),
directory: Some(PathBuf::from("/tmp")), directory: Some(PathBuf::from("/tmp")),
s3: None, s3: None,
smb: None,
}; };
let recent: Bookmark = Bookmark { let recent: Bookmark = Bookmark {
address: Some(String::from("192.168.1.2")), address: Some(String::from("192.168.1.2")),
@@ -187,6 +258,7 @@ mod tests {
password: Some(String::from("password")), password: Some(String::from("password")),
directory: Some(PathBuf::from("/home")), directory: Some(PathBuf::from("/home")),
s3: None, s3: None,
smb: None,
}; };
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1); let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
bookmarks.insert(String::from("test"), bookmark); bookmarks.insert(String::from("test"), bookmark);
@@ -275,6 +347,7 @@ mod tests {
password: Some(String::from("password")), password: Some(String::from("password")),
directory: Some(PathBuf::from("/tmp")), directory: Some(PathBuf::from("/tmp")),
s3: None, s3: None,
smb: None,
}; };
let params = FileTransferParams::from(bookmark); let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::Sftp); assert_eq!(params.protocol, FileTransferProtocol::Sftp);
@@ -307,6 +380,7 @@ mod tests {
secret_access_key: Some(String::from("pluto")), secret_access_key: Some(String::from("pluto")),
new_path_style: Some(true), new_path_style: Some(true),
}), }),
smb: None,
}; };
let params = FileTransferParams::from(bookmark); let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::AwsS3); assert_eq!(params.protocol, FileTransferProtocol::AwsS3);
@@ -323,4 +397,64 @@ mod tests {
assert_eq!(gparams.secret_access_key.as_deref().unwrap(), "pluto"); assert_eq!(gparams.secret_access_key.as_deref().unwrap(), "pluto");
assert_eq!(gparams.new_path_style, true); assert_eq!(gparams.new_path_style, true);
} }
#[test]
#[cfg(unix)]
fn should_get_ftparams_from_smb_bookmark() {
let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::Smb,
address: Some("localhost".to_string()),
port: Some(445),
username: Some("foo".to_string()),
password: Some("bar".to_string()),
directory: Some(PathBuf::from("/tmp")),
s3: None,
smb: Some(SmbParams {
share: "test".to_string(),
workgroup: Some("testone".to_string()),
}),
};
let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::Smb);
assert_eq!(
params.entry_directory.as_deref().unwrap(),
std::path::Path::new("/tmp")
);
let smb_params = params.params.smb_params().unwrap();
assert_eq!(smb_params.address.as_str(), "localhost");
assert_eq!(smb_params.port, 445);
assert_eq!(smb_params.share.as_str(), "test");
assert_eq!(smb_params.password.as_deref().unwrap(), "bar");
assert_eq!(smb_params.username.as_deref().unwrap(), "foo");
assert_eq!(smb_params.workgroup.as_deref().unwrap(), "testone");
}
#[test]
#[cfg(windows)]
fn should_get_ftparams_from_smb_bookmark() {
let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::Smb,
address: Some("localhost".to_string()),
port: Some(445),
username: None,
password: None,
directory: Some(PathBuf::from("/tmp")),
s3: None,
smb: Some(SmbParams {
share: "test".to_string(),
workgroup: None,
}),
};
let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::Smb);
assert_eq!(
params.entry_directory.as_deref().unwrap(),
std::path::Path::new("/tmp")
);
let smb_params = params.params.smb_params().unwrap();
assert_eq!(smb_params.address.as_str(), "localhost");
assert_eq!(smb_params.share.as_str(), "test");
}
} }
+48 -2
View File
@@ -115,7 +115,7 @@ mod tests {
use tuirealm::tui::style::Color; use tuirealm::tui::style::Color;
use super::*; use super::*;
use crate::config::bookmarks::{Bookmark, S3Params, UserHosts}; use crate::config::bookmarks::{Bookmark, S3Params, SmbParams, UserHosts};
use crate::config::params::UserConfig; use crate::config::params::UserConfig;
use crate::config::themes::Theme; use crate::config::themes::Theme;
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
@@ -366,7 +366,7 @@ mod tests {
assert_eq!(host.username.as_deref().unwrap(), "root"); assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(host.password, None); assert_eq!(host.password, None);
// Verify bookmarks // Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 4); assert_eq!(hosts.bookmarks.len(), 5);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap(); let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31"); assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
assert_eq!(host.port.unwrap(), 22); assert_eq!(host.port.unwrap(), 22);
@@ -404,6 +404,20 @@ mod tests {
assert_eq!(s3.access_key.as_deref().unwrap(), "pippo"); assert_eq!(s3.access_key.as_deref().unwrap(), "pippo");
assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto"); assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto");
assert_eq!(s3.new_path_style.unwrap(), true); assert_eq!(s3.new_path_style.unwrap(), true);
// smb
let host = hosts.bookmarks.get("smb").unwrap();
assert_eq!(host.address.as_deref().unwrap(), "localhost");
assert_eq!(host.port.unwrap(), 445);
#[cfg(unix)]
assert_eq!(host.username.as_deref().unwrap(), "test");
#[cfg(unix)]
assert_eq!(host.password.as_deref().unwrap(), "test");
let smb = host.smb.as_ref().unwrap();
assert_eq!(smb.share.as_str(), "temp");
#[cfg(unix)]
assert_eq!(smb.workgroup.as_deref().unwrap(), "test");
} }
#[test] #[test]
@@ -429,6 +443,7 @@ mod tests {
password: None, password: None,
directory: None, directory: None,
s3: None, s3: None,
smb: None,
}, },
); );
bookmarks.insert( bookmarks.insert(
@@ -441,6 +456,7 @@ mod tests {
password: Some(String::from("password")), password: Some(String::from("password")),
directory: Some(PathBuf::from("/tmp")), directory: Some(PathBuf::from("/tmp")),
s3: None, s3: None,
smb: None,
}, },
); );
bookmarks.insert( bookmarks.insert(
@@ -461,6 +477,24 @@ mod tests {
secret_access_key: None, secret_access_key: None,
new_path_style: None, new_path_style: None,
}), }),
smb: None,
},
);
let smb_params: Option<SmbParams> = Some(SmbParams {
share: "test".to_string(),
workgroup: None,
});
bookmarks.insert(
String::from("smb"),
Bookmark {
address: Some("localhost".to_string()),
port: Some(445),
protocol: FileTransferProtocol::Smb,
username: None,
password: None,
directory: None,
s3: None,
smb: smb_params,
}, },
); );
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1); let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
@@ -474,6 +508,7 @@ mod tests {
password: Some(String::from("aaa")), password: Some(String::from("aaa")),
directory: Some(PathBuf::from("/tmp")), directory: Some(PathBuf::from("/tmp")),
s3: None, s3: None,
smb: None,
}, },
); );
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
@@ -527,6 +562,17 @@ mod tests {
secret_access_key = "pluto" secret_access_key = "pluto"
new_path_style = true new_path_style = true
[bookmarks.smb]
protocol = "SMB"
address = "localhost"
port = 445
username = "test"
password = "test"
[bookmarks.smb.smb]
share = "temp"
workgroup = "test"
[recents] [recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" } ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
"#; "#;
+19 -19
View File
@@ -11,7 +11,7 @@ use bytesize::ByteSize;
use lazy_regex::{Lazy, Regex}; use lazy_regex::{Lazy, Regex};
use remotefs::File; use remotefs::File;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
#[cfg(target_family = "unix")] #[cfg(unix)]
use users::{get_group_by_gid, get_user_by_uid}; use users::{get_group_by_gid, get_user_by_uid};
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
@@ -211,7 +211,7 @@ impl Formatter {
_fmt_extra: Option<&String>, _fmt_extra: Option<&String>,
) -> String { ) -> String {
// Get username // Get username
#[cfg(target_family = "unix")] #[cfg(unix)]
let group: String = match fsentry.metadata().gid { let group: String = match fsentry.metadata().gid {
Some(gid) => match get_group_by_gid(gid) { Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
@@ -219,7 +219,7 @@ impl Formatter {
}, },
None => 0.to_string(), None => 0.to_string(),
}; };
#[cfg(target_os = "windows")] #[cfg(windows)]
let group: String = match fsentry.metadata().gid { let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(), Some(gid) => gid.to_string(),
None => 0.to_string(), None => 0.to_string(),
@@ -420,7 +420,7 @@ impl Formatter {
_fmt_extra: Option<&String>, _fmt_extra: Option<&String>,
) -> String { ) -> String {
// Get username // Get username
#[cfg(target_family = "unix")] #[cfg(unix)]
let username: String = match fsentry.metadata().uid { let username: String = match fsentry.metadata().uid {
Some(uid) => match get_user_by_uid(uid) { Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
@@ -428,7 +428,7 @@ impl Formatter {
}, },
None => 0.to_string(), None => 0.to_string(),
}; };
#[cfg(target_os = "windows")] #[cfg(windows)]
let username: String = match fsentry.metadata().uid { let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(), Some(uid) => uid.to_string(),
None => 0.to_string(), None => 0.to_string(),
@@ -592,7 +592,7 @@ mod tests {
mode: Some(UnixPex::from(0o644)), mode: Some(UnixPex::from(0o644)),
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -600,7 +600,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -623,7 +623,7 @@ mod tests {
mode: Some(UnixPex::from(0o644)), mode: Some(UnixPex::from(0o644)),
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -631,7 +631,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -654,7 +654,7 @@ mod tests {
mode: None, mode: None,
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -662,7 +662,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -685,7 +685,7 @@ mod tests {
mode: None, mode: None,
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -693,7 +693,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -723,7 +723,7 @@ mod tests {
mode: Some(UnixPex::from(0o755)), mode: Some(UnixPex::from(0o755)),
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -731,7 +731,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -754,7 +754,7 @@ mod tests {
mode: None, mode: None,
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -762,7 +762,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
@@ -864,7 +864,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn should_fmt_path() { fn should_fmt_path() {
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry = File { let entry = File {
@@ -896,7 +896,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn should_fmt_utf8_path() { fn should_fmt_utf8_path() {
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry = File { let entry = File {
+2 -2
View File
@@ -513,7 +513,7 @@ mod tests {
mode: Some(UnixPex::from(0o644)), mode: Some(UnixPex::from(0o644)),
}, },
}; };
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!( assert_eq!(
explorer.fmt_file(&entry), explorer.fmt_file(&entry),
format!( format!(
@@ -521,7 +521,7 @@ mod tests {
fmt_time(t, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!( assert_eq!(
explorer.fmt_file(&entry), explorer.fmt_file(&entry),
format!( format!(
+63
View File
@@ -7,9 +7,16 @@ use std::path::PathBuf;
use remotefs::RemoteFs; use remotefs::RemoteFs;
use remotefs_aws_s3::AwsS3Fs; use remotefs_aws_s3::AwsS3Fs;
use remotefs_ftp::FtpFs; use remotefs_ftp::FtpFs;
#[cfg(smb_unix)]
use remotefs_smb::SmbOptions;
#[cfg(smb)]
use remotefs_smb::{SmbCredentials, SmbFs};
use remotefs_ssh::{ScpFs, SftpFs, SshConfigParseRule, SshOpts}; use remotefs_ssh::{ScpFs, SftpFs, SshConfigParseRule, SshOpts};
#[cfg(not(smb))]
use super::params::{AwsS3Params, GenericProtocolParams}; use super::params::{AwsS3Params, GenericProtocolParams};
#[cfg(smb)]
use super::params::{AwsS3Params, GenericProtocolParams, SmbParams};
use super::{FileTransferProtocol, ProtocolParams}; use super::{FileTransferProtocol, ProtocolParams};
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::sshkey_storage::SshKeyStorage; use crate::system::sshkey_storage::SshKeyStorage;
@@ -39,6 +46,10 @@ impl Builder {
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => { (FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
Box::new(Self::sftp_client(params, config_client)) Box::new(Self::sftp_client(params, config_client))
} }
#[cfg(smb)]
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
Box::new(Self::smb_client(params))
}
(protocol, params) => { (protocol, params) => {
error!("Invalid params for protocol '{:?}'", protocol); error!("Invalid params for protocol '{:?}'", protocol);
panic!("Invalid protocol '{protocol:?}' with parameters of type {params:?}") panic!("Invalid protocol '{protocol:?}' with parameters of type {params:?}")
@@ -98,6 +109,50 @@ impl Builder {
Self::build_ssh_opts(params, config_client).into() Self::build_ssh_opts(params, config_client).into()
} }
#[cfg(smb_unix)]
fn smb_client(params: SmbParams) -> SmbFs {
let mut credentials = SmbCredentials::default()
.server(format!("smb://{}:{}", params.address, params.port))
.share(params.share);
if let Some(username) = params.username {
credentials = credentials.username(username);
}
if let Some(password) = params.password {
credentials = credentials.password(password);
}
if let Some(workgroup) = params.workgroup {
credentials = credentials.workgroup(workgroup);
}
match SmbFs::try_new(
credentials,
SmbOptions::default()
.one_share_per_server(true)
.case_sensitive(false),
) {
Ok(fs) => fs,
Err(e) => {
error!("Invalid params for protocol SMB: {e}");
panic!("Invalid params for protocol SMB: {e}")
}
}
}
#[cfg(windows)]
fn smb_client(params: SmbParams) -> SmbFs {
let mut credentials = SmbCredentials::new(params.address, params.share);
if let Some(username) = params.username {
credentials = credentials.username(username);
}
if let Some(password) = params.password {
credentials = credentials.password(password);
}
SmbFs::new(credentials)
}
/// Build ssh options from generic protocol params and client configuration /// Build ssh options from generic protocol params and client configuration
fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts { fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts {
let mut opts = SshOpts::new(params.address) let mut opts = SshOpts::new(params.address)
@@ -187,6 +242,14 @@ mod test {
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client); let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
} }
#[test]
#[cfg(smb)]
fn should_build_smb_fs() {
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Smb, params, &config_client);
}
#[test] #[test]
#[should_panic] #[should_panic]
fn should_not_build_fs() { fn should_not_build_fs() {
+17 -5
View File
@@ -13,10 +13,11 @@ pub use params::{FileTransferParams, ProtocolParams};
#[derive(PartialEq, Eq, Debug, Clone, Copy)] #[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum FileTransferProtocol { pub enum FileTransferProtocol {
Sftp,
Scp,
Ftp(bool), // Bool is for secure (true => ftps)
AwsS3, AwsS3,
Ftp(bool), // Bool is for secure (true => ftps)
Scp,
Sftp,
Smb,
} }
// Traits // Traits
@@ -24,13 +25,14 @@ pub enum FileTransferProtocol {
impl std::string::ToString for FileTransferProtocol { impl std::string::ToString for FileTransferProtocol {
fn to_string(&self) -> String { fn to_string(&self) -> String {
String::from(match self { String::from(match self {
FileTransferProtocol::AwsS3 => "S3",
FileTransferProtocol::Ftp(secure) => match secure { FileTransferProtocol::Ftp(secure) => match secure {
true => "FTPS", true => "FTPS",
false => "FTP", false => "FTP",
}, },
FileTransferProtocol::Scp => "SCP", FileTransferProtocol::Scp => "SCP",
FileTransferProtocol::Sftp => "SFTP", FileTransferProtocol::Sftp => "SFTP",
FileTransferProtocol::AwsS3 => "S3", FileTransferProtocol::Smb => "SMB",
}) })
} }
} }
@@ -41,9 +43,10 @@ impl std::str::FromStr for FileTransferProtocol {
match s.to_ascii_uppercase().as_str() { match s.to_ascii_uppercase().as_str() {
"FTP" => Ok(FileTransferProtocol::Ftp(false)), "FTP" => Ok(FileTransferProtocol::Ftp(false)),
"FTPS" => Ok(FileTransferProtocol::Ftp(true)), "FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"S3" => Ok(FileTransferProtocol::AwsS3),
"SCP" => Ok(FileTransferProtocol::Scp), "SCP" => Ok(FileTransferProtocol::Scp),
"SFTP" => Ok(FileTransferProtocol::Sftp), "SFTP" => Ok(FileTransferProtocol::Sftp),
"S3" => Ok(FileTransferProtocol::AwsS3), "SMB" => Ok(FileTransferProtocol::Smb),
_ => Err(s.to_string()), _ => Err(s.to_string()),
} }
} }
@@ -104,6 +107,14 @@ mod tests {
FileTransferProtocol::from_str("scp").ok().unwrap(), FileTransferProtocol::from_str("scp").ok().unwrap(),
FileTransferProtocol::Scp FileTransferProtocol::Scp
); );
assert_eq!(
FileTransferProtocol::from_str("SMB").ok().unwrap(),
FileTransferProtocol::Smb
);
assert_eq!(
FileTransferProtocol::from_str("smb").ok().unwrap(),
FileTransferProtocol::Smb
);
assert_eq!( assert_eq!(
FileTransferProtocol::from_str("S3").ok().unwrap(), FileTransferProtocol::from_str("S3").ok().unwrap(),
FileTransferProtocol::AwsS3 FileTransferProtocol::AwsS3
@@ -126,5 +137,6 @@ mod tests {
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP")); assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP")); assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3")); assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
assert_eq!(FileTransferProtocol::Smb.to_string(), String::from("SMB"));
} }
} }
+127
View File
@@ -19,6 +19,7 @@ pub struct FileTransferParams {
pub enum ProtocolParams { pub enum ProtocolParams {
Generic(GenericProtocolParams), Generic(GenericProtocolParams),
AwsS3(AwsS3Params), AwsS3(AwsS3Params),
Smb(SmbParams),
} }
/// Protocol params used by most common protocols /// Protocol params used by most common protocols
@@ -44,6 +45,19 @@ pub struct AwsS3Params {
pub new_path_style: bool, pub new_path_style: bool,
} }
/// Connection parameters for SMB protocol
#[derive(Debug, Clone)]
pub struct SmbParams {
pub address: String,
#[cfg(unix)]
pub port: u16,
pub share: String,
pub username: Option<String>,
pub password: Option<String>,
#[cfg(unix)]
pub workgroup: Option<String>,
}
impl FileTransferParams { impl FileTransferParams {
/// Instantiates a new `FileTransferParams` /// Instantiates a new `FileTransferParams`
pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self { pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self {
@@ -66,6 +80,7 @@ impl FileTransferParams {
match &self.params { match &self.params {
ProtocolParams::AwsS3(params) => params.password_missing(), ProtocolParams::AwsS3(params) => params.password_missing(),
ProtocolParams::Generic(params) => params.password_missing(), ProtocolParams::Generic(params) => params.password_missing(),
ProtocolParams::Smb(params) => params.password_missing(),
} }
} }
@@ -74,6 +89,7 @@ impl FileTransferParams {
match &mut self.params { match &mut self.params {
ProtocolParams::AwsS3(params) => params.set_default_secret(secret), ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
ProtocolParams::Generic(params) => params.set_default_secret(secret), ProtocolParams::Generic(params) => params.set_default_secret(secret),
ProtocolParams::Smb(params) => params.set_default_secret(secret),
} }
} }
} }
@@ -117,6 +133,15 @@ impl ProtocolParams {
_ => None, _ => None,
} }
} }
#[cfg(test)]
/// Retrieve SMB parameters if any
pub fn smb_params(&self) -> Option<&SmbParams> {
match self {
ProtocolParams::Smb(params) => Some(params),
_ => None,
}
}
} }
// -- Generic protocol params // -- Generic protocol params
@@ -235,6 +260,61 @@ impl AwsS3Params {
} }
} }
// -- SMB params
impl SmbParams {
/// Instantiates a new `AwsS3Params` struct
pub fn new<S: AsRef<str>>(address: S, share: S) -> Self {
Self {
address: address.as_ref().to_string(),
#[cfg(unix)]
port: 445,
share: share.as_ref().to_string(),
username: None,
password: None,
#[cfg(unix)]
workgroup: None,
}
}
#[cfg(unix)]
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn username(mut self, username: Option<impl ToString>) -> Self {
self.username = username.map(|x| x.to_string());
self
}
pub fn password(mut self, password: Option<impl ToString>) -> Self {
self.password = password.map(|x| x.to_string());
self
}
#[cfg(unix)]
pub fn workgroup(mut self, workgroup: Option<impl ToString>) -> Self {
self.workgroup = workgroup.map(|x| x.to_string());
self
}
/// Returns whether a password is supposed to be required for this protocol params.
/// The result true is returned ONLY if the supposed secret is MISSING!!!
pub fn password_missing(&self) -> bool {
self.password.is_none()
}
/// Set password
#[cfg(unix)]
pub fn set_default_secret(&mut self, secret: String) {
self.password = Some(secret);
}
#[cfg(windows)]
pub fn set_default_secret(&mut self, _secret: String) {}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@@ -304,6 +384,53 @@ mod test {
assert_eq!(params.new_path_style, true); assert_eq!(params.new_path_style, true);
} }
#[test]
fn should_init_smb_params() {
let params = SmbParams::new("localhost", "temp");
assert_eq!(&params.address, "localhost");
#[cfg(unix)]
assert_eq!(params.port, 445);
assert_eq!(&params.share, "temp");
#[cfg(unix)]
assert!(params.username.is_none());
#[cfg(unix)]
assert!(params.password.is_none());
#[cfg(unix)]
assert!(params.workgroup.is_none());
}
#[test]
#[cfg(unix)]
fn should_init_smb_params_with_optionals() {
let params = SmbParams::new("localhost", "temp")
.port(3456)
.username(Some("foo"))
.password(Some("bar"))
.workgroup(Some("baz"));
assert_eq!(&params.address, "localhost");
assert_eq!(params.port, 3456);
assert_eq!(&params.share, "temp");
assert_eq!(params.username.as_deref().unwrap(), "foo");
assert_eq!(params.password.as_deref().unwrap(), "bar");
assert_eq!(params.workgroup.as_deref().unwrap(), "baz");
}
#[test]
#[cfg(windows)]
fn should_init_smb_params_with_optionals() {
let params = SmbParams::new("localhost", "temp")
.username(Some("foo"))
.password(Some("bar"));
assert_eq!(&params.address, "localhost");
assert_eq!(&params.share, "temp");
assert_eq!(params.username.as_deref().unwrap(), "foo");
assert_eq!(params.password.as_deref().unwrap(), "bar");
}
#[test] #[test]
fn references() { fn references() {
let mut params = let mut params =
+31 -31
View File
@@ -4,15 +4,15 @@
// ext // ext
// Metadata ext // Metadata ext
#[cfg(target_family = "unix")] #[cfg(unix)]
use std::fs::set_permissions; use std::fs::set_permissions;
use std::fs::{self, File as StdFile, OpenOptions}; use std::fs::{self, File as StdFile, OpenOptions};
#[cfg(target_family = "unix")] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use filetime::{self, FileTime}; use filetime::{self, FileTime};
#[cfg(target_family = "unix")] #[cfg(unix)]
use remotefs::fs::UnixPex; use remotefs::fs::UnixPex;
use remotefs::fs::{File, FileType, Metadata}; use remotefs::fs::{File, FileType, Metadata};
use thiserror::Error; use thiserror::Error;
@@ -420,7 +420,7 @@ impl Localhost {
filetime::set_file_atime(path, atime) filetime::set_file_atime(path, atime)
.map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?; .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?;
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
if let Some(mode) = metadata.mode { if let Some(mode) = metadata.mode {
self.chmod(path, mode)?; self.chmod(path, mode)?;
} }
@@ -454,7 +454,7 @@ impl Localhost {
} }
/// Change file mode to file, according to UNIX permissions /// Change file mode to file, according to UNIX permissions
#[cfg(target_family = "unix")] #[cfg(unix)]
pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> { pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> {
let path: PathBuf = self.to_path(path); let path: PathBuf = self.to_path(path);
// Get metadta // Get metadta
@@ -586,7 +586,7 @@ impl Localhost {
} }
/// Create a symlink at path pointing at target /// Create a symlink at path pointing at target
#[cfg(target_family = "unix")] #[cfg(unix)]
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> { pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> {
let path = self.to_path(path); let path = self.to_path(path);
std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| { std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| {
@@ -644,19 +644,19 @@ impl Localhost {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[cfg(target_family = "unix")] #[cfg(unix)]
use std::fs::File as StdFile; use std::fs::File as StdFile;
#[cfg(target_family = "unix")] #[cfg(unix)]
use std::io::Write; use std::io::Write;
use std::ops::AddAssign; use std::ops::AddAssign;
#[cfg(target_family = "unix")] #[cfg(unix)]
use std::os::unix::fs::{symlink, PermissionsExt}; use std::os::unix::fs::{symlink, PermissionsExt};
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
#[cfg(target_family = "unix")] #[cfg(unix)]
use crate::utils::test_helpers::make_fsentry; use crate::utils::test_helpers::make_fsentry;
use crate::utils::test_helpers::{create_sample_file, make_dir_at, make_file_at}; use crate::utils::test_helpers::{create_sample_file, make_dir_at, make_file_at};
@@ -669,7 +669,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_new() { fn test_host_localhost_new() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert_eq!(host.wrkdir, PathBuf::from("/dev")); assert_eq!(host.wrkdir, PathBuf::from("/dev"));
@@ -683,7 +683,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_os = "windows")] #[cfg(windows)]
fn test_host_localhost_new() { fn test_host_localhost_new() {
let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap();
assert_eq!(host.wrkdir, PathBuf::from("C:\\users")); assert_eq!(host.wrkdir, PathBuf::from("C:\\users"));
@@ -705,14 +705,14 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_pwd() { fn test_host_localhost_pwd() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert_eq!(host.pwd(), PathBuf::from("/dev")); assert_eq!(host.pwd(), PathBuf::from("/dev"));
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_list_files() { fn test_host_localhost_list_files() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Scan dir // Scan dir
@@ -725,7 +725,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_change_dir() { fn test_host_localhost_change_dir() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
let new_dir: PathBuf = PathBuf::from("/dev"); let new_dir: PathBuf = PathBuf::from("/dev");
@@ -741,7 +741,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
#[should_panic] #[should_panic]
fn test_host_localhost_change_dir_failed() { fn test_host_localhost_change_dir_failed() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
@@ -750,7 +750,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_open_read() { fn test_host_localhost_open_read() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Create temp file // Create temp file
@@ -759,7 +759,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
#[should_panic] #[should_panic]
fn test_host_localhost_open_read_err_no_such_file() { fn test_host_localhost_open_read_err_no_such_file() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
@@ -780,7 +780,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_open_write() { fn test_host_localhost_open_write() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Create temp file // Create temp file
@@ -799,7 +799,7 @@ mod tests {
assert!(host.open_file_write(file.path()).is_err()); assert!(host.open_file_write(file.path()).is_err());
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn test_host_localhost_symlinks() { fn test_host_localhost_symlinks() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -837,7 +837,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_mkdir() { fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
@@ -862,7 +862,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_remove() { fn test_host_localhost_remove() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file // Create sample file
@@ -891,7 +891,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_host_localhost_rename() { fn test_host_localhost_rename() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file // Create sample file
@@ -944,7 +944,7 @@ mod tests {
assert_eq!(new_metadata.metadata().modified, Some(new_mtime)); assert_eq!(new_metadata.metadata().modified, Some(new_mtime));
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn test_host_chmod() { fn test_host_chmod() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -963,7 +963,7 @@ mod tests {
.is_err()); .is_err());
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn test_host_copy_file_absolute() { fn test_host_copy_file_absolute() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -993,7 +993,7 @@ mod tests {
.is_err()); .is_err());
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn test_host_copy_file_relative() { fn test_host_copy_file_relative() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -1015,7 +1015,7 @@ mod tests {
assert_eq!(host.files.len(), 2); assert_eq!(host.files.len(), 2);
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn test_host_copy_directory_absolute() { fn test_host_copy_directory_absolute() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -1046,7 +1046,7 @@ mod tests {
assert!(host.stat(test_file_path.as_path()).is_ok()); assert!(host.stat(test_file_path.as_path()).is_ok());
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn test_host_copy_directory_relative() { fn test_host_copy_directory_relative() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@@ -1081,9 +1081,9 @@ mod tests {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
// Execute // Execute
#[cfg(target_family = "unix")] #[cfg(unix)]
assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\n"); assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\n");
#[cfg(target_os = "windows")] #[cfg(windows)]
assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\r\n"); assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\r\n");
} }
@@ -1120,7 +1120,7 @@ mod tests {
assert_eq!(result[1].name(), "examples.csv"); assert_eq!(result[1].name(), "examples.csv");
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
#[test] #[test]
fn should_create_symlink() { fn should_create_symlink() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
-22
View File
@@ -398,28 +398,6 @@ mod tests {
assert_eq!(client.recents_size, 16); assert_eq!(client.recents_size, 16);
} }
#[test]
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn test_system_bookmarks_new_err() {
assert!(BookmarksClient::new(
Path::new("/tmp/oifoif/omar"),
Path::new("/tmp/efnnu/omar"),
16
)
.is_err());
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
assert!(
BookmarksClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar"), 16).is_err()
);
}
#[test] #[test]
fn test_system_bookmarks_new_from_existing() { fn test_system_bookmarks_new_from_existing() {
+1 -1
View File
@@ -329,7 +329,7 @@ mod test {
/* /*
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn should_poll_file_moved() { fn should_poll_file_moved() {
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap(); let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
let tempdir = TempDir::new().unwrap(); let tempdir = TempDir::new().unwrap();
+13 -1
View File
@@ -4,7 +4,7 @@
// Locals // Locals
use super::{AuthActivity, FileTransferParams}; use super::{AuthActivity, FileTransferParams};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams, SmbParams};
impl AuthActivity { impl AuthActivity {
/// Delete bookmark /// Delete bookmark
@@ -157,6 +157,7 @@ impl AuthActivity {
match bookmark.params { match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params), ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params), ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(params),
} }
} }
@@ -178,4 +179,15 @@ impl AuthActivity {
self.mount_s3_session_token(params.session_token.as_deref().unwrap_or("")); self.mount_s3_session_token(params.session_token.as_deref().unwrap_or(""));
self.mount_s3_new_path_style(params.new_path_style); self.mount_s3_new_path_style(params.new_path_style);
} }
fn load_bookmark_smb_into_gui(&mut self, params: SmbParams) {
self.mount_address(params.address.as_str());
#[cfg(unix)]
self.mount_port(params.port);
self.mount_username(params.username.as_deref().unwrap_or(""));
self.mount_password(params.password.as_deref().unwrap_or(""));
self.mount_smb_share(&params.share);
#[cfg(unix)]
self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or(""));
}
} }
+90 -10
View File
@@ -8,6 +8,11 @@ use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
use crate::ui::activities::auth::{
RADIO_PROTOCOL_FTP, RADIO_PROTOCOL_FTPS, RADIO_PROTOCOL_S3, RADIO_PROTOCOL_SCP,
RADIO_PROTOCOL_SFTP, RADIO_PROTOCOL_SMB,
};
use super::{FileTransferProtocol, FormMsg, Msg, UiMsg}; use super::{FileTransferProtocol, FormMsg, Msg, UiMsg};
// -- protocol // -- protocol
@@ -26,7 +31,11 @@ impl ProtocolRadio {
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(&["SFTP", "SCP", "FTP", "FTPS", "S3"]) .choices(if cfg!(smb) {
&["SFTP", "SCP", "FTP", "FTPS", "S3", "SMB"]
} else {
&["SFTP", "SCP", "FTP", "FTPS", "S3"]
})
.foreground(color) .foreground(color)
.rewind(true) .rewind(true)
.title("Protocol", Alignment::Left) .title("Protocol", Alignment::Left)
@@ -37,10 +46,11 @@ impl ProtocolRadio {
/// Convert radio index for protocol into a `FileTransferProtocol` /// Convert radio index for protocol into a `FileTransferProtocol`
fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol { fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol {
match protocol { match protocol {
1 => FileTransferProtocol::Scp, RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false), RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true), RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3, RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3,
RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb,
_ => FileTransferProtocol::Sftp, _ => FileTransferProtocol::Sftp,
} }
} }
@@ -48,11 +58,12 @@ impl ProtocolRadio {
/// Convert `FileTransferProtocol` enum into radio group index /// Convert `FileTransferProtocol` enum into radio group index
fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize { fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize {
match protocol { match protocol {
FileTransferProtocol::Sftp => 0, FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP,
FileTransferProtocol::Scp => 1, FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP,
FileTransferProtocol::Ftp(false) => 2, FileTransferProtocol::Ftp(false) => RADIO_PROTOCOL_FTP,
FileTransferProtocol::Ftp(true) => 3, FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS,
FileTransferProtocol::AwsS3 => 4, FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3,
FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB,
} }
} }
} }
@@ -673,3 +684,72 @@ fn handle_input_ev(
_ => None, _ => None,
} }
} }
#[derive(MockComponent)]
pub struct InputSmbShare {
component: Input,
}
impl InputSmbShare {
pub fn new(host: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Share", Alignment::Left)
.input_type(InputType::Text)
.value(host),
}
}
}
impl Component<Msg, NoUserEvent> for InputSmbShare {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Ui(UiMsg::SmbShareBlurDown),
Msg::Ui(UiMsg::SmbShareBlurUp),
)
}
}
#[cfg(unix)]
#[derive(MockComponent)]
pub struct InputSmbWorkgroup {
component: Input,
}
#[cfg(unix)]
impl InputSmbWorkgroup {
pub fn new(host: &str, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.title("Workgroup", Alignment::Left)
.input_type(InputType::Text)
.value(host),
}
}
}
#[cfg(unix)]
impl Component<Msg, NoUserEvent> for InputSmbWorkgroup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev(
self,
ev,
Msg::Ui(UiMsg::SmbWorkgroupDown),
Msg::Ui(UiMsg::SmbWorkgroupUp),
)
}
}
+3 -1
View File
@@ -13,10 +13,12 @@ pub use bookmarks::{
BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup, BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup,
RecentsList, RecentsList,
}; };
#[cfg(unix)]
pub use form::InputSmbWorkgroup;
pub use form::{ pub use form::{
InputAddress, InputPassword, InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputAddress, InputPassword, InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket,
InputS3Endpoint, InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3Endpoint, InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
InputS3SessionToken, InputUsername, ProtocolRadio, RadioS3NewPathStyle, InputS3SessionToken, InputSmbShare, InputUsername, ProtocolRadio, RadioS3NewPathStyle,
}; };
pub use popup::{ pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
+24 -1
View File
@@ -14,6 +14,7 @@ impl AuthActivity {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22, FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21, FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
FileTransferProtocol::Smb => 445,
} }
} }
@@ -36,7 +37,10 @@ impl AuthActivity {
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> { pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
match self.protocol { match self.protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(), FileTransferProtocol::AwsS3 => self.collect_s3_host_params(),
protocol => self.collect_generic_host_params(protocol), FileTransferProtocol::Smb => self.collect_smb_host_params(),
FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => self.collect_generic_host_params(self.protocol),
} }
} }
@@ -72,6 +76,25 @@ impl AuthActivity {
}) })
} }
pub(super) fn collect_smb_host_params(&self) -> Result<FileTransferParams, &'static str> {
let params = self.get_smb_params_input();
if params.address.is_empty() {
return Err("Invalid address");
}
#[cfg(unix)]
if params.port == 0 {
return Err("Invalid port");
}
if params.share.is_empty() {
return Err("Invalid share");
}
Ok(FileTransferParams {
protocol: FileTransferProtocol::Smb,
params: ProtocolParams::Smb(params),
entry_directory: self.get_input_remote_directory(),
})
}
// -- update install // -- update install
/// If enabled in configuration, check for updates from Github /// If enabled in configuration, check for updates from Github
+19
View File
@@ -23,6 +23,14 @@ use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient; use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
// radio
const RADIO_PROTOCOL_SFTP: usize = 0;
const RADIO_PROTOCOL_SCP: usize = 1;
const RADIO_PROTOCOL_FTP: usize = 2;
const RADIO_PROTOCOL_FTPS: usize = 3;
const RADIO_PROTOCOL_S3: usize = 4;
const RADIO_PROTOCOL_SMB: usize = 5;
// -- components // -- components
#[derive(Debug, Eq, PartialEq, Clone, Hash)] #[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id { pub enum Id {
@@ -55,6 +63,9 @@ pub enum Id {
S3SecretAccessKey, S3SecretAccessKey,
S3SecurityToken, S3SecurityToken,
S3SessionToken, S3SessionToken,
SmbShare,
#[cfg(unix)]
SmbWorkgroup,
Subtitle, Subtitle,
Title, Title,
Username, Username,
@@ -125,6 +136,12 @@ pub enum UiMsg {
S3SecurityTokenBlurUp, S3SecurityTokenBlurUp,
S3SessionTokenBlurDown, S3SessionTokenBlurDown,
S3SessionTokenBlurUp, S3SessionTokenBlurUp,
SmbShareBlurDown,
SmbShareBlurUp,
#[cfg(unix)]
SmbWorkgroupDown,
#[cfg(unix)]
SmbWorkgroupUp,
BookmarkNameBlur, BookmarkNameBlur,
SaveBookmarkPasswordBlur, SaveBookmarkPasswordBlur,
ShowDeleteBookmarkPopup, ShowDeleteBookmarkPopup,
@@ -143,6 +160,7 @@ pub enum UiMsg {
enum InputMask { enum InputMask {
Generic, Generic,
AwsS3, AwsS3,
Smb,
} }
// Store keys // Store keys
@@ -218,6 +236,7 @@ impl AuthActivity {
FileTransferProtocol::Ftp(_) FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp | FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => InputMask::Generic, | FileTransferProtocol::Sftp => InputMask::Generic,
FileTransferProtocol::Smb => InputMask::Smb,
} }
} }
} }
+59 -4
View File
@@ -68,6 +68,7 @@ impl AuthActivity {
.app .app
.active(match self.input_mask() { .active(match self.input_mask() {
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Password,
InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::S3Bucket,
}) })
.is_ok()); .is_ok());
@@ -79,6 +80,7 @@ impl AuthActivity {
.app .app
.active(match self.input_mask() { .active(match self.input_mask() {
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Password,
InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::S3Bucket,
}) })
.is_ok()); .is_ok());
@@ -113,7 +115,12 @@ impl AuthActivity {
fn update_ui(&mut self, msg: UiMsg) -> Option<Msg> { fn update_ui(&mut self, msg: UiMsg) -> Option<Msg> {
match msg { match msg {
UiMsg::AddressBlurDown => { UiMsg::AddressBlurDown => {
assert!(self.app.active(&Id::Port).is_ok()); let id = if cfg!(windows) && self.input_mask() == InputMask::Smb {
&Id::SmbShare
} else {
&Id::Port
};
assert!(self.app.active(id).is_ok());
} }
UiMsg::AddressBlurUp => { UiMsg::AddressBlurUp => {
assert!(self.app.active(&Id::Protocol).is_ok()); assert!(self.app.active(&Id::Protocol).is_ok());
@@ -155,13 +162,30 @@ impl AuthActivity {
assert!(self.app.active(&Id::BookmarksList).is_ok()); assert!(self.app.active(&Id::BookmarksList).is_ok());
} }
UiMsg::PasswordBlurDown => { UiMsg::PasswordBlurDown => {
assert!(self.app.active(&Id::RemoteDirectory).is_ok()); assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::RemoteDirectory,
#[cfg(unix)]
InputMask::Smb => &Id::SmbWorkgroup,
#[cfg(windows)]
InputMask::Smb => &Id::RemoteDirectory,
InputMask::AwsS3 => panic!("this shouldn't happen (password on s3)"),
})
.is_ok());
} }
UiMsg::PasswordBlurUp => { UiMsg::PasswordBlurUp => {
assert!(self.app.active(&Id::Username).is_ok()); assert!(self.app.active(&Id::Username).is_ok());
} }
UiMsg::PortBlurDown => { UiMsg::PortBlurDown => {
assert!(self.app.active(&Id::Username).is_ok()); assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Username,
InputMask::Smb => &Id::SmbShare,
InputMask::AwsS3 => panic!("this shouldn't happen (port on s3)"),
})
.is_ok());
} }
UiMsg::PortBlurUp => { UiMsg::PortBlurUp => {
assert!(self.app.active(&Id::Address).is_ok()); assert!(self.app.active(&Id::Address).is_ok());
@@ -171,6 +195,7 @@ impl AuthActivity {
.app .app
.active(match self.input_mask() { .active(match self.input_mask() {
InputMask::Generic => &Id::Address, InputMask::Generic => &Id::Address,
InputMask::Smb => &Id::Address,
InputMask::AwsS3 => &Id::S3Bucket, InputMask::AwsS3 => &Id::S3Bucket,
}) })
.is_ok()); .is_ok());
@@ -189,6 +214,10 @@ impl AuthActivity {
.app .app
.active(match self.input_mask() { .active(match self.input_mask() {
InputMask::Generic => &Id::Password, InputMask::Generic => &Id::Password,
#[cfg(unix)]
InputMask::Smb => &Id::SmbWorkgroup,
#[cfg(windows)]
InputMask::Smb => &Id::Password,
InputMask::AwsS3 => &Id::S3NewPathStyle, InputMask::AwsS3 => &Id::S3NewPathStyle,
}) })
.is_ok()); .is_ok());
@@ -247,6 +276,25 @@ impl AuthActivity {
UiMsg::S3NewPathStyleBlurUp => { UiMsg::S3NewPathStyleBlurUp => {
assert!(self.app.active(&Id::S3SessionToken).is_ok()); assert!(self.app.active(&Id::S3SessionToken).is_ok());
} }
UiMsg::SmbShareBlurDown => {
assert!(self.app.active(&Id::Username).is_ok());
}
UiMsg::SmbShareBlurUp => {
let id = if cfg!(windows) && self.input_mask() == InputMask::Smb {
&Id::Address
} else {
&Id::Port
};
assert!(self.app.active(id).is_ok());
}
#[cfg(unix)]
UiMsg::SmbWorkgroupDown => {
assert!(self.app.active(&Id::RemoteDirectory).is_ok());
}
#[cfg(unix)]
UiMsg::SmbWorkgroupUp => {
assert!(self.app.active(&Id::Password).is_ok());
}
UiMsg::SaveBookmarkPasswordBlur => { UiMsg::SaveBookmarkPasswordBlur => {
assert!(self.app.active(&Id::BookmarkName).is_ok()); assert!(self.app.active(&Id::BookmarkName).is_ok());
} }
@@ -272,7 +320,14 @@ impl AuthActivity {
assert!(self.app.active(&Id::Password).is_ok()); assert!(self.app.active(&Id::Password).is_ok());
} }
UiMsg::UsernameBlurUp => { UiMsg::UsernameBlurUp => {
assert!(self.app.active(&Id::Port).is_ok()); assert!(self
.app
.active(match self.input_mask() {
InputMask::Generic => &Id::Port,
InputMask::Smb => &Id::SmbShare,
InputMask::AwsS3 => panic!("this shouldn't happen (username on s3)"),
})
.is_ok());
} }
UiMsg::WindowResized => { UiMsg::WindowResized => {
self.redraw = true; self.redraw = true;
+167 -2
View File
@@ -11,7 +11,7 @@ use tuirealm::tui::widgets::Clear;
use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause}; use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause};
use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask}; use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams, SmbParams};
use crate::filetransfer::FileTransferParams; use crate::filetransfer::FileTransferParams;
use crate::utils::ui::{Popup, Size}; use crate::utils::ui::{Popup, Size};
@@ -56,6 +56,9 @@ impl AuthActivity {
self.mount_s3_security_token(""); self.mount_s3_security_token("");
self.mount_s3_session_token(""); self.mount_s3_session_token("");
self.mount_s3_new_path_style(false); self.mount_s3_new_path_style(false);
self.mount_smb_share("");
#[cfg(unix)]
self.mount_smb_workgroup("");
// Version notice // Version notice
if let Some(version) = self if let Some(version) = self
.context() .context()
@@ -150,7 +153,7 @@ impl AuthActivity {
InputMask::Generic => Layout::default() InputMask::Generic => Layout::default()
.constraints( .constraints(
[ [
Constraint::Length(3), // host Constraint::Length(3), // address
Constraint::Length(3), // port Constraint::Length(3), // port
Constraint::Length(3), // username Constraint::Length(3), // username
Constraint::Length(3), // password Constraint::Length(3), // password
@@ -160,6 +163,36 @@ impl AuthActivity {
) )
.direction(Direction::Vertical) .direction(Direction::Vertical)
.split(auth_chunks[4]), .split(auth_chunks[4]),
#[cfg(unix)]
InputMask::Smb => Layout::default()
.constraints(
[
Constraint::Length(3), // address
Constraint::Length(3), // port
Constraint::Length(3), // share
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // workgroup
Constraint::Length(3), // remote directory
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
#[cfg(windows)]
InputMask::Smb => Layout::default()
.constraints(
[
Constraint::Length(3), // address
Constraint::Length(3), // share
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // remote directory
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
}; };
// Create bookmark chunks // Create bookmark chunks
let bookmark_chunks = Layout::default() let bookmark_chunks = Layout::default()
@@ -188,6 +221,13 @@ impl AuthActivity {
self.app.view(&view_ids[2], f, input_mask[2]); self.app.view(&view_ids[2], f, input_mask[2]);
self.app.view(&view_ids[3], f, input_mask[3]); self.app.view(&view_ids[3], f, input_mask[3]);
} }
InputMask::Smb => {
let view_ids = self.get_smb_view();
self.app.view(&view_ids[0], f, input_mask[0]);
self.app.view(&view_ids[1], f, input_mask[1]);
self.app.view(&view_ids[2], f, input_mask[2]);
self.app.view(&view_ids[3], f, input_mask[3]);
}
} }
// Bookmark chunks // Bookmark chunks
self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]); self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]);
@@ -713,6 +753,31 @@ impl AuthActivity {
.is_ok()); .is_ok());
} }
pub(crate) fn mount_smb_share(&mut self, share: &str) {
let color = self.theme().auth_password;
assert!(self
.app
.remount(
Id::SmbShare,
Box::new(components::InputSmbShare::new(share, color)),
vec![]
)
.is_ok());
}
#[cfg(unix)]
pub(crate) fn mount_smb_workgroup(&mut self, workgroup: &str) {
let color = self.theme().auth_address;
assert!(self
.app
.remount(
Id::SmbWorkgroup,
Box::new(components::InputSmbWorkgroup::new(workgroup, color)),
vec![]
)
.is_ok());
}
// -- query // -- query
/// Collect input values from view /// Collect input values from view
@@ -748,6 +813,37 @@ impl AuthActivity {
.new_path_style(new_path_style) .new_path_style(new_path_style)
} }
/// Collect s3 input values from view
#[cfg(unix)]
pub(super) fn get_smb_params_input(&self) -> SmbParams {
let share: String = self.get_input_smb_share();
let workgroup: Option<String> = self.get_input_smb_workgroup();
let address: String = self.get_input_addr();
let port: u16 = self.get_input_port();
let username = self.get_input_username();
let password = self.get_input_password();
SmbParams::new(address, share)
.port(port)
.username(username)
.password(password)
.workgroup(workgroup)
}
#[cfg(windows)]
pub(super) fn get_smb_params_input(&self) -> SmbParams {
let share: String = self.get_input_smb_share();
let address: String = self.get_input_addr();
let username = self.get_input_username();
let password = self.get_input_password();
SmbParams::new(address, share)
.username(username)
.password(password)
}
pub(super) fn get_input_remote_directory(&self) -> Option<PathBuf> { pub(super) fn get_input_remote_directory(&self) -> Option<PathBuf> {
match self.app.state(&Id::RemoteDirectory) { match self.app.state(&Id::RemoteDirectory) {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => { Ok(State::One(StateValue::String(x))) if !x.is_empty() => {
@@ -851,6 +947,21 @@ impl AuthActivity {
) )
} }
pub(super) fn get_input_smb_share(&self) -> String {
match self.app.state(&Id::SmbShare) {
Ok(State::One(StateValue::String(x))) => x,
_ => String::new(),
}
}
#[cfg(unix)]
pub(super) fn get_input_smb_workgroup(&self) -> Option<String> {
match self.app.state(&Id::SmbWorkgroup) {
Ok(State::One(StateValue::String(x))) => Some(x),
_ => None,
}
}
/// Get new bookmark params /// Get new bookmark params
pub(super) fn get_new_bookmark(&self) -> (String, bool) { pub(super) fn get_new_bookmark(&self) -> (String, bool) {
let name = match self.app.state(&Id::BookmarkName) { let name = match self.app.state(&Id::BookmarkName) {
@@ -874,6 +985,7 @@ impl AuthActivity {
match self.input_mask() { match self.input_mask() {
InputMask::AwsS3 => 12, InputMask::AwsS3 => 12,
InputMask::Generic => 12, InputMask::Generic => 12,
InputMask::Smb => 12,
} }
} }
@@ -913,6 +1025,25 @@ impl AuthActivity {
protocol, username, params.address, params.port protocol, username, params.address, params.port
) )
} }
#[cfg(unix)]
ProtocolParams::Smb(params) => {
let username: String = match params.username {
None => String::default(),
Some(u) => format!("{u}@"),
};
format!(
"\\\\{username}{}:{}\\{}",
params.address, params.port, params.share
)
}
#[cfg(windows)]
ProtocolParams::Smb(params) => {
let username: String = match params.username {
None => String::default(),
Some(u) => format!("{u}@"),
};
format!("\\\\{username}{}\\{}", params.address, params.share)
}
} }
} }
@@ -966,6 +1097,40 @@ impl AuthActivity {
} }
} }
#[cfg(unix)]
fn get_smb_view(&self) -> [Id; 4] {
match self.app.focus() {
Some(&Id::Address | &Id::Port | &Id::SmbShare | &Id::Username) => {
[Id::Address, Id::Port, Id::SmbShare, Id::Username]
}
Some(&Id::Password) => [Id::Port, Id::SmbShare, Id::Username, Id::Password],
Some(&Id::SmbWorkgroup) => [Id::SmbShare, Id::Username, Id::Password, Id::SmbWorkgroup],
Some(&Id::RemoteDirectory) => [
Id::Username,
Id::Password,
Id::SmbWorkgroup,
Id::RemoteDirectory,
],
_ => [Id::Address, Id::Port, Id::SmbShare, Id::Username],
}
}
#[cfg(windows)]
fn get_smb_view(&self) -> [Id; 4] {
match self.app.focus() {
Some(&Id::Address | &Id::Password | &Id::SmbShare | &Id::Username) => {
[Id::Address, Id::SmbShare, Id::Username, Id::Password]
}
Some(&Id::RemoteDirectory) => [
Id::SmbShare,
Id::Username,
Id::Password,
Id::RemoteDirectory,
],
_ => [Id::Address, Id::SmbShare, Id::Username, Id::Password],
}
}
fn init_global_listener(&mut self) { fn init_global_listener(&mut self) {
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
assert!(self assert!(self
@@ -3,7 +3,7 @@ use remotefs::fs::UnixPex;
use super::{FileTransferActivity, LogLevel}; use super::{FileTransferActivity, LogLevel};
impl FileTransferActivity { impl FileTransferActivity {
#[cfg(target_family = "unix")] #[cfg(unix)]
pub fn action_local_chmod(&mut self, mode: UnixPex) { pub fn action_local_chmod(&mut self, mode: UnixPex) {
let files = self.get_local_selected_entries().get_files(); let files = self.get_local_selected_entries().get_files();
@@ -51,7 +51,7 @@ impl FileTransferActivity {
} }
} }
#[cfg(target_family = "unix")] #[cfg(unix)]
pub fn action_find_local_chmod(&mut self, mode: UnixPex) { pub fn action_find_local_chmod(&mut self, mode: UnixPex) {
let files = self.get_found_selected_entries().get_files(); let files = self.get_found_selected_entries().get_files();
@@ -9,7 +9,7 @@ use super::{FileTransferActivity, LogLevel, SelectedFile};
impl FileTransferActivity { impl FileTransferActivity {
/// Create symlink on localhost /// Create symlink on localhost
#[cfg(target_family = "unix")] #[cfg(unix)]
pub(crate) fn action_local_symlink(&mut self, name: String) { pub(crate) fn action_local_symlink(&mut self, name: String) {
if let SelectedFile::One(entry) = self.get_local_selected_entries() { if let SelectedFile::One(entry) = self.get_local_selected_entries() {
match self match self
@@ -33,7 +33,7 @@ impl FileTransferActivity {
} }
} }
#[cfg(target_family = "windows")] #[cfg(windows)]
pub(crate) fn action_local_symlink(&mut self, _name: String) { pub(crate) fn action_local_symlink(&mut self, _name: String) {
self.mount_error("Symlinks are not supported on Windows hosts"); self.mount_error("Symlinks are not supported on Windows hosts");
} }
@@ -13,7 +13,7 @@ use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan,
}; };
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue};
#[cfg(target_family = "unix")] #[cfg(unix)]
use users::{get_group_by_gid, get_user_by_uid}; use users::{get_group_by_gid, get_user_by_uid};
use super::super::Browser; use super::super::Browser;
@@ -445,7 +445,7 @@ impl FileInfoPopup {
.add_col(TextSpan::from("Last access time: ")) .add_col(TextSpan::from("Last access time: "))
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
// User // User
#[cfg(target_family = "unix")] #[cfg(unix)]
let username: String = match file.metadata().uid { let username: String = match file.metadata().uid {
Some(uid) => match get_user_by_uid(uid) { Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
@@ -453,10 +453,10 @@ impl FileInfoPopup {
}, },
None => String::from("0"), None => String::from("0"),
}; };
#[cfg(target_os = "windows")] #[cfg(windows)]
let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); let username: String = format!("{}", file.metadata().uid.unwrap_or(0));
// Group // Group
#[cfg(target_family = "unix")] #[cfg(unix)]
let group: String = match file.metadata().gid { let group: String = match file.metadata().gid {
Some(gid) => match get_group_by_gid(gid) { Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(), Some(group) => group.name().to_string_lossy().to_string(),
@@ -464,7 +464,7 @@ impl FileInfoPopup {
}, },
None => String::from("0"), None => String::from("0"),
}; };
#[cfg(target_os = "windows")] #[cfg(windows)]
let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
texts texts
.add_row() .add_row()
+8
View File
@@ -111,6 +111,7 @@ impl FileTransferActivity {
match &ft_params.params { match &ft_params.params {
ProtocolParams::Generic(params) => params.address.clone(), ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(), ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Smb(params) => params.address.clone(),
} }
} }
@@ -133,6 +134,13 @@ impl FileTransferActivity {
); );
format!("Connecting to {}…", params.bucket_name) format!("Connecting to {}…", params.bucket_name)
} }
ProtocolParams::Smb(params) => {
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.share
);
format!("Connecting to \\\\{}\\{}…", params.address, params.share)
}
} }
} }
+5 -4
View File
@@ -220,6 +220,8 @@ pub struct FileTransferActivity {
cache: Option<TempDir>, cache: Option<TempDir>,
/// Fs watcher /// Fs watcher
fswatcher: Option<FsWatcher>, fswatcher: Option<FsWatcher>,
/// conncted once
connected: bool,
} }
impl FileTransferActivity { impl FileTransferActivity {
@@ -252,6 +254,7 @@ impl FileTransferActivity {
None None
} }
}, },
connected: false,
} }
} }
@@ -372,14 +375,12 @@ impl Activity for FileTransferActivity {
return; return;
} }
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error) // Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && !self.app.mounted(&Id::FatalPopup) { if (!self.client.is_connected() || !self.connected) && !self.app.mounted(&Id::FatalPopup) {
let ftparams = self.context().ft_params().unwrap(); let ftparams = self.context().ft_params().unwrap();
// print params // print params
let msg: String = Self::get_connection_msg(&ftparams.params); let msg: String = Self::get_connection_msg(&ftparams.params);
// Set init state to connecting popup // Set init state to connecting popup
self.mount_wait(msg.as_str()); self.mount_blocking_wait(msg.as_str());
// Force ui draw
self.view();
// Connect to remote // Connect to remote
self.connect(); self.connect();
// Redraw // Redraw
@@ -57,6 +57,7 @@ impl FileTransferActivity {
// Connect to remote // Connect to remote
match self.client.connect() { match self.client.connect() {
Ok(Welcome { banner, .. }) => { Ok(Welcome { banner, .. }) => {
self.connected = true;
if let Some(banner) = banner { if let Some(banner) = banner {
// Log welcome // Log welcome
self.log( self.log(
+6 -6
View File
@@ -36,13 +36,13 @@ impl FileTransferActivity {
self.umount_chmod(); self.umount_chmod();
self.mount_blocking_wait("Applying new file mode…"); self.mount_blocking_wait("Applying new file mode…");
match self.browser.tab() { match self.browser.tab() {
#[cfg(target_family = "unix")] #[cfg(unix)]
FileExplorerTab::Local => self.action_local_chmod(mode), FileExplorerTab::Local => self.action_local_chmod(mode),
#[cfg(target_family = "unix")] #[cfg(unix)]
FileExplorerTab::FindLocal => self.action_find_local_chmod(mode), FileExplorerTab::FindLocal => self.action_find_local_chmod(mode),
FileExplorerTab::Remote => self.action_remote_chmod(mode), FileExplorerTab::Remote => self.action_remote_chmod(mode),
FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode), FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode),
#[cfg(target_family = "windows")] #[cfg(windows)]
FileExplorerTab::Local | FileExplorerTab::FindLocal => {} FileExplorerTab::Local | FileExplorerTab::FindLocal => {}
} }
self.umount_wait(); self.umount_wait();
@@ -441,13 +441,13 @@ impl FileTransferActivity {
} }
UiMsg::ShowChmodPopup => { UiMsg::ShowChmodPopup => {
let selected_file = match self.browser.tab() { let selected_file = match self.browser.tab() {
#[cfg(target_family = "unix")] #[cfg(unix)]
FileExplorerTab::Local => self.get_local_selected_entries(), FileExplorerTab::Local => self.get_local_selected_entries(),
#[cfg(target_family = "unix")] #[cfg(unix)]
FileExplorerTab::FindLocal => self.get_found_selected_entries(), FileExplorerTab::FindLocal => self.get_found_selected_entries(),
FileExplorerTab::Remote => self.get_remote_selected_entries(), FileExplorerTab::Remote => self.get_remote_selected_entries(),
FileExplorerTab::FindRemote => self.get_found_selected_entries(), FileExplorerTab::FindRemote => self.get_found_selected_entries(),
#[cfg(target_family = "windows")] #[cfg(windows)]
FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None, FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None,
}; };
if let Some(mode) = selected_file.unix_pex() { if let Some(mode) = selected_file.unix_pex() {
+11 -6
View File
@@ -11,6 +11,10 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent};
use super::{ConfigMsg, Msg}; use super::{ConfigMsg, Msg};
use crate::explorer::GroupDirs as GroupDirsEnum; use crate::explorer::GroupDirs as GroupDirsEnum;
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
use crate::ui::activities::setup::{
RADIO_PROTOCOL_FTP, RADIO_PROTOCOL_FTPS, RADIO_PROTOCOL_S3, RADIO_PROTOCOL_SCP,
RADIO_PROTOCOL_SFTP, RADIO_PROTOCOL_SMB,
};
use crate::utils::parser::parse_bytesize; use crate::utils::parser::parse_bytesize;
// -- components // -- components
@@ -63,16 +67,17 @@ impl DefaultProtocol {
.color(Color::Cyan) .color(Color::Cyan)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(&["SFTP", "SCP", "FTP", "FTPS", "S3"]) .choices(&["SFTP", "SCP", "FTP", "FTPS", "S3", "SMB"])
.foreground(Color::Cyan) .foreground(Color::Cyan)
.rewind(true) .rewind(true)
.title("Default protocol", Alignment::Left) .title("Default protocol", Alignment::Left)
.value(match protocol { .value(match protocol {
FileTransferProtocol::AwsS3 => 4, FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP,
FileTransferProtocol::Ftp(true) => 3, FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP,
FileTransferProtocol::Ftp(false) => 2, FileTransferProtocol::Ftp(false) => RADIO_PROTOCOL_FTP,
FileTransferProtocol::Scp => 1, FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS,
FileTransferProtocol::Sftp => 0, FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3,
FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB,
}), }),
} }
} }
+8
View File
@@ -24,6 +24,14 @@ use crate::config::themes::Theme;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider; use crate::system::theme_provider::ThemeProvider;
// radio
const RADIO_PROTOCOL_SFTP: usize = 0;
const RADIO_PROTOCOL_SCP: usize = 1;
const RADIO_PROTOCOL_FTP: usize = 2;
const RADIO_PROTOCOL_FTPS: usize = 3;
const RADIO_PROTOCOL_S3: usize = 4;
const RADIO_PROTOCOL_SMB: usize = 5;
// -- components // -- components
#[derive(Debug, Eq, PartialEq, Clone, Hash)] #[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Id { enum Id {
+9 -4
View File
@@ -13,6 +13,10 @@ use tuirealm::{State, StateValue};
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout}; use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
use crate::explorer::GroupDirs; use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
use crate::ui::activities::setup::{
RADIO_PROTOCOL_FTP, RADIO_PROTOCOL_FTPS, RADIO_PROTOCOL_S3, RADIO_PROTOCOL_SCP,
RADIO_PROTOCOL_SMB,
};
use crate::utils::fmt::fmt_bytes; use crate::utils::fmt::fmt_bytes;
impl SetupActivity { impl SetupActivity {
@@ -268,10 +272,11 @@ impl SetupActivity {
self.app.state(&Id::Config(IdConfig::DefaultProtocol)) self.app.state(&Id::Config(IdConfig::DefaultProtocol))
{ {
let protocol: FileTransferProtocol = match protocol { let protocol: FileTransferProtocol = match protocol {
1 => FileTransferProtocol::Scp, RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false), RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true), RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3, RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3,
RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb,
_ => FileTransferProtocol::Sftp, _ => FileTransferProtocol::Sftp,
}; };
self.config_mut().set_default_protocol(protocol); self.config_mut().set_default_protocol(protocol);
+1 -1
View File
@@ -308,7 +308,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(target_family = "unix")] #[cfg(unix)]
fn test_utils_fmt_path_elide() { fn test_utils_fmt_path_elide() {
let p: &Path = Path::new("/develop/pippo"); let p: &Path = Path::new("/develop/pippo");
// Under max size // Under max size
+178 -3
View File
@@ -12,6 +12,8 @@ use lazy_regex::{Lazy, Regex};
use tuirealm::tui::style::Color; use tuirealm::tui::style::Color;
use tuirealm::utils::parser as tuirealm_parser; use tuirealm::utils::parser as tuirealm_parser;
#[cfg(smb)]
use crate::filetransfer::params::SmbParams;
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
#[cfg(not(test))] // NOTE: don't use configuration during tests #[cfg(not(test))] // NOTE: don't use configuration during tests
@@ -25,9 +27,10 @@ use crate::system::environment;
* This regex matches the protocol used as option * This regex matches the protocol used as option
* Regex matches: * Regex matches:
* - group 1: Some(protocol) | None * - group 1: Some(protocol) | None
* - group 2: Some(other args) * - group 2: SMB windows prefix
* - group 3: Some(other args)
*/ */
static REMOTE_OPT_PROTOCOL_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([a-z0-9]+)://)?(?:(.+))"); static REMOTE_OPT_PROTOCOL_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([a-z0-9]+)://)?(\\\\)?(?:(.+))");
/** /**
* Regex matches: * Regex matches:
@@ -50,6 +53,30 @@ static REMOTE_GENERIC_OPT_REGEX: Lazy<Regex> = lazy_regex!(
static REMOTE_S3_OPT_REGEX: Lazy<Regex> = static REMOTE_S3_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([^@]+)@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?"); lazy_regex!(r"(?:([^@]+)@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?");
/**
* Regex matches:
* - group 1: username
* - group 2: address
* - group 3: port?
* - group 4: share?
* - group 5: remote-dir?
*/
#[cfg(smb_unix)]
static REMOTE_SMB_OPT_REGEX: Lazy<Regex> = lazy_regex!(
r"(?:([^@]+)@)?(?:([^/:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?:/([^/]+))?(?:(/.+))?"
);
/**
* Regex matches:
* - group 1: username?
* - group 2: address
* - group 3: share
* - group 4: remote-dir?
*/
#[cfg(windows)]
static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([^@]+)@)?(?:([^:\\]+))(?:\\([^\\]+))?(?:(\\.+))?");
/** /**
* Regex matches: * Regex matches:
* - group 1: Version * - group 1: Version
@@ -83,6 +110,21 @@ static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B
/// - sftp://172.26.104.1:4022 /// - sftp://172.26.104.1:4022
/// - sftp://172.26.104.1 /// - sftp://172.26.104.1
/// - ... /// - ...
///
/// For s3:
///
/// s3://<bucket-name>@<region>[:profile][:/wrkdir]
///
/// For SMB:
///
/// on UNIX derived (macos, linux, ...)
///
/// smb://[username@]<address>[:port]/<share>[/path]
///
/// on Windows
///
/// \\<address>\<share>[\path]
///
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> { pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
// Set protocol to default protocol // Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests #[cfg(not(test))] // NOTE: don't use configuration during tests
@@ -108,6 +150,8 @@ pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
// Match against regex for protocol type // Match against regex for protocol type
match protocol { match protocol {
FileTransferProtocol::AwsS3 => parse_s3_remote_opt(s.as_str()), FileTransferProtocol::AwsS3 => parse_s3_remote_opt(s.as_str()),
#[cfg(smb)]
FileTransferProtocol::Smb => parse_smb_remote_opts(s.as_str()),
protocol => parse_generic_remote_opt(s.as_str(), protocol), protocol => parse_generic_remote_opt(s.as_str(), protocol),
} }
} }
@@ -127,13 +171,15 @@ fn parse_remote_opt_protocol(
let protocol = match protocol { let protocol = match protocol {
Some(Ok(protocol)) => protocol, Some(Ok(protocol)) => protocol,
Some(Err(err)) => return Err(err), Some(Err(err)) => return Err(err),
#[cfg(windows)]
None if groups.get(2).is_some() => FileTransferProtocol::Smb,
None => default, None => default,
}; };
// Return protocol and remaining arguments // Return protocol and remaining arguments
Ok(( Ok((
protocol, protocol,
groups groups
.get(2) .get(3)
.map(|x| x.as_str().to_string()) .map(|x| x.as_str().to_string())
.unwrap_or_default(), .unwrap_or_default(),
)) ))
@@ -220,6 +266,70 @@ fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
} }
} }
/// Parse remote options for smb protocol
#[cfg(smb_unix)]
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
match REMOTE_SMB_OPT_REGEX.captures(s) {
Some(groups) => {
let username: Option<String> = match groups.get(1) {
Some(group) => Some(group.as_str().to_string()),
None => Some(whoami::username()),
};
let address = match groups.get(2) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
let port = match groups.get(3) {
Some(port) => match port.as_str().parse::<u16>() {
// Try to parse port
Ok(p) => p,
Err(err) => return Err(format!("Bad port \"{}\": {}", port.as_str(), err)),
},
None => 445,
};
let share = match groups.get(4) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
let entry_directory: Option<PathBuf> =
groups.get(5).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(
FileTransferProtocol::Smb,
ProtocolParams::Smb(SmbParams::new(address, share).port(port).username(username)),
)
.entry_directory(entry_directory))
}
None => Err(String::from("Bad remote host syntax!")),
}
}
#[cfg(windows)]
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
match REMOTE_SMB_OPT_REGEX.captures(s) {
Some(groups) => {
let username = groups.get(1).map(|x| x.as_str().to_string());
let address = match groups.get(2) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
let share = match groups.get(3) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
let entry_directory: Option<PathBuf> =
groups.get(4).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(
FileTransferProtocol::Smb,
ProtocolParams::Smb(SmbParams::new(address, share).username(username)),
)
.entry_directory(entry_directory))
}
None => Err(String::from("Bad remote host syntax!")),
}
}
/// Parse semver string /// Parse semver string
pub fn parse_semver(haystack: &str) -> Option<String> { pub fn parse_semver(haystack: &str) -> Option<String> {
match SEMVER_REGEX.captures(haystack) { match SEMVER_REGEX.captures(haystack) {
@@ -502,6 +612,71 @@ mod tests {
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err()); assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
} }
#[test]
#[cfg(smb_unix)]
fn should_parse_smb_address() {
let result = parse_remote_opt("smb://myserver/myshare").ok().unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.port, 445);
assert_eq!(params.share.as_str(), "myshare");
assert!(params.username.is_some());
assert!(params.password.is_none());
assert!(params.workgroup.is_none());
assert!(result.entry_directory.is_none());
}
#[test]
#[cfg(smb_unix)]
fn should_parse_smb_address_with_opts() {
let result = parse_remote_opt("smb://omar@myserver:4445/myshare/dir/subdir")
.ok()
.unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.port, 4445);
assert_eq!(params.username.as_deref().unwrap(), "omar");
assert!(params.password.is_none());
assert!(params.workgroup.is_none());
assert_eq!(params.share.as_str(), "myshare");
assert_eq!(
result.entry_directory.as_deref().unwrap(),
std::path::Path::new("/dir/subdir")
);
}
#[test]
#[cfg(windows)]
fn should_parse_smb_address() {
let result = parse_remote_opt(&String::from("\\\\myserver\\myshare"))
.ok()
.unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.share.as_str(), "myshare");
assert!(result.entry_directory.is_none());
}
#[test]
#[cfg(windows)]
fn should_parse_smb_address_with_opts() {
let result = parse_remote_opt(&String::from("\\\\omar@myserver\\myshare\\path"))
.ok()
.unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.share.as_str(), "myshare");
assert_eq!(params.username.as_deref().unwrap(), "omar");
assert_eq!(
result.entry_directory.as_deref().unwrap(),
std::path::Path::new("\\path")
);
}
#[test] #[test]
fn test_utils_parse_semver() { fn test_utils_parse_semver() {
assert_eq!( assert_eq!(