summary refs log tree commit diff
path: root/src/main.rs
blob: ab4c044c92a6d3d1cc1669425660fab7ceaa83cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate diesel;

mod schema;
use schema::links;

mod types;
use types::*;

use actix_web::{middleware, web, HttpServer, App, HttpResponse, Result};
use actix_web::web::{Json, Path, Data};
use diesel::{PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, QueryResult};
use dotenv::dotenv;
use diesel::r2d2::{ConnectionManager, Pool};

fn establish_connection() -> Pool<ConnectionManager<PgConnection>> {
    dotenv().ok();
    let database_url = std::env::var("DATABASE_URL").expect("Cannot find DATABASE_URL. Check .env file.");
    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.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(_) => {
            if add_entry_to_database(Entry { hash: hash.clone() , url: user_url }, connection).is_err() {

            }
        }
    };
    hash
}

async fn root() -> HttpResponse {
    HttpResponse::Ok().body("Please make a POST request to / in the format {'url': '<your_url>'} with the URL you want to shorten!")
}

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().body("The URL you entered does not follow the proper URL format.");
        },
    };
    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::NotFound().body("The URL you specified could not be found.")
    }
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "actix_web=info");
    env_logger::init();

    let pool = establish_connection();

    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))
            )
    })
        .bind("localhost:3000")?
        .run()
        .await

}