diff --git a/README.md b/README.md index 59b4a63..3bf30f5 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,17 @@ See the [Wiki](https://github.com/YGGverse/btracker/wiki/Screenshots) page ## Usage ``` bash -btracker --preload=/path/to/aquatic-crawler/preload\ +btracker --public=/path/to/aquatic-crawler/preload\ --scrape=udp://127.0.0.1:6969\ --tracker=udp://[302:68d0:f0d5:b88d::fdb]:6969\ --tracker=udp://tracker.ygg:6969 ``` -* The `--preload` argument specifies the location of the crawled torrents (see [aquatic-crawler](https://github.com/yggverse/aquatic-crawler)) +* The `--public` argument specifies the location of the crawled torrents (see [aquatic-crawler](https://github.com/yggverse/aquatic-crawler)) + * make sure this location also contains a copy (or symlink) of the `/public` files from this crate (see the [Rocket deploying specification](https://rocket.rs/guide/v0.5/deploying/) for details) * The `--scrape` argument is optional and enables statistics for peers, seeders, and leechers - * it is recommended to use the local address for faster performance - * this argument supports multiple definitions for both the IPv4 and IPv6 protocols, parsed from the URL value - * take a look at the `--udp` option if you want to customize the default binding for UDP scrapes + * it is recommended to use the local address for faster performance + * this argument supports multiple definitions for both the IPv4 and IPv6 protocols, parsed from the URL value + * take a look at the `--udp` option if you want to customize the default binding for UDP scrapes * Define as many `--tracker`(s) as required * Append `RUST_LOG=debug` for detailed information output; use `--debug` to configure as `rocket::Config::debug_default()` * See the project [Wiki](https://github.com/YGGverse/btracker/wiki) for more details (including [systemd](https://github.com/YGGverse/btracker/wiki/Systemd) and [nginx](https://github.com/YGGverse/btracker/wiki/Nginx) examples) diff --git a/public/.gitignore b/public/.gitignore new file mode 100644 index 0000000..3e17ef5 --- /dev/null +++ b/public/.gitignore @@ -0,0 +1,3 @@ +/* +!.gitignore +!/theme \ No newline at end of file diff --git a/static/theme/default.css b/public/theme/default.css similarity index 97% rename from static/theme/default.css rename to public/theme/default.css index ad34f5e..720e4f1 100644 --- a/static/theme/default.css +++ b/public/theme/default.css @@ -45,6 +45,7 @@ h1, h2, h3, h4, h5 { h1, h2 { font-size: 14px; + padding: 0 8px; } table { @@ -108,7 +109,7 @@ main > a { float: right; margin-left: 8px; opacity: .96; - padding: 8px; + padding: 8px 12px; } /* item row */ @@ -129,7 +130,7 @@ main > div > div { border-top: 1px solid var(--separator); margin-top: 16px; overflow: hidden; - padding-top: 16px; + padding: 16px 8px 0; } main > div > div > ul { diff --git a/static/theme/default/leechers.svg b/public/theme/default/leechers.svg similarity index 100% rename from static/theme/default/leechers.svg rename to public/theme/default/leechers.svg diff --git a/static/theme/default/magnet.svg b/public/theme/default/magnet.svg similarity index 100% rename from static/theme/default/magnet.svg rename to public/theme/default/magnet.svg diff --git a/static/theme/default/peers.svg b/public/theme/default/peers.svg similarity index 100% rename from static/theme/default/peers.svg rename to public/theme/default/peers.svg diff --git a/static/theme/default/seeders.svg b/public/theme/default/seeders.svg similarity index 100% rename from static/theme/default/seeders.svg rename to public/theme/default/seeders.svg diff --git a/src/config.rs b/src/config.rs index 94d9420..55614e1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,9 +8,13 @@ use url::Url; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Config { - /// Path to the [aquatic-crawler](https://github.com/yggverse/aquatic-crawler) preload files + /// Path to the `public` directory + /// + /// This location must contain: + /// * the default or custom `/public/*` files (see the [Rocket deploying specification](https://rocket.rs/guide/v0.5/deploying/)) + /// * torrents with files collected by the [aquatic-crawler](https://github.com/yggverse/aquatic-crawler) #[arg(long)] - pub preload: PathBuf, + pub public: PathBuf, /// Server name #[arg(long, default_value_t = String::from("βtracker"))] @@ -34,15 +38,11 @@ pub struct Config { #[arg(long, default_value_t = String::from("%d/%m/%Y %H:%M"))] pub format_time: String, - /// Path to the framework assets - #[arg(long, default_value_t = String::from("./static"))] - pub statics: String, - /// Default listing limit #[arg(long, default_value_t = 20)] pub list_limit: usize, - /// Default capacity (estimated torrents in the `preload` directory) + /// Default capacity (estimated torrents in the `public` directory) #[arg(long, default_value_t = 1000)] pub capacity: usize, diff --git a/src/feed.rs b/src/feed.rs index 64c4e21..8566644 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -60,7 +60,7 @@ impl Feed { pub fn push(&self, buffer: &mut String, torrent: Torrent) { buffer.push_str(&format!( "{}{}{}", - &torrent.info_hash, + torrent.info_hash, escape( torrent .name diff --git a/src/main.rs b/src/main.rs index a8452fa..21f494c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,14 +4,15 @@ extern crate rocket; mod config; mod feed; mod meta; +mod public; mod scraper; -mod storage; mod torrent; use config::Config; use feed::Feed; use meta::Meta; use plurify::Plurify; +use public::{Order, Public, Sort}; use rocket::{ State, http::Status, @@ -21,14 +22,13 @@ use rocket::{ use rocket_dyn_templates::{Template, context}; use scraper::{Scrape, Scraper}; use std::str::FromStr; -use storage::{Order, Sort, Storage}; use torrent::Torrent; #[get("/?")] fn index( page: Option, scraper: &State, - storage: &State, + public: &State, meta: &State, ) -> Result> { #[derive(Serialize)] @@ -42,14 +42,14 @@ fn index( size: String, torrent: Torrent, } - let (total, torrents) = storage + let (total, torrents) = public .torrents( Some((Sort::Modified, Order::Desc)), - page.map(|p| if p > 0 { p - 1 } else { p } * storage.default_limit), - Some(storage.default_limit), + page.map(|p| if p > 0 { p - 1 } else { p } * public.default_limit), + Some(public.default_limit), ) .map_err(|e| { - error!("Torrents storage read error: `{e}`"); + error!("Torrents public storage read error: `{e}`"); Custom(Status::InternalServerError, E.to_string()) })?; Ok(Template::render( @@ -57,11 +57,11 @@ fn index( context! { meta: meta.inner(), back: page.map(|p| uri!(index(if p > 2 { Some(p - 1) } else { None }))), - next: if page.unwrap_or(1) * storage.default_limit >= total { None } + next: if page.unwrap_or(1) * public.default_limit >= total { None } else { Some(uri!(index(Some(page.map_or(2, |p| p + 1))))) }, rows: torrents .into_iter() - .filter_map(|t| match Torrent::from_storage(&t.bytes, t.time) { + .filter_map(|t| match Torrent::from_public(&t.bytes, t.time) { Ok(torrent) => Some(R { created: torrent.creation_date.map(|t| t.format(&meta.format_time).to_string()), files: torrent.files(), @@ -80,7 +80,7 @@ fn index( pagination_totals: format!( "Page {} / {} ({total} {} total)", page.unwrap_or(1), - (total as f64 / storage.default_limit as f64).ceil(), + (total as f64 / public.default_limit as f64).ceil(), total.plurify(&["torrent", "torrents", "torrents"]) ) }, @@ -90,11 +90,11 @@ fn index( #[get("/")] fn info( info_hash: &str, - storage: &State, + public: &State, scraper: &State, meta: &State, ) -> Result> { - match storage.torrent(librqbit_core::Id20::from_str(info_hash).map_err(|e| { + match public.torrent(librqbit_core::Id20::from_str(info_hash).map_err(|e| { warn!("Torrent info-hash parse error: `{e}`"); Custom(Status::BadRequest, Status::BadRequest.to_string()) })?) { @@ -102,10 +102,11 @@ fn info( #[derive(Serialize)] #[serde(crate = "rocket::serde")] struct F { + href: Option, path: String, size: String, } - let torrent = Torrent::from_storage(&t.bytes, t.time).map_err(|e| { + let torrent = Torrent::from_public(&t.bytes, t.time).map_err(|e| { error!("Torrent parse error: `{e}`"); Custom(Status::InternalServerError, E.to_string()) })?; @@ -117,15 +118,19 @@ fn info( files_total: torrent.files(), files_list: torrent.files.as_ref().map(|f| { f.iter() - .map(|f| F { - path: f.path(), - size: f.size(), + .map(|f| { + let p = f.path(); + F { + href: public.href(&torrent.info_hash, &p), + path: p, + size: f.size(), + } }) .collect::>() }), indexed: torrent.time.format(&meta.format_time).to_string(), magnet: torrent.magnet(meta.trackers.as_ref()), - scrape: scraper.scrape(info_hash), + scrape: scraper.scrape(&torrent.info_hash), size: torrent.size(), torrent }, @@ -136,23 +141,23 @@ fn info( } #[get("/rss")] -fn rss(feed: &State, storage: &State) -> Result, Custom> { +fn rss(feed: &State, public: &State) -> Result, Custom> { let mut b = feed.transaction(1024); // @TODO - for t in storage + for t in public .torrents( Some((Sort::Modified, Order::Desc)), None, - Some(storage.default_limit), + Some(public.default_limit), ) .map_err(|e| { - error!("Torrent storage read error: `{e}`"); + error!("Torrent public storage read error: `{e}`"); Custom(Status::InternalServerError, E.to_string()) })? .1 { feed.push( &mut b, - Torrent::from_storage(&t.bytes, t.time).map_err(|e| { + Torrent::from_public(&t.bytes, t.time).map_err(|e| { error!("Torrent parse error: `{e}`"); Custom(Status::InternalServerError, E.to_string()) })?, @@ -199,7 +204,6 @@ fn rocket() -> _ { }) .map(|a| (config.udp, a)), ); - let storage = Storage::init(config.preload, config.list_limit, config.capacity).unwrap(); rocket::build() .attach(Template::fairing()) .configure(rocket::Config { @@ -213,7 +217,7 @@ fn rocket() -> _ { }) .manage(feed) .manage(scraper) - .manage(storage) + .manage(Public::init(config.public.clone(), config.list_limit, config.capacity).unwrap()) .manage(Meta { canonical: config.canonical_url, description: config.description, @@ -222,7 +226,7 @@ fn rocket() -> _ { trackers: config.tracker, version: env!("CARGO_PKG_VERSION").into(), }) - .mount("/", rocket::fs::FileServer::from(config.statics)) + .mount("/", rocket::fs::FileServer::from(config.public)) .mount("/", routes![index, info, rss]) } diff --git a/src/storage.rs b/src/public.rs similarity index 83% rename from src/storage.rs rename to src/public.rs index db2add9..7bab095 100644 --- a/src/storage.rs +++ b/src/public.rs @@ -25,13 +25,13 @@ pub struct Torrent { pub time: DateTime, } -pub struct Storage { +pub struct Public { pub default_limit: usize, default_capacity: usize, root: PathBuf, } -impl Storage { +impl Public { // Constructors pub fn init( @@ -40,7 +40,7 @@ impl Storage { default_capacity: usize, ) -> Result { if !root.is_dir() { - return Err("Storage root is not directory".into()); + return Err("Public root is not directory".into()); } Ok(Self { default_limit, @@ -88,6 +88,21 @@ impl Storage { Ok((t, b)) } + pub fn href(&self, info_hash: &str, path: &str) -> Option { + let mut relative = PathBuf::from(info_hash); + relative.push(path); + + let mut absolute = PathBuf::from(&self.root); + absolute.push(&relative); + + let c = absolute.canonicalize().ok()?; + if c.starts_with(&self.root) && c.exists() { + Some(relative.to_string_lossy().into()) + } else { + None + } + } + // Helpers fn files(&self, sort_order: Option<(Sort, Order)>) -> Result, Error> { diff --git a/src/torrent.rs b/src/torrent.rs index 162ed4d..94596b4 100644 --- a/src/torrent.rs +++ b/src/torrent.rs @@ -2,7 +2,7 @@ mod file; use chrono::{DateTime, Utc}; use file::File; -use librqbit_core::{torrent_metainfo, torrent_metainfo::TorrentMetaV1Owned}; +use librqbit_core::torrent_metainfo::{self, TorrentMetaV1Owned}; use rocket::serde::Serialize; #[derive(Clone, Debug, Serialize)] @@ -25,7 +25,7 @@ pub struct Torrent { } impl Torrent { - pub fn from_storage(bytes: &[u8], time: DateTime) -> Result { + pub fn from_public(bytes: &[u8], time: DateTime) -> Result { let i: TorrentMetaV1Owned = torrent_metainfo::torrent_from_bytes(bytes).map_err(|e| e.to_string())?; Ok(Torrent { @@ -84,11 +84,7 @@ impl Torrent { } pub fn magnet(&self, trackers: Option<&Vec>) -> String { - let mut b = if self.info_hash.len() == 40 { - format!("magnet:?xt=urn:btih:{}", self.info_hash) - } else { - todo!("info-hash v2 yet not supported") // librqbit_core::hash_id::Id - }; + let mut b = format!("magnet:?xt=urn:btih:{}", self.info_hash); if let Some(t) = trackers { for tracker in t { b.push_str("&tr="); diff --git a/templates/info.html.tera b/templates/info.html.tera index 33194f9..8974d7f 100644 --- a/templates/info.html.tera +++ b/templates/info.html.tera @@ -31,7 +31,13 @@ {% for file in files_list %} - {{ file.path }} + + {% if file.href %} + {{ file.path }} + {% else %} + {{ file.path }} + {% endif %} + {{ file.size }} {% endfor %}