mirror of
https://github.com/YGGverse/btracker-gemini.git
synced 2026-03-31 09:05:30 +00:00
implement btracker-scrape API, use shared State Arc for initiated components
This commit is contained in:
parent
64ad6f01a8
commit
d9edf977c7
3 changed files with 101 additions and 41 deletions
|
|
@ -12,6 +12,7 @@ repository = "https://github.com/YGGverse/btracker-gemini"
|
|||
[dependencies]
|
||||
anyhow = "1.0.95"
|
||||
btracker-fs = { version = "0.2.0", features = ["public"] }
|
||||
btracker-scrape = "0.1.0"
|
||||
chrono = "^0.4.20"
|
||||
clap = { version = "4.5.30", features = ["derive"] }
|
||||
log = "0.4.28"
|
||||
|
|
@ -27,4 +28,5 @@ regex = "1.11.2"
|
|||
# development
|
||||
[patch.crates-io]
|
||||
btracker-fs = { git = "https://github.com/YGGverse/btracker-fs.git" }
|
||||
btracker-scrape = { git = "https://github.com/YGGverse/btracker-scrape.git" }
|
||||
# btracker-fs = { path = "../btracker-fs" }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use clap::Parser;
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
path::PathBuf,
|
||||
};
|
||||
use url::Url;
|
||||
|
|
@ -47,4 +47,13 @@ pub struct Config {
|
|||
/// Default index capacity
|
||||
#[arg(short, long, default_value_t = 1000)]
|
||||
pub capacity: usize,
|
||||
|
||||
/// Bind scrape UDP server
|
||||
///
|
||||
/// * requires `tracker` value(s) to enable scrape features
|
||||
#[arg(long, default_values_t = vec![
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
|
||||
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0))
|
||||
])]
|
||||
pub udp: Vec<SocketAddr>,
|
||||
}
|
||||
|
|
|
|||
129
src/main.rs
129
src/main.rs
|
|
@ -4,6 +4,7 @@ mod route;
|
|||
|
||||
use anyhow::Result;
|
||||
use btracker_fs::public::{Order, Public, Sort, Torrent};
|
||||
use btracker_scrape::*;
|
||||
use config::Config;
|
||||
use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes};
|
||||
use log::*;
|
||||
|
|
@ -11,7 +12,7 @@ use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream};
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
net::{SocketAddr, TcpListener, TcpStream},
|
||||
net::{IpAddr, SocketAddr, TcpListener, TcpStream},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
thread,
|
||||
|
|
@ -35,8 +36,43 @@ fn main() -> Result<()> {
|
|||
.init()
|
||||
}
|
||||
|
||||
let config = Arc::new(Config::parse());
|
||||
let public = Arc::new(Public::init(&config.storage, config.limit, config.capacity).unwrap());
|
||||
let config = Config::parse();
|
||||
let state = Arc::new(State {
|
||||
public: Public::init(&config.storage, config.limit, config.capacity).unwrap(),
|
||||
scrape: Scrape::init(
|
||||
config
|
||||
.tracker
|
||||
.as_ref()
|
||||
.map(|u| {
|
||||
u.iter()
|
||||
.map(|url| {
|
||||
use std::str::FromStr;
|
||||
if url.scheme() == "tcp" {
|
||||
todo!("TCP scrape is not implemented")
|
||||
}
|
||||
if url.scheme() != "udp" {
|
||||
todo!("Scheme `{}` is not supported", url.scheme())
|
||||
}
|
||||
SocketAddr::new(
|
||||
IpAddr::from_str(
|
||||
url.host_str()
|
||||
.expect("Required valid host value")
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']'),
|
||||
)
|
||||
.unwrap(),
|
||||
url.port().expect("Required valid port value"),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.map(|a| (config.udp, a)),
|
||||
),
|
||||
format_date: config.format_date,
|
||||
name: config.name,
|
||||
description: config.description,
|
||||
tracker: config.tracker,
|
||||
});
|
||||
|
||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls
|
||||
let acceptor = TlsAcceptor::new(Identity::from_pkcs12(
|
||||
|
|
@ -48,7 +84,7 @@ fn main() -> Result<()> {
|
|||
&config.password,
|
||||
)?)?;
|
||||
|
||||
let listener = TcpListener::bind(&config.bind)?;
|
||||
let listener = TcpListener::bind(config.bind)?;
|
||||
|
||||
info!("Server started on `{}`", config.bind);
|
||||
|
||||
|
|
@ -56,11 +92,10 @@ fn main() -> Result<()> {
|
|||
match stream {
|
||||
Ok(stream) => {
|
||||
thread::spawn({
|
||||
let config = config.clone();
|
||||
let public = public.clone();
|
||||
let state = state.clone();
|
||||
let peer = stream.peer_addr()?;
|
||||
let connection = acceptor.accept(stream);
|
||||
move || handle(config, public, peer, connection)
|
||||
move || handle(state, peer, connection)
|
||||
});
|
||||
}
|
||||
Err(e) => error!("{e}"),
|
||||
|
|
@ -70,8 +105,7 @@ fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
fn handle(
|
||||
config: Arc<Config>,
|
||||
public: Arc<Public>,
|
||||
state: Arc<State>,
|
||||
peer: SocketAddr,
|
||||
connection: Result<TlsStream<TcpStream>, HandshakeError<TcpStream>>,
|
||||
) {
|
||||
|
|
@ -110,7 +144,7 @@ fn handle(
|
|||
if header_buffer.last().is_some_and(|&b| b == b'\n') {
|
||||
// header bytes contain valid Gemini **request**
|
||||
if let Ok(request) = request::Gemini::from_bytes(&header_buffer) {
|
||||
return response(request, &config, &public, &peer, &mut stream);
|
||||
return response(request, &state, &peer, &mut stream);
|
||||
}
|
||||
|
||||
// header bytes received but yet could not be parsed,
|
||||
|
|
@ -148,8 +182,7 @@ fn handle(
|
|||
|
||||
fn response(
|
||||
request: titanite::request::Gemini,
|
||||
config: &Config,
|
||||
public: &Public,
|
||||
state: &State,
|
||||
peer: &SocketAddr,
|
||||
stream: &mut TlsStream<TcpStream>,
|
||||
) {
|
||||
|
|
@ -157,7 +190,7 @@ fn response(
|
|||
use titanite::response::*;
|
||||
debug!("Incoming request from `{peer}` to `{}`", request.url.path());
|
||||
send(
|
||||
&match Route::from_url(&request.url, public) {
|
||||
&match Route::from_url(&request.url, &state.public) {
|
||||
Route::File(ref path) => success::Default {
|
||||
data: &std::fs::read(path).unwrap(),
|
||||
meta: success::default::Meta {
|
||||
|
|
@ -174,6 +207,8 @@ fn response(
|
|||
"image/webp"
|
||||
} else if e == "txt" || e == "log" {
|
||||
"text/plain"
|
||||
} else if e == "gemini" || e == "gmi" {
|
||||
"text/gemini"
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -184,7 +219,7 @@ fn response(
|
|||
},
|
||||
}
|
||||
.into_bytes(),
|
||||
Route::List { page, keyword } => match list(config, public, keyword.as_deref(), page) {
|
||||
Route::List { page, keyword } => match list(state, keyword.as_deref(), page) {
|
||||
Ok(data) => success::Default {
|
||||
data: data.as_bytes(),
|
||||
meta: success::default::Meta {
|
||||
|
|
@ -204,8 +239,8 @@ fn response(
|
|||
message: Some("Keyword, file, hash...".into()),
|
||||
})
|
||||
.into_bytes(),
|
||||
Route::Info(id) => match public.torrent(id) {
|
||||
Some(torrent) => match info(config, public, torrent) {
|
||||
Route::Info(id) => match state.public.torrent(id) {
|
||||
Some(torrent) => match info(state, torrent) {
|
||||
Ok(data) => success::Default {
|
||||
data: data.as_bytes(),
|
||||
meta: success::default::Meta {
|
||||
|
|
@ -267,12 +302,7 @@ fn send(data: &[u8], stream: &mut TlsStream<TcpStream>, callback: impl FnOnce(Re
|
|||
})());
|
||||
}
|
||||
|
||||
fn list(
|
||||
config: &Config,
|
||||
public: &Public,
|
||||
keyword: Option<&str>,
|
||||
page: Option<usize>,
|
||||
) -> Result<String> {
|
||||
fn list(state: &State, keyword: Option<&str>, page: Option<usize>) -> Result<String> {
|
||||
use plurify::Plurify;
|
||||
|
||||
/// format search keyword as the pagination query
|
||||
|
|
@ -280,22 +310,22 @@ fn list(
|
|||
keyword.map(|k| format!("?{}", k)).unwrap_or_default()
|
||||
}
|
||||
|
||||
let (total, torrents) = public.torrents(
|
||||
let (total, torrents) = state.public.torrents(
|
||||
keyword,
|
||||
Some((Sort::Modified, Order::Desc)),
|
||||
page.map(|p| if p > 0 { p - 1 } else { p } * public.default_limit),
|
||||
Some(public.default_limit),
|
||||
page.map(|p| if p > 0 { p - 1 } else { p } * state.public.default_limit),
|
||||
Some(state.public.default_limit),
|
||||
)?;
|
||||
|
||||
let mut b = Vec::new();
|
||||
|
||||
b.push(format!("# {}\n", config.name));
|
||||
b.push(format!("# {}\n", state.name));
|
||||
|
||||
if let Some(ref description) = config.description {
|
||||
if let Some(ref description) = state.description {
|
||||
b.push(format!("{description}\n"));
|
||||
}
|
||||
|
||||
if let Some(ref trackers) = config.tracker {
|
||||
if let Some(ref trackers) = state.tracker {
|
||||
//b.push(format!("## Connect\n"));
|
||||
b.push("```".into());
|
||||
for tracker in trackers {
|
||||
|
|
@ -321,11 +351,16 @@ fn list(
|
|||
.unwrap_or_default()
|
||||
));
|
||||
b.push(format!(
|
||||
"{} • {} • {}\n",
|
||||
torrent.time.format(&config.format_date),
|
||||
"{} • {} • {}{}\n",
|
||||
torrent.time.format(&state.format_date),
|
||||
format::total(&i),
|
||||
format::files(&i),
|
||||
))
|
||||
state
|
||||
.scrape
|
||||
.scrape(i.info_hash.0)
|
||||
.map(|s| format!(" ↑ {} ↓ {} ⏲ {}", s.seeders, s.peers, s.leechers))
|
||||
.unwrap_or_default()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,11 +369,11 @@ fn list(
|
|||
b.push(format!(
|
||||
"Page {} / {} ({total} {} total)\n",
|
||||
page.unwrap_or(1),
|
||||
(total as f64 / public.default_limit as f64).ceil(),
|
||||
(total as f64 / state.public.default_limit as f64).ceil(),
|
||||
total.plurify(&["torrent", "torrents", "torrents"])
|
||||
));
|
||||
|
||||
if page.unwrap_or(1) * public.default_limit < total {
|
||||
if page.unwrap_or(1) * state.public.default_limit < total {
|
||||
b.push(format!(
|
||||
"=> /{}{} Next",
|
||||
page.map_or(2, |p| p + 1),
|
||||
|
|
@ -363,7 +398,7 @@ fn list(
|
|||
Ok(b.join("\n"))
|
||||
}
|
||||
|
||||
fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
||||
fn info(state: &State, torrent: Torrent) -> Result<String> {
|
||||
struct File {
|
||||
path: Option<PathBuf>,
|
||||
length: u64,
|
||||
|
|
@ -387,25 +422,30 @@ fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
|||
.name
|
||||
.as_ref()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(config.name.clone())
|
||||
.unwrap_or(state.name.clone())
|
||||
));
|
||||
|
||||
b.push(format!(
|
||||
"{} • {} • {}\n",
|
||||
torrent.time.format(&config.format_date),
|
||||
"{} • {} • {}{}\n",
|
||||
torrent.time.format(&state.format_date),
|
||||
format::total(&i),
|
||||
format::files(&i),
|
||||
state
|
||||
.scrape
|
||||
.scrape(i.info_hash.0)
|
||||
.map(|s| format!(" ↑ {} ↓ {} ⏲ {}", s.seeders, s.peers, s.leechers))
|
||||
.unwrap_or_default()
|
||||
));
|
||||
|
||||
b.push(format!(
|
||||
"=> {} Magnet\n",
|
||||
format::magnet(&i, config.tracker.as_ref())
|
||||
format::magnet(&i, state.tracker.as_ref())
|
||||
));
|
||||
|
||||
if let Some(files) = i.info.files.map(|files| {
|
||||
let mut b = Vec::with_capacity(files.len());
|
||||
for f in files {
|
||||
let mut p = std::path::PathBuf::new();
|
||||
let mut p = PathBuf::new();
|
||||
b.push(File {
|
||||
length: f.length,
|
||||
path: match f.full_path(&mut p) {
|
||||
|
|
@ -423,7 +463,7 @@ fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
|||
b.push("## Files\n".into());
|
||||
for file in files {
|
||||
let p = file.path();
|
||||
b.push(match public.href(&i.info_hash.as_string(), &p) {
|
||||
b.push(match state.public.href(&i.info_hash.as_string(), &p) {
|
||||
Some(href) => format!(
|
||||
"=> {} {} ({})",
|
||||
urlencoding::encode(&href),
|
||||
|
|
@ -437,3 +477,12 @@ fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
|||
|
||||
Ok(b.join("\n"))
|
||||
}
|
||||
|
||||
struct State {
|
||||
description: Option<String>,
|
||||
format_date: String,
|
||||
name: String,
|
||||
public: Public,
|
||||
scrape: Scrape,
|
||||
tracker: Option<Vec<url::Url>>,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue