From 6d6a53ef70ef2113f1e2f73798b6c47433911196 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 6 Feb 2024 20:52:05 +0300 Subject: [PATCH 01/43] Add Get and Post handlers with simple storage --- cmd/shortener/main.go | 97 ++++++++++++++++++++++++++++++++++++++++++- go.mod | 5 +++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 go.mod diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 38dd16d..bcc7f1d 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,3 +1,98 @@ package main -func main() {} +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" +) + +type Storage struct { + storage map[string]string +} + +func (s *Storage) Add(short string, url string) { + s.storage[short] = url +} + +func (s *Storage) Get(short string) (string, bool) { + url, ok := s.storage[short] + return url, ok +} + +func CreateStorage() *Storage { + return &Storage{storage: make(map[string]string)} +} + +var urlStorage = CreateStorage() + +func shortenURL(url string) string { + // Решил использовать хэширование и первые символы результата, как короткую форму URL + hash := md5.Sum([]byte(url)) + hashString := hex.EncodeToString(hash[:]) + shortURL := hashString[:8] + return shortURL +} + +func main() { + mux := http.NewServeMux() + mux.HandleFunc(`/`, defineHandler) + + err := http.ListenAndServe(`:8080`, mux) + + if err != nil { + fmt.Println("Error:", err) + } +} + +func defineHandler(res http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPost && req.URL.Path == "/" { + postHandler(res, req) + } else if req.Method == http.MethodGet && req.URL.Path != "/" { + getHandler(res, req) + } else { + invalidRequestHandler(res, req) + } +} + +func postHandler(res http.ResponseWriter, req *http.Request) { + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(res, "Error reading request body", http.StatusInternalServerError) + return + } + defer req.Body.Close() + + url := string(body) + short := shortenURL(url) + + urlStorage.Add(short, url) + + res.WriteHeader(http.StatusCreated) + res.Header().Set("Content-Type", "text/plain") + res.Write([]byte(short)) +} + +func getHandler(res http.ResponseWriter, req *http.Request) { + id := req.URL.Path[len("/"):] + if id == "" || strings.Contains(id, "/") { + http.Error(res, "Bad Request", http.StatusBadRequest) + return + } + + url, ok := urlStorage.Get(id) + if !ok { + http.NotFound(res, req) + return + } + + res.Header().Set("Location", url) + res.WriteHeader(http.StatusTemporaryRedirect) + res.Write(nil) +} + +func invalidRequestHandler(res http.ResponseWriter, req *http.Request) { + http.Error(res, "Bad Request", http.StatusBadRequest) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9d8bb95 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/shilin-anton/urlreducer + +go 1.21.3 + +require github.com/gorilla/mux v1.8.1 // indirect From 99b61eb022b8686b3c96f7e2e672565646bb3218 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Wed, 7 Feb 2024 11:01:17 +0300 Subject: [PATCH 02/43] Update responses --- cmd/shortener/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index bcc7f1d..32b4df6 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -72,7 +72,7 @@ func postHandler(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusCreated) res.Header().Set("Content-Type", "text/plain") - res.Write([]byte(short)) + res.Write([]byte("http://localhost:8080/" + short)) } func getHandler(res http.ResponseWriter, req *http.Request) { @@ -87,10 +87,10 @@ func getHandler(res http.ResponseWriter, req *http.Request) { http.NotFound(res, req) return } - - res.Header().Set("Location", url) res.WriteHeader(http.StatusTemporaryRedirect) - res.Write(nil) + + res.Header().Set("Content-Type", "text/plain") + res.Write([]byte("Location: " + url)) } func invalidRequestHandler(res http.ResponseWriter, req *http.Request) { From dd4b6c82227f5fa3169757e11e3e417e58c054ba Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Wed, 7 Feb 2024 11:45:42 +0300 Subject: [PATCH 03/43] Fix WriteReader order --- cmd/shortener/main.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 32b4df6..773b181 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -70,8 +70,8 @@ func postHandler(res http.ResponseWriter, req *http.Request) { urlStorage.Add(short, url) - res.WriteHeader(http.StatusCreated) res.Header().Set("Content-Type", "text/plain") + res.WriteHeader(http.StatusCreated) res.Write([]byte("http://localhost:8080/" + short)) } @@ -87,10 +87,8 @@ func getHandler(res http.ResponseWriter, req *http.Request) { http.NotFound(res, req) return } + res.Header().Set("Location", url) res.WriteHeader(http.StatusTemporaryRedirect) - - res.Header().Set("Content-Type", "text/plain") - res.Write([]byte("Location: " + url)) } func invalidRequestHandler(res http.ResponseWriter, req *http.Request) { From 0cddb61c39a536f02a2de0a0c763035e6e05ee74 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Fri, 9 Feb 2024 21:46:07 +0300 Subject: [PATCH 04/43] Change project structure and add tests --- .gitignore | 1 + cmd/shortener/main.go | 90 ++------------------------- go.mod | 8 ++- go.sum | 10 +++ internal/app/handlers/handler.go | 90 +++++++++++++++++++++++++++ internal/app/handlers/handler_test.go | 77 +++++++++++++++++++++++ internal/app/server/server.go | 36 +++++++++++ internal/app/storage/storage.go | 17 +++++ 8 files changed, 243 insertions(+), 86 deletions(-) create mode 100644 go.sum create mode 100644 internal/app/handlers/handler.go create mode 100644 internal/app/handlers/handler_test.go create mode 100644 internal/app/server/server.go create mode 100644 internal/app/storage/storage.go diff --git a/.gitignore b/.gitignore index 3362f51..5f09eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ vendor/ # IDEs directories .idea .vscode +cmd/.DS_Store diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 773b181..0fb2106 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,96 +1,16 @@ package main import ( - "crypto/md5" - "encoding/hex" "fmt" - "io" - "net/http" - "strings" + "github.com/shilin-anton/urlreducer/internal/app/server" + "github.com/shilin-anton/urlreducer/internal/app/storage" ) -type Storage struct { - storage map[string]string -} - -func (s *Storage) Add(short string, url string) { - s.storage[short] = url -} - -func (s *Storage) Get(short string) (string, bool) { - url, ok := s.storage[short] - return url, ok -} - -func CreateStorage() *Storage { - return &Storage{storage: make(map[string]string)} -} - -var urlStorage = CreateStorage() - -func shortenURL(url string) string { - // Решил использовать хэширование и первые символы результата, как короткую форму URL - hash := md5.Sum([]byte(url)) - hashString := hex.EncodeToString(hash[:]) - shortURL := hashString[:8] - return shortURL -} - func main() { - mux := http.NewServeMux() - mux.HandleFunc(`/`, defineHandler) - - err := http.ListenAndServe(`:8080`, mux) - + myStorage := storage.New() + myServer := server.New(myStorage) + err := myServer.Start() if err != nil { fmt.Println("Error:", err) } } - -func defineHandler(res http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodPost && req.URL.Path == "/" { - postHandler(res, req) - } else if req.Method == http.MethodGet && req.URL.Path != "/" { - getHandler(res, req) - } else { - invalidRequestHandler(res, req) - } -} - -func postHandler(res http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(res, "Error reading request body", http.StatusInternalServerError) - return - } - defer req.Body.Close() - - url := string(body) - short := shortenURL(url) - - urlStorage.Add(short, url) - - res.Header().Set("Content-Type", "text/plain") - res.WriteHeader(http.StatusCreated) - res.Write([]byte("http://localhost:8080/" + short)) -} - -func getHandler(res http.ResponseWriter, req *http.Request) { - id := req.URL.Path[len("/"):] - if id == "" || strings.Contains(id, "/") { - http.Error(res, "Bad Request", http.StatusBadRequest) - return - } - - url, ok := urlStorage.Get(id) - if !ok { - http.NotFound(res, req) - return - } - res.Header().Set("Location", url) - res.WriteHeader(http.StatusTemporaryRedirect) -} - -func invalidRequestHandler(res http.ResponseWriter, req *http.Request) { - http.Error(res, "Bad Request", http.StatusBadRequest) -} diff --git a/go.mod b/go.mod index 9d8bb95..21d815b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,10 @@ module github.com/shilin-anton/urlreducer go 1.21.3 -require github.com/gorilla/mux v1.8.1 // indirect +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go new file mode 100644 index 0000000..df68c0d --- /dev/null +++ b/internal/app/handlers/handler.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "crypto/md5" + "encoding/hex" + "github.com/shilin-anton/urlreducer/internal/app/storage" + "io" + "net/http" + "strings" +) + +type Storage interface { + Add(short string, url string) + Get(short string) (string, bool) +} + +type Server struct { + data Storage + handler http.Handler +} + +func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.handler.ServeHTTP(w, r) +} + +func New() *Server { + mux := http.NewServeMux() + S := &Server{ + data: make(storage.Storage), + handler: mux, + } + mux.HandleFunc("/", S.DefineHandler) + return S +} + +func shortenURL(url string) string { + // Решил использовать хэширование и первые символы результата, как короткую форму URL + hash := md5.Sum([]byte(url)) + hashString := hex.EncodeToString(hash[:]) + shortURL := hashString[:8] + return shortURL +} + +func (s Server) DefineHandler(res http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPost && req.URL.Path == "/" { + s.PostHandler(res, req) + } else if req.Method == http.MethodGet && req.URL.Path != "/" { + s.GetHandler(res, req) + } else { + s.InvalidRequestHandler(res, req) + } +} + +func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(res, "Error reading request body", http.StatusInternalServerError) + return + } + defer req.Body.Close() + + url := string(body) + short := shortenURL(url) + + s.data.Add(short, url) + + res.Header().Set("Content-Type", "text/plain") + res.WriteHeader(http.StatusCreated) + res.Write([]byte("http://localhost:8080/" + short)) +} + +func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { + id := req.URL.Path[len("/"):] + if id == "" || strings.Contains(id, "/") { + http.Error(res, "Bad Request", http.StatusBadRequest) + return + } + + url, ok := s.data.Get(id) + if !ok { + http.NotFound(res, req) + return + } + res.Header().Set("Location", url) + res.WriteHeader(http.StatusTemporaryRedirect) +} + +func (s Server) InvalidRequestHandler(res http.ResponseWriter, req *http.Request) { + http.Error(res, "Bad Request", http.StatusBadRequest) +} diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go new file mode 100644 index 0000000..4276925 --- /dev/null +++ b/internal/app/handlers/handler_test.go @@ -0,0 +1,77 @@ +package handlers + +import ( + "github.com/shilin-anton/urlreducer/internal/app/storage" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestDefineHandler(t *testing.T) { + store := storage.New() + store.Add("test_short", "https://smth.ru") + + srv := &Server{ + data: store, + handler: nil, + } + + tests := []struct { + name string + method string + url string + requestBody string + wantStatusCode int + wantLocationHeader string + }{ + { + name: "Valid POST request", + method: http.MethodPost, + url: "/", + requestBody: "http://example.com", + wantStatusCode: http.StatusCreated, + }, + { + name: "Valid GET request with existing short link", + method: http.MethodGet, + url: "/test_short", + wantStatusCode: http.StatusTemporaryRedirect, + wantLocationHeader: "https://smth.ru", + }, + { + name: "Invalid GET request with non-existing short link", + method: http.MethodGet, + url: "/non_existing_short_link", + wantStatusCode: http.StatusNotFound, + }, + { + name: "Invalid request", + method: http.MethodDelete, + url: "/", + wantStatusCode: http.StatusBadRequest, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(test.method, test.url, strings.NewReader(test.requestBody)) + w := httptest.NewRecorder() + h := http.HandlerFunc(srv.DefineHandler) + h(w, req) + + resp := w.Result() + + if resp.StatusCode != test.wantStatusCode { + t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode) + } + + if test.wantLocationHeader != "" { + location := resp.Header.Get("Location") + if location != test.wantLocationHeader { + t.Errorf("unexpected Location header: got %s, want %s", location, test.wantLocationHeader) + } + } + }) + } +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go new file mode 100644 index 0000000..b20ae72 --- /dev/null +++ b/internal/app/server/server.go @@ -0,0 +1,36 @@ +package server + +import ( + "net/http" + + "github.com/shilin-anton/urlreducer/internal/app/handlers" +) + +type Storage interface { + Add(short string, url string) + Get(short string) (string, bool) +} + +type server struct { + host string + handler http.Handler + storage Storage +} + +func New(storage Storage) *server { + handler := handlers.New() + S := &server{ + host: "localhost:8080", + handler: handler, + storage: storage, + } + return S +} + +func (s server) Start() error { + err := http.ListenAndServe(s.host, s.handler) + if err != nil { + return err + } + return nil +} diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go new file mode 100644 index 0000000..7490c1f --- /dev/null +++ b/internal/app/storage/storage.go @@ -0,0 +1,17 @@ +package storage + +type Storage map[string]string + +func (s Storage) Add(short string, url string) { + s[short] = url +} + +func (s Storage) Get(short string) (string, bool) { + url, ok := s[short] + return url, ok +} + +func New() Storage { + storage := make(map[string]string) + return storage +} From 7b0b190aeec2d0c3623194a07d30c4e7e25c83c5 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Fri, 9 Feb 2024 21:49:28 +0300 Subject: [PATCH 05/43] Close body in a test --- internal/app/handlers/handler_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 4276925..3d9d4ee 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -2,6 +2,7 @@ package handlers import ( "github.com/shilin-anton/urlreducer/internal/app/storage" + "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "strings" @@ -62,6 +63,9 @@ func TestDefineHandler(t *testing.T) { resp := w.Result() + err := req.Body.Close() + require.NoError(t, err) + if resp.StatusCode != test.wantStatusCode { t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode) } From 491b1e7df92698b2d256d4e19879dac5a35c9ced Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Fri, 9 Feb 2024 21:51:32 +0300 Subject: [PATCH 06/43] Move body.close before Result --- internal/app/handlers/handler_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 3d9d4ee..7cd128b 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -61,10 +61,10 @@ func TestDefineHandler(t *testing.T) { h := http.HandlerFunc(srv.DefineHandler) h(w, req) - resp := w.Result() - err := req.Body.Close() require.NoError(t, err) + + resp := w.Result() if resp.StatusCode != test.wantStatusCode { t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode) From c1464656ea316a0e50d7db19debf03c97b5a1272 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Fri, 9 Feb 2024 21:55:14 +0300 Subject: [PATCH 07/43] Add defer on body closing --- internal/app/handlers/handler_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 7cd128b..e355a0b 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -2,7 +2,6 @@ package handlers import ( "github.com/shilin-anton/urlreducer/internal/app/storage" - "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "strings" @@ -61,10 +60,8 @@ func TestDefineHandler(t *testing.T) { h := http.HandlerFunc(srv.DefineHandler) h(w, req) - err := req.Body.Close() - require.NoError(t, err) - resp := w.Result() + defer resp.Body.Close() if resp.StatusCode != test.wantStatusCode { t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode) From e4bd0041bbd8d5a7d5eb228fd0527159fea5f5b6 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sat, 10 Feb 2024 20:43:53 +0300 Subject: [PATCH 08/43] Add chi usage, Change tests --- cmd/shortener/main.go | 6 +-- go.mod | 8 +--- go.sum | 12 +---- internal/app/handlers/handler.go | 33 ++++---------- internal/app/handlers/handler_test.go | 64 +++++++++++++++++---------- internal/app/server/server.go | 14 +++--- 6 files changed, 59 insertions(+), 78 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 0fb2106..13fbf6e 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "github.com/shilin-anton/urlreducer/internal/app/server" "github.com/shilin-anton/urlreducer/internal/app/storage" ) @@ -9,8 +8,5 @@ import ( func main() { myStorage := storage.New() myServer := server.New(myStorage) - err := myServer.Start() - if err != nil { - fmt.Println("Error:", err) - } + myServer.Start() } diff --git a/go.mod b/go.mod index 21d815b..5347f96 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,4 @@ module github.com/shilin-anton/urlreducer go 1.21.3 -require github.com/stretchr/testify v1.8.4 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +require github.com/go-chi/chi/v5 v5.0.11 diff --git a/go.sum b/go.sum index fa4b6e6..fd04e27 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,2 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= +github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index df68c0d..1628873 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -3,10 +3,10 @@ package handlers import ( "crypto/md5" "encoding/hex" + "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/storage" "io" "net/http" - "strings" ) type Storage interface { @@ -24,12 +24,15 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func New() *Server { - mux := http.NewServeMux() + r := chi.NewRouter() + S := &Server{ data: make(storage.Storage), - handler: mux, + handler: r, } - mux.HandleFunc("/", S.DefineHandler) + r.Get("/{short}", S.GetHandler) + r.Post("/", S.PostHandler) + return S } @@ -41,16 +44,6 @@ func shortenURL(url string) string { return shortURL } -func (s Server) DefineHandler(res http.ResponseWriter, req *http.Request) { - if req.Method == http.MethodPost && req.URL.Path == "/" { - s.PostHandler(res, req) - } else if req.Method == http.MethodGet && req.URL.Path != "/" { - s.GetHandler(res, req) - } else { - s.InvalidRequestHandler(res, req) - } -} - func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { body, err := io.ReadAll(req.Body) if err != nil { @@ -70,13 +63,9 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { } func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { - id := req.URL.Path[len("/"):] - if id == "" || strings.Contains(id, "/") { - http.Error(res, "Bad Request", http.StatusBadRequest) - return - } + short := chi.URLParam(req, "short") - url, ok := s.data.Get(id) + url, ok := s.data.Get(short) if !ok { http.NotFound(res, req) return @@ -84,7 +73,3 @@ func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Location", url) res.WriteHeader(http.StatusTemporaryRedirect) } - -func (s Server) InvalidRequestHandler(res http.ResponseWriter, req *http.Request) { - http.Error(res, "Bad Request", http.StatusBadRequest) -} diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index e355a0b..986b1c1 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -8,22 +8,16 @@ import ( "testing" ) -func TestDefineHandler(t *testing.T) { - store := storage.New() - store.Add("test_short", "https://smth.ru") - - srv := &Server{ - data: store, - handler: nil, - } +func TestPostHandler(t *testing.T) { + storage.New() + srv := New() tests := []struct { - name string - method string - url string - requestBody string - wantStatusCode int - wantLocationHeader string + name string + method string + url string + requestBody string + wantStatusCode int }{ { name: "Valid POST request", @@ -32,6 +26,36 @@ func TestDefineHandler(t *testing.T) { requestBody: "http://example.com", wantStatusCode: http.StatusCreated, }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(test.method, test.url, strings.NewReader(test.requestBody)) + w := httptest.NewRecorder() + + srv.handler.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != test.wantStatusCode { + t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode) + } + }) + } +} + +func TestGetHandler(t *testing.T) { + srv := New() + srv.data.Add("test_short", "https://smth.ru") + + tests := []struct { + name string + method string + url string + wantStatusCode int + wantLocationHeader string + }{ { name: "Valid GET request with existing short link", method: http.MethodGet, @@ -45,20 +69,14 @@ func TestDefineHandler(t *testing.T) { url: "/non_existing_short_link", wantStatusCode: http.StatusNotFound, }, - { - name: "Invalid request", - method: http.MethodDelete, - url: "/", - wantStatusCode: http.StatusBadRequest, - }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - req := httptest.NewRequest(test.method, test.url, strings.NewReader(test.requestBody)) + req := httptest.NewRequest(test.method, test.url, nil) w := httptest.NewRecorder() - h := http.HandlerFunc(srv.DefineHandler) - h(w, req) + + srv.handler.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() diff --git a/internal/app/server/server.go b/internal/app/server/server.go index b20ae72..e849e5b 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -1,9 +1,9 @@ package server import ( - "net/http" - "github.com/shilin-anton/urlreducer/internal/app/handlers" + "log" + "net/http" ) type Storage interface { @@ -20,17 +20,13 @@ type server struct { func New(storage Storage) *server { handler := handlers.New() S := &server{ - host: "localhost:8080", + host: ":8080", handler: handler, storage: storage, } return S } -func (s server) Start() error { - err := http.ListenAndServe(s.host, s.handler) - if err != nil { - return err - } - return nil +func (s server) Start() { + log.Fatal(http.ListenAndServe(":8080", s.handler)) } From 86ff93323348c1dde3eec47b2c7fb63d34fa6029 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sat, 10 Feb 2024 20:46:46 +0300 Subject: [PATCH 09/43] Remove unnecessary storage creation --- internal/app/handlers/handler_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 986b1c1..7a18f64 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -1,7 +1,6 @@ package handlers import ( - "github.com/shilin-anton/urlreducer/internal/app/storage" "net/http" "net/http/httptest" "strings" @@ -9,7 +8,6 @@ import ( ) func TestPostHandler(t *testing.T) { - storage.New() srv := New() tests := []struct { From d6a19a0eb81153f39cdd6eeced475332ff915bc8 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sun, 11 Feb 2024 15:23:56 +0300 Subject: [PATCH 10/43] Add config to handle flags --- cmd/shortener/main.go | 3 +++ internal/app/config/config.go | 14 ++++++++++++++ internal/app/handlers/handler.go | 3 ++- internal/app/server/server.go | 5 ++--- 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 internal/app/config/config.go diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 13fbf6e..c045e89 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,11 +1,14 @@ package main import ( + "github.com/shilin-anton/urlreducer/internal/app/config" "github.com/shilin-anton/urlreducer/internal/app/server" "github.com/shilin-anton/urlreducer/internal/app/storage" ) func main() { + config.ParseFlags() + myStorage := storage.New() myServer := server.New(myStorage) myServer.Start() diff --git a/internal/app/config/config.go b/internal/app/config/config.go new file mode 100644 index 0000000..5eb9096 --- /dev/null +++ b/internal/app/config/config.go @@ -0,0 +1,14 @@ +package config + +import ( + "flag" +) + +var RunAddr string +var BaseAddr string + +func ParseFlags() { + flag.StringVar(&RunAddr, "a", "localhost:8080", "address and port to run server") + flag.StringVar(&BaseAddr, "b", "http://localhost:8080/", "base URL before short link") + flag.Parse() +} diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 1628873..1180058 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "github.com/go-chi/chi/v5" + "github.com/shilin-anton/urlreducer/internal/app/config" "github.com/shilin-anton/urlreducer/internal/app/storage" "io" "net/http" @@ -59,7 +60,7 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "text/plain") res.WriteHeader(http.StatusCreated) - res.Write([]byte("http://localhost:8080/" + short)) + res.Write([]byte(config.BaseAddr + short)) } func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { diff --git a/internal/app/server/server.go b/internal/app/server/server.go index e849e5b..bd476cb 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -1,6 +1,7 @@ package server import ( + "github.com/shilin-anton/urlreducer/internal/app/config" "github.com/shilin-anton/urlreducer/internal/app/handlers" "log" "net/http" @@ -12,7 +13,6 @@ type Storage interface { } type server struct { - host string handler http.Handler storage Storage } @@ -20,7 +20,6 @@ type server struct { func New(storage Storage) *server { handler := handlers.New() S := &server{ - host: ":8080", handler: handler, storage: storage, } @@ -28,5 +27,5 @@ func New(storage Storage) *server { } func (s server) Start() { - log.Fatal(http.ListenAndServe(":8080", s.handler)) + log.Fatal(http.ListenAndServe(config.RunAddr, s.handler)) } From 8fcb776fdb1c2ffd995de5a704bcbef6e140a32b Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sun, 11 Feb 2024 15:29:09 +0300 Subject: [PATCH 11/43] FIx run address --- internal/app/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 5eb9096..ee124d9 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -8,7 +8,7 @@ var RunAddr string var BaseAddr string func ParseFlags() { - flag.StringVar(&RunAddr, "a", "localhost:8080", "address and port to run server") + flag.StringVar(&RunAddr, "a", "localhost:8080/", "address and port to run server") flag.StringVar(&BaseAddr, "b", "http://localhost:8080/", "base URL before short link") flag.Parse() } From e595f4c05464c5d37bf529f8b1197b69b2327ce8 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sun, 11 Feb 2024 15:30:45 +0300 Subject: [PATCH 12/43] Fix host --- internal/app/config/config.go | 2 +- internal/app/handlers/handler.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index ee124d9..5eb9096 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -8,7 +8,7 @@ var RunAddr string var BaseAddr string func ParseFlags() { - flag.StringVar(&RunAddr, "a", "localhost:8080/", "address and port to run server") + flag.StringVar(&RunAddr, "a", "localhost:8080", "address and port to run server") flag.StringVar(&BaseAddr, "b", "http://localhost:8080/", "base URL before short link") flag.Parse() } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 1180058..78714c2 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -60,7 +60,7 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "text/plain") res.WriteHeader(http.StatusCreated) - res.Write([]byte(config.BaseAddr + short)) + res.Write([]byte(config.BaseAddr + "/" + short)) } func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { From f4296deb71001966ea6e09ce4199694e5f715635 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sun, 11 Feb 2024 15:35:30 +0300 Subject: [PATCH 13/43] Add prefix on save short link --- internal/app/handlers/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 78714c2..92367b7 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -42,7 +42,7 @@ func shortenURL(url string) string { hash := md5.Sum([]byte(url)) hashString := hex.EncodeToString(hash[:]) shortURL := hashString[:8] - return shortURL + return "/" + shortURL } func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { @@ -60,7 +60,7 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "text/plain") res.WriteHeader(http.StatusCreated) - res.Write([]byte(config.BaseAddr + "/" + short)) + res.Write([]byte(config.BaseAddr + short)) } func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { From 06c9355c8780743be3912a9adc39e9d932c7dd45 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Sun, 11 Feb 2024 15:45:30 +0300 Subject: [PATCH 14/43] FIx --- internal/app/config/config.go | 2 +- internal/app/handlers/handler.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 5eb9096..792a62d 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -9,6 +9,6 @@ var BaseAddr string func ParseFlags() { flag.StringVar(&RunAddr, "a", "localhost:8080", "address and port to run server") - flag.StringVar(&BaseAddr, "b", "http://localhost:8080/", "base URL before short link") + flag.StringVar(&BaseAddr, "b", "http://localhost:8080", "base URL before short link") flag.Parse() } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 92367b7..78714c2 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -42,7 +42,7 @@ func shortenURL(url string) string { hash := md5.Sum([]byte(url)) hashString := hex.EncodeToString(hash[:]) shortURL := hashString[:8] - return "/" + shortURL + return shortURL } func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { @@ -60,7 +60,7 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "text/plain") res.WriteHeader(http.StatusCreated) - res.Write([]byte(config.BaseAddr + short)) + res.Write([]byte(config.BaseAddr + "/" + short)) } func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { From 53def49bbbaae822191698cd971320bd0cdfb7db Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 12 Feb 2024 14:27:35 +0300 Subject: [PATCH 15/43] Add ENV params --- cmd/shortener/main.go | 4 ++-- internal/app/config/config.go | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index c045e89..199488c 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -7,8 +7,8 @@ import ( ) func main() { - config.ParseFlags() - + config.ParseConfig() + myStorage := storage.New() myServer := server.New(myStorage) myServer.Start() diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 792a62d..953cf07 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -2,13 +2,26 @@ package config import ( "flag" + "fmt" + "os" ) var RunAddr string var BaseAddr string -func ParseFlags() { - flag.StringVar(&RunAddr, "a", "localhost:8080", "address and port to run server") - flag.StringVar(&BaseAddr, "b", "http://localhost:8080", "base URL before short link") +const defaultRunURL = "localhost:8080" +const defaultBaseURL = "http://localhost:8080" + +func ParseConfig() { + flag.StringVar(&RunAddr, "a", defaultRunURL, "address and port to run server") + flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") flag.Parse() + + if envRunAddr := os.Getenv("SERVER_ADDRESS"); envRunAddr != "" { + RunAddr = envRunAddr + } + if envBaseAddr := os.Getenv("BASE_URL"); envBaseAddr != "" { + BaseAddr = envBaseAddr + } + fmt.Printf("Server is running on %s\nBase URL is %s\n", RunAddr, BaseAddr) } From 809a58398f00b9b7c4e810b41066ea67c5ce0b12 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 19 Mar 2024 14:33:51 +0300 Subject: [PATCH 16/43] Fixes after review --- internal/app/config/config.go | 2 -- internal/app/handlers/handler.go | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 953cf07..11088c8 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -2,7 +2,6 @@ package config import ( "flag" - "fmt" "os" ) @@ -23,5 +22,4 @@ func ParseConfig() { if envBaseAddr := os.Getenv("BASE_URL"); envBaseAddr != "" { BaseAddr = envBaseAddr } - fmt.Printf("Server is running on %s\nBase URL is %s\n", RunAddr, BaseAddr) } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 78714c2..5461fcf 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -27,14 +27,14 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func New() *Server { r := chi.NewRouter() - S := &Server{ + s := &Server{ data: make(storage.Storage), handler: r, } - r.Get("/{short}", S.GetHandler) - r.Post("/", S.PostHandler) + r.Get("/{short}", s.GetHandler) + r.Post("/", s.PostHandler) - return S + return s } func shortenURL(url string) string { From b3b87b425e848ac1a4060d014e0db6a1c0876626 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Wed, 20 Mar 2024 18:17:13 +0300 Subject: [PATCH 17/43] Add logger --- cmd/shortener/main.go | 6 ++++ go.mod | 7 +++- go.sum | 14 ++++++++ internal/app/config/config.go | 6 ++++ internal/app/handlers/handler.go | 44 +++++++++++++++++++++++ internal/logger/logger.go | 60 ++++++++++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 internal/logger/logger.go diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 199488c..db6ce47 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -4,10 +4,16 @@ import ( "github.com/shilin-anton/urlreducer/internal/app/config" "github.com/shilin-anton/urlreducer/internal/app/server" "github.com/shilin-anton/urlreducer/internal/app/storage" + "github.com/shilin-anton/urlreducer/internal/logger" + "log" ) func main() { config.ParseConfig() + err := logger.Initialize(config.LogLevel) + if err != nil { + log.Fatal("Error initializing logger: %v\n", err) + } myStorage := storage.New() myServer := server.New(myStorage) diff --git a/go.mod b/go.mod index 5347f96..12b6330 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/shilin-anton/urlreducer go 1.21.3 -require github.com/go-chi/chi/v5 v5.0.11 +require ( + github.com/go-chi/chi/v5 v5.0.11 + go.uber.org/zap v1.27.0 +) + +require go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum index fd04e27..65e5c0d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 11088c8..014f66a 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -7,13 +7,16 @@ import ( var RunAddr string var BaseAddr string +var LogLevel string const defaultRunURL = "localhost:8080" const defaultBaseURL = "http://localhost:8080" +const defaultLogLevel = "info" func ParseConfig() { flag.StringVar(&RunAddr, "a", defaultRunURL, "address and port to run server") flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") + flag.StringVar(&LogLevel, "l", defaultLogLevel, "log level") flag.Parse() if envRunAddr := os.Getenv("SERVER_ADDRESS"); envRunAddr != "" { @@ -22,4 +25,7 @@ func ParseConfig() { if envBaseAddr := os.Getenv("BASE_URL"); envBaseAddr != "" { BaseAddr = envBaseAddr } + if envLogLevel := os.Getenv("LOG_LEVEL"); envLogLevel != "" { + LogLevel = envLogLevel + } } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 5461fcf..4660594 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -6,8 +6,11 @@ import ( "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/config" "github.com/shilin-anton/urlreducer/internal/app/storage" + "github.com/shilin-anton/urlreducer/internal/logger" "io" "net/http" + "strconv" + "time" ) type Storage interface { @@ -20,6 +23,28 @@ type Server struct { handler http.Handler } +// types for logger +type responseData struct { + status int + size int +} + +type loggingResponseWriter struct { + http.ResponseWriter + responseData *responseData +} + +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.responseData.status = code + lrw.ResponseWriter.WriteHeader(code) +} + +func (lrw *loggingResponseWriter) Write(data []byte) (int, error) { + size, err := lrw.ResponseWriter.Write(data) + lrw.responseData.size += size + return size, err +} + func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.ServeHTTP(w, r) } @@ -27,6 +52,9 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func New() *Server { r := chi.NewRouter() + r.Use(requestLoggerMiddleware) + r.Use(responseLoggerMiddleware) + s := &Server{ data: make(storage.Storage), handler: r, @@ -74,3 +102,19 @@ func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Location", url) res.WriteHeader(http.StatusTemporaryRedirect) } + +func requestLoggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + logger.RequestLogger(r.RequestURI, r.Method, time.Since(start).String()) + next.ServeHTTP(w, r) + }) +} + +func responseLoggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + lrw := &loggingResponseWriter{ResponseWriter: w, responseData: &responseData{}} + next.ServeHTTP(lrw, r) + logger.ResponseLogger(strconv.Itoa(lrw.responseData.status), strconv.Itoa(lrw.responseData.size)) + }) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..8774af5 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,60 @@ +package logger + +import ( + "go.uber.org/zap" + "net/http" +) + +// Log синглтон. +var Log *zap.Logger = zap.NewNop() + +type ( + // Структура для хранения сведений об ответе + responseData struct { + status int + size int + } + + // добавляем реализацию http.ResponseWriter + loggingResponseWriter struct { + http.ResponseWriter // встраиваем оригинальный http.ResponseWriter + responseData *responseData + } +) + +// Initialize инициализирует логер. +func Initialize(level string) error { + // преобразуем текстовый уровень логирования в zap.AtomicLevel + lvl, err := zap.ParseAtomicLevel(level) + if err != nil { + return err + } + // создаём новую конфигурацию логера + cfg := zap.NewProductionConfig() + // устанавливаем уровень + cfg.Level = lvl + // создаём логер на основе конфигурации + zl, err := cfg.Build() + if err != nil { + return err + } + // устанавливаем синглтон + Log = zl + return nil +} + +func RequestLogger(uri string, method string, duration string) { + Log.Info("got incoming HTTP request", + zap.String("URI", uri), + zap.String("method", method), + zap.String("duration", duration), + ) +} + +// ResponseLogger — middleware-логер для HTTP-ответов. +func ResponseLogger(status string, size string) { + Log.Info("HTTP response has been sent", + zap.String("code", status), + zap.String("size", size), + ) +} From 37ec3426220893d11e5ae8df7dd0f1e3f6f2bd4f Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Wed, 20 Mar 2024 18:20:07 +0300 Subject: [PATCH 18/43] Fix linter offences --- cmd/shortener/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index db6ce47..6125eac 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -12,7 +12,7 @@ func main() { config.ParseConfig() err := logger.Initialize(config.LogLevel) if err != nil { - log.Fatal("Error initializing logger: %v\n", err) + log.Fatal("Error initializing logger") } myStorage := storage.New() From 68bc72e57f974dc90e5c2025e2643f9d97d30048 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 25 Mar 2024 17:00:43 +0300 Subject: [PATCH 19/43] Add /shorten json handler and test --- .gitignore | 1 + go.mod | 10 ++++- go.sum | 56 +++++++++++++++++++++++ internal/app/handlers/handler.go | 44 ++++++++++++++++++ internal/app/handlers/handler_test.go | 65 +++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5f09eaa..46831dc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ vendor/ .idea .vscode cmd/.DS_Store +.DS_Store diff --git a/go.mod b/go.mod index 12b6330..eded9b0 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,15 @@ go 1.21.3 require ( github.com/go-chi/chi/v5 v5.0.11 + github.com/stretchr/testify v1.8.1 go.uber.org/zap v1.27.0 ) -require go.uber.org/multierr v1.10.0 // indirect +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-resty/resty/v2 v2.12.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/net v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 65e5c0d..1c5b0f4 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,72 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 4660594..55a2175 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -1,8 +1,10 @@ package handlers import ( + "bytes" "crypto/md5" "encoding/hex" + "encoding/json" "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/config" "github.com/shilin-anton/urlreducer/internal/app/storage" @@ -29,6 +31,14 @@ type responseData struct { size int } +type shortenRequest struct { + Url string `json:"url"` +} + +type shortenResponse struct { + Result string `json:"result"` +} + type loggingResponseWriter struct { http.ResponseWriter responseData *responseData @@ -61,6 +71,7 @@ func New() *Server { } r.Get("/{short}", s.GetHandler) r.Post("/", s.PostHandler) + r.Post("/shorten", s.PostShortenHandler) return s } @@ -103,6 +114,39 @@ func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTemporaryRedirect) } +func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { + var request shortenRequest + var buf bytes.Buffer + _, err := buf.ReadFrom(req.Body) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + if err = json.Unmarshal(buf.Bytes(), &request); err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } + if request.Url == "" { + http.Error(res, "url must be passed", http.StatusUnprocessableEntity) + return + } + + short := shortenURL(request.Url) + s.data.Add(short, request.Url) + + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusCreated) + response := shortenResponse{ + Result: config.BaseAddr + "/" + short, + } + + enc := json.NewEncoder(res) + if err = enc.Encode(response); err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } +} + func requestLoggerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 7a18f64..8d7cc6f 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -1,6 +1,9 @@ package handlers import ( + "github.com/shilin-anton/urlreducer/internal/app/config" + "github.com/stretchr/testify/assert" + "io" "net/http" "net/http/httptest" "strings" @@ -92,3 +95,65 @@ func TestGetHandler(t *testing.T) { }) } } + +func TestServer_PostShortenHandler(t *testing.T) { + srv := New() + config.BaseAddr = "http://localhost:8080" + + testCases := []struct { + name string + method string + body string + expectedCode int + expectedBody string + }{ + { + name: "method_post_without_body", + method: http.MethodPost, + expectedCode: http.StatusBadRequest, + expectedBody: "", + }, + { + name: "method_post_unsupported_type", + method: http.MethodPost, + body: `{"url": ""}`, + expectedCode: http.StatusUnprocessableEntity, + expectedBody: "", + }, + { + name: "method_post_success", + method: http.MethodPost, + body: `{"url": "https://yandex.ru"}`, + expectedCode: http.StatusCreated, + expectedBody: `{"result": "http://localhost:8080/e9db20b2"}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(tc.method, "/shorten", strings.NewReader(tc.body)) + w := httptest.NewRecorder() + + if len(tc.body) > 0 { + req.Header.Set("Content-Type", "application/json") + } + + srv.handler.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != tc.expectedCode { + t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, tc.expectedCode) + } + if tc.expectedBody != "" { + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read response body: %v", err) + } + bodyString := string(body) + assert.JSONEq(t, tc.expectedBody, bodyString) + } + }) + } +} From 0b79cd29910e6769bad5a0ce017ffe223ff9918e Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 25 Mar 2024 17:03:12 +0300 Subject: [PATCH 20/43] Fix struct key --- internal/app/handlers/handler.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 55a2175..6aa4c69 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -32,7 +32,7 @@ type responseData struct { } type shortenRequest struct { - Url string `json:"url"` + URL string `json:"url"` } type shortenResponse struct { @@ -126,13 +126,13 @@ func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { http.Error(res, err.Error(), http.StatusBadRequest) return } - if request.Url == "" { + if request.URL == "" { http.Error(res, "url must be passed", http.StatusUnprocessableEntity) return } - short := shortenURL(request.Url) - s.data.Add(short, request.Url) + short := shortenURL(request.URL) + s.data.Add(short, request.URL) res.Header().Set("Content-Type", "application/json") res.WriteHeader(http.StatusCreated) From e4d5a712d194baa156757e791eb3ad9551fb3c19 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 25 Mar 2024 17:06:48 +0300 Subject: [PATCH 21/43] Fix route --- internal/app/handlers/handler.go | 2 +- internal/app/handlers/handler_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 6aa4c69..c0cf748 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -71,7 +71,7 @@ func New() *Server { } r.Get("/{short}", s.GetHandler) r.Post("/", s.PostHandler) - r.Post("/shorten", s.PostShortenHandler) + r.Post("/api/shorten", s.PostShortenHandler) return s } diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 8d7cc6f..40d4950 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -131,7 +131,7 @@ func TestServer_PostShortenHandler(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - req := httptest.NewRequest(tc.method, "/shorten", strings.NewReader(tc.body)) + req := httptest.NewRequest(tc.method, "/api/shorten", strings.NewReader(tc.body)) w := httptest.NewRecorder() if len(tc.body) > 0 { From 050e81ac50a16691251077350847be87b007905a Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 26 Mar 2024 15:49:13 +0300 Subject: [PATCH 22/43] Add gzip middleware --- internal/app/gzip/gzip.go | 71 ++++++++++++++++++++++++++++++++ internal/app/handlers/handler.go | 33 +++++++++++++-- 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 internal/app/gzip/gzip.go diff --git a/internal/app/gzip/gzip.go b/internal/app/gzip/gzip.go new file mode 100644 index 0000000..2047f7b --- /dev/null +++ b/internal/app/gzip/gzip.go @@ -0,0 +1,71 @@ +package gzip + +import ( + "compress/gzip" + "io" + "net/http" +) + +// compressWriter реализует интерфейс http.ResponseWriter и позволяет прозрачно для сервера +// сжимать передаваемые данные и выставлять правильные HTTP-заголовки +type compressWriter struct { + w http.ResponseWriter + zw *gzip.Writer +} + +func NewCompressWriter(w http.ResponseWriter) *compressWriter { + return &compressWriter{ + w: w, + zw: gzip.NewWriter(w), + } +} + +func (c *compressWriter) Header() http.Header { + return c.w.Header() +} + +func (c *compressWriter) Write(p []byte) (int, error) { + return c.zw.Write(p) +} + +func (c *compressWriter) WriteHeader(statusCode int) { + if statusCode < 300 { + c.w.Header().Set("Content-Encoding", "gzip") + } + c.w.WriteHeader(statusCode) +} + +// Close закрывает gzip.Writer и досылает все данные из буфера. +func (c *compressWriter) Close() error { + return c.zw.Close() +} + +// compressReader реализует интерфейс io.ReadCloser и позволяет прозрачно для сервера +// декомпрессировать получаемые от клиента данные +type compressReader struct { + r io.ReadCloser + zr *gzip.Reader +} + +func NewCompressReader(r io.ReadCloser) (*compressReader, error) { + zr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + + return &compressReader{ + r: r, + zr: zr, + }, nil +} + +func (c compressReader) Read(p []byte) (n int, err error) { + return c.zr.Read(p) +} + +func (c *compressReader) Close() error { + if err := c.r.Close(); err != nil { + return err + } + return c.zr.Close() +} diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index c0cf748..902f1a5 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -7,11 +7,13 @@ import ( "encoding/json" "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/config" + "github.com/shilin-anton/urlreducer/internal/app/gzip" "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/shilin-anton/urlreducer/internal/logger" "io" "net/http" "strconv" + "strings" "time" ) @@ -69,9 +71,9 @@ func New() *Server { data: make(storage.Storage), handler: r, } - r.Get("/{short}", s.GetHandler) - r.Post("/", s.PostHandler) - r.Post("/api/shorten", s.PostShortenHandler) + r.Get("/{short}", gzipMiddleware(s.GetHandler)) + r.Post("/", gzipMiddleware(s.PostHandler)) + r.Post("/api/shorten", gzipMiddleware(s.PostShortenHandler)) return s } @@ -162,3 +164,28 @@ func responseLoggerMiddleware(next http.Handler) http.Handler { logger.ResponseLogger(strconv.Itoa(lrw.responseData.status), strconv.Itoa(lrw.responseData.size)) }) } + +func gzipMiddleware(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ow := w + acceptEncoding := r.Header.Get("Accept-Encoding") + supportsGzip := strings.Contains(acceptEncoding, "gzip") + if supportsGzip { + cw := gzip.NewCompressWriter(w) + ow = cw + defer cw.Close() + } + contentEncoding := r.Header.Get("Content-Encoding") + sendsGzip := strings.Contains(contentEncoding, "gzip") + if sendsGzip { + cr, err := gzip.NewCompressReader(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + r.Body = cr + defer cr.Close() + } + h.ServeHTTP(ow, r) + } +} From 8597962c697a7ac0cd1c135c521988c6cd8fdb45 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Wed, 27 Mar 2024 15:07:49 +0300 Subject: [PATCH 23/43] Add file-manager to control local file storage Small storage fix --- .gitignore | 1 + cmd/shortener/main.go | 2 + go.mod | 2 - go.sum | 47 --------- internal/app/config/config.go | 7 ++ internal/app/file-manager/file-manager.go | 118 ++++++++++++++++++++++ internal/app/handlers/handler.go | 39 +++++-- internal/app/handlers/handler_test.go | 9 +- internal/app/server/server.go | 4 +- internal/app/storage/storage.go | 13 +++ 10 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 internal/app/file-manager/file-manager.go diff --git a/.gitignore b/.gitignore index 46831dc..2d66831 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ vendor/ .vscode cmd/.DS_Store .DS_Store +/tmp diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 6125eac..7df6986 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/shilin-anton/urlreducer/internal/app/config" + file_manager "github.com/shilin-anton/urlreducer/internal/app/file-manager" "github.com/shilin-anton/urlreducer/internal/app/server" "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/shilin-anton/urlreducer/internal/logger" @@ -16,6 +17,7 @@ func main() { } myStorage := storage.New() + file_manager.ReadFromFile(&myStorage) myServer := server.New(myStorage) myServer.Start() } diff --git a/go.mod b/go.mod index eded9b0..376dd62 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-resty/resty/v2 v2.12.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/net v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1c5b0f4..1d6b63a 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= -github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -14,57 +12,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 014f66a..6c3e773 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -8,15 +8,19 @@ import ( var RunAddr string var BaseAddr string var LogLevel string +var FilePath string const defaultRunURL = "localhost:8080" const defaultBaseURL = "http://localhost:8080" const defaultLogLevel = "info" +const defaultFilePath = "/tmp/short-url-db.json" func ParseConfig() { flag.StringVar(&RunAddr, "a", defaultRunURL, "address and port to run server") flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") flag.StringVar(&LogLevel, "l", defaultLogLevel, "log level") + flag.StringVar(&FilePath, "f", defaultFilePath, "file storage path") + flag.Parse() if envRunAddr := os.Getenv("SERVER_ADDRESS"); envRunAddr != "" { @@ -28,4 +32,7 @@ func ParseConfig() { if envLogLevel := os.Getenv("LOG_LEVEL"); envLogLevel != "" { LogLevel = envLogLevel } + if envFilePath := os.Getenv("FILE_STORAGE_PATH"); envFilePath != "" { + FilePath = envFilePath + } } diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go new file mode 100644 index 0000000..9eaf1d2 --- /dev/null +++ b/internal/app/file-manager/file-manager.go @@ -0,0 +1,118 @@ +package file_manager + +import ( + "bufio" + "encoding/json" + "github.com/shilin-anton/urlreducer/internal/app/config" + "github.com/shilin-anton/urlreducer/internal/app/storage" + "log" + "os" + "strconv" +) + +type record struct { + UUID string `json:"uuid"` + ShortUrl string `json:"short_url"` + OriginalUrl string `json:"original_url"` +} + +type FileWriter struct { + file *os.File + scanner *bufio.Scanner + writer *bufio.Writer +} + +type FileReader struct { + file *os.File + scanner *bufio.Scanner +} + +func NewWriter() (*FileWriter, error) { + file, err := os.OpenFile(config.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return nil, err + } + + return &FileWriter{ + file: file, + scanner: bufio.NewScanner(file), + writer: bufio.NewWriter(file), + }, nil +} + +func (fw *FileWriter) Close() error { + return fw.file.Close() +} + +func NewReader() (*FileReader, error) { + file, err := os.OpenFile(config.FilePath, os.O_RDONLY|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + + return &FileReader{ + file: file, + scanner: bufio.NewScanner(file), + }, nil +} + +func (fr *FileReader) Close() { + fr.file.Close() +} + +func ReadFromFile(storage *storage.Storage) { + if localStorageDisabled() { + return + } + + reader, err := NewReader() + defer reader.Close() + if err != nil { + log.Fatal("Error opening file:", err) + } + + for reader.scanner.Scan() { + line := reader.scanner.Bytes() + + rec := &record{} + if err := json.Unmarshal(line, &rec); err != nil { + log.Fatal("Error decoding data from file:", err) + } + storage.Add(rec.ShortUrl, rec.OriginalUrl) + } + + if err := reader.scanner.Err(); err != nil { + log.Fatal("Error scanning from file:", err) + } +} + +func AddRecord(short string, url string, uuid int) error { + if localStorageDisabled() { + return nil + } + + writer, err := NewWriter() + if err != nil { + return err + } + defer writer.Close() + + newRecord := record{ + UUID: strconv.Itoa(uuid), + ShortUrl: short, + OriginalUrl: url, + } + recordJSON, err := json.Marshal(newRecord) + if err != nil { + return err + } + if _, err := writer.file.WriteString(string(recordJSON) + "\n"); err != nil { + return err + } + + return nil +} + +func localStorageDisabled() bool { + return config.FilePath == "" +} diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 902f1a5..14ac3af 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -7,8 +7,8 @@ import ( "encoding/json" "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/config" + file_manager "github.com/shilin-anton/urlreducer/internal/app/file-manager" "github.com/shilin-anton/urlreducer/internal/app/gzip" - "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/shilin-anton/urlreducer/internal/logger" "io" "net/http" @@ -20,6 +20,8 @@ import ( type Storage interface { Add(short string, url string) Get(short string) (string, bool) + FindByValue(url string) (string, bool) + Size() int } type Server struct { @@ -61,14 +63,14 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.ServeHTTP(w, r) } -func New() *Server { +func New(storage Storage) *Server { r := chi.NewRouter() r.Use(requestLoggerMiddleware) r.Use(responseLoggerMiddleware) s := &Server{ - data: make(storage.Storage), + data: storage, handler: r, } r.Get("/{short}", gzipMiddleware(s.GetHandler)) @@ -95,9 +97,18 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { defer req.Body.Close() url := string(body) - short := shortenURL(url) - - s.data.Add(short, url) + var short string + if existShort, contains := s.data.FindByValue(url); !contains { + short = shortenURL(url) + uuid := s.data.Size() + 1 + if err := file_manager.AddRecord(short, url, uuid); err != nil { + http.Error(res, "Error store data to file", http.StatusInternalServerError) + return + } + s.data.Add(short, url) + } else { + short = existShort + } res.Header().Set("Content-Type", "text/plain") res.WriteHeader(http.StatusCreated) @@ -133,8 +144,20 @@ func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { return } - short := shortenURL(request.URL) - s.data.Add(short, request.URL) + //short := shortenURL(request.URL) + //s.data.Add(short, request.URL) + var short string + if existShort, contains := s.data.FindByValue(request.URL); !contains { + short = shortenURL(request.URL) + uuid := s.data.Size() + 1 + if err := file_manager.AddRecord(short, request.URL, uuid); err != nil { + http.Error(res, "Error store data to file", http.StatusInternalServerError) + return + } + s.data.Add(short, request.URL) + } else { + short = existShort + } res.Header().Set("Content-Type", "application/json") res.WriteHeader(http.StatusCreated) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 40d4950..1ff03be 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -2,6 +2,7 @@ package handlers import ( "github.com/shilin-anton/urlreducer/internal/app/config" + "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/stretchr/testify/assert" "io" "net/http" @@ -11,7 +12,8 @@ import ( ) func TestPostHandler(t *testing.T) { - srv := New() + config.FilePath = "" + srv := New(storage.New()) tests := []struct { name string @@ -47,7 +49,7 @@ func TestPostHandler(t *testing.T) { } func TestGetHandler(t *testing.T) { - srv := New() + srv := New(storage.New()) srv.data.Add("test_short", "https://smth.ru") tests := []struct { @@ -97,8 +99,9 @@ func TestGetHandler(t *testing.T) { } func TestServer_PostShortenHandler(t *testing.T) { - srv := New() + config.FilePath = "" config.BaseAddr = "http://localhost:8080" + srv := New(storage.New()) testCases := []struct { name string diff --git a/internal/app/server/server.go b/internal/app/server/server.go index bd476cb..9df84bf 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -10,6 +10,8 @@ import ( type Storage interface { Add(short string, url string) Get(short string) (string, bool) + FindByValue(url string) (string, bool) + Size() int } type server struct { @@ -18,7 +20,7 @@ type server struct { } func New(storage Storage) *server { - handler := handlers.New() + handler := handlers.New(storage) S := &server{ handler: handler, storage: storage, diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go index 7490c1f..d17e667 100644 --- a/internal/app/storage/storage.go +++ b/internal/app/storage/storage.go @@ -15,3 +15,16 @@ func New() Storage { storage := make(map[string]string) return storage } + +func (s Storage) FindByValue(url string) (string, bool) { + for k, v := range s { + if v == url { + return k, true + } + } + return "", false +} + +func (s Storage) Size() int { + return len(s) +} From 914a91851310894b75dca5060d0a7a8241d68d66 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Wed, 27 Mar 2024 15:14:01 +0300 Subject: [PATCH 24/43] Fix linter offences --- cmd/shortener/main.go | 4 ++-- internal/app/file-manager/file-manager.go | 14 +++++++------- internal/app/handlers/handler.go | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 7df6986..15d7dfc 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -2,7 +2,7 @@ package main import ( "github.com/shilin-anton/urlreducer/internal/app/config" - file_manager "github.com/shilin-anton/urlreducer/internal/app/file-manager" + filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" "github.com/shilin-anton/urlreducer/internal/app/server" "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/shilin-anton/urlreducer/internal/logger" @@ -17,7 +17,7 @@ func main() { } myStorage := storage.New() - file_manager.ReadFromFile(&myStorage) + filemanager.ReadFromFile(&myStorage) myServer := server.New(myStorage) myServer.Start() } diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go index 9eaf1d2..1142bcf 100644 --- a/internal/app/file-manager/file-manager.go +++ b/internal/app/file-manager/file-manager.go @@ -1,4 +1,4 @@ -package file_manager +package filemanager import ( "bufio" @@ -12,8 +12,8 @@ import ( type record struct { UUID string `json:"uuid"` - ShortUrl string `json:"short_url"` - OriginalUrl string `json:"original_url"` + ShortURL string `json:"short_url"` + OriginalURL string `json:"original_url"` } type FileWriter struct { @@ -66,10 +66,10 @@ func ReadFromFile(storage *storage.Storage) { } reader, err := NewReader() - defer reader.Close() if err != nil { log.Fatal("Error opening file:", err) } + defer reader.Close() for reader.scanner.Scan() { line := reader.scanner.Bytes() @@ -78,7 +78,7 @@ func ReadFromFile(storage *storage.Storage) { if err := json.Unmarshal(line, &rec); err != nil { log.Fatal("Error decoding data from file:", err) } - storage.Add(rec.ShortUrl, rec.OriginalUrl) + storage.Add(rec.ShortURL, rec.OriginalURL) } if err := reader.scanner.Err(); err != nil { @@ -99,8 +99,8 @@ func AddRecord(short string, url string, uuid int) error { newRecord := record{ UUID: strconv.Itoa(uuid), - ShortUrl: short, - OriginalUrl: url, + ShortURL: short, + OriginalURL: url, } recordJSON, err := json.Marshal(newRecord) if err != nil { diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 14ac3af..f719121 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -7,7 +7,7 @@ import ( "encoding/json" "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/config" - file_manager "github.com/shilin-anton/urlreducer/internal/app/file-manager" + filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" "github.com/shilin-anton/urlreducer/internal/app/gzip" "github.com/shilin-anton/urlreducer/internal/logger" "io" @@ -101,7 +101,7 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { if existShort, contains := s.data.FindByValue(url); !contains { short = shortenURL(url) uuid := s.data.Size() + 1 - if err := file_manager.AddRecord(short, url, uuid); err != nil { + if err := filemanager.AddRecord(short, url, uuid); err != nil { http.Error(res, "Error store data to file", http.StatusInternalServerError) return } @@ -150,7 +150,7 @@ func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { if existShort, contains := s.data.FindByValue(request.URL); !contains { short = shortenURL(request.URL) uuid := s.data.Size() + 1 - if err := file_manager.AddRecord(short, request.URL, uuid); err != nil { + if err := filemanager.AddRecord(short, request.URL, uuid); err != nil { http.Error(res, "Error store data to file", http.StatusInternalServerError) return } From ac7ff68172cb6b32fd141c246ac0ef9631994dba Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 1 Apr 2024 10:04:54 +0300 Subject: [PATCH 25/43] Remove unused types from logger --- internal/logger/logger.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 8774af5..d5a2de8 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -2,26 +2,11 @@ package logger import ( "go.uber.org/zap" - "net/http" ) // Log синглтон. var Log *zap.Logger = zap.NewNop() -type ( - // Структура для хранения сведений об ответе - responseData struct { - status int - size int - } - - // добавляем реализацию http.ResponseWriter - loggingResponseWriter struct { - http.ResponseWriter // встраиваем оригинальный http.ResponseWriter - responseData *responseData - } -) - // Initialize инициализирует логер. func Initialize(level string) error { // преобразуем текстовый уровень логирования в zap.AtomicLevel From 8a18246aba7722a955346da925759768cde01fa8 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 1 Apr 2024 15:08:32 +0300 Subject: [PATCH 26/43] Replace storage import by interface in fileManager --- internal/app/file-manager/file-manager.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go index 1142bcf..357860c 100644 --- a/internal/app/file-manager/file-manager.go +++ b/internal/app/file-manager/file-manager.go @@ -4,7 +4,6 @@ import ( "bufio" "encoding/json" "github.com/shilin-anton/urlreducer/internal/app/config" - "github.com/shilin-anton/urlreducer/internal/app/storage" "log" "os" "strconv" @@ -27,6 +26,10 @@ type FileReader struct { scanner *bufio.Scanner } +type Storage interface { + Add(shortURL, originalURL string) +} + func NewWriter() (*FileWriter, error) { file, err := os.OpenFile(config.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { @@ -60,7 +63,7 @@ func (fr *FileReader) Close() { fr.file.Close() } -func ReadFromFile(storage *storage.Storage) { +func ReadFromFile(storage Storage) { if localStorageDisabled() { return } From 4f0ae4b58c8cc9e86397d6e4fbb692e75eeb7be1 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 1 Apr 2024 18:42:01 +0300 Subject: [PATCH 27/43] Temp commit --- cmd/shortener/main.go | 16 +++-- internal/app/file-manager/file-manager.go | 72 ++++++++++++++++++++--- internal/app/handlers/handler.go | 11 +++- internal/app/server/server.go | 41 +++++++------ internal/app/storage/storage.go | 8 ++- 5 files changed, 112 insertions(+), 36 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 15d7dfc..df5468c 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -3,10 +3,11 @@ package main import ( "github.com/shilin-anton/urlreducer/internal/app/config" filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" - "github.com/shilin-anton/urlreducer/internal/app/server" + handler "github.com/shilin-anton/urlreducer/internal/app/handlers" "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/shilin-anton/urlreducer/internal/logger" "log" + "net/http" ) func main() { @@ -16,8 +17,15 @@ func main() { log.Fatal("Error initializing logger") } + fl := filemanager.New() myStorage := storage.New() - filemanager.ReadFromFile(&myStorage) - myServer := server.New(myStorage) - myServer.Start() + fl.ReadFromFile(myStorage) + myHandler := handler.New(myStorage) + log.Fatal(http.ListenAndServe(config.RunAddr, myHandler)) + + //myStorage := storage.New() + //fl := filemanager.New() + //fl.ReadFromFile(&myStorage) + //myServer := server.New(myStorage, fl) + //myServer.Start() } diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go index 357860c..b4d9906 100644 --- a/internal/app/file-manager/file-manager.go +++ b/internal/app/file-manager/file-manager.go @@ -4,9 +4,9 @@ import ( "bufio" "encoding/json" "github.com/shilin-anton/urlreducer/internal/app/config" + "github.com/shilin-anton/urlreducer/internal/app/storage" "log" "os" - "strconv" ) type record struct { @@ -15,6 +15,9 @@ type record struct { OriginalURL string `json:"original_url"` } +type ExportedManager struct { +} + type FileWriter struct { file *os.File scanner *bufio.Scanner @@ -26,10 +29,6 @@ type FileReader struct { scanner *bufio.Scanner } -type Storage interface { - Add(shortURL, originalURL string) -} - func NewWriter() (*FileWriter, error) { file, err := os.OpenFile(config.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { @@ -63,7 +62,37 @@ func (fr *FileReader) Close() { fr.file.Close() } -func ReadFromFile(storage Storage) { +func New() *ExportedManager { + return &ExportedManager{} +} + +//func ReadFromFile(storage Storage) { +// if localStorageDisabled() { +// return +// } +// +// reader, err := NewReader() +// if err != nil { +// log.Fatal("Error opening file:", err) +// } +// defer reader.Close() +// +// for reader.scanner.Scan() { +// line := reader.scanner.Bytes() +// +// rec := &record{} +// if err := json.Unmarshal(line, &rec); err != nil { +// log.Fatal("Error decoding data from file:", err) +// } +// storage.Add(rec.ShortURL, rec.OriginalURL) +// } +// +// if err := reader.scanner.Err(); err != nil { +// log.Fatal("Error scanning from file:", err) +// } +//} + +func (em *ExportedManager) ReadFromFile(storage *storage.Storage) { if localStorageDisabled() { return } @@ -89,7 +118,7 @@ func ReadFromFile(storage Storage) { } } -func AddRecord(short string, url string, uuid int) error { +func (em *ExportedManager) AddRecord(short string, url string, uuid string) error { if localStorageDisabled() { return nil } @@ -101,7 +130,7 @@ func AddRecord(short string, url string, uuid int) error { defer writer.Close() newRecord := record{ - UUID: strconv.Itoa(uuid), + UUID: uuid, ShortURL: short, OriginalURL: url, } @@ -116,6 +145,33 @@ func AddRecord(short string, url string, uuid int) error { return nil } +//func AddRecord(short string, url string, uuid int) error { +// if localStorageDisabled() { +// return nil +// } +// +// writer, err := NewWriter() +// if err != nil { +// return err +// } +// defer writer.Close() +// +// newRecord := record{ +// UUID: strconv.Itoa(uuid), +// ShortURL: short, +// OriginalURL: url, +// } +// recordJSON, err := json.Marshal(newRecord) +// if err != nil { +// return err +// } +// if _, err := writer.file.WriteString(string(recordJSON) + "\n"); err != nil { +// return err +// } +// +// return nil +//} + func localStorageDisabled() bool { return config.FilePath == "" } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index f719121..5adc7ce 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -7,7 +7,6 @@ import ( "encoding/json" "github.com/go-chi/chi/v5" "github.com/shilin-anton/urlreducer/internal/app/config" - filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" "github.com/shilin-anton/urlreducer/internal/app/gzip" "github.com/shilin-anton/urlreducer/internal/logger" "io" @@ -24,9 +23,14 @@ type Storage interface { Size() int } +type Manager interface { + AddRecord(short string, url string, uuid string) error +} + type Server struct { data Storage handler http.Handler + manager Manager } // types for logger @@ -101,7 +105,8 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { if existShort, contains := s.data.FindByValue(url); !contains { short = shortenURL(url) uuid := s.data.Size() + 1 - if err := filemanager.AddRecord(short, url, uuid); err != nil { + // s.manger.AddRecord()... + if err := s.manager.AddRecord(short, url, strconv.Itoa(uuid)); err != nil { http.Error(res, "Error store data to file", http.StatusInternalServerError) return } @@ -150,7 +155,7 @@ func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { if existShort, contains := s.data.FindByValue(request.URL); !contains { short = shortenURL(request.URL) uuid := s.data.Size() + 1 - if err := filemanager.AddRecord(short, request.URL, uuid); err != nil { + if err := s.manager.AddRecord(short, request.URL, strconv.Itoa(uuid)); err != nil { http.Error(res, "Error store data to file", http.StatusInternalServerError) return } diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 9df84bf..1ef2ce6 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -1,33 +1,36 @@ package server import ( - "github.com/shilin-anton/urlreducer/internal/app/config" - "github.com/shilin-anton/urlreducer/internal/app/handlers" - "log" "net/http" ) -type Storage interface { - Add(short string, url string) - Get(short string) (string, bool) - FindByValue(url string) (string, bool) - Size() int -} +//type Storage interface { +// Add(short string, url string) +// Get(short string) (string, bool) +// FindByValue(url string) (string, bool) +// Size() int +//} type server struct { handler http.Handler - storage Storage + //storage Storage + //manager Manager } -func New(storage Storage) *server { - handler := handlers.New(storage) - S := &server{ - handler: handler, - storage: storage, - } - return S -} +//type Manager interface { +// AddRecord(short string, url string, uuid string) error +//} + +//func New(storage Storage, manager Manager) *server { +// handler := handlers.New(storage) +// S := &server{ +// handler: handler, +// storage: storage, +// manager: manager, +// } +// return S +//} func (s server) Start() { - log.Fatal(http.ListenAndServe(config.RunAddr, s.handler)) + //log.Fatal(http.ListenAndServe(config.RunAddr, s.handler)) } diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go index d17e667..fc09eba 100644 --- a/internal/app/storage/storage.go +++ b/internal/app/storage/storage.go @@ -2,6 +2,10 @@ package storage type Storage map[string]string +type Manager interface { + ReadFromFile(storage *Storage) +} + func (s Storage) Add(short string, url string) { s[short] = url } @@ -11,8 +15,8 @@ func (s Storage) Get(short string) (string, bool) { return url, ok } -func New() Storage { - storage := make(map[string]string) +func New() *Storage { + storage := &Storage{} return storage } From 156fe729fbe415c82c67ac0fa7f7a4651e8b7455 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 1 Apr 2024 19:08:22 +0300 Subject: [PATCH 28/43] Refactor code Remove storage import from file-manager, Change logic in main, Change code style in files, Remove server package, Fix tests --- cmd/shortener/main.go | 8 +-- internal/app/config/config.go | 10 ++-- internal/app/file-manager/file-manager.go | 60 ++--------------------- internal/app/handlers/handler.go | 5 +- internal/app/handlers/handler_test.go | 22 ++++++--- internal/app/server/server.go | 36 -------------- internal/app/storage/storage.go | 4 -- 7 files changed, 29 insertions(+), 116 deletions(-) delete mode 100644 internal/app/server/server.go diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index df5468c..055d018 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -20,12 +20,6 @@ func main() { fl := filemanager.New() myStorage := storage.New() fl.ReadFromFile(myStorage) - myHandler := handler.New(myStorage) + myHandler := handler.New(myStorage, fl) log.Fatal(http.ListenAndServe(config.RunAddr, myHandler)) - - //myStorage := storage.New() - //fl := filemanager.New() - //fl.ReadFromFile(&myStorage) - //myServer := server.New(myStorage, fl) - //myServer.Start() } diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 6c3e773..421f02c 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -10,10 +10,12 @@ var BaseAddr string var LogLevel string var FilePath string -const defaultRunURL = "localhost:8080" -const defaultBaseURL = "http://localhost:8080" -const defaultLogLevel = "info" -const defaultFilePath = "/tmp/short-url-db.json" +const ( + defaultRunURL = "localhost:8080" + defaultBaseURL = "http://localhost:8080" + defaultLogLevel = "info" + defaultFilePath = "/tmp/short-url-db.json" +) func ParseConfig() { flag.StringVar(&RunAddr, "a", defaultRunURL, "address and port to run server") diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go index b4d9906..89e52e5 100644 --- a/internal/app/file-manager/file-manager.go +++ b/internal/app/file-manager/file-manager.go @@ -4,7 +4,6 @@ import ( "bufio" "encoding/json" "github.com/shilin-anton/urlreducer/internal/app/config" - "github.com/shilin-anton/urlreducer/internal/app/storage" "log" "os" ) @@ -29,6 +28,10 @@ type FileReader struct { scanner *bufio.Scanner } +type Storage interface { + Add(short, url string) +} + func NewWriter() (*FileWriter, error) { file, err := os.OpenFile(config.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { @@ -66,33 +69,7 @@ func New() *ExportedManager { return &ExportedManager{} } -//func ReadFromFile(storage Storage) { -// if localStorageDisabled() { -// return -// } -// -// reader, err := NewReader() -// if err != nil { -// log.Fatal("Error opening file:", err) -// } -// defer reader.Close() -// -// for reader.scanner.Scan() { -// line := reader.scanner.Bytes() -// -// rec := &record{} -// if err := json.Unmarshal(line, &rec); err != nil { -// log.Fatal("Error decoding data from file:", err) -// } -// storage.Add(rec.ShortURL, rec.OriginalURL) -// } -// -// if err := reader.scanner.Err(); err != nil { -// log.Fatal("Error scanning from file:", err) -// } -//} - -func (em *ExportedManager) ReadFromFile(storage *storage.Storage) { +func (em *ExportedManager) ReadFromFile(storage Storage) { if localStorageDisabled() { return } @@ -145,33 +122,6 @@ func (em *ExportedManager) AddRecord(short string, url string, uuid string) erro return nil } -//func AddRecord(short string, url string, uuid int) error { -// if localStorageDisabled() { -// return nil -// } -// -// writer, err := NewWriter() -// if err != nil { -// return err -// } -// defer writer.Close() -// -// newRecord := record{ -// UUID: strconv.Itoa(uuid), -// ShortURL: short, -// OriginalURL: url, -// } -// recordJSON, err := json.Marshal(newRecord) -// if err != nil { -// return err -// } -// if _, err := writer.file.WriteString(string(recordJSON) + "\n"); err != nil { -// return err -// } -// -// return nil -//} - func localStorageDisabled() bool { return config.FilePath == "" } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 5adc7ce..b1c14b3 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -67,7 +67,7 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.ServeHTTP(w, r) } -func New(storage Storage) *Server { +func New(storage Storage, manager Manager) *Server { r := chi.NewRouter() r.Use(requestLoggerMiddleware) @@ -76,6 +76,7 @@ func New(storage Storage) *Server { s := &Server{ data: storage, handler: r, + manager: manager, } r.Get("/{short}", gzipMiddleware(s.GetHandler)) r.Post("/", gzipMiddleware(s.PostHandler)) @@ -149,8 +150,6 @@ func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { return } - //short := shortenURL(request.URL) - //s.data.Add(short, request.URL) var short string if existShort, contains := s.data.FindByValue(request.URL); !contains { short = shortenURL(request.URL) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 1ff03be..15ceda5 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -2,6 +2,7 @@ package handlers import ( "github.com/shilin-anton/urlreducer/internal/app/config" + filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" "github.com/shilin-anton/urlreducer/internal/app/storage" "github.com/stretchr/testify/assert" "io" @@ -13,7 +14,9 @@ import ( func TestPostHandler(t *testing.T) { config.FilePath = "" - srv := New(storage.New()) + fl := filemanager.New() + myStorage := storage.New() + myHandler := New(myStorage, fl) tests := []struct { name string @@ -36,7 +39,7 @@ func TestPostHandler(t *testing.T) { req := httptest.NewRequest(test.method, test.url, strings.NewReader(test.requestBody)) w := httptest.NewRecorder() - srv.handler.ServeHTTP(w, req) + myHandler.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() @@ -49,8 +52,11 @@ func TestPostHandler(t *testing.T) { } func TestGetHandler(t *testing.T) { - srv := New(storage.New()) - srv.data.Add("test_short", "https://smth.ru") + config.FilePath = "" + fl := filemanager.New() + myStorage := storage.New() + myStorage.Add("test_short", "https://smth.ru") + myHandler := New(myStorage, fl) tests := []struct { name string @@ -79,7 +85,7 @@ func TestGetHandler(t *testing.T) { req := httptest.NewRequest(test.method, test.url, nil) w := httptest.NewRecorder() - srv.handler.ServeHTTP(w, req) + myHandler.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() @@ -101,7 +107,9 @@ func TestGetHandler(t *testing.T) { func TestServer_PostShortenHandler(t *testing.T) { config.FilePath = "" config.BaseAddr = "http://localhost:8080" - srv := New(storage.New()) + fl := filemanager.New() + myStorage := storage.New() + myHandler := New(myStorage, fl) testCases := []struct { name string @@ -141,7 +149,7 @@ func TestServer_PostShortenHandler(t *testing.T) { req.Header.Set("Content-Type", "application/json") } - srv.handler.ServeHTTP(w, req) + myHandler.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() diff --git a/internal/app/server/server.go b/internal/app/server/server.go deleted file mode 100644 index 1ef2ce6..0000000 --- a/internal/app/server/server.go +++ /dev/null @@ -1,36 +0,0 @@ -package server - -import ( - "net/http" -) - -//type Storage interface { -// Add(short string, url string) -// Get(short string) (string, bool) -// FindByValue(url string) (string, bool) -// Size() int -//} - -type server struct { - handler http.Handler - //storage Storage - //manager Manager -} - -//type Manager interface { -// AddRecord(short string, url string, uuid string) error -//} - -//func New(storage Storage, manager Manager) *server { -// handler := handlers.New(storage) -// S := &server{ -// handler: handler, -// storage: storage, -// manager: manager, -// } -// return S -//} - -func (s server) Start() { - //log.Fatal(http.ListenAndServe(config.RunAddr, s.handler)) -} diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go index fc09eba..31647f7 100644 --- a/internal/app/storage/storage.go +++ b/internal/app/storage/storage.go @@ -2,10 +2,6 @@ package storage type Storage map[string]string -type Manager interface { - ReadFromFile(storage *Storage) -} - func (s Storage) Add(short string, url string) { s[short] = url } From 0b79f6334a9c77aeb76205f689ffb246fa3cc530 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 2 Apr 2024 11:19:36 +0300 Subject: [PATCH 29/43] More refactor Separate filemanager layer from handlers, Improve filemanager handling in storage --- cmd/shortener/main.go | 5 ++-- internal/app/file-manager/file-manager.go | 8 ++---- internal/app/handlers/handler.go | 20 +++---------- internal/app/handlers/handler_test.go | 12 ++++---- internal/app/storage/storage.go | 35 ++++++++++++++++------- 5 files changed, 38 insertions(+), 42 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 055d018..66b2ab7 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -18,8 +18,7 @@ func main() { } fl := filemanager.New() - myStorage := storage.New() - fl.ReadFromFile(myStorage) - myHandler := handler.New(myStorage, fl) + myStorage := storage.New(fl) + myHandler := handler.New(myStorage) log.Fatal(http.ListenAndServe(config.RunAddr, myHandler)) } diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go index 89e52e5..925c10d 100644 --- a/internal/app/file-manager/file-manager.go +++ b/internal/app/file-manager/file-manager.go @@ -28,10 +28,6 @@ type FileReader struct { scanner *bufio.Scanner } -type Storage interface { - Add(short, url string) -} - func NewWriter() (*FileWriter, error) { file, err := os.OpenFile(config.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { @@ -69,7 +65,7 @@ func New() *ExportedManager { return &ExportedManager{} } -func (em *ExportedManager) ReadFromFile(storage Storage) { +func (em *ExportedManager) ReadFromFile(data map[string]string) { if localStorageDisabled() { return } @@ -87,7 +83,7 @@ func (em *ExportedManager) ReadFromFile(storage Storage) { if err := json.Unmarshal(line, &rec); err != nil { log.Fatal("Error decoding data from file:", err) } - storage.Add(rec.ShortURL, rec.OriginalURL) + data[rec.ShortURL] = rec.OriginalURL } if err := reader.scanner.Err(); err != nil { diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index b1c14b3..cd0d31b 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -17,20 +17,14 @@ import ( ) type Storage interface { - Add(short string, url string) + Add(short string, url string) error Get(short string) (string, bool) FindByValue(url string) (string, bool) - Size() int -} - -type Manager interface { - AddRecord(short string, url string, uuid string) error } type Server struct { data Storage handler http.Handler - manager Manager } // types for logger @@ -67,7 +61,7 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.ServeHTTP(w, r) } -func New(storage Storage, manager Manager) *Server { +func New(storage Storage) *Server { r := chi.NewRouter() r.Use(requestLoggerMiddleware) @@ -76,7 +70,6 @@ func New(storage Storage, manager Manager) *Server { s := &Server{ data: storage, handler: r, - manager: manager, } r.Get("/{short}", gzipMiddleware(s.GetHandler)) r.Post("/", gzipMiddleware(s.PostHandler)) @@ -105,13 +98,10 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { var short string if existShort, contains := s.data.FindByValue(url); !contains { short = shortenURL(url) - uuid := s.data.Size() + 1 - // s.manger.AddRecord()... - if err := s.manager.AddRecord(short, url, strconv.Itoa(uuid)); err != nil { + if err := s.data.Add(short, url); err != nil { http.Error(res, "Error store data to file", http.StatusInternalServerError) return } - s.data.Add(short, url) } else { short = existShort } @@ -153,12 +143,10 @@ func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { var short string if existShort, contains := s.data.FindByValue(request.URL); !contains { short = shortenURL(request.URL) - uuid := s.data.Size() + 1 - if err := s.manager.AddRecord(short, request.URL, strconv.Itoa(uuid)); err != nil { + if err := s.data.Add(short, request.URL); err != nil { http.Error(res, "Error store data to file", http.StatusInternalServerError) return } - s.data.Add(short, request.URL) } else { short = existShort } diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 15ceda5..164ef5b 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -15,8 +15,8 @@ import ( func TestPostHandler(t *testing.T) { config.FilePath = "" fl := filemanager.New() - myStorage := storage.New() - myHandler := New(myStorage, fl) + myStorage := storage.New(fl) + myHandler := New(myStorage) tests := []struct { name string @@ -54,9 +54,9 @@ func TestPostHandler(t *testing.T) { func TestGetHandler(t *testing.T) { config.FilePath = "" fl := filemanager.New() - myStorage := storage.New() + myStorage := storage.New(fl) myStorage.Add("test_short", "https://smth.ru") - myHandler := New(myStorage, fl) + myHandler := New(myStorage) tests := []struct { name string @@ -108,8 +108,8 @@ func TestServer_PostShortenHandler(t *testing.T) { config.FilePath = "" config.BaseAddr = "http://localhost:8080" fl := filemanager.New() - myStorage := storage.New() - myHandler := New(myStorage, fl) + myStorage := storage.New(fl) + myHandler := New(myStorage) testCases := []struct { name string diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go index 31647f7..dd8ee70 100644 --- a/internal/app/storage/storage.go +++ b/internal/app/storage/storage.go @@ -1,30 +1,43 @@ package storage -type Storage map[string]string +import "strconv" -func (s Storage) Add(short string, url string) { - s[short] = url +type Storage struct { + data map[string]string + manager Manager +} + +type Manager interface { + AddRecord(short string, url string, uuid string) error + ReadFromFile(storage map[string]string) +} + +func (s Storage) Add(short string, url string) error { + s.data[short] = url + uuid := len(s.data) + 1 + err := s.manager.AddRecord(short, url, strconv.Itoa(uuid)) + return err } func (s Storage) Get(short string) (string, bool) { - url, ok := s[short] + url, ok := s.data[short] return url, ok } -func New() *Storage { - storage := &Storage{} +func New(manager Manager) *Storage { + storage := &Storage{ + data: make(map[string]string), + manager: manager, + } + manager.ReadFromFile(storage.data) return storage } func (s Storage) FindByValue(url string) (string, bool) { - for k, v := range s { + for k, v := range s.data { if v == url { return k, true } } return "", false } - -func (s Storage) Size() int { - return len(s) -} From 5db583b64e1fd8afc424f47c4b777413d37abd1b Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 6 May 2024 10:59:47 +0300 Subject: [PATCH 30/43] Add psql --- cmd/shortener/main.go | 14 ++++++++++++-- go.mod | 8 ++++++++ go.sum | 25 ++++++++++++++++++++++++- internal/app/config/config.go | 6 ++++++ internal/app/handlers/handler.go | 15 ++++++++++++++- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 66b2ab7..b377610 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,6 +1,9 @@ package main import ( + "database/sql" + "fmt" + _ "github.com/jackc/pgx/v5/stdlib" "github.com/shilin-anton/urlreducer/internal/app/config" filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" handler "github.com/shilin-anton/urlreducer/internal/app/handlers" @@ -14,11 +17,18 @@ func main() { config.ParseConfig() err := logger.Initialize(config.LogLevel) if err != nil { - log.Fatal("Error initializing logger") + log.Fatal("Error initializing logger", err.Error()) } + dbConfig := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", + config.DbDSN, `postgres`, ``, `shortener`) fl := filemanager.New() myStorage := storage.New(fl) - myHandler := handler.New(myStorage) + db, err := sql.Open("pgx", dbConfig) + if err != nil { + log.Fatal("Error initializing DB: ", err.Error()) + } + defer db.Close() + myHandler := handler.New(myStorage, db) log.Fatal(http.ListenAndServe(config.RunAddr, myHandler)) } diff --git a/go.mod b/go.mod index 376dd62..498f569 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,21 @@ go 1.21.3 require ( github.com/go-chi/chi/v5 v5.0.11 + github.com/jackc/pgx/v5 v5.5.5 github.com/stretchr/testify v1.8.1 go.uber.org/zap v1.27.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1d6b63a..3eaeea5 100644 --- a/go.sum +++ b/go.sum @@ -3,11 +3,27 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -18,8 +34,15 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 421f02c..0710e99 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -9,12 +9,14 @@ var RunAddr string var BaseAddr string var LogLevel string var FilePath string +var DbDSN string const ( defaultRunURL = "localhost:8080" defaultBaseURL = "http://localhost:8080" defaultLogLevel = "info" defaultFilePath = "/tmp/short-url-db.json" + defaultDSN = "127.0.0.1" ) func ParseConfig() { @@ -22,6 +24,7 @@ func ParseConfig() { flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") flag.StringVar(&LogLevel, "l", defaultLogLevel, "log level") flag.StringVar(&FilePath, "f", defaultFilePath, "file storage path") + flag.StringVar(&DbDSN, "d", defaultDSN, "database DSN") flag.Parse() @@ -37,4 +40,7 @@ func ParseConfig() { if envFilePath := os.Getenv("FILE_STORAGE_PATH"); envFilePath != "" { FilePath = envFilePath } + if envDSN := os.Getenv("DATABASE_DSN"); envDSN != "" { + DbDSN = envDSN + } } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index cd0d31b..97f299c 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -3,6 +3,7 @@ package handlers import ( "bytes" "crypto/md5" + "database/sql" "encoding/hex" "encoding/json" "github.com/go-chi/chi/v5" @@ -25,6 +26,7 @@ type Storage interface { type Server struct { data Storage handler http.Handler + db *sql.DB } // types for logger @@ -61,7 +63,7 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.ServeHTTP(w, r) } -func New(storage Storage) *Server { +func New(storage Storage, db *sql.DB) *Server { r := chi.NewRouter() r.Use(requestLoggerMiddleware) @@ -70,10 +72,12 @@ func New(storage Storage) *Server { s := &Server{ data: storage, handler: r, + db: db, } r.Get("/{short}", gzipMiddleware(s.GetHandler)) r.Post("/", gzipMiddleware(s.PostHandler)) r.Post("/api/shorten", gzipMiddleware(s.PostShortenHandler)) + r.Get("/ping", gzipMiddleware(s.GetPingHandler)) return s } @@ -123,6 +127,15 @@ func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTemporaryRedirect) } +func (s Server) GetPingHandler(res http.ResponseWriter, req *http.Request) { + if err := s.db.Ping(); err != nil { + http.Error(res, "no DB connection", http.StatusInternalServerError) + return + } + + res.WriteHeader(http.StatusOK) +} + func (s Server) PostShortenHandler(res http.ResponseWriter, req *http.Request) { var request shortenRequest var buf bytes.Buffer From 2659effc8dfbfb613a7dcbf6078d5ff708c41d62 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 6 May 2024 11:04:42 +0300 Subject: [PATCH 31/43] Fix handler test --- internal/app/handlers/handler_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index 164ef5b..a3acd96 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -16,7 +16,7 @@ func TestPostHandler(t *testing.T) { config.FilePath = "" fl := filemanager.New() myStorage := storage.New(fl) - myHandler := New(myStorage) + myHandler := New(myStorage, nil) tests := []struct { name string @@ -56,7 +56,7 @@ func TestGetHandler(t *testing.T) { fl := filemanager.New() myStorage := storage.New(fl) myStorage.Add("test_short", "https://smth.ru") - myHandler := New(myStorage) + myHandler := New(myStorage, nil) tests := []struct { name string @@ -109,7 +109,7 @@ func TestServer_PostShortenHandler(t *testing.T) { config.BaseAddr = "http://localhost:8080" fl := filemanager.New() myStorage := storage.New(fl) - myHandler := New(myStorage) + myHandler := New(myStorage, nil) testCases := []struct { name string From 5dacc74bc3d37672234d390734d3273d31c8718f Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 6 May 2024 11:07:22 +0300 Subject: [PATCH 32/43] Fix linter offences --- cmd/shortener/main.go | 2 +- internal/app/config/config.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index b377610..5f84461 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -20,7 +20,7 @@ func main() { log.Fatal("Error initializing logger", err.Error()) } dbConfig := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", - config.DbDSN, `postgres`, ``, `shortener`) + config.DBDSN, `postgres`, ``, `shortener`) fl := filemanager.New() myStorage := storage.New(fl) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 0710e99..35e1c6b 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -9,7 +9,7 @@ var RunAddr string var BaseAddr string var LogLevel string var FilePath string -var DbDSN string +var DBDSN string const ( defaultRunURL = "localhost:8080" @@ -24,7 +24,7 @@ func ParseConfig() { flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") flag.StringVar(&LogLevel, "l", defaultLogLevel, "log level") flag.StringVar(&FilePath, "f", defaultFilePath, "file storage path") - flag.StringVar(&DbDSN, "d", defaultDSN, "database DSN") + flag.StringVar(&DBDSN, "d", defaultDSN, "database DSN") flag.Parse() @@ -41,6 +41,6 @@ func ParseConfig() { FilePath = envFilePath } if envDSN := os.Getenv("DATABASE_DSN"); envDSN != "" { - DbDSN = envDSN + DBDSN = envDSN } } From 9ba98e78a9e8cb3c9b36a39853e4182c31b3f183 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 6 May 2024 11:13:20 +0300 Subject: [PATCH 33/43] Unwrap gzip middleware --- cmd/shortener/main.go | 2 +- internal/app/handlers/handler.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 5f84461..e321175 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -28,7 +28,7 @@ func main() { if err != nil { log.Fatal("Error initializing DB: ", err.Error()) } - defer db.Close() + //defer db.Close() myHandler := handler.New(myStorage, db) log.Fatal(http.ListenAndServe(config.RunAddr, myHandler)) } diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 97f299c..1e293ae 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -77,7 +77,7 @@ func New(storage Storage, db *sql.DB) *Server { r.Get("/{short}", gzipMiddleware(s.GetHandler)) r.Post("/", gzipMiddleware(s.PostHandler)) r.Post("/api/shorten", gzipMiddleware(s.PostShortenHandler)) - r.Get("/ping", gzipMiddleware(s.GetPingHandler)) + r.Get("/ping", s.GetPingHandler) return s } From 49f4b1ed194541cc518d58b5024cabe79685ae84 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 6 May 2024 11:22:08 +0300 Subject: [PATCH 34/43] Change default DSN --- internal/app/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 35e1c6b..64841f0 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -16,7 +16,7 @@ const ( defaultBaseURL = "http://localhost:8080" defaultLogLevel = "info" defaultFilePath = "/tmp/short-url-db.json" - defaultDSN = "127.0.0.1" + defaultDSN = "localhost" ) func ParseConfig() { From e4cf49fa6a696e6c181082ba12e899010fd2ebfd Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Mon, 6 May 2024 12:13:01 +0300 Subject: [PATCH 35/43] Refactor and add db mock --- cmd/shortener/main.go | 14 ++++---- go.mod | 1 + go.sum | 25 ++++++++++++++ internal/app/config/config.go | 2 +- internal/app/handlers/handler.go | 8 ++--- internal/app/handlers/handler_test.go | 12 +++---- internal/app/storage/db.go | 28 ++++++++++++++++ internal/app/storage/mock/db.go | 48 +++++++++++++++++++++++++++ internal/app/storage/storage.go | 8 ++++- 9 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 internal/app/storage/db.go create mode 100644 internal/app/storage/mock/db.go diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index e321175..3cf568b 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,8 +1,6 @@ package main import ( - "database/sql" - "fmt" _ "github.com/jackc/pgx/v5/stdlib" "github.com/shilin-anton/urlreducer/internal/app/config" filemanager "github.com/shilin-anton/urlreducer/internal/app/file-manager" @@ -19,16 +17,16 @@ func main() { if err != nil { log.Fatal("Error initializing logger", err.Error()) } - dbConfig := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", - config.DBDSN, `postgres`, ``, `shortener`) fl := filemanager.New() - myStorage := storage.New(fl) - db, err := sql.Open("pgx", dbConfig) + + db, err := storage.NewDB(config.DBDSN) if err != nil { log.Fatal("Error initializing DB: ", err.Error()) } - //defer db.Close() - myHandler := handler.New(myStorage, db) + defer db.Close() + + myStorage := storage.New(fl, db) + myHandler := handler.New(myStorage) log.Fatal(http.ListenAndServe(config.RunAddr, myHandler)) } diff --git a/go.mod b/go.mod index 498f569..dce3620 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.3 require ( github.com/go-chi/chi/v5 v5.0.11 + github.com/golang/mock v1.6.0 github.com/jackc/pgx/v5 v5.5.5 github.com/stretchr/testify v1.8.1 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index 3eaeea5..ee5d90a 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -28,18 +30,41 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 64841f0..849da6b 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -16,7 +16,7 @@ const ( defaultBaseURL = "http://localhost:8080" defaultLogLevel = "info" defaultFilePath = "/tmp/short-url-db.json" - defaultDSN = "localhost" + defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" ) func ParseConfig() { diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 1e293ae..09acbe0 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -3,7 +3,6 @@ package handlers import ( "bytes" "crypto/md5" - "database/sql" "encoding/hex" "encoding/json" "github.com/go-chi/chi/v5" @@ -21,12 +20,12 @@ type Storage interface { Add(short string, url string) error Get(short string) (string, bool) FindByValue(url string) (string, bool) + PingDB() error } type Server struct { data Storage handler http.Handler - db *sql.DB } // types for logger @@ -63,7 +62,7 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.ServeHTTP(w, r) } -func New(storage Storage, db *sql.DB) *Server { +func New(storage Storage) *Server { r := chi.NewRouter() r.Use(requestLoggerMiddleware) @@ -72,7 +71,6 @@ func New(storage Storage, db *sql.DB) *Server { s := &Server{ data: storage, handler: r, - db: db, } r.Get("/{short}", gzipMiddleware(s.GetHandler)) r.Post("/", gzipMiddleware(s.PostHandler)) @@ -128,7 +126,7 @@ func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { } func (s Server) GetPingHandler(res http.ResponseWriter, req *http.Request) { - if err := s.db.Ping(); err != nil { + if err := s.data.PingDB(); err != nil { http.Error(res, "no DB connection", http.StatusInternalServerError) return } diff --git a/internal/app/handlers/handler_test.go b/internal/app/handlers/handler_test.go index a3acd96..357f988 100644 --- a/internal/app/handlers/handler_test.go +++ b/internal/app/handlers/handler_test.go @@ -15,8 +15,8 @@ import ( func TestPostHandler(t *testing.T) { config.FilePath = "" fl := filemanager.New() - myStorage := storage.New(fl) - myHandler := New(myStorage, nil) + myStorage := storage.New(fl, nil) + myHandler := New(myStorage) tests := []struct { name string @@ -54,9 +54,9 @@ func TestPostHandler(t *testing.T) { func TestGetHandler(t *testing.T) { config.FilePath = "" fl := filemanager.New() - myStorage := storage.New(fl) + myStorage := storage.New(fl, nil) myStorage.Add("test_short", "https://smth.ru") - myHandler := New(myStorage, nil) + myHandler := New(myStorage) tests := []struct { name string @@ -108,8 +108,8 @@ func TestServer_PostShortenHandler(t *testing.T) { config.FilePath = "" config.BaseAddr = "http://localhost:8080" fl := filemanager.New() - myStorage := storage.New(fl) - myHandler := New(myStorage, nil) + myStorage := storage.New(fl, nil) + myHandler := New(myStorage) testCases := []struct { name string diff --git a/internal/app/storage/db.go b/internal/app/storage/db.go new file mode 100644 index 0000000..5dabb4e --- /dev/null +++ b/internal/app/storage/db.go @@ -0,0 +1,28 @@ +package storage + +import ( + "database/sql" + _ "github.com/jackc/pgx/v5/stdlib" +) + +type DataBase struct { + DB *sql.DB +} + +type DataBaseInterface interface { + Ping() error + //Close() +} + +func NewDB(dataBaseDSN string) (*DataBase, error) { + db, err := sql.Open("pgx", dataBaseDSN) + return &DataBase{DB: db}, err +} + +func (db *DataBase) Close() { + db.DB.Close() +} + +func (db *DataBase) Ping() error { + return db.DB.Ping() +} diff --git a/internal/app/storage/mock/db.go b/internal/app/storage/mock/db.go new file mode 100644 index 0000000..7fe45e6 --- /dev/null +++ b/internal/app/storage/mock/db.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/storage/db.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockDataBaseInterface is a mock of DataBaseInterface interface. +type MockDataBaseInterface struct { + ctrl *gomock.Controller + recorder *MockDataBaseInterfaceMockRecorder +} + +// MockDataBaseInterfaceMockRecorder is the mock recorder for MockDataBaseInterface. +type MockDataBaseInterfaceMockRecorder struct { + mock *MockDataBaseInterface +} + +// NewMockDataBaseInterface creates a new mock instance. +func NewMockDataBaseInterface(ctrl *gomock.Controller) *MockDataBaseInterface { + mock := &MockDataBaseInterface{ctrl: ctrl} + mock.recorder = &MockDataBaseInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDataBaseInterface) EXPECT() *MockDataBaseInterfaceMockRecorder { + return m.recorder +} + +// Ping mocks base method. +func (m *MockDataBaseInterface) Ping() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Ping") + ret0, _ := ret[0].(error) + return ret0 +} + +// Ping indicates an expected call of Ping. +func (mr *MockDataBaseInterfaceMockRecorder) Ping() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockDataBaseInterface)(nil).Ping)) +} diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go index dd8ee70..bc96afa 100644 --- a/internal/app/storage/storage.go +++ b/internal/app/storage/storage.go @@ -5,6 +5,7 @@ import "strconv" type Storage struct { data map[string]string manager Manager + db DataBaseInterface } type Manager interface { @@ -24,10 +25,11 @@ func (s Storage) Get(short string) (string, bool) { return url, ok } -func New(manager Manager) *Storage { +func New(manager Manager, db DataBaseInterface) *Storage { storage := &Storage{ data: make(map[string]string), manager: manager, + db: db, } manager.ReadFromFile(storage.data) return storage @@ -41,3 +43,7 @@ func (s Storage) FindByValue(url string) (string, bool) { } return "", false } + +func (s Storage) PingDB() error { + return s.db.Ping() +} From d95a08c6cd5989854e8821a2444f633e1896e934 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 15:58:30 +0300 Subject: [PATCH 36/43] Add database storage --- internal/app/file-manager/file-manager.go | 12 ------- internal/app/handlers/handler.go | 6 ++-- internal/app/storage/db.go | 22 +++++++++++- internal/app/storage/storage.go | 42 ++++++++++++++++++----- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/internal/app/file-manager/file-manager.go b/internal/app/file-manager/file-manager.go index 925c10d..e1712d4 100644 --- a/internal/app/file-manager/file-manager.go +++ b/internal/app/file-manager/file-manager.go @@ -66,10 +66,6 @@ func New() *ExportedManager { } func (em *ExportedManager) ReadFromFile(data map[string]string) { - if localStorageDisabled() { - return - } - reader, err := NewReader() if err != nil { log.Fatal("Error opening file:", err) @@ -92,10 +88,6 @@ func (em *ExportedManager) ReadFromFile(data map[string]string) { } func (em *ExportedManager) AddRecord(short string, url string, uuid string) error { - if localStorageDisabled() { - return nil - } - writer, err := NewWriter() if err != nil { return err @@ -117,7 +109,3 @@ func (em *ExportedManager) AddRecord(short string, url string, uuid string) erro return nil } - -func localStorageDisabled() bool { - return config.FilePath == "" -} diff --git a/internal/app/handlers/handler.go b/internal/app/handlers/handler.go index 09acbe0..af7584e 100644 --- a/internal/app/handlers/handler.go +++ b/internal/app/handlers/handler.go @@ -18,7 +18,7 @@ import ( type Storage interface { Add(short string, url string) error - Get(short string) (string, bool) + Get(short string) (string, error) FindByValue(url string) (string, bool) PingDB() error } @@ -116,8 +116,8 @@ func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) { func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) { short := chi.URLParam(req, "short") - url, ok := s.data.Get(short) - if !ok { + url, err := s.data.Get(short) + if err != nil || url == "" { http.NotFound(res, req) return } diff --git a/internal/app/storage/db.go b/internal/app/storage/db.go index 5dabb4e..1330c4a 100644 --- a/internal/app/storage/db.go +++ b/internal/app/storage/db.go @@ -1,6 +1,7 @@ package storage import ( + "context" "database/sql" _ "github.com/jackc/pgx/v5/stdlib" ) @@ -11,7 +12,8 @@ type DataBase struct { type DataBaseInterface interface { Ping() error - //Close() + AddRecord(short string, url string) error + GetRecord(short string) (string, error) } func NewDB(dataBaseDSN string) (*DataBase, error) { @@ -19,6 +21,24 @@ func NewDB(dataBaseDSN string) (*DataBase, error) { return &DataBase{DB: db}, err } +func (db *DataBase) AddRecord(short, url string) error { + _, err := db.DB.ExecContext(context.Background(), + "INSERT INTO urls (short, url) VALUES ($1, $2)", short, url) + return err +} + +func (db *DataBase) GetRecord(short string) (string, error) { + row := db.DB.QueryRowContext(context.Background(), + "SELECT url FROM urls where short = $1", short) + + var url string + err := row.Scan(&url) + if err != nil { + return "", err + } + return url, nil +} + func (db *DataBase) Close() { db.DB.Close() } diff --git a/internal/app/storage/storage.go b/internal/app/storage/storage.go index bc96afa..dbe0a91 100644 --- a/internal/app/storage/storage.go +++ b/internal/app/storage/storage.go @@ -1,6 +1,9 @@ package storage -import "strconv" +import ( + "github.com/shilin-anton/urlreducer/internal/app/config" + "strconv" +) type Storage struct { data map[string]string @@ -13,16 +16,29 @@ type Manager interface { ReadFromFile(storage map[string]string) } +type DB interface { + AddRecord(short string, url string) error + GetRecord(short string) (string, error) +} + func (s Storage) Add(short string, url string) error { s.data[short] = url - uuid := len(s.data) + 1 - err := s.manager.AddRecord(short, url, strconv.Itoa(uuid)) - return err + if isDB() { + return s.db.AddRecord(short, url) + } else if isLocalStorage() { + uuid := len(s.data) + 1 + return s.manager.AddRecord(short, url, strconv.Itoa(uuid)) + } + return nil } -func (s Storage) Get(short string) (string, bool) { - url, ok := s.data[short] - return url, ok +func (s Storage) Get(short string) (string, error) { + if isDB() { + url, err := s.db.GetRecord(short) + return url, err + } + url := s.data[short] + return url, nil } func New(manager Manager, db DataBaseInterface) *Storage { @@ -31,7 +47,9 @@ func New(manager Manager, db DataBaseInterface) *Storage { manager: manager, db: db, } - manager.ReadFromFile(storage.data) + if isLocalStorage() { + manager.ReadFromFile(storage.data) + } return storage } @@ -44,6 +62,14 @@ func (s Storage) FindByValue(url string) (string, bool) { return "", false } +func isLocalStorage() bool { + return config.FilePath != "" +} + +func isDB() bool { + return config.DBDSN != "" +} + func (s Storage) PingDB() error { return s.db.Ping() } From 6ccac89d1c7ad0ac9c65da16eb62bc3baba03f22 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 16:08:41 +0300 Subject: [PATCH 37/43] commented default DSN --- internal/app/config/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 849da6b..1e935b0 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -16,7 +16,8 @@ const ( defaultBaseURL = "http://localhost:8080" defaultLogLevel = "info" defaultFilePath = "/tmp/short-url-db.json" - defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" + //defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" + defaultDSN = "" ) func ParseConfig() { From b4f538940255ba34c821d133b33b333dfab4aca7 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 16:29:07 +0300 Subject: [PATCH 38/43] Revert default DSN --- internal/app/config/config.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 1e935b0..849da6b 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -16,8 +16,7 @@ const ( defaultBaseURL = "http://localhost:8080" defaultLogLevel = "info" defaultFilePath = "/tmp/short-url-db.json" - //defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" - defaultDSN = "" + defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" ) func ParseConfig() { From e1ddc2c1a20dcdce5467aae131a32cb43fe89967 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 17:33:41 +0300 Subject: [PATCH 39/43] Remove default DSN --- internal/app/config/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 849da6b..1e935b0 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -16,7 +16,8 @@ const ( defaultBaseURL = "http://localhost:8080" defaultLogLevel = "info" defaultFilePath = "/tmp/short-url-db.json" - defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" + //defaultDSN = "host=127.0.0.1 user=postgres dbname=shortener sslmode=disable" + defaultDSN = "" ) func ParseConfig() { From 7e53f33e9facb8d5377b41b9c502bad0c5d74c91 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 17:56:22 +0300 Subject: [PATCH 40/43] Change var name --- internal/app/config/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 1e935b0..3bebd46 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -9,7 +9,7 @@ var RunAddr string var BaseAddr string var LogLevel string var FilePath string -var DBDSN string +var DatabaseDSN string const ( defaultRunURL = "localhost:8080" @@ -25,7 +25,7 @@ func ParseConfig() { flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") flag.StringVar(&LogLevel, "l", defaultLogLevel, "log level") flag.StringVar(&FilePath, "f", defaultFilePath, "file storage path") - flag.StringVar(&DBDSN, "d", defaultDSN, "database DSN") + flag.StringVar(&DatabaseDSN, "d", defaultDSN, "database DSN") flag.Parse() @@ -42,6 +42,6 @@ func ParseConfig() { FilePath = envFilePath } if envDSN := os.Getenv("DATABASE_DSN"); envDSN != "" { - DBDSN = envDSN + DatabaseDSN = envDSN } } From 4870d3e13cd380608637ca21177945e7b084d60e Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 18:08:40 +0300 Subject: [PATCH 41/43] Add table creation if not exists --- internal/app/config/config.go | 6 +++--- internal/app/storage/db.go | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 3bebd46..1e935b0 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -9,7 +9,7 @@ var RunAddr string var BaseAddr string var LogLevel string var FilePath string -var DatabaseDSN string +var DBDSN string const ( defaultRunURL = "localhost:8080" @@ -25,7 +25,7 @@ func ParseConfig() { flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link") flag.StringVar(&LogLevel, "l", defaultLogLevel, "log level") flag.StringVar(&FilePath, "f", defaultFilePath, "file storage path") - flag.StringVar(&DatabaseDSN, "d", defaultDSN, "database DSN") + flag.StringVar(&DBDSN, "d", defaultDSN, "database DSN") flag.Parse() @@ -42,6 +42,6 @@ func ParseConfig() { FilePath = envFilePath } if envDSN := os.Getenv("DATABASE_DSN"); envDSN != "" { - DatabaseDSN = envDSN + DBDSN = envDSN } } diff --git a/internal/app/storage/db.go b/internal/app/storage/db.go index 1330c4a..01b48ac 100644 --- a/internal/app/storage/db.go +++ b/internal/app/storage/db.go @@ -18,6 +18,17 @@ type DataBaseInterface interface { func NewDB(dataBaseDSN string) (*DataBase, error) { db, err := sql.Open("pgx", dataBaseDSN) + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS urls ( + id SERIAL PRIMARY KEY, + url VARCHAR(255) NOT NULL, + short VARCHAR(50) NOT NULL + )`) + if err != nil { + db.Close() + return nil, err + } + return &DataBase{DB: db}, err } From 0abe9090cf8369e96c13c58f86be00ef47f5369d Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 18:11:30 +0300 Subject: [PATCH 42/43] Add err handling --- internal/app/storage/db.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/app/storage/db.go b/internal/app/storage/db.go index 01b48ac..80fae83 100644 --- a/internal/app/storage/db.go +++ b/internal/app/storage/db.go @@ -18,6 +18,9 @@ type DataBaseInterface interface { func NewDB(dataBaseDSN string) (*DataBase, error) { db, err := sql.Open("pgx", dataBaseDSN) + if err != nil { + return nil, err + } _, err = db.Exec(`CREATE TABLE IF NOT EXISTS urls ( id SERIAL PRIMARY KEY, From 39b1b52d7756e9ec59db362f6064ffbb5267be07 Mon Sep 17 00:00:00 2001 From: Anton Shilin Date: Tue, 7 May 2024 18:22:58 +0300 Subject: [PATCH 43/43] Add db context and fix storage creation --- cmd/shortener/main.go | 16 +++++++++++----- internal/app/storage/db.go | 4 +++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 3cf568b..c3da68d 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -18,13 +18,19 @@ func main() { log.Fatal("Error initializing logger", err.Error()) } - fl := filemanager.New() + var fl *filemanager.ExportedManager + if config.FilePath != "" { + fl = filemanager.New() + } - db, err := storage.NewDB(config.DBDSN) - if err != nil { - log.Fatal("Error initializing DB: ", err.Error()) + var db *storage.DataBase + if config.DBDSN != "" { + db, err = storage.NewDB(config.DBDSN) + if err != nil { + log.Fatal("Error initializing DB: ", err.Error()) + } + defer db.Close() } - defer db.Close() myStorage := storage.New(fl, db) myHandler := handler.New(myStorage) diff --git a/internal/app/storage/db.go b/internal/app/storage/db.go index 80fae83..f0f57f5 100644 --- a/internal/app/storage/db.go +++ b/internal/app/storage/db.go @@ -22,7 +22,9 @@ func NewDB(dataBaseDSN string) (*DataBase, error) { return nil, err } - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS urls ( + ctx := context.Background() + + _, err = db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS urls ( id SERIAL PRIMARY KEY, url VARCHAR(255) NOT NULL, short VARCHAR(50) NOT NULL