Redis URL shortner in Go hosted on heroku

Hope you have read my previous article on testing redis on heroku & App engine .

[wpi_designer_button text=’Download’ link=’https://github.com/arjunsk/go_url_shortener’ style_id=’48’ icon=’github’ target=’_blank’]

OR

[wpi_designer_button text=’Preview’ link=’https://shortify-go.herokuapp.com/’ style_id=’48’ icon=’cloud’ target=’_blank’]

Features Included:

1. Used online redis provided by redislabs

2. Proper folder structure ( I believe so)

3. Shows notifications in go

4. Added support for clipboard.js

I hope you have covered the basics of golang. Infact this youtube channel is highly recommended.

Get Started:

Directory structure: I wanted to in-cooperate MVC architecture. I didn’t use Go Web Frameworks as i wanted to learn the in and outs of the project. I hope this project structure is correct. ( Correct me if I am wrong)

The app has got 3 pages, namely – app, about, 404

MVC can be identified as

Model :- They are kind of data structures that deal with the database. It includes the db functions.

View :- They are those UI elements that are shown on screen.

Controllers :- They are related the functions specific to a page.

Pretty much similar to Ionic right ?

Public :- It is where you place all the public files accessible to viewers.

1. Views

We can template common layout attributes and include them in every page.

header.html

{{define "header"}}
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>{{.Title}}</title> <!--Note this part -->

	<link  href="/public/css/main.css"    type="text/css"     rel="stylesheet">
	...

</head>
<body>
{{end}}

footer.html

{{define "footer"}}

    /public/js/jquery.js

    /public/js/clipboard.min.js
    /public/js/notify.min.js

    /public/js/main.js

  </body>
</html>
{{end}}

navigation.html

{{define "navigation"}}
<div id="nav">
    <ul>
        <li>
            <a href="/about">About</a>
        </li>
        <li>
            <a href="/">App</a>
        </li>
    </ul>
</div>
{{end}}

app.html

{{template "header" .}} <!-- By the . we are passing page data to the header.html also -->
{{template "navigation" .}}

	<div class="wrap">
		
		.... 
		
		<form action="" class="container" method="POST">
		  <input value="{{.Long_url}}" name="long_url" class="urlTerm" type="text"
				 placeholder="Long URL, eg :- http://www.google.com/... ">
		  ....
		</form>
	
		<div class="container">	
			<input  id="shortenURL" readonly type="text" class="copyTerm" value="{{.Short_url}}" placeholder="Short URL" />
			
			<!--data-clipboard-target="#shortenURL" will be used by clipboard.js -->
			<button data-clipboard-target="#shortenURL" class="copyButton">
				<i class="fa fa-clipboard" aria-hidden="true"></i>
			</button>
		</div>

		
	</div>


{{template "footer" .}}

2. public / js / main.js

var clipboard = new Clipboard('.copyButton'); // initializing Clipboard object

//Handles on success of copy function
clipboard.on('success', function(e) {
    showInfoMessage("Copied!");
});


// Notification is handled using jQuery
// These functions will be called from go. Similar to echo "<scripts>...</scripts>" in PHP
function showSuccessMessage(message){
    $.notify(message, "success");
}

function showInfoMessage(message){
    $.notify(message, "info");
}

function showWarningMessage(message){
    $.notify(message,"warn");
}

function showErrorMessage(message) {
    $.notify(message, "error");
}

3. Server.go

package main

import(
	"html/template"
	"net/http"
	"os"
	"go_shortify_web_app_heroku/models"
	"go_shortify_web_app_heroku/controllers"
)

// This is passed to every page to set the page details
type pageData struct {
	Title string
	Short_url string
	Long_url string
}

var tpl *template.Template 
var page_data pageData
var host_name string = "https://app_id.herokuapp.com"

// used for showing notification popup using js
var notify_type int
var notify_msg string


func init() {
	tpl = template.Must(template.ParseGlob("views/*.html")) // Parses only html files from view folder
        models.Redis_db_init() // initialise the db connection
}

func main(){
	// This is core Handler that server all the files in the public folder with appropraite content type.
	// Else for eg :- png will be loaded as document type, and hinder page load
	http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public")))) 
	
	//Handlers to handle url regex 
	http.HandleFunc("/about",AboutHandler)
	http.HandleFunc("/404",ErrorHandler)
	http.HandleFunc("/", HomeHandler)
	
	// We are lisening to port that is set in the heroku environment using os.Getenv
	http.ListenAndServe(":"+os.Getenv("PORT"), nil) 
}

func ErrorHandler(w http.ResponseWriter, r *http.Request) {
	page_data = pageData{Title:"404"} // setting page title
	tpl.ExecuteTemplate(w, "error.html",page_data) // opens error.html page
}

func AboutHandler(w http.ResponseWriter, r *http.Request) {
	page_data = pageData{Title:"About"}
	tpl.ExecuteTemplate(w, "about.html",page_data)
}

func HomeHandler(w http.ResponseWriter, r *http.Request) {
	// for eg:- app.heroku.com/1234 is the URL 
	//then shortCode = 1234 ( we are excluding the beginging / by using [1:] )
	
	shortCode := r.URL.Path[1:] 
	
	// The variables are static. So we need to re/initialize it every time
	page_data = pageData{Title:"Shortify",Short_url:"",Long_url:""}
	
	notify_type = 0 // notify-off : read the controllers.ShowNotifications

	if len(shortCode) != 0 {  // meaning we have a shortcode to check in the database
 
		if err != nil {
			redirect_url = host_name + "/404"
		}

		controllers.RedirectTo(w,r,redirect_url) // redirect to long url

		return

	}else if r.Method == "POST" { // Handles post data. If post url is correct, then save it to db

		long_url := r.PostFormValue("long_url") //get form data by html form id

		err := controllers.ValidateURL(long_url)// validate url

		if err != nil {
			notify_type,notify_msg  = 4, "Invalid URL." // we can set two variables in one line
		}else{
			short_url := host_name + "/" + models.Redis_db_save(long_url)
			page_data = pageData{Title:"Shortify", Short_url:short_url ,Long_url:long_url} // setting page data 

			notify_type,notify_msg  = 1, "URL shortified."
		}
	}



	tpl.ExecuteTemplate(w, "app.html",page_data) // page datas are set in view/.html at appropraite places
	
	controllers.ShowNotifications(w,notify_type,notify_msg) // run this after loading the page

}


4. models / redis_db.go

//Models are data structures for representing database concepts.
package models

import (
	"github.com/garyburd/redigo/redis"
	"fmt"
	"go_shortify_web_app_heroku/controllers"
)


var redisPool *redis.Pool // creating redis pool enables us to reuse redigo connections
// http://stackoverflow.com/questions/24387350/re-using-redigo-connection-instead-of-recreating-it-every-time

func Redis_db_init(){
	redisAddr :=  "redis-XXXXX.c9.us-east-1-2.ec2.cloud.redislabs.com:XXXXX"
	redisPool = &redis.Pool{
		Dial: func() (redis.Conn, error) {
			conn, err := redis.Dial("tcp", redisAddr)
			return conn, err
		},
	}
}

func Redis_db_save(long_url string) (string) {

	// Reusing redisConn
	redisConn := redisPool.Get()
	defer redisConn.Close()

	new_short_code := controllers.Hash(long_url) // Hash the long url , ie number code
	redisConn.Do("SET", new_short_code, long_url) // Save the has along with long url into db
	return fmt.Sprint(new_short_code) // .Sprint => String print converts long to string
}

func Redis_db_get(shortCode string) (string,error) {
	redisConn := redisPool.Get()
	defer redisConn.Close()

	redirect_url, err := redis.String(redisConn.Do("GET", shortCode))
	return redirect_url,err
}

5. controllers / app.go

package controllers

import (
	"hash/fnv"
	"net/http"
	"io"
	"fmt"
	"net/url"
)

// Hash the long url into shortcode
func Hash(s string) uint32 {
	h := fnv.New32a()
	h.Write([]byte(s))
	return h.Sum32()
}

// Use statusFound. It mainly deal with setting HTTP response data 
func RedirectTo(w http.ResponseWriter, r *http.Request, urlStr string){
	http.Redirect(w, r, urlStr, http.StatusFound)
}

// Checks if it is a valid url, ie http://google.com
func ValidateURL(long_url string) (error){
	_,err := url.ParseRequestURI(long_url)
	return err
}

// Setting notify_script based on notify_type
// no-notify = 0 which is default, that means exit the function .
func ShowNotifications(w io.Writer,notify_type int,msg string)  {
	var notify_script string = ""
	switch notify_type {
	case 1:
		notify_script = "<script>showSuccessMessage(\""+msg+"\")</script>"
	case 2:
		notify_script = "<script>showInfoMessage(\""+msg+"\")</script>"
	case 3:
		notify_script = "<script>showWarningMessage(\""+msg+"\")</script>"
	case 4:
		notify_script = "<script>showErrorMessage(\""+msg+"\")</script>"
	default:
		return
	}

	fmt.Fprint(w,notify_script) // Fprint => File Print ie it writes this into the html files, once loaded.
}


I hope i explained something valuable with this tutorial. If i am wrong, do correct me.

To deploy this app on heroku or app engine, follow my previous article.

Happy coding.

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s