initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
/target
|
||||||
|
.idea/
|
||||||
|
links.db
|
||||||
|
# temporary sqlite files
|
||||||
|
links.db-shm
|
||||||
|
links.db-wal
|
||||||
Generated
+2171
File diff suppressed because it is too large
Load Diff
+12
@@ -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"
|
||||||
@@ -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
@@ -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");
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: SqlitePool,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user