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 complete_upload(&self, session_id: &str) -> Result<(), Error> {
44        let response: ApiResponse<()> = self
45            .post(
46                &format!("/callback/onedrive/finish/{}", session_id),
47                &serde_json::json!({}),
48            )
49            .await?;
50        if response.code == 0 {
51            Ok(())
52        } else {
53            Err(Error::Api {
54                code: response.code,
55                message: response.msg,
56            })
57        }
58    }
59
60    /// Upload one chunk of an open session (`POST /file/upload/{sessionId}/{index}`).
61    ///
62    /// The index has to reach the server: V3 derives the append offset from it
63    /// (`AppendStart = chunkSize * index`) and rejects out-of-order chunks, so
64    /// pinning the URL to chunk 0 made every multi-chunk upload either fail or
65    /// overwrite the first chunk.
66    ///
67    /// V3 answers 200 even for failures and carries the real outcome in the
68    /// body's `code`, so the status alone must not be read as success.
69    pub async fn upload_chunk(
70        &self,
71        session_id: &str,
72        chunk_index: u32,
73        data: Vec<u8>,
74    ) -> Result<(), Error> {
75        let url = self.get_url(&format!("/file/upload/{}/{}", session_id, chunk_index));
76        let mut request = self.http_client.post(&url).body(data);
77
78        if let Some(cookie) = &self.session_cookie {
79            request = request.header("Cookie", format!("cloudreve-session={}", cookie));
80        }
81
82        let response = request.send().await?;
83        let status = response.status();
84        let raw_text = response.text().await.unwrap_or_default();
85
86        if let Ok(api_response) = serde_json::from_str::<ApiResponse<serde_json::Value>>(&raw_text)
87        {
88            return match api_response.code {
89                0 => Ok(()),
90                code => Err(Error::Api {
91                    code,
92                    message: api_response.msg,
93                }),
94            };
95        }
96
97        if status.is_success() {
98            Ok(())
99        } else {
100            Err(Error::Api {
101                code: status.as_u16() as i32,
102                message: format!("Upload failed with status: {}", status),
103            })
104        }
105    }
106
107    /// 原地覆盖一个已存在文件的内容(`PUT /file/update/{id}`)。
108    ///
109    /// V3 的上传会话没有 overwrite 语义:同名文件已存在时,建会话会被
110    /// GenericAfterUpload 挡回 40004 Object existed。网页端的文本编辑器保存走的
111    /// 就是这个接口,服务端以 fsctx.Overwrite 模式写回原文件,id 和路径都不变。
112    ///
113    /// 服务端从 Content-Length 取长度,所以这里显式带上;响应仍是 HTTP 200 +
114    /// body 里的 code。
115    pub async fn update_file_content(&self, id: &str, content: Vec<u8>) -> Result<(), Error> {
116        let url = self.get_url(&format!("/file/update/{}", urlencoding::encode(id)));
117        let mut request = self
118            .http_client
119            .put(&url)
120            .header("Content-Type", "application/octet-stream")
121            .header("Content-Length", content.len().to_string())
122            .body(content);
123
124        if let Some(cookie) = &self.session_cookie {
125            request = request.header("Cookie", format!("cloudreve-session={}", cookie));
126        }
127
128        let response = request.send().await?;
129        let status = response.status();
130        let raw_text = response.text().await.unwrap_or_default();
131
132        if let Ok(api_response) = serde_json::from_str::<ApiResponse<serde_json::Value>>(&raw_text)
133        {
134            return match api_response.code {
135                0 => Ok(()),
136                code => Err(Error::Api {
137                    code,
138                    message: api_response.msg,
139                }),
140            };
141        }
142
143        Err(Error::Api {
144            code: status.as_u16() as i32,
145            message: raw_text.trim().to_string(),
146        })
147    }
148
149    /// Delete one upload session by id (`DELETE /file/upload/{sessionId}`).
150    ///
151    /// Opening a session makes V3 insert a placeholder file row that keeps the
152    /// name taken. If the upload never finishes, every later `upload_file` for
153    /// the same path fails with 40054 "Upload session existed" until the
154    /// server-side GC runs (`upload_session_timeout`, 24h by default). Deleting
155    /// the session drops that placeholder and is the only way a client can clear
156    /// the conflict itself.
157    ///
158    /// A session the server no longer knows returns `CodeUploadSessionExpired`;
159    /// callers that are only cleaning up can treat that as already done.
160    pub async fn delete_upload_session(&self, session_id: &str) -> Result<(), Error> {
161        let response: ApiResponse<()> = self
162            .delete(&format!("/file/upload/{}", urlencoding::encode(session_id)))
163            .await?;
164        match response.code {
165            0 => Ok(()),
166            code => Err(Error::Api {
167                code,
168                message: response.msg,
169            }),
170        }
171    }
172
173    /// Delete every upload placeholder the current user owns
174    /// (`DELETE /file/upload`).
175    ///
176    /// This is the recovery path for orphan sessions whose ids the client lost
177    /// (killed mid-upload, local store wiped, session created but the response
178    /// never arrived). It is account-wide, so any upload still in flight loses
179    /// its placeholder too — only call it when nothing else is uploading.
180    pub async fn delete_all_upload_sessions(&self) -> Result<(), Error> {
181        let response: ApiResponse<()> = self.delete("/file/upload").await?;
182        match response.code {
183            0 => Ok(()),
184            code => Err(Error::Api {
185                code,
186                message: response.msg,
187            }),
188        }
189    }
190
191    pub async fn download_file(&self, id: &str) -> Result<DownloadUrl, Error> {
192        // V3 returns ApiResponse with data as string (download URL path)
193        let response: ApiResponse<String> = self
194            .put(&format!("/file/download/{}", id), &serde_json::json!({}))
195            .await?;
196        match response.data {
197            Some(url_path) => Ok(DownloadUrl { url: url_path }),
198            None => Err(Error::Api {
199                code: response.code,
200                message: response.msg,
201            }),
202        }
203    }
204
205    pub async fn get_file_source(
206        &self,
207        request: &FileSourceRequest,
208    ) -> Result<Vec<FileSource>, Error> {
209        let response: ApiResponse<Vec<FileSource>> = self.post("/file/source", request).await?;
210        match response.data {
211            Some(sources) => Ok(sources),
212            None => Err(Error::Api {
213                code: response.code,
214                message: response.msg,
215            }),
216        }
217    }
218
219    pub async fn preview_file(&self, id: &str) -> Result<DirectoryList, Error> {
220        let response: ApiResponse<DirectoryList> =
221            self.get(&format!("/file/preview/{}", id)).await?;
222        match response.data {
223            Some(list) => Ok(list),
224            None => Err(Error::Api {
225                code: response.code,
226                message: response.msg,
227            }),
228        }
229    }
230
231    pub async fn get_thumbnail(&self, id: &str) -> Result<DirectoryList, Error> {
232        let response: ApiResponse<DirectoryList> = self.get(&format!("/file/thumb/{}", id)).await?;
233        match response.data {
234            Some(list) => Ok(list),
235            None => Err(Error::Api {
236                code: response.code,
237                message: response.msg,
238            }),
239        }
240    }
241
242    pub async fn create_file(&self, request: &CreateFileRequest<'_>) -> Result<(), Error> {
243        let response: ApiResponse<()> = self.post("/file/create", request).await?;
244        if response.code == 0 {
245            Ok(())
246        } else {
247            Err(Error::Api {
248                code: response.code,
249                message: response.msg,
250            })
251        }
252    }
253}