use std::collections::HashMap;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::types::ContentTypeId;
use crate::utils;
pub const ERROR_CONNECTION_FAIL: i32 = -1;
pub const ERROR_TIMEOUT: i32 = -2;
pub const ERROR_SERVER_RESET: i32 = -3;
pub const ERROR_SEND_ERROR: i32 = -4;
pub const ERROR_SKIPPED: i32 = -6;
pub const CACHE_TYPE_HAS_CACHE_CONTROL: u32 = 1;
pub const CACHE_TYPE_HAS_EXPIRES: u32 = 2;
pub const CACHE_TYPE_HAS_ETAG: u32 = 4;
pub const CACHE_TYPE_HAS_LAST_MODIFIED: u32 = 8;
pub const CACHE_TYPE_HAS_MAX_AGE: u32 = 16;
pub const CACHE_TYPE_HAS_S_MAX_AGE: u32 = 32;
pub const CACHE_TYPE_HAS_STALE_WHILE_REVALIDATE: u32 = 64;
pub const CACHE_TYPE_HAS_STALE_IF_ERROR: u32 = 128;
pub const CACHE_TYPE_HAS_PUBLIC: u32 = 256;
pub const CACHE_TYPE_HAS_PRIVATE: u32 = 512;
pub const CACHE_TYPE_HAS_NO_CACHE: u32 = 1024;
pub const CACHE_TYPE_HAS_NO_STORE: u32 = 2048;
pub const CACHE_TYPE_HAS_MUST_REVALIDATE: u32 = 4096;
pub const CACHE_TYPE_HAS_PROXY_REVALIDATE: u32 = 8192;
pub const CACHE_TYPE_HAS_IMMUTABLE: u32 = 16384;
pub const CACHE_TYPE_NO_CACHE_HEADERS: u32 = 32768;
pub const CACHE_TYPE_NOT_AVAILABLE: u32 = 65536;
pub const SOURCE_INIT_URL: i32 = 5;
pub const SOURCE_A_HREF: i32 = 10;
pub const SOURCE_IMG_SRC: i32 = 20;
pub const SOURCE_IMG_SRCSET: i32 = 21;
pub const SOURCE_INPUT_SRC: i32 = 22;
pub const SOURCE_SOURCE_SRC: i32 = 23;
pub const SOURCE_VIDEO_SRC: i32 = 24;
pub const SOURCE_AUDIO_SRC: i32 = 25;
pub const SOURCE_SCRIPT_SRC: i32 = 30;
pub const SOURCE_INLINE_SCRIPT_SRC: i32 = 40;
pub const SOURCE_LINK_HREF: i32 = 50;
pub const SOURCE_CSS_URL: i32 = 60;
pub const SOURCE_JS_URL: i32 = 70;
pub const SOURCE_REDIRECT: i32 = 80;
pub const SOURCE_SITEMAP: i32 = 90;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisitedUrl {
pub uq_id: String,
pub source_uq_id: String,
pub source_attr: i32,
pub url: String,
pub status_code: i32,
pub request_time: f64,
pub request_time_formatted: String,
pub size: Option<i64>,
pub size_formatted: Option<String>,
pub content_encoding: Option<String>,
pub content_type: ContentTypeId,
pub content_type_header: Option<String>,
pub extras: Option<HashMap<String, String>>,
pub is_external: bool,
pub is_allowed_for_crawling: bool,
pub cache_type_flags: u32,
pub cache_lifetime: Option<i64>,
}
impl VisitedUrl {
#[allow(clippy::too_many_arguments)]
pub fn new(
uq_id: String,
source_uq_id: String,
source_attr: i32,
url: String,
status_code: i32,
request_time: f64,
size: Option<i64>,
content_type: ContentTypeId,
content_type_header: Option<String>,
content_encoding: Option<String>,
extras: Option<HashMap<String, String>>,
is_external: bool,
is_allowed_for_crawling: bool,
cache_type_flags: u32,
cache_lifetime: Option<i64>,
) -> Self {
let request_time_formatted = utils::get_formatted_duration(request_time);
let size_formatted = size.map(|s| utils::get_formatted_size(s, 0));
Self {
uq_id,
source_uq_id,
source_attr,
url,
status_code,
request_time,
request_time_formatted,
size,
size_formatted,
content_encoding,
content_type,
content_type_header,
extras,
is_external,
is_allowed_for_crawling,
cache_type_flags,
cache_lifetime,
}
}
pub fn is_https(&self) -> bool {
self.url.starts_with("https://")
}
pub fn is_static_file(&self) -> bool {
matches!(
self.content_type,
ContentTypeId::Image
| ContentTypeId::Script
| ContentTypeId::Stylesheet
| ContentTypeId::Video
| ContentTypeId::Audio
| ContentTypeId::Document
| ContentTypeId::Font
| ContentTypeId::Json
| ContentTypeId::Xml
)
}
pub fn is_image(&self) -> bool {
self.content_type == ContentTypeId::Image
}
pub fn is_video(&self) -> bool {
self.content_type == ContentTypeId::Video
}
pub fn get_source_description(&self, source_url: Option<&str>) -> String {
let source = source_url.unwrap_or("unknown");
match self.source_attr {
SOURCE_INIT_URL => "Initial URL".to_string(),
SOURCE_A_HREF => format!("<a href> on {}", source),
SOURCE_IMG_SRC => format!("<img src> on {}", source),
SOURCE_IMG_SRCSET => format!("<img srcset> on {}", source),
SOURCE_INPUT_SRC => format!("<input src> on {}", source),
SOURCE_SOURCE_SRC => format!("<source src> on {}", source),
SOURCE_VIDEO_SRC => format!("<video src> on {}", source),
SOURCE_AUDIO_SRC => format!("<audio src> on {}", source),
SOURCE_SCRIPT_SRC => format!("<script src> on {}", source),
SOURCE_INLINE_SCRIPT_SRC => format!("<script> on {}", source),
SOURCE_LINK_HREF => format!("<link href> on {}", source),
SOURCE_CSS_URL => format!("CSS url() on {}", source),
SOURCE_JS_URL => format!("JS url on {}", source),
SOURCE_REDIRECT => format!("Redirect from {}", source),
SOURCE_SITEMAP => format!("URL in sitemap {}", source),
_ => "Unknown source".to_string(),
}
}
pub fn get_source_short_name(&self) -> &'static str {
match self.source_attr {
SOURCE_INIT_URL => "Initial URL",
SOURCE_A_HREF => "<a href>",
SOURCE_IMG_SRC => "<img src>",
SOURCE_IMG_SRCSET => "<img srcset>",
SOURCE_INPUT_SRC => "<input src>",
SOURCE_SOURCE_SRC => "<source src>",
SOURCE_VIDEO_SRC => "<video src>",
SOURCE_AUDIO_SRC => "<audio src>",
SOURCE_SCRIPT_SRC => "<script src>",
SOURCE_INLINE_SCRIPT_SRC => "inline <script src>",
SOURCE_LINK_HREF => "<link href>",
SOURCE_CSS_URL => "css url()",
SOURCE_JS_URL => "js url",
SOURCE_REDIRECT => "redirect",
SOURCE_SITEMAP => "sitemap",
_ => "unknown",
}
}
pub fn looks_like_static_file_by_url(&self) -> bool {
use once_cell::sync::Lazy;
static RE_STATIC_FILE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)\.(jpg|jpeg|png|gif|webp|svg|ico|js|css|txt|woff2|woff|ttf|eot|mp4|webm|ogg|mp3|wav|flac|pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar|gz|bz2|7z|xml|json)",
).unwrap()
});
RE_STATIC_FILE.is_match(&self.url)
}
pub fn has_error_status_code(&self) -> bool {
self.status_code < 0
}
pub fn get_scheme(&self) -> Option<String> {
url::Url::parse(&self.url).ok().map(|u| u.scheme().to_string())
}
pub fn get_host(&self) -> Option<String> {
url::Url::parse(&self.url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_string()))
}
pub fn get_port(&self) -> u16 {
if let Ok(parsed) = url::Url::parse(&self.url) {
parsed.port().unwrap_or_else(|| if self.is_https() { 443 } else { 80 })
} else if self.is_https() {
443
} else {
80
}
}
pub fn get_cache_type_label(&self) -> String {
let mut labels = Vec::new();
if self.cache_type_flags & CACHE_TYPE_HAS_CACHE_CONTROL != 0 {
labels.push("Cache-Control");
} else if self.cache_type_flags & CACHE_TYPE_HAS_EXPIRES != 0 {
labels.push("Expires");
}
if self.cache_type_flags & CACHE_TYPE_HAS_ETAG != 0 {
labels.push("ETag");
}
if self.cache_type_flags & CACHE_TYPE_HAS_LAST_MODIFIED != 0 {
labels.push("Last-Modified");
}
if labels.is_empty() {
"No cache headers".to_string()
} else {
labels.join(" + ")
}
}
}