Skip to main content

remote_fs/
client.rs

1use std::ops::{Deref, DerefMut};
2use std::path::Path;
3
4use async_trait::async_trait;
5
6use crate::error::{Error, Result};
7use crate::traits::RemoteFileSystem;
8use crate::types::{Config, FileEntry, FileMeta, Protocol};
9
10#[cfg(feature = "ftp")]
11use crate::ftp::FtpClient;
12#[cfg(feature = "ftp")]
13use crate::types::FtpConfig;
14
15#[cfg(feature = "sftp")]
16use crate::sftp::SftpClient;
17#[cfg(feature = "sftp")]
18use crate::types::SftpConfig;
19
20#[cfg(feature = "smb")]
21use crate::smb::SmbClient;
22#[cfg(feature = "smb")]
23use crate::types::SmbConfig;
24
25/// Unified client over SMB / FTP / SFTP.
26///
27/// Holds a type-erased backend (`Box<dyn RemoteFileSystem>`), so trait methods
28/// forward in one line — no per-protocol `match` boilerplate.
29///
30/// Backends are gated by Cargo features: `smb`, `ftp`, `sftp`.
31pub struct Client {
32    protocol: Protocol,
33    inner: Box<dyn RemoteFileSystem>,
34}
35
36impl Client {
37    #[cfg_attr(
38        not(any(feature = "smb", feature = "ftp", feature = "sftp")),
39        allow(dead_code)
40    )]
41    fn new(protocol: Protocol, inner: Box<dyn RemoteFileSystem>) -> Self {
42        Self { protocol, inner }
43    }
44
45    /// Create a client from a generic config and connect.
46    pub async fn connect_with(config: &Config) -> Result<Self> {
47        #[cfg(not(any(feature = "smb", feature = "ftp", feature = "sftp")))]
48        {
49            let _ = config;
50            return Err(Error::FeatureDisabled(
51                "enable at least one of: smb, ftp, sftp",
52            ));
53        }
54
55        #[cfg(any(feature = "smb", feature = "ftp", feature = "sftp"))]
56        {
57            let (protocol, mut inner): (Protocol, Box<dyn RemoteFileSystem>) = match config {
58                #[cfg(feature = "smb")]
59                Config::Smb(cfg) => (Protocol::Smb, Box::new(SmbClient::new(cfg.clone()))),
60                #[cfg(feature = "ftp")]
61                Config::Ftp(cfg) => (Protocol::Ftp, Box::new(FtpClient::new(cfg.clone()))),
62                #[cfg(feature = "sftp")]
63                Config::Sftp(cfg) => (Protocol::Sftp, Box::new(SftpClient::new(cfg.clone()))),
64            };
65            inner.connect().await?;
66            Ok(Self::new(protocol, inner))
67        }
68    }
69
70    /// Create an SMB client and connect.
71    #[cfg(feature = "smb")]
72    pub async fn smb(config: &SmbConfig) -> Result<Self> {
73        Self::connect_with(&Config::Smb(config.clone())).await
74    }
75
76    /// Create an FTP client and connect.
77    #[cfg(feature = "ftp")]
78    pub async fn ftp(config: &FtpConfig) -> Result<Self> {
79        Self::connect_with(&Config::Ftp(config.clone())).await
80    }
81
82    /// Create an SFTP client and connect.
83    #[cfg(feature = "sftp")]
84    pub async fn sftp(config: &SftpConfig) -> Result<Self> {
85        Self::connect_with(&Config::Sftp(config.clone())).await
86    }
87
88    /// Active protocol of this client.
89    pub fn protocol(&self) -> Protocol {
90        self.protocol
91    }
92
93    /// Borrow the underlying filesystem trait object.
94    pub fn as_fs(&self) -> &dyn RemoteFileSystem {
95        &*self.inner
96    }
97
98    /// Mutably borrow the underlying filesystem trait object.
99    pub fn as_fs_mut(&mut self) -> &mut dyn RemoteFileSystem {
100        &mut *self.inner
101    }
102}
103
104impl Deref for Client {
105    type Target = dyn RemoteFileSystem;
106
107    fn deref(&self) -> &Self::Target {
108        &*self.inner
109    }
110}
111
112impl DerefMut for Client {
113    fn deref_mut(&mut self) -> &mut Self::Target {
114        &mut *self.inner
115    }
116}
117
118#[async_trait]
119impl RemoteFileSystem for Client {
120    async fn connect(&mut self) -> Result<()> {
121        self.inner.connect().await
122    }
123
124    async fn disconnect(&mut self) -> Result<()> {
125        self.inner.disconnect().await
126    }
127
128    fn is_connected(&self) -> bool {
129        self.inner.is_connected()
130    }
131
132    async fn list(&mut self, path: &str) -> Result<Vec<FileEntry>> {
133        self.inner.list(path).await
134    }
135
136    async fn metadata(&mut self, path: &str) -> Result<FileMeta> {
137        self.inner.metadata(path).await
138    }
139
140    async fn exists(&mut self, path: &str) -> Result<bool> {
141        self.inner.exists(path).await
142    }
143
144    async fn mkdir(&mut self, path: &str) -> Result<()> {
145        self.inner.mkdir(path).await
146    }
147
148    async fn mkdir_all(&mut self, path: &str) -> Result<()> {
149        self.inner.mkdir_all(path).await
150    }
151
152    async fn rmdir(&mut self, path: &str) -> Result<()> {
153        self.inner.rmdir(path).await
154    }
155
156    async fn remove_file(&mut self, path: &str) -> Result<()> {
157        self.inner.remove_file(path).await
158    }
159
160    async fn remove(&mut self, path: &str) -> Result<()> {
161        self.inner.remove(path).await
162    }
163
164    async fn rename(&mut self, from: &str, to: &str) -> Result<()> {
165        self.inner.rename(from, to).await
166    }
167
168    async fn read(&mut self, path: &str) -> Result<Vec<u8>> {
169        self.inner.read(path).await
170    }
171
172    async fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
173        self.inner.write(path, data).await
174    }
175
176    async fn download(&mut self, remote: &str, local: &Path) -> Result<()> {
177        self.inner.download(remote, local).await
178    }
179
180    async fn upload(&mut self, local: &Path, remote: &str) -> Result<()> {
181        self.inner.upload(local, remote).await
182    }
183}
184
185/// Builder for constructing a [`Client`].
186#[derive(Debug, Default)]
187pub struct ClientBuilder {
188    config: Option<Config>,
189}
190
191impl ClientBuilder {
192    pub fn new() -> Self {
193        Self::default()
194    }
195
196    #[cfg(feature = "smb")]
197    pub fn smb(mut self, config: SmbConfig) -> Self {
198        self.config = Some(Config::Smb(config));
199        self
200    }
201
202    #[cfg(feature = "ftp")]
203    pub fn ftp(mut self, config: FtpConfig) -> Self {
204        self.config = Some(Config::Ftp(config));
205        self
206    }
207
208    #[cfg(feature = "sftp")]
209    pub fn sftp(mut self, config: SftpConfig) -> Self {
210        self.config = Some(Config::Sftp(config));
211        self
212    }
213
214    pub fn config(mut self, config: Config) -> Self {
215        self.config = Some(config);
216        self
217    }
218
219    pub async fn build(self) -> Result<Client> {
220        let config = self
221            .config
222            .ok_or_else(|| Error::Other("missing connection config".into()))?;
223        Client::connect_with(&config).await
224    }
225}