Rewrite pastebin.run in warp
This commit is contained in:
parent
e71ea2df5b
commit
cfd05073e4
1177
Cargo.lock
generated
1177
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
|
@ -6,16 +6,14 @@ edition = "2018"
|
|||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
actix-diesel = "0.3.0"
|
||||
actix-web = "0.7.10"
|
||||
ammonia = "2.0.0"
|
||||
askama = { version = "0.8.0", features = ["with-actix-web"] }
|
||||
askama = { version = "0.8.0" }
|
||||
chrono = "0.4.6"
|
||||
diesel = { version = "1.4.1", features = ["chrono", "postgres"] }
|
||||
env_logger = "0.6.0"
|
||||
futures = "0.1.25"
|
||||
diesel = { version = "1.4.1", features = ["chrono", "postgres", "r2d2"] }
|
||||
env_logger = { version = "0.6.0", default-features = false }
|
||||
lazy_static = "1.3.0"
|
||||
log = "0.4.6"
|
||||
pulldown-cmark = "0.4.0"
|
||||
rand = "0.6.5"
|
||||
serde = { version = "1.0.88", features = ["derive"] }
|
||||
warp = "0.1.15"
|
||||
|
|
300
src/main.rs
300
src/main.rs
|
@ -1,304 +1,22 @@
|
|||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
||||
mod models;
|
||||
mod routes;
|
||||
mod schema;
|
||||
|
||||
use actix_diesel::dsl::AsyncRunQueryDsl;
|
||||
use actix_diesel::{AsyncError, Database};
|
||||
use actix_web::error::InternalError;
|
||||
use actix_web::fs::{NamedFile, StaticFiles};
|
||||
use actix_web::http::header::{
|
||||
CACHE_CONTROL, CONTENT_SECURITY_POLICY, LOCATION, REFERRER_POLICY, X_FRAME_OPTIONS,
|
||||
X_XSS_PROTECTION,
|
||||
};
|
||||
use actix_web::http::{Method, StatusCode};
|
||||
use actix_web::middleware::{DefaultHeaders, Logger};
|
||||
use actix_web::{server, App, AsyncResponder, Form, HttpResponse, Path, State};
|
||||
use ammonia::Builder;
|
||||
use askama::actix_web::TemplateIntoResponse;
|
||||
use askama::Template;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use diesel::prelude::*;
|
||||
use futures::future;
|
||||
use futures::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
use log::info;
|
||||
use pulldown_cmark::{html, Options, Parser};
|
||||
use rand::prelude::*;
|
||||
use schema::{languages, pastes};
|
||||
use serde::de::IgnoredAny;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, io};
|
||||
use diesel::r2d2::{ConnectionManager, PooledConnection};
|
||||
use warp::Reply;
|
||||
|
||||
type AsyncResponse = Box<dyn Future<Item = HttpResponse, Error = actix_web::Error>>;
|
||||
type Connection = PooledConnection<ConnectionManager<PgConnection>>;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct Index {
|
||||
languages: Vec<Language>,
|
||||
pub fn render(template: impl Template) -> impl Reply {
|
||||
warp::reply::html(template.render().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Queryable)]
|
||||
struct Language {
|
||||
id: i32,
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn index(db: State<Database<PgConnection>>) -> AsyncResponse {
|
||||
fetch_languages(&db)
|
||||
.and_then(|languages| Index { languages }.into_response())
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn fetch_languages(
|
||||
db: &Database<PgConnection>,
|
||||
) -> impl Future<Item = Vec<Language>, Error = actix_web::Error> {
|
||||
languages::table
|
||||
.select((languages::language_id, languages::name))
|
||||
.order((languages::priority.asc(), languages::name.asc()))
|
||||
.load_async(&db)
|
||||
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR).into())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PasteForm {
|
||||
language: i32,
|
||||
code: String,
|
||||
autodelete: Option<IgnoredAny>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "pastes"]
|
||||
struct NewPaste {
|
||||
identifier: String,
|
||||
delete_at: Option<DateTime<Utc>>,
|
||||
language_id: i32,
|
||||
paste: String,
|
||||
}
|
||||
|
||||
const CHARACTERS: &[u8] = b"23456789bcdfghjkmnpqrstvwxzBCDFGHJKLMNPQRSTVWX_-";
|
||||
|
||||
fn insert_paste(db: State<Database<PgConnection>>, Form(form): Form<PasteForm>) -> AsyncResponse {
|
||||
let mut rng = thread_rng();
|
||||
let identifier: String = (0..10)
|
||||
.map(|_| char::from(*CHARACTERS.choose(&mut rng).expect("a random character")))
|
||||
.collect();
|
||||
let delete_at = form.autodelete.map(|_| Utc::now() + Duration::hours(24));
|
||||
let cloned_identifier = identifier.clone();
|
||||
diesel::insert_into(pastes::table)
|
||||
.values(NewPaste {
|
||||
identifier,
|
||||
delete_at,
|
||||
language_id: form.language,
|
||||
paste: form.code,
|
||||
})
|
||||
.execute_async(&db)
|
||||
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR).into())
|
||||
.map(move |_| {
|
||||
HttpResponse::Found()
|
||||
.header(LOCATION, format!("/{}", cloned_identifier))
|
||||
.finish()
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "viewpaste.html")]
|
||||
struct DisplayPaste {
|
||||
languages: Vec<Language>,
|
||||
paste: Paste,
|
||||
}
|
||||
|
||||
#[derive(Queryable)]
|
||||
struct QueryPaste {
|
||||
paste: String,
|
||||
language_id: i32,
|
||||
delete_at: Option<DateTime<Utc>>,
|
||||
is_markdown: bool,
|
||||
no_follow: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "404.html")]
|
||||
struct PasteNotFound;
|
||||
|
||||
impl QueryPaste {
|
||||
fn into_paste(self) -> Paste {
|
||||
let QueryPaste {
|
||||
paste,
|
||||
language_id,
|
||||
delete_at,
|
||||
is_markdown,
|
||||
no_follow,
|
||||
} = self;
|
||||
let markdown = if is_markdown {
|
||||
render_markdown(&paste, no_follow)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
Paste {
|
||||
paste,
|
||||
language_id,
|
||||
delete_at,
|
||||
markdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Paste {
|
||||
paste: String,
|
||||
language_id: i32,
|
||||
delete_at: Option<DateTime<Utc>>,
|
||||
markdown: String,
|
||||
}
|
||||
|
||||
fn display_paste(
|
||||
db: State<Database<PgConnection>>,
|
||||
requested_identifier: Path<String>,
|
||||
) -> AsyncResponse {
|
||||
delete_old_pastes(&db)
|
||||
.and_then(|_| {
|
||||
pastes::table
|
||||
.inner_join(languages::table)
|
||||
.select((
|
||||
pastes::paste,
|
||||
pastes::language_id,
|
||||
pastes::delete_at,
|
||||
languages::is_markdown,
|
||||
pastes::no_follow,
|
||||
))
|
||||
.filter(pastes::identifier.eq(requested_identifier.into_inner()))
|
||||
.get_optional_result_async::<QueryPaste>(&db)
|
||||
.map(|paste| (db, paste))
|
||||
})
|
||||
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR).into())
|
||||
.and_then(|(db, paste)| match paste {
|
||||
None => future::Either::A(future::ok(
|
||||
HttpResponse::NotFound().body(PasteNotFound.render().unwrap()),
|
||||
)),
|
||||
Some(paste) => future::Either::B(fetch_languages(&db).and_then(|languages| {
|
||||
DisplayPaste {
|
||||
languages,
|
||||
paste: paste.into_paste(),
|
||||
}
|
||||
.into_response()
|
||||
})),
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn delete_old_pastes(
|
||||
db: &Database<PgConnection>,
|
||||
) -> impl Future<Item = (), Error = AsyncError<diesel::result::Error>> {
|
||||
diesel::delete(pastes::table)
|
||||
.filter(pastes::delete_at.lt(Utc::now()))
|
||||
.execute_async(&db)
|
||||
.map(|pastes| {
|
||||
if pastes > 0 {
|
||||
info!("Deleted {} paste(s)", pastes);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_markdown(markdown: &str, no_follow: bool) -> String {
|
||||
lazy_static! {
|
||||
static ref FILTER: Builder<'static> = {
|
||||
let mut builder = Builder::new();
|
||||
builder.link_rel(Some("noopener noreferrer nofollow"));
|
||||
builder
|
||||
};
|
||||
}
|
||||
let mut output = String::new();
|
||||
html::push_html(
|
||||
&mut output,
|
||||
Parser::new_ext(markdown, Options::ENABLE_TABLES),
|
||||
);
|
||||
if no_follow {
|
||||
FILTER.clean(&output).to_string()
|
||||
} else {
|
||||
ammonia::clean(&output)
|
||||
}
|
||||
}
|
||||
|
||||
fn raw(db: State<Database<PgConnection>>, requested_identifier: Path<String>) -> AsyncResponse {
|
||||
delete_old_pastes(&db)
|
||||
.and_then(move |_| {
|
||||
pastes::table
|
||||
.select(pastes::paste)
|
||||
.filter(pastes::identifier.eq(requested_identifier.into_inner()))
|
||||
.get_optional_result_async::<String>(&db)
|
||||
})
|
||||
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR).into())
|
||||
.map(|paste| match paste {
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
Some(paste) => HttpResponse::Ok().content_type("text/plain").body(paste),
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn favicon(_: ()) -> io::Result<NamedFile> {
|
||||
NamedFile::open("static/favicon.ico")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Queryable)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ApiLanguage {
|
||||
mode: Option<String>,
|
||||
mime: String,
|
||||
}
|
||||
|
||||
fn api_language(db: State<Database<PgConnection>>, id: Path<i32>) -> AsyncResponse {
|
||||
languages::table
|
||||
.find(id.into_inner())
|
||||
.select((languages::highlighter_mode, languages::mime))
|
||||
.get_optional_result_async(&db)
|
||||
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR).into())
|
||||
.map(|json: Option<ApiLanguage>| match json {
|
||||
Some(json) => HttpResponse::Ok()
|
||||
.header(CACHE_CONTROL, "max-age=14400")
|
||||
.json(json),
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let db = Database::open(env::var("DATABASE_URL").expect("DATABASE_URL required"));
|
||||
server::new(move || {
|
||||
App::with_state(db.clone())
|
||||
.middleware(Logger::default())
|
||||
.middleware(
|
||||
DefaultHeaders::new()
|
||||
.header(
|
||||
CONTENT_SECURITY_POLICY,
|
||||
concat!(
|
||||
"default-src 'self'; ",
|
||||
"img-src *; ",
|
||||
"object-src 'none'; ",
|
||||
"base-uri 'none'; ",
|
||||
"frame-ancestors 'none'",
|
||||
),
|
||||
)
|
||||
.header(X_FRAME_OPTIONS, "DENY")
|
||||
.header(X_XSS_PROTECTION, "1; mode=block")
|
||||
.header(REFERRER_POLICY, "no-referrer"),
|
||||
)
|
||||
.resource("/", |r| {
|
||||
r.method(Method::GET).with(index);
|
||||
r.method(Method::POST).with(insert_paste);
|
||||
})
|
||||
.resource("/favicon.ico", |r| r.method(Method::GET).with(favicon))
|
||||
.handler("/static", StaticFiles::new("static").unwrap())
|
||||
.resource("/{identifier}", |r| {
|
||||
r.method(Method::GET).with(display_paste)
|
||||
})
|
||||
.resource("/{identifier}/raw", |r| r.method(Method::GET).with(raw))
|
||||
.resource("/api/v0/language/{id}", |r| {
|
||||
r.method(Method::GET).with(api_language)
|
||||
})
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run();
|
||||
Ok(())
|
||||
warp::serve(routes::routes()).run(([127, 0, 0, 1], 8080));
|
||||
}
|
||||
|
|
5
src/models.rs
Normal file
5
src/models.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod language;
|
||||
mod paste;
|
||||
|
||||
pub use language::Language;
|
||||
pub use paste::Paste;
|
19
src/models/language.rs
Normal file
19
src/models/language.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use crate::schema::languages::dsl::*;
|
||||
use crate::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
|
||||
#[derive(Queryable)]
|
||||
pub struct Language {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub fn fetch(db: &PgConnection) -> Vec<Language> {
|
||||
languages
|
||||
.select((language_id, name))
|
||||
.order((priority.asc(), name.asc()))
|
||||
.load(db)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
25
src/models/paste.rs
Normal file
25
src/models/paste.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use crate::schema::pastes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::prelude::*;
|
||||
use log::info;
|
||||
|
||||
#[derive(Queryable)]
|
||||
pub struct Paste {
|
||||
pub paste: String,
|
||||
pub language_id: i32,
|
||||
pub delete_at: Option<DateTime<Utc>>,
|
||||
pub is_markdown: bool,
|
||||
pub no_follow: bool,
|
||||
}
|
||||
|
||||
impl Paste {
|
||||
pub fn delete_old(db: &PgConnection) {
|
||||
let pastes = diesel::delete(pastes::table)
|
||||
.filter(pastes::delete_at.lt(Utc::now()))
|
||||
.execute(db)
|
||||
.unwrap();
|
||||
if pastes > 0 {
|
||||
info!("Deleted {} paste(s)", pastes);
|
||||
}
|
||||
}
|
||||
}
|
92
src/routes.rs
Normal file
92
src/routes.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
mod api_language;
|
||||
mod display_paste;
|
||||
mod index;
|
||||
mod insert_paste;
|
||||
mod raw_paste;
|
||||
|
||||
use crate::render;
|
||||
use askama::Template;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use std::env;
|
||||
use warp::http::header::{
|
||||
HeaderMap, HeaderValue, CONTENT_SECURITY_POLICY, REFERRER_POLICY, X_FRAME_OPTIONS,
|
||||
X_XSS_PROTECTION,
|
||||
};
|
||||
use warp::http::StatusCode;
|
||||
use warp::{path, Filter, Rejection, Reply};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "404.html")]
|
||||
struct NotFound;
|
||||
|
||||
pub fn routes() -> impl Filter<Extract = (impl Reply,)> {
|
||||
let pool = Pool::new(ConnectionManager::new(
|
||||
env::var("DATABASE_URL").expect("DATABASE_URL required"),
|
||||
))
|
||||
.expect("Couldn't create a connection pool");
|
||||
let db = warp::any().map(move || pool.get().unwrap());
|
||||
let index = warp::path::end()
|
||||
.and(warp::get2())
|
||||
.and(db.clone())
|
||||
.map(index::index);
|
||||
let display_paste = warp::path::param()
|
||||
.and(warp::path::end())
|
||||
.and(warp::get2())
|
||||
.and(db.clone())
|
||||
.and_then(display_paste::display_paste);
|
||||
let raw_paste = path!(String / "raw")
|
||||
.and(warp::path::end())
|
||||
.and(warp::get2())
|
||||
.and(db.clone())
|
||||
.and_then(raw_paste::raw_paste);
|
||||
let insert_paste = warp::path::end()
|
||||
.and(warp::post2())
|
||||
.and(warp::body::content_length_limit(1_000_000))
|
||||
.and(warp::body::form())
|
||||
.and(db.clone())
|
||||
.map(insert_paste::insert_paste);
|
||||
let api_language = path!("api" / "v0" / "language" / i32)
|
||||
.and(warp::path::end())
|
||||
.and(warp::get2())
|
||||
.and(db)
|
||||
.and_then(api_language::api_language);
|
||||
let static_dir = warp::path("static").and(warp::fs::dir("static"));
|
||||
let favicon = warp::path("favicon.ico")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("static/favicon.ico"));
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_SECURITY_POLICY,
|
||||
HeaderValue::from_static(concat!(
|
||||
"default-src 'self'; ",
|
||||
"img-src *; ",
|
||||
"object-src 'none'; ",
|
||||
"base-uri 'none'; ",
|
||||
"frame-ancestors 'none'",
|
||||
)),
|
||||
);
|
||||
headers.insert(X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
|
||||
headers.insert(X_XSS_PROTECTION, HeaderValue::from_static("1; mode=block"));
|
||||
headers.insert(REFERRER_POLICY, HeaderValue::from_static("no-referrer"));
|
||||
index
|
||||
.or(favicon)
|
||||
.or(display_paste)
|
||||
.or(raw_paste)
|
||||
.or(insert_paste)
|
||||
.or(api_language)
|
||||
.or(static_dir)
|
||||
.recover(not_found)
|
||||
.with(warp::reply::with::headers(headers))
|
||||
.with(warp::log("pastebinrun"))
|
||||
}
|
||||
|
||||
fn not_found(rejection: Rejection) -> Result<impl Reply, Rejection> {
|
||||
if rejection.is_not_found() {
|
||||
Ok(warp::reply::with_status(
|
||||
render(NotFound),
|
||||
StatusCode::NOT_FOUND,
|
||||
))
|
||||
} else {
|
||||
Err(rejection)
|
||||
}
|
||||
}
|
26
src/routes/api_language.rs
Normal file
26
src/routes/api_language.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use crate::schema::languages::dsl::*;
|
||||
use crate::Connection;
|
||||
use diesel::prelude::*;
|
||||
use serde::Serialize;
|
||||
use warp::http::header::CACHE_CONTROL;
|
||||
use warp::{Rejection, Reply};
|
||||
|
||||
#[derive(Serialize, Queryable)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ApiLanguage {
|
||||
mode: Option<String>,
|
||||
mime: String,
|
||||
}
|
||||
|
||||
pub fn api_language(id: i32, db: Connection) -> Result<impl Reply, Rejection> {
|
||||
languages
|
||||
.find(id)
|
||||
.select((highlighter_mode, mime))
|
||||
.get_result(&db)
|
||||
.optional()
|
||||
.unwrap()
|
||||
.ok_or_else(warp::reject::not_found)
|
||||
.map(|json: ApiLanguage| {
|
||||
warp::reply::with_header(warp::reply::json(&json), CACHE_CONTROL, "max-age=14400")
|
||||
})
|
||||
}
|
94
src/routes/display_paste.rs
Normal file
94
src/routes/display_paste.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use crate::models::{Language, Paste};
|
||||
use crate::schema::{languages, pastes};
|
||||
use crate::{render, Connection};
|
||||
use ammonia::Builder;
|
||||
use askama::Template;
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
use pulldown_cmark::{Options, Parser};
|
||||
use warp::{Rejection, Reply};
|
||||
|
||||
struct TemplatePaste {
|
||||
paste: String,
|
||||
language_id: i32,
|
||||
delete_at: Option<DateTime<Utc>>,
|
||||
markdown: String,
|
||||
}
|
||||
|
||||
impl TemplatePaste {
|
||||
fn from_paste(paste: Paste) -> Self {
|
||||
let Paste {
|
||||
paste,
|
||||
language_id,
|
||||
delete_at,
|
||||
is_markdown,
|
||||
no_follow,
|
||||
} = paste;
|
||||
let markdown = if is_markdown {
|
||||
render_markdown(&paste, no_follow)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
Self {
|
||||
paste,
|
||||
language_id,
|
||||
delete_at,
|
||||
markdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_markdown(markdown: &str, no_follow: bool) -> String {
|
||||
lazy_static! {
|
||||
static ref FILTER: Builder<'static> = {
|
||||
let mut builder = Builder::new();
|
||||
builder.link_rel(Some("noopener noreferrer nofollow"));
|
||||
builder
|
||||
};
|
||||
}
|
||||
let mut output = String::new();
|
||||
pulldown_cmark::html::push_html(
|
||||
&mut output,
|
||||
Parser::new_ext(markdown, Options::ENABLE_TABLES),
|
||||
);
|
||||
if no_follow {
|
||||
FILTER.clean(&output).to_string()
|
||||
} else {
|
||||
ammonia::clean(&output)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "viewpaste.html")]
|
||||
struct DisplayPaste {
|
||||
languages: Vec<Language>,
|
||||
paste: TemplatePaste,
|
||||
}
|
||||
|
||||
pub fn display_paste(
|
||||
requested_identifier: String,
|
||||
db: Connection,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
Paste::delete_old(&db);
|
||||
let languages = Language::fetch(&db);
|
||||
let paste = pastes::table
|
||||
.inner_join(languages::table)
|
||||
.select((
|
||||
pastes::paste,
|
||||
pastes::language_id,
|
||||
pastes::delete_at,
|
||||
languages::is_markdown,
|
||||
pastes::no_follow,
|
||||
))
|
||||
.filter(pastes::identifier.eq(requested_identifier))
|
||||
.get_result(&db)
|
||||
.optional()
|
||||
.unwrap();
|
||||
paste.ok_or_else(warp::reject::not_found).map(|paste| {
|
||||
render(DisplayPaste {
|
||||
languages,
|
||||
paste: TemplatePaste::from_paste(paste),
|
||||
})
|
||||
})
|
||||
}
|
15
src/routes/index.rs
Normal file
15
src/routes/index.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::models::Language;
|
||||
use crate::{render, Connection};
|
||||
use askama::Template;
|
||||
use warp::Reply;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct Index {
|
||||
languages: Vec<Language>,
|
||||
}
|
||||
|
||||
pub fn index(connection: Connection) -> impl Reply {
|
||||
let languages = Language::fetch(&connection);
|
||||
render(Index { languages })
|
||||
}
|
46
src/routes/insert_paste.rs
Normal file
46
src/routes/insert_paste.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use crate::schema::pastes;
|
||||
use crate::Connection;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use diesel::prelude::*;
|
||||
use rand::prelude::*;
|
||||
use serde::de::IgnoredAny;
|
||||
use serde::Deserialize;
|
||||
use warp::http::Uri;
|
||||
use warp::Reply;
|
||||
|
||||
const CHARACTERS: &[u8] = b"23456789bcdfghjkmnpqrstvwxzBCDFGHJKLMNPQRSTVWX_-";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PasteForm {
|
||||
language: i32,
|
||||
code: String,
|
||||
autodelete: Option<IgnoredAny>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "pastes"]
|
||||
struct NewPaste {
|
||||
identifier: String,
|
||||
delete_at: Option<DateTime<Utc>>,
|
||||
language_id: i32,
|
||||
paste: String,
|
||||
}
|
||||
|
||||
pub fn insert_paste(form: PasteForm, db: Connection) -> impl Reply {
|
||||
let mut rng = thread_rng();
|
||||
let identifier: String = (0..10)
|
||||
.map(|_| char::from(*CHARACTERS.choose(&mut rng).expect("a random character")))
|
||||
.collect();
|
||||
let delete_at = form.autodelete.map(|_| Utc::now() + Duration::hours(24));
|
||||
let cloned_identifier = identifier.clone();
|
||||
diesel::insert_into(pastes::table)
|
||||
.values(NewPaste {
|
||||
identifier,
|
||||
delete_at,
|
||||
language_id: form.language,
|
||||
paste: form.code,
|
||||
})
|
||||
.execute(&db)
|
||||
.unwrap();
|
||||
warp::redirect(format!("/{}", cloned_identifier).parse::<Uri>().unwrap())
|
||||
}
|
16
src/routes/raw_paste.rs
Normal file
16
src/routes/raw_paste.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use crate::models::Paste;
|
||||
use crate::schema::pastes::dsl::*;
|
||||
use crate::Connection;
|
||||
use diesel::prelude::*;
|
||||
use warp::Rejection;
|
||||
|
||||
pub fn raw_paste(requested_identifier: String, db: Connection) -> Result<String, Rejection> {
|
||||
Paste::delete_old(&db);
|
||||
pastes
|
||||
.select(paste)
|
||||
.filter(identifier.eq(requested_identifier))
|
||||
.get_result(&db)
|
||||
.optional()
|
||||
.unwrap()
|
||||
.ok_or_else(warp::reject::not_found)
|
||||
}
|
Loading…
Reference in New Issue
Block a user