use anyhow::{Result, bail}; use std::{collections::HashSet, fs::File, io::Write, path::PathBuf, str::FromStr}; use url::Url; /// Export crawl index to the RSS file pub struct Rss { file: File, target: PathBuf, tmp: PathBuf, trackers: Option>, } impl Rss { /// Create writable file for given `filepath` pub fn new( filepath: &str, title: &str, link: &Option, description: &Option, trackers: Option>, ) -> Result { // prevent from reading of the incomplete file let tmp = PathBuf::from_str(&format!("{filepath}.tmp"))?; // init public destination let target = PathBuf::from_str(filepath)?; if target.is_dir() { bail!("RSS path `{}` is directory", target.to_string_lossy()) } // init temporary file to write let mut file = File::create(&tmp)?; file.write_all( b"", )?; file.write_all(escape(title).as_bytes())?; file.write_all(b"")?; if let Some(s) = link { file.write_all(b"")?; file.write_all(escape(s).as_bytes())?; file.write_all(b"")? } if let Some(s) = description { file.write_all(b"")?; file.write_all(escape(s).as_bytes())?; file.write_all(b"")? } Ok(Self { file, target, trackers, tmp, }) } /// Append `item` to the feed `channel` pub fn push( &mut self, infohash: &str, title: &str, description: Option, pub_date: Option<&str>, ) -> Result<()> { self.file.write_all( format!( "{infohash}{}{}", escape(title), escape(&crate::magnet(infohash, self.trackers.as_ref())) ) .as_bytes(), )?; if let Some(s) = description { self.file.write_all(b"")?; self.file.write_all(escape(&s).as_bytes())?; self.file.write_all(b"")? } if let Some(s) = pub_date { self.file.write_all(b"")?; self.file.write_all(escape(s).as_bytes())?; self.file.write_all(b"")? } self.file.write_all(b"")?; Ok(()) } /// Write final bytes, replace public file with temporary one pub fn commit(mut self) -> Result<()> { self.file.write_all(b"")?; std::fs::rename(self.tmp, self.target)?; Ok(()) } } pub fn item_description(size: Option, list: Option<&Vec<(String, u64)>>) -> Option { use crate::format::Format; if size.is_none() && list.is_none() { return None; } let mut b = Vec::with_capacity(list.map(|l| l.len()).unwrap_or_default() + 1); if let Some(s) = size { b.push(s.bytes()) } if let Some(l) = list { for (path, size) in l { b.push(format!("{path} ({})", size.bytes())) } } Some(b.join("\n")) } fn escape(subject: &str) -> String { subject .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace("'", "'") }