use chrono::{DateTime, Utc}; use librqbit_core::{torrent_metainfo, torrent_metainfo::TorrentMetaV1Owned}; use rocket::serde::Serialize; use std::{ fs::{self, DirEntry}, path::PathBuf, }; #[derive(Clone, Debug, Default)] pub enum Sort { #[default] Modified, } #[derive(Clone, Debug, Default)] pub enum Order { #[default] Asc, Desc, } #[derive(Clone, Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct File { pub name: Option, pub length: u64, } #[derive(Clone, Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct Torrent { pub announce: Option, pub comment: Option, pub created_by: Option, pub creation_date: Option>, pub files: Option>, pub info_hash: String, pub is_private: bool, pub length: Option, pub name: Option, pub publisher_url: Option, pub publisher: Option, pub size: u64, /// File (modified) pub time: DateTime, } pub struct Storage { pub default_limit: usize, default_capacity: usize, root: PathBuf, } impl Storage { // Constructors pub fn init( root: PathBuf, default_limit: usize, default_capacity: usize, ) -> Result { if !root.is_dir() { return Err("Storage root is not directory".into()); } Ok(Self { default_limit, default_capacity, root: root.canonicalize().map_err(|e| e.to_string())?, }) } // Getters pub fn torrents( &self, sort_order: Option<(Sort, Order)>, start: Option, limit: Option, ) -> Result<(usize, Vec), String> { let f = self.files(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) { if file .path() .extension() .is_none_or(|e| e.is_empty() || e.to_string_lossy() != "torrent") { return Err("Unexpected file extension".into()); } let i: TorrentMetaV1Owned = torrent_metainfo::torrent_from_bytes( &fs::read(file.path()).map_err(|e| e.to_string())?, ) .map_err(|e| e.to_string())?; b.push(Torrent { info_hash: i.info_hash.as_string(), announce: i.announce.map(|a| a.to_string()), comment: i.comment.map(|c| c.to_string()), created_by: i.created_by.map(|c| c.to_string()), creation_date: i .creation_date .map(|t| DateTime::from_timestamp_nanos(t as i64)), size: i.info.length.unwrap_or_default() + i.info .files .as_ref() .map(|files| files.iter().map(|f| f.length).sum::()) .unwrap_or_default(), files: i.info.files.map(|files| { let limit = 1000; // @TODO let mut b = Vec::with_capacity(files.len()); let mut i = files.iter(); let mut t = 0; for f in i.by_ref() { if t < limit { t += 1; b.push(File { name: String::from_utf8( f.path .iter() .enumerate() .flat_map(|(n, b)| { if n == 0 { b.0.to_vec() } else { let mut p = vec![b'/']; p.extend(b.0.to_vec()); p } }) .collect(), ) .ok(), length: f.length, }); continue; } // limit reached: count sizes left and use placeholder as the last item name let mut l = 0; for f in i.by_ref() { l += f.length } b.push(File { name: Some("...".to_string()), length: l, }); break; } b[..t].sort_by(|a, b| a.name.cmp(&b.name)); // @TODO optional b }), publisher_url: i.publisher_url.map(|u| u.to_string()), publisher: i.publisher.map(|p| p.to_string()), is_private: i.info.private, length: i.info.length, name: i.info.name.map(|e| e.to_string()), time: file .metadata() .map_err(|e| e.to_string())? .modified() .map_err(|e| e.to_string())? .into(), }) } Ok((t, b)) } // Helpers fn files(&self, sort_order: Option<(Sort, Order)>) -> Result, String> { let mut b = Vec::with_capacity(self.default_capacity); for entry in fs::read_dir(&self.root).map_err(|e| e.to_string())? { let e = entry.map_err(|e| e.to_string())?; match e.file_type() { Ok(t) => { if t.is_file() { b.push((e.metadata().unwrap().modified().unwrap(), e)) } } Err(e) => warn!("{}", e.to_string()), } } if let Some((sort, order)) = sort_order { match sort { Sort::Modified => match order { Order::Asc => b.sort_by(|a, b| a.0.cmp(&b.0)), Order::Desc => b.sort_by(|a, b| b.0.cmp(&a.0)), }, } } Ok(b.into_iter().map(|e| e.1).collect()) } }