From 891973f5325e741988bc79daa6b0bf7ffde1cfc0 Mon Sep 17 00:00:00 2001 From: yaqubroli Date: Mon, 19 Dec 2022 13:39:29 -0800 Subject: [PATCH] Overhauled database, added pastebin functionality --- 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 @@ - - - - URL submission form - - - - -

URL shortener

-
- - - -
- -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 @@ - - - - URL shortened - - - - -

URL shortened successfully

-

Your url is accessible at {{domain}}/{{shortened}}

- -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 @@ + + + + URL submission form + + + + +

URL shortener

+
+ + + + +
+
+

Pastebin

+
+ + + + +
+ +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 @@ + + + + Paste + + + + +

Paste

+

Your paste is accessible at {{domain}}/{{shortened}}

+ +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 @@ + + + + URL shortened + + + + +

URL shortened successfully

+

Your url is accessible at {{domain}}/{{shortened}}

+ +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 for ContentType { + fn from(item: u8) -> Self { + match item { + 0 => ContentType::Url, + 1 => ContentType::Pastebin, + 255 => ContentType::All, + _ => ContentType::Unimplemented + } + } +} + +impl From 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) -> 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::("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::("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::("content").unwrap(); + let content_type = ContentType::from(row.as_ref().unwrap().get::("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) -> 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 { +pub async fn static_file(path: web::Path, app_data: web::Data) -> 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 { - Ok(NamedFile::open("index.html")?) +pub async fn index(app_data: web::Data) -> 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, app_data: web::Data) -> 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, app_data: web::Data) -> 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) -> 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, app_data: web::Data) -> 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 { 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 differ diff --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 -- rgit 0.1.5