use chrono::{DateTime, Utc}; use std::{fs, io::Error, path::PathBuf, time::SystemTime}; #[derive(Clone, Debug, Default)] pub enum Sort { #[default] Modified, } #[derive(Clone, Debug, Default)] pub enum Order { #[default] Asc, Desc, } pub struct Torrent { pub bytes: Vec, pub time: DateTime, } pub struct Public { default_capacity: usize, pub default_limit: usize, root: PathBuf, } impl Public { // Constructors pub fn init( root: &PathBuf, default_limit: usize, default_capacity: usize, ) -> Result { if !root.is_dir() { return Err("Public root is not directory".into()); } Ok(Self { default_capacity, default_limit, root: root.canonicalize().map_err(|e| e.to_string())?, }) } // Getters pub fn torrent(&self, info_hash: librqbit_core::Id20) -> Option { let mut p = PathBuf::from(&self.root); p.push(format!("{}.{E}", info_hash.as_string())); Some(Torrent { bytes: fs::read(&p).ok()?, time: p.metadata().ok()?.modified().ok()?.into(), }) } pub fn torrents( &self, keyword: Option<&str>, sort_order: Option<(Sort, Order)>, start: Option, limit: Option, ) -> Result<(usize, Vec), Error> { let f = self.files(keyword, sort_order)?; let t = f.len(); let l = limit.unwrap_or(t); let mut b = Vec::with_capacity(l); for file in f.into_iter().skip(start.unwrap_or_default()).take(l) { b.push(Torrent { bytes: fs::read(file.path)?, time: file.modified.into(), }) } Ok((t, b)) } /// Build URI for given `path` /// /// * result requires URL encode 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 } } /// Return canonical absolute path /// /// * `None` if the given URI does not exist or has denied location pub fn path(&self, relative: &str) -> Option { let mut p = PathBuf::from(&self.root); p.push(&relative); let c = p.canonicalize().ok()?; if c.starts_with(&self.root) && c.exists() { Some(c) } else { None } } // Helpers fn files( &self, keyword: Option<&str>, sort_order: Option<(Sort, Order)>, ) -> Result, Error> { let mut files = Vec::with_capacity(self.default_capacity); for dir_entry in fs::read_dir(&self.root)? { let entry = dir_entry?; let path = entry.path(); if !path.is_file() || path.extension().is_none_or(|e| e != E) { continue; } if let Some(k) = keyword && !k.trim_matches(S).is_empty() && !librqbit_core::torrent_metainfo::torrent_from_bytes(&fs::read(&path)?) .is_ok_and(|m: librqbit_core::torrent_metainfo::TorrentMetaV1Owned| { k.split(S) .filter(|s| !s.is_empty()) .map(|s| s.trim().to_lowercase()) .all(|q| { m.info_hash.as_string().to_lowercase().contains(&q) || m.info .name .as_ref() .is_some_and(|n| n.to_string().to_lowercase().contains(&q)) || m.comment .as_ref() .is_some_and(|c| c.to_string().to_lowercase().contains(&q)) || m.created_by .as_ref() .is_some_and(|c| c.to_string().to_lowercase().contains(&q)) || m.publisher .as_ref() .is_some_and(|p| p.to_string().to_lowercase().contains(&q)) || m.publisher_url .as_ref() .is_some_and(|u| u.to_string().to_lowercase().contains(&q)) || m.announce .as_ref() .is_some_and(|a| a.to_string().to_lowercase().contains(&q)) || m.announce_list.iter().any(|l| { l.iter().any(|a| a.to_string().to_lowercase().contains(&q)) }) || m.info.files.as_ref().is_some_and(|f| { f.iter().any(|f| { let mut p = PathBuf::new(); f.full_path(&mut p).is_ok_and(|_| { p.to_string_lossy().to_lowercase().contains(&q) }) }) }) }) }) { continue; } files.push(File { modified: entry.metadata()?.modified()?, path, }) } if let Some((sort, order)) = sort_order { match sort { Sort::Modified => match order { Order::Asc => files.sort_by(|a, b| a.modified.cmp(&b.modified)), Order::Desc => files.sort_by(|a, b| b.modified.cmp(&a.modified)), }, } } Ok(files) } } // Local members /// Torrent file extension const E: &str = "torrent"; /// Search keyword separators const S: &[char] = &[ '_', '-', ':', ';', ',', '(', ')', '[', ']', '/', '!', '?', ' ', // @TODO make optional ]; struct File { modified: SystemTime, path: PathBuf, }