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(-)
@@ -71,14 +71,12 @@
[application.html]
templating = true
path = "html"
domain = "localhost:4000"
[application.database]
host = "localhost"
port = 3306
username = "root"
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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
}
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();
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 @@
}
}
pub async fn count_urls (connection: &mut PooledConn) -> u64 {
let mut result = connection.exec_iter("SELECT COUNT(*) FROM urls", ()).unwrap();
pub async fn count_entries (connection: &mut PooledConn, content_type: ContentType) -> u64 {
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
}
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
}
}
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
}
}
}
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();
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
}
}
}
@@ -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
}
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)
}
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 {
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());
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 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 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")
}
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();
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 {
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()
}
)
}
}
@@ -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
}
@@ -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)
@@ -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> {
@@ -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();
contents
.replace("{{url}}", &schema.url)
.replace("{{content}}", &schema.content)
.replace("{{shortened}}", &schema.shortened)
.replace("{{domain}}", &schema.domain)
.replace("{{count}}", &schema.count)
@@ -1,5 +1,0 @@
body {
font-family: sans-serif;
background-color: #f0f0f0;
text-align: center;
}
@@ -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":{}}
@@ -1,0 +1,5 @@
body {
font-family: sans-serif;
background-color: #f0f0f0;
text-align: center;
}
Binary files a/target/debug/url_shortener and a/target/debug/url_shortener differ
@@ -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