Merge branch 'api-v1-pastes' into 'master'

Implement v1 API for pastes

See merge request pastebin.run/server!66
This commit is contained in:
Konrad Borowski 2019-11-03 17:01:52 +00:00
commit 1706b78a00
9 changed files with 133 additions and 50 deletions

1
Cargo.lock generated
View File

@ -151,6 +151,7 @@ dependencies = [
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@ -9,7 +9,7 @@ build = "src/build.rs"
[dependencies]
ammonia = "3.0.0"
base64 = "0.10.1"
chrono = "0.4.6"
chrono = { version = "0.4.9", features = ["serde"] }
diesel = { version = "1.4.1", features = ["chrono", "postgres", "r2d2"] }
diesel_migrations = "1.4.0"
env_logger = { version = "0.6.0", default-features = false }

View File

@ -1,3 +1,4 @@
pub mod language;
pub mod paste;
pub mod rejection;
pub mod session;

View File

@ -1,4 +1,5 @@
use crate::schema::pastes;
use crate::models::rejection::CustomRejection;
use crate::schema::{languages, pastes};
use crate::Connection;
use ammonia::Builder;
use chrono::{DateTime, Utc};
@ -6,6 +7,7 @@ use diesel::prelude::*;
use lazy_static::lazy_static;
use log::info;
use pulldown_cmark::{Options, Parser};
use rand::seq::SliceRandom;
use warp::Rejection;
#[derive(Queryable)]
@ -29,6 +31,47 @@ impl Paste {
}
}
const CHARACTERS: &[u8] = b"23456789bcdfghjkmnpqrstvwxzBCDFGHJKLMNPQRSTVWX-";
#[derive(Insertable)]
#[table_name = "pastes"]
struct InsertPaste {
identifier: String,
delete_at: Option<DateTime<Utc>>,
language_id: i32,
paste: String,
}
pub fn insert(
connection: &Connection,
delete_at: Option<DateTime<Utc>>,
language: &str,
paste: String,
) -> Result<String, Rejection> {
let mut rng = rand::thread_rng();
let identifier: String = (0..10)
.map(|_| char::from(*CHARACTERS.choose(&mut rng).expect("a random character")))
.collect();
let language_id = languages::table
.select(languages::language_id)
.filter(languages::identifier.eq(language))
.get_result(connection)
.optional()
.map_err(warp::reject::custom)?
.ok_or_else(|| warp::reject::custom(CustomRejection::UnrecognizedLanguageIdentifier))?;
let insert_paste = InsertPaste {
identifier,
delete_at,
language_id,
paste,
};
diesel::insert_into(pastes::table)
.values(&insert_paste)
.execute(connection)
.map_err(warp::reject::custom)?;
Ok(insert_paste.identifier)
}
pub struct ExternPaste {
pub paste: String,
pub language_id: i32,

26
src/models/rejection.rs Normal file
View File

@ -0,0 +1,26 @@
use std::error::Error;
use std::fmt::{Display, Formatter, Result};
use warp::http::StatusCode;
#[derive(Debug)]
pub enum CustomRejection {
UnrecognizedLanguageIdentifier,
}
impl CustomRejection {
pub fn status_code(&self) -> StatusCode {
match self {
Self::UnrecognizedLanguageIdentifier => StatusCode::BAD_REQUEST,
}
}
}
impl Display for CustomRejection {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Self::UnrecognizedLanguageIdentifier => write!(f, "unrecognized language identifier"),
}
}
}
impl Error for CustomRejection {}

View File

@ -1 +1,2 @@
pub mod languages;
pub mod pastes;

View File

@ -0,0 +1,32 @@
use crate::models::paste;
use crate::Connection;
use chrono::{DateTime, Utc};
use futures::Future;
use futures03::TryFutureExt;
use serde::Deserialize;
use tokio_executor::blocking;
use warp::Rejection;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PasteForm {
delete_at: Option<DateTime<Utc>>,
#[serde(default = "default_language")]
language: String,
code: String,
}
fn default_language() -> String {
"plain-text".into()
}
pub fn insert_paste(
PasteForm {
delete_at,
language,
code,
}: PasteForm,
connection: Connection,
) -> impl Future<Item = String, Error = Rejection> {
blocking::run(move || paste::insert(&connection, delete_at, &language, code)).compat()
}

View File

@ -1,10 +1,8 @@
use crate::schema::{languages, pastes};
use crate::models::paste;
use crate::Connection;
use chrono::{DateTime, Duration, Utc};
use diesel::prelude::*;
use chrono::{Duration, Utc};
use futures::Future;
use futures03::TryFutureExt;
use rand::prelude::*;
use serde::de::IgnoredAny;
use serde::Deserialize;
use tokio_executor::blocking;
@ -12,8 +10,6 @@ use warp::http::header::LOCATION;
use warp::http::StatusCode;
use warp::{reply, Rejection, Reply};
const CHARACTERS: &[u8] = b"23456789bcdfghjkmnpqrstvwxzBCDFGHJKLMNPQRSTVWX-";
#[derive(Deserialize)]
pub struct PasteForm {
language: String,
@ -21,49 +17,21 @@ pub struct PasteForm {
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,
PasteForm {
language,
code,
autodelete,
}: PasteForm,
connection: Connection,
) -> impl Future<Item = impl Reply, Error = Rejection> {
blocking::run(move || {
let mut rng = thread_rng();
let identifier: String = (0..10)
.map(|_| char::from(*CHARACTERS.choose(&mut rng).expect("a random character")))
.collect();
let cloned_identifier = identifier.clone();
let PasteForm {
language,
code,
autodelete,
} = form;
let delete_at = autodelete.map(|_| Utc::now() + Duration::hours(24));
let language_id = languages::table
.select(languages::language_id)
.filter(languages::identifier.eq(language))
.get_result(&connection)
.map_err(warp::reject::custom)?;
diesel::insert_into(pastes::table)
.values(NewPaste {
identifier,
delete_at,
language_id,
paste: code,
})
.execute(&connection)
.map_err(warp::reject::custom)?;
let identifier = paste::insert(&connection, delete_at, &language, code)?;
Ok(reply::with_header(
StatusCode::SEE_OTHER,
LOCATION,
format!("/{}", cloned_identifier),
format!("/{}", identifier),
))
})
.compat()

View File

@ -7,6 +7,7 @@ mod insert_paste;
mod raw_paste;
mod run;
use crate::models::rejection::CustomRejection;
use crate::models::session::Session;
use crate::templates::{self, RenderRucte};
use crate::Connection;
@ -110,14 +111,19 @@ fn api_v0(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
language.or(run).boxed()
}
fn api_v1_languages(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
path!("api" / "v1")
.and(warp::path("languages"))
fn api_v1(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
let languages = warp::path("languages")
.and(warp::path::end())
.and(warp::get2())
.and(connection(pool.clone()))
.and_then(api_v1::languages::languages);
let pastes = warp::path("pastes")
.and(warp::path::end())
.and(warp::body::content_length_limit(1_000_000))
.and(warp::body::form())
.and(connection(pool))
.and_then(api_v1::languages::languages)
.boxed()
.and_then(api_v1::pastes::insert_paste);
path!("api" / "v1").and(languages.or(pastes)).boxed()
}
fn static_dir() -> BoxedFilter<(impl Reply,)> {
@ -141,7 +147,7 @@ pub fn routes(
.or(favicon())
.or(options(pool.clone()))
.or(api_v0(pool.clone()))
.or(api_v1_languages(pool.clone()))
.or(api_v1(pool.clone()))
.or(raw_paste(pool.clone()))
.or(display_paste(pool.clone()))
.or(static_dir())
@ -174,7 +180,12 @@ fn not_found(pool: PgPool) -> impl Clone + Fn(Rejection) -> NotFoundFuture {
move |rejection| {
let pool = pool.clone();
async move {
if rejection.is_not_found() {
if let Some(rejection) = rejection.find_cause::<CustomRejection>() {
Response::builder()
.status(rejection.status_code())
.body(rejection.to_string().into_bytes())
.map_err(warp::reject::custom)
} else if rejection.is_not_found() {
let session = get_session(pool.clone()).await?;
session
.render()