feat(gcs): add Google Cloud Storage support (#443)

* feat(gcs): add Google Cloud Storage support

Closes #436

* fix: honor configured protocol and finalization errors

* ci: remove TruffleHog checks
This commit is contained in:
Christian Visintin
2026-08-30 18:58:17 +02:00
committed by GitHub
parent 98a1ce42dc
commit 974b6d6917
51 changed files with 2101 additions and 96 deletions
+3 -12
View File
@@ -2,10 +2,9 @@
#
# termscp pre-commit hook.
#
# Runs three gates before a commit is recorded:
# 1. trufflehog -- scan the staged tree for verified/unknown secrets
# 2. dprint -- check formatting (Markdown, TOML, YAML, Rust)
# 3. cargo-deny -- advisories, licenses, bans, and sources
# Runs two gates before a commit is recorded:
# 1. dprint -- check formatting (Markdown, TOML, YAML, Rust)
# 2. cargo-deny -- advisories, licenses, bans, and sources
#
# Install with `just setup_githooks` (sets core.hooksPath to .githooks).
# Bypass in an emergency with `git commit --no-verify`.
@@ -20,8 +19,6 @@ fail() {
command -v just >/dev/null 2>&1 || fail "just not found; install it to run the pre-commit checks"
if ! git diff --cached --quiet --diff-filter=ACMR --; then
command -v trufflehog >/dev/null 2>&1 || fail "trufflehog not found; install it or commit with --no-verify"
# Check the exact tree that will be committed, not possibly different
# working-tree contents. The trailing slash is required by checkout-index.
index_tree="$(mktemp -d)"
@@ -31,12 +28,6 @@ if ! git diff --cached --quiet --diff-filter=ACMR --; then
trap cleanup EXIT
git checkout-index --all --prefix="$index_tree/"
echo "pre-commit: scanning staged tree for secrets"
(
cd "$index_tree"
just scan_secrets . --fail-on-scan-errors
)
echo "pre-commit: checking staged-tree formatting"
(
cd "$index_tree"
+1 -1
View File
@@ -260,7 +260,7 @@ jobs:
BASE="https://github.com/veeso/termscp/releases/latest/download"
cat > tap/Formula/termscp.rb <<EOF
class Termscp < Formula
desc "A feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3/Kube/SMB/WebDAV"
desc "A feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3/GCS/Kube/SMB/WebDAV"
homepage "https://termscp.rs/"
license "MIT"
version "$VERSION"
+3 -3
View File
@@ -4,9 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
termscp is a terminal file transfer client with a TUI (Terminal User Interface), supporting SFTP, SCP, FTP/FTPS, Kube, S3, SMB, and WebDAV protocols. It features a dual-pane file explorer (local + remote), bookmarks, system keyring integration, file watching/sync, an embedded terminal, and customizable themes.
termscp is a terminal file transfer client with a TUI (Terminal User Interface), supporting SFTP, SCP, FTP/FTPS, Kube, S3, GCS, SMB, and WebDAV protocols. It features a dual-pane file explorer (local + remote), bookmarks, system keyring integration, file watching/sync, an embedded terminal, and customizable themes.
- **Language**: Rust (edition 2024, MSRV 1.89.0)
- **Language**: Rust (edition 2024, MSRV 1.98.0)
- **UI Framework**: tuirealm v3 (built on crossterm)
- **File Transfer**: remotefs ecosystem
@@ -97,7 +97,7 @@ Platform-specific dependencies: SSH and FTP crates use different TLS backends on
### File Transfer Protocols
`FileTransferProtocol` enum maps to protocol-specific parameter types (`ProtocolParams` enum) and `RemoteFsBuilder` constructs the appropriate `RemoteFs` client. Each protocol has its own params struct (e.g., `GenericProtocolParams` for SSH-based, `AwsS3Params`, `KubeProtocolParams`, `SmbParams`, `WebDAVProtocolParams`).
`FileTransferProtocol` enum maps to protocol-specific parameter types (`ProtocolParams` enum) and `RemoteFsBuilder` constructs the appropriate `RemoteFs` client. Each protocol has its own params struct (e.g., `GenericProtocolParams` for SSH-based, `AwsS3Params`, `GoogleCloudStorageParams`, `KubeProtocolParams`, `SmbParams`, `WebDAVProtocolParams`).
## Code Conventions
Generated
+911 -12
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -10,7 +10,8 @@ keywords = ["terminal", "ftp", "scp", "sftp", "tui"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV"
rust-version = "1.98.0"
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV"
[package.metadata.rpm]
package = "termscp"
@@ -59,6 +60,7 @@ rand = "0.10"
regex = "1"
remotefs = "0.3"
remotefs-aws-s3 = "0.4"
remotefs-gcs = "0.1"
remotefs-kube = "0.4"
remotefs-smb = { version = "0.3", optional = true }
remotefs-ssh = { version = "0.8", default-features = false, features = ["russh"] }
@@ -67,6 +69,7 @@ rpassword = "7"
self_update = { version = "0.42", default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] }
semver = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
shellexpand = "3"
simplelog = "0.12"
ssh2-config = "0.7"
+2 -1
View File
@@ -35,7 +35,7 @@
## About termscp 🖥
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD**, **NetBSD** and **Windows** compatible.
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/Google Cloud Storage (GCS)/WebDAV. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD**, **NetBSD** and **Windows** compatible.
![Explorer](assets/images/explorer.gif)
@@ -49,6 +49,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
- **FTP** and **FTPS**
- **Kube**
- **S3**
- **Google Cloud Storage (GCS)**
- **SMB**
- **WebDAV**
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
+1 -1
View File
@@ -1,6 +1,6 @@
[book]
title = "termscp"
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV"
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV"
authors = ["Christian Visintin"]
language = "en"
src = "."
+3 -3
View File
@@ -6,7 +6,7 @@ documentation for termscp modules, which can instead be found on Rust Docs at
guidelines to implement features such as file transfers and additions to the
user interface.
termscp is written in Rust (edition 2024, MSRV 1.89.0). The user interface is
termscp is written in Rust (edition 2024, MSRV 1.98.0). The user interface is
built with [tuirealm](https://github.com/veeso/tui-realm) v3, which runs on top
of [crossterm](https://github.com/crossterm-rs/crossterm).
@@ -37,8 +37,8 @@ In addition to the 3 core modules, others have been added over time:
storage and the bookmarks.
- **utils**: contains the utilities used by pretty much all of the project.
termscp supports the following protocols: SFTP, SCP, FTP/FTPS, Kube, S3, SMB and
WebDAV.
termscp supports the following protocols: SFTP, SCP, FTP/FTPS, Kube, S3, GCS,
SMB and WebDAV.
## Activities
+1 -1
View File
@@ -56,7 +56,7 @@ directory `/tmp`:
termscp scp://omar@192.168.1.31:4022:/tmp
```
For protocol-specific address syntax (S3, Kube, WebDAV, and SMB), see
For protocol-specific address syntax (S3, GCS, Kube, WebDAV, and SMB), see
[Connection parameters](connection-parameters.md).
## How the password is provided
@@ -128,6 +128,37 @@ ways to do this.
Your credentials are safe: termscp does not manipulate these values directly.
They are consumed directly by the `s3` crate.
## Google Cloud Storage
termscp supports Google Cloud Storage (GCS) buckets through the Google Cloud
Storage JSON API.
Authentication-form fields:
- Bucket name (required)
- Endpoint (defaults to `https://storage.googleapis.com`)
- Optional service-account JSON path
Leave the service-account JSON path empty to use Application Default
Credentials (ADC). ADC can obtain credentials from
`GOOGLE_APPLICATION_CREDENTIALS`, local gcloud ADC credentials, or the Google
Cloud metadata service when termscp runs on Google Cloud infrastructure.
When a service-account JSON path is supplied, termscp reads that file when it
connects. Bookmarks store only the path and never copy the service-account JSON
or its private key.
The dedicated CLI syntax is:
```txt
gcs://<bucket>[:/working/directory]
```
CLI connections use ADC and the default endpoint. Use the authentication form
or a bookmark when you need a custom endpoint or a service-account JSON path.
The selected ADC identity or service account must have IAM permissions for the
storage operations you want to perform.
## SMB
Authentication-form fields:
+1 -1
View File
@@ -9,7 +9,7 @@ same time. termscp runs on Linux, macOS, FreeBSD, NetBSD, and Windows.
## Features
- Multiple transfer protocols: SFTP, SCP, FTP and FTPS, Kube, S3, SMB,
- Multiple transfer protocols: SFTP, SCP, FTP and FTPS, Kube, S3, GCS, SMB,
and WebDAV.
- Dual-pane explorer to browse and operate on both the remote and the local
file system: create, remove, rename, search, view, and edit files.
+2 -2
View File
@@ -2,12 +2,12 @@
<link rel="icon" type="image/svg+xml" href="/shared/termscp.svg">
<meta property="og:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
<meta property="og:image" content="https://docs.termscp.rs/og_preview.jpg">
<meta property="og:url" content="https://docs.termscp.rs/">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
<meta name="twitter:image" content="https://docs.termscp.rs/og_preview.jpg">
+4 -1
View File
@@ -1,4 +1,4 @@
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV.
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV.
Basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and
to interact with the local file system.
@@ -9,6 +9,9 @@ Features:
- SCP
- FTP and FTPS
- S3
- GCS
- SMB
- WebDAV
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
- Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
+2 -1
View File
@@ -35,7 +35,7 @@
## 关于 termscp 🖥
termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/SFTP/FTP/Kube/S3/WebDAV。 简而言之,它是一个带有 TUI 的终端工具,可以连接到远程服务器进行文件的检索和上传,并能够与本地文件系统进行交互。 它兼容 **Linux**、**MacOS**、**FreeBSD**、**NetBSD** 和 **Windows** 操作系统。
termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/SFTP/FTP/Kube/S3/Google Cloud Storage (GCS)/WebDAV。 简而言之,它是一个带有 TUI 的终端工具,可以连接到远程服务器进行文件的检索和上传,并能够与本地文件系统进行交互。 它兼容 **Linux**、**MacOS**、**FreeBSD**、**NetBSD** 和 **Windows** 操作系统。
![Explorer](assets/images/explorer.gif)
@@ -49,6 +49,7 @@ termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/S
- **FTP** 和 **FTPS**
- **Kube**
- **S3**
- **Google Cloud Storage (GCS)**
- **SMB**
- **WebDAV**
- 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作
+1 -1
View File
@@ -1,6 +1,6 @@
[book]
title = "termscp"
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV"
description = "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV"
authors = ["Christian Visintin"]
language = "zh"
src = "."
+2 -2
View File
@@ -2,7 +2,7 @@
欢迎阅读 termscp 的开发者手册。本章不包含 termscp 各模块的文档,相关文档可以在 Rust Docs 上找到:<https://docs.rs/termscp>。本章描述 termscp 的工作原理,以及实现诸如文件传输和用户界面扩展等功能的指南。
termscp 使用 Rust 编写(edition 2024MSRV 1.89.0)。用户界面使用 [tuirealm](https://github.com/veeso/tui-realm) v3 构建,它运行在 [crossterm](https://github.com/crossterm-rs/crossterm) 之上。
termscp 使用 Rust 编写(edition 2024MSRV 1.98.0)。用户界面使用 [tuirealm](https://github.com/veeso/tui-realm) v3 构建,它运行在 [crossterm](https://github.com/crossterm-rs/crossterm) 之上。
## termscp 的工作原理
@@ -20,7 +20,7 @@ termscp 基本上由 3 个核心模块组成:
- **system**:提供与配置、ssh 密钥存储和书签交互的方式。
- **utils**:包含几乎整个项目都会使用的工具。
termscp 支持以下协议:SFTP、SCP、FTP/FTPS、Kube、S3、SMB 和 WebDAV。
termscp 支持以下协议:SFTP、SCP、FTP/FTPS、Kube、S3、GCS、SMB 和 WebDAV。
## Activities
+1 -1
View File
@@ -46,7 +46,7 @@ termscp scp://omar@192.168.1.31:4022
termscp scp://omar@192.168.1.31:4022:/tmp
```
有关各协议专属的地址语法(S3、Kube、WebDAV 和 SMB),请参阅[连接参数](connection-parameters.md)。
有关各协议专属的地址语法(S3、GCS、Kube、WebDAV 和 SMB),请参阅[连接参数](connection-parameters.md)。
## 密码的提供方式
@@ -114,6 +114,33 @@ s3://buckethead@eu-central-1:default:/assets
你的凭据是安全的:termscp 不会直接操作这些值。它们由 `s3` crate 直接使用。
## Google Cloud Storage
termscp 通过 Google Cloud Storage JSON API 支持 Google Cloud StorageGCS)存储桶。
认证表单字段:
- 存储桶名称(必填)
- 端点(默认为 `https://storage.googleapis.com`
- 可选的服务账号 JSON 路径
将服务账号 JSON 路径留空即可使用应用默认凭据(ADC)。当 termscp 在 Google
Cloud 基础设施上运行时,ADC 可以从 `GOOGLE_APPLICATION_CREDENTIALS`、本地
gcloud ADC 凭据或 Google Cloud 元数据服务中获取凭据。
提供服务账号 JSON 路径后,termscp 会在连接时读取该文件。书签只保存该路径,
不会复制服务账号 JSON 或其中的私钥。
专用的 CLI 语法如下:
```txt
gcs://<bucket>[:/working/directory]
```
CLI 连接使用 ADC 和默认端点。如果需要自定义端点或服务账号 JSON 路径,请使用
认证表单或书签。所选 ADC 身份或服务账号必须拥有你想执行的存储操作所需的 IAM
权限。
## SMB
认证表单字段:
+1 -1
View File
@@ -6,7 +6,7 @@ termscp 是一款功能丰富、带有 TUI(终端用户界面)的终端文
## 功能特性
- 支持多种传输协议:SFTP、SCP、FTP 和 FTPS、Kube、S3、SMB 以及 WebDAV。
- 支持多种传输协议:SFTP、SCP、FTP 和 FTPS、Kube、S3、GCS、SMB 以及 WebDAV。
- 双面板浏览器,可同时浏览并操作远程和本地文件系统:创建、删除、重命名、搜索、查看和编辑文件。
- 书签和最近连接记录,帮助你快速重新连接到常用的主机。
- 使用你喜爱的编辑器查看和编辑文件。
+2 -2
View File
@@ -2,12 +2,12 @@
<link rel="icon" type="image/svg+xml" href="/shared/termscp.svg">
<meta property="og:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
<meta property="og:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
<meta property="og:image" content="https://docs.termscp.rs/og_preview.jpg">
<meta property="og:url" content="https://docs.termscp.rs/">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{#if chapter_title}}{{ chapter_title }} · {{/if}}{{ book_title }}">
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/SMB/WebDAV">
<meta name="twitter:description" content="A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/GCS/SMB/WebDAV">
<meta name="twitter:image" content="https://docs.termscp.rs/og_preview.jpg">
-5
View File
@@ -25,11 +25,6 @@ doc args="":
deny args="":
cargo deny check {{ args }}
# Scan for secrets with trufflehog (defaults to the whole working tree)
[group('code_check')]
scan_secrets *args=".":
trufflehog filesystem {{ args }} --results=verified,unknown --fail --no-update
# Point git at the tracked .githooks directory (installs the pre-commit hook)
[group('code_check')]
setup_githooks:
+1
View File
@@ -36,6 +36,7 @@ Address syntax can be:
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
- `gcs://<bucket>[:/working/directory]` for Google Cloud Storage (ADC)
- `\\\\<server>[:port]\\<share>[\\path]` for SMB (on Windows)
- `smb://[user@]<server>[:port]</share>[/path]` for SMB (on other systems)
+72 -2
View File
@@ -3,6 +3,7 @@
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
mod aws_s3;
mod gcs;
mod kube;
mod smb;
@@ -14,11 +15,12 @@ use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub use self::aws_s3::S3Params;
pub use self::gcs::GcsParams;
pub use self::kube::KubeParams;
pub use self::smb::SmbParams;
use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams,
SmbParams as TransferSmbParams, WebDAVProtocolParams,
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
ProtocolParams, SmbParams as TransferSmbParams, WebDAVProtocolParams,
};
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
@@ -55,6 +57,8 @@ pub struct Bookmark {
pub kube: Option<KubeParams>,
/// S3 params; optional. When used other fields are empty for sure
pub s3: Option<S3Params>,
/// Google Cloud Storage params; optional. When used other fields are empty for sure
pub gcs: Option<GcsParams>,
/// SMB params; optional. Extra params required for SMB protocol
pub smb: Option<SmbParams>,
}
@@ -78,6 +82,7 @@ impl From<FileTransferParams> for Bookmark {
local_path,
kube: None,
s3: None,
gcs: None,
smb: None,
},
ProtocolParams::AwsS3(params) => Self {
@@ -90,6 +95,7 @@ impl From<FileTransferParams> for Bookmark {
local_path,
kube: None,
s3: Some(S3Params::from(params)),
gcs: None,
smb: None,
},
ProtocolParams::Kube(params) => Self {
@@ -102,6 +108,7 @@ impl From<FileTransferParams> for Bookmark {
local_path,
kube: Some(KubeParams::from(params)),
s3: None,
gcs: None,
smb: None,
},
ProtocolParams::Smb(params) => Self {
@@ -118,6 +125,7 @@ impl From<FileTransferParams> for Bookmark {
local_path,
kube: None,
s3: None,
gcs: None,
},
ProtocolParams::WebDAV(parms) => Self {
protocol,
@@ -129,6 +137,20 @@ impl From<FileTransferParams> for Bookmark {
local_path,
kube: None,
s3: None,
gcs: None,
smb: None,
},
ProtocolParams::GoogleCloudStorage(params) => Self {
protocol,
address: None,
port: None,
username: None,
password: None,
remote_path,
local_path,
kube: None,
s3: None,
gcs: Some(GcsParams::from(params)),
smb: None,
},
}
@@ -144,6 +166,14 @@ impl From<Bookmark> for FileTransferParams {
let params = AwsS3Params::from(params);
Self::new(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params))
}
FileTransferProtocol::GoogleCloudStorage => {
let params = bookmark.gcs.unwrap_or_default();
let params = GoogleCloudStorageParams::from(params);
Self::new(
FileTransferProtocol::GoogleCloudStorage,
ProtocolParams::GoogleCloudStorage(params),
)
}
FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => {
@@ -224,6 +254,7 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
#[test]
fn test_bookmarks_default() {
@@ -244,6 +275,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
kube: None,
s3: None,
gcs: None,
smb: None,
};
let recent: Bookmark = Bookmark {
@@ -256,6 +288,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
kube: None,
s3: None,
gcs: None,
smb: None,
};
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
@@ -348,6 +381,37 @@ mod tests {
assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto");
}
#[test]
fn should_convert_gcs_params_to_bookmark_and_back() {
let transfer = FileTransferParams::new(
FileTransferProtocol::GoogleCloudStorage,
ProtocolParams::GoogleCloudStorage(
GoogleCloudStorageParams::new("archive-bucket")
.service_account_key(Some("/keys/archive.json")),
),
)
.remote_path(Some("/backups"));
let bookmark = Bookmark::from(transfer);
let gcs = bookmark.gcs.as_ref().unwrap();
assert_eq!(gcs.bucket, "archive-bucket");
assert_eq!(gcs.endpoint, DEFAULT_GCS_ENDPOINT);
assert_eq!(
gcs.service_account_key.as_deref(),
Some("/keys/archive.json")
);
assert_eq!(bookmark.password, None);
let restored = FileTransferParams::from(bookmark);
let params = restored.params.gcs_params().unwrap();
assert_eq!(params.bucket_name, "archive-bucket");
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
assert_eq!(
params.service_account_key.as_deref(),
Some("/keys/archive.json")
);
}
#[test]
fn bookmark_from_kube_ftparams() {
let params = ProtocolParams::Kube(KubeProtocolParams {
@@ -388,6 +452,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
kube: None,
s3: None,
gcs: None,
smb: None,
};
let params = FileTransferParams::from(bookmark);
@@ -419,6 +484,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
kube: None,
s3: None,
gcs: None,
smb: None,
};
let params = FileTransferParams::from(bookmark);
@@ -457,6 +523,7 @@ mod tests {
secret_access_key: Some(String::from("pluto")),
new_path_style: Some(true),
}),
gcs: None,
smb: None,
};
let params = FileTransferParams::from(bookmark);
@@ -497,6 +564,7 @@ mod tests {
client_key: Some(String::from("key")),
}),
s3: None,
gcs: None,
smb: None,
};
let params = FileTransferParams::from(bookmark);
@@ -533,6 +601,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
kube: None,
s3: None,
gcs: None,
smb: Some(SmbParams {
share: "test".to_string(),
workgroup: Some("testone".to_string()),
@@ -571,6 +640,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
s3: None,
kube: None,
gcs: None,
smb: Some(SmbParams {
share: "test".to_string(),
workgroup: None,
+65
View File
@@ -0,0 +1,65 @@
//! ## Bookmark Google Cloud Storage Parameters
//!
//! Stores bookmark-specific Google Cloud Storage connection settings.
use serde::{Deserialize, Serialize};
use crate::filetransfer::params::{DEFAULT_GCS_ENDPOINT, GoogleCloudStorageParams};
fn default_gcs_endpoint() -> String {
DEFAULT_GCS_ENDPOINT.to_string()
}
/// Google Cloud Storage connection parameters stored in a bookmark.
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)]
pub struct GcsParams {
/// Bucket name to open.
pub bucket: String,
/// Google Cloud Storage endpoint URL.
#[serde(default = "default_gcs_endpoint")]
pub endpoint: String,
/// Optional path to a service-account JSON file.
pub service_account_key: Option<String>,
}
impl From<GoogleCloudStorageParams> for GcsParams {
fn from(params: GoogleCloudStorageParams) -> Self {
Self {
bucket: params.bucket_name,
endpoint: if params.endpoint.is_empty() {
default_gcs_endpoint()
} else {
params.endpoint
},
service_account_key: params.service_account_key,
}
}
}
impl From<GcsParams> for GoogleCloudStorageParams {
fn from(params: GcsParams) -> Self {
GoogleCloudStorageParams::new(params.bucket)
.endpoint(if params.endpoint.is_empty() {
default_gcs_endpoint()
} else {
params.endpoint
})
.service_account_key(params.service_account_key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_normalize_empty_endpoint() {
let params = GoogleCloudStorageParams::from(GcsParams {
bucket: String::from("archive-bucket"),
endpoint: String::new(),
service_account_key: None,
});
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
}
}
+106 -1
View File
@@ -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(), 6);
assert_eq!(hosts.bookmarks.len(), 7);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
assert_eq!(host.port.unwrap(), 22);
@@ -404,6 +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);
// Google Cloud Storage bucket
let host: &Bookmark = hosts.bookmarks.get("gcs-bucket").unwrap();
assert_eq!(host.address, None);
assert_eq!(host.port, None);
assert_eq!(host.username, None);
assert_eq!(host.password, None);
assert_eq!(host.protocol, FileTransferProtocol::GoogleCloudStorage);
let gcs = host.gcs.as_ref().unwrap();
assert_eq!(gcs.bucket, "archive-bucket");
assert_eq!(gcs.endpoint, "https://storage.googleapis.com");
assert_eq!(
gcs.service_account_key.as_deref(),
Some("/keys/archive.json")
);
// Kube pod
let host: &Bookmark = hosts.bookmarks.get("pod").unwrap();
assert_eq!(host.address, None);
@@ -461,6 +475,45 @@ mod tests {
);
}
#[test]
fn should_deserialize_gcs_bookmark_without_service_account_key() {
let toml_file = create_gcs_adc_toml_bookmark();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().rewind().unwrap();
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
let host = hosts.bookmarks.get("gcs-adc").unwrap();
assert_eq!(host.protocol, FileTransferProtocol::GoogleCloudStorage);
let gcs = host.gcs.as_ref().unwrap();
assert_eq!(gcs.bucket, "adc-bucket");
assert_eq!(gcs.endpoint, "https://storage.googleapis.com");
assert_eq!(gcs.service_account_key, None);
}
#[test]
fn should_serialize_gcs_bookmark_fields() {
let toml_file = create_good_toml_bookmarks();
toml_file.as_file().sync_all().unwrap();
toml_file.as_file().rewind().unwrap();
let hosts: UserHosts = deserialize(Box::new(toml_file)).unwrap();
let output_file = tempfile::NamedTempFile::new().unwrap();
let output_path = output_file.path().to_path_buf();
serialize(
&hosts,
Box::new(std::fs::File::create(&output_path).unwrap()),
)
.unwrap();
let output = std::fs::read_to_string(output_path).unwrap();
assert!(output.contains("protocol = \"GCS\""));
assert!(output.contains("bucket = \"archive-bucket\""));
assert!(output.contains("endpoint = \"https://storage.googleapis.com\""));
assert!(output.contains("service_account_key = \"/keys/archive.json\""));
assert!(output.contains("directory = \"/backups\""));
}
#[test]
fn test_should_fail_deserialize_bookmark_with_invalid_protocol() {
let toml_file: tempfile::NamedTempFile = create_invalid_protocol_toml_bookmarks();
@@ -486,6 +539,7 @@ mod tests {
local_path: None,
kube: None,
s3: None,
gcs: None,
smb: None,
},
);
@@ -501,6 +555,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
kube: None,
s3: None,
gcs: None,
smb: None,
},
);
@@ -524,6 +579,27 @@ mod tests {
new_path_style: None,
}),
kube: None,
gcs: None,
smb: None,
},
);
bookmarks.insert(
String::from("gcs-bucket"),
Bookmark {
address: None,
port: None,
protocol: FileTransferProtocol::GoogleCloudStorage,
username: None,
password: None,
remote_path: Some(PathBuf::from("/backups")),
local_path: None,
kube: None,
s3: None,
gcs: Some(crate::config::bookmarks::GcsParams {
bucket: "archive-bucket".to_string(),
endpoint: "https://storage.googleapis.com".to_string(),
service_account_key: Some("/keys/archive.json".to_string()),
}),
smb: None,
},
);
@@ -539,6 +615,7 @@ mod tests {
remote_path: None,
local_path: None,
s3: None,
gcs: None,
smb: None,
kube: Some(KubeParams {
namespace: Some("my-namespace".to_string()),
@@ -566,6 +643,7 @@ mod tests {
local_path: None,
s3: None,
kube: None,
gcs: None,
smb: smb_params,
},
);
@@ -582,6 +660,7 @@ mod tests {
local_path: Some(PathBuf::from("/usr")),
s3: None,
kube: None,
gcs: None,
smb: None,
},
);
@@ -656,6 +735,15 @@ mod tests {
secret_access_key = "pluto"
new_path_style = true
[bookmarks.gcs-bucket]
protocol = "GCS"
directory = "/backups"
[bookmarks.gcs-bucket.gcs]
bucket = "archive-bucket"
endpoint = "https://storage.googleapis.com"
service_account_key = "/keys/archive.json"
[bookmarks.pod]
protocol = "KUBE"
[bookmarks.pod.kube]
@@ -684,6 +772,23 @@ mod tests {
tmpfile
}
fn create_gcs_adc_toml_bookmark() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
[bookmarks]
[bookmarks.gcs-adc]
protocol = "GCS"
[bookmarks.gcs-adc.gcs]
bucket = "adc-bucket"
[recents]
"#;
tmpfile.write_all(file_content.as_bytes()).unwrap();
tmpfile
}
fn create_v14_pod_bookmark() -> tempfile::NamedTempFile {
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
let file_content: &str = r#"
+15
View File
@@ -17,6 +17,7 @@ pub use remotefs_builder::RemoteFsBuilder;
pub enum FileTransferProtocol {
AwsS3,
Ftp(bool), // Bool is for secure (true => ftps)
GoogleCloudStorage,
Kube,
Scp,
Sftp,
@@ -37,6 +38,7 @@ impl std::fmt::Display for FileTransferProtocol {
true => "FTPS",
false => "FTP",
},
FileTransferProtocol::GoogleCloudStorage => "GCS",
FileTransferProtocol::Kube => "KUBE",
FileTransferProtocol::Scp => "SCP",
FileTransferProtocol::Sftp => "SFTP",
@@ -53,6 +55,7 @@ impl std::str::FromStr for FileTransferProtocol {
match s.to_ascii_uppercase().as_str() {
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"GCS" => Ok(FileTransferProtocol::GoogleCloudStorage),
"KUBE" => Ok(FileTransferProtocol::Kube),
"S3" => Ok(FileTransferProtocol::AwsS3),
"SCP" => Ok(FileTransferProtocol::Scp),
@@ -143,6 +146,14 @@ mod tests {
FileTransferProtocol::from_str("s3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
assert_eq!(
FileTransferProtocol::from_str("GCS").ok().unwrap(),
FileTransferProtocol::GoogleCloudStorage
);
assert_eq!(
FileTransferProtocol::from_str("gcs").ok().unwrap(),
FileTransferProtocol::GoogleCloudStorage
);
// Error
assert!(FileTransferProtocol::from_str("dummy").is_err());
// To String
@@ -161,6 +172,10 @@ mod tests {
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
assert_eq!(
FileTransferProtocol::GoogleCloudStorage.to_string(),
String::from("GCS")
);
assert_eq!(FileTransferProtocol::Smb.to_string(), String::from("SMB"));
assert_eq!(
FileTransferProtocol::WebDAV.to_string(),
+15
View File
@@ -3,6 +3,7 @@
//! file transfer parameters
mod aws_s3;
mod google_cloud_storage;
mod kube;
mod smb;
mod webdav;
@@ -10,6 +11,7 @@ mod webdav;
use std::path::{Path, PathBuf};
pub use self::aws_s3::AwsS3Params;
pub use self::google_cloud_storage::{DEFAULT_GCS_ENDPOINT, GoogleCloudStorageParams};
pub use self::kube::KubeProtocolParams;
pub use self::smb::SmbParams;
pub use self::webdav::WebDAVProtocolParams;
@@ -66,6 +68,7 @@ impl FileTransferParams {
pub enum ProtocolParams {
Generic(GenericProtocolParams),
AwsS3(AwsS3Params),
GoogleCloudStorage(GoogleCloudStorageParams),
Kube(KubeProtocolParams),
Smb(SmbParams),
WebDAV(WebDAVProtocolParams),
@@ -76,6 +79,7 @@ impl ProtocolParams {
match self {
ProtocolParams::AwsS3(params) => params.password_missing(),
ProtocolParams::Generic(params) => params.password_missing(),
ProtocolParams::GoogleCloudStorage(params) => params.password_missing(),
ProtocolParams::Kube(params) => params.password_missing(),
ProtocolParams::Smb(params) => params.password_missing(),
ProtocolParams::WebDAV(params) => params.password_missing(),
@@ -87,6 +91,7 @@ impl ProtocolParams {
match self {
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
ProtocolParams::Generic(params) => params.set_default_secret(secret),
ProtocolParams::GoogleCloudStorage(params) => params.set_default_secret(secret),
ProtocolParams::Kube(params) => params.set_default_secret(secret),
ProtocolParams::Smb(params) => params.set_default_secret(secret),
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
@@ -97,6 +102,7 @@ impl ProtocolParams {
match self {
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::GoogleCloudStorage(params) => params.bucket_name.clone(),
ProtocolParams::Kube(params) => params
.namespace
.as_ref()
@@ -193,6 +199,15 @@ impl ProtocolParams {
}
}
#[cfg(test)]
/// Retrieve Google Cloud Storage parameters if any.
pub fn gcs_params(&self) -> Option<&GoogleCloudStorageParams> {
match self {
ProtocolParams::GoogleCloudStorage(params) => Some(params),
_ => None,
}
}
#[cfg(test)]
/// Retrieve Kube params parameters if any
pub fn kube_params(&self) -> Option<&KubeProtocolParams> {
@@ -0,0 +1,77 @@
//! ## Google Cloud Storage Parameters
//!
//! Defines the runtime connection parameters used to build Google Cloud
//! Storage clients.
/// Google Cloud Storage's default JSON API endpoint.
pub const DEFAULT_GCS_ENDPOINT: &str = "https://storage.googleapis.com";
/// Connection parameters for Google Cloud Storage.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GoogleCloudStorageParams {
/// Target bucket name.
pub bucket_name: String,
/// Google Cloud Storage endpoint URL.
pub endpoint: String,
/// Optional path to a service-account JSON file.
pub service_account_key: Option<String>,
}
impl GoogleCloudStorageParams {
/// Creates Google Cloud Storage parameters using the default endpoint.
pub fn new<S: Into<String>>(bucket_name: S) -> Self {
Self {
bucket_name: bucket_name.into(),
endpoint: DEFAULT_GCS_ENDPOINT.to_string(),
service_account_key: None,
}
}
/// Sets the Google Cloud Storage endpoint.
pub fn endpoint<S: Into<String>>(mut self, endpoint: S) -> Self {
self.endpoint = endpoint.into();
self
}
/// Sets the optional service-account JSON file path.
pub fn service_account_key<S: Into<String>>(mut self, path: Option<S>) -> Self {
self.service_account_key = path.map(Into::into);
self
}
/// Reports whether the protocol's default secret is missing.
pub fn password_missing(&self) -> bool {
false
}
/// Ignores generic password secrets because GCS uses ADC or a credential file.
pub fn set_default_secret(&mut self, _secret: String) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_use_google_storage_default_endpoint() {
let params = GoogleCloudStorageParams::new("my-bucket");
assert_eq!(params.bucket_name, "my-bucket");
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
assert_eq!(params.service_account_key, None);
assert!(!params.password_missing());
}
#[test]
fn should_override_endpoint_and_credentials_path() {
let params = GoogleCloudStorageParams::new("my-bucket")
.endpoint("http://127.0.0.1:4443")
.service_account_key(Some("credentials.json"));
assert_eq!(params.endpoint, "http://127.0.0.1:4443");
assert_eq!(
params.service_account_key.as_deref(),
Some("credentials.json")
);
}
}
+94 -2
View File
@@ -8,6 +8,8 @@ use std::sync::Arc;
use remotefs::RemoteFs;
use remotefs_aws_s3::AwsS3Fs;
use remotefs_ftp::FtpFs;
use remotefs_gcs::credentials::service_account;
use remotefs_gcs::{GoogleCloudStorageCredentials, GoogleCloudStorageFs};
use remotefs_kube::KubeMultiPodFs as KubeFs;
#[cfg(smb_unix)]
use remotefs_smb::SmbOptions;
@@ -20,9 +22,9 @@ use remotefs_ssh::{
use remotefs_webdav::WebDAVFs;
#[cfg(not(smb))]
use super::params::{AwsS3Params, GenericProtocolParams};
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams};
#[cfg(smb)]
use super::params::{AwsS3Params, GenericProtocolParams, SmbParams};
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, SmbParams};
use super::params::{KubeProtocolParams, WebDAVProtocolParams};
use super::{FileTransferProtocol, ProtocolParams};
use crate::system::config_client::ConfigClient;
@@ -48,6 +50,10 @@ impl RemoteFsBuilder {
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
Ok(Box::new(Self::ftp_client(params, secure)))
}
(
FileTransferProtocol::GoogleCloudStorage,
ProtocolParams::GoogleCloudStorage(params),
) => Ok(Box::new(Self::gcs_client(params)?)),
(FileTransferProtocol::Kube, ProtocolParams::Kube(params)) => {
Ok(Box::new(Self::kube_client(params)?))
}
@@ -108,6 +114,36 @@ impl RemoteFsBuilder {
Ok(client)
}
/// Build a Google Cloud Storage client from parameters.
fn gcs_client(params: GoogleCloudStorageParams) -> Result<GoogleCloudStorageFs, String> {
let runtime = Self::tokio_runtime()?;
let mut client = match params.service_account_key {
None => GoogleCloudStorageFs::new(params.bucket_name, &runtime),
Some(path) => {
let raw = std::fs::read_to_string(&path).map_err(|error| {
format!("Unable to read GCS service-account file '{path}': {error}")
})?;
let key = serde_json::from_str(&raw).map_err(|error| {
format!("Invalid GCS service-account JSON in '{path}': {error}")
})?;
let credentials = {
let _guard = runtime.enter();
service_account::Builder::new(key).build()
}
.map_err(|error| {
format!("Invalid GCS service-account credentials in '{path}': {error}")
})?;
GoogleCloudStorageFs::with_credentials(
params.bucket_name,
GoogleCloudStorageCredentials::custom(credentials),
&runtime,
)
}
};
client = client.endpoint(params.endpoint);
Ok(client)
}
/// Build ftp client from parameters
fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs {
let mut client = FtpFs::new(params.address, params.port).passive_mode();
@@ -298,6 +334,62 @@ mod test {
);
}
#[test]
fn should_build_gcs_fs_with_application_default_credentials() {
let params = ProtocolParams::GoogleCloudStorage(GoogleCloudStorageParams::new("my-bucket"));
let config_client = get_config_client();
assert!(
RemoteFsBuilder::build(
FileTransferProtocol::GoogleCloudStorage,
params,
&config_client,
)
.is_ok()
);
}
#[test]
fn should_reject_missing_gcs_service_account_file() {
let directory = tempfile::TempDir::new().unwrap();
let missing = directory.path().join("missing.json");
let params = GoogleCloudStorageParams::new("my-bucket")
.service_account_key(Some(missing.to_string_lossy().into_owned()));
assert!(RemoteFsBuilder::gcs_client(params).is_err());
}
#[test]
fn should_reject_malformed_gcs_service_account_json() {
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(file.path(), "not-json").unwrap();
let params = GoogleCloudStorageParams::new("my-bucket")
.service_account_key(Some(file.path().to_string_lossy().into_owned()));
assert!(RemoteFsBuilder::gcs_client(params).is_err());
}
#[test]
fn should_build_gcs_fs_with_service_account_file() {
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
file.path(),
r#"{
"type": "service_account",
"client_email": "termscp@example.iam.gserviceaccount.com",
"private_key_id": "test-key",
"private_key": "-----BEGIN PRIVATE KEY-----\ninvalid-test-key\n-----END PRIVATE KEY-----\n",
"project_id": "termscp-test",
"universe_domain": "googleapis.com"
}"#,
)
.unwrap();
let params = GoogleCloudStorageParams::new("my-bucket")
.service_account_key(Some(file.path().to_string_lossy().into_owned()));
assert!(RemoteFsBuilder::gcs_client(params).is_ok());
}
#[test]
fn should_build_ftp_fs() {
let params = ProtocolParams::Generic(
+65 -1
View File
@@ -379,7 +379,9 @@ mod tests {
use tempfile::TempDir;
use super::*;
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams};
use crate::filetransfer::params::{
AwsS3Params, DEFAULT_GCS_ENDPOINT, GenericProtocolParams, GoogleCloudStorageParams,
};
use crate::filetransfer::{FileTransferProtocol, ProtocolParams};
#[test]
@@ -526,6 +528,58 @@ mod tests {
assert_eq!(params.secret_access_key, None);
}
#[test]
fn should_preserve_gcs_service_account_path_for_saved_bookmarks() {
for save_password in [true, false] {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
client
.add_bookmark(
"gcs-bucket",
make_gcs_ftparams(Some("/keys/archive.json")),
save_password,
)
.unwrap();
let bookmark = client.get_bookmark("gcs-bucket").unwrap();
let params = bookmark.params.gcs_params().unwrap();
assert_eq!(params.bucket_name, "archive-bucket");
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
assert_eq!(
params.service_account_key.as_deref(),
Some("/keys/archive.json")
);
assert_eq!(bookmark.password_missing(), false);
}
}
#[test]
fn should_make_gcs_recent_without_password() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16, true).unwrap();
client
.add_recent(make_gcs_ftparams(Some("/keys/archive.json")).remote_path(Some("/backups")))
.unwrap();
let recent_key = client.iter_recents().next().unwrap().clone();
let recent = client.get_recent(&recent_key).unwrap();
let params = recent.params.gcs_params().unwrap();
assert_eq!(params.bucket_name, "archive-bucket");
assert_eq!(params.endpoint, DEFAULT_GCS_ENDPOINT);
assert_eq!(
params.service_account_key.as_deref(),
Some("/keys/archive.json")
);
assert_eq!(recent.password_missing(), false);
assert_eq!(recent.remote_path.as_deref(), Some(Path::new("/backups")));
}
#[test]
fn test_system_bookmarks_manipulate_bookmarks() {
@@ -932,6 +986,16 @@ mod tests {
)
}
fn make_gcs_ftparams(service_account_key: Option<&str>) -> FileTransferParams {
FileTransferParams::new(
FileTransferProtocol::GoogleCloudStorage,
ProtocolParams::GoogleCloudStorage(
GoogleCloudStorageParams::new("archive-bucket")
.service_account_key(service_account_key),
),
)
}
fn ftparams_to_tup(
params: FileTransferParams,
) -> (String, u16, FileTransferProtocol, String, Option<String>) {
+17
View File
@@ -532,6 +532,23 @@ mod tests {
);
}
#[test]
fn should_round_trip_gcs_as_default_protocol() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
client.set_default_protocol(FileTransferProtocol::GoogleCloudStorage);
assert_eq!(
client.get_default_protocol(),
FileTransferProtocol::GoogleCloudStorage
);
assert_eq!(client.config.user_interface.default_protocol, "GCS");
}
#[test]
fn test_system_config_show_hidden_files() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
+40 -6
View File
@@ -31,9 +31,10 @@ const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2;
const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3;
const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4;
const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5;
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6;
const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 7;
const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last
const HOST_BRIDGE_RADIO_PROTOCOL_GCS: usize = 6;
const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 7;
const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 8;
const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 9; // Keep as last
// remote protocol radio
const REMOTE_RADIO_PROTOCOL_SFTP: usize = 0;
@@ -41,9 +42,10 @@ const REMOTE_RADIO_PROTOCOL_SCP: usize = 1;
const REMOTE_RADIO_PROTOCOL_FTP: usize = 2;
const REMOTE_RADIO_PROTOCOL_FTPS: usize = 3;
const REMOTE_RADIO_PROTOCOL_S3: usize = 4;
const REMOTE_RADIO_PROTOCOL_KUBE: usize = 5;
const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 6;
const REMOTE_RADIO_PROTOCOL_SMB: usize = 7; // Keep as last
const REMOTE_RADIO_PROTOCOL_GCS: usize = 5;
const REMOTE_RADIO_PROTOCOL_KUBE: usize = 6;
const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 7;
const REMOTE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last
// -- components
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
@@ -74,6 +76,9 @@ pub enum Id {
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum AuthFormId {
Address,
GcsBucket,
GcsEndpoint,
GcsServiceAccountKey,
KubeNamespace,
KubeClusterUrl,
KubeUsername,
@@ -153,6 +158,12 @@ pub enum UiAuthFormMsg {
AddressBlurDown,
AddressBlurUp,
ChangeFormTab,
GcsBucketBlurDown,
GcsBucketBlurUp,
GcsEndpointBlurDown,
GcsEndpointBlurUp,
GcsServiceAccountKeyBlurDown,
GcsServiceAccountKeyBlurUp,
KubeNamespaceBlurDown,
KubeNamespaceBlurUp,
KubeClusterUrlBlurDown,
@@ -209,6 +220,7 @@ pub enum UiAuthFormMsg {
enum InputMask {
Generic,
AwsS3,
Gcs,
Kube,
Localhost,
Smb,
@@ -301,6 +313,10 @@ impl AuthActivity {
Self::file_transfer_protocol_input_mask(self.remote_protocol)
}
fn set_remote_protocol(&mut self, protocol: FileTransferProtocol) {
self.remote_protocol = protocol;
}
/// Get current input mask to show
fn host_bridge_input_mask(&self) -> InputMask {
match self.host_bridge_protocol {
@@ -315,6 +331,7 @@ impl AuthActivity {
fn file_transfer_protocol_input_mask(protocol: FileTransferProtocol) -> InputMask {
match protocol {
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
FileTransferProtocol::GoogleCloudStorage => InputMask::Gcs,
FileTransferProtocol::Ftp(_)
| FileTransferProtocol::Scp
| FileTransferProtocol::Sftp => InputMask::Generic,
@@ -411,3 +428,20 @@ impl Activity for AuthActivity {
self.context.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_set_configured_remote_protocol() {
let mut activity = AuthActivity::new(Duration::ZERO);
activity.set_remote_protocol(FileTransferProtocol::GoogleCloudStorage);
assert_eq!(
activity.remote_protocol,
FileTransferProtocol::GoogleCloudStorage
);
}
}
+24 -2
View File
@@ -6,8 +6,8 @@
use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol};
use crate::filetransfer::HostBridgeParams;
use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
WebDAVProtocolParams,
AwsS3Params, DEFAULT_GCS_ENDPOINT, GenericProtocolParams, GoogleCloudStorageParams,
KubeProtocolParams, ProtocolParams, SmbParams, WebDAVProtocolParams,
};
impl AuthActivity {
@@ -201,6 +201,9 @@ impl AuthActivity {
ProtocolParams::AwsS3(params) => {
self.load_bookmark_s3_into_gui(FormTab::HostBridge, params)
}
ProtocolParams::GoogleCloudStorage(params) => {
self.load_bookmark_gcs_into_gui(FormTab::HostBridge, params)
}
ProtocolParams::Kube(params) => {
self.load_bookmark_kube_into_gui(FormTab::HostBridge, params)
}
@@ -240,6 +243,9 @@ impl AuthActivity {
ProtocolParams::AwsS3(params) => {
self.load_bookmark_s3_into_gui(FormTab::Remote, params)
}
ProtocolParams::GoogleCloudStorage(params) => {
self.load_bookmark_gcs_into_gui(FormTab::Remote, params)
}
ProtocolParams::Kube(params) => {
self.load_bookmark_kube_into_gui(FormTab::Remote, params)
}
@@ -276,6 +282,22 @@ impl AuthActivity {
self.mount_s3_new_path_style(form_tab, params.new_path_style);
}
fn load_bookmark_gcs_into_gui(&mut self, form_tab: FormTab, params: GoogleCloudStorageParams) {
self.mount_gcs_bucket(form_tab, &params.bucket_name);
self.mount_gcs_endpoint(
form_tab,
if params.endpoint.is_empty() {
DEFAULT_GCS_ENDPOINT
} else {
&params.endpoint
},
);
self.mount_gcs_service_account_key(
form_tab,
params.service_account_key.as_deref().unwrap_or(""),
);
}
fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) {
self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or(""));
self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or(""));
+6 -6
View File
@@ -16,12 +16,12 @@ pub use bookmarks::{
#[cfg(posix)]
pub use form::InputSmbWorkgroup;
pub use form::{
HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey,
InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword,
InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint,
InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken,
InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle,
RemoteProtocolRadio,
HostBridgeProtocolRadio, InputAddress, InputGcsBucket, InputGcsEndpoint,
InputGcsServiceAccountKey, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl,
InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword, InputPort,
InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile,
InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken,
InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, RemoteProtocolRadio,
};
pub use popup::{
ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup,
+10 -5
View File
@@ -13,14 +13,18 @@ use tuirealm::props::{
use super::{FileTransferProtocol, FormMsg, Msg, UiMsg};
use crate::ui::activities::auth::{
FormTab, HOST_BRIDGE_RADIO_PROTOCOL_FTP, HOST_BRIDGE_RADIO_PROTOCOL_FTPS,
HOST_BRIDGE_RADIO_PROTOCOL_KUBE, HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST,
HOST_BRIDGE_RADIO_PROTOCOL_S3, HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
HOST_BRIDGE_RADIO_PROTOCOL_GCS, HOST_BRIDGE_RADIO_PROTOCOL_KUBE,
HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST, HOST_BRIDGE_RADIO_PROTOCOL_S3,
HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP,
HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, HostBridgeProtocol,
REMOTE_RADIO_PROTOCOL_FTP, REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE,
REMOTE_RADIO_PROTOCOL_S3, REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP,
REMOTE_RADIO_PROTOCOL_SMB, REMOTE_RADIO_PROTOCOL_WEBDAV, UiAuthFormMsg,
REMOTE_RADIO_PROTOCOL_FTP, REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_GCS,
REMOTE_RADIO_PROTOCOL_KUBE, REMOTE_RADIO_PROTOCOL_S3, REMOTE_RADIO_PROTOCOL_SCP,
REMOTE_RADIO_PROTOCOL_SFTP, REMOTE_RADIO_PROTOCOL_SMB, REMOTE_RADIO_PROTOCOL_WEBDAV,
UiAuthFormMsg,
};
#[path = "form/gcs.rs"]
mod gcs;
#[path = "form/generic.rs"]
mod generic;
#[path = "form/kube.rs"]
@@ -36,6 +40,7 @@ mod smb;
#[path = "form/webdav.rs"]
mod webdav;
pub use gcs::{InputGcsBucket, InputGcsEndpoint, InputGcsServiceAccountKey};
pub use generic::{InputAddress, InputPassword, InputPort, InputUsername};
pub use kube::{
InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace,
@@ -0,0 +1,144 @@
//! ## Google Cloud Storage Form
//!
//! Input components for Google Cloud Storage authentication parameters.
use tuirealm::component::{AppComponent, Component};
use super::*;
#[derive(Component)]
pub struct InputGcsBucket {
component: Input,
form_tab: FormTab,
}
impl InputGcsBucket {
pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder(tuirealm::props::SpanStatic::styled(
"my-bucket",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Bucket").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text)
.value(bucket),
form_tab,
}
}
}
impl AppComponent<Msg, NoUserEvent> for InputGcsBucket {
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let (on_key_down, on_key_up) = match self.form_tab {
FormTab::Remote => (
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsBucketBlurDown)),
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsBucketBlurUp)),
),
FormTab::HostBridge => (
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsBucketBlurDown)),
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsBucketBlurUp)),
),
};
let form_tab = self.form_tab;
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
}
}
#[derive(Component)]
pub struct InputGcsEndpoint {
component: Input,
form_tab: FormTab,
}
impl InputGcsEndpoint {
pub fn new(endpoint: &str, form_tab: FormTab, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder(tuirealm::props::SpanStatic::styled(
"https://storage.googleapis.com",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Endpoint").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text)
.value(endpoint),
form_tab,
}
}
}
impl AppComponent<Msg, NoUserEvent> for InputGcsEndpoint {
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let (on_key_down, on_key_up) = match self.form_tab {
FormTab::Remote => (
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsEndpointBlurDown)),
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsEndpointBlurUp)),
),
FormTab::HostBridge => (
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsEndpointBlurDown)),
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsEndpointBlurUp)),
),
};
let form_tab = self.form_tab;
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
}
}
#[derive(Component)]
pub struct InputGcsServiceAccountKey {
component: Input,
form_tab: FormTab,
}
impl InputGcsServiceAccountKey {
pub fn new(path: &str, form_tab: FormTab, color: Color) -> Self {
Self {
component: Input::default()
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.placeholder(tuirealm::props::SpanStatic::styled(
"Optional service-account JSON path",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Service account JSON").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text)
.value(path),
form_tab,
}
}
}
impl AppComponent<Msg, NoUserEvent> for InputGcsServiceAccountKey {
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let (on_key_down, on_key_up) = match self.form_tab {
FormTab::Remote => (
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsServiceAccountKeyBlurDown)),
Msg::Ui(UiMsg::Remote(UiAuthFormMsg::GcsServiceAccountKeyBlurUp)),
),
FormTab::HostBridge => (
Msg::Ui(UiMsg::HostBridge(
UiAuthFormMsg::GcsServiceAccountKeyBlurDown,
)),
Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::GcsServiceAccountKeyBlurUp)),
),
};
let form_tab = self.form_tab;
handle_input_ev(self, ev, on_key_down, on_key_up, form_tab)
}
}
@@ -28,9 +28,12 @@ impl RemoteProtocolRadio {
.modifiers(BorderType::Rounded),
)
.choices(if cfg!(smb) {
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV", "SMB"].into_iter()
vec![
"SFTP", "SCP", "FTP", "FTPS", "S3", "GCS", "Kube", "WebDAV", "SMB",
]
.into_iter()
} else {
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "GCS", "Kube", "WebDAV"].into_iter()
})
.rewind(true)
.title(Title::from("Protocol").alignment(HorizontalAlignment::Left))
@@ -44,6 +47,7 @@ impl RemoteProtocolRadio {
REMOTE_RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false),
REMOTE_RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true),
REMOTE_RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3,
REMOTE_RADIO_PROTOCOL_GCS => FileTransferProtocol::GoogleCloudStorage,
REMOTE_RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb,
REMOTE_RADIO_PROTOCOL_KUBE => FileTransferProtocol::Kube,
REMOTE_RADIO_PROTOCOL_WEBDAV => FileTransferProtocol::WebDAV,
@@ -58,6 +62,7 @@ impl RemoteProtocolRadio {
FileTransferProtocol::Ftp(false) => REMOTE_RADIO_PROTOCOL_FTP,
FileTransferProtocol::Ftp(true) => REMOTE_RADIO_PROTOCOL_FTPS,
FileTransferProtocol::AwsS3 => REMOTE_RADIO_PROTOCOL_S3,
FileTransferProtocol::GoogleCloudStorage => REMOTE_RADIO_PROTOCOL_GCS,
FileTransferProtocol::Kube => REMOTE_RADIO_PROTOCOL_KUBE,
FileTransferProtocol::Smb => REMOTE_RADIO_PROTOCOL_SMB,
FileTransferProtocol::WebDAV => REMOTE_RADIO_PROTOCOL_WEBDAV,
@@ -128,6 +133,7 @@ impl HostBridgeProtocolRadio {
"FTP",
"FTPS",
"S3",
"GCS",
"Kube",
"WebDAV",
"SMB",
@@ -141,6 +147,7 @@ impl HostBridgeProtocolRadio {
"FTP",
"FTPS",
"S3",
"GCS",
"Kube",
"WebDAV",
]
@@ -168,6 +175,9 @@ impl HostBridgeProtocolRadio {
HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3) => {
HOST_BRIDGE_RADIO_PROTOCOL_S3
}
HostBridgeProtocol::Remote(FileTransferProtocol::GoogleCloudStorage) => {
HOST_BRIDGE_RADIO_PROTOCOL_GCS
}
HostBridgeProtocol::Remote(FileTransferProtocol::Smb) => HOST_BRIDGE_RADIO_PROTOCOL_SMB,
HostBridgeProtocol::Remote(FileTransferProtocol::Kube) => {
HOST_BRIDGE_RADIO_PROTOCOL_KUBE
@@ -194,6 +204,9 @@ impl HostBridgeProtocolRadio {
HOST_BRIDGE_RADIO_PROTOCOL_S3 => {
HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3)
}
HOST_BRIDGE_RADIO_PROTOCOL_GCS => {
HostBridgeProtocol::Remote(FileTransferProtocol::GoogleCloudStorage)
}
HOST_BRIDGE_RADIO_PROTOCOL_SMB => HostBridgeProtocol::Remote(FileTransferProtocol::Smb),
HOST_BRIDGE_RADIO_PROTOCOL_KUBE => {
HostBridgeProtocol::Remote(FileTransferProtocol::Kube)
+27
View File
@@ -17,6 +17,7 @@ impl AuthActivity {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
FileTransferProtocol::GoogleCloudStorage => 22, // Doesn't matter, since not used
FileTransferProtocol::Kube => 22, // Doesn't matter, since not used
FileTransferProtocol::Smb => 445,
FileTransferProtocol::WebDAV => 80, // Doesn't matter, since not used
@@ -45,6 +46,9 @@ impl AuthActivity {
HostBridgeProtocol::Remote(remote) => {
let transfer_params = match remote {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::HostBridge),
FileTransferProtocol::GoogleCloudStorage => {
self.collect_gcs_host_params(FormTab::HostBridge)
}
FileTransferProtocol::Kube => {
self.collect_kube_host_params(FormTab::HostBridge)
}
@@ -71,6 +75,9 @@ impl AuthActivity {
pub(super) fn collect_remote_host_params(&self) -> Result<FileTransferParams, &'static str> {
match self.remote_protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::Remote),
FileTransferProtocol::GoogleCloudStorage => {
self.collect_gcs_host_params(FormTab::Remote)
}
FileTransferProtocol::Kube => self.collect_kube_host_params(FormTab::Remote),
FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::Remote),
FileTransferProtocol::Ftp(_)
@@ -136,6 +143,26 @@ impl AuthActivity {
})
}
/// Get input values from fields or return an error if fields are invalid to work as GCS.
pub(super) fn collect_gcs_host_params(
&self,
form_tab: FormTab,
) -> Result<FileTransferParams, &'static str> {
let params = self.get_gcs_params_input(form_tab);
if params.bucket_name.is_empty() {
return Err("Invalid bucket");
}
if params.endpoint.is_empty() {
return Err("Invalid endpoint");
}
Ok(FileTransferParams {
protocol: FileTransferProtocol::GoogleCloudStorage,
params: ProtocolParams::GoogleCloudStorage(params),
local_path: self.get_input_local_directory(form_tab),
remote_path: self.get_input_remote_directory(form_tab),
})
}
/// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_kube_host_params(
&self,
+50
View File
@@ -86,6 +86,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Remote(AuthFormId::Password),
InputMask::Smb => &Id::Remote(AuthFormId::Password),
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
InputMask::Gcs => &Id::Remote(AuthFormId::GcsBucket),
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
},
@@ -94,6 +95,7 @@ impl AuthActivity {
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
InputMask::Gcs => &Id::HostBridge(AuthFormId::GcsBucket),
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
},
@@ -112,6 +114,7 @@ impl AuthActivity {
InputMask::Generic => &Id::Remote(AuthFormId::Password),
InputMask::Smb => &Id::Remote(AuthFormId::Password),
InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket),
InputMask::Gcs => &Id::Remote(AuthFormId::GcsBucket),
InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::Remote(AuthFormId::Password),
},
@@ -120,6 +123,7 @@ impl AuthActivity {
InputMask::Generic => &Id::HostBridge(AuthFormId::Password),
InputMask::Smb => &Id::HostBridge(AuthFormId::Password),
InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket),
InputMask::Gcs => &Id::HostBridge(AuthFormId::GcsBucket),
InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace),
InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password),
},
@@ -280,6 +284,7 @@ impl AuthActivity {
#[cfg(win)]
InputMask::Smb => Id::HostBridge(AuthFormId::RemoteDirectory),
InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"),
InputMask::Gcs => unreachable!("this shouldn't happen (password on gcs)"),
InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"),
InputMask::WebDAV => Id::HostBridge(AuthFormId::RemoteDirectory),
};
@@ -294,6 +299,7 @@ impl AuthActivity {
InputMask::Smb => Id::HostBridge(AuthFormId::SmbShare),
InputMask::Localhost
| InputMask::AwsS3
| InputMask::Gcs
| InputMask::Kube
| InputMask::WebDAV => {
unreachable!("this shouldn't happen (port on s3/kube/webdav)")
@@ -310,6 +316,7 @@ impl AuthActivity {
InputMask::Generic => Id::HostBridge(AuthFormId::Address),
InputMask::Smb => Id::HostBridge(AuthFormId::Address),
InputMask::AwsS3 => Id::HostBridge(AuthFormId::S3Bucket),
InputMask::Gcs => Id::HostBridge(AuthFormId::GcsBucket),
InputMask::Kube => Id::HostBridge(AuthFormId::KubeNamespace),
InputMask::WebDAV => Id::HostBridge(AuthFormId::WebDAVUri),
};
@@ -331,6 +338,7 @@ impl AuthActivity {
InputMask::Smb => Id::HostBridge(AuthFormId::Password),
InputMask::Kube => Id::HostBridge(AuthFormId::KubeClientKey),
InputMask::AwsS3 => Id::HostBridge(AuthFormId::S3NewPathStyle),
InputMask::Gcs => Id::HostBridge(AuthFormId::GcsServiceAccountKey),
InputMask::WebDAV => Id::HostBridge(AuthFormId::Password),
};
self.activate_component(id);
@@ -389,6 +397,24 @@ impl AuthActivity {
UiAuthFormMsg::S3NewPathStyleBlurUp => {
self.activate_component(Id::HostBridge(AuthFormId::S3SessionToken))
}
UiAuthFormMsg::GcsBucketBlurDown => {
self.activate_component(Id::HostBridge(AuthFormId::GcsEndpoint))
}
UiAuthFormMsg::GcsBucketBlurUp => {
self.activate_component(Id::HostBridge(AuthFormId::Protocol))
}
UiAuthFormMsg::GcsEndpointBlurDown => {
self.activate_component(Id::HostBridge(AuthFormId::GcsServiceAccountKey))
}
UiAuthFormMsg::GcsEndpointBlurUp => {
self.activate_component(Id::HostBridge(AuthFormId::GcsBucket))
}
UiAuthFormMsg::GcsServiceAccountKeyBlurDown => {
self.activate_component(Id::HostBridge(AuthFormId::RemoteDirectory))
}
UiAuthFormMsg::GcsServiceAccountKeyBlurUp => {
self.activate_component(Id::HostBridge(AuthFormId::GcsEndpoint))
}
UiAuthFormMsg::KubeClientCertBlurDown => {
self.activate_component(Id::HostBridge(AuthFormId::KubeClientKey))
}
@@ -448,6 +474,7 @@ impl AuthActivity {
InputMask::Smb => Id::HostBridge(AuthFormId::SmbShare),
InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"),
InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"),
InputMask::Gcs => unreachable!("this shouldn't happen (username on gcs)"),
InputMask::WebDAV => Id::HostBridge(AuthFormId::WebDAVUri),
};
self.activate_component(id);
@@ -496,6 +523,7 @@ impl AuthActivity {
#[cfg(win)]
InputMask::Smb => Id::Remote(AuthFormId::RemoteDirectory),
InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"),
InputMask::Gcs => unreachable!("this shouldn't happen (password on gcs)"),
InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"),
InputMask::WebDAV => Id::Remote(AuthFormId::RemoteDirectory),
};
@@ -510,6 +538,7 @@ impl AuthActivity {
InputMask::Smb => Id::Remote(AuthFormId::SmbShare),
InputMask::Localhost
| InputMask::AwsS3
| InputMask::Gcs
| InputMask::Kube
| InputMask::WebDAV => {
unreachable!("this shouldn't happen (port on s3/kube/webdav)")
@@ -526,6 +555,7 @@ impl AuthActivity {
InputMask::Generic => Id::Remote(AuthFormId::Address),
InputMask::Smb => Id::Remote(AuthFormId::Address),
InputMask::AwsS3 => Id::Remote(AuthFormId::S3Bucket),
InputMask::Gcs => Id::Remote(AuthFormId::GcsBucket),
InputMask::Kube => Id::Remote(AuthFormId::KubeNamespace),
InputMask::WebDAV => Id::Remote(AuthFormId::WebDAVUri),
};
@@ -547,6 +577,7 @@ impl AuthActivity {
InputMask::Smb => Id::Remote(AuthFormId::Password),
InputMask::Kube => Id::Remote(AuthFormId::KubeClientKey),
InputMask::AwsS3 => Id::Remote(AuthFormId::S3NewPathStyle),
InputMask::Gcs => Id::Remote(AuthFormId::GcsServiceAccountKey),
InputMask::WebDAV => Id::Remote(AuthFormId::Password),
};
self.activate_component(id);
@@ -605,6 +636,24 @@ impl AuthActivity {
UiAuthFormMsg::S3NewPathStyleBlurUp => {
self.activate_component(Id::Remote(AuthFormId::S3SessionToken))
}
UiAuthFormMsg::GcsBucketBlurDown => {
self.activate_component(Id::Remote(AuthFormId::GcsEndpoint))
}
UiAuthFormMsg::GcsBucketBlurUp => {
self.activate_component(Id::Remote(AuthFormId::Protocol))
}
UiAuthFormMsg::GcsEndpointBlurDown => {
self.activate_component(Id::Remote(AuthFormId::GcsServiceAccountKey))
}
UiAuthFormMsg::GcsEndpointBlurUp => {
self.activate_component(Id::Remote(AuthFormId::GcsBucket))
}
UiAuthFormMsg::GcsServiceAccountKeyBlurDown => {
self.activate_component(Id::Remote(AuthFormId::RemoteDirectory))
}
UiAuthFormMsg::GcsServiceAccountKeyBlurUp => {
self.activate_component(Id::Remote(AuthFormId::GcsEndpoint))
}
UiAuthFormMsg::KubeClientCertBlurDown => {
self.activate_component(Id::Remote(AuthFormId::KubeClientKey))
}
@@ -664,6 +713,7 @@ impl AuthActivity {
InputMask::Smb => Id::Remote(AuthFormId::SmbShare),
InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"),
InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"),
InputMask::Gcs => unreachable!("this shouldn't happen (username on gcs)"),
InputMask::WebDAV => Id::Remote(AuthFormId::WebDAVUri),
};
self.activate_component(id);
+10
View File
@@ -11,6 +11,7 @@ use super::{
AuthActivity, AuthFormId, Context, FileTransferProtocol, FormTab, HostBridgeProtocol, Id,
InputMask, components,
};
use crate::filetransfer::params::DEFAULT_GCS_ENDPOINT;
use crate::utils::ui::{Popup, Size};
#[path = "view/mounting.rs"]
@@ -52,6 +53,9 @@ impl AuthActivity {
self.mount_port(FormTab::HostBridge, 22);
self.mount_username(FormTab::HostBridge, "");
self.mount_password(FormTab::HostBridge, "");
self.mount_gcs_bucket(FormTab::HostBridge, "");
self.mount_gcs_endpoint(FormTab::HostBridge, DEFAULT_GCS_ENDPOINT);
self.mount_gcs_service_account_key(FormTab::HostBridge, "");
self.mount_s3_bucket(FormTab::HostBridge, "");
self.mount_s3_profile(FormTab::HostBridge, "");
self.mount_s3_region(FormTab::HostBridge, "");
@@ -72,6 +76,7 @@ impl AuthActivity {
self.mount_webdav_uri(FormTab::HostBridge, "");
let remote_default_protocol = self.context().config().get_default_protocol();
self.set_remote_protocol(remote_default_protocol);
self.mount_remote_protocol(remote_default_protocol);
self.mount_remote_directory(FormTab::Remote, "");
self.mount_local_directory(FormTab::Remote, "");
@@ -82,6 +87,9 @@ impl AuthActivity {
);
self.mount_username(FormTab::Remote, "");
self.mount_password(FormTab::Remote, "");
self.mount_gcs_bucket(FormTab::Remote, "");
self.mount_gcs_endpoint(FormTab::Remote, DEFAULT_GCS_ENDPOINT);
self.mount_gcs_service_account_key(FormTab::Remote, "");
self.mount_s3_bucket(FormTab::Remote, "");
self.mount_s3_profile(FormTab::Remote, "");
self.mount_s3_region(FormTab::Remote, "");
@@ -264,6 +272,7 @@ impl AuthActivity {
.split(protocol_and_mask_chunks[1]);
match self.host_bridge_input_mask() {
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_host_bridge_s3_view()),
InputMask::Gcs => self.render_view_ids(f, input_mask, self.get_host_bridge_gcs_view()),
InputMask::Generic => {
self.render_view_ids(f, input_mask, self.get_host_bridge_generic_params_view())
}
@@ -311,6 +320,7 @@ impl AuthActivity {
.split(protocol_and_mask_chunks[1]);
match self.remote_input_mask() {
InputMask::AwsS3 => self.render_view_ids(f, input_mask, self.get_remote_s3_view()),
InputMask::Gcs => self.render_view_ids(f, input_mask, self.get_remote_gcs_view()),
InputMask::Generic => {
self.render_view_ids(f, input_mask, self.get_remote_generic_params_view())
}
+50
View File
@@ -383,6 +383,56 @@ impl AuthActivity {
}
}
pub(in crate::ui::activities::auth) fn mount_gcs_bucket(
&mut self,
form_tab: FormTab,
bucket: &str,
) {
let color = self.theme().auth_address;
let id = Self::form_tab_id(form_tab, AuthFormId::GcsBucket);
if let Err(err) = self.app.remount(
id,
Box::new(components::InputGcsBucket::new(bucket, form_tab, color)),
vec![],
) {
error!("Failed to remount component: {err}");
}
}
pub(in crate::ui::activities::auth) fn mount_gcs_endpoint(
&mut self,
form_tab: FormTab,
endpoint: &str,
) {
let color = self.theme().auth_username;
let id = Self::form_tab_id(form_tab, AuthFormId::GcsEndpoint);
if let Err(err) = self.app.remount(
id,
Box::new(components::InputGcsEndpoint::new(endpoint, form_tab, color)),
vec![],
) {
error!("Failed to remount component: {err}");
}
}
pub(in crate::ui::activities::auth) fn mount_gcs_service_account_key(
&mut self,
form_tab: FormTab,
path: &str,
) {
let color = self.theme().auth_password;
let id = Self::form_tab_id(form_tab, AuthFormId::GcsServiceAccountKey);
if let Err(err) = self.app.remount(
id,
Box::new(components::InputGcsServiceAccountKey::new(
path, form_tab, color,
)),
vec![],
) {
error!("Failed to remount component: {err}");
}
}
pub(in crate::ui::activities::auth) fn mount_s3_bucket(
&mut self,
form_tab: FormTab,
+65 -2
View File
@@ -6,8 +6,8 @@ use tuirealm::state::{State, StateValue};
use super::*;
use crate::filetransfer::FileTransferParams;
use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams,
WebDAVProtocolParams,
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
ProtocolParams, SmbParams, WebDAVProtocolParams,
};
impl AuthActivity {
@@ -48,6 +48,15 @@ impl AuthActivity {
.new_path_style(new_path_style)
}
pub(in crate::ui::activities::auth) fn get_gcs_params_input(
&self,
form_tab: FormTab,
) -> GoogleCloudStorageParams {
GoogleCloudStorageParams::new(self.get_input_gcs_bucket(form_tab))
.endpoint(self.get_input_gcs_endpoint(form_tab))
.service_account_key(self.get_input_gcs_service_account_key(form_tab))
}
pub(in crate::ui::activities::auth) fn get_kube_params_input(
&self,
form_tab: FormTab,
@@ -213,6 +222,45 @@ impl AuthActivity {
}
}
pub(in crate::ui::activities::auth) fn get_input_gcs_bucket(
&self,
form_tab: FormTab,
) -> String {
match self
.app
.state(&Self::form_tab_id(form_tab, AuthFormId::GcsBucket))
{
Ok(State::Single(StateValue::String(value))) => value,
_ => String::new(),
}
}
pub(in crate::ui::activities::auth) fn get_input_gcs_endpoint(
&self,
form_tab: FormTab,
) -> String {
match self
.app
.state(&Self::form_tab_id(form_tab, AuthFormId::GcsEndpoint))
{
Ok(State::Single(StateValue::String(value))) => value,
_ => String::new(),
}
}
pub(in crate::ui::activities::auth) fn get_input_gcs_service_account_key(
&self,
form_tab: FormTab,
) -> Option<String> {
match self.app.state(&Self::form_tab_id(
form_tab,
AuthFormId::GcsServiceAccountKey,
)) {
Ok(State::Single(StateValue::String(value))) if !value.is_empty() => Some(value),
_ => None,
}
}
pub(in crate::ui::activities::auth) fn get_input_s3_region(
&self,
form_tab: FormTab,
@@ -428,6 +476,7 @@ impl AuthActivity {
fn input_mask_size(input_mask: InputMask) -> u16 {
match input_mask {
InputMask::AwsS3
| InputMask::Gcs
| InputMask::Generic
| InputMask::Kube
| InputMask::Smb
@@ -446,6 +495,11 @@ impl AuthActivity {
pub(in crate::ui::activities::auth) fn fmt_recent(b: FileTransferParams) -> String {
let protocol = b.protocol.to_string().to_lowercase();
let remote_path = b
.remote_path
.as_ref()
.map(|path| format!(" {}", path.display()))
.unwrap_or_default();
match b.params {
ProtocolParams::AwsS3(s3) => {
let profile = match s3.profile {
@@ -471,6 +525,15 @@ impl AuthActivity {
protocol, username, params.address, params.port
)
}
ProtocolParams::GoogleCloudStorage(params) => format!(
"{protocol}://{} ({}){remote_path}",
params.bucket_name,
if params.endpoint.is_empty() {
crate::filetransfer::params::DEFAULT_GCS_ENDPOINT
} else {
params.endpoint.as_str()
},
),
ProtocolParams::Kube(params) => {
format!(
"{}://{}{}",
+34
View File
@@ -159,6 +159,40 @@ impl AuthActivity {
}
}
pub(in crate::ui::activities::auth) fn get_host_bridge_gcs_view(&self) -> [Id; 4] {
match self.app.focus() {
Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [
Id::HostBridge(AuthFormId::GcsEndpoint),
Id::HostBridge(AuthFormId::GcsServiceAccountKey),
Id::HostBridge(AuthFormId::RemoteDirectory),
Id::HostBridge(AuthFormId::LocalDirectory),
],
_ => [
Id::HostBridge(AuthFormId::GcsBucket),
Id::HostBridge(AuthFormId::GcsEndpoint),
Id::HostBridge(AuthFormId::GcsServiceAccountKey),
Id::HostBridge(AuthFormId::RemoteDirectory),
],
}
}
pub(in crate::ui::activities::auth) fn get_remote_gcs_view(&self) -> [Id; 4] {
match self.app.focus() {
Some(&Id::Remote(AuthFormId::LocalDirectory)) => [
Id::Remote(AuthFormId::GcsEndpoint),
Id::Remote(AuthFormId::GcsServiceAccountKey),
Id::Remote(AuthFormId::RemoteDirectory),
Id::Remote(AuthFormId::LocalDirectory),
],
_ => [
Id::Remote(AuthFormId::GcsBucket),
Id::Remote(AuthFormId::GcsEndpoint),
Id::Remote(AuthFormId::GcsServiceAccountKey),
Id::Remote(AuthFormId::RemoteDirectory),
],
}
}
pub(in crate::ui::activities::auth) fn get_host_bridge_kube_view(&self) -> [Id; 4] {
match self.app.focus() {
Some(&Id::HostBridge(AuthFormId::KubeClientCert)) => [
@@ -64,6 +64,7 @@ impl FileTransferActivity {
match params {
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::GoogleCloudStorage(params) => params.bucket_name.clone(),
ProtocolParams::Kube(params) => {
params.namespace.clone().unwrap_or("default".to_string())
}
@@ -93,6 +94,11 @@ impl FileTransferActivity {
);
format!("Connecting to {}", params.bucket_name)
}
ProtocolParams::GoogleCloudStorage(params) => format!(
"Connecting to GCS bucket '{bucket}' at {endpoint}…",
bucket = params.bucket_name,
endpoint = params.endpoint,
),
ProtocolParams::Kube(params) => {
let namespace = params.namespace.as_deref().unwrap_or("default");
info!("Client is not connected to remote; connecting to namespace {namespace}",);
@@ -32,6 +32,10 @@ enum TransferErrorReason {
RemoteHostError(HostError),
}
fn handle_remote_finalize_result(result: Result<(), HostError>) -> Result<(), TransferErrorReason> {
result.map_err(TransferErrorReason::RemoteHostError)
}
/// Represents the entity to send or receive during a transfer.
/// - File: describes an individual `File` to send
/// - Any: Can be any kind of `File`, but just one
@@ -525,12 +529,7 @@ impl FileTransferActivity {
}
}
// Finalize stream
if let Err(err) = self.browser.remote_pane_mut().fs.finalize_write(writer) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{err}\""),
);
}
handle_remote_finalize_result(self.browser.remote_pane_mut().fs.finalize_write(writer))?;
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
@@ -922,6 +921,24 @@ impl FileTransferActivity {
}
}
#[cfg(test)]
mod finalize_test {
use super::*;
use crate::host::HostErrorType;
#[test]
fn should_propagate_remote_finalize_error() {
let error = HostError::from(HostErrorType::CouldNotCreateFile);
let result = handle_remote_finalize_result(Err(error));
assert!(matches!(
result,
Err(TransferErrorReason::RemoteHostError(_))
));
}
}
#[cfg(test)]
mod worklist_test {
use std::time::SystemTime;
+3 -2
View File
@@ -32,8 +32,9 @@ const RADIO_PROTOCOL_FTP: usize = 2;
const RADIO_PROTOCOL_FTPS: usize = 3;
const RADIO_PROTOCOL_KUBE: usize = 4;
const RADIO_PROTOCOL_S3: usize = 5;
const RADIO_PROTOCOL_SMB: usize = 6;
const RADIO_PROTOCOL_WEBDAV: usize = 7;
const RADIO_PROTOCOL_GCS: usize = 6;
const RADIO_PROTOCOL_SMB: usize = 7;
const RADIO_PROTOCOL_WEBDAV: usize = 8;
// -- components
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
+7 -3
View File
@@ -14,8 +14,9 @@ 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_KUBE, RADIO_PROTOCOL_S3,
RADIO_PROTOCOL_SCP, RADIO_PROTOCOL_SFTP, RADIO_PROTOCOL_SMB, RADIO_PROTOCOL_WEBDAV,
RADIO_PROTOCOL_FTP, RADIO_PROTOCOL_FTPS, RADIO_PROTOCOL_GCS, RADIO_PROTOCOL_KUBE,
RADIO_PROTOCOL_S3, RADIO_PROTOCOL_SCP, RADIO_PROTOCOL_SFTP, RADIO_PROTOCOL_SMB,
RADIO_PROTOCOL_WEBDAV,
};
use crate::utils::parser::parse_bytesize;
@@ -73,7 +74,9 @@ impl DefaultProtocol {
.color(Color::Cyan)
.modifiers(BorderType::Rounded),
)
.choices(["SFTP", "SCP", "FTP", "FTPS", "Kube", "S3", "SMB", "WebDAV"])
.choices([
"SFTP", "SCP", "FTP", "FTPS", "Kube", "S3", "GCS", "SMB", "WebDAV",
])
.foreground(Color::Cyan)
.rewind(true)
.title(Title::from("Default protocol").alignment(HorizontalAlignment::Left))
@@ -84,6 +87,7 @@ impl DefaultProtocol {
FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS,
FileTransferProtocol::Kube => RADIO_PROTOCOL_KUBE,
FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3,
FileTransferProtocol::GoogleCloudStorage => RADIO_PROTOCOL_GCS,
FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB,
FileTransferProtocol::WebDAV => RADIO_PROTOCOL_WEBDAV,
}),
+3 -2
View File
@@ -12,8 +12,8 @@ use tuirealm::state::{State, StateValue};
use tuirealm::terminal::TerminalAdapter;
use super::{
Context, Id, IdCommon, IdConfig, RADIO_PROTOCOL_KUBE, RADIO_PROTOCOL_WEBDAV, SetupActivity,
ViewLayout, components,
Context, Id, IdCommon, IdConfig, RADIO_PROTOCOL_GCS, RADIO_PROTOCOL_KUBE,
RADIO_PROTOCOL_WEBDAV, SetupActivity, ViewLayout, components,
};
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol;
@@ -272,6 +272,7 @@ impl SetupActivity {
RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true),
RADIO_PROTOCOL_KUBE => FileTransferProtocol::Kube,
RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3,
RADIO_PROTOCOL_GCS => FileTransferProtocol::GoogleCloudStorage,
RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb,
RADIO_PROTOCOL_WEBDAV => FileTransferProtocol::WebDAV,
_ => FileTransferProtocol::Sftp,
+34
View File
@@ -40,6 +40,9 @@ pub(super) static REMOTE_KUBE_OPT_REGEX: Lazy<Regex> =
pub(super) static REMOTE_S3_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:(.+[^@])@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?");
/// Regex matches Google Cloud Storage remote options.
pub(super) static REMOTE_GCS_OPT_REGEX: Lazy<Regex> = lazy_regex!(r"^([^:]+)(?::(/.*))?$");
/// Regex matches SMB remote options on Unix platforms.
#[cfg(smb_unix)]
pub(super) static REMOTE_SMB_OPT_REGEX: Lazy<Regex> = lazy_regex!(
@@ -82,6 +85,10 @@ static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B
///
/// `s3://<bucket-name>@<region>[:profile][:/wrkdir]`
///
/// For Google Cloud Storage:
///
/// `gcs://<bucket>[:/working/directory]`
///
/// For SMB:
///
/// on UNIX derived (macos, linux, ...)
@@ -472,6 +479,33 @@ mod tests {
assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
}
#[test]
fn should_parse_google_cloud_storage_address() {
let result = parse_remote_opt("gcs://my-bucket").unwrap();
let params = result.params.gcs_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::GoogleCloudStorage);
assert_eq!(result.remote_path, None);
assert_eq!(params.bucket_name, "my-bucket");
assert_eq!(params.endpoint, "https://storage.googleapis.com");
assert_eq!(params.service_account_key, None);
}
#[test]
fn should_parse_google_cloud_storage_working_directory() {
let result = parse_remote_opt("gcs://my-bucket:/assets/images").unwrap();
assert_eq!(
result.remote_path.as_deref(),
Some(std::path::Path::new("/assets/images"))
);
}
#[test]
fn should_reject_google_cloud_storage_address_without_bucket() {
assert!(parse_remote_opt("gcs://:/assets").is_err());
}
#[test]
fn should_parse_kube_address() {
let result = parse_remote_opt("kube://my-namespace@http://localhost:1234$/tmp")
+1
View File
@@ -9,6 +9,7 @@ pub(super) fn default_port_for_protocol(protocol: FileTransferProtocol) -> u16 {
match protocol {
FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => 22,
FileTransferProtocol::GoogleCloudStorage => 22,
_ => 22,
}
}
+19 -2
View File
@@ -11,12 +11,14 @@ use super::credentials::{optional_capture, required_capture};
use super::ports::{default_port_for_protocol, parse_port};
use super::protocol::parse_remote_opt_protocol;
use super::{
REMOTE_GENERIC_OPT_REGEX, REMOTE_KUBE_OPT_REGEX, REMOTE_S3_OPT_REGEX, REMOTE_WEBDAV_OPT_REGEX,
REMOTE_GCS_OPT_REGEX, REMOTE_GENERIC_OPT_REGEX, REMOTE_KUBE_OPT_REGEX, REMOTE_S3_OPT_REGEX,
REMOTE_WEBDAV_OPT_REGEX,
};
#[cfg(smb)]
use crate::filetransfer::params::SmbParams;
use crate::filetransfer::params::{
AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, WebDAVProtocolParams,
AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, KubeProtocolParams,
ProtocolParams, WebDAVProtocolParams,
};
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
#[cfg(not(test))]
@@ -30,6 +32,7 @@ pub(super) fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
match protocol {
FileTransferProtocol::AwsS3 => parse_s3_remote_opt(remote.as_str()),
FileTransferProtocol::GoogleCloudStorage => parse_gcs_remote_opt(remote.as_str()),
FileTransferProtocol::Kube => parse_kube_remote_opt(remote.as_str()),
#[cfg(smb)]
FileTransferProtocol::Smb => parse_smb_remote_opts(remote.as_str()),
@@ -122,6 +125,20 @@ fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
.remote_path(remote_path))
}
fn parse_gcs_remote_opt(s: &str) -> Result<FileTransferParams, String> {
let groups = REMOTE_GCS_OPT_REGEX
.captures(s)
.ok_or_else(|| String::from("Bad Google Cloud Storage syntax!"))?;
let bucket = required_capture(&groups, 1, "bucket")?;
let remote_path = groups.get(2).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(
FileTransferProtocol::GoogleCloudStorage,
ProtocolParams::GoogleCloudStorage(GoogleCloudStorageParams::new(bucket)),
)
.remote_path(remote_path))
}
fn parse_kube_remote_opt(s: &str) -> Result<FileTransferParams, String> {
let groups = REMOTE_KUBE_OPT_REGEX
.captures(s)