From 96c3df1b419c6728d9bffb432d542ef001b40e84 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 8 Sep 2025 21:49:09 +0300 Subject: [PATCH] implement search feature --- Cargo.toml | 1 + src/main.rs | 160 ++++++++++++++++++++++++--------------------------- src/route.rs | 42 ++++++++++++++ 3 files changed, 119 insertions(+), 84 deletions(-) create mode 100644 src/route.rs diff --git a/Cargo.toml b/Cargo.toml index 3e84f6d..ebf5689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ librqbit-core = "5.0.0" plurify = "0.2.0" url = "2.5.7" urlencoding = "2.1.3" +regex = "1.11.2" # development [patch.crates-io] diff --git a/src/main.rs b/src/main.rs index 8ba1658..9059048 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,13 @@ mod config; mod format; +mod route; use anyhow::Result; use btracker_fs::public::{Order, Public, Sort, Torrent}; use chrono::Local; use clap::Parser; use config::Config; -use librqbit_core::{ - Id20, - torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes}, -}; +use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes}; use log::*; use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream}; use std::{ @@ -17,7 +15,6 @@ use std::{ io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}, path::PathBuf, - str::FromStr, sync::Arc, thread, }; @@ -155,23 +152,16 @@ fn response( peer: &SocketAddr, stream: &mut TlsStream, ) { + use route::Route; + debug!("Incoming request from `{peer}` to `{}`", request.url.path()); - let path = request.url.path().trim_matches('/'); - // try index page - if path.is_empty() { - send( - &match index( - config, - public, - request.url.query(), - request.url.query_pairs().find_map(|a| { - if a.0 == "page" { - a.1.parse::().ok() - } else { - None - } - }), - ) { + + let route = Route::from_url(&request.url); + + // try index page, including optional page value + match route { + Route::List { page } => send( + &match list(config, public, request.url.query(), page) { Ok(data) => response::success::Default { data: data.as_bytes(), meta: response::success::default::Meta { @@ -193,11 +183,8 @@ fn response( error!("Internal server error on handle peer `{peer}` request: `{e}`") } }, - ) - } - // try search - else if path == "search" { - send( + ), + Route::Search => send( &response::Input::Default(response::input::Default { message: Some("Keyword, file, hash...".into()), }) @@ -211,58 +198,55 @@ fn response( ) } }, - ) - } - // try info page - else if let Ok(id) = Id20::from_str(path) - && let Some(torrent) = public.torrent(id) - { - send( - &match info(config, torrent) { - Ok(data) => response::success::Default { - data: data.as_bytes(), - meta: response::success::default::Meta { - mime: "text/gemini".to_string(), - }, - } - .into_bytes(), - Err(e) => { - error!("Internal server error on handle peer `{peer}` request: `{e}`"); - response::failure::temporary::General { - message: Some("Internal server error".to_string()), + ), + Route::Info(id) => match public.torrent(id) { + Some(torrent) => send( + &match info(config, torrent) { + Ok(data) => response::success::Default { + data: data.as_bytes(), + meta: response::success::default::Meta { + mime: "text/gemini".to_string(), + }, } - .into_bytes() - } - }, - stream, - |result| { - if let Err(e) = result { - error!("Internal server error on handle peer `{peer}` request: `{e}`") - } - }, - ) - } - // not found - else { - warn!( - "Requested resource `{}` not found by peer `{peer}`", - request.url.as_str() - ); - send( - &response::Failure::Permanent(response::failure::Permanent::NotFound( - response::failure::permanent::NotFound { message: None }, - )) - .into_bytes(), - stream, - |result| { - if let Err(e) = result { - error!( - "Internal server error on handle peer `{peer}` request `{}`: `{e}`", - request.url.as_str() - ) - } - }, - ) + .into_bytes(), + Err(e) => { + error!("Internal server error on handle peer `{peer}` request: `{e}`"); + response::failure::temporary::General { + message: Some("Internal server error".to_string()), + } + .into_bytes() + } + }, + stream, + |result| { + if let Err(e) = result { + error!("Internal server error on handle peer `{peer}` request: `{e}`") + } + }, + ), + None => todo!(), + }, + Route::NotFound => { + warn!( + "Requested resource `{}` not found by peer `{peer}`", + request.url.as_str() + ); + send( + &response::Failure::Permanent(response::failure::Permanent::NotFound( + response::failure::permanent::NotFound { message: None }, + )) + .into_bytes(), + stream, + |result| { + if let Err(e) = result { + error!( + "Internal server error on handle peer `{peer}` request `{}`: `{e}`", + request.url.as_str() + ) + } + }, + ) + } } } @@ -283,7 +267,7 @@ fn send(data: &[u8], stream: &mut TlsStream, callback: impl FnOnce(Re // rotes -fn index( +fn list( config: &Config, public: &Public, keyword: Option<&str>, @@ -291,6 +275,10 @@ fn index( ) -> Result { use plurify::Plurify; + fn query(keyword: Option<&str>) -> String { + keyword.map(|k| format!("?{}", k)).unwrap_or_default() + } + let (total, torrents) = public.torrents( keyword, Some((Sort::Modified, Order::Desc)), @@ -347,18 +335,22 @@ fn index( if let Some(p) = page { b.push(format!( - "=> /{} Back", + "=> {}{} Back", if p > 2 { - Some(format!("?page={}", p - 1)) + format!("/{}", p - 1) } else { - None - } - .unwrap_or_default() + "/".into() + }, + query(keyword) )) } if page.unwrap_or(1) * public.default_limit < total { - b.push(format!("=> /?page={} Next", page.map_or(2, |p| p + 1))) + b.push(format!( + "=> /{}{} Next", + page.map_or(2, |p| p + 1), + query(keyword) + )) } b.push("\n=> /search Search".into()); diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 0000000..4e6e578 --- /dev/null +++ b/src/route.rs @@ -0,0 +1,42 @@ +use std::str::FromStr; + +use librqbit_core::Id20; +use regex::Regex; +use url::Url; + +pub enum Route { + Info(Id20), + List { page: Option }, + NotFound, + Search, +} + +impl Route { + pub fn from_url(url: &Url) -> Self { + let p = url.path().to_lowercase(); + let q = url.query(); + + if p.is_empty() { + return Self::List { page: None }; + } + + if let Ok(id) = Id20::from_str(p.trim_matches('/')) { + return Self::Info(id); + } + + if p == "/search" && q.is_none() { + return Self::Search; + } + + if Regex::new(r"^/(|search)").unwrap().is_match(&p) { + return Self::List { + page: Regex::new(r"/(\d+)$").unwrap().captures(&p).map(|c| { + c.get(1) + .map_or(1, |p| p.as_str().parse::().unwrap_or(1)) + }), + }; + } + + Self::NotFound + } +}