cloudreve-api 0.9.0

A Rust library for interacting with Cloudreve API
Documentation
//! URI handling utilities for Cloudreve v4 API
//!
//! This module provides utilities for converting file paths to the Cloudreve URI format
//! and validating URIs according to the Cloudreve API specification.

/// Cloudreve URI prefix for user files
pub const CLOUDREVE_URI_PREFIX: &str = "cloudreve://my/";

/// Converts a file path to Cloudreve URI format
///
/// # Arguments
/// * `path` - File path (can be absolute, relative, or already a URI)
///
/// # Returns
/// A properly formatted Cloudreve URI string
///
/// # Examples
/// ```
/// use cloudreve_api::api::v4::uri::path_to_uri;
///
/// assert_eq!(path_to_uri("/path/to/file.txt"), "cloudreve://my/path/to/file.txt");
/// assert_eq!(path_to_uri("path/to/file.txt"), "cloudreve://my/path/to/file.txt");
/// assert_eq!(path_to_uri("cloudreve://my/path/to/file.txt"), "cloudreve://my/path/to/file.txt");
/// ```
pub fn path_to_uri(path: &str) -> String {
    // If already a valid Cloudreve URI, return as-is
    if path.starts_with("cloudreve://") {
        return path.to_string();
    }

    // Remove leading slash if present
    let trimmed_path = path.strip_prefix('/').unwrap_or(path);

    // Build URI
    format!("{}{}", CLOUDREVE_URI_PREFIX, trimmed_path)
}

/// Validates if a string is a properly formatted Cloudreve URI
///
/// # Arguments
/// * `uri` - URI string to validate
///
/// # Returns
/// `true` if the URI is valid, `false` otherwise
pub fn is_valid_uri(uri: &str) -> bool {
    uri.starts_with(CLOUDREVE_URI_PREFIX) && uri.len() >= CLOUDREVE_URI_PREFIX.len()
}

/// Extracts the path component from a Cloudreve URI
///
/// # Arguments
/// * `uri` - Cloudreve URI string
///
/// # Returns
/// The path component, or an error if the URI is invalid
pub fn uri_to_path(uri: &str) -> Result<&str, String> {
    if !uri.starts_with(CLOUDREVE_URI_PREFIX) {
        return Err(format!(
            "Invalid Cloudreve URI: expected format 'cloudreve://my/...', got: {}",
            uri
        ));
    }

    let path = &uri[CLOUDREVE_URI_PREFIX.len() - 1..];
    Ok(path)
}

/// Extracts the path from a Cloudreve URI and percent-decodes it
///
/// URIs carry their path segments encoded, so [`uri_to_path`] on its own hands
/// back things like `/photos/Screenshot%20-%20%E5%89%AF%E6%9C%AC.png`. Use this
/// whenever the path is headed for display or for an API expecting a plain path.
///
/// # Examples
/// ```
/// use cloudreve_api::api::v4::uri::uri_to_decoded_path;
///
/// assert_eq!(
///     uri_to_decoded_path("cloudreve://my/docs/a%20b.txt").unwrap(),
///     "/docs/a b.txt"
/// );
/// ```
pub fn uri_to_decoded_path(uri: &str) -> Result<String, String> {
    let raw = uri_to_path(uri)?;
    // Undecodable input falls back to the raw form: better ugly than dropped.
    Ok(urlencoding::decode(raw)
        .map(|decoded| decoded.into_owned())
        .unwrap_or_else(|_| raw.to_string()))
}

/// Converts multiple paths to URIs
///
/// # Arguments
/// * `paths` - Slice of path strings
///
/// # Returns
/// Vector of converted URIs
pub fn paths_to_uris(paths: &[&str]) -> Vec<String> {
    paths.iter().map(|p| path_to_uri(p)).collect()
}

/// Builds the URI that asks the server to search instead of list
///
/// Cloudreve expresses "search under this folder" as a URI carrying its own
/// query string. Since that query rides inside the `uri` parameter of the
/// request, every path segment is encoded here and the whole URI gets encoded
/// again by the caller — otherwise the inner `?` would terminate the outer
/// query and the server would see a plain listing request.
///
/// # Arguments
/// * `path` - Folder to search under; empty or "/" searches the whole drive
/// * `keyword` - Name fragment to look for
/// * `case_folding` - Match regardless of letter case
///
/// # Examples
/// ```
/// use cloudreve_api::api::v4::uri::search_uri;
///
/// assert_eq!(
///     search_uri("/photos", "cat", true),
///     "cloudreve://my/photos?name=cat&case_folding=true"
/// );
/// ```
pub fn search_uri(path: &str, keyword: &str, case_folding: bool) -> String {
    let scope = path
        .split('/')
        .filter(|segment| !segment.is_empty())
        .map(|segment| urlencoding::encode(segment).into_owned())
        .collect::<Vec<String>>()
        .join("/");
    format!(
        "{}{}?name={}&case_folding={}",
        CLOUDREVE_URI_PREFIX,
        scope,
        urlencoding::encode(keyword),
        case_folding
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_path_to_uri_absolute() {
        assert_eq!(
            path_to_uri("/path/to/file.txt"),
            "cloudreve://my/path/to/file.txt"
        );
    }

    #[test]
    fn test_path_to_uri_relative() {
        assert_eq!(
            path_to_uri("path/to/file.txt"),
            "cloudreve://my/path/to/file.txt"
        );
    }

    #[test]
    fn test_path_to_uri_already_uri() {
        assert_eq!(
            path_to_uri("cloudreve://my/path/to/file.txt"),
            "cloudreve://my/path/to/file.txt"
        );
    }

    #[test]
    fn test_path_to_uri_root() {
        assert_eq!(path_to_uri("/"), "cloudreve://my/");
    }

    #[test]
    fn test_is_valid_uri() {
        assert!(is_valid_uri("cloudreve://my/path/to/file.txt"));
        assert!(is_valid_uri("cloudreve://my/"));
        assert!(!is_valid_uri("/path/to/file.txt"));
        assert!(!is_valid_uri("path/to/file.txt"));
        assert!(!is_valid_uri("cloudreve://"));
    }

    #[test]
    fn test_uri_to_path() {
        assert_eq!(
            uri_to_path("cloudreve://my/path/to/file.txt").unwrap(),
            "/path/to/file.txt"
        );
        assert_eq!(uri_to_path("cloudreve://my/").unwrap(), "/");
    }

    #[test]
    fn test_uri_to_path_invalid() {
        assert!(uri_to_path("/path/to/file.txt").is_err());
        assert!(uri_to_path("cloudreve://").is_err());
    }

    #[test]
    fn test_paths_to_uris() {
        let paths = vec!["/file1.txt", "file2.txt", "cloudreve://my/file3.txt"];
        let uris = paths_to_uris(&paths);
        assert_eq!(
            uris,
            vec![
                "cloudreve://my/file1.txt",
                "cloudreve://my/file2.txt",
                "cloudreve://my/file3.txt"
            ]
        );
    }
}