aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 769ed03f952e4daf877cdc0ce2640b7963fdceb0 (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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use anyhow::{Context, Result};
use hyper::header::CONTENT_TYPE;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};
use multer::Multipart;
use nanoid::nanoid;
use rusqlite::{params, Connection, OpenFlags, NO_PARAMS};
use url::form_urlencoded;

use std::collections::HashMap;
use std::path::Path;

fn shorten<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<String> {
    let mut stmt = conn.prepare("select * from urls where link = ?1")?;
    let mut rows = stmt.query(params![url.as_ref().to_string()])?;
    if let Some(row) = rows.next()? {
        return Ok(row.get(1)?);
    } else {
        let new_id = nanoid!(4);
        conn.execute(
            "insert into urls (link, shortlink) values (?1, ?2)",
            params![url.as_ref().to_string(), new_id],
        )?;
        return Ok(new_id);
    }
}

fn get_link<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<Option<String>> {
    let url = url.as_ref();
    let mut stmt = conn.prepare("select * from urls where shortlink = ?1")?;
    let mut rows = stmt.query(params![url.to_string()])?;
    if let Some(row) = rows.next()? {
        return Ok(row.get(0)?);
    } else {
        return Ok(None);
    }
}

async fn process_multipart(body: Body, boundary: String) -> Result<Response<Body>> {
    let mut m = Multipart::new(body, boundary);
    if let Some(field) = m.next_field().await? {
        let content = field
            .text()
            .await
            .with_context(|| format!("Expected field name"))?;
        eprintln!("{}", content);
    }
    Ok(Response::builder()
        .status(StatusCode::OK)
        .body(Body::empty())?)
}

async fn shortner_service(req: Request<Body>) -> Result<Response<Body>> {
    let mut conn = init_db("./urls.db_3").unwrap();

    match req.method() {
        &Method::POST => {
            let boundary = req
                .headers()
                .get(CONTENT_TYPE)
                .and_then(|ct| ct.to_str().ok())
                .and_then(|ct| multer::parse_boundary(ct).ok());

            // Send `BAD_REQUEST` status if the content-type is not multipart/form-data.
            if boundary.is_none() {
                let b = hyper::body::to_bytes(req)
                    .await
                    .with_context(|| format!("Failed to stream request body!"))?;

                let params = form_urlencoded::parse(b.as_ref())
                    .into_owned()
                    .collect::<HashMap<String, String>>();

                if let Some(n) = params.get("shorten") {
                    let shortlink = shorten(n, &mut conn)?;
                    eprintln!("{}", shortlink);
                    let res = Response::builder()
                        .status(StatusCode::OK)
                        .header("content-type", "text/html")
                        .body(Body::from(shortlink))?;
                    return Ok(res);
                } else {
                    eprintln!("hello world");
                    let res = Response::builder()
                        .status(StatusCode::UNPROCESSABLE_ENTITY)
                        .body(Body::empty())?;
                    return Ok(res);
                }
            }

            if let Ok(res) = process_multipart(req.into_body(), boundary.unwrap()).await {
                return Ok(res);
            } else {
                panic!("nani");
            }
        }
        &Method::GET => {
            let shortlink = req.uri().path().to_string();
            let link = get_link(&shortlink[1..], &mut conn);
            if let Some(l) = link.unwrap() {
                Ok(Response::builder()
                    .header("Location", &l)
                    .header("content-type", "text/html")
                    .status(StatusCode::MOVED_PERMANENTLY)
                    .body(Body::from(format!(
                        "You will be redirected to: {}. If not, click the link.",
                        &l
                    )))?)
            } else {
                Ok(Response::builder()
                    .status(StatusCode::NOT_FOUND)
                    .body(Body::empty())?)
            }
        }
        _ => {
            return Ok(Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Body::empty())?)
        }
    }
}

fn init_db<P: AsRef<Path>>(p: P) -> Result<Connection> {
    let conn = Connection::open_with_flags(
        p,
        OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE,
    )?;
    conn.execute(
        "CREATE TABLE IF NOT EXISTS urls (
            link TEXT PRIMARY KEY,
            shortlink TEXT NOT NULL
        )",
        NO_PARAMS,
    )?;
    return Ok(conn);
}

#[tokio::main]
async fn main() -> Result<()> {
    let addr = ([127, 0, 0, 1], 3000).into();

    let service =
        make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(shortner_service)) });

    let server = Server::bind(&addr).serve(service);
    println!("Listening on http://{}", addr);
    server.await.unwrap();
    Ok(())
}