feat: add api docs and refactor AppEvent to struct variant

This commit is contained in:
spinline
2026-02-03 22:02:28 +03:00
parent 07f148ed4e
commit 33ee44e1f0
6 changed files with 113 additions and 30 deletions

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
axum = { version = "0.7", features = ["macros", "ws"] } axum = { version = "0.8", features = ["macros", "ws"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tower = { version = "0.4", features = ["util", "timeout"] } tower = { version = "0.4", features = ["util", "timeout"] }
tower-http = { version = "0.5", features = ["fs", "trace", "cors", "compression-full"] } tower-http = { version = "0.5", features = ["fs", "trace", "cors", "compression-full"] }
@@ -24,3 +24,5 @@ mime_guess = "2.0"
shared = { path = "../shared" } shared = { path = "../shared" }
thiserror = "2.0.18" thiserror = "2.0.18"
dotenvy = "0.15.7" dotenvy = "0.15.7"
utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }

View File

@@ -8,13 +8,16 @@ use axum::{
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use serde::Deserialize; use serde::Deserialize;
use shared::TorrentActionRequest; use shared::TorrentActionRequest;
use utoipa::ToSchema;
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "../frontend/dist"] #[folder = "../frontend/dist"]
pub struct Asset; pub struct Asset;
#[derive(Deserialize)] #[derive(Deserialize, ToSchema)]
pub struct AddTorrentRequest { pub struct AddTorrentRequest {
/// Magnet link or Torrent file URL
#[schema(example = "magnet:?xt=urn:btih:...")]
uri: String, uri: String,
} }
@@ -46,6 +49,16 @@ pub async fn static_handler(uri: Uri) -> impl IntoResponse {
} }
} }
/// Add a new torrent via magnet link or URL
#[utoipa::path(
post,
path = "/api/torrents/add",
request_body = AddTorrentRequest,
responses(
(status = 200, description = "Torrent added successfully"),
(status = 500, description = "Internal server error or rTorrent fault")
)
)]
pub async fn add_torrent_handler( pub async fn add_torrent_handler(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<AddTorrentRequest>, Json(payload): Json<AddTorrentRequest>,
@@ -193,6 +206,18 @@ async fn delete_torrent_with_data(
} }
} }
/// Perform an action on a torrent (start, stop, delete)
#[utoipa::path(
post,
path = "/api/torrents/action",
request_body = TorrentActionRequest,
responses(
(status = 200, description = "Action executed successfully"),
(status = 400, description = "Invalid action or request"),
(status = 403, description = "Forbidden: Security risk detected"),
(status = 500, description = "Internal server error")
)
)]
pub async fn handle_torrent_action( pub async fn handle_torrent_action(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<TorrentActionRequest>, Json(payload): Json<TorrentActionRequest>,

View File

@@ -22,6 +22,8 @@ use tower_http::{
cors::CorsLayer, cors::CorsLayer,
trace::TraceLayer, trace::TraceLayer,
}; };
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
@@ -47,6 +49,27 @@ struct Args {
port: u16, port: u16,
} }
#[derive(OpenApi)]
#[openapi(
paths(
handlers::add_torrent_handler,
handlers::handle_torrent_action
),
components(
schemas(
handlers::AddTorrentRequest,
shared::TorrentActionRequest,
shared::Torrent,
shared::TorrentStatus,
shared::Theme
)
),
tags(
(name = "vibetorrent", description = "VibeTorrent API")
)
)]
struct ApiDoc;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Load .env file // Load .env file
@@ -122,8 +145,10 @@ async fn main() {
match diff::diff_torrents(&previous_torrents, &new_torrents) { match diff::diff_torrents(&previous_torrents, &new_torrents) {
diff::DiffResult::FullUpdate => { diff::DiffResult::FullUpdate => {
let _ = let _ = event_bus_tx.send(AppEvent::FullList {
event_bus_tx.send(AppEvent::FullList(new_torrents.clone(), now)); torrents: new_torrents.clone(),
timestamp: now,
});
} }
diff::DiffResult::Partial(updates) => { diff::DiffResult::Partial(updates) => {
for update in updates { for update in updates {
@@ -144,6 +169,7 @@ async fn main() {
}); });
let app = Router::new() let app = Router::new()
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/api/events", get(sse::sse_handler)) .route("/api/events", get(sse::sse_handler))
.route("/api/torrents/add", post(handlers::add_torrent_handler)) .route("/api/torrents/add", post(handlers::add_torrent_handler))
.route( .route(

View File

@@ -1,8 +1,7 @@
use leptos::*;
use shared::{Torrent, AppEvent};
use futures::StreamExt; use futures::StreamExt;
use gloo_net::eventsource::futures::EventSource; use gloo_net::eventsource::futures::EventSource;
use leptos::*;
use shared::{AppEvent, Torrent};
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FilterStatus { pub enum FilterStatus {
@@ -41,7 +40,11 @@ pub fn provide_torrent_store() {
let filter = create_rw_signal(FilterStatus::All); let filter = create_rw_signal(FilterStatus::All);
let search_query = create_rw_signal(String::new()); let search_query = create_rw_signal(String::new());
let store = TorrentStore { torrents, filter, search_query }; let store = TorrentStore {
torrents,
filter,
search_query,
};
provide_context(store); provide_context(store);
// Initialize SSE connection // Initialize SSE connection
@@ -54,21 +57,40 @@ pub fn provide_torrent_store() {
if let Some(data_str) = msg.data().as_string() { if let Some(data_str) = msg.data().as_string() {
if let Ok(event) = serde_json::from_str::<AppEvent>(&data_str) { if let Ok(event) = serde_json::from_str::<AppEvent>(&data_str) {
match event { match event {
AppEvent::FullList(list, _) => { AppEvent::FullList { torrents: list, .. } => {
torrents.set(list); torrents.set(list);
} }
AppEvent::Update(update) => { AppEvent::Update(update) => {
torrents.update(|list| { torrents.update(|list| {
if let Some(t) = list.iter_mut().find(|t| t.hash == update.hash) { if let Some(t) = list.iter_mut().find(|t| t.hash == update.hash)
if let Some(name) = update.name { t.name = name; } {
if let Some(size) = update.size { t.size = size; } if let Some(name) = update.name {
if let Some(down_rate) = update.down_rate { t.down_rate = down_rate; } t.name = name;
if let Some(up_rate) = update.up_rate { t.up_rate = up_rate; } }
if let Some(percent_complete) = update.percent_complete { t.percent_complete = percent_complete; } if let Some(size) = update.size {
if let Some(completed) = update.completed { t.completed = completed; } t.size = size;
if let Some(eta) = update.eta { t.eta = eta; } }
if let Some(status) = update.status { t.status = status; } if let Some(down_rate) = update.down_rate {
if let Some(error_message) = update.error_message { t.error_message = error_message; } t.down_rate = down_rate;
}
if let Some(up_rate) = update.up_rate {
t.up_rate = up_rate;
}
if let Some(percent_complete) = update.percent_complete {
t.percent_complete = percent_complete;
}
if let Some(completed) = update.completed {
t.completed = completed;
}
if let Some(eta) = update.eta {
t.eta = eta;
}
if let Some(status) = update.status {
t.status = status;
}
if let Some(error_message) = update.error_message {
t.error_message = error_message;
}
} }
}); });
} }

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies] [dependencies]
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
utoipa = { version = "5.4.0", features = ["axum_extras"] }

View File

@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
pub struct Torrent { pub struct Torrent {
pub hash: String, pub hash: String,
pub name: String, pub name: String,
@@ -15,7 +16,7 @@ pub struct Torrent {
pub added_date: i64, pub added_date: i64,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
pub enum TorrentStatus { pub enum TorrentStatus {
Downloading, Downloading,
Seeding, Seeding,
@@ -25,14 +26,17 @@ pub enum TorrentStatus {
Queued, Queued,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
#[serde(tag = "type", content = "data")] #[serde(tag = "type", content = "data")]
pub enum AppEvent { pub enum AppEvent {
FullList(Vec<Torrent>, u64), // u64 is likely free_space_bytes FullList {
torrents: Vec<Torrent>,
timestamp: u64,
},
Update(TorrentUpdate), Update(TorrentUpdate),
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct TorrentUpdate { pub struct TorrentUpdate {
pub hash: String, pub hash: String,
pub name: Option<String>, pub name: Option<String>,
@@ -46,14 +50,17 @@ pub struct TorrentUpdate {
pub error_message: Option<String>, pub error_message: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct TorrentActionRequest { pub struct TorrentActionRequest {
/// The hash of the torrent
#[schema(example = "5D4C9065...")]
pub hash: String, pub hash: String,
pub action: String, // "start", "stop", "delete" /// The action to perform: "start", "stop", "delete", "delete_with_data"
#[schema(example = "start")]
pub action: String,
} }
// Added Theme here to separate it from backend logic but allow frontend usage via shared #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum Theme { pub enum Theme {
Midnight, Midnight,
Light, Light,