1use std::sync::{Arc, Mutex};
2use std::time::Duration;
3
4use crate::app::FirebaseApp;
5use crate::app_check::FirebaseAppCheckInternal;
6use crate::auth::Auth;
7use crate::component::Provider;
8use crate::storage::constants::{
9 DEFAULT_HOST, DEFAULT_MAX_OPERATION_RETRY_TIME_MS, DEFAULT_MAX_UPLOAD_RETRY_TIME_MS,
10 DEFAULT_PROTOCOL,
11};
12use crate::storage::error::{internal_error, no_default_bucket, StorageResult};
13use crate::storage::location::Location;
14use crate::storage::reference::StorageReference;
15use crate::storage::request::{BackoffConfig, HttpClient, RequestInfo};
16use crate::storage::util::is_url;
17
18#[derive(Clone)]
19pub struct FirebaseStorageImpl {
20 app: FirebaseApp,
21 auth_provider: Provider,
22 app_check_provider: Provider,
23 firebase_version: Option<String>,
24 url_override: Option<String>,
25 state: Arc<Mutex<FirebaseStorageState>>,
26}
27
28struct FirebaseStorageState {
29 bucket: Option<Location>,
30 host: String,
31 protocol: String,
32 max_operation_retry_time_ms: u64,
33 max_upload_retry_time_ms: u64,
34 override_auth_token: Option<String>,
35 is_using_emulator: bool,
36}
37
38impl FirebaseStorageImpl {
39 #[allow(clippy::too_many_arguments)]
40 pub fn new(
41 app: FirebaseApp,
42 auth_provider: Provider,
43 app_check_provider: Provider,
44 url_override: Option<String>,
45 firebase_version: Option<String>,
46 ) -> StorageResult<Self> {
47 let host = DEFAULT_HOST.to_string();
48 let bucket = if let Some(url) = url_override.as_ref() {
49 Some(Location::from_bucket_spec(url, &host)?)
50 } else {
51 extract_bucket(&host, &app)?
52 };
53
54 let state = FirebaseStorageState {
55 bucket,
56 host,
57 protocol: DEFAULT_PROTOCOL.to_string(),
58 max_operation_retry_time_ms: DEFAULT_MAX_OPERATION_RETRY_TIME_MS,
59 max_upload_retry_time_ms: DEFAULT_MAX_UPLOAD_RETRY_TIME_MS,
60 override_auth_token: None,
61 is_using_emulator: false,
62 };
63
64 Ok(Self {
65 app,
66 auth_provider,
67 app_check_provider,
68 firebase_version,
69 url_override,
70 state: Arc::new(Mutex::new(state)),
71 })
72 }
73
74 pub fn app(&self) -> &FirebaseApp {
75 &self.app
76 }
77
78 pub fn host(&self) -> String {
79 self.state.lock().unwrap().host.clone()
80 }
81
82 pub fn protocol(&self) -> String {
83 self.state.lock().unwrap().protocol.clone()
84 }
85
86 pub fn auth_provider(&self) -> Provider {
87 self.auth_provider.clone()
88 }
89
90 pub fn app_check_provider(&self) -> Provider {
91 self.app_check_provider.clone()
92 }
93
94 pub fn firebase_version(&self) -> Option<&str> {
95 self.firebase_version.as_deref()
96 }
97
98 pub fn bucket(&self) -> Option<Location> {
99 self.state.lock().unwrap().bucket.clone()
100 }
101
102 pub fn max_upload_retry_time(&self) -> u64 {
103 self.state.lock().unwrap().max_upload_retry_time_ms
104 }
105
106 pub fn max_operation_retry_time(&self) -> u64 {
107 self.state.lock().unwrap().max_operation_retry_time_ms
108 }
109
110 pub fn set_max_upload_retry_time(&self, millis: u64) {
111 self.state.lock().unwrap().max_upload_retry_time_ms = millis;
112 }
113
114 pub fn set_max_operation_retry_time(&self, millis: u64) {
115 self.state.lock().unwrap().max_operation_retry_time_ms = millis;
116 }
117
118 pub fn is_using_emulator(&self) -> bool {
119 self.state.lock().unwrap().is_using_emulator
120 }
121
122 pub fn connect_emulator(
123 &self,
124 host: &str,
125 port: u16,
126 mock_user_token: Option<String>,
127 ) -> StorageResult<()> {
128 let host_string = format!("{host}:{port}");
129 let bucket = self.compute_bucket_for_host(&host_string)?;
130 let mut state = self.state.lock().unwrap();
131 state.host = host_string;
132 state.bucket = bucket;
133 state.protocol = "http".to_string();
134 state.is_using_emulator = true;
135 state.override_auth_token = mock_user_token;
136 Ok(())
137 }
138
139 pub fn set_host(&self, host: &str) -> StorageResult<()> {
140 let bucket = self.compute_bucket_for_host(host)?;
141 let mut state = self.state.lock().unwrap();
142 state.host = host.to_string();
143 state.bucket = bucket;
144 Ok(())
145 }
146
147 fn compute_bucket_for_host(&self, host: &str) -> StorageResult<Option<Location>> {
148 if let Some(url) = self.url_override.as_ref() {
149 Ok(Some(Location::from_bucket_spec(url, host)?))
150 } else {
151 extract_bucket(host, &self.app)
152 }
153 }
154
155 pub fn make_storage_reference(&self, location: Location) -> StorageReference {
156 StorageReference::new(self.clone(), location)
157 }
158
159 pub fn root_reference(&self) -> StorageResult<StorageReference> {
160 let state = self.state.lock().unwrap();
161 let bucket = state.bucket.clone().ok_or_else(no_default_bucket)?;
162 Ok(StorageReference::new(self.clone(), bucket))
163 }
164
165 pub fn reference_from_path(&self, path: Option<&str>) -> StorageResult<StorageReference> {
166 let location = match path {
167 Some(path) if is_url(path) => Location::from_url(path, &self.host())?,
168 Some(path) => {
169 let base = self.bucket().ok_or_else(no_default_bucket)?;
170 let child_path = crate::storage::path::child(base.path(), path);
171 Location::new(base.bucket(), child_path)
172 }
173 None => self.bucket().ok_or_else(no_default_bucket)?,
174 };
175 Ok(StorageReference::new(self.clone(), location))
176 }
177
178 pub fn http_client(&self) -> StorageResult<HttpClient> {
179 let timeout = Duration::from_millis(self.max_operation_retry_time());
180 let config = BackoffConfig::standard_operation().with_total_timeout(timeout);
181 HttpClient::new(self.is_using_emulator(), config)
182 }
183
184 pub fn upload_http_client(&self) -> StorageResult<HttpClient> {
185 let timeout = Duration::from_millis(self.max_upload_retry_time());
186 let config = BackoffConfig::upload_operation(timeout);
187 HttpClient::new(self.is_using_emulator(), config)
188 }
189
190 pub fn run_request<O>(&self, info: RequestInfo<O>) -> StorageResult<O> {
191 let client = self.http_client()?;
192 let info = self.prepare_request(info)?;
193 client.execute(info)
194 }
195
196 pub fn run_upload_request<O>(&self, info: RequestInfo<O>) -> StorageResult<O> {
197 let client = self.upload_http_client()?;
198 let info = self.prepare_request(info)?;
199 client.execute(info)
200 }
201
202 fn prepare_request<O>(&self, mut info: RequestInfo<O>) -> StorageResult<RequestInfo<O>> {
203 if let Some(token) = self.auth_token()? {
204 if !token.is_empty() {
205 info.headers
206 .insert("Authorization".to_string(), format!("Firebase {token}"));
207 }
208 }
209
210 if let Some(token) = self.app_check_token()? {
211 if !token.is_empty() {
212 info.headers
213 .insert("X-Firebase-AppCheck".to_string(), token);
214 }
215 }
216
217 if !info.headers.contains_key("X-Firebase-Storage-Version") {
218 let version = format!(
219 "webjs/{}",
220 self.firebase_version.as_deref().unwrap_or("AppManager")
221 );
222 info.headers
223 .insert("X-Firebase-Storage-Version".to_string(), version);
224 }
225
226 if let Some(app_id) = self.app.options().app_id {
227 if !app_id.is_empty() {
228 info.headers
229 .entry("X-Firebase-GMPID".to_string())
230 .or_insert(app_id);
231 }
232 }
233
234 Ok(info)
235 }
236
237 fn auth_token(&self) -> StorageResult<Option<String>> {
238 if let Some(token) = {
239 let state = self.state.lock().unwrap();
240 state.override_auth_token.clone()
241 } {
242 return Ok(Some(token));
243 }
244
245 let auth = match self
246 .auth_provider
247 .get_immediate_with_options::<Auth>(None, true)
248 {
249 Ok(Some(auth)) => auth,
250 Ok(None) => return Ok(None),
251 Err(err) => {
252 return Err(internal_error(format!(
253 "failed to resolve auth provider: {err}"
254 )))
255 }
256 };
257
258 match auth.get_token(false) {
259 Ok(Some(token)) if token.is_empty() => Ok(None),
260 Ok(Some(token)) => Ok(Some(token)),
261 Ok(None) => Ok(None),
262 Err(err) => Err(internal_error(format!(
263 "failed to obtain auth token: {err}"
264 ))),
265 }
266 }
267
268 fn app_check_token(&self) -> StorageResult<Option<String>> {
269 let app_check = match self
270 .app_check_provider
271 .get_immediate_with_options::<FirebaseAppCheckInternal>(None, true)
272 {
273 Ok(Some(app_check)) => app_check,
274 Ok(None) => return Ok(None),
275 Err(err) => {
276 return Err(internal_error(format!(
277 "failed to resolve app check provider: {err}"
278 )))
279 }
280 };
281
282 let result = app_check
283 .get_token(false)
284 .map_err(|err| internal_error(format!("failed to obtain App Check token: {err}")))?;
285
286 if let Some(error) = result.error {
287 return Err(internal_error(error.to_string()));
288 }
289 if let Some(error) = result.internal_error {
290 return Err(internal_error(error.to_string()));
291 }
292
293 if result.token.is_empty() {
294 Ok(None)
295 } else {
296 Ok(Some(result.token))
297 }
298 }
299}
300
301fn extract_bucket(host: &str, app: &FirebaseApp) -> StorageResult<Option<Location>> {
302 let options = app.options();
303 match options.storage_bucket {
304 Some(bucket) => Ok(Some(Location::from_bucket_spec(&bucket, host)?)),
305 None => Ok(None),
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::app::api::initialize_app;
313 use crate::app::{FirebaseAppSettings, FirebaseOptions};
314 use crate::app_check::api::{initialize_app_check, token_with_ttl};
315 use crate::app_check::{AppCheckOptions, AppCheckProvider, AppCheckToken};
316 use crate::component::types::{ComponentError, DynService, InstanceFactoryOptions};
317 use crate::component::{Component, ComponentType};
318 use crate::storage::request::{RequestInfo, ResponseHandler};
319 use reqwest::Method;
320 use std::sync::atomic::{AtomicUsize, Ordering};
321 use std::sync::Arc;
322 use std::time::Duration;
323
324 fn unique_settings(prefix: &str) -> FirebaseAppSettings {
325 static COUNTER: AtomicUsize = AtomicUsize::new(0);
326 FirebaseAppSettings {
327 name: Some(format!(
328 "{prefix}-{}",
329 COUNTER.fetch_add(1, Ordering::SeqCst)
330 )),
331 ..Default::default()
332 }
333 }
334
335 fn base_options() -> FirebaseOptions {
336 FirebaseOptions {
337 storage_bucket: Some("my-bucket".into()),
338 app_id: Some("1:123:web:abc".into()),
339 ..Default::default()
340 }
341 }
342
343 fn test_request() -> RequestInfo<()> {
344 let handler: ResponseHandler<()> = Arc::new(|_| Ok(()));
345 RequestInfo::new(
346 "https://example.com",
347 Method::GET,
348 Duration::from_secs(5),
349 handler,
350 )
351 }
352
353 fn build_storage_with<F>(configure: F) -> FirebaseStorageImpl
354 where
355 F: Fn(&FirebaseApp),
356 {
357 let app = initialize_app(base_options(), Some(unique_settings("storage-service")))
358 .expect("failed to initialize app");
359 configure(&app);
360
361 let container = app.container();
362 let auth_provider = container.get_provider("auth-internal");
363 let app_check_provider = container.get_provider("app-check-internal");
364 FirebaseStorageImpl::new(
365 app,
366 auth_provider,
367 app_check_provider,
368 None,
369 Some("test-sdk".into()),
370 )
371 .expect("storage construction should succeed")
372 }
373
374 #[test]
375 fn prepare_request_adds_headers_for_emulator_override() {
376 let storage = build_storage_with(|_| {});
377 storage
378 .connect_emulator("localhost", 9199, Some("mock-token".into()))
379 .unwrap();
380
381 let prepared = storage.prepare_request(test_request()).unwrap();
382
383 assert_eq!(
384 prepared.headers.get("Authorization"),
385 Some(&"Firebase mock-token".to_string())
386 );
387
388 let expected_version = format!(
389 "webjs/{}",
390 storage.firebase_version().unwrap_or("AppManager")
391 );
392 assert_eq!(
393 prepared.headers.get("X-Firebase-Storage-Version"),
394 Some(&expected_version)
395 );
396
397 assert_eq!(
398 prepared.headers.get("X-Firebase-GMPID"),
399 Some(&"1:123:web:abc".to_string())
400 );
401
402 assert!(prepared.headers.get("X-Firebase-AppCheck").is_none());
403 }
404
405 #[derive(Clone)]
406 struct StaticAppCheckProvider;
407
408 impl AppCheckProvider for StaticAppCheckProvider {
409 fn get_token(&self) -> crate::app_check::AppCheckResult<AppCheckToken> {
410 token_with_ttl("app-check-token", Duration::from_secs(60))
411 }
412 }
413
414 fn register_app_check(app: &FirebaseApp) {
415 let app_clone = app.clone();
416 let factory =
417 Arc::new(
418 move |_: &crate::component::ComponentContainer,
419 _: InstanceFactoryOptions|
420 -> Result<DynService, ComponentError> {
421 let provider = Arc::new(StaticAppCheckProvider);
422 let options = AppCheckOptions::new(provider);
423 let app_check = initialize_app_check(Some(app_clone.clone()), options)
424 .map_err(|err| ComponentError::InitializationFailed {
425 name: "app-check-internal".to_string(),
426 reason: err.to_string(),
427 })?;
428 Ok(Arc::new(FirebaseAppCheckInternal::new(app_check)) as DynService)
429 },
430 );
431
432 let component = Component::new("app-check-internal", factory, ComponentType::Private);
433 app.container().add_or_overwrite_component(component);
434 }
435
436 #[test]
437 fn prepare_request_includes_app_check_header_when_available() {
438 let storage = build_storage_with(|app| register_app_check(app));
439 let prepared = storage.prepare_request(test_request()).unwrap();
440
441 assert_eq!(
442 prepared.headers.get("X-Firebase-AppCheck"),
443 Some(&"app-check-token".to_string())
444 );
445 }
446}