initial commit :)
This commit is contained in:
commit
b82670fdb2
16 changed files with 1983 additions and 0 deletions
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
@ -0,0 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/target
|
||||
|
||||
/misc
|
||||
|
||||
/data.db
|
||||
/data.db-*
|
1507
Cargo.lock
generated
Normal file
1507
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "char_lt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.133", features = ["derive"] }
|
||||
sqlx = { version = "0.5.10", features = ["sqlite", "runtime-tokio-native-tls"] }
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
warp = "0.3.2"
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# char.lt
|
||||
|
||||
- website (`static` folder)
|
||||
- filedump (`misc` folder - gitignored)
|
||||
- personal url shortener :)
|
||||
- random gen prefix with `_` + mixed-case 5-char alphabetical
|
||||
- custom links: anything goes (randoms take precedence though so maybe dont prefix `_`)
|
||||
|
||||
## setup
|
||||
|
||||
```shell
|
||||
$ touch data.db
|
||||
$ export DATABASE_URL="sqlite://data.db"
|
||||
$
|
||||
$ cd static/inter
|
||||
static/inter $ # download inter.zip from GitHub and extract the contents of 'Inter Web'
|
||||
static/inter $ cd ../../
|
||||
$
|
||||
$ cargo run --release
|
||||
[...]
|
||||
Listening at http://127.0.0.1:8000
|
||||
```
|
5
build.rs
Normal file
5
build.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
// generated by `sqlx migrate build-script`
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
7
migrations/20220114060531_create_redirects_table.sql
Normal file
7
migrations/20220114060531_create_redirects_table.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE redirects (
|
||||
path TEXT NOT NULL,
|
||||
target_location TEXT NOT NULL,
|
||||
redirect_code INT NOT NULL DEFAULT 302
|
||||
) STRICT;
|
||||
|
||||
CREATE UNIQUE INDEX redirects_idx ON redirects(path);
|
42
src/main.rs
Normal file
42
src/main.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use std::{net::SocketAddr, path::PathBuf};
|
||||
use warp::Filter;
|
||||
|
||||
mod redirects;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// TODO: Can we do some perfect hashing with the static dir?
|
||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| String::from("./static"));
|
||||
let static_files = warp::fs::dir(static_dir.clone());
|
||||
|
||||
let misc_files_dir = std::env::var("MISC_DIR").unwrap_or_else(|_| String::from("misc"));
|
||||
let misc_files = warp::fs::dir(misc_files_dir);
|
||||
|
||||
let redirect_routes = redirects::routes(&static_dir).await;
|
||||
|
||||
let not_found_route = {
|
||||
let not_found_page = {
|
||||
let mut path = PathBuf::from(&static_dir);
|
||||
path.push("404.html");
|
||||
path
|
||||
};
|
||||
|
||||
warp::fs::file(not_found_page)
|
||||
.map(|reply| warp::reply::with_status(reply, warp::hyper::StatusCode::NOT_FOUND))
|
||||
};
|
||||
|
||||
let routes = static_files
|
||||
.or(misc_files)
|
||||
.or(redirect_routes)
|
||||
.or(not_found_route);
|
||||
|
||||
let host = std::env::var("BIND_ADDR")
|
||||
.map(|addr| {
|
||||
addr.parse::<SocketAddr>()
|
||||
.unwrap_or_else(|_| panic!("{addr} is not a valid socket address"))
|
||||
})
|
||||
.unwrap_or_else(|_| ([127, 0, 0, 1], 8080).into());
|
||||
println!("Listening on http://{host} ...");
|
||||
|
||||
warp::serve(routes).bind(host).await;
|
||||
}
|
115
src/redirects.rs
Normal file
115
src/redirects.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use sqlx::{Executor, SqlitePool};
|
||||
use warp::{
|
||||
filters::BoxedFilter,
|
||||
hyper::{self},
|
||||
reply::Response,
|
||||
Filter, Rejection, Reply,
|
||||
};
|
||||
|
||||
// if we're making a debug build, we don't want to expand the migrate! macro at all
|
||||
|
||||
#[inline]
|
||||
#[cfg(debug_assertions)]
|
||||
fn migrate(_db: &SqlitePool) {}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(debug_assertions))]
|
||||
async fn migrate(db: &SqlitePool) {
|
||||
let _ = sqlx::migrate!().run(db).await;
|
||||
}
|
||||
|
||||
struct Redirect {
|
||||
path: String,
|
||||
target_location: String,
|
||||
redirect_code: i64,
|
||||
}
|
||||
|
||||
impl Redirect {
|
||||
fn status_code(&self) -> hyper::StatusCode {
|
||||
hyper::StatusCode::from_u16(self.redirect_code as u16)
|
||||
.expect("Status codes in database should always be valid")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AddRedirectForm {
|
||||
path: Option<String>,
|
||||
target: String,
|
||||
status_code: Option<u16>,
|
||||
}
|
||||
|
||||
async fn add_redirect(
|
||||
db: SqlitePool,
|
||||
add_redirect_form: AddRedirectForm,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
async fn create_redirect(db: SqlitePool, redir: Redirect) -> sqlx::Result<()> {
|
||||
let mut conn = db.acquire().await?;
|
||||
|
||||
let query = sqlx::query!(
|
||||
"INSERT INTO redirects VALUES (?, ?, ?)",
|
||||
redir.path,
|
||||
redir.target_location,
|
||||
redir.redirect_code
|
||||
);
|
||||
|
||||
let _ = conn.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok("hello")
|
||||
}
|
||||
|
||||
async fn lookup_redirect(path: String, db: SqlitePool) -> Result<impl Reply, Rejection> {
|
||||
async fn find_redirect(db: SqlitePool, path: &str) -> sqlx::Result<Redirect> {
|
||||
let mut conn = db.acquire().await?;
|
||||
let query = sqlx::query_as!(Redirect, "SELECT * FROM redirects WHERE path = ?", path);
|
||||
query.fetch_one(&mut conn).await
|
||||
}
|
||||
|
||||
match find_redirect(db, &path).await {
|
||||
Ok(redir) => Ok(warp::reply::with_header(
|
||||
redir.status_code(),
|
||||
hyper::header::LOCATION,
|
||||
redir.target_location,
|
||||
)),
|
||||
Err(_) => Err(warp::reject()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn routes(static_dir: &str) -> BoxedFilter<(Response,)> {
|
||||
let database_uri = std::env!("DATABASE_URL");
|
||||
|
||||
let db = SqlitePool::connect(database_uri)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Failed to open SQLite DB: {}", database_uri));
|
||||
migrate(&db);
|
||||
|
||||
let shorten_html = {
|
||||
let mut path = PathBuf::from(static_dir);
|
||||
path.push("shorten.html");
|
||||
path
|
||||
};
|
||||
|
||||
let shorten_route = warp::path!("shorten")
|
||||
.and(warp::get().and(warp::fs::file(shorten_html))) // TODO: Templating to list existing shortlinks?
|
||||
.or({
|
||||
let db = db.clone();
|
||||
warp::post()
|
||||
.map(move || db.clone())
|
||||
.and(warp::body::form())
|
||||
.and_then(add_redirect)
|
||||
});
|
||||
|
||||
let lookup_route = warp::path::param()
|
||||
.and(warp::path::end())
|
||||
.and(warp::any().map(move || db.clone()))
|
||||
.and_then(lookup_redirect);
|
||||
|
||||
shorten_route
|
||||
.or(lookup_route)
|
||||
.map(Reply::into_response)
|
||||
.boxed()
|
||||
}
|
25
static/404.html
Normal file
25
static/404.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>404 - char.lt</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/layout.css" />
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
|
||||
<link rel="stylesheet" href="/inter/inter.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<main>
|
||||
<h1>not found :(</h1>
|
||||
<p>
|
||||
sorry, this link doesn't work! why not learn
|
||||
<a href="/">about me</a> instead?
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
70
static/css/layout.css
Normal file
70
static/css/layout.css
Normal file
|
@ -0,0 +1,70 @@
|
|||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 60ch;
|
||||
width: 60ch;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
padding: 1em;
|
||||
border: 2px solid white;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
header aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
height: 10rem;
|
||||
width: 10rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
section + section {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
43
static/css/styles.css
Normal file
43
static/css/styles.css
Normal file
|
@ -0,0 +1,43 @@
|
|||
html {
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(255, 167, 248);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 1.125em;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.inline-list {
|
||||
list-style: none;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inline-list li {
|
||||
display: block;
|
||||
|
||||
padding-inline-end: 0.5ch;
|
||||
}
|
||||
|
||||
.inline-list li + li {
|
||||
border-inline-start: 1px solid white;
|
||||
padding-inline-start: 0.5ch;
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
}
|
114
static/index.html
Normal file
114
static/index.html
Normal file
|
@ -0,0 +1,114 @@
|
|||
<!--
|
||||
hey! you might be interested in the source code for this site
|
||||
(both front- + back-end) at https://git.lavender.software/charlotte/char.lt
|
||||
<3
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>charlotte ✨</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/layout.css" />
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
|
||||
<link rel="stylesheet" href="/inter/inter.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<header>
|
||||
<aside>
|
||||
<h1><abbr title="charlotte athena som">charlotte ✨</abbr></h1>
|
||||
|
||||
<ul id="traits" class="inline-list">
|
||||
<li id="age">21</li>
|
||||
<li>trans</li>
|
||||
<li>catgirl bunnygirl enby</li>
|
||||
<li>lesbian</li>
|
||||
<li>anarchist</li>
|
||||
<li>communist</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<img
|
||||
class="hero-image"
|
||||
alt="charlotte's profile picture"
|
||||
src="/pfp.jpg"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<p>welcome to <code>char.lt</code>: the domain, is, like, my name!!</p>
|
||||
|
||||
<p>
|
||||
hey, i'm charlotte! i like music and videogames and programming and
|
||||
being hot and i speak multiple languages
|
||||
</p>
|
||||
|
||||
<p>
|
||||
i like breaking the rules
|
||||
<small>(usually the ones that protect power)</small>, so i enjoy
|
||||
software piracy, videogame cheat development
|
||||
<small>(death to microtransactions!)</small>, other cursed computer
|
||||
things, and being obnoxiously queer everywhere i go <3
|
||||
</p>
|
||||
|
||||
<section>
|
||||
my pronouns are:
|
||||
|
||||
<ul id="pronouns" class="inline-list">
|
||||
<li><a href="https://pronoun.is/she">she/her</a></li>
|
||||
<li><a href="https://pronoun.is/they">they/them</a></li>
|
||||
<li><a href="https://pronoun.is/fae">fae/faer</a></li>
|
||||
<li>
|
||||
<a href="https://pronoun.is/bun/bun/buns/buns/bunself">
|
||||
bun/buns
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
talk to me in:
|
||||
|
||||
<ul id="languages" class="inline-list">
|
||||
<li>english</li>
|
||||
<li>french</li>
|
||||
<li>spanish</li>
|
||||
<li>korean</li>
|
||||
<li>toki pona</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ul id="links" class="inline-list">
|
||||
<li>
|
||||
<a rel="me" href="https://som.codes">som.codes</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://hackery.site">hackery.site</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://trans.enby.town/charlotte">
|
||||
trans.enby.town
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://github.com/videogame-hacker">github</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://twitter.com/half_kh_hacker">twitter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://last.fm/user/half-kh-hacker">last.fm</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>a site by <a href="https://som.codes">charlotte som</a></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
2
static/inter/.gitignore
vendored
Normal file
2
static/inter/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
BIN
static/pfp.jpg
Normal file
BIN
static/pfp.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 310 KiB |
3
static/shorten.html
Normal file
3
static/shorten.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<p>TODO! sorry :(</p>
|
Loading…
Reference in a new issue