fix(integration): decode persisted settings and align ports

This commit is contained in:
NimBold
2026-06-18 07:52:48 +03:30
parent 5200c1e453
commit e2dd387a8c
5 changed files with 306 additions and 130 deletions
+4 -13
View File
@@ -15,14 +15,12 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_store::StoreExt;
use tokio::sync::watch;
use tower_http::cors::{Any, CorsLayer};
use ts_rs::TS;
pub const EXTENSION_SERVER_PORT: u16 = 23522;
pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive<u16> =
EXTENSION_SERVER_PORT..=23531;
pub const EXTENSION_SERVER_PORT: u16 = 6412;
pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive<u16> = EXTENSION_SERVER_PORT..=6422;
const MAX_URL_COUNT: usize = 200;
const SIGNATURE_MAX_AGE_MS: u64 = 60_000;
const MAIN_QUEUE_ID: &str = "00000000-0000-0000-0000-000000000001";
@@ -228,7 +226,7 @@ async fn enqueue_extension_download(
app_handle: &AppHandle,
download: &ExtensionDownload,
) -> Result<(), String> {
let Some(settings) = read_settings(app_handle) else {
let Ok(settings) = crate::settings::load_settings(app_handle) else {
return Err("settings unavailable".to_string());
};
let state = app_handle.state::<crate::AppState>();
@@ -309,13 +307,6 @@ async fn enqueue_extension_download(
Ok(())
}
fn read_settings(app_handle: &AppHandle) -> Option<crate::ipc::PersistedSettings> {
let store = app_handle.store("store.bin").ok()?;
let settings_value = store.get("settings")?;
let settings_text = settings_value.as_str()?;
serde_json::from_str(settings_text).ok()
}
fn merge_headers(referer: Option<&str>, headers: Option<&str>) -> Option<String> {
let mut values = Vec::new();
if let Some(referer) = referer {
@@ -339,7 +330,7 @@ fn filename_from_url(raw_url: &str) -> String {
.and_then(|url| {
url.path_segments()
.and_then(Iterator::last)
.and_then(|segment| sanitize_filename(segment))
.and_then(sanitize_filename)
})
.unwrap_or_else(|| "download".to_string())
}
+19 -43
View File
@@ -517,6 +517,7 @@ mod parity;
pub mod error;
pub mod commands;
pub mod retry;
mod settings;
pub use error::AppError;
// Retained only for compatibility with the optional aria2 diagnostic monitor.
@@ -2153,20 +2154,9 @@ pub fn run() {
let max_concurrent = {
use tauri_plugin_store::StoreExt;
let mut capacity = crate::queue::DEFAULT_MAX_CONCURRENT;
if let Ok(store) = app.handle().store("store.bin") {
if let Some(settings_val) = store.get("settings") {
if let Some(settings_str) = settings_val.as_str() {
if let Ok(settings_json) = serde_json::from_str::<serde_json::Value>(settings_str) {
if let Some(n) = settings_json.get("maxConcurrentDownloads").and_then(|v| v.as_u64()) {
capacity = n as usize;
}
}
}
}
}
capacity
crate::settings::load_settings(app.handle())
.map(|settings| settings.max_concurrent_downloads)
.unwrap_or(crate::queue::DEFAULT_MAX_CONCURRENT)
};
let queue_manager = Arc::new(queue::QueueManager::new(app.handle().clone(), max_concurrent));
@@ -2234,21 +2224,9 @@ pub fn run() {
}
crate::scheduler::spawn_scheduler(app.handle().clone());
let mut global_speed_limit = String::new();
{
use tauri_plugin_store::StoreExt;
if let Ok(store) = app.handle().store("store.bin") {
if let Some(settings_val) = store.get("settings") {
if let Some(settings_str) = settings_val.as_str() {
if let Ok(settings_json) = serde_json::from_str::<serde_json::Value>(settings_str) {
if let Some(limit) = settings_json.get("globalSpeedLimit").and_then(|v| v.as_str()) {
global_speed_limit = limit.to_string();
}
}
}
}
}
}
let global_speed_limit = crate::settings::load_settings(app.handle())
.map(|settings| settings.global_speed_limit)
.unwrap_or_default();
match resolve_bundled_binary_path(app.handle(), "aria2c") {
Ok(binary_path) => {
@@ -2281,22 +2259,20 @@ pub fn run() {
std::thread::spawn(move || {
use std::io::BufRead;
let reader = std::io::BufReader::new(stderr);
for line in reader.lines() {
if let Ok(trimmed) = line {
let trimmed = trimmed.trim().to_string();
if let Ok(mut stderr_lock) = daemon_app.state::<Aria2DaemonGuard>().last_stderr.lock() {
stderr_lock.push_str(&trimmed);
stderr_lock.push('\n');
let excess = stderr_lock.len().saturating_sub(8192);
if excess > 0 {
let _ = stderr_lock.drain(..excess);
}
}
let lower = trimmed.to_lowercase();
if lower.contains("error") || lower.contains("critical") {
log::error!("aria2c stderr: {}", trimmed);
for line in reader.lines().map_while(Result::ok) {
let trimmed = line.trim().to_string();
if let Ok(mut stderr_lock) = daemon_app.state::<Aria2DaemonGuard>().last_stderr.lock() {
stderr_lock.push_str(&trimmed);
stderr_lock.push('\n');
let excess = stderr_lock.len().saturating_sub(8192);
if excess > 0 {
let _ = stderr_lock.drain(..excess);
}
}
let lower = trimmed.to_lowercase();
if lower.contains("error") || lower.contains("critical") {
log::error!("aria2c stderr: {}", trimmed);
}
}
});
}
+15 -16
View File
@@ -257,17 +257,18 @@ impl<R: tauri::Runtime> QueueManager<R> {
loop {
let debt = self.slots_to_retire.load(Ordering::Relaxed);
let to_deduct = std::cmp::min(debt, delta);
match self.slots_to_retire.compare_exchange_weak(
debt,
debt - to_deduct,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => {
delta -= to_deduct;
break;
}
Err(_) => {}
if self
.slots_to_retire
.compare_exchange_weak(
debt,
debt - to_deduct,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok()
{
delta -= to_deduct;
break;
}
}
if delta > 0 {
@@ -580,7 +581,7 @@ impl<R: tauri::Runtime> QueueManager<R> {
}
let payload = payload.unwrap();
let this = Arc::clone(&self);
let this = Arc::clone(self);
let stale_gid = gid.to_string();
let id_for_task = id.clone();
let error_for_emit = error.clone();
@@ -890,8 +891,7 @@ impl SidecarSpawner for ProductionSpawner {
let mut cancel_rx = state
.download_coordinator
.register_media(id.to_string())
.await
.map_err(|e| e)?;
.await?;
let outcome = crate::start_media_download_internal(
self.app_handle.clone(),
id,
@@ -941,8 +941,7 @@ impl SidecarSpawner for ProductionSpawner {
proxy: payload.proxy.clone(),
},
)))
.await
.map_err(|e| e)?;
.await?;
Ok(())
}
}
+36 -58
View File
@@ -1,8 +1,6 @@
use tauri::Emitter;
use chrono::{Local, Datelike};
use chrono::{Datelike, Local};
use std::time::Duration;
use tauri::Emitter;
pub fn spawn_scheduler(app_handle: tauri::AppHandle) {
tauri::async_runtime::spawn(async move {
@@ -10,68 +8,48 @@ pub fn spawn_scheduler(app_handle: tauri::AppHandle) {
loop {
interval.tick().await;
let settings_opt = {
use tauri_plugin_store::StoreExt;
let store = app_handle.store("store.bin");
if let Ok(store) = store {
store.get("settings").and_then(|v| v.as_str().map(|s| s.to_string()))
} else {
None
if let Ok(settings) = crate::settings::load_settings(&app_handle) {
let scheduler = settings.scheduler;
if !scheduler.enabled {
continue;
}
};
if let Some(settings_str) = settings_opt {
if let Ok(mut settings) = serde_json::from_str::<serde_json::Value>(&settings_str) {
if let Ok(scheduler) = serde_json::from_value::<crate::ipc::SchedulerSettings>(settings.get("scheduler").unwrap_or(&serde_json::json!({})).clone()) {
if !scheduler.enabled {
continue;
}
let now = Local::now();
let current_time = now.format("%H:%M").to_string();
let current_day = now.weekday().num_days_from_sunday();
let now = Local::now();
let current_time = now.format("%H:%M").to_string();
let current_day = now.weekday().num_days_from_sunday();
let allowed_today =
scheduler.everyday || scheduler.selected_days.contains(&current_day);
if !allowed_today {
continue;
}
let allowed_today = scheduler.everyday || scheduler.selected_days.contains(&current_day);
if !allowed_today {
continue;
}
let date_key = now.format("%Y-%m-%d").to_string();
let trigger_key = format!("{}-{}", date_key, current_time);
let date_key = now.format("%Y-%m-%d").to_string();
let trigger_key = format!("{}-{}", date_key, current_time);
if scheduler.start_time == current_time
&& settings.scheduler_last_start_key != trigger_key
{
let key = trigger_key.clone();
let _ = crate::settings::update_settings_state(&app_handle, |state| {
state.insert("schedulerLastStartKey".to_string(), serde_json::json!(key));
state.insert("schedulerRunning".to_string(), serde_json::json!(true));
});
let last_start_key = settings.get("schedulerLastStartKey").and_then(|v| v.as_str()).unwrap_or("").to_string();
let last_stop_key = settings.get("schedulerLastStopKey").and_then(|v| v.as_str()).unwrap_or("").to_string();
let _ = app_handle.emit("schedule-trigger", "start");
}
if scheduler.start_time == current_time && last_start_key != trigger_key {
settings["schedulerLastStartKey"] = serde_json::json!(trigger_key.clone());
settings["schedulerRunning"] = serde_json::json!(true);
let _ = app_handle.emit("schedule-trigger", "start");
if let Ok(updated) = serde_json::to_string(&settings) {
use tauri_plugin_store::StoreExt;
if let Ok(store) = app_handle.store("store.bin") {
store.set("settings", serde_json::json!(updated));
let _ = store.save();
}
}
}
if scheduler.stop_time_enabled
&& scheduler.stop_time == current_time
&& settings.scheduler_last_stop_key != trigger_key
{
let key = trigger_key.clone();
let _ = crate::settings::update_settings_state(&app_handle, |state| {
state.insert("schedulerLastStopKey".to_string(), serde_json::json!(key));
state.insert("schedulerRunning".to_string(), serde_json::json!(false));
});
if scheduler.stop_time_enabled && scheduler.stop_time == current_time && last_stop_key != trigger_key {
settings["schedulerLastStopKey"] = serde_json::json!(trigger_key.clone());
settings["schedulerRunning"] = serde_json::json!(false);
let _ = app_handle.emit("schedule-trigger", "stop");
if let Ok(updated) = serde_json::to_string(&settings) {
use tauri_plugin_store::StoreExt;
if let Ok(store) = app_handle.store("store.bin") {
store.set("settings", serde_json::json!(updated));
let _ = store.save();
}
}
}
}
let _ = app_handle.emit("schedule-trigger", "stop");
}
}
}
+232
View File
@@ -0,0 +1,232 @@
use crate::ipc::{
AppFontSize, ListRowDensity, MediaCookieSource, PersistedSettings, PostQueueAction, ProxyMode,
SchedulerSettings, SettingsTab, Theme,
};
use serde_json::{Map, Value};
use std::collections::HashMap;
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
const SETTINGS_STORE: &str = "store.bin";
const SETTINGS_KEY: &str = "settings";
pub fn load_settings(app_handle: &AppHandle) -> Result<PersistedSettings, String> {
let store = app_handle
.store(SETTINGS_STORE)
.map_err(|error| format!("failed to open settings store: {error}"))?;
let stored = store
.get(SETTINGS_KEY)
.ok_or_else(|| "settings are not persisted yet".to_string())?;
decode_stored_settings(&stored)
}
pub fn decode_stored_settings(stored: &Value) -> Result<PersistedSettings, String> {
let document = decode_document(stored)?;
let state = settings_state(&document)?;
let mut merged = serde_json::to_value(default_settings())
.map_err(|error| format!("failed to serialize settings defaults: {error}"))?;
merge_json(&mut merged, state);
let mut settings: PersistedSettings = serde_json::from_value(merged)
.map_err(|error| format!("invalid persisted settings: {error}"))?;
validate_settings(&mut settings);
Ok(settings)
}
pub fn update_settings_state(
app_handle: &AppHandle,
update: impl FnOnce(&mut Map<String, Value>),
) -> Result<(), String> {
let store = app_handle
.store(SETTINGS_STORE)
.map_err(|error| format!("failed to open settings store: {error}"))?;
let stored = store
.get(SETTINGS_KEY)
.ok_or_else(|| "settings are not persisted yet".to_string())?;
let was_string = stored.is_string();
let mut document = decode_document(&stored)?;
update(settings_state_mut(&mut document)?);
let stored = if was_string {
Value::String(
serde_json::to_string(&document)
.map_err(|error| format!("failed to encode settings: {error}"))?,
)
} else {
document
};
store.set(SETTINGS_KEY, stored);
store
.save()
.map_err(|error| format!("failed to save settings: {error}"))
}
fn decode_document(stored: &Value) -> Result<Value, String> {
match stored {
Value::String(text) => serde_json::from_str(text)
.map_err(|error| format!("failed to parse persisted settings: {error}")),
Value::Object(_) => Ok(stored.clone()),
_ => Err("persisted settings must be a JSON string or object".to_string()),
}
}
fn settings_state(document: &Value) -> Result<&Value, String> {
if let Some(state) = document.get("state") {
if !state.is_object() {
return Err("persisted settings state must be an object".to_string());
}
Ok(state)
} else if document.is_object() {
Ok(document)
} else {
Err("persisted settings must be an object".to_string())
}
}
fn settings_state_mut(document: &mut Value) -> Result<&mut Map<String, Value>, String> {
let has_envelope = document.get("state").is_some();
let state = if has_envelope {
document
.get_mut("state")
.ok_or_else(|| "persisted settings state is missing".to_string())?
} else {
document
};
state
.as_object_mut()
.ok_or_else(|| "persisted settings state must be an object".to_string())
}
fn merge_json(target: &mut Value, source: &Value) {
match (target, source) {
(Value::Object(target), Value::Object(source)) => {
for (key, value) in source {
if let Some(target_value) = target.get_mut(key) {
merge_json(target_value, value);
} else {
target.insert(key.clone(), value.clone());
}
}
}
(target, source) => *target = source.clone(),
}
}
fn validate_settings(settings: &mut PersistedSettings) {
if settings.max_concurrent_downloads == 0 {
settings.max_concurrent_downloads = default_settings().max_concurrent_downloads;
}
}
fn default_settings() -> PersistedSettings {
let download_directories = [
("Musics", "~/Downloads/Musics"),
("Movies", "~/Downloads/Movies"),
("Compressed", "~/Downloads/Compressed"),
("Documents", "~/Downloads/Documents"),
("Pictures", "~/Downloads/Pictures"),
("Applications", "~/Downloads/Applications"),
("Other", "~/Downloads/Other"),
]
.into_iter()
.map(|(category, path)| (category.to_string(), path.to_string()))
.collect::<HashMap<_, _>>();
PersistedSettings {
theme: Theme::System,
default_download_path: "~/Downloads".to_string(),
max_concurrent_downloads: 3,
global_speed_limit: String::new(),
is_sidebar_visible: true,
active_settings_tab: SettingsTab::Downloads,
scheduler: SchedulerSettings {
enabled: false,
start_time: "00:00".to_string(),
stop_time_enabled: false,
stop_time: "08:00".to_string(),
everyday: true,
selected_days: vec![0, 1, 2, 3, 4, 5, 6],
post_queue_action: PostQueueAction::None,
},
scheduler_last_start_key: String::new(),
scheduler_last_stop_key: String::new(),
last_custom_speed_limit_ki_b: 1024,
per_server_connections: 16,
max_automatic_retries: 3,
show_notifications: true,
play_completion_sound: true,
app_font_size: AppFontSize::Standard,
list_row_density: ListRowDensity::Standard,
show_dock_badge: true,
show_menu_bar_icon: true,
proxy_mode: ProxyMode::None,
proxy_host: String::new(),
proxy_port: 8080,
custom_user_agent: String::new(),
ask_where_to_save_each_file: false,
prevents_sleep_while_downloading: true,
media_cookie_source: MediaCookieSource::None,
download_directories,
site_logins: Vec::new(),
extension_pairing_token: String::new(),
auto_check_updates: true,
}
}
#[cfg(test)]
mod tests {
use super::decode_stored_settings;
use serde_json::{json, Value};
#[test]
fn decodes_zustand_envelope_and_preserves_non_default_startup_settings() {
let stored = json!({
"state": {
"maxConcurrentDownloads": 7,
"globalSpeedLimit": "2M",
"scheduler": {
"enabled": true,
"startTime": "06:30",
"stopTimeEnabled": true,
"stopTime": "23:15",
"everyday": false,
"selectedDays": [1, 3, 5],
"postQueueAction": "sleep"
}
},
"version": 0
});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert_eq!(settings.max_concurrent_downloads, 7);
assert_eq!(settings.global_speed_limit, "2M");
assert!(settings.scheduler.enabled);
assert_eq!(settings.scheduler.start_time, "06:30");
assert_eq!(settings.scheduler.selected_days, vec![1, 3, 5]);
assert_eq!(settings.default_download_path, "~/Downloads");
}
#[test]
fn decodes_legacy_top_level_settings() {
let stored = json!({
"maxConcurrentDownloads": 5,
"globalSpeedLimit": "512K"
});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert_eq!(settings.max_concurrent_downloads, 5);
assert_eq!(settings.global_speed_limit, "512K");
assert!(!settings.scheduler.enabled);
}
#[test]
fn replaces_zero_concurrency_with_the_safe_default() {
let stored = json!({"state": {"maxConcurrentDownloads": 0}, "version": 0});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert_eq!(settings.max_concurrent_downloads, 3);
}
}