From e874e4e47390c8da2768ed72f4556264b9acd882 Mon Sep 17 00:00:00 2001
From: Maurice <mauricegolverdingen@gmail.com>
Date: Wed, 2 Nov 2022 18:49:43 +0100
Subject: [PATCH] Added URL shortener

---
 README.md   |   2 +
 about.html  |  13 +++--
 src/main.rs | 135 +++++++++++++++++++++++++++++++++++-----------------
 3 files changed, 104 insertions(+), 46 deletions(-)

diff --git a/README.md b/README.md
index 8b2feb9..387fcfc 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,8 @@ Welcome at PASTABBLE!
 ## About
 Pastable is a lightweight and fast pastebin alternative and URL-shortener made by Plabble and written in Rust.
 
+For usage, see `about.html`
+
 ## Environment variables
 | Variable | Default |
 | -------- | ------- |
diff --git a/about.html b/about.html
index 02a6dbd..82bda4b 100644
--- a/about.html
+++ b/about.html
@@ -17,12 +17,19 @@
         Get /: Show this page
         Get /{id}: Get paste by ID and return in plain text
         Post /: Create new paste (plain text raw body or form parameter 'content') and return random generated key 
-        Post /{id}: Create new paste (plain text raw body) and return requested key if available, else random generated key 
+        Post /{id}: Create new paste (plain text raw body) and return requested key if available, else random generated key
+        
+        Get /to/{id}: Navigate to shortcut link (browser redirect)
+        Post /to: Create new link (plain text raw body or form parameter 'link') and return random generated key
+        Post /to/{id}: Create new link (plain text raw body) and return requested key if available, else random generated key
       
       FROM TERMINAL
-        Pipe your input into: curl -F 'content=&lt;-' paste.plabble.org -w "\n"
-        Or post it as raw data like: curl paste.plabble.org -H "Content-Type: text/plain" -d @- -w "\n"
+        Pipe your input into: curl -F 'content=&lt;-' paste.plabble.org -w '\n'
+        Or post it as raw data like: curl paste.plabble.org -H 'Content-Type: text/plain' -d @- -w '\n'
         You can create an alias for this! To get the result: curl paste.plabble.org/YOURID
+
+        For links:
+        Pipe your input into: curl -F 'link=&lt;-' paste.plabble.org/to -w '\n'
     </pre>
   </body>
 </html>
diff --git a/src/main.rs b/src/main.rs
index 79142a3..7e148b2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,15 @@
 use std::{env, fs};
 
 use chrono::Utc;
-use kv::{Config, Store, Json};
+use kv::{Config, Store, Json, Bucket, Value};
 use paste::Paste;
 use rand::{Rng, distributions::Alphanumeric};
-use rouille::{Response, router, try_or_400, post_input};
+use rouille::{Response, router, try_or_400, post_input, Request};
 
 mod paste;
 
 // Generate random key function
-fn new_key() -> String {
+fn random_string() -> String {
     rand::thread_rng()
         .sample_iter(&Alphanumeric)
         .take(5)
@@ -17,6 +17,18 @@ fn new_key() -> String {
         .collect()
 }
 
+// Generate key using provided key or generate new one
+fn generate_key<T : Value>(key: Option<String>, store: &Bucket<String, T>) -> String {
+    let mut new_key = key.unwrap_or(random_string());
+
+    loop {
+        if !store.contains(&new_key).unwrap() {
+            break new_key;
+        }
+        new_key = random_string();
+    }
+}
+
 fn main() {
     // Get settings
     let port: u16 = env::var("PORT").unwrap_or(String::from("8080")).parse().expect("Failed to parse PORT variable");
@@ -26,9 +38,13 @@ fn main() {
     let store = Store::new(config).expect("Failed to initialize database store");
     let about = fs::read_to_string("./about.html").expect("about.html not found!!");
 
-    // Get pastes bucket (default)
+    // Get pastes bucket
     let pastes = store.bucket::<String, Json<Paste>>(Some("pastes"))
-        .expect("Failed to open default bucket");
+        .expect("Failed to open pastes bucket");
+
+    // Get links bucket
+    let links = store.bucket::<String, String>(Some("links"))
+        .expect("Failed to open links bucket");
     
     // Start server
     println!("Started PASTABBLE server on port {}", port);
@@ -37,9 +53,22 @@ fn main() {
             (GET) (/) => {
                 Response::html(&about)
             },
+            (GET) (/to/{id: String}) => {
+                match links.get(&id).expect("Failed to access links DB") {
+                    Some(lnk) => {
+                        Response::redirect_301(lnk)
+                    },
+                    None => {
+                        Response::text(format!("Redirect with ID '{}' not found", &id))
+                            .with_status_code(404)
+                    }
+                }
+            },
+            (POST) (/to/{id: String}) => { register_link(&links, req, Some(id)) },
+            (POST) (/to) => { register_link(&links, req, None) },
             (GET) (/{id: String}) => {
                 // Get note from database, if exists
-                match pastes.get(&id).expect("Failed to access DB") {
+                match pastes.get(&id).expect("Failed to access pastes DB") {
                     Some(paste) => {
                         Response::text(paste.0.content)
                     },
@@ -49,45 +78,65 @@ fn main() {
                     }
                 }
             },
-            (POST) (/{id: String}) => {
-                // Use provided key or generate new one
-                let key = if !id.is_empty() && !pastes.contains(&id).unwrap() {
-                    id
-                } else { 
-                    loop {
-                        let new_key = new_key();
-                        if !pastes.contains(&new_key).expect("Could not access store") {
-                            break new_key;
-                        }
-                    }
-                };
-
-                // Try read body from form data, and if not present from request body
-                let body = match post_input!(&req, {
-                    content: String
-                }) {
-                    Ok(f) => {
-                        f.content
-                    },
-                    Err(_) => {
-                        try_or_400!(rouille::input::plain_text_body(&req))
-                    }
-                };
-
-                // Create and save new note
-                let paste = Json(Paste { 
-                    content: body,
-                    expires: None, 
-                    language: None, 
-                    created: Utc::now()
-                });
-
-                pastes.set(&key, &paste).expect("Failed to save note");
-
-                // Return key
-                Response::text(key)
+            (POST) (/{id: String}) => { 
+                let id = if id.is_empty() { None } else { Some(id) };
+                register_paste(&pastes, req, id) 
             },
             _ => Response::empty_404()
         )
     });
+}
+
+// Register paste handler
+fn register_paste(pastes: &Bucket<String, Json<Paste>>, req: &Request, id: Option<String>) -> Response {
+    
+    // Try read body from form data, and if not present from request body
+    let body = match post_input!(req, {
+        content: String
+    }) {
+        Ok(f) => {
+            f.content
+        },
+        Err(_) => {
+            try_or_400!(rouille::input::plain_text_body(req))
+        }
+    };
+
+    // Create and save new note
+    let paste = Json(Paste { 
+        content: body,
+        expires: None, 
+        language: None, 
+        created: Utc::now()
+    });
+
+    let key = generate_key(id, &pastes);
+    pastes.set(&key, &paste).expect("Failed to save note");
+    pastes.flush().expect("Failed to svae paste to database");
+
+    // Return key
+    Response::text(key)
+}
+
+// Register link route handler
+fn register_link(links: &Bucket<String,String>, req: &Request, id: Option<String>) -> Response {
+
+    // Try read body from form data, and if not present from request body
+    let link = match post_input!(req, {
+        link: String
+    }) {
+        Ok(f) => {
+            f.link
+        },
+        Err(_) => {
+            try_or_400!(rouille::input::plain_text_body(req))
+        }
+    }.trim().to_string();
+
+    let key = generate_key(id, &links);
+    links.set(&key, &link).expect("Failed to save link");
+    links.flush().expect("Failed to save link to database");
+
+    // Return key
+    Response::text(key)
 }
\ No newline at end of file