use anyhow::{Context, Result};
use dirs::data_local_dir;
use reqwest::blocking::get;
use std::fs;
use std::path::{Path, PathBuf};
use url::Url;
use crate::WEBAPP_PATH;
fn icons_dir() -> Result<PathBuf> {
let data_local =
data_local_dir().ok_or_else(|| anyhow::anyhow!("No data local directory found"))?;
let icons_dir = data_local.join(WEBAPP_PATH).join("icons");
fs::create_dir_all(&icons_dir)
.with_context(|| format!("Failed to create icons directory: {:?}", icons_dir))?;
Ok(icons_dir)
}
fn get_extension(path: &Path) -> Option<String> {
path.extension()
.map(|ext| ext.to_string_lossy().to_string())
}
fn get_stored_icon_path(codename: &str, ext: &str) -> PathBuf {
icons_dir()
.expect("Failed to get icons dir")
.join(format!("{}.{}", codename, ext))
}
pub fn resolve_icon(codename: &str, icon_input: &str) -> Result<String> {
if !icon_input.contains('/') && !icon_input.starts_with("http") {
return Ok(icon_input.to_string());
}
let input_path = Path::new(icon_input);
if icon_input.starts_with("https://") {
let ext = icon_extension_from_url(icon_input);
let output_path = get_stored_icon_path(codename, &ext);
download_icon(icon_input, &output_path)?;
return Ok(output_path.to_string_lossy().to_string());
}
if input_path.exists() {
let ext = get_extension(input_path).context("Cannot determine icon extension from file")?;
let output_path = get_stored_icon_path(codename, &ext);
copy_icon(input_path, &output_path)?;
return Ok(output_path.to_string_lossy().to_string());
}
Err(anyhow::anyhow!("Invalid icon path: {}", icon_input))
}
fn icon_extension_from_url(url: &str) -> String {
let default_ext = "png".to_string();
if let Some(query_start) = url.find('?') {
let url_without_query = &url[..query_start];
Path::new(url_without_query)
.extension()
.map(|ext| ext.to_string_lossy().to_string())
.unwrap_or(default_ext)
} else {
Path::new(url)
.extension()
.map(|ext| ext.to_string_lossy().to_string())
.unwrap_or(default_ext)
}
}
fn copy_icon(src: &Path, dest: &Path) -> Result<()> {
fs::copy(src, dest)
.with_context(|| format!("Failed to copy icon from {:?} to {:?}", src, dest))?;
Ok(())
}
fn download_icon(url: &str, dest: &Path) -> Result<()> {
let response = get(url).with_context(|| format!("Failed to download icon from {}", url))?;
let mut file = fs::File::create(dest)
.with_context(|| format!("Failed to create icon file at {:?}", dest))?;
let content = response
.bytes()
.with_context(|| format!("Failed to read response from {}", url))?;
use std::io::Write;
file.write_all(&content)
.with_context(|| format!("Failed to write icon to {:?}", dest))?;
Ok(())
}
pub fn delete_icon(codename: &str) -> Result<()> {
let icons_dir = icons_dir()?;
let mut found = vec![];
for entry in fs::read_dir(&icons_dir)
.with_context(|| format!("Failed to read icons directory: {:?}", icons_dir))?
{
let entry = entry?;
let path = entry.path();
if let Some(name) = path.file_name() {
let name_str = name.to_string_lossy();
if name_str.starts_with(&format!("{}.", codename)) {
found.push(path);
}
}
}
for icon_path in found {
fs::remove_file(&icon_path)
.with_context(|| format!("Failed to delete icon at {:?}", icon_path))?;
}
Ok(())
}
pub fn fetch_favicon(url: &str, codename: &str) -> Result<String> {
let parsed_url = Url::parse(url).with_context(|| format!("Failed to parse URL: {}", url))?;
let base_url = format!(
"{}://{}",
parsed_url.scheme(),
parsed_url.host_str().unwrap()
);
if let Some(favicon_url) =
fetch_favicon_from_html(&base_url).context("Failed to fetch and parse HTML")?
{
return download_and_store_favicon(&favicon_url, codename);
}
let ico_url = format!("{}/favicon.ico", base_url);
if test_url_exists(&ico_url) {
return download_and_store_favicon(&ico_url, codename);
}
let domain = parsed_url.host_str().unwrap();
let google_url = format!("https://www.google.com/s2/favicons?domain={}&sz=64", domain);
download_and_store_favicon(&google_url, codename)
}
fn fetch_favicon_from_html(base_url: &str) -> Result<Option<String>> {
let response =
get(base_url).with_context(|| format!("Failed to fetch HTML from {}", base_url))?;
let html = response
.text()
.with_context(|| format!("Failed to read HTML from {}", base_url))?;
let icon_regex = regex::Regex::new(
r#"<link[^>]+rel=["'](?:icon|apple-touch-icon|shortcut icon)["'][^>]+href=["']([^"']+)["']"#,
)?;
for cap in icon_regex.captures_iter(&html) {
if let Some(href) = cap.get(1) {
let href_str = href.as_str();
let favicon_url = if href_str.starts_with("http") {
href_str.to_string()
} else if href_str.starts_with("//") {
format!("{}:{}", base_url.split(':').next().unwrap(), href_str)
} else if href_str.starts_with('/') {
format!("{}{}", base_url, href_str)
} else {
format!("{}/{}", base_url, href_str)
};
if test_url_exists(&favicon_url) {
return Ok(Some(favicon_url));
}
}
}
Ok(None)
}
fn test_url_exists(url: &str) -> bool {
get(url)
.map(|resp| resp.status().is_success())
.unwrap_or(false)
}
fn download_and_store_favicon(url: &str, codename: &str) -> Result<String> {
let ext = icon_extension_from_url(url);
let output_path = get_stored_icon_path(codename, &ext);
download_icon(url, &output_path)?;
Ok(output_path.to_string_lossy().to_string())
}