initial commit

This commit is contained in:
2026-03-25 18:01:07 -05:00
commit 5910840ef1
8 changed files with 2295 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
/target
.idea/
links.db
# temporary sqlite files
links.db-shm
links.db-wal
Generated
+2171
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "rs-links"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.8"
rand = "0.10.0"
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1.50.0", features = ["rt-multi-thread", "signal"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
+18
View File
@@ -0,0 +1,18 @@
# rs-links
A basic link shortener.
## Building
Build like any other Rust project. However, you will need to have a database set up at compile time, otherwise compilation will fail when the `query!` macro cannot find a database.
You can set this up with the `sqlx` CLI:
```shell
cargo install sqlx-cli
export DATABASE_URL=sqlite:links.db
sqlx database create
sqlx migrate run
cargo run
```
The application binds to `0.0.0.0:24580`.
@@ -0,0 +1,5 @@
create table links (
code text primary key,
long_url text not null,
created_at text not null default (current_timestamp)
);
+55
View File
@@ -0,0 +1,55 @@
mod resolve;
mod state;
use crate::resolve::resolve;
use crate::state::AppState;
use axum::Router;
use axum::routing::get;
use sqlx::SqlitePool;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let pool = SqlitePool::connect("sqlite:links.db?mode=rwc")
.await
.expect("failed to connect to db");
tracing::info!("connected to db!");
tracing::info!("running migrations...");
sqlx::migrate!()
.run(&pool)
.await
.expect("failed to run migrations");
// clone the pool so we can shut it down later
let pool_clone = pool.clone();
// shared app state
let state = AppState { db: pool };
// channel so we can shut things down cleanly
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
tracing::info!("shutting down...");
let _ = shutdown_tx.send(());
});
let app = Router::new()
.route("/{code}", get(resolve))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:24580")
.await
.expect("failed to bind");
tracing::info!("app is listening!");
// this falls through once the server shuts down
axum::serve(listener, app)
.with_graceful_shutdown(async {
shutdown_rx.await.ok();
})
.await
.expect("failed to serve");
// close the database
pool_clone.close().await;
tracing::info!("finished");
}
+22
View File
@@ -0,0 +1,22 @@
use crate::state::AppState;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::Redirect;
pub async fn resolve(
Path(code): Path<String>,
State(state): State<AppState>,
) -> Result<Redirect, StatusCode> {
let link = match sqlx::query!("select long_url from links where code = ?", code)
.fetch_one(&state.db)
.await
{
Ok(row) => row,
Err(sqlx::Error::RowNotFound) => return Err(StatusCode::NOT_FOUND),
Err(e) => {
tracing::error!("db error with code {}: {}", code, e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
Ok(Redirect::temporary(&link.long_url))
}
+6
View File
@@ -0,0 +1,6 @@
use sqlx::SqlitePool;
#[derive(Clone)]
pub struct AppState {
pub db: SqlitePool,
}