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}