1#![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
40pub const PROD_API_ADDRESS: &str = "https://service.endbasic.dev/";
42
43#[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
66#[cfg_attr(test, derive(Serialize))]
67pub struct AccessToken(String);
68
69impl AccessToken {
70 #[cfg(test)]
72 pub(crate) fn new<S: Into<String>>(token: S) -> Self {
73 Self(token.into())
74 }
75
76 pub(crate) fn as_str(&self) -> &str {
78 &self.0
79 }
80}
81
82#[derive(Deserialize)]
84#[cfg_attr(test, derive(Debug, Serialize))]
85pub struct ErrorResponse {
86 pub(crate) message: String,
87}
88
89#[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#[derive(Deserialize)]
99#[cfg_attr(test, derive(Debug, Serialize))]
100pub struct DirectoryEntry {
101 filename: String,
102 mtime: u64,
103 length: u64,
104}
105
106#[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#[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 fn with_get_content(mut self) -> Self {
126 self.get_content = true;
127 self
128 }
129
130 fn with_get_readers(mut self) -> Self {
132 self.get_readers = true;
133 self
134 }
135}
136
137#[derive(Default, Deserialize)]
139#[cfg_attr(test, derive(Debug, PartialEq, Serialize))]
140pub struct GetFileResponse {
141 content: Option<String>,
143
144 readers: Option<Vec<String>>,
145}
146
147impl GetFileResponse {
148 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#[derive(Debug, Default, Eq, PartialEq, Serialize)]
165#[cfg_attr(test, derive(Deserialize))]
166pub struct PatchFileRequest {
167 content: Option<String>,
169
170 add_readers: Option<Vec<String>>,
171 remove_readers: Option<Vec<String>>,
172}
173
174impl PatchFileRequest {
175 fn with_content<C: AsRef<[u8]>>(mut self, content: C) -> Self {
177 self.content = Some(BASE64_STANDARD.encode(content));
178 self
179 }
180
181 #[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 #[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#[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#[async_trait(?Send)]
208pub trait Service {
209 async fn signup(&mut self, request: &SignupRequest) -> io::Result<()>;
211
212 async fn login(&mut self, username: &str, password: &str) -> io::Result<LoginResponse>;
217
218 async fn logout(&mut self) -> io::Result<()>;
220
221 fn is_logged_in(&self) -> bool;
223
224 fn logged_in_username(&self) -> Option<String>;
226
227 async fn get_files(&mut self, username: &str) -> io::Result<GetFilesResponse>;
230
231 async fn get_file(
234 &mut self,
235 username: &str,
236 filename: &str,
237 request: &GetFileRequest,
238 ) -> io::Result<GetFileResponse>;
239
240 async fn patch_file(
243 &mut self,
244 username: &str,
245 filename: &str,
246 request: &PatchFileRequest,
247 ) -> io::Result<()>;
248
249 async fn delete_file(&mut self, username: &str, filename: &str) -> io::Result<()>;
252}