seance/backend/
fs.rs

1use std::{
2    error::Error,
3    ffi::OsString,
4    fmt,
5    io::{Error as IoError, ErrorKind as IoErrorKind},
6    num::ParseIntError,
7    path::{Path, PathBuf},
8    string::FromUtf8Error,
9    time::SystemTimeError,
10};
11
12use tokio::fs;
13
14use crate::{backend::SessionBackend, utils::now};
15
16/// Filesystem session backend
17#[derive(Clone)]
18pub struct FilesystemBackend {
19    root: PathBuf,
20}
21
22impl FilesystemBackend {
23    /// Creates a new backend
24    ///
25    /// # Arguments
26    ///
27    /// * root - Path to sessions directory
28    ///
29    /// Note that you MUST create `root` directory before using this backend
30    pub fn new<P: Into<PathBuf>>(root: P) -> Self {
31        Self { root: root.into() }
32    }
33}
34
35impl SessionBackend for FilesystemBackend {
36    type Error = FilesystemBackendError;
37
38    async fn get_sessions(&mut self) -> Result<Vec<String>, Self::Error> {
39        let mut result = Vec::new();
40        let mut entries = match fs::read_dir(&self.root).await {
41            Ok(entries) => entries,
42            Err(error) => {
43                return match error.kind() {
44                    IoErrorKind::NotFound => Ok(result),
45                    _ => Err(FilesystemBackendError::GetSessions(error)),
46                };
47            }
48        };
49        while let Some(entry) = entries
50            .next_entry()
51            .await
52            .map_err(FilesystemBackendError::GetSessions)?
53        {
54            let file_name = entry.file_name();
55            result.push(match file_name.into_string() {
56                Ok(file_name) => file_name,
57                Err(file_name) => return Err(FilesystemBackendError::GetSessionName(file_name)),
58            })
59        }
60        Ok(result)
61    }
62
63    async fn get_session_age(&mut self, session_id: &str) -> Result<Option<u64>, Self::Error> {
64        let session_root = self.root.clone().join(session_id);
65        if is_session_root_exists(&session_root).await? {
66            Ok(Some(TimeMarker::read(session_root).await?))
67        } else {
68            Ok(None)
69        }
70    }
71
72    async fn remove_session(&mut self, session_id: &str) -> Result<(), Self::Error> {
73        let session_root = self.root.clone().join(session_id);
74        if is_session_root_exists(&session_root).await? {
75            let mut entries = fs::read_dir(&session_root)
76                .await
77                .map_err(FilesystemBackendError::RemoveSession)?;
78            while let Some(entry) = entries
79                .next_entry()
80                .await
81                .map_err(FilesystemBackendError::RemoveSession)?
82            {
83                fs::remove_file(entry.path())
84                    .await
85                    .map_err(FilesystemBackendError::RemoveSession)?;
86            }
87            fs::remove_dir(session_root)
88                .await
89                .map_err(FilesystemBackendError::RemoveSession)?;
90        }
91        Ok(())
92    }
93
94    async fn read_value(&mut self, session_id: &str, key: &str) -> Result<Option<Vec<u8>>, Self::Error> {
95        let session_root = self.root.clone().join(session_id);
96        if is_session_root_exists(&session_root).await? {
97            match fs::read(session_root.join(key)).await {
98                Ok(data) => Ok(Some(data)),
99                Err(error) => match error.kind() {
100                    IoErrorKind::NotFound => Ok(None),
101                    _ => Err(FilesystemBackendError::ReadValue(error)),
102                },
103            }
104        } else {
105            Ok(None)
106        }
107    }
108
109    async fn write_value(&mut self, session_id: &str, key: &str, value: &[u8]) -> Result<(), Self::Error> {
110        let session_root = self.root.clone().join(session_id);
111        if !is_session_root_exists(&session_root).await? {
112            fs::create_dir_all(&session_root)
113                .await
114                .map_err(FilesystemBackendError::WriteValue)?;
115            TimeMarker::create(&session_root).await?;
116        }
117        fs::write(session_root.join(key), value)
118            .await
119            .map_err(FilesystemBackendError::WriteValue)?;
120        Ok(())
121    }
122
123    async fn remove_value(&mut self, session_id: &str, key: &str) -> Result<(), Self::Error> {
124        let session_root = self.root.clone().join(session_id);
125        if is_session_root_exists(&session_root).await? {
126            if let Err(error) = fs::remove_file(session_root.join(key)).await {
127                return match error.kind() {
128                    IoErrorKind::NotFound => Ok(()),
129                    _ => Err(FilesystemBackendError::RemoveValue(error)),
130                };
131            }
132        }
133        Ok(())
134    }
135}
136
137const TIME_MARKER: &str = ".__created";
138
139struct TimeMarker;
140
141impl TimeMarker {
142    async fn create<P: AsRef<Path>>(root: P) -> Result<(), FilesystemBackendError> {
143        let timestamp = now().map_err(FilesystemBackendError::TimeMarkerInitValue)?;
144        let timestamp = format!("{timestamp}");
145        fs::write(root.as_ref().join(TIME_MARKER), timestamp)
146            .await
147            .map_err(FilesystemBackendError::TimeMarkerCreate)?;
148        Ok(())
149    }
150
151    async fn read<P: AsRef<Path>>(root: P) -> Result<u64, FilesystemBackendError> {
152        let data = fs::read(root.as_ref().join(TIME_MARKER))
153            .await
154            .map_err(FilesystemBackendError::TimeMarkerRead)?;
155        let data = String::from_utf8(data).map_err(FilesystemBackendError::TimeMarkerGetString)?;
156        let timestamp = data
157            .parse::<u64>()
158            .map_err(FilesystemBackendError::TimeMarkerParseValue)?;
159        Ok(timestamp)
160    }
161}
162
163async fn is_session_root_exists<P: AsRef<Path>>(path: P) -> Result<bool, FilesystemBackendError> {
164    let path = path.as_ref();
165    match fs::metadata(&path).await {
166        Ok(meta) => {
167            if meta.is_dir() {
168                Ok(true)
169            } else {
170                Err(FilesystemBackendError::SessionRootOccupied(path.to_path_buf()))
171            }
172        }
173        Err(error) => match error.kind() {
174            IoErrorKind::NotFound => Ok(false),
175            _ => Err(FilesystemBackendError::SessionRootMetadata(error)),
176        },
177    }
178}
179
180/// An error occurred in filesystem backend
181#[derive(Debug)]
182pub enum FilesystemBackendError {
183    /// Failed to get sessions list
184    // #[snafu(display("failed to get sessions list: {}", source))]
185    GetSessions(IoError),
186    /// Failed to convert session directory name to string
187    // #[snafu(display("failed to get session name: {:?}", name))]
188    GetSessionName(OsString),
189    /// Failed to read a value
190    // #[snafu(display("failed to read a value: {}", source))]
191    ReadValue(IoError),
192    /// Failed to remove session
193    // #[snafu(display("failed to remove session: {}", source))]
194    RemoveSession(IoError),
195    /// Failed to remove a value
196    // #[snafu(display("failed to remove a value: {}", source))]
197    RemoveValue(IoError),
198    /// Failed to get session root metadata
199    // #[snafu(display("failed to get session root metadata: {}", source))]
200    SessionRootMetadata(IoError),
201    /// Session directory is occupied by a file
202    // #[snafu(display("session root '{}' is occupied", path.display()))]
203    SessionRootOccupied(PathBuf),
204    /// Failed to create time marker for a session
205    // #[snafu(display("failed to create time marker: {}", source))]
206    TimeMarkerCreate(IoError),
207    /// Failed to get current time for time marker
208    // #[snafu(display("failed to initialize value for time marker: {}", source))]
209    TimeMarkerInitValue(SystemTimeError),
210    /// Failed to read data from time marker
211    // #[snafu(display("time marker contains non UTF-8 string: {}", source))]
212    TimeMarkerGetString(FromUtf8Error),
213    /// Failed to parse value for a time marker
214    // #[snafu(display("failed to parse time marker value: {}", source))]
215    TimeMarkerParseValue(ParseIntError),
216    /// Failed to read time marker data from a file
217    // #[snafu(display("failed to read time marker data: {}", source))]
218    TimeMarkerRead(IoError),
219    /// Failed to write a value
220    // #[snafu(display("failed to write a value: {}", source))]
221    WriteValue(IoError),
222}
223
224impl fmt::Display for FilesystemBackendError {
225    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
226        use self::FilesystemBackendError::*;
227        match self {
228            GetSessions(err) => write!(out, "failed to get sessions list: {err}"),
229            GetSessionName(name) => write!(out, "failed to get session name: {name:?}"),
230            ReadValue(err) => write!(out, "failed to read a value: {err}"),
231            RemoveSession(err) => write!(out, "failed to remove session: {err}"),
232            RemoveValue(err) => write!(out, "failed to remove a value: {err}"),
233            SessionRootMetadata(err) => {
234                write!(out, "failed to get session root metadata: {err}")
235            }
236            SessionRootOccupied(path) => {
237                write!(out, "session root '{}' is occupied", path.display())
238            }
239            TimeMarkerCreate(err) => write!(out, "failed to create time marker: {err}"),
240            TimeMarkerInitValue(err) => {
241                write!(out, "failed to initialize value for time marker: {err}")
242            }
243            TimeMarkerGetString(err) => {
244                write!(out, "time marker contains non UTF-8 string: {err}")
245            }
246            TimeMarkerParseValue(err) => {
247                write!(out, "failed to parse time marker value: {err}")
248            }
249            TimeMarkerRead(err) => write!(out, "failed to read time marker data: {err}"),
250            WriteValue(err) => write!(out, "failed to write a value: {err}"),
251        }
252    }
253}
254
255impl Error for FilesystemBackendError {
256    fn source(&self) -> Option<&(dyn Error + 'static)> {
257        use self::FilesystemBackendError::*;
258        Some(match self {
259            GetSessions(err) => err,
260            GetSessionName(_) => return None,
261            ReadValue(err) => err,
262            RemoveSession(err) => err,
263            RemoveValue(err) => err,
264            SessionRootMetadata(err) => err,
265            SessionRootOccupied(_) => return None,
266            TimeMarkerCreate(err) => err,
267            TimeMarkerInitValue(err) => err,
268            TimeMarkerGetString(err) => err,
269            TimeMarkerParseValue(err) => err,
270            TimeMarkerRead(err) => err,
271            WriteValue(err) => err,
272        })
273    }
274}