1#![doc = include_str!("../readme.md")]
2
3use std::borrow::Cow;
4
5pub mod builder;
8
9pub mod entry;
12
13pub mod file;
16
17pub mod folder;
20
21pub mod general;
24
25pub mod stream;
28
29mod date;
32
33mod request;
36
37pub use reqwest;
39use sha1::Digest;
40
41const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
43
44pub const EU_REGION: &str = "https://eapi.pcloud.com";
46
47pub const US_REGION: &str = "https://api.pcloud.com";
49
50#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
52pub enum Region {
53 #[serde(alias = "EU")]
55 Eu,
56 #[serde(alias = "US")]
58 Us,
59}
60
61impl Region {
62 const fn base_url(&self) -> &'static str {
64 match self {
65 Self::Eu => EU_REGION,
66 Self::Us => US_REGION,
67 }
68 }
69}
70
71impl Region {
72 pub fn from_env() -> Option<Self> {
76 let name = std::env::var("PCLOUD_REGION").ok()?;
77 match name.as_str() {
78 "eu" | "EU" => Some(Self::Eu),
79 "us" | "US" => Some(Self::Us),
80 _ => None,
81 }
82 }
83}
84
85#[derive(Clone, serde::Serialize, serde::Deserialize)]
87#[serde(untagged)]
88pub enum Credentials {
89 AccessToken { access_token: String },
91 Authorization { auth: String },
93 UsernamePassword { username: String, password: String },
95 UsernamePasswordDigest {
97 username: String,
98 digest: String,
99 passworddigest: String,
100 },
101 Anonymous,
103}
104
105impl Credentials {
106 pub fn access_token(value: impl Into<String>) -> Self {
108 Self::AccessToken {
109 access_token: value.into(),
110 }
111 }
112
113 pub fn authorization(value: impl Into<String>) -> Self {
115 Self::Authorization { auth: value.into() }
116 }
117
118 pub fn username_password(username: impl Into<String>, password: impl Into<String>) -> Self {
120 Self::UsernamePassword {
121 username: username.into(),
122 password: password.into(),
123 }
124 }
125
126 pub fn username_password_digest(
127 username: impl Into<String>,
128 digest: impl Into<String>,
129 password: impl AsRef<[u8]>,
130 ) -> Self {
131 let username = username.into();
132 let digest = digest.into();
133
134 let mut hasher = sha1::Sha1::default();
135 hasher.update(username.to_lowercase().as_bytes());
136 let username_hash = hasher.finalize();
137
138 let username_hash = username_hash
139 .iter()
140 .map(|byte| format!("{:02x}", byte))
141 .collect::<String>();
142
143 let mut hasher = sha1::Sha1::default();
144 hasher.update(password.as_ref());
145 hasher.update(username_hash.as_bytes());
146 hasher.update(digest.as_bytes());
147 let password_hash = hasher.finalize();
148 let password_hash_slice = password_hash.as_slice();
149
150 let passworddigest = password_hash_slice
151 .iter()
152 .map(|byte| format!("{:02x}", byte))
153 .collect::<String>();
154
155 Self::UsernamePasswordDigest {
156 username,
157 digest,
158 passworddigest,
159 }
160 }
161
162 pub fn anonymous() -> Self {
164 Self::Anonymous
165 }
166}
167
168impl Credentials {
169 pub fn from_env() -> Self {
187 if let Ok(access_token) = std::env::var("PCLOUD_ACCESS_TOKEN") {
188 Self::AccessToken { access_token }
189 } else if let (Ok(username), Ok(password)) = (
190 std::env::var("PCLOUD_USERNAME"),
191 std::env::var("PCLOUD_PASSWORD"),
192 ) {
193 Self::UsernamePassword { username, password }
194 } else {
195 Self::Anonymous
196 }
197 }
198}
199
200impl std::fmt::Debug for Credentials {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct(stringify!(Credentials))
203 .finish_non_exhaustive()
204 }
205}
206
207#[derive(Debug)]
209pub struct Client {
210 base_url: Cow<'static, str>,
211 credentials: Credentials,
212 inner: reqwest::Client,
213}
214
215impl Default for Client {
216 fn default() -> Self {
217 Self {
218 base_url: crate::EU_REGION.into(),
219 credentials: Credentials::Anonymous,
220 inner: reqwest::Client::default(),
221 }
222 }
223}
224
225impl Client {
226 #[inline]
228 pub fn builder() -> crate::builder::ClientBuilder {
229 Default::default()
230 }
231
232 pub fn new(
238 base_url: impl Into<Cow<'static, str>>,
239 credentials: Credentials,
240 ) -> reqwest::Result<Self> {
241 Ok(Self {
242 base_url: base_url.into(),
243 credentials,
244 inner: reqwest::ClientBuilder::new()
245 .user_agent(USER_AGENT)
246 .build()?,
247 })
248 }
249
250 pub fn set_credentials(&mut self, credentials: Credentials) {
252 self.credentials = credentials;
253 }
254
255 pub fn with_credentials(mut self, credentials: Credentials) -> Self {
257 self.set_credentials(credentials);
258 self
259 }
260}
261
262pub type Result<V> = std::result::Result<V, Error>;
264
265#[derive(Debug, thiserror::Error)]
267pub enum Error {
268 #[error("protocol error status {0}: {1}")]
270 Protocol(u16, String),
271 #[error("network error")]
273 Reqwest(
274 #[from]
275 #[source]
276 reqwest::Error,
277 ),
278 #[error("unable to decode pcloud response")]
280 SerdeJson(
281 #[from]
282 #[source]
283 serde_json::Error,
284 ),
285 #[error("unable to download file")]
287 Download(#[source] std::io::Error),
288 #[error("unable to upload file")]
290 Upload(#[source] std::io::Error),
291}