firebase_rs_sdk/storage/
list.rs1use crate::storage::error::{internal_error, StorageResult};
2use crate::storage::location::Location;
3use crate::storage::reference::StorageReference;
4use crate::storage::service::FirebaseStorageImpl;
5use serde::Deserialize;
6
7#[derive(Clone, Debug, Default)]
8pub struct ListOptions {
9 pub max_results: Option<u32>,
10 pub page_token: Option<String>,
11}
12
13#[derive(Clone, Default)]
14pub struct ListResult {
15 pub prefixes: Vec<StorageReference>,
16 pub items: Vec<StorageReference>,
17 pub next_page_token: Option<String>,
18}
19
20#[derive(Deserialize)]
21#[serde(rename_all = "camelCase")]
22struct ListResponse {
23 #[serde(default)]
24 prefixes: Vec<String>,
25 #[serde(default)]
26 items: Vec<ListItem>,
27 #[serde(default)]
28 next_page_token: Option<String>,
29}
30
31#[derive(Deserialize)]
32#[serde(rename_all = "camelCase")]
33struct ListItem {
34 name: String,
35 #[serde(default)]
36 bucket: Option<String>,
37}
38
39pub fn build_list_options(prefix: &Location, options: &ListOptions) -> Vec<(String, String)> {
40 let mut params = Vec::new();
41 let prefix_path = if prefix.is_root() {
42 "".to_string()
43 } else {
44 format!("{}/", prefix.path())
45 };
46 params.push(("prefix".to_string(), prefix_path));
47 params.push(("delimiter".to_string(), "/".to_string()));
48 if let Some(token) = &options.page_token {
49 params.push(("pageToken".to_string(), token.clone()));
50 }
51 if let Some(max) = options.max_results {
52 params.push(("maxResults".to_string(), max.to_string()));
53 }
54 params
55}
56
57pub fn parse_list_result(
58 storage: &FirebaseStorageImpl,
59 bucket: &str,
60 response: serde_json::Value,
61) -> StorageResult<ListResult> {
62 let parsed: ListResponse = serde_json::from_value(response.clone()).map_err(|err| {
63 internal_error(format!("invalid list response: {err}; payload: {response}"))
64 })?;
65
66 let mut result = ListResult::default();
67
68 for prefix in parsed.prefixes {
69 let trimmed = prefix.trim_end_matches('/');
70 let location = Location::new(bucket, trimmed);
71 result
72 .prefixes
73 .push(StorageReference::new(storage.clone(), location));
74 }
75
76 for item in parsed.items {
77 let item_bucket = item.bucket.unwrap_or_else(|| bucket.to_string());
78 let location = Location::new(item_bucket, item.name);
79 result
80 .items
81 .push(StorageReference::new(storage.clone(), location));
82 }
83
84 result.next_page_token = parsed.next_page_token;
85 Ok(result)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::app::api::initialize_app;
92 use crate::app::{FirebaseAppSettings, FirebaseOptions};
93
94 fn unique_settings() -> FirebaseAppSettings {
95 use std::sync::atomic::{AtomicUsize, Ordering};
96 static COUNTER: AtomicUsize = AtomicUsize::new(0);
97 FirebaseAppSettings {
98 name: Some(format!(
99 "storage-list-{}",
100 COUNTER.fetch_add(1, Ordering::SeqCst)
101 )),
102 ..Default::default()
103 }
104 }
105
106 fn build_storage() -> FirebaseStorageImpl {
107 let options = FirebaseOptions {
108 storage_bucket: Some("my-bucket".into()),
109 ..Default::default()
110 };
111 let app = initialize_app(options, Some(unique_settings())).unwrap();
112 let container = app.container();
113 let auth_provider = container.get_provider("auth-internal");
114 let app_check_provider = container.get_provider("app-check-internal");
115 FirebaseStorageImpl::new(app, auth_provider, app_check_provider, None, None).unwrap()
116 }
117
118 #[test]
119 fn parses_list_response() {
120 let storage = build_storage();
121 let json = serde_json::json!({
122 "prefixes": ["photos/"],
123 "items": [
124 {"name": "photos/cat.jpg"},
125 {"name": "photos/dog.jpg"}
126 ],
127 "nextPageToken": "abc"
128 });
129 let result = parse_list_result(&storage, "my-bucket", json).unwrap();
130 assert_eq!(result.prefixes.len(), 1);
131 assert_eq!(result.items.len(), 2);
132 assert_eq!(result.next_page_token.as_deref(), Some("abc"));
133 }
134}