firebase_rs_sdk/storage/
list.rs

1use 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}