Files
NextcloudUploadTool/gui/src-tauri/src/commands.rs

286 lines
8.5 KiB
Rust

use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use nextcloud_client::{
initiate_login_flow as api_initiate_login_flow,
poll_login_flow as api_poll_login_flow,
ClientConfig, CredentialStore, NextcloudClient, StoredAccount, UploadOptions,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LoginFlowInitPayload {
pub login_url: String,
pub poll_endpoint: String,
pub poll_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileInfo {
pub path: String,
pub name: String,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GuiUploadResult {
pub file_path: String,
pub file_name: String,
pub remote_path: String,
pub bytes_uploaded: u64,
pub share_url: Option<String>,
pub direct_download_url: Option<String>,
}
#[tauri::command]
pub fn list_accounts() -> Result<Vec<StoredAccount>, String> {
CredentialStore::list_accounts().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_default_account() -> Result<Option<StoredAccount>, String> {
let accounts = CredentialStore::list_accounts().map_err(|e| e.to_string())?;
Ok(accounts.into_iter().find(|a| a.is_default))
}
#[tauri::command]
pub fn set_default_account(account_id: String) -> Result<(), String> {
CredentialStore::set_default_account(&account_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn delete_account(account_id: String) -> Result<(), String> {
CredentialStore::delete_account(&account_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn initiate_login_flow(server_url: String) -> Result<LoginFlowInitPayload, String> {
let normalized = ClientConfig::normalize_url(&server_url).map_err(|e| e.to_string())?;
let http = reqwest::Client::new();
let flow = api_initiate_login_flow(&http, &normalized)
.await
.map_err(|e| e.to_string())?;
// Attempt to launch desktop browser automatically
let _ = open::that(&flow.login);
Ok(LoginFlowInitPayload {
login_url: flow.login,
poll_endpoint: flow.poll.endpoint,
poll_token: flow.poll.token,
})
}
#[tauri::command]
pub async fn poll_login_flow(
endpoint: String,
token: String,
is_default: bool,
label: Option<String>,
) -> Result<Option<StoredAccount>, String> {
let http = reqwest::Client::new();
let poll_res = api_poll_login_flow(&http, &endpoint, &token)
.await
.map_err(|e| e.to_string())?;
if let Some(creds) = poll_res {
let mut account = CredentialStore::save_account(
&creds.server,
&creds.login_name,
&creds.app_password,
is_default,
)
.map_err(|e| e.to_string())?;
if let Some(lbl) = label {
account.label = Some(lbl);
let mut accounts = CredentialStore::list_accounts().map_err(|e| e.to_string())?;
if let Some(a) = accounts.iter_mut().find(|a| a.id == account.id) {
a.label = account.label.clone();
}
CredentialStore::save_accounts(&accounts).map_err(|e| e.to_string())?;
}
Ok(Some(account))
} else {
Ok(None)
}
}
#[tauri::command]
pub async fn manual_login(
server_url: String,
username: String,
app_password: String,
is_default: bool,
label: Option<String>,
) -> Result<StoredAccount, String> {
let normalized = ClientConfig::normalize_url(&server_url).map_err(|e| e.to_string())?;
let config = ClientConfig::with_credentials(normalized.as_str(), &username, &app_password)
.map_err(|e| e.to_string())?;
let client = NextcloudClient::new(config).map_err(|e| e.to_string())?;
// Verify connectivity and credentials
client.test_connection().await.map_err(|e| e.to_string())?;
let mut account = CredentialStore::save_account(
normalized.as_str(),
&username,
&app_password,
is_default,
)
.map_err(|e| e.to_string())?;
if let Some(lbl) = label {
account.label = Some(lbl);
let mut accounts = CredentialStore::list_accounts().map_err(|e| e.to_string())?;
if let Some(a) = accounts.iter_mut().find(|a| a.id == account.id) {
a.label = account.label.clone();
}
CredentialStore::save_accounts(&accounts).map_err(|e| e.to_string())?;
}
Ok(account)
}
#[tauri::command]
pub async fn select_files() -> Result<Vec<String>, String> {
let files = rfd::AsyncFileDialog::new()
.set_title("Select Files to Upload to Nextcloud")
.pick_files()
.await;
match files {
Some(handles) => {
let paths = handles
.into_iter()
.map(|h| h.path().to_string_lossy().to_string())
.collect();
Ok(paths)
}
None => Ok(Vec::new()),
}
}
#[tauri::command]
pub fn get_file_info(file_path: String) -> Result<FileInfo, String> {
let path = PathBuf::from(&file_path);
if !path.exists() {
return Err(format!("File '{}' does not exist", file_path));
}
let metadata = fs::metadata(&path).map_err(|e| e.to_string())?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file")
.to_string();
Ok(FileInfo {
path: file_path,
name,
size: metadata.len(),
})
}
#[tauri::command]
pub async fn upload_file(
file_path: String,
remote_dir: String,
create_share: bool,
share_password: Option<String>,
account_id: Option<String>,
) -> Result<GuiUploadResult, String> {
let client = match account_id {
Some(id) if !id.is_empty() => {
CredentialStore::create_client_for_account(&id).map_err(|e| e.to_string())?
}
_ => CredentialStore::create_client_for_default().map_err(|e| e.to_string())?,
};
let local_path = PathBuf::from(&file_path);
if !local_path.exists() {
return Err(format!("File '{}' does not exist", file_path));
}
let file_name = local_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file")
.to_string();
let clean_dir = remote_dir.trim_matches('/');
let remote_path = if clean_dir.is_empty() {
file_name.clone()
} else {
format!("{clean_dir}/{file_name}")
};
let options = UploadOptions {
remote_path: remote_path.clone(),
create_share,
share_password,
overwrite: true,
};
let res = client
.upload_and_share(&local_path, &options, None)
.await
.map_err(|e| e.to_string())?;
Ok(GuiUploadResult {
file_path,
file_name,
remote_path,
bytes_uploaded: res.bytes_uploaded,
share_url: res.share_url,
direct_download_url: res.direct_download_url,
})
}
#[cfg(test)]
mod tests {
use super::{FileInfo, GuiUploadResult, LoginFlowInitPayload};
#[test]
fn test_login_flow_payload_serialization() {
let payload = LoginFlowInitPayload {
login_url: "https://cloud.example.com/index.php/login/v2/flow/123".to_string(),
poll_endpoint: "https://cloud.example.com/index.php/login/v2/poll".to_string(),
poll_token: "abc-token".to_string(),
};
let json = serde_json::to_string(&payload).unwrap();
let deserialized: LoginFlowInitPayload = serde_json::from_str(&json).unwrap();
assert_eq!(payload, deserialized);
}
#[test]
fn test_file_info_serialization() {
let file_info = FileInfo {
path: "/path/to/document.pdf".to_string(),
name: "document.pdf".to_string(),
size: 2048,
};
let json = serde_json::to_string(&file_info).unwrap();
let deserialized: FileInfo = serde_json::from_str(&json).unwrap();
assert_eq!(file_info, deserialized);
}
#[test]
fn test_gui_upload_result_serialization() {
let res = GuiUploadResult {
file_path: "/tmp/test.pdf".to_string(),
file_name: "test.pdf".to_string(),
remote_path: "Uploads/test.pdf".to_string(),
bytes_uploaded: 1024,
share_url: Some("https://cloud.example.com/s/ABC".to_string()),
direct_download_url: Some("https://cloud.example.com/index.php/s/ABC/download".to_string()),
};
let json = serde_json::to_string(&res).unwrap();
let deserialized: GuiUploadResult = serde_json::from_str(&json).unwrap();
assert_eq!(res, deserialized);
}
}