use reqwest::Client; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Clone)] pub struct MiniCloudApi { client: Client, pub server_url: String, pub access_token: String, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LoginResponse { pub access_token: String, pub user: UserInfo, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserInfo { pub id: i64, pub username: String, pub role: String, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FileEntry { pub id: i64, pub name: String, pub is_folder: bool, pub size: Option, pub checksum: Option, pub updated_at: Option, pub children: Option>, pub locked: Option, pub locked_by: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct SyncTreeResponse { pub tree: Vec, } #[derive(Debug, Serialize, Deserialize)] #[allow(dead_code)] pub struct SyncChangesResponse { pub changes: Vec, pub server_time: String, } #[derive(Debug, Serialize, Deserialize)] #[allow(dead_code)] pub struct LockResponse { pub locked: Option, pub locked_by: Option, pub error: Option, } impl MiniCloudApi { pub fn new(server_url: &str) -> Self { Self { client: Client::builder() .danger_accept_invalid_certs(false) .build() .unwrap(), server_url: server_url.trim_end_matches('/').to_string(), access_token: String::new(), } } fn auth_header(&self) -> String { format!("Bearer {}", self.access_token) } pub async fn login(&mut self, username: &str, password: &str) -> Result { let url = format!("{}/api/auth/login", self.server_url); let body = serde_json::json!({ "username": username, "password": password, }); let resp = self.client.post(&url) .json(&body) .send() .await .map_err(|e| format!("Verbindungsfehler: {}", e))?; if !resp.status().is_success() { let text = resp.text().await.unwrap_or_default(); return Err(format!("Login fehlgeschlagen: {}", text)); } let data: LoginResponse = resp.json().await .map_err(|e| format!("Antwort-Fehler: {}", e))?; self.access_token = data.access_token.clone(); Ok(data) } pub async fn refresh_token(&mut self) -> Result { let url = format!("{}/api/auth/refresh", self.server_url); let resp = self.client.post(&url) .header("Authorization", self.auth_header()) .send() .await .map_err(|e| format!("Refresh fehlgeschlagen: {}", e))?; if !resp.status().is_success() { return Err("Token-Refresh fehlgeschlagen".to_string()); } let data: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?; if let Some(token) = data.get("access_token").and_then(|t| t.as_str()) { self.access_token = token.to_string(); Ok(token.to_string()) } else { Err("Kein Token in Antwort".to_string()) } } pub async fn get_sync_tree(&self) -> Result, String> { let url = format!("{}/api/sync/tree", self.server_url); let resp = self.client.get(&url) .header("Authorization", self.auth_header()) .send() .await .map_err(|e| format!("Sync-Tree Fehler: {}", e))?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(format!("Sync-Tree HTTP {}: {}", status, text)); } let data: SyncTreeResponse = resp.json().await .map_err(|e| format!("Sync-Tree Parse-Fehler: {}", e))?; Ok(data.tree) } #[allow(dead_code)] pub async fn get_changes(&self, since: &str) -> Result { let url = format!("{}/api/sync/changes?since={}", self.server_url, since); let resp = self.client.get(&url) .header("Authorization", self.auth_header()) .send() .await .map_err(|e| format!("Changes Fehler: {}", e))?; resp.json().await.map_err(|e| format!("Parse-Fehler: {}", e)) } pub async fn download_file(&self, file_id: i64, dest: &Path) -> Result<(), String> { let url = format!("{}/api/files/{}/download?token={}", self.server_url, file_id, self.access_token); let resp = self.client.get(&url) .send() .await .map_err(|e| format!("Download Fehler: {}", e))?; if !resp.status().is_success() { return Err(format!("Download fehlgeschlagen: {}", resp.status())); } let bytes = resp.bytes().await.map_err(|e| e.to_string())?; if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; } std::fs::write(dest, &bytes).map_err(|e| format!("Schreiben fehlgeschlagen: {}", e)) } pub async fn upload_file(&self, file_path: &Path, parent_id: Option) -> Result { let url = format!("{}/api/files/upload", self.server_url); let file_name = file_path.file_name() .and_then(|n| n.to_str()) .unwrap_or("file") .to_string(); let file_bytes = std::fs::read(file_path) .map_err(|e| format!("Datei lesen fehlgeschlagen: {}", e))?; let mut form = reqwest::multipart::Form::new() .part("file", reqwest::multipart::Part::bytes(file_bytes).file_name(file_name)); if let Some(pid) = parent_id { form = form.text("parent_id", pid.to_string()); } let resp = self.client.post(&url) .header("Authorization", self.auth_header()) .multipart(form) .send() .await .map_err(|e| format!("Upload Fehler: {}", e))?; if !resp.status().is_success() { let text = resp.text().await.unwrap_or_default(); return Err(format!("Upload fehlgeschlagen: {}", text)); } resp.json().await.map_err(|e| format!("Parse-Fehler: {}", e)) } pub async fn create_folder(&self, name: &str, parent_id: Option) -> Result { let url = format!("{}/api/files/folder", self.server_url); let body = serde_json::json!({ "name": name, "parent_id": parent_id, }); let resp = self.client.post(&url) .header("Authorization", self.auth_header()) .json(&body) .send() .await .map_err(|e| format!("Create-Folder Verbindungsfehler: {}", e))?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(format!("Create-Folder fehlgeschlagen ({}): {}", status, text)); } resp.json().await.map_err(|e| format!("Create-Folder Parse-Fehler: {}", e)) } pub async fn lock_file(&self, file_id: i64, client_info: &str) -> Result<(), String> { let url = format!("{}/api/files/{}/lock", self.server_url, file_id); let body = serde_json::json!({ "client_info": client_info }); let resp = self.client.post(&url) .header("Authorization", self.auth_header()) .json(&body) .send() .await .map_err(|e| e.to_string())?; if resp.status().as_u16() == 423 { let data: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?; let by = data.get("locked_by").and_then(|v| v.as_str()).unwrap_or("?"); return Err(format!("Datei gesperrt von {}", by)); } if !resp.status().is_success() { return Err("Lock fehlgeschlagen".to_string()); } Ok(()) } pub async fn unlock_file(&self, file_id: i64) -> Result<(), String> { let url = format!("{}/api/files/{}/unlock", self.server_url, file_id); self.client.post(&url) .header("Authorization", self.auth_header()) .send() .await .map_err(|e| e.to_string())?; Ok(()) } pub async fn delete_file(&self, file_id: i64) -> Result<(), String> { let url = format!("{}/api/files/{}", self.server_url, file_id); let resp = self.client.delete(&url) .header("Authorization", self.auth_header()) .send() .await .map_err(|e| format!("Delete Fehler: {}", e))?; if !resp.status().is_success() { let text = resp.text().await.unwrap_or_default(); return Err(format!("Delete fehlgeschlagen: {}", text)); } Ok(()) } pub async fn heartbeat(&self, file_id: i64) -> Result<(), String> { let url = format!("{}/api/files/{}/heartbeat", self.server_url, file_id); self.client.post(&url) .header("Authorization", self.auth_header()) .send() .await .map_err(|e| e.to_string())?; Ok(()) } }