Skip to main content

cloudreve_api/api/v4/
file.rs

1//! File-related API endpoints for Cloudreve v4 API
2
3use crate::Error;
4use crate::api::v4::ApiV4Client;
5use crate::api::v4::models::*;
6use crate::api::v4::uri::*;
7
8/// File management methods
9impl ApiV4Client {
10    pub async fn upload_file(&self, request: &UploadRequest<'_>) -> Result<File, Error> {
11        let response: ApiResponse<File> = self.post("/file", request).await?;
12        match response.data {
13            Some(data) => Ok(data),
14            None => Err(Error::InvalidResponse(format!(
15                "API returned no data for upload_file request: {:?}",
16                response
17            ))),
18        }
19    }
20
21    /// Lists files in a directory with full response including metadata
22    ///
23    /// Returns a complete `ListResponse` containing:
24    /// - `files`: Vector of files/folders in the directory
25    /// - `parent`: Information about the parent directory
26    /// - `pagination`: Pagination information (page, total_items, etc.)
27    /// - `storage_policy`: Preferred storage policy for uploads to this directory
28    /// - `props`: Navigator capabilities and settings
29    ///
30    /// # Arguments
31    /// * `request` - ListFilesRequest with path and optional pagination params
32    pub async fn list_files(&self, request: &ListFilesRequest<'_>) -> Result<ListResponse, Error> {
33        let mut url = "/file".to_string();
34        // The URI has to be percent-encoded before it goes into the query. A search
35        // URI carries its own `?name=...`; left raw, that inner `?` would cut the
36        // outer query short and the server would answer with a plain listing.
37        let uri = match request.path.strip_prefix('/') {
38            Some(path) => path_to_uri(path),
39            None => path_to_uri(request.path),
40        };
41        url.push_str(&format!("?uri={}", urlencoding::encode(&uri)));
42        if let Some(page) = request.page {
43            url.push_str(&format!("&page={}", page));
44        }
45        if let Some(page_size) = request.page_size {
46            url.push_str(&format!("&page_size={}", page_size));
47        }
48        if let Some(order_by) = request.order_by {
49            url.push_str(&format!("&order_by={}", order_by));
50        }
51        if let Some(order_direction) = request.order_direction {
52            url.push_str(&format!("&order_direction={}", order_direction));
53        }
54        if let Some(next_page_token) = request.next_page_token {
55            url.push_str(&format!(
56                "&next_page_token={}",
57                urlencoding::encode(next_page_token)
58            ));
59        }
60
61        let response: ApiResponse<ListResponse> = self.get(&url).await?;
62        match response.data {
63            Some(data) => Ok(data),
64            None => Err(Error::InvalidResponse(format!(
65                "API returned no data for list_files request: {:?}",
66                response
67            ))),
68        }
69    }
70
71    pub async fn get_file_info(&self, file_path: &str) -> Result<File, Error> {
72        // URI encode the path for V4 API, use /file/info endpoint
73        let uri = path_to_uri(file_path);
74        let response: ApiResponse<File> = self.get(&format!("/file/info?uri={}", uri)).await?;
75        match response.data {
76            Some(data) => Ok(data),
77            None => Err(Error::InvalidResponse(format!(
78                "API returned no data for get_file_info request: {:?}",
79                response
80            ))),
81        }
82    }
83
84    pub async fn get_file_stat(&self, file_path: &str) -> Result<FileStat, Error> {
85        let response: ApiResponse<FileStat> =
86            self.get(&format!("/file/stat/{}", file_path)).await?;
87        match response.data {
88            Some(data) => Ok(data),
89            None => Err(Error::InvalidResponse(format!(
90                "API returned no data for get_file_stat request: {:?}",
91                response
92            ))),
93        }
94    }
95
96    /// Search for files by name under a folder.
97    ///
98    /// Returns the same shape as [`ApiV4Client::list_files`], so paging works
99    /// identically — carry `pagination.next_token` into the next call.
100    pub async fn search_files(
101        &self,
102        request: &SearchFilesRequest<'_>,
103    ) -> Result<ListResponse, Error> {
104        let uri = search_uri(request.path, request.keyword, request.case_folding);
105        self.list_files(&ListFilesRequest {
106            path: &uri,
107            page: request.page,
108            page_size: request.page_size,
109            order_by: None,
110            order_direction: None,
111            next_page_token: request.next_page_token,
112        })
113        .await
114    }
115
116    /// Move any number of files or folders to a destination directory.
117    ///
118    /// Like [`ApiV4Client::delete_files`], the endpoint is inherently batched:
119    /// a partial failure comes back as [`Error::Aggregate`] keyed by source
120    /// URI, so callers can tell which items moved without re-probing.
121    pub async fn move_file(&self, request: &MoveFileRequest<'_>) -> Result<(), Error> {
122        let response: ApiResponse<serde_json::Value> = self.post("/file/move", request).await?;
123        match response.code {
124            0 => Ok(()),
125            _ => Err(response.into_error()),
126        }
127    }
128
129    /// Copy any number of files or folders to a destination directory.
130    ///
131    /// Reports partial failures the same way [`ApiV4Client::move_file`] does.
132    pub async fn copy_file(&self, request: &CopyFileRequest<'_>) -> Result<(), Error> {
133        // V4 API uses /file/move endpoint with copy=true for copy operations
134        let move_request = MoveFileRequest {
135            uris: request.uris.clone(),
136            dst: request.dst,
137            copy: Some(true),
138        };
139        self.move_file(&move_request).await
140    }
141
142    pub async fn rename_file(
143        &self,
144        request: &RenameFileRequest<'_>,
145    ) -> Result<crate::api::v4::models::File, Error> {
146        let response: ApiResponse<crate::api::v4::models::File> =
147            self.post("/file/rename", request).await?;
148        match response.data {
149            Some(data) => Ok(data),
150            None => Err(Error::InvalidResponse(format!(
151                "API returned no data for rename_file request: {:?}",
152                response
153            ))),
154        }
155    }
156
157    pub async fn delete_file(&self, file_path: &str) -> Result<(), Error> {
158        let request = DeleteFileRequest {
159            uris: vec![file_path],
160            unlink: None,
161            skip_soft_delete: None,
162        };
163        self.delete_files(&request).await
164    }
165
166    /// Delete any number of files or folders in a single request.
167    ///
168    /// The endpoint takes a list, so deleting N items costs one round trip
169    /// rather than N. Failures keep their structure instead of collapsing into
170    /// a bare code:
171    ///
172    /// - a partial failure returns [`Error::Aggregate`], keyed by the URI that
173    ///   was sent, with each entry's own business code — every URI absent from
174    ///   that map was deleted;
175    /// - a lock conflict returns [`Error::ApiWithData`] holding the unlock
176    ///   tokens, which [`ApiV4Client::unlock_files`] can then release.
177    pub async fn delete_files(&self, request: &DeleteFileRequest<'_>) -> Result<(), Error> {
178        let uris = paths_to_uris(&request.uris);
179        let converted = DeleteFileRequest {
180            uris: uris.iter().map(|s| s.as_str()).collect(),
181            unlink: request.unlink,
182            skip_soft_delete: request.skip_soft_delete,
183        };
184        let response: ApiResponse<serde_json::Value> =
185            self.delete_with_body("/file", &converted).await?;
186        match response.code {
187            0 => Ok(()),
188            _ => Err(response.into_error()),
189        }
190    }
191
192    /// Release locks using the tokens from a 40073 conflict response.
193    ///
194    /// This is how a client force-unlocks files it does not itself hold locks
195    /// on — the server hands out one token per conflicting file, and they are
196    /// surrendered together. To unlock by path instead, use
197    /// [`ApiV4Client::force_unlock`].
198    pub async fn unlock_files(&self, request: &UnlockFilesRequest<'_>) -> Result<(), Error> {
199        if request.tokens.is_empty() {
200            return Ok(());
201        }
202        let response: ApiResponse<serde_json::Value> =
203            self.delete_with_body("/file/lock", request).await?;
204        match response.code {
205            0 => Ok(()),
206            _ => Err(response.into_error()),
207        }
208    }
209
210    pub async fn create_directory(&self, path: &str) -> Result<(), Error> {
211        let uri = path_to_uri(path);
212        let request = CreateFileRequest {
213            uri: &uri,
214            r#type: CreateFileType::Folder,
215            metadata: None,
216            err_on_conflict: None,
217        };
218        // Success is carried by `code` alone. Going through `create_file` would
219        // additionally demand the created folder in `data`, turning a body-less
220        // success into a spurious failure.
221        let response: ApiResponse<serde_json::Value> = self.post("/file/create", &request).await?;
222        match response.code {
223            0 => Ok(()),
224            _ => Err(response.into_error()),
225        }
226    }
227
228    pub async fn set_file_permission(
229        &self,
230        request: &SetFilePermissionRequest<'_>,
231    ) -> Result<(), Error> {
232        let response: ApiResponse<()> = self.post("/file/permission", request).await?;
233        match response.code {
234            0 => Ok(()),
235            code => Err(Error::Api {
236                code,
237                message: response.msg,
238            }),
239        }
240    }
241
242    pub async fn delete_file_permission(&self, path: &str) -> Result<(), Error> {
243        let uri = path_to_uri(path);
244        let response: ApiResponse<()> = self
245            .delete(&format!("/file/permission?uri={}", uri))
246            .await?;
247        match response.code {
248            0 => Ok(()),
249            code => Err(Error::Api {
250                code,
251                message: response.msg,
252            }),
253        }
254    }
255
256    pub async fn create_upload_session(
257        &self,
258        request: &CreateUploadSessionRequest<'_>,
259    ) -> Result<UploadSessionResponse, Error> {
260        let response: ApiResponse<UploadSessionResponse> =
261            self.put("/file/upload", request).await?;
262        match response.data {
263            Some(data) => Ok(data),
264            None => Err(Error::InvalidResponse(format!(
265                "API returned no data for create_upload_session request: {:?}",
266                response
267            ))),
268        }
269    }
270
271    pub async fn upload_file_chunk(
272        &self,
273        session_id: &str,
274        index: u32,
275        chunk_data: &[u8],
276    ) -> Result<(), Error> {
277        let url = format!("/file/upload/{}/{}", session_id, index);
278        let full_url = self.get_url(&url);
279
280        let mut request = self.http_client.post(&full_url).body(chunk_data.to_vec());
281
282        if let Some(token) = &self.token {
283            request = request.bearer_auth(token);
284        }
285
286        let response = request.send().await?;
287        let status = response.status();
288
289        if !status.is_success() {
290            let error_text = response
291                .text()
292                .await
293                .unwrap_or_else(|_| "Unknown error".to_string());
294            return Err(Error::Api {
295                code: status.as_u16() as i32,
296                message: error_text,
297            });
298        }
299
300        Ok(())
301    }
302
303    pub async fn delete_upload_session(&self, path: &str, session_id: &str) -> Result<(), Error> {
304        let uri = path_to_uri(path);
305        let request = DeleteUploadSessionRequest {
306            id: session_id,
307            uri: &uri,
308        };
309        let url = self.get_url("/file/upload");
310
311        let body = serde_json::to_string(&request)?;
312
313        let mut http_req = self.http_client.delete(&url);
314        if let Some(token) = &self.token {
315            http_req = http_req.bearer_auth(token);
316        }
317
318        let response = http_req
319            .header("Content-Type", "application/json")
320            .body(body)
321            .send()
322            .await?;
323
324        let status = response.status();
325        if !status.is_success() {
326            let error_text = response
327                .text()
328                .await
329                .unwrap_or_else(|_| "Unknown error".to_string());
330            return Err(Error::Api {
331                code: status.as_u16() as i32,
332                message: error_text,
333            });
334        }
335
336        Ok(())
337    }
338
339    pub async fn get_thumbnail_url(
340        &self,
341        path: &str,
342        width: Option<u32>,
343        height: Option<u32>,
344    ) -> Result<String, Error> {
345        let uri = path_to_uri(path);
346        let mut url = format!("/file/thumb?uri={}", uri);
347        if let Some(w) = width {
348            url.push_str(&format!("&width={}", w));
349        }
350        if let Some(h) = height {
351            url.push_str(&format!("&height={}", h));
352        }
353
354        let response: ApiResponse<String> = self.get(&url).await?;
355        match response.data {
356            Some(data) => Ok(data),
357            None => Err(Error::InvalidResponse(format!(
358                "API returned no data for get_thumbnail_url request: {:?}",
359                response
360            ))),
361        }
362    }
363
364    pub async fn get_file_content(&self, path: &str) -> Result<String, Error> {
365        let uri = path_to_uri(path);
366        let response: ApiResponse<String> = self.get(&format!("/file/content?uri={}", uri)).await?;
367        match response.data {
368            Some(data) => Ok(data),
369            None => Err(Error::InvalidResponse(format!(
370                "API returned no data for get_file_content request: {:?}",
371                response
372            ))),
373        }
374    }
375
376    pub async fn update_file_content(
377        &self,
378        request: &UpdateFileContentRequest<'_>,
379    ) -> Result<(), Error> {
380        let response: ApiResponse<()> = self.put("/file/content", request).await?;
381        match response.code {
382            0 => Ok(()),
383            code => Err(Error::Api {
384                code,
385                message: response.msg,
386            }),
387        }
388    }
389
390    pub async fn create_viewer_session(
391        &self,
392        request: &CreateViewerSessionRequest<'_>,
393    ) -> Result<ViewerSessionResponse, Error> {
394        let response: ApiResponse<ViewerSessionResponse> =
395            self.put("/file/viewerSession", request).await?;
396        match response.data {
397            Some(data) => Ok(data),
398            None => Err(Error::InvalidResponse(format!(
399                "API returned no data for create_viewer_session request: {:?}",
400                response
401            ))),
402        }
403    }
404
405    /// Create an empty file or a folder at `request.uri`.
406    pub async fn create_file(&self, request: &CreateFileRequest<'_>) -> Result<File, Error> {
407        let uri = path_to_uri(request.uri);
408        let converted = CreateFileRequest {
409            uri: &uri,
410            r#type: request.r#type,
411            metadata: request.metadata.clone(),
412            err_on_conflict: request.err_on_conflict,
413        };
414        let response: ApiResponse<File> = self.post("/file/create", &converted).await?;
415        if response.code != 0 {
416            return Err(Error::Api {
417                code: response.code,
418                message: response.msg,
419            });
420        }
421        match response.data {
422            Some(data) => Ok(data),
423            None => Err(Error::InvalidResponse(format!(
424                "API returned no data for create_file request: {:?}",
425                response
426            ))),
427        }
428    }
429
430    pub async fn rename_multiple(&self, request: &RenameMultipleRequest<'_>) -> Result<(), Error> {
431        let response: ApiResponse<()> = self.post("/file/rename", request).await?;
432        match response.code {
433            0 => Ok(()),
434            code => Err(Error::Api {
435                code,
436                message: response.msg,
437            }),
438        }
439    }
440
441    /// Move or copy files, depending on `request.copy`.
442    ///
443    /// The endpoint expects `uris`/`dst`, so this maps `from`/`to` onto
444    /// [`MoveFileRequest`] rather than serializing them directly — sending the
445    /// request's own field names would be rejected as a missing `uris`.
446    pub async fn move_copy_files(&self, request: &MoveCopyFileRequest<'_>) -> Result<(), Error> {
447        let move_request = MoveFileRequest {
448            uris: request.from.clone(),
449            dst: request.to,
450            copy: request.copy,
451        };
452        self.move_file(&move_request).await
453    }
454
455    pub async fn create_download_url(
456        &self,
457        request: &CreateDownloadUrlRequest<'_>,
458    ) -> Result<DownloadUrlResponse, Error> {
459        let uris = paths_to_uris(&request.uris);
460        let uris_refs: Vec<&str> = uris.iter().map(|s| s.as_str()).collect();
461
462        let converted_request = CreateDownloadUrlRequest {
463            uris: uris_refs,
464            download: request.download,
465            redirect: request.redirect,
466            entity: request.entity,
467            use_primary_site_url: request.use_primary_site_url,
468            skip_error: request.skip_error,
469            archive: request.archive,
470            no_cache: request.no_cache,
471        };
472
473        let response: ApiResponse<DownloadUrlResponse> =
474            self.post("/file/url", &converted_request).await?;
475        match response.data {
476            Some(data) => Ok(data),
477            None => Err(Error::InvalidResponse(format!(
478                "API returned no data for create_download_url request: {:?}",
479                response
480            ))),
481        }
482    }
483
484    pub async fn restore_from_trash(&self, request: &RestoreFileRequest<'_>) -> Result<(), Error> {
485        let uris = paths_to_uris(&request.uris);
486        let uris_refs: Vec<&str> = uris.iter().map(|s| s.as_str()).collect();
487
488        let converted_request = RestoreFileRequest { uris: uris_refs };
489
490        let response: ApiResponse<()> = self.post("/file/restore", &converted_request).await?;
491        match response.code {
492            0 => Ok(()),
493            code => Err(Error::Api {
494                code,
495                message: response.msg,
496            }),
497        }
498    }
499
500    pub async fn force_unlock(&self, path: &str) -> Result<(), Error> {
501        let uri = path_to_uri(path);
502        let response: ApiResponse<()> = self.delete(&format!("/file/lock?uri={}", uri)).await?;
503        match response.code {
504            0 => Ok(()),
505            code => Err(Error::Api {
506                code,
507                message: response.msg,
508            }),
509        }
510    }
511
512    pub async fn patch_metadata(
513        &self,
514        path: &str,
515        request: &UpdateMetadataRequest,
516    ) -> Result<(), Error> {
517        let uri = path_to_uri(path);
518        let full_url = format!("/file/metadata?uri={}", uri);
519        let response: ApiResponse<()> = self.patch(&full_url, request).await?;
520        match response.code {
521            0 => Ok(()),
522            code => Err(Error::Api {
523                code,
524                message: response.msg,
525            }),
526        }
527    }
528
529    pub async fn mount_storage_policy(
530        &self,
531        path: &str,
532        request: &MountStoragePolicyRequest,
533    ) -> Result<(), Error> {
534        let uri = path_to_uri(path);
535        let full_url = format!("/file/policy?uri={}", uri);
536        let response: ApiResponse<()> = self.patch(&full_url, request).await?;
537        match response.code {
538            0 => Ok(()),
539            code => Err(Error::Api {
540                code,
541                message: response.msg,
542            }),
543        }
544    }
545
546    pub async fn update_view_settings(
547        &self,
548        path: &str,
549        request: &UpdateViewRequest,
550    ) -> Result<(), Error> {
551        let uri = path_to_uri(path);
552        let full_url = format!("/file/view?uri={}", uri);
553        let response: ApiResponse<()> = self.patch(&full_url, request).await?;
554        match response.code {
555            0 => Ok(()),
556            code => Err(Error::Api {
557                code,
558                message: response.msg,
559            }),
560        }
561    }
562
563    pub async fn get_file_activities(
564        &self,
565        path: &str,
566        page: Option<u32>,
567        page_size: Option<u32>,
568    ) -> Result<FileActivitiesResponse, Error> {
569        let uri = path_to_uri(path);
570        let mut url = format!("/file/activities?uri={}", uri);
571        if let Some(p) = page {
572            url.push_str(&format!("&page={}", p));
573        }
574        if let Some(ps) = page_size {
575            url.push_str(&format!("&page_size={}", ps));
576        }
577
578        let response: ApiResponse<FileActivitiesResponse> = self.get(&url).await?;
579        match response.data {
580            Some(data) => Ok(data),
581            None => Err(Error::InvalidResponse(format!(
582                "API returned no data for get_file_activities request: {:?}",
583                response
584            ))),
585        }
586    }
587
588    pub async fn get_file_info_extended(
589        &self,
590        request: &GetFileInfoRequest<'_>,
591    ) -> Result<File, Error> {
592        let uri = path_to_uri(request.uri);
593        let mut url = format!("/file/info?uri={}", uri);
594        if let Some(include_extended) = request.include_extended_info {
595            url.push_str(&format!("&extended={}", include_extended));
596        }
597
598        let response: ApiResponse<File> = self.get(&url).await?;
599        match response.data {
600            Some(data) => Ok(data),
601            None => Err(Error::InvalidResponse(format!(
602                "API returned no data for get_file_info_extended request: {:?}",
603                response
604            ))),
605        }
606    }
607
608    pub async fn get_archive_list(
609        &self,
610        request: &GetArchiveListRequest<'_>,
611    ) -> Result<ArchiveListResponse, Error> {
612        let uri = path_to_uri(request.uri);
613        let url = format!("/file/archive?uri={}", uri);
614
615        let response: ApiResponse<ArchiveListResponse> = self.get(&url).await?;
616        match response.data {
617            Some(data) => Ok(data),
618            None => Err(Error::InvalidResponse(format!(
619                "API returned no data for get_archive_list request: {:?}",
620                response
621            ))),
622        }
623    }
624
625    /// Create direct links for files
626    ///
627    /// Creates permanent direct links that can be used to access file content directly.
628    /// Only file owners or administrators can create direct links.
629    ///
630    /// # Arguments
631    /// * `request` - CreateDirectLinkRequest with list of file URIs
632    ///
633    /// # Returns
634    /// Vector of DirectLinkItem containing link and file_url
635    pub async fn create_direct_link(
636        &self,
637        request: &CreateDirectLinkRequest<'_>,
638    ) -> Result<Vec<DirectLinkItem>, Error> {
639        let uris = paths_to_uris(&request.uris);
640        let uris_refs: Vec<&str> = uris.iter().map(|s| s.as_str()).collect();
641
642        let converted_request = CreateDirectLinkRequest { uris: uris_refs };
643
644        let response: ApiResponse<Vec<DirectLinkItem>> =
645            self.put("/file/source", &converted_request).await?;
646        match response.data {
647            Some(data) => Ok(data),
648            None => Err(Error::InvalidResponse(format!(
649                "API returned no data for create_direct_link request: {:?}",
650                response
651            ))),
652        }
653    }
654
655    /// Delete a direct link
656    ///
657    /// Only file owners can delete direct links.
658    ///
659    /// # Arguments
660    /// * `id` - ID of the direct link to delete
661    pub async fn delete_direct_link(&self, id: &str) -> Result<(), Error> {
662        let response: ApiResponse<()> = self.delete(&format!("/file/source{}", id)).await?;
663        match response.code {
664            0 => Ok(()),
665            code => Err(Error::Api {
666                code,
667                message: response.msg,
668            }),
669        }
670    }
671}