1
0
Fork 0

Publish shitty code

This commit is contained in:
Honbra 2024-01-30 19:27:14 +01:00
commit 2e36ee2758
Signed by: honbra
GPG key ID: B61CC9ADABE2D952
24 changed files with 2845 additions and 0 deletions

120
src/app/api/links.rs Normal file
View file

@ -0,0 +1,120 @@
use axum::{
extract::{Path, State},
routing::{get, post},
Json, Router,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use sqlx::{query, PgPool};
use ulid::Ulid;
use url::Url;
use uuid::Uuid;
use crate::error::AppError;
pub fn router(db: PgPool) -> Router {
Router::new()
.route("/", post(create_link))
.route(
"/:id",
get(get_link_info).put(update_link).delete(delete_link),
)
.with_state(db)
}
#[derive(Serialize)]
struct Link {
id: Ulid,
slug: String,
destination: String,
}
async fn get_link_info(
State(db): State<PgPool>,
Path(id): Path<Ulid>,
) -> Result<Json<Link>, AppError> {
let link = query!(
"SELECT id, slug, destination FROM link WHERE id = $1",
Uuid::from(id),
)
.fetch_one(&db)
.await?;
Ok(Json(Link {
id: Ulid::from(link.id),
slug: link.slug,
destination: link.destination,
}))
}
#[derive(Deserialize)]
struct CreateLinkRequestBody {
slug: String,
destination: Url,
}
async fn create_link(
State(db): State<PgPool>,
Json(CreateLinkRequestBody { slug, destination }): Json<CreateLinkRequestBody>,
) -> Result<Json<Link>, AppError> {
let id = Ulid::new();
let result = query!(
"INSERT INTO link (id, slug, destination) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
Uuid::from(id),
slug,
destination.to_string(),
)
.execute(&db)
.await?;
match result.rows_affected() {
1 => Ok(Json(Link {
id,
slug,
destination: destination.to_string(),
})),
0 => Err(AppError::LinkExists(id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}
#[derive(Deserialize)]
struct UpdateLinkRequestBody {
destination: Url,
}
async fn update_link(
State(db): State<PgPool>,
Path(id): Path<Ulid>,
Json(UpdateLinkRequestBody { destination }): Json<UpdateLinkRequestBody>,
) -> Result<StatusCode, AppError> {
let result = query!(
"UPDATE link SET destination = $2 WHERE id = $1",
Uuid::from(id),
destination.to_string(),
)
.execute(&db)
.await?;
match result.rows_affected() {
1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::LinkNotFound(id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}
async fn delete_link(
State(db): State<PgPool>,
Path(id): Path<Ulid>,
) -> Result<StatusCode, AppError> {
let result = query!("DELETE FROM link WHERE id = $1", Uuid::from(id))
.execute(&db)
.await?;
match result.rows_affected() {
1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::LinkNotFound(id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}

8
src/app/api/mod.rs Normal file
View file

@ -0,0 +1,8 @@
mod links;
use axum::Router;
use sqlx::PgPool;
pub fn router(db: PgPool) -> Router {
Router::new().nest("/links", links::router(db))
}

36
src/app/mod.rs Normal file
View file

@ -0,0 +1,36 @@
mod api;
mod root;
use axum::{body::Body, Router};
use http::Request;
use sqlx::{postgres::PgConnectOptions, PgPool};
use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{field, span, Level};
use crate::config::Config;
pub async fn build_app(config: Config) -> eyre::Result<Router> {
let db = PgPool::connect_with(
PgConnectOptions::new()
.application_name("ncpn")
.socket(&config.db_socket)
.username(&config.db_username)
.database(&config.db_database),
)
.await?;
Ok(root::router(db.clone(), config.default_destination)
.nest("/api", api::router(db))
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<Body>| {
span!(
Level::INFO,
"http-request",
uri = field::display(request.uri()),
)
})
.on_request(DefaultOnRequest::new().level(Level::DEBUG))
.on_response(DefaultOnResponse::new().level(Level::INFO)),
))
}

69
src/app/root.rs Normal file
View file

@ -0,0 +1,69 @@
use std::sync::Arc;
use axum::{
extract::{Path, State},
response::Redirect,
routing::get,
Router,
};
use sqlx::{query, PgPool};
use tracing::{error, field, instrument};
use ulid::Ulid;
use url::Url;
use uuid::Uuid;
use crate::error::AppError;
#[derive(Clone)]
struct SharedState {
db: PgPool,
default_destination: Arc<Url>,
}
pub fn router(db: PgPool, default_destination: Url) -> Router {
Router::new()
.route("/:slug", get(redirect))
.with_state(SharedState {
db,
default_destination: Arc::new(default_destination),
})
}
async fn redirect(
State(SharedState {
db,
default_destination,
}): State<SharedState>,
Path(slug): Path<String>,
) -> Result<Redirect, AppError> {
let result = query!("SELECT id, destination FROM link WHERE slug = $1", slug)
.fetch_optional(&db)
.await?
.map(|r| (Ulid::from(r.id), r.destination));
Ok(match result {
Some((id, destination)) => {
tokio::spawn(increase_visit_count(id, db));
Redirect::temporary(&destination)
}
None => Redirect::temporary(default_destination.as_str()),
})
}
#[instrument(skip(db))]
async fn increase_visit_count(id: Ulid, db: PgPool) {
let result = query!(
"UPDATE link SET visit_count = visit_count + 1 WHERE id = $1",
Uuid::from(id),
)
.execute(&db)
.await;
match result {
Ok(result) if result.rows_affected() != 1 => {
error!(err = field::display(AppError::ImpossibleAffectedRows(result.rows_affected())));
}
Err(err) => error!(err = field::display(err)),
_ => {}
}
}