Skip to main content

opentalk_nextcloud_client/
client.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use std::sync::Arc;
6
7use log::warn;
8use reqwest::StatusCode;
9use reqwest_dav as dav;
10use url::Url;
11
12use crate::{
13    types::{OcsPassword, ShareAnswer},
14    Error, Result, ShareCreator, ShareId, ShareType, ShareUpdater,
15};
16
17#[derive(Clone)]
18pub struct Client {
19    pub(crate) inner: Arc<ClientRef>,
20}
21
22pub(crate) struct ClientRef {
23    pub(crate) dav_client: dav::Client,
24    pub(crate) http_client: reqwest::Client,
25    pub(crate) base_url: Url,
26    pub(crate) username: String,
27    pub(crate) password: String,
28}
29
30impl Client {
31    pub fn new(base_url: Url, username: String, password: String) -> Result<Self> {
32        let dav_url = base_url.join("remote.php/dav")?;
33        let mut http_headers = reqwest::header::HeaderMap::new();
34        http_headers.insert(
35            "OCS-APIRequest",
36            reqwest::header::HeaderValue::from_static("true"),
37        );
38        http_headers.insert(
39            reqwest::header::ACCEPT,
40            reqwest::header::HeaderValue::from_static("application/json"),
41        );
42        Ok(Self {
43            inner: Arc::new(ClientRef {
44                dav_client: dav::ClientBuilder::new()
45                    .set_host(dav_url.to_string())
46                    .set_auth(dav::Auth::Basic(username.clone(), password.clone()))
47                    .build()?,
48                http_client: reqwest::ClientBuilder::new()
49                    .default_headers(http_headers)
50                    .build()?,
51                base_url,
52                username,
53                password,
54            }),
55        })
56    }
57
58    pub async fn create_folder(&self, path: &str) -> Result<()> {
59        self.inner.dav_client.mkcol(path).await?;
60        Ok(())
61    }
62
63    pub async fn delete(&self, path: &str) -> Result<()> {
64        if let Err(e) = self.inner.dav_client.delete(path).await {
65            return Err(Error::FileNotFound {
66                file_path: path.to_owned(),
67                source: e,
68            });
69        }
70
71        Ok(())
72    }
73
74    pub fn create_share(&self, path: &str, share_type: ShareType) -> ShareCreator {
75        ShareCreator::new(self.clone(), path.to_string(), share_type)
76    }
77
78    pub fn update_share(&self, id: ShareId) -> ShareUpdater {
79        ShareUpdater::new(self.clone(), id)
80    }
81
82    pub async fn delete_share(&self, share_id: ShareId) -> Result<()> {
83        let url = self
84            .share_api_base_url()?
85            .join("shares/")?
86            .join(share_id.as_str())?;
87
88        let request = self
89            .inner
90            .http_client
91            .delete(url)
92            .basic_auth(&self.inner.username, Some(&self.inner.password));
93        let answer = request.send().await?;
94
95        match answer.status() {
96            StatusCode::CONTINUE | StatusCode::OK => {}
97            StatusCode::UNAUTHORIZED => {
98                // 401
99                return Err(Error::Unauthorized);
100            }
101            StatusCode::NOT_FOUND => {
102                // 404
103                return Err(Error::ShareNotFound { share_id });
104            }
105            status_code => {
106                warn!("Received unexpected status code {status_code} from NextCloud server.");
107                match answer.text().await {
108                    Ok(text) => {
109                        warn!("Response for unexpected status code {status_code}:\n{text}");
110                    }
111                    Err(e) => {
112                        warn!("Error retrieving body from NextCloud: {e}");
113                    }
114                }
115                return Err(Error::UnexpectedStatusCode { status_code });
116            }
117        }
118        Ok(())
119    }
120
121    pub async fn generate_password(&self) -> Result<String> {
122        let url = self.password_policy_base_url()?.join("generate")?;
123
124        let request = self
125            .inner
126            .http_client
127            .get(url)
128            .basic_auth(&self.inner.username, Some(&self.inner.password));
129
130        let answer = request.send().await?;
131
132        match answer.status() {
133            StatusCode::CONTINUE | StatusCode::OK => {}
134            StatusCode::UNAUTHORIZED => {
135                // 401
136                return Err(Error::Unauthorized);
137            }
138            status_code => {
139                warn!("Received unexpected status code {status_code} from NextCloud server.");
140                match answer.text().await {
141                    Ok(text) => {
142                        warn!("Response for unexpected status code {status_code}:\n{text}");
143                    }
144                    Err(e) => {
145                        warn!("Error retrieving body from NextCloud: {e}");
146                    }
147                }
148                return Err(Error::UnexpectedStatusCode { status_code });
149            }
150        }
151
152        let answer: ShareAnswer<OcsPassword> = answer.json().await?;
153
154        Ok(answer.ocs.data.password)
155    }
156
157    pub(crate) fn password_policy_base_url(&self) -> Result<Url> {
158        Ok(self
159            .inner
160            .base_url
161            .join("ocs/v2.php/apps/password_policy/api/v1/")?)
162    }
163
164    pub(crate) fn share_api_base_url(&self) -> Result<Url> {
165        Ok(self
166            .inner
167            .base_url
168            .join("ocs/v2.php/apps/files_sharing/api/v1/")?)
169    }
170}