diff --git a/src/format.rs b/src/format.rs index 1170454..64a706b 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,17 +1,50 @@ -pub fn size(value: u64) -> String { - const KB: f32 = 1024.0; - const MB: f32 = KB * KB; - const GB: f32 = MB * KB; +use librqbit_core::torrent_metainfo::TorrentMetaV1Owned; +use plurify::Plurify; +use url::Url; - let f = value as f32; - - if f < KB { - format!("{value} B") - } else if f < MB { - format!("{:.2} KB", f / KB) - } else if f < GB { - format!("{:.2} MB", f / MB) - } else { - format!("{:.2} GB", f / GB) - } +pub fn files(meta: &TorrentMetaV1Owned) -> String { + let total = meta.info.files.as_ref().map(|f| f.len()).unwrap_or(1); + format!("{total} {}", total.plurify(&["file", "files", "files"])) +} + +pub fn size(meta: &TorrentMetaV1Owned) -> String { + fn s(value: u64) -> String { + const KB: f32 = 1024.0; + const MB: f32 = KB * KB; + const GB: f32 = MB * KB; + + let f = value as f32; + + if f < KB { + format!("{value} B") + } else if f < MB { + format!("{:.2} KB", f / KB) + } else if f < GB { + format!("{:.2} MB", f / MB) + } else { + format!("{:.2} GB", f / GB) + } + } + s(meta + .info + .files + .as_ref() + .map(|files| files.iter().map(|file| file.length).sum::()) + .unwrap_or_default() + + meta.info.length.unwrap_or_default()) +} + +pub fn magnet(meta: &TorrentMetaV1Owned, trackers: Option<&Vec>) -> String { + let mut b = format!("magnet:?xt=urn:btih:{}", meta.info_hash.as_string()); + if let Some(ref n) = meta.info.name { + b.push_str("&dn="); + b.push_str(&urlencoding::encode(&n.to_string())) + } + if let Some(t) = trackers { + for tracker in t { + b.push_str("&tr="); + b.push_str(&urlencoding::encode(tracker.as_str())) + } + } + b } diff --git a/src/main.rs b/src/main.rs index e6c6438..67a8dcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,23 +2,25 @@ mod config; mod format; use anyhow::Result; -use btracker_fs::public::{Order, Public, Sort}; +use btracker_fs::public::{Order, Public, Sort, Torrent}; use chrono::Local; use clap::Parser; use config::Config; -use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes}; +use librqbit_core::{ + Id20, + torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes}, +}; use log::*; use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream}; -use plurify::Plurify; use std::{ fs::File, io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}, + str::FromStr, sync::Arc, thread, }; use titanite::*; -use url::Url; fn main() -> Result<()> { if std::env::var("RUST_LOG").is_ok() { @@ -153,10 +155,10 @@ fn response( stream: &mut TlsStream, ) { debug!("Incoming request from `{peer}` to `{}`", request.url.path()); - + let p = request.url.path().trim_matches('/'); // try index page - if request.url.path().trim_end_matches("/").is_empty() { - return send( + if p.is_empty() { + send( &match index( config, public, @@ -184,25 +186,74 @@ fn response( } }, stream, - |result| match result { - Ok(()) => debug!("Home page request from peer `{peer}`"), - Err(e) => error!("Internal server error on handle peer `{peer}` request: `{e}`"), + |result| { + if let Err(e) = result { + error!("Internal server error on handle peer `{peer}` request: `{e}`") + } }, - ); + ) } // try info page - todo!() -} - -fn close(stream: &mut TlsStream) -> Result<()> { - stream.flush()?; - // close connection gracefully - // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections - stream.shutdown()?; - Ok(()) + else if let Ok(id) = Id20::from_str(p) + && 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()), + } + .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() + ) + } + }, + ) + } } fn send(data: &[u8], stream: &mut TlsStream, callback: impl FnOnce(Result<()>)) { + fn close(stream: &mut TlsStream) -> Result<()> { + stream.flush()?; + // close connection gracefully + // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections + stream.shutdown()?; + Ok(()) + } callback((|| { stream.write_all(data)?; close(stream)?; @@ -210,7 +261,11 @@ fn send(data: &[u8], stream: &mut TlsStream, callback: impl FnOnce(Re })()); } +// rotes + fn index(config: &Config, public: &Public, page: Option) -> Result { + use plurify::Plurify; + let (total, torrents) = public.torrents( None, // @TODO Some((Sort::Modified, Order::Desc)), @@ -246,13 +301,9 @@ fn index(config: &Config, public: &Public, page: Option) -> Result {} Magnet\n", - magnet(&i, config.tracker.as_ref()) - ))*/ } b.push("## Navigation\n".into()); @@ -283,33 +334,33 @@ fn index(config: &Config, public: &Public, page: Option) -> Result String { - let total = meta.info.files.as_ref().map(|f| f.len()).unwrap_or(1); - format!("{total} {}", total.plurify(&["file", "files", "files"])) -} +fn info(config: &Config, torrent: Torrent) -> Result { + let i: TorrentMetaV1Owned = torrent_from_bytes(&torrent.bytes)?; -fn size(meta: &TorrentMetaV1Owned) -> String { - format::size( - meta.info - .files + let mut b = Vec::new(); + + b.push(format!("# {}\n", config.name)); + + b.push(format!( + "## {}\n", + i.info + .name .as_ref() - .map(|files| files.iter().map(|file| file.length).sum::()) + .map(|n| n.to_string()) .unwrap_or_default() - + meta.info.length.unwrap_or_default(), - ) -} + )); -fn magnet(meta: &TorrentMetaV1Owned, trackers: Option<&Vec>) -> String { - let mut b = format!("magnet:?xt=urn:btih:{}", meta.info_hash.as_string()); - if let Some(ref n) = meta.info.name { - b.push_str("&dn="); - b.push_str(&urlencoding::encode(&n.to_string())) - } - if let Some(t) = trackers { - for tracker in t { - b.push_str("&tr="); - b.push_str(&urlencoding::encode(tracker.as_str())) - } - } - b + b.push(format!( + "{} • {} • {}\n", + torrent.time.format(&config.date), + format::size(&i), + format::files(&i), + )); + + b.push(format!( + "=> {} Magnet\n", + format::magnet(&i, config.tracker.as_ref()) + )); + + Ok(b.join("\n")) }