diff --git a/go.mod b/go.mod index 765d452..3714698 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/fasthttp/router v1.4.9 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/valyala/fasthttp v1.36.0 - github.com/wolviecb/short/shortie v0.0.0-20220412102501-705312b4dffe + github.com/valyala/fasthttp v1.37.0 + internal/shortie v0.0.0-00010101000000-000000000000 ) +replace internal/shortie => ./internal/shortie + require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/klauspost/compress v1.15.4 // indirect diff --git a/go.sum b/go.sum index b95a36f..2842fa2 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,11 @@ -github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/fasthttp/router v1.4.8 h1:4zj4sAzXibjA6ZW19MdMe3GaYD1SM+TXrMLzHcVMBOI= -github.com/fasthttp/router v1.4.8/go.mod h1:UUtJdXFYlqYRQ32EAtWOvNYIZ1XfyC5JJIknWai6foI= github.com/fasthttp/router v1.4.9 h1:8s1HEqP+GvsC2B8vPdLAPHJegs4s28z7UsraPuHM1K8= github.com/fasthttp/router v1.4.9/go.mod h1:oWPrQCi9QOrzxKC+rZuliS1+JhYj2bpR01J6T8vUDUQ= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.6/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= -github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ= github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -23,27 +14,13 @@ github.com/savsgio/gotils v0.0.0-20220401102855-e56b59f40436 h1:sfTahD3f2BSjx9U3 github.com/savsgio/gotils v0.0.0-20220401102855-e56b59f40436/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= 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/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= -github.com/valyala/fasthttp v1.35.0 h1:wwkR8mZn2NbigFsaw2Zj5r+xkmzjbrA/lyTmiSlal/Y= -github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= -github.com/valyala/fasthttp v1.36.0 h1:NhqfO/cB7Ajn1czkKnWkMHyPYr5nyND14ZGPk23g0/c= github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE= +github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/wolviecb/short/shortie v0.0.0-20210110145449-4edfb0344225 h1:BB+2AaTpi+jtEJaNTmoM12ddkgAwMFgTtNXnVWJMtvY= -github.com/wolviecb/short/shortie v0.0.0-20210110145449-4edfb0344225/go.mod h1:MP++yL+vt6bdR5ResHARM9XzrdxEzqYPkyODGTsjFJs= -github.com/wolviecb/short/shortie v0.0.0-20220412102501-705312b4dffe h1:3GiAtzvN0/Xig4LOCI5t6MNDI9CY0YQnNyktrXYF3yM= -github.com/wolviecb/short/shortie v0.0.0-20220412102501-705312b4dffe/go.mod h1:MP++yL+vt6bdR5ResHARM9XzrdxEzqYPkyODGTsjFJs= -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/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -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/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -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/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -51,8 +28,6 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/shortie/go.mod b/internal/shortie/go.mod similarity index 79% rename from shortie/go.mod rename to internal/shortie/go.mod index 3a028be..c970af8 100644 --- a/shortie/go.mod +++ b/internal/shortie/go.mod @@ -1,11 +1,11 @@ -module github.com/wolviecb/short/shortie +module shortie go 1.18 require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/valyala/fasthttp v1.36.0 + github.com/valyala/fasthttp v1.37.0 ) require ( diff --git a/shortie/go.sum b/internal/shortie/go.sum similarity index 94% rename from shortie/go.sum rename to internal/shortie/go.sum index a004854..cd69a54 100644 --- a/shortie/go.sum +++ b/internal/shortie/go.sum @@ -9,8 +9,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR 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.36.0 h1:NhqfO/cB7Ajn1czkKnWkMHyPYr5nyND14ZGPk23g0/c= -github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.37.0 h1:7WHCyI7EAkQMVmrfBhWTCOaeROb1aCBiTopx63LkMbE= +github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/shortie/shortie.go b/internal/shortie/shortie.go similarity index 90% rename from shortie/shortie.go rename to internal/shortie/shortie.go index 03779a4..c2bcc31 100644 --- a/shortie/shortie.go +++ b/internal/shortie/shortie.go @@ -4,11 +4,11 @@ import ( "encoding/json" "fmt" "html/template" - "io/ioutil" "log" "math/rand" "net/http" "net/url" + "os" "regexp" "strings" "time" @@ -57,6 +57,9 @@ var ( Path string = "" // DumpFile is the file to dump URL data DumpFile string = "urls.json" + // Error Definitions + ErrBadRequest = fmt.Errorf("bad request") + ErrNotFound = fmt.Errorf("not found") ) // get executes the GET command @@ -98,7 +101,7 @@ func redirect(k string) (string, error) { key := rgx.FindString(k) key, status := get(key) if !status { - return "", fmt.Errorf("Not Found") + return "", ErrNotFound } u, _ := url.Parse(key) if u.Scheme == "" { @@ -110,13 +113,12 @@ func redirect(k string) (string, error) { // 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) { +func shortener(u string, s int) (string, error) { var su string - us := string(u) - if !govalidator.IsURL(string(us)) { - return su, fmt.Errorf("Bad Request") + if !govalidator.IsURL(string(u)) { + return su, ErrBadRequest } - pu, _ := url.Parse(us) + pu, _ := url.Parse(u) for { su = randStringBytesMaskImprSrc(s) @@ -131,16 +133,19 @@ func shortener(u []byte, s int) (string, error) { } // dumpDbToFile dumps the kv pairs from the in memory database to file -func dumpDbTOFile() (int, error) { +func dumpDbTOFile(f *os.File) (int, error) { i := Pool.Items() dumpObj, _ := json.Marshal(i) - return len(i), ioutil.WriteFile(DumpFile, dumpObj, 0644) + if _, err := f.Write(dumpObj); err != nil { + return len(i), err + } + return len(i), nil } // 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) + jsonFile, err := os.ReadFile(DumpFile) if err != nil { return 0, err } @@ -154,7 +159,7 @@ func loadFromFile() (int, error) { return len(dumpObj), err } -// itemsFromPost loads kv pairs from a json POST to the in memory database +// loadFromJSON loads kv pairs from a json to the in memory database func loadFromJSON(j []byte) (int, error) { dumpObj := make(map[string]cache.Item) err := json.Unmarshal(j, &dumpObj) @@ -195,7 +200,7 @@ func IndexHandler(t *template.Template) func(ctx *fasthttp.RequestCtx) { 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) + suf, err := shortener(string(ctx.FormValue("url")), URLSize) if err != nil { ctx.SetStatusCode(fasthttp.StatusBadRequest) t.Execute(ctx, body{ @@ -247,7 +252,14 @@ func Redir(t *template.Template) func(ctx *fasthttp.RequestCtx) { // KV db to the DumpFile file func ToFile(t *template.Template) func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) { - i, err := dumpDbTOFile() + f, err := os.Create(DumpFile) + if err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) + t.Execute(ctx, internalError("Failed to create DB dump file", err)) + return + } + i, err := dumpDbTOFile(f) if err != nil { ctx.SetStatusCode(http.StatusInternalServerError) ctx.Response.Header.SetCanonical([]byte("Content-Type"), []byte("text/html")) diff --git a/internal/shortie/shortie_test.go b/internal/shortie/shortie_test.go new file mode 100644 index 0000000..c2b106d --- /dev/null +++ b/internal/shortie/shortie_test.go @@ -0,0 +1,246 @@ +package shortie + +import ( + "errors" + "fmt" + "os" + "reflect" + "testing" + + "github.com/patrickmn/go-cache" +) + +var ( + store = map[string]cache.Item{"google": {Object: "https://google.com"}} +) + +func TestGet(t *testing.T) { + Pool = cache.NewFrom(Exp, Cleanup, store) + tt := []struct { + name string + i string + v string + f bool + }{ + { + "Missing", + "test", + "", + false, + }, + { + "Found", + "google", + "https://google.com", + true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + v, f := get(tc.i) + if !reflect.DeepEqual(v, tc.v) { + t.Fatalf("expected: %v, got %v", tc.v, v) + } + if !reflect.DeepEqual(f, tc.f) { + t.Fatalf("expected: %v, got %v", tc.f, f) + } + }) + } + Pool.Flush() +} + +func TestSet(t *testing.T) { + Pool = cache.New(Exp, Cleanup) + tt := []struct { + name string + k string + v string + f bool + }{ + { + "Set", + "Key", + "Value", + true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + set(tc.v, tc.k) + v, err := get(tc.k) + if !reflect.DeepEqual(tc.v, v) { + t.Fatalf("expected: %v, got %v", tc.v, v) + } + if !reflect.DeepEqual(err, true) { + t.Fatalf("Key %s not found", tc.k) + } + }) + } + Pool.Flush() +} + +func TestRandStringBytesMaskImprSrc(t *testing.T) { + tt := []struct { + name string + i int + }{ + { + "10", + 10, + }, + { + "1", + 1, + }, + { + "100", + 100, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + s := randStringBytesMaskImprSrc(tc.i) + if len(s) != tc.i { + t.Fatalf("Bad Length, expecting %v got %v", tc.i, len(s)) + } + }) + } +} + +func benchmarkRandStringBytesMasImprSrc(i int, b *testing.B) { + for n := 0; n < b.N; n++ { + randStringBytesMaskImprSrc(i) + } +} + +func BenchmarkRandStringBytesMasImprSrc10(b *testing.B) { + benchmarkRandStringBytesMasImprSrc(10, b) +} + +func BenchmarkRandStringBytesMasImprSrc100(b *testing.B) { + benchmarkRandStringBytesMasImprSrc(100, b) +} + +func BenchmarkRandStringBytesMasImprSrc1000(b *testing.B) { + benchmarkRandStringBytesMasImprSrc(1000, b) +} + +func TestRedirect(t *testing.T) { + Pool = cache.NewFrom(Exp, Cleanup, store) + tt := []struct { + name string + i string + r string + err error + }{ + { + "Not Found", + "badkey", + "", + ErrNotFound, + }, + { + "Found", + "google", + "https://google.com", + nil, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + r, err := redirect(tc.i) + if !errors.Is(tc.err, err) { + t.Fatalf("Bad error, expected %v, got %v", tc.err, err) + } + if !reflect.DeepEqual(tc.r, r) { + t.Fatalf("Bad value, expected %v, got %v", tc.r, r) + } + }) + } +} + +func TestShortener(t *testing.T) { + tt := []struct { + name string + i string + s int + rs int + err error + }{ + { + "Bad Request", + "badurl", + 10, + 0, + ErrBadRequest, + }, + { + "Good Request", + "https://www.google.com", + 10, + 10, + nil, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + r, err := shortener(tc.i, tc.s) + if !errors.Is(tc.err, err) { + t.Fatalf("Bad error, expected %v, got %v", tc.err, err) + } + if len(r) != tc.rs { + t.Fatalf("Bad value, expected %v, got %v (%s)", tc.s, len(r), r) + } + }) + Pool.Flush() + } +} + +func TestDumpDbTOFile(t *testing.T) { + tt := []struct { + name string + p *cache.Cache + r int + f *os.File + err error + }{ + { + "Error", + cache.New(Exp, Cleanup), + 0, + func() *os.File { f, _ := os.Create(fmt.Sprintf("%s/.json", randStringBytesMaskImprSrc(5))); return f }(), + os.ErrInvalid, + }, + { + "0-v", + cache.New(Exp, Cleanup), + 0, + func() *os.File { f, _ := os.Create(fmt.Sprintf("%s.json", randStringBytesMaskImprSrc(5))); return f }(), + nil, + }, + { + "1-v", + cache.NewFrom(Exp, Cleanup, store), + 1, + func() *os.File { f, _ := os.Create(fmt.Sprintf("%s.json", randStringBytesMaskImprSrc(5))); return f }(), + nil, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + Pool = tc.p + r, err := dumpDbTOFile(tc.f) + if !errors.Is(tc.err, err) { + t.Fatalf("Bad error, expected %#v, got %#v", tc.err, err) + } + if tc.r != r { + t.Fatalf("Bad value, expected %#v, got %#v", tc.r, r) + + } + }) + if tc.f != nil { + os.Remove(tc.f.Name()) + } + Pool.Flush() + } +} diff --git a/main.go b/main.go index fcc17c8..80c51a2 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,9 @@ import ( "strings" "time" + "internal/shortie" + "github.com/valyala/fasthttp" - "github.com/wolviecb/short/shortie" "github.com/asaskevich/govalidator" "github.com/fasthttp/router"