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#[derive(Clone)]
18pub struct FilesystemBackend {
19 root: PathBuf,
20}
21
22impl FilesystemBackend {
23 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#[derive(Debug)]
182pub enum FilesystemBackendError {
183 GetSessions(IoError),
186 GetSessionName(OsString),
189 ReadValue(IoError),
192 RemoveSession(IoError),
195 RemoveValue(IoError),
198 SessionRootMetadata(IoError),
201 SessionRootOccupied(PathBuf),
204 TimeMarkerCreate(IoError),
207 TimeMarkerInitValue(SystemTimeError),
210 TimeMarkerGetString(FromUtf8Error),
213 TimeMarkerParseValue(ParseIntError),
216 TimeMarkerRead(IoError),
219 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}