endbasic_client/
lib.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//! EndBASIC service client.
17
18// Keep these in sync with other top-level files.
19#![allow(clippy::await_holding_refcell_ref)]
20#![allow(clippy::collapsible_else_if)]
21#![warn(anonymous_parameters, bad_style, missing_docs)]
22#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)]
23#![warn(unsafe_code)]
24
25use async_trait::async_trait;
26use base64::prelude::*;
27use endbasic_std::storage::DiskSpace;
28use serde::{Deserialize, Serialize};
29use std::io;
30
31mod cloud;
32pub use cloud::CloudService;
33mod cmds;
34pub use cmds::add_all;
35mod drive;
36pub(crate) use drive::CloudDriveFactory;
37#[cfg(test)]
38pub(crate) mod testutils;
39
40/// Base address of the production REST API.
41pub const PROD_API_ADDRESS: &str = "https://service.endbasic.dev/";
42
43/// Wrapper over `DiskSpace` to implement (de)serialization.
44#[derive(Debug, Deserialize)]
45#[cfg_attr(test, derive(PartialEq, Serialize))]
46struct SerdeDiskSpace {
47    bytes: u64,
48    files: u64,
49}
50
51impl From<DiskSpace> for SerdeDiskSpace {
52    fn from(ds: DiskSpace) -> Self {
53        SerdeDiskSpace { bytes: ds.bytes, files: ds.files }
54    }
55}
56
57impl From<SerdeDiskSpace> for DiskSpace {
58    fn from(sds: SerdeDiskSpace) -> Self {
59        DiskSpace { bytes: sds.bytes, files: sds.files }
60    }
61}
62
63/// An opaque access token obtained during authentication and used for all subsequent requests
64/// against the server.
65#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
66#[cfg_attr(test, derive(Serialize))]
67pub struct AccessToken(String);
68
69impl AccessToken {
70    /// Creates a new access token based on the raw `token` string.
71    #[cfg(test)]
72    pub(crate) fn new<S: Into<String>>(token: S) -> Self {
73        Self(token.into())
74    }
75
76    /// Obtains the textual representation of the token so that it can be sent back to the server.
77    pub(crate) fn as_str(&self) -> &str {
78        &self.0
79    }
80}
81
82/// Representation of the details of an error response.
83#[derive(Deserialize)]
84#[cfg_attr(test, derive(Debug, Serialize))]
85pub struct ErrorResponse {
86    pub(crate) message: String,
87}
88
89/// Representation of a login response.
90#[derive(Deserialize)]
91#[cfg_attr(test, derive(Debug, Serialize))]
92pub struct LoginResponse {
93    pub(crate) access_token: AccessToken,
94    motd: Vec<String>,
95}
96
97/// Representation of a single directory entry as returned by the server.
98#[derive(Deserialize)]
99#[cfg_attr(test, derive(Debug, Serialize))]
100pub struct DirectoryEntry {
101    filename: String,
102    mtime: u64,
103    length: u64,
104}
105
106/// Representation of a directory enumeration response.
107#[derive(Deserialize)]
108#[cfg_attr(test, derive(Debug, Serialize))]
109pub struct GetFilesResponse {
110    files: Vec<DirectoryEntry>,
111    disk_quota: Option<SerdeDiskSpace>,
112    disk_free: Option<SerdeDiskSpace>,
113}
114
115/// Representation of a file query.
116#[derive(Debug, Default, Eq, PartialEq, Serialize)]
117#[cfg_attr(test, derive(Deserialize))]
118pub struct GetFileRequest {
119    get_content: bool,
120    get_readers: bool,
121}
122
123impl GetFileRequest {
124    /// Requests the file's content from the server.
125    fn with_get_content(mut self) -> Self {
126        self.get_content = true;
127        self
128    }
129
130    /// Requests the file's readers from the server.
131    fn with_get_readers(mut self) -> Self {
132        self.get_readers = true;
133        self
134    }
135}
136
137/// Representation of the response to a file query.
138#[derive(Default, Deserialize)]
139#[cfg_attr(test, derive(Debug, PartialEq, Serialize))]
140pub struct GetFileResponse {
141    /// Base64-encoded file content.
142    content: Option<String>,
143
144    readers: Option<Vec<String>>,
145}
146
147impl GetFileResponse {
148    /// Processes the content of the response, ensuring it is valid base64.
149    fn decoded_content(&self) -> io::Result<Option<Vec<u8>>> {
150        match self.content.as_ref() {
151            Some(content) => match BASE64_STANDARD.decode(content) {
152                Ok(content) => Ok(Some(content)),
153                Err(e) => Err(io::Error::new(
154                    io::ErrorKind::InvalidData,
155                    format!("File content is not properly base64-encoded: {}", e),
156                )),
157            },
158            None => Ok(None),
159        }
160    }
161}
162
163/// Representation of an atomic file update.
164#[derive(Debug, Default, Eq, PartialEq, Serialize)]
165#[cfg_attr(test, derive(Deserialize))]
166pub struct PatchFileRequest {
167    /// Base64-encoded file content.
168    content: Option<String>,
169
170    add_readers: Option<Vec<String>>,
171    remove_readers: Option<Vec<String>>,
172}
173
174impl PatchFileRequest {
175    /// Updates the file's content with `content`.  The content is automatically base64-encoded.
176    fn with_content<C: AsRef<[u8]>>(mut self, content: C) -> Self {
177        self.content = Some(BASE64_STANDARD.encode(content));
178        self
179    }
180
181    /// Adds `readers` to the file's reader ACLs.
182    #[cfg(test)]
183    fn with_add_readers<R: Into<Vec<String>>>(mut self, readers: R) -> Self {
184        self.add_readers = Some(readers.into());
185        self
186    }
187
188    /// Removes `readers` from the file's reader ACLs.
189    #[cfg(test)]
190    fn with_remove_readers<R: Into<Vec<String>>>(mut self, readers: R) -> Self {
191        self.remove_readers = Some(readers.into());
192        self
193    }
194}
195
196/// Representation of a signup request.
197#[derive(Debug, Default, Eq, PartialEq, Serialize)]
198#[cfg_attr(test, derive(Deserialize))]
199pub struct SignupRequest {
200    username: String,
201    password: String,
202    email: String,
203    promotional_email: bool,
204}
205
206/// Abstract interface to interact with an EndBASIC service server.
207#[async_trait(?Send)]
208pub trait Service {
209    /// Interactively creates an account based on the details provided in `request`.
210    async fn signup(&mut self, request: &SignupRequest) -> io::Result<()>;
211
212    /// Sends an authentication request to the service with `username` and `password` to obtain an
213    /// access token for the session.
214    ///
215    /// If logging is successful, the access token is cached for future retrieval.
216    async fn login(&mut self, username: &str, password: &str) -> io::Result<LoginResponse>;
217
218    /// Logs out from the service and clears the access token from this object.
219    async fn logout(&mut self) -> io::Result<()>;
220
221    /// Checks if there is an active session against the service.
222    fn is_logged_in(&self) -> bool;
223
224    /// Returns the logged in username if there is an active session.
225    fn logged_in_username(&self) -> Option<String>;
226
227    /// Sends a request to the server to obtain the list of files owned by `username` with a
228    /// previously-acquired `access_token`.
229    async fn get_files(&mut self, username: &str) -> io::Result<GetFilesResponse>;
230
231    /// Sends a request to the server to obtain the metadata and/or the contents of `filename` owned
232    /// by `username` as specified in `request` with a previously-acquired `access_token`.
233    async fn get_file(
234        &mut self,
235        username: &str,
236        filename: &str,
237        request: &GetFileRequest,
238    ) -> io::Result<GetFileResponse>;
239
240    /// Sends a request to the server to update the metadata and/or the contents of `filename` owned
241    /// by `username` as specified in `request` with a previously-acquired `access_token`.
242    async fn patch_file(
243        &mut self,
244        username: &str,
245        filename: &str,
246        request: &PatchFileRequest,
247    ) -> io::Result<()>;
248
249    /// Sends a request to the server to delete `filename` owned by `username` with a
250    /// previously-acquired `access_token`.
251    async fn delete_file(&mut self, username: &str, filename: &str) -> io::Result<()>;
252}