libunftp/storage/
storage_backend.rs

1//! Defines the service provider interface for storage back-end implementors.
2
3use super::error::Error;
4use crate::auth::UserDetail;
5use crate::storage::ErrorKind;
6use async_trait::async_trait;
7use chrono::{
8    Datelike,
9    prelude::{DateTime, Utc},
10};
11use md5::{Digest, Md5};
12use std::{
13    fmt::{self, Debug, Formatter, Write},
14    io,
15    path::Path,
16    result,
17    time::SystemTime,
18};
19use tokio::io::AsyncReadExt;
20
21/// Tells if STOR/RETR restarts are supported by the storage back-end
22/// i.e. starting from a different byte offset.
23pub const FEATURE_RESTART: u32 = 0b0000_0001;
24/// Whether or not this storage backend supports the SITE MD5 command
25pub const FEATURE_SITEMD5: u32 = 0b0000_0010;
26
27/// Result type used by traits in this module
28pub type Result<T> = result::Result<T, Error>;
29
30/// Represents the metadata of a _FTP File_
31pub trait Metadata {
32    /// Returns the length (size) of the file in bytes.
33    fn len(&self) -> u64;
34
35    /// Returns `self.len() == 0`.
36    fn is_empty(&self) -> bool {
37        self.len() == 0
38    }
39
40    /// Returns true if the path is a directory.
41    fn is_dir(&self) -> bool;
42
43    /// Returns true if the path is a file.
44    fn is_file(&self) -> bool;
45
46    /// Returns true if the path is a symbolic link.
47    fn is_symlink(&self) -> bool;
48
49    /// Returns the last modified time of the path.
50    fn modified(&self) -> Result<SystemTime>;
51
52    /// Returns the `gid` of the file.
53    fn gid(&self) -> u32;
54
55    /// Returns the `uid` of the file.
56    fn uid(&self) -> u32;
57
58    /// Returns the number of links to the file. The default implementation always returns `1`
59    fn links(&self) -> u64 {
60        1
61    }
62
63    /// Returns the `permissions` of the file. The default implementation assumes unix permissions
64    /// and defaults to "rwxr-xr-x" (octal 7755)
65    fn permissions(&self) -> Permissions {
66        Permissions(0o7755)
67    }
68
69    /// If this is a symlink, return the path to its target
70    fn readlink(&self) -> Option<&Path> {
71        None
72    }
73}
74
75/// Represents the permissions of a _FTP File_
76pub struct Permissions(pub u32);
77
78const PERM_READ: u32 = 0b100100100;
79const PERM_WRITE: u32 = 0b010010010;
80const PERM_EXEC: u32 = 0b001001001;
81const PERM_USER: u32 = 0b111000000;
82const PERM_GROUP: u32 = 0b000111000;
83const PERM_OTHERS: u32 = 0b000000111;
84
85impl std::fmt::Display for Permissions {
86    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
87        f.write_char(if self.0 & PERM_USER & PERM_READ > 0 { 'r' } else { '-' })?;
88        f.write_char(if self.0 & PERM_USER & PERM_WRITE > 0 { 'w' } else { '-' })?;
89        f.write_char(if self.0 & PERM_USER & PERM_EXEC > 0 { 'x' } else { '-' })?;
90        f.write_char(if self.0 & PERM_GROUP & PERM_READ > 0 { 'r' } else { '-' })?;
91        f.write_char(if self.0 & PERM_GROUP & PERM_WRITE > 0 { 'w' } else { '-' })?;
92        f.write_char(if self.0 & PERM_GROUP & PERM_EXEC > 0 { 'x' } else { '-' })?;
93        f.write_char(if self.0 & PERM_OTHERS & PERM_READ > 0 { 'r' } else { '-' })?;
94        f.write_char(if self.0 & PERM_OTHERS & PERM_WRITE > 0 { 'w' } else { '-' })?;
95        f.write_char(if self.0 & PERM_OTHERS & PERM_EXEC > 0 { 'x' } else { '-' })?;
96        Ok(())
97    }
98}
99
100/// Fileinfo contains the path and `Metadata` of a file.
101///
102/// [`Metadata`]: ./trait.Metadata.html
103#[derive(Clone)]
104pub struct Fileinfo<P, M>
105where
106    P: AsRef<Path>,
107    M: Metadata,
108{
109    /// The full path to the file
110    pub path: P,
111    /// The file's metadata
112    pub metadata: M,
113}
114
115impl<P, M> std::fmt::Display for Fileinfo<P, M>
116where
117    P: AsRef<Path>,
118    M: Metadata,
119{
120    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
121        let modified: String = self
122            .metadata
123            .modified()
124            .map(|modified| {
125                let modified = DateTime::<Utc>::from(modified);
126                let now = Utc::now();
127                if modified.year() == now.year() {
128                    modified.format("%b %d %H:%M").to_string()
129                } else {
130                    modified.format("%b %d %Y").to_string()
131                }
132            })
133            .unwrap_or_else(|_| "--- -- --:--".to_string());
134        let basename = self.path.as_ref().components().next_back();
135        let path = match basename {
136            Some(v) => v.as_os_str().to_string_lossy(),
137            None => {
138                return Err(std::fmt::Error);
139            }
140        };
141        let perms = format!("{}", self.metadata.permissions());
142        let link_target = if self.metadata.is_symlink() {
143            match self.metadata.readlink() {
144                Some(t) => format!(" -> {}", t.display()),
145                None => {
146                    // We ought to log an error here, but don't have access to the logger variable
147                    "".to_string()
148                }
149            }
150        } else {
151            "".to_string()
152        };
153        write!(
154            f,
155            "{filetype}{permissions} {links:>12} {owner:>12} {group:>12} {size:#14} {modified:>12} {path}{link_target}",
156            filetype = if self.metadata.is_dir() {
157                "d"
158            } else if self.metadata.is_symlink() {
159                "l"
160            } else {
161                "-"
162            },
163            permissions = perms,
164            links = self.metadata.links(),
165            owner = self.metadata.uid(),
166            group = self.metadata.gid(),
167            size = self.metadata.len(),
168            modified = modified,
169            path = path,
170        )
171    }
172}
173
174/// The `StorageBackend` trait can be implemented to create custom FTP virtual file systems. Once
175/// implemented it needs to be registered with the [`Server`] on construction.
176///
177/// [`Server`]: ../struct.Server.html
178#[async_trait]
179pub trait StorageBackend<User: UserDetail>: Send + Sync + Debug {
180    /// The concrete type of the _metadata_ used by this storage backend.
181    type Metadata: Metadata + Sync + Send;
182
183    /// Restrict the backend's capabilities commensurate with the provided
184    /// [`UserDetail`](crate::auth::UserDetail).
185    ///
186    /// Once restricted, it may never be unrestricted.
187    fn enter(&mut self, _user_detail: &User) -> io::Result<()> {
188        Ok(())
189    }
190
191    /// Implement to set the name of the storage back-end. By default it returns the type signature.
192    fn name(&self) -> &str {
193        std::any::type_name::<Self>()
194    }
195
196    /// Tells which optional features are supported by the storage back-end
197    /// Return a value with bits set according to the FEATURE_* constants.
198    fn supported_features(&self) -> u32 {
199        0
200    }
201
202    /// Returns the `Metadata` for the given file.
203    ///
204    /// [`Metadata`]: ./trait.Metadata.html
205    async fn metadata<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<Self::Metadata>;
206
207    /// Returns the MD5 hash for the given file.
208    ///
209    /// Whether or not you want to implement the md5 method yourself,
210    /// or you want to let your StorageBackend make use of the below
211    /// default implementation, you must still explicitly enable the
212    /// feature via the
213    /// [supported_features](crate::storage::StorageBackend::supported_features)
214    /// method.
215    ///
216    /// When implementing, use the lower case 2-digit hexadecimal
217    /// format (like the output of the `md5sum` command)
218    async fn md5<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<String>
219    where
220        P: AsRef<Path> + Send + Debug,
221    {
222        let mut md5sum = Md5::new();
223        let mut reader = self.get(user, path, 0).await?;
224        let mut buffer = vec![0_u8; 1024 * 1024 * 10];
225
226        while let Ok(n) = reader.read(&mut buffer[..]).await {
227            if n == 0 {
228                break;
229            }
230            md5sum.update(&buffer[0..n]);
231        }
232
233        Ok(format!("{:x}", md5sum.finalize()))
234    }
235
236    /// Returns the list of files in the given directory.
237    async fn list<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<Vec<Fileinfo<std::path::PathBuf, Self::Metadata>>>
238    where
239        <Self as StorageBackend<User>>::Metadata: Metadata;
240
241    /// Returns some bytes that make up a directory listing that can immediately be sent to the client.
242    #[allow(clippy::type_complexity)]
243    #[tracing_attributes::instrument]
244    async fn list_fmt<P>(&self, user: &User, path: P) -> std::result::Result<std::io::Cursor<Vec<u8>>, Error>
245    where
246        P: AsRef<Path> + Send + Debug,
247        Self::Metadata: Metadata + 'static,
248    {
249        let list = self.list(user, path).await?;
250
251        let buffer = list.iter().fold(String::new(), |mut buf, fi| {
252            let _ = write!(buf, "{}\r\n", fi);
253            buf
254        });
255
256        let file_infos: Vec<u8> = buffer.into_bytes();
257
258        Ok(std::io::Cursor::new(file_infos))
259    }
260
261    /// Returns directory listing as a vec of strings used for multi line response in the control channel.
262    #[tracing_attributes::instrument]
263    async fn list_vec<P>(&self, user: &User, path: P) -> std::result::Result<Vec<String>, Error>
264    where
265        P: AsRef<Path> + Send + Debug,
266        Self::Metadata: Metadata + 'static,
267    {
268        let inlist = self.list(user, path).await?;
269        let out = inlist.iter().map(|fi| fi.to_string()).collect::<Vec<String>>();
270
271        Ok(out)
272    }
273
274    /// Returns some bytes that make up a NLST directory listing (only the basename) that can
275    /// immediately be sent to the client.
276    #[allow(clippy::type_complexity)]
277    #[tracing_attributes::instrument]
278    async fn nlst<P>(&self, user: &User, path: P) -> std::result::Result<std::io::Cursor<Vec<u8>>, std::io::Error>
279    where
280        P: AsRef<Path> + Send + Debug,
281        Self::Metadata: Metadata + 'static,
282    {
283        let list = self.list(user, path).await.map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?;
284
285        let buffer = list.iter().fold(String::new(), |mut buf, fi| {
286            let _ = write!(
287                buf,
288                "{}\r\n",
289                fi.path.file_name().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap_or("")
290            );
291            buf
292        });
293
294        let file_infos: Vec<u8> = buffer.into_bytes();
295
296        Ok(std::io::Cursor::new(file_infos))
297    }
298
299    /// Gets the content of the given FTP file from offset start_pos file by copying it to the output writer.
300    /// The starting position will only be greater than zero if the storage back-end implementation
301    /// advertises to support partial reads through the supported_features method i.e. the result
302    /// from supported_features yield 1 if a logical and operation is applied with FEATURE_RESTART.
303    async fn get_into<'a, P, W: ?Sized>(&self, user: &User, path: P, start_pos: u64, output: &'a mut W) -> Result<u64>
304    where
305        W: tokio::io::AsyncWrite + Unpin + Sync + Send,
306        P: AsRef<Path> + Send + Debug,
307    {
308        let mut reader = self.get(user, path, start_pos).await?;
309        Ok(tokio::io::copy(&mut reader, output).await.map_err(Error::from)?)
310    }
311
312    /// Returns the content of the given file from offset start_pos.
313    /// The starting position will only be greater than zero if the storage back-end implementation
314    /// advertises to support partial reads through the supported_features method i.e. the result
315    /// from supported_features yield 1 if a logical and operation is applied with FEATURE_RESTART.
316    async fn get<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P, start_pos: u64) -> Result<Box<dyn tokio::io::AsyncRead + Send + Sync + Unpin>>;
317
318    /// Writes bytes from the given reader to the specified path starting at offset start_pos in the file
319    async fn put<P: AsRef<Path> + Send + Debug, R: tokio::io::AsyncRead + Send + Sync + Unpin + 'static>(
320        &self,
321        user: &User,
322        input: R,
323        path: P,
324        start_pos: u64,
325    ) -> Result<u64>;
326
327    /// Deletes the file at the given path.
328    async fn del<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<()>;
329
330    /// Creates the given directory.
331    async fn mkd<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<()>;
332
333    /// Renames the given file to the given new filename.
334    async fn rename<P: AsRef<Path> + Send + Debug>(&self, user: &User, from: P, to: P) -> Result<()>;
335
336    /// Deletes the given directory.
337    async fn rmd<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<()>;
338
339    /// Changes the working directory to the given path.
340    async fn cwd<P: AsRef<Path> + Send + Debug>(&self, user: &User, path: P) -> Result<()>;
341}
342
343// Maps IO errors to FTP errors in a sensible way.
344// We try to capture all the permanent failures.
345// The rest is assumed to be 'retryable' so they map to 4xx FTP reply, in this case a LocalError
346impl From<std::io::Error> for Error {
347    fn from(err: std::io::Error) -> Self {
348        let kind = err.kind();
349        let raw_os_error = err.raw_os_error();
350        match (kind, raw_os_error) {
351            (std::io::ErrorKind::NotFound, _) => Error::new(ErrorKind::PermanentFileNotAvailable, err),
352            // Could also be a directory, but we don't know
353            (std::io::ErrorKind::AlreadyExists, _) => Error::new(ErrorKind::PermanentFileNotAvailable, err),
354            (std::io::ErrorKind::PermissionDenied, _) => Error::new(ErrorKind::PermissionDenied, err),
355            // The below should be changed when the io_error_more issues are resolved (https://github.com/rust-lang/rust/issues/86442)
356            // For each workaround, I mention the ErrorKind that can can replace it when stable
357            // TODO: find a workaround for Windows
358            // DirectoryNotEmpty
359            #[cfg(unix)]
360            (_, Some(libc::ENOTEMPTY)) => Error::new(ErrorKind::PermanentDirectoryNotEmpty, err),
361            // NotADirectory
362            #[cfg(unix)]
363            (_, Some(libc::ENOTDIR)) => Error::new(ErrorKind::PermanentDirectoryNotAvailable, err),
364            // IsADirectory, FileTooLarge, NotSeekable, InvalidFilename, FilesystemLoop
365            #[cfg(unix)]
366            (_, Some(libc::EISDIR) | Some(libc::EFBIG) | Some(libc::ESPIPE) | Some(libc::ENAMETOOLONG) | Some(libc::ELOOP)) => {
367                Error::new(ErrorKind::PermanentFileNotAvailable, err)
368            }
369            // StorageFull
370            #[cfg(unix)]
371            (_, Some(libc::ENOSPC)) => Error::new(ErrorKind::InsufficientStorageSpaceError, err),
372            // ReadOnlyFilesystem - Read-only filesystem can be considered a permission error
373            #[cfg(unix)]
374            (_, Some(libc::EROFS)) => Error::new(ErrorKind::PermissionDenied, err),
375            // Retryable error: Client most likely forcefully aborted the connection or there was a network issue
376            (std::io::ErrorKind::ConnectionReset, _) => Error::new(ErrorKind::ConnectionClosed, err),
377            // Retryable error: Client most likely intentionally closed the connection
378            (std::io::ErrorKind::BrokenPipe, _) => Error::new(ErrorKind::ConnectionClosed, err),
379            // Retryable error: There was likely a network issue
380            (std::io::ErrorKind::ConnectionAborted, _) => Error::new(ErrorKind::ConnectionClosed, err),
381            // Other errors are assumed to be local transient problems, retryable for the client
382            _ => Error::new(ErrorKind::LocalError, err),
383        }
384    }
385}