From 8932cea4c937a45fa130e22aeb950b15b1251dea Mon Sep 17 00:00:00 2001 From: Tom Andrade Date: Sun, 10 Jan 2021 14:42:13 +0100 Subject: [PATCH] modulerize short functions --- shortie/go.mod | 11 ++ shortie/go.sum | 27 ++++ shortie/shortie.go | 307 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 shortie/go.mod create mode 100644 shortie/go.sum create mode 100644 shortie/shortie.go diff --git a/shortie/go.mod b/shortie/go.mod new file mode 100644 index 0000000..ca30784 --- /dev/null +++ b/shortie/go.mod @@ -0,0 +1,11 @@ +module github.com/wolviecb/short/shortie + +go 1.15 + +require ( + github.com/andybalholm/brotli v1.0.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef + github.com/klauspost/compress v1.11.6 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/valyala/fasthttp v1.19.0 +) diff --git a/shortie/go.sum b/shortie/go.sum new file mode 100644 index 0000000..ade383c --- /dev/null +++ b/shortie/go.sum @@ -0,0 +1,27 @@ +github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= +github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.6 h1:EgWPCW6O3n1D5n99Zq3xXBt9uCwRGvpwGOusOLNBRSQ= +github.com/klauspost/compress v1.11.6/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.19.0 h1:PfTS4PeH3xDr3WomrDS2ID8lU2GskK1xS3YG6gIpibU= +github.com/valyala/fasthttp v1.19.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/shortie/shortie.go b/shortie/shortie.go new file mode 100644 index 0000000..5088e27 --- /dev/null +++ b/shortie/shortie.go @@ -0,0 +1,307 @@ +package shortie + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net/http" + "net/url" + "regexp" + "strings" + "text/template" + "time" + + "github.com/asaskevich/govalidator" + "github.com/patrickmn/go-cache" + "github.com/valyala/fasthttp" +) + +type body struct { + FullHeader bool + IsGhost bool + HasForm bool + IsLink bool + H1 string + H3 string + Line1 string + Line2 string +} + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // Base strings for randStringBytesMaskImprSrc + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = Src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +// redirect receives a key searches the kv database for it and if +// found returns the value, or a error if not found +func redirect(k string) (string, error) { + rgx, _ := regexp.Compile("[a-zA-Z0-9]+") + key := rgx.FindString(k) + key, status := get(key) + if !status { + return "", fmt.Errorf("Not Found") + } + u, _ := url.Parse(key) + if u.Scheme == "" { + u.Scheme = "https" + } + return u.String(), nil +} + +// shortener receive a url, validates the url, generate a random suffix string +// of urlSize size, checks if the suffix string is ensure on the kv database +// and then writes the kv pair (suffix, url) to the database, returning the suffix +func shortener(u []byte, s int) (string, error) { + var su string + us := string(u) + if !govalidator.IsURL(string(us)) { + return su, fmt.Errorf("Bad Request") + } + pu, _ := url.Parse(us) + + for { + su = randStringBytesMaskImprSrc(s) + _, status := get(su) + if !status { + break + } + } + + set(pu.String(), su) + return su, nil +} + +// dumpDbToFile dumps the kv pairs from the in memory database to file +func dumpDbTOFile() (int, error) { + i := Pool.Items() + dumpObj, _ := json.Marshal(i) + return len(i), ioutil.WriteFile(DumpFile, dumpObj, 0644) +} + +// loadFromFile loads kv pairs from the dumpFile json to the in memory database +func loadFromFile() (int, error) { + dumpObj := make(map[string]cache.Item) + jsonFile, err := ioutil.ReadFile(DumpFile) + if err != nil { + return 0, err + } + + err = json.Unmarshal([]byte(jsonFile), &dumpObj) + if err != nil { + return 0, err + } + + Pool = cache.NewFrom(Exp, Cleanup, dumpObj) + return len(dumpObj), err +} + +// itemsFromPost loads kv pairs from a json POST to the in memory database +func loadFromJSON(j []byte) (int, error) { + dumpObj := make(map[string]cache.Item) + err := json.Unmarshal(j, &dumpObj) + if err != nil { + return 0, err + } + + Pool = cache.NewFrom(Exp, Cleanup, dumpObj) + return len(dumpObj), nil +} + +func internalError(msg string, err error) body { + log.Println(err) + return body{ + FullHeader: true, + IsGhost: true, + HasForm: true, + H1: "500", + H3: msg, + Line1: "Boo, the ghost is broken :(", + Line2: "His last words where: " + err.Error(), + } +} + +// IndexHandler return a fasthttp.RequestHandler function that genetares the index page +func IndexHandler(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, body{ + HasForm: true, + Line1: "Welcome to Short, the simple URL shortener,", + Line2: "Type an URL below to shorten it", + }) + } +} + +// Short return a fasthttp.RequestHandler function that genetares the shortener page +func Short(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + suf, err := shortener(ctx.FormValue("url"), URLSize) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + t.Execute(ctx, body{ + FullHeader: true, + IsGhost: true, + HasForm: true, + H1: "400", + H3: err.Error(), + Line1: "Boo, looks like this ghost stole this page!", + Line2: "But you can type an URL below to shorten it", + }) + return + } + ru, _ := url.Parse(fmt.Sprintf("%s://%s:%v/%s%s", Proto, Domain, Port, Path, suf)) + t.Execute(ctx, body{ + IsLink: true, + Line1: ru.String(), + }) + } +} + +// Redir return a fasthttp.RequestHandler function that genetares the shortener redirect page +func Redir(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + key := ctx.UserValue("key") + if Path != "" { + key = strings.Replace(key.(string), Path, "", 1) + } + u, err := redirect(key.(string)) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusNotFound) + t.Execute(ctx, body{ + FullHeader: true, + IsGhost: true, + HasForm: true, + H1: "404", + H3: err.Error(), + Line1: "Boo, looks like this ghost stole this page!", + Line2: "But you can type an URL below to shorten it", + }) + return + } + ctx.Redirect(u, fasthttp.StatusFound) + } +} + +// ToFile return a fasthttp.RequestHandler function that dumps the contents of the +// KV db to the DumpFile file +func ToFile(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + i, err := dumpDbTOFile() + if err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, internalError("Failed to dump kv DB to file", err)) + return + } + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, body{Line1: fmt.Sprintf("Exported %v items to %v", i, DumpFile)}) + } +} + +// FromFile return a fasthttp.RequestHandler function that loads the content of the DumpFile into the +// KV db +func FromFile(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + i, err := loadFromFile() + if err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, internalError("Error loading DB from file", err)) + return + } + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, body{Line1: fmt.Sprintf("Imported %v items to the DB", i)}) + } +} + +// Dump return a fasthttp.RequestHandler function that dumps the contents of the +// KV db to the ctx handler +func Dump(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + dumpObj, err := json.Marshal(Pool.Items()) + if err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, internalError("Unable to dump key value db: ", err)) + return + } + fmt.Fprintf(ctx, "%s", dumpObj) + } +} + +// FromPost return a fasthttp.RequestHandler function that loads the content of the JSON POST into the +// KV db +func FromPost(t *template.Template) func(ctx *fasthttp.RequestCtx) { + return func(ctx *fasthttp.RequestCtx) { + i, err := loadFromJSON(ctx.PostBody()) + if err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, internalError("Error loading DB", err)) + return + } + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, body{Line1: fmt.Sprintf("Imported %v items to the DB", i)}) + } +}