Add QR codes, link expiry, password protection, previews, audit trail and bulk operations
This commit is contained in:
1 parent
157aa81252
commit
bcbcc90d98
88 files changed
+5354
-815
No files matched your search
@@ -1,15 +1,15 @@
|
||||
use std::time::Duration;
|
||||
use tracing::{info, error};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::db::Db;
|
||||
use super::{log_job_end, log_job_start};
|
||||
use crate::analytics::aggregate_day;
|
||||
use super::{log_job_start, log_job_end};
|
||||
use crate::db::Db;
|
||||
|
||||
pub async fn run_aggregator(db: Db, interval_mins: u64) {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(interval_mins * 60)).await;
|
||||
info!("Running background analytics aggregator...");
|
||||
|
||||
|
||||
let job_id = log_job_start(&db.system, "analytics_aggregator");
|
||||
match perform_aggregation(&db).await {
|
||||
Ok(_) => log_job_end(&db.system, &job_id, "success", None),
|
||||
|
||||
+17
-11
@@ -1,8 +1,8 @@
|
||||
use std::time::Duration;
|
||||
use tracing::{info, error};
|
||||
use crate::db::Db;
|
||||
use super::{log_job_end, log_job_start};
|
||||
use crate::config::Config;
|
||||
use super::{log_job_start, log_job_end};
|
||||
use crate::db::Db;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub async fn run_backup_scheduler(db: Db, config: Config) {
|
||||
if !config.backup_enabled {
|
||||
@@ -10,12 +10,15 @@ pub async fn run_backup_scheduler(db: Db, config: Config) {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Starting background backup scheduler (interval: {} mins)...", config.backup_interval_mins);
|
||||
info!(
|
||||
"Starting background backup scheduler (interval: {} mins)...",
|
||||
config.backup_interval_mins
|
||||
);
|
||||
loop {
|
||||
// Run backup every configured interval
|
||||
tokio::time::sleep(Duration::from_secs(config.backup_interval_mins * 60)).await;
|
||||
info!("Running background database backup...");
|
||||
|
||||
|
||||
let job_id = log_job_start(&db.system, "database_backup");
|
||||
match perform_backup(&db, &config).await {
|
||||
Ok(path) => {
|
||||
@@ -31,13 +34,16 @@ pub async fn run_backup_scheduler(db: Db, config: Config) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn perform_backup(db: &Db, config: &Config) -> Result<String, Box<dyn std::error::Error>> {
|
||||
use std::fs::File;
|
||||
pub async fn perform_backup(
|
||||
db: &Db,
|
||||
config: &Config,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
use chrono::Utc;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use tar::Builder;
|
||||
use chrono::Utc;
|
||||
use rusqlite::params;
|
||||
use std::fs::File;
|
||||
use tar::Builder;
|
||||
use uuid::Uuid;
|
||||
|
||||
let out_dir = config.backup_dir.clone();
|
||||
@@ -60,7 +66,7 @@ pub async fn perform_backup(db: &Db, config: &Config) -> Result<String, Box<dyn
|
||||
tar.append_path_with_name(&db_file, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tar.into_inner()?.finish()?;
|
||||
let size_bytes = std::fs::metadata(&tar_path)?.len();
|
||||
let path_str = tar_path.to_string_lossy().to_string();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::db::Db;
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
/// Background job that marks expired URLs.
|
||||
///
|
||||
/// Runs every 60 seconds. Any URL with `expires_at < NOW()` and `expired = 0`
|
||||
/// gets flipped to `expired = 1`.
|
||||
pub async fn run_expiry_checker(db: Db) {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
|
||||
let count = {
|
||||
let conn = db.content.lock().unwrap();
|
||||
crate::db::content::expire_urls(&conn).unwrap_or(0)
|
||||
};
|
||||
|
||||
if count > 0 {
|
||||
info!(expired_count = count, "Expired URLs marked");
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
-23
@@ -1,17 +1,18 @@
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, error};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
use reqwest::Client;
|
||||
use rusqlite::params;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{log_job_end, log_job_start};
|
||||
use crate::db::Db;
|
||||
use super::{log_job_start, log_job_end};
|
||||
|
||||
pub async fn run_link_checker(db: Db, interval_mins: u64) {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.user_agent("bzod-link-checker/0.1")
|
||||
.redirect(reqwest::redirect::Policy::limited(10))
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -19,7 +20,7 @@ pub async fn run_link_checker(db: Db, interval_mins: u64) {
|
||||
// Sleep first to give server time to start up
|
||||
tokio::time::sleep(Duration::from_secs(interval_mins * 60)).await;
|
||||
info!("Running background link health check...");
|
||||
|
||||
|
||||
let job_id = log_job_start(&db.system, "link_checker");
|
||||
match perform_link_check(&db, &client).await {
|
||||
Ok(_) => log_job_end(&db.system, &job_id, "success", None),
|
||||
@@ -32,19 +33,29 @@ pub async fn run_link_checker(db: Db, interval_mins: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn perform_link_check(db: &Db, client: &Client) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub async fn perform_link_check(
|
||||
db: &Db,
|
||||
client: &Client,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let urls = {
|
||||
let conn = db.content.lock().unwrap();
|
||||
crate::db::content::list_urls_for_health_check(&conn)?
|
||||
};
|
||||
|
||||
for (id, dest) in urls {
|
||||
let (status, status_code, err_msg) = check_url_health(client, &dest).await;
|
||||
let (status, detail_status, status_code, latency_ms, err_msg) =
|
||||
check_url_health(client, &dest).await;
|
||||
{
|
||||
let conn = db.content.lock().unwrap();
|
||||
crate::db::content::update_url_health(&conn, &id, &status)?;
|
||||
crate::db::content::update_url_health_extended(
|
||||
&conn,
|
||||
&id,
|
||||
&status,
|
||||
&detail_status,
|
||||
Some(latency_ms),
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
// Log to system.db.health_checks
|
||||
{
|
||||
let conn = db.system.lock().unwrap();
|
||||
@@ -64,31 +75,77 @@ pub async fn perform_link_check(db: &Db, client: &Client) -> Result<(), Box<dyn
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_url_health(client: &Client, url: &str) -> (String, Option<u16>, Option<String>) {
|
||||
let res = client.get(url)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.send()
|
||||
.await;
|
||||
/// Check URL health with detailed classification and latency measurement.
|
||||
///
|
||||
/// Returns: (general_status, detail_status, status_code, latency_ms, error_message)
|
||||
async fn check_url_health(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
) -> (String, String, Option<u16>, i64, Option<String>) {
|
||||
let start = Instant::now();
|
||||
let res = client.get(url).timeout(Duration::from_secs(5)).send().await;
|
||||
let latency_ms = start.elapsed().as_millis() as i64;
|
||||
|
||||
match res {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let code = status.as_u16();
|
||||
if status.is_success() || status.is_redirection() {
|
||||
("healthy".to_string(), Some(code), None)
|
||||
} else if status == reqwest::StatusCode::NOT_FOUND || status == reqwest::StatusCode::GONE {
|
||||
("dead".to_string(), Some(code), Some(format!("HTTP {}", code)))
|
||||
(
|
||||
"healthy".to_string(),
|
||||
"healthy".to_string(),
|
||||
Some(code),
|
||||
latency_ms,
|
||||
None,
|
||||
)
|
||||
} else if status == reqwest::StatusCode::NOT_FOUND
|
||||
|| status == reqwest::StatusCode::GONE
|
||||
{
|
||||
(
|
||||
"dead".to_string(),
|
||||
"dead".to_string(),
|
||||
Some(code),
|
||||
latency_ms,
|
||||
Some(format!("HTTP {}", code)),
|
||||
)
|
||||
} else {
|
||||
("suspect".to_string(), Some(code), Some(format!("HTTP {}", code)))
|
||||
(
|
||||
"suspect".to_string(),
|
||||
format!("http_{}", code),
|
||||
Some(code),
|
||||
latency_ms,
|
||||
Some(format!("HTTP {}", code)),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let err_str = err.to_string();
|
||||
if err.is_timeout() || err.is_connect() {
|
||||
("suspect".to_string(), None, Some(err_str))
|
||||
let detail = if err.is_timeout() {
|
||||
"timeout".to_string()
|
||||
} else if err.is_connect() {
|
||||
if err_str.contains("dns") || err_str.contains("resolve") {
|
||||
"dns_failure".to_string()
|
||||
} else if err_str.contains("tls")
|
||||
|| err_str.contains("ssl")
|
||||
|| err_str.contains("certificate")
|
||||
{
|
||||
"tls_error".to_string()
|
||||
} else {
|
||||
"connection_refused".to_string()
|
||||
}
|
||||
} else if err.is_redirect() {
|
||||
"redirect_loop".to_string()
|
||||
} else {
|
||||
("dead".to_string(), None, Some(err_str))
|
||||
}
|
||||
"unknown_error".to_string()
|
||||
};
|
||||
|
||||
let general = if err.is_timeout() || err.is_connect() {
|
||||
"suspect"
|
||||
} else {
|
||||
"dead"
|
||||
};
|
||||
|
||||
(general.to_string(), detail, None, latency_ms, Some(err_str))
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -1,16 +1,18 @@
|
||||
use std::sync::Mutex;
|
||||
use rusqlite::{Connection, params};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
use rusqlite::{params, Connection};
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod healthcheck;
|
||||
pub mod retention;
|
||||
pub mod aggregate;
|
||||
pub mod backup;
|
||||
pub mod expiry;
|
||||
pub mod healthcheck;
|
||||
pub mod retention;
|
||||
|
||||
pub use healthcheck::{run_link_checker, perform_link_check};
|
||||
pub use aggregate::{perform_aggregation, run_aggregator};
|
||||
pub use expiry::run_expiry_checker;
|
||||
pub use healthcheck::{perform_link_check, run_link_checker};
|
||||
pub use retention::run_retention_cleaner;
|
||||
pub use aggregate::{run_aggregator, perform_aggregation};
|
||||
|
||||
pub fn log_job_start(conn: &Mutex<Connection>, job_name: &str) -> String {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::time::Duration;
|
||||
use tracing::{info, error};
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::{log_job_end, log_job_start};
|
||||
use crate::db::Db;
|
||||
use super::{log_job_start, log_job_end};
|
||||
|
||||
pub async fn run_retention_cleaner(db: Db, retention_days_opt: Option<i64>) {
|
||||
let retention_days = match retention_days_opt {
|
||||
@@ -14,7 +14,7 @@ pub async fn run_retention_cleaner(db: Db, retention_days_opt: Option<i64>) {
|
||||
// Check once every 24 hours
|
||||
tokio::time::sleep(Duration::from_secs(24 * 3600)).await;
|
||||
info!("Running background data retention cleanup...");
|
||||
|
||||
|
||||
let job_id = log_job_start(&db.system, "retention_cleaner");
|
||||
let conn = db.analytics.lock().unwrap();
|
||||
match crate::db::analytics::retention_cleanup(&conn, retention_days) {
|
||||
|
||||
Reference in new issue
Block a user