From fa1189658a911db852ba974a7382509fb4ffbb7c Mon Sep 17 00:00:00 2001 From: Melonai Date: Mon, 11 Jan 2021 11:38:26 +0100 Subject: Fully split client and server --- server/src/main.rs | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++ server/src/schema.rs | 6 +++ server/src/types.rs | 27 +++++++++++ 3 files changed, 160 insertions(+) create mode 100644 server/src/main.rs create mode 100644 server/src/schema.rs create mode 100644 server/src/types.rs (limited to 'server/src') diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..6ca2fe3 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,127 @@ +extern crate openssl; +#[macro_use] extern crate diesel; +#[macro_use] extern crate serde_derive; + +mod schema; +use schema::links; + +mod types; +use types::*; + +use actix_web::{middleware, web, HttpServer, App, HttpResponse, Result, HttpRequest}; +use actix_web::web::{Json, Path, Data}; +use diesel::{PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, QueryResult}; +use dotenv::dotenv; +use diesel::r2d2::{ConnectionManager, Pool}; +use actix_files::{Files, NamedFile}; + +fn establish_connection() -> Pool> { + let database_url = std::env::var("DATABASE_URL").expect("Cannot find DATABASE_URL. Check .env file."); + let manager = ConnectionManager::::new(database_url); + Pool::builder().max_size(4).build(manager).expect("Failed to create pool.") +} + +fn make_url(url_to_check: &str) -> Result { + let url_object = match url::Url::parse(url_to_check) { + Ok(result_url) => result_url, + Err(_) => return Err(()) + }; + if !url_object.cannot_be_a_base() && + url_object.has_host() && + url_object.host_str().map_or(false, |h| h.contains('.')) && + url_object.domain().is_some() + { + Ok(format!("https://{}{}{}", + url_object.domain().unwrap(), + url_object.path(), + url_object.query().map_or("".to_owned(), |q| format!("?{}", q))) + ) + } else { + Err(()) + } +} + +fn add_entry_to_database(entry: Entry, connection: &PgConnection) -> QueryResult { + diesel::insert_into(links::table).values(&entry).get_result::(connection) +} + +fn get_url_from_database(url_hash: &String, connection: &PgConnection) -> Result { + links::table.filter(links::hash.eq(url_hash)).select(links::url).first(connection) +} + +fn get_hash_from_string(to_hash: &String) -> String { + let mut hasher = crc32fast::Hasher::new(); + hasher.update(to_hash.as_bytes()); + base64::encode_config(hasher.finalize().to_ne_bytes(), base64::URL_SAFE_NO_PAD).chars().take(3).collect() +} + +fn add_to_database_safely(mut hash: String, user_url: String, connection: &PgConnection) -> String { + match get_url_from_database(&hash, connection) { + Ok(other_url) => { + if user_url != other_url { + hash = add_to_database_safely(get_hash_from_string(&hash), user_url, connection); + } + } + Err(_) => { + add_entry_to_database(Entry { hash: hash.clone() , url: user_url }, connection).unwrap(); + } + }; + hash +} + +async fn root(req: HttpRequest) -> HttpResponse { + NamedFile::open("./client/build/index.html").unwrap().into_response(&req).unwrap() +} + +async fn shorten(params: Json, state: Data) -> HttpResponse { + let user_url = match make_url(¶ms.url) { + Ok(parse_result) => parse_result, + Err(_) => { + return HttpResponse::BadRequest().json(ErrorResponse{error: "The URL you entered does not follow the proper URL format.".to_string()}); + }, + }; + let hash = add_to_database_safely(get_hash_from_string(&user_url), user_url, &state.get().expect("Could not get a connection from pool")); + + HttpResponse::Ok().json(UserResponse{ hash }) +} + +async fn redirect(info: Path, state: Data) -> HttpResponse { + match get_url_from_database(&info, &state.get().expect("Could not get a connection from pool")) { + Ok(url) => HttpResponse::TemporaryRedirect().header("Location", url).finish(), + Err(_) => HttpResponse::TemporaryRedirect().header("Location", "/").finish() + } +} + +#[actix_rt::main] +async fn main() -> std::io::Result<()> { + dotenv().ok(); + std::env::set_var("RUST_LOG", "actix_web=info"); + env_logger::init(); + + let pool = establish_connection(); + let port = std::env::var("PORT") + .unwrap() + .parse() + .expect("PORT must be a number."); + + HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .data(pool.clone()) + .service( + web::resource("/") + .route(web::get().to(root)) + .route(web::post().to(shorten)) + ) + .service( + web::resource("/{hash}") + .route(web::get().to(redirect)) + ) + .service( + Files::new("/client/", "./client/build/") + ) + }) + .bind(("0.0.0.0", port))? + .run() + .await +} diff --git a/server/src/schema.rs b/server/src/schema.rs new file mode 100644 index 0000000..5d823c0 --- /dev/null +++ b/server/src/schema.rs @@ -0,0 +1,6 @@ +table! { + links (hash) { + hash -> Text, + url -> Text, + } +} \ No newline at end of file diff --git a/server/src/types.rs b/server/src/types.rs new file mode 100644 index 0000000..0a4926e --- /dev/null +++ b/server/src/types.rs @@ -0,0 +1,27 @@ +use diesel::PgConnection; +use diesel::r2d2::{ConnectionManager, Pool}; +use crate::schema::links; + +#[derive(Debug, Deserialize)] +pub struct UserData { + pub url: String +} + +#[derive(Debug, Serialize)] +pub struct UserResponse { + pub hash: String +} + +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub error: String +} + +#[derive(Queryable, Insertable)] +#[table_name="links"] +pub struct Entry { + pub hash: String, + pub url: String +} + +pub type PoolState = Pool>; \ No newline at end of file -- cgit 1.4.1