openssh_sftp_client/fs/
mod.rs

1use crate::{
2    file::OpenOptions,
3    lowlevel::{self, Extensions},
4    metadata::{MetaData, MetaDataBuilder, Permissions},
5    Auxiliary, Buffer, Error, Id, OwnedHandle, WriteEnd, WriteEndWithCachedId,
6};
7
8use std::{
9    borrow::Cow,
10    cmp::min,
11    convert::TryInto,
12    path::{Path, PathBuf},
13};
14
15use bytes::BytesMut;
16
17mod dir;
18pub use dir::{DirEntry, ReadDir};
19
20type AwaitableStatus = lowlevel::AwaitableStatus<Buffer>;
21type AwaitableAttrs = lowlevel::AwaitableAttrs<Buffer>;
22type SendLinkingRequest =
23    fn(&mut WriteEnd, Id, Cow<'_, Path>, Cow<'_, Path>) -> Result<AwaitableStatus, Error>;
24
25type SendRmRequest = fn(&mut WriteEnd, Id, Cow<'_, Path>) -> Result<AwaitableStatus, Error>;
26type SendMetadataRequest = fn(&mut WriteEnd, Id, Cow<'_, Path>) -> Result<AwaitableAttrs, Error>;
27
28/// A struct used to perform operations on remote filesystem.
29#[derive(Debug, Clone)]
30pub struct Fs {
31    write_end: WriteEndWithCachedId,
32    cwd: Box<Path>,
33}
34
35impl Fs {
36    pub(super) fn new(write_end: WriteEndWithCachedId, cwd: PathBuf) -> Self {
37        Self {
38            write_end,
39            cwd: cwd.into_boxed_path(),
40        }
41    }
42
43    fn get_auxiliary(&self) -> &Auxiliary {
44        self.write_end.get_auxiliary()
45    }
46
47    /// Return current working dir.
48    pub fn cwd(&self) -> &Path {
49        &self.cwd
50    }
51
52    /// Set current working dir.
53    ///
54    /// * `cwd` - Can include `~`.
55    ///           If it is empty, then it is set to use the default
56    ///           directory set by the remote `sftp-server`.
57    pub fn set_cwd(&mut self, cwd: impl Into<PathBuf>) {
58        self.cwd = cwd.into().into_boxed_path();
59    }
60
61    fn concat_path_if_needed<'path>(&self, path: &'path Path) -> Cow<'path, Path> {
62        if path.is_absolute() || self.cwd.as_os_str().is_empty() {
63            Cow::Borrowed(path)
64        } else {
65            Cow::Owned(self.cwd.join(path))
66        }
67    }
68}
69
70impl Fs {
71    /// Open a remote dir
72    pub async fn open_dir(&mut self, path: impl AsRef<Path>) -> Result<Dir, Error> {
73        async fn inner(this: &mut Fs, path: &Path) -> Result<Dir, Error> {
74            let path = this.concat_path_if_needed(path);
75
76            this.write_end
77                .send_request(|write_end, id| Ok(write_end.send_opendir_request(id, path)?.wait()))
78                .await
79                .map(|handle| Dir(OwnedHandle::new(this.write_end.clone(), handle)))
80        }
81
82        inner(self, path.as_ref()).await
83    }
84
85    /// Create a directory builder.
86    pub fn dir_builder(&mut self) -> DirBuilder<'_> {
87        DirBuilder {
88            fs: self,
89            metadata_builder: MetaDataBuilder::new(),
90        }
91    }
92
93    /// Creates a new, empty directory at the provided path.
94    pub async fn create_dir(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
95        async fn inner(this: &mut Fs, path: &Path) -> Result<(), Error> {
96            this.dir_builder().create(path).await
97        }
98
99        inner(self, path.as_ref()).await
100    }
101
102    async fn remove_impl(&mut self, path: &Path, f: SendRmRequest) -> Result<(), Error> {
103        let path = self.concat_path_if_needed(path);
104
105        self.write_end
106            .send_request(|write_end, id| Ok(f(write_end, id, path)?.wait()))
107            .await
108    }
109
110    /// Removes an existing, empty directory.
111    pub async fn remove_dir(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
112        self.remove_impl(path.as_ref(), WriteEnd::send_rmdir_request)
113            .await
114    }
115
116    /// Removes a file from remote filesystem.
117    pub async fn remove_file(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
118        self.remove_impl(path.as_ref(), WriteEnd::send_remove_request)
119            .await
120    }
121
122    /// Returns the canonical, absolute form of a path with all intermediate
123    /// components normalized and symbolic links resolved.
124    ///
125    /// If the remote server supports the `expand-path` extension, then this
126    /// method will also expand tilde characters (“~”) in the path. You can
127    /// check it with [`Sftp::support_expand_path`](crate::sftp::Sftp::support_expand_path).
128    pub async fn canonicalize(&mut self, path: impl AsRef<Path>) -> Result<PathBuf, Error> {
129        async fn inner(this: &mut Fs, path: &Path) -> Result<PathBuf, Error> {
130            let path = this.concat_path_if_needed(path);
131
132            let f = if this
133                .get_auxiliary()
134                .extensions()
135                .contains(Extensions::EXPAND_PATH)
136            {
137                // This supports canonicalisation of relative paths and those that
138                // need tilde-expansion, i.e. “~”, “~/…” and “~user/…”.
139                //
140                // These paths are expanded using shell-like rules and the resultant
141                // path is canonicalised similarly to WriteEnd::send_realpath_request.
142                WriteEnd::send_expand_path_request
143            } else {
144                WriteEnd::send_realpath_request
145            };
146
147            this.write_end
148                .send_request(|write_end, id| Ok(f(write_end, id, path)?.wait()))
149                .await
150                .map(Into::into)
151        }
152
153        inner(self, path.as_ref()).await
154    }
155
156    async fn linking_impl(
157        &mut self,
158        src: &Path,
159        dst: &Path,
160        f: SendLinkingRequest,
161    ) -> Result<(), Error> {
162        let src = self.concat_path_if_needed(src);
163        let dst = self.concat_path_if_needed(dst);
164
165        self.write_end
166            .send_request(|write_end, id| Ok(f(write_end, id, src, dst)?.wait()))
167            .await
168    }
169
170    /// Creates a new hard link on the remote filesystem.
171    ///
172    /// # Precondition
173    ///
174    /// Require extension `hardlink`
175    ///
176    /// You can check it with [`Sftp::support_hardlink`](crate::sftp::Sftp::support_hardlink).
177    pub async fn hard_link(
178        &mut self,
179        src: impl AsRef<Path>,
180        dst: impl AsRef<Path>,
181    ) -> Result<(), Error> {
182        async fn inner(this: &mut Fs, src: &Path, dst: &Path) -> Result<(), Error> {
183            if !this
184                .get_auxiliary()
185                .extensions()
186                .contains(Extensions::HARDLINK)
187            {
188                return Err(Error::UnsupportedExtension(&"hardlink"));
189            }
190
191            this.linking_impl(src, dst, WriteEnd::send_hardlink_request)
192                .await
193        }
194
195        inner(self, src.as_ref(), dst.as_ref()).await
196    }
197
198    /// Creates a new symlink on the remote filesystem.
199    pub async fn symlink(
200        &mut self,
201        src: impl AsRef<Path>,
202        dst: impl AsRef<Path>,
203    ) -> Result<(), Error> {
204        self.linking_impl(src.as_ref(), dst.as_ref(), WriteEnd::send_symlink_request)
205            .await
206    }
207
208    /// Renames a file or directory to a new name, replacing the original file if to already exists.
209    ///
210    /// If the server supports the `posix-rename` extension, it will be used.
211    /// You can check it with [`Sftp::support_posix_rename`](crate::sftp::Sftp::support_posix_rename).
212    ///
213    /// This will not work if the new name is on a different mount point.
214    pub async fn rename(
215        &mut self,
216        from: impl AsRef<Path>,
217        to: impl AsRef<Path>,
218    ) -> Result<(), Error> {
219        async fn inner(this: &mut Fs, from: &Path, to: &Path) -> Result<(), Error> {
220            let f = if this
221                .get_auxiliary()
222                .extensions()
223                .contains(Extensions::POSIX_RENAME)
224            {
225                // posix rename is guaranteed to be atomic
226                WriteEnd::send_posix_rename_request
227            } else {
228                WriteEnd::send_rename_request
229            };
230
231            this.linking_impl(from, to, f).await
232        }
233
234        inner(self, from.as_ref(), to.as_ref()).await
235    }
236
237    /// Reads a symbolic link, returning the file that the link points to.
238    pub async fn read_link(&mut self, path: impl AsRef<Path>) -> Result<PathBuf, Error> {
239        async fn inner(this: &mut Fs, path: &Path) -> Result<PathBuf, Error> {
240            let path = this.concat_path_if_needed(path);
241
242            this.write_end
243                .send_request(|write_end, id| Ok(write_end.send_readlink_request(id, path)?.wait()))
244                .await
245                .map(Into::into)
246        }
247
248        inner(self, path.as_ref()).await
249    }
250
251    async fn set_metadata_impl(&mut self, path: &Path, metadata: MetaData) -> Result<(), Error> {
252        let path = self.concat_path_if_needed(path);
253
254        self.write_end
255            .send_request(|write_end, id| {
256                Ok(write_end
257                    .send_setstat_request(id, path, metadata.into_inner())?
258                    .wait())
259            })
260            .await
261    }
262
263    /// Change the metadata of a file or a directory.
264    pub async fn set_metadata(
265        &mut self,
266        path: impl AsRef<Path>,
267        metadata: MetaData,
268    ) -> Result<(), Error> {
269        self.set_metadata_impl(path.as_ref(), metadata).await
270    }
271
272    /// Changes the permissions found on a file or a directory.
273    pub async fn set_permissions(
274        &mut self,
275        path: impl AsRef<Path>,
276        perm: Permissions,
277    ) -> Result<(), Error> {
278        async fn inner(this: &mut Fs, path: &Path, perm: Permissions) -> Result<(), Error> {
279            this.set_metadata_impl(path, MetaDataBuilder::new().permissions(perm).create())
280                .await
281        }
282
283        inner(self, path.as_ref(), perm).await
284    }
285
286    async fn metadata_impl(
287        &mut self,
288        path: &Path,
289        f: SendMetadataRequest,
290    ) -> Result<MetaData, Error> {
291        let path = self.concat_path_if_needed(path);
292
293        self.write_end
294            .send_request(|write_end, id| Ok(f(write_end, id, path)?.wait()))
295            .await
296            .map(MetaData::new)
297    }
298
299    /// Given a path, queries the file system to get information about a file,
300    /// directory, etc.
301    pub async fn metadata(&mut self, path: impl AsRef<Path>) -> Result<MetaData, Error> {
302        self.metadata_impl(path.as_ref(), WriteEnd::send_stat_request)
303            .await
304    }
305
306    /// Queries the file system metadata for a path.
307    pub async fn symlink_metadata(&mut self, path: impl AsRef<Path>) -> Result<MetaData, Error> {
308        self.metadata_impl(path.as_ref(), WriteEnd::send_lstat_request)
309            .await
310    }
311
312    /// Reads the entire contents of a file into a bytes.
313    pub async fn read(&mut self, path: impl AsRef<Path>) -> Result<BytesMut, Error> {
314        async fn inner(this: &mut Fs, path: &Path) -> Result<BytesMut, Error> {
315            let path = this.concat_path_if_needed(path);
316
317            let mut file = OpenOptions::open_inner(
318                lowlevel::OpenOptions::new().read(true),
319                false,
320                false,
321                false,
322                path.as_ref(),
323                this.write_end.clone(),
324            )
325            .await?;
326            let max_read_len = file.max_read_len_impl();
327
328            let cap_to_reserve: usize = if let Some(len) = file.metadata().await?.len() {
329                // To detect EOF, we need to a little bit more then the length
330                // of the file.
331                len.saturating_add(300)
332                    .try_into()
333                    .unwrap_or(max_read_len as usize)
334            } else {
335                max_read_len as usize
336            };
337
338            let mut buffer = BytesMut::with_capacity(cap_to_reserve);
339
340            loop {
341                let cnt = buffer.len();
342
343                let n: u32 = if cnt <= cap_to_reserve {
344                    // To detect EOF, we need to a little bit more then the
345                    // length of the file.
346                    (cap_to_reserve - cnt)
347                        .saturating_add(300)
348                        .try_into()
349                        .map(|n| min(n, max_read_len))
350                        .unwrap_or(max_read_len)
351                } else {
352                    max_read_len
353                };
354                buffer.reserve(n.try_into().unwrap_or(usize::MAX));
355
356                if let Some(bytes) = file.read(n, buffer.split_off(cnt)).await? {
357                    buffer.unsplit(bytes);
358                } else {
359                    // Eof
360                    break Ok(buffer);
361                }
362            }
363        }
364
365        inner(self, path.as_ref()).await
366    }
367
368    /// Open/Create a file for writing and write the entire `contents` into it.
369    pub async fn write(
370        &mut self,
371        path: impl AsRef<Path>,
372        content: impl AsRef<[u8]>,
373    ) -> Result<(), Error> {
374        async fn inner(this: &mut Fs, path: &Path, content: &[u8]) -> Result<(), Error> {
375            let path = this.concat_path_if_needed(path);
376
377            OpenOptions::open_inner(
378                lowlevel::OpenOptions::new().write(true),
379                true,
380                true,
381                false,
382                path.as_ref(),
383                this.write_end.clone(),
384            )
385            .await?
386            .write_all(content)
387            .await
388        }
389
390        inner(self, path.as_ref(), content.as_ref()).await
391    }
392}
393
394/// Remote Directory
395#[repr(transparent)]
396#[derive(Debug, Clone)]
397pub struct Dir(OwnedHandle);
398
399impl Dir {
400    /// Read dir.
401    pub fn read_dir(self) -> ReadDir {
402        ReadDir::new(self)
403    }
404
405    /// Close dir.
406    pub async fn close(self) -> Result<(), Error> {
407        self.0.close().await
408    }
409}
410
411/// Builder for new directory to create.
412#[derive(Debug)]
413pub struct DirBuilder<'a> {
414    fs: &'a mut Fs,
415    metadata_builder: MetaDataBuilder,
416}
417
418impl DirBuilder<'_> {
419    /// Reset builder back to default.
420    pub fn reset(&mut self) -> &mut Self {
421        self.metadata_builder = MetaDataBuilder::new();
422        self
423    }
424
425    /// Set id of the dir to be built.
426    pub fn id(&mut self, (uid, gid): (u32, u32)) -> &mut Self {
427        self.metadata_builder.id((uid, gid));
428        self
429    }
430
431    /// Set permissions of the dir to be built.
432    pub fn permissions(&mut self, perm: Permissions) -> &mut Self {
433        self.metadata_builder.permissions(perm);
434        self
435    }
436}
437
438impl DirBuilder<'_> {
439    /// Creates the specified directory with the configured options.
440    pub async fn create(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
441        async fn inner(this: &mut DirBuilder<'_>, path: &Path) -> Result<(), Error> {
442            let fs = &mut this.fs;
443
444            let path = fs.concat_path_if_needed(path);
445            let attrs = this.metadata_builder.create().into_inner();
446
447            fs.write_end
448                .send_request(|write_end, id| {
449                    Ok(write_end.send_mkdir_request(id, path, attrs)?.wait())
450                })
451                .await
452        }
453
454        inner(self, path.as_ref()).await
455    }
456}