mirror of
https://mzte.de/git/LordMZTE/brevo
synced 2024-12-12 16:32:57 +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