about summary refs log tree commit diff
path: root/server/src/main.rs
blob: 515fc8f679274f674c64e89aa7d5e7b834150b1f (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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
extern crate openssl;
#[macro_use] extern crate diesel;
#[macro_use] extern crate diesel_migrations;
#[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 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());
}

#[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();
    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/")
            )
    })
        .bind(("0.0.0.0", 80))?
        .run()
        .await
}