Refactored database, added templating, added shortener
Diff
Cargo.lock | 8 ++++----
Cargo.toml | 6 ++++--
config_defaults.toml | 1 +
index.html | 2 ++
results.html | 12 ++++++++++++
src/database.rs | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
src/endpoints.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
src/main.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++++-------------------
src/settings.rs | 11 +++++++++--
src/shortener.rs | 39 +++++++++++++++++++++++++++++++++++++++
src/templating.rs | 22 ++++++++++++++++++++++
src/url.rs | 8 ++++++++
static/style.css | 5 +++++
target/.rustc_info.json | 2 +-
target/debug/url_shortener | 0
target/debug/url_shortener.d | 2 +-
16 files changed, 283 insertions(+), 180 deletions(-)
@@ -2071,18 +2071,18 @@
[[package]]
name = "thiserror"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
@@ -11,8 +11,10 @@
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
actix-files = "0.6.2"
actix-settings = "0.6.0"
serde = { version = "1.0", features = ["derive"] }
mysql = "23.0.0"
actix-files = "0.6.2"
@@ -1,7 +1,8 @@
[actix]
hosts = [
["0.0.0.0", 9000]
]
@@ -1,7 +1,9 @@
<!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>
@@ -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,142 +1,98 @@
use actix_settings::BasicSettings;
use crate::settings;
use mysql::prelude::*;
use mysql::*;
let url = format!(
"mysql://{}:{}@{}:{}/{}",
config.db.user, config.db.password, config.db.host, config.db.port, config.db.database
);
println!("Connecting to database at {}.", url);
let pool = Pool::new(url.as_str()).expect("Unable to connect to database.");
if !does_table_exist(&pool) {
println!("Table does not exist. Creating them.");
create_table(&pool);
#[derive(Debug)]
pub struct RetrievedUrl {
pub url: String,
pub success: bool
}
#[derive(Debug)]
pub struct SubmittedUrl {
pub shortened: String,
pub success: bool
}
pub async fn init (settings: &BasicSettings<settings::AppSettings>) -> Pool {
let database_settings = &settings.application.database;
let url = format!("mysql://{}:{}@{}:{}/{}", database_settings.username, database_settings.password, database_settings.host, database_settings.port, database_settings.database);
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`");
} else {
println!("Table `urls` already exists");
}
pool
} */
pub fn does_table_exist(pool: &Pool) -> bool {
let mut conn = pool.get_conn().unwrap();
let result: Vec<String> = conn
.exec_map(
r"
SELECT table_name FROM information_schema.tables WHERE table_schema = :database
",
params! {
"database" => "url_shortener"
},
|table_name| table_name,
)
.unwrap();
result.len() > 0
}
pub fn create_table(pool: &Pool) {
let mut conn = pool.get_conn().unwrap();
conn.query_drop(
r"
CREATE TABLE IF NOT EXISTS `urls` (
`id` INT NOT NULL AUTO_INCREMENT,
`url` VARCHAR(255) NOT NULL,
`shortened` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
",
)
.unwrap();
println!("Table created.");
}
pub fn insert_url(pool: &Pool, url: &str, shortened: &str) {
let mut conn = pool.get_conn().unwrap();
conn.exec_drop(
r"
INSERT INTO `urls` (`url`, `shortened`) VALUES (:url, :shortened)
",
params! {
"url" => url,
"shortened" => shortened
},
)
.unwrap();
pub async fn retrieve_url (connection: &mut PooledConn, shortened: &str) -> RetrievedUrl {
let mut result = connection.exec_iter("SELECT url FROM urls WHERE shortened = :shortened", params! {
"shortened" => shortened
}).unwrap();
let row = result.next().unwrap();
let url = row.unwrap().get::<String, _>("url").unwrap();
RetrievedUrl {
url,
success: true
}
}
pub fn get_url(pool: &Pool, shortened: &str) -> Option<String> {
let mut conn = pool.get_conn().unwrap();
let result: Vec<String> = conn
.exec_map(
r"
SELECT `url` FROM `urls` WHERE `shortened` = :shortened
",
params! {
pub async fn submit_url (connection: &mut PooledConn, url: &str, shortened: &str) -> SubmittedUrl {
let row = connection.exec_iter("SELECT shortened FROM urls WHERE shortened = :shortened", params! {
"shortened" => shortened
}).unwrap().next();
if row.is_some() {
SubmittedUrl {
shortened: shortened.to_string(),
success: false
}
} else {
let row = connection.exec_iter("SELECT shortened FROM urls WHERE url = :url", params! {
"url" => url
}).unwrap().next();
if row.is_some() {
let shortened = row.unwrap().unwrap().get::<String, _>("shortened").unwrap();
SubmittedUrl {
shortened,
success: true
}
} else {
connection.exec_drop("INSERT INTO urls (url, shortened) VALUES (:url, :shortened)", params! {
"url" => url,
"shortened" => shortened
},
|url| url,
)
.unwrap();
result.into_iter().next()
}
pub fn get_shortened(pool: &Pool, url: &str) -> Option<String> {
let mut conn = pool.get_conn().unwrap();
let result: Vec<String> = conn
.exec_map(
r"
SELECT `shortened` FROM `urls` WHERE `url` = :url
",
params! {
"url" => url
},
|shortened| shortened,
)
.unwrap();
result.into_iter().next()
}
pub fn url_exists(pool: &Pool, url: &str) -> bool {
let mut conn = pool.get_conn().unwrap();
let result: Vec<String> = conn
.exec_map(
r"
SELECT `url` FROM `urls` WHERE `url` = :url
",
params! {
"url" => url
},
|url| url,
)
.unwrap();
result.len() > 0
}).unwrap();
SubmittedUrl {
shortened: shortened.to_string(),
success: true
}
}
}
}
pub fn shortened_exists(pool: &Pool, shortened: &str) -> bool {
let mut conn = pool.get_conn().unwrap();
let result: Vec<String> = conn
.exec_map(
r"
SELECT `shortened` FROM `urls` WHERE `shortened` = :shortened
",
params! {
"shortened" => shortened
},
|shortened| shortened,
)
.unwrap();
result.len() > 0
pub async fn count_urls (connection: &mut PooledConn) -> u64 {
let mut result = connection.exec_iter("SELECT COUNT(*) FROM urls", ()).unwrap();
let row = result.next().unwrap();
let count = row.unwrap().get::<u64, _>("COUNT(*)").unwrap();
count
}
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();
let row = result.next();
if row.is_some() {
true
} else {
false
}
}
pub fn count_urls(pool: &Pool) -> u64 {
let mut conn = pool.get_conn().unwrap();
let result: Vec<u64> = conn
.exec_map(
r"
SELECT COUNT(*) FROM `urls`
",
(),
|count| count,
)
.unwrap();
result.into_iter().next().unwrap()
}
@@ -1,29 +1,61 @@
use actix_files::NamedFile;
use std::path::PathBuf;
use actix_web::{web, HttpResponse, HttpRequest, Responder};
use actix_files::NamedFile;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::{database::{self, SubmittedUrl}, shortener::{self, base64}, url, templating};
#[derive(Deserialize, Serialize, Debug)]
pub struct Submission {
url: String
}
pub async fn file(req: HttpRequest) -> Result<NamedFile, actix_web::Error> {
let path_string = req.match_info().query("filename");
let path: PathBuf = if path_string != "" {
PathBuf::from(path_string)
} else {
PathBuf::from("index.html")
};
Ok(NamedFile::open(path)?)
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 hello(req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("Hello world!")
let path_string = path.into_inner();
println!("Accessing file {:?}", path_string);
Ok(NamedFile::open(PathBuf::from(format!("static/{}", path_string)))?)
} */
pub async fn index() -> Result<NamedFile, actix_web::Error> {
Ok(NamedFile::open("index.html")?)
}
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());
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()
}
)
);
}
}
HttpResponse::InternalServerError().body("An error occured while submitting your URL")
}
pub async fn submit_url(form: web::Form<Submission>) -> impl Responder {
println!("Received submission: {:?}", form);
HttpResponse::Ok().body(format!("Received submission: {:?}", form))
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();
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()
}
@@ -1,41 +1,58 @@
use actix_settings::{BasicSettings, ApplySettings};
use actix_web::{web, App, HttpServer, middleware::Condition, middleware::Logger, middleware::Compress};
use mysql::Pool;
pub mod database;
pub mod endpoints;
pub mod settings;
pub mod shortener;
pub mod url;
pub mod templating;
TODO:
* - make config accessible universally, or somehow transfer config data to how functions work
* - make database accessible universally
* - make shortened url generation
* - make shortened url redirect
* - clean up code
* - add HTML templates
* - implement other functions (pastebin maybe?)
*/
#[derive(Clone)]
pub struct AppData {
config: BasicSettings<settings::AppSettings>,
database: Pool
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Importing settings...");
let settings = settings::init();
HttpServer::new(move || {
let settings = settings.clone();
App::new()
.wrap(Condition::new(
settings.actix.enable_compression,
Compress::default(),
))
.wrap(Condition::new(
settings.actix.enable_log,
Logger::default(),
))
.app_data(web::Data::new(settings.clone()))
.route("/", web::get().to(endpoints::hello))
.route("/{filename:.*}", web::get().to(endpoints::file))
.route("/", web::post().to(endpoints::submit_url))
println!("Starting server...");
let server = HttpServer::new({
let app_data = AppData {
config: settings.clone(),
database: database::init(&settings).await
};
move || {
App::new()
.wrap(Condition::new(
settings.actix.enable_compression,
Compress::default(),
))
.wrap(Condition::new(
settings.actix.enable_log,
Logger::default(),
))
.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))
}
})
.bind(("127.0.0.1", 4000))?
.run()
.await
.apply_settings(&settings)
.run();
println!("Server started!");
server.await
}
@@ -1,9 +1,10 @@
use actix_settings::{ApplySettings as _, Settings, BasicSettings};
use actix_settings::{BasicSettings};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AppSettings {
pub database: DatabaseSettings
pub database: DatabaseSettings,
pub templating: TemplatingSettings
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -13,6 +14,12 @@
pub username: String,
pub password: String,
pub database: String
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TemplatingSettings {
pub enabled: bool,
pub domain: String
}
pub fn init () -> BasicSettings<AppSettings> {
@@ -1,0 +1,39 @@
const BASE64_CHARS: [char; 64] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z', '-', '_',
];
pub fn base64(num: u64) -> String {
let mut result = String::new();
let mut num = num;
while num > 0 {
let remainder = num % 64;
num = num / 64;
result.push(BASE64_CHARS[remainder as usize]);
}
result
}
pub fn random_base64(num: u64) -> String {
let mut result = String::new();
let mut num = num;
let mut length = 4;
while num > 0 {
let remainder = num % 64;
num = num / 64;
result.push(BASE64_CHARS[remainder as usize]);
if num > 0 {
length += 1;
}
}
while result.len() < length {
result.push(BASE64_CHARS[rand::random::<usize>() % 64]);
}
result
} */
@@ -1,0 +1,22 @@
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
pub struct TemplateSchema {
pub url: String,
pub shortened: String,
pub domain: String,
pub count: String
}
pub fn read_and_apply_templates(path: PathBuf, schema: TemplateSchema) -> String {
let mut file = File::open(path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
contents
.replace("{{url}}", &schema.url)
.replace("{{shortened}}", &schema.shortened)
.replace("{{domain}}", &schema.domain)
.replace("{{count}}", &schema.count)
}
@@ -1,0 +1,8 @@
pub fn format_url(url: String) -> String {
if url.contains("://") {
url
} else {
format!("http://{}", url)
}
}
@@ -1,0 +1,5 @@
body {
font-family: sans-serif;
background-color: #f0f0f0;
text-align: center;
}
@@ -1,1 +1,1 @@
{"rustc_fingerprint":13098977256995595289,"outputs":{"15697416045686424142":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\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":""},"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":{"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":{}}
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/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