Skip to main content

cloudreve_api/api/v3/
file.rs

1//! File-related API endpoints for Cloudreve API v3
2
3use crate::Error;
4use crate::api::v3::ApiV3Client;
5use crate::api::v3::models::*;
6
7impl ApiV3Client {
8    /// Search for files by keyword, scoped to `path`.
9    ///
10    /// Pass "/" as `path` to search the entire drive. The response reuses the
11    /// directory listing shape, so the matches arrive in `objects`.
12    pub async fn search_files(&self, keyword: &str, path: &str) -> Result<DirectoryList, Error> {
13        let scope = if path.is_empty() { "/" } else { path };
14        let endpoint = format!(
15            "/file/search/keywords/{}?path={}",
16            urlencoding::encode(keyword),
17            urlencoding::encode(scope)
18        );
19        let response: ApiResponse<DirectoryList> = self.get(&endpoint).await?;
20        match response.data {
21            Some(list) => Ok(list),
22            None => Err(Error::Api {
23                code: response.code,
24                message: response.msg,
25            }),
26        }
27    }
28
29    pub async fn upload_file(
30        &self,
31        request: &UploadFileRequest<'_>,
32    ) -> Result<UploadSession, Error> {
33        let response: ApiResponse<UploadSession> = self.put("/file/upload", request).await?;
34        match response.data {
35            Some(session) => Ok(session),
36            None => Err(Error::Api {
37                code: response.code,
38                message: response.msg,
39            }),
40        }
41    }
42
43    pub async fn upload_chunk(
44        &self,
45        session_id: &str,
46        chunk_index: u32,
47        data: Vec<u8>,
48    ) -> Result<(), Error> {
49        let url = self.get_url(&format!("/file/upload/{}/{}", session_id, chunk_index));
50        let mut request = self.http_client.post(&url).body(data);
51
52        if let Some(cookie) = &self.session_cookie {
53            request = request.header("Cookie", format!("cloudreve-session={}", cookie));
54        }
55
56        let response = request.send().await?;
57        let status = response.status();
58        let raw_text = response.text().await.unwrap_or_default();
59
60        if let Ok(api_response) = serde_json::from_str::<ApiResponse<serde_json::Value>>(&raw_text)
61        {
62            return match api_response.code {
63                0 => Ok(()),
64                code => Err(Error::Api {
65                    code,
66                    message: api_response.msg,
67                }),
68            };
69        }
70
71        if status.is_success() {
72            Ok(())
73        } else {
74            Err(Error::Api {
75                code: status.as_u16() as i32,
76                message: format!("Upload failed with status: {}", status),
77            })
78        }
79    }
80
81    /// 原地覆盖一个已存在文件的内容(`PUT /file/update/{id}`)。
82    ///
83    /// V3 的上传会话没有 overwrite 语义:同名文件已存在时,建会话会被
84    /// GenericAfterUpload 挡回 40004 Object existed。网页端的文本编辑器保存走的
85    /// 就是这个接口,服务端以 fsctx.Overwrite 模式写回原文件,id 和路径都不变。
86    ///
87    /// 服务端从 Content-Length 取长度,所以这里显式带上;响应仍是 HTTP 200 +
88    /// body 里的 code。
89    pub async fn update_file_content(&self, id: &str, content: Vec<u8>) -> Result<(), Error> {
90        let url = self.get_url(&format!("/file/update/{}", urlencoding::encode(id)));
91        let mut request = self
92            .http_client
93            .put(&url)
94            .header("Content-Type", "application/octet-stream")
95            .header("Content-Length", content.len().to_string())
96            .body(content);
97
98        if let Some(cookie) = &self.session_cookie {
99            request = request.header("Cookie", format!("cloudreve-session={}", cookie));
100        }
101
102        let response = request.send().await?;
103        let status = response.status();
104        let raw_text = response.text().await.unwrap_or_default();
105
106        if let Ok(api_response) = serde_json::from_str::<ApiResponse<serde_json::Value>>(&raw_text)
107        {
108            return match api_response.code {
109                0 => Ok(()),
110                code => Err(Error::Api {
111                    code,
112                    message: api_response.msg,
113                }),
114            };
115        }
116
117        Err(Error::Api {
118            code: status.as_u16() as i32,
119            message: raw_text.trim().to_string(),
120        })
121    }
122
123    /// Delete one upload session by id (`DELETE /file/upload/{sessionId}`).
124    ///
125    /// Opening a session makes V3 insert a placeholder file row that keeps the
126    /// name taken. If the upload never finishes, every later `upload_file` for
127    /// the same path fails with 40054 "Upload session existed" until the
128    /// server-side GC runs (`upload_session_timeout`, 24h by default). Deleting
129    /// the session drops that placeholder and is the only way a client can clear
130    /// the conflict itself.
131    ///
132    /// A session the server no longer knows returns `CodeUploadSessionExpired`;
133    /// callers that are only cleaning up can treat that as already done.
134    pub async fn delete_upload_session(&self, session_id: &str) -> Result<(), Error> {
135        let response: ApiResponse<()> = self
136            .delete(&format!("/file/upload/{}", urlencoding::encode(session_id)))
137            .await?;
138        match response.code {
139            0 => Ok(()),
140            code => Err(Error::Api {
141                code,
142                message: response.msg,
143            }),
144        }
145    }
146
147    /// Delete every upload placeholder the current user owns
148    /// (`DELETE /file/upload`).
149    ///
150    /// This is the recovery path for orphan sessions whose ids the client lost
151    /// (killed mid-upload, local store wiped, session created but the response
152    /// never arrived). It is account-wide, so any upload still in flight loses
153    /// its placeholder too — only call it when nothing else is uploading.
154    pub async fn delete_all_upload_sessions(&self) -> Result<(), Error> {
155        let response: ApiResponse<()> = self.delete("/file/upload").await?;
156        match response.code {
157            0 => Ok(()),
158            code => Err(Error::Api {
159                code,
160                message: response.msg,
161            }),
162        }
163    }
164
165    pub async fn download_file(&self, id: &str) -> Result<DownloadUrl, Error> {
166        // V3 returns ApiResponse with data as string (download URL path)
167        let response: ApiResponse<String> = self
168            .put(&format!("/file/download/{}", id), &serde_json::json!({}))
169            .await?;
170        match response.data {
171            Some(url_path) => Ok(DownloadUrl { url: url_path }),
172            None => Err(Error::Api {
173                code: response.code,
174                message: response.msg,
175            }),
176        }
177    }
178
179    pub async fn get_file_source(
180        &self,
181        request: &FileSourceRequest,
182    ) -> Result<Vec<FileSource>, Error> {
183        let response: ApiResponse<Vec<FileSource>> = self.post("/file/source", request).await?;
184        match response.data {
185            Some(sources) => Ok(sources),
186            None => Err(Error::Api {
187                code: response.code,
188                message: response.msg,
189            }),
190        }
191    }
192
193    /// 拉取文件预览内容(`GET /file/preview/{id}`)。
194    ///
195    /// 这个端点要么 302 跳到直链、要么直接把内容写进响应体,**从不返回 JSON**
196    /// ——早先这里按 `ApiResponse<DirectoryList>` 解,任何一种成功路径都会解析失败。
197    /// reqwest 默认跟随重定向,所以直接拿字节即可;出错时 V3 会回 200 + JSON 错误体,
198    /// 由 [`Self::fetch_binary`] 识别。
199    pub async fn preview_file(&self, id: &str) -> Result<Vec<u8>, Error> {
200        self.fetch_binary(&format!("/file/preview/{}", urlencoding::encode(id)))
201            .await
202    }
203
204    /// 拉取缩略图(`GET /file/thumb/{id}`)。
205    ///
206    /// 和 [`Self::preview_file`] 一样:301 跳转或原始图片字节,不是 JSON。
207    pub async fn get_thumbnail(&self, id: &str) -> Result<Vec<u8>, Error> {
208        self.fetch_binary(&format!("/file/thumb/{}", urlencoding::encode(id)))
209            .await
210    }
211
212    /// 拉一个"正常情况下返回二进制、出错才返回 JSON"的端点。
213    ///
214    /// V3 的错误一律是 HTTP 200 + body 里的 code,所以状态码判断不够;这里先按
215    /// JSON 试解一次,解出来且 code != 0 才算错误,否则整个 body 就是内容本身。
216    async fn fetch_binary(&self, endpoint: &str) -> Result<Vec<u8>, Error> {
217        let url = self.get_url(endpoint);
218        let mut request = self.http_client.get(&url);
219        if let Some(cookie) = &self.session_cookie {
220            request = request.header("Cookie", format!("cloudreve-session={}", cookie));
221        }
222
223        let response = request.send().await?;
224        let status = response.status();
225        let bytes = response.bytes().await?.to_vec();
226
227        if let Ok(api_response) = serde_json::from_slice::<ApiResponse<serde_json::Value>>(&bytes)
228            && api_response.code != 0
229        {
230            return Err(Error::Api {
231                code: api_response.code,
232                message: api_response.msg,
233            });
234        }
235
236        if !status.is_success() {
237            return Err(Error::Api {
238                code: status.as_u16() as i32,
239                message: String::from_utf8_lossy(&bytes).trim().to_string(),
240            });
241        }
242
243        Ok(bytes)
244    }
245
246    pub async fn create_file(&self, request: &CreateFileRequest<'_>) -> Result<(), Error> {
247        let response: ApiResponse<()> = self.post("/file/create", request).await?;
248        if response.code == 0 {
249            Ok(())
250        } else {
251            Err(Error::Api {
252                code: response.code,
253                message: response.msg,
254            })
255        }
256    }
257}