1use crate::*;
19use async_trait::async_trait;
20use endbasic_std::storage::{Drive, DriveFactory, DriveFiles, FileAcls, Metadata};
21use std::cell::RefCell;
22use std::collections::BTreeMap;
23use std::io;
24use std::rc::Rc;
25use std::str;
26
27struct CloudDrive {
29 service: Rc<RefCell<dyn Service>>,
30 username: String,
31}
32
33impl CloudDrive {
34 fn new<S: Into<String>>(service: Rc<RefCell<dyn Service>>, username: S) -> Self {
36 let username = username.into();
37 Self { service, username }
38 }
39}
40
41#[async_trait(?Send)]
42impl Drive for CloudDrive {
43 async fn delete(&mut self, filename: &str) -> io::Result<()> {
44 self.service.borrow_mut().delete_file(&self.username, filename).await
45 }
46
47 async fn enumerate(&self) -> io::Result<DriveFiles> {
48 let response = self.service.borrow_mut().get_files(&self.username).await?;
49 let mut entries = BTreeMap::default();
50 for e in response.files {
51 let date = match time::OffsetDateTime::from_unix_timestamp(e.mtime as i64) {
52 Ok(date) => date,
53 Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))),
54 };
55 entries.insert(e.filename, Metadata { date, length: e.length });
56 }
57 Ok(DriveFiles::new(
58 entries,
59 response.disk_quota.map(|x| x.into()),
60 response.disk_free.map(|x| x.into()),
61 ))
62 }
63
64 async fn get(&self, filename: &str) -> io::Result<String> {
65 let request = GetFileRequest::default().with_get_content();
66 let response =
67 self.service.borrow_mut().get_file(&self.username, filename, &request).await?;
68 match response.decoded_content()? {
69 Some(content) => match String::from_utf8(content) {
70 Ok(s) => Ok(s),
71 Err(e) => Err(io::Error::new(
72 io::ErrorKind::InvalidData,
73 format!("Requested file is not valid UTF-8: {}", e),
74 )),
75 },
76 None => Err(io::Error::new(
77 io::ErrorKind::InvalidData,
78 "Server response is missing the file content".to_string(),
79 )),
80 }
81 }
82
83 async fn get_acls(&self, filename: &str) -> io::Result<FileAcls> {
84 let request = GetFileRequest::default().with_get_readers();
85 let response =
86 self.service.borrow_mut().get_file(&self.username, filename, &request).await?;
87 match response.readers {
88 Some(readers) => Ok(FileAcls::default().with_readers(readers)),
89 None => Err(io::Error::new(
90 io::ErrorKind::InvalidData,
91 "Server response is missing the readers list".to_string(),
92 )),
93 }
94 }
95
96 async fn put(&mut self, filename: &str, content: &str) -> io::Result<()> {
97 let request = PatchFileRequest::default().with_content(content.as_bytes());
98 self.service.borrow_mut().patch_file(&self.username, filename, &request).await
99 }
100
101 async fn update_acls(
102 &mut self,
103 filename: &str,
104 add: &FileAcls,
105 remove: &FileAcls,
106 ) -> io::Result<()> {
107 let mut request = PatchFileRequest::default();
108
109 let add = add.readers();
110 if !add.is_empty() {
111 request.add_readers = Some(add.to_vec());
112 }
113
114 let remove = remove.readers();
115 if !remove.is_empty() {
116 request.remove_readers = Some(remove.to_vec());
117 }
118
119 self.service.borrow_mut().patch_file(&self.username, filename, &request).await
120 }
121}
122
123pub struct CloudDriveFactory {
125 service: Rc<RefCell<dyn Service>>,
126}
127
128impl CloudDriveFactory {
129 pub(crate) fn new(service: Rc<RefCell<dyn Service>>) -> Self {
131 Self { service }
132 }
133}
134
135impl DriveFactory for CloudDriveFactory {
136 fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
137 if !target.is_empty() {
138 Ok(Box::from(CloudDrive::new(self.service.clone(), target)))
139 } else {
140 Err(io::Error::new(
141 io::ErrorKind::InvalidInput,
142 "Must specify a username to mount a cloud-backed drive",
143 ))
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::testutils::*;
152
153 #[tokio::test]
154 async fn test_clouddrive_delete() {
155 let service = Rc::from(RefCell::from(MockService::default()));
156 service.borrow_mut().do_login().await;
157 let mut drive = CloudDrive::new(service.clone(), "the-user");
158
159 service.borrow_mut().add_mock_delete_file("the-user", "the-filename", Ok(()));
160 drive.delete("the-filename").await.unwrap();
161
162 service.take().verify_all_used();
163 }
164
165 #[tokio::test]
166 async fn test_clouddrive_enumerate() {
167 let service = Rc::from(RefCell::from(MockService::default()));
168 service.borrow_mut().do_login().await;
169 let drive = CloudDrive::new(service.clone(), "the-user");
170
171 service.borrow_mut().add_mock_get_files(
172 "the-user",
173 Ok(GetFilesResponse {
174 files: vec![
175 DirectoryEntry { filename: "one".to_owned(), mtime: 9000, length: 15 },
176 DirectoryEntry { filename: "two".to_owned(), mtime: 8000, length: 17 },
177 ],
178 disk_quota: Some(DiskSpace::new(10000, 100).into()),
179 disk_free: Some(DiskSpace::new(123, 45).into()),
180 }),
181 );
182 let result = drive.enumerate().await.unwrap();
183 assert_eq!(2, result.dirents().len());
184 assert_eq!(
185 &Metadata {
186 date: time::OffsetDateTime::from_unix_timestamp(9000).unwrap(),
187 length: 15
188 },
189 result.dirents().get("one").unwrap()
190 );
191 assert_eq!(
192 &Metadata {
193 date: time::OffsetDateTime::from_unix_timestamp(8000).unwrap(),
194 length: 17
195 },
196 result.dirents().get("two").unwrap()
197 );
198 assert_eq!(&DiskSpace::new(10000, 100), result.disk_quota().as_ref().unwrap());
199 assert_eq!(&DiskSpace::new(123, 45), result.disk_free().as_ref().unwrap());
200
201 service.take().verify_all_used();
202 }
203
204 #[tokio::test]
205 async fn test_clouddrive_get() {
206 let service = Rc::from(RefCell::from(MockService::default()));
207 service.borrow_mut().do_login().await;
208 let drive = CloudDrive::new(service.clone(), "the-user");
209
210 let request = GetFileRequest::default().with_get_content();
211 let response = GetFileResponse {
212 content: Some(BASE64_STANDARD.encode("some content")),
213 ..Default::default()
214 };
215 service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response));
216 let result = drive.get("the-filename").await.unwrap();
217 assert_eq!("some content", result);
218
219 service.take().verify_all_used();
220 }
221
222 #[tokio::test]
223 async fn test_clouddrive_get_no_content() {
224 let service = Rc::from(RefCell::from(MockService::default()));
225 service.borrow_mut().do_login().await;
226 let drive = CloudDrive::new(service.clone(), "the-user");
227
228 let request = GetFileRequest::default().with_get_content();
229 let response = GetFileResponse::default();
230 service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response));
231 let err = drive.get("the-filename").await.unwrap_err();
232 assert_eq!(io::ErrorKind::InvalidData, err.kind());
233 assert!(format!("{}", err).contains("missing the file content"));
234
235 service.take().verify_all_used();
236 }
237
238 #[tokio::test]
239 async fn test_clouddrive_get_invalid_utf8() {
240 let service = Rc::from(RefCell::from(MockService::default()));
241 service.borrow_mut().do_login().await;
242 let drive = CloudDrive::new(service.clone(), "the-user");
243
244 let request = GetFileRequest::default().with_get_content();
245 let response = GetFileResponse {
246 content: Some(BASE64_STANDARD.encode([0x00, 0xc3, 0x28])),
247 ..Default::default()
248 };
249 service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response));
250 let err = drive.get("the-filename").await.unwrap_err();
251 assert_eq!(io::ErrorKind::InvalidData, err.kind());
252 assert!(format!("{}", err).contains("not valid UTF-8"));
253
254 service.take().verify_all_used();
255 }
256
257 #[tokio::test]
258 async fn test_clouddrive_get_acls() {
259 let service = Rc::from(RefCell::from(MockService::default()));
260 service.borrow_mut().do_login().await;
261 let drive = CloudDrive::new(service.clone(), "the-user");
262
263 let request = GetFileRequest::default().with_get_readers();
264 let response = GetFileResponse {
265 readers: Some(vec!["r1".to_owned(), "r2".to_owned()]),
266 ..Default::default()
267 };
268 service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response));
269 let result = drive.get_acls("the-filename").await.unwrap();
270 assert_eq!(FileAcls::default().with_readers(["r1".to_owned(), "r2".to_owned()]), result);
271
272 service.take().verify_all_used();
273 }
274
275 #[tokio::test]
276 async fn test_clouddrive_get_acls_no_readers() {
277 let service = Rc::from(RefCell::from(MockService::default()));
278 service.borrow_mut().do_login().await;
279 let drive = CloudDrive::new(service.clone(), "the-user");
280
281 let request = GetFileRequest::default().with_get_readers();
282 let response = GetFileResponse::default();
283 service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response));
284 let err = drive.get_acls("the-filename").await.unwrap_err();
285 assert_eq!(io::ErrorKind::InvalidData, err.kind());
286 assert!(format!("{}", err).contains("missing the readers list"));
287
288 service.take().verify_all_used();
289 }
290
291 #[tokio::test]
292 async fn test_clouddrive_put_new() {
293 let service = Rc::from(RefCell::from(MockService::default()));
294 service.borrow_mut().do_login().await;
295 let mut drive = CloudDrive::new(service.clone(), "the-user");
296
297 let request = PatchFileRequest::default().with_content("some content");
298 service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(()));
299 drive.put("the-filename", "some content").await.unwrap();
300
301 service.take().verify_all_used();
302 }
303
304 #[tokio::test]
305 async fn test_clouddrive_put_existing() {
306 let service = Rc::from(RefCell::from(MockService::default()));
307 service.borrow_mut().do_login().await;
308 let mut drive = CloudDrive::new(service.clone(), "the-user");
309
310 let request = PatchFileRequest::default().with_content("some content");
311 service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(()));
312 drive.put("the-filename", "some content").await.unwrap();
313
314 let request = PatchFileRequest::default().with_content("some other content");
315 service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(()));
316 drive.put("the-filename", "some other content").await.unwrap();
317
318 service.take().verify_all_used();
319 }
320
321 #[tokio::test]
322 async fn test_clouddrive_put_acls() {
323 let service = Rc::from(RefCell::from(MockService::default()));
324 service.borrow_mut().do_login().await;
325 let mut drive = CloudDrive::new(service.clone(), "the-user");
326
327 let request = PatchFileRequest::default()
328 .with_add_readers(["r1".to_owned(), "r2".to_owned()])
329 .with_remove_readers(["r2".to_owned(), "r3".to_owned()]);
330 service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(()));
331 drive
332 .update_acls(
333 "the-filename",
334 &FileAcls::default().with_readers(["r1".to_owned(), "r2".to_owned()]),
335 &FileAcls::default().with_readers(["r2".to_owned(), "r3".to_owned()]),
336 )
337 .await
338 .unwrap();
339
340 service.take().verify_all_used();
341 }
342
343 #[test]
344 fn test_clouddrive_system_path() {
345 let service = Rc::from(RefCell::from(MockService::default()));
346 let drive = CloudDrive::new(service, "");
347 assert!(drive.system_path("foo").is_none());
348 }
349
350 #[test]
351 fn test_login_and_mount_other_user() {
352 let mut t = ClientTester::default();
353 t.get_service().borrow_mut().add_mock_login(
354 "mock-username",
355 "mock-password",
356 Ok(LoginResponse { access_token: AccessToken::new("random token"), motd: vec![] }),
357 );
358 t.get_service().borrow_mut().add_mock_get_files(
359 "mock-username",
360 Ok(GetFilesResponse {
361 files: vec![DirectoryEntry {
362 filename: "one".to_owned(),
363 mtime: 1622556024,
364 length: 15,
365 }],
366 disk_quota: Some(DiskSpace::new(10000, 100).into()),
367 disk_free: Some(DiskSpace::new(123, 45).into()),
368 }),
369 );
370 t.get_service().borrow_mut().add_mock_get_files(
371 "user2",
372 Ok(GetFilesResponse {
373 files: vec![DirectoryEntry {
374 filename: "two".to_owned(),
375 mtime: 1622556024,
376 length: 17,
377 }],
378 disk_quota: None,
379 disk_free: None,
380 }),
381 );
382 t.run(format!(
383 r#"LOGIN "{}", "{}": MOUNT "cloud://user2" AS "x": DIR "cloud:/": DIR "x:/""#,
384 "mock-username", "mock-password",
385 ))
386 .expect_access_token("random token")
387 .expect_prints([
388 "",
389 " Directory of CLOUD:/",
390 "",
391 " Modified Size Name",
392 " 2021-06-01 14:00 15 one",
393 "",
394 " 1 file(s), 15 bytes",
395 " 123 of 10000 bytes free",
396 "",
397 "",
398 " Directory of X:/",
399 "",
400 " Modified Size Name",
401 " 2021-06-01 14:00 17 two",
402 "",
403 " 1 file(s), 17 bytes",
404 "",
405 ])
406 .check();
407 }
408}