nextcloud-preview 0.1.3

Nextcloud preview and thumbnail generation client
Documentation
//! Asynchronous client for Nextcloud's Preview API.

use nextcloud_sdk_core::{
    ApiPath, AuthProvider, Error, Headers, HttpMethod, Request, Response, StatusError, Transport,
};

use crate::options::PreviewOptions;
use crate::query::{build_file_id_query, build_path_query};

#[cfg(test)]
mod tests;

/// Relative path to Nextcloud's preview endpoint beneath `/index.php`.
pub const PREVIEW_ENDPOINT: &str = "core/preview.png";

/// Client for fetching image previews and thumbnails from Nextcloud.
///
/// Executes calls through any [`Transport`] and applies credentials via an [`AuthProvider`].
/// Both are held by reference, allowing the client to be freely cloned or borrowed across
/// tasks without runtime dispatch or connection pooling overhead.
///
/// Previews are generated by Nextcloud's core preview endpoint at `/core/preview.png`
/// (accessed via `/index.php/core/preview.png`).
///
/// Reference:
/// - Official Nextcloud Preview endpoint:
///   `GET /core/preview.png?fileId={fileId}&x={x}&y={y}&a={a}&mode={mode}`
///   or `GET /core/preview.png?file={filePath}&x={x}&y={y}`
#[derive(Clone, Copy, Debug)]
pub struct PreviewClient<'a, T: Transport, A: AuthProvider> {
    transport: &'a T,
    auth: &'a A,
}

impl<'a, T: Transport, A: AuthProvider> PreviewClient<'a, T, A> {
    /// Binds a preview client to a transport and credential provider.
    pub const fn new(transport: &'a T, auth: &'a A) -> Self {
        Self { transport, auth }
    }

    /// The transport this client sends requests through.
    pub const fn transport(&self) -> &'a T {
        self.transport
    }

    /// The credential provider applied to every call.
    pub const fn auth(&self) -> &'a A {
        self.auth
    }

    /// Fetches a preview by numeric file cache ID (`fileId`).
    ///
    /// Sends `GET /index.php/core/preview.png?fileId={file_id}...`.
    ///
    /// Returns the raw preview image bytes on success.
    ///
    /// # Errors
    /// Returns [`Error::Status`] if the server responds with a non-2xx status code,
    /// or [`Error::Transport`] if the underlying network request fails.
    pub async fn get_by_file_id(
        &self,
        file_id: u64,
        options: &PreviewOptions,
    ) -> Result<Vec<u8>, Error> {
        tracing::debug!(file_id, "Fetching file preview by ID");
        let query = build_file_id_query(file_id, options);
        self.fetch_preview(&query).await
    }

    /// Fetches a preview by file path (`file`).
    ///
    /// Sends `GET /index.php/core/preview.png?file={path}...`.
    ///
    /// Returns the raw preview image bytes on success.
    ///
    /// # Errors
    /// Returns [`Error::Status`] if the server responds with a non-2xx status code,
    /// or [`Error::Transport`] if the underlying network request fails.
    pub async fn get_by_path(
        &self,
        path: &str,
        options: &PreviewOptions,
    ) -> Result<Vec<u8>, Error> {
        tracing::debug!(path, "Fetching file preview by path");
        let query = build_path_query(path, options);
        self.fetch_preview(&query).await
    }

    async fn fetch_preview(&self, query: &str) -> Result<Vec<u8>, Error> {
        let api_path = ApiPath::public(format!("{PREVIEW_ENDPOINT}{query}"))?;
        let headers = self.authenticated_headers();
        let request = Request::get(api_path.clone()).with_headers(headers);

        let response = self.transport.execute(request).await?;
        ensure_success(HttpMethod::Get, api_path, response)
    }

    fn authenticated_headers(&self) -> Headers {
        let mut headers = Headers::new();
        self.auth.authenticate(&mut headers);
        headers
    }
}

fn ensure_success(method: HttpMethod, path: ApiPath, response: Response) -> Result<Vec<u8>, Error> {
    if response.status().is_success() {
        Ok(response.into_body())
    } else {
        tracing::error!(
            method = ?method,
            path = %path.as_str(),
            status = %response.status(),
            "Preview request failed"
        );
        Err(Error::Status(StatusError::new(
            method.as_str(),
            path,
            response.status(),
            response.body(),
        )))
    }
}