From b7369162d243081b05c67f2fd79ebb25eb87a60c Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Sat, 13 May 2023 15:00:16 +0200 Subject: [PATCH] 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 --- .github/workflows/coverage.yml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 1 - CHANGELOG.md | 2 + Cargo.lock | 35 ++++ Cargo.toml | 7 + README.md | 4 + build.rs | 15 ++ dist/build/aarch64_centos7/Dockerfile | 1 + dist/build/aarch64_debian9/Dockerfile | 2 + dist/build/x86_64_centos7/Dockerfile | 1 + dist/build/x86_64_debian9/Dockerfile | 2 + docs/de/README.md | 3 + docs/de/man.md | 17 ++ docs/es/README.md | 9 +- docs/es/man.md | 17 ++ docs/fr/README.md | 9 +- docs/fr/man.md | 18 ++ docs/it/README.md | 9 +- docs/it/man.md | 18 ++ docs/man.md | 17 ++ docs/zh-CN/README.md | 3 + docs/zh-CN/man.md | 17 ++ install.sh | 8 +- src/cli_opts.rs | 2 + src/config/bookmarks.rs | 136 ++++++++++++- src/config/serialization.rs | 50 ++++- src/explorer/formatter.rs | 38 ++-- src/explorer/mod.rs | 4 +- src/filetransfer/builder.rs | 63 ++++++ src/filetransfer/mod.rs | 22 ++- src/filetransfer/params.rs | 127 ++++++++++++ src/host/mod.rs | 62 +++--- src/system/bookmarks_client.rs | 22 --- src/system/watcher/mod.rs | 2 +- src/ui/activities/auth/bookmarks.rs | 14 +- src/ui/activities/auth/components/form.rs | 100 +++++++++- src/ui/activities/auth/components/mod.rs | 4 +- src/ui/activities/auth/misc.rs | 25 ++- src/ui/activities/auth/mod.rs | 19 ++ src/ui/activities/auth/update.rs | 63 +++++- src/ui/activities/auth/view.rs | 169 +++++++++++++++- .../activities/filetransfer/actions/chmod.rs | 4 +- .../filetransfer/actions/symlink.rs | 4 +- .../filetransfer/components/popups.rs | 10 +- src/ui/activities/filetransfer/misc.rs | 8 + src/ui/activities/filetransfer/mod.rs | 9 +- src/ui/activities/filetransfer/session.rs | 1 + src/ui/activities/filetransfer/update.rs | 12 +- src/ui/activities/setup/components/config.rs | 17 +- src/ui/activities/setup/mod.rs | 8 + src/ui/activities/setup/view/setup.rs | 13 +- src/utils/fmt.rs | 2 +- src/utils/parser.rs | 181 +++++++++++++++++- 54 files changed, 1256 insertions(+), 154 deletions(-) create mode 100644 build.rs diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index efe9fc0..9cc402d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - 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 uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 93b88a7..b1aa19b 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - 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 with: toolchain: stable diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 5092704..ea56ebb 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -8,7 +8,6 @@ env: jobs: build: runs-on: macos-latest - steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6794b32..269f13f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - [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 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 diff --git a/Cargo.lock b/Cargo.lock index f242f29..1ed020d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.24" @@ -2042,6 +2048,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "percent-encoding" version = "2.2.0" @@ -2259,6 +2278,20 @@ dependencies = [ "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]] name = "remotefs-ssh" version = "0.2.0" @@ -2897,6 +2930,7 @@ dependencies = [ "argh", "bitflags 2.2.0", "bytesize", + "cfg_aliases", "chrono", "content_inspector", "dirs 5.0.0", @@ -2916,6 +2950,7 @@ dependencies = [ "remotefs", "remotefs-aws-s3", "remotefs-ftp", + "remotefs-smb", "remotefs-ssh", "rpassword", "self_update", diff --git a/Cargo.toml b/Cargo.toml index f63a7f6..468677d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,11 +70,18 @@ wildmatch = "^2.1" pretty_assertions = "^1.3" serial_test = "^2.0" +[build-dependencies] +cfg_aliases = "0.1" + [features] default = [ "with-keyring" ] github-actions = [ ] 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\")".dependencies] remotefs-ftp = { version = "^0.1.2", features = [ "native-tls" ] } diff --git a/README.md b/README.md index 28486a3..012ada0 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for - **SCP** - **FTP** and **FTPS** - **S3** + - **SMB** - 🖥 Explore and operate on the remote and on the local machine file system with a handy UI - Create, remove, rename, search, view and edit files - ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections @@ -187,9 +188,11 @@ For more information or other platforms, please visit [termscp.veeso.dev](https: - **Linux** users: - libdbus-1 - pkg-config + - libsmbclient - **FreeBSD** or, **NetBSD** users: - dbus - pkgconf + - libsmbclient ### Optional Requirements ✔️ @@ -261,6 +264,7 @@ termscp is powered by these awesome projects: - [edit](https://github.com/milkey-mouse/edit) - [keyring-rs](https://github.com/hwchen/keyring-rs) - [open-rs](https://github.com/Byron/open-rs) +- [pavao](https://github.com/veeso/pavao) - [remotefs](https://github.com/veeso/remotefs-rs) - [rpassword](https://github.com/conradkleinespel/rpassword) - [self_update](https://github.com/jaemk/self_update) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e1dcbd8 --- /dev/null +++ b/build.rs @@ -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)) } + } +} diff --git a/dist/build/aarch64_centos7/Dockerfile b/dist/build/aarch64_centos7/Dockerfile index 1264477..ba94119 100644 --- a/dist/build/aarch64_centos7/Dockerfile +++ b/dist/build/aarch64_centos7/Dockerfile @@ -9,6 +9,7 @@ RUN yum -y install \ gcc \ make \ dbus-devel \ + libsmbclient-devel \ bash \ rpm-build # Install rust diff --git a/dist/build/aarch64_debian9/Dockerfile b/dist/build/aarch64_debian9/Dockerfile index 15d069a..5de422d 100644 --- a/dist/build/aarch64_debian9/Dockerfile +++ b/dist/build/aarch64_debian9/Dockerfile @@ -8,6 +8,8 @@ RUN apt update && apt install -y \ pkg-config \ libdbus-1-dev \ build-essential \ + libsmbclient-dev \ + libsmbclient \ bash \ curl diff --git a/dist/build/x86_64_centos7/Dockerfile b/dist/build/x86_64_centos7/Dockerfile index 1264477..ba94119 100644 --- a/dist/build/x86_64_centos7/Dockerfile +++ b/dist/build/x86_64_centos7/Dockerfile @@ -9,6 +9,7 @@ RUN yum -y install \ gcc \ make \ dbus-devel \ + libsmbclient-devel \ bash \ rpm-build # Install rust diff --git a/dist/build/x86_64_debian9/Dockerfile b/dist/build/x86_64_debian9/Dockerfile index 15d069a..5de422d 100644 --- a/dist/build/x86_64_debian9/Dockerfile +++ b/dist/build/x86_64_debian9/Dockerfile @@ -8,6 +8,8 @@ RUN apt update && apt install -y \ pkg-config \ libdbus-1-dev \ build-essential \ + libsmbclient-dev \ + libsmbclient \ bash \ curl diff --git a/docs/de/README.md b/docs/de/README.md index 4437b8a..16e1b8f 100644 --- a/docs/de/README.md +++ b/docs/de/README.md @@ -137,6 +137,7 @@ Termscp ist ein funktionsreicher Terminal-Dateitransfer und Explorer mit Unterst - **SCP** - **FTP** und **FTPS** - **S3** + - **SMB** - 🖥 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 - ⭐ 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 - libdbus-1 - pkg-config + - libsmbclient - **FreeBSD** Benutzer: - libssh - dbus - pkgconf + - libsmbclient ### Optionale Softwareanforderungen ✔️ diff --git a/docs/de/man.md b/docs/de/man.md index 85a875c..8d9128e 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -4,6 +4,7 @@ - [Usage ❓](#usage-) - [Address argument 🌎](#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-) - [S3 connection parameters](#s3-connection-parameters) - [S3 credentials 🦊](#s3-credentials-) @@ -105,6 +106,22 @@ e.g. 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@]\[\path\...] +``` + +**Other systems** syntax: + +```txt +smb://[username@][:port]/[/path/.../] +``` + #### How Password can be provided 🔐 You have probably noticed, that, when providing the address as argument, there's no way to provide the password. diff --git a/docs/es/README.md b/docs/es/README.md index 079e9d9..265b312 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -137,6 +137,7 @@ Termscp es un explorador y transferencia de archivos de terminal rico en funcion - **SCP** - **FTP** y **FTPS** - **S3** + - **SMB** - 🖥 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 - ⭐ 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 ❗ -- Usuarios **Linux**: - - libssh +- **Linux** users: - libdbus-1 - pkg-config -- Usuarios **FreeBSD**: - - libssh + - libsmbclient +- **FreeBSD** or, **NetBSD** users: - dbus - pkgconf + - libsmbclient ### Requisitos opcionales ✔️ diff --git a/docs/es/man.md b/docs/es/man.md index cad7200..569fe6d 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -4,6 +4,7 @@ - [Uso ❓](#uso-) - [Argumento dirección 🌎](#argumento-dirección-) - [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-) - [S3 parámetros de conexión](#s3-parámetros-de-conexión) - [Credenciales de S3 🦊](#credenciales-de-s3-) @@ -105,6 +106,22 @@ por ejemplo 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@]\[\path\...] +``` + +**Other systems** sintaxis: + +```txt +smb://[username@][:port]/[/path/.../] +``` + #### 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. diff --git a/docs/fr/README.md b/docs/fr/README.md index a321fb5..1820bc4 100644 --- a/docs/fr/README.md +++ b/docs/fr/README.md @@ -137,6 +137,7 @@ Termscp est un file transfer et explorateur de fichiers de terminal riche en fon - **SCP** - **FTP** et **FTPS** - **S3** + - **SMB** - 🖥 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 - ⭐ 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 ❗ -- utilisateurs **Linux**: - - libssh +- **Linux** users: - libdbus-1 - pkg-config -- utilisateurs **FreeBSD**: - - libssh + - libsmbclient +- **FreeBSD** or, **NetBSD** users: - dbus - pkgconf + - libsmbclient ### Requis facultatives ✔️ diff --git a/docs/fr/man.md b/docs/fr/man.md index dacc17a..873986d 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -4,6 +4,7 @@ - [Usage ❓](#usage-) - [Argument d'adresse 🌎](#argument-dadresse-) - [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-) - [S3 paramètres de connexion](#s3-paramètres-de-connexion) - [Identifiants S3 🦊](#identifiants-s3-) @@ -103,6 +104,23 @@ e.g. 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@]\[\path\...] +``` + +syntaxe **Other systems**: + +```txt +smb://[username@][:port]/[/path/.../] +``` + + #### Comment le mot de passe peut être fourni 🔐 Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe. diff --git a/docs/it/README.md b/docs/it/README.md index 6787d98..a780256 100644 --- a/docs/it/README.md +++ b/docs/it/README.md @@ -137,6 +137,7 @@ Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a S - **SCP** - **FTP** and **FTPS** - **S3** + - **SMB** - 🖥 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 - ⭐ 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 ❗ -- Utenti **Linux**: - - libssh +- **Linux** users: - libdbus-1 - pkg-config -- Utenti **FreeBSD**: - - libssh + - libsmbclient +- **FreeBSD** or, **NetBSD** users: - dbus - pkgconf + - libsmbclient ### Requisiti opzionali ✔️ diff --git a/docs/it/man.md b/docs/it/man.md index 68d3d15..132a341 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -4,6 +4,7 @@ - [Argomenti da linea di comando ❓](#argomenti-da-linea-di-comando-) - [Argomento indirizzo 🌎](#argomento-indirizzo-) - [Argomento indirizzo per AWS S3](#argomento-indirizzo-per-aws-s3) + - [Indirizzo SMB](#indirizzo-smb) - [Come fornire la password 🔐](#come-fornire-la-password-) - [Parametri di connessione S3](#parametri-di-connessione-s3) - [Credenziali S3 🦊](#credenziali-s3-) @@ -101,6 +102,23 @@ e.g. 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@]\[\path\...] +``` + +**Altri sistemi**: + +```txt +smb://[username@][:port]/[/path/.../] +``` + + #### Come fornire la password 🔐 Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo: diff --git a/docs/man.md b/docs/man.md index 6018972..14a564e 100644 --- a/docs/man.md +++ b/docs/man.md @@ -4,6 +4,7 @@ - [Usage ❓](#usage-) - [Address argument 🌎](#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-) - [S3 connection parameters](#s3-connection-parameters) - [S3 credentials 🦊](#s3-credentials-) @@ -103,6 +104,22 @@ e.g. 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@]\[\path\...] +``` + +**Other systems** syntax: + +```txt +smb://[username@][:port]/[/path/.../] +``` + #### How Password can be provided 🔐 You have probably noticed, that, when providing the address as argument, there's no way to provide the password. diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 4f5028b..9158356 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -139,6 +139,7 @@ termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/S - **SCP** - **FTP** and **FTPS** - **S3** + - **SMB** - 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作 - 创建、删除、重命名、搜索、查看和编辑文件 - ⭐ 通过“内置书签”和“最近连接”快速连接到您的主机 @@ -188,10 +189,12 @@ choco install termscp - libssh - libdbus-1 - pkg-config + - libsmbclient - **FreeBSD** 用户: - libssh - dbus - pkgconf + - libsmbclient ### 可选项 ✔️ diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index b7accc2..5b26db0 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -4,6 +4,7 @@ - [用法](#用法) - [地址参数](#地址参数) - [AWS S3 地址参数](#aws-s3-地址参数) + - [SMB 地址参数](#smb-地址参数) - [如何输入密码](#如何输入密码) - [S3 连接参数](#s3-连接参数) - [Aws S3 凭证](#aws-s3-凭证) @@ -102,6 +103,22 @@ s3://@[:profile][:/wrkdir] s3://buckethead@eu-central-1:default:/assets ``` +#### SMB 地址参数 + +SMB 对 CLI 地址参数有不同的语法,无论您是在 Windows 还是其他系统上,这都是不同的: + +**Windows** 句法: + +```txt +\\[username@]\[\path\...] +``` + +**其他系统** 句法: + +```txt +smb://[username@][:port]/[/path/.../] +``` + #### 如何输入密码 你可能已经注意到,url参数中没有办法直接附加密码,你可以通过以下三种方式提供密码: diff --git a/install.sh b/install.sh index 60c8329..e45fcd8 100755 --- a/install.sh +++ b/install.sh @@ -304,14 +304,14 @@ install_bsd_cargo_deps() { set -e confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?" 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" } install_linux_cargo_deps() { - local debian_deps="gcc pkg-config libdbus-1-dev" - local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel" - local arch_deps="gcc openssl pkg-config dbus" + local debian_deps="gcc pkg-config libdbus-1-dev libsmbclient-dev" + local rpm_deps="gcc openssl pkgconfig libdbus-devel openssl-devel libsmbclient-devel" + local arch_deps="gcc openssl pkg-config dbus smbclient" local deps_cmd="" # Get pkg manager if has apt; then diff --git a/src/cli_opts.rs b/src/cli_opts.rs index 1ee60dd..44c7f0e 100644 --- a/src/cli_opts.rs +++ b/src/cli_opts.rs @@ -28,6 +28,8 @@ Address syntax can be: - `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp - `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol + - `\\\\[:port]\\[\\path]` for SMB (on Windows) + - `smb://[user@][:port][/path]` for SMB (on other systems) Please, report issues to Please, consider supporting the author ")] diff --git a/src/config/bookmarks.rs b/src/config/bookmarks.rs index 204432a..711598d 100644 --- a/src/config/bookmarks.rs +++ b/src/config/bookmarks.rs @@ -9,7 +9,9 @@ use std::str::FromStr; use serde::de::Error as DeError; 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}; /// UserHosts contains all the hosts saved by the user in the data storage @@ -40,6 +42,8 @@ pub struct Bookmark { pub directory: Option, /// S3 params; optional. When used other fields are empty for sure pub s3: Option, + /// SMB params; optional. Extra params required for SMB protocol + pub smb: Option, } /// Connection parameters for Aws s3 protocol @@ -55,6 +59,13 @@ pub struct S3Params { pub new_path_style: Option, } +/// Extra Connection parameters for SMB protocol +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)] +pub struct SmbParams { + pub share: String, + pub workgroup: Option, +} + // -- impls impl From for Bookmark { @@ -71,6 +82,7 @@ impl From for Bookmark { password: params.password, directory, s3: None, + smb: None, }, ProtocolParams::AwsS3(params) => Self { protocol, @@ -80,6 +92,20 @@ impl From for Bookmark { password: None, directory, 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 for FileTransferParams { .password(bookmark.password); 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 } @@ -133,6 +183,26 @@ impl From for AwsS3Params { } } +#[cfg(unix)] +impl From for SmbParams { + fn from(params: TransferSmbParams) -> Self { + Self { + share: params.share, + workgroup: params.workgroup, + } + } +} + +#[cfg(windows)] +impl From for SmbParams { + fn from(params: TransferSmbParams) -> Self { + Self { + share: params.share, + workgroup: None, + } + } +} + fn deserialize_protocol<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -178,6 +248,7 @@ mod tests { password: Some(String::from("password")), directory: Some(PathBuf::from("/tmp")), s3: None, + smb: None, }; let recent: Bookmark = Bookmark { address: Some(String::from("192.168.1.2")), @@ -187,6 +258,7 @@ mod tests { password: Some(String::from("password")), directory: Some(PathBuf::from("/home")), s3: None, + smb: None, }; let mut bookmarks: HashMap = HashMap::with_capacity(1); bookmarks.insert(String::from("test"), bookmark); @@ -275,6 +347,7 @@ mod tests { password: Some(String::from("password")), directory: Some(PathBuf::from("/tmp")), s3: None, + smb: None, }; let params = FileTransferParams::from(bookmark); assert_eq!(params.protocol, FileTransferProtocol::Sftp); @@ -307,6 +380,7 @@ mod tests { secret_access_key: Some(String::from("pluto")), new_path_style: Some(true), }), + smb: None, }; let params = FileTransferParams::from(bookmark); 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.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"); + } } diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 7d02cfa..20a648b 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -115,7 +115,7 @@ mod tests { use tuirealm::tui::style::Color; 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::themes::Theme; use crate::filetransfer::FileTransferProtocol; @@ -366,7 +366,7 @@ mod tests { assert_eq!(host.username.as_deref().unwrap(), "root"); assert_eq!(host.password, None); // Verify bookmarks - assert_eq!(hosts.bookmarks.len(), 4); + assert_eq!(hosts.bookmarks.len(), 5); let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap(); assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31"); assert_eq!(host.port.unwrap(), 22); @@ -404,6 +404,20 @@ mod tests { assert_eq!(s3.access_key.as_deref().unwrap(), "pippo"); assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto"); assert_eq!(s3.new_path_style.unwrap(), true); + + // 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] @@ -429,6 +443,7 @@ mod tests { password: None, directory: None, s3: None, + smb: None, }, ); bookmarks.insert( @@ -441,6 +456,7 @@ mod tests { password: Some(String::from("password")), directory: Some(PathBuf::from("/tmp")), s3: None, + smb: None, }, ); bookmarks.insert( @@ -461,6 +477,24 @@ mod tests { secret_access_key: None, new_path_style: None, }), + smb: None, + }, + ); + let smb_params: Option = 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 = HashMap::with_capacity(1); @@ -474,6 +508,7 @@ mod tests { password: Some(String::from("aaa")), directory: Some(PathBuf::from("/tmp")), s3: None, + smb: None, }, ); let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); @@ -527,6 +562,17 @@ mod tests { secret_access_key = "pluto" new_path_style = true + [bookmarks.smb] + protocol = "SMB" + address = "localhost" + port = 445 + username = "test" + password = "test" + + [bookmarks.smb.smb] + share = "temp" + workgroup = "test" + [recents] ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" } "#; diff --git a/src/explorer/formatter.rs b/src/explorer/formatter.rs index f6534fb..c3c7744 100644 --- a/src/explorer/formatter.rs +++ b/src/explorer/formatter.rs @@ -11,7 +11,7 @@ use bytesize::ByteSize; use lazy_regex::{Lazy, Regex}; use remotefs::File; use unicode_width::UnicodeWidthStr; -#[cfg(target_family = "unix")] +#[cfg(unix)] use users::{get_group_by_gid, get_user_by_uid}; use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; @@ -211,7 +211,7 @@ impl Formatter { _fmt_extra: Option<&String>, ) -> String { // Get username - #[cfg(target_family = "unix")] + #[cfg(unix)] let group: String = match fsentry.metadata().gid { Some(gid) => match get_group_by_gid(gid) { Some(user) => user.name().to_string_lossy().to_string(), @@ -219,7 +219,7 @@ impl Formatter { }, None => 0.to_string(), }; - #[cfg(target_os = "windows")] + #[cfg(windows)] let group: String = match fsentry.metadata().gid { Some(gid) => gid.to_string(), None => 0.to_string(), @@ -420,7 +420,7 @@ impl Formatter { _fmt_extra: Option<&String>, ) -> String { // Get username - #[cfg(target_family = "unix")] + #[cfg(unix)] let username: String = match fsentry.metadata().uid { Some(uid) => match get_user_by_uid(uid) { Some(user) => user.name().to_string_lossy().to_string(), @@ -428,7 +428,7 @@ impl Formatter { }, None => 0.to_string(), }; - #[cfg(target_os = "windows")] + #[cfg(windows)] let username: String = match fsentry.metadata().uid { Some(uid) => uid.to_string(), None => 0.to_string(), @@ -592,7 +592,7 @@ mod tests { mode: Some(UnixPex::from(0o644)), }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( formatter.fmt(&entry), format!( @@ -600,7 +600,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( formatter.fmt(&entry), format!( @@ -623,7 +623,7 @@ mod tests { mode: Some(UnixPex::from(0o644)), }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( formatter.fmt(&entry), format!( @@ -631,7 +631,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( formatter.fmt(&entry), format!( @@ -654,7 +654,7 @@ mod tests { mode: None, }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( formatter.fmt(&entry), format!( @@ -662,7 +662,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( formatter.fmt(&entry), format!( @@ -685,7 +685,7 @@ mod tests { mode: None, }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( formatter.fmt(&entry), format!( @@ -693,7 +693,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( formatter.fmt(&entry), format!( @@ -723,7 +723,7 @@ mod tests { mode: Some(UnixPex::from(0o755)), }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( formatter.fmt(&entry), format!( @@ -731,7 +731,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( formatter.fmt(&entry), format!( @@ -754,7 +754,7 @@ mod tests { mode: None, }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( formatter.fmt(&entry), format!( @@ -762,7 +762,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( formatter.fmt(&entry), format!( @@ -864,7 +864,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn should_fmt_path() { let t: SystemTime = SystemTime::now(); let entry = File { @@ -896,7 +896,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn should_fmt_utf8_path() { let t: SystemTime = SystemTime::now(); let entry = File { diff --git a/src/explorer/mod.rs b/src/explorer/mod.rs index 28b3ac9..56d988d 100644 --- a/src/explorer/mod.rs +++ b/src/explorer/mod.rs @@ -513,7 +513,7 @@ mod tests { mode: Some(UnixPex::from(0o644)), }, }; - #[cfg(target_family = "unix")] + #[cfg(unix)] assert_eq!( explorer.fmt_file(&entry), format!( @@ -521,7 +521,7 @@ mod tests { fmt_time(t, "%b %d %Y %H:%M") ) ); - #[cfg(target_os = "windows")] + #[cfg(windows)] assert_eq!( explorer.fmt_file(&entry), format!( diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs index b6d8164..84a4de0 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/builder.rs @@ -7,9 +7,16 @@ use std::path::PathBuf; use remotefs::RemoteFs; use remotefs_aws_s3::AwsS3Fs; 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}; +#[cfg(not(smb))] use super::params::{AwsS3Params, GenericProtocolParams}; +#[cfg(smb)] +use super::params::{AwsS3Params, GenericProtocolParams, SmbParams}; use super::{FileTransferProtocol, ProtocolParams}; use crate::system::config_client::ConfigClient; use crate::system::sshkey_storage::SshKeyStorage; @@ -39,6 +46,10 @@ impl Builder { (FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => { Box::new(Self::sftp_client(params, config_client)) } + #[cfg(smb)] + (FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => { + Box::new(Self::smb_client(params)) + } (protocol, params) => { error!("Invalid params for protocol '{:?}'", protocol); panic!("Invalid protocol '{protocol:?}' with parameters of type {params:?}") @@ -98,6 +109,50 @@ impl Builder { 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 fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts { let mut opts = SshOpts::new(params.address) @@ -187,6 +242,14 @@ mod test { 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] #[should_panic] fn should_not_build_fs() { diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 52a998f..28255fc 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -13,10 +13,11 @@ pub use params::{FileTransferParams, ProtocolParams}; #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub enum FileTransferProtocol { - Sftp, - Scp, - Ftp(bool), // Bool is for secure (true => ftps) AwsS3, + Ftp(bool), // Bool is for secure (true => ftps) + Scp, + Sftp, + Smb, } // Traits @@ -24,13 +25,14 @@ pub enum FileTransferProtocol { impl std::string::ToString for FileTransferProtocol { fn to_string(&self) -> String { String::from(match self { + FileTransferProtocol::AwsS3 => "S3", FileTransferProtocol::Ftp(secure) => match secure { true => "FTPS", false => "FTP", }, FileTransferProtocol::Scp => "SCP", 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() { "FTP" => Ok(FileTransferProtocol::Ftp(false)), "FTPS" => Ok(FileTransferProtocol::Ftp(true)), + "S3" => Ok(FileTransferProtocol::AwsS3), "SCP" => Ok(FileTransferProtocol::Scp), "SFTP" => Ok(FileTransferProtocol::Sftp), - "S3" => Ok(FileTransferProtocol::AwsS3), + "SMB" => Ok(FileTransferProtocol::Smb), _ => Err(s.to_string()), } } @@ -104,6 +107,14 @@ mod tests { FileTransferProtocol::from_str("scp").ok().unwrap(), FileTransferProtocol::Scp ); + assert_eq!( + FileTransferProtocol::from_str("SMB").ok().unwrap(), + FileTransferProtocol::Smb + ); + assert_eq!( + FileTransferProtocol::from_str("smb").ok().unwrap(), + FileTransferProtocol::Smb + ); assert_eq!( FileTransferProtocol::from_str("S3").ok().unwrap(), FileTransferProtocol::AwsS3 @@ -126,5 +137,6 @@ mod tests { assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP")); assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP")); assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3")); + assert_eq!(FileTransferProtocol::Smb.to_string(), String::from("SMB")); } } diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index 75b502b..ec0544d 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -19,6 +19,7 @@ pub struct FileTransferParams { pub enum ProtocolParams { Generic(GenericProtocolParams), AwsS3(AwsS3Params), + Smb(SmbParams), } /// Protocol params used by most common protocols @@ -44,6 +45,19 @@ pub struct AwsS3Params { 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, + pub password: Option, + #[cfg(unix)] + pub workgroup: Option, +} + impl FileTransferParams { /// Instantiates a new `FileTransferParams` pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self { @@ -66,6 +80,7 @@ impl FileTransferParams { match &self.params { ProtocolParams::AwsS3(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 { ProtocolParams::AwsS3(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, } } + + #[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 @@ -235,6 +260,61 @@ impl AwsS3Params { } } +// -- SMB params + +impl SmbParams { + /// Instantiates a new `AwsS3Params` struct + pub fn new>(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) -> Self { + self.username = username.map(|x| x.to_string()); + self + } + + pub fn password(mut self, password: Option) -> Self { + self.password = password.map(|x| x.to_string()); + self + } + + #[cfg(unix)] + pub fn workgroup(mut self, workgroup: Option) -> 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)] mod test { @@ -304,6 +384,53 @@ mod test { assert_eq!(params.new_path_style, true); } + #[test] + fn should_init_smb_params() { + let params = SmbParams::new("localhost", "temp"); + assert_eq!(¶ms.address, "localhost"); + + #[cfg(unix)] + assert_eq!(params.port, 445); + assert_eq!(¶ms.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!(¶ms.address, "localhost"); + assert_eq!(params.port, 3456); + assert_eq!(¶ms.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!(¶ms.address, "localhost"); + assert_eq!(¶ms.share, "temp"); + assert_eq!(params.username.as_deref().unwrap(), "foo"); + assert_eq!(params.password.as_deref().unwrap(), "bar"); + } + #[test] fn references() { let mut params = diff --git a/src/host/mod.rs b/src/host/mod.rs index 9d552f5..b0d45f1 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -4,15 +4,15 @@ // ext // Metadata ext -#[cfg(target_family = "unix")] +#[cfg(unix)] use std::fs::set_permissions; use std::fs::{self, File as StdFile, OpenOptions}; -#[cfg(target_family = "unix")] +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use filetime::{self, FileTime}; -#[cfg(target_family = "unix")] +#[cfg(unix)] use remotefs::fs::UnixPex; use remotefs::fs::{File, FileType, Metadata}; use thiserror::Error; @@ -420,7 +420,7 @@ impl Localhost { filetime::set_file_atime(path, atime) .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?; } - #[cfg(target_family = "unix")] + #[cfg(unix)] if let Some(mode) = metadata.mode { self.chmod(path, mode)?; } @@ -454,7 +454,7 @@ impl Localhost { } /// 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> { let path: PathBuf = self.to_path(path); // Get metadta @@ -586,7 +586,7 @@ impl Localhost { } /// Create a symlink at path pointing at target - #[cfg(target_family = "unix")] + #[cfg(unix)] pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> { let path = self.to_path(path); std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| { @@ -644,19 +644,19 @@ impl Localhost { #[cfg(test)] mod tests { - #[cfg(target_family = "unix")] + #[cfg(unix)] use std::fs::File as StdFile; - #[cfg(target_family = "unix")] + #[cfg(unix)] use std::io::Write; use std::ops::AddAssign; - #[cfg(target_family = "unix")] + #[cfg(unix)] use std::os::unix::fs::{symlink, PermissionsExt}; use std::time::{Duration, SystemTime}; use pretty_assertions::assert_eq; use super::*; - #[cfg(target_family = "unix")] + #[cfg(unix)] use crate::utils::test_helpers::make_fsentry; use crate::utils::test_helpers::{create_sample_file, make_dir_at, make_file_at}; @@ -669,7 +669,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_new() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); assert_eq!(host.wrkdir, PathBuf::from("/dev")); @@ -683,7 +683,7 @@ mod tests { } #[test] - #[cfg(target_os = "windows")] + #[cfg(windows)] fn test_host_localhost_new() { let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap(); assert_eq!(host.wrkdir, PathBuf::from("C:\\users")); @@ -705,14 +705,14 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_pwd() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); assert_eq!(host.pwd(), PathBuf::from("/dev")); } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_list_files() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); // Scan dir @@ -725,7 +725,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_change_dir() { let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); let new_dir: PathBuf = PathBuf::from("/dev"); @@ -741,7 +741,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] #[should_panic] fn test_host_localhost_change_dir_failed() { let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); @@ -750,7 +750,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_open_read() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); // Create temp file @@ -759,7 +759,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] #[should_panic] fn test_host_localhost_open_read_err_no_such_file() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); @@ -780,7 +780,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_open_write() { let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); // Create temp file @@ -799,7 +799,7 @@ mod tests { assert!(host.open_file_write(file.path()).is_err()); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn test_host_localhost_symlinks() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); @@ -837,7 +837,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_mkdir() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); @@ -862,7 +862,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_remove() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file @@ -891,7 +891,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_host_localhost_rename() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); // Create sample file @@ -944,7 +944,7 @@ mod tests { assert_eq!(new_metadata.metadata().modified, Some(new_mtime)); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn test_host_chmod() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); @@ -963,7 +963,7 @@ mod tests { .is_err()); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn test_host_copy_file_absolute() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); @@ -993,7 +993,7 @@ mod tests { .is_err()); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn test_host_copy_file_relative() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); @@ -1015,7 +1015,7 @@ mod tests { assert_eq!(host.files.len(), 2); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn test_host_copy_directory_absolute() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); @@ -1046,7 +1046,7 @@ mod tests { assert!(host.stat(test_file_path.as_path()).is_ok()); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn test_host_copy_directory_relative() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); @@ -1081,9 +1081,9 @@ mod tests { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); // Execute - #[cfg(target_family = "unix")] + #[cfg(unix)] 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"); } @@ -1120,7 +1120,7 @@ mod tests { assert_eq!(result[1].name(), "examples.csv"); } - #[cfg(target_family = "unix")] + #[cfg(unix)] #[test] fn should_create_symlink() { let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index 17ed4c0..2c3c04a 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -398,28 +398,6 @@ mod tests { 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] fn test_system_bookmarks_new_from_existing() { diff --git a/src/system/watcher/mod.rs b/src/system/watcher/mod.rs index 8c78ccb..0a95b9f 100644 --- a/src/system/watcher/mod.rs +++ b/src/system/watcher/mod.rs @@ -329,7 +329,7 @@ mod test { /* #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn should_poll_file_moved() { let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap(); let tempdir = TempDir::new().unwrap(); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index fdd3cc5..60520e4 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -4,7 +4,7 @@ // Locals use super::{AuthActivity, FileTransferParams}; -use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; +use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams, SmbParams}; impl AuthActivity { /// Delete bookmark @@ -157,6 +157,7 @@ impl AuthActivity { match bookmark.params { ProtocolParams::AwsS3(params) => self.load_bookmark_s3_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_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(¶ms.share); + #[cfg(unix)] + self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or("")); + } } diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index db0555f..6013230 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -8,6 +8,11 @@ use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; 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}; // -- protocol @@ -26,7 +31,11 @@ impl ProtocolRadio { .color(color) .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) .rewind(true) .title("Protocol", Alignment::Left) @@ -37,10 +46,11 @@ impl ProtocolRadio { /// Convert radio index for protocol into a `FileTransferProtocol` fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol { match protocol { - 1 => FileTransferProtocol::Scp, - 2 => FileTransferProtocol::Ftp(false), - 3 => FileTransferProtocol::Ftp(true), - 4 => FileTransferProtocol::AwsS3, + RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp, + RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false), + RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true), + RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3, + RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb, _ => FileTransferProtocol::Sftp, } } @@ -48,11 +58,12 @@ impl ProtocolRadio { /// Convert `FileTransferProtocol` enum into radio group index fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize { match protocol { - FileTransferProtocol::Sftp => 0, - FileTransferProtocol::Scp => 1, - FileTransferProtocol::Ftp(false) => 2, - FileTransferProtocol::Ftp(true) => 3, - FileTransferProtocol::AwsS3 => 4, + FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP, + FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP, + FileTransferProtocol::Ftp(false) => RADIO_PROTOCOL_FTP, + FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS, + FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3, + FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB, } } } @@ -673,3 +684,72 @@ fn handle_input_ev( _ => 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 for InputSmbShare { + fn on(&mut self, ev: Event) -> Option { + 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 for InputSmbWorkgroup { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::SmbWorkgroupDown), + Msg::Ui(UiMsg::SmbWorkgroupUp), + ) + } +} diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index f61b5e8..ec227db 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -13,10 +13,12 @@ pub use bookmarks::{ BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup, RecentsList, }; +#[cfg(unix)] +pub use form::InputSmbWorkgroup; pub use form::{ InputAddress, InputPassword, InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, - InputS3SessionToken, InputUsername, ProtocolRadio, RadioS3NewPathStyle, + InputS3SessionToken, InputSmbShare, InputUsername, ProtocolRadio, RadioS3NewPathStyle, }; pub use popup::{ ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index 1feeceb..ff40ae8 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -14,6 +14,7 @@ impl AuthActivity { FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22, FileTransferProtocol::Ftp(_) => 21, 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 { match self.protocol { 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 { + 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 /// If enabled in configuration, check for updates from Github diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 7dacf8f..600ef47 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -23,6 +23,14 @@ use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::system::bookmarks_client::BookmarksClient; use crate::system::config_client::ConfigClient; +// radio +const RADIO_PROTOCOL_SFTP: usize = 0; +const RADIO_PROTOCOL_SCP: usize = 1; +const RADIO_PROTOCOL_FTP: usize = 2; +const RADIO_PROTOCOL_FTPS: usize = 3; +const RADIO_PROTOCOL_S3: usize = 4; +const RADIO_PROTOCOL_SMB: usize = 5; + // -- components #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum Id { @@ -55,6 +63,9 @@ pub enum Id { S3SecretAccessKey, S3SecurityToken, S3SessionToken, + SmbShare, + #[cfg(unix)] + SmbWorkgroup, Subtitle, Title, Username, @@ -125,6 +136,12 @@ pub enum UiMsg { S3SecurityTokenBlurUp, S3SessionTokenBlurDown, S3SessionTokenBlurUp, + SmbShareBlurDown, + SmbShareBlurUp, + #[cfg(unix)] + SmbWorkgroupDown, + #[cfg(unix)] + SmbWorkgroupUp, BookmarkNameBlur, SaveBookmarkPasswordBlur, ShowDeleteBookmarkPopup, @@ -143,6 +160,7 @@ pub enum UiMsg { enum InputMask { Generic, AwsS3, + Smb, } // Store keys @@ -218,6 +236,7 @@ impl AuthActivity { FileTransferProtocol::Ftp(_) | FileTransferProtocol::Scp | FileTransferProtocol::Sftp => InputMask::Generic, + FileTransferProtocol::Smb => InputMask::Smb, } } } diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index 303f758..c680695 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -68,6 +68,7 @@ impl AuthActivity { .app .active(match self.input_mask() { InputMask::Generic => &Id::Password, + InputMask::Smb => &Id::Password, InputMask::AwsS3 => &Id::S3Bucket, }) .is_ok()); @@ -79,6 +80,7 @@ impl AuthActivity { .app .active(match self.input_mask() { InputMask::Generic => &Id::Password, + InputMask::Smb => &Id::Password, InputMask::AwsS3 => &Id::S3Bucket, }) .is_ok()); @@ -113,7 +115,12 @@ impl AuthActivity { fn update_ui(&mut self, msg: UiMsg) -> Option { match msg { 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 => { assert!(self.app.active(&Id::Protocol).is_ok()); @@ -155,13 +162,30 @@ impl AuthActivity { assert!(self.app.active(&Id::BookmarksList).is_ok()); } 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 => { assert!(self.app.active(&Id::Username).is_ok()); } 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 => { assert!(self.app.active(&Id::Address).is_ok()); @@ -171,6 +195,7 @@ impl AuthActivity { .app .active(match self.input_mask() { InputMask::Generic => &Id::Address, + InputMask::Smb => &Id::Address, InputMask::AwsS3 => &Id::S3Bucket, }) .is_ok()); @@ -189,6 +214,10 @@ impl AuthActivity { .app .active(match self.input_mask() { InputMask::Generic => &Id::Password, + #[cfg(unix)] + InputMask::Smb => &Id::SmbWorkgroup, + #[cfg(windows)] + InputMask::Smb => &Id::Password, InputMask::AwsS3 => &Id::S3NewPathStyle, }) .is_ok()); @@ -247,6 +276,25 @@ impl AuthActivity { UiMsg::S3NewPathStyleBlurUp => { 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 => { assert!(self.app.active(&Id::BookmarkName).is_ok()); } @@ -272,7 +320,14 @@ impl AuthActivity { assert!(self.app.active(&Id::Password).is_ok()); } 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 => { self.redraw = true; diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 0ca5df6..b185755 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -11,7 +11,7 @@ use tuirealm::tui::widgets::Clear; use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause}; 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::utils::ui::{Popup, Size}; @@ -56,6 +56,9 @@ impl AuthActivity { self.mount_s3_security_token(""); self.mount_s3_session_token(""); self.mount_s3_new_path_style(false); + self.mount_smb_share(""); + #[cfg(unix)] + self.mount_smb_workgroup(""); // Version notice if let Some(version) = self .context() @@ -150,7 +153,7 @@ impl AuthActivity { InputMask::Generic => Layout::default() .constraints( [ - Constraint::Length(3), // host + Constraint::Length(3), // address Constraint::Length(3), // port Constraint::Length(3), // username Constraint::Length(3), // password @@ -160,6 +163,36 @@ impl AuthActivity { ) .direction(Direction::Vertical) .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 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[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 self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]); @@ -713,6 +753,31 @@ impl AuthActivity { .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 /// Collect input values from view @@ -748,6 +813,37 @@ impl AuthActivity { .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 = 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 { match self.app.state(&Id::RemoteDirectory) { 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 { + match self.app.state(&Id::SmbWorkgroup) { + Ok(State::One(StateValue::String(x))) => Some(x), + _ => None, + } + } + /// Get new bookmark params pub(super) fn get_new_bookmark(&self) -> (String, bool) { let name = match self.app.state(&Id::BookmarkName) { @@ -874,6 +985,7 @@ impl AuthActivity { match self.input_mask() { InputMask::AwsS3 => 12, InputMask::Generic => 12, + InputMask::Smb => 12, } } @@ -913,6 +1025,25 @@ impl AuthActivity { 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) { use tuirealm::event::{Key, KeyEvent, KeyModifiers}; assert!(self diff --git a/src/ui/activities/filetransfer/actions/chmod.rs b/src/ui/activities/filetransfer/actions/chmod.rs index ffcc7ff..bb0ac6c 100644 --- a/src/ui/activities/filetransfer/actions/chmod.rs +++ b/src/ui/activities/filetransfer/actions/chmod.rs @@ -3,7 +3,7 @@ use remotefs::fs::UnixPex; use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { - #[cfg(target_family = "unix")] + #[cfg(unix)] pub fn action_local_chmod(&mut self, mode: UnixPex) { 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) { let files = self.get_found_selected_entries().get_files(); diff --git a/src/ui/activities/filetransfer/actions/symlink.rs b/src/ui/activities/filetransfer/actions/symlink.rs index a7b72b1..679b6fb 100644 --- a/src/ui/activities/filetransfer/actions/symlink.rs +++ b/src/ui/activities/filetransfer/actions/symlink.rs @@ -9,7 +9,7 @@ use super::{FileTransferActivity, LogLevel, SelectedFile}; impl FileTransferActivity { /// Create symlink on localhost - #[cfg(target_family = "unix")] + #[cfg(unix)] pub(crate) fn action_local_symlink(&mut self, name: String) { if let SelectedFile::One(entry) = self.get_local_selected_entries() { match self @@ -33,7 +33,7 @@ impl FileTransferActivity { } } - #[cfg(target_family = "windows")] + #[cfg(windows)] pub(crate) fn action_local_symlink(&mut self, _name: String) { self.mount_error("Symlinks are not supported on Windows hosts"); } diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index 90f23c7..52c1b1a 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -13,7 +13,7 @@ use tuirealm::props::{ Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, }; 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 super::super::Browser; @@ -445,7 +445,7 @@ impl FileInfoPopup { .add_col(TextSpan::from("Last access time: ")) .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); // User - #[cfg(target_family = "unix")] + #[cfg(unix)] let username: String = match file.metadata().uid { Some(uid) => match get_user_by_uid(uid) { Some(user) => user.name().to_string_lossy().to_string(), @@ -453,10 +453,10 @@ impl FileInfoPopup { }, None => String::from("0"), }; - #[cfg(target_os = "windows")] + #[cfg(windows)] let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); // Group - #[cfg(target_family = "unix")] + #[cfg(unix)] let group: String = match file.metadata().gid { Some(gid) => match get_group_by_gid(gid) { Some(group) => group.name().to_string_lossy().to_string(), @@ -464,7 +464,7 @@ impl FileInfoPopup { }, None => String::from("0"), }; - #[cfg(target_os = "windows")] + #[cfg(windows)] let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); texts .add_row() diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index d1ae3d5..2035c8f 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -111,6 +111,7 @@ impl FileTransferActivity { match &ft_params.params { ProtocolParams::Generic(params) => params.address.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) } + ProtocolParams::Smb(params) => { + info!( + "Client is not connected to remote; connecting to {}:{}", + params.address, params.share + ); + format!("Connecting to \\\\{}\\{}…", params.address, params.share) + } } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index d64f330..6b4c07a 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -220,6 +220,8 @@ pub struct FileTransferActivity { cache: Option, /// Fs watcher fswatcher: Option, + /// conncted once + connected: bool, } impl FileTransferActivity { @@ -252,6 +254,7 @@ impl FileTransferActivity { None } }, + connected: false, } } @@ -372,14 +375,12 @@ impl Activity for FileTransferActivity { return; } // 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(); // print params let msg: String = Self::get_connection_msg(&ftparams.params); // Set init state to connecting popup - self.mount_wait(msg.as_str()); - // Force ui draw - self.view(); + self.mount_blocking_wait(msg.as_str()); // Connect to remote self.connect(); // Redraw diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 9ba9540..5750345 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -57,6 +57,7 @@ impl FileTransferActivity { // Connect to remote match self.client.connect() { Ok(Welcome { banner, .. }) => { + self.connected = true; if let Some(banner) = banner { // Log welcome self.log( diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 1da2840..6bf84fc 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -36,13 +36,13 @@ impl FileTransferActivity { self.umount_chmod(); self.mount_blocking_wait("Applying new file mode…"); match self.browser.tab() { - #[cfg(target_family = "unix")] + #[cfg(unix)] FileExplorerTab::Local => self.action_local_chmod(mode), - #[cfg(target_family = "unix")] + #[cfg(unix)] FileExplorerTab::FindLocal => self.action_find_local_chmod(mode), FileExplorerTab::Remote => self.action_remote_chmod(mode), FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode), - #[cfg(target_family = "windows")] + #[cfg(windows)] FileExplorerTab::Local | FileExplorerTab::FindLocal => {} } self.umount_wait(); @@ -441,13 +441,13 @@ impl FileTransferActivity { } UiMsg::ShowChmodPopup => { let selected_file = match self.browser.tab() { - #[cfg(target_family = "unix")] + #[cfg(unix)] FileExplorerTab::Local => self.get_local_selected_entries(), - #[cfg(target_family = "unix")] + #[cfg(unix)] FileExplorerTab::FindLocal => self.get_found_selected_entries(), FileExplorerTab::Remote => self.get_remote_selected_entries(), FileExplorerTab::FindRemote => self.get_found_selected_entries(), - #[cfg(target_family = "windows")] + #[cfg(windows)] FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None, }; if let Some(mode) = selected_file.unix_pex() { diff --git a/src/ui/activities/setup/components/config.rs b/src/ui/activities/setup/components/config.rs index 630e4a9..afebffc 100644 --- a/src/ui/activities/setup/components/config.rs +++ b/src/ui/activities/setup/components/config.rs @@ -11,6 +11,10 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use super::{ConfigMsg, Msg}; use crate::explorer::GroupDirs as GroupDirsEnum; 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; // -- components @@ -63,16 +67,17 @@ impl DefaultProtocol { .color(Color::Cyan) .modifiers(BorderType::Rounded), ) - .choices(&["SFTP", "SCP", "FTP", "FTPS", "S3"]) + .choices(&["SFTP", "SCP", "FTP", "FTPS", "S3", "SMB"]) .foreground(Color::Cyan) .rewind(true) .title("Default protocol", Alignment::Left) .value(match protocol { - FileTransferProtocol::AwsS3 => 4, - FileTransferProtocol::Ftp(true) => 3, - FileTransferProtocol::Ftp(false) => 2, - FileTransferProtocol::Scp => 1, - FileTransferProtocol::Sftp => 0, + FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP, + FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP, + FileTransferProtocol::Ftp(false) => RADIO_PROTOCOL_FTP, + FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS, + FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3, + FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB, }), } } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index 411e5da..9448070 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -24,6 +24,14 @@ use crate::config::themes::Theme; use crate::system::config_client::ConfigClient; 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 #[derive(Debug, Eq, PartialEq, Clone, Hash)] enum Id { diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 0b1fe95..f218dd4 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -13,6 +13,10 @@ use tuirealm::{State, StateValue}; use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout}; use crate::explorer::GroupDirs; 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; impl SetupActivity { @@ -268,10 +272,11 @@ impl SetupActivity { self.app.state(&Id::Config(IdConfig::DefaultProtocol)) { let protocol: FileTransferProtocol = match protocol { - 1 => FileTransferProtocol::Scp, - 2 => FileTransferProtocol::Ftp(false), - 3 => FileTransferProtocol::Ftp(true), - 4 => FileTransferProtocol::AwsS3, + RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp, + RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false), + RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true), + RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3, + RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb, _ => FileTransferProtocol::Sftp, }; self.config_mut().set_default_protocol(protocol); diff --git a/src/utils/fmt.rs b/src/utils/fmt.rs index acdfc57..4bb555b 100644 --- a/src/utils/fmt.rs +++ b/src/utils/fmt.rs @@ -308,7 +308,7 @@ mod tests { } #[test] - #[cfg(target_family = "unix")] + #[cfg(unix)] fn test_utils_fmt_path_elide() { let p: &Path = Path::new("/develop/pippo"); // Under max size diff --git a/src/utils/parser.rs b/src/utils/parser.rs index e25574a..bedbc67 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -12,6 +12,8 @@ use lazy_regex::{Lazy, Regex}; use tuirealm::tui::style::Color; use tuirealm::utils::parser as tuirealm_parser; +#[cfg(smb)] +use crate::filetransfer::params::SmbParams; use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; #[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 * Regex matches: * - 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 = lazy_regex!(r"(?:([a-z0-9]+)://)?(?:(.+))"); +static REMOTE_OPT_PROTOCOL_REGEX: Lazy = lazy_regex!(r"(?:([a-z0-9]+)://)?(\\\\)?(?:(.+))"); /** * Regex matches: @@ -50,6 +53,30 @@ static REMOTE_GENERIC_OPT_REGEX: Lazy = lazy_regex!( static REMOTE_S3_OPT_REGEX: Lazy = 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 = 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 = + lazy_regex!(r"(?:([^@]+)@)?(?:([^:\\]+))(?:\\([^\\]+))?(?:(\\.+))?"); + /** * Regex matches: * - group 1: Version @@ -83,6 +110,21 @@ static BYTESIZE_REGEX: Lazy = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B /// - sftp://172.26.104.1:4022 /// - sftp://172.26.104.1 /// - ... +/// +/// For s3: +/// +/// s3://@[:profile][:/wrkdir] +/// +/// For SMB: +/// +/// on UNIX derived (macos, linux, ...) +/// +/// smb://[username@]
[:port]/[/path] +/// +/// on Windows +/// +/// \\
\[\path] +/// pub fn parse_remote_opt(s: &str) -> Result { // Set protocol to default protocol #[cfg(not(test))] // NOTE: don't use configuration during tests @@ -108,6 +150,8 @@ pub fn parse_remote_opt(s: &str) -> Result { // Match against regex for protocol type match protocol { 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), } } @@ -127,13 +171,15 @@ fn parse_remote_opt_protocol( let protocol = match protocol { Some(Ok(protocol)) => protocol, Some(Err(err)) => return Err(err), + #[cfg(windows)] + None if groups.get(2).is_some() => FileTransferProtocol::Smb, None => default, }; // Return protocol and remaining arguments Ok(( protocol, groups - .get(2) + .get(3) .map(|x| x.as_str().to_string()) .unwrap_or_default(), )) @@ -220,6 +266,70 @@ fn parse_s3_remote_opt(s: &str) -> Result { } } +/// Parse remote options for smb protocol +#[cfg(smb_unix)] +fn parse_smb_remote_opts(s: &str) -> Result { + match REMOTE_SMB_OPT_REGEX.captures(s) { + Some(groups) => { + let username: Option = 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::() { + // 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 = + 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 { + 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 = + 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 pub fn parse_semver(haystack: &str) -> Option { match SEMVER_REGEX.captures(haystack) { @@ -502,6 +612,71 @@ mod tests { 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] fn test_utils_parse_semver() { assert_eq!(