Skip to main content

cloudreve_api/api/v4/
uri.rs

1//! URI handling utilities for Cloudreve v4 API
2//!
3//! This module provides utilities for converting file paths to the Cloudreve URI format
4//! and validating URIs according to the Cloudreve API specification.
5
6/// Cloudreve URI prefix for user files
7pub const CLOUDREVE_URI_PREFIX: &str = "cloudreve://my/";
8
9/// Converts a file path to Cloudreve URI format
10///
11/// # Arguments
12/// * `path` - File path (can be absolute, relative, or already a URI)
13///
14/// # Returns
15/// A properly formatted Cloudreve URI string
16///
17/// # Examples
18/// ```
19/// use cloudreve_api::api::v4::uri::path_to_uri;
20///
21/// assert_eq!(path_to_uri("/path/to/file.txt"), "cloudreve://my/path/to/file.txt");
22/// assert_eq!(path_to_uri("path/to/file.txt"), "cloudreve://my/path/to/file.txt");
23/// assert_eq!(path_to_uri("cloudreve://my/path/to/file.txt"), "cloudreve://my/path/to/file.txt");
24/// ```
25pub fn path_to_uri(path: &str) -> String {
26    // If already a valid Cloudreve URI, return as-is
27    if path.starts_with("cloudreve://") {
28        return path.to_string();
29    }
30
31    // Remove leading slash if present
32    let trimmed_path = path.strip_prefix('/').unwrap_or(path);
33
34    // Build URI
35    format!("{}{}", CLOUDREVE_URI_PREFIX, trimmed_path)
36}
37
38/// Validates if a string is a properly formatted Cloudreve URI
39///
40/// # Arguments
41/// * `uri` - URI string to validate
42///
43/// # Returns
44/// `true` if the URI is valid, `false` otherwise
45pub fn is_valid_uri(uri: &str) -> bool {
46    uri.starts_with(CLOUDREVE_URI_PREFIX) && uri.len() >= CLOUDREVE_URI_PREFIX.len()
47}
48
49/// Extracts the path component from a Cloudreve URI
50///
51/// # Arguments
52/// * `uri` - Cloudreve URI string
53///
54/// # Returns
55/// The path component, or an error if the URI is invalid
56pub fn uri_to_path(uri: &str) -> Result<&str, String> {
57    if !uri.starts_with(CLOUDREVE_URI_PREFIX) {
58        return Err(format!(
59            "Invalid Cloudreve URI: expected format 'cloudreve://my/...', got: {}",
60            uri
61        ));
62    }
63
64    let path = &uri[CLOUDREVE_URI_PREFIX.len() - 1..];
65    Ok(path)
66}
67
68/// Extracts the path from a Cloudreve URI and percent-decodes it
69///
70/// URIs carry their path segments encoded, so [`uri_to_path`] on its own hands
71/// back things like `/photos/Screenshot%20-%20%E5%89%AF%E6%9C%AC.png`. Use this
72/// whenever the path is headed for display or for an API expecting a plain path.
73///
74/// # Examples
75/// ```
76/// use cloudreve_api::api::v4::uri::uri_to_decoded_path;
77///
78/// assert_eq!(
79///     uri_to_decoded_path("cloudreve://my/docs/a%20b.txt").unwrap(),
80///     "/docs/a b.txt"
81/// );
82/// ```
83pub fn uri_to_decoded_path(uri: &str) -> Result<String, String> {
84    let raw = uri_to_path(uri)?;
85    // Undecodable input falls back to the raw form: better ugly than dropped.
86    Ok(urlencoding::decode(raw)
87        .map(|decoded| decoded.into_owned())
88        .unwrap_or_else(|_| raw.to_string()))
89}
90
91/// Converts multiple paths to URIs
92///
93/// # Arguments
94/// * `paths` - Slice of path strings
95///
96/// # Returns
97/// Vector of converted URIs
98pub fn paths_to_uris(paths: &[&str]) -> Vec<String> {
99    paths.iter().map(|p| path_to_uri(p)).collect()
100}
101
102/// Builds the URI that asks the server to search instead of list
103///
104/// Cloudreve expresses "search under this folder" as a URI carrying its own
105/// query string. Since that query rides inside the `uri` parameter of the
106/// request, every path segment is encoded here and the whole URI gets encoded
107/// again by the caller — otherwise the inner `?` would terminate the outer
108/// query and the server would see a plain listing request.
109///
110/// # Arguments
111/// * `path` - Folder to search under; empty or "/" searches the whole drive
112/// * `keyword` - Name fragment to look for
113/// * `case_folding` - Match regardless of letter case
114///
115/// # Examples
116/// ```
117/// use cloudreve_api::api::v4::uri::search_uri;
118///
119/// assert_eq!(
120///     search_uri("/photos", "cat", true),
121///     "cloudreve://my/photos?name=cat&case_folding=true"
122/// );
123/// ```
124pub fn search_uri(path: &str, keyword: &str, case_folding: bool) -> String {
125    let scope = path
126        .split('/')
127        .filter(|segment| !segment.is_empty())
128        .map(|segment| urlencoding::encode(segment).into_owned())
129        .collect::<Vec<String>>()
130        .join("/");
131    format!(
132        "{}{}?name={}&case_folding={}",
133        CLOUDREVE_URI_PREFIX,
134        scope,
135        urlencoding::encode(keyword),
136        case_folding
137    )
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_path_to_uri_absolute() {
146        assert_eq!(
147            path_to_uri("/path/to/file.txt"),
148            "cloudreve://my/path/to/file.txt"
149        );
150    }
151
152    #[test]
153    fn test_path_to_uri_relative() {
154        assert_eq!(
155            path_to_uri("path/to/file.txt"),
156            "cloudreve://my/path/to/file.txt"
157        );
158    }
159
160    #[test]
161    fn test_path_to_uri_already_uri() {
162        assert_eq!(
163            path_to_uri("cloudreve://my/path/to/file.txt"),
164            "cloudreve://my/path/to/file.txt"
165        );
166    }
167
168    #[test]
169    fn test_path_to_uri_root() {
170        assert_eq!(path_to_uri("/"), "cloudreve://my/");
171    }
172
173    #[test]
174    fn test_is_valid_uri() {
175        assert!(is_valid_uri("cloudreve://my/path/to/file.txt"));
176        assert!(is_valid_uri("cloudreve://my/"));
177        assert!(!is_valid_uri("/path/to/file.txt"));
178        assert!(!is_valid_uri("path/to/file.txt"));
179        assert!(!is_valid_uri("cloudreve://"));
180    }
181
182    #[test]
183    fn test_uri_to_path() {
184        assert_eq!(
185            uri_to_path("cloudreve://my/path/to/file.txt").unwrap(),
186            "/path/to/file.txt"
187        );
188        assert_eq!(uri_to_path("cloudreve://my/").unwrap(), "/");
189    }
190
191    #[test]
192    fn test_uri_to_path_invalid() {
193        assert!(uri_to_path("/path/to/file.txt").is_err());
194        assert!(uri_to_path("cloudreve://").is_err());
195    }
196
197    #[test]
198    fn test_paths_to_uris() {
199        let paths = vec!["/file1.txt", "file2.txt", "cloudreve://my/file3.txt"];
200        let uris = paths_to_uris(&paths);
201        assert_eq!(
202            uris,
203            vec![
204                "cloudreve://my/file1.txt",
205                "cloudreve://my/file2.txt",
206                "cloudreve://my/file3.txt"
207            ]
208        );
209    }
210}