Publish shitty code
This commit is contained in:
commit
2e36ee2758
24 changed files with 2845 additions and 0 deletions
120
src/app/api/links.rs
Normal file
120
src/app/api/links.rs
Normal 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
8
src/app/api/mod.rs
Normal 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
36
src/app/mod.rs
Normal 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
69
src/app/root.rs
Normal 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)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue