wezterm_ssh/sftp/
mod.rs

1use super::{SessionRequest, SessionSender};
2use crate::sftp::dir::{Dir, DirRequest};
3use crate::sftp::file::{File, FileRequest};
4use crate::sftp::types::{Metadata, OpenFileType, OpenOptions, RenameOptions, WriteMode};
5use camino::Utf8PathBuf;
6use error::SftpError;
7use smol::channel::{bounded, RecvError, Sender};
8use std::convert::TryInto;
9use std::io;
10use thiserror::Error;
11
12pub(crate) mod dir;
13pub(crate) mod error;
14pub(crate) mod file;
15pub(crate) mod types;
16
17fn into_invalid_data<E>(err: E) -> io::Error
18where
19    E: Into<Box<dyn std::error::Error + Send + Sync>>,
20{
21    io::Error::new(io::ErrorKind::InvalidData, err)
22}
23
24/// Represents the result of some SFTP channel operation
25pub type SftpChannelResult<T> = Result<T, SftpChannelError>;
26
27/// Represents an error that can occur when working with the SFTP channel
28#[derive(Debug, Error)]
29pub enum SftpChannelError {
30    #[error(transparent)]
31    Sftp(#[from] SftpError),
32
33    #[error("File IO failed: {}", .0)]
34    FileIo(#[from] std::io::Error),
35
36    #[error("Failed to send request: {}", .0)]
37    SendFailed(#[from] anyhow::Error),
38
39    #[error("Failed to receive response: {}", .0)]
40    RecvFailed(#[from] RecvError),
41
42    #[cfg(feature = "ssh2")]
43    #[error("Library-specific error: {}", .0)]
44    Ssh2(#[source] ssh2::Error),
45
46    #[cfg(feature = "libssh-rs")]
47    #[error("Library-specific error: {}", .0)]
48    LibSsh(#[source] libssh_rs::Error),
49
50    #[error("Not Implemented")]
51    NotImplemented,
52}
53
54/// Represents an open sftp channel for performing filesystem operations
55#[derive(Clone, Debug)]
56pub struct Sftp {
57    pub(crate) tx: SessionSender,
58}
59
60impl Sftp {
61    /// Open a handle to a file.
62    pub async fn open_with_mode<T, E>(
63        &self,
64        filename: T,
65        opts: OpenOptions,
66    ) -> SftpChannelResult<File>
67    where
68        T: TryInto<Utf8PathBuf, Error = E>,
69        E: Into<Box<dyn std::error::Error + Send + Sync>>,
70    {
71        let (reply, rx) = bounded(1);
72
73        self.tx
74            .send(SessionRequest::Sftp(SftpRequest::OpenWithMode(
75                OpenWithMode {
76                    filename: filename.try_into().map_err(into_invalid_data)?,
77                    opts,
78                },
79                reply,
80            )))
81            .await?;
82        let mut result = rx.recv().await??;
83        result.initialize_sender(self.tx.clone());
84        Ok(result)
85    }
86
87    /// Helper to open a file in the `Read` mode.
88    pub async fn open<T, E>(&self, filename: T) -> SftpChannelResult<File>
89    where
90        T: TryInto<Utf8PathBuf, Error = E>,
91        E: Into<Box<dyn std::error::Error + Send + Sync>>,
92    {
93        self.open_with_mode(
94            filename,
95            OpenOptions {
96                read: true,
97                write: None,
98                mode: 0,
99                ty: OpenFileType::File,
100            },
101        )
102        .await
103    }
104
105    /// Helper to create a file in write-only mode with truncation.
106    pub async fn create<T, E>(&self, filename: T) -> SftpChannelResult<File>
107    where
108        T: TryInto<Utf8PathBuf, Error = E>,
109        E: Into<Box<dyn std::error::Error + Send + Sync>>,
110    {
111        self.open_with_mode(
112            filename,
113            OpenOptions {
114                read: false,
115                write: Some(WriteMode::Write),
116                mode: 0o666,
117                ty: OpenFileType::File,
118            },
119        )
120        .await
121    }
122
123    /// Helper to open a directory for reading its contents.
124    pub async fn open_dir<T, E>(&self, filename: T) -> SftpChannelResult<Dir>
125    where
126        T: TryInto<Utf8PathBuf, Error = E>,
127        E: Into<Box<dyn std::error::Error + Send + Sync>>,
128    {
129        let (reply, rx) = bounded(1);
130        self.tx
131            .send(SessionRequest::Sftp(SftpRequest::OpenDir(
132                filename.try_into().map_err(into_invalid_data)?,
133                reply,
134            )))
135            .await?;
136        let mut result = rx.recv().await??;
137        result.initialize_sender(self.tx.clone());
138        Ok(result)
139    }
140
141    /// Convenience function to read the files in a directory.
142    ///
143    /// The returned paths are all joined with dirname when returned, and the paths . and .. are
144    /// filtered out of the returned list.
145    pub async fn read_dir<T, E>(
146        &self,
147        filename: T,
148    ) -> SftpChannelResult<Vec<(Utf8PathBuf, Metadata)>>
149    where
150        T: TryInto<Utf8PathBuf, Error = E>,
151        E: Into<Box<dyn std::error::Error + Send + Sync>>,
152    {
153        let (reply, rx) = bounded(1);
154        self.tx
155            .send(SessionRequest::Sftp(SftpRequest::ReadDir(
156                filename.try_into().map_err(into_invalid_data)?,
157                reply,
158            )))
159            .await?;
160        let result = rx.recv().await??;
161        Ok(result)
162    }
163
164    /// Create a directory on the remote filesystem.
165    pub async fn create_dir<T, E>(&self, filename: T, mode: i32) -> SftpChannelResult<()>
166    where
167        T: TryInto<Utf8PathBuf, Error = E>,
168        E: Into<Box<dyn std::error::Error + Send + Sync>>,
169    {
170        let (reply, rx) = bounded(1);
171        self.tx
172            .send(SessionRequest::Sftp(SftpRequest::CreateDir(
173                CreateDir {
174                    filename: filename.try_into().map_err(into_invalid_data)?,
175                    mode,
176                },
177                reply,
178            )))
179            .await?;
180        let result = rx.recv().await??;
181        Ok(result)
182    }
183
184    /// Remove a directory from the remote filesystem.
185    pub async fn remove_dir<T, E>(&self, filename: T) -> SftpChannelResult<()>
186    where
187        T: TryInto<Utf8PathBuf, Error = E>,
188        E: Into<Box<dyn std::error::Error + Send + Sync>>,
189    {
190        let (reply, rx) = bounded(1);
191        self.tx
192            .send(SessionRequest::Sftp(SftpRequest::RemoveDir(
193                filename.try_into().map_err(into_invalid_data)?,
194                reply,
195            )))
196            .await?;
197        let result = rx.recv().await??;
198        Ok(result)
199    }
200
201    /// Get the metadata for a file, performed by stat(2).
202    pub async fn metadata<T, E>(&self, filename: T) -> SftpChannelResult<Metadata>
203    where
204        T: TryInto<Utf8PathBuf, Error = E>,
205        E: Into<Box<dyn std::error::Error + Send + Sync>>,
206    {
207        let (reply, rx) = bounded(1);
208        self.tx
209            .send(SessionRequest::Sftp(SftpRequest::Metadata(
210                filename.try_into().map_err(into_invalid_data)?,
211                reply,
212            )))
213            .await?;
214        let result = rx.recv().await??;
215        Ok(result)
216    }
217
218    /// Get the metadata for a file, performed by lstat(2).
219    pub async fn symlink_metadata<T, E>(&self, filename: T) -> SftpChannelResult<Metadata>
220    where
221        T: TryInto<Utf8PathBuf, Error = E>,
222        E: Into<Box<dyn std::error::Error + Send + Sync>>,
223    {
224        let (reply, rx) = bounded(1);
225        self.tx
226            .send(SessionRequest::Sftp(SftpRequest::SymlinkMetadata(
227                filename.try_into().map_err(into_invalid_data)?,
228                reply,
229            )))
230            .await?;
231        let result = rx.recv().await??;
232        Ok(result)
233    }
234
235    /// Set the metadata for a file.
236    pub async fn set_metadata<T, E>(&self, filename: T, metadata: Metadata) -> SftpChannelResult<()>
237    where
238        T: TryInto<Utf8PathBuf, Error = E>,
239        E: Into<Box<dyn std::error::Error + Send + Sync>>,
240    {
241        let (reply, rx) = bounded(1);
242        self.tx
243            .send(SessionRequest::Sftp(SftpRequest::SetMetadata(
244                SetMetadata {
245                    filename: filename.try_into().map_err(into_invalid_data)?,
246                    metadata,
247                },
248                reply,
249            )))
250            .await?;
251        let result = rx.recv().await??;
252        Ok(result)
253    }
254
255    /// Create symlink at `target` pointing at `path`.
256    pub async fn symlink<T1, T2, E1, E2>(&self, path: T1, target: T2) -> SftpChannelResult<()>
257    where
258        T1: TryInto<Utf8PathBuf, Error = E1>,
259        T2: TryInto<Utf8PathBuf, Error = E2>,
260        E1: Into<Box<dyn std::error::Error + Send + Sync>>,
261        E2: Into<Box<dyn std::error::Error + Send + Sync>>,
262    {
263        let (reply, rx) = bounded(1);
264        self.tx
265            .send(SessionRequest::Sftp(SftpRequest::Symlink(
266                Symlink {
267                    path: path.try_into().map_err(into_invalid_data)?,
268                    target: target.try_into().map_err(into_invalid_data)?,
269                },
270                reply,
271            )))
272            .await?;
273        let result = rx.recv().await??;
274        Ok(result)
275    }
276
277    /// Read a symlink at `path`.
278    pub async fn read_link<T, E>(&self, path: T) -> SftpChannelResult<Utf8PathBuf>
279    where
280        T: TryInto<Utf8PathBuf, Error = E>,
281        E: Into<Box<dyn std::error::Error + Send + Sync>>,
282    {
283        let (reply, rx) = bounded(1);
284        self.tx
285            .send(SessionRequest::Sftp(SftpRequest::ReadLink(
286                path.try_into().map_err(into_invalid_data)?,
287                reply,
288            )))
289            .await?;
290        let result = rx.recv().await??;
291        Ok(result)
292    }
293
294    /// Resolve the real path for `path`.
295    pub async fn canonicalize<T, E>(&self, path: T) -> SftpChannelResult<Utf8PathBuf>
296    where
297        T: TryInto<Utf8PathBuf, Error = E>,
298        E: Into<Box<dyn std::error::Error + Send + Sync>>,
299    {
300        let (reply, rx) = bounded(1);
301        self.tx
302            .send(SessionRequest::Sftp(SftpRequest::Canonicalize(
303                path.try_into().map_err(into_invalid_data)?,
304                reply,
305            )))
306            .await?;
307        let result = rx.recv().await??;
308        Ok(result)
309    }
310
311    /// Rename the filesystem object on the remote filesystem.
312    pub async fn rename<T1, T2, E1, E2>(
313        &self,
314        src: T1,
315        dst: T2,
316        opts: RenameOptions,
317    ) -> SftpChannelResult<()>
318    where
319        T1: TryInto<Utf8PathBuf, Error = E1>,
320        T2: TryInto<Utf8PathBuf, Error = E2>,
321        E1: Into<Box<dyn std::error::Error + Send + Sync>>,
322        E2: Into<Box<dyn std::error::Error + Send + Sync>>,
323    {
324        let (reply, rx) = bounded(1);
325        self.tx
326            .send(SessionRequest::Sftp(SftpRequest::Rename(
327                Rename {
328                    src: src.try_into().map_err(into_invalid_data)?,
329                    dst: dst.try_into().map_err(into_invalid_data)?,
330                    opts,
331                },
332                reply,
333            )))
334            .await?;
335        let result = rx.recv().await??;
336        Ok(result)
337    }
338
339    /// Remove a file on the remote filesystem.
340    pub async fn remove_file<T, E>(&self, file: T) -> SftpChannelResult<()>
341    where
342        T: TryInto<Utf8PathBuf, Error = E>,
343        E: Into<Box<dyn std::error::Error + Send + Sync>>,
344    {
345        let (reply, rx) = bounded(1);
346        self.tx
347            .send(SessionRequest::Sftp(SftpRequest::RemoveFile(
348                file.try_into().map_err(into_invalid_data)?,
349                reply,
350            )))
351            .await?;
352        let result = rx.recv().await??;
353        Ok(result)
354    }
355}
356
357#[derive(Debug)]
358pub(crate) enum SftpRequest {
359    OpenWithMode(OpenWithMode, Sender<SftpChannelResult<File>>),
360    OpenDir(Utf8PathBuf, Sender<SftpChannelResult<Dir>>),
361    ReadDir(
362        Utf8PathBuf,
363        Sender<SftpChannelResult<Vec<(Utf8PathBuf, Metadata)>>>,
364    ),
365    CreateDir(CreateDir, Sender<SftpChannelResult<()>>),
366    RemoveDir(Utf8PathBuf, Sender<SftpChannelResult<()>>),
367    Metadata(Utf8PathBuf, Sender<SftpChannelResult<Metadata>>),
368    SymlinkMetadata(Utf8PathBuf, Sender<SftpChannelResult<Metadata>>),
369    SetMetadata(SetMetadata, Sender<SftpChannelResult<()>>),
370    Symlink(Symlink, Sender<SftpChannelResult<()>>),
371    ReadLink(Utf8PathBuf, Sender<SftpChannelResult<Utf8PathBuf>>),
372    Canonicalize(Utf8PathBuf, Sender<SftpChannelResult<Utf8PathBuf>>),
373    Rename(Rename, Sender<SftpChannelResult<()>>),
374    RemoveFile(Utf8PathBuf, Sender<SftpChannelResult<()>>),
375
376    /// Specialized type for file-based operations
377    File(FileRequest),
378    Dir(DirRequest),
379}
380
381#[derive(Debug)]
382pub(crate) struct OpenWithMode {
383    pub filename: Utf8PathBuf,
384    pub opts: OpenOptions,
385}
386
387#[derive(Debug)]
388pub(crate) struct CreateDir {
389    pub filename: Utf8PathBuf,
390    pub mode: i32,
391}
392
393#[derive(Debug)]
394pub(crate) struct SetMetadata {
395    pub filename: Utf8PathBuf,
396    pub metadata: Metadata,
397}
398
399#[derive(Debug)]
400pub(crate) struct Symlink {
401    pub path: Utf8PathBuf,
402    pub target: Utf8PathBuf,
403}
404
405#[derive(Debug)]
406pub(crate) struct Rename {
407    pub src: Utf8PathBuf,
408    pub dst: Utf8PathBuf,
409    pub opts: RenameOptions,
410}
411
412#[cfg(feature = "ssh2")]
413impl From<ssh2::Error> for SftpChannelError {
414    fn from(err: ssh2::Error) -> Self {
415        use std::convert::TryFrom;
416        match SftpError::try_from(err) {
417            Ok(x) => Self::Sftp(x),
418            Err(x) => Self::Ssh2(x),
419        }
420    }
421}
422
423#[cfg(feature = "libssh-rs")]
424impl From<libssh_rs::Error> for SftpChannelError {
425    fn from(err: libssh_rs::Error) -> Self {
426        Self::LibSsh(err)
427    }
428}