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

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/.direnv
/config.toml

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM link WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "487d4f0b61f41ae9fedc59ebdb4755a2846509ccea2ea5a6b1cc81263fc17ccb"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE link SET visit_count = visit_count + 1 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "4d94b8d9c3af0a9cbddc706ee82869355500bfe6cb97f5e20e10ddfddd523136"
}

View file

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, slug, destination FROM link WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "slug",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "destination",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "54b60a99c8cff423cf3b202a0f4f70158a445b5f7d3b867b4068ab6a5b6557a0"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE link SET destination = $2 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "c586609a7040e4a8191a904d527051c1540041d4c6305f0d8162bbef0d7bff0d"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, destination FROM link WHERE slug = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "destination",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "e83004dd947b684af5ea9319fe136910e75d96a24800fd884c6cb3c1b6a03a89"
}

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO link (id, slug, destination) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ecbfe9b3c87beef7162b5c5fc7e2cc25d5420a4dc5b02257f78d4afea7f9517c"
}

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"sqlfluff.dialect": "postgres"
}

2147
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

26
Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "ncpn"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.6.20", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing"] }
eyre = "0.6.8"
figment = { version = "0.10.11", features = ["env", "toml"] }
http = "0.2.9"
serde = { version = "1.0.189", features = ["derive"] }
sqlx = { version = "0.7.3", features = ["runtime-tokio", "postgres", "uuid"] }
thiserror = "1.0.51"
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros"] }
tower-http = { version = "0.4.4", features = ["trace"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
ulid = { version = "1.1.0", features = ["uuid", "serde"] }
url = { version = "2.5.0", features = ["serde"] }
uuid = "1.7.0"
[profile.release]
strip = true
lto = true
codegen-units = 1
panic = "abort"

116
flake.lock Normal file
View file

@ -0,0 +1,116 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1706371002,
"narHash": "sha256-dwuorKimqSYgyu8Cw6ncKhyQjUDOyuXoxDTVmAXq88s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c002c6aa977ad22c60398daaa9be52f2203d0006",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1706634984,
"narHash": "sha256-xn7lGPE8gRGBe3Lt8ESoN/uUHm7IrbiV7siupwjHX1o=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "883b84c426107a8ec020e7124f263d7c35a5bb9f",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

41
flake.nix Normal file
View file

@ -0,0 +1,41 @@
{
description = "Non-creative project name";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
rust = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
extensions = [ "rust-src" ];
});
in
with pkgs;
{
devShells.default = mkShell rec {
buildInputs = [ rust ] ++ [
rust-analyzer
pkg-config
postgresql
sqlfluff
sqlx-cli
];
LD_LIBRARY_PATH = "${lib.makeLibraryPath buildInputs}";
# ssh -NL /home/honbra/.s.PGSQL.5432:/var/run/postgresql/.s.PGSQL.5432 <user>@<host>
# good luck setting up /home/honbra on your machine
DATABASE_URL = "postgresql:///ncpn?host=/home/honbra&user=honbra";
};
}
);
}

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS link;

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS link (
id UUID PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
destination TEXT NOT NULL,
visit_count INT NOT NULL DEFAULT 0
);

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

3
rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
wrap_comments = true

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)),
_ => {}
}
}

40
src/config.rs Normal file
View file

@ -0,0 +1,40 @@
use std::{net::SocketAddr, path::PathBuf};
use serde::Deserialize;
use url::Url;
#[derive(Clone, Debug, Deserialize)]
pub struct Config {
#[serde(default = "default_listen_addr")]
pub listen_addr: SocketAddr,
#[serde(default = "default_db_socket")]
pub db_socket: PathBuf,
#[serde(default = "default_db_username")]
pub db_username: String,
#[serde(default = "default_db_database")]
pub db_database: String,
#[serde(default = "default2_destination")]
pub default_destination: Url,
}
fn default_listen_addr() -> SocketAddr {
([0, 0, 0, 0], 3000).into()
}
fn default_db_socket() -> PathBuf {
"/var/run/postgresql/".into()
}
fn default_db_username() -> String {
"ncpn".into()
}
fn default_db_database() -> String {
default_db_username()
}
fn default2_destination() -> Url {
"https://goob.cc/r".parse().expect("hardcoded URL is valid")
}

43
src/error.rs Normal file
View file

@ -0,0 +1,43 @@
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use tracing::{error, field};
use ulid::Ulid;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("link already exists ({0})")]
LinkExists(Ulid),
#[error("link not found ({0})")]
LinkNotFound(Ulid),
#[error("database returned an impossible number of affected rows ({0})")]
ImpossibleAffectedRows(u64),
#[error("database error")]
Database(#[from] sqlx::Error),
#[error(transparent)]
Other(#[from] eyre::Report),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
error!(err = field::display(&self));
match self {
Self::LinkExists(_) => (StatusCode::BAD_REQUEST, "Link already exists").into_response(),
Self::LinkNotFound(_) => (StatusCode::NOT_FOUND, "Link not found").into_response(),
Self::ImpossibleAffectedRows(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Database returned an impossible number of affected rows",
)
.into_response(),
Self::Database(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"A database error has occured",
)
.into_response(),
Self::Other(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("An error has occured:\n{err:?}"),
)
.into_response(),
}
}
}

59
src/main.rs Normal file
View file

@ -0,0 +1,59 @@
mod app;
mod config;
mod error;
use eyre::Context;
use figment::{
providers::{Env, Format, Toml},
Figment,
};
use tokio::runtime::Runtime;
use tracing::{debug, field, Level};
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt};
use self::app::build_app;
use crate::config::Config;
fn main() -> eyre::Result<()> {
let filter = filter::Targets::new()
.with_target("honbra_api", Level::TRACE)
.with_target("tower_http::trace::on_response", Level::TRACE)
.with_target("tower_http::trace::on_request", Level::TRACE)
.with_default(Level::INFO);
let tracing_layer = tracing_subscriber::fmt::layer();
tracing_subscriber::registry()
.with(tracing_layer)
.with(filter)
.try_init()
.map_err(eyre::Error::msg)
.context("failed to initialize tracing subscriber")?;
let config: Config = Figment::new()
.merge(Toml::file("config.toml"))
.merge(Env::raw())
.extract()
.context("failed to parse config")?;
let rt = Runtime::new().context("failed to create tokio runtime")?;
rt.block_on(async move {
let listen_addr = config.listen_addr;
let router = build_app(config)
.await
.context("failed to build app")?
.into_make_service();
debug!(addr = field::display(&listen_addr), "binding");
axum::Server::try_bind(&listen_addr)
.context("unable to bind to server address")?
.serve(router)
.await
.context("server encountered a runtime error")?;
Ok(())
})
}