A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
1use actix_cors::Cors; 2use actix_web::{web, App, HttpResponse, HttpServer}; 3use anyhow::Result; 4use rust_embed::RustEmbed; 5use simplelink::check_and_generate_admin_token; 6use simplelink::{handlers, AppState}; 7use sqlx::postgres::PgPoolOptions; 8use tracing::info; 9 10#[derive(RustEmbed)] 11#[folder = "static/"] 12struct Asset; 13 14async fn serve_static_file(path: &str) -> HttpResponse { 15 match Asset::get(path) { 16 Some(content) => { 17 let mime = mime_guess::from_path(path).first_or_octet_stream(); 18 HttpResponse::Ok() 19 .content_type(mime.as_ref()) 20 .body(content.data.into_owned()) 21 } 22 None => HttpResponse::NotFound().body("404 Not Found"), 23 } 24} 25 26#[actix_web::main] 27async fn main() -> Result<()> { 28 // Load environment variables from .env file 29 dotenv::dotenv().ok(); 30 31 // Initialize logging 32 tracing_subscriber::fmt::init(); 33 34 // Database connection string from environment 35 let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); 36 37 // Create database connection pool 38 let pool = PgPoolOptions::new() 39 .max_connections(5) 40 .acquire_timeout(std::time::Duration::from_secs(3)) 41 .connect(&database_url) 42 .await?; 43 44 // Run database migrations 45 sqlx::migrate!("./migrations").run(&pool).await?; 46 47 let admin_token = check_and_generate_admin_token(&pool).await?; 48 49 let state = AppState { 50 db: pool, 51 admin_token, 52 }; 53 54 let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 55 let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); 56 info!("Starting server at http://{}:{}", host, port); 57 58 // Start HTTP server 59 HttpServer::new(move || { 60 let cors = Cors::default() 61 .allow_any_origin() 62 .allow_any_method() 63 .allow_any_header() 64 .max_age(3600); 65 66 App::new() 67 .wrap(cors) 68 .app_data(web::Data::new(state.clone())) 69 .service( 70 web::scope("/api") 71 .route("/shorten", web::post().to(handlers::create_short_url)) 72 .route("/links", web::get().to(handlers::get_all_links)) 73 .route("/links/{id}", web::delete().to(handlers::delete_link)) 74 .route( 75 "/links/{id}/clicks", 76 web::get().to(handlers::get_link_clicks), 77 ) 78 .route( 79 "/links/{id}/sources", 80 web::get().to(handlers::get_link_sources), 81 ) 82 .route("/auth/register", web::post().to(handlers::register)) 83 .route("/auth/login", web::post().to(handlers::login)) 84 .route("/health", web::get().to(handlers::health_check)), 85 ) 86 .service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url))) 87 .default_service(web::route().to(|req: actix_web::HttpRequest| async move { 88 let path = req.path().trim_start_matches('/'); 89 let path = if path.is_empty() { "index.html" } else { path }; 90 serve_static_file(path).await 91 })) 92 }) 93 .workers(2) 94 .backlog(10_000) 95 .bind(format!("{}:{}", host, port))? 96 .run() 97 .await?; 98 99 Ok(()) 100}