Skip to main content

cloudreve_api/
client.rs

1//! Unified Cloudreve client with automatic version detection
2
3use crate::Error;
4use crate::api::v3::ApiV3Client;
5use crate::api::v4::ApiV4Client as ApiV4ClientInner;
6use crate::api::{ApiVersion, VersionInfo};
7use log::debug;
8
9/// Unified Cloudreve client that automatically handles version differences
10pub enum UnifiedClient {
11    V3(ApiV3Client),
12    V4(ApiV4ClientInner),
13}
14
15impl UnifiedClient {
16    /// Create a new client with automatic version detection
17    pub async fn new(base_url: &str) -> Result<Self, Error> {
18        Self::with_version(base_url, None).await
19    }
20
21    /// Create a new client with explicit version or auto-detection
22    pub async fn with_version(base_url: &str, version: Option<ApiVersion>) -> Result<Self, Error> {
23        let base_url = base_url.trim_end_matches('/');
24
25        match version {
26            Some(ApiVersion::V3) => {
27                debug!("Creating V3 client for {}", base_url);
28                Ok(UnifiedClient::V3(ApiV3Client::new(base_url)))
29            }
30            Some(ApiVersion::V4) => {
31                debug!("Creating V4 client for {}", base_url);
32                Ok(UnifiedClient::V4(ApiV4ClientInner::new(base_url)))
33            }
34            None => {
35                // Auto-detect version
36                debug!("Auto-detecting API version for {}", base_url);
37                Self::detect_version(base_url).await
38            }
39        }
40    }
41
42    /// Detect the API version by trying endpoints
43    async fn detect_version(base_url: &str) -> Result<Self, Error> {
44        let base_url = base_url.trim_end_matches('/');
45
46        // Try V4 first (newer version)
47        debug!("Trying V4 endpoint...");
48        let v4_client = ApiV4ClientInner::new(base_url);
49        match v4_client.ping().await {
50            Ok(_) => {
51                debug!("V4 endpoint available, using V4 client");
52                return Ok(UnifiedClient::V4(v4_client));
53            }
54            Err(e) => {
55                debug!("V4 endpoint failed: {}", e);
56            }
57        }
58
59        // Try V3
60        debug!("Trying V3 endpoint...");
61        let v3_client = ApiV3Client::new(base_url);
62        match v3_client.ping().await {
63            Ok(_) => {
64                debug!("V3 endpoint available, using V3 client");
65                Ok(UnifiedClient::V3(v3_client))
66            }
67            Err(e) => {
68                debug!("V3 endpoint failed: {}", e);
69                Err(Error::InvalidResponse(
70                    "Could not detect API version. Neither V3 nor V4 endpoints responded."
71                        .to_string(),
72                ))
73            }
74        }
75    }
76
77    /// Get version information
78    pub async fn get_version(&self) -> Result<VersionInfo, Error> {
79        match self {
80            UnifiedClient::V3(client) => client.get_version().await,
81            UnifiedClient::V4(client) => client.get_version().await,
82        }
83    }
84
85    /// Get the API version
86    pub fn api_version(&self) -> ApiVersion {
87        match self {
88            UnifiedClient::V3(_) => ApiVersion::V3,
89            UnifiedClient::V4(_) => ApiVersion::V4,
90        }
91    }
92
93    /// Get the base URL
94    pub fn base_url(&self) -> &str {
95        match self {
96            UnifiedClient::V3(client) => &client.base_url,
97            UnifiedClient::V4(client) => &client.base_url,
98        }
99    }
100
101    /// Check if the client is using V3
102    pub fn is_v3(&self) -> bool {
103        matches!(self, UnifiedClient::V3(_))
104    }
105
106    /// Check if the client is using V4
107    pub fn is_v4(&self) -> bool {
108        matches!(self, UnifiedClient::V4(_))
109    }
110
111    /// Get V3 client reference if applicable
112    pub fn as_v3(&self) -> Option<&ApiV3Client> {
113        match self {
114            UnifiedClient::V3(client) => Some(client),
115            _ => None,
116        }
117    }
118
119    /// Get V4 client reference if applicable
120    pub fn as_v4(&self) -> Option<&ApiV4ClientInner> {
121        match self {
122            UnifiedClient::V4(client) => Some(client),
123            _ => None,
124        }
125    }
126
127    /// Get mutable V3 client reference if applicable
128    pub fn as_v3_mut(&mut self) -> Option<&mut ApiV3Client> {
129        match self {
130            UnifiedClient::V3(client) => Some(client),
131            _ => None,
132        }
133    }
134
135    /// Get mutable V4 client reference if applicable
136    pub fn as_v4_mut(&mut self) -> Option<&mut ApiV4ClientInner> {
137        match self {
138            UnifiedClient::V4(client) => Some(client),
139            _ => None,
140        }
141    }
142
143    /// Upload a file to the server
144    ///
145    /// `last_modified_ms` carries the original file's Unix millis so the server
146    /// can stamp the entry with the source time instead of "now". Pass `None`
147    /// to let the server use the upload moment.
148    pub async fn upload_file(
149        &self,
150        path: &str,
151        content: Vec<u8>,
152        policy_id: Option<&str>,
153        overwrite: bool,
154        last_modified_ms: Option<u64>,
155    ) -> Result<(), Error> {
156        match self {
157            UnifiedClient::V3(client) => {
158                // V3: Get policy from parent directory
159                let parent_dir = if let Some(pos) = path.rfind('/') {
160                    if pos == 0 { "/" } else { &path[..pos] }
161                } else {
162                    "/"
163                };
164                let file_name = path.rsplit('/').next().unwrap_or("file");
165
166                let dir_list = client.list_directory(parent_dir).await?;
167                let policy_id = policy_id.unwrap_or(&dir_list.policy.id);
168
169                // V3 的 last_modified 单位是毫秒,不是秒:服务端拿到后直接
170                // time.UnixMilli(service.LastModified)(service/explorer/upload.go),
171                // 从这个字段引入时就是这样。之前按秒发过去,2026 年的时间戳会被
172                // 当成 1.7e9 毫秒,云端把文件标成 1970-01-22。
173                let last_modified = last_modified_ms.map(|ms| ms as i64).unwrap_or(0);
174                let request = crate::api::v3::models::UploadFileRequest {
175                    path: parent_dir,
176                    name: file_name,
177                    policy_id,
178                    size: content.len() as i64,
179                    last_modified,
180                    mime_type: "",
181                };
182                let session = client.upload_file(&request).await?;
183                client.upload_chunk(&session.session_id, 0, content).await?;
184
185                // Ignore complete_upload errors (may not be needed)
186                let _ = client.complete_upload(&session.session_id).await;
187                Ok(())
188            }
189            UnifiedClient::V4(client) => {
190                // V4: Get policy from parent directory
191                let parent_dir = if let Some(pos) = path.rfind('/') {
192                    if pos == 0 { "/" } else { &path[..pos] }
193                } else {
194                    "/"
195                };
196
197                let policy_id_str = match client
198                    .list_files(&crate::api::v4::models::ListFilesRequest {
199                        path: parent_dir,
200                        page: Some(0),
201                        page_size: Some(1),
202                        ..Default::default()
203                    })
204                    .await
205                {
206                    Ok(response) => response
207                        .storage_policy
208                        .map(|p| p.id)
209                        .unwrap_or_else(|| "default".to_string()),
210                    Err(_) => "default".to_string(),
211                };
212
213                // Use entity_type="version" for overwrite
214                let entity_type = if overwrite { Some("version") } else { None };
215                let request = crate::api::v4::models::CreateUploadSessionRequest {
216                    uri: &crate::api::v4::uri::path_to_uri(path),
217                    size: content.len() as u64,
218                    policy_id: &policy_id_str,
219                    last_modified: last_modified_ms,
220                    mime_type: None,
221                    metadata: None,
222                    entity_type,
223                };
224                let session = client.create_upload_session(&request).await?;
225                client
226                    .upload_file_chunk(&session.session_id, 0, &content)
227                    .await?;
228                Ok(())
229            }
230        }
231    }
232}
233
234// Implement Clone for the unified client
235impl Clone for UnifiedClient {
236    fn clone(&self) -> Self {
237        match self {
238            UnifiedClient::V3(client) => UnifiedClient::V3(client.clone()),
239            UnifiedClient::V4(client) => UnifiedClient::V4(client.clone()),
240        }
241    }
242}
243
244impl std::fmt::Debug for UnifiedClient {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        match self {
247            UnifiedClient::V3(client) => f.debug_tuple("UnifiedClient::V3").field(client).finish(),
248            UnifiedClient::V4(client) => f.debug_tuple("UnifiedClient::V4").field(client).finish(),
249        }
250    }
251}