1use crate::Error;
4use crate::api::v3::ApiV3Client;
5use crate::api::v4::ApiV4Client as ApiV4ClientInner;
6use crate::api::{ApiVersion, VersionInfo};
7use log::debug;
8
9pub enum UnifiedClient {
11 V3(ApiV3Client),
12 V4(ApiV4ClientInner),
13}
14
15impl UnifiedClient {
16 pub async fn new(base_url: &str) -> Result<Self, Error> {
18 Self::with_version(base_url, None).await
19 }
20
21 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 debug!("Auto-detecting API version for {}", base_url);
37 Self::detect_version(base_url).await
38 }
39 }
40 }
41
42 async fn detect_version(base_url: &str) -> Result<Self, Error> {
44 let base_url = base_url.trim_end_matches('/');
45
46 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 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 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 pub fn api_version(&self) -> ApiVersion {
87 match self {
88 UnifiedClient::V3(_) => ApiVersion::V3,
89 UnifiedClient::V4(_) => ApiVersion::V4,
90 }
91 }
92
93 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 pub fn is_v3(&self) -> bool {
103 matches!(self, UnifiedClient::V3(_))
104 }
105
106 pub fn is_v4(&self) -> bool {
108 matches!(self, UnifiedClient::V4(_))
109 }
110
111 pub fn as_v3(&self) -> Option<&ApiV3Client> {
113 match self {
114 UnifiedClient::V3(client) => Some(client),
115 _ => None,
116 }
117 }
118
119 pub fn as_v4(&self) -> Option<&ApiV4ClientInner> {
121 match self {
122 UnifiedClient::V4(client) => Some(client),
123 _ => None,
124 }
125 }
126
127 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 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 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 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 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 let _ = client.complete_upload(&session.session_id).await;
187 Ok(())
188 }
189 UnifiedClient::V4(client) => {
190 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 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
234impl 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}