Initial commit for desktop configuration. Broken now

This commit is contained in:
Igor Katson 2023-12-06 14:30:32 +00:00
parent dd355b0a74
commit a3475784e9
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
8 changed files with 304 additions and 57 deletions

View file

@ -0,0 +1,132 @@
use std::{
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
path::PathBuf,
time::Duration,
};
use librqbit::{dht::PersistentDht, Session};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
#[derive(Clone, Serialize, Deserialize)]
pub struct RqbitDesktopConfigDht {
pub disable: bool,
pub disable_persistence: bool,
pub persistence_filename: PathBuf,
}
impl Default for RqbitDesktopConfigDht {
fn default() -> Self {
Self {
disable: false,
disable_persistence: false,
persistence_filename: PersistentDht::default_persistence_filename().unwrap(),
}
}
}
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct RqbitDesktopConfigTcpListen {
pub disable: bool,
pub min_port: u16,
pub max_port: u16,
}
impl Default for RqbitDesktopConfigTcpListen {
fn default() -> Self {
Self {
disable: false,
// TODO: use consts from librqbit
min_port: 4240,
max_port: 4260,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RqbitDesktopConfigPersistence {
pub disable: bool,
pub filename: PathBuf,
}
impl Default for RqbitDesktopConfigPersistence {
fn default() -> Self {
Self {
disable: false,
filename: Session::default_persistence_filename().unwrap(),
}
}
}
#[serde_as]
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct RqbitDesktopConfigPeerOpts {
#[serde_as(as = "serde_with::DurationSeconds")]
pub connect_timeout: Duration,
#[serde_as(as = "serde_with::DurationSeconds")]
pub read_write_timeout: Duration,
}
impl Default for RqbitDesktopConfigPeerOpts {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(2),
read_write_timeout: Duration::from_secs(10),
}
}
}
#[serde_as]
#[derive(Clone, Serialize, Deserialize)]
pub struct RqbitDesktopConfigHttpApi {
pub disable: bool,
pub listen_addr: SocketAddr,
pub read_only: bool,
}
impl Default for RqbitDesktopConfigHttpApi {
fn default() -> Self {
Self {
disable: Default::default(),
listen_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3030)),
read_only: false,
}
}
}
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
pub struct RqbitDesktopConfigUpnp {
pub disable: bool,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RqbitDesktopConfig {
pub default_download_location: PathBuf,
pub dht: RqbitDesktopConfigDht,
pub tcp_listen: RqbitDesktopConfigTcpListen,
pub upnp: RqbitDesktopConfigUpnp,
pub persistence: RqbitDesktopConfigPersistence,
pub peer_opts: RqbitDesktopConfigPeerOpts,
pub http_api: RqbitDesktopConfigHttpApi,
}
impl Default for RqbitDesktopConfig {
fn default() -> Self {
let download_folder = directories::UserDirs::new()
.expect("directories::UserDirs::new()")
.download_dir()
.expect("download_dir()")
.to_path_buf();
Self {
default_download_location: download_folder,
dht: Default::default(),
tcp_listen: Default::default(),
upnp: Default::default(),
persistence: Default::default(),
peer_opts: Default::default(),
http_api: Default::default(),
}
}
}

View file

@ -1,24 +1,134 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod config;
use std::sync::Arc;
use anyhow::Context;
use config::RqbitDesktopConfig;
use http::StatusCode;
use librqbit::{
api::{
ApiAddTorrentResponse, EmptyJsonResponse, TorrentDetailsResponse, TorrentListResponse,
TorrentStats,
},
librqbit_spawn, AddTorrent, AddTorrentOptions, Api, ApiError, Session, SessionOptions,
dht::PersistentDhtConfig,
librqbit_spawn, AddTorrent, AddTorrentOptions, Api, ApiError, PeerConnectionOptions, Session,
SessionOptions,
};
use parking_lot::RwLock;
use tracing::error_span;
struct State {
const ERR_NOT_CONFIGURED: ApiError =
ApiError::new_from_text(StatusCode::FAILED_DEPENDENCY, "not configured");
struct StateShared {
config: config::RqbitDesktopConfig,
api: Api,
session: Arc<Session>,
}
impl StateShared {}
struct State {
shared: Arc<RwLock<Option<StateShared>>>,
rust_log_reload_tx: tokio::sync::mpsc::UnboundedSender<String>,
}
impl State {
fn api(&self) -> Result<Api, ApiError> {
let g = self.shared.read();
match &*g {
Some(s) => Ok(s.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 config_clone = config.clone();
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);
}
*self.shared.write() = Some(StateShared {
config: config_clone,
api,
session,
});
Ok(())
}
}
#[tauri::command]
fn torrents_list(state: tauri::State<State>) -> TorrentListResponse {
state.api.api_torrent_list()
fn config_default() -> config::RqbitDesktopConfig {
config::RqbitDesktopConfig::default()
}
#[tauri::command]
fn config_current(state: tauri::State<'_, State>) -> Option<config::RqbitDesktopConfig> {
state.current_config()
}
#[tauri::command]
async fn config_change(
state: tauri::State<'_, State>,
config: RqbitDesktopConfig,
) -> Result<EmptyJsonResponse, ApiError> {
state.configure(config).await.map(|_| EmptyJsonResponse {})
}
#[tauri::command]
fn torrents_list(state: tauri::State<State>) -> Result<TorrentListResponse, ApiError> {
Ok(state.api()?.api_torrent_list())
}
#[tauri::command]
@ -28,7 +138,7 @@ async fn torrent_create_from_url(
opts: Option<AddTorrentOptions>,
) -> Result<ApiAddTorrentResponse, ApiError> {
state
.api
.api()?
.api_add_torrent(AddTorrent::Url(url.into()), opts)
.await
}
@ -45,7 +155,7 @@ async fn torrent_create_from_base64_file(
.context("invalid base64")
.map_err(|e| ApiError::new_from_anyhow(StatusCode::BAD_REQUEST, e))?;
state
.api
.api()?
.api_add_torrent(AddTorrent::TorrentFileBytes(bytes.into()), opts)
.await
}
@ -55,7 +165,7 @@ async fn torrent_details(
state: tauri::State<'_, State>,
id: usize,
) -> Result<TorrentDetailsResponse, ApiError> {
state.api.api_torrent_details(id)
state.api()?.api_torrent_details(id)
}
#[tauri::command]
@ -63,7 +173,7 @@ async fn torrent_stats(
state: tauri::State<'_, State>,
id: usize,
) -> Result<TorrentStats, ApiError> {
state.api.api_stats_v1(id)
state.api()?.api_stats_v1(id)
}
#[tauri::command]
@ -71,7 +181,7 @@ async fn torrent_action_delete(
state: tauri::State<'_, State>,
id: usize,
) -> Result<EmptyJsonResponse, ApiError> {
state.api.api_torrent_action_delete(id)
state.api()?.api_torrent_action_delete(id)
}
#[tauri::command]
@ -79,7 +189,7 @@ async fn torrent_action_pause(
state: tauri::State<'_, State>,
id: usize,
) -> Result<EmptyJsonResponse, ApiError> {
state.api.api_torrent_action_pause(id)
state.api()?.api_torrent_action_pause(id)
}
#[tauri::command]
@ -87,7 +197,7 @@ async fn torrent_action_forget(
state: tauri::State<'_, State>,
id: usize,
) -> Result<EmptyJsonResponse, ApiError> {
state.api.api_torrent_action_forget(id)
state.api()?.api_torrent_action_forget(id)
}
#[tauri::command]
@ -95,7 +205,7 @@ async fn torrent_action_start(
state: tauri::State<'_, State>,
id: usize,
) -> Result<EmptyJsonResponse, ApiError> {
state.api.api_torrent_action_start(id)
state.api()?.api_torrent_action_start(id)
}
#[tauri::command]
@ -133,41 +243,15 @@ fn init_logging() -> tokio::sync::mpsc::UnboundedSender<String> {
reload_tx
}
async fn start_session() {
async fn start() {
tauri::async_runtime::set(tokio::runtime::Handle::current());
let rust_log_reload_tx = init_logging();
tauri::async_runtime::set(tokio::runtime::Handle::current());
let download_folder = directories::UserDirs::new()
.expect("directories::UserDirs::new()")
.download_dir()
.expect("download_dir()")
.to_path_buf();
let session = Session::new_with_opts(
download_folder,
SessionOptions {
disable_dht: false,
disable_dht_persistence: false,
persistence: true,
listen_port_range: Some(4240..4260),
..Default::default()
},
)
.await
.expect("couldn't set up librqbit session");
let api = Api::new(session.clone(), None);
librqbit_spawn(
"http api",
error_span!("http_api"),
librqbit::http_api::HttpApi::new(session, Some(rust_log_reload_tx))
.make_http_api_and_run("127.0.0.1:3030".parse().unwrap(), false),
);
tauri::Builder::default()
.manage(State { api })
.manage(State {
shared: Arc::new(RwLock::new(None)),
rust_log_reload_tx,
})
.invoke_handler(tauri::generate_handler![
torrents_list,
torrent_details,
@ -178,7 +262,10 @@ async fn start_session() {
torrent_action_forget,
torrent_action_start,
torrent_create_from_base64_file,
get_version
get_version,
config_default,
config_current,
config_change,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
@ -189,5 +276,5 @@ fn main() {
.enable_all()
.build()
.expect("couldn't set up tokio runtime")
.block_on(start_session())
.block_on(start())
}