lib_client_google_drive/
client.rs

1use bytes::Bytes;
2use lib_client_google_auth::AuthStrategy;
3use reqwest::header::HeaderMap;
4use serde::de::DeserializeOwned;
5use std::sync::Arc;
6use tracing::{debug, warn};
7
8use crate::error::{Error, Result};
9use crate::types::*;
10
11const DEFAULT_BASE_URL: &str = "https://www.googleapis.com/drive/v3";
12const UPLOAD_BASE_URL: &str = "https://www.googleapis.com/upload/drive/v3";
13
14pub struct ClientBuilder<A> {
15    auth: A,
16    base_url: String,
17}
18
19impl ClientBuilder<()> {
20    pub fn new() -> Self {
21        Self {
22            auth: (),
23            base_url: DEFAULT_BASE_URL.to_string(),
24        }
25    }
26
27    pub fn auth<S: AuthStrategy + 'static>(self, auth: S) -> ClientBuilder<S> {
28        ClientBuilder {
29            auth,
30            base_url: self.base_url,
31        }
32    }
33}
34
35impl Default for ClientBuilder<()> {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl<A: AuthStrategy + 'static> ClientBuilder<A> {
42    pub fn base_url(mut self, url: impl Into<String>) -> Self {
43        self.base_url = url.into();
44        self
45    }
46
47    pub fn build(self) -> Client {
48        Client {
49            http: reqwest::Client::new(),
50            auth: Arc::new(self.auth),
51            base_url: self.base_url,
52        }
53    }
54}
55
56#[derive(Clone)]
57pub struct Client {
58    http: reqwest::Client,
59    auth: Arc<dyn AuthStrategy>,
60    base_url: String,
61}
62
63impl Client {
64    pub fn builder() -> ClientBuilder<()> {
65        ClientBuilder::new()
66    }
67
68    async fn request<T: DeserializeOwned>(
69        &self,
70        method: reqwest::Method,
71        path: &str,
72        body: Option<&impl serde::Serialize>,
73    ) -> Result<T> {
74        let url = format!("{}{}", self.base_url, path);
75        debug!("Drive API request: {} {}", method, url);
76
77        let mut headers = HeaderMap::new();
78        self.auth.apply(&mut headers).await?;
79
80        let mut request = self.http.request(method, &url).headers(headers);
81
82        if let Some(body) = body {
83            request = request.json(body);
84        }
85
86        let response = request.send().await?;
87        self.handle_response(response).await
88    }
89
90    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
91        let status = response.status();
92
93        if status.is_success() {
94            let body = response.text().await?;
95            serde_json::from_str(&body).map_err(Error::from)
96        } else {
97            let status_code = status.as_u16();
98            let body = response.text().await.unwrap_or_default();
99            warn!("Drive API error ({}): {}", status_code, body);
100
101            match status_code {
102                401 => Err(Error::Unauthorized),
103                403 => Err(Error::Forbidden(body)),
104                404 => Err(Error::NotFound(body)),
105                429 => Err(Error::RateLimited { retry_after: 60 }),
106                _ => Err(Error::Api {
107                    status: status_code,
108                    message: body,
109                }),
110            }
111        }
112    }
113
114    /// Get a file's metadata.
115    pub async fn get_file(&self, id: &str) -> Result<File> {
116        self.request(
117            reqwest::Method::GET,
118            &format!("/files/{}?fields=*", id),
119            None::<&()>,
120        )
121        .await
122    }
123
124    /// List files.
125    pub async fn list_files(
126        &self,
127        query: Option<&str>,
128        page_token: Option<&str>,
129        page_size: Option<u32>,
130    ) -> Result<FileList> {
131        let mut params = vec![("fields", "files(*),nextPageToken".to_string())];
132
133        if let Some(q) = query {
134            params.push(("q", q.to_string()));
135        }
136        if let Some(token) = page_token {
137            params.push(("pageToken", token.to_string()));
138        }
139        if let Some(size) = page_size {
140            params.push(("pageSize", size.to_string()));
141        }
142
143        let query_string: String = params
144            .iter()
145            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
146            .collect::<Vec<_>>()
147            .join("&");
148
149        self.request(
150            reqwest::Method::GET,
151            &format!("/files?{}", query_string),
152            None::<&()>,
153        )
154        .await
155    }
156
157    /// Create a file (metadata only, for folders).
158    pub async fn create_file(&self, metadata: FileMetadata) -> Result<File> {
159        self.request(reqwest::Method::POST, "/files", Some(&metadata))
160            .await
161    }
162
163    /// Create a file with content.
164    pub async fn create_file_with_content(
165        &self,
166        metadata: FileMetadata,
167        content: Vec<u8>,
168        mime_type: &str,
169    ) -> Result<File> {
170        let url = format!("{}{}?uploadType=multipart&fields=*", UPLOAD_BASE_URL, "/files");
171        debug!("Drive API upload: POST {}", url);
172
173        let mut headers = HeaderMap::new();
174        self.auth.apply(&mut headers).await?;
175
176        let metadata_json = serde_json::to_string(&metadata)?;
177        let form = reqwest::multipart::Form::new()
178            .part(
179                "metadata",
180                reqwest::multipart::Part::text(metadata_json)
181                    .mime_str("application/json")?,
182            )
183            .part(
184                "file",
185                reqwest::multipart::Part::bytes(content).mime_str(mime_type)?,
186            );
187
188        let response = self.http.post(&url).headers(headers).multipart(form).send().await?;
189        self.handle_response(response).await
190    }
191
192    /// Update a file's metadata.
193    pub async fn update_file(&self, id: &str, metadata: FileMetadata) -> Result<File> {
194        self.request(
195            reqwest::Method::PATCH,
196            &format!("/files/{}?fields=*", id),
197            Some(&metadata),
198        )
199        .await
200    }
201
202    /// Delete a file.
203    pub async fn delete_file(&self, id: &str) -> Result<()> {
204        let url = format!("{}/files/{}", self.base_url, id);
205        debug!("Drive API request: DELETE {}", url);
206
207        let mut headers = HeaderMap::new();
208        self.auth.apply(&mut headers).await?;
209
210        let response = self.http.delete(&url).headers(headers).send().await?;
211        let status = response.status();
212
213        if status.is_success() {
214            Ok(())
215        } else {
216            let status_code = status.as_u16();
217            let body = response.text().await.unwrap_or_default();
218            Err(Error::Api {
219                status: status_code,
220                message: body,
221            })
222        }
223    }
224
225    /// Download a file's content.
226    pub async fn download_file(&self, id: &str) -> Result<Bytes> {
227        let url = format!("{}/files/{}?alt=media", self.base_url, id);
228        debug!("Drive API download: GET {}", url);
229
230        let mut headers = HeaderMap::new();
231        self.auth.apply(&mut headers).await?;
232
233        let response = self.http.get(&url).headers(headers).send().await?;
234        let status = response.status();
235
236        if status.is_success() {
237            Ok(response.bytes().await?)
238        } else {
239            let status_code = status.as_u16();
240            let body = response.text().await.unwrap_or_default();
241            Err(Error::Api {
242                status: status_code,
243                message: body,
244            })
245        }
246    }
247
248    /// Create a folder.
249    pub async fn create_folder(&self, name: &str, parent: Option<&str>) -> Result<File> {
250        let mut metadata = FileMetadata::folder(name);
251        if let Some(p) = parent {
252            metadata = metadata.parent(p);
253        }
254        self.create_file(metadata).await
255    }
256
257    /// Share a file.
258    pub async fn share_file(&self, id: &str, permission: Permission) -> Result<Permission> {
259        self.request(
260            reqwest::Method::POST,
261            &format!("/files/{}/permissions", id),
262            Some(&permission),
263        )
264        .await
265    }
266
267    /// List file permissions.
268    pub async fn list_permissions(&self, id: &str) -> Result<PermissionList> {
269        self.request(
270            reqwest::Method::GET,
271            &format!("/files/{}/permissions", id),
272            None::<&()>,
273        )
274        .await
275    }
276
277    /// Get about info.
278    pub async fn about(&self) -> Result<About> {
279        self.request(
280            reqwest::Method::GET,
281            "/about?fields=user,storageQuota",
282            None::<&()>,
283        )
284        .await
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use lib_client_google_auth::ApiKeyAuth;
292
293    #[test]
294    fn test_builder() {
295        let client = Client::builder()
296            .auth(ApiKeyAuth::new("test-key"))
297            .build();
298        assert_eq!(client.base_url, DEFAULT_BASE_URL);
299    }
300}