A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
at master 4.3 kB view raw
1use anyhow::Result; 2use rand::Rng; 3use sqlx::migrate::MigrateDatabase; 4use sqlx::postgres::PgPoolOptions; 5use sqlx::{Postgres, Sqlite}; 6use std::fs::File; 7use std::io::Write; 8use tracing::info; 9 10use models::DatabasePool; 11 12pub mod auth; 13pub mod error; 14pub mod handlers; 15pub mod models; 16 17#[derive(Clone)] 18pub struct AppState { 19 pub db: DatabasePool, 20 pub admin_token: Option<String>, 21} 22 23pub async fn create_db_pool() -> Result<DatabasePool> { 24 let database_url = std::env::var("DATABASE_URL").ok(); 25 26 match database_url { 27 Some(url) if url.starts_with("postgres://") || url.starts_with("postgresql://") => { 28 info!("Using PostgreSQL database"); 29 let pool = PgPoolOptions::new() 30 .max_connections(5) 31 .acquire_timeout(std::time::Duration::from_secs(3)) 32 .connect(&url) 33 .await?; 34 35 Ok(DatabasePool::Postgres(pool)) 36 } 37 _ => { 38 info!("No PostgreSQL connection string found, using SQLite"); 39 40 // Get the project root directory 41 let project_root = std::env::current_dir()?; 42 let data_dir = project_root.join("data"); 43 44 // Create a data directory if it doesn't exist 45 if !data_dir.exists() { 46 std::fs::create_dir_all(&data_dir)?; 47 } 48 49 let db_path = data_dir.join("simplelink.db"); 50 let sqlite_url = format!("sqlite://{}", db_path.display()); 51 52 // Check if database exists and create it if it doesn't 53 if !Sqlite::database_exists(&sqlite_url).await.unwrap_or(false) { 54 info!("Creating new SQLite database at {}", db_path.display()); 55 Sqlite::create_database(&sqlite_url).await?; 56 info!("Database created successfully"); 57 } else { 58 info!("Database already exists"); 59 } 60 61 let pool = sqlx::sqlite::SqlitePoolOptions::new() 62 .max_connections(5) 63 .connect(&sqlite_url) 64 .await?; 65 66 Ok(DatabasePool::Sqlite(pool)) 67 } 68 } 69} 70 71pub async fn run_migrations(pool: &DatabasePool) -> Result<()> { 72 match pool { 73 DatabasePool::Postgres(pool) => { 74 // Use the root migrations directory for postgres 75 sqlx::migrate!().run(pool).await?; 76 } 77 DatabasePool::Sqlite(pool) => { 78 sqlx::migrate!("./migrations/sqlite").run(pool).await?; 79 } 80 } 81 Ok(()) 82} 83 84pub async fn check_and_generate_admin_token(db: &DatabasePool) -> anyhow::Result<Option<String>> { 85 // Check if any users exist 86 let user_count = match db { 87 DatabasePool::Postgres(pool) => { 88 let mut tx = pool.begin().await?; 89 let count = sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users") 90 .fetch_one(&mut *tx) 91 .await? 92 .0; 93 tx.commit().await?; 94 count 95 } 96 DatabasePool::Sqlite(pool) => { 97 let mut tx = pool.begin().await?; 98 let count = sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users") 99 .fetch_one(&mut *tx) 100 .await? 101 .0; 102 tx.commit().await?; 103 count 104 } 105 }; 106 107 if user_count == 0 { 108 let token: String = (0..32) 109 .map(|_| { 110 let idx = rand::thread_rng().gen_range(0..62); 111 match idx { 112 0..=9 => (b'0' + idx as u8) as char, 113 10..=35 => (b'a' + (idx - 10) as u8) as char, 114 _ => (b'A' + (idx - 36) as u8) as char, 115 } 116 }) 117 .collect(); 118 119 // Get the project root directory 120 let project_root = std::env::current_dir()?; 121 let token_path = project_root.join("admin-setup-token.txt"); 122 123 // Save token to file 124 let mut file = File::create(token_path)?; 125 writeln!(file, "{}", token)?; 126 127 info!("No users found - generated admin setup token"); 128 info!("Token has been saved to admin-setup-token.txt"); 129 info!("Use this token to create the admin user"); 130 info!("Admin setup token: {}", token); 131 132 Ok(Some(token)) 133 } else { 134 Ok(None) 135 } 136}