webdav_server/storage/
service.rs1use bytes::Bytes;
2use futures::Stream;
3use std::error::Error as StdErr;
4use std::io::ErrorKind::{self};
5use tokio::fs::{self, File};
6
7use crate::error::internal::Error::Message;
8use crate::info;
9use crate::logger::Module;
10use crate::storage::file::Metadata;
11use crate::storage::transaction::Transaction;
12use crate::storage::{
13 Error::{FileAlreadyExists, Internal, InvalidFileName, NotFound},
14 Payload, Result,
15};
16use std::path::PathBuf;
17
18#[derive(Default, Clone)]
19pub struct Service {
28 pub(crate) vault_path: PathBuf,
29}
30
31impl Service {
32 pub fn new<P>(vault_path: P) -> Self
38 where
39 P: Into<PathBuf>,
40 {
41 Self {
42 vault_path: vault_path.into(),
43 }
44 }
45
46 pub async fn try_save<S, E>(
61 &self,
62 file_name: &str,
63 payload: Payload<S, E>,
64 ) -> Result<Metadata>
65 where
66 S: Stream<Item = std::result::Result<Bytes, E>> + Unpin,
67 E: Into<Box<dyn StdErr + Send + Sync>>,
68 {
69 let transaction = self.begin_transaction(&file_name)?;
70
71 let file_metadata = transaction.commit(payload).await?;
72
73 info!(Module::Storage, "Comitted CAS blob for {file_name} to disk");
74 Ok(file_metadata)
75 }
76
77 fn begin_transaction<T>(&self, file: &T) -> Result<Transaction>
83 where
84 T: AsRef<str>,
85 {
86 let file_name = file.as_ref();
87 if file_name.is_empty()
88 || file_name.contains('/')
89 || file_name.contains('\\')
90 {
91 return Err(InvalidFileName);
92 }
93
94 Ok(Transaction::new(&self.vault_path))
95 }
96}
97
98impl Service {
99 pub async fn delete_blob(&self, hash_str: &str) -> Result<()> {
109 let file_path = self.vault_path.join(hash_str);
110
111 match fs::remove_file(file_path).await {
112 Ok(()) => Ok(()),
113 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
114 Err(x) => {
115 Err(Internal(Message(format!("Failed to delete file : {x}"))))
116 }
117 }
118 }
119
120 pub async fn get_blob(&self, hash_str: &str) -> Result<File> {
130 let file_path = self.vault_path.join(hash_str);
131
132 File::open(file_path).await.map_err(|_| NotFound)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use core::result::Result;
139 use std::io::Error as IoErr;
140
141 use tokio::fs::File;
142
143 use super::*;
144
145 use crate::storage::tests::with_temp_service;
146
147 #[tokio::test]
148 async fn reject_invalid_filename() -> crate::storage::Result<()> {
149 with_temp_service(|service| async move {
150 let result = service.begin_transaction(&"o///reo/hiuh//i");
152 assert!(result.is_err());
153 assert!(matches!(result, Err(InvalidFileName)));
154
155 let result = service.begin_transaction(&"");
156 assert!(result.is_err());
157
158 assert!(matches!(result, Err(InvalidFileName)));
159 let result = service.begin_transaction(&"../../../../etc/passwd");
160 assert!(result.is_err());
161 assert!(matches!(result, Err(InvalidFileName)))
162 })
163 .await;
164
165 Ok(())
166 }
167
168 #[tokio::test]
169 async fn concurrent_write_collisions_dont_panic() {
170 with_temp_service(|service| async move {
171 let service_a = service.clone();
172 let service_b = service.clone();
173
174 let task_a = tokio::spawn(async move {
175 let chunks: Vec<Result<Bytes, IoErr>> =
176 vec![Ok(Bytes::from("some_data"))];
177 let stream = futures::stream::iter(chunks);
178
179 service_a
180 .try_save("dev1_upload.rs", Payload::new(9u64, stream))
181 .await
182 });
183
184 let task_b = tokio::spawn(async move {
185 let payload: Vec<Result<Bytes, IoErr>> =
186 vec![Ok(Bytes::from("some_data"))];
187 let stream = futures::stream::iter(payload);
188
189 service_b
190 .try_save("some_other_file.rs", Payload::new(9u64, stream))
191 .await
192 });
193
194 let (result_a, result_b) = tokio::join!(task_a, task_b);
195
196 let metadata_a = result_a.unwrap().expect("task_a failed");
198 let metadata_b = result_b.unwrap().expect("task_b failed");
199
200 assert_eq!(
202 metadata_a.hash.to_string(),
203 metadata_b.hash.to_string()
204 );
205
206 let expected_path =
207 service.vault_path.join(metadata_a.hash.to_string());
208 assert!(expected_path.exists());
210 })
211 .await;
212 }
213
214 #[tokio::test]
215 async fn validation_success() {
216 with_temp_service(async move |service| {
217 let result = service.begin_transaction(&"oreo.tmp.jks");
219 assert!(result.is_ok());
220 assert!(matches!(result, Ok(_)));
221 })
222 .await
223 }
224}