Skip to main content

cloudreve_sdk_api/models/
uri.rs

1use anyhow::Result;
2use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, NON_ALPHANUMERIC};
3use std::{
4    collections::HashMap,
5    fmt::{Display, Formatter},
6};
7use url::Url;
8
9use super::explorer::file_type;
10
11/// Character set for encoding that matches JavaScript's encodeURIComponent.
12/// encodeURIComponent escapes all characters except: A-Z a-z 0-9 - _ . ! ~ * ' ( )
13const ENCODE_URI_COMPONENT_SET: &AsciiSet = &NON_ALPHANUMERIC
14    .remove(b'-')
15    .remove(b'_')
16    .remove(b'.')
17    .remove(b'!')
18    .remove(b'~')
19    .remove(b'*')
20    .remove(b'\'')
21    .remove(b'(')
22    .remove(b')');
23
24/// Encode a string using the same rules as JavaScript's encodeURIComponent
25fn encode_uri_component(s: &str) -> String {
26    percent_encode(s.as_bytes(), ENCODE_URI_COMPONENT_SET).to_string()
27}
28
29/// Decode a percent-encoded string
30fn decode_uri_component(s: &str) -> String {
31    percent_decode_str(s)
32        .decode_utf8()
33        .unwrap_or_else(|_| s.into())
34        .to_string()
35}
36
37/// Cloudreve URI prefix
38pub const CR_URI_PREFIX: &str = "cloudreve://";
39const HTTP_URI_PREFIX: &str = "http://";
40
41/// Filesystem types
42pub mod filesystem {
43    pub const MY: &str = "my";
44    pub const SHARE: &str = "share";
45    pub const SHARED_BY_ME: &str = "shared_by_me";
46    pub const SHARED_WITH_ME: &str = "shared_with_me";
47    pub const TRASH: &str = "trash";
48}
49
50/// URI Query parameters
51pub mod uri_query {
52    pub const NAME: &str = "name";
53    pub const NAME_OP_OR: &str = "name_op_or";
54    pub const METADATA_PREFIX: &str = "meta_";
55    pub const METADATA_STRONG_MATCH: &str = "exact_meta_";
56    pub const CASE_FOLDING: &str = "case_folding";
57    pub const TYPE: &str = "type";
58    pub const CATEGORY: &str = "category";
59    pub const SIZE_GTE: &str = "size_gte";
60    pub const SIZE_LTE: &str = "size_lte";
61    pub const CREATED_GTE: &str = "created_gte";
62    pub const CREATED_LTE: &str = "created_lte";
63    pub const UPDATED_GTE: &str = "updated_gte";
64    pub const UPDATED_LTE: &str = "updated_lte";
65}
66
67/// Search categories
68pub mod uri_search_category {
69    pub const IMAGE: &str = "image";
70    pub const VIDEO: &str = "video";
71    pub const AUDIO: &str = "audio";
72    pub const DOCUMENT: &str = "document";
73}
74
75/// Search parameters
76#[derive(Debug, Clone, Default)]
77pub struct SearchParam {
78    pub name: Option<Vec<String>>,
79    pub name_op_or: Option<bool>,
80    pub metadata: Option<HashMap<String, String>>,
81    pub metadata_strong_match: Option<HashMap<String, String>>,
82    pub case_folding: Option<bool>,
83    pub category: Option<String>,
84    pub type_: Option<i32>,
85    pub size_gte: Option<u64>,
86    pub size_lte: Option<u64>,
87    pub created_at_gte: Option<i64>,
88    pub created_at_lte: Option<i64>,
89    pub updated_at_gte: Option<i64>,
90    pub updated_at_lte: Option<i64>,
91}
92
93/// Custom error type for URI operations
94#[derive(Debug, Clone)]
95pub enum UriError {
96    InvalidPrefix(String),
97    ParseError(String),
98}
99
100impl Display for UriError {
101    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102        match self {
103            UriError::InvalidPrefix(uri) => write!(f, "Invalid cloudreve URI prefix: {}", uri),
104            UriError::ParseError(msg) => write!(f, "URI parse error: {}", msg),
105        }
106    }
107}
108
109impl std::error::Error for UriError {}
110
111// Implement conversion from url::ParseError
112impl From<url::ParseError> for UriError {
113    fn from(err: url::ParseError) -> Self {
114        UriError::ParseError(err.to_string())
115    }
116}
117
118/// Cloudreve URI
119#[derive(Debug, Clone)]
120pub struct CrUri {
121    url: Url,
122}
123
124impl CrUri {
125    /// Create a new CrUri from a string
126    pub fn new(u: &str) -> Result<Self> {
127        if !u.starts_with(CR_URI_PREFIX) {
128            return Err(anyhow::anyhow!("Invalid cloudreve URI prefix: {}", u));
129        }
130
131        // Replace prefix with standard HTTP for compatibility
132        let u = u.replace(CR_URI_PREFIX, HTTP_URI_PREFIX);
133        let mut url = Url::parse(&u)?;
134
135        // Remove ending slash if present
136        let path = url.path().trim_end_matches('/').to_string();
137        url.set_path(&path);
138
139        Ok(Self { url })
140    }
141
142    /// Get the ID (username) from the URI
143    pub fn id(&self) -> String {
144        self.url.username().to_string()
145    }
146
147    /// Get the password from the URI
148    pub fn password(&self) -> String {
149        self.url.password().unwrap_or("").to_string()
150    }
151
152    /// Check if the URI has search parameters
153    pub fn is_search(&self) -> bool {
154        self.url.query_pairs().next().is_some()
155    }
156
157    /// Get all values for a query parameter
158    pub fn query(&self, key: &str) -> Vec<String> {
159        self.url
160            .query_pairs()
161            .filter(|(k, _)| k == key)
162            .map(|(_, v)| v.to_string())
163            .collect()
164    }
165
166    /// Add a query parameter
167    pub fn add_query(&mut self, key: &str, value: &str) -> &mut Self {
168        {
169            self.url.query_pairs_mut().append_pair(key, value);
170        }
171        self
172    }
173
174    /// Set search parameters
175    pub fn set_search_param(&mut self, param: SearchParam) -> &mut Self {
176        // Clear all existing query parameters
177        self.url.set_query(None);
178
179        if let Some(names) = param.name {
180            if self.fs() == filesystem::TRASH {
181                // In trash, search by restore_uri metadata
182                let encoded = urlencoding::encode(&names.join(" ")).to_string();
183                self.add_query(
184                    &format!("{}restore_uri", uri_query::METADATA_PREFIX),
185                    &encoded,
186                );
187            } else {
188                for name in names {
189                    self.add_query(uri_query::NAME, &name);
190                }
191            }
192        }
193
194        if param.name_op_or.unwrap_or(false) {
195            self.add_query(uri_query::NAME_OP_OR, "");
196        }
197
198        if param.case_folding.unwrap_or(false) {
199            self.add_query(uri_query::CASE_FOLDING, "");
200        }
201
202        if let Some(category) = param.category {
203            self.add_query(uri_query::CATEGORY, &category);
204        }
205
206        if let Some(type_) = param.type_ {
207            let type_str = if type_ == file_type::FOLDER {
208                "folder"
209            } else {
210                "file"
211            };
212            self.add_query(uri_query::TYPE, type_str);
213        }
214
215        if let Some(metadata) = param.metadata {
216            for (k, v) in metadata {
217                self.add_query(&format!("{}{}", uri_query::METADATA_PREFIX, k), &v);
218            }
219        }
220
221        if let Some(metadata_strong_match) = param.metadata_strong_match {
222            for (k, v) in metadata_strong_match {
223                self.add_query(&format!("{}{}", uri_query::METADATA_STRONG_MATCH, k), &v);
224            }
225        }
226
227        if let Some(size_gte) = param.size_gte {
228            self.add_query(uri_query::SIZE_GTE, &size_gte.to_string());
229        }
230
231        if let Some(size_lte) = param.size_lte {
232            self.add_query(uri_query::SIZE_LTE, &size_lte.to_string());
233        }
234
235        if let Some(created_at_gte) = param.created_at_gte {
236            self.add_query(uri_query::CREATED_GTE, &created_at_gte.to_string());
237        }
238
239        if let Some(created_at_lte) = param.created_at_lte {
240            self.add_query(uri_query::CREATED_LTE, &created_at_lte.to_string());
241        }
242
243        if let Some(updated_at_gte) = param.updated_at_gte {
244            self.add_query(uri_query::UPDATED_GTE, &updated_at_gte.to_string());
245        }
246
247        if let Some(updated_at_lte) = param.updated_at_lte {
248            self.add_query(uri_query::UPDATED_LTE, &updated_at_lte.to_string());
249        }
250
251        self
252    }
253
254    /// Get search parameters from the URI
255    pub fn search_params(&self) -> Option<SearchParam> {
256        if !self.is_search() {
257            return None;
258        }
259
260        let mut res = SearchParam::default();
261
262        for (k, v) in self.url.query_pairs() {
263            match k.as_ref() {
264                uri_query::NAME => {
265                    if res.name.is_none() {
266                        res.name = Some(Vec::new());
267                    }
268                    res.name.as_mut().unwrap().push(v.to_string());
269                }
270                uri_query::NAME_OP_OR => {
271                    res.name_op_or = Some(true);
272                }
273                uri_query::CASE_FOLDING => {
274                    res.case_folding = Some(true);
275                }
276                uri_query::CATEGORY => {
277                    res.category = Some(v.to_string());
278                }
279                uri_query::TYPE => {
280                    res.type_ = Some(if v == "file" {
281                        file_type::FILE
282                    } else {
283                        file_type::FOLDER
284                    });
285                }
286                uri_query::SIZE_GTE => {
287                    res.size_gte = v.parse().ok();
288                }
289                uri_query::SIZE_LTE => {
290                    res.size_lte = v.parse().ok();
291                }
292                uri_query::CREATED_GTE => {
293                    res.created_at_gte = v.parse().ok();
294                }
295                uri_query::CREATED_LTE => {
296                    res.created_at_lte = v.parse().ok();
297                }
298                uri_query::UPDATED_GTE => {
299                    res.updated_at_gte = v.parse().ok();
300                }
301                uri_query::UPDATED_LTE => {
302                    res.updated_at_lte = v.parse().ok();
303                }
304                _ => {
305                    if k.starts_with(uri_query::METADATA_PREFIX) {
306                        if res.metadata.is_none() {
307                            res.metadata = Some(HashMap::new());
308                        }
309                        let key = k[uri_query::METADATA_PREFIX.len()..].to_string();
310                        res.metadata.as_mut().unwrap().insert(key, v.to_string());
311                    } else if k.starts_with(uri_query::METADATA_STRONG_MATCH) {
312                        if res.metadata_strong_match.is_none() {
313                            res.metadata_strong_match = Some(HashMap::new());
314                        }
315                        let key = k[uri_query::METADATA_STRONG_MATCH.len()..].to_string();
316                        res.metadata_strong_match
317                            .as_mut()
318                            .unwrap()
319                            .insert(key, v.to_string());
320                    }
321                }
322            }
323        }
324
325        Some(res)
326    }
327
328    /// Get the path from the URI (decoded)
329    pub fn path(&self) -> String {
330        decode_uri_component(self.url.path())
331    }
332
333    /// Set the path of the URI
334    pub fn set_path(&mut self, path: &str) -> &mut Self {
335        let encoded_segments: Vec<String> = path
336            .split('/')
337            .map(|p| encode_uri_component(p))
338            .collect();
339        let encoded_path = encoded_segments.join("/");
340        self.url.set_path(&encoded_path);
341        self
342    }
343
344    /// Set the username
345    pub fn set_username(&mut self, username: &str) -> Result<&mut Self, ()> {
346        self.url.set_username(username)?;
347        Ok(self)
348    }
349
350    /// Set the password
351    pub fn set_password(&mut self, password: &str) -> Result<&mut Self, ()> {
352        self.url.set_password(Some(password))?;
353        Ok(self)
354    }
355
356    /// Get the path without leading slash
357    pub fn path_trimmed(&self) -> String {
358        self.url.path().trim_start_matches('/').to_string()
359    }
360
361    /// Join paths to the current path
362    pub fn join(&mut self, paths: &[&str]) -> &mut Self {
363        let current_path = self.url.path();
364        let mut result = current_path.to_string();
365
366        for p in paths {
367            let encoded = encode_uri_component(p);
368            if !result.ends_with('/') {
369                result.push('/');
370            }
371            result.push_str(&encoded);
372        }
373
374        self.url.set_path(&result);
375        self
376    }
377
378    /// Join a raw path (may be absolute or relative)
379    pub fn join_raw(&mut self, raw_path: &str) -> &mut Self {
380        if raw_path.starts_with('/') {
381            // Absolute path - replace the entire pathname
382            self.url.set_path(raw_path);
383        } else {
384            // Relative path - resolve it against the current path
385            let current = self.url.path().trim_end_matches('/');
386            let new_path = if current.is_empty() {
387                format!("/{}", raw_path)
388            } else {
389                format!("{}/{}", current, raw_path)
390            };
391            self.url.set_path(&new_path);
392        }
393        self
394    }
395
396    /// Get path elements as a vector of strings
397    pub fn elements(&self) -> Vec<String> {
398        let trimmed = self.path_trimmed();
399        if trimmed.is_empty() {
400            return Vec::new();
401        }
402
403        trimmed
404            .split('/')
405            .map(|p| decode_uri_component(p))
406            .collect()
407    }
408
409    /// Check if the URI is pointing to root
410    pub fn is_root(&self) -> bool {
411        let path = self.url.path();
412        path.is_empty() || path == "/"
413    }
414
415    /// Get the filesystem type (hostname)
416    pub fn fs(&self) -> String {
417        self.url.host_str().unwrap_or("").to_string()
418    }
419
420    /// Get the root ID
421    pub fn root_id(&self) -> String {
422        // TODO: Implement SessionManager integration
423        let user_id = "0"; // SessionManager.currentLoginOrNull()?.user.id ?? "0"
424        format!("{}/{}/{}", self.fs(), self.url.username(), user_id)
425    }
426
427    /// Get the base URI (without path)
428    pub fn base(&self, exclude_search: bool) -> String {
429        let mut new_url = self.url.clone();
430        new_url.set_path("");
431        if exclude_search {
432            new_url.set_query(None);
433        }
434
435        new_url
436            .to_string()
437            .replace(HTTP_URI_PREFIX, CR_URI_PREFIX)
438            .trim_end_matches('/')
439            .to_string()
440    }
441
442    /// Returns the URI without searching query string, with exceptions
443    pub fn pure_uri(&self, exceptions: &[&str]) -> Result<CrUri> {
444        let mut new_uri = CrUri::new(&self.to_string())?;
445
446        let keys_for_del: Vec<String> = new_uri
447            .url
448            .query_pairs()
449            .filter(|(k, _)| !exceptions.contains(&k.as_ref()))
450            .map(|(k, _)| k.to_string())
451            .collect();
452
453        for key in keys_for_del {
454            // Create a new query string without the deleted keys
455            let filtered_pairs: Vec<(String, String)> = new_uri
456                .url
457                .query_pairs()
458                .filter(|(k, _)| k != &key)
459                .map(|(k, v)| (k.to_string(), v.to_string()))
460                .collect();
461
462            new_uri.url.set_query(None);
463            for (k, v) in filtered_pairs {
464                new_uri.add_query(&k, &v);
465            }
466        }
467
468        Ok(new_uri)
469    }
470
471    /// Get the parent URI
472    pub fn parent(&self) -> Result<CrUri> {
473        let mut new_uri = CrUri::new(&self.to_string())?;
474        let mut path = new_uri.elements();
475        path.pop();
476
477        let new_path = if !path.is_empty() {
478            format!("/{}", path.join("/"))
479        } else {
480            String::new()
481        };
482
483        new_uri.set_path(&new_path);
484        Ok(new_uri)
485    }
486
487    /// Convert the URI to a string
488    pub fn to_string(&self) -> String {
489        self.url
490            .to_string()
491            .replace(HTTP_URI_PREFIX, CR_URI_PREFIX)
492            .trim_end_matches('/')
493            .to_string()
494    }
495}
496
497/// Create a new "my" filesystem URI
498pub fn new_my_uri(uid: Option<&str>) -> Result<CrUri> {
499    match uid {
500        Some(uid) => CrUri::new(&format!("cloudreve://{}@my", uid)),
501        None => CrUri::new("cloudreve://my"),
502    }
503}