Desktop state persistence
This commit is contained in:
parent
d258a9afe2
commit
53868ad45e
7 changed files with 171 additions and 79 deletions
|
|
@ -40,6 +40,10 @@ impl Api {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn session(&self) -> &Arc<Session> {
|
||||
&self.session
|
||||
}
|
||||
|
||||
pub fn mgr_handle(&self, idx: TorrentId) -> Result<ManagedTorrentHandle> {
|
||||
self.session
|
||||
.get(idx)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use librqbit::{dht::PersistentDht, Session};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfigDht {
|
||||
pub disable: bool,
|
||||
pub disable_persistence: bool,
|
||||
|
|
@ -25,7 +25,7 @@ impl Default for RqbitDesktopConfigDht {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfigTcpListen {
|
||||
pub disable: bool,
|
||||
pub min_port: u16,
|
||||
|
|
@ -43,7 +43,7 @@ impl Default for RqbitDesktopConfigTcpListen {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfigPersistence {
|
||||
pub disable: bool,
|
||||
pub filename: PathBuf,
|
||||
|
|
@ -59,7 +59,7 @@ impl Default for RqbitDesktopConfigPersistence {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfigPeerOpts {
|
||||
#[serde_as(as = "serde_with::DurationSeconds")]
|
||||
pub connect_timeout: Duration,
|
||||
|
|
@ -78,7 +78,7 @@ impl Default for RqbitDesktopConfigPeerOpts {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfigHttpApi {
|
||||
pub disable: bool,
|
||||
pub listen_addr: SocketAddr,
|
||||
|
|
@ -95,12 +95,12 @@ impl Default for RqbitDesktopConfigHttpApi {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfigUpnp {
|
||||
pub disable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RqbitDesktopConfig {
|
||||
pub default_download_location: PathBuf,
|
||||
pub dht: RqbitDesktopConfigDht,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
|
||||
mod config;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, BufWriter},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use config::RqbitDesktopConfig;
|
||||
|
|
@ -18,104 +22,177 @@ use librqbit::{
|
|||
SessionOptions,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use tracing::error_span;
|
||||
use serde::Serialize;
|
||||
use tracing::{error, error_span};
|
||||
|
||||
const ERR_NOT_CONFIGURED: ApiError =
|
||||
ApiError::new_from_text(StatusCode::FAILED_DEPENDENCY, "not configured");
|
||||
|
||||
struct StateShared {
|
||||
config: config::RqbitDesktopConfig,
|
||||
api: Api,
|
||||
session: Arc<Session>,
|
||||
api: Option<Api>,
|
||||
}
|
||||
|
||||
type RustLogReloadTx = tokio::sync::mpsc::UnboundedSender<String>;
|
||||
|
||||
impl StateShared {}
|
||||
|
||||
struct State {
|
||||
config_filename: String,
|
||||
shared: Arc<RwLock<Option<StateShared>>>,
|
||||
rust_log_reload_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
rust_log_reload_tx: RustLogReloadTx,
|
||||
}
|
||||
|
||||
fn read_config(path: &str) -> anyhow::Result<RqbitDesktopConfig> {
|
||||
let rdr = BufReader::new(File::open(path)?);
|
||||
Ok(serde_json::from_reader(rdr)?)
|
||||
}
|
||||
|
||||
fn write_config(path: &str, config: &RqbitDesktopConfig) -> anyhow::Result<()> {
|
||||
let tmp = format!("{}.tmp", path);
|
||||
let mut tmp_file = BufWriter::new(
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(&tmp)?,
|
||||
);
|
||||
serde_json::to_writer(&mut tmp_file, config)?;
|
||||
std::fs::rename(tmp, path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn api_from_config(
|
||||
rust_log_reload_tx: &RustLogReloadTx,
|
||||
config: &RqbitDesktopConfig,
|
||||
) -> anyhow::Result<Api> {
|
||||
let session = Session::new_with_opts(
|
||||
config.default_download_location.clone(),
|
||||
SessionOptions {
|
||||
disable_dht: config.dht.disable,
|
||||
disable_dht_persistence: config.dht.disable_persistence,
|
||||
dht_config: Some(PersistentDhtConfig {
|
||||
config_filename: Some(config.dht.persistence_filename.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
persistence: !config.persistence.disable,
|
||||
persistence_filename: Some(config.persistence.filename.clone()),
|
||||
peer_opts: Some(PeerConnectionOptions {
|
||||
connect_timeout: Some(config.peer_opts.connect_timeout),
|
||||
read_write_timeout: Some(config.peer_opts.read_write_timeout),
|
||||
..Default::default()
|
||||
}),
|
||||
listen_port_range: if !config.tcp_listen.disable {
|
||||
Some(config.tcp_listen.min_port..config.tcp_listen.max_port)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
enable_upnp_port_forwarding: !config.upnp.disable,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("couldn't set up librqbit session")?;
|
||||
|
||||
let api = Api::new(session.clone(), None);
|
||||
|
||||
if !config.http_api.disable {
|
||||
let http_api_task =
|
||||
librqbit::http_api::HttpApi::new(session.clone(), Some(rust_log_reload_tx.clone()))
|
||||
.make_http_api_and_run(config.http_api.listen_addr, config.http_api.read_only);
|
||||
|
||||
session.spawn("http api", error_span!("http_api"), http_api_task);
|
||||
}
|
||||
Ok(api)
|
||||
}
|
||||
|
||||
impl State {
|
||||
async fn new(rust_log_reload_tx: tokio::sync::mpsc::UnboundedSender<String>) -> Self {
|
||||
let config_filename = directories::ProjectDirs::from("com", "rqbit", "desktop")
|
||||
.expect("directories::ProjectDirs::from")
|
||||
.config_dir()
|
||||
.to_str()
|
||||
.expect("to_str()")
|
||||
.to_owned();
|
||||
|
||||
if let Ok(config) = read_config(&config_filename) {
|
||||
let api = api_from_config(&rust_log_reload_tx, &config).await.ok();
|
||||
let shared = Arc::new(RwLock::new(Some(StateShared { config, api })));
|
||||
|
||||
return Self {
|
||||
config_filename,
|
||||
shared,
|
||||
rust_log_reload_tx,
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
config_filename,
|
||||
rust_log_reload_tx,
|
||||
shared: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn api(&self) -> Result<Api, ApiError> {
|
||||
let g = self.shared.read();
|
||||
match &*g {
|
||||
Some(s) => Ok(s.api.clone()),
|
||||
match g.as_ref().and_then(|s| s.api.as_ref()) {
|
||||
Some(api) => Ok(api.clone()),
|
||||
None => Err(ERR_NOT_CONFIGURED),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_config(&self) -> Option<RqbitDesktopConfig> {
|
||||
self.shared.read().as_ref().map(|s| s.config.clone())
|
||||
}
|
||||
|
||||
async fn configure(&self, config: RqbitDesktopConfig) -> Result<(), ApiError> {
|
||||
let existing = self.shared.write().take();
|
||||
|
||||
if let Some(existing) = existing {
|
||||
existing.session.stop().await;
|
||||
{
|
||||
let g = self.shared.read();
|
||||
if let Some(shared) = g.as_ref() {
|
||||
if shared.api.is_some() && shared.config == config {
|
||||
// The config didn't change, and the API is running, nothing to do.
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config_clone = config.clone();
|
||||
let existing = self.shared.write().as_mut().and_then(|s| s.api.take());
|
||||
|
||||
let session = Session::new_with_opts(
|
||||
config.default_download_location,
|
||||
SessionOptions {
|
||||
disable_dht: config.dht.disable,
|
||||
disable_dht_persistence: config.dht.disable_persistence,
|
||||
dht_config: Some(PersistentDhtConfig {
|
||||
config_filename: Some(config.dht.persistence_filename),
|
||||
..Default::default()
|
||||
}),
|
||||
persistence: !config.persistence.disable,
|
||||
persistence_filename: Some(config.persistence.filename),
|
||||
peer_opts: Some(PeerConnectionOptions {
|
||||
connect_timeout: Some(config.peer_opts.connect_timeout),
|
||||
read_write_timeout: Some(config.peer_opts.read_write_timeout),
|
||||
..Default::default()
|
||||
}),
|
||||
listen_port_range: if !config.tcp_listen.disable {
|
||||
Some(config.tcp_listen.min_port..config.tcp_listen.max_port)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
enable_upnp_port_forwarding: !config.upnp.disable,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("couldn't set up librqbit session")?;
|
||||
|
||||
let api = Api::new(session.clone(), None);
|
||||
|
||||
if !config.http_api.disable {
|
||||
let http_api_task = librqbit::http_api::HttpApi::new(
|
||||
session.clone(),
|
||||
Some(self.rust_log_reload_tx.clone()),
|
||||
)
|
||||
.make_http_api_and_run(config.http_api.listen_addr, config.http_api.read_only);
|
||||
|
||||
session.spawn("http api", error_span!("http_api"), http_api_task);
|
||||
if let Some(api) = existing {
|
||||
api.session().stop().await;
|
||||
}
|
||||
|
||||
*self.shared.write() = Some(StateShared {
|
||||
config: config_clone,
|
||||
api,
|
||||
session,
|
||||
let api = api_from_config(&self.rust_log_reload_tx, &config).await?;
|
||||
if let Err(e) = write_config(&self.config_filename, &config) {
|
||||
error!("error writing config: {:#}", e);
|
||||
}
|
||||
|
||||
let mut g = self.shared.write();
|
||||
*g = Some(StateShared {
|
||||
config,
|
||||
api: Some(api),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
struct CurrentState {
|
||||
config: Option<RqbitDesktopConfig>,
|
||||
configured: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn config_default() -> config::RqbitDesktopConfig {
|
||||
config::RqbitDesktopConfig::default()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn config_current(state: tauri::State<'_, State>) -> Option<config::RqbitDesktopConfig> {
|
||||
state.current_config()
|
||||
fn config_current(state: tauri::State<'_, State>) -> CurrentState {
|
||||
let g = state.shared.read();
|
||||
match &*g {
|
||||
Some(s) => CurrentState {
|
||||
config: Some(s.config.clone()),
|
||||
configured: s.api.is_some(),
|
||||
},
|
||||
None => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -247,11 +324,10 @@ async fn start() {
|
|||
tauri::async_runtime::set(tokio::runtime::Handle::current());
|
||||
let rust_log_reload_tx = init_logging();
|
||||
|
||||
let state = State::new(rust_log_reload_tx).await;
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(State {
|
||||
shared: Arc::new(RwLock::new(None)),
|
||||
rust_log_reload_tx,
|
||||
})
|
||||
.manage(state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
torrents_list,
|
||||
torrent_details,
|
||||
|
|
|
|||
|
|
@ -43,3 +43,8 @@ export interface RqbitDesktopConfig {
|
|||
peer_opts: RqbitDesktopConfigPeerOpts;
|
||||
http_api: RqbitDesktopConfigHttpApi;
|
||||
}
|
||||
|
||||
export interface CurrentDesktopState {
|
||||
config: RqbitDesktopConfig | null,
|
||||
configured: boolean,
|
||||
}
|
||||
|
|
@ -164,6 +164,7 @@ export const ConfigModal: React.FC<{
|
|||
name="dht.disable_persistence"
|
||||
checked={!config.dht.disable_persistence}
|
||||
onChange={handleToggleChange}
|
||||
disabled={config.dht.disable}
|
||||
help="Enable to store DHT state in a file periodically. If disabled, DHT will bootstrap from scratch on restart."
|
||||
/>
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ export const ConfigModal: React.FC<{
|
|||
name="dht.persistence_filename"
|
||||
value={config.dht.persistence_filename}
|
||||
inputType="text"
|
||||
disabled={config.dht.disable}
|
||||
onChange={handleInputChange}
|
||||
help="The filename to store DHT state into"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
|
|||
import { APIContext, RqbitWebUI } from "./rqbit-webui-src/rqbit-web";
|
||||
import { API } from "./api";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { RqbitDesktopConfig } from "./configuration";
|
||||
import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration";
|
||||
import { RqbitDesktop } from "./rqbit-desktop";
|
||||
|
||||
async function get_version(): Promise<string> {
|
||||
|
|
@ -14,11 +14,15 @@ async function get_default_config(): Promise<RqbitDesktopConfig> {
|
|||
return invoke<RqbitDesktopConfig>("config_default");
|
||||
}
|
||||
|
||||
Promise.all([get_version(), get_default_config()]).then(([version, config]) => {
|
||||
async function get_current_config(): Promise<CurrentDesktopState> {
|
||||
return invoke<CurrentDesktopState>("config_current");
|
||||
}
|
||||
|
||||
Promise.all([get_version(), get_default_config(), get_current_config()]).then(([version, defaultConfig, currentState]) => {
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<APIContext.Provider value={API}>
|
||||
<RqbitDesktop version={version} defaultConfig={config} />
|
||||
<RqbitDesktop version={version} defaultConfig={defaultConfig} currentState={currentState} />
|
||||
</APIContext.Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useState } from "react";
|
||||
import { RqbitWebUI } from "./rqbit-webui-src/rqbit-web";
|
||||
import { RqbitDesktopConfig } from "./configuration";
|
||||
import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration";
|
||||
import { ConfigModal } from "./configure";
|
||||
|
||||
|
||||
export const RqbitDesktop: React.FC<{
|
||||
version: string,
|
||||
defaultConfig: RqbitDesktopConfig,
|
||||
}> = ({ version, defaultConfig }) => {
|
||||
let [configured, setConfigured] = useState<boolean>(false);
|
||||
let [config, setConfig] = useState<RqbitDesktopConfig>(defaultConfig);
|
||||
currentState: CurrentDesktopState,
|
||||
}> = ({ version, defaultConfig, currentState }) => {
|
||||
let [configured, setConfigured] = useState<boolean>(currentState.configured);
|
||||
let [config, setConfig] = useState<RqbitDesktopConfig>(currentState.config ?? defaultConfig);
|
||||
let [configurationOpened, setConfigurationOpened] = useState<boolean>(false);
|
||||
|
||||
return <>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue