diff options
| author | Melonai <einebeere@gmail.com> | 2021-01-14 22:52:36 +0100 |
|---|---|---|
| committer | Melonai <einebeere@gmail.com> | 2021-01-14 22:52:36 +0100 |
| commit | c26070ef2d25eb8192843aa417f2ebe3eb008aba (patch) | |
| tree | 6429d2e98541f8c8685c31467ce821d6fad15206 /server | |
| parent | 778c01dbe071a862a38800c706c0c706f081b6cb (diff) | |
| download | shorest-c26070ef2d25eb8192843aa417f2ebe3eb008aba.tar.zst shorest-c26070ef2d25eb8192843aa417f2ebe3eb008aba.zip | |
Refactor server
Diffstat (limited to 'server')
| -rw-r--r-- | server/Cargo.lock | 1 | ||||
| -rw-r--r-- | server/Cargo.toml | 1 | ||||
| -rw-r--r-- | server/src/database.rs | 92 | ||||
| -rw-r--r-- | server/src/main.rs | 142 | ||||
| -rw-r--r-- | server/src/messages.rs | 22 | ||||
| -rw-r--r-- | server/src/parsing.rs | 17 | ||||
| -rw-r--r-- | server/src/routes.rs | 64 | ||||
| -rw-r--r-- | server/src/types.rs | 27 |
8 files changed, 225 insertions, 141 deletions
diff --git a/server/Cargo.lock b/server/Cargo.lock index 9c0005c..09cb0ab 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1537,6 +1537,7 @@ dependencies = [ "diesel_migrations", "dotenv", "env_logger", + "log", "openssl", "serde", "serde_derive", diff --git a/server/Cargo.toml b/server/Cargo.toml index 46dd94a..23261ed 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,5 +20,6 @@ url = "2.2.0" diesel = { version = "1.4.5", features = ["postgres", "r2d2"] } diesel_migrations = "1.4.0" actix-files = "0.5.0" +log = "0.4" openssl = "*" diff --git a/server/src/database.rs b/server/src/database.rs new file mode 100644 index 0000000..89ed1af --- /dev/null +++ b/server/src/database.rs @@ -0,0 +1,92 @@ +use crate::parsing::get_hash_from_string; +use crate::schema::links; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::{ + ExpressionMethods, OptionalExtension, PgConnection, QueryDsl, QueryResult, RunQueryDsl, +}; + +embed_migrations!("migrations"); + +#[derive(Queryable, Insertable)] +#[table_name = "links"] +pub struct ShortenedLink { + pub hash: String, + pub url: String, +} + +// Creates a connection pool to the database +pub fn establish_connection() -> Pool<ConnectionManager<PgConnection>> { + let password = + std::env::var("POSTGRES_PASSWORD").expect("Cannot find POSTGRES_PASSWORD in environment."); + let database_url = format!("postgresql://postgres:{}@postgres/shorest", password); + let manager = ConnectionManager::<PgConnection>::new(database_url); + + Pool::builder() + .build(manager) + .expect("Failed to create pool.") +} + +/// Gets a link from the database. +pub fn get_link_from_database( + url_hash: &String, + connection: &PgConnection, +) -> actix_web::Result<Option<String>, diesel::result::Error> { + links::table + .filter(links::hash.eq(url_hash)) + .select(links::url) + .first(connection) + .optional() +} + +/// Inserts a link into the database. +fn add_link_to_database( + entry: ShortenedLink, + connection: &PgConnection, +) -> QueryResult<ShortenedLink> { + diesel::insert_into(links::table) + .values(&entry) + .get_result::<ShortenedLink>(connection) +} + +/// Inserts a new link into the database, avoiding collisions. +pub fn add_link_to_database_safely( + mut hash: String, + user_url: &str, + connection: &PgConnection, +) -> Result<String, diesel::result::Error> { + // Check whether a link with the same hash exists within the database + match get_link_from_database(&hash, connection)? { + Some(other_url) => { + // We found a collision! + if user_url != other_url { + warn!("'{}' and '{}' have colliding hashes.", user_url, other_url); + + // Try inserting again using the hash of the collided hash + hash = + add_link_to_database_safely(get_hash_from_string(&hash), user_url, connection)?; + } + } + None => { + // No link with the same hash exists, put it in the database + add_link_to_database( + ShortenedLink { + hash: hash.clone(), + url: user_url.to_string(), + }, + connection, + )?; + } + }; + Ok(hash) +} + +pub fn run_migrations(pool: &PoolState) { + let migration_connection = &pool + .get() + .expect("Could not connect to database to run migrations"); + + embedded_migrations::run_with_output(migration_connection, &mut std::io::stdout()) + .expect("Failed to run migrations."); +} + +pub type PoolState = Pool<ConnectionManager<PgConnection>>; diff --git a/server/src/main.rs b/server/src/main.rs index 515fc8f..4e5429b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,131 +1,45 @@ extern crate openssl; -#[macro_use] extern crate diesel; -#[macro_use] extern crate diesel_migrations; -#[macro_use] extern crate serde_derive; - +#[macro_use] +extern crate diesel; +#[macro_use] +extern crate diesel_migrations; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; + +mod database; +mod messages; +mod parsing; +mod routes; 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 diesel::r2d2::{ConnectionManager, Pool}; -use actix_files::{Files, NamedFile}; - -fn establish_connection() -> Pool<ConnectionManager<PgConnection>> { - let password = std::env::var("POSTGRES_PASSWORD").expect("Cannot find POSTGRES_PASSWORD in environment."); - let database_url = format!("postgresql://postgres:{}@postgres/shorest", password); - let manager = ConnectionManager::<PgConnection>::new(database_url); - Pool::builder().max_size(4).build(manager).expect("Failed to create pool.") -} - -fn make_url(url_to_check: &str) -> Result<String, ()> { - 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<Entry> { - diesel::insert_into(links::table).values(&entry).get_result::<Entry>(connection) -} - -fn get_url_from_database(url_hash: &String, connection: &PgConnection) -> Result<String, diesel::result::Error> { - 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/index.html").unwrap().into_response(&req).unwrap() -} - -async fn shorten(params: Json<UserData>, state: Data<PoolState>) -> 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<String>, state: Data<PoolState>) -> 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() - } -} - -embed_migrations!("migrations"); - -fn run_migrations(pool: &PoolState) { - let migration_connection = &pool.get().expect("Could not connect to database to run migrations"); - embedded_migrations::run_with_output(migration_connection, &mut std::io::stdout()); -} +use actix_files::Files; +use actix_web::{middleware, web, App, HttpServer}; #[actix_rt::main] async fn main() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "actix_web=info"); + std::env::set_var("RUST_LOG", "shorest,actix_web=info"); env_logger::init(); - let pool = establish_connection(); - run_migrations(&pool); + info!("Starting shorest {}!", std::env!("CARGO_PKG_VERSION")); + + let pool = database::establish_connection(); + database::run_migrations(&pool); 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/") + web::resource("/") + .route(web::get().to(routes::root)) + .route(web::post().to(routes::shorten)), ) + .service(web::resource("/{hash:[^/]{3}$}").route(web::get().to(routes::redirect))) + .service(Files::new("/", "./client/")) }) - .bind(("0.0.0.0", 80))? - .run() - .await + .bind(("0.0.0.0", 80))? + .run() + .await } diff --git a/server/src/messages.rs b/server/src/messages.rs new file mode 100644 index 0000000..33f6afb --- /dev/null +++ b/server/src/messages.rs @@ -0,0 +1,22 @@ +#[derive(Debug, Deserialize)] +pub struct ShortenRequest { + pub url: String, +} + +#[derive(Debug, Serialize)] +pub struct ShortenResponse { + pub hash: String, +} + +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub error: String, +} + +impl ErrorResponse { + pub fn new(message: &str) -> Self { + ErrorResponse { + error: message.to_string(), + } + } +} diff --git a/server/src/parsing.rs b/server/src/parsing.rs new file mode 100644 index 0000000..40a855a --- /dev/null +++ b/server/src/parsing.rs @@ -0,0 +1,17 @@ +use base64::{encode_config, URL_SAFE_NO_PAD}; +use crc32fast::Hasher; +use url::{ParseError, Url}; + +pub fn make_url(given_url: &str) -> Result<String, ParseError> { + let parsed_url = Url::parse(given_url)?; + Ok(parsed_url.into_string()) +} + +pub fn get_hash_from_string(to_hash: &String) -> String { + let mut hasher = Hasher::new(); + hasher.update(to_hash.as_bytes()); + encode_config(hasher.finalize().to_ne_bytes(), URL_SAFE_NO_PAD) + .chars() + .take(3) + .collect() +} diff --git a/server/src/routes.rs b/server/src/routes.rs new file mode 100644 index 0000000..0055b92 --- /dev/null +++ b/server/src/routes.rs @@ -0,0 +1,64 @@ +use crate::database::{add_link_to_database_safely, get_link_from_database, PoolState}; +use crate::messages::{ErrorResponse, ShortenRequest, ShortenResponse}; +use crate::parsing::{get_hash_from_string, make_url}; +use actix_files::NamedFile; +use actix_web::web::{Data, Json, Path}; +use actix_web::{web, Responder}; +use actix_web::{HttpRequest, HttpResponse}; + +pub async fn root(req: HttpRequest) -> impl Responder { + NamedFile::open("./client/index.html") + .unwrap() + .into_response(&req) + .unwrap() +} + +pub async fn shorten(params: Json<ShortenRequest>, state: Data<PoolState>) -> HttpResponse { + let user_url = match make_url(¶ms.url) { + Ok(url) => url, + Err(_) => { + return HttpResponse::BadRequest().json(ErrorResponse::new( + "The URL you entered does not follow the proper URL format.", + )) + } + }; + + let connection = state.get().expect("Could not get a connection from pool"); + + // Generate the initial hash, this is almost always the same as the inserted hash, but not on collisions. + let initial_hash = get_hash_from_string(&user_url); + + // Insert the URL + let hash = + match web::block(move || add_link_to_database_safely(initial_hash, &user_url, &connection)) + .await + { + Ok(hash) => hash, + Err(e) => { + error!("Error occurred on adding new link: {:?}", e); + return HttpResponse::InternalServerError().finish(); + } + }; + + HttpResponse::Ok().json(ShortenResponse { hash }) +} + +pub async fn redirect(info: Path<String>, state: Data<PoolState>) -> impl Responder { + let connection = state.get().expect("Could not get a connection from pool"); + let url_result = web::block(move || get_link_from_database(&info, &connection)).await; + + match url_result { + Ok(found_url) => match found_url { + Some(url) => HttpResponse::TemporaryRedirect() + .header("Location", url) + .finish(), + None => HttpResponse::TemporaryRedirect() + .header("Location", "/") + .finish(), + }, + Err(e) => { + error!("Error when getting link: {:?}", e); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/server/src/types.rs b/server/src/types.rs deleted file mode 100644 index 0a4926e..0000000 --- a/server/src/types.rs +++ /dev/null @@ -1,27 +0,0 @@ -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<ConnectionManager<PgConnection>>; \ No newline at end of file |
