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

View File

@@ -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:

View File

@@ -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

View File

@@ -8,7 +8,6 @@ env:
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1

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.
- [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

35
Cargo.lock generated
View File

@@ -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",

View File

@@ -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" ] }

View File

@@ -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)

15
build.rs Normal file
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)) }
}
}

View File

@@ -9,6 +9,7 @@ RUN yum -y install \
gcc \
make \
dbus-devel \
libsmbclient-devel \
bash \
rpm-build
# Install rust

View File

@@ -8,6 +8,8 @@ RUN apt update && apt install -y \
pkg-config \
libdbus-1-dev \
build-essential \
libsmbclient-dev \
libsmbclient \
bash \
curl

View File

@@ -9,6 +9,7 @@ RUN yum -y install \
gcc \
make \
dbus-devel \
libsmbclient-devel \
bash \
rpm-build
# Install rust

View File

@@ -8,6 +8,8 @@ RUN apt update && apt install -y \
pkg-config \
libdbus-1-dev \
build-essential \
libsmbclient-dev \
libsmbclient \
bash \
curl

View File

@@ -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 ✔️

View File

@@ -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@]<server-name>\<share>[\path\...]
```
**Other systems** syntax:
```txt
smb://[username@]<server-name>[:port]/<share>[/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.

View File

@@ -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 ✔️

View File

@@ -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@]<server-name>\<share>[\path\...]
```
**Other systems** sintaxis:
```txt
smb://[username@]<server-name>[:port]/<share>[/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.

View File

@@ -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 ✔️

View File

@@ -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@]<server-name>\<share>[\path\...]
```
syntaxe **Other systems**:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Comment le mot de passe peut être fourni 🔐
Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe.

View File

@@ -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 ✔️

View File

@@ -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@]<server-name>\<share>[\path\...]
```
**Altri sistemi**:
```txt
smb://[username@]<server-name>[:port]/<share>[/path/.../]
```
#### Come fornire la password 🔐
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:

View File

@@ -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@]<server-name>\<share>[\path\...]
```
**Other systems** syntax:
```txt
smb://[username@]<server-name>[:port]/<share>[/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.

View File

@@ -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
### 可选项 ✔️

View File

@@ -4,6 +4,7 @@
- [用法](#用法)
- [地址参数](#地址参数)
- [AWS S3 地址参数](#aws-s3-地址参数)
- [SMB 地址参数](#smb-地址参数)
- [如何输入密码](#如何输入密码)
- [S3 连接参数](#s3-连接参数)
- [Aws S3 凭证](#aws-s3-凭证)
@@ -102,6 +103,22 @@ s3://<bucket-name>@<region>[:profile][:/wrkdir]
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参数中没有办法直接附加密码你可以通过以下三种方式提供密码

View File

@@ -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

View File

@@ -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
- `\\\\<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, consider supporting the author <https://ko-fi.com/veeso>")]

View File

@@ -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<PathBuf>,
/// S3 params; optional. When used other fields are empty for sure
pub s3: Option<S3Params>,
/// SMB params; optional. Extra params required for SMB protocol
pub smb: Option<SmbParams>,
}
/// Connection parameters for Aws s3 protocol
@@ -55,6 +59,13 @@ pub struct S3Params {
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
impl From<FileTransferParams> for Bookmark {
@@ -71,6 +82,7 @@ impl From<FileTransferParams> for Bookmark {
password: params.password,
directory,
s3: None,
smb: None,
},
ProtocolParams::AwsS3(params) => Self {
protocol,
@@ -80,6 +92,20 @@ impl From<FileTransferParams> 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<Bookmark> 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<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>
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<String, Bookmark> = 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");
}
}

View File

@@ -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<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);
@@ -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" }
"#;

View File

@@ -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 {

View File

@@ -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!(

View File

@@ -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() {

View File

@@ -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"));
}
}

View File

@@ -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<String>,
pub password: Option<String>,
#[cfg(unix)]
pub workgroup: Option<String>,
}
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<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)]
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!(&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]
fn references() {
let mut params =

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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(&params.share);
#[cfg(unix)]
self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or(""));
}
}

View File

@@ -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<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),
)
}
}

View File

@@ -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,

View File

@@ -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<FileTransferParams, &'static str> {
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<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
/// If enabled in configuration, check for updates from Github

View File

@@ -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,
}
}
}

View File

@@ -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<Msg> {
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;

View File

@@ -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<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> {
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<String> {
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

View File

@@ -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();

View File

@@ -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");
}

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -220,6 +220,8 @@ pub struct FileTransferActivity {
cache: Option<TempDir>,
/// Fs watcher
fswatcher: Option<FsWatcher>,
/// 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

View File

@@ -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(

View File

@@ -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() {

View File

@@ -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,
}),
}
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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

View File

@@ -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<Regex> = lazy_regex!(r"(?:([a-z0-9]+)://)?(?:(.+))");
static REMOTE_OPT_PROTOCOL_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([a-z0-9]+)://)?(\\\\)?(?:(.+))");
/**
* Regex matches:
@@ -50,6 +53,30 @@ static REMOTE_GENERIC_OPT_REGEX: Lazy<Regex> = lazy_regex!(
static REMOTE_S3_OPT_REGEX: Lazy<Regex> =
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:
* - 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
/// - ...
///
/// 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> {
// 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<FileTransferParams, String> {
// 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<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
pub fn parse_semver(haystack: &str) -> Option<String> {
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!(