1use super::{
4 http::{self, Body},
5 onedrive_item::{item_kind, Kind},
6 pagination::Pagination,
7 OAuthSession,
8};
9use crate::{
10 Cooldown, DropboxAccess, DropboxLocation, OAuthAccess, StorageConfig, StorageError,
11 StorageFailure,
12};
13use reqwest::Method;
14use serde_json::{json, Value};
15use std::sync::Arc;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct StorageFolder {
20 pub name: String,
22 pub storage: StorageConfig,
24}
25
26pub(super) struct OAuthFolders {
28 session: OAuthSession,
29 access: OAuthAccess,
30 api: String,
31 cooldown: Arc<dyn Cooldown>,
32}
33
34impl OAuthFolders {
35 pub(super) fn new(
36 session: OAuthSession,
37 access: OAuthAccess,
38 api: String,
39 cooldown: Arc<dyn Cooldown>,
40 ) -> Self {
41 Self { session, access, api, cooldown }
42 }
43
44 pub(super) async fn account_label(&self) -> Result<String, StorageError> {
45 crate::cooldown::cooling(self.cooldown.as_ref())?;
46 let result = self.session.account_at(&self.api).await;
47 crate::cooldown::observed(self.cooldown.as_ref(), result).await
48 }
49
50 fn require(&self, access: OAuthAccess) -> Result<(), StorageError> {
51 if self.access != access {
52 return Err(StorageFailure::InvalidConfiguration
53 .with_source("folder selection requires the matching sign-in access"));
54 }
55 Ok(())
56 }
57
58 async fn request(&self, method: Method, url: &str, body: Body) -> Result<Value, StorageError> {
59 Ok(self.request_response(method, url, body).await?.0)
60 }
61
62 async fn request_response(
65 &self,
66 method: Method,
67 url: &str,
68 body: Body,
69 ) -> Result<(Value, http::ProviderResponse), StorageError> {
70 crate::cooldown::cooling(self.cooldown.as_ref())?;
71 http::same_origin(&self.api, url)?;
72 let result = async {
73 http::json_response(
74 self.access.provider(),
75 self.session.send(method, url, &[], body, true).await?,
76 )
77 .await
78 }
79 .await;
80 crate::cooldown::observed(self.cooldown.as_ref(), result).await
81 }
82
83 pub(super) async fn onedrive_root(&self) -> Result<StorageFolder, StorageError> {
85 self.require(OAuthAccess::OneDrive)?;
86 let url = http::endpoint(&self.api, &["me", "drive", "root"], &[])?;
87 let value = self.request(Method::GET, &url, Body::Empty).await?;
88 let drive = http::string(&value["parentReference"], "driveId")?;
89 if item_kind(&value)? != Kind::Folder {
90 return Err(StorageFailure::Protocol.with_source("OneDrive did not return a folder"));
91 }
92 onedrive_folder(&value, drive, http::string(&value, "name")?)
93 }
94
95 pub(super) async fn onedrive_folders(
97 &self,
98 parent: &StorageConfig,
99 ) -> Result<Vec<StorageFolder>, StorageError> {
100 self.require(OAuthAccess::OneDrive)?;
101 let (drive, folder) = onedrive_parent(parent)?;
102 let mut url =
103 http::endpoint(&self.api, &["drives", drive, "items", folder, "children"], &[])?;
104 let mut pages = Pagination::new();
105 let mut folders = Vec::new();
106 loop {
107 pages.check(&url)?;
108 let value = self.request(Method::GET, &url, Body::Empty).await?;
109 for item in http::array(&value, "value")? {
110 if let Some(folder) = onedrive_child(item, drive)? {
111 folders.push(folder);
112 }
113 }
114 match http::next_link(&value)? {
115 None => return Ok(folders),
116 Some(next) => url = next.into(),
117 }
118 }
119 }
120
121 pub(super) async fn create_onedrive_folder(
123 &self,
124 parent: &StorageConfig,
125 name: &str,
126 ) -> Result<StorageFolder, StorageError> {
127 self.require(OAuthAccess::OneDrive)?;
128 let (drive, folder) = onedrive_parent(parent)?;
129 let url = http::endpoint(&self.api, &["drives", drive, "items", folder, "children"], &[])?;
130 let value = self
131 .request(
132 Method::POST,
133 &url,
134 Body::Json(crate::providers::onedrive_item::new_folder(name)),
135 )
136 .await?;
137 if item_kind(&value)? != Kind::Folder {
138 return Err(StorageFailure::Protocol.with_source("OneDrive did not return a folder"));
139 }
140 onedrive_folder(&value, drive, http::string(&value, "name")?)
141 }
142
143 pub(super) async fn dropbox_shared_folders(&self) -> Result<Vec<StorageFolder>, StorageError> {
145 if self.access == OAuthAccess::Dropbox(DropboxAccess::AppFolder) {
146 return Err(StorageFailure::DropboxAppFolderNotShareable.into());
147 }
148 self.require(OAuthAccess::Dropbox(DropboxAccess::FullDropbox))?;
149 let mut url = http::endpoint(&self.api, &["sharing", "list_folders"], &[])?;
150 let mut body = json!({});
151 let mut pages = Pagination::new();
152 let mut folders = Vec::new();
153 loop {
154 let value = self.request(Method::POST, &url, Body::Json(body)).await?;
155 for item in http::array(&value, "entries")? {
156 folders.push(dropbox_folder(item)?);
157 }
158 match http::page_cursor(&value)? {
159 None => return Ok(folders),
160 Some(cursor) => {
161 pages.check(cursor)?;
162 url = http::endpoint(&self.api, &["sharing", "list_folders", "continue"], &[])?;
163 body = json!({"cursor": cursor});
164 }
165 }
166 }
167 }
168
169 pub(super) async fn create_dropbox_shared_folder(
175 &self,
176 name: &str,
177 ) -> Result<StorageFolder, StorageError> {
178 if self.access == OAuthAccess::Dropbox(DropboxAccess::AppFolder) {
179 return Err(StorageFailure::DropboxAppFolderNotShareable.into());
180 }
181 self.require(OAuthAccess::Dropbox(DropboxAccess::FullDropbox))?;
182 if name.is_empty() || name.contains('/') {
183 return Err(StorageFailure::InvalidConfiguration
184 .with_source("a Dropbox folder name is one non-empty path segment"));
185 }
186 let path = format!("/{name}");
187 let lookup = http::endpoint(&self.api, &["files", "get_metadata"], &[])?;
188 match self.request(Method::POST, &lookup, Body::Json(json!({ "path": path }))).await {
189 Ok(_) => {
190 return Err(StorageFailure::AlreadyExists
191 .with_source("the Dropbox folder name is already taken"))
192 }
193 Err(error) if error.failure() == StorageFailure::NotFound => {}
194 Err(error) => return Err(error),
195 }
196 let share = http::endpoint(&self.api, &["sharing", "share_folder"], &[])?;
197 let launched = self
198 .request(Method::POST, &share, Body::Json(json!({"path":path,"force_async":false})))
199 .await?;
200 let shared = match http::string(&launched, ".tag")? {
201 "complete" => launched,
202 "async_job_id" => {
203 let job = http::string(&launched, "async_job_id")?;
204 let status =
205 http::endpoint(&self.api, &["sharing", "check_share_job_status"], &[])?;
206 super::dropbox_job::completed(&self.session, "folder sharing", || {
207 self.request_response(
208 Method::POST,
209 &status,
210 Body::Json(json!({"async_job_id":job})),
211 )
212 })
213 .await?
214 }
215 _ => return Err(StorageFailure::Protocol.with_source("invalid Dropbox share launch")),
216 };
217 dropbox_folder(&shared)
218 }
219}
220
221fn dropbox_folder(item: &Value) -> Result<StorageFolder, StorageError> {
224 let storage = StorageConfig::Dropbox {
225 account_label: None,
226 folder_label: None,
227 location: DropboxLocation::SharedFolder {
228 namespace_id: http::string(item, "shared_folder_id")?.into(),
229 },
230 };
231 storage.validate().map_err(|error| StorageFailure::Protocol.with_source(error))?;
232 Ok(StorageFolder { name: http::string(item, "name")?.into(), storage })
233}
234
235fn onedrive_parent(parent: &StorageConfig) -> Result<(&str, &str), StorageError> {
236 parent.validate()?;
237 match parent {
238 StorageConfig::OneDrive { drive_id, folder_id, .. } => Ok((drive_id, folder_id)),
239 _ => {
240 Err(StorageFailure::InvalidConfiguration.with_source("parent is not a OneDrive folder"))
241 }
242 }
243}
244
245fn onedrive_child(item: &Value, parent_drive: &str) -> Result<Option<StorageFolder>, StorageError> {
246 let remote = match item.get("remoteItem") {
247 None | Some(Value::Null) => None,
248 Some(value) if value.is_object() => Some(value),
249 Some(_) => return Err(StorageFailure::Protocol.with_source("invalid OneDrive remote item")),
250 };
251 let target = remote.unwrap_or(item);
252 if item_kind(target)? != Kind::Folder {
253 tracing::debug!("OneDrive folder picker skipped a non-folder item");
254 return Ok(None);
255 }
256 let drive = match remote {
257 Some(_) => http::string(&target["parentReference"], "driveId")?,
258 None => parent_drive,
259 };
260 Ok(Some(onedrive_folder(target, drive, http::string(item, "name")?)?))
261}
262
263fn onedrive_folder(value: &Value, drive: &str, name: &str) -> Result<StorageFolder, StorageError> {
264 if let Some(reference) = value.get("parentReference") {
265 if http::string(reference, "driveId")? != drive {
266 return Err(
267 StorageFailure::Protocol.with_source("OneDrive folder belongs to another drive")
268 );
269 }
270 }
271 let storage = StorageConfig::OneDrive {
272 account_label: None,
273 folder_label: None,
274 drive_id: drive.into(),
275 folder_id: http::string(value, "id")?.into(),
276 };
277 storage.validate().map_err(|error| StorageFailure::Protocol.with_source(error))?;
278 Ok(StorageFolder { name: name.into(), storage })
279}