refactor: migrate torrent/settings endpoints to Leptos Server Functions and remove third_party/coarsetime
Some checks failed
Build MIPS Binary / build (push) Failing after 4m15s
Some checks failed
Build MIPS Binary / build (push) Failing after 4m15s
This commit is contained in:
@@ -9,6 +9,11 @@ pub mod xmlrpc;
|
||||
|
||||
pub mod server_fns;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerContext {
|
||||
pub scgi_socket_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
|
||||
pub struct Torrent {
|
||||
pub hash: String,
|
||||
|
||||
@@ -1,26 +1,2 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::xmlrpc::{self, RtorrentClient};
|
||||
|
||||
#[server(GetVersion, "/api/server_fns")]
|
||||
pub async fn get_version() -> Result<String, ServerFnError> {
|
||||
let socket_path = std::env::var("RTORRENT_SOCKET").unwrap_or_else(|_| "/tmp/rtorrent.sock".to_string());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let client = RtorrentClient::new(&socket_path);
|
||||
match client.call("system.client_version", &[]).await {
|
||||
Ok(xml) => {
|
||||
let version = xmlrpc::parse_string_response(&xml).unwrap_or(xml);
|
||||
Ok(version)
|
||||
},
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
pub mod torrent;
|
||||
pub mod settings;
|
||||
|
||||
58
shared/src/server_fns/settings.rs
Normal file
58
shared/src/server_fns/settings.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::GlobalLimitRequest;
|
||||
|
||||
#[server(GetGlobalLimits, "/api/server_fns")]
|
||||
pub async fn get_global_limits() -> Result<GlobalLimitRequest, ServerFnError> {
|
||||
use crate::xmlrpc::{self, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
|
||||
let down = match client.call("throttle.global_down.max_rate", &[]).await {
|
||||
Ok(xml) => xmlrpc::parse_i64_response(&xml).unwrap_or(0),
|
||||
Err(_) => -1,
|
||||
};
|
||||
|
||||
let up = match client.call("throttle.global_up.max_rate", &[]).await {
|
||||
Ok(xml) => xmlrpc::parse_i64_response(&xml).unwrap_or(0),
|
||||
Err(_) => -1,
|
||||
};
|
||||
|
||||
Ok(GlobalLimitRequest {
|
||||
max_download_rate: Some(down),
|
||||
max_upload_rate: Some(up),
|
||||
})
|
||||
}
|
||||
|
||||
#[server(SetGlobalLimits, "/api/server_fns")]
|
||||
pub async fn set_global_limits(
|
||||
max_download_rate: Option<i64>,
|
||||
max_upload_rate: Option<i64>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use crate::xmlrpc::{RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
|
||||
if let Some(down) = max_download_rate {
|
||||
let down_kb = down / 1024;
|
||||
client
|
||||
.call(
|
||||
"throttle.global_down.max_rate.set_kb",
|
||||
&[RpcParam::from(""), RpcParam::Int(down_kb)],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to set down limit: {}", e)))?;
|
||||
}
|
||||
|
||||
if let Some(up) = max_upload_rate {
|
||||
let up_kb = up / 1024;
|
||||
client
|
||||
.call(
|
||||
"throttle.global_up.max_rate.set_kb",
|
||||
&[RpcParam::from(""), RpcParam::Int(up_kb)],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to set up limit: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
274
shared/src/server_fns/torrent.rs
Normal file
274
shared/src/server_fns/torrent.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::{TorrentFile, TorrentPeer, TorrentTracker};
|
||||
|
||||
#[server(AddTorrent, "/api/server_fns")]
|
||||
pub async fn add_torrent(uri: String) -> Result<(), ServerFnError> {
|
||||
use crate::xmlrpc::{RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
let params = vec![RpcParam::from(""), RpcParam::from(uri.as_str())];
|
||||
|
||||
match client.call("load.start", ¶ms).await {
|
||||
Ok(response) => {
|
||||
if response.contains("faultCode") {
|
||||
return Err(ServerFnError::new("rTorrent returned fault"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(ServerFnError::new(format!("Failed to add torrent: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(TorrentAction, "/api/server_fns")]
|
||||
pub async fn torrent_action(hash: String, action: String) -> Result<String, ServerFnError> {
|
||||
use crate::xmlrpc::{RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
|
||||
if action == "delete_with_data" {
|
||||
return delete_torrent_with_data_inner(&client, &hash).await;
|
||||
}
|
||||
|
||||
let method = match action.as_str() {
|
||||
"start" => "d.start",
|
||||
"stop" => "d.stop",
|
||||
"delete" => "d.erase",
|
||||
_ => return Err(ServerFnError::new("Invalid action")),
|
||||
};
|
||||
|
||||
let params = vec![RpcParam::from(hash.as_str())];
|
||||
match client.call(method, ¶ms).await {
|
||||
Ok(_) => Ok("Action executed".to_string()),
|
||||
Err(e) => Err(ServerFnError::new(format!("RPC error: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn delete_torrent_with_data_inner(
|
||||
client: &crate::xmlrpc::RtorrentClient,
|
||||
hash: &str,
|
||||
) -> Result<String, ServerFnError> {
|
||||
use crate::xmlrpc::{parse_string_response, RpcParam};
|
||||
|
||||
let params_hash = vec![RpcParam::from(hash)];
|
||||
|
||||
let path_xml = client
|
||||
.call("d.base_path", ¶ms_hash)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to call rTorrent: {}", e)))?;
|
||||
|
||||
let path = parse_string_response(&path_xml)
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to parse path: {}", e)))?;
|
||||
|
||||
let root_xml = client
|
||||
.call("directory.default", &[])
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to get download root: {}", e)))?;
|
||||
|
||||
let root_path_str = parse_string_response(&root_xml)
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to parse root path: {}", e)))?;
|
||||
|
||||
let root_path = tokio::fs::canonicalize(std::path::Path::new(&root_path_str))
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Invalid download root: {}", e)))?;
|
||||
|
||||
let target_path_raw = std::path::Path::new(&path);
|
||||
if !tokio::fs::try_exists(target_path_raw).await.unwrap_or(false) {
|
||||
client
|
||||
.call("d.erase", ¶ms_hash)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to erase torrent: {}", e)))?;
|
||||
return Ok("Torrent removed (Data not found)".to_string());
|
||||
}
|
||||
|
||||
let target_path = tokio::fs::canonicalize(target_path_raw)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Invalid data path: {}", e)))?;
|
||||
|
||||
if !target_path.starts_with(&root_path) {
|
||||
return Err(ServerFnError::new(
|
||||
"Security Error: Cannot delete files outside download directory",
|
||||
));
|
||||
}
|
||||
|
||||
if target_path == root_path {
|
||||
return Err(ServerFnError::new(
|
||||
"Security Error: Cannot delete the download root directory",
|
||||
));
|
||||
}
|
||||
|
||||
client
|
||||
.call("d.erase", ¶ms_hash)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to erase torrent: {}", e)))?;
|
||||
|
||||
let delete_result = if target_path.is_dir() {
|
||||
tokio::fs::remove_dir_all(&target_path).await
|
||||
} else {
|
||||
tokio::fs::remove_file(&target_path).await
|
||||
};
|
||||
|
||||
match delete_result {
|
||||
Ok(_) => Ok("Torrent and data deleted".to_string()),
|
||||
Err(e) => Err(ServerFnError::new(format!("Failed to delete data: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetFiles, "/api/server_fns")]
|
||||
pub async fn get_files(hash: String) -> Result<Vec<TorrentFile>, ServerFnError> {
|
||||
use crate::xmlrpc::{parse_multicall_response, RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
let params = vec![
|
||||
RpcParam::from(hash.as_str()),
|
||||
RpcParam::from(""),
|
||||
RpcParam::from("f.path="),
|
||||
RpcParam::from("f.size_bytes="),
|
||||
RpcParam::from("f.completed_chunks="),
|
||||
RpcParam::from("f.priority="),
|
||||
];
|
||||
|
||||
let xml = client
|
||||
.call("f.multicall", ¶ms)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("RPC error: {}", e)))?;
|
||||
|
||||
let rows = parse_multicall_response(&xml)
|
||||
.map_err(|e| ServerFnError::new(format!("Parse error: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, row)| TorrentFile {
|
||||
index: idx as u32,
|
||||
path: row.get(0).cloned().unwrap_or_default(),
|
||||
size: row.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
completed_chunks: row.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
priority: row.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server(GetPeers, "/api/server_fns")]
|
||||
pub async fn get_peers(hash: String) -> Result<Vec<TorrentPeer>, ServerFnError> {
|
||||
use crate::xmlrpc::{parse_multicall_response, RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
let params = vec![
|
||||
RpcParam::from(hash.as_str()),
|
||||
RpcParam::from(""),
|
||||
RpcParam::from("p.address="),
|
||||
RpcParam::from("p.client_version="),
|
||||
RpcParam::from("p.down_rate="),
|
||||
RpcParam::from("p.up_rate="),
|
||||
RpcParam::from("p.completed_percent="),
|
||||
];
|
||||
|
||||
let xml = client
|
||||
.call("p.multicall", ¶ms)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("RPC error: {}", e)))?;
|
||||
|
||||
let rows = parse_multicall_response(&xml)
|
||||
.map_err(|e| ServerFnError::new(format!("Parse error: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| TorrentPeer {
|
||||
ip: row.get(0).cloned().unwrap_or_default(),
|
||||
client: row.get(1).cloned().unwrap_or_default(),
|
||||
down_rate: row.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
up_rate: row.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
progress: row.get(4).and_then(|s| s.parse().ok()).unwrap_or(0.0),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server(GetTrackers, "/api/server_fns")]
|
||||
pub async fn get_trackers(hash: String) -> Result<Vec<TorrentTracker>, ServerFnError> {
|
||||
use crate::xmlrpc::{parse_multicall_response, RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
let params = vec![
|
||||
RpcParam::from(hash.as_str()),
|
||||
RpcParam::from(""),
|
||||
RpcParam::from("t.url="),
|
||||
RpcParam::from("t.activity_date_last="),
|
||||
RpcParam::from("t.message="),
|
||||
];
|
||||
|
||||
let xml = client
|
||||
.call("t.multicall", ¶ms)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("RPC error: {}", e)))?;
|
||||
|
||||
let rows = parse_multicall_response(&xml)
|
||||
.map_err(|e| ServerFnError::new(format!("Parse error: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| TorrentTracker {
|
||||
url: row.get(0).cloned().unwrap_or_default(),
|
||||
status: "Unknown".to_string(),
|
||||
message: row.get(2).cloned().unwrap_or_default(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server(SetFilePriority, "/api/server_fns")]
|
||||
pub async fn set_file_priority(
|
||||
hash: String,
|
||||
file_index: u32,
|
||||
priority: u8,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use crate::xmlrpc::{RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
|
||||
let target = format!("{}:f{}", hash, file_index);
|
||||
let params = vec![
|
||||
RpcParam::from(target.as_str()),
|
||||
RpcParam::from(priority as i64),
|
||||
];
|
||||
|
||||
client
|
||||
.call("f.set_priority", ¶ms)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("RPC error: {}", e)))?;
|
||||
|
||||
let _ = client
|
||||
.call("d.update_priorities", &[RpcParam::from(hash.as_str())])
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(SetLabel, "/api/server_fns")]
|
||||
pub async fn set_label(hash: String, label: String) -> Result<(), ServerFnError> {
|
||||
use crate::xmlrpc::{RpcParam, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
let params = vec![RpcParam::from(hash.as_str()), RpcParam::from(label)];
|
||||
|
||||
client
|
||||
.call("d.custom1.set", ¶ms)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("RPC error: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetVersion, "/api/server_fns")]
|
||||
pub async fn get_version() -> Result<String, ServerFnError> {
|
||||
use crate::xmlrpc::{parse_string_response, RtorrentClient};
|
||||
let ctx = expect_context::<crate::ServerContext>();
|
||||
let client = RtorrentClient::new(&ctx.scgi_socket_path);
|
||||
|
||||
match client.call("system.client_version", &[]).await {
|
||||
Ok(xml) => {
|
||||
let version = parse_string_response(&xml).unwrap_or(xml);
|
||||
Ok(version)
|
||||
}
|
||||
Err(e) => Err(ServerFnError::new(format!("Failed to get version: {}", e))),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user