junobuild_storage/
utils.rs

1use crate::constants::{
2    ASSET_ENCODING_NO_COMPRESSION, WELL_KNOWN_CUSTOM_DOMAINS, WELL_KNOWN_II_ALTERNATIVE_ORIGINS,
3};
4use crate::http::types::HeaderField;
5use crate::strategies::StorageAssertionsStrategy;
6use crate::types::interface::AssetNoContent;
7use crate::types::state::FullPath;
8use crate::types::store::{Asset, AssetEncoding, AssetKey};
9use candid::Principal;
10use junobuild_collections::constants::assets::COLLECTION_ASSET_KEY;
11use junobuild_collections::types::core::CollectionKey;
12use junobuild_collections::types::rules::Permission;
13use junobuild_shared::list::{filter_timestamps, matcher_regex};
14use junobuild_shared::types::core::Blob;
15use junobuild_shared::types::list::ListParams;
16use junobuild_shared::types::state::{Controllers, UserId};
17use regex::Regex;
18
19pub fn map_asset_no_content(asset: &Asset) -> (FullPath, AssetNoContent) {
20    (asset.key.full_path.clone(), AssetNoContent::from(asset))
21}
22
23pub fn filter_values<'a>(
24    caller: Principal,
25    controllers: &'a Controllers,
26    permission: &'a Permission,
27    collection: CollectionKey,
28    ListParams {
29        matcher,
30        order: _,
31        paginate: _,
32        owner,
33    }: &'a ListParams,
34    assets: &'a [(&'a FullPath, &'a Asset)],
35    assertions: &impl StorageAssertionsStrategy,
36) -> Result<Vec<(&'a FullPath, &'a Asset)>, String> {
37    let (regex_key, regex_description) = matcher_regex(matcher)?;
38
39    let result = assets
40        .iter()
41        .filter_map(|(key, asset)| {
42            if filter_collection(collection.clone(), asset)
43                && filter_full_path(&regex_key, asset)
44                && filter_description(&regex_description, asset)
45                && filter_owner(*owner, asset)
46                && filter_timestamps(matcher, *asset)
47                && assertions.assert_list_permission(
48                    permission,
49                    asset.key.owner,
50                    caller,
51                    &collection,
52                    controllers,
53                )
54            {
55                Some((*key, *asset))
56            } else {
57                None
58            }
59        })
60        .collect();
61
62    Ok(result)
63}
64
65fn filter_full_path(regex: &Option<Regex>, asset: &Asset) -> bool {
66    match regex {
67        None => true,
68        Some(re) => re.is_match(&asset.key.full_path),
69    }
70}
71
72fn filter_description(regex: &Option<Regex>, asset: &Asset) -> bool {
73    match regex {
74        None => true,
75        Some(re) => match &asset.key.description {
76            None => false,
77            Some(description) => re.is_match(description),
78        },
79    }
80}
81
82fn filter_collection(collection: CollectionKey, asset: &Asset) -> bool {
83    asset.key.collection == collection
84}
85
86fn filter_owner(filter_owner: Option<UserId>, asset: &Asset) -> bool {
87    match filter_owner {
88        None => true,
89        Some(filter_owner) => filter_owner == asset.key.owner,
90    }
91}
92
93pub fn filter_collection_values<'a>(
94    collection: CollectionKey,
95    assets: &'a [(&'a FullPath, &'a Asset)],
96) -> Vec<(&'a FullPath, &'a Asset)> {
97    assets
98        .iter()
99        .filter_map(|(key, asset)| {
100            if filter_collection(collection.clone(), asset) {
101                Some((*key, *asset))
102            } else {
103                None
104            }
105        })
106        .collect()
107}
108
109pub fn get_token_protected_asset(
110    asset: &Asset,
111    asset_token: &String,
112    token: Option<String>,
113) -> Option<Asset> {
114    match token {
115        None => None,
116        Some(token) => {
117            if &token == asset_token {
118                return Some(asset.clone());
119            }
120
121            None
122        }
123    }
124}
125
126pub fn should_include_asset_for_deletion(collection: &CollectionKey, asset_path: &String) -> bool {
127    let excluded_paths = [
128        WELL_KNOWN_CUSTOM_DOMAINS.to_string(),
129        WELL_KNOWN_II_ALTERNATIVE_ORIGINS.to_string(),
130    ];
131
132    collection != COLLECTION_ASSET_KEY || !excluded_paths.contains(asset_path)
133}
134
135pub fn map_content_type_headers(content_type: &str) -> Vec<HeaderField> {
136    vec![HeaderField(
137        "content-type".to_string(),
138        content_type.to_string(),
139    )]
140}
141
142pub fn map_content_encoding(content: &Blob) -> AssetEncoding {
143    let max_chunk_size = 1_900_000; // Max 1.9 MB per chunk
144    let chunks = content
145        .chunks(max_chunk_size)
146        .map(|chunk| chunk.to_vec())
147        .collect();
148
149    AssetEncoding::from(&chunks)
150}
151
152pub fn create_asset_with_content(
153    content: &str,
154    headers: &[HeaderField],
155    existing_asset: Option<Asset>,
156    key: AssetKey,
157) -> Asset {
158    let mut asset: Asset = Asset::prepare(key, headers.to_vec(), &existing_asset);
159
160    let encoding = map_content_encoding(&content.as_bytes().to_vec());
161
162    asset
163        .encodings
164        .insert(ASSET_ENCODING_NO_COMPRESSION.to_string(), encoding);
165
166    asset
167}
168
169pub fn clone_asset_encoding_content_chunks(encoding: &AssetEncoding, chunk_index: usize) -> Blob {
170    encoding.content_chunks[chunk_index].clone()
171}