// Copyright (C) 2024 Philipp Hofer // // Licensed under the EUPL, Version 1.2 or - as soon they will be approved by // the European Commission - subsequent versions of the EUPL (the "Licence"). // You may not use this work except in compliance with the Licence. // // You should have received a copy of the European Union Public License along // with this program. If not, you may obtain a copy of the Licence at: // // // Unless required by applicable law or agreed to in writing, software // distributed under the Licence is distributed on an "AS IS" basis, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the Licence for the specific language governing permissions and // limitations under the Licence. use std::{fs, io, path::Path}; use time::{format_description, OffsetDateTime}; #[derive(Debug)] #[allow(dead_code)] pub struct Error { pub(crate) msg: String, } impl Error { pub(crate) fn new(msg: &str) -> Self { Self { msg: msg.into() } } } impl From for Error { fn from(value: ureq::Error) -> Self { Self { msg: value.to_string(), } } } impl From for Error { fn from(value: io::Error) -> Self { Self { msg: value.to_string(), } } } impl From for Error { fn from(value: serde_json::Error) -> Self { Self { msg: value.to_string(), } } } impl From for Error { fn from(value: toml::de::Error) -> Self { Self { msg: value.to_string(), } } } impl From for Error { fn from(value: roxmltree::Error) -> Self { Self { msg: value.to_string(), } } } /// Returns the current date in YYYY-MM-DD format. Needed for RIS API query to get current version of the overview. pub(crate) fn current_date() -> String { let local_date = OffsetDateTime::now_utc(); let format = format_description::parse("[year]-[month]-[day]") .expect("Can't fail, supplied format is fine"); local_date.format(&format).expect("Failed to format date") } #[cfg(not(test))] pub(crate) fn get_cache_dir() -> Result { let cache_dir = directories::BaseDirs::new().ok_or(Error { msg: "directories crate could not find basedirs.".into(), })?; let cache_dir = cache_dir.cache_dir(); Ok(format!( "{}/risp/", cache_dir .to_str() .ok_or(Error::new("Cache dir can't be found"))? )) } #[cfg(test)] pub(crate) fn get_cache_dir() -> Result { Ok("./data/cache/".into()) } /// Clears the cache directory used for storing law texts from RIS. /// /// This function deletes all files and directories within the cache directory, effectively clearing all cached law texts. /// The cache is used to avoid repeated web requests for the same information, by storing the law texts locally after the first retrieval. /// The cache directory location is determined by the [`cache_dir` method from the `directories` crate's `BaseDirs` struct](https://docs.rs/directories/latest/directories/struct.BaseDirs.html#method.cache_dir). /// /// # Returns /// /// Returns `Ok(())` if the cache was successfully cleared. If an error occurs during the operation, it returns an `Error`. /// /// # Errors /// /// This function can return an `Error` in cases such as: /// /// - Failure to retrieve the cache directory path. /// - Issues reading the cache directory contents. /// - Errors encountered while attempting to delete files or directories within the cache directory. pub fn clear_cache() -> Result<(), Error> { Ok(delete_all_in_dir(Path::new(&get_cache_dir()?))?) } fn delete_all_in_dir>(dir_path: P) -> std::io::Result<()> { let entries = fs::read_dir(dir_path)?; for entry in entries { let entry = entry?; let path = entry.path(); if path.is_dir() { delete_all_in_dir(&path)?; fs::remove_dir(&path)?; } else { // If it's a file, delete it fs::remove_file(&path)?; } } Ok(()) } pub(crate) fn fetch_with_retries(url: &str) -> Result { let mut attempts = 0; let max_attempts = 100; loop { match ureq::get(url).call() { Ok(response) => { // If the request is successful, return the response body match response.into_string() { Ok(d) => return Ok(d), Err(e) => return Err(e.into()), } } Err(ureq::Error::Transport(_)) => { // Increment the attempt counter attempts += 1; // Check if the maximum number of attempts has been reached if attempts >= max_attempts { return Err(Error::new("Maximum attempts reached.")); } // TODO: Implement a backoff strategy here. As a simple example, we sleep for 1 second. std::thread::sleep(std::time::Duration::from_secs(1)); } Err(e) => { // For non-transport errors, return the error immediately. return Err(e.into()); } } } }