firebase_admin_sdk/storage/
file.rs1use crate::core::middleware::AuthMiddleware;
2use crate::storage::StorageError;
3use reqwest::header;
4use reqwest_middleware::ClientWithMiddleware;
5use rsa::pkcs1::DecodeRsaPrivateKey;
6use rsa::pkcs8::DecodePrivateKey;
7use rsa::{Pkcs1v15Sign, RsaPrivateKey};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::time::{SystemTime, Duration};
11use url::Url;
12
13pub struct File {
15 client: ClientWithMiddleware,
16 base_url: String,
17 bucket_name: String,
18 name: String,
19 middleware: AuthMiddleware,
20}
21
22#[derive(Debug, Clone)]
24pub struct GetSignedUrlOptions {
25 pub method: SignedUrlMethod,
27 pub expires: SystemTime,
29 pub content_type: Option<String>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum SignedUrlMethod {
36 GET,
37 PUT,
38 POST,
39 DELETE,
40}
41
42impl SignedUrlMethod {
43 fn as_str(&self) -> &'static str {
44 match self {
45 SignedUrlMethod::GET => "GET",
46 SignedUrlMethod::PUT => "PUT",
47 SignedUrlMethod::POST => "POST",
48 SignedUrlMethod::DELETE => "DELETE",
49 }
50 }
51}
52
53#[derive(Debug, Serialize, Deserialize, Default)]
55#[serde(rename_all = "camelCase")]
56pub struct ObjectMetadata {
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub name: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub bucket: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub generation: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub metageneration: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub content_type: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub time_created: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub updated: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub storage_class: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub size: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub md5_hash: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub media_link: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub content_encoding: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub content_disposition: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub cache_control: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub metadata: Option<std::collections::HashMap<String, String>>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub crc32c: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub etag: Option<String>,
109}
110
111impl File {
112 pub(crate) fn new(
113 client: ClientWithMiddleware,
114 base_url: String,
115 bucket_name: String,
116 name: String,
117 middleware: AuthMiddleware,
118 ) -> Self {
119 Self {
120 client,
121 base_url,
122 bucket_name,
123 name,
124 middleware,
125 }
126 }
127
128 pub fn name(&self) -> &str {
130 &self.name
131 }
132
133 pub fn bucket(&self) -> &str {
135 &self.bucket_name
136 }
137
138 pub fn get_signed_url(&self, options: GetSignedUrlOptions) -> Result<String, StorageError> {
144 let key = &self.middleware.key;
145 let client_email = &key.client_email;
146 let private_key_pem = &key.private_key;
147
148 if client_email.is_empty() || private_key_pem.is_empty() {
149 return Err(StorageError::ProjectIdMissing); }
151
152 let now = SystemTime::now();
153
154
155
156 let iso_date = chrono::DateTime::<chrono::Utc>::from(now).format("%Y%m%dT%H%M%SZ").to_string();
157
158
159 let date_stamp = &iso_date[0..8]; let credential_scope = format!("{}/auto/storage/goog4_request", date_stamp);
162
163 let host = "storage.googleapis.com";
164 let canonical_headers = format!("host:{}\n", host);
165 let signed_headers = "host";
166
167 let encoded_name = url::form_urlencoded::byte_serialize(self.name.as_bytes())
169 .collect::<String>()
170 .replace("+", "%20");
171
172 let canonical_uri = format!("/{}/{}", self.bucket_name, encoded_name);
173
174 let duration_seconds = options
176 .expires
177 .duration_since(now)
178 .unwrap_or(Duration::from_secs(0))
179 .as_secs();
180
181 let mut query_params = vec![
182 ("X-Goog-Algorithm", "GOOG4-RSA-SHA256".to_string()),
183 (
184 "X-Goog-Credential",
185 format!("{}/{}", client_email, credential_scope),
186 ),
187 ("X-Goog-Date", iso_date.clone()),
188 ("X-Goog-Expires", duration_seconds.to_string()),
189 ("X-Goog-SignedHeaders", signed_headers.to_string()),
190 ];
191
192 query_params.sort_by(|a, b| a.0.cmp(b.0));
193
194 let canonical_query_string = query_params.iter()
195 .map(|(k, v)| {
196 let encoded_k = url::form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>();
197 let encoded_v = url::form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>();
198 format!("{}={}", encoded_k, encoded_v)
199 })
200 .collect::<Vec<_>>()
201 .join("&");
202
203 let payload_hash = "UNSIGNED-PAYLOAD";
204
205 let canonical_request = format!(
206 "{}\n{}\n{}\n{}\n\n{}\n{}",
207 options.method.as_str(),
208 canonical_uri,
209 canonical_query_string,
210 canonical_headers,
211 signed_headers,
212 payload_hash
213 );
214
215 let algorithm = "GOOG4-RSA-SHA256";
216 let request_hash = Sha256::digest(canonical_request.as_bytes());
217 let request_hash_hex = hex::encode(request_hash);
218
219 let string_to_sign = format!(
220 "{}\n{}\n{}\n{}",
221 algorithm, iso_date, credential_scope, request_hash_hex
222 );
223
224 let hash_to_sign = Sha256::digest(string_to_sign.as_bytes());
225
226 let priv_key = if private_key_pem.contains("BEGIN RSA PRIVATE KEY") {
227 RsaPrivateKey::from_pkcs1_pem(private_key_pem).map_err(|e| {
228 StorageError::ApiError(format!("Invalid private key (PKCS1): {}", e))
229 })?
230 } else {
231 RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(|e| {
232 StorageError::ApiError(format!("Invalid private key (PKCS8): {}", e))
233 })?
234 };
235
236 let signature = priv_key
237 .sign(Pkcs1v15Sign::new::<Sha256>(), &hash_to_sign)
238 .map_err(|e| StorageError::ApiError(format!("Signing failed: {}", e)))?;
239
240 let signature_hex = hex::encode(signature);
241
242 let final_url = format!(
243 "https://{}{}?{}&X-Goog-Signature={}",
244 host, canonical_uri, canonical_query_string, signature_hex
245 );
246
247 Ok(final_url)
248 }
249
250 pub async fn save(
259 &self,
260 body: impl Into<reqwest::Body>,
261 mime_type: &str,
262 ) -> Result<(), StorageError> {
263 let url = if self.base_url.contains("storage.googleapis.com/storage/v1") {
273 format!(
274 "https://storage.googleapis.com/upload/storage/v1/b/{}/o",
275 self.bucket_name
276 )
277 } else {
278 if self.base_url.contains("/storage/v1") {
281 let upload_base = self.base_url.replace("/storage/v1", "/upload/storage/v1");
282 format!("{}/b/{}/o", upload_base, self.bucket_name)
283 } else {
284 if self.base_url.ends_with("/storage/v1") {
299 let upload_base = self.base_url.replace("/storage/v1", "/upload/storage/v1");
300 format!("{}/b/{}/o", upload_base, self.bucket_name)
301 } else {
302 format!(
308 "{}/upload/storage/v1/b/{}/o",
309 self.base_url, self.bucket_name
310 )
311 }
312 }
313 };
314
315 let mut url_obj = Url::parse(&url).map_err(|e| StorageError::ApiError(e.to_string()))?;
316 url_obj
317 .query_pairs_mut()
318 .append_pair("uploadType", "media")
319 .append_pair("name", &self.name);
320
321 let response = self
322 .client
323 .post(url_obj)
324 .header(header::CONTENT_TYPE, mime_type)
325 .body(body)
326 .send()
327 .await?;
328
329 if !response.status().is_success() {
330 let status = response.status();
331 let text = response.text().await.unwrap_or_default();
332 return Err(StorageError::ApiError(format!(
333 "Upload failed {}: {}",
334 status, text
335 )));
336 }
337
338 Ok(())
339 }
340
341 pub async fn download(&self) -> Result<bytes::Bytes, StorageError> {
343 let encoded_name =
346 url::form_urlencoded::byte_serialize(self.name.as_bytes()).collect::<String>();
347 let url = format!(
348 "{}/b/{}/o/{}",
349 self.base_url, self.bucket_name, encoded_name
350 );
351
352 let mut url_obj = Url::parse(&url).map_err(|e| StorageError::ApiError(e.to_string()))?;
353 url_obj.query_pairs_mut().append_pair("alt", "media");
354
355 let response = self.client.get(url_obj).send().await?;
356
357 if !response.status().is_success() {
358 let status = response.status();
359 let text = response.text().await.unwrap_or_default();
360 return Err(StorageError::ApiError(format!(
361 "Download failed {}: {}",
362 status, text
363 )));
364 }
365
366 Ok(response.bytes().await?)
367 }
368
369 pub async fn delete(&self) -> Result<(), StorageError> {
371 let encoded_name =
372 url::form_urlencoded::byte_serialize(self.name.as_bytes()).collect::<String>();
373 let url = format!(
374 "{}/b/{}/o/{}",
375 self.base_url, self.bucket_name, encoded_name
376 );
377
378 let response = self.client.delete(&url).send().await?;
379
380 if !response.status().is_success() {
381 let status = response.status();
382 let text = response.text().await.unwrap_or_default();
383 return Err(StorageError::ApiError(format!(
384 "Delete failed {}: {}",
385 status, text
386 )));
387 }
388
389 Ok(())
390 }
391
392 pub async fn get_metadata(&self) -> Result<ObjectMetadata, StorageError> {
394 let encoded_name =
395 url::form_urlencoded::byte_serialize(self.name.as_bytes()).collect::<String>();
396 let url = format!(
397 "{}/b/{}/o/{}",
398 self.base_url, self.bucket_name, encoded_name
399 );
400
401 let response = self.client.get(&url).send().await?;
402
403 if !response.status().is_success() {
404 let status = response.status();
405 let text = response.text().await.unwrap_or_default();
406 return Err(StorageError::ApiError(format!(
407 "Get metadata failed {}: {}",
408 status, text
409 )));
410 }
411
412 Ok(response.json().await?)
413 }
414
415 pub async fn set_metadata(&self, metadata: &ObjectMetadata) -> Result<ObjectMetadata, StorageError> {
424 let encoded_name =
425 url::form_urlencoded::byte_serialize(self.name.as_bytes()).collect::<String>();
426 let url = format!(
427 "{}/b/{}/o/{}",
428 self.base_url, self.bucket_name, encoded_name
429 );
430
431 let response = self
432 .client
433 .patch(&url)
434 .header(header::CONTENT_TYPE, "application/json")
435 .json(metadata)
436 .send()
437 .await?;
438
439 if !response.status().is_success() {
440 let status = response.status();
441 let text = response.text().await.unwrap_or_default();
442 return Err(StorageError::ApiError(format!(
443 "Set metadata failed {}: {}",
444 status, text
445 )));
446 }
447
448 Ok(response.json().await?)
449 }
450}