From fc95236dc7af1751dc20612b744f8ea3f8343987 Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 19 Nov 2018 23:51:23 +0300 Subject: [PATCH 001/104] Initial commit --- LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf4486a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 777777miSSU7777777 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8320212 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# NonRelDB-server +Server for redis like db on golang. From 2de422f46ad91465ea06bb480d9a54d0d3ec3500 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 03:22:16 +0300 Subject: [PATCH 002/104] Added json encode & decode for map --- encoding/map/json/json_decode.go | 14 ++++++++++++++ encoding/map/json/json_encode.go | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 encoding/map/json/json_decode.go create mode 100644 encoding/map/json/json_encode.go diff --git a/encoding/map/json/json_decode.go b/encoding/map/json/json_decode.go new file mode 100644 index 0000000..f0da48a --- /dev/null +++ b/encoding/map/json/json_decode.go @@ -0,0 +1,14 @@ +package json + +import ( + "encoding/json" +) + +func UnpackFromJSON(b []byte) *map[string]interface{}{ + var m map[string]interface{} + err := json.Unmarshal(b, &m) + if err != nil{ + panic(err) + } + return &m +} \ No newline at end of file diff --git a/encoding/map/json/json_encode.go b/encoding/map/json/json_encode.go new file mode 100644 index 0000000..f8c3808 --- /dev/null +++ b/encoding/map/json/json_encode.go @@ -0,0 +1,22 @@ +package json + +import ( + "encoding/json" +) + +func PackToJSON(key string, value interface{}) []byte{ + m := map[string]interface{}{key : value} + b, err := json.Marshal(m) + if err != nil{ + panic(err) + } + return b +} + +func PackMapToJson(m map[string]interface{}) []byte{ + b, err := json.Marshal(m) + if err != nil{ + panic(err) + } + return b +} \ No newline at end of file From 01fbd786cbde6dcbb6368a206650d01c0dca27c2 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 03:43:03 +0300 Subject: [PATCH 003/104] Added string & bytes file writing & reading --- file/file_read.go | 21 +++++++++++++++++++++ file/file_write.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 file/file_read.go create mode 100644 file/file_write.go diff --git a/file/file_read.go b/file/file_read.go new file mode 100644 index 0000000..260b1d2 --- /dev/null +++ b/file/file_read.go @@ -0,0 +1,21 @@ +package file + +import ( + "io/ioutil" +) + +func OpenAndReadString(name string) string{ + b, err := ioutil.ReadFile(name) + if err != nil{ + panic(err) + } + return string(b) +} + +func OpenAndRead(name string) []byte{ + b, err := ioutil.ReadFile(name) + if err != nil{ + panic(err) + } + return b +} \ No newline at end of file diff --git a/file/file_write.go b/file/file_write.go new file mode 100644 index 0000000..eb0c9ba --- /dev/null +++ b/file/file_write.go @@ -0,0 +1,31 @@ +package file + +import ( + "os" +) + +func CreateAndWriteString(name string, value string){ + f, err := os.Create(name) + if err != nil{ + panic(err) + } + _, err = f.WriteString(value) + if err != nil{ + panic(err) + } + f.Close() +} + +func CreateAndWrite(name string, value []byte){ + f, err := os.Create(name) + if err != nil{ + panic(err) + } + + _, err = f.Write(value) + if err != nil{ + panic(err) + } + f.Close() +} + From fda4df295d2a136c39ff432588641fefc6b0421c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 14:11:13 +0300 Subject: [PATCH 004/104] Changed dir struct & added comments to existing modules --- {file => util/file}/file_read.go | 2 ++ {file => util/file}/file_write.go | 2 ++ {encoding/map => util}/json/json_decode.go | 0 {encoding/map => util}/json/json_encode.go | 0 4 files changed, 4 insertions(+) rename {file => util/file}/file_read.go (61%) rename {file => util/file}/file_write.go (76%) rename {encoding/map => util}/json/json_decode.go (100%) rename {encoding/map => util}/json/json_encode.go (100%) diff --git a/file/file_read.go b/util/file/file_read.go similarity index 61% rename from file/file_read.go rename to util/file/file_read.go index 260b1d2..6c89f7f 100644 --- a/file/file_read.go +++ b/util/file/file_read.go @@ -4,6 +4,7 @@ import ( "io/ioutil" ) +// OpenAndReadString | receives file name, reads this file and returns its string content. func OpenAndReadString(name string) string{ b, err := ioutil.ReadFile(name) if err != nil{ @@ -12,6 +13,7 @@ func OpenAndReadString(name string) string{ return string(b) } +// OpenAndRead | Receives file name, reads this file and returns byte array from it. func OpenAndRead(name string) []byte{ b, err := ioutil.ReadFile(name) if err != nil{ diff --git a/file/file_write.go b/util/file/file_write.go similarity index 76% rename from file/file_write.go rename to util/file/file_write.go index eb0c9ba..0216dbd 100644 --- a/file/file_write.go +++ b/util/file/file_write.go @@ -4,6 +4,7 @@ import ( "os" ) +// CreateAndWriteString | Creates file and writes string to it. func CreateAndWriteString(name string, value string){ f, err := os.Create(name) if err != nil{ @@ -16,6 +17,7 @@ func CreateAndWriteString(name string, value string){ f.Close() } +// CreateAndWrite | Creates file and writes byte array to it. func CreateAndWrite(name string, value []byte){ f, err := os.Create(name) if err != nil{ diff --git a/encoding/map/json/json_decode.go b/util/json/json_decode.go similarity index 100% rename from encoding/map/json/json_decode.go rename to util/json/json_decode.go diff --git a/encoding/map/json/json_encode.go b/util/json/json_encode.go similarity index 100% rename from encoding/map/json/json_encode.go rename to util/json/json_encode.go From ce5848ad6e74954c109c17a12e1131f2ad0a86cf Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 15:00:12 +0300 Subject: [PATCH 005/104] Added comments for json util & marshal indent and fixedjson decode func signature --- util/json/json_decode.go | 1 + util/json/json_encode.go | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/util/json/json_decode.go b/util/json/json_decode.go index f0da48a..e7b142b 100644 --- a/util/json/json_decode.go +++ b/util/json/json_decode.go @@ -4,6 +4,7 @@ import ( "encoding/json" ) +// UnpackFromJSON | Receives json bytes and returns map pointer. func UnpackFromJSON(b []byte) *map[string]interface{}{ var m map[string]interface{} err := json.Unmarshal(b, &m) diff --git a/util/json/json_encode.go b/util/json/json_encode.go index f8c3808..3419e27 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -4,17 +4,19 @@ import ( "encoding/json" ) +// PackToJSON | Receives string key and interface value and returns json bytes. func PackToJSON(key string, value interface{}) []byte{ m := map[string]interface{}{key : value} - b, err := json.Marshal(m) + b, err := json.MarshalIndent(m,""," ") if err != nil{ panic(err) } return b } -func PackMapToJson(m map[string]interface{}) []byte{ - b, err := json.Marshal(m) +// PackMapToJSON | Receives map and returns json bytes. +func PackMapToJSON(m map[string]interface{}) []byte{ + b, err := json.MarshalIndent(m,""," ") if err != nil{ panic(err) } From 289dfef2eabfb3d4b7ab54448443bbf67f40a4c2 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 15:23:46 +0300 Subject: [PATCH 006/104] Added inmemory service --- util/inmemory/inmemory_service.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 util/inmemory/inmemory_service.go diff --git a/util/inmemory/inmemory_service.go b/util/inmemory/inmemory_service.go new file mode 100644 index 0000000..0578345 --- /dev/null +++ b/util/inmemory/inmemory_service.go @@ -0,0 +1,22 @@ +package inmemory + +import ( + "NonRelDB-server/util/json" + "NonRelDB-server/util/file" +) + +var Storage *map[string]interface{} + +// InitDBFromStorage | Receives filename and load its content to inmemory storage. +func InitDBFromStorage(filename string){ + j := file.OpenAndReadString(filename) + jb := []byte(j) + Storage = json.UnpackFromJSON(jb) +} + +// SaveDBToStorage | Receives file name and saves inmemory storage to it. +func SaveDBToStorage(filename string){ + jb := json.PackMapToJSON(*Storage) + j := string(jb) + file.CreateAndWriteString(filename, j) +} From be48fabaf9beaf032c899b5744b40a463b711aa7 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 19:52:36 +0300 Subject: [PATCH 007/104] Added inmemory storage init & query service --- util/inmemory/inmemory_service.go | 5 +++++ util/query/query_service.go | 35 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 util/query/query_service.go diff --git a/util/inmemory/inmemory_service.go b/util/inmemory/inmemory_service.go index 0578345..46e0c0d 100644 --- a/util/inmemory/inmemory_service.go +++ b/util/inmemory/inmemory_service.go @@ -7,6 +7,11 @@ import ( var Storage *map[string]interface{} +func init(){ + s := make(map[string]interface{}) + Storage = &s +} + // InitDBFromStorage | Receives filename and load its content to inmemory storage. func InitDBFromStorage(filename string){ j := file.OpenAndReadString(filename) diff --git a/util/query/query_service.go b/util/query/query_service.go new file mode 100644 index 0000000..e7c0911 --- /dev/null +++ b/util/query/query_service.go @@ -0,0 +1,35 @@ +package query + +import ( + "NonRelDB-server/util/inmemory" +) + +// Get | Receives and key and returns value according its key. +func Get(key string) interface{}{ + s := inmemory.Storage + v := (*s)[key] + if v != nil{ + return v; + } else { + return "Value with this key not found" + } +} + +// Set | Set value according to key. +func Set(key string, value interface{}) string{ + s := inmemory.Storage + (*s)[key] = value + return "Value has changed" +} + +// Del | Del value according to key. +func Del(key string) interface{}{ + s := inmemory.Storage + v := (*s)[key] + if v != nil { + delete(*s,key) + return v; + } else { + return "Value with this key not found" + } +} \ No newline at end of file From 253c4f431e45e3a3bc2c759280283700df00e654 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 20 Nov 2018 20:30:01 +0300 Subject: [PATCH 008/104] Changed value to string type --- util/inmemory/inmemory_service.go | 4 ++-- util/json/json_decode.go | 4 ++-- util/json/json_encode.go | 6 +++--- util/query/query_service.go | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/util/inmemory/inmemory_service.go b/util/inmemory/inmemory_service.go index 46e0c0d..5d1eb5c 100644 --- a/util/inmemory/inmemory_service.go +++ b/util/inmemory/inmemory_service.go @@ -5,10 +5,10 @@ import ( "NonRelDB-server/util/file" ) -var Storage *map[string]interface{} +var Storage *map[string]string func init(){ - s := make(map[string]interface{}) + s := make(map[string]string) Storage = &s } diff --git a/util/json/json_decode.go b/util/json/json_decode.go index e7b142b..21cb06d 100644 --- a/util/json/json_decode.go +++ b/util/json/json_decode.go @@ -5,8 +5,8 @@ import ( ) // UnpackFromJSON | Receives json bytes and returns map pointer. -func UnpackFromJSON(b []byte) *map[string]interface{}{ - var m map[string]interface{} +func UnpackFromJSON(b []byte) *map[string]string{ + var m map[string]string err := json.Unmarshal(b, &m) if err != nil{ panic(err) diff --git a/util/json/json_encode.go b/util/json/json_encode.go index 3419e27..b32d24e 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -5,8 +5,8 @@ import ( ) // PackToJSON | Receives string key and interface value and returns json bytes. -func PackToJSON(key string, value interface{}) []byte{ - m := map[string]interface{}{key : value} +func PackToJSON(key string, value string) []byte{ + m := map[string]string{key : value} b, err := json.MarshalIndent(m,""," ") if err != nil{ panic(err) @@ -15,7 +15,7 @@ func PackToJSON(key string, value interface{}) []byte{ } // PackMapToJSON | Receives map and returns json bytes. -func PackMapToJSON(m map[string]interface{}) []byte{ +func PackMapToJSON(m map[string]string) []byte{ b, err := json.MarshalIndent(m,""," ") if err != nil{ panic(err) diff --git a/util/query/query_service.go b/util/query/query_service.go index e7c0911..6aa86b5 100644 --- a/util/query/query_service.go +++ b/util/query/query_service.go @@ -5,10 +5,10 @@ import ( ) // Get | Receives and key and returns value according its key. -func Get(key string) interface{}{ +func Get(key string) string { s := inmemory.Storage v := (*s)[key] - if v != nil{ + if v != ""{ return v; } else { return "Value with this key not found" @@ -16,17 +16,17 @@ func Get(key string) interface{}{ } // Set | Set value according to key. -func Set(key string, value interface{}) string{ +func Set(key string, value string) string{ s := inmemory.Storage (*s)[key] = value return "Value has changed" } // Del | Del value according to key. -func Del(key string) interface{}{ +func Del(key string) string{ s := inmemory.Storage v := (*s)[key] - if v != nil { + if v != "" { delete(*s,key) return v; } else { From b2c4cb2762e4e013870cc855aaf1da2eded1d938 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 21 Nov 2018 14:50:54 +0300 Subject: [PATCH 009/104] The project was divided into client and server side for docker file comfort --- README.md | 4 ++-- {util => server/util}/file/file_read.go | 0 {util => server/util}/file/file_write.go | 0 {util => server/util}/inmemory/inmemory_service.go | 4 ++-- {util => server/util}/json/json_decode.go | 0 {util => server/util}/json/json_encode.go | 0 {util => server/util}/query/query_service.go | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename {util => server/util}/file/file_read.go (100%) rename {util => server/util}/file/file_write.go (100%) rename {util => server/util}/inmemory/inmemory_service.go (90%) rename {util => server/util}/json/json_decode.go (100%) rename {util => server/util}/json/json_encode.go (100%) rename {util => server/util}/query/query_service.go (94%) diff --git a/README.md b/README.md index 8320212..5282e1a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# NonRelDB-server -Server for redis like db on golang. +# NonRelDB +Redis like db on golang. diff --git a/util/file/file_read.go b/server/util/file/file_read.go similarity index 100% rename from util/file/file_read.go rename to server/util/file/file_read.go diff --git a/util/file/file_write.go b/server/util/file/file_write.go similarity index 100% rename from util/file/file_write.go rename to server/util/file/file_write.go diff --git a/util/inmemory/inmemory_service.go b/server/util/inmemory/inmemory_service.go similarity index 90% rename from util/inmemory/inmemory_service.go rename to server/util/inmemory/inmemory_service.go index 5d1eb5c..9ea4659 100644 --- a/util/inmemory/inmemory_service.go +++ b/server/util/inmemory/inmemory_service.go @@ -1,8 +1,8 @@ package inmemory import ( - "NonRelDB-server/util/json" - "NonRelDB-server/util/file" + "NonRelDB/server/util/json" + "NonRelDB/server/util/file" ) var Storage *map[string]string diff --git a/util/json/json_decode.go b/server/util/json/json_decode.go similarity index 100% rename from util/json/json_decode.go rename to server/util/json/json_decode.go diff --git a/util/json/json_encode.go b/server/util/json/json_encode.go similarity index 100% rename from util/json/json_encode.go rename to server/util/json/json_encode.go diff --git a/util/query/query_service.go b/server/util/query/query_service.go similarity index 94% rename from util/query/query_service.go rename to server/util/query/query_service.go index 6aa86b5..b306fae 100644 --- a/util/query/query_service.go +++ b/server/util/query/query_service.go @@ -1,7 +1,7 @@ package query import ( - "NonRelDB-server/util/inmemory" + "NonRelDB/server/util/inmemory" ) // Get | Receives and key and returns value according its key. From f4ddf5f5679b9959fa5bfc68e7562272f835c978 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 21 Nov 2018 16:00:18 +0300 Subject: [PATCH 010/104] Now you need manually init in-memory storage --- server/util/inmemory/inmemory_service.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/util/inmemory/inmemory_service.go b/server/util/inmemory/inmemory_service.go index 9ea4659..314b7ac 100644 --- a/server/util/inmemory/inmemory_service.go +++ b/server/util/inmemory/inmemory_service.go @@ -5,9 +5,11 @@ import ( "NonRelDB/server/util/file" ) +// Storage | Global variable for kv storage. var Storage *map[string]string -func init(){ +// InitDBInMemory | Init kv db in memory. +func InitDBInMemory(){ s := make(map[string]string) Storage = &s } From 66a7458b8c4e9c4f9de588ae078657c332d9ffe9 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 00:42:29 +0300 Subject: [PATCH 011/104] Added handlers & entry point for server --- server/handler/handle_connection.go | 20 ++++++++++ server/handler/handle_listener.go | 19 +++++++++ server/handler/handle_query.go | 22 +++++++++++ server/main.go | 60 +++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 server/handler/handle_connection.go create mode 100644 server/handler/handle_listener.go create mode 100644 server/handler/handle_query.go create mode 100644 server/main.go diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go new file mode 100644 index 0000000..d67f9d0 --- /dev/null +++ b/server/handler/handle_connection.go @@ -0,0 +1,20 @@ +package handler + +import ( + "net" +) + +func HandleConnection(c net.Conn){ + defer c.Close() + for { + req := make([]byte,1024) + lenReq, err := c.Read(req) + if err != nil { + panic(err) + } + query := string(req[:lenReq]) + resp_str := HandleQuery(query) + resp := []byte(resp_str) + c.Write(resp) + } +} \ No newline at end of file diff --git a/server/handler/handle_listener.go b/server/handler/handle_listener.go new file mode 100644 index 0000000..80d0e9e --- /dev/null +++ b/server/handler/handle_listener.go @@ -0,0 +1,19 @@ +package handler + +import ( + "net" +) + +func HandleListener(l net.Listener){ + defer l.Close() + for { + c, err := l.Accept() + if err != nil { + // log.Print(err.Error()) + // errorLogger.Println(err.Error()) + c.Close() + } + // infoLogger.Println("Connection successfully accepted") + go HandleConnection(c) + } +} \ No newline at end of file diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go new file mode 100644 index 0000000..4ee2280 --- /dev/null +++ b/server/handler/handle_query.go @@ -0,0 +1,22 @@ +package handler + +import ( + "NonRelDB/server/util/query" + "strings" +) + +func HandleQuery(q string) string { + p := strings.Split(q, " ") + if len(p) == 2 { + if strings.ToLower(p[0]) == "get" { + return query.Get(p[1]) + } else if strings.ToLower(p[0]) == "del" { + return query.Del(p[1]) + } + } else if len(p) == 3 { + if strings.ToLower(p[0]) == "set" { + return query.Set(p[1], p[2]) + } + } + return "Undefined query" +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..2e4ab77 --- /dev/null +++ b/server/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "NonRelDB/server/handler" + "NonRelDB/server/util/inmemory" + "flag" + "fmt" + "net" + "os" + "os/signal" + "syscall" +) + +var ip string +var port string +var mode string +var location string + +func init() { + flag.StringVar(&ip, "ip", "127.0.0.1", "Defines host ip") + flag.StringVar(&port, "port", "9090", "Defines host port") + flag.StringVar(&port, "p", "9090", "Defines host port") + flag.StringVar(&mode, "mode", "memory", "Defines storage location") + flag.StringVar(&mode, "m", "memory", "Defines storage location") + flag.StringVar(&location, "location", "storage.json", "Defines storage location") + flag.StringVar(&location, "l", "storage.json", "Defines storage location") + flag.Parse() +} + +func cleanup(){ + sign := make(chan os.Signal, 2) + signal.Notify(sign, os.Interrupt, syscall.SIGTERM) + go func() { + <-sign + fmt.Println("\r- Ctrl+C pressed in Terminal") + inmemory.SaveDBToStorage(location) + os.Exit(0) + }() +} + +func storageInit(){ + if mode == "memory" { + inmemory.InitDBInMemory() + } else if mode == "disk" { + inmemory.InitDBFromStorage(location) + } +} + + + +func main() { + storageInit() + cleanup() + fmt.Printf("Server started listening on %s:%s",ip,port) + l, err := net.Listen("tcp", ip+":"+port) + if err != nil { + panic(err) + } + handler.HandleListener(l) +} From 21de01d79aae7571002857a7440f8ed7f144e7fc Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 01:18:21 +0300 Subject: [PATCH 012/104] Query & In-Memory services moved to storage package dir --- server/storage/inmemory/inmemory_service.go | 29 +++++++++++++++++ server/storage/query/query_service.go | 35 +++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 server/storage/inmemory/inmemory_service.go create mode 100644 server/storage/query/query_service.go diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go new file mode 100644 index 0000000..314b7ac --- /dev/null +++ b/server/storage/inmemory/inmemory_service.go @@ -0,0 +1,29 @@ +package inmemory + +import ( + "NonRelDB/server/util/json" + "NonRelDB/server/util/file" +) + +// Storage | Global variable for kv storage. +var Storage *map[string]string + +// InitDBInMemory | Init kv db in memory. +func InitDBInMemory(){ + s := make(map[string]string) + Storage = &s +} + +// InitDBFromStorage | Receives filename and load its content to inmemory storage. +func InitDBFromStorage(filename string){ + j := file.OpenAndReadString(filename) + jb := []byte(j) + Storage = json.UnpackFromJSON(jb) +} + +// SaveDBToStorage | Receives file name and saves inmemory storage to it. +func SaveDBToStorage(filename string){ + jb := json.PackMapToJSON(*Storage) + j := string(jb) + file.CreateAndWriteString(filename, j) +} diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go new file mode 100644 index 0000000..b306fae --- /dev/null +++ b/server/storage/query/query_service.go @@ -0,0 +1,35 @@ +package query + +import ( + "NonRelDB/server/util/inmemory" +) + +// Get | Receives and key and returns value according its key. +func Get(key string) string { + s := inmemory.Storage + v := (*s)[key] + if v != ""{ + return v; + } else { + return "Value with this key not found" + } +} + +// Set | Set value according to key. +func Set(key string, value string) string{ + s := inmemory.Storage + (*s)[key] = value + return "Value has changed" +} + +// Del | Del value according to key. +func Del(key string) string{ + s := inmemory.Storage + v := (*s)[key] + if v != "" { + delete(*s,key) + return v; + } else { + return "Value with this key not found" + } +} \ No newline at end of file From d9f60c6d6446effef9c8ee7fb4cacb1b4da61d9d Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 03:09:24 +0300 Subject: [PATCH 013/104] Added logging & fixed storage package imports & refactored entry point for server --- server/handler/handle_connection.go | 9 ++++++- server/handler/handle_listener.go | 6 ++--- server/handler/handle_query.go | 2 +- server/log/logger.go | 21 ++++++++++++++++ server/main.go | 36 ++++++++++++++------------- server/storage/query/query_service.go | 2 +- 6 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 server/log/logger.go diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index d67f9d0..4d5bde7 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -2,6 +2,7 @@ package handler import ( "net" + "NonRelDB/server/log" ) func HandleConnection(c net.Conn){ @@ -9,12 +10,18 @@ func HandleConnection(c net.Conn){ for { req := make([]byte,1024) lenReq, err := c.Read(req) + if err != nil { - panic(err) + log.Error.Println(err.Error()) + return } + query := string(req[:lenReq]) + log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), query) + resp_str := HandleQuery(query) resp := []byte(resp_str) + log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp_str) c.Write(resp) } } \ No newline at end of file diff --git a/server/handler/handle_listener.go b/server/handler/handle_listener.go index 80d0e9e..ca5aab4 100644 --- a/server/handler/handle_listener.go +++ b/server/handler/handle_listener.go @@ -2,6 +2,7 @@ package handler import ( "net" + "NonRelDB/server/log" ) func HandleListener(l net.Listener){ @@ -9,11 +10,10 @@ func HandleListener(l net.Listener){ for { c, err := l.Accept() if err != nil { - // log.Print(err.Error()) - // errorLogger.Println(err.Error()) + log.Warning.Printf("Failed connection from %s",c.RemoteAddr().String()) c.Close() } - // infoLogger.Println("Connection successfully accepted") + log.Info.Printf("%s was connected to server",c.RemoteAddr().String()) go HandleConnection(c) } } \ No newline at end of file diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 4ee2280..2b7f763 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -1,7 +1,7 @@ package handler import ( - "NonRelDB/server/util/query" + "NonRelDB/server/storage/query" "strings" ) diff --git a/server/log/logger.go b/server/log/logger.go new file mode 100644 index 0000000..0664e04 --- /dev/null +++ b/server/log/logger.go @@ -0,0 +1,21 @@ +package log + +import ( + "io/ioutil" + "log" + "os" +) + +var ( + Trace *log.Logger + Info *log.Logger + Warning *log.Logger + Error *log.Logger +) + +func init(){ + Trace = log.New(ioutil.Discard,"[TRACE] ", log.Ldate|log.Ltime|log.Lshortfile) + Info = log.New(os.Stdout,"[INFO] ", log.Ldate|log.Ltime|log.Lshortfile) + Warning = log.New(os.Stdout, "[WARNING] ", log.Ldate|log.Ltime|log.Lshortfile) + Error = log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile) +} \ No newline at end of file diff --git a/server/main.go b/server/main.go index 2e4ab77..b37311a 100644 --- a/server/main.go +++ b/server/main.go @@ -2,9 +2,9 @@ package main import ( "NonRelDB/server/handler" - "NonRelDB/server/util/inmemory" + "NonRelDB/server/log" + "NonRelDB/server/storage/inmemory" "flag" - "fmt" "net" "os" "os/signal" @@ -27,18 +27,7 @@ func init() { flag.Parse() } -func cleanup(){ - sign := make(chan os.Signal, 2) - signal.Notify(sign, os.Interrupt, syscall.SIGTERM) - go func() { - <-sign - fmt.Println("\r- Ctrl+C pressed in Terminal") - inmemory.SaveDBToStorage(location) - os.Exit(0) - }() -} - -func storageInit(){ +func storageInit() { if mode == "memory" { inmemory.InitDBInMemory() } else if mode == "disk" { @@ -46,15 +35,28 @@ func storageInit(){ } } +func cleanup() { + sign := make(chan os.Signal, 2) + signal.Notify(sign, os.Interrupt, syscall.SIGTERM) + go func() { + <-sign + log.Info.Println("Ctrl+C pressed in Terminal") + inmemory.SaveDBToStorage(location) + os.Exit(0) + }() +} +func main() { -func main() { storageInit() cleanup() - fmt.Printf("Server started listening on %s:%s",ip,port) + l, err := net.Listen("tcp", ip+":"+port) + if err != nil { - panic(err) + log.Error.Panicln(err.Error()) } + + log.Info.Printf("Server started listening on %s", l.Addr().String()) handler.HandleListener(l) } diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go index b306fae..3c7c40a 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/query/query_service.go @@ -1,7 +1,7 @@ package query import ( - "NonRelDB/server/util/inmemory" + "NonRelDB/server/storage/inmemory" ) // Get | Receives and key and returns value according its key. From f3f9798e1b94a4eb8d8a2051b0d048ce867672bc Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 16:50:43 +0300 Subject: [PATCH 014/104] Added logging to json package & fixed nil assignment map panic in json_decode.go --- server/util/json/json_decode.go | 6 ++++-- server/util/json/json_encode.go | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/util/json/json_decode.go b/server/util/json/json_decode.go index 21cb06d..11b375e 100644 --- a/server/util/json/json_decode.go +++ b/server/util/json/json_decode.go @@ -2,14 +2,16 @@ package json import ( "encoding/json" + "NonRelDB/server/log" ) // UnpackFromJSON | Receives json bytes and returns map pointer. func UnpackFromJSON(b []byte) *map[string]string{ - var m map[string]string + m := make(map[string]string) err := json.Unmarshal(b, &m) if err != nil{ - panic(err) + log.Warning.Println(err.Error()) + log.Warning.Println("No bytes. Will be returned zero map") } return &m } \ No newline at end of file diff --git a/server/util/json/json_encode.go b/server/util/json/json_encode.go index b32d24e..e1b95b9 100644 --- a/server/util/json/json_encode.go +++ b/server/util/json/json_encode.go @@ -2,6 +2,7 @@ package json import ( "encoding/json" + "NonRelDB/server/log" ) // PackToJSON | Receives string key and interface value and returns json bytes. @@ -9,7 +10,7 @@ func PackToJSON(key string, value string) []byte{ m := map[string]string{key : value} b, err := json.MarshalIndent(m,""," ") if err != nil{ - panic(err) + log.Error.Println(err.Error()) } return b } @@ -18,7 +19,7 @@ func PackToJSON(key string, value string) []byte{ func PackMapToJSON(m map[string]string) []byte{ b, err := json.MarshalIndent(m,""," ") if err != nil{ - panic(err) + log.Error.Println(err.Error()) } return b } \ No newline at end of file From 0fc2cab076a5d1643fed08372b92e8641cabb0c5 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 16:52:42 +0300 Subject: [PATCH 015/104] Added logging to file package --- server/util/file/file_read.go | 9 +++++++-- server/util/file/file_write.go | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/server/util/file/file_read.go b/server/util/file/file_read.go index 6c89f7f..c3f40ea 100644 --- a/server/util/file/file_read.go +++ b/server/util/file/file_read.go @@ -2,22 +2,27 @@ package file import ( "io/ioutil" + "NonRelDB/server/log" ) // OpenAndReadString | receives file name, reads this file and returns its string content. func OpenAndReadString(name string) string{ b, err := ioutil.ReadFile(name) + if err != nil{ - panic(err) + log.Error.Panicln(err.Error()) } + return string(b) } // OpenAndRead | Receives file name, reads this file and returns byte array from it. func OpenAndRead(name string) []byte{ b, err := ioutil.ReadFile(name) + if err != nil{ - panic(err) + log.Error.Panicln(err.Error()) } + return b } \ No newline at end of file diff --git a/server/util/file/file_write.go b/server/util/file/file_write.go index 0216dbd..7877450 100644 --- a/server/util/file/file_write.go +++ b/server/util/file/file_write.go @@ -2,32 +2,38 @@ package file import ( "os" + "NonRelDB/server/log" ) // CreateAndWriteString | Creates file and writes string to it. func CreateAndWriteString(name string, value string){ f, err := os.Create(name) + + defer f.Close() + if err != nil{ - panic(err) + log.Error.Panicln(err.Error()) } + _, err = f.WriteString(value) if err != nil{ - panic(err) + log.Error.Panicln(err.Error()) } - f.Close() } // CreateAndWrite | Creates file and writes byte array to it. func CreateAndWrite(name string, value []byte){ f, err := os.Create(name) + + defer f.Close() + if err != nil{ - panic(err) + log.Error.Panicln(err.Error()) } _, err = f.Write(value) if err != nil{ - panic(err) + log.Error.Panicln(err.Error()) } - f.Close() } From 0661dc00b9731279065bdc0d50dc7f90a68b30c8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 16:58:41 +0300 Subject: [PATCH 016/104] Added logging to inmemory package & add behaviour if db file not exist --- server/storage/inmemory/inmemory_service.go | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 314b7ac..b8cf770 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -1,8 +1,10 @@ package inmemory import ( + "os" "NonRelDB/server/util/json" "NonRelDB/server/util/file" + "NonRelDB/server/log" ) // Storage | Global variable for kv storage. @@ -11,11 +13,30 @@ var Storage *map[string]string // InitDBInMemory | Init kv db in memory. func InitDBInMemory(){ s := make(map[string]string) - Storage = &s + Storage = &s + log.Info.Println("DB successfully created in-memory") } // InitDBFromStorage | Receives filename and load its content to inmemory storage. func InitDBFromStorage(filename string){ + log.Info.Println("[" + filename+"]") + log.Info.Println(len(filename)) + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + log.Warning.Println(err.Error()) + log.Warning.Printf("Storage doesnt exist. Will be created new with name %s", filename) + + f, err := os.Create(filename) + + if err != nil { + log.Error.Panicln(err.Error()) + } + log.Info.Printf("DB successfully initialized from %s", filename) + f.Close() + } + j := file.OpenAndReadString(filename) jb := []byte(j) Storage = json.UnpackFromJSON(jb) @@ -25,5 +46,7 @@ func InitDBFromStorage(filename string){ func SaveDBToStorage(filename string){ jb := json.PackMapToJSON(*Storage) j := string(jb) + file.CreateAndWriteString(filename, j) + log.Info.Printf("DB successfully saved to %s", filename) } From e964f87140fea75d9c57006d5979ac33d90dc472 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 17:01:44 +0300 Subject: [PATCH 017/104] Fixed saving to db with memory mode --- server/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/main.go b/server/main.go index b37311a..4c8fa03 100644 --- a/server/main.go +++ b/server/main.go @@ -41,13 +41,14 @@ func cleanup() { go func() { <-sign log.Info.Println("Ctrl+C pressed in Terminal") - inmemory.SaveDBToStorage(location) + if (mode == "disk"){ + inmemory.SaveDBToStorage(location) + } os.Exit(0) }() } func main() { - storageInit() cleanup() From 9872c8aabcb55ee3b63108261b73737e8f06ef54 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 18:51:55 +0300 Subject: [PATCH 018/104] Fixed logging in in-memory service --- server/storage/inmemory/inmemory_service.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index b8cf770..5666fd9 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -19,8 +19,7 @@ func InitDBInMemory(){ // InitDBFromStorage | Receives filename and load its content to inmemory storage. func InitDBFromStorage(filename string){ - log.Info.Println("[" + filename+"]") - log.Info.Println(len(filename)) + defer log.Info.Printf("DB successfully initialized from %s", filename) _, err := os.Stat(filename) @@ -33,7 +32,6 @@ func InitDBFromStorage(filename string){ if err != nil { log.Error.Panicln(err.Error()) } - log.Info.Printf("DB successfully initialized from %s", filename) f.Close() } From c8b30bb5d426a9bf51f61739c1d9d48af82590a8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 22 Nov 2018 20:10:54 +0300 Subject: [PATCH 019/104] Started work on keys command --- server/handler/handle_query.go | 2 ++ server/storage/query/query_service.go | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 2b7f763..513afc7 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -12,6 +12,8 @@ func HandleQuery(q string) string { return query.Get(p[1]) } else if strings.ToLower(p[0]) == "del" { return query.Del(p[1]) + } else if strings.ToLower(p[0]) == "keys" { + return query.Keys(p[1]) } } else if len(p) == 3 { if strings.ToLower(p[0]) == "set" { diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go index 3c7c40a..da1d3b3 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/query/query_service.go @@ -1,7 +1,10 @@ package query import ( + "regexp" + "strings" "NonRelDB/server/storage/inmemory" + "NonRelDB/server/log" ) // Get | Receives and key and returns value according its key. @@ -32,4 +35,25 @@ func Del(key string) string{ } else { return "Value with this key not found" } +} + +func Keys(pattern string) string { + s := inmemory.Storage + var keys []string + + for k, _ := range (*s) { + m, err := regexp.MatchString("\r" + pattern, k) + if err != nil { + log.Warning.Println(err.Error()) + } + if m { + keys = append(keys, k) + } + } + + if keys != nil { + return strings.Join(keys,",") + } else { + return "Keys with this pattern not found" + } } \ No newline at end of file From 6d62021f4c3b86e5cdfca98d8942ef457b01808b Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 23 Nov 2018 03:03:10 +0300 Subject: [PATCH 020/104] Keys with pattern feature completed --- server/storage/query/query_service.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go index da1d3b3..7f9feba 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/query/query_service.go @@ -41,12 +41,14 @@ func Keys(pattern string) string { s := inmemory.Storage var keys []string - for k, _ := range (*s) { - m, err := regexp.MatchString("\r" + pattern, k) - if err != nil { - log.Warning.Println(err.Error()) - } - if m { + re, err := regexp.Compile(pattern) + + if err != nil { + log.Warning.Println(err.Error()) + } + + for k := range (*s) { + if re.MatchString(k) { keys = append(keys, k) } } From abb8101af8d9d49011af8c0d520e5a98432b5d4c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 23 Nov 2018 03:35:55 +0300 Subject: [PATCH 021/104] Fixed crash on incorrect pattern in keys feature --- server/storage/query/query_service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go index 7f9feba..9e1068d 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/query/query_service.go @@ -45,6 +45,7 @@ func Keys(pattern string) string { if err != nil { log.Warning.Println(err.Error()) + return "Pattern is incorrect" } for k := range (*s) { From 576e95ef1e4156ce4b8156b4620a1f608f56444f Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 02:41:09 +0300 Subject: [PATCH 022/104] Added getter to storage & comment to Keys func --- server/storage/inmemory/inmemory_service.go | 13 +++++++++---- server/storage/query/query_service.go | 9 +++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 5666fd9..34919bd 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -8,12 +8,17 @@ import ( ) // Storage | Global variable for kv storage. -var Storage *map[string]string +var storage *map[string]string + +// GetStorage | Getter for storage. +func GetStorage() *map[string]string { + return storage +} // InitDBInMemory | Init kv db in memory. func InitDBInMemory(){ s := make(map[string]string) - Storage = &s + storage = &s log.Info.Println("DB successfully created in-memory") } @@ -37,12 +42,12 @@ func InitDBFromStorage(filename string){ j := file.OpenAndReadString(filename) jb := []byte(j) - Storage = json.UnpackFromJSON(jb) + storage = json.UnpackFromJSON(jb) } // SaveDBToStorage | Receives file name and saves inmemory storage to it. func SaveDBToStorage(filename string){ - jb := json.PackMapToJSON(*Storage) + jb := json.PackMapToJSON(*storage) j := string(jb) file.CreateAndWriteString(filename, j) diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go index 9e1068d..0a5ff81 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/query/query_service.go @@ -9,7 +9,7 @@ import ( // Get | Receives and key and returns value according its key. func Get(key string) string { - s := inmemory.Storage + s := inmemory.GetStorage() v := (*s)[key] if v != ""{ return v; @@ -20,14 +20,14 @@ func Get(key string) string { // Set | Set value according to key. func Set(key string, value string) string{ - s := inmemory.Storage + s := inmemory.GetStorage() (*s)[key] = value return "Value has changed" } // Del | Del value according to key. func Del(key string) string{ - s := inmemory.Storage + s := inmemory.GetStorage() v := (*s)[key] if v != "" { delete(*s,key) @@ -37,8 +37,9 @@ func Del(key string) string{ } } +// Keys | Returns keys which match to pattern. func Keys(pattern string) string { - s := inmemory.Storage + s := inmemory.GetStorage() var keys []string re, err := regexp.Compile(pattern) From 4af3fe8053d9b064cf51069a1978f5badb109b8b Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 02:45:52 +0300 Subject: [PATCH 023/104] Fixed 'set' query handle --- server/handler/handle_query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 513afc7..4b24081 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -15,9 +15,9 @@ func HandleQuery(q string) string { } else if strings.ToLower(p[0]) == "keys" { return query.Keys(p[1]) } - } else if len(p) == 3 { + } else if len(p) >= 3 { if strings.ToLower(p[0]) == "set" { - return query.Set(p[1], p[2]) + return query.Set(p[1], strings.Join(p[2:]," ")) } } return "Undefined query" From d45617fcf4bac704237b4cbb07f57f0447481275 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 15:53:17 +0300 Subject: [PATCH 024/104] Removed forgotten query service from util package dir --- server/util/query/query_service.go | 35 ------------------------------ 1 file changed, 35 deletions(-) delete mode 100644 server/util/query/query_service.go diff --git a/server/util/query/query_service.go b/server/util/query/query_service.go deleted file mode 100644 index b306fae..0000000 --- a/server/util/query/query_service.go +++ /dev/null @@ -1,35 +0,0 @@ -package query - -import ( - "NonRelDB/server/util/inmemory" -) - -// Get | Receives and key and returns value according its key. -func Get(key string) string { - s := inmemory.Storage - v := (*s)[key] - if v != ""{ - return v; - } else { - return "Value with this key not found" - } -} - -// Set | Set value according to key. -func Set(key string, value string) string{ - s := inmemory.Storage - (*s)[key] = value - return "Value has changed" -} - -// Del | Del value according to key. -func Del(key string) string{ - s := inmemory.Storage - v := (*s)[key] - if v != "" { - delete(*s,key) - return v; - } else { - return "Value with this key not found" - } -} \ No newline at end of file From 9876dad7ffb09145b1d06b1050656ecd7fb62995 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 16:03:09 +0300 Subject: [PATCH 025/104] Removed forgotten in-memory service from util package dir --- server/util/inmemory/inmemory_service.go | 29 ------------------------ 1 file changed, 29 deletions(-) delete mode 100644 server/util/inmemory/inmemory_service.go diff --git a/server/util/inmemory/inmemory_service.go b/server/util/inmemory/inmemory_service.go deleted file mode 100644 index 314b7ac..0000000 --- a/server/util/inmemory/inmemory_service.go +++ /dev/null @@ -1,29 +0,0 @@ -package inmemory - -import ( - "NonRelDB/server/util/json" - "NonRelDB/server/util/file" -) - -// Storage | Global variable for kv storage. -var Storage *map[string]string - -// InitDBInMemory | Init kv db in memory. -func InitDBInMemory(){ - s := make(map[string]string) - Storage = &s -} - -// InitDBFromStorage | Receives filename and load its content to inmemory storage. -func InitDBFromStorage(filename string){ - j := file.OpenAndReadString(filename) - jb := []byte(j) - Storage = json.UnpackFromJSON(jb) -} - -// SaveDBToStorage | Receives file name and saves inmemory storage to it. -func SaveDBToStorage(filename string){ - jb := json.PackMapToJSON(*Storage) - j := string(jb) - file.CreateAndWriteString(filename, j) -} From 71042a5e73ae796470808bdc7e700dc8d5f69a2c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 16:05:53 +0300 Subject: [PATCH 026/104] Fixed imports in query handler --- server/handler/handle_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 4b24081..20bc550 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -1,8 +1,8 @@ package handler import ( - "NonRelDB/server/storage/query" "strings" + "NonRelDB/server/storage/query" ) func HandleQuery(q string) string { From 76519ad0dc8f3fcf54f1b13e67296ec682e7d3ba Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 16:12:25 +0300 Subject: [PATCH 027/104] Started working on client side and moved util package out --- client/main.go | 5 +++++ {server/util => util}/file/file_read.go | 0 {server/util => util}/file/file_write.go | 0 {server/util => util}/json/json_decode.go | 0 {server/util => util}/json/json_encode.go | 0 5 files changed, 5 insertions(+) create mode 100644 client/main.go rename {server/util => util}/file/file_read.go (100%) rename {server/util => util}/file/file_write.go (100%) rename {server/util => util}/json/json_decode.go (100%) rename {server/util => util}/json/json_encode.go (100%) diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..39c4c98 --- /dev/null +++ b/client/main.go @@ -0,0 +1,5 @@ +package main + +func main(){ + +} \ No newline at end of file diff --git a/server/util/file/file_read.go b/util/file/file_read.go similarity index 100% rename from server/util/file/file_read.go rename to util/file/file_read.go diff --git a/server/util/file/file_write.go b/util/file/file_write.go similarity index 100% rename from server/util/file/file_write.go rename to util/file/file_write.go diff --git a/server/util/json/json_decode.go b/util/json/json_decode.go similarity index 100% rename from server/util/json/json_decode.go rename to util/json/json_decode.go diff --git a/server/util/json/json_encode.go b/util/json/json_encode.go similarity index 100% rename from server/util/json/json_encode.go rename to util/json/json_encode.go From ba8aaec910428a2671248e72418f27e87beec88f Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 17:54:38 +0300 Subject: [PATCH 028/104] Mov logger out of server & fixed imports for logger & added connection handler for clint & entry point for client --- client/handler/handle_connection.go | 44 +++++++++++++++++++++ client/main.go | 27 ++++++++++++- {server/log => log}/logger.go | 0 server/handler/handle_connection.go | 2 +- server/handler/handle_listener.go | 2 +- server/main.go | 2 +- server/storage/inmemory/inmemory_service.go | 6 +-- server/storage/query/query_service.go | 2 +- util/file/file_read.go | 2 +- util/file/file_write.go | 2 +- util/json/json_decode.go | 2 +- util/json/json_encode.go | 2 +- 12 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 client/handler/handle_connection.go rename {server/log => log}/logger.go (100%) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go new file mode 100644 index 0000000..d1abe6a --- /dev/null +++ b/client/handler/handle_connection.go @@ -0,0 +1,44 @@ +package handler + +import ( + "net" + "bufio" + "os" + "fmt" + "NonRelDB/log" +) + +func HandleConnection(c net.Conn){ + for { + consoleReader := bufio.NewReader(os.Stdin) + // netReader := bufio.NewReader(c) + // netWriter := bufio.NewWriter(c) + + fmt.Print("\nNonRelDB> ") + command, err := consoleReader.ReadString('\n') + command = command[:len(command)-1] + + if err != nil { + log.Error.Panicln(err.Error()) + } + + if command == "exit"{ + fmt.Println("Good bye") + return + } + + req := []byte(command) + c.Write(req) + + resp := make([]byte, 1024) + lenResp, err := c.Read(resp) + + if err != nil { + log.Error.Panicln(err.Error()) + } + + value := string(resp[:lenResp]) + + fmt.Println(value) + } +} \ No newline at end of file diff --git a/client/main.go b/client/main.go index 39c4c98..778e080 100644 --- a/client/main.go +++ b/client/main.go @@ -1,5 +1,30 @@ package main +import ( + "net" + "flag" + "NonRelDB/log" + "NonRelDB/client/handler" +) + +var host string +var port string + +func init(){ + flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") + flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") + flag.StringVar(&port, "port", "9090", "Defines host port") + flag.StringVar(&port, "p", "9090", "Defines host port") + flag.Parse() +} + func main(){ - + c, err := net.Dial("tcp", host + ":" + port) + defer c.Close() + + if err != nil { + log.Error.Panicln(err.Error()) + } + + handler.HandleConnection(c) } \ No newline at end of file diff --git a/server/log/logger.go b/log/logger.go similarity index 100% rename from server/log/logger.go rename to log/logger.go diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 4d5bde7..8d278bd 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -2,7 +2,7 @@ package handler import ( "net" - "NonRelDB/server/log" + "NonRelDB/log" ) func HandleConnection(c net.Conn){ diff --git a/server/handler/handle_listener.go b/server/handler/handle_listener.go index ca5aab4..dbd7b14 100644 --- a/server/handler/handle_listener.go +++ b/server/handler/handle_listener.go @@ -2,7 +2,7 @@ package handler import ( "net" - "NonRelDB/server/log" + "NonRelDB/log" ) func HandleListener(l net.Listener){ diff --git a/server/main.go b/server/main.go index 4c8fa03..2d4e83c 100644 --- a/server/main.go +++ b/server/main.go @@ -2,7 +2,7 @@ package main import ( "NonRelDB/server/handler" - "NonRelDB/server/log" + "NonRelDB/log" "NonRelDB/server/storage/inmemory" "flag" "net" diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 34919bd..f22a198 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -2,9 +2,9 @@ package inmemory import ( "os" - "NonRelDB/server/util/json" - "NonRelDB/server/util/file" - "NonRelDB/server/log" + "NonRelDB/util/json" + "NonRelDB/util/file" + "NonRelDB/log" ) // Storage | Global variable for kv storage. diff --git a/server/storage/query/query_service.go b/server/storage/query/query_service.go index 0a5ff81..1689d4c 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/query/query_service.go @@ -4,7 +4,7 @@ import ( "regexp" "strings" "NonRelDB/server/storage/inmemory" - "NonRelDB/server/log" + "NonRelDB/log" ) // Get | Receives and key and returns value according its key. diff --git a/util/file/file_read.go b/util/file/file_read.go index c3f40ea..b8d7ab3 100644 --- a/util/file/file_read.go +++ b/util/file/file_read.go @@ -2,7 +2,7 @@ package file import ( "io/ioutil" - "NonRelDB/server/log" + "NonRelDB/log" ) // OpenAndReadString | receives file name, reads this file and returns its string content. diff --git a/util/file/file_write.go b/util/file/file_write.go index 7877450..128b115 100644 --- a/util/file/file_write.go +++ b/util/file/file_write.go @@ -2,7 +2,7 @@ package file import ( "os" - "NonRelDB/server/log" + "NonRelDB/log" ) // CreateAndWriteString | Creates file and writes string to it. diff --git a/util/json/json_decode.go b/util/json/json_decode.go index 11b375e..a1492a3 100644 --- a/util/json/json_decode.go +++ b/util/json/json_decode.go @@ -2,7 +2,7 @@ package json import ( "encoding/json" - "NonRelDB/server/log" + "NonRelDB/log" ) // UnpackFromJSON | Receives json bytes and returns map pointer. diff --git a/util/json/json_encode.go b/util/json/json_encode.go index e1b95b9..7d0b931 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -2,7 +2,7 @@ package json import ( "encoding/json" - "NonRelDB/server/log" + "NonRelDB/log" ) // PackToJSON | Receives string key and interface value and returns json bytes. From 9e479ae0baf1b2ee9bfbd45abb0f0a36399929b6 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 24 Nov 2018 18:42:26 +0300 Subject: [PATCH 029/104] Added exit command for client & server exit command handle --- client/handler/handle_connection.go | 1 + server/handler/handle_connection.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index d1abe6a..8e2667b 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -24,6 +24,7 @@ func HandleConnection(c net.Conn){ if command == "exit"{ fmt.Println("Good bye") + c.Write([]byte(command)) return } diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 8d278bd..ab40a3b 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -17,6 +17,11 @@ func HandleConnection(c net.Conn){ } query := string(req[:lenReq]) + + if query == "exit" { + log.Info.Printf("%s disconnected from server",c.RemoteAddr().String()) + return + } log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), query) resp_str := HandleQuery(query) From 0eccdaa0fa09ca685f70f31eeab9701fda588eed Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 27 Nov 2018 20:03:27 +0300 Subject: [PATCH 030/104] Removed query service & instead of added sync map with interface & adopted existing code to it --- server/handler/handle_query.go | 10 ++-- server/storage/inmemory/inmemory_service.go | 22 +++++--- .../query_service.go => sync/sync_map.go} | 51 +++++++++++++------ 3 files changed, 56 insertions(+), 27 deletions(-) rename server/storage/{query/query_service.go => sync/sync_map.go} (51%) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 20bc550..26df7d1 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -2,22 +2,22 @@ package handler import ( "strings" - "NonRelDB/server/storage/query" + "NonRelDB/server/storage/inmemory" ) func HandleQuery(q string) string { p := strings.Split(q, " ") if len(p) == 2 { if strings.ToLower(p[0]) == "get" { - return query.Get(p[1]) + return inmemory.GetStorage().Get(p[1]) } else if strings.ToLower(p[0]) == "del" { - return query.Del(p[1]) + return inmemory.GetStorage().Del(p[1]) } else if strings.ToLower(p[0]) == "keys" { - return query.Keys(p[1]) + return inmemory.GetStorage().Keys(p[1]) } } else if len(p) >= 3 { if strings.ToLower(p[0]) == "set" { - return query.Set(p[1], strings.Join(p[2:]," ")) + return inmemory.GetStorage().Set(p[1], strings.Join(p[2:]," ")) } } return "Undefined query" diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index f22a198..48188f2 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -5,20 +5,28 @@ import ( "NonRelDB/util/json" "NonRelDB/util/file" "NonRelDB/log" + "NonRelDB/server/storage/sync" ) // Storage | Global variable for kv storage. -var storage *map[string]string + +var storage sync.Map // GetStorage | Getter for storage. -func GetStorage() *map[string]string { - return storage +func GetStorage() *sync.Map { + return &storage +} + +// SetStorage | Setter for storage +func SetStorage(sm sync.Map){ + storage = sm } // InitDBInMemory | Init kv db in memory. func InitDBInMemory(){ - s := make(map[string]string) - storage = &s + storage := sync.Map{} + m := make(map[string]string) + storage.SetMap(&m) log.Info.Println("DB successfully created in-memory") } @@ -42,12 +50,12 @@ func InitDBFromStorage(filename string){ j := file.OpenAndReadString(filename) jb := []byte(j) - storage = json.UnpackFromJSON(jb) + storage.SetMap(json.UnpackFromJSON(jb)) } // SaveDBToStorage | Receives file name and saves inmemory storage to it. func SaveDBToStorage(filename string){ - jb := json.PackMapToJSON(*storage) + jb := json.PackMapToJSON((*storage.GetMap())) j := string(jb) file.CreateAndWriteString(filename, j) diff --git a/server/storage/query/query_service.go b/server/storage/sync/sync_map.go similarity index 51% rename from server/storage/query/query_service.go rename to server/storage/sync/sync_map.go index 1689d4c..44353ac 100644 --- a/server/storage/query/query_service.go +++ b/server/storage/sync/sync_map.go @@ -1,16 +1,33 @@ -package query +package sync import ( + "sync" "regexp" "strings" - "NonRelDB/server/storage/inmemory" "NonRelDB/log" ) +// Map | Map synchronized with mutex. +type Map struct { + sync.Mutex + m *map[string]string +} + +// GetMap | Getter for map. +func (sm *Map) GetMap() *map[string]string { + return sm.m +} + +// SetMap | Setter for map. +func (sm *Map) SetMap(m *map[string]string){ + sm.m = m +} + // Get | Receives and key and returns value according its key. -func Get(key string) string { - s := inmemory.GetStorage() - v := (*s)[key] +func (sm *Map) Get(key string) string { + sm.Lock() + defer sm.Unlock() + v := (*sm.m)[key] if v != ""{ return v; } else { @@ -19,18 +36,20 @@ func Get(key string) string { } // Set | Set value according to key. -func Set(key string, value string) string{ - s := inmemory.GetStorage() - (*s)[key] = value +func (sm *Map) Set(key string, value string) string{ + sm.Lock() + defer sm.Unlock() + (*sm.m)[key] = value return "Value has changed" } // Del | Del value according to key. -func Del(key string) string{ - s := inmemory.GetStorage() - v := (*s)[key] +func (sm *Map) Del(key string) string{ + sm.Lock() + defer sm.Unlock() + v := (*sm.m)[key] if v != "" { - delete(*s,key) + delete((*sm.m),key) return v; } else { return "Value with this key not found" @@ -38,8 +57,10 @@ func Del(key string) string{ } // Keys | Returns keys which match to pattern. -func Keys(pattern string) string { - s := inmemory.GetStorage() +func (sm *Map) Keys(pattern string) string { + sm.Lock() + defer sm.Unlock() + var keys []string re, err := regexp.Compile(pattern) @@ -49,7 +70,7 @@ func Keys(pattern string) string { return "Pattern is incorrect" } - for k := range (*s) { + for k := range ((*sm.m)) { if re.MatchString(k) { keys = append(keys, k) } From 31e6b92139d029c0a06e4b5594d92ae4bb2bf022 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 29 Nov 2018 20:38:25 +0300 Subject: [PATCH 031/104] Added writing & reading of unknown length messages between server and clients --- client/handler/handle_connection.go | 22 ++++++++-------------- server/handler/handle_connection.go | 18 +++++++++--------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 8e2667b..0142736 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -1,6 +1,7 @@ package handler import ( + "strings" "net" "bufio" "os" @@ -9,14 +10,11 @@ import ( ) func HandleConnection(c net.Conn){ + consoleReader := bufio.NewReader(os.Stdin) + netReader := bufio.NewReader(c) for { - consoleReader := bufio.NewReader(os.Stdin) - // netReader := bufio.NewReader(c) - // netWriter := bufio.NewWriter(c) - - fmt.Print("\nNonRelDB> ") + fmt.Print("NonRelDB> ") command, err := consoleReader.ReadString('\n') - command = command[:len(command)-1] if err != nil { log.Error.Panicln(err.Error()) @@ -24,22 +22,18 @@ func HandleConnection(c net.Conn){ if command == "exit"{ fmt.Println("Good bye") - c.Write([]byte(command)) + fmt.Print(c, command) return } - req := []byte(command) - c.Write(req) + fmt.Fprintf(c, strings.Trim(command," ")) - resp := make([]byte, 1024) - lenResp, err := c.Read(resp) + resp, err := netReader.ReadString('\n') if err != nil { log.Error.Panicln(err.Error()) } - value := string(resp[:lenResp]) - - fmt.Println(value) + fmt.Println(resp) } } \ No newline at end of file diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index ab40a3b..9e6f182 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -1,32 +1,32 @@ package handler import ( + "strings" + "bufio" "net" "NonRelDB/log" ) func HandleConnection(c net.Conn){ defer c.Close() + netReader := bufio.NewReader(c) for { - req := make([]byte,1024) - lenReq, err := c.Read(req) + query, err := netReader.ReadString('\n') if err != nil { log.Error.Println(err.Error()) return - } - - query := string(req[:lenReq]) + } if query == "exit" { log.Info.Printf("%s disconnected from server",c.RemoteAddr().String()) return } + log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), query) - resp_str := HandleQuery(query) - resp := []byte(resp_str) - log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp_str) - c.Write(resp) + resp := HandleQuery(strings.TrimSpace(query)) + log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) + c.Write([]byte(resp + "\n")) } } \ No newline at end of file From 3762e787d8c495d30512959904f29d26e9fb9209 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 30 Nov 2018 21:06:30 +0300 Subject: [PATCH 032/104] Removed indent from json encoder & added dump feature --- server/handler/handle_query.go | 6 ++++++ util/json/json_encode.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 26df7d1..945f3a0 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -3,10 +3,16 @@ package handler import ( "strings" "NonRelDB/server/storage/inmemory" + "NonRelDB/util/json" ) func HandleQuery(q string) string { p := strings.Split(q, " ") + if len(p) == 1 { + if strings.ToLower(p[0]) == "dump"{ + return string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) + } + } if len(p) == 2 { if strings.ToLower(p[0]) == "get" { return inmemory.GetStorage().Get(p[1]) diff --git a/util/json/json_encode.go b/util/json/json_encode.go index 7d0b931..46d6b7a 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -8,7 +8,7 @@ import ( // PackToJSON | Receives string key and interface value and returns json bytes. func PackToJSON(key string, value string) []byte{ m := map[string]string{key : value} - b, err := json.MarshalIndent(m,""," ") + b, err := json.Marshal(m) if err != nil{ log.Error.Println(err.Error()) } @@ -17,7 +17,7 @@ func PackToJSON(key string, value string) []byte{ // PackMapToJSON | Receives map and returns json bytes. func PackMapToJSON(m map[string]string) []byte{ - b, err := json.MarshalIndent(m,""," ") + b, err := json.Marshal(m) if err != nil{ log.Error.Println(err.Error()) } From 1e6390e0256c178e45f2391068b662236cb38d21 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sun, 2 Dec 2018 20:16:24 +0300 Subject: [PATCH 033/104] Completed dump & restore feature and added .gitignore --- .gitignore | 16 ++++------------ client/handler/handle_connection.go | 7 +++---- client/main.go | 26 ++++++++++++++++++++++++++ server/handler/handle_connection.go | 16 +++++++++++++++- server/handler/handle_query.go | 6 ------ 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index f1c181e..ccfa6e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,4 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +client/dump.json +client/main +server/main +server/storage.json diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 0142736..1cae021 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -1,7 +1,6 @@ package handler import ( - "strings" "net" "bufio" "os" @@ -20,13 +19,13 @@ func HandleConnection(c net.Conn){ log.Error.Panicln(err.Error()) } - if command == "exit"{ + if command == "exit\n"{ fmt.Println("Good bye") - fmt.Print(c, command) + fmt.Fprintf(c, command) return } - fmt.Fprintf(c, strings.Trim(command," ")) + fmt.Fprintf(c, command) resp, err := netReader.ReadString('\n') diff --git a/client/main.go b/client/main.go index 778e080..b98610c 100644 --- a/client/main.go +++ b/client/main.go @@ -1,6 +1,9 @@ package main import ( + "NonRelDB/util/file" + "fmt" + "bufio" "net" "flag" "NonRelDB/log" @@ -9,12 +12,18 @@ import ( var host string var port string +var dump bool +var restore bool +var location string func init(){ flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") flag.StringVar(&port, "port", "9090", "Defines host port") flag.StringVar(&port, "p", "9090", "Defines host port") + flag.BoolVar(&dump, "dump", false, "Requests db dump in json format from server") + flag.BoolVar(&restore, "restore", false, "Restores received dump to file") + flag.StringVar(&location, "location", "dump.json", "Defines location of dump") flag.Parse() } @@ -26,5 +35,22 @@ func main(){ log.Error.Panicln(err.Error()) } + if dump { + // c.Write([]byte("dump\n")) + fmt.Fprintf(c, "dump\n") + dbDump, err := bufio.NewReader(c).ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Println(dbDump) + + if restore { + file.CreateAndWriteString(location, dbDump) + } + return + } + handler.HandleConnection(c) } \ No newline at end of file diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 9e6f182..8e18947 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -1,17 +1,26 @@ package handler import ( + "fmt" "strings" "bufio" "net" "NonRelDB/log" + "NonRelDB/util/json" + "NonRelDB/server/storage/inmemory" ) func HandleConnection(c net.Conn){ defer c.Close() + netReader := bufio.NewReader(c) + for { + query, err := netReader.ReadString('\n') + query = strings.TrimSuffix(query,"\n") + + fmt.Println("[" + query + "]") if err != nil { log.Error.Println(err.Error()) @@ -21,12 +30,17 @@ func HandleConnection(c net.Conn){ if query == "exit" { log.Info.Printf("%s disconnected from server",c.RemoteAddr().String()) return + } else if query == "dump" { + log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) + dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) + fmt.Fprintf(c, dbDump + "\n") + return } log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), query) resp := HandleQuery(strings.TrimSpace(query)) log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) - c.Write([]byte(resp + "\n")) + fmt.Fprintf(c, resp + "\n") } } \ No newline at end of file diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 945f3a0..26df7d1 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -3,16 +3,10 @@ package handler import ( "strings" "NonRelDB/server/storage/inmemory" - "NonRelDB/util/json" ) func HandleQuery(q string) string { p := strings.Split(q, " ") - if len(p) == 1 { - if strings.ToLower(p[0]) == "dump"{ - return string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) - } - } if len(p) == 2 { if strings.ToLower(p[0]) == "get" { return inmemory.GetStorage().Get(p[1]) From f127519e16325e9c4e4bb91ad167193b5e482782 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 3 Dec 2018 04:42:01 +0300 Subject: [PATCH 034/104] Added collection util package & slice util with index func --- util/collection/slice_util.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 util/collection/slice_util.go diff --git a/util/collection/slice_util.go b/util/collection/slice_util.go new file mode 100644 index 0000000..06260d4 --- /dev/null +++ b/util/collection/slice_util.go @@ -0,0 +1,23 @@ +package collection + +import ( + "net" +) + +func Index(slice []interface{}, value interface{}) int{ + for index, slice_value := range slice { + if slice_value == value { + return index + } + } + return -1 +} + +func ConnIndex(slice []net.Conn, value net.Conn) int{ + for index, slice_value := range slice { + if slice_value == value { + return index + } + } + return -1 +} \ No newline at end of file From cc9ec1a109b05f8e57df1021d39d0df2cafe8cf8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 3 Dec 2018 04:44:40 +0300 Subject: [PATCH 035/104] Added hardcoded topic with subscribe & publish features --- client/handler/handle_connection.go | 9 +++++- client/handler/handle_topic.go | 36 +++++++++++++++++++++++ server/handler/handle_connection.go | 18 ++++++++++-- server/topic/topic_service.go | 44 +++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 client/handler/handle_topic.go create mode 100644 server/topic/topic_service.go diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 1cae021..7f41bc1 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -1,6 +1,7 @@ package handler import ( + "strings" "net" "bufio" "os" @@ -23,8 +24,14 @@ func HandleConnection(c net.Conn){ fmt.Println("Good bye") fmt.Fprintf(c, command) return + } else if strings.Contains(command, "subscribe"){ + fmt.Fprintf(c, command) + HandleTopic(c, *netReader, strings.Split(command, " ")[1]) + } else if strings.Contains(command, "publish"){ + fmt.Fprintf(c, command) + continue } - + fmt.Fprintf(c, command) resp, err := netReader.ReadString('\n') diff --git a/client/handler/handle_topic.go b/client/handler/handle_topic.go new file mode 100644 index 0000000..0d92842 --- /dev/null +++ b/client/handler/handle_topic.go @@ -0,0 +1,36 @@ +package handler + +import ( + "NonRelDB/log" + "fmt" + "bufio" + "net" + "os" + "os/signal" + "syscall" +) + +func quit(c net.Conn, topic string) { + sign := make(chan os.Signal, 2) + signal.Notify(sign, os.Interrupt, syscall.SIGTERM) + go func() { + <-sign + fmt.Println("Ctrl+C pressed in Terminal") + fmt.Fprintf(c, "unsubscribe " + topic + "\n") + os.Exit(0) + }() +} + +func HandleTopic(c net.Conn, r bufio.Reader, topic string){ + quit(c, topic) + fmt.Printf("Reading messages from %s... (press Ctrl + C to stop)\n", topic) + for { + msg, err := r.ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Print(msg) + } +} \ No newline at end of file diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 8e18947..bb5e4e8 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -8,6 +8,7 @@ import ( "NonRelDB/log" "NonRelDB/util/json" "NonRelDB/server/storage/inmemory" + "NonRelDB/server/topic" ) func HandleConnection(c net.Conn){ @@ -20,8 +21,6 @@ func HandleConnection(c net.Conn){ query, err := netReader.ReadString('\n') query = strings.TrimSuffix(query,"\n") - fmt.Println("[" + query + "]") - if err != nil { log.Error.Println(err.Error()) return @@ -35,6 +34,21 @@ func HandleConnection(c net.Conn){ dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) fmt.Fprintf(c, dbDump + "\n") return + } else if strings.Contains(query, "subscribe") || strings.Contains(query, "publish") { + qp := strings.Split(query, " ") + if len(qp) == 2 { + if strings.ToLower(qp[0]) == "subscribe" { + topic.Subscribe(qp[1], c) + } else if strings.ToLower(qp[0]) == "unsubscribe" { + topic.Unsubscribe(qp[1], c) + return + } + } else if len(qp) == 3 { + if strings.ToLower(qp[0]) == "publish" { + topic.Publish(qp[1],qp[2]) + } + } + continue } log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), query) diff --git a/server/topic/topic_service.go b/server/topic/topic_service.go new file mode 100644 index 0000000..cefe326 --- /dev/null +++ b/server/topic/topic_service.go @@ -0,0 +1,44 @@ +package topic + +import ( + "NonRelDB/log" + "fmt" + "net" + "NonRelDB/util/collection" +) + +var topics map[string][]net.Conn + +func init(){ + topics = make(map[string][]net.Conn) +} + +func Subscribe(name string, c net.Conn){ + if topics[name] == nil { + topics[name] = make([]net.Conn, 10) + } + topics[name] = append(topics[name], c) + log.Info.Printf("%s just subscribed %s", c.RemoteAddr().String(), name) +} + +func Unsubscribe(name string, c net.Conn) { + if topics[name] != nil || len(topics) != 0{ + index := collection.ConnIndex(topics[name], c) + if index != -1 { + topics[name][index] = nil + log.Info.Printf("%s just unsubscribed %s", c.RemoteAddr().String(), name) + } + } +} + +func Publish(name string, msg string){ + if topics[name] != nil || len(topics) != 0 { + log.Info.Printf("%s just published in %s", msg , name) + for _, listener := range topics[name] { + str := fmt.Sprintf("[%s]: %s", name, msg) + if (listener != nil){ + fmt.Fprintf(listener, str + "\n") + } + } + } +} \ No newline at end of file From 8675b959222882ef9767d661b1527ad453d2b8ed Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 3 Dec 2018 05:00:08 +0300 Subject: [PATCH 036/104] Added functions with indent for pack to json --- util/json/json_encode.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/util/json/json_encode.go b/util/json/json_encode.go index 46d6b7a..dd19e30 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -15,6 +15,15 @@ func PackToJSON(key string, value string) []byte{ return b } +func PackToJSONIndent(key string, value string) []byte{ + m := map[string]string{key : value} + b, err := json.MarshalIndent(m,"", " ") + if err != nil{ + log.Error.Println(err.Error()) + } + return b +} + // PackMapToJSON | Receives map and returns json bytes. func PackMapToJSON(m map[string]string) []byte{ b, err := json.Marshal(m) @@ -22,4 +31,12 @@ func PackMapToJSON(m map[string]string) []byte{ log.Error.Println(err.Error()) } return b +} + +func PackMapToJSONIndent(m map[string]string) []byte{ + b, err := json.MarshalIndent(m, "", " ") + if err != nil{ + log.Error.Println(err.Error()) + } + return b } \ No newline at end of file From 92ec5cdefaa8778406fde39a7fec42f6dcd8f090 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 3 Dec 2018 07:14:11 +0300 Subject: [PATCH 037/104] Server's query handler reworked using regexp --- server/handler/handle_query.go | 65 +++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 26df7d1..6cd89c6 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -1,24 +1,63 @@ package handler import ( + "fmt" "strings" + "regexp" "NonRelDB/server/storage/inmemory" ) +var keyGetDelReg *regexp.Regexp +var keySetReg *regexp.Regexp +var valueReg *regexp.Regexp +var getReg *regexp.Regexp +var setReg *regexp.Regexp +var delReg *regexp.Regexp +var keysReg *regexp.Regexp + +func init(){ + + keyGetDelReg = regexp.MustCompile("\\s(.*)$") + + keySetReg = regexp.MustCompile("\\s(.*)\\s") + + valueReg = regexp.MustCompile("\"(.*)\"$") + + getReg = regexp.MustCompile("^get\\s(.*)$") + + setReg = regexp.MustCompile("^set\\s(.*)\\s\"(.*)\"$") + + delReg = regexp.MustCompile("^del\\s(.*)$") + + keysReg = regexp.MustCompile("^keys\\s\"(.*?)\"$") +} + func HandleQuery(q string) string { - p := strings.Split(q, " ") - if len(p) == 2 { - if strings.ToLower(p[0]) == "get" { - return inmemory.GetStorage().Get(p[1]) - } else if strings.ToLower(p[0]) == "del" { - return inmemory.GetStorage().Del(p[1]) - } else if strings.ToLower(p[0]) == "keys" { - return inmemory.GetStorage().Keys(p[1]) - } - } else if len(p) >= 3 { - if strings.ToLower(p[0]) == "set" { - return inmemory.GetStorage().Set(p[1], strings.Join(p[2:]," ")) - } + fmt.Println("[" + q + "]") + + if getReg.MatchString(q) { + key := strings.Trim(keyGetDelReg.FindString(q), " ") + fmt.Println("[" + key + "]") + return inmemory.GetStorage().Get(key) + + } else if setReg.MatchString(q) { + key := strings.Trim(keySetReg.FindString(q), " ") + value := strings.Trim(valueReg.FindString(q), "\"") + fmt.Println("[" + key + "]") + fmt.Println("[" + value + "]") + return inmemory.GetStorage().Set(key, value ) + + } else if delReg.MatchString(q) { + key := strings.Trim(keyGetDelReg.FindString(q), " ") + fmt.Println("[" + key + "]") + return inmemory.GetStorage().Del(key) + + } else if keysReg.MatchString(q) { + reg := strings.Trim(valueReg.FindString(q),"\"") + fmt.Println("[" + reg + "]") + return inmemory.GetStorage().Keys(reg) + } + return "Undefined query" } From 13c4c386a23ed02fdd5406a768f4fc29f724a13c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 3 Dec 2018 07:16:33 +0300 Subject: [PATCH 038/104] Removed print for debug in server's query handler --- server/handler/handle_query.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 6cd89c6..cc816a6 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -33,8 +33,6 @@ func init(){ } func HandleQuery(q string) string { - fmt.Println("[" + q + "]") - if getReg.MatchString(q) { key := strings.Trim(keyGetDelReg.FindString(q), " ") fmt.Println("[" + key + "]") From 834e03fff97b80acb179ab38e08ceb62a3650cc7 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 4 Dec 2018 05:37:12 +0300 Subject: [PATCH 039/104] Server's connection handler reworked using regexp & subcribe/publish working incorrect for a while --- server/handler/handle_connection.go | 81 +++++++++++++++++++---------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index bb5e4e8..7121812 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -1,6 +1,7 @@ package handler import ( + "regexp" "fmt" "strings" "bufio" @@ -11,6 +12,28 @@ import ( "NonRelDB/server/topic" ) +var queryReg *regexp.Regexp +var exitReg *regexp.Regexp +var dumpReg *regexp.Regexp +var subscribeReg *regexp.Regexp +var unsubscribeReg *regexp.Regexp +var topicNameReg *regexp.Regexp +var publishReg *regexp.Regexp +var publishTopicNameReg *regexp.Regexp +var msgReg *regexp.Regexp + +func init(){ + queryReg = regexp.MustCompile("^(get\\s(.*))|(set\\s(.*)\\s\"(.*)\")|(del\\s(.*))|(keys\\s\"(.*?)\")$") + exitReg = regexp.MustCompile("^exit$") + dumpReg = regexp.MustCompile("^dump$") + subscribeReg = regexp.MustCompile("^subscribe\\s(.*)$") + unsubscribeReg = regexp.MustCompile("^unsubscrube\\s(.*)$") + topicNameReg = regexp.MustCompile("\\s(.*)$") + publishReg = regexp.MustCompile("^publish\\s(.*)\\s\"(.*)\"$") + publishTopicNameReg = regexp.MustCompile("\\s(.*)\\s") + msgReg = regexp.MustCompile("\"(.*)\"$") +} + func HandleConnection(c net.Conn){ defer c.Close() @@ -18,43 +41,45 @@ func HandleConnection(c net.Conn){ for { - query, err := netReader.ReadString('\n') - query = strings.TrimSuffix(query,"\n") + req, err := netReader.ReadString('\n') + req = strings.TrimSuffix(req,"\n") if err != nil { log.Error.Println(err.Error()) return } - if query == "exit" { - log.Info.Printf("%s disconnected from server",c.RemoteAddr().String()) - return - } else if query == "dump" { - log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) + log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), req) + + if queryReg.MatchString(req){ + resp := HandleQuery(req) + fmt.Fprintf(c, resp + "\n") + log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) + + } else if exitReg.MatchString(req){ + log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) + + } else if dumpReg.MatchString(req){ dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) fmt.Fprintf(c, dbDump + "\n") - return - } else if strings.Contains(query, "subscribe") || strings.Contains(query, "publish") { - qp := strings.Split(query, " ") - if len(qp) == 2 { - if strings.ToLower(qp[0]) == "subscribe" { - topic.Subscribe(qp[1], c) - } else if strings.ToLower(qp[0]) == "unsubscribe" { - topic.Unsubscribe(qp[1], c) - return - } - } else if len(qp) == 3 { - if strings.ToLower(qp[0]) == "publish" { - topic.Publish(qp[1],qp[2]) - } - } + log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) + + } else if subscribeReg.MatchString(req){ + topicName := strings.Trim(topicNameReg.FindString(req), " ") + topic.Subscribe(topicName, c) + continue + + } else if unsubscribeReg.MatchString(req){ + topicName := strings.Trim(topicNameReg.FindString(req), " ") + topic.Unsubscribe(topicName, c) + return + + } else if publishReg.MatchString(req){ + topicName := strings.Trim(publishTopicNameReg.FindString(req), " ") + msg := strings.Trim(msgReg.FindString(req), "\"") + topic.Publish(topicName, msg) continue + } - - log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), query) - - resp := HandleQuery(strings.TrimSpace(query)) - log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) - fmt.Fprintf(c, resp + "\n") } } \ No newline at end of file From bb0b97deab1731d50307dab3a7af163002a5d023 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 5 Dec 2018 00:36:07 +0300 Subject: [PATCH 040/104] Added regex util --- util/regex/regex_util.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 util/regex/regex_util.go diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go new file mode 100644 index 0000000..b555803 --- /dev/null +++ b/util/regex/regex_util.go @@ -0,0 +1,17 @@ +package regex + +import ( + "regexp" +) + +var ValueReg *regexp.Regexp +var QueryReg *regexp.Regexp +var ExitReg *regexp.Regexp +var DumpReg *regexp.Regexp + +func init(){ + ValueReg = regexp.MustCompile("\"(.*)\"") + QueryReg = regexp.MustCompile("^(get|set|del|keys)") + ExitReg = regexp.MustCompile("^exit$") + DumpReg = regexp.MustCompile("^dump$") +} \ No newline at end of file From 8f2270de81c60da5c5f7c04e725a6d1ba3eae47c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 5 Dec 2018 00:37:13 +0300 Subject: [PATCH 041/104] Reworked connection & query handle using mix of regexp and str split --- server/handler/handle_connection.go | 94 ++++++++++++++--------------- server/handler/handle_query.go | 69 ++++++--------------- 2 files changed, 63 insertions(+), 100 deletions(-) diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 7121812..b2069ce 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -1,7 +1,6 @@ package handler import ( - "regexp" "fmt" "strings" "bufio" @@ -10,28 +9,50 @@ import ( "NonRelDB/util/json" "NonRelDB/server/storage/inmemory" "NonRelDB/server/topic" + "NonRelDB/util/regex" ) -var queryReg *regexp.Regexp -var exitReg *regexp.Regexp -var dumpReg *regexp.Regexp -var subscribeReg *regexp.Regexp -var unsubscribeReg *regexp.Regexp -var topicNameReg *regexp.Regexp -var publishReg *regexp.Regexp -var publishTopicNameReg *regexp.Regexp -var msgReg *regexp.Regexp +func HandleRequest(req string, c net.Conn) rune{ + if regex.QueryReg.MatchString(req){ + resp:= HandleQuery(req) + SentResponse(resp, c) + return 'c' -func init(){ - queryReg = regexp.MustCompile("^(get\\s(.*))|(set\\s(.*)\\s\"(.*)\")|(del\\s(.*))|(keys\\s\"(.*?)\")$") - exitReg = regexp.MustCompile("^exit$") - dumpReg = regexp.MustCompile("^dump$") - subscribeReg = regexp.MustCompile("^subscribe\\s(.*)$") - unsubscribeReg = regexp.MustCompile("^unsubscrube\\s(.*)$") - topicNameReg = regexp.MustCompile("\\s(.*)$") - publishReg = regexp.MustCompile("^publish\\s(.*)\\s\"(.*)\"$") - publishTopicNameReg = regexp.MustCompile("\\s(.*)\\s") - msgReg = regexp.MustCompile("\"(.*)\"$") + } else if regex.ExitReg.MatchString(req){ + log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) + return 'r' + + } else if regex.DumpReg.MatchString(req){ + dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) + fmt.Fprintf(c, dbDump + "\n") + log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) + return 'r' + + } else { + reqParts := strings.Split(req, " ")[:2] + + switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ + case "subscribe":{ + topic.Subscribe(reqParts[1], c) + return 'c' + } + case "unsubscribe":{ + topic.Unsubscribe(reqParts[1], c) + return 'r' + } + case "publish":{ + msg := regex.ValueReg.FindString(req) + topic.Publish(reqParts[1],msg) + return 'c' + } + } + } + return 'c' +} + +func SentResponse(resp string, c net.Conn){ + fmt.Fprintf(c, resp + "\n") + log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) } func HandleConnection(c net.Conn){ @@ -40,7 +61,6 @@ func HandleConnection(c net.Conn){ netReader := bufio.NewReader(c) for { - req, err := netReader.ReadString('\n') req = strings.TrimSuffix(req,"\n") @@ -51,35 +71,9 @@ func HandleConnection(c net.Conn){ log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), req) - if queryReg.MatchString(req){ - resp := HandleQuery(req) - fmt.Fprintf(c, resp + "\n") - log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) - - } else if exitReg.MatchString(req){ - log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) - - } else if dumpReg.MatchString(req){ - dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) - fmt.Fprintf(c, dbDump + "\n") - log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) - - } else if subscribeReg.MatchString(req){ - topicName := strings.Trim(topicNameReg.FindString(req), " ") - topic.Subscribe(topicName, c) - continue - - } else if unsubscribeReg.MatchString(req){ - topicName := strings.Trim(topicNameReg.FindString(req), " ") - topic.Unsubscribe(topicName, c) - return - - } else if publishReg.MatchString(req){ - topicName := strings.Trim(publishTopicNameReg.FindString(req), " ") - msg := strings.Trim(msgReg.FindString(req), "\"") - topic.Publish(topicName, msg) - continue - + switch handleCtx := HandleRequest(req, c); handleCtx{ + case 'r': return + case 'c': continue } } } \ No newline at end of file diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index cc816a6..dea4a75 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -1,60 +1,29 @@ package handler import ( - "fmt" "strings" - "regexp" "NonRelDB/server/storage/inmemory" + "NonRelDB/util/regex" ) -var keyGetDelReg *regexp.Regexp -var keySetReg *regexp.Regexp -var valueReg *regexp.Regexp -var getReg *regexp.Regexp -var setReg *regexp.Regexp -var delReg *regexp.Regexp -var keysReg *regexp.Regexp - -func init(){ - - keyGetDelReg = regexp.MustCompile("\\s(.*)$") - - keySetReg = regexp.MustCompile("\\s(.*)\\s") - - valueReg = regexp.MustCompile("\"(.*)\"$") - - getReg = regexp.MustCompile("^get\\s(.*)$") - - setReg = regexp.MustCompile("^set\\s(.*)\\s\"(.*)\"$") - - delReg = regexp.MustCompile("^del\\s(.*)$") - - keysReg = regexp.MustCompile("^keys\\s\"(.*?)\"$") -} - -func HandleQuery(q string) string { - if getReg.MatchString(q) { - key := strings.Trim(keyGetDelReg.FindString(q), " ") - fmt.Println("[" + key + "]") - return inmemory.GetStorage().Get(key) - - } else if setReg.MatchString(q) { - key := strings.Trim(keySetReg.FindString(q), " ") - value := strings.Trim(valueReg.FindString(q), "\"") - fmt.Println("[" + key + "]") - fmt.Println("[" + value + "]") - return inmemory.GetStorage().Set(key, value ) - - } else if delReg.MatchString(q) { - key := strings.Trim(keyGetDelReg.FindString(q), " ") - fmt.Println("[" + key + "]") - return inmemory.GetStorage().Del(key) - - } else if keysReg.MatchString(q) { - reg := strings.Trim(valueReg.FindString(q),"\"") - fmt.Println("[" + reg + "]") - return inmemory.GetStorage().Keys(reg) - +func HandleQuery(query string) string { + queryParts := strings.Split(query, " ")[:2] + + switch queryCtx := strings.ToLower(queryParts[0]); queryCtx{ + case "get":{ + return inmemory.GetStorage().Get(queryParts[1]) + } + case "set":{ + value := strings.Trim(regex.ValueReg.FindString(query),"\"") + return inmemory.GetStorage().Set(queryParts[1], value) + } + case "del":{ + return inmemory.GetStorage().Del(queryParts[1]) + } + case "keys":{ + pattern := regex.ValueReg.FindString(query) + return inmemory.GetStorage().Keys(pattern) + } } return "Undefined query" From 4b08f2921036d2cbab1c9d3fe9f6a99049a8314d Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 5 Dec 2018 01:06:45 +0300 Subject: [PATCH 042/104] Refactored client's connection handler --- client/handler/handle_connection.go | 48 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 7f41bc1..56adfb3 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -7,39 +7,51 @@ import ( "os" "fmt" "NonRelDB/log" + "NonRelDB/util/regex" ) +func SentRequest(req string, c net.Conn){ + fmt.Fprintf(c, req) +} + func HandleConnection(c net.Conn){ consoleReader := bufio.NewReader(os.Stdin) netReader := bufio.NewReader(c) for { fmt.Print("NonRelDB> ") - command, err := consoleReader.ReadString('\n') + req, err := consoleReader.ReadString('\n') if err != nil { log.Error.Panicln(err.Error()) } - - if command == "exit\n"{ + + if regex.QueryReg.MatchString(req){ + SentRequest(req, c) + resp, err := netReader.ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Println(resp) + + } else if regex.ExitReg.MatchString(req){ fmt.Println("Good bye") - fmt.Fprintf(c, command) + SentRequest(req, c) return - } else if strings.Contains(command, "subscribe"){ - fmt.Fprintf(c, command) - HandleTopic(c, *netReader, strings.Split(command, " ")[1]) - } else if strings.Contains(command, "publish"){ - fmt.Fprintf(c, command) - continue - } - - fmt.Fprintf(c, command) - resp, err := netReader.ReadString('\n') + } else { + reqParts := strings.Split(req, " ")[:2] - if err != nil { - log.Error.Panicln(err.Error()) + switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ + case "subscribe":{ + SentRequest(req, c) + HandleTopic(c, *netReader, reqParts[1]) + } + case "publish":{ + SentRequest(req, c) + } } - - fmt.Println(resp) } +} } \ No newline at end of file From 967c20ebce41abe773015b54ff876e369d7cb56e Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 6 Dec 2018 16:56:46 +0300 Subject: [PATCH 043/104] Fixed func signature in client --- client/handler/handle_connection.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 56adfb3..b489968 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -10,7 +10,7 @@ import ( "NonRelDB/util/regex" ) -func SentRequest(req string, c net.Conn){ +func SendRequest(req string, c net.Conn){ fmt.Fprintf(c, req) } @@ -26,7 +26,7 @@ func HandleConnection(c net.Conn){ } if regex.QueryReg.MatchString(req){ - SentRequest(req, c) + SendRequest(req, c) resp, err := netReader.ReadString('\n') if err != nil { @@ -37,7 +37,7 @@ func HandleConnection(c net.Conn){ } else if regex.ExitReg.MatchString(req){ fmt.Println("Good bye") - SentRequest(req, c) + SendRequest(req, c) return } else { @@ -45,11 +45,11 @@ func HandleConnection(c net.Conn){ switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ case "subscribe":{ - SentRequest(req, c) + SendRequest(req, c) HandleTopic(c, *netReader, reqParts[1]) } case "publish":{ - SentRequest(req, c) + SendRequest(req, c) } } } From 7e134757669de9a9c11b25eacd6b120689248dba Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 6 Dec 2018 17:12:38 +0300 Subject: [PATCH 044/104] Added comments to client's, server's dir's packages --- client/handler/handle_connection.go | 2 ++ client/handler/handle_topic.go | 2 ++ client/main.go | 2 +- server/handler/handle_connection.go | 3 +++ server/handler/handle_listener.go | 1 + server/handler/handle_query.go | 1 + server/main.go | 3 +++ server/storage/inmemory/inmemory_service.go | 13 ++++++------- server/storage/sync/sync_map.go | 14 +++++++------- server/topic/topic_service.go | 4 ++++ 10 files changed, 30 insertions(+), 15 deletions(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index b489968..c170a38 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -10,10 +10,12 @@ import ( "NonRelDB/util/regex" ) +// SendRequest sends request to specified connection. func SendRequest(req string, c net.Conn){ fmt.Fprintf(c, req) } +// HandleConnection handling communication with server. func HandleConnection(c net.Conn){ consoleReader := bufio.NewReader(os.Stdin) netReader := bufio.NewReader(c) diff --git a/client/handler/handle_topic.go b/client/handler/handle_topic.go index 0d92842..0f226b9 100644 --- a/client/handler/handle_topic.go +++ b/client/handler/handle_topic.go @@ -10,6 +10,7 @@ import ( "syscall" ) +// quit Handling Ctrl + C press. func quit(c net.Conn, topic string) { sign := make(chan os.Signal, 2) signal.Notify(sign, os.Interrupt, syscall.SIGTERM) @@ -21,6 +22,7 @@ func quit(c net.Conn, topic string) { }() } +// HandleTopic listening messages from specified topic. func HandleTopic(c net.Conn, r bufio.Reader, topic string){ quit(c, topic) fmt.Printf("Reading messages from %s... (press Ctrl + C to stop)\n", topic) diff --git a/client/main.go b/client/main.go index b98610c..4e94ec5 100644 --- a/client/main.go +++ b/client/main.go @@ -27,6 +27,7 @@ func init(){ flag.Parse() } +// main entry point for client. func main(){ c, err := net.Dial("tcp", host + ":" + port) defer c.Close() @@ -36,7 +37,6 @@ func main(){ } if dump { - // c.Write([]byte("dump\n")) fmt.Fprintf(c, "dump\n") dbDump, err := bufio.NewReader(c).ReadString('\n') diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index b2069ce..af780fd 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -12,6 +12,7 @@ import ( "NonRelDB/util/regex" ) +// HandleRequest handling request from client. func HandleRequest(req string, c net.Conn) rune{ if regex.QueryReg.MatchString(req){ resp:= HandleQuery(req) @@ -50,11 +51,13 @@ func HandleRequest(req string, c net.Conn) rune{ return 'c' } +// SentResponse sends response to specified connection. func SentResponse(resp string, c net.Conn){ fmt.Fprintf(c, resp + "\n") log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) } +// HandleConnection handling communication with client. func HandleConnection(c net.Conn){ defer c.Close() diff --git a/server/handler/handle_listener.go b/server/handler/handle_listener.go index dbd7b14..66e3b3d 100644 --- a/server/handler/handle_listener.go +++ b/server/handler/handle_listener.go @@ -5,6 +5,7 @@ import ( "NonRelDB/log" ) +// HandleListener accepts clients and runs their handlers. func HandleListener(l net.Listener){ defer l.Close() for { diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index dea4a75..889aaab 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -6,6 +6,7 @@ import ( "NonRelDB/util/regex" ) +// HandleQuery handling queries to db. func HandleQuery(query string) string { queryParts := strings.Split(query, " ")[:2] diff --git a/server/main.go b/server/main.go index 2d4e83c..9c63734 100644 --- a/server/main.go +++ b/server/main.go @@ -27,6 +27,7 @@ func init() { flag.Parse() } +// storageInit init of storage. func storageInit() { if mode == "memory" { inmemory.InitDBInMemory() @@ -35,6 +36,7 @@ func storageInit() { } } +// cleanup storage cleanup. func cleanup() { sign := make(chan os.Signal, 2) signal.Notify(sign, os.Interrupt, syscall.SIGTERM) @@ -48,6 +50,7 @@ func cleanup() { }() } +// main entry point for server. func main() { storageInit() cleanup() diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 48188f2..04cb29f 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -8,21 +8,20 @@ import ( "NonRelDB/server/storage/sync" ) -// Storage | Global variable for kv storage. - +// Global variable for kv storage. var storage sync.Map -// GetStorage | Getter for storage. +// GetStorage getter for storage. func GetStorage() *sync.Map { return &storage } -// SetStorage | Setter for storage +// SetStorage setter for storage func SetStorage(sm sync.Map){ storage = sm } -// InitDBInMemory | Init kv db in memory. +// InitDBInMemory init kv db in memory. func InitDBInMemory(){ storage := sync.Map{} m := make(map[string]string) @@ -30,7 +29,7 @@ func InitDBInMemory(){ log.Info.Println("DB successfully created in-memory") } -// InitDBFromStorage | Receives filename and load its content to inmemory storage. +// InitDBFromStorage receives filename and load its content to inmemory storage. func InitDBFromStorage(filename string){ defer log.Info.Printf("DB successfully initialized from %s", filename) @@ -53,7 +52,7 @@ func InitDBFromStorage(filename string){ storage.SetMap(json.UnpackFromJSON(jb)) } -// SaveDBToStorage | Receives file name and saves inmemory storage to it. +// SaveDBToStorage receives file name and saves inmemory storage to it. func SaveDBToStorage(filename string){ jb := json.PackMapToJSON((*storage.GetMap())) j := string(jb) diff --git a/server/storage/sync/sync_map.go b/server/storage/sync/sync_map.go index 44353ac..672c66c 100644 --- a/server/storage/sync/sync_map.go +++ b/server/storage/sync/sync_map.go @@ -7,23 +7,23 @@ import ( "NonRelDB/log" ) -// Map | Map synchronized with mutex. +// Map map synchronized with mutex. type Map struct { sync.Mutex m *map[string]string } -// GetMap | Getter for map. +// GetMap getter for map. func (sm *Map) GetMap() *map[string]string { return sm.m } -// SetMap | Setter for map. +// SetMap setter for map. func (sm *Map) SetMap(m *map[string]string){ sm.m = m } -// Get | Receives and key and returns value according its key. +// Get receives and key and returns value according its key. func (sm *Map) Get(key string) string { sm.Lock() defer sm.Unlock() @@ -35,7 +35,7 @@ func (sm *Map) Get(key string) string { } } -// Set | Set value according to key. +// Set set value according to key. func (sm *Map) Set(key string, value string) string{ sm.Lock() defer sm.Unlock() @@ -43,7 +43,7 @@ func (sm *Map) Set(key string, value string) string{ return "Value has changed" } -// Del | Del value according to key. +// Del del value according to key. func (sm *Map) Del(key string) string{ sm.Lock() defer sm.Unlock() @@ -56,7 +56,7 @@ func (sm *Map) Del(key string) string{ } } -// Keys | Returns keys which match to pattern. +// Keys returns keys which match to pattern. func (sm *Map) Keys(pattern string) string { sm.Lock() defer sm.Unlock() diff --git a/server/topic/topic_service.go b/server/topic/topic_service.go index cefe326..52b1627 100644 --- a/server/topic/topic_service.go +++ b/server/topic/topic_service.go @@ -7,12 +7,14 @@ import ( "NonRelDB/util/collection" ) +// Storage for topic. var topics map[string][]net.Conn func init(){ topics = make(map[string][]net.Conn) } +// Subscribe adding client to specified topic. func Subscribe(name string, c net.Conn){ if topics[name] == nil { topics[name] = make([]net.Conn, 10) @@ -21,6 +23,7 @@ func Subscribe(name string, c net.Conn){ log.Info.Printf("%s just subscribed %s", c.RemoteAddr().String(), name) } +// Unsubscribe removing client from specified topic. func Unsubscribe(name string, c net.Conn) { if topics[name] != nil || len(topics) != 0{ index := collection.ConnIndex(topics[name], c) @@ -31,6 +34,7 @@ func Unsubscribe(name string, c net.Conn) { } } +// Publish publishes message to specified topic. func Publish(name string, msg string){ if topics[name] != nil || len(topics) != 0 { log.Info.Printf("%s just published in %s", msg , name) From 50ea5faac518aa07173617992edf420a8d426754 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 02:55:00 +0300 Subject: [PATCH 045/104] Added comments to util package's dir & fixed variables & funcs naming --- log/logger.go | 1 + server/handler/handle_connection.go | 8 ++++---- server/handler/handle_query.go | 4 ++-- util/collection/slice_util.go | 2 ++ util/file/file_read.go | 4 ++-- util/file/file_write.go | 4 ++-- util/json/json_decode.go | 2 +- util/json/json_encode.go | 6 ++++-- util/regex/regex_util.go | 12 +++++++----- 9 files changed, 25 insertions(+), 18 deletions(-) diff --git a/log/logger.go b/log/logger.go index 0664e04..6982409 100644 --- a/log/logger.go +++ b/log/logger.go @@ -6,6 +6,7 @@ import ( "os" ) +// Presents 4 logging levels: trace, info, warning and error. var ( Trace *log.Logger Info *log.Logger diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index af780fd..9116ad7 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -16,7 +16,7 @@ import ( func HandleRequest(req string, c net.Conn) rune{ if regex.QueryReg.MatchString(req){ resp:= HandleQuery(req) - SentResponse(resp, c) + SendResponse(resp, c) return 'c' } else if regex.ExitReg.MatchString(req){ @@ -42,7 +42,7 @@ func HandleRequest(req string, c net.Conn) rune{ return 'r' } case "publish":{ - msg := regex.ValueReg.FindString(req) + msg := regex.DoubleQuoteReg.FindString(req) topic.Publish(reqParts[1],msg) return 'c' } @@ -51,8 +51,8 @@ func HandleRequest(req string, c net.Conn) rune{ return 'c' } -// SentResponse sends response to specified connection. -func SentResponse(resp string, c net.Conn){ +// SendResponse sends response to specified connection. +func SendResponse(resp string, c net.Conn){ fmt.Fprintf(c, resp + "\n") log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) } diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 889aaab..b4f662b 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -15,14 +15,14 @@ func HandleQuery(query string) string { return inmemory.GetStorage().Get(queryParts[1]) } case "set":{ - value := strings.Trim(regex.ValueReg.FindString(query),"\"") + value := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") return inmemory.GetStorage().Set(queryParts[1], value) } case "del":{ return inmemory.GetStorage().Del(queryParts[1]) } case "keys":{ - pattern := regex.ValueReg.FindString(query) + pattern := regex.DoubleQuoteReg.FindString(query) return inmemory.GetStorage().Keys(pattern) } } diff --git a/util/collection/slice_util.go b/util/collection/slice_util.go index 06260d4..3a284d9 100644 --- a/util/collection/slice_util.go +++ b/util/collection/slice_util.go @@ -4,6 +4,7 @@ import ( "net" ) +// Index returns index of neccessary element in interface{} slice if found, otherwise -1. func Index(slice []interface{}, value interface{}) int{ for index, slice_value := range slice { if slice_value == value { @@ -13,6 +14,7 @@ func Index(slice []interface{}, value interface{}) int{ return -1 } +// ConnIndex returns index of neccessary element in net.Conn slice if found, otherwise -1. func ConnIndex(slice []net.Conn, value net.Conn) int{ for index, slice_value := range slice { if slice_value == value { diff --git a/util/file/file_read.go b/util/file/file_read.go index b8d7ab3..0f14465 100644 --- a/util/file/file_read.go +++ b/util/file/file_read.go @@ -5,7 +5,7 @@ import ( "NonRelDB/log" ) -// OpenAndReadString | receives file name, reads this file and returns its string content. +// OpenAndReadString receives file name, reads this file and returns its string content. func OpenAndReadString(name string) string{ b, err := ioutil.ReadFile(name) @@ -16,7 +16,7 @@ func OpenAndReadString(name string) string{ return string(b) } -// OpenAndRead | Receives file name, reads this file and returns byte array from it. +// OpenAndRead receives file name, reads this file and returns byte array from it. func OpenAndRead(name string) []byte{ b, err := ioutil.ReadFile(name) diff --git a/util/file/file_write.go b/util/file/file_write.go index 128b115..e1625d6 100644 --- a/util/file/file_write.go +++ b/util/file/file_write.go @@ -5,7 +5,7 @@ import ( "NonRelDB/log" ) -// CreateAndWriteString | Creates file and writes string to it. +// CreateAndWriteString creates file and writes string to it. func CreateAndWriteString(name string, value string){ f, err := os.Create(name) @@ -21,7 +21,7 @@ func CreateAndWriteString(name string, value string){ } } -// CreateAndWrite | Creates file and writes byte array to it. +// CreateAndWrite creates file and writes byte array to it. func CreateAndWrite(name string, value []byte){ f, err := os.Create(name) diff --git a/util/json/json_decode.go b/util/json/json_decode.go index a1492a3..d7a70c8 100644 --- a/util/json/json_decode.go +++ b/util/json/json_decode.go @@ -5,7 +5,7 @@ import ( "NonRelDB/log" ) -// UnpackFromJSON | Receives json bytes and returns map pointer. +// UnpackFromJSON receives json bytes and returns map pointer. func UnpackFromJSON(b []byte) *map[string]string{ m := make(map[string]string) err := json.Unmarshal(b, &m) diff --git a/util/json/json_encode.go b/util/json/json_encode.go index dd19e30..9327ead 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -5,7 +5,7 @@ import ( "NonRelDB/log" ) -// PackToJSON | Receives string key and interface value and returns json bytes. +// PackToJSON receives string key and interface value and returns json bytes. func PackToJSON(key string, value string) []byte{ m := map[string]string{key : value} b, err := json.Marshal(m) @@ -15,6 +15,7 @@ func PackToJSON(key string, value string) []byte{ return b } +// PackToJSONIndent receives string key and interface value and returns json bytes with indent. func PackToJSONIndent(key string, value string) []byte{ m := map[string]string{key : value} b, err := json.MarshalIndent(m,"", " ") @@ -24,7 +25,7 @@ func PackToJSONIndent(key string, value string) []byte{ return b } -// PackMapToJSON | Receives map and returns json bytes. +// PackMapToJSON receives map and returns json bytes. func PackMapToJSON(m map[string]string) []byte{ b, err := json.Marshal(m) if err != nil{ @@ -33,6 +34,7 @@ func PackMapToJSON(m map[string]string) []byte{ return b } +// PackMapToJSONIndent receives map and returns json bytes with indent. func PackMapToJSONIndent(m map[string]string) []byte{ b, err := json.MarshalIndent(m, "", " ") if err != nil{ diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index b555803..c1c5a37 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -4,13 +4,15 @@ import ( "regexp" ) -var ValueReg *regexp.Regexp -var QueryReg *regexp.Regexp -var ExitReg *regexp.Regexp -var DumpReg *regexp.Regexp +var ( + DoubleQuoteReg *regexp.Regexp + QueryReg *regexp.Regexp + ExitReg *regexp.Regexp + DumpReg *regexp.Regexp +) func init(){ - ValueReg = regexp.MustCompile("\"(.*)\"") + DoubleQuoteReg = regexp.MustCompile("\"(.*)\"") QueryReg = regexp.MustCompile("^(get|set|del|keys)") ExitReg = regexp.MustCompile("^exit$") DumpReg = regexp.MustCompile("^dump$") From 617f140e30a3d9f26cb5a749f86995a5004eea68 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 03:06:08 +0300 Subject: [PATCH 046/104] Refactored variables naming --- server/storage/inmemory/inmemory_service.go | 20 ++++----- server/storage/sync/sync_map.go | 50 ++++++++++----------- util/file/file_read.go | 8 ++-- util/file/file_write.go | 12 ++--- util/json/json_decode.go | 8 ++-- util/json/json_encode.go | 24 +++++----- 6 files changed, 61 insertions(+), 61 deletions(-) diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 04cb29f..9e5f136 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -17,15 +17,15 @@ func GetStorage() *sync.Map { } // SetStorage setter for storage -func SetStorage(sm sync.Map){ - storage = sm +func SetStorage(syncMap sync.Map){ + storage = syncMap } // InitDBInMemory init kv db in memory. func InitDBInMemory(){ storage := sync.Map{} - m := make(map[string]string) - storage.SetMap(&m) + syncMap := make(map[string]string) + storage.SetMap(&syncMap) log.Info.Println("DB successfully created in-memory") } @@ -47,16 +47,16 @@ func InitDBFromStorage(filename string){ f.Close() } - j := file.OpenAndReadString(filename) - jb := []byte(j) - storage.SetMap(json.UnpackFromJSON(jb)) + jsonString := file.OpenAndReadString(filename) + jsonBytes := []byte(jsonString) + storage.SetMap(json.UnpackFromJSON(jsonBytes)) } // SaveDBToStorage receives file name and saves inmemory storage to it. func SaveDBToStorage(filename string){ - jb := json.PackMapToJSON((*storage.GetMap())) - j := string(jb) + jsonBytes := json.PackMapToJSON((*storage.GetMap())) + jsonString := string(jsonBytes) - file.CreateAndWriteString(filename, j) + file.CreateAndWriteString(filename, jsonString) log.Info.Printf("DB successfully saved to %s", filename) } diff --git a/server/storage/sync/sync_map.go b/server/storage/sync/sync_map.go index 672c66c..3bd2dee 100644 --- a/server/storage/sync/sync_map.go +++ b/server/storage/sync/sync_map.go @@ -10,24 +10,24 @@ import ( // Map map synchronized with mutex. type Map struct { sync.Mutex - m *map[string]string + storage *map[string]string } // GetMap getter for map. -func (sm *Map) GetMap() *map[string]string { - return sm.m +func (syncMap *Map) GetMap() *map[string]string { + return syncMap.storage } // SetMap setter for map. -func (sm *Map) SetMap(m *map[string]string){ - sm.m = m +func (syncMap *Map) SetMap(storage *map[string]string){ + syncMap.storage = storage } // Get receives and key and returns value according its key. -func (sm *Map) Get(key string) string { - sm.Lock() - defer sm.Unlock() - v := (*sm.m)[key] +func (syncMap *Map) Get(key string) string { + syncMap.Lock() + defer syncMap.Unlock() + v := (*syncMap.storage)[key] if v != ""{ return v; } else { @@ -36,20 +36,20 @@ func (sm *Map) Get(key string) string { } // Set set value according to key. -func (sm *Map) Set(key string, value string) string{ - sm.Lock() - defer sm.Unlock() - (*sm.m)[key] = value +func (syncMap *Map) Set(key string, value string) string{ + syncMap.Lock() + defer syncMap.Unlock() + (*syncMap.storage)[key] = value return "Value has changed" } // Del del value according to key. -func (sm *Map) Del(key string) string{ - sm.Lock() - defer sm.Unlock() - v := (*sm.m)[key] +func (syncMap *Map) Del(key string) string{ + syncMap.Lock() + defer syncMap.Unlock() + v := (*syncMap.storage)[key] if v != "" { - delete((*sm.m),key) + delete((*syncMap.storage),key) return v; } else { return "Value with this key not found" @@ -57,22 +57,22 @@ func (sm *Map) Del(key string) string{ } // Keys returns keys which match to pattern. -func (sm *Map) Keys(pattern string) string { - sm.Lock() - defer sm.Unlock() +func (syncMap *Map) Keys(pattern string) string { + syncMap.Lock() + defer syncMap.Unlock() var keys []string - re, err := regexp.Compile(pattern) + regex, err := regexp.Compile(pattern) if err != nil { log.Warning.Println(err.Error()) return "Pattern is incorrect" } - for k := range ((*sm.m)) { - if re.MatchString(k) { - keys = append(keys, k) + for key := range ((*syncMap.storage)) { + if regex.MatchString(key) { + keys = append(keys, key) } } diff --git a/util/file/file_read.go b/util/file/file_read.go index 0f14465..ca203a6 100644 --- a/util/file/file_read.go +++ b/util/file/file_read.go @@ -7,22 +7,22 @@ import ( // OpenAndReadString receives file name, reads this file and returns its string content. func OpenAndReadString(name string) string{ - b, err := ioutil.ReadFile(name) + bytes, err := ioutil.ReadFile(name) if err != nil{ log.Error.Panicln(err.Error()) } - return string(b) + return string(bytes) } // OpenAndRead receives file name, reads this file and returns byte array from it. func OpenAndRead(name string) []byte{ - b, err := ioutil.ReadFile(name) + bytes, err := ioutil.ReadFile(name) if err != nil{ log.Error.Panicln(err.Error()) } - return b + return bytes } \ No newline at end of file diff --git a/util/file/file_write.go b/util/file/file_write.go index e1625d6..0b165e1 100644 --- a/util/file/file_write.go +++ b/util/file/file_write.go @@ -7,15 +7,15 @@ import ( // CreateAndWriteString creates file and writes string to it. func CreateAndWriteString(name string, value string){ - f, err := os.Create(name) + file, err := os.Create(name) - defer f.Close() + defer file.Close() if err != nil{ log.Error.Panicln(err.Error()) } - _, err = f.WriteString(value) + _, err = file.WriteString(value) if err != nil{ log.Error.Panicln(err.Error()) } @@ -23,15 +23,15 @@ func CreateAndWriteString(name string, value string){ // CreateAndWrite creates file and writes byte array to it. func CreateAndWrite(name string, value []byte){ - f, err := os.Create(name) + file, err := os.Create(name) - defer f.Close() + defer file.Close() if err != nil{ log.Error.Panicln(err.Error()) } - _, err = f.Write(value) + _, err = file.Write(value) if err != nil{ log.Error.Panicln(err.Error()) } diff --git a/util/json/json_decode.go b/util/json/json_decode.go index d7a70c8..e45a284 100644 --- a/util/json/json_decode.go +++ b/util/json/json_decode.go @@ -6,12 +6,12 @@ import ( ) // UnpackFromJSON receives json bytes and returns map pointer. -func UnpackFromJSON(b []byte) *map[string]string{ - m := make(map[string]string) - err := json.Unmarshal(b, &m) +func UnpackFromJSON(bytes []byte) *map[string]string{ + kvMap := make(map[string]string) + err := json.Unmarshal(bytes, &kvMap) if err != nil{ log.Warning.Println(err.Error()) log.Warning.Println("No bytes. Will be returned zero map") } - return &m + return &kvMap } \ No newline at end of file diff --git a/util/json/json_encode.go b/util/json/json_encode.go index 9327ead..fa6c1cf 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -7,38 +7,38 @@ import ( // PackToJSON receives string key and interface value and returns json bytes. func PackToJSON(key string, value string) []byte{ - m := map[string]string{key : value} - b, err := json.Marshal(m) + kvMap := map[string]string{key : value} + bytes, err := json.Marshal(kvMap) if err != nil{ log.Error.Println(err.Error()) } - return b + return bytes } // PackToJSONIndent receives string key and interface value and returns json bytes with indent. func PackToJSONIndent(key string, value string) []byte{ - m := map[string]string{key : value} - b, err := json.MarshalIndent(m,"", " ") + kvMap := map[string]string{key : value} + bytes, err := json.MarshalIndent(kvMap,"", " ") if err != nil{ log.Error.Println(err.Error()) } - return b + return bytes } // PackMapToJSON receives map and returns json bytes. -func PackMapToJSON(m map[string]string) []byte{ - b, err := json.Marshal(m) +func PackMapToJSON(kvMap map[string]string) []byte{ + bytes, err := json.Marshal(kvMap) if err != nil{ log.Error.Println(err.Error()) } - return b + return bytes } // PackMapToJSONIndent receives map and returns json bytes with indent. -func PackMapToJSONIndent(m map[string]string) []byte{ - b, err := json.MarshalIndent(m, "", " ") +func PackMapToJSONIndent(kvMap map[string]string) []byte{ + bytes, err := json.MarshalIndent(kvMap, "", " ") if err != nil{ log.Error.Println(err.Error()) } - return b + return bytes } \ No newline at end of file From bd70dc1950e17c355169ed0f0170ada790471214 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 04:46:39 +0300 Subject: [PATCH 047/104] Added regex for publish & (un)subcribe --- util/regex/regex_util.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index c1c5a37..b923b3a 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -7,6 +7,7 @@ import ( var ( DoubleQuoteReg *regexp.Regexp QueryReg *regexp.Regexp + TopicReg *regexp.Regexp ExitReg *regexp.Regexp DumpReg *regexp.Regexp ) @@ -14,6 +15,7 @@ var ( func init(){ DoubleQuoteReg = regexp.MustCompile("\"(.*)\"") QueryReg = regexp.MustCompile("^(get|set|del|keys)") + TopicReg = regexp.MustCompile("^(subscribe|publish|unsubscribe)") ExitReg = regexp.MustCompile("^exit$") DumpReg = regexp.MustCompile("^dump$") } \ No newline at end of file From 3705fe2b246c46f7957d80a995f6e6f971d2a6c9 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 04:47:14 +0300 Subject: [PATCH 048/104] Fixed cases when server/client crashes --- client/handler/handle_connection.go | 30 +++++++++++++++--------- server/handler/handle_connection.go | 36 ++++++++++++++++------------- server/handler/handle_query.go | 34 ++++++++++++++------------- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index c170a38..210a70b 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -12,7 +12,7 @@ import ( // SendRequest sends request to specified connection. func SendRequest(req string, c net.Conn){ - fmt.Fprintf(c, req) + fmt.Fprintf(c, req + "\n") } // HandleConnection handling communication with server. @@ -22,7 +22,8 @@ func HandleConnection(c net.Conn){ for { fmt.Print("NonRelDB> ") req, err := consoleReader.ReadString('\n') - + req = strings.Trim(req, "\n") + if err != nil { log.Error.Panicln(err.Error()) } @@ -42,18 +43,25 @@ func HandleConnection(c net.Conn){ SendRequest(req, c) return - } else { - reqParts := strings.Split(req, " ")[:2] + } else if regex.TopicReg.MatchString(req) { + reqParts := strings.Split(req, " ") - switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ - case "subscribe":{ - SendRequest(req, c) - HandleTopic(c, *netReader, reqParts[1]) + switch reqPartsLen := len(reqParts); reqPartsLen{ + case 2:{ + if strings.ToLower(reqParts[0]) == "subscribe"{ + SendRequest(req, c) + HandleTopic(c, *netReader, reqParts[1]) + } } - case "publish":{ - SendRequest(req, c) + case 3:{ + if strings.ToLower(reqParts[0]) == "publish"{ + SendRequest(req, c) + } } + } + } else { + fmt.Println("Bad request") + continue } } -} } \ No newline at end of file diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 9116ad7..dcf1b28 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -29,24 +29,28 @@ func HandleRequest(req string, c net.Conn) rune{ log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) return 'r' - } else { - reqParts := strings.Split(req, " ")[:2] + } else if regex.TopicReg.MatchString(req){ + reqParts := strings.Split(req, " ") - switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ - case "subscribe":{ - topic.Subscribe(reqParts[1], c) - return 'c' - } - case "unsubscribe":{ - topic.Unsubscribe(reqParts[1], c) - return 'r' - } - case "publish":{ - msg := regex.DoubleQuoteReg.FindString(req) - topic.Publish(reqParts[1],msg) - return 'c' - } + if len(reqParts) >= 2{ + switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ + case "subscribe":{ + topic.Subscribe(reqParts[1], c) + return 'c' + } + case "unsubscribe":{ + topic.Unsubscribe(reqParts[1], c) + return 'r' + } + case "publish":{ + msg := regex.DoubleQuoteReg.FindString(req) + topic.Publish(reqParts[1],msg) + return 'c' + } + } } + } else { + SendResponse("Bad request", c) } return 'c' } diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index b4f662b..b0ffbde 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -8,22 +8,24 @@ import ( // HandleQuery handling queries to db. func HandleQuery(query string) string { - queryParts := strings.Split(query, " ")[:2] - - switch queryCtx := strings.ToLower(queryParts[0]); queryCtx{ - case "get":{ - return inmemory.GetStorage().Get(queryParts[1]) - } - case "set":{ - value := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") - return inmemory.GetStorage().Set(queryParts[1], value) - } - case "del":{ - return inmemory.GetStorage().Del(queryParts[1]) - } - case "keys":{ - pattern := regex.DoubleQuoteReg.FindString(query) - return inmemory.GetStorage().Keys(pattern) + queryParts := strings.Split(query, " ") + + if len (queryParts) >= 2 { + switch queryCtx := strings.ToLower(queryParts[0]); queryCtx{ + case "get":{ + return inmemory.GetStorage().Get(queryParts[1]) + } + case "set":{ + value := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") + return inmemory.GetStorage().Set(queryParts[1], value) + } + case "del":{ + return inmemory.GetStorage().Del(queryParts[1]) + } + case "keys":{ + pattern := regex.DoubleQuoteReg.FindString(query) + return inmemory.GetStorage().Keys(pattern) + } } } From 1111dea4e39905ce6799a4c5394bed00719735bf Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 04:53:54 +0300 Subject: [PATCH 049/104] Variable naming fix --- client/handler/handle_connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 210a70b..91eab91 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -46,7 +46,7 @@ func HandleConnection(c net.Conn){ } else if regex.TopicReg.MatchString(req) { reqParts := strings.Split(req, " ") - switch reqPartsLen := len(reqParts); reqPartsLen{ + switch reqCtx := len(reqParts); reqCtx{ case 2:{ if strings.ToLower(reqParts[0]) == "subscribe"{ SendRequest(req, c) From 7c62633a7a308de635540809b930ae9c9a55614c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 05:18:35 +0300 Subject: [PATCH 050/104] Made request handling more confident against crashes --- client/handler/handle_connection.go | 8 +++---- server/handler/handle_connection.go | 36 ++++++++++++++++------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 91eab91..250600a 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -46,19 +46,17 @@ func HandleConnection(c net.Conn){ } else if regex.TopicReg.MatchString(req) { reqParts := strings.Split(req, " ") - switch reqCtx := len(reqParts); reqCtx{ - case 2:{ + if len(reqParts) == 2{ if strings.ToLower(reqParts[0]) == "subscribe"{ SendRequest(req, c) HandleTopic(c, *netReader, reqParts[1]) } - } - case 3:{ + } else if len(reqParts) >= 3{ if strings.ToLower(reqParts[0]) == "publish"{ SendRequest(req, c) } - } } + } else { fmt.Println("Bad request") continue diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index dcf1b28..ef99313 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -32,26 +32,30 @@ func HandleRequest(req string, c net.Conn) rune{ } else if regex.TopicReg.MatchString(req){ reqParts := strings.Split(req, " ") - if len(reqParts) >= 2{ - switch reqCtx := strings.ToLower(reqParts[0]); reqCtx{ - case "subscribe":{ - topic.Subscribe(reqParts[1], c) - return 'c' - } - case "unsubscribe":{ - topic.Unsubscribe(reqParts[1], c) - return 'r' - } - case "publish":{ - msg := regex.DoubleQuoteReg.FindString(req) - topic.Publish(reqParts[1],msg) - return 'c' - } - } + if len(reqParts) == 2 { + if strings.ToLower(reqParts[0]) == "subscribe"{ + topic.Subscribe(reqParts[1], c) + return 'c' + + } else if strings.ToLower(reqParts[0]) == "unsubscribe"{ + topic.Unsubscribe(reqParts[1], c) + return 'r' + + } + + } else if len(reqParts) >= 3{ + if strings.ToLower(reqParts[0]) == "publish"{ + msg := regex.DoubleQuoteReg.FindString(req) + topic.Publish(reqParts[1], msg) + return 'c' + } + } + } else { SendResponse("Bad request", c) } + return 'c' } From 7365f71553b4d9d6cb36b688ea2cf235cdd4cdfc Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 05:33:36 +0300 Subject: [PATCH 051/104] Fixed keys feature --- server/handler/handle_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index b0ffbde..a0ea778 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -23,7 +23,7 @@ func HandleQuery(query string) string { return inmemory.GetStorage().Del(queryParts[1]) } case "keys":{ - pattern := regex.DoubleQuoteReg.FindString(query) + pattern := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") return inmemory.GetStorage().Keys(pattern) } } From 8bedb5bd666a668c399cd3a8b334caa13922fdac Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 7 Dec 2018 05:36:01 +0300 Subject: [PATCH 052/104] Moved sync.Map from server's dir package to util --- server/storage/inmemory/inmemory_service.go | 2 +- {server/storage => util}/sync/sync_map.go | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {server/storage => util}/sync/sync_map.go (100%) diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 9e5f136..7a794d5 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -5,7 +5,7 @@ import ( "NonRelDB/util/json" "NonRelDB/util/file" "NonRelDB/log" - "NonRelDB/server/storage/sync" + "NonRelDB/util/sync" ) // Global variable for kv storage. diff --git a/server/storage/sync/sync_map.go b/util/sync/sync_map.go similarity index 100% rename from server/storage/sync/sync_map.go rename to util/sync/sync_map.go From 0a71b084e7bd23b952f4cb7be6967f87b04a5ad3 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 8 Dec 2018 04:04:39 +0300 Subject: [PATCH 053/104] Moved HandleRequest func body to main to avoid pointless rune switch & return --- server/handler/handle_connection.go | 87 ++++++++++++----------------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index ef99313..faf66dd 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -12,53 +12,6 @@ import ( "NonRelDB/util/regex" ) -// HandleRequest handling request from client. -func HandleRequest(req string, c net.Conn) rune{ - if regex.QueryReg.MatchString(req){ - resp:= HandleQuery(req) - SendResponse(resp, c) - return 'c' - - } else if regex.ExitReg.MatchString(req){ - log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) - return 'r' - - } else if regex.DumpReg.MatchString(req){ - dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) - fmt.Fprintf(c, dbDump + "\n") - log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) - return 'r' - - } else if regex.TopicReg.MatchString(req){ - reqParts := strings.Split(req, " ") - - if len(reqParts) == 2 { - if strings.ToLower(reqParts[0]) == "subscribe"{ - topic.Subscribe(reqParts[1], c) - return 'c' - - } else if strings.ToLower(reqParts[0]) == "unsubscribe"{ - topic.Unsubscribe(reqParts[1], c) - return 'r' - - } - - } else if len(reqParts) >= 3{ - if strings.ToLower(reqParts[0]) == "publish"{ - msg := regex.DoubleQuoteReg.FindString(req) - topic.Publish(reqParts[1], msg) - return 'c' - } - - } - - } else { - SendResponse("Bad request", c) - } - - return 'c' -} - // SendResponse sends response to specified connection. func SendResponse(resp string, c net.Conn){ fmt.Fprintf(c, resp + "\n") @@ -82,9 +35,43 @@ func HandleConnection(c net.Conn){ log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), req) - switch handleCtx := HandleRequest(req, c); handleCtx{ - case 'r': return - case 'c': continue + if regex.QueryReg.MatchString(req){ + resp:= HandleQuery(req) + SendResponse(resp, c) + + } else if regex.ExitReg.MatchString(req){ + log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) + return + + } else if regex.DumpReg.MatchString(req){ + dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) + fmt.Fprintf(c, dbDump + "\n") + log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) + return + + } else if regex.TopicReg.MatchString(req){ + reqParts := strings.Split(req, " ") + + if len(reqParts) == 2 { + if strings.ToLower(reqParts[0]) == "subscribe"{ + topic.Subscribe(reqParts[1], c) + + } else if strings.ToLower(reqParts[0]) == "unsubscribe"{ + topic.Unsubscribe(reqParts[1], c) + return + + } + + } else if len(reqParts) >= 3{ + if strings.ToLower(reqParts[0]) == "publish"{ + msg := regex.DoubleQuoteReg.FindString(req) + topic.Publish(reqParts[1], msg) + } + + } + + } else { + SendResponse("Bad request", c) } } } \ No newline at end of file From 130f25a14a9bb83d13f953032a5a0fa3ba61907b Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 20:06:44 +0300 Subject: [PATCH 054/104] Removed Index func from util because it unused --- util/collection/slice_util.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/util/collection/slice_util.go b/util/collection/slice_util.go index 3a284d9..09dca24 100644 --- a/util/collection/slice_util.go +++ b/util/collection/slice_util.go @@ -4,16 +4,6 @@ import ( "net" ) -// Index returns index of neccessary element in interface{} slice if found, otherwise -1. -func Index(slice []interface{}, value interface{}) int{ - for index, slice_value := range slice { - if slice_value == value { - return index - } - } - return -1 -} - // ConnIndex returns index of neccessary element in net.Conn slice if found, otherwise -1. func ConnIndex(slice []net.Conn, value net.Conn) int{ for index, slice_value := range slice { From 705080a3935ae61d17e1d8c836d85219450f9b92 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 21:48:54 +0300 Subject: [PATCH 055/104] Added *.out files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ccfa6e4..06da8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ client/dump.json client/main server/main server/storage.json +*.out From 3c6533cd881d15f719b0138e1c2c06ffdff66cbf Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 21:49:23 +0300 Subject: [PATCH 056/104] Added unit tests for util/file package --- util/file/file_read_test.go | 67 ++++++++++++++++++++++++++++++++++++ util/file/file_write_test.go | 33 ++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 util/file/file_read_test.go create mode 100644 util/file/file_write_test.go diff --git a/util/file/file_read_test.go b/util/file/file_read_test.go new file mode 100644 index 0000000..17cf618 --- /dev/null +++ b/util/file/file_read_test.go @@ -0,0 +1,67 @@ +package file + +import ( + "os" + "testing" + "github.com/stretchr/testify/assert" +) + +func TestOpenAndReadString__SameString__Success(t *testing.T){ + file, _ := os.Create("test.txt") + + file.WriteString("123") + + file.Close() + + assert.Equal(t, "123", OpenAndReadString("test.txt")) + + os.Remove("test.txt") +} + +func TestOpenAndReadString__OtherString__Fail(t *testing.T){ + file, _ := os.Create("test.txt") + + file.WriteString("123") + + file.Close() + + assert.NotEqual(t, "124", OpenAndReadString("test.txt")) + + os.Remove("test.txt") +} + +func TestOpenAndReadString__FileNotExist__Fail(t *testing.T){ + assert.Panics(t,func(){ + OpenAndReadString("test.txt") + }) +} + +func TestOpenAndRead__SameByteArray__Success(t *testing.T){ + file, _ := os.Create("test.bin") + + file.Write([]byte("123")) + + file.Close() + + assert.Equal(t, []byte("123"), OpenAndRead("test.bin")) + + os.Remove("test.bin") +} + +func TestOpenAndRead__DifferentByteArray__Success(t *testing.T){ + file, _ := os.Create("test.bin") + + file.Write([]byte("123")) + + file.Close() + + assert.NotEqual(t, []byte("124"), OpenAndRead("test.bin")) + + os.Remove("test.bin") +} + +func TestOpenAndRead__FileNotExist__Fail(t *testing.T){ + assert.Panics(t,func(){ + OpenAndRead("test.bin") + }) +} \ No newline at end of file diff --git a/util/file/file_write_test.go b/util/file/file_write_test.go new file mode 100644 index 0000000..a4cd11c --- /dev/null +++ b/util/file/file_write_test.go @@ -0,0 +1,33 @@ +package file + +import( + "testing" + "github.com/stretchr/testify/assert" + "os" +) + +func TestCreateAndWriteString__SameString__Success(t *testing.T){ + CreateAndWriteString("test.txt", "123") + assert.Equal(t, "123", OpenAndReadString("test.txt")) + os.Remove("test.txt") +} + +func TestCreateAndWriteString__OtherString__Fail(t *testing.T){ + CreateAndWriteString("test.txt", "123") + assert.NotEqual(t, "124", OpenAndReadString("test.txt")) + os.Remove("test.txt") +} + +func TestCreateAndWrite__SameByteArray__Success(t *testing.T){ + CreateAndWrite("test.bin", []byte("123")) + assert.Equal(t, []byte("123"), OpenAndRead("test.bin")) + os.Remove("test.bin") +} + +func TestCreateAndWrite__OtherByteArray__Success(t *testing.T){ + CreateAndWrite("test.bin", []byte("123")) + assert.NotEqual(t, []byte("124"), OpenAndRead("test.bin")) + os.Remove("test.bin") +} + + From d75c4f8bd41c1b577543550610e308663785e4e6 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 21:49:49 +0300 Subject: [PATCH 057/104] Added unit tests for util/json package --- util/json/json_decode_test.go | 33 ++++++++++++++++++++ util/json/json_encode_test.go | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 util/json/json_decode_test.go create mode 100644 util/json/json_encode_test.go diff --git a/util/json/json_decode_test.go b/util/json/json_decode_test.go new file mode 100644 index 0000000..3adbe3c --- /dev/null +++ b/util/json/json_decode_test.go @@ -0,0 +1,33 @@ +package json + +import ( + "testing" + "github.com/stretchr/testify/assert" + "encoding/json" +) + +func TestUnpackMapFromJSON__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + mapBytes, _ := json.Marshal(testMap) + assert.Equal(t, testMap, *UnpackFromJSON(mapBytes)) +} + +func TestUnpackMapFromJSON__DiffMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + mapBytes, _ := json.Marshal(testMap) + testMap["1"] = "1" + assert.NotEqual(t, testMap, *UnpackFromJSON(mapBytes)) +} + +func TestUnpackMapFromJSON__SameMapWithIndent__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + mapBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.Equal(t, testMap, *UnpackFromJSON(mapBytes)) +} + +func TestUnpackMapFromJSON__DiffMapWithIndent__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + mapBytes, _ := json.MarshalIndent(testMap, "", " ") + testMap["1"] = "1" + assert.NotEqual(t, testMap, *UnpackFromJSON(mapBytes)) +} \ No newline at end of file diff --git a/util/json/json_encode_test.go b/util/json/json_encode_test.go new file mode 100644 index 0000000..d56bb3f --- /dev/null +++ b/util/json/json_encode_test.go @@ -0,0 +1,58 @@ +package json + +import ( + "github.com/stretchr/testify/assert" + "testing" + "encoding/json" +) + +func TestPackToJSON__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one"} + expBytes, _ := json.Marshal(testMap) + assert.Equal(t, expBytes, PackToJSON("1", testMap["1"])) +} + +func TestPackToJSON__OtherMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one"} + expBytes, _ := json.Marshal(testMap) + assert.NotEqual(t, expBytes, PackToJSON("1", "two")) +} + +func TestPackToJSONIndent__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.Equal(t, expBytes, PackToJSONIndent("1", testMap["1"])) +} + +func TestPackToJSONIndent__OtherMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one"} + expBytes, _ := json.MarshalIndent(testMap,"", " ") + assert.NotEqual(t, expBytes, PackToJSONIndent("1", "two")) +} + + +func TestPackMapToJSON__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + expBytes, _ := json.Marshal(testMap) + assert.Equal(t, expBytes, PackMapToJSON(testMap)) +} + +func TestPackMapToJSON__DiffMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + expBytes, _ := json.Marshal(testMap) + testMap["1"] = "1" + assert.NotEqual(t, expBytes, PackMapToJSON(testMap)) +} + +func TestPackMapToJSONIndent__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.Equal(t, expBytes, PackMapToJSONIndent(testMap)) +} + +func TestPackMapToJSONIndent__DiffMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + testMap["1"] = "1" + assert.NotEqual(t, expBytes, PackMapToJSONIndent(testMap)) +} \ No newline at end of file From d133685263384cb37cc5347941db81005777c845 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 22:19:36 +0300 Subject: [PATCH 058/104] Added unit tests for util/collection package --- util/collection/slice_util_test.go | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 util/collection/slice_util_test.go diff --git a/util/collection/slice_util_test.go b/util/collection/slice_util_test.go new file mode 100644 index 0000000..2586060 --- /dev/null +++ b/util/collection/slice_util_test.go @@ -0,0 +1,38 @@ +package collection + +import ( + "testing" + "github.com/stretchr/testify/assert" + "net" +) + +type testConn struct{ + net.Conn + id int +} + +func TestConIndex__Contains__Success(t *testing.T){ + testSlice := make([]net.Conn, 10, 10) + + neededConn := testConn{id: 123} + + for i := 0; i < len(testSlice); i++ { + testSlice[i] = testConn{id : i} + } + + testSlice[4] = neededConn + + assert.Equal(t, 4, ConnIndex(testSlice, neededConn)) +} + +func TestConIndex__DoesntContain__Fail(t *testing.T){ + testSlice := make([]net.Conn, 10, 10) + + neededConn := testConn{id: 123} + + for i := 0; i < len(testSlice); i++ { + testSlice[i] = testConn{id : i} + } + + assert.Equal(t, -1, ConnIndex(testSlice, neededConn)) +} \ No newline at end of file From 58e1b850f250c96bd9233dad3ca56782e94f79aa Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 22:44:59 +0300 Subject: [PATCH 059/104] Added unit tests for util/regex package --- util/regex/regex_util_test.go | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 util/regex/regex_util_test.go diff --git a/util/regex/regex_util_test.go b/util/regex/regex_util_test.go new file mode 100644 index 0000000..998977b --- /dev/null +++ b/util/regex/regex_util_test.go @@ -0,0 +1,75 @@ +package regex + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDoubleQuoteReg__Matching__Success(t *testing.T) { + assert.True(t, DoubleQuoteReg.MatchString("\"123\"")) +} + +func TestDoublQuoteReg__NotMatching__Fail(t *testing.T) { + assert.False(t, DoubleQuoteReg.MatchString("123")) +} + +func TestDoubleQuoteReg__Parse123__Success(t *testing.T) { + assert.Equal(t, "\"123\"", DoubleQuoteReg.FindString("\"123\"")) +} + +func TestDoubleQuoteReg__Parse123WithoutQuotes__Fail(t *testing.T) { + assert.NotEqual(t, "\"123\"", DoubleQuoteReg.FindString("123")) +} + +func TestQueryReg__Get__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("get 123")) +} + +func TestQueryReg__Set__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("get 123 \"123\"")) +} + +func TestQueryReg__Del__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("get 123")) +} + +func TestQueryReg__Keys__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("keys \"\\*\"")) +} + +func TestQueryReg__123__Fail(t *testing.T) { + assert.False(t, QueryReg.MatchString("123")) +} + +func TestTopicReg__Subscribe__Success(t *testing.T) { + assert.True(t, TopicReg.MatchString("subscribe redis")) +} + +func TestTopicReg__Unsubscribe__Success(t *testing.T) { + assert.True(t, TopicReg.MatchString("unsubscribe redis")) +} + +func TestTopicReg__Publish__Success(t *testing.T) { + assert.True(t, TopicReg.MatchString("publish redis \"Hello World\"")) +} + +func TestTopicReg__123__Fail(t *testing.T) { + assert.False(t, TopicReg.MatchString("123")) +} + +func TestExitReg__Exit__Success(t *testing.T) { + assert.True(t, ExitReg.MatchString("exit")) +} + +func TestExitReg__ExitWithSpaces__Fail(t *testing.T) { + assert.False(t, ExitReg.MatchString(" exit ")) +} + +func TestDumpReg__Dump__Success(t *testing.T) { + assert.True(t, DumpReg.MatchString("dump")) +} + +func TestDumpReg__DumpWithSpaces__Fail(t *testing.T) { + assert.False(t, DumpReg.MatchString(" dump ")) +} From dabf786631e1f408970e903af9563ef6f04598a1 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 10 Dec 2018 23:14:23 +0300 Subject: [PATCH 060/104] Added unit tests for util/sync package --- util/sync/sync_map_test.go | 96 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 util/sync/sync_map_test.go diff --git a/util/sync/sync_map_test.go b/util/sync/sync_map_test.go new file mode 100644 index 0000000..db4bb49 --- /dev/null +++ b/util/sync/sync_map_test.go @@ -0,0 +1,96 @@ +package sync + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestGetMap__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one"} + syncMap := Map{} + syncMap.storage = &testMap + assert.Equal(t, &testMap, syncMap.GetMap()) +} + +func TestGetMap__DiffMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one"} + syncMap := Map{} + syncMap.storage = &testMap + diffMap := map[string]string{"2" : "two"} + assert.NotEqual(t, &diffMap, syncMap.GetMap()) +} + +func TestSetMap__SameMap__Success(t *testing.T){ + testMap := map[string]string{"1" : "one"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, &testMap, syncMap.GetMap()) +} + +func TestSetMap__DiffMap__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one"} + syncMap := Map{} + syncMap.SetMap(&testMap) + diffMap := map[string]string{"2" : "two"} + assert.NotEqual(t, &diffMap, syncMap.GetMap()) +} + +func TestGet__Existing__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "one", syncMap.Get("1")) +} + +func TestGet__NotExisting__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "Value with this key not found", syncMap.Get("4")) +} + +func TestSet__Changed__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + syncMap.Set("1","123") + assert.Equal(t, "123", syncMap.Get("1")) +} + +func TestDel__DeletedAndReturned__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "one", syncMap.Del("1")) +} + +func TestDel__DeletedAndNotFound__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + syncMap.Del("1") + assert.Equal(t, "Value with this key not found", syncMap.Get("1")) +} + +func TestKeys__FoundWildcard__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "1,2,3", syncMap.Keys("/*")) +} + +func TestKeys__IncorrectWildcard__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "Pattern is incorrect", syncMap.Keys("*")) +} + +func TestKeys__NotFound__Fail(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "Keys with this pattern not found", syncMap.Keys("123")) +} + + \ No newline at end of file From 731b66ad67149d5aab13e46d0d80fa4d3680001e Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 11 Dec 2018 00:04:27 +0300 Subject: [PATCH 061/104] Fixed restore feature --- client/main.go | 17 ++++++++++++----- server/handler/handle_connection.go | 13 ++++++++++++- server/storage/inmemory/inmemory_service.go | 5 +++++ util/regex/regex_util.go | 2 ++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/client/main.go b/client/main.go index 4e94ec5..4d081ef 100644 --- a/client/main.go +++ b/client/main.go @@ -1,7 +1,7 @@ package main import ( - "NonRelDB/util/file" + "os" "fmt" "bufio" "net" @@ -22,8 +22,7 @@ func init(){ flag.StringVar(&port, "port", "9090", "Defines host port") flag.StringVar(&port, "p", "9090", "Defines host port") flag.BoolVar(&dump, "dump", false, "Requests db dump in json format from server") - flag.BoolVar(&restore, "restore", false, "Restores received dump to file") - flag.StringVar(&location, "location", "dump.json", "Defines location of dump") + flag.BoolVar(&restore, "restore", false, "Restores db from dumped file") flag.Parse() } @@ -45,10 +44,18 @@ func main(){ } fmt.Println(dbDump) + return + } + + if restore { + fmt.Fprintf(c, "restore\n") + dbRestore, err := bufio.NewReader(os.Stdin).ReadString('\n') - if restore { - file.CreateAndWriteString(location, dbDump) + if err != nil { + log.Error.Panicln(err.Error()) } + + fmt.Fprintf(c, dbRestore) return } diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index faf66dd..74c9c3b 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -48,7 +48,18 @@ func HandleConnection(c net.Conn){ fmt.Fprintf(c, dbDump + "\n") log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) return - + + } else if regex.RestoreReg.MatchString(req){ + dbDump, err := netReader.ReadString('\n') + + if err != nil { + log.Warning.Println(err.Error()) + } + + inmemory.RestoreDBFromDump([]byte(dbDump)) + log.Info.Printf("Successfully restored dump from %s", c.RemoteAddr().String()) + return + } else if regex.TopicReg.MatchString(req){ reqParts := strings.Split(req, " ") diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 7a794d5..34cfa1a 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -52,6 +52,11 @@ func InitDBFromStorage(filename string){ storage.SetMap(json.UnpackFromJSON(jsonBytes)) } +// RestoreDBFromDump restores db from received dump. +func RestoreDBFromDump(dump []byte){ + storage.SetMap(json.UnpackFromJSON(dump)) +} + // SaveDBToStorage receives file name and saves inmemory storage to it. func SaveDBToStorage(filename string){ jsonBytes := json.PackMapToJSON((*storage.GetMap())) diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index b923b3a..e0c4b72 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -10,6 +10,7 @@ var ( TopicReg *regexp.Regexp ExitReg *regexp.Regexp DumpReg *regexp.Regexp + RestoreReg *regexp.Regexp ) func init(){ @@ -18,4 +19,5 @@ func init(){ TopicReg = regexp.MustCompile("^(subscribe|publish|unsubscribe)") ExitReg = regexp.MustCompile("^exit$") DumpReg = regexp.MustCompile("^dump$") + RestoreReg = regexp.MustCompile("^restore$") } \ No newline at end of file From 85eee19f9593d8ffc0ef273091e612739a5035da Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Tue, 11 Dec 2018 00:11:39 +0300 Subject: [PATCH 062/104] Updated unit tests in util/regex package --- util/regex/regex_util_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/util/regex/regex_util_test.go b/util/regex/regex_util_test.go index 998977b..6b97b4a 100644 --- a/util/regex/regex_util_test.go +++ b/util/regex/regex_util_test.go @@ -73,3 +73,12 @@ func TestDumpReg__Dump__Success(t *testing.T) { func TestDumpReg__DumpWithSpaces__Fail(t *testing.T) { assert.False(t, DumpReg.MatchString(" dump ")) } + +func TestRestoreReg__Restore__Success(t *testing.T) { + assert.True(t, RestoreReg.MatchString("restore")) +} + +func TestRestoreReg__RestoreWithSpaces__Fail(t *testing.T) { + assert.False(t, RestoreReg.MatchString(" restore ")) +} + From 912b6aa904ebadd12fcc858613be15036f70c8ad Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 10:59:27 +0300 Subject: [PATCH 063/104] Made storage in in-memory service pointer instead value --- server/storage/inmemory/inmemory_service.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 34cfa1a..6e71839 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -9,21 +9,21 @@ import ( ) // Global variable for kv storage. -var storage sync.Map +var storage *sync.Map // GetStorage getter for storage. func GetStorage() *sync.Map { - return &storage + return storage } // SetStorage setter for storage -func SetStorage(syncMap sync.Map){ +func SetStorage(syncMap *sync.Map){ storage = syncMap } // InitDBInMemory init kv db in memory. func InitDBInMemory(){ - storage := sync.Map{} + storage = &sync.Map{} syncMap := make(map[string]string) storage.SetMap(&syncMap) log.Info.Println("DB successfully created in-memory") @@ -31,8 +31,7 @@ func InitDBInMemory(){ // InitDBFromStorage receives filename and load its content to inmemory storage. func InitDBFromStorage(filename string){ - defer log.Info.Printf("DB successfully initialized from %s", filename) - + storage = &sync.Map{} _, err := os.Stat(filename) if os.IsNotExist(err) { @@ -50,6 +49,7 @@ func InitDBFromStorage(filename string){ jsonString := file.OpenAndReadString(filename) jsonBytes := []byte(jsonString) storage.SetMap(json.UnpackFromJSON(jsonBytes)) + log.Info.Printf("DB successfully initialized from %s", filename) } // RestoreDBFromDump restores db from received dump. From 90e41395c3ead5d981ef6c6eb4cf7be96a362810 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 11:36:39 +0300 Subject: [PATCH 064/104] Added unit tests for storage/inmemory package --- .../storage/inmemory/inmemory_service_test.go | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 server/storage/inmemory/inmemory_service_test.go diff --git a/server/storage/inmemory/inmemory_service_test.go b/server/storage/inmemory/inmemory_service_test.go new file mode 100644 index 0000000..efd546f --- /dev/null +++ b/server/storage/inmemory/inmemory_service_test.go @@ -0,0 +1,78 @@ +package inmemory + +import ( + "NonRelDB/util/json" + "os" + "testing" + "github.com/stretchr/testify/assert" + "NonRelDB/util/sync" + "NonRelDB/util/file" +) + +func TestGetStorage__Empty__Fail(t *testing.T){ + var syncMap *sync.Map + assert.Equal(t, syncMap, GetStorage()) +} + +func TestGetStorage__Existing__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := sync.Map{} + syncMap.SetMap(&testMap) + storage = &syncMap + assert.Equal(t, &syncMap, GetStorage()) +} + +func TestSetStorage__NonEmpty__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := sync.Map{} + syncMap.SetMap(&testMap) + SetStorage(&syncMap) + assert.Equal(t, &syncMap, GetStorage()) +} + +func TestSetStorage__Empty__Fail(t *testing.T){ + emptyMap := sync.Map{} + SetStorage(&emptyMap) + assert.Equal(t, &emptyMap, GetStorage()) +} + +func TestInitDBInMemory__Success(t *testing.T){ + assert.NotPanics(t, func(){ + InitDBInMemory() + }) +} + +func TestInitDBFromStorage__Existing__Success(t *testing.T){ + file.CreateAndWriteString("test.json","{\"123\":\"123\"}") + InitDBFromStorage("test.json") + testMap := map[string]string{"123":"123"} + assert.Equal(t, testMap, *(GetStorage().GetMap())) + + os.Remove("test.json") +} + +func TestInitDBFromStorage__NotExisting__Success(t *testing.T){ + InitDBFromStorage("test.json") + testMap := map[string]string{} + assert.Equal(t, testMap, *(GetStorage().GetMap())) + + os.Remove("test.json") +} + +func TestRestoreDBFromDump__NonEmpty__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + RestoreDBFromDump(json.PackMapToJSON(testMap)) + assert.Equal(t, testMap, *(GetStorage().GetMap())) +} + +func TestSaveDBToStorage__NonEmpty__Success(t *testing.T){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + syncMap := sync.Map{} + syncMap.SetMap(&testMap) + SetStorage(&syncMap) + SaveDBToStorage("test.json") + assert.Equal(t, "{\"1\":\"one\",\"2\":\"two\",\"3\":\"three\"}", file.OpenAndReadString("test.json")) + + os.Remove("test.json") +} + From 9311053f5f9b4b47b858ac9f166e563bb045ea25 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 14:50:43 +0300 Subject: [PATCH 065/104] Made query handler more independent & fixed dependencies --- server/handler/handle_connection.go | 2 +- server/handler/handle_query.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index 74c9c3b..f4b7840 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -36,7 +36,7 @@ func HandleConnection(c net.Conn){ log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), req) if regex.QueryReg.MatchString(req){ - resp:= HandleQuery(req) + resp:= HandleQuery(req, inmemory.GetStorage()) SendResponse(resp, c) } else if regex.ExitReg.MatchString(req){ diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index a0ea778..b173fea 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -1,26 +1,27 @@ package handler import ( + "NonRelDB/util/sync" "strings" "NonRelDB/server/storage/inmemory" "NonRelDB/util/regex" ) // HandleQuery handling queries to db. -func HandleQuery(query string) string { +func HandleQuery(query string, syncMap *sync.Map) string { queryParts := strings.Split(query, " ") if len (queryParts) >= 2 { switch queryCtx := strings.ToLower(queryParts[0]); queryCtx{ case "get":{ - return inmemory.GetStorage().Get(queryParts[1]) + return syncMap.Get(queryParts[1]) } case "set":{ value := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") - return inmemory.GetStorage().Set(queryParts[1], value) + return syncMap.Set(queryParts[1], value) } case "del":{ - return inmemory.GetStorage().Del(queryParts[1]) + return syncMap.Del(queryParts[1]) } case "keys":{ pattern := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") From a6262e885e557db72f453bb5b993f9083dcd6e88 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 20:48:11 +0300 Subject: [PATCH 066/104] Fixed keys feature in sync map --- util/sync/sync_map.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/sync/sync_map.go b/util/sync/sync_map.go index 3bd2dee..4cc0e0c 100644 --- a/util/sync/sync_map.go +++ b/util/sync/sync_map.go @@ -70,7 +70,7 @@ func (syncMap *Map) Keys(pattern string) string { return "Pattern is incorrect" } - for key := range ((*syncMap.storage)) { + for key := range (*syncMap.storage) { if regex.MatchString(key) { keys = append(keys, key) } From a76cb95c38fd7d5c6247f232e6e46bb361a8a5d8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 20:49:00 +0300 Subject: [PATCH 067/104] Fixed handle for keys feature in query handler --- server/handler/handle_query.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index b173fea..3e8905a 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -3,7 +3,6 @@ package handler import ( "NonRelDB/util/sync" "strings" - "NonRelDB/server/storage/inmemory" "NonRelDB/util/regex" ) @@ -25,7 +24,7 @@ func HandleQuery(query string, syncMap *sync.Map) string { } case "keys":{ pattern := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") - return inmemory.GetStorage().Keys(pattern) + return syncMap.Keys(pattern) } } } From b4cd0ca9d91b5f620dd8b6831790c452d120a41a Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 20:49:36 +0300 Subject: [PATCH 068/104] Added unit tests for query handler --- server/handler/handle_query_test.go | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 server/handler/handle_query_test.go diff --git a/server/handler/handle_query_test.go b/server/handler/handle_query_test.go new file mode 100644 index 0000000..35d1fe7 --- /dev/null +++ b/server/handler/handle_query_test.go @@ -0,0 +1,63 @@ +package handler + +import ( + "NonRelDB/util/sync" + "testing" + "github.com/stretchr/testify/assert" +) + + +var testSyncMap *sync.Map + +// Used as setup in testing. +func init(){ + testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} + testSyncMap = &sync.Map{} + testSyncMap.SetMap(&testMap) +} + +func TestHandleQuery__GetExisting__Found(t *testing.T){ + assert.Equal(t, "one", HandleQuery("get 1", testSyncMap)) +} + +func TestHandleQuery__GetExisting__NotFound(t *testing.T){ + assert.Equal(t, "Value with this key not found", HandleQuery("get 4", testSyncMap)) +} + +func TestHandleQuery__SetExisting__Changed(t *testing.T){ + assert.Equal(t, "two", HandleQuery("get 2", testSyncMap)) + assert.Equal(t, "Value has changed", HandleQuery("set 2 \"2\"",testSyncMap)) + assert.Equal(t, "2", HandleQuery("get 2", testSyncMap)) +} + +func TestHandleQuery__SetNonExistring__Created(t *testing.T){ + assert.Equal(t, "Value with this key not found", HandleQuery("get 4", testSyncMap)) + assert.Equal(t, "Value has changed", HandleQuery("set 4 \"four\"", testSyncMap)) + assert.Equal(t, "four", HandleQuery("get 4", testSyncMap)) +} + +func TestHandleQuery__DelNonExisting__NotFound(t *testing.T){ + assert.Equal(t, "Value with this key not found", HandleQuery("del 5", testSyncMap)) +} + +func TestHandleQuery__DelExisting__Deleted(t *testing.T){ + assert.Equal(t, "three", HandleQuery("del 3", testSyncMap)) + assert.Equal(t, "Value with this key not found", HandleQuery("get 3", testSyncMap)) +} + +func TestHandleQuery__KeysExistingCorrectWildcard__Found(t *testing.T){ + querySet := HandleQuery("keys \"/*\"", testSyncMap) + assert.True(t, "1,2,4" == querySet || "1,4,2" == querySet || "2,1,4" == querySet || "2,4,1" == querySet || "4,1,2" == querySet || "4,2,1" == querySet) +} + +func TestHandleQuery__KeysNotExisting__NotFound(t *testing.T){ + assert.Equal(t, "Keys with this pattern not found", HandleQuery("keys \"123\"", testSyncMap)) +} + +func TestHandleQuery__KeysIncorrectWildcard__IncorrectPattern(t *testing.T){ + assert.Equal(t, "Pattern is incorrect", HandleQuery("keys \"*\"", testSyncMap)) +} + +func TestHandleQuery__123__UndefinedQuery(t *testing.T){ + assert.Equal(t, "Undefined query", HandleQuery("123", testSyncMap)) +} From 2eb5afab7e1d4b4a58bd79bc9d177e37f965e778 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 21:17:03 +0300 Subject: [PATCH 069/104] Changed prefix in client cmd from 'NonRelDb' to server's adr --- client/handler/handle_connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index 250600a..f647cc3 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -20,7 +20,7 @@ func HandleConnection(c net.Conn){ consoleReader := bufio.NewReader(os.Stdin) netReader := bufio.NewReader(c) for { - fmt.Print("NonRelDB> ") + fmt.Printf("%s> ", c.RemoteAddr().String()) req, err := consoleReader.ReadString('\n') req = strings.Trim(req, "\n") From a81cd626498a127b0863d8ac2df3e7d5045545c5 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 21:31:44 +0300 Subject: [PATCH 070/104] Changed names for entrypoint files --- client/client.go | 63 +++++++++++++++++++++++++++++++++++++++++++++ server/server.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 client/client.go create mode 100644 server/server.go diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..4d081ef --- /dev/null +++ b/client/client.go @@ -0,0 +1,63 @@ +package main + +import ( + "os" + "fmt" + "bufio" + "net" + "flag" + "NonRelDB/log" + "NonRelDB/client/handler" +) + +var host string +var port string +var dump bool +var restore bool +var location string + +func init(){ + flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") + flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") + flag.StringVar(&port, "port", "9090", "Defines host port") + flag.StringVar(&port, "p", "9090", "Defines host port") + flag.BoolVar(&dump, "dump", false, "Requests db dump in json format from server") + flag.BoolVar(&restore, "restore", false, "Restores db from dumped file") + flag.Parse() +} + +// main entry point for client. +func main(){ + c, err := net.Dial("tcp", host + ":" + port) + defer c.Close() + + if err != nil { + log.Error.Panicln(err.Error()) + } + + if dump { + fmt.Fprintf(c, "dump\n") + dbDump, err := bufio.NewReader(c).ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Println(dbDump) + return + } + + if restore { + fmt.Fprintf(c, "restore\n") + dbRestore, err := bufio.NewReader(os.Stdin).ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Fprintf(c, dbRestore) + return + } + + handler.HandleConnection(c) +} \ No newline at end of file diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..9c63734 --- /dev/null +++ b/server/server.go @@ -0,0 +1,66 @@ +package main + +import ( + "NonRelDB/server/handler" + "NonRelDB/log" + "NonRelDB/server/storage/inmemory" + "flag" + "net" + "os" + "os/signal" + "syscall" +) + +var ip string +var port string +var mode string +var location string + +func init() { + flag.StringVar(&ip, "ip", "127.0.0.1", "Defines host ip") + flag.StringVar(&port, "port", "9090", "Defines host port") + flag.StringVar(&port, "p", "9090", "Defines host port") + flag.StringVar(&mode, "mode", "memory", "Defines storage location") + flag.StringVar(&mode, "m", "memory", "Defines storage location") + flag.StringVar(&location, "location", "storage.json", "Defines storage location") + flag.StringVar(&location, "l", "storage.json", "Defines storage location") + flag.Parse() +} + +// storageInit init of storage. +func storageInit() { + if mode == "memory" { + inmemory.InitDBInMemory() + } else if mode == "disk" { + inmemory.InitDBFromStorage(location) + } +} + +// cleanup storage cleanup. +func cleanup() { + sign := make(chan os.Signal, 2) + signal.Notify(sign, os.Interrupt, syscall.SIGTERM) + go func() { + <-sign + log.Info.Println("Ctrl+C pressed in Terminal") + if (mode == "disk"){ + inmemory.SaveDBToStorage(location) + } + os.Exit(0) + }() +} + +// main entry point for server. +func main() { + storageInit() + cleanup() + + l, err := net.Listen("tcp", ip+":"+port) + + if err != nil { + log.Error.Panicln(err.Error()) + } + + log.Info.Printf("Server started listening on %s", l.Addr().String()) + handler.HandleListener(l) +} From 33d9eaa4808623eb089d197b0c49763fc0e24e8a Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Wed, 12 Dec 2018 21:37:23 +0300 Subject: [PATCH 071/104] Fixed .gitignore for binaries & out & json files --- .gitignore | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 06da8ea..38322d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -client/dump.json -client/main -server/main -server/storage.json +client/client +server/server *.out +*.json From bc5032b6001676adc780818ee8dc55bf1636cd2e Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Thu, 13 Dec 2018 23:05:49 +0300 Subject: [PATCH 072/104] Deleted old entry points files --- client/main.go | 63 ----------------------------------------------- server/main.go | 66 -------------------------------------------------- 2 files changed, 129 deletions(-) delete mode 100644 client/main.go delete mode 100644 server/main.go diff --git a/client/main.go b/client/main.go deleted file mode 100644 index 4d081ef..0000000 --- a/client/main.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "os" - "fmt" - "bufio" - "net" - "flag" - "NonRelDB/log" - "NonRelDB/client/handler" -) - -var host string -var port string -var dump bool -var restore bool -var location string - -func init(){ - flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") - flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") - flag.StringVar(&port, "port", "9090", "Defines host port") - flag.StringVar(&port, "p", "9090", "Defines host port") - flag.BoolVar(&dump, "dump", false, "Requests db dump in json format from server") - flag.BoolVar(&restore, "restore", false, "Restores db from dumped file") - flag.Parse() -} - -// main entry point for client. -func main(){ - c, err := net.Dial("tcp", host + ":" + port) - defer c.Close() - - if err != nil { - log.Error.Panicln(err.Error()) - } - - if dump { - fmt.Fprintf(c, "dump\n") - dbDump, err := bufio.NewReader(c).ReadString('\n') - - if err != nil { - log.Error.Panicln(err.Error()) - } - - fmt.Println(dbDump) - return - } - - if restore { - fmt.Fprintf(c, "restore\n") - dbRestore, err := bufio.NewReader(os.Stdin).ReadString('\n') - - if err != nil { - log.Error.Panicln(err.Error()) - } - - fmt.Fprintf(c, dbRestore) - return - } - - handler.HandleConnection(c) -} \ No newline at end of file diff --git a/server/main.go b/server/main.go deleted file mode 100644 index 9c63734..0000000 --- a/server/main.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "NonRelDB/server/handler" - "NonRelDB/log" - "NonRelDB/server/storage/inmemory" - "flag" - "net" - "os" - "os/signal" - "syscall" -) - -var ip string -var port string -var mode string -var location string - -func init() { - flag.StringVar(&ip, "ip", "127.0.0.1", "Defines host ip") - flag.StringVar(&port, "port", "9090", "Defines host port") - flag.StringVar(&port, "p", "9090", "Defines host port") - flag.StringVar(&mode, "mode", "memory", "Defines storage location") - flag.StringVar(&mode, "m", "memory", "Defines storage location") - flag.StringVar(&location, "location", "storage.json", "Defines storage location") - flag.StringVar(&location, "l", "storage.json", "Defines storage location") - flag.Parse() -} - -// storageInit init of storage. -func storageInit() { - if mode == "memory" { - inmemory.InitDBInMemory() - } else if mode == "disk" { - inmemory.InitDBFromStorage(location) - } -} - -// cleanup storage cleanup. -func cleanup() { - sign := make(chan os.Signal, 2) - signal.Notify(sign, os.Interrupt, syscall.SIGTERM) - go func() { - <-sign - log.Info.Println("Ctrl+C pressed in Terminal") - if (mode == "disk"){ - inmemory.SaveDBToStorage(location) - } - os.Exit(0) - }() -} - -// main entry point for server. -func main() { - storageInit() - cleanup() - - l, err := net.Listen("tcp", ip+":"+port) - - if err != nil { - log.Error.Panicln(err.Error()) - } - - log.Info.Printf("Server started listening on %s", l.Addr().String()) - handler.HandleListener(l) -} From 587485d8de23caff3227b74ff2f549b47594a6fb Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 03:05:28 +0300 Subject: [PATCH 073/104] Added VERSION file --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..9f8e9b6 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0 \ No newline at end of file From a80ef8c7159aec3b8706de1fac45b4fd3b84db3c Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 03:05:49 +0300 Subject: [PATCH 074/104] Added Dockerfile --- Dockerfile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..22a35fa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.11-alpine3.8 + +EXPOSE 9090 + +RUN mkdir /NonRelDB/ + +ADD . /go/src/NonRelDB/ + +WORKDIR /go/src/NonRelDB/server + +RUN go build *.go + +WORKDIR /go/src/NonRelDB/client + +RUN go build *.go + +ENTRYPOINT [ "/go/src/NonRelDB/server/server" ] + From 9cef8615819cff8d3e73502426fdf1263bd80e69 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 16:33:51 +0300 Subject: [PATCH 075/104] Fixed sync map tests --- util/sync/sync_map_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/util/sync/sync_map_test.go b/util/sync/sync_map_test.go index db4bb49..b28300a 100644 --- a/util/sync/sync_map_test.go +++ b/util/sync/sync_map_test.go @@ -76,7 +76,9 @@ func TestKeys__FoundWildcard__Success(t *testing.T){ testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} syncMap := Map{} syncMap.SetMap(&testMap) - assert.Equal(t, "1,2,3", syncMap.Keys("/*")) + querySet := syncMap.Keys("/*") + assert.True(t, "1,2,3" == querySet || "1,3,2" == querySet || "2,1,3" == querySet || "2,3,1" == querySet || "3,2,1" == querySet || "3,1,2" == querySet) + } func TestKeys__IncorrectWildcard__Fail(t *testing.T){ From 90e7408423f47db4d2e17eec4b2e85f2eb65cae2 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 16:36:20 +0300 Subject: [PATCH 076/104] Added Makefile --- Makefile | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..720bad1 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +REPOSITORY_PATH := $(HOME)/Dev/Go/src/NonRelDB + +VERSION := $(shell cat VERSION) + +.PHONY: fmt +check: + go vet $(REPOSITORY_PATH)/server + go vet $(REPOSITORY_PATH)/client + go vet $(REPOSITORY_PATH)/log + go vet $(REPOSITORY_PATH)/util/collection + go vet $(REPOSITORY_PATH)/util/file + go vet $(REPOSITORY_PATH)/util/json + go vet $(REPOSITORY_PATH)/util/regex + go vet $(REPOSITORY_PATH)/util/sync + + goimports $(REPOSITORY_PATH)/server + goimports $(REPOSITORY_PATH)/client + goimports $(REPOSITORY_PATH)/log + goimports $(REPOSITORY_PATH)/util/collection + goimports $(REPOSITORY_PATH)/util/file + goimports $(REPOSITORY_PATH)/util/json + goimports $(REPOSITORY_PATH)/util/regex + goimports $(REPOSITORY_PATH)/util/sync + + + golint $(REPOSITORY_PATH)/server + golint $(REPOSITORY_PATH)/client + golint $(REPOSITORY_PATH)/log + golint $(REPOSITORY_PATH)/util/collection + golint $(REPOSITORY_PATH)/util/file + golint $(REPOSITORY_PATH)/util/json + golint $(REPOSITORY_PATH)/util/regex + golint $(REPOSITORY_PATH)/util/sync + +clean: + rm server/server && rm client/client + +build-server: + go build -o server/server $(REPOSITORY_PATH)/server/server.go + +build-client: + go build -o client/client $(REPOSITORY_PATH)/client/client.go + +build-container: + sudo docker build -t "nonreldb" . + +run: + sudo docker run --net=host nonreldb + +test: + echo "Running unit & integration tests" + go test $(REPOSITORY_PATH)/server/handler -coverprofile cover.out + go test $(REPOSITORY_PATH)/server/storage/inmemory -coverprofile cover.out + go test $(REPOSITORY_PATH)/util/collection -coverprofile cover.out + go test $(REPOSITORY_PATH)/util/file -coverprofile cover.out + go test $(REPOSITORY_PATH)/util/json -coverprofile cover.out + go test $(REPOSITORY_PATH)/util/regex -coverprofile cover.out + go test $(REPOSITORY_PATH)/util/sync -coverprofile cover.out + + + + From ba0a78306d72a76bf53e83fa98d479c490f4abb8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 16:44:44 +0300 Subject: [PATCH 077/104] Fixed errors occured with golint --- util/collection/slice_util.go | 4 ++-- util/regex/regex_util.go | 6 ++++++ util/sync/sync_map.go | 9 +++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/util/collection/slice_util.go b/util/collection/slice_util.go index 09dca24..2630068 100644 --- a/util/collection/slice_util.go +++ b/util/collection/slice_util.go @@ -6,8 +6,8 @@ import ( // ConnIndex returns index of neccessary element in net.Conn slice if found, otherwise -1. func ConnIndex(slice []net.Conn, value net.Conn) int{ - for index, slice_value := range slice { - if slice_value == value { + for index, sliceValue := range slice { + if sliceValue == value { return index } } diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index e0c4b72..f28a619 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -5,11 +5,17 @@ import ( ) var ( + // DoubleQuoteReg regexp for values inside double quotes. DoubleQuoteReg *regexp.Regexp + // QueryReg regexp which checks is this query to db. QueryReg *regexp.Regexp + // TopicReg regexp which checks is this topic's command. TopicReg *regexp.Regexp + // ExitReg regexp which checks is this exit command. ExitReg *regexp.Regexp + // DumpReg regexp which checks is this dump command. DumpReg *regexp.Regexp + // RestoreReg regexp which check is this restore command. RestoreReg *regexp.Regexp ) diff --git a/util/sync/sync_map.go b/util/sync/sync_map.go index 4cc0e0c..7ccedcb 100644 --- a/util/sync/sync_map.go +++ b/util/sync/sync_map.go @@ -30,9 +30,8 @@ func (syncMap *Map) Get(key string) string { v := (*syncMap.storage)[key] if v != ""{ return v; - } else { - return "Value with this key not found" } + return "Value with this key not found" } // Set set value according to key. @@ -51,9 +50,8 @@ func (syncMap *Map) Del(key string) string{ if v != "" { delete((*syncMap.storage),key) return v; - } else { - return "Value with this key not found" } + return "Value with this key not found" } // Keys returns keys which match to pattern. @@ -78,7 +76,6 @@ func (syncMap *Map) Keys(pattern string) string { if keys != nil { return strings.Join(keys,",") - } else { - return "Keys with this pattern not found" } + return "Keys with this pattern not found" } \ No newline at end of file From 4dac52090e2665fcb8bd539d54915d8b4821e705 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 17:15:59 +0300 Subject: [PATCH 078/104] Added common coverage.out & coverage html generation --- Makefile | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 720bad1..8fae7e7 100644 --- a/Makefile +++ b/Makefile @@ -49,13 +49,12 @@ run: test: echo "Running unit & integration tests" - go test $(REPOSITORY_PATH)/server/handler -coverprofile cover.out - go test $(REPOSITORY_PATH)/server/storage/inmemory -coverprofile cover.out - go test $(REPOSITORY_PATH)/util/collection -coverprofile cover.out - go test $(REPOSITORY_PATH)/util/file -coverprofile cover.out - go test $(REPOSITORY_PATH)/util/json -coverprofile cover.out - go test $(REPOSITORY_PATH)/util/regex -coverprofile cover.out - go test $(REPOSITORY_PATH)/util/sync -coverprofile cover.out + go test $(REPOSITORY_PATH)/server/handler $(REPOSITORY_PATH)/server/storage/inmemory \ + $(REPOSITORY_PATH)/util/collection $(REPOSITORY_PATH)/util/file \ + $(REPOSITORY_PATH)/util/json $(REPOSITORY_PATH)/util/sync \ + $(REPOSITORY_PATH)/util/regex -coverprofile coverage.out + + go tool cover -html=coverage.out From 6c965b2b0149035481f1518a5563bc76bb6ac589 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 17:28:47 +0300 Subject: [PATCH 079/104] Fixed tests for sync map --- util/sync/sync_map_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/sync/sync_map_test.go b/util/sync/sync_map_test.go index b28300a..77ea43f 100644 --- a/util/sync/sync_map_test.go +++ b/util/sync/sync_map_test.go @@ -69,7 +69,7 @@ func TestDel__DeletedAndNotFound__Fail(t *testing.T){ syncMap := Map{} syncMap.SetMap(&testMap) syncMap.Del("1") - assert.Equal(t, "Value with this key not found", syncMap.Get("1")) + assert.Equal(t, "Value with this key not found", syncMap.Del("1")) } func TestKeys__FoundWildcard__Success(t *testing.T){ From 72e46641266ae876b664b3b23f691fec524a83df Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 18:24:32 +0300 Subject: [PATCH 080/104] Optimized Makefile --- Makefile | 39 ++++++++------------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index 8fae7e7..d0b5e25 100644 --- a/Makefile +++ b/Makefile @@ -4,33 +4,11 @@ VERSION := $(shell cat VERSION) .PHONY: fmt check: - go vet $(REPOSITORY_PATH)/server - go vet $(REPOSITORY_PATH)/client - go vet $(REPOSITORY_PATH)/log - go vet $(REPOSITORY_PATH)/util/collection - go vet $(REPOSITORY_PATH)/util/file - go vet $(REPOSITORY_PATH)/util/json - go vet $(REPOSITORY_PATH)/util/regex - go vet $(REPOSITORY_PATH)/util/sync - - goimports $(REPOSITORY_PATH)/server - goimports $(REPOSITORY_PATH)/client - goimports $(REPOSITORY_PATH)/log - goimports $(REPOSITORY_PATH)/util/collection - goimports $(REPOSITORY_PATH)/util/file - goimports $(REPOSITORY_PATH)/util/json - goimports $(REPOSITORY_PATH)/util/regex - goimports $(REPOSITORY_PATH)/util/sync - - - golint $(REPOSITORY_PATH)/server - golint $(REPOSITORY_PATH)/client - golint $(REPOSITORY_PATH)/log - golint $(REPOSITORY_PATH)/util/collection - golint $(REPOSITORY_PATH)/util/file - golint $(REPOSITORY_PATH)/util/json - golint $(REPOSITORY_PATH)/util/regex - golint $(REPOSITORY_PATH)/util/sync + go vet NonRelDB/... + + goimports **/*.go + + golint **/*.go clean: rm server/server && rm client/client @@ -49,13 +27,12 @@ run: test: echo "Running unit & integration tests" - go test $(REPOSITORY_PATH)/server/handler $(REPOSITORY_PATH)/server/storage/inmemory \ - $(REPOSITORY_PATH)/util/collection $(REPOSITORY_PATH)/util/file \ - $(REPOSITORY_PATH)/util/json $(REPOSITORY_PATH)/util/sync \ - $(REPOSITORY_PATH)/util/regex -coverprofile coverage.out + + go test NonRelDB/... -coverprofile coverage.out go tool cover -html=coverage.out + From 1e3eb5aec7a92bbba789a78de54f1e48544f7fdb Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 20:34:18 +0300 Subject: [PATCH 081/104] Refactored code style using goimports --- client/client.go | 18 +++--- client/handler/handle_connection.go | 42 ++++++------- client/handler/handle_topic.go | 12 ++-- log/logger.go | 14 ++--- server/handler/handle_connection.go | 62 +++++++++---------- server/handler/handle_listener.go | 12 ++-- server/handler/handle_query.go | 26 ++++---- server/handler/handle_query_test.go | 28 ++++----- server/server.go | 6 +- server/storage/inmemory/inmemory_service.go | 20 +++--- .../storage/inmemory/inmemory_service_test.go | 38 ++++++------ server/topic/topic_service.go | 20 +++--- util/collection/slice_util.go | 4 +- util/collection/slice_util_test.go | 19 +++--- util/file/file_read.go | 22 +++---- util/file/file_read_test.go | 19 +++--- util/file/file_write.go | 15 +++-- util/file/file_write_test.go | 15 +++-- util/json/json_decode.go | 8 +-- util/json/json_decode_test.go | 21 ++++--- util/json/json_encode.go | 26 ++++---- util/json/json_encode_test.go | 42 ++++++------- util/regex/regex_util.go | 4 +- util/regex/regex_util_test.go | 1 - util/sync/sync_map.go | 26 ++++---- util/sync/sync_map_test.go | 57 +++++++++-------- 26 files changed, 290 insertions(+), 287 deletions(-) diff --git a/client/client.go b/client/client.go index 4d081ef..8be1a7a 100644 --- a/client/client.go +++ b/client/client.go @@ -1,13 +1,13 @@ package main import ( - "os" - "fmt" + "NonRelDB/client/handler" + "NonRelDB/log" "bufio" - "net" "flag" - "NonRelDB/log" - "NonRelDB/client/handler" + "fmt" + "net" + "os" ) var host string @@ -16,7 +16,7 @@ var dump bool var restore bool var location string -func init(){ +func init() { flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") flag.StringVar(&port, "port", "9090", "Defines host port") @@ -27,8 +27,8 @@ func init(){ } // main entry point for client. -func main(){ - c, err := net.Dial("tcp", host + ":" + port) +func main() { + c, err := net.Dial("tcp", host+":"+port) defer c.Close() if err != nil { @@ -60,4 +60,4 @@ func main(){ } handler.HandleConnection(c) -} \ No newline at end of file +} diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go index f647cc3..63982f1 100644 --- a/client/handler/handle_connection.go +++ b/client/handler/handle_connection.go @@ -1,22 +1,22 @@ package handler import ( - "strings" - "net" - "bufio" - "os" - "fmt" "NonRelDB/log" "NonRelDB/util/regex" + "bufio" + "fmt" + "net" + "os" + "strings" ) // SendRequest sends request to specified connection. -func SendRequest(req string, c net.Conn){ - fmt.Fprintf(c, req + "\n") +func SendRequest(req string, c net.Conn) { + fmt.Fprintf(c, req+"\n") } // HandleConnection handling communication with server. -func HandleConnection(c net.Conn){ +func HandleConnection(c net.Conn) { consoleReader := bufio.NewReader(os.Stdin) netReader := bufio.NewReader(c) for { @@ -28,7 +28,7 @@ func HandleConnection(c net.Conn){ log.Error.Panicln(err.Error()) } - if regex.QueryReg.MatchString(req){ + if regex.QueryReg.MatchString(req) { SendRequest(req, c) resp, err := netReader.ReadString('\n') @@ -38,7 +38,7 @@ func HandleConnection(c net.Conn){ fmt.Println(resp) - } else if regex.ExitReg.MatchString(req){ + } else if regex.ExitReg.MatchString(req) { fmt.Println("Good bye") SendRequest(req, c) return @@ -46,20 +46,20 @@ func HandleConnection(c net.Conn){ } else if regex.TopicReg.MatchString(req) { reqParts := strings.Split(req, " ") - if len(reqParts) == 2{ - if strings.ToLower(reqParts[0]) == "subscribe"{ - SendRequest(req, c) - HandleTopic(c, *netReader, reqParts[1]) - } - } else if len(reqParts) >= 3{ - if strings.ToLower(reqParts[0]) == "publish"{ - SendRequest(req, c) - } + if len(reqParts) == 2 { + if strings.ToLower(reqParts[0]) == "subscribe" { + SendRequest(req, c) + HandleTopic(c, *netReader, reqParts[1]) + } + } else if len(reqParts) >= 3 { + if strings.ToLower(reqParts[0]) == "publish" { + SendRequest(req, c) + } } } else { fmt.Println("Bad request") - continue + continue } } -} \ No newline at end of file +} diff --git a/client/handler/handle_topic.go b/client/handler/handle_topic.go index 0f226b9..5c6b986 100644 --- a/client/handler/handle_topic.go +++ b/client/handler/handle_topic.go @@ -2,8 +2,8 @@ package handler import ( "NonRelDB/log" - "fmt" "bufio" + "fmt" "net" "os" "os/signal" @@ -17,22 +17,22 @@ func quit(c net.Conn, topic string) { go func() { <-sign fmt.Println("Ctrl+C pressed in Terminal") - fmt.Fprintf(c, "unsubscribe " + topic + "\n") + fmt.Fprintf(c, "unsubscribe "+topic+"\n") os.Exit(0) }() } // HandleTopic listening messages from specified topic. -func HandleTopic(c net.Conn, r bufio.Reader, topic string){ +func HandleTopic(c net.Conn, r bufio.Reader, topic string) { quit(c, topic) fmt.Printf("Reading messages from %s... (press Ctrl + C to stop)\n", topic) for { msg, err := r.ReadString('\n') - + if err != nil { log.Error.Panicln(err.Error()) } - + fmt.Print(msg) } -} \ No newline at end of file +} diff --git a/log/logger.go b/log/logger.go index 6982409..3d93de9 100644 --- a/log/logger.go +++ b/log/logger.go @@ -8,15 +8,15 @@ import ( // Presents 4 logging levels: trace, info, warning and error. var ( - Trace *log.Logger - Info *log.Logger + Trace *log.Logger + Info *log.Logger Warning *log.Logger - Error *log.Logger + Error *log.Logger ) -func init(){ - Trace = log.New(ioutil.Discard,"[TRACE] ", log.Ldate|log.Ltime|log.Lshortfile) - Info = log.New(os.Stdout,"[INFO] ", log.Ldate|log.Ltime|log.Lshortfile) +func init() { + Trace = log.New(ioutil.Discard, "[TRACE] ", log.Ldate|log.Ltime|log.Lshortfile) + Info = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime|log.Lshortfile) Warning = log.New(os.Stdout, "[WARNING] ", log.Ldate|log.Ltime|log.Lshortfile) Error = log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile) -} \ No newline at end of file +} diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go index f4b7840..39e11bd 100644 --- a/server/handler/handle_connection.go +++ b/server/handler/handle_connection.go @@ -1,55 +1,55 @@ package handler import ( - "fmt" - "strings" - "bufio" - "net" "NonRelDB/log" - "NonRelDB/util/json" "NonRelDB/server/storage/inmemory" "NonRelDB/server/topic" + "NonRelDB/util/json" "NonRelDB/util/regex" + "bufio" + "fmt" + "net" + "strings" ) // SendResponse sends response to specified connection. -func SendResponse(resp string, c net.Conn){ - fmt.Fprintf(c, resp + "\n") +func SendResponse(resp string, c net.Conn) { + fmt.Fprintf(c, resp+"\n") log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) } // HandleConnection handling communication with client. -func HandleConnection(c net.Conn){ +func HandleConnection(c net.Conn) { defer c.Close() netReader := bufio.NewReader(c) - + for { req, err := netReader.ReadString('\n') - req = strings.TrimSuffix(req,"\n") + req = strings.TrimSuffix(req, "\n") if err != nil { log.Error.Println(err.Error()) return - } + } log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), req) - if regex.QueryReg.MatchString(req){ - resp:= HandleQuery(req, inmemory.GetStorage()) + if regex.QueryReg.MatchString(req) { + resp := HandleQuery(req, inmemory.GetStorage()) SendResponse(resp, c) - - } else if regex.ExitReg.MatchString(req){ + + } else if regex.ExitReg.MatchString(req) { log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) return - - } else if regex.DumpReg.MatchString(req){ + + } else if regex.DumpReg.MatchString(req) { dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) - fmt.Fprintf(c, dbDump + "\n") + fmt.Fprintf(c, dbDump+"\n") log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) return - } else if regex.RestoreReg.MatchString(req){ + } else if regex.RestoreReg.MatchString(req) { dbDump, err := netReader.ReadString('\n') if err != nil { @@ -60,29 +60,29 @@ func HandleConnection(c net.Conn){ log.Info.Printf("Successfully restored dump from %s", c.RemoteAddr().String()) return - } else if regex.TopicReg.MatchString(req){ + } else if regex.TopicReg.MatchString(req) { reqParts := strings.Split(req, " ") - + if len(reqParts) == 2 { - if strings.ToLower(reqParts[0]) == "subscribe"{ + if strings.ToLower(reqParts[0]) == "subscribe" { topic.Subscribe(reqParts[1], c) - - } else if strings.ToLower(reqParts[0]) == "unsubscribe"{ + + } else if strings.ToLower(reqParts[0]) == "unsubscribe" { topic.Unsubscribe(reqParts[1], c) return - + } - - } else if len(reqParts) >= 3{ - if strings.ToLower(reqParts[0]) == "publish"{ + + } else if len(reqParts) >= 3 { + if strings.ToLower(reqParts[0]) == "publish" { msg := regex.DoubleQuoteReg.FindString(req) topic.Publish(reqParts[1], msg) } - + } - + } else { SendResponse("Bad request", c) } } -} \ No newline at end of file +} diff --git a/server/handler/handle_listener.go b/server/handler/handle_listener.go index 66e3b3d..ec6c503 100644 --- a/server/handler/handle_listener.go +++ b/server/handler/handle_listener.go @@ -1,20 +1,20 @@ package handler import ( - "net" "NonRelDB/log" + "net" ) // HandleListener accepts clients and runs their handlers. -func HandleListener(l net.Listener){ +func HandleListener(l net.Listener) { defer l.Close() for { c, err := l.Accept() if err != nil { - log.Warning.Printf("Failed connection from %s",c.RemoteAddr().String()) + log.Warning.Printf("Failed connection from %s", c.RemoteAddr().String()) c.Close() } - log.Info.Printf("%s was connected to server",c.RemoteAddr().String()) + log.Info.Printf("%s was connected to server", c.RemoteAddr().String()) go HandleConnection(c) - } -} \ No newline at end of file + } +} diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go index 3e8905a..ba3cb8b 100644 --- a/server/handler/handle_query.go +++ b/server/handler/handle_query.go @@ -1,29 +1,33 @@ package handler import ( + "NonRelDB/util/regex" "NonRelDB/util/sync" "strings" - "NonRelDB/util/regex" ) // HandleQuery handling queries to db. func HandleQuery(query string, syncMap *sync.Map) string { queryParts := strings.Split(query, " ") - - if len (queryParts) >= 2 { - switch queryCtx := strings.ToLower(queryParts[0]); queryCtx{ - case "get":{ + + if len(queryParts) >= 2 { + switch queryCtx := strings.ToLower(queryParts[0]); queryCtx { + case "get": + { return syncMap.Get(queryParts[1]) } - case "set":{ - value := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") + case "set": + { + value := strings.Trim(regex.DoubleQuoteReg.FindString(query), "\"") return syncMap.Set(queryParts[1], value) - } - case "del":{ + } + case "del": + { return syncMap.Del(queryParts[1]) } - case "keys":{ - pattern := strings.Trim(regex.DoubleQuoteReg.FindString(query),"\"") + case "keys": + { + pattern := strings.Trim(regex.DoubleQuoteReg.FindString(query), "\"") return syncMap.Keys(pattern) } } diff --git a/server/handler/handle_query_test.go b/server/handler/handle_query_test.go index 35d1fe7..40daad5 100644 --- a/server/handler/handle_query_test.go +++ b/server/handler/handle_query_test.go @@ -3,61 +3,61 @@ package handler import ( "NonRelDB/util/sync" "testing" + "github.com/stretchr/testify/assert" ) - var testSyncMap *sync.Map // Used as setup in testing. -func init(){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func init() { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} testSyncMap = &sync.Map{} testSyncMap.SetMap(&testMap) } -func TestHandleQuery__GetExisting__Found(t *testing.T){ +func TestHandleQuery__GetExisting__Found(t *testing.T) { assert.Equal(t, "one", HandleQuery("get 1", testSyncMap)) } -func TestHandleQuery__GetExisting__NotFound(t *testing.T){ +func TestHandleQuery__GetExisting__NotFound(t *testing.T) { assert.Equal(t, "Value with this key not found", HandleQuery("get 4", testSyncMap)) } -func TestHandleQuery__SetExisting__Changed(t *testing.T){ +func TestHandleQuery__SetExisting__Changed(t *testing.T) { assert.Equal(t, "two", HandleQuery("get 2", testSyncMap)) - assert.Equal(t, "Value has changed", HandleQuery("set 2 \"2\"",testSyncMap)) + assert.Equal(t, "Value has changed", HandleQuery("set 2 \"2\"", testSyncMap)) assert.Equal(t, "2", HandleQuery("get 2", testSyncMap)) } -func TestHandleQuery__SetNonExistring__Created(t *testing.T){ +func TestHandleQuery__SetNonExistring__Created(t *testing.T) { assert.Equal(t, "Value with this key not found", HandleQuery("get 4", testSyncMap)) assert.Equal(t, "Value has changed", HandleQuery("set 4 \"four\"", testSyncMap)) assert.Equal(t, "four", HandleQuery("get 4", testSyncMap)) } -func TestHandleQuery__DelNonExisting__NotFound(t *testing.T){ +func TestHandleQuery__DelNonExisting__NotFound(t *testing.T) { assert.Equal(t, "Value with this key not found", HandleQuery("del 5", testSyncMap)) } -func TestHandleQuery__DelExisting__Deleted(t *testing.T){ +func TestHandleQuery__DelExisting__Deleted(t *testing.T) { assert.Equal(t, "three", HandleQuery("del 3", testSyncMap)) assert.Equal(t, "Value with this key not found", HandleQuery("get 3", testSyncMap)) } -func TestHandleQuery__KeysExistingCorrectWildcard__Found(t *testing.T){ +func TestHandleQuery__KeysExistingCorrectWildcard__Found(t *testing.T) { querySet := HandleQuery("keys \"/*\"", testSyncMap) assert.True(t, "1,2,4" == querySet || "1,4,2" == querySet || "2,1,4" == querySet || "2,4,1" == querySet || "4,1,2" == querySet || "4,2,1" == querySet) } -func TestHandleQuery__KeysNotExisting__NotFound(t *testing.T){ +func TestHandleQuery__KeysNotExisting__NotFound(t *testing.T) { assert.Equal(t, "Keys with this pattern not found", HandleQuery("keys \"123\"", testSyncMap)) } -func TestHandleQuery__KeysIncorrectWildcard__IncorrectPattern(t *testing.T){ +func TestHandleQuery__KeysIncorrectWildcard__IncorrectPattern(t *testing.T) { assert.Equal(t, "Pattern is incorrect", HandleQuery("keys \"*\"", testSyncMap)) } -func TestHandleQuery__123__UndefinedQuery(t *testing.T){ +func TestHandleQuery__123__UndefinedQuery(t *testing.T) { assert.Equal(t, "Undefined query", HandleQuery("123", testSyncMap)) } diff --git a/server/server.go b/server/server.go index 9c63734..29bed2f 100644 --- a/server/server.go +++ b/server/server.go @@ -1,8 +1,8 @@ package main import ( - "NonRelDB/server/handler" "NonRelDB/log" + "NonRelDB/server/handler" "NonRelDB/server/storage/inmemory" "flag" "net" @@ -43,7 +43,7 @@ func cleanup() { go func() { <-sign log.Info.Println("Ctrl+C pressed in Terminal") - if (mode == "disk"){ + if mode == "disk" { inmemory.SaveDBToStorage(location) } os.Exit(0) @@ -56,7 +56,7 @@ func main() { cleanup() l, err := net.Listen("tcp", ip+":"+port) - + if err != nil { log.Error.Panicln(err.Error()) } diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go index 6e71839..3a9befe 100644 --- a/server/storage/inmemory/inmemory_service.go +++ b/server/storage/inmemory/inmemory_service.go @@ -1,11 +1,11 @@ package inmemory import ( - "os" - "NonRelDB/util/json" - "NonRelDB/util/file" "NonRelDB/log" + "NonRelDB/util/file" + "NonRelDB/util/json" "NonRelDB/util/sync" + "os" ) // Global variable for kv storage. @@ -17,12 +17,12 @@ func GetStorage() *sync.Map { } // SetStorage setter for storage -func SetStorage(syncMap *sync.Map){ +func SetStorage(syncMap *sync.Map) { storage = syncMap } // InitDBInMemory init kv db in memory. -func InitDBInMemory(){ +func InitDBInMemory() { storage = &sync.Map{} syncMap := make(map[string]string) storage.SetMap(&syncMap) @@ -30,7 +30,7 @@ func InitDBInMemory(){ } // InitDBFromStorage receives filename and load its content to inmemory storage. -func InitDBFromStorage(filename string){ +func InitDBFromStorage(filename string) { storage = &sync.Map{} _, err := os.Stat(filename) @@ -39,7 +39,7 @@ func InitDBFromStorage(filename string){ log.Warning.Printf("Storage doesnt exist. Will be created new with name %s", filename) f, err := os.Create(filename) - + if err != nil { log.Error.Panicln(err.Error()) } @@ -52,13 +52,13 @@ func InitDBFromStorage(filename string){ log.Info.Printf("DB successfully initialized from %s", filename) } -// RestoreDBFromDump restores db from received dump. -func RestoreDBFromDump(dump []byte){ +// RestoreDBFromDump restores db from received dump. +func RestoreDBFromDump(dump []byte) { storage.SetMap(json.UnpackFromJSON(dump)) } // SaveDBToStorage receives file name and saves inmemory storage to it. -func SaveDBToStorage(filename string){ +func SaveDBToStorage(filename string) { jsonBytes := json.PackMapToJSON((*storage.GetMap())) jsonString := string(jsonBytes) diff --git a/server/storage/inmemory/inmemory_service_test.go b/server/storage/inmemory/inmemory_service_test.go index efd546f..29d03d3 100644 --- a/server/storage/inmemory/inmemory_service_test.go +++ b/server/storage/inmemory/inmemory_service_test.go @@ -1,57 +1,58 @@ package inmemory import ( + "NonRelDB/util/file" "NonRelDB/util/json" + "NonRelDB/util/sync" "os" "testing" + "github.com/stretchr/testify/assert" - "NonRelDB/util/sync" - "NonRelDB/util/file" ) -func TestGetStorage__Empty__Fail(t *testing.T){ +func TestGetStorage__Empty__Fail(t *testing.T) { var syncMap *sync.Map assert.Equal(t, syncMap, GetStorage()) } -func TestGetStorage__Existing__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestGetStorage__Existing__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := sync.Map{} syncMap.SetMap(&testMap) storage = &syncMap assert.Equal(t, &syncMap, GetStorage()) } -func TestSetStorage__NonEmpty__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestSetStorage__NonEmpty__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := sync.Map{} syncMap.SetMap(&testMap) SetStorage(&syncMap) assert.Equal(t, &syncMap, GetStorage()) } -func TestSetStorage__Empty__Fail(t *testing.T){ +func TestSetStorage__Empty__Fail(t *testing.T) { emptyMap := sync.Map{} SetStorage(&emptyMap) assert.Equal(t, &emptyMap, GetStorage()) } -func TestInitDBInMemory__Success(t *testing.T){ - assert.NotPanics(t, func(){ +func TestInitDBInMemory__Success(t *testing.T) { + assert.NotPanics(t, func() { InitDBInMemory() }) } -func TestInitDBFromStorage__Existing__Success(t *testing.T){ - file.CreateAndWriteString("test.json","{\"123\":\"123\"}") +func TestInitDBFromStorage__Existing__Success(t *testing.T) { + file.CreateAndWriteString("test.json", "{\"123\":\"123\"}") InitDBFromStorage("test.json") - testMap := map[string]string{"123":"123"} + testMap := map[string]string{"123": "123"} assert.Equal(t, testMap, *(GetStorage().GetMap())) os.Remove("test.json") } -func TestInitDBFromStorage__NotExisting__Success(t *testing.T){ +func TestInitDBFromStorage__NotExisting__Success(t *testing.T) { InitDBFromStorage("test.json") testMap := map[string]string{} assert.Equal(t, testMap, *(GetStorage().GetMap())) @@ -59,14 +60,14 @@ func TestInitDBFromStorage__NotExisting__Success(t *testing.T){ os.Remove("test.json") } -func TestRestoreDBFromDump__NonEmpty__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestRestoreDBFromDump__NonEmpty__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} RestoreDBFromDump(json.PackMapToJSON(testMap)) assert.Equal(t, testMap, *(GetStorage().GetMap())) } -func TestSaveDBToStorage__NonEmpty__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestSaveDBToStorage__NonEmpty__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := sync.Map{} syncMap.SetMap(&testMap) SetStorage(&syncMap) @@ -75,4 +76,3 @@ func TestSaveDBToStorage__NonEmpty__Success(t *testing.T){ os.Remove("test.json") } - diff --git a/server/topic/topic_service.go b/server/topic/topic_service.go index 52b1627..0375b82 100644 --- a/server/topic/topic_service.go +++ b/server/topic/topic_service.go @@ -2,30 +2,30 @@ package topic import ( "NonRelDB/log" + "NonRelDB/util/collection" "fmt" "net" - "NonRelDB/util/collection" ) // Storage for topic. var topics map[string][]net.Conn -func init(){ +func init() { topics = make(map[string][]net.Conn) } // Subscribe adding client to specified topic. -func Subscribe(name string, c net.Conn){ +func Subscribe(name string, c net.Conn) { if topics[name] == nil { topics[name] = make([]net.Conn, 10) - } + } topics[name] = append(topics[name], c) log.Info.Printf("%s just subscribed %s", c.RemoteAddr().String(), name) } // Unsubscribe removing client from specified topic. func Unsubscribe(name string, c net.Conn) { - if topics[name] != nil || len(topics) != 0{ + if topics[name] != nil || len(topics) != 0 { index := collection.ConnIndex(topics[name], c) if index != -1 { topics[name][index] = nil @@ -35,14 +35,14 @@ func Unsubscribe(name string, c net.Conn) { } // Publish publishes message to specified topic. -func Publish(name string, msg string){ +func Publish(name string, msg string) { if topics[name] != nil || len(topics) != 0 { - log.Info.Printf("%s just published in %s", msg , name) + log.Info.Printf("%s just published in %s", msg, name) for _, listener := range topics[name] { str := fmt.Sprintf("[%s]: %s", name, msg) - if (listener != nil){ - fmt.Fprintf(listener, str + "\n") + if listener != nil { + fmt.Fprintf(listener, str+"\n") } } } -} \ No newline at end of file +} diff --git a/util/collection/slice_util.go b/util/collection/slice_util.go index 2630068..d295979 100644 --- a/util/collection/slice_util.go +++ b/util/collection/slice_util.go @@ -5,11 +5,11 @@ import ( ) // ConnIndex returns index of neccessary element in net.Conn slice if found, otherwise -1. -func ConnIndex(slice []net.Conn, value net.Conn) int{ +func ConnIndex(slice []net.Conn, value net.Conn) int { for index, sliceValue := range slice { if sliceValue == value { return index } } return -1 -} \ No newline at end of file +} diff --git a/util/collection/slice_util_test.go b/util/collection/slice_util_test.go index 2586060..c80e1a7 100644 --- a/util/collection/slice_util_test.go +++ b/util/collection/slice_util_test.go @@ -1,23 +1,24 @@ package collection import ( + "net" "testing" + "github.com/stretchr/testify/assert" - "net" ) -type testConn struct{ +type testConn struct { net.Conn id int } -func TestConIndex__Contains__Success(t *testing.T){ +func TestConIndex__Contains__Success(t *testing.T) { testSlice := make([]net.Conn, 10, 10) - + neededConn := testConn{id: 123} for i := 0; i < len(testSlice); i++ { - testSlice[i] = testConn{id : i} + testSlice[i] = testConn{id: i} } testSlice[4] = neededConn @@ -25,14 +26,14 @@ func TestConIndex__Contains__Success(t *testing.T){ assert.Equal(t, 4, ConnIndex(testSlice, neededConn)) } -func TestConIndex__DoesntContain__Fail(t *testing.T){ +func TestConIndex__DoesntContain__Fail(t *testing.T) { testSlice := make([]net.Conn, 10, 10) - + neededConn := testConn{id: 123} for i := 0; i < len(testSlice); i++ { - testSlice[i] = testConn{id : i} + testSlice[i] = testConn{id: i} } assert.Equal(t, -1, ConnIndex(testSlice, neededConn)) -} \ No newline at end of file +} diff --git a/util/file/file_read.go b/util/file/file_read.go index ca203a6..84dfdd9 100644 --- a/util/file/file_read.go +++ b/util/file/file_read.go @@ -1,28 +1,28 @@ package file import ( - "io/ioutil" "NonRelDB/log" + "io/ioutil" ) // OpenAndReadString receives file name, reads this file and returns its string content. -func OpenAndReadString(name string) string{ +func OpenAndReadString(name string) string { bytes, err := ioutil.ReadFile(name) - - if err != nil{ + + if err != nil { log.Error.Panicln(err.Error()) } - + return string(bytes) } -// OpenAndRead receives file name, reads this file and returns byte array from it. -func OpenAndRead(name string) []byte{ +// OpenAndRead receives file name, reads this file and returns byte array from it. +func OpenAndRead(name string) []byte { bytes, err := ioutil.ReadFile(name) - - if err != nil{ + + if err != nil { log.Error.Panicln(err.Error()) } - + return bytes -} \ No newline at end of file +} diff --git a/util/file/file_read_test.go b/util/file/file_read_test.go index 17cf618..cd8b503 100644 --- a/util/file/file_read_test.go +++ b/util/file/file_read_test.go @@ -3,10 +3,11 @@ package file import ( "os" "testing" + "github.com/stretchr/testify/assert" ) -func TestOpenAndReadString__SameString__Success(t *testing.T){ +func TestOpenAndReadString__SameString__Success(t *testing.T) { file, _ := os.Create("test.txt") file.WriteString("123") @@ -18,7 +19,7 @@ func TestOpenAndReadString__SameString__Success(t *testing.T){ os.Remove("test.txt") } -func TestOpenAndReadString__OtherString__Fail(t *testing.T){ +func TestOpenAndReadString__OtherString__Fail(t *testing.T) { file, _ := os.Create("test.txt") file.WriteString("123") @@ -30,13 +31,13 @@ func TestOpenAndReadString__OtherString__Fail(t *testing.T){ os.Remove("test.txt") } -func TestOpenAndReadString__FileNotExist__Fail(t *testing.T){ - assert.Panics(t,func(){ +func TestOpenAndReadString__FileNotExist__Fail(t *testing.T) { + assert.Panics(t, func() { OpenAndReadString("test.txt") }) } -func TestOpenAndRead__SameByteArray__Success(t *testing.T){ +func TestOpenAndRead__SameByteArray__Success(t *testing.T) { file, _ := os.Create("test.bin") file.Write([]byte("123")) @@ -48,7 +49,7 @@ func TestOpenAndRead__SameByteArray__Success(t *testing.T){ os.Remove("test.bin") } -func TestOpenAndRead__DifferentByteArray__Success(t *testing.T){ +func TestOpenAndRead__DifferentByteArray__Success(t *testing.T) { file, _ := os.Create("test.bin") file.Write([]byte("123")) @@ -60,8 +61,8 @@ func TestOpenAndRead__DifferentByteArray__Success(t *testing.T){ os.Remove("test.bin") } -func TestOpenAndRead__FileNotExist__Fail(t *testing.T){ - assert.Panics(t,func(){ +func TestOpenAndRead__FileNotExist__Fail(t *testing.T) { + assert.Panics(t, func() { OpenAndRead("test.bin") }) -} \ No newline at end of file +} diff --git a/util/file/file_write.go b/util/file/file_write.go index 0b165e1..f96d6d0 100644 --- a/util/file/file_write.go +++ b/util/file/file_write.go @@ -1,39 +1,38 @@ package file import ( - "os" "NonRelDB/log" + "os" ) // CreateAndWriteString creates file and writes string to it. -func CreateAndWriteString(name string, value string){ +func CreateAndWriteString(name string, value string) { file, err := os.Create(name) defer file.Close() - if err != nil{ + if err != nil { log.Error.Panicln(err.Error()) } _, err = file.WriteString(value) - if err != nil{ + if err != nil { log.Error.Panicln(err.Error()) } } // CreateAndWrite creates file and writes byte array to it. -func CreateAndWrite(name string, value []byte){ +func CreateAndWrite(name string, value []byte) { file, err := os.Create(name) defer file.Close() - if err != nil{ + if err != nil { log.Error.Panicln(err.Error()) } _, err = file.Write(value) - if err != nil{ + if err != nil { log.Error.Panicln(err.Error()) } } - diff --git a/util/file/file_write_test.go b/util/file/file_write_test.go index a4cd11c..fa54bfb 100644 --- a/util/file/file_write_test.go +++ b/util/file/file_write_test.go @@ -1,33 +1,32 @@ package file -import( +import ( + "os" "testing" + "github.com/stretchr/testify/assert" - "os" ) -func TestCreateAndWriteString__SameString__Success(t *testing.T){ +func TestCreateAndWriteString__SameString__Success(t *testing.T) { CreateAndWriteString("test.txt", "123") assert.Equal(t, "123", OpenAndReadString("test.txt")) os.Remove("test.txt") } -func TestCreateAndWriteString__OtherString__Fail(t *testing.T){ +func TestCreateAndWriteString__OtherString__Fail(t *testing.T) { CreateAndWriteString("test.txt", "123") assert.NotEqual(t, "124", OpenAndReadString("test.txt")) os.Remove("test.txt") } -func TestCreateAndWrite__SameByteArray__Success(t *testing.T){ +func TestCreateAndWrite__SameByteArray__Success(t *testing.T) { CreateAndWrite("test.bin", []byte("123")) assert.Equal(t, []byte("123"), OpenAndRead("test.bin")) os.Remove("test.bin") } -func TestCreateAndWrite__OtherByteArray__Success(t *testing.T){ +func TestCreateAndWrite__OtherByteArray__Success(t *testing.T) { CreateAndWrite("test.bin", []byte("123")) assert.NotEqual(t, []byte("124"), OpenAndRead("test.bin")) os.Remove("test.bin") } - - diff --git a/util/json/json_decode.go b/util/json/json_decode.go index e45a284..05459da 100644 --- a/util/json/json_decode.go +++ b/util/json/json_decode.go @@ -1,17 +1,17 @@ package json import ( - "encoding/json" "NonRelDB/log" + "encoding/json" ) // UnpackFromJSON receives json bytes and returns map pointer. -func UnpackFromJSON(bytes []byte) *map[string]string{ +func UnpackFromJSON(bytes []byte) *map[string]string { kvMap := make(map[string]string) err := json.Unmarshal(bytes, &kvMap) - if err != nil{ + if err != nil { log.Warning.Println(err.Error()) log.Warning.Println("No bytes. Will be returned zero map") } return &kvMap -} \ No newline at end of file +} diff --git a/util/json/json_decode_test.go b/util/json/json_decode_test.go index 3adbe3c..f4c3ca2 100644 --- a/util/json/json_decode_test.go +++ b/util/json/json_decode_test.go @@ -1,33 +1,34 @@ package json import ( + "encoding/json" "testing" + "github.com/stretchr/testify/assert" - "encoding/json" ) -func TestUnpackMapFromJSON__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestUnpackMapFromJSON__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} mapBytes, _ := json.Marshal(testMap) assert.Equal(t, testMap, *UnpackFromJSON(mapBytes)) } -func TestUnpackMapFromJSON__DiffMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestUnpackMapFromJSON__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} mapBytes, _ := json.Marshal(testMap) testMap["1"] = "1" assert.NotEqual(t, testMap, *UnpackFromJSON(mapBytes)) } -func TestUnpackMapFromJSON__SameMapWithIndent__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestUnpackMapFromJSON__SameMapWithIndent__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} mapBytes, _ := json.MarshalIndent(testMap, "", " ") assert.Equal(t, testMap, *UnpackFromJSON(mapBytes)) } -func TestUnpackMapFromJSON__DiffMapWithIndent__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestUnpackMapFromJSON__DiffMapWithIndent__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} mapBytes, _ := json.MarshalIndent(testMap, "", " ") testMap["1"] = "1" assert.NotEqual(t, testMap, *UnpackFromJSON(mapBytes)) -} \ No newline at end of file +} diff --git a/util/json/json_encode.go b/util/json/json_encode.go index fa6c1cf..979471a 100644 --- a/util/json/json_encode.go +++ b/util/json/json_encode.go @@ -1,44 +1,44 @@ package json import ( - "encoding/json" "NonRelDB/log" + "encoding/json" ) // PackToJSON receives string key and interface value and returns json bytes. -func PackToJSON(key string, value string) []byte{ - kvMap := map[string]string{key : value} +func PackToJSON(key string, value string) []byte { + kvMap := map[string]string{key: value} bytes, err := json.Marshal(kvMap) - if err != nil{ + if err != nil { log.Error.Println(err.Error()) } return bytes } // PackToJSONIndent receives string key and interface value and returns json bytes with indent. -func PackToJSONIndent(key string, value string) []byte{ - kvMap := map[string]string{key : value} - bytes, err := json.MarshalIndent(kvMap,"", " ") - if err != nil{ +func PackToJSONIndent(key string, value string) []byte { + kvMap := map[string]string{key: value} + bytes, err := json.MarshalIndent(kvMap, "", " ") + if err != nil { log.Error.Println(err.Error()) } return bytes } // PackMapToJSON receives map and returns json bytes. -func PackMapToJSON(kvMap map[string]string) []byte{ +func PackMapToJSON(kvMap map[string]string) []byte { bytes, err := json.Marshal(kvMap) - if err != nil{ + if err != nil { log.Error.Println(err.Error()) } return bytes } // PackMapToJSONIndent receives map and returns json bytes with indent. -func PackMapToJSONIndent(kvMap map[string]string) []byte{ +func PackMapToJSONIndent(kvMap map[string]string) []byte { bytes, err := json.MarshalIndent(kvMap, "", " ") - if err != nil{ + if err != nil { log.Error.Println(err.Error()) } return bytes -} \ No newline at end of file +} diff --git a/util/json/json_encode_test.go b/util/json/json_encode_test.go index d56bb3f..ee591de 100644 --- a/util/json/json_encode_test.go +++ b/util/json/json_encode_test.go @@ -1,58 +1,58 @@ package json import ( - "github.com/stretchr/testify/assert" - "testing" "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" ) -func TestPackToJSON__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestPackToJSON__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} expBytes, _ := json.Marshal(testMap) assert.Equal(t, expBytes, PackToJSON("1", testMap["1"])) } -func TestPackToJSON__OtherMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestPackToJSON__OtherMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} expBytes, _ := json.Marshal(testMap) assert.NotEqual(t, expBytes, PackToJSON("1", "two")) } -func TestPackToJSONIndent__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestPackToJSONIndent__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} expBytes, _ := json.MarshalIndent(testMap, "", " ") assert.Equal(t, expBytes, PackToJSONIndent("1", testMap["1"])) } -func TestPackToJSONIndent__OtherMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one"} - expBytes, _ := json.MarshalIndent(testMap,"", " ") +func TestPackToJSONIndent__OtherMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") assert.NotEqual(t, expBytes, PackToJSONIndent("1", "two")) } - -func TestPackMapToJSON__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestPackMapToJSON__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} expBytes, _ := json.Marshal(testMap) assert.Equal(t, expBytes, PackMapToJSON(testMap)) } -func TestPackMapToJSON__DiffMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestPackMapToJSON__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} expBytes, _ := json.Marshal(testMap) testMap["1"] = "1" assert.NotEqual(t, expBytes, PackMapToJSON(testMap)) } -func TestPackMapToJSONIndent__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestPackMapToJSONIndent__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} expBytes, _ := json.MarshalIndent(testMap, "", " ") assert.Equal(t, expBytes, PackMapToJSONIndent(testMap)) } -func TestPackMapToJSONIndent__DiffMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestPackMapToJSONIndent__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} expBytes, _ := json.MarshalIndent(testMap, "", " ") testMap["1"] = "1" assert.NotEqual(t, expBytes, PackMapToJSONIndent(testMap)) -} \ No newline at end of file +} diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index f28a619..91ab383 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -19,11 +19,11 @@ var ( RestoreReg *regexp.Regexp ) -func init(){ +func init() { DoubleQuoteReg = regexp.MustCompile("\"(.*)\"") QueryReg = regexp.MustCompile("^(get|set|del|keys)") TopicReg = regexp.MustCompile("^(subscribe|publish|unsubscribe)") ExitReg = regexp.MustCompile("^exit$") DumpReg = regexp.MustCompile("^dump$") RestoreReg = regexp.MustCompile("^restore$") -} \ No newline at end of file +} diff --git a/util/regex/regex_util_test.go b/util/regex/regex_util_test.go index 6b97b4a..bec2817 100644 --- a/util/regex/regex_util_test.go +++ b/util/regex/regex_util_test.go @@ -81,4 +81,3 @@ func TestRestoreReg__Restore__Success(t *testing.T) { func TestRestoreReg__RestoreWithSpaces__Fail(t *testing.T) { assert.False(t, RestoreReg.MatchString(" restore ")) } - diff --git a/util/sync/sync_map.go b/util/sync/sync_map.go index 7ccedcb..5b2bd5f 100644 --- a/util/sync/sync_map.go +++ b/util/sync/sync_map.go @@ -1,13 +1,13 @@ package sync import ( - "sync" + "NonRelDB/log" "regexp" "strings" - "NonRelDB/log" + "sync" ) -// Map map synchronized with mutex. +// Map map synchronized with mutex. type Map struct { sync.Mutex storage *map[string]string @@ -19,7 +19,7 @@ func (syncMap *Map) GetMap() *map[string]string { } // SetMap setter for map. -func (syncMap *Map) SetMap(storage *map[string]string){ +func (syncMap *Map) SetMap(storage *map[string]string) { syncMap.storage = storage } @@ -28,14 +28,14 @@ func (syncMap *Map) Get(key string) string { syncMap.Lock() defer syncMap.Unlock() v := (*syncMap.storage)[key] - if v != ""{ - return v; + if v != "" { + return v } return "Value with this key not found" } // Set set value according to key. -func (syncMap *Map) Set(key string, value string) string{ +func (syncMap *Map) Set(key string, value string) string { syncMap.Lock() defer syncMap.Unlock() (*syncMap.storage)[key] = value @@ -43,13 +43,13 @@ func (syncMap *Map) Set(key string, value string) string{ } // Del del value according to key. -func (syncMap *Map) Del(key string) string{ +func (syncMap *Map) Del(key string) string { syncMap.Lock() defer syncMap.Unlock() v := (*syncMap.storage)[key] if v != "" { - delete((*syncMap.storage),key) - return v; + delete((*syncMap.storage), key) + return v } return "Value with this key not found" } @@ -68,14 +68,14 @@ func (syncMap *Map) Keys(pattern string) string { return "Pattern is incorrect" } - for key := range (*syncMap.storage) { + for key := range *syncMap.storage { if regex.MatchString(key) { keys = append(keys, key) } } if keys != nil { - return strings.Join(keys,",") + return strings.Join(keys, ",") } return "Keys with this pattern not found" -} \ No newline at end of file +} diff --git a/util/sync/sync_map_test.go b/util/sync/sync_map_test.go index 77ea43f..6c0cd4f 100644 --- a/util/sync/sync_map_test.go +++ b/util/sync/sync_map_test.go @@ -2,78 +2,79 @@ package sync import ( "testing" + "github.com/stretchr/testify/assert" ) -func TestGetMap__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestGetMap__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} syncMap := Map{} syncMap.storage = &testMap assert.Equal(t, &testMap, syncMap.GetMap()) } -func TestGetMap__DiffMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestGetMap__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} syncMap := Map{} syncMap.storage = &testMap - diffMap := map[string]string{"2" : "two"} + diffMap := map[string]string{"2": "two"} assert.NotEqual(t, &diffMap, syncMap.GetMap()) } -func TestSetMap__SameMap__Success(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestSetMap__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} syncMap := Map{} syncMap.SetMap(&testMap) assert.Equal(t, &testMap, syncMap.GetMap()) } -func TestSetMap__DiffMap__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one"} +func TestSetMap__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} syncMap := Map{} syncMap.SetMap(&testMap) - diffMap := map[string]string{"2" : "two"} + diffMap := map[string]string{"2": "two"} assert.NotEqual(t, &diffMap, syncMap.GetMap()) } -func TestGet__Existing__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestGet__Existing__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) assert.Equal(t, "one", syncMap.Get("1")) } -func TestGet__NotExisting__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestGet__NotExisting__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) assert.Equal(t, "Value with this key not found", syncMap.Get("4")) } -func TestSet__Changed__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestSet__Changed__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) - syncMap.Set("1","123") + syncMap.Set("1", "123") assert.Equal(t, "123", syncMap.Get("1")) } -func TestDel__DeletedAndReturned__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestDel__DeletedAndReturned__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) assert.Equal(t, "one", syncMap.Del("1")) } -func TestDel__DeletedAndNotFound__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestDel__DeletedAndNotFound__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) syncMap.Del("1") assert.Equal(t, "Value with this key not found", syncMap.Del("1")) } -func TestKeys__FoundWildcard__Success(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestKeys__FoundWildcard__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) querySet := syncMap.Keys("/*") @@ -81,18 +82,16 @@ func TestKeys__FoundWildcard__Success(t *testing.T){ } -func TestKeys__IncorrectWildcard__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestKeys__IncorrectWildcard__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) assert.Equal(t, "Pattern is incorrect", syncMap.Keys("*")) } -func TestKeys__NotFound__Fail(t *testing.T){ - testMap := map[string]string{"1" : "one", "2" : "two", "3" : "three"} +func TestKeys__NotFound__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} syncMap := Map{} syncMap.SetMap(&testMap) assert.Equal(t, "Keys with this pattern not found", syncMap.Keys("123")) } - - \ No newline at end of file From 2bd2bfc4305b738eda927cea9dba890f58896f99 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 21:23:52 +0300 Subject: [PATCH 082/104] Added goimports & golint to dockerfile and checks(go vet, goimports, golint) --- Dockerfile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Dockerfile b/Dockerfile index 22a35fa..b6cc441 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,19 @@ FROM golang:1.11-alpine3.8 EXPOSE 9090 +# Preparation stage. +RUN apk update && apk upgrade && apk add git + +RUN go get golang.org/x/tools/cmd/goimports + +RUN go get -u golang.org/x/lint/golint + RUN mkdir /NonRelDB/ ADD . /go/src/NonRelDB/ + +# Build stage. WORKDIR /go/src/NonRelDB/server RUN go build *.go @@ -14,5 +23,15 @@ WORKDIR /go/src/NonRelDB/client RUN go build *.go +# Check stage. +WORKDIR /go/src/NonRelDB + +RUN go vet **/*.go + +RUN goimports **/*.go + +RUN golint **/*.go + +# Entrypoint bind. ENTRYPOINT [ "/go/src/NonRelDB/server/server" ] From 0baa3c17487b68698dc3adff10b2087a5996ff2d Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 21:24:26 +0300 Subject: [PATCH 083/104] Deleted VERSION file because it's useless --- VERSION | 1 - 1 file changed, 1 deletion(-) delete mode 100644 VERSION diff --git a/VERSION b/VERSION deleted file mode 100644 index 9f8e9b6..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file From c1081d357afb884e471c6b8bc80281a5ec56320f Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 21:25:00 +0300 Subject: [PATCH 084/104] Imporved & optimized Makefile --- Makefile | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index d0b5e25..8d5541a 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,7 @@ -REPOSITORY_PATH := $(HOME)/Dev/Go/src/NonRelDB +CURRENT_DIR = $(shell pwd) -VERSION := $(shell cat VERSION) - -.PHONY: fmt check: - go vet NonRelDB/... + go vet ./... goimports **/*.go @@ -14,12 +11,12 @@ clean: rm server/server && rm client/client build-server: - go build -o server/server $(REPOSITORY_PATH)/server/server.go + go build -o server/server $(CURRENT_DIR)/server/server.go build-client: - go build -o client/client $(REPOSITORY_PATH)/client/client.go + go build -o client/client $(CURRENT_DIR)/client/client.go -build-container: +build: sudo docker build -t "nonreldb" . run: @@ -28,7 +25,7 @@ run: test: echo "Running unit & integration tests" - go test NonRelDB/... -coverprofile coverage.out + go test ./... -coverprofile coverage.out go tool cover -html=coverage.out From 6bd307d379978a1487165297655e200b061bfa12 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 21:28:49 +0300 Subject: [PATCH 085/104] Fixed golint in check target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8d5541a..30cfbf7 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ check: goimports **/*.go - golint **/*.go + golint ./... clean: rm server/server && rm client/client From a6d5f808ab0ebcac9593ea934a14a1ecc407dca4 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 21:29:10 +0300 Subject: [PATCH 086/104] Added requirements --- Project.pdf | Bin 0 -> 143768 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Project.pdf diff --git a/Project.pdf b/Project.pdf new file mode 100644 index 0000000000000000000000000000000000000000..53f81b51e30e1630527e0b5e1ba73244145b4560 GIT binary patch literal 143768 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

$pqB)i}tR}_Rx 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 literal 0 HcmV?d00001 From 3ee27ff4a4c03cd4f7867eb0d55163d282cd8462 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 21:39:53 +0300 Subject: [PATCH 087/104] Fixed Dockerfile dependencies --- Dockerfile | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index b6cc441..fca26a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,34 +3,37 @@ FROM golang:1.11-alpine3.8 EXPOSE 9090 # Preparation stage. +RUN apk add build-base + RUN apk update && apk upgrade && apk add git RUN go get golang.org/x/tools/cmd/goimports RUN go get -u golang.org/x/lint/golint +RUN go get github.com/stretchr/testify + RUN mkdir /NonRelDB/ ADD . /go/src/NonRelDB/ +# Check stage. +WORKDIR /go/src/NonRelDB -# Build stage. -WORKDIR /go/src/NonRelDB/server - -RUN go build *.go +RUN go vet ./... -WORKDIR /go/src/NonRelDB/client +RUN goimports **/*.go -RUN go build *.go +RUN golint ./... -# Check stage. -WORKDIR /go/src/NonRelDB +# Build stage. +WORKDIR /go/src/NonRelDB/server -RUN go vet **/*.go +RUN go build server.go -RUN goimports **/*.go +WORKDIR /go/src/NonRelDB/client -RUN golint **/*.go +RUN go build client.go # Entrypoint bind. ENTRYPOINT [ "/go/src/NonRelDB/server/server" ] From ce2652b2afe064be08ebe3a354cbb48ef5b81644 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 22:06:34 +0300 Subject: [PATCH 088/104] Fixed checks in Makefile & Dockerfile --- Dockerfile | 4 ++-- Makefile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index fca26a7..33756ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,11 +20,11 @@ ADD . /go/src/NonRelDB/ # Check stage. WORKDIR /go/src/NonRelDB -RUN go vet ./... +RUN go vet **/*.go RUN goimports **/*.go -RUN golint ./... +RUN golint **/*.go # Build stage. WORKDIR /go/src/NonRelDB/server diff --git a/Makefile b/Makefile index 30cfbf7..46d79c0 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ CURRENT_DIR = $(shell pwd) check: - go vet ./... + go vet **/*.go goimports **/*.go - golint ./... + golint **/*.go clean: rm server/server && rm client/client From 92efa4b8e7f098f2bba69f656922e41a8f7f4787 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Fri, 14 Dec 2018 22:32:10 +0300 Subject: [PATCH 089/104] Now command must contain letters with same register --- util/regex/regex_util.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index 91ab383..91e82d0 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -21,9 +21,9 @@ var ( func init() { DoubleQuoteReg = regexp.MustCompile("\"(.*)\"") - QueryReg = regexp.MustCompile("^(get|set|del|keys)") - TopicReg = regexp.MustCompile("^(subscribe|publish|unsubscribe)") - ExitReg = regexp.MustCompile("^exit$") - DumpReg = regexp.MustCompile("^dump$") - RestoreReg = regexp.MustCompile("^restore$") + QueryReg = regexp.MustCompile("^(get|GET|set|SET|del|DEL|keys|KEYS)") + TopicReg = regexp.MustCompile("^(subscribe|SUBSCRIBE|publish|PUBLISH|unsubscribe|UNSUBSCRIBE)") + ExitReg = regexp.MustCompile("^(exit|EXIT)$") + DumpReg = regexp.MustCompile("^(dump|DUMP)$") + RestoreReg = regexp.MustCompile("^(restore|DUMP)$") } From 99ad964af223fd4a2c3ee5afd9a72f2669b7e1d3 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sat, 15 Dec 2018 18:54:58 +0300 Subject: [PATCH 090/104] Fixed restore regex --- util/regex/regex_util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go index 91e82d0..575e886 100644 --- a/util/regex/regex_util.go +++ b/util/regex/regex_util.go @@ -25,5 +25,5 @@ func init() { TopicReg = regexp.MustCompile("^(subscribe|SUBSCRIBE|publish|PUBLISH|unsubscribe|UNSUBSCRIBE)") ExitReg = regexp.MustCompile("^(exit|EXIT)$") DumpReg = regexp.MustCompile("^(dump|DUMP)$") - RestoreReg = regexp.MustCompile("^(restore|DUMP)$") + RestoreReg = regexp.MustCompile("^(restore|RESTORE)$") } From 92df5c1eb7445c8d4b4592e3aeb5f975aa653779 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sun, 16 Dec 2018 21:21:46 +0300 Subject: [PATCH 091/104] Removed unused mkdir from dockerfile --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 33756ca..4b4b8ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,6 @@ RUN go get -u golang.org/x/lint/golint RUN go get github.com/stretchr/testify -RUN mkdir /NonRelDB/ - ADD . /go/src/NonRelDB/ # Check stage. From 8e542db7a0c9a964cfa49b4ac9507e406b2154c8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Sun, 16 Dec 2018 21:26:04 +0300 Subject: [PATCH 092/104] Fixed checks in makefile & dockerfile --- Dockerfile | 6 +++--- Makefile | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b4b8ab..72451ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,11 +18,11 @@ ADD . /go/src/NonRelDB/ # Check stage. WORKDIR /go/src/NonRelDB -RUN go vet **/*.go +RUN go vet ./... -RUN goimports **/*.go +RUN goimports ./ -RUN golint **/*.go +RUN golint ./... # Build stage. WORKDIR /go/src/NonRelDB/server diff --git a/Makefile b/Makefile index 46d79c0..2da1a8d 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ CURRENT_DIR = $(shell pwd) check: - go vet **/*.go + go vet ./... - goimports **/*.go + goimports ./ - golint **/*.go + golint ./... clean: rm server/server && rm client/client From 9bc932407b85cacfd7b1a07fa3f6c6044378d8a8 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 17 Dec 2018 01:12:56 +0300 Subject: [PATCH 093/104] Changed ip flag to host flag --- server/server.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index 29bed2f..6b901df 100644 --- a/server/server.go +++ b/server/server.go @@ -11,13 +11,14 @@ import ( "syscall" ) -var ip string +var host string var port string var mode string var location string func init() { - flag.StringVar(&ip, "ip", "127.0.0.1", "Defines host ip") + flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") + flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") flag.StringVar(&port, "port", "9090", "Defines host port") flag.StringVar(&port, "p", "9090", "Defines host port") flag.StringVar(&mode, "mode", "memory", "Defines storage location") @@ -55,7 +56,7 @@ func main() { storageInit() cleanup() - l, err := net.Listen("tcp", ip+":"+port) + l, err := net.Listen("tcp", host+":"+port) if err != nil { log.Error.Panicln(err.Error()) From 2103e5593addce56012cac4d565cdbafe5b4d780 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 17 Dec 2018 01:14:53 +0300 Subject: [PATCH 094/104] Fixed server's location flag description --- server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 6b901df..e932c7c 100644 --- a/server/server.go +++ b/server/server.go @@ -23,8 +23,8 @@ func init() { flag.StringVar(&port, "p", "9090", "Defines host port") flag.StringVar(&mode, "mode", "memory", "Defines storage location") flag.StringVar(&mode, "m", "memory", "Defines storage location") - flag.StringVar(&location, "location", "storage.json", "Defines storage location") - flag.StringVar(&location, "l", "storage.json", "Defines storage location") + flag.StringVar(&location, "location", "storage.json", "Defines storage location on disk") + flag.StringVar(&location, "l", "storage.json", "Defines storage location on disk") flag.Parse() } From b6ca36c27301148d45ee69365cafb2de8315f3f9 Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 01:59:38 +0300 Subject: [PATCH 095/104] Update README.md --- README.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5282e1a..af717a5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# NonRelDB -Redis like db on golang. +## NonRelDB + +NonRelDB is an in-memory database that persists on disk. The data model is key-value. Written on golang. + +## Installation +First of all you need to install [git](https://git-scm.com/) and [docker](https://www.docker.com/) +Then you need to clone repository on your pc from github + + git clone https://github.com/777777miSSU7777777/NonRelDB.git + +In cloned repository you will find **Makefile** with following targets: + + - **build-server** - builds server's executable binary file. + - **build-client** - builds client's executable binary file. + - **build** - builds docker container with server's & client's binaries. Also includes go vet, goimports and golint. Entrypoint is server with default configuration. + - **check** - runs subsequently go vet, goimports and golint on the project. Fails if any error occurs. + - **test** - runs unit & integration tests. Fail if any test don't pass. + - **run** - runs built docker container. +## Usage +### Server's flags + - **-host -h** - defines host ip (default is 127.0.0.1) + - **-port -p** - defines host port (default is 9090) + - **-mode -m** - defines storage location (default is "memory"). Possible options are "memory" and "disk". + - **-location -l** - defines storage location on disk (default is "storage.json"). +### Client's flags + - **-host -h** - defines host ip (default is 127.0.0.1) + - **-port -p** - defines host port (default is 9090) + - **--dump** - requests full database dump in json format on stdout. + Usage example + + ./client --dump > dump.json + + - **--restore** - restores database from stdin. + Usage example + + + ./client --restore < dump.json + +### Commands +Commands can be entered only in one register (**GET** and **get** but not **Get**). + + **List of supported commands** + - **GET** - returns the value if existing, otherwise message "Value with this key not found". + Example + + + GET 123 + + + - **SET** - set the value if existing, otherwise creates new. Also returns message "Value has changed". + **Value must be in double quotes.* + Example + + + SET 123 "123" + + + - **DEL** - deletes value from storage and returns it's value if existing, otherwise message "Value with this key not found". + Example + + + DEL 123 + +- **KEYS** - returns all keys matching to entered regexp pattern, otherwise message "Keys with this pattern not found" or "Pattern is incorrect". +**Regex pattern must be in double quotes.* +Example + + KEYS "/*" + +- **SUBSCRIBE** - subscribes the client on specified channel. +Example + + SUBSCRIBE redis + +- **UNSUBSCRIBE** - unsubscribes the client from specified channel. +*Cannot use from client because after subscribe client turns into listening state. +Example + + USUBSCRIBE redis + +- **PUBLISH** - sends the message to specified channel. +*\*Message must be in double quotes.* +Example + + PUBLISH redis "Hello world" + +## Project requirements + + - There no verbose mode and flag because logger any way logs full user's request. + - Not implemented TAB completion of the commands in cli. From 2fb120b85bf7ea7b66f4cf9149500d3a3871975d Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 02:12:27 +0300 Subject: [PATCH 096/104] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af717a5..611f394 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## NonRelDB -NonRelDB is an in-memory database that persists on disk. The data model is key-value. Written on golang. +NonRelDB is an in-memory database that persists on disk. The data model is key-value. Written on pure golang. ## Installation First of all you need to install [git](https://git-scm.com/) and [docker](https://www.docker.com/) @@ -14,7 +14,7 @@ In cloned repository you will find **Makefile** with following targets: - **build-client** - builds client's executable binary file. - **build** - builds docker container with server's & client's binaries. Also includes go vet, goimports and golint. Entrypoint is server with default configuration. - **check** - runs subsequently go vet, goimports and golint on the project. Fails if any error occurs. - - **test** - runs unit & integration tests. Fail if any test don't pass. + - **test** - runs unit & integration tests. Fails if any test don't pass. - **run** - runs built docker container. ## Usage ### Server's flags From 574745a410e073a3f84e9c54eb97ba0885290e70 Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 03:05:23 +0300 Subject: [PATCH 097/104] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 611f394..fbc0d29 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Example SUBSCRIBE redis - **UNSUBSCRIBE** - unsubscribes the client from specified channel. -*Cannot use from client because after subscribe client turns into listening state. +**Cannot use from client because after subscribe client turns into listening state.* Example USUBSCRIBE redis From d47ca3a492bf403972daf3a80908b45c08ec7dd3 Mon Sep 17 00:00:00 2001 From: Oleg Fedorovich <6x7missu7x7@gmail.com> Date: Mon, 17 Dec 2018 04:43:00 +0300 Subject: [PATCH 098/104] Divided clean into two targets, added docker clean before build & run target --- Makefile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2da1a8d..922a304 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ check: golint ./... -clean: +clean-binaries: rm server/server && rm client/client build-server: @@ -16,11 +16,14 @@ build-server: build-client: go build -o client/client $(CURRENT_DIR)/client/client.go -build: +clean: + sudo docker system prune + +build: clean sudo docker build -t "nonreldb" . -run: - sudo docker run --net=host nonreldb +run: clean + sudo docker run -d --net=host nonreldb test: echo "Running unit & integration tests" @@ -32,4 +35,3 @@ test: - From 1b25ee5455d2eb1f08aab0b1e69384b450ecf269 Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 04:46:58 +0300 Subject: [PATCH 099/104] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fbc0d29..4567426 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,12 @@ In cloned repository you will find **Makefile** with following targets: - **build-server** - builds server's executable binary file. - **build-client** - builds client's executable binary file. + - **clean-binaries** - removes server's & client's binaries on local machine. - **build** - builds docker container with server's & client's binaries. Also includes go vet, goimports and golint. Entrypoint is server with default configuration. + - **clean** - cleans docker's unused containers, networks, volumes and dangling images. - **check** - runs subsequently go vet, goimports and golint on the project. Fails if any error occurs. - **test** - runs unit & integration tests. Fails if any test don't pass. - - **run** - runs built docker container. + - **run** - runs built docker container in detached mode. ## Usage ### Server's flags - **-host -h** - defines host ip (default is 127.0.0.1) From 5ac6a96aa9359ab576b61bf07ca20df1d55f47dd Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 04:49:58 +0300 Subject: [PATCH 100/104] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4567426..8e472c4 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Example SUBSCRIBE redis - **UNSUBSCRIBE** - unsubscribes the client from specified channel. -**Cannot use from client because after subscribe client turns into listening state.* +**Cannot use from client because after subscribe client turns into listening state.* Example USUBSCRIBE redis From 2971d16a0c13269a4d8156dd7a4208acf5daae89 Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 06:24:15 +0300 Subject: [PATCH 101/104] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e472c4..976c8cd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ In cloned repository you will find **Makefile** with following targets: - **build-server** - builds server's executable binary file. - **build-client** - builds client's executable binary file. - **clean-binaries** - removes server's & client's binaries on local machine. - - **build** - builds docker container with server's & client's binaries. Also includes go vet, goimports and golint. Entrypoint is server with default configuration. + - **build** - copies server's & client's & dependencies src, adds go vet, goimports and golint. Runs checks and if no errors were occured, builds server's & client's binaries. Entrypoint is server with default configuration. - **clean** - cleans docker's unused containers, networks, volumes and dangling images. - **check** - runs subsequently go vet, goimports and golint on the project. Fails if any error occurs. - **test** - runs unit & integration tests. Fails if any test don't pass. From 9d4f3ee479bc8553dd017f4e804d10d3f72593bf Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Mon, 17 Dec 2018 19:21:57 +0300 Subject: [PATCH 102/104] Fixed README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 976c8cd..455d4b3 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Example **Cannot use from client because after subscribe client turns into listening state.* Example - USUBSCRIBE redis + UNSUBSCRIBE redis - **PUBLISH** - sends the message to specified channel. *\*Message must be in double quotes.* From 226e07534389ac5bc3b858938921df26d455f6fb Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Wed, 19 Dec 2018 11:52:57 +0300 Subject: [PATCH 103/104] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 455d4b3..ba7f093 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,7 @@ Example - There no verbose mode and flag because logger any way logs full user's request. - Not implemented TAB completion of the commands in cli. + + ## Notice + - Server & client will crash with non-existing flag's values. + - Not implented -help flah. From 4b79df12f59534e9955f3c59b7796002b3e6c4ed Mon Sep 17 00:00:00 2001 From: 777777miSSU7777777 <43317471+777777miSSU7777777@users.noreply.github.com> Date: Wed, 19 Dec 2018 11:53:24 +0300 Subject: [PATCH 104/104] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba7f093..2307fe1 100644 --- a/README.md +++ b/README.md @@ -93,4 +93,4 @@ Example ## Notice - Server & client will crash with non-existing flag's values. - - Not implented -help flah. + - Not implented -help flag.