aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client/package.json1
-rw-r--r--server/Cargo.lock1
-rw-r--r--server/Cargo.toml1
-rw-r--r--server/src/database.rs92
-rw-r--r--server/src/main.rs140
-rw-r--r--server/src/messages.rs22
-rw-r--r--server/src/parsing.rs17
-rw-r--r--server/src/routes.rs64
-rw-r--r--server/src/types.rs27
9 files changed, 224 insertions, 141 deletions
diff --git a/client/package.json b/client/package.json
index 9c46742..89ea8c3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -2,7 +2,6 @@
"name": "shorest",
"version": "0.2.0",
"private": true,
- "homepage": "/client/",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
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(&params.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(&params.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