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        let query = build_file_id_query(file_id, options);
66        self.fetch_preview(&query).await
67    }
68
69    /// Fetches a preview by file path (`file`).
70    ///
71    /// Sends `GET /index.php/core/preview.png?file={path}...`.
72    ///
73    /// Returns the raw preview image bytes on success.
74    ///
75    /// # Errors
76    /// Returns [`Error::Status`] if the server responds with a non-2xx status code,
77    /// or [`Error::Transport`] if the underlying network request fails.
78    pub async fn get_by_path(
79        &self,
80        path: &str,
81        options: &PreviewOptions,
82    ) -> Result<Vec<u8>, Error> {
83        let query = build_path_query(path, options);
84        self.fetch_preview(&query).await
85    }
86
87    async fn fetch_preview(&self, query: &str) -> Result<Vec<u8>, Error> {
88        let api_path = ApiPath::public(format!("{PREVIEW_ENDPOINT}{query}"))?;
89        let headers = self.authenticated_headers();
90        let request = Request::get(api_path.clone()).with_headers(headers);
91
92        let response = self.transport.execute(request).await?;
93        ensure_success(HttpMethod::Get, api_path, response)
94    }
95
96    fn authenticated_headers(&self) -> Headers {
97        let mut headers = Headers::new();
98        self.auth.authenticate(&mut headers);
99        headers
100    }
101}
102
103fn ensure_success(method: HttpMethod, path: ApiPath, response: Response) -> Result<Vec<u8>, Error> {
104    if response.status().is_success() {
105        Ok(response.into_body())
106    } else {
107        Err(Error::Status(StatusError::new(
108            method.as_str(),
109            path,
110            response.status(),
111            response.body(),
112        )))
113    }
114}