Implement get

This commit is contained in:
Shav Kinderlehrer 2024-04-05 22:41:10 -04:00
commit fd0a930245
8 changed files with 2312 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2034
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "url_shortener"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.7.5"
color-eyre = "0.6.3"
eyre = "0.6.12"
info_utils = "2.2.3"
serde = "1.0.197"
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "macros", "migrate", "tls-rustls"] }
tokio = { version = "1.37.0", features = ["full"] }
url = "2.5.0"

27
flake.lock Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1712163089,
"narHash": "sha256-Um+8kTIrC19vD4/lUCN9/cU9kcOsD1O1m+axJqQPyMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fd281bd6b7d3e32ddfa399853946f782553163b5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

30
flake.nix Normal file
View File

@ -0,0 +1,30 @@
{
description = "Molerat client implementation";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs = { self, nixpkgs }:
let
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default =
pkgs.mkShell {
buildInputs = with pkgs; [
rustc
rustfmt
cargo
rust-analyzer
libiconv
clippy
pkg-config
darwin.apple_sdk.frameworks.SystemConfiguration
];
shellHook = ''
exec zsh
'';
};
};
}

99
src/get.rs Normal file
View File

@ -0,0 +1,99 @@
use std::net::SocketAddr;
use axum::extract::{ConnectInfo, Path};
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Redirect};
use axum::Extension;
use info_utils::prelude::*;
use crate::ServerState;
use crate::UrlRow;
pub async fn get_index() -> Html<&'static str> {
Html("hello, world!")
}
pub async fn get_id(
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Extension(state): Extension<ServerState>,
Path(id): Path<String>,
) -> impl IntoResponse {
let mut show_request = false;
log!("Request for '{}' from {}", id.clone(), addr.ip());
let mut use_id = id;
if use_id.ends_with('+') {
show_request = true;
use_id.pop();
}
let item = sqlx::query_as!(UrlRow, "SELECT * FROM chela.urls WHERE id = $1", use_id)
.fetch_one(&state.db_pool)
.await;
if let Ok(it) = item {
if url::Url::parse(&it.url).is_ok() {
if show_request {
return Html(format!(
"<pre>{}/{} -> <a href={}>{}</a></pre>",
state.host, it.id, it.url, it.url
))
.into_response();
} else {
log!("Redirecting {} -> {}", it.id, it.url);
save_analytics(headers, it.clone(), addr, state).await;
return Redirect::temporary(it.url.as_str()).into_response();
}
}
}
return (StatusCode::NOT_FOUND, Html("<pre>404</pre>")).into_response();
}
pub async fn save_analytics(
headers: HeaderMap,
item: UrlRow,
addr: SocketAddr,
state: ServerState,
) {
let id = item.id;
let ip = addr.ip().to_string();
let referer = match headers.get("referer") {
Some(it) => {
if let Ok(i) = it.to_str() {
Some(i)
} else {
None
}
}
None => None,
};
let user_agent = match headers.get("user-agent") {
Some(it) => {
if let Ok(i) = it.to_str() {
Some(i)
} else {
None
}
}
None => None,
};
let res = sqlx::query!(
"
INSERT INTO chela.tracking (id,ip,referrer,user_agent)
VALUES ($1,$2,$3,$4)
",
id,
ip,
referer,
user_agent
)
.execute(&state.db_pool)
.await;
if res.is_ok() {
log!("Saved analytics for '{id}' from {ip}");
}
}

102
src/main.rs Normal file
View File

@ -0,0 +1,102 @@
use std::net::SocketAddr;
use axum::routing::{get, post};
use axum::Router;
use sqlx::postgres::PgPoolOptions;
use sqlx::{Pool, Postgres};
use info_utils::prelude::*;
pub mod get;
pub mod post;
const LISTEN_ADDRESS: &'static str = "0.0.0.0:3000";
#[derive(Clone)]
pub struct ServerState {
pub db_pool: Pool<Postgres>,
pub host: String,
}
#[derive(Debug, Clone)]
pub struct UrlRow {
pub index: i32,
pub id: String,
pub url: String,
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
color_eyre::install()?;
let db_pool = init_db().await?;
let server_state = ServerState {
db_pool,
host: "trkt.in".to_string(),
};
let router = init_routes(server_state)?;
let listener = tokio::net::TcpListener::bind(LISTEN_ADDRESS).await?;
log!("Listening at {}", LISTEN_ADDRESS);
axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}
async fn init_db() -> eyre::Result<Pool<Postgres>> {
let db_pool = PgPoolOptions::new()
.max_connections(15)
.connect(std::env::var("DATABASE_URL")?.as_str())
.await?;
log!("Successfully connected to database");
sqlx::query!("CREATE SCHEMA IF NOT EXISTS chela")
.execute(&db_pool)
.await?;
log!("Created schema chela");
sqlx::query!(
"
CREATE TABLE IF NOT EXISTS chela.urls (
index SERIAL PRIMARY KEY,
id TEXT NOT NULL UNIQUE,
url TEXT NOT NULL
)
",
)
.execute(&db_pool)
.await?;
log!("Created table chela.urls");
sqlx::query!(
"
CREATE TABLE IF NOT EXISTS chela.tracking (
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
id TEXT NOT NULL,
ip TEXT NOT NULL,
referrer TEXT,
user_agent TEXT
)
",
)
.execute(&db_pool)
.await?;
log!("Created table chela.tracking");
Ok(db_pool)
}
fn init_routes(state: ServerState) -> eyre::Result<Router> {
let router = Router::new()
.route("/", get(get::get_index))
.route("/:id", get(get::get_id))
.route("/", post(post::create_link))
.layer(axum::Extension(state));
Ok(router)
}

3
src/post.rs Normal file
View File

@ -0,0 +1,3 @@
pub async fn create_link() {
}