cloudreve_sdk_api/models/
uri.rs1use 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
11const 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
24fn encode_uri_component(s: &str) -> String {
26 percent_encode(s.as_bytes(), ENCODE_URI_COMPONENT_SET).to_string()
27}
28
29fn 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
37pub const CR_URI_PREFIX: &str = "cloudreve://";
39const HTTP_URI_PREFIX: &str = "http://";
40
41pub 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
50pub 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
67pub 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#[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#[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
111impl From<url::ParseError> for UriError {
113 fn from(err: url::ParseError) -> Self {
114 UriError::ParseError(err.to_string())
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct CrUri {
121 url: Url,
122}
123
124impl CrUri {
125 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 let u = u.replace(CR_URI_PREFIX, HTTP_URI_PREFIX);
133 let mut url = Url::parse(&u)?;
134
135 let path = url.path().trim_end_matches('/').to_string();
137 url.set_path(&path);
138
139 Ok(Self { url })
140 }
141
142 pub fn id(&self) -> String {
144 self.url.username().to_string()
145 }
146
147 pub fn password(&self) -> String {
149 self.url.password().unwrap_or("").to_string()
150 }
151
152 pub fn is_search(&self) -> bool {
154 self.url.query_pairs().next().is_some()
155 }
156
157 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 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 pub fn set_search_param(&mut self, param: SearchParam) -> &mut Self {
176 self.url.set_query(None);
178
179 if let Some(names) = param.name {
180 if self.fs() == filesystem::TRASH {
181 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 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 pub fn path(&self) -> String {
330 decode_uri_component(self.url.path())
331 }
332
333 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 pub fn set_username(&mut self, username: &str) -> Result<&mut Self, ()> {
346 self.url.set_username(username)?;
347 Ok(self)
348 }
349
350 pub fn set_password(&mut self, password: &str) -> Result<&mut Self, ()> {
352 self.url.set_password(Some(password))?;
353 Ok(self)
354 }
355
356 pub fn path_trimmed(&self) -> String {
358 self.url.path().trim_start_matches('/').to_string()
359 }
360
361 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 pub fn join_raw(&mut self, raw_path: &str) -> &mut Self {
380 if raw_path.starts_with('/') {
381 self.url.set_path(raw_path);
383 } else {
384 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 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 pub fn is_root(&self) -> bool {
411 let path = self.url.path();
412 path.is_empty() || path == "/"
413 }
414
415 pub fn fs(&self) -> String {
417 self.url.host_str().unwrap_or("").to_string()
418 }
419
420 pub fn root_id(&self) -> String {
422 let user_id = "0"; format!("{}/{}/{}", self.fs(), self.url.username(), user_id)
425 }
426
427 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 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 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 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 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
497pub 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}