This commit is contained in:
maurice committed 2026-11-14 18:20:29 +01:00
1 parent 35e7259775
commit b444b5759d
17 files changed
+1043 -31

No files matched your search

+5 -3
View File
@@ -11,7 +11,7 @@ mod db;
mod paste;
mod routes;
use crate::routes::{
delete_note, get_note, get_redirect, home_page, list_notes, post_note, post_once,
delete_note, get_note, get_redirect, gui_page, home_page, list_notes, post_note, post_once,
post_redirect, put_note, put_redirect,
};
@@ -24,6 +24,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new()
.route("/", get(home_page).post(post_note))
.route("/gui", get(gui_page))
.route("/once", post(post_once))
.route("/once/{id}", post(post_once))
.route("/to", post(post_redirect))
@@ -46,8 +47,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
))
.with_state(pool);
let listener = TcpListener::bind("0.0.0.0:3000").await?;
println!("Listening on http://localhost:3000");
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_owned());
let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?;
println!("Listening on http://localhost:{port}");
serve(listener, app).await?;
Ok(())
}
+2 -2
View File
@@ -1,6 +1,6 @@
use sqlx::SqlitePool;
#[derive(sqlx::FromRow)]
#[derive(serde::Serialize, sqlx::FromRow)]
pub struct Paste {
pub id: String,
#[sqlx(rename = "type")]
@@ -15,7 +15,7 @@ pub struct Paste {
pub owner: Option<String>,
}
#[derive(sqlx::Type)]
#[derive(serde::Serialize, sqlx::Type)]
#[repr(i64)]
pub enum PasteType {
TextV1 = 0,
+2
View File
@@ -4,6 +4,7 @@ mod create;
mod delete_note;
mod get_note;
mod get_redirect;
mod gui;
mod home;
mod input;
mod list_notes;
@@ -18,6 +19,7 @@ mod update;
pub use delete_note::delete_note;
pub use get_note::get_note;
pub use get_redirect::get_redirect;
pub use gui::gui_page;
pub use home::home_page;
pub use list_notes::list_notes;
pub use post_note::post_note;
+22 -5
View File
@@ -1,4 +1,5 @@
use axum::{Extension, extract::Path, http::StatusCode};
use irosashi::Highlighter;
use sqlx::SqlitePool;
use crate::{auth::User, paste::PasteType};
@@ -9,21 +10,30 @@ pub async fn create(
db: &SqlitePool,
id: Option<Path<String>>,
user: Option<Extension<User>>,
input: PasteInput,
mut input: PasteInput,
paste_type: PasteType,
) -> Result<String, StatusCode> {
let mut id = id.map(|Path(id)| id).filter(|id| {
!matches!(id.as_str(), "list" | "once" | "to" | "static") && !id.contains('/')
!matches!(id.as_str(), "gui" | "list" | "once" | "to" | "static") && !id.contains('/')
});
if matches!(paste_type, PasteType::TextV1) && input.language.is_none() {
let highlighter = Highlighter::new().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
input.language = highlighter
.detect_language_by_first_line(input.content.lines().next().unwrap_or_default());
}
let owner = user.map(|Extension(user)| user.username);
let expiration = input.expiration()?;
loop {
let candidate = id.take().unwrap_or_else(generate_id);
let created = sqlx::query_scalar(
"INSERT INTO notes (id, type, content, language, expiration, max_views, created_at, owner)
VALUES (COALESCE(?, lower(hex(randomblob(12)))), ?, ?, ?, ?, ?, unixepoch(), ?)
VALUES (?, ?, ?, ?, ?, ?, unixepoch(), ?)
ON CONFLICT(id) DO NOTHING RETURNING id",
)
.bind(id.as_deref())
.bind(candidate)
.bind(&paste_type)
.bind(&input.content)
.bind(&input.language)
@@ -33,9 +43,16 @@ pub async fn create(
.fetch_optional(db)
.await
.map_err(db_error)?;
if let Some(id) = created {
return Ok(id);
}
id = None;
}
}
fn generate_id() -> String {
const ALPHABET: &[u8] = b"23456789ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz";
(0..8)
.map(|_| ALPHABET[rand::random_range(0..ALPHABET.len())] as char)
.collect()
}
+13 -5
View File
@@ -2,7 +2,7 @@ use askama::Template;
use askama_web::WebTemplate;
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
http::{HeaderMap, HeaderValue, StatusCode},
response::{IntoResponse, Response},
};
use irosashi::{CodeToHtmlOptions, Highlighter};
@@ -15,6 +15,7 @@ use super::{db_error, return_html};
#[derive(Template, WebTemplate)]
#[template(path = "note.html")]
pub struct NotePage {
paste: Paste,
code: String,
}
@@ -28,7 +29,13 @@ pub async fn get_note(
.map_err(db_error)?
.ok_or(StatusCode::NOT_FOUND)?;
if return_html(&headers) {
let mut metadata = HeaderMap::new();
metadata.insert("x-paste-views", HeaderValue::from(paste.views));
if let Some(max) = paste.max_views {
metadata.insert("x-paste-max-views", HeaderValue::from(max));
}
let response = if return_html(&headers) {
let highlighter = Highlighter::new().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let code = highlighter
.code_to_html(
@@ -39,8 +46,9 @@ pub async fn get_note(
),
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(NotePage { code }.into_response())
NotePage { paste, code }.into_response()
} else {
Ok(paste.content.into_response())
}
paste.content.into_response()
};
Ok((metadata, response).into_response())
}
+17
View File
@@ -0,0 +1,17 @@
use askama::Template;
use askama_web::WebTemplate;
use axum::http::StatusCode;
use irosashi::Highlighter;
#[derive(Template, WebTemplate)]
#[template(path = "gui.html")]
pub struct GuiPage {
languages: Vec<String>,
}
pub async fn gui_page() -> Result<GuiPage, StatusCode> {
let highlighter = Highlighter::new().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(GuiPage {
languages: highlighter.languages(),
})
}
+42 -2
View File
@@ -1,10 +1,50 @@
use std::sync::LazyLock;
use askama::Template;
use askama_web::WebTemplate;
use pulldown_cmark::{Event, Options, Parser, Tag, html};
static README_HTML: LazyLock<String> = LazyLock::new(|| {
let events: Vec<_> = Parser::new_ext(
include_str!("../../README.md"),
Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS,
)
.collect();
let anchored = events.iter().enumerate().map(|(index, event)| {
if let Event::Start(Tag::Heading(level, None, _)) = event {
let title: String = events[index + 1..]
.iter()
.take_while(|event| !matches!(event, Event::End(Tag::Heading(..))))
.filter_map(|event| match event {
Event::Text(text) | Event::Code(text) => Some(text.as_ref()),
_ => None,
})
.collect();
let slug: String = title
.to_lowercase()
.chars()
.filter(|character| {
character.is_alphanumeric() || *character == '-' || character.is_whitespace()
})
.collect();
let slug = slug.split_whitespace().collect::<Vec<_>>().join("-");
return Event::Html(format!("<{level} id=\"{slug}\">").into());
}
event.clone()
});
let mut rendered = String::new();
html::push_html(&mut rendered, anchored);
rendered
});
#[derive(Template, WebTemplate)]
#[template(path = "home.html")]
pub struct HomePage;
pub struct HomePage {
readme: &'static str,
}
pub async fn home_page() -> HomePage {
HomePage
HomePage {
readme: &README_HTML,
}
}
+9 -3
View File
@@ -1,9 +1,9 @@
use askama::Template;
use askama_web::WebTemplate;
use axum::{
Extension,
Extension, Json,
extract::State,
http::{HeaderMap, StatusCode},
http::{HeaderMap, StatusCode, header::ACCEPT},
response::{IntoResponse, Response},
};
use sqlx::SqlitePool;
@@ -38,7 +38,13 @@ pub async fn list_notes(
.await
.map_err(db_error)?;
if return_html(&headers) {
if headers
.get(ACCEPT)
.and_then(|value| value.to_str().ok())
.is_some_and(|accept| accept.contains("application/json"))
{
Ok(Json(notes).into_response())
} else if return_html(&headers) {
Ok(ListPage { notes }.into_response())
} else {
Ok(notes