endbasic_client/
drive.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Cloud-based implementation of an EndBASIC storage drive.
17
18use 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
27/// A drive backed by a remote EndBASIC service.
28struct CloudDrive {
29    service: Rc<RefCell<dyn Service>>,
30    username: String,
31}
32
33impl CloudDrive {
34    /// Creates a new cloud drive against `service` to access the files owned by `username`.
35    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
123/// Factory for cloud drives.
124pub struct CloudDriveFactory {
125    service: Rc<RefCell<dyn Service>>,
126}
127
128impl CloudDriveFactory {
129    /// Creates a new cloud drive factory that uses `service` to connect to the remote service.
130    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}