pcloud/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3use std::borrow::Cow;
4
5// Module responsible for building requests to the API, including setting parameters and
6// configuring request details such as method type, headers, and body content.
7pub mod builder;
8
9// Module for handling entries in the system. This could include creating, modifying,
10// or retrieving data related to various types of entries (e.g., file or folder entries).
11pub mod entry;
12
13// Module for dealing with files, including operations like file uploads, downloads,
14// file metadata retrieval, and manipulation.
15pub mod file;
16
17// Module for handling folder-related operations such as creating, listing,
18// or manipulating folders in the system.
19pub mod folder;
20
21/// Module handling general operations
22/// https://docs.pcloud.com/methods/general/
23pub mod general;
24
25// Module for working with streams, likely including streaming files or media
26// content, such as audio and video, over the network or from storage.
27pub mod stream;
28
29// Private module responsible for date manipulation, likely for handling timestamps
30// or other date-related utilities across the library.
31mod date;
32
33// Private module that contains the logic for handling HTTP requests, such as sending GET, POST,
34// PUT requests, serializing parameters, and processing responses from the API.
35mod request;
36
37// Re-exporting the reqwest crate for convenient access
38pub use reqwest;
39use sha1::Digest;
40
41/// The default user agent used by the HTTP client, derived from crate name and version.
42const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
43
44/// Base URL for the EU region.
45pub const EU_REGION: &str = "https://eapi.pcloud.com";
46
47/// Base URL for the US region.
48pub const US_REGION: &str = "https://api.pcloud.com";
49
50/// Represents a pCloud API region.
51#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
52pub enum Region {
53    /// Europe region endpoint
54    #[serde(alias = "EU")]
55    Eu,
56    /// United States region endpoint
57    #[serde(alias = "US")]
58    Us,
59}
60
61impl Region {
62    /// Returns the base URL associated with the selected region.
63    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    /// Attempts to create a `Region` from the `PCLOUD_REGION` environment variable.
73    ///
74    /// Recognizes `"eu"`, `"EU"`, `"us"`, and `"US"` as valid inputs.
75    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/// Authentication credentials used for pCloud API requests.
86#[derive(Clone, serde::Serialize, serde::Deserialize)]
87#[serde(untagged)]
88pub enum Credentials {
89    /// Uses a personal access token.
90    AccessToken { access_token: String },
91    /// Uses an authorization token.
92    Authorization { auth: String },
93    /// Uses a username and password for authentication.
94    UsernamePassword { username: String, password: String },
95    /// Uses a username and password for authentication.
96    UsernamePasswordDigest {
97        username: String,
98        digest: String,
99        passworddigest: String,
100    },
101    /// Without authentication, used for getting a digest
102    Anonymous,
103}
104
105impl Credentials {
106    /// Creates credentials using an access token.
107    pub fn access_token(value: impl Into<String>) -> Self {
108        Self::AccessToken {
109            access_token: value.into(),
110        }
111    }
112
113    /// Creates credentials using an authorization token.
114    pub fn authorization(value: impl Into<String>) -> Self {
115        Self::Authorization { auth: value.into() }
116    }
117
118    /// Creates credentials using a username and password.
119    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    /// Creates an anonymous credential
163    pub fn anonymous() -> Self {
164        Self::Anonymous
165    }
166}
167
168impl Credentials {
169    /// Creates a credential based on the environment variables
170    ///
171    /// When `PCLOUD_ACCESS_TOKEN` is set, a `Credentials::AccessToken` will be created.
172    ///
173    /// When `PCLOUD_USERNAME` and `PCLOUD_PASSWORD` are set, a `Credentials::UsernamePassword` will be created.
174    ///
175    /// If none are set, `Credentials::Anonymous` is returned.
176    ///
177    /// ```rust
178    /// use pcloud::Credentials;
179    ///
180    /// match Credentials::from_env() {
181    ///     Credentials::AccessToken { .. } => println!("uses an access token"),
182    ///     Credentials::UsernamePassword { .. } => println!("uses a username and a password"),
183    ///     _ => eprintln!("no credentials provided"),
184    /// }
185    /// ```
186    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/// HTTP client used to interact with the pCloud API.
208#[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    /// Creates a new `ClientBuilder` instance for custom configuration.
227    #[inline]
228    pub fn builder() -> crate::builder::ClientBuilder {
229        Default::default()
230    }
231
232    /// Creates a new `Client` with the specified base URL and credentials.
233    ///
234    /// # Errors
235    ///
236    /// Returns a `reqwest::Error` if the inner HTTP client fails to build.
237    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    /// Update the credentials of the client
251    pub fn set_credentials(&mut self, credentials: Credentials) {
252        self.credentials = credentials;
253    }
254
255    /// Take ownership of the client and update the credentials
256    pub fn with_credentials(mut self, credentials: Credentials) -> Self {
257        self.set_credentials(credentials);
258        self
259    }
260}
261
262/// A type alias for results returned by pCloud API operations.
263pub type Result<V> = std::result::Result<V, Error>;
264
265/// Errors that can occur when using the pCloud client or interacting with the API.
266#[derive(Debug, thiserror::Error)]
267pub enum Error {
268    /// An error response from the API, including status code and message.
269    #[error("protocol error status {0}: {1}")]
270    Protocol(u16, String),
271    /// A network-related error from the underlying HTTP client.
272    #[error("network error")]
273    Reqwest(
274        #[from]
275        #[source]
276        reqwest::Error,
277    ),
278    /// An error occurred while parsing a JSON response.
279    #[error("unable to decode pcloud response")]
280    SerdeJson(
281        #[from]
282        #[source]
283        serde_json::Error,
284    ),
285    /// An I/O error occurred while downloading a file.
286    #[error("unable to download file")]
287    Download(#[source] std::io::Error),
288    /// An I/O error occurred while uploading a file.
289    #[error("unable to upload file")]
290    Upload(#[source] std::io::Error),
291}