remote-fs 0.1.2

Unified async file operations over SMB, FTP, and SFTP
Documentation
use std::ops::{Deref, DerefMut};
use std::path::Path;

use async_trait::async_trait;

use crate::error::{Error, Result};
use crate::traits::RemoteFileSystem;
use crate::types::{Config, FileEntry, FileMeta, Protocol};

#[cfg(feature = "ftp")]
use crate::ftp::FtpClient;
#[cfg(feature = "ftp")]
use crate::types::FtpConfig;

#[cfg(feature = "sftp")]
use crate::sftp::SftpClient;
#[cfg(feature = "sftp")]
use crate::types::SftpConfig;

#[cfg(feature = "smb")]
use crate::smb::SmbClient;
#[cfg(feature = "smb")]
use crate::types::SmbConfig;

/// Unified client over SMB / FTP / SFTP.
///
/// Holds a type-erased backend (`Box<dyn RemoteFileSystem>`), so trait methods
/// forward in one line — no per-protocol `match` boilerplate.
///
/// Backends are gated by Cargo features: `smb`, `ftp`, `sftp`.
pub struct Client {
    protocol: Protocol,
    inner: Box<dyn RemoteFileSystem>,
}

impl Client {
    #[cfg_attr(
        not(any(feature = "smb", feature = "ftp", feature = "sftp")),
        allow(dead_code)
    )]
    fn new(protocol: Protocol, inner: Box<dyn RemoteFileSystem>) -> Self {
        Self { protocol, inner }
    }

    /// Create a client from a generic config and connect.
    pub async fn connect_with(config: &Config) -> Result<Self> {
        #[cfg(not(any(feature = "smb", feature = "ftp", feature = "sftp")))]
        {
            let _ = config;
            return Err(Error::FeatureDisabled(
                "enable at least one of: smb, ftp, sftp",
            ));
        }

        #[cfg(any(feature = "smb", feature = "ftp", feature = "sftp"))]
        {
            let (protocol, mut inner): (Protocol, Box<dyn RemoteFileSystem>) = match config {
                #[cfg(feature = "smb")]
                Config::Smb(cfg) => (Protocol::Smb, Box::new(SmbClient::new(cfg.clone()))),
                #[cfg(feature = "ftp")]
                Config::Ftp(cfg) => (Protocol::Ftp, Box::new(FtpClient::new(cfg.clone()))),
                #[cfg(feature = "sftp")]
                Config::Sftp(cfg) => (Protocol::Sftp, Box::new(SftpClient::new(cfg.clone()))),
            };
            inner.connect().await?;
            Ok(Self::new(protocol, inner))
        }
    }

    /// Create an SMB client and connect.
    #[cfg(feature = "smb")]
    pub async fn smb(config: &SmbConfig) -> Result<Self> {
        Self::connect_with(&Config::Smb(config.clone())).await
    }

    /// Create an FTP client and connect.
    #[cfg(feature = "ftp")]
    pub async fn ftp(config: &FtpConfig) -> Result<Self> {
        Self::connect_with(&Config::Ftp(config.clone())).await
    }

    /// Create an SFTP client and connect.
    #[cfg(feature = "sftp")]
    pub async fn sftp(config: &SftpConfig) -> Result<Self> {
        Self::connect_with(&Config::Sftp(config.clone())).await
    }

    /// Active protocol of this client.
    pub fn protocol(&self) -> Protocol {
        self.protocol
    }

    /// Borrow the underlying filesystem trait object.
    pub fn as_fs(&self) -> &dyn RemoteFileSystem {
        &*self.inner
    }

    /// Mutably borrow the underlying filesystem trait object.
    pub fn as_fs_mut(&mut self) -> &mut dyn RemoteFileSystem {
        &mut *self.inner
    }
}

impl Deref for Client {
    type Target = dyn RemoteFileSystem;

    fn deref(&self) -> &Self::Target {
        &*self.inner
    }
}

impl DerefMut for Client {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut *self.inner
    }
}

#[async_trait]
impl RemoteFileSystem for Client {
    async fn connect(&mut self) -> Result<()> {
        self.inner.connect().await
    }

    async fn disconnect(&mut self) -> Result<()> {
        self.inner.disconnect().await
    }

    fn is_connected(&self) -> bool {
        self.inner.is_connected()
    }

    async fn list(&mut self, path: &str) -> Result<Vec<FileEntry>> {
        self.inner.list(path).await
    }

    async fn metadata(&mut self, path: &str) -> Result<FileMeta> {
        self.inner.metadata(path).await
    }

    async fn exists(&mut self, path: &str) -> Result<bool> {
        self.inner.exists(path).await
    }

    async fn mkdir(&mut self, path: &str) -> Result<()> {
        self.inner.mkdir(path).await
    }

    async fn mkdir_all(&mut self, path: &str) -> Result<()> {
        self.inner.mkdir_all(path).await
    }

    async fn rmdir(&mut self, path: &str) -> Result<()> {
        self.inner.rmdir(path).await
    }

    async fn remove_file(&mut self, path: &str) -> Result<()> {
        self.inner.remove_file(path).await
    }

    async fn remove(&mut self, path: &str) -> Result<()> {
        self.inner.remove(path).await
    }

    async fn rename(&mut self, from: &str, to: &str) -> Result<()> {
        self.inner.rename(from, to).await
    }

    async fn read(&mut self, path: &str) -> Result<Vec<u8>> {
        self.inner.read(path).await
    }

    async fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
        self.inner.write(path, data).await
    }

    async fn download(&mut self, remote: &str, local: &Path) -> Result<()> {
        self.inner.download(remote, local).await
    }

    async fn upload(&mut self, local: &Path, remote: &str) -> Result<()> {
        self.inner.upload(local, remote).await
    }
}

/// Builder for constructing a [`Client`].
#[derive(Debug, Default)]
pub struct ClientBuilder {
    config: Option<Config>,
}

impl ClientBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    #[cfg(feature = "smb")]
    pub fn smb(mut self, config: SmbConfig) -> Self {
        self.config = Some(Config::Smb(config));
        self
    }

    #[cfg(feature = "ftp")]
    pub fn ftp(mut self, config: FtpConfig) -> Self {
        self.config = Some(Config::Ftp(config));
        self
    }

    #[cfg(feature = "sftp")]
    pub fn sftp(mut self, config: SftpConfig) -> Self {
        self.config = Some(Config::Sftp(config));
        self
    }

    pub fn config(mut self, config: Config) -> Self {
        self.config = Some(config);
        self
    }

    pub async fn build(self) -> Result<Client> {
        let config = self
            .config
            .ok_or_else(|| Error::Other("missing connection config".into()))?;
        Client::connect_with(&config).await
    }
}