🏡 index : old_projects/url_shortener.git

author yaqubroli <walchuk2018@icloud.com> 2022-12-19 13:39:29.0 -08:00:00
committer yaqubroli <walchuk2018@icloud.com> 2022-12-19 13:39:29.0 -08:00:00
commit
891973f5325e741988bc79daa6b0bf7ffde1cfc0 [patch]
tree
8fd3bb32ddb5d54620f97bec1150197593c7cdba
parent
db28093f4edf6196ce536184dea1cfe24f70a698
download
891973f5325e741988bc79daa6b0bf7ffde1cfc0.tar.gz

Overhauled database, added pastebin functionality



Diff

 config_defaults.toml         |  12 +++++-------
 index.html                   |  16 ----------------
 results.html                 |  12 ------------
 html/index.html              |  25 +++++++++++++++++++++++++
 html/paste.html              |  12 ++++++++++++
 html/url.html                |  12 ++++++++++++
 src/database.rs              | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 src/endpoints.rs             | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 src/full_path.rs             |  13 +++++++++++++
 src/main.rs                  |   8 ++++----
 src/settings.rs              |  13 +++++++++----
 src/templating.rs            |   4 ++--
 static/style.css             |   5 -----
 target/.rustc_info.json      |   2 +-
 html/static/style.css        |   5 +++++
 target/debug/url_shortener   |   0 
 target/debug/url_shortener.d |   2 +-
 17 files changed, 313 insertions(+), 105 deletions(-)

diff --git a/config_defaults.toml b/config_defaults.toml
index 5185ce6..f95cc57 100644
--- a/config_defaults.toml
+++ a/config_defaults.toml
@@ -71,14 +71,12 @@
# The `application` table be used to express application-specific settings.
# See the `README.md` file for more details on how to use this.

[application.html]
templating = true
path = "html"
domain = "localhost:4000"

[application.database]
# pub struct DatabaseSettings {
#    pub host: String,
#    pub port: u16,
#    pub username: String,
#    pub password: String,
#    pub database: String
# }
host = "localhost"
port = 3306
username = "root"
diff --git a/index.html b/index.html
deleted file mode 100644
index 2563b5a..0000000 100644
--- a/index.html
+++ /dev/null
@@ -1,16 +1,0 @@
<!DOCTYPE html>
<html>
<head>
    <title>URL submission form</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="static/style.css" />
</head>
<body>
    <h1>URL shortener</h1>
    <form action="/" method="post">
        <label for="url">URL:</label>
        <input type="text" name="url" id="url" />
        <input type="submit" value="Shorten" />
    </form>
</body>
</html>
diff --git a/results.html b/results.html
deleted file mode 100644
index 61ed301..0000000 100644
--- a/results.html
+++ /dev/null
@@ -1,12 +1,0 @@
<!DOCTYPE html>
<html>
<head>
    <title>URL shortened</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="static/style.css" />
</head>
<body>
    <h1>URL shortened successfully</h1>
    <p>Your url is accessible at {{domain}}/{{shortened}}</p>
</body>
</html>
diff --git a/html/index.html b/html/index.html
new file mode 100644
index 0000000..e7fd072 100644
--- /dev/null
+++ a/html/index.html
@@ -1,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
    <title>URL submission form</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="static/style.css" />
</head>
<body>
    <h1>URL shortener</h1>
    <form action="/" method="post">
        <input type="hidden" name="content_type" value="Url" />
        <label for="content">URL:</label>
        <input type="text" name="content" id="url" />
        <input type="submit" value="Shorten" />
    </form>
    <hr>
    <h1>Pastebin</h1>
    <form action="/" method="post">
        <input type="hidden" name="content_type" value="Pastebin" />
        <label for="content">Paste:</label>
        <textarea name="content" id="paste" rows="10" cols="50"></textarea>
        <input type="submit" value="Paste" />
    </form>
</body>
</html>
diff --git a/html/paste.html b/html/paste.html
new file mode 100644
index 0000000..df023f9 100644
--- /dev/null
+++ a/html/paste.html
@@ -1,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
    <title>Paste</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="static/style.css" />
</head>
<body>
    <h1>Paste</h1>
    <p>Your paste is accessible at {{domain}}/{{shortened}}</p>
</body>
</html>
diff --git a/html/url.html b/html/url.html
new file mode 100644
index 0000000..61ed301 100644
--- /dev/null
+++ a/html/url.html
@@ -1,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
    <title>URL shortened</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="static/style.css" />
</head>
<body>
    <h1>URL shortened successfully</h1>
    <p>Your url is accessible at {{domain}}/{{shortened}}</p>
</body>
</html>
diff --git a/src/database.rs b/src/database.rs
index ebeefb7..7b63c96 100644
--- a/src/database.rs
+++ a/src/database.rs
@@ -1,9 +1,40 @@
use actix_settings::BasicSettings;
use crate::settings;
use serde::{Serialize, Deserialize};
use crate::{settings, shortener};

use mysql::prelude::*;
use mysql::*;

#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub enum ContentType {
    Url,
    Pastebin,
    Unimplemented,
    All
}

impl From<u8> for ContentType {
    fn from(item: u8) -> Self {
        match item {
            0 => ContentType::Url,
            1 => ContentType::Pastebin,
            255 => ContentType::All,
            _ => ContentType::Unimplemented
        }
    }
}

impl From<ContentType> for u8 {
    fn from(item: ContentType) -> Self {
        match item {
            ContentType::Url => 0,
            ContentType::Pastebin => 1,
            ContentType::All => 255,
            _ => 255
        }
    }
}

#[derive(Debug)]
pub struct RetrievedUrl {
    pub url: String,
@@ -13,9 +44,20 @@
#[derive(Debug)]
pub struct SubmittedUrl {
    pub shortened: String,
    pub success: bool
    pub success: bool,
}

pub struct SubmittedEntry {
    pub shortened: String,
    pub success: bool,
}

pub struct RetrievedEntry {
    pub content: String,
    pub success: bool,
    pub content_type: ContentType
}

// Description: This function takes in a settings struct and returns a mysql connection pool
pub async fn init (settings: &BasicSettings<settings::AppSettings>) -> Pool {
    let database_settings = &settings.application.database;
@@ -23,10 +65,10 @@
    let pool = Pool::new(url.as_str()).unwrap();
    // create the table if it doesn't exist
    let mut connection = pool.get_conn().unwrap();
    if create_table(&mut connection).await {
        println!("Created table `urls`");
    if create_entries_table(&mut connection).await {
        println!("Created table `entries`");
    } else {
        println!("Table `urls` already exists");
        println!("Table `entries` already exists, no need to create it");
    }
    pool
}
@@ -77,22 +119,69 @@
    }
}

// Description: This function takes in a connection and returns a u64 which is the number of rows in the table
pub async fn count_urls (connection: &mut PooledConn) -> u64 {
    let mut result = connection.exec_iter("SELECT COUNT(*) FROM urls", ()).unwrap();
// Description: This function takes in a connection and a ContentType, and counts the number of entries with that content_type
pub async fn count_entries (connection: &mut PooledConn, content_type: ContentType) -> u64 {
    // if content_type is ContentType::All, then we don't need to filter by content_type
    if content_type == ContentType::All {
        let mut result = connection.exec_iter("SELECT COUNT(*) FROM entries", ()).unwrap();
        let row = result.next().unwrap();
        let count = row.unwrap().get::<u64, _>("COUNT(*)").unwrap();
        return count;
    }
    let mut result = connection.exec_iter("SELECT COUNT(*) FROM entries WHERE content_type = :content_type", params! {
        "content_type" => content_type as u8
    }).unwrap();
    let row = result.next().unwrap();
    let count = row.unwrap().get::<u64, _>("COUNT(*)").unwrap();
    count
}

// Description: This function takes in a connection and a shortened url and returns a RetrievedEntry struct, where `content` is the content column and `success` is true if the shortened url exists in the database
pub async fn retrieve_entry (connection: &mut PooledConn, shortened: &str) -> RetrievedEntry {
    let mut result = connection.exec_iter("SELECT content, content_type FROM entries WHERE shortened = :shortened", params! {
        "shortened" => shortened
    }).unwrap();
    let row = result.next().unwrap();
    let content = row.as_ref().unwrap().get::<String, _>("content").unwrap();
    let content_type = ContentType::from(row.as_ref().unwrap().get::<u8, _>("content_type").unwrap());
    RetrievedEntry {
        content,
        success: true,
        content_type
    }
}

// Description: This function takes in a connection, a `content` string, and a content_type u8 and returns a SubmittedEntry struct, where `shortened` is the shortened url column and `success` is true if the shortened url does not exist in the database.
pub async fn submit_entry (connection: &mut PooledConn, content: &str, content_type: &ContentType) -> SubmittedEntry {
    let shortened = shortener::base64(count_entries(connection, ContentType::All).await);
    let row = connection.exec_iter("SELECT shortened FROM entries WHERE shortened = :shortened", params! {
        "shortened" => shortened.clone()
    }).unwrap().next();
    if row.is_some() {
        SubmittedEntry {
            shortened: shortened.clone().to_string(),
            success: false
        }
    } else {
        connection.exec_drop("INSERT INTO entries (content, shortened, content_type) VALUES (:content, :shortened, :content_type)", params! {
            "content" => content,
            "shortened" => shortened.clone(),
            "content_type" => u8::from(content_type.clone())
        }).unwrap();
        SubmittedEntry {
            shortened: shortened.clone().to_string(),
            success: true
        }
    }
}

// Description: This function takes in a connection and returns a bool which is true if the table was created
pub async fn create_table (connection: &mut PooledConn) -> bool {
    let mut result = connection.exec_iter("CREATE TABLE IF NOT EXISTS urls (url VARCHAR(255) NOT NULL, shortened VARCHAR(255) NOT NULL, PRIMARY KEY (shortened))", ()).unwrap();
// Description: This function takes in a connection and creates the table `entries`, which has rows `content`, `shortened`, and `content_type` (byte)
pub async fn create_entries_table (connection: &mut PooledConn) -> bool {
    let mut result = connection.exec_iter("CREATE TABLE IF NOT EXISTS entries (content TEXT(65535) NOT NULL, shortened VARCHAR(255) NOT NULL, content_type TINYINT NOT NULL, PRIMARY KEY (shortened))", ()).unwrap();
    let row = result.next();
    if row.is_some() {
        true
    } else {
        false
    }
}

}
diff --git a/src/endpoints.rs b/src/endpoints.rs
index e16a1a0..452959c 100644
--- a/src/endpoints.rs
+++ a/src/endpoints.rs
@@ -1,61 +1,143 @@
use std::path::PathBuf;
use actix_files::NamedFile;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};

use crate::{database::{self, SubmittedUrl}, shortener::{self, base64}, url, templating};
use crate::{database, shortener, url, templating, full_path};
use crate::database::ContentType;

#[derive(Deserialize, Serialize, Debug)]
pub struct Submission {
    url: String
    content: String,
    content_type: ContentType
}
// write a version of the above function, but without using NamedFile and using standard rust fs libraries instead
pub async fn static_file(path: web::Path<String>) -> impl Responder {
    let path_string = path.into_inner();
    println!("Accessing file {:?}", path_string);
    let file = std::fs::read_to_string(PathBuf::from(format!("static/{}", path_string))).unwrap();
    HttpResponse::Ok().body(file)
}

/* -> Result<NamedFile, actix_web::Error> {
pub async fn static_file(path: web::Path<String>, app_data: web::Data<crate::AppData>) -> impl Responder {
    let path_string = path.into_inner();
    println!("Accessing file {:?}", path_string);
    Ok(NamedFile::open(PathBuf::from(format!("static/{}", path_string)))?)
} */
    let body = if app_data.config.application.html.template_static && path_string.ends_with(".html") {
        templating::read_and_apply_templates(
            full_path::get_full_path(&app_data, &path_string, true),
            templating::TemplateSchema {
                content: "".to_string(),
                shortened: "".to_string(),
                domain: app_data.config.application.html.domain.clone(),
                count: if app_data.config.application.html.count {
                    database::count_entries(&mut app_data.database.get_conn().unwrap(), ContentType::All).await.to_string()
                } else {
                    "".to_string()
                }
            }
        )
    } else {
        std::fs::read_to_string(full_path::get_full_path(&app_data, &path_string, true)).unwrap()
    };
    HttpResponse::Ok().body(body)
}

pub async fn index() -> Result<NamedFile, actix_web::Error> {
    Ok(NamedFile::open("index.html")?)
pub async fn index(app_data: web::Data<crate::AppData>) -> impl Responder {
    // serve index.html, templated if enabled
    if app_data.config.application.html.template_index {
        HttpResponse::Ok().body(
            templating::read_and_apply_templates(
                full_path::get_full_path(
                    &app_data, 
                    "index.html", 
                    false
                ),
                templating::TemplateSchema {
                    content: "".to_string(),
                    shortened: "".to_string(),
                    domain: app_data.config.application.html.domain.clone(),
                    count: if app_data.config.application.html.count {
                        database::count_entries(&mut app_data.database.get_conn().unwrap(), ContentType::All).await.to_string()
                    } else {
                        "".to_string()
                    }
                }
            )
        )
    } else {
        HttpResponse::Ok().body(
            std::fs::read_to_string(full_path::get_full_path(&app_data, "index.html", true)).unwrap()
        )
    }
}
pub async fn submit_url(form: web::Form<Submission>, app_data: web::Data<crate::AppData>) -> impl Responder {
    let url = url::format_url(form.url.clone());


// Takes some content and submits an entry to the database, and serves the appropriate response (url.html or paste.html) depending on the content type
pub async fn submit_entry(form: web::Form<Submission>, app_data: web::Data<crate::AppData>) -> impl Responder {
    let mut connection = app_data.database.get_conn().unwrap();
    let count = database::count_urls(&mut connection).await;
    for n in 0..3{
        let shortened = shortener::base64(count);
        let submitted_url = database::submit_url(&mut connection, &url, &shortened).await;
        if submitted_url.success {
            return HttpResponse::Ok().body(
                templating::read_and_apply_templates(
                    PathBuf::from("results.html"),
                    templating::TemplateSchema {
                        url: url,
                        shortened: submitted_url.shortened,
                        domain: app_data.config.application.templating.domain.clone(),
                        count: count.to_string()
                    }
                )
            );
    let count = database::count_entries(&mut connection, ContentType::All).await;
    for _n in 0..3 {
        let submitted_entry = database::submit_entry(&mut connection, &form.content, &form.content_type).await;
        if submitted_entry.success {
            if app_data.config.application.html.template {
                return HttpResponse::Ok().body(
                    templating::read_and_apply_templates(
                        full_path::get_full_path(
                            &app_data, 
                            // If the content type is a URL, serve the url.html template, otherwise serve the paste.html template
                            if form.content_type == ContentType::Url {
                                "url.html"
                            } else {
                                "paste.html"
                            }, 
                            false),
                        templating::TemplateSchema {
                            content: form.content.clone(),
                            shortened: submitted_entry.shortened.clone(),
                            domain: app_data.config.application.html.domain.clone(),
                            count: count.to_string()
                        }
                    )
                );
            } else {
                // If the content type is a URL, tell them the shortened URL, otherwise tell them the paste ID
                if form.content_type == ContentType::Url {
                    return HttpResponse::Ok().body(format!("Your URL has been shortened to {}/{}", app_data.config.application.html.domain, submitted_entry.shortened));
                } else {
                    return HttpResponse::Ok().body(format!("Your paste is accessible at {}/{}", app_data.config.application.html.domain, submitted_entry.shortened));
                }
            }
        } else {
            return HttpResponse::InternalServerError().body("An error occured while submitting your entry.")
        }
    }
    HttpResponse::InternalServerError().body("An error occured while submitting your URL")
}

// Takes a shortened URL and redirects to the original URL
pub async fn redirect_url(path: web::Path<(String)>, app_data: web::Data<crate::AppData>) -> impl Responder {
    println!("Redirect request recieved to {:?}", path);
    let (shortened) = path.into_inner();
// Takes a shortened URL and, depending on whether the content type is a URL or a paste, redirects to the original URL or serves the paste as plaintext
pub async fn serve_entry(path: web::Path<String>, app_data: web::Data<crate::AppData>) -> impl Responder {
    let shortened = path.into_inner();
    let mut connection = app_data.database.get_conn().unwrap();
    let retrieved_url = database::retrieve_url(&mut connection, &shortened).await;
    println!("Redirecting to {:?}", retrieved_url.url);
    HttpResponse::Found().header("Location", retrieved_url.url).finish()
}
    let entry = database::retrieve_entry(&mut connection, &shortened).await;
    if entry.success {
        if entry.content_type == ContentType::Url {
            HttpResponse::Found().append_header(("Location", entry.content)).finish()
        } else {
            // If the content type is a paste, serve it as plaintext. Set the MIME type before serving it.
            HttpResponse::Ok().content_type("text/plain").body(entry.content)
        }
    } else {
        HttpResponse::NotFound().body(
            if app_data.config.application.html.template {
                templating::read_and_apply_templates(
                    full_path::get_full_path(&app_data, "404.html", false),
                    templating::TemplateSchema {
                        content: "".to_string(),
                        shortened: "".to_string(),
                        domain: app_data.config.application.html.domain.clone(),
                        count: if app_data.config.application.html.count {
                            database::count_entries(&mut app_data.database.get_conn().unwrap(), ContentType::All).await.to_string()
                        } else {
                            "".to_string()
                        }
                    }
                )
            } else {
                std::fs::read_to_string(PathBuf::from(format!("static/404.html"))).unwrap()
            }
        )
    }
} 
diff --git a/src/full_path.rs b/src/full_path.rs
new file mode 100644
index 0000000..347fa3b 100644
--- /dev/null
+++ a/src/full_path.rs
@@ -1,0 +1,13 @@
use std::path::PathBuf;

use crate::AppData;

pub fn get_full_path (app_data: &AppData, path: &str, static_: bool) -> PathBuf {
    let mut full_path = PathBuf::from(app_data.config.application.html.path.clone());
    if static_ {
        full_path.push(app_data.config.application.html.static_path.clone());
    }
    full_path.push(path);
    print!("Full path: {:?}", full_path);
    full_path
}
diff --git a/src/main.rs b/src/main.rs
index 222801b..0024e6e 100644
--- a/src/main.rs
+++ a/src/main.rs
@@ -8,12 +8,12 @@
pub mod shortener;
pub mod url;
pub mod templating;
pub mod full_path;

/*

TODO:
 * - clean up code
 * - add HTML templates
 * - implement other functions (pastebin maybe?)
 * - add logging
*/

#[derive(Clone)]
@@ -47,8 +47,8 @@
                .app_data(web::Data::new(app_data.clone()))
                .route("/", web::get().to(endpoints::index))
                .route("/static/{filename}", web::get().to(endpoints::static_file))
                .route("/{shortened}", web::get().to(endpoints::redirect_url))
                .route("/", web::post().to(endpoints::submit_url))
                .route("/{shortened}", web::get().to(endpoints::serve_entry))
                .route("/", web::post().to(endpoints::submit_entry))
        }
    })
    .apply_settings(&settings)
diff --git a/src/settings.rs b/src/settings.rs
index b195b88..16b1473 100644
--- a/src/settings.rs
+++ a/src/settings.rs
@@ -1,10 +1,10 @@
use actix_settings::{BasicSettings};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AppSettings {
    pub database: DatabaseSettings,
    pub templating: TemplatingSettings
    pub html: HtmlSettings
}

#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -17,9 +17,14 @@
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TemplatingSettings {
    pub enabled: bool,
    pub domain: String
pub struct HtmlSettings {
    pub template: bool,
    pub template_index: bool,
    pub template_static: bool,
    pub count: bool,
    pub domain: String,
    pub path: String,
    pub static_path: String
}

pub fn init () -> BasicSettings<AppSettings> {
diff --git a/src/templating.rs b/src/templating.rs
index da40319..9536232 100644
--- a/src/templating.rs
+++ a/src/templating.rs
@@ -1,9 +1,9 @@
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;

pub struct TemplateSchema {
    pub url: String,
    pub content: String,
    pub shortened: String,
    pub domain: String,
    pub count: String
@@ -15,7 +15,7 @@
    file.read_to_string(&mut contents).unwrap();
    // Hardcoded templates, will change this if/when the amount of templates increases
    contents
        .replace("{{url}}", &schema.url)
        .replace("{{content}}", &schema.content)
        .replace("{{shortened}}", &schema.shortened)
        .replace("{{domain}}", &schema.domain)
        .replace("{{count}}", &schema.count)
diff --git a/static/style.css b/static/style.css
deleted file mode 100644
index 359f792..0000000 100644
--- a/static/style.css
+++ /dev/null
@@ -1,5 +1,0 @@
body {
    font-family: sans-serif;
    background-color: #f0f0f0;
    text-align: center;
}
diff --git a/target/.rustc_info.json b/target/.rustc_info.json
index 5d51843..7928b5a 100644
--- a/target/.rustc_info.json
+++ a/target/.rustc_info.json
@@ -1,1 +1,1 @@
{"rustc_fingerprint":13098977256995595289,"outputs":{"10376369925670944939":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/opt/homebrew/Cellar/rust/1.65.0\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"15697416045686424142":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.65.0\nbinary: rustc\ncommit-hash: unknown\ncommit-date: unknown\nhost: aarch64-apple-darwin\nrelease: 1.65.0\nLLVM version: 15.0.0\n","stderr":""}},"successes":{}}
{"rustc_fingerprint":13098977256995595289,"outputs":{"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.65.0\nbinary: rustc\ncommit-hash: unknown\ncommit-date: unknown\nhost: aarch64-apple-darwin\nrelease: 1.65.0\nLLVM version: 15.0.0\n","stderr":""},"10376369925670944939":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/opt/homebrew/Cellar/rust/1.65.0\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"15697416045686424142":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n","stderr":""}},"successes":{}}
diff --git a/html/static/style.css b/html/static/style.css
new file mode 100644
index 0000000..359f792 100644
--- /dev/null
+++ a/html/static/style.css
@@ -1,0 +1,5 @@
body {
    font-family: sans-serif;
    background-color: #f0f0f0;
    text-align: center;
}
diff --git a/target/debug/url_shortener b/target/debug/url_shortener
index 7858f6b0913859e465e41578426dfb769e3c9e8f..810b445b86457400f02a7c03324665984f33364c 100755
Binary files a/target/debug/url_shortener and a/target/debug/url_shortener differdiff --git a/target/debug/url_shortener.d b/target/debug/url_shortener.d
index e49228f..f3273be 100644
--- a/target/debug/url_shortener.d
+++ a/target/debug/url_shortener.d
@@ -1,1 +1,1 @@
/Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/target/debug/url_shortener: /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/build.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/database.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/endpoints.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/main.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/settings.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/shortener.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/templating.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/url.rs
/Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/target/debug/url_shortener: /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/build.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/database.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/endpoints.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/full_path.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/main.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/settings.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/shortener.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/templating.rs /Users/yaqub/OneDrive\ -\ University\ of\ St\ Andrews/dev/rust/url_shortener/src/url.rs