add UNTESTED raw endpoint

This commit is contained in:
Dominic Harris 2022-03-14 08:59:08 -04:00
parent 1ec6c2031b
commit 0e73e822fb
No known key found for this signature in database
GPG Key ID: 93CCF85F3E2A4F65
3 changed files with 206 additions and 147 deletions

View File

@ -23,6 +23,8 @@
**GET** `/api/p/:id` - Get a paste by ID
**GET** `/api/p/r/:id` - Get a pastes raw content
**POST** `/api/p/n` - Post a new paste
**GET** `/api/s` - Get stats about the instance

View File

@ -1,163 +1,25 @@
mod config;
mod models;
mod routes;
use std::{io, path::PathBuf};
use actix_cors::Cors;
use actix_governor::{Governor, GovernorConfigBuilder};
use actix_web::{
get, post,
web::{self, Data},
App, HttpResponse, HttpServer, Responder,
App, HttpServer,
};
use config::Config;
use chrono::Duration;
use models::{
ApiError, ApiResponse, GetPasteResponse, GetStatsResponse, NewPasteResponse, PartialPaste,
Paste,
};
use nanoid::nanoid;
use sqlx::{
postgres::{PgPoolOptions, PgRow},
types::chrono::Utc,
PgPool, Row,
};
use sqlx::{postgres::PgPoolOptions, PgPool};
use crate::routes::{get_paste, get_raw_paste, get_stats, new_paste};
#[derive(Clone)]
struct AppState {
config: Config,
pool: PgPool,
}
#[get("/s")]
async fn get_stats(state: web::Data<AppState>) -> impl Responder {
// TODO: Maybe there's a less hacky way to do this..?
let count: Result<i64, sqlx::Error> = sqlx::query(r#"SELECT COUNT(*) FROM pastes"#)
.try_map(|row: PgRow| row.try_get::<i64, _>("count"))
.fetch_one(&state.pool)
.await;
if let Err(e) = count {
eprintln!("Error occurred while retrieving paste count: {:?}", e);
return HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Error occurred while retrieving paste count, please try again."
.to_string(),
},
});
}
HttpResponse::Ok().json(ApiResponse {
success: true,
data: GetStatsResponse {
count: count.unwrap(),
},
})
}
#[get("/{id}")]
async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> impl Responder {
let id = id.into_inner();
let res: Result<Paste, sqlx::Error> =
sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#)
.bind(id.clone())
.fetch_one(&state.pool)
.await;
match res {
Ok(p) => {
// this may be worth handling at some point..
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
.bind(id.clone())
.execute(&state.pool)
.await;
HttpResponse::Ok().json(ApiResponse {
success: true,
data: GetPasteResponse {
id: p.id,
content: p.content,
views: p.views + 1,
expires_at: p.expires_at,
},
})
}
Err(e) => match e {
sqlx::Error::RowNotFound => {
return HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: format!("Paste {id} wasnt found."),
},
});
}
_ => {
eprintln!("Error occurred while getting paste: {:?}", e);
HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Unknown error occurred, please try again.".to_string(),
},
})
}
},
}
}
#[post("/n")]
async fn new_paste(state: web::Data<AppState>, data: web::Json<PartialPaste>) -> impl Responder {
if data.content.is_empty() || data.content.len() > state.config.pastes.character_limit {
let character_limit = state.config.pastes.character_limit;
return HttpResponse::BadRequest().json(ApiResponse {
success: false,
data: ApiError {
message: format!("Maximum file length exceeded, maximum is {character_limit} characters. Or the content is blank.."),
},
});
}
let length = state.config.pastes.id_length;
let id = nanoid!(length);
let expires_at = if state.config.pastes.days_til_expiration == -1 {
None
} else {
Some(Utc::now() + Duration::days(state.config.pastes.days_til_expiration))
};
let res =
sqlx::query(r#"INSERT INTO pastes("id", "content", "expires_at") VALUES ($1, $2, $3)"#)
.bind(id.clone())
.bind(data.content.clone())
.bind(expires_at)
.execute(&state.pool)
.await;
match res {
Ok(_) => HttpResponse::Ok().json(ApiResponse {
success: true,
data: NewPasteResponse {
id,
content: data.content.clone(),
},
}),
Err(e) => {
eprintln!("Error occurred while creating paste: {:?}", e);
HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Unknown error occurred, please try again.".to_string(),
},
})
}
}
pub struct AppState {
pub config: Config,
pub pool: PgPool,
}
#[actix_rt::main]
@ -200,7 +62,8 @@ async fn main() -> io::Result<()> {
web::scope("/p")
.wrap(Governor::new(&paste_governor))
.service(get_paste)
.service(new_paste),
.service(new_paste)
.service(get_raw_paste),
)
})
.bind(address)?

194
backend/src/routes.rs Normal file
View File

@ -0,0 +1,194 @@
use actix_web::{
get, post,
web::{self},
HttpResponse, Responder,
};
use chrono::Duration;
use nanoid::nanoid;
use sqlx::{postgres::PgRow, types::chrono::Utc, Row};
use crate::{
models::{
ApiError, ApiResponse, GetPasteResponse, GetStatsResponse, NewPasteResponse, PartialPaste,
Paste,
},
AppState,
};
#[get("/s")]
pub async fn get_stats(state: web::Data<AppState>) -> impl Responder {
// TODO: Maybe there's a less hacky way to do this..?
let count: Result<i64, sqlx::Error> = sqlx::query(r#"SELECT COUNT(*) FROM pastes"#)
.try_map(|row: PgRow| row.try_get::<i64, _>("count"))
.fetch_one(&state.pool)
.await;
if let Err(e) = count {
eprintln!("Error occurred while retrieving paste count: {:?}", e);
return HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Error occurred while retrieving paste count, please try again."
.to_string(),
},
});
}
HttpResponse::Ok().json(ApiResponse {
success: true,
data: GetStatsResponse {
count: count.unwrap(),
},
})
}
#[get("/{id}")]
pub async fn get_paste(state: web::Data<AppState>, id: web::Path<String>) -> impl Responder {
let id = id.into_inner();
let res: Result<Paste, sqlx::Error> =
sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#)
.bind(id.clone())
.fetch_one(&state.pool)
.await;
match res {
Ok(p) => {
// this may be worth handling at some point..
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
.bind(id.clone())
.execute(&state.pool)
.await;
HttpResponse::Ok().json(ApiResponse {
success: true,
data: GetPasteResponse {
id: p.id,
content: p.content,
views: p.views + 1,
expires_at: p.expires_at,
},
})
}
Err(e) => match e {
sqlx::Error::RowNotFound => {
return HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: format!("Paste {id} wasnt found."),
},
});
}
_ => {
eprintln!("Error occurred while getting paste: {:?}", e);
HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Unknown error occurred, please try again.".to_string(),
},
})
}
},
}
}
#[get("/r/{id}")]
pub async fn get_raw_paste(state: web::Data<AppState>, id: web::Path<String>) -> impl Responder {
let id = id.into_inner();
let res: Result<Paste, sqlx::Error> =
sqlx::query_as::<_, Paste>(r#"SELECT * FROM pastes WHERE "id" = $1"#)
.bind(id.clone())
.fetch_one(&state.pool)
.await;
match res {
Ok(p) => {
// this may be worth handling at some point..
let _ = sqlx::query(r#"UPDATE pastes SET "views" = "views" + 1 WHERE "id" = $1"#)
.bind(id.clone())
.execute(&state.pool)
.await;
HttpResponse::Ok().body(p.content)
}
Err(e) => match e {
sqlx::Error::RowNotFound => {
return HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: format!("Paste {id} wasnt found."),
},
});
}
_ => {
eprintln!("Error occurred while getting paste: {:?}", e);
HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Unknown error occurred, please try again.".to_string(),
},
})
}
},
}
}
#[post("/n")]
pub async fn new_paste(
state: web::Data<AppState>,
data: web::Json<PartialPaste>,
) -> impl Responder {
if data.content.is_empty() || data.content.len() > state.config.pastes.character_limit {
let character_limit = state.config.pastes.character_limit;
return HttpResponse::BadRequest().json(ApiResponse {
success: false,
data: ApiError {
message: format!("Maximum file length exceeded, maximum is {character_limit} characters. Or the content is blank.."),
},
});
}
let length = state.config.pastes.id_length;
let id = nanoid!(length);
let expires_at = if state.config.pastes.days_til_expiration == -1 {
None
} else {
Some(Utc::now() + Duration::days(state.config.pastes.days_til_expiration))
};
let res =
sqlx::query(r#"INSERT INTO pastes("id", "content", "expires_at") VALUES ($1, $2, $3)"#)
.bind(id.clone())
.bind(data.content.clone())
.bind(expires_at)
.execute(&state.pool)
.await;
match res {
Ok(_) => HttpResponse::Ok().json(ApiResponse {
success: true,
data: NewPasteResponse {
id,
content: data.content.clone(),
},
}),
Err(e) => {
eprintln!("Error occurred while creating paste: {:?}", e);
HttpResponse::InternalServerError().json(ApiResponse {
success: false,
data: ApiError {
message: "Unknown error occurred, please try again.".to_string(),
},
})
}
}
}