fix: harden WebDAV sync — async credentials, consolidated command, Onyx subfolder
This commit is contained in:
parent
9e57f1df3c
commit
58f37b08d6
|
|
@ -99,13 +99,6 @@ fn repo_mut(state: &mut AppState) -> Result<&mut TaskRepository, String> {
|
||||||
state.repo.as_mut().ok_or_else(|| "Repository not initialized".to_string())
|
state.repo.as_mut().ok_or_else(|| "Repository not initialized".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Debug ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn log_debug(msg: String) {
|
|
||||||
eprintln!("[frontend] {msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Config commands ──────────────────────────────────────────────────
|
// ── Config commands ──────────────────────────────────────────────────
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -473,19 +466,27 @@ fn add_webdav_workspace(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn store_credentials(
|
async fn store_credentials(
|
||||||
domain: String,
|
domain: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
|
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn load_credentials(domain: String) -> Result<(String, String), String> {
|
async fn load_credentials(domain: String) -> Result<(String, String), String> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
webdav::load_credentials(&domain)
|
webdav::load_credentials(&domain)
|
||||||
.map(|(u, p)| ((*u).clone(), (*p).clone()))
|
.map(|(u, p)| ((*u).clone(), (*p).clone()))
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -505,21 +506,39 @@ async fn test_webdav_connection(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn sync_workspace(
|
async fn sync_workspace(
|
||||||
workspace_name: String,
|
workspace_name: String,
|
||||||
workspace_path: String,
|
|
||||||
webdav_url: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
mode: String,
|
mode: String,
|
||||||
state: State<'_, Mutex<AppState>>,
|
state: State<'_, Mutex<AppState>>,
|
||||||
) -> Result<SyncResult, String> {
|
) -> Result<SyncResult, String> {
|
||||||
|
// Step 1: read config
|
||||||
|
let (workspace_path, webdav_url) = {
|
||||||
|
let s = lock_state(&state)?;
|
||||||
|
let ws = s.config.workspaces.get(&workspace_name)
|
||||||
|
.ok_or("Workspace not found")?;
|
||||||
|
(ws.path.clone(), ws.webdav_url.clone().ok_or("No WebDAV URL configured")?)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: load credentials
|
||||||
|
let domain = webdav_url
|
||||||
|
.split("://")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|rest| rest.split('/').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let (username, password) = tokio::task::spawn_blocking(move || {
|
||||||
|
webdav::load_credentials(&domain)
|
||||||
|
.map(|(u, p)| ((*u).clone(), (*p).clone()))
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())??;
|
||||||
|
|
||||||
let sync_mode = match mode.as_str() {
|
let sync_mode = match mode.as_str() {
|
||||||
"push" => SyncMode::Push,
|
"push" => SyncMode::Push,
|
||||||
"pull" => SyncMode::Pull,
|
"pull" => SyncMode::Pull,
|
||||||
_ => SyncMode::Full,
|
_ => SyncMode::Full,
|
||||||
};
|
};
|
||||||
eprintln!("[sync] starting sync: workspace={workspace_name} path={workspace_path} url={webdav_url} mode={mode}");
|
|
||||||
let result = sync::sync_workspace(
|
let result = sync::sync_workspace(
|
||||||
&PathBuf::from(&workspace_path),
|
&workspace_path,
|
||||||
&webdav_url,
|
&webdav_url,
|
||||||
&username,
|
&username,
|
||||||
&password,
|
&password,
|
||||||
|
|
@ -527,22 +546,14 @@ async fn sync_workspace(
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| e.to_string())?;
|
||||||
eprintln!("[sync] sync_workspace error: {e}");
|
|
||||||
e.to_string()
|
|
||||||
})?;
|
|
||||||
eprintln!("[sync] sync complete: uploaded={} downloaded={} errors={}", result.uploaded, result.downloaded, result.errors.len());
|
|
||||||
|
|
||||||
// Persist last_sync timestamp to config
|
|
||||||
{
|
{
|
||||||
eprintln!("[sync] acquiring state lock...");
|
|
||||||
let mut s = lock_state(&state)?;
|
let mut s = lock_state(&state)?;
|
||||||
eprintln!("[sync] lock acquired, saving config...");
|
|
||||||
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
|
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
|
||||||
ws.last_sync = Some(Utc::now());
|
ws.last_sync = Some(Utc::now());
|
||||||
}
|
}
|
||||||
s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())?;
|
s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())?;
|
||||||
eprintln!("[sync] config saved");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
|
|
@ -632,7 +643,6 @@ pub fn run() {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
log_debug,
|
|
||||||
get_config,
|
get_config,
|
||||||
save_config,
|
save_config,
|
||||||
add_workspace,
|
add_workspace,
|
||||||
|
|
|
||||||
|
|
@ -510,7 +510,28 @@ pub async fn sync_workspace(
|
||||||
mode: SyncMode,
|
mode: SyncMode,
|
||||||
on_progress: Option<ProgressCallback>,
|
on_progress: Option<ProgressCallback>,
|
||||||
) -> Result<SyncResult> {
|
) -> Result<SyncResult> {
|
||||||
let client = WebDavClient::new(webdav_url, username, password)?;
|
// Wrap entire sync in a hard timeout — reqwest's built-in timeout
|
||||||
|
// doesn't reliably fire on Windows native TLS when the server is unreachable.
|
||||||
|
match tokio::time::timeout(
|
||||||
|
crate::webdav::REQUEST_TIMEOUT * 2,
|
||||||
|
sync_workspace_inner(workspace_path, webdav_url, username, password, mode, on_progress),
|
||||||
|
).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => Err(Error::WebDav("Sync timed out — server may be unreachable".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_workspace_inner(
|
||||||
|
workspace_path: &Path,
|
||||||
|
webdav_url: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
mode: SyncMode,
|
||||||
|
on_progress: Option<ProgressCallback>,
|
||||||
|
) -> Result<SyncResult> {
|
||||||
|
// Sync into an "Onyx" subfolder so we don't scan the user's entire cloud storage
|
||||||
|
let sync_url = format!("{}/Onyx", webdav_url.trim_end_matches('/'));
|
||||||
|
let client = WebDavClient::new(&sync_url, username, password)?;
|
||||||
let mut sync_state = SyncState::load(workspace_path);
|
let mut sync_state = SyncState::load(workspace_path);
|
||||||
let queue = OfflineQueue::load(workspace_path);
|
let queue = OfflineQueue::load(workspace_path);
|
||||||
let mut result = SyncResult::default();
|
let mut result = SyncResult::default();
|
||||||
|
|
@ -521,7 +542,8 @@ pub async fn sync_workspace(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure remote root exists
|
// Ensure remote Onyx folder exists (creates it on first sync)
|
||||||
|
client.create_dir("").await.ok();
|
||||||
client.test_connection().await?;
|
client.test_connection().await?;
|
||||||
|
|
||||||
// Scan local files
|
// Scan local files
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
use std::time::Duration;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
/// Hard timeout for any WebDAV network operation.
|
||||||
|
pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
/// Information about a file on the remote WebDAV server.
|
/// Information about a file on the remote WebDAV server.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RemoteFileInfo {
|
pub struct RemoteFileInfo {
|
||||||
|
|
@ -32,8 +36,8 @@ impl WebDavClient {
|
||||||
let base_url = base_url.trim_end_matches('/').to_string();
|
let base_url = base_url.trim_end_matches('/').to_string();
|
||||||
Self {
|
Self {
|
||||||
_client: Client::builder()
|
_client: Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.connect_timeout(std::time::Duration::from_secs(10))
|
.connect_timeout(Duration::from_secs(10))
|
||||||
.build()
|
.build()
|
||||||
.unwrap_or_else(|_| Client::new()),
|
.unwrap_or_else(|_| Client::new()),
|
||||||
_base_url: base_url,
|
_base_url: base_url,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue