2025-05-24 02:31:23 +03:00
|
|
|
use std::{io, path::PathBuf};
|
|
|
|
|
|
|
|
|
|
use color_eyre::eyre::{WrapErr as _, eyre};
|
|
|
|
|
use tokio::io::AsyncWriteExt as _;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
use crate::event::{AppEvent, Event};
|
|
|
|
|
use crate::{fs::get_cache_path, utils::is_docker};
|
|
|
|
|
|
2025-08-05 10:41:12 +03:00
|
|
|
#[derive(Clone, Copy)]
|
2025-05-24 02:31:23 +03:00
|
|
|
pub enum DbType {
|
|
|
|
|
Asn,
|
|
|
|
|
Geo,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DbType {
|
2025-08-05 10:41:12 +03:00
|
|
|
const fn name(self) -> &'static str {
|
2025-05-24 13:51:22 +03:00
|
|
|
match self {
|
|
|
|
|
Self::Asn => "ASN",
|
|
|
|
|
Self::Geo => "geolocation",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 10:41:12 +03:00
|
|
|
const fn url(self) -> &'static str {
|
2025-05-24 02:31:23 +03:00
|
|
|
match self {
|
2025-08-10 22:00:30 +03:00
|
|
|
Self::Asn => {
|
2025-09-18 14:20:57 +03:00
|
|
|
"https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/refs/heads/download/GeoLite2-ASN.mmdb"
|
2025-08-10 22:00:30 +03:00
|
|
|
}
|
|
|
|
|
Self::Geo => {
|
2025-09-18 14:20:57 +03:00
|
|
|
"https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/refs/heads/download/GeoLite2-City.mmdb"
|
2025-08-10 22:00:30 +03:00
|
|
|
}
|
2025-05-24 02:31:23 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 22:00:30 +03:00
|
|
|
async fn db_path(self) -> crate::Result<PathBuf> {
|
2025-09-26 17:08:57 +03:00
|
|
|
let mut cache_path = get_cache_path().await?;
|
2025-05-24 02:31:23 +03:00
|
|
|
match self {
|
|
|
|
|
Self::Asn => cache_path.push("asn_database.mmdb"),
|
|
|
|
|
Self::Geo => cache_path.push("geolocation_database.mmdb"),
|
|
|
|
|
}
|
|
|
|
|
Ok(cache_path)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 22:00:30 +03:00
|
|
|
async fn etag_path(self) -> crate::Result<PathBuf> {
|
2025-09-26 17:08:57 +03:00
|
|
|
let mut db_path = self.db_path().await?;
|
2025-05-24 02:31:23 +03:00
|
|
|
db_path.set_extension("mmdb.etag");
|
|
|
|
|
Ok(db_path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn save_db(
|
2025-08-05 12:16:28 +03:00
|
|
|
self,
|
2025-05-24 02:31:23 +03:00
|
|
|
mut response: reqwest::Response,
|
|
|
|
|
#[cfg(feature = "tui")] tx: tokio::sync::mpsc::UnboundedSender<Event>,
|
2025-08-10 22:00:30 +03:00
|
|
|
) -> crate::Result<()> {
|
2025-05-24 02:31:23 +03:00
|
|
|
#[cfg(feature = "tui")]
|
2025-07-15 15:13:55 +03:00
|
|
|
drop(tx.send(Event::App(AppEvent::IpDbTotal(
|
2025-08-05 12:16:28 +03:00
|
|
|
self,
|
2025-05-24 02:31:23 +03:00
|
|
|
response.content_length(),
|
2025-07-15 15:13:55 +03:00
|
|
|
))));
|
2025-05-24 02:31:23 +03:00
|
|
|
|
|
|
|
|
let db_path = self.db_path().await?;
|
|
|
|
|
let mut file =
|
|
|
|
|
tokio::fs::File::create(&db_path).await.wrap_err_with(|| {
|
2025-09-30 07:53:39 +03:00
|
|
|
format!("failed to create file: {}", db_path.display())
|
2025-05-24 02:31:23 +03:00
|
|
|
})?;
|
2025-09-26 17:08:57 +03:00
|
|
|
while let Some(chunk) = response.chunk().await? {
|
2025-05-24 02:31:23 +03:00
|
|
|
file.write_all(&chunk).await.wrap_err_with(|| {
|
2025-09-30 07:53:39 +03:00
|
|
|
format!("failed to write to file: {}", db_path.display())
|
2025-05-24 02:31:23 +03:00
|
|
|
})?;
|
|
|
|
|
#[cfg(feature = "tui")]
|
2025-08-05 12:16:28 +03:00
|
|
|
drop(
|
|
|
|
|
tx.send(Event::App(AppEvent::IpDbDownloaded(
|
|
|
|
|
self,
|
|
|
|
|
chunk.len(),
|
|
|
|
|
))),
|
|
|
|
|
);
|
2025-05-24 02:31:23 +03:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 22:00:30 +03:00
|
|
|
async fn save_etag(self, etag: impl AsRef<[u8]>) -> crate::Result<()> {
|
2025-05-24 13:51:22 +03:00
|
|
|
let path = self.etag_path().await?;
|
|
|
|
|
tokio::fs::write(&path, etag).await.wrap_err_with(move || {
|
2025-09-30 07:53:39 +03:00
|
|
|
format!("failed to write to file: {}", path.display())
|
2025-05-24 13:51:22 +03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn read_etag(
|
2025-08-05 12:16:28 +03:00
|
|
|
self,
|
2025-08-10 22:00:30 +03:00
|
|
|
) -> crate::Result<Option<reqwest::header::HeaderValue>> {
|
2025-05-24 13:51:22 +03:00
|
|
|
let path = self.etag_path().await?;
|
|
|
|
|
match tokio::fs::read_to_string(&path).await {
|
|
|
|
|
Ok(text) => Ok(text.parse().ok()),
|
|
|
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
|
|
|
|
Err(e) => Err(e).wrap_err_with(move || {
|
2025-09-30 07:53:39 +03:00
|
|
|
format!("failed to read file to string: {}", path.display())
|
2025-05-24 13:51:22 +03:00
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 22:00:30 +03:00
|
|
|
async fn remove_etag(self) -> crate::Result<()> {
|
2025-05-24 13:51:22 +03:00
|
|
|
let path = self.etag_path().await?;
|
|
|
|
|
match tokio::fs::remove_file(&path).await {
|
|
|
|
|
Ok(()) => Ok(()),
|
|
|
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
|
|
|
|
Err(e) => Err(e).wrap_err_with(move || {
|
2025-09-30 07:53:39 +03:00
|
|
|
format!("failed to remove file: {}", path.display())
|
2025-05-24 13:51:22 +03:00
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn download(
|
2025-05-24 02:31:23 +03:00
|
|
|
self,
|
2025-08-30 23:02:18 +03:00
|
|
|
http_client: reqwest_middleware::ClientWithMiddleware,
|
2025-05-24 02:31:23 +03:00
|
|
|
#[cfg(feature = "tui")] tx: tokio::sync::mpsc::UnboundedSender<Event>,
|
2025-08-10 22:00:30 +03:00
|
|
|
) -> crate::Result<()> {
|
2025-05-24 02:31:23 +03:00
|
|
|
let db_path = self.db_path().await?;
|
|
|
|
|
let mut headers = reqwest::header::HeaderMap::new();
|
2025-06-24 12:34:44 +03:00
|
|
|
#[expect(clippy::collapsible_if)]
|
2025-05-24 02:31:23 +03:00
|
|
|
if tokio::fs::metadata(&db_path).await.is_ok_and(|m| m.is_file()) {
|
2025-09-26 17:08:57 +03:00
|
|
|
if let Some(etag) = self.read_etag().await? {
|
2025-05-24 02:31:23 +03:00
|
|
|
headers.insert(reqwest::header::IF_NONE_MATCH, etag);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let response = http_client
|
|
|
|
|
.get(self.url())
|
|
|
|
|
.headers(headers)
|
|
|
|
|
.send()
|
2025-09-26 17:08:57 +03:00
|
|
|
.await?
|
|
|
|
|
.error_for_status()?;
|
2025-05-24 02:31:23 +03:00
|
|
|
|
|
|
|
|
if response.status() == reqwest::StatusCode::NOT_MODIFIED {
|
2025-05-30 21:58:07 +00:00
|
|
|
tracing::info!(
|
2025-05-24 02:31:23 +03:00
|
|
|
"Latest {} database is already cached at {}",
|
|
|
|
|
self.name(),
|
|
|
|
|
db_path.display()
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if response.status() != reqwest::StatusCode::OK {
|
|
|
|
|
return Err(eyre!(
|
|
|
|
|
"HTTP status error ({}) for url ({})",
|
|
|
|
|
response.status(),
|
|
|
|
|
response.url()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let etag = response.headers().get(reqwest::header::ETAG).cloned();
|
|
|
|
|
|
|
|
|
|
self.save_db(
|
|
|
|
|
response,
|
|
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
tx.clone(),
|
|
|
|
|
)
|
2025-09-26 17:08:57 +03:00
|
|
|
.await?;
|
2025-05-24 02:31:23 +03:00
|
|
|
|
|
|
|
|
if is_docker().await {
|
2025-05-30 21:58:07 +00:00
|
|
|
tracing::info!(
|
2025-05-24 02:31:23 +03:00
|
|
|
"Downloaded {} database to Docker volume ({} in container)",
|
|
|
|
|
self.name(),
|
|
|
|
|
db_path.display()
|
|
|
|
|
);
|
|
|
|
|
} else {
|
2025-05-30 21:58:07 +00:00
|
|
|
tracing::info!(
|
2025-05-24 02:31:23 +03:00
|
|
|
"Downloaded {} database to {}",
|
|
|
|
|
self.name(),
|
|
|
|
|
db_path.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-15 17:19:50 +03:00
|
|
|
drop(db_path);
|
2025-05-24 02:31:23 +03:00
|
|
|
|
|
|
|
|
if let Some(etag_value) = etag {
|
2025-09-26 17:08:57 +03:00
|
|
|
self.save_etag(etag_value).await
|
2025-05-24 02:31:23 +03:00
|
|
|
} else {
|
2025-09-26 17:08:57 +03:00
|
|
|
self.remove_etag().await
|
2025-05-24 02:31:23 +03:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-24 13:51:22 +03:00
|
|
|
|
|
|
|
|
pub async fn open_mmap(
|
|
|
|
|
self,
|
2025-08-10 22:00:30 +03:00
|
|
|
) -> crate::Result<maxminddb::Reader<maxminddb::Mmap>> {
|
2025-05-24 13:51:22 +03:00
|
|
|
let path = self.db_path().await?;
|
|
|
|
|
tokio::task::spawn_blocking(move || maxminddb::Reader::open_mmap(path))
|
2025-09-26 17:08:57 +03:00
|
|
|
.await?
|
2025-05-24 13:51:22 +03:00
|
|
|
.wrap_err_with(move || {
|
2025-09-30 07:53:39 +03:00
|
|
|
format!("failed to open IP database: {}", self.name())
|
2025-05-24 13:51:22 +03:00
|
|
|
})
|
|
|
|
|
}
|
2025-05-24 02:31:23 +03:00
|
|
|
}
|