mirror of
https://github.com/veeso/termscp.git
synced 2026-08-31 01:15:56 -07:00
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:
committed by
GitHub
parent
98a1ce42dc
commit
974b6d6917
+3
-12
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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,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 = "."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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** 操作系统。
|
||||
|
||||

|
||||
|
||||
@@ -49,6 +49,7 @@ termscp 是一个功能丰富的终端文件浏览和传输工具,支持 SCP/S
|
||||
- **FTP** 和 **FTPS**
|
||||
- **Kube**
|
||||
- **S3**
|
||||
- **Google Cloud Storage (GCS)**
|
||||
- **SMB**
|
||||
- **WebDAV**
|
||||
- 🖥 使用便捷的 UI 在远程和本地文件系统上浏览和操作
|
||||
|
||||
@@ -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,7 +2,7 @@
|
||||
|
||||
欢迎阅读 termscp 的开发者手册。本章不包含 termscp 各模块的文档,相关文档可以在 Rust Docs 上找到:<https://docs.rs/termscp>。本章描述 termscp 的工作原理,以及实现诸如文件传输和用户界面扩展等功能的指南。
|
||||
|
||||
termscp 使用 Rust 编写(edition 2024,MSRV 1.89.0)。用户界面使用 [tuirealm](https://github.com/veeso/tui-realm) v3 构建,它运行在 [crossterm](https://github.com/crossterm-rs/crossterm) 之上。
|
||||
termscp 使用 Rust 编写(edition 2024,MSRV 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
|
||||
|
||||
|
||||
@@ -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 Storage(GCS)存储桶。
|
||||
|
||||
认证表单字段:
|
||||
|
||||
- 存储桶名称(必填)
|
||||
- 端点(默认为 `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
@@ -6,7 +6,7 @@ termscp 是一款功能丰富、带有 TUI(终端用户界面)的终端文
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 支持多种传输协议:SFTP、SCP、FTP 和 FTPS、Kube、S3、SMB 以及 WebDAV。
|
||||
- 支持多种传输协议:SFTP、SCP、FTP 和 FTPS、Kube、S3、GCS、SMB 以及 WebDAV。
|
||||
- 双面板浏览器,可同时浏览并操作远程和本地文件系统:创建、删除、重命名、搜索、查看和编辑文件。
|
||||
- 书签和最近连接记录,帮助你快速重新连接到常用的主机。
|
||||
- 使用你喜爱的编辑器查看和编辑文件。
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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#"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶ms.bucket_name);
|
||||
self.mount_gcs_endpoint(
|
||||
form_tab,
|
||||
if params.endpoint.is_empty() {
|
||||
DEFAULT_GCS_ENDPOINT
|
||||
} else {
|
||||
¶ms.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(""));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
"{}://{}{}",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user