mod cli; mod db; mod server; use std::{ fs, path::{Path, PathBuf}, sync::Arc, }; use clap::Parser; use tera::Tera; use tokio::sync::Mutex; use tower_http::compression::CompressionLayer; use cli::UserCmd; use db::DbError; const DB_FILENAME: &str = "db.sqlite3"; const IMG_DIR: &str = "imgs"; const TMP_DIR: &str = "tmp"; #[derive(Clone)] pub struct Context { pool: db::DbPool, tera: Arc, data_dir: PathBuf, tmp_dir_counter: Arc>, } impl Context { pub async fn tmp_file_path(&self) -> PathBuf { let mut guard = self.tmp_dir_counter.lock().await; let path = self.data_dir.join(TMP_DIR).join(guard.to_string()); *guard = guard.wrapping_add(1); path } } fn run_user_cli(data_dir: impl AsRef, cmd: UserCmd) -> Result<(), DbError> { let pool = db::initialize_db(data_dir.as_ref().join(DB_FILENAME), false)?; match cmd.cmd { cli::UserSubCmd::List => { let users = db::User::all(&pool)?; println!("id\tusername\tis admin"); for user in users { println!("{}\t{}\t{}", user.id, user.username, user.admin); } } cli::UserSubCmd::Add { username, password, admin, } => { db::NewUser::new(username, password, admin).insert(&pool)?; } cli::UserSubCmd::Remove { username } => { db::User::remove_by_username(&pool, &username)?; } } Ok(()) } #[tokio::main] async fn main() { let args = cli::Cli::parse(); match args.cmd { cli::Subcommands::Serve { domain, port } => { tracing_subscriber::fmt::init(); if !fs::exists(&args.data_dir).unwrap() { fs::create_dir_all(&args.data_dir).unwrap(); } if !fs::exists(args.data_dir.join(IMG_DIR)).unwrap() { fs::create_dir(args.data_dir.join(IMG_DIR)).unwrap(); } if !fs::exists(args.data_dir.join(TMP_DIR)).unwrap() { fs::create_dir(args.data_dir.join(TMP_DIR)).unwrap(); } let pool = db::initialize_db(args.data_dir.join(DB_FILENAME), true).unwrap(); let tera = Tera::new(&format!("{}/**/*", args.templates_dir.to_string_lossy())).unwrap(); let ctx = Context { pool, tera: Arc::new(tera), data_dir: args.data_dir.clone(), tmp_dir_counter: Arc::new(Mutex::new(0)), }; let app = server::app(ctx, &args.static_dir) .layer(CompressionLayer::new().br(true).gzip(true)); let address = format!("{}:{}", domain, port); tracing::info!("Starting server on {address}"); let listener = tokio::net::TcpListener::bind(address).await.unwrap(); axum::serve(listener, app.into_make_service()) .await .unwrap(); } cli::Subcommands::User(cmd) => { run_user_cli(&args.data_dir, cmd).unwrap(); } } }