// utils.rs // // Copyright 2017 Jordan Petridis // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later //! Helper utilities for accomplishing various tasks. use chrono::prelude::*; use rayon::prelude::*; use url::{Position, Url}; use crate::dbqueries; use crate::errors::DataError; use crate::models::{EpisodeCleanerModel, Save, Show}; use crate::xdg_dirs::DL_DIR; use std::fs; use std::path::Path; /// Scan downloaded `episode` entries that might have broken `local_uri`s and /// set them to `None`. fn download_checker() -> Result<(), DataError> { let mut episodes = dbqueries::get_downloaded_episodes()?; episodes .par_iter_mut() .filter_map(|ep| { if !Path::new(ep.local_uri()?).exists() { return Some(ep); } None }) .for_each(|ep| { ep.set_local_uri(None); ep.save() .map_err(|err| error!("{}", err)) .map_err(|_| error!("Error while trying to update episode: {:#?}", ep)) .ok(); }); Ok(()) } /// Delete watched `episodes` that have exceded their liftime after played. fn played_cleaner(cleanup_date: DateTime) -> Result<(), DataError> { let mut episodes = dbqueries::get_played_cleaner_episodes()?; let now_utc = cleanup_date.timestamp() as i32; episodes .par_iter_mut() .filter(|ep| ep.local_uri().is_some() && ep.played().is_some()) .for_each(|ep| { let limit = ep.played().unwrap(); if now_utc > limit { delete_local_content(ep) .map(|_| info!("Episode {:?} was deleted succesfully.", ep.local_uri())) .map_err(|err| error!("Error: {}", err)) .map_err(|_| error!("Failed to delete file: {:?}", ep.local_uri())) .ok(); } }); Ok(()) } /// Check `ep.local_uri` field and delete the file it points to. fn delete_local_content(ep: &mut EpisodeCleanerModel) -> Result<(), DataError> { if ep.local_uri().is_some() { let uri = ep.local_uri().unwrap().to_owned(); if Path::new(&uri).exists() { let res = fs::remove_file(&uri); if res.is_ok() { ep.set_local_uri(None); ep.save()?; } else { error!("Error while trying to delete file: {}", uri); error!("{}", res.unwrap_err()); }; } } else { error!( "Something went wrong evaluating the following path: {:?}", ep.local_uri(), ); } Ok(()) } /// Database cleaning tasks. /// /// Runs a download checker which looks for `Episode.local_uri` entries that /// doesn't exist and sets them to None /// /// Runs a cleaner for played Episode's that are pass the lifetime limit and /// scheduled for removal. pub fn checkup(cleanup_date: DateTime) -> Result<(), DataError> { info!("Running database checks."); download_checker()?; played_cleaner(cleanup_date)?; info!("Checks completed."); Ok(()) } /// Remove fragment identifiers and query pairs from a URL /// If url parsing fails, return's a trimmed version of the original input. pub fn url_cleaner(s: &str) -> String { // Copied from the cookbook. // https://rust-lang-nursery.github.io/rust-cookbook/net.html // #remove-fragment-identifiers-and-query-pairs-from-a-url match Url::parse(s) { Ok(parsed) => parsed[..Position::AfterPath].to_owned(), _ => s.trim().to_owned(), } } /// Returns the URI of a Show Downloads given it's title. pub fn get_download_folder(pd_title: &str) -> Result { // It might be better to make it a hash of the title or the Show rowid let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title); // Create the folder fs::DirBuilder::new() .recursive(true) .create(&download_fold)?; Ok(download_fold) } /// Removes all the entries associated with the given show from the database, /// and deletes all of the downloaded content. // TODO: Write Tests pub fn delete_show(pd: &Show) -> Result<(), DataError> { dbqueries::remove_feed(pd)?; info!("{} was removed succesfully.", pd.title()); let fold = get_download_folder(pd.title())?; fs::remove_dir_all(&fold)?; info!("All the content at, {} was removed succesfully", &fold); Ok(()) } #[cfg(test)] use crate::Feed; #[cfg(test)] /// Helper function that open a local file, parse the rss::Channel and gives back a Feed object. /// Alternative Feed constructor to be used for tests. pub fn get_feed(file_path: &str, id: i32) -> Feed { use crate::feed::FeedBuilder; use rss::Channel; use std::fs; use std::io::BufReader; // open the xml file let feed = fs::File::open(file_path).unwrap(); // parse it into a channel let chan = Channel::read_from(BufReader::new(feed)).unwrap(); FeedBuilder::default() .channel(chan) .source_id(id) .build() .unwrap() } #[cfg(test)] mod tests { extern crate tempdir; use self::tempdir::TempDir; use super::*; use chrono::Duration; use failure::Error; use crate::database::truncate_db; use crate::models::NewEpisodeBuilder; use std::fs::File; use std::io::Write; fn helper_db() -> Result { // Clean the db truncate_db()?; // Setup tmp file stuff let tmp_dir = TempDir::new("podcasts_test")?; let valid_path = tmp_dir.path().join("virtual_dl.mp3"); let bad_path = tmp_dir.path().join("invalid_thing.mp3"); let mut tmp_file = File::create(&valid_path)?; writeln!(tmp_file, "Foooo")?; // Setup episodes let n1 = NewEpisodeBuilder::default() .title("foo_bar".to_string()) .show_id(0) .build() .unwrap() .to_episode()?; let n2 = NewEpisodeBuilder::default() .title("bar_baz".to_string()) .show_id(1) .build() .unwrap() .to_episode()?; let mut ep1 = dbqueries::get_episode_cleaner_from_pk(n1.title(), n1.show_id())?; let mut ep2 = dbqueries::get_episode_cleaner_from_pk(n2.title(), n2.show_id())?; ep1.set_local_uri(Some(valid_path.to_str().unwrap())); ep2.set_local_uri(Some(bad_path.to_str().unwrap())); ep1.save()?; ep2.save()?; Ok(tmp_dir) } #[test] fn test_download_checker() -> Result<(), Error> { let tmp_dir = helper_db()?; download_checker()?; let episodes = dbqueries::get_downloaded_episodes()?; let valid_path = tmp_dir.path().join("virtual_dl.mp3"); assert_eq!(episodes.len(), 1); assert_eq!( Some(valid_path.to_str().unwrap()), episodes.first().unwrap().local_uri() ); let _tmp_dir = helper_db()?; download_checker()?; let episode = dbqueries::get_episode_cleaner_from_pk("bar_baz", 1)?; assert!(episode.local_uri().is_none()); Ok(()) } #[test] fn test_download_cleaner() -> Result<(), Error> { let _tmp_dir = helper_db()?; let mut episode: EpisodeCleanerModel = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?.into(); let valid_path = episode.local_uri().unwrap().to_owned(); delete_local_content(&mut episode)?; assert_eq!(Path::new(&valid_path).exists(), false); Ok(()) } #[test] fn test_played_cleaner_expired() -> Result<(), Error> { let _tmp_dir = helper_db()?; let mut episode = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?; let cleanup_date = Utc::now() - Duration::seconds(1000); let epoch = cleanup_date.timestamp() as i32 - 1; episode.set_played(Some(epoch)); episode.save()?; let valid_path = episode.local_uri().unwrap().to_owned(); // This should delete the file played_cleaner(cleanup_date)?; assert_eq!(Path::new(&valid_path).exists(), false); Ok(()) } #[test] fn test_played_cleaner_none() -> Result<(), Error> { let _tmp_dir = helper_db()?; let mut episode = dbqueries::get_episode_cleaner_from_pk("foo_bar", 0)?; let cleanup_date = Utc::now() - Duration::seconds(1000); let epoch = cleanup_date.timestamp() as i32 + 1; episode.set_played(Some(epoch)); episode.save()?; let valid_path = episode.local_uri().unwrap().to_owned(); // This should not delete the file played_cleaner(cleanup_date)?; assert_eq!(Path::new(&valid_path).exists(), true); Ok(()) } #[test] fn test_url_cleaner() -> Result<(), Error> { let good_url = "http://traffic.megaphone.fm/FL8608731318.mp3"; let bad_url = "http://traffic.megaphone.fm/FL8608731318.mp3?updated=1484685184"; assert_eq!(url_cleaner(bad_url), good_url); assert_eq!(url_cleaner(good_url), good_url); assert_eq!(url_cleaner(&format!(" {}\t\n", bad_url)), good_url); Ok(()) } #[test] // This test needs access to local system so we ignore it by default. #[ignore] fn test_get_dl_folder() -> Result<(), Error> { let foo_ = format!("{}/{}", DL_DIR.to_str().unwrap(), "foo"); assert_eq!(get_download_folder("foo")?, foo_); let _ = fs::remove_dir_all(foo_); Ok(()) } }