Skip to main content

nextcloud_preview/
client.rs

1//! Asynchronous client for Nextcloud's Preview API.
2
3use nextcloud_sdk_core::{
4    ApiPath, AuthProvider, Error, Headers, HttpMethod, Request, Response, StatusError, Transport,
5};
6
7use crate::options::PreviewOptions;
8use crate::query::{build_file_id_query, build_path_query};
9
10#[cfg(test)]
11mod tests;
12
13/// Relative path to Nextcloud's preview endpoint beneath `/index.php`.
14pub const PREVIEW_ENDPOINT: &str = "core/preview.png";
15
16/// Client for fetching image previews and thumbnails from Nextcloud.
17///
18/// Executes calls through any [`Transport`] and applies credentials via an [`AuthProvider`].
19/// Both are held by reference, allowing the client to be freely cloned or borrowed across
20/// tasks without runtime dispatch or connection pooling overhead.
21///
22/// Previews are generated by Nextcloud's core preview endpoint at `/core/preview.png`
23/// (accessed via `/index.php/core/preview.png`).
24///
25/// Reference:
26/// - Official Nextcloud Preview endpoint:
27///   `GET /core/preview.png?fileId={fileId}&x={x}&y={y}&a={a}&mode={mode}`
28///   or `GET /core/preview.png?file={filePath}&x={x}&y={y}`
29#[derive(Clone, Copy, Debug)]
30pub struct PreviewClient<'a, T: Transport, A: AuthProvider> {
31    transport: &'a T,
32    auth: &'a A,
33}
34
35impl<'a, T: Transport, A: AuthProvider> PreviewClient<'a, T, A> {
36    /// Binds a preview client to a transport and credential provider.
37    pub const fn new(transport: &'a T, auth: &'a A) -> Self {
38        Self { transport, auth }
39    }
40
41    /// The transport this client sends requests through.
42    pub const fn transport(&self) -> &'a T {
43        self.transport
44    }
45
46    /// The credential provider applied to every call.
47    pub const fn auth(&self) -> &'a A {
48        self.auth
49    }
50
51    /// Fetches a preview by numeric file cache ID (`fileId`).
52    ///
53    /// Sends `GET /index.php/core/preview.png?fileId={file_id}...`.
54    ///
55    /// Returns the raw preview image bytes on success.
56    ///
57    /// # Errors
58    /// Returns [`Error::Status`] if the server responds with a non-2xx status code,
59    /// or [`Error::Transport`] if the underlying network request fails.
60    pub async fn get_by_file_id(
61        &self,
62        file_id: u64,
63        options: &PreviewOptions,
64    ) -> Result<Vec<u8>, Error> {
65        tracing::debug!(file_id, "Fetching file preview by ID");
66        let query = build_file_id_query(file_id, options);
67        self.fetch_preview(&query).await
68    }
69
70    /// Fetches a preview by file path (`file`).
71    ///
72    /// Sends `GET /index.php/core/preview.png?file={path}...`.
73    ///
74    /// Returns the raw preview image bytes on success.
75    ///
76    /// # Errors
77    /// Returns [`Error::Status`] if the server responds with a non-2xx status code,
78    /// or [`Error::Transport`] if the underlying network request fails.
79    pub async fn get_by_path(
80        &self,
81        path: &str,
82        options: &PreviewOptions,
83    ) -> Result<Vec<u8>, Error> {
84        tracing::debug!(path, "Fetching file preview by path");
85        let query = build_path_query(path, options);
86        self.fetch_preview(&query).await
87    }
88
89    async fn fetch_preview(&self, query: &str) -> Result<Vec<u8>, Error> {
90        let api_path = ApiPath::public(format!("{PREVIEW_ENDPOINT}{query}"))?;
91        let headers = self.authenticated_headers();
92        let request = Request::get(api_path.clone()).with_headers(headers);
93
94        let response = self.transport.execute(request).await?;
95        ensure_success(HttpMethod::Get, api_path, response)
96    }
97
98    fn authenticated_headers(&self) -> Headers {
99        let mut headers = Headers::new();
100        self.auth.authenticate(&mut headers);
101        headers
102    }
103}
104
105fn ensure_success(method: HttpMethod, path: ApiPath, response: Response) -> Result<Vec<u8>, Error> {
106    if response.status().is_success() {
107        Ok(response.into_body())
108    } else {
109        tracing::error!(
110            method = ?method,
111            path = %path.as_str(),
112            status = %response.status(),
113            "Preview request failed"
114        );
115        Err(Error::Status(StatusError::new(
116            method.as_str(),
117            path,
118            response.status(),
119            response.body(),
120        )))
121    }
122}