/
short.go
153 lines (118 loc) · 3.59 KB
/
short.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*
Package short is a simple web based api. It will produce shortest hash possible for a url. It will use longer hashes only in case of collision.
For example for https://example.org is redis is empty it will return /5
short has has two entries:
/post which accepts a url and returns a short version of it.
{"url": "https://example.org"}
/TOKEN which based on request headers will return a json reply or will redirect the agent to the original url.
short uses redis for its backned storage. You can set the redis url in command or in code.
$ REDISURL="redis://:PASSWORD@pub-zone.redislabs.com:12919/0"
Example of using this package is available in example directory.
*/
package short
import (
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"github.com/asaskevich/govalidator"
"github.com/garyburd/redigo/redis"
)
//Site defines main attributes of shortener service.
type Site struct {
// Host will define the prefix to be used for generated short token for redirects.
Host string
// RedisURL defines the redis instance to use. If it is empty REDISURL environment variable will be used.
RedisURL string
}
func (site Site) redisURL() string {
if site.RedisURL != "" {
return site.RedisURL
}
return os.Getenv("REDISURL")
}
func (site Site) redisdb() redis.Conn {
redisdb, err := redis.DialURL(site.redisURL())
if err != nil {
panic(err)
}
return redisdb
}
func (site Site) saveShort(url string) (shortest string, err error) {
if !govalidator.IsURL(url) {
return "", errors.New("invalid url")
}
redisdb := site.redisdb()
defer redisdb.Close()
hash := fmt.Sprintf("%x", md5.Sum([]byte(url)))
similar, _ := redis.String(redisdb.Do("GET", "i:"+hash))
if similar != "" {
return site.Host + similar, nil
}
for hashShortestLen := 1; hashShortestLen <= 32; hashShortestLen++ {
s, _ := redisdb.Do("GET", hash[0:hashShortestLen])
if s == nil {
shortest = hash[0:hashShortestLen]
break
}
}
if shortest == "" {
return "", errors.New("url shortening failed")
}
redisdb.Do("SET", shortest, url)
redisdb.Do("SET", "i:"+hash, shortest)
return site.Host + shortest, nil
}
type shortRequest struct {
URL string
}
/*
Post Only accepts json data in the following format
{"url": "https://example.org"}
return results is always in the following format:
{"short":"https://short.me/ab", "error":""}
If there was any error short will be empty and error message will show.
*/
func (site Site) Post(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
decoder := json.NewDecoder(r.Body)
var t shortRequest
decoder.Decode(&t)
shortURL, err := site.saveShort(t.URL)
errMsg := ""
if err != nil {
errMsg = err.Error()
}
if r.Header.Get("Content-Type") == "application/json" {
fmt.Fprintf(w, "{\"short\":\"%s\", \"error\":\"%s\"}", shortURL, errMsg)
return
}
}
/*
Redirect will either return a json value in the following format:
{"url":"https://example.org", "error":""}
or it will do a 301, http redirect. Return action is based on headers.
*/
func (site Site) Redirect(w http.ResponseWriter, r *http.Request) {
redisdb := site.redisdb()
defer redisdb.Close()
t, _ := redis.String(redisdb.Do("GET", r.URL.Path[1:]))
u, _ := url.Parse(t)
errMsg := ""
if u.String() == "" {
errMsg = "not found"
}
if r.Header.Get("Content-Type") == "application/json" {
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprintf(w, "{\"url\":\"%s\", \"error\":\"%s\"}", u.String(), errMsg)
return
}
if errMsg != "" {
fmt.Fprintf(w, errMsg)
return
}
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
}