From 1fb2a210a11869d7580719f7bb89b637a8ade064 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Wed, 1 May 2019 16:42:17 +0200 Subject: [PATCH] new repo from git.xdrm.io/go/aicra@v0.2.0 --- aicra.json | 18 ---- api.json | 18 ++-- controller.plugin/ROOT/main.go | 179 ------------------------------- controller.plugin/token/main.go | 60 ----------- db/db.go | 81 -------------- go.mod | 10 ++ go.sum | 31 ++++++ main.go | 38 ++++++- middleware.plugin/1-auth/main.go | 47 -------- service/auth/auth.go | 102 ++++++++++++++++++ service/shortener/shortener.go | 157 +++++++++++++++++++++++++++ storage/redis.go | 76 +++++++++++++ 12 files changed, 419 insertions(+), 398 deletions(-) delete mode 100644 aicra.json delete mode 100644 controller.plugin/ROOT/main.go delete mode 100644 controller.plugin/token/main.go delete mode 100644 db/db.go create mode 100644 go.mod create mode 100644 go.sum delete mode 100644 middleware.plugin/1-auth/main.go create mode 100644 service/auth/auth.go create mode 100644 service/shortener/shortener.go create mode 100644 storage/redis.go diff --git a/aicra.json b/aicra.json deleted file mode 100644 index 1c09a07..0000000 --- a/aicra.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "root": ".", - "host": "127.0.0.1", - "port": 8080, - - "driver": "plugin", - - "types": { - "default": true, - "folder": "type" - }, - "controllers": { - "folder": "controller.plugin" - }, - "middlewares": { - "folder": "middleware.plugin" - } -} \ No newline at end of file diff --git a/api.json b/api.json index fb61b61..6f55a25 100644 --- a/api.json +++ b/api.json @@ -3,7 +3,7 @@ "info": "redirects to given tiny url", "scope": [[]], "in": { - "URL#0": { "info": "tiny url to redirect to", "name": "url", "type": "varchar(1,30)" } + "URL#0": { "info": "tiny url to redirect to", "name": "url", "type": "string(1,30)" } }, "out": {} }, @@ -12,8 +12,8 @@ "info": "creates a new tiny url", "scope": [["admin"]], "in": { - "URL#0": { "info": "preferred tiny url", "type": "varchar(1,30)", "name": "url" }, - "target": { "info": "url to shorten", "type": "varchar(5,300)" } + "URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" }, + "target": { "info": "url to shorten", "type": "string(5,300)" } }, "out": {} }, @@ -22,8 +22,8 @@ "info": "overrides an existing tiny url", "scope": [["admin"]], "in": { - "URL#0": { "info": "preferred tiny url", "type": "varchar(1,30)", "name": "url" }, - "target": { "info": "url to shorten", "type": "varchar(5,300)" } + "URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" }, + "target": { "info": "url to shorten", "type": "string(5,300)" } }, "out": {} }, @@ -32,7 +32,7 @@ "info": "removes an existing tiny url", "scope": [["admin"]], "in": { - "URL#0": { "info": "preferred tiny url", "type": "varchar(1,30)", "name": "url" } + "URL#0": { "info": "preferred tiny url", "type": "string(1,30)", "name": "url" } }, "out": {} }, @@ -40,13 +40,13 @@ "/": { "token": { "POST": { - "info": "creates a 1-minute access token", + "info": "creates a 5-minute access token", "scope": [[]], "in": { - "URL#0": { "info": "wanted role", "type": "varchar(3,10)", "name": "role" } + "URL#0": { "info": "wanted role", "type": "string(3,10)", "name": "role" } }, "out": { - "token": { "info": "access token", "type": "varchar(128,128)" } + "token": { "info": "access token", "type": "string(128,128)" } } } } diff --git a/controller.plugin/ROOT/main.go b/controller.plugin/ROOT/main.go deleted file mode 100644 index 55070b6..0000000 --- a/controller.plugin/ROOT/main.go +++ /dev/null @@ -1,179 +0,0 @@ -package main - -import ( - "git.xdrm.io/example/aicra/db" - "git.xdrm.io/go/aicra/api" - "git.xdrm.io/go/aicra/driver" - e "git.xdrm.io/go/aicra/err" -) - -func main() {} - -type RootController int - -func Export() driver.Controller { return new(RootController) } - -// Redirects to an url from a key -func (rctl RootController) Get(d api.Arguments) api.Response { - - r := api.NewResponse() - - /* (1) Init redis connection */ - cli := db.Connect() - if cli == nil { - r.Err = e.Failure - return *r - } - - /* (2) Extract api input */ - key, err := d.GetString("url") - - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("url") - r.Err.Put(err.Error()) - return *r - } - - /* (3) Check if match for this key */ - val := cli.Get(db.DATA, key) - if val == nil { - r.Err = e.NoMatchFound - return *r - } - - /* (4) Redirect to value */ - r.Set("_REDIRECT_", string(val)) - r.Err = e.Success - return *r -} - -// Stores a new tinyurl/fullurl combination -func (rctl RootController) Post(d api.Arguments) api.Response { - - r := api.NewResponse() - /* (1) Init redis connection */ - cli := db.Connect() - if cli == nil { - r.Err = e.Failure - return *r - } - - /* (2) Extract api input */ - target, err := d.GetString("target") - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("target") - r.Err.Put(err.Error()) - return *r - } - - url, err := d.GetString("url") - - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("url") - r.Err.Put(err.Error()) - return *r - } - - /* (3) Check if key already used */ - if cli.Get(db.DATA, url) != nil { - r.Err = e.AlreadyExists - r.Err.Put("url") - return *r - } - - /* (4) Store */ - if !cli.Set(db.DATA, url, target) { - r.Err = e.Failure - return *r - } - - r.Err = e.Success - return *r -} - -// Overrides a existing tinyurl with new target -func (rctl RootController) Put(d api.Arguments) api.Response { - - r := api.NewResponse() - - /* (1) Init redis connection */ - cli := db.Connect() - if cli == nil { - r.Err = e.Failure - return *r - } - - /* (2) Extract api input */ - target, err := d.GetString("target") - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("target") - r.Err.Put(err.Error()) - return *r - } - - url, err := d.GetString("url") - - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("url") - r.Err.Put(err.Error()) - return *r - } - - /* (3) Check if key already used */ - if cli.Get(db.DATA, url) == nil { - r.Err = e.NoMatchFound - return *r - } - - /* (4) Update */ - if !cli.Set(db.DATA, url, target) { - r.Err = e.Failure - return *r - } - - r.Err = e.Success - return *r -} - -// Deletes an existing tinyurl -func (rctl RootController) Delete(d api.Arguments) api.Response { - - r := api.NewResponse() - - /* (1) Init redis connection */ - cli := db.Connect() - if cli == nil { - r.Err = e.Failure - return *r - } - - /* (2) Extract api input */ - url, err := d.GetString("url") - - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("url") - r.Err.Put(err.Error()) - return *r - } - - /* (3) Check if key already used */ - if cli.Get(db.DATA, url) == nil { - r.Err = e.NoMatchFound - return *r - } - - /* (4) Delete */ - if !cli.Del(db.DATA, url) { - r.Err = e.Failure - return *r - } - - r.Err = e.Success - return *r -} diff --git a/controller.plugin/token/main.go b/controller.plugin/token/main.go deleted file mode 100644 index a05e3a3..0000000 --- a/controller.plugin/token/main.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "crypto/sha512" - "encoding/hex" - "git.xdrm.io/example/aicra/db" - "git.xdrm.io/go/aicra/api" - "git.xdrm.io/go/aicra/driver" - e "git.xdrm.io/go/aicra/err" - "strconv" - "time" -) - -func main() {} - -type TokenController int - -func Export() driver.Controller { return new(TokenController) } - -// Builds an access token from credentials -func (tctl TokenController) Post(d api.Arguments) api.Response { - - r := api.NewResponse() - - /* (1) Init redis connection */ - cli := db.Connect() - if cli == nil { - r.Err = e.Failure - return *r - } - - /* (2) Extract api input */ - role, err := d.GetString("role") - if err != nil { - r.Err = e.InvalidParam - r.Err.Put("url") - r.Err.Put(err.Error()) - return *r - } - - /* (3) Generate token */ - hasher := sha512.New() - defer hasher.Reset() - hasher.Write([]byte(strconv.FormatInt(time.Now().Unix(), 5))) - token := hex.EncodeToString(hasher.Sum(nil)) - - /* (4) Store */ - if !cli.Set(db.TOKEN, token, role, time.Minute) { - r.Err = e.Failure - return *r - } - - r.Set("token", token) - r.Err = e.Success - return *r -} - -func (tctl TokenController) Get(d api.Arguments) api.Response { return *api.NewResponse() } -func (tctl TokenController) Put(d api.Arguments) api.Response { return *api.NewResponse() } -func (tctl TokenController) Delete(d api.Arguments) api.Response { return *api.NewResponse() } diff --git a/db/db.go b/db/db.go deleted file mode 100644 index 0f477ae..0000000 --- a/db/db.go +++ /dev/null @@ -1,81 +0,0 @@ -package db - -import ( - "fmt" - "github.com/go-redis/redis" - "time" -) - -const NONCE = "go-tiny-url" - -type Domain string - -const ( - DATA Domain = "data" - TOKEN Domain = "token" -) - -type db redis.Client - -// Returns a connected client to dataset -func Connect() *db { - - cli := redis.NewClient(&redis.Options{ - Addr: "127.0.0.1:6379", - Password: "", - DB: 0, - }) - - if _, err := cli.Ping().Result(); err != nil { - return nil - } - - return (*db)(cli) -} - -// returns value from key (nil if nothing) -func (c *db) Get(dom Domain, key string) []byte { - - // 1. Try to get - if val, err := (*redis.Client)(c).Get(fmt.Sprintf("%s:%s:%s", NONCE, dom, key)).Result(); err != nil { - // 2. nil if nothing found - return nil - } else { - // 3. return if found - return []byte(val) - } - -} - -// stores a value for a key (success state in return) -func (c *db) Set(dom Domain, key string, value string, exp ...time.Duration) bool { - - var expiration time.Duration = 0 - if len(exp) > 0 { - expiration = exp[0] - } - - // 1. Try to set - if (*redis.Client)(c).Set(fmt.Sprintf("%s:%s:%s", NONCE, dom, key), value, expiration).Err() != nil { - // 2. failure - return false - } - - // 3. success - return true - -} - -// deletes the value for a key (success state in return) -func (c *db) Del(dom Domain, key string) bool { - - // 1. Try to set - if (*redis.Client)(c).Del(fmt.Sprintf("%s:%s:%s", NONCE, dom, key)).Err() != nil { - // 2. failure - return false - } - - // 3. success - return true - -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3e7a231 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.xdrm.io/example/aicra + +go 1.12 + +require ( + git.xdrm.io/go/aicra v0.2.0 + github.com/go-redis/redis v6.15.2+incompatible + github.com/onsi/ginkgo v1.8.0 // indirect + github.com/onsi/gomega v1.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e7e78c4 --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +git.xdrm.io/go/aicra v0.2.0 h1:JrX773PfTaI4jYkeihpdv0U1caLGSsAKpBh21W6W0Ik= +git.xdrm.io/go/aicra v0.2.0/go.mod h1:ulAzCdKqUN5X4eWQSER70QXSYteSXtybAqupcUuYPdw= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= +github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index cce0c56..f1c89f6 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,54 @@ package main import ( - "git.xdrm.io/go/aicra" "log" "net/http" + + "git.xdrm.io/example/aicra/service/auth" + "git.xdrm.io/example/aicra/service/shortener" + + "git.xdrm.io/example/aicra/storage" + "git.xdrm.io/go/aicra" + "git.xdrm.io/go/aicra/typecheck/builtin" ) func main() { listenTo := "127.0.0.1:4242" + // 1. build server + log.Printf("[server] building") server, err := aicra.New("api.json") if err != nil { - panic(err) + log.Fatalf("/!\\ cannot init server: %v\n", err) } - log.Printf("[Server up] %s\n", listenTo) + // 2. add type checkers + server.Checkers.Add(builtin.NewAny()) + server.Checkers.Add(builtin.NewString()) + server.Checkers.Add(builtin.NewFloat64()) + + // 3. storage connect + log.Printf("[storage] connecting") + storageClient, err := storage.New() + if err != nil { + log.Fatalf("/!\\ cannot connect %v", err) + } + defer storageClient.Close() + + // 4. init services + authService := auth.New(storageClient) + shortenerService := shortener.New(storageClient, authService) + + // 5. wire services + shortenerService.Wire(server) + authService.Wire(server) + + // 6. listen and serve + log.Printf("[server] listening") err = http.ListenAndServe(listenTo, server) if err != nil { - panic(err) + log.Fatalf("/!\\ cannot listen: %v\n", err) } } diff --git a/middleware.plugin/1-auth/main.go b/middleware.plugin/1-auth/main.go deleted file mode 100644 index f6c0c1a..0000000 --- a/middleware.plugin/1-auth/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "git.xdrm.io/example/aicra/db" - "git.xdrm.io/go/aicra/driver" - "net/http" - "strings" -) - -func main() {} - -type AuthMiddleware int - -func Export() driver.Middleware { return new(AuthMiddleware) } - -// Authentication middleware -func (amw AuthMiddleware) Inspect(req http.Request, scope *[]string) { - - // 1. get authorization header - token := req.Header.Get("Authorization") - - // fail if no header - if len(token) < 1 { - return - } - - // 2. fail on invalid token format - if len(token) != 128 || strings.ContainsAny(token, "$-_") { - return - } - - // 3. get role for this token - cli := db.Connect() - if cli == nil { - return - } - defer cli.Close() - - role := cli.Get(db.TOKEN, token) - if role == nil { - return - } - - // add role to scope - *scope = append(*scope, string(role)) - -} diff --git a/service/auth/auth.go b/service/auth/auth.go new file mode 100644 index 0000000..f5cc856 --- /dev/null +++ b/service/auth/auth.go @@ -0,0 +1,102 @@ +package auth + +import ( + "crypto/sha512" + "encoding/hex" + "log" + "strconv" + "strings" + "time" + + "git.xdrm.io/example/aicra/storage" + "git.xdrm.io/go/aicra" + "git.xdrm.io/go/aicra/api" +) + +// Service manages the url shortener +type Service struct { + storage *storage.Client +} + +// New returns a bare service +func New(storage *storage.Client) *Service { + log.Printf("[service.auth] created") + return &Service{ + storage: storage, + } +} + +// Wire to the aicra server +func (svc *Service) Wire(server *aicra.Server) { + log.Printf("[service.auth] wired") + server.HandleFunc("POST", "/token", svc.generateToken) +} + +// generateToken generates a token valid for 5 mintes +func (svc *Service) generateToken(req api.Request, res *api.Response) { + + // 1. extract input + role, err := req.Param.GetString("role") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("role") + res.Err.Put(err.Error()) + return + } + + // 2. generate token + hasher := sha512.New() + defer hasher.Reset() + hasher.Write([]byte(strconv.FormatInt(time.Now().Unix(), 5))) + token := hex.EncodeToString(hasher.Sum(nil)) + + // 3. store token + if !svc.storage.Set(storage.TOKEN, token, role, 5*time.Minute) { + res.Err = api.ErrorFailure() + return + } + + // 4. return data + res.Data["token"] = token + res.Err = api.ErrorSuccess() + +} + +// CheckToken returns whether a token is valid +func (svc *Service) CheckToken(handler api.HandlerFunc) api.HandlerFunc { + return func(req api.Request, res *api.Response) { + + // success if no scope [['admin']] + if req.Scope == nil || len(req.Scope) < 1 { + handler(req, res) + return + } + + headerToken := req.Request.Header.Get("Authorization") + + // fail if invalid header + if len(headerToken) != 128 || strings.ContainsAny(headerToken, "$-_") { + res.Err = api.ErrorPermission() + return + } + + tokenRole := svc.storage.Get(storage.TOKEN, headerToken) + + // fail if the role of the token does not match any scope + for _, scope := range req.Scope { + if scope == nil || len(scope) != 1 { + continue + } + + // success + if scope[0] == string(tokenRole) { + handler(req, res) + return + } + } + + // failure + res.Err = api.ErrorPermission() + return + } +} diff --git a/service/shortener/shortener.go b/service/shortener/shortener.go new file mode 100644 index 0000000..6791bec --- /dev/null +++ b/service/shortener/shortener.go @@ -0,0 +1,157 @@ +package shortener + +import ( + "log" + "net/http" + + "git.xdrm.io/example/aicra/service/auth" + "git.xdrm.io/example/aicra/storage" + "git.xdrm.io/go/aicra" + "git.xdrm.io/go/aicra/api" +) + +// Service manages the url shortener +type Service struct { + storage *storage.Client + authService *auth.Service +} + +// New returns a bare service +func New(storage *storage.Client, auth *auth.Service) *Service { + log.Printf("[service.shortener] created") + return &Service{ + storage: storage, + authService: auth, + } +} + +// Wire to the aicra server +func (svc *Service) Wire(server *aicra.Server) { + log.Printf("[service.shortener] wired") + server.HandleFunc("GET", "/", svc.redirect) + server.HandleFunc("POST", "/", svc.authService.CheckToken(svc.register)) + server.HandleFunc("PUT", "/", svc.authService.CheckToken(svc.update)) + server.HandleFunc("DELETE", "/", svc.authService.CheckToken(svc.delete)) +} + +// redirect from a tiny url to the long url +func (svc *Service) redirect(req api.Request, res *api.Response) { + + // 1. extract input + tinyURL, err := req.Param.GetString("url") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("url") + res.Err.Put(err.Error()) + return + } + + // 2. check in db if exists + longURL := svc.storage.Get(storage.DATA, tinyURL) + if longURL == nil { + res.Err = api.ErrorNoMatchFound() + return + } + + // 3. redirect + res.Status = http.StatusPermanentRedirect + res.Headers.Set("Location", string(longURL)) + res.Err = api.ErrorSuccess() + +} + +// register registers a new tiny url to a long one +func (svc *Service) register(req api.Request, res *api.Response) { + + // 1. extract arguments + longURL, err := req.Param.GetString("target") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("target") + res.Err.Put(err.Error()) + return + } + tinyURL, err := req.Param.GetString("url") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("url") + res.Err.Put(err.Error()) + return + } + + // 2. fail if already used + if svc.storage.Get(storage.DATA, tinyURL) != nil { + res.Err = api.ErrorAlreadyExists() + res.Err.Put("url") + return + } + + // 3. store association + if !svc.storage.Set(storage.DATA, tinyURL, longURL) { + res.Err = api.ErrorFailure() + return + } + + res.Err = api.ErrorSuccess() +} + +// update updates an existing tiny url to a new long one +func (svc *Service) update(req api.Request, res *api.Response) { + + // 1. extract arguments + longURL, err := req.Param.GetString("target") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("target") + res.Err.Put(err.Error()) + return + } + tinyURL, err := req.Param.GetString("url") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("url") + res.Err.Put(err.Error()) + return + } + + // 2. fail if not already existing + if svc.storage.Get(storage.DATA, tinyURL) == nil { + res.Err = api.ErrorNoMatchFound() + return + } + + // 3. update association + if !svc.storage.Set(storage.DATA, tinyURL, longURL) { + res.Err = api.ErrorFailure() + return + } + + res.Err = api.ErrorSuccess() +} + +// delete removes a new tiny url +func (svc *Service) delete(req api.Request, res *api.Response) { + + // 1. extract arguments + tinyURL, err := req.Param.GetString("url") + if err != nil { + res.Err = api.ErrorInvalidParam() + res.Err.Put("url") + res.Err.Put(err.Error()) + return + } + + // 2. fail if not already existing + if svc.storage.Get(storage.DATA, tinyURL) == nil { + res.Err = api.ErrorNoMatchFound() + return + } + + // 3. update association + if !svc.storage.Del(storage.DATA, tinyURL) { + res.Err = api.ErrorFailure() + return + } + + res.Err = api.ErrorSuccess() +} diff --git a/storage/redis.go b/storage/redis.go new file mode 100644 index 0000000..f255bbf --- /dev/null +++ b/storage/redis.go @@ -0,0 +1,76 @@ +package storage + +import ( + "fmt" + "time" + + "github.com/go-redis/redis" +) + +const nonce = "go-tiny-url" + +const ( + // DATA domain used to store actual data + DATA string = "data" + // TOKEN domain used to store tokens + TOKEN string = "token" +) + +// Client is a wrapper around the redis client +type Client struct { + client *redis.Client +} + +// New returns new client +func New() (*Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: "127.0.0.1:6379", + Password: "", + DB: 0, + }) + + if _, err := client.Ping().Result(); err != nil { + return nil, err + } + + return &Client{ + client: client, + }, nil +} + +// Close closes the connection +func (c *Client) Close() error { + return c.client.Close() +} + +// Get returns a value from key or NIL +func (c *Client) Get(dom, key string) []byte { + + redisKey := fmt.Sprintf("%s:%s:%s", nonce, dom, key) + + val, err := c.client.Get(redisKey).Result() + if err != nil { + return nil + } + + return []byte(val) +} + +// Set stores a value for a key (success state in return) +func (c *Client) Set(dom, key string, value string, exp ...time.Duration) bool { + redisKey := fmt.Sprintf("%s:%s:%s", nonce, dom, key) + + var expiration time.Duration + if len(exp) > 0 { + expiration = exp[0] + } + + return c.client.Set(redisKey, value, expiration).Err() == nil +} + +// Del deletes the value for a key (success state in return) +func (c *Client) Del(dom, key string) bool { + redisKey := fmt.Sprintf("%s:%s:%s", nonce, dom, key) + + return c.client.Del(redisKey).Err() == nil +}