Add QR codes, link expiry, password protection, previews, audit trail and bulk operations

This commit is contained in:
thakares committed 2026-06-12 19:57:53 +05:30
1 parent 157aa81252
commit bcbcc90d98
88 files changed
+5354 -815

No files matched your search

+9 -5
View File
@@ -1,20 +1,24 @@
use std::path::PathBuf;
use tracing::info;
use crate::config::Config;
use crate::db::Db;
use crate::jobs::backup::perform_backup;
use std::path::PathBuf;
use tracing::info;
pub async fn run(
out: Option<String>,
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(o) = out { config.backup_dir = PathBuf::from(o); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
if let Some(o) = out {
config.backup_dir = PathBuf::from(o);
}
// Init DB connections to ensure databases exist and migrate if needed
let db = Db::init(&config)?;
info!("Starting database backup...");
let backup_path = perform_backup(&db, &config).await?;
info!("Database backup generated successfully: {}", backup_path);
+11 -6
View File
@@ -1,16 +1,18 @@
use std::path::PathBuf;
use std::io::{self, Write};
use tracing::{info, error};
use crate::auth::hash_password;
use crate::config::Config;
use crate::db::Db;
use crate::auth::hash_password;
use std::io::{self, Write};
use std::path::PathBuf;
use tracing::{error, info};
pub async fn run(
username: Option<String>,
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
let db = Db::init(&config)?;
let final_username = match username {
@@ -32,7 +34,10 @@ pub async fn run(
let hash = hash_password(&password).map_err(|e| e.to_string())?;
let conn = db.admin.lock().unwrap();
let u = crate::db::admin::create_user(&conn, &final_username, &hash)?;
info!("Successfully created admin user: {} (ID: {})", u.username, u.id);
info!(
"Successfully created admin user: {} (ID: {})",
u.username, u.id
);
Ok(())
}
+30 -20
View File
@@ -1,8 +1,8 @@
use std::path::PathBuf;
use tracing::info;
use crate::config::Config;
use crate::db::sqlite;
use rusqlite::Connection;
use std::path::PathBuf;
use tracing::info;
/// Run comprehensive database diagnostics.
///
@@ -12,7 +12,9 @@ pub async fn run(
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
info!("Running BZOD database diagnostics...");
println!("BZOD Database Doctor");
@@ -35,27 +37,35 @@ pub async fn run(
}
match Connection::open(&db_path) {
Ok(conn) => {
match sqlite::collect_health_report(&conn, db_name) {
Ok(report) => {
println!("Database: {}", report.database);
println!(" Path: {:?}", db_path);
println!(" Schema version: {}", report.schema_version);
println!(" Journal mode: {}", report.journal_mode);
println!(" Foreign keys: {}", if report.foreign_keys_enabled { "enabled" } else { "DISABLED" });
println!(" Integrity: {}", if report.integrity_ok { "ok" } else { "FAILED" });
if !report.integrity_ok || !report.foreign_keys_enabled {
all_healthy = false;
Ok(conn) => match sqlite::collect_health_report(&conn, db_name) {
Ok(report) => {
println!("Database: {}", report.database);
println!(" Path: {:?}", db_path);
println!(" Schema version: {}", report.schema_version);
println!(" Journal mode: {}", report.journal_mode);
println!(
" Foreign keys: {}",
if report.foreign_keys_enabled {
"enabled"
} else {
"DISABLED"
}
}
Err(e) => {
println!("Database: {}", db_name);
println!(" Status: ERROR collecting health report: {}", e);
);
println!(
" Integrity: {}",
if report.integrity_ok { "ok" } else { "FAILED" }
);
if !report.integrity_ok || !report.foreign_keys_enabled {
all_healthy = false;
}
}
}
Err(e) => {
println!("Database: {}", db_name);
println!(" Status: ERROR collecting health report: {}", e);
all_healthy = false;
}
},
Err(e) => {
println!("Database: {}", db_name);
println!(" Status: FAILED to open: {}", e);
+5 -3
View File
@@ -1,14 +1,16 @@
use std::path::PathBuf;
use tracing::info;
use crate::config::Config;
use crate::db::Db;
use std::path::PathBuf;
use tracing::info;
pub async fn run(
data_dir: Option<String>,
dry_run: bool,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
if dry_run {
info!("Dry run enabled: pending database migrations will be reported but not applied.");
+5 -5
View File
@@ -1,13 +1,13 @@
use clap::{Parser, Subcommand};
pub mod serve;
pub mod backup;
pub mod restore;
pub mod migrate;
pub mod stats;
pub mod validate;
pub mod create_admin;
pub mod doctor;
pub mod migrate;
pub mod restore;
pub mod serve;
pub mod stats;
pub mod validate;
#[derive(Parser)]
#[command(name = "bzod")]
+12 -7
View File
@@ -1,17 +1,19 @@
use std::path::PathBuf;
use crate::config::Config;
use flate2::read::GzDecoder;
use std::fs::File;
use std::io::{self, Write};
use tracing::{info, error};
use flate2::read::GzDecoder;
use std::path::PathBuf;
use tar::Archive;
use crate::config::Config;
use tracing::{error, info};
pub async fn run(
file: String,
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
let file_path = PathBuf::from(file);
if !file_path.exists() {
@@ -19,12 +21,15 @@ pub async fn run(
return Ok(());
}
info!("WARNING: Restoring will overwrite existing databases in {:?}", config.data_dir);
info!(
"WARNING: Restoring will overwrite existing databases in {:?}",
config.data_dir
);
print!("Are you sure you want to restore? (y/N): ");
let _ = io::stdout().flush();
let mut confirm = String::new();
let _ = io::stdin().read_line(&mut confirm);
if !confirm.trim().eq_ignore_ascii_case("y") {
info!("Restore cancelled.");
return Ok(());
+21 -10
View File
@@ -1,11 +1,11 @@
use crate::analytics::AnalyticsQueue;
use crate::config::Config;
use crate::db::Db;
use crate::state::AppState;
use crate::web::create_router;
use std::path::PathBuf;
use std::time::Instant;
use tracing::info;
use crate::config::Config;
use crate::db::Db;
use crate::analytics::AnalyticsQueue;
use crate::state::AppState;
use crate::web::create_router;
pub async fn run(
host: Option<String>,
@@ -13,16 +13,22 @@ pub async fn run(
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(h) = host { config.host = h; }
if let Some(p) = port { config.port = p; }
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(h) = host {
config.host = h;
}
if let Some(p) = port {
config.port = p;
}
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
info!("Starting BZOD server on {}:{}", config.host, config.port);
info!("Database directory: {:?}", config.data_dir);
// Init DBs
let db = Db::init(&config)?;
// Init Queue
let queue = AnalyticsQueue::new(db.clone(), 1000);
@@ -52,6 +58,11 @@ pub async fn run(
crate::jobs::backup::run_backup_scheduler(backup_db, backup_config).await;
});
let expiry_db = db.clone();
tokio::spawn(async move {
crate::jobs::run_expiry_checker(expiry_db).await;
});
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
@@ -67,7 +78,7 @@ pub async fn run(
let router = create_router(state);
let addr = format!("{}:{}", config.host, config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
info!("Listening for requests on http://{}", addr);
axum::serve(listener, router).await?;
+16 -6
View File
@@ -1,23 +1,30 @@
use std::path::PathBuf;
use crate::config::Config;
use crate::db::Db;
use std::path::PathBuf;
pub async fn run(
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
let db = Db::init(&config)?;
println!("=== BZOD Database Stats ===");
println!("Storage Directory: {:?}", config.data_dir);
let files = vec!["admin.db", "content.db", "analytics.db", "system.db"];
for f in files {
let p = config.data_dir.join(f);
if p.exists() {
let sz = std::fs::metadata(&p)?.len();
println!(" File: {} - Size: {} bytes ({:.2} MB)", f, sz, sz as f64 / 1_048_576.0);
println!(
" File: {} - Size: {} bytes ({:.2} MB)",
f,
sz,
sz as f64 / 1_048_576.0
);
}
}
@@ -31,7 +38,10 @@ pub async fn run(
let conn = db.content.lock().unwrap();
crate::db::content::get_url_counts(&conn)?
};
println!("Shortened URLs: {} total ({} active / {} dead)", urls_total, urls_active, urls_dead);
println!(
"Shortened URLs: {} total ({} active / {} dead)",
urls_total, urls_active, urls_dead
);
let pages_count = {
let conn = db.content.lock().unwrap();
+9 -7
View File
@@ -1,24 +1,26 @@
use std::path::PathBuf;
use tracing::info;
use reqwest::Client;
use std::time::Duration;
use crate::config::Config;
use crate::db::Db;
use reqwest::Client;
use std::path::PathBuf;
use std::time::Duration;
use tracing::info;
pub async fn run(
data_dir: Option<String>,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir { config.data_dir = PathBuf::from(d); }
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
let db = Db::init(&config)?;
info!("Running one-shot link validation...");
let client = Client::builder()
.timeout(Duration::from_secs(10))
.user_agent("bzod-cli-checker/0.1")
.build()?;
crate::jobs::perform_link_check(&db, &client).await?;
info!("Link validation complete.");