use std::path::PathBuf;
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::{
PavaoSmbCredentials as SmbCredentials, PavaoSmbFs as SmbFs, PavaoSmbOptions as SmbOptions,
SmbDialect as RemoteSmbDialect,
};
#[cfg(smb_windows)]
use remotefs_smb::{WNetSmbCredentials as SmbCredentials, WNetSmbFs as SmbFs};
use remotefs_ssh::{
NoCheckServerKey, RusshSession as SshSession, ScpFs, SftpFs, SshAgentIdentity,
SshConfigParseRule, SshOpts,
};
use remotefs_webdav::WebDAVFs;
#[cfg(smb_unix)]
use super::params::SmbDialect;
#[cfg(not(smb))]
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams};
#[cfg(smb)]
use super::params::{AwsS3Params, GenericProtocolParams, GoogleCloudStorageParams, SmbParams};
use super::params::{KubeProtocolParams, WebDAVProtocolParams};
use super::{FileTransferProtocol, ProtocolParams};
use crate::system::config_client::ConfigClient;
use crate::system::sshkey_storage::SshKeyStorage;
pub struct RemoteFsBuilder;
impl RemoteFsBuilder {
pub fn build(
protocol: FileTransferProtocol,
params: ProtocolParams,
config_client: &ConfigClient,
) -> Result<Box<dyn RemoteFs>, String> {
match (protocol, params) {
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
Ok(Box::new(Self::aws_s3_client(params)?))
}
(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)?))
}
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
Ok(Box::new(Self::scp_client(params, config_client)?))
}
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
Ok(Box::new(Self::sftp_client(params, config_client)?))
}
#[cfg(smb)]
(FileTransferProtocol::Smb, ProtocolParams::Smb(params)) => {
Ok(Box::new(Self::smb_client(params)?))
}
(FileTransferProtocol::WebDAV, ProtocolParams::WebDAV(params)) => {
Ok(Box::new(Self::webdav_client(params)))
}
(protocol, params) => {
error!("Invalid params for protocol '{:?}'", protocol);
Err(format!(
"Invalid protocol '{protocol:?}' with parameters of type {params:?}",
))
}
}
}
fn aws_s3_client(params: AwsS3Params) -> Result<AwsS3Fs, String> {
let rt = Arc::new(
tokio::runtime::Builder::new_current_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
);
let mut client =
AwsS3Fs::new(params.bucket_name, &rt).new_path_style(params.new_path_style);
if let Some(region) = params.region {
client = client.region(region);
}
if let Some(profile) = params.profile {
client = client.profile(profile);
}
if let Some(endpoint) = params.endpoint {
client = client.endpoint(endpoint);
}
if let Some(access_key) = params.access_key {
client = client.access_key(access_key);
}
if let Some(secret_access_key) = params.secret_access_key {
client = client.secret_access_key(secret_access_key);
}
if let Some(security_token) = params.security_token {
client = client.security_token(security_token);
}
if let Some(session_token) = params.session_token {
client = client.session_token(session_token);
}
Ok(client)
}
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)
}
fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs {
let mut client = FtpFs::new(params.address, params.port).passive_mode();
if let Some(username) = params.username {
client = client.username(username);
}
if let Some(password) = params.password {
client = client.password(password);
}
if secure {
client = client.secure(true, true);
}
client
}
fn kube_client(params: KubeProtocolParams) -> Result<KubeFs, String> {
let rt = Self::tokio_runtime()?;
let kube_fs = KubeFs::new(&rt);
if let Some(config) = params.config() {
Ok(kube_fs.config(config))
} else {
Ok(kube_fs)
}
}
fn scp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> Result<ScpFs<SshSession<NoCheckServerKey>>, String> {
let opts = Self::build_ssh_opts(params, config_client);
let rt = Self::tokio_runtime()?;
Ok(ScpFs::russh(opts, rt))
}
fn sftp_client(
params: GenericProtocolParams,
config_client: &ConfigClient,
) -> Result<SftpFs<SshSession<NoCheckServerKey>>, String> {
let opts = Self::build_ssh_opts(params, config_client);
let rt = Self::tokio_runtime()?;
Ok(SftpFs::russh(opts, rt))
}
#[cfg(smb_unix)]
fn smb_dialect_bounds(dialect: SmbDialect) -> (RemoteSmbDialect, RemoteSmbDialect) {
match dialect {
SmbDialect::Auto => (RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb311),
SmbDialect::Smb1 => (RemoteSmbDialect::Nt1, RemoteSmbDialect::Nt1),
SmbDialect::Smb2 => (RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb210),
SmbDialect::Smb3 => (RemoteSmbDialect::Smb300, RemoteSmbDialect::Smb311),
}
}
#[cfg(smb_unix)]
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
let mut credentials = SmbCredentials::default()
.server(format!("smb://{}:{}", params.address, params.port))
.share(params.share);
if let Some(username) = params.username {
credentials = credentials.username(username);
}
if let Some(password) = params.password {
credentials = credentials.password(password);
}
if let Some(workgroup) = params.workgroup {
credentials = credentials.workgroup(workgroup);
}
let (min_dialect, max_dialect) = Self::smb_dialect_bounds(params.dialect);
SmbFs::try_new_with_dialect(
credentials,
SmbOptions::default()
.one_share_per_server(true)
.case_sensitive(false),
min_dialect,
max_dialect,
)
.map_err(|e| {
error!("Invalid params for protocol SMB: {e}");
format!("Invalid params for protocol SMB: {e}")
})
}
#[cfg(smb_windows)]
fn smb_client(params: SmbParams) -> Result<SmbFs, String> {
let mut credentials = SmbCredentials::new(params.address, params.share);
if let Some(username) = params.username {
credentials = credentials.username(username);
}
if let Some(password) = params.password {
credentials = credentials.password(password);
}
Ok(SmbFs::new(credentials))
}
fn webdav_client(params: WebDAVProtocolParams) -> WebDAVFs {
WebDAVFs::new(¶ms.username, ¶ms.password, ¶ms.uri)
}
fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts {
let mut opts = SshOpts::new(params.address.clone())
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
.ssh_agent_identity(Some(SshAgentIdentity::All))
.port(params.port);
if let Some(username) = params.username {
opts = opts.username(username);
} else if let Ok(username) = whoami::username() {
opts = opts.username(username);
}
if let Some(password) = params.password
&& !password.is_empty()
{
opts = opts.password(password);
}
if let Some(config_path) = config_client.get_ssh_config() {
opts = opts.config_file(
PathBuf::from(config_path),
SshConfigParseRule::ALLOW_UNKNOWN_FIELDS,
);
}
opts
}
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
SshKeyStorage::from(config_client)
}
fn tokio_runtime() -> Result<Arc<tokio::runtime::Runtime>, String> {
Ok(Arc::new(
tokio::runtime::Builder::new_current_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|e| format!("Unable to create tokio runtime: {e}"))?,
))
}
}
#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
#[cfg(smb)]
use serial_test::serial;
use tempfile::TempDir;
use super::*;
#[test]
fn should_build_aws_s3_fs() {
let params = ProtocolParams::AwsS3(
AwsS3Params::new("omar", Some("eu-west-1"), Some("test"))
.endpoint(Some("http://localhost:9000"))
.new_path_style(true)
.access_key(Some("pippo"))
.secret_access_key(Some("pluto"))
.security_token(Some("omar"))
.session_token(Some("gerry-scotti")),
);
let config_client = get_config_client();
assert!(
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_ok()
);
}
#[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(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(21)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
assert!(
RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client).is_ok()
);
}
#[test]
fn test_should_build_kube_fs() {
let params = ProtocolParams::Kube(KubeProtocolParams {
namespace: Some("namespace".to_string()),
cluster_url: Some("cluster_url".to_string()),
username: Some("username".to_string()),
client_cert: Some("client_cert".to_string()),
client_key: Some("client_key".to_string()),
});
let config_client = get_config_client();
assert!(RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client).is_ok());
}
#[test]
fn should_build_scp_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(22)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
assert!(RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client).is_ok());
}
#[test]
fn should_build_sftp_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(22)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
assert!(RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client).is_ok());
}
#[test]
#[cfg(smb)]
#[serial]
fn should_build_smb_fs() {
let params = ProtocolParams::Smb(SmbParams::new("localhost", "share"));
let config_client = get_config_client();
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
}
#[test]
#[cfg(smb_unix)]
fn should_map_smb_dialect_to_bounds() {
use remotefs_smb::SmbDialect as RemoteSmbDialect;
use crate::filetransfer::params::SmbDialect;
assert_eq!(
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Auto),
(RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb311)
);
assert_eq!(
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb1),
(RemoteSmbDialect::Nt1, RemoteSmbDialect::Nt1)
);
assert_eq!(
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb2),
(RemoteSmbDialect::Smb202, RemoteSmbDialect::Smb210)
);
assert_eq!(
RemoteFsBuilder::smb_dialect_bounds(SmbDialect::Smb3),
(RemoteSmbDialect::Smb300, RemoteSmbDialect::Smb311)
);
}
#[test]
#[cfg(smb)]
#[serial]
fn should_build_smb_fs_with_dialect() {
use crate::filetransfer::params::SmbDialect;
let params =
ProtocolParams::Smb(SmbParams::new("localhost", "share").dialect(SmbDialect::Smb1));
let config_client = get_config_client();
assert!(RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client).is_ok());
}
#[test]
fn should_not_build_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(22)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
assert!(
RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client).is_err()
);
}
fn get_config_client() -> ConfigClient {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
.ok()
.unwrap()
}
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let mut k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
k.push("ssh-keys/");
c.push("config.toml");
(c, k)
}
}