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}