mirror of
https://mzte.de/git/LordMZTE/brevo
synced 2024-12-12 17:02:56 +01:00
init
This commit is contained in:
commit
6e3d6114ff
12 changed files with 438 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
34
Cargo.toml
Normal file
34
Cargo.toml
Normal file
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "brevo"
|
||||
version = "0.1.0"
|
||||
authors = ["LordMZTE <lord@mzte.de>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.41"
|
||||
clap = "2.33.3"
|
||||
env_logger = "0.8.4"
|
||||
log = "0.4.14"
|
||||
rand = "0.8.4"
|
||||
structopt = "0.3.21"
|
||||
tera = "1.11.0"
|
||||
toml = "0.5.8"
|
||||
warp = "0.3.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.126"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "0.5.5"
|
||||
features = ["mysql", "runtime-tokio-rustls"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.7.1"
|
||||
features = ["macros", "rt-multi-thread", "fs"]
|
||||
|
||||
[dependencies.url]
|
||||
version = "2.2.2"
|
||||
features = ["serde"]
|
50
assets/index.html.tera
Normal file
50
assets/index.html.tera
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>brevo</title>
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
background-color: #282a36;
|
||||
color: #f8f8f2;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#url_input {
|
||||
width: 80vw;
|
||||
height: 30px;
|
||||
background-color: #282a36;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
#submit_button {
|
||||
height: 30px;
|
||||
background-color: #50fa7b;
|
||||
color: #282a36;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content">
|
||||
<div>
|
||||
<h1>Shorten URL</h1>
|
||||
<form action="" method="POST" accept-charset="utf-8">
|
||||
<input type="text" name="url" id="url_input" placeholder="enter URL here...">
|
||||
<button type="submit" id="submit_button">Create!</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
29
assets/success.html.tera
Normal file
29
assets/success.html.tera
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>brevo</title>
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
background-color: #282a36;
|
||||
color: #f8f8f2;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #bd93f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Success! Your link is <a href="{{ link }}">{{ link }}</a>
|
||||
</body>
|
||||
|
||||
</html>
|
12
brevo.service
Normal file
12
brevo.service
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Systemd service for brevo
|
||||
|
||||
[Unit]
|
||||
Description=Link Shortener written in Rust
|
||||
|
||||
[Service]
|
||||
Environment=RUST_LOG=info
|
||||
ExecStart=/usr/bin/brevo
|
||||
ExecReload=/usr/bin/brevo
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
5
defaultconfig.toml
Normal file
5
defaultconfig.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
database_url = "mysql://brevo:brevo@127.0.0.1:3306/brevo"
|
||||
database = "brevo"
|
||||
bind_addr = "127.0.0.1:3001"
|
||||
link_len = 6
|
||||
base_url = "http://127.0.0.1:3001/"
|
12
rustfmt.toml
Normal file
12
rustfmt.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
unstable_features = true
|
||||
binop_separator = "Back"
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_matchers = true
|
||||
format_strings = true
|
||||
imports_layout = "HorizontalVertical"
|
||||
match_block_trailing_comma = true
|
||||
merge_imports = true
|
||||
normalize_comments = true
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
wrap_comments = true
|
13
src/config.rs
Normal file
13
src/config.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub database_url: String,
|
||||
pub bind_addr: SocketAddr,
|
||||
pub database: String,
|
||||
pub link_len: u8,
|
||||
pub base_url: Url,
|
||||
}
|
142
src/handlers.rs
Normal file
142
src/handlers.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::util::gen_id;
|
||||
use crate::util::make_link;
|
||||
use crate::util::render_reject;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
use tera::Context;
|
||||
use url::Url;
|
||||
use warp::http::uri::InvalidUri;
|
||||
use warp::redirect;
|
||||
use warp::{
|
||||
http::Uri,
|
||||
reject::{self, Reject},
|
||||
reply, Rejection, Reply,
|
||||
};
|
||||
|
||||
use crate::{sqlargs, util::sql_reject, Brevo};
|
||||
|
||||
pub async fn index(brevo: Arc<Brevo>) -> Result<Box<dyn Reply>, Rejection> {
|
||||
let rendered = brevo.tera.render("index.html", &Context::new());
|
||||
match rendered {
|
||||
Err(_) => Err(reject::custom(BrevoReject::RenderFail)),
|
||||
Ok(r) => Ok(Box::new(reply::html(r))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn shortened(id: String, brevo: Arc<Brevo>) -> Result<Box<dyn Reply>, Rejection> {
|
||||
make_table(Arc::clone(&brevo)).await?;
|
||||
let url = sqlx::query_with(
|
||||
"
|
||||
SELECT url FROM urls
|
||||
WHERE id = ?
|
||||
",
|
||||
sqlargs![&id],
|
||||
)
|
||||
.fetch_optional(&brevo.pool)
|
||||
.await
|
||||
.map_err(sql_reject)?;
|
||||
|
||||
match url {
|
||||
Some(u) => Ok(Box::new(redirect::permanent(
|
||||
u.try_get::<String, _>(0)
|
||||
.map_err(sql_reject)?
|
||||
.parse::<Uri>()
|
||||
.map_err(|e| reject::custom(BrevoReject::UriParseError(e)))?,
|
||||
))),
|
||||
None => Err(reject::not_found()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit(form_data: SubmitForm, brevo: Arc<Brevo>) -> Result<Box<dyn Reply>, Rejection> {
|
||||
make_table(Arc::clone(&brevo)).await?;
|
||||
let url = form_data.url.as_str();
|
||||
let id = sqlx::query_with(
|
||||
"
|
||||
SELECT id FROM urls
|
||||
WHERE url = ?
|
||||
",
|
||||
sqlargs![url],
|
||||
)
|
||||
.fetch_optional(&brevo.pool)
|
||||
.await
|
||||
.map_err(sql_reject)?;
|
||||
|
||||
if let Some(id) = id {
|
||||
let id = id.try_get::<String, _>(0).map_err(sql_reject)?;
|
||||
let rendered = brevo
|
||||
.tera
|
||||
.render(
|
||||
"success.html",
|
||||
&Context::from_serialize(SuccessContent {
|
||||
link: make_link(Arc::clone(&brevo), &id)?,
|
||||
})
|
||||
.map_err(render_reject)?,
|
||||
)
|
||||
.map_err(render_reject)?;
|
||||
|
||||
Ok(Box::new(reply::html(rendered)))
|
||||
} else {
|
||||
let id = gen_id(Arc::clone(&brevo)).await;
|
||||
sqlx::query_with(
|
||||
"
|
||||
INSERT INTO urls ( id, url ) VALUES
|
||||
( ?, ? )
|
||||
",
|
||||
sqlargs![&id, url],
|
||||
)
|
||||
.execute(&brevo.pool)
|
||||
.await
|
||||
.map_err(sql_reject)?;
|
||||
|
||||
let rendered = brevo
|
||||
.tera
|
||||
.render(
|
||||
"success.html",
|
||||
&Context::from_serialize(SuccessContent {
|
||||
link: make_link(Arc::clone(&brevo), &id)?,
|
||||
})
|
||||
.map_err(render_reject)?,
|
||||
)
|
||||
.map_err(render_reject)?;
|
||||
Ok(Box::new(reply::html(rendered)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn make_table(brevo: Arc<Brevo>) -> Result<(), Rejection> {
|
||||
// can't parameterize VARCHAR len, but this should be safe
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS urls (
|
||||
id VARCHAR({}) PRIMARY KEY,
|
||||
url TEXT NOT NULL UNIQUE
|
||||
)
|
||||
",
|
||||
brevo.config.link_len
|
||||
))
|
||||
.execute(&brevo.pool)
|
||||
.await
|
||||
.map_err(sql_reject)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SuccessContent {
|
||||
link: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SubmitForm {
|
||||
url: Url,
|
||||
}
|
||||
|
||||
// TODO: implement reject handerls
|
||||
#[derive(Debug)]
|
||||
pub enum BrevoReject {
|
||||
RenderFail,
|
||||
SqlError(sqlx::Error),
|
||||
UrlParseError(url::ParseError),
|
||||
UriParseError(InvalidUri),
|
||||
}
|
||||
|
||||
impl Reject for BrevoReject {}
|
79
src/main.rs
Normal file
79
src/main.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use tokio::sync::Mutex;
|
||||
use crate::config::Config;
|
||||
use anyhow::Context;
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use sqlx::MySqlPool;
|
||||
use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
use tera::Tera;
|
||||
use warp::Filter;
|
||||
|
||||
mod config;
|
||||
mod handlers;
|
||||
mod util;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
#[structopt(index = 1, help = "config file to use")]
|
||||
config: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let config = load_config(&opt.config)
|
||||
.await
|
||||
.context("error loading config")?;
|
||||
|
||||
let pool = MySqlPool::connect(&config.database_url)
|
||||
.await
|
||||
.context("error creating mysql pool")?;
|
||||
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![
|
||||
("index.html", include_str!("../assets/index.html.tera")),
|
||||
("success.html", include_str!("../assets/success.html.tera")),
|
||||
])
|
||||
.context("error adding templates to tera")?;
|
||||
|
||||
let brevo = Arc::new(Brevo {
|
||||
config,
|
||||
pool,
|
||||
tera,
|
||||
rng: Mutex::new(StdRng::from_rng(rand::thread_rng())?),
|
||||
});
|
||||
let brevo_idx = Arc::clone(&brevo);
|
||||
let brevo_submit = Arc::clone(&brevo);
|
||||
let brevo_srv = Arc::clone(&brevo);
|
||||
|
||||
let routes = warp::get()
|
||||
.and(
|
||||
warp::path::end()
|
||||
.and_then(move || handlers::index(Arc::clone(&brevo_idx)))
|
||||
.or(warp::path::param::<String>()
|
||||
.and(warp::path::end())
|
||||
.and_then(move |id| handlers::shortened(id, Arc::clone(&brevo)))),
|
||||
)
|
||||
.or(warp::post()
|
||||
.and(warp::body::content_length_limit(1024 * 16))
|
||||
.and(warp::body::form())
|
||||
.and_then(move |form_data| handlers::submit(form_data, Arc::clone(&brevo_submit))));
|
||||
|
||||
warp::serve(routes).run(brevo_srv.config.bind_addr).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_config(path: &str) -> anyhow::Result<Config> {
|
||||
let data = tokio::fs::read(path).await?;
|
||||
Ok(toml::from_slice::<Config>(&data)?)
|
||||
}
|
||||
|
||||
pub struct Brevo {
|
||||
pub config: Config,
|
||||
pub pool: MySqlPool,
|
||||
pub tera: Tera,
|
||||
pub rng: Mutex<StdRng>,
|
||||
}
|
44
src/util.rs
Normal file
44
src/util.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::handlers::BrevoReject;
|
||||
use crate::Brevo;
|
||||
use rand::seq::IteratorRandom;
|
||||
use warp::reject;
|
||||
use warp::Rejection;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! sqlargs {
|
||||
($($item:expr),+) => {{
|
||||
use sqlx::Arguments;
|
||||
let mut args = sqlx::mysql::MySqlArguments::default();
|
||||
$(
|
||||
args.add($item);
|
||||
)*
|
||||
args
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn sql_reject(e: sqlx::Error) -> Rejection {
|
||||
reject::custom(BrevoReject::SqlError(e))
|
||||
}
|
||||
|
||||
pub fn render_reject<T>(_: T) -> Rejection {
|
||||
reject::custom(BrevoReject::RenderFail)
|
||||
}
|
||||
|
||||
pub fn make_link(brevo: Arc<Brevo>, id: &str) -> Result<String, Rejection> {
|
||||
brevo
|
||||
.config
|
||||
.base_url
|
||||
.join(id)
|
||||
.map(String::from)
|
||||
.map_err(|e| reject::custom(BrevoReject::UrlParseError(e)))
|
||||
}
|
||||
|
||||
pub async fn gen_id(brevo: Arc<Brevo>) -> String {
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
.chars()
|
||||
.choose_multiple(&mut *brevo.rng.lock().await, brevo.config.link_len as usize)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
16
testenv/docker-compose.yml
Normal file
16
testenv/docker-compose.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
version: "3.1"
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.6.2
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
MARIADB_USER: brevo
|
||||
MARIADB_PASSWORD: brevo
|
||||
MARIADB_DATABASE: brevo
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
- 8080:8080
|
Loading…
Reference in a new issue