aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/db.rs19
-rw-r--r--src/main.rs145
-rw-r--r--src/service.rs121
3 files changed, 145 insertions, 140 deletions
diff --git a/src/db.rs b/src/db.rs
new file mode 100644
index 0000000..a5c0f85
--- /dev/null
+++ b/src/db.rs
@@ -0,0 +1,19 @@
1use anyhow::Result;
2use rusqlite::{Connection, OpenFlags, NO_PARAMS};
3
4use std::path::Path;
5
6pub fn init_db<P: AsRef<Path>>(p: P) -> Result<Connection> {
7 let conn = Connection::open_with_flags(
8 p,
9 OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE,
10 )?;
11 conn.execute(
12 "CREATE TABLE IF NOT EXISTS urls (
13 link TEXT PRIMARY KEY,
14 shortlink TEXT NOT NULL
15 )",
16 NO_PARAMS,
17 )?;
18 return Ok(conn);
19}
diff --git a/src/main.rs b/src/main.rs
index 86af042..d31abfe 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,145 +1,10 @@
1use anyhow::{Context, Result}; 1use anyhow::Result;
2use hyper::header::CONTENT_TYPE;
3use hyper::service::{make_service_fn, service_fn}; 2use hyper::service::{make_service_fn, service_fn};
4use hyper::{Body, Method, Request, Response, Server, StatusCode}; 3use hyper::Server;
5use multer::Multipart;
6use nanoid::nanoid;
7use rusqlite::{params, Connection, OpenFlags, NO_PARAMS};
8use url::form_urlencoded;
9 4
10use std::collections::HashMap; 5mod db;
11use std::path::Path; 6mod service;
12 7use service::shortner_service;
13fn respond_with_shortlink<S: AsRef<str>>(shortlink: S) -> Response<Body> {
14 Response::builder()
15 .status(StatusCode::OK)
16 .header("content-type", "text/html")
17 .body(Body::from(shortlink.as_ref().to_string()))
18 .unwrap()
19}
20
21fn respond_with_status(s: StatusCode) -> Response<Body> {
22 Response::builder().status(s).body(Body::empty()).unwrap()
23}
24
25fn shorten<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<String> {
26 let mut stmt = conn.prepare("select * from urls where link = ?1")?;
27 let mut rows = stmt.query(params![url.as_ref().to_string()])?;
28 if let Some(row) = rows.next()? {
29 return Ok(row.get(1)?);
30 } else {
31 let new_id = nanoid!(4);
32 conn.execute(
33 "insert into urls (link, shortlink) values (?1, ?2)",
34 params![url.as_ref().to_string(), new_id],
35 )?;
36 return Ok(new_id);
37 }
38}
39
40fn get_link<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<Option<String>> {
41 let url = url.as_ref();
42 let mut stmt = conn.prepare("select * from urls where shortlink = ?1")?;
43 let mut rows = stmt.query(params![url.to_string()])?;
44 if let Some(row) = rows.next()? {
45 return Ok(row.get(0)?);
46 } else {
47 return Ok(None);
48 }
49}
50
51async fn process_multipart(
52 body: Body,
53 boundary: String,
54 conn: &mut Connection,
55) -> Result<Response<Body>> {
56 let mut m = Multipart::new(body, boundary);
57 if let Some(field) = m.next_field().await? {
58 if field.name() == Some("shorten") {
59 let content = field
60 .text()
61 .await
62 .with_context(|| format!("Expected field name"))?;
63
64 let shortlink = shorten(content, conn)?;
65 return Ok(respond_with_shortlink(shortlink));
66 }
67 }
68 Ok(Response::builder()
69 .status(StatusCode::OK)
70 .body(Body::empty())?)
71}
72
73async fn shortner_service(req: Request<Body>) -> Result<Response<Body>> {
74 let mut conn = init_db("./urls.db_3").unwrap();
75
76 match req.method() {
77 &Method::POST => {
78 let boundary = req
79 .headers()
80 .get(CONTENT_TYPE)
81 .and_then(|ct| ct.to_str().ok())
82 .and_then(|ct| multer::parse_boundary(ct).ok());
83
84 if boundary.is_none() {
85 let b = hyper::body::to_bytes(req)
86 .await
87 .with_context(|| format!("Failed to stream request body!"))?;
88
89 let params = form_urlencoded::parse(b.as_ref())
90 .into_owned()
91 .collect::<HashMap<String, String>>();
92
93 if let Some(n) = params.get("shorten") {
94 let s = shorten(n, &mut conn)?;
95 return Ok(respond_with_shortlink(s));
96 } else {
97 return Ok(respond_with_status(StatusCode::UNPROCESSABLE_ENTITY));
98 }
99 }
100
101 return process_multipart(req.into_body(), boundary.unwrap(), &mut conn).await;
102 }
103 &Method::GET => {
104 let shortlink = req.uri().path().to_string();
105 let link = get_link(&shortlink[1..], &mut conn);
106 if let Some(l) = link.unwrap() {
107 Ok(Response::builder()
108 .header("Location", &l)
109 .header("content-type", "text/html")
110 .status(StatusCode::MOVED_PERMANENTLY)
111 .body(Body::from(format!(
112 "You will be redirected to: {}. If not, click the link.",
113 &l
114 )))?)
115 } else {
116 Ok(Response::builder()
117 .status(StatusCode::NOT_FOUND)
118 .body(Body::empty())?)
119 }
120 }
121 _ => {
122 return Ok(Response::builder()
123 .status(StatusCode::NOT_FOUND)
124 .body(Body::empty())?)
125 }
126 }
127}
128
129fn init_db<P: AsRef<Path>>(p: P) -> Result<Connection> {
130 let conn = Connection::open_with_flags(
131 p,
132 OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE,
133 )?;
134 conn.execute(
135 "CREATE TABLE IF NOT EXISTS urls (
136 link TEXT PRIMARY KEY,
137 shortlink TEXT NOT NULL
138 )",
139 NO_PARAMS,
140 )?;
141 return Ok(conn);
142}
143 8
144fn main() -> Result<()> { 9fn main() -> Result<()> {
145 smol::run(async { 10 smol::run(async {
diff --git a/src/service.rs b/src/service.rs
new file mode 100644
index 0000000..55a42bf
--- /dev/null
+++ b/src/service.rs
@@ -0,0 +1,121 @@
1use anyhow::{Context, Result};
2use hyper::header::CONTENT_TYPE;
3use hyper::{Body, Method, Request, Response, StatusCode};
4use multer::Multipart;
5use nanoid::nanoid;
6use rusqlite::{params, Connection};
7use url::form_urlencoded;
8
9use std::collections::HashMap;
10
11use crate::db::init_db;
12
13fn respond_with_shortlink<S: AsRef<str>>(shortlink: S) -> Response<Body> {
14 Response::builder()
15 .status(StatusCode::OK)
16 .header("content-type", "text/html")
17 .body(Body::from(shortlink.as_ref().to_string()))
18 .unwrap()
19}
20
21fn respond_with_status(s: StatusCode) -> Response<Body> {
22 Response::builder().status(s).body(Body::empty()).unwrap()
23}
24
25fn shorten<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<String> {
26 let mut stmt = conn.prepare("select * from urls where link = ?1")?;
27 let mut rows = stmt.query(params![url.as_ref().to_string()])?;
28 if let Some(row) = rows.next()? {
29 return Ok(row.get(1)?);
30 } else {
31 let new_id = nanoid!(4);
32 conn.execute(
33 "insert into urls (link, shortlink) values (?1, ?2)",
34 params![url.as_ref().to_string(), new_id],
35 )?;
36 return Ok(new_id);
37 }
38}
39
40fn get_link<S: AsRef<str>>(url: S, conn: &mut Connection) -> Result<Option<String>> {
41 let url = url.as_ref();
42 let mut stmt = conn.prepare("select * from urls where shortlink = ?1")?;
43 let mut rows = stmt.query(params![url.to_string()])?;
44 if let Some(row) = rows.next()? {
45 return Ok(row.get(0)?);
46 } else {
47 return Ok(None);
48 }
49}
50
51async fn process_multipart(
52 body: Body,
53 boundary: String,
54 conn: &mut Connection,
55) -> Result<Response<Body>> {
56 let mut m = Multipart::new(body, boundary);
57 if let Some(field) = m.next_field().await? {
58 if field.name() == Some("shorten") {
59 let content = field
60 .text()
61 .await
62 .with_context(|| format!("Expected field name"))?;
63
64 let shortlink = shorten(content, conn)?;
65 return Ok(respond_with_shortlink(shortlink));
66 }
67 }
68 Ok(Response::builder()
69 .status(StatusCode::OK)
70 .body(Body::empty())?)
71}
72
73pub async fn shortner_service(req: Request<Body>) -> Result<Response<Body>> {
74 let mut conn = init_db("./urls.db_3").unwrap();
75
76 match req.method() {
77 &Method::POST => {
78 let boundary = req
79 .headers()
80 .get(CONTENT_TYPE)
81 .and_then(|ct| ct.to_str().ok())
82 .and_then(|ct| multer::parse_boundary(ct).ok());
83
84 if boundary.is_none() {
85 let b = hyper::body::to_bytes(req)
86 .await
87 .with_context(|| format!("Failed to stream request body!"))?;
88
89 let params = form_urlencoded::parse(b.as_ref())
90 .into_owned()
91 .collect::<HashMap<String, String>>();
92
93 if let Some(n) = params.get("shorten") {
94 let s = shorten(n, &mut conn)?;
95 return Ok(respond_with_shortlink(s));
96 } else {
97 return Ok(respond_with_status(StatusCode::UNPROCESSABLE_ENTITY));
98 }
99 }
100
101 return process_multipart(req.into_body(), boundary.unwrap(), &mut conn).await;
102 }
103 &Method::GET => {
104 let shortlink = req.uri().path().to_string();
105 let link = get_link(&shortlink[1..], &mut conn);
106 if let Some(l) = link.unwrap() {
107 Ok(Response::builder()
108 .header("Location", &l)
109 .header("content-type", "text/html")
110 .status(StatusCode::MOVED_PERMANENTLY)
111 .body(Body::from(format!(
112 "You will be redirected to: {}. If not, click the link.",
113 &l
114 )))?)
115 } else {
116 Ok(respond_with_status(StatusCode::NOT_FOUND))
117 }
118 }
119 _ => Ok(respond_with_status(StatusCode::NOT_FOUND)),
120 }
121}