diff --git a/.github/workflows/shortenertest.yml b/.github/workflows/shortenertest.yml index 5c9e229..b2b3d5a 100644 --- a/.github/workflows/shortenertest.yml +++ b/.github/workflows/shortenertest.yml @@ -8,9 +8,18 @@ on: jobs: + branchtest: + runs-on: ubuntu-latest + + steps: + - name: Check branch name + run: | + if [[ ! $GITHUB_HEAD_REF =~ ^iter[0-9]+$ ]]; then echo "Branch name must match pattern 'iter'" && exit 1; fi + shortenertest: runs-on: ubuntu-latest - container: golang:1.17 + container: golang:1.18 + needs: branchtest services: postgres: @@ -31,7 +40,7 @@ jobs: - name: Download autotests binaries uses: robinraju/release-downloader@v1.2 with: - repository: Yandex-Practicum/go-autotests-bin + repository: Yandex-Practicum/go-autotests latest: true fileName: "*" out-file-path: .tools @@ -48,30 +57,110 @@ jobs: go build -o shortener - name: "Code increment #1" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter1' || + github.head_ref == 'iter2' || + github.head_ref == 'iter3' || + github.head_ref == 'iter4' || + github.head_ref == 'iter5' || + github.head_ref == 'iter6' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration1$ \ -binary-path=cmd/shortener/shortener - name: "Code increment #2" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter2' || + github.head_ref == 'iter3' || + github.head_ref == 'iter4' || + github.head_ref == 'iter5' || + github.head_ref == 'iter6' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration2$ -source-path=. - name: "Code increment #3" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter3' || + github.head_ref == 'iter4' || + github.head_ref == 'iter5' || + github.head_ref == 'iter6' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration3$ -source-path=. - name: "Code increment #4" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter4' || + github.head_ref == 'iter5' || + github.head_ref == 'iter6' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration4$ \ -source-path=. \ -binary-path=cmd/shortener/shortener - name: "Code increment #5" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter5' || + github.head_ref == 'iter6' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | SERVER_HOST=$(random domain) SERVER_PORT=$(random unused-port) @@ -82,7 +171,20 @@ jobs: -server-base-url="http://$SERVER_HOST:$SERVER_PORT" - name: "Code increment #6" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter6' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | SERVER_PORT=$(random unused-port) TEMP_FILE=$(random tempfile) @@ -93,7 +195,19 @@ jobs: -source-path=. - name: "Code increment #7" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter7' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | SERVER_PORT=$(random unused-port) TEMP_FILE=$(random tempfile) @@ -104,21 +218,51 @@ jobs: -source-path=. - name: "Code increment #8" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter8' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration8$ \ -source-path=. \ -binary-path=cmd/shortener/shortener - name: "Code increment #9" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter9' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration9$ \ -source-path=. \ -binary-path=cmd/shortener/shortener - name: "Code increment #10" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter10' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration10$ \ -source-path=. \ @@ -126,29 +270,135 @@ jobs: -database-dsn='postgres://postgres:postgres@postgres:5432/praktikum?sslmode=disable' - name: "Code increment #11" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter11' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration11$ \ -binary-path=cmd/shortener/shortener \ -database-dsn='postgres://postgres:postgres@postgres:5432/praktikum?sslmode=disable' - name: "Code increment #12" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter12' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration12$ \ -binary-path=cmd/shortener/shortener \ -database-dsn='postgres://postgres:postgres@postgres:5432/praktikum?sslmode=disable' - name: "Code increment #13" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter13' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration13$ \ -binary-path=cmd/shortener/shortener \ -database-dsn='postgres://postgres:postgres@postgres:5432/praktikum?sslmode=disable' - name: "Code increment #14" - if: always() + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' run: | shortenertest -test.v -test.run=^TestIteration14$ \ -binary-path=cmd/shortener/shortener \ -database-dsn='postgres://postgres:postgres@postgres:5432/praktikum?sslmode=disable' + + - name: "Code increment #14 (degradation)" + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' + run: | + shortenertest -test.v -test.run=^TestIteration14$ \ + -binary-path=cmd/shortener/shortener + + - name: "Code increment #14 (race detection)" + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter14' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' + run: | + go test -v -race ./... + + - name: "Code increment #15" + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter15' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' + run: | + shortenertest -test.v -test.run=^TestIteration15$ -source-path=. + + - name: "Code increment #16" + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter16' || + github.head_ref == 'iter17' + run: | + shortenertest -test.v -test.run=^TestIteration16$ -source-path=. + + - name: "Code increment #17" + if: | + github.ref == 'refs/heads/main' || + github.head_ref == 'iter17' + run: | + shortenertest -test.v -test.run=^TestIteration17$ -source-path=. + + - name: "Code increment #18" + if: github.repository == '' + run: | + echo "Not implemented" + + - name: "Code increment #19" + if: github.repository == '' + run: | + echo "Not implemented" + + - name: "Code increment #20" + if: github.repository == '' + run: | + echo "Not implemented" + + - name: "Code increment #21" + if: github.repository == '' + run: | + echo "Not implemented" + + - name: "Code increment #22" + if: github.repository == '' + run: | + echo "Not implemented" + + - name: "Code increment #23" + if: github.repository == '' + run: | + echo "Not implemented" + + - name: "Code increment #24" + if: github.repository == '' + run: | + echo "Not implemented" diff --git a/.github/workflows/statictest.yml b/.github/workflows/statictest.yml index 9c7df43..781a222 100644 --- a/.github/workflows/statictest.yml +++ b/.github/workflows/statictest.yml @@ -10,7 +10,7 @@ jobs: statictest: runs-on: ubuntu-latest - container: golang:1.17 + container: golang:1.18 steps: - name: Checkout code uses: actions/checkout@v2 @@ -18,7 +18,7 @@ jobs: - name: Download statictest binary uses: robinraju/release-downloader@v1 with: - repository: Yandex-Practicum/go-autotests-bin + repository: Yandex-Practicum/go-autotests latest: true fileName: statictest out-file-path: .tools diff --git a/.gitignore b/.gitignore index 3362f51..7b85ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ cmd/shortener/shortener # Dependency directories (remove the comment below to include it) vendor/ +data/ # IDEs directories .idea diff --git a/go.mod b/go.mod index 0b3df0f..f6b4fef 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,9 @@ require ( github.com/caarlos0/env/v6 v6.9.3 github.com/go-chi/chi/v5 v5.0.7 github.com/jackc/pgx/v4 v4.17.0 - github.com/lib/pq v1.10.6 github.com/stretchr/testify v1.8.0 github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 - go.uber.org/zap v1.21.0 + go.uber.org/zap v1.23.0 ) require ( @@ -21,11 +20,12 @@ require ( github.com/jackc/pgproto3/v2 v2.3.1 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgtype v1.12.0 // indirect + github.com/lib/pq v1.10.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.4.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 1548697..aba0c18 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -133,12 +135,16 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/internal/app/app.go b/internal/app/app.go index ed42ee5..2374c72 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -39,7 +39,7 @@ func NewApp( fansShortService := fans.NewFansShortService(store.ShortURL, 4) - r := handlers.NewRouter(log, baseURL, services.ShorterService, store.ShortURL, store, fansShortService) + r := handlers.NewRouter(log, baseURL, services.ShorterService, store.ShortURL, fansShortService, store) serv := server.NewServer(log, serverAddress, r) serv.Start() diff --git a/internal/handlers/router.go b/internal/handlers/router.go index 5065637..310dd20 100644 --- a/internal/handlers/router.go +++ b/internal/handlers/router.go @@ -1,18 +1,15 @@ package handlers import ( - "context" - "github.com/shreyner/go-shortener/internal/pkg/fans" - "github.com/shreyner/go-shortener/internal/repositories" - "net/http" - "time" - + "compress/gzip" "github.com/go-chi/chi/v5" chiMiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/shreyner/go-shortener/internal/pkg/fans" + "github.com/shreyner/go-shortener/internal/repositories" + "github.com/shreyner/go-shortener/internal/storage" "go.uber.org/zap" "github.com/shreyner/go-shortener/internal/middlewares" - "github.com/shreyner/go-shortener/internal/storage" ) var cookieSecretKey = []byte("triy6n9rw3") @@ -22,61 +19,51 @@ func NewRouter( baseURL string, shorterService ShortedService, shortURIRepository repositories.ShortURLRepository, - storage *storage.Storage, fansShortService *fans.FansShortService, + storage *storage.Storage, ) *chi.Mux { r := chi.NewRouter() r.Use(chiMiddleware.RequestID) r.Use(chiMiddleware.RealIP) - r.Use(chiMiddleware.Logger) + r.Use(middlewares.NewStructuredLogger(log)) r.Use(chiMiddleware.Recoverer) + r.Use(chiMiddleware.Compress(gzip.BestSpeed)) + + authMiddleware := middlewares.AuthHandler(log, cookieSecretKey) shortedHandler := NewShortedHandler(log, baseURL, shorterService, shortURIRepository, fansShortService) + storeHandler := NewStoreHandler(log, storage) r.Route("/api", func(r chi.Router) { - r.With(middlewares.AuthHandler(cookieSecretKey)).Route("/shorten", func(r chi.Router) { - r. - With( - chiMiddleware.AllowContentEncoding("gzip"), - middlewares.GzlibCompressHandler, - ). - Post("/", shortedHandler.APICreate) - - r.Post("/batch", shortedHandler.APICreateBatch) - }) - - r.With(middlewares.AuthHandler(cookieSecretKey)).Route("/user", func(r chi.Router) { - r.Route("/urls", func(r chi.Router) { - r.Get("/", shortedHandler.APIUserURLs) - r.Delete("/", shortedHandler.APIUserDeleteURLs) + r.With( + chiMiddleware.AllowContentType("application/json", "application/gzip", "application/x-gzip"), + authMiddleware, + ). + Group(func(r chi.Router) { + r.Route("/shorten", func(r chi.Router) { + r.Post("/", shortedHandler.APICreate) + r.Post("/batch", shortedHandler.APICreateBatch) + }) + + r.Route("/user", func(r chi.Router) { + r.Route("/urls", func(r chi.Router) { + r.Get("/", shortedHandler.APIUserURLs) + r.Delete("/", shortedHandler.APIUserDeleteURLs) + }) + }) }) - }) }) r.With( - chiMiddleware.AllowContentEncoding("gzip"), - middlewares.GzlibCompressHandler, - middlewares.AuthHandler(cookieSecretKey), - ). - Post("/", shortedHandler.Create) - - r.Get("/{id}", shortedHandler.Get) - - r.Get("/ping", func(rw http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - - if err := storage.PingContext(ctx); err != nil { - log.Error("can't ping to database", zap.Error(err)) - rw.WriteHeader(http.StatusInternalServerError) - - return - } - - rw.WriteHeader(http.StatusOK) + chiMiddleware.AllowContentType("text/plain", "application/gzip", "application/x-gzip"), + authMiddleware, + ).Group(func(r chi.Router) { + r.Post("/", shortedHandler.Create) + r.Get("/{id}", shortedHandler.Get) }) - return r + r.Get("/ping", storeHandler.Ping) + return r } diff --git a/internal/handlers/shorted.go b/internal/handlers/shorted.go index 5b7ee8e..4a06c8c 100644 --- a/internal/handlers/shorted.go +++ b/internal/handlers/shorted.go @@ -1,24 +1,18 @@ package handlers import ( - "bytes" - "compress/gzip" "context" "encoding/json" "errors" "fmt" + "github.com/go-chi/chi/v5" "github.com/shreyner/go-shortener/internal/pkg/fans" "github.com/shreyner/go-shortener/internal/repositories" + "github.com/timewasted/go-accept-headers" + "go.uber.org/zap" "io" - "log" - "mime" "net/http" "net/url" - "strings" - - "github.com/go-chi/chi/v5" - "github.com/timewasted/go-accept-headers" - "go.uber.org/zap" "github.com/shreyner/go-shortener/internal/core" "github.com/shreyner/go-shortener/internal/middlewares" @@ -48,12 +42,12 @@ func NewShortedHandler( log *zap.Logger, baseURL string, shorterService ShortedService, - ShorterRepository repositories.ShortURLRepository, + shorterRepository repositories.ShortURLRepository, fansShortService *fans.FansShortService, ) *ShortedHandler { return &ShortedHandler{ ShorterService: shorterService, - ShorterRepository: ShorterRepository, + ShorterRepository: shorterRepository, baseURL: baseURL, log: log, fansShortService: fansShortService, @@ -61,45 +55,22 @@ func NewShortedHandler( } func (sh *ShortedHandler) Create(wr http.ResponseWriter, r *http.Request) { - mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + body, err := io.ReadAll(r.Body) if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - return - } + sh.log.Error("error read all content body", zap.Error(err)) + http.Error(wr, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - if mediaType != "text/plain" && mediaType != "application/x-gzip" { - http.Error(wr, "bad request", http.StatusBadRequest) return } - var body []byte - - if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { - if body, err = Decompress(r.Body); err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - - return - } - - } else { - body, err = io.ReadAll(r.Body) - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - - return - } - } defer r.Body.Close() _, err = url.ParseRequestURI(string(body)) if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, "Invalid url", http.StatusBadRequest) + sh.log.Error("error parse url from body", zap.Error(err)) + http.Error(wr, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } @@ -117,8 +88,8 @@ func (sh *ShortedHandler) Create(wr http.ResponseWriter, r *http.Request) { } if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) + sh.log.Error("error create short url", zap.Error(err)) + http.Error(wr, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -133,12 +104,12 @@ func (sh *ShortedHandler) Get(wr http.ResponseWriter, r *http.Request) { shortURL, ok := sh.ShorterService.GetByID(shortCode) if !ok { - http.Error(wr, "Not Found", http.StatusNotFound) + http.Error(wr, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if shortURL.IsDeleted { - http.Error(wr, "Was deleted", http.StatusGone) + http.Error(wr, http.StatusText(http.StatusGone), http.StatusGone) return } @@ -154,19 +125,6 @@ type ShortedResponseDTO struct { } func (sh *ShortedHandler) APICreate(wr http.ResponseWriter, r *http.Request) { - mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - return - } - - if mediaType != ContentTypeJSON { - http.Error(wr, "bad request", http.StatusBadRequest) - return - } - acceptHeader := r.Header.Get("Accept") if acceptHeader != "" { @@ -183,30 +141,21 @@ func (sh *ShortedHandler) APICreate(wr http.ResponseWriter, r *http.Request) { } } - var body []byte - - if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { - if body, err = Decompress(r.Body); err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - - return - } - } else { - body, err = io.ReadAll(r.Body) - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) + body, err := io.ReadAll(r.Body) + if err != nil { + sh.log.Error("error read all content body", zap.Error(err)) + http.Error(wr, err.Error(), http.StatusInternalServerError) - return - } + return } + defer r.Body.Close() var shortedCreateDTO ShortedCreateDTO if err := json.Unmarshal(body, &shortedCreateDTO); err != nil { - http.Error(wr, "Error parse body", http.StatusInternalServerError) + sh.log.Error("error json parse", zap.Error(err)) + http.Error(wr, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -275,19 +224,6 @@ type ShortedResponseBatchDTO struct { } func (sh *ShortedHandler) APICreateBatch(wr http.ResponseWriter, r *http.Request) { - mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - return - } - - if mediaType != ContentTypeJSON { - http.Error(wr, "bad request", http.StatusBadRequest) - return - } - acceptHeader := r.Header.Get("Accept") if acceptHeader != "" { @@ -304,24 +240,14 @@ func (sh *ShortedHandler) APICreateBatch(wr http.ResponseWriter, r *http.Request } } - var body []byte - - if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { - if body, err = Decompress(r.Body); err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - - return - } - } else { - body, err = io.ReadAll(r.Body) - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) + body, err := io.ReadAll(r.Body) + if err != nil { + sh.log.Error("error read all content body", zap.Error(err)) + http.Error(wr, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } + return } + defer r.Body.Close() var shortedCreateBatchDTO []*ShortedCreateBatchDTO @@ -407,19 +333,6 @@ func (sh *ShortedHandler) APIUserURLs(wr http.ResponseWriter, r *http.Request) { } func (sh *ShortedHandler) APIUserDeleteURLs(wr http.ResponseWriter, r *http.Request) { - mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - return - } - - if mediaType != ContentTypeJSON { - http.Error(wr, "bad request", http.StatusBadRequest) - return - } - acceptHeader := r.Header.Get("Accept") if acceptHeader != "" { @@ -436,23 +349,12 @@ func (sh *ShortedHandler) APIUserDeleteURLs(wr http.ResponseWriter, r *http.Requ } } - var body []byte - - if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { - if body, err = Decompress(r.Body); err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) - - return - } - } else { - body, err = io.ReadAll(r.Body) - if err != nil { - log.Printf("error: %s", err.Error()) - http.Error(wr, err.Error(), http.StatusInternalServerError) + body, err := io.ReadAll(r.Body) + if err != nil { + sh.log.Error("error read all content body", zap.Error(err)) + http.Error(wr, err.Error(), http.StatusInternalServerError) - return - } + return } defer r.Body.Close() @@ -472,21 +374,3 @@ func (sh *ShortedHandler) APIUserDeleteURLs(wr http.ResponseWriter, r *http.Requ wr.WriteHeader(http.StatusAccepted) } - -func Decompress(dateRead io.Reader) ([]byte, error) { - gr, err := gzip.NewReader(dateRead) - - if err != nil { - return nil, err - } - - defer gr.Close() - - var b bytes.Buffer - - if _, err := b.ReadFrom(gr); err != nil { - return nil, fmt.Errorf("failed decopress data :%w", err) - } - - return b.Bytes(), nil -} diff --git a/internal/handlers/shorted_test.go b/internal/handlers/shorted_test.go index 63b4e18..b151d70 100644 --- a/internal/handlers/shorted_test.go +++ b/internal/handlers/shorted_test.go @@ -135,7 +135,7 @@ func TestShortedHandler_ShortedCreate(t *testing.T) { defer resp.Body.Close() mockService.AssertNotCalled(t, "Create") - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + assert.Equal(t, http.StatusUnsupportedMediaType, resp.StatusCode) }) t.Run("should error method not allowed for POST /some", func(t *testing.T) { @@ -269,7 +269,7 @@ func TestShortedHandler_ApiCreate(t *testing.T) { defer resp.Body.Close() mockService.AssertNotCalled(t, "Create") - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + assert.Equal(t, http.StatusUnsupportedMediaType, resp.StatusCode) }) t.Run("should error for incorrect Accept", func(t *testing.T) { diff --git a/internal/handlers/store.go b/internal/handlers/store.go new file mode 100644 index 0000000..11893d7 --- /dev/null +++ b/internal/handlers/store.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "context" + "github.com/shreyner/go-shortener/internal/storage" + "go.uber.org/zap" + "net/http" + "time" +) + +type StoreHandler struct { + log *zap.Logger + store *storage.Storage +} + +func NewStoreHandler(log *zap.Logger, store *storage.Storage) *StoreHandler { + return &StoreHandler{ + log: log, + store: store, + } +} + +func (s *StoreHandler) Ping(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + + if err := s.store.PingContext(ctx); err != nil { + s.log.Error("can't ping to database", zap.Error(err)) + w.WriteHeader(http.StatusInternalServerError) + + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/internal/middlewares/auth-handler.go b/internal/middlewares/auth-handler.go index 261fd0b..af4831e 100644 --- a/internal/middlewares/auth-handler.go +++ b/internal/middlewares/auth-handler.go @@ -6,7 +6,7 @@ import ( "crypto/cipher" "crypto/sha256" "encoding/hex" - "log" + "go.uber.org/zap" "net/http" "github.com/shreyner/go-shortener/internal/pkg/random" @@ -26,7 +26,7 @@ func GetUserIDFromCtx(ctx context.Context) string { return v } -func AuthHandler(key []byte) func(next http.Handler) http.Handler { +func AuthHandler(log *zap.Logger, key []byte) func(next http.Handler) http.Handler { sh := sha256.New() sh.Write(key) @@ -35,14 +35,14 @@ func AuthHandler(key []byte) func(next http.Handler) http.Handler { aesBlock, err := aes.NewCipher(keyHash) if err != nil { // TODO: Убрать Fatalln. Обычный error. Сделать выбрасывание http ошибки - log.Fatalln(err) + log.Fatal("error", zap.Error(err)) } aesGCM, err := cipher.NewGCM(aesBlock) if err != nil { // TODO: Убрать Fatalln. Обычный error. Сделать выбрасывание http ошибки // TODO: Пробежаться и посомтреть по коду - log.Fatalln(err) + log.Fatal("error", zap.Error(err)) } nonce := keyHash[len(keyHash)-aesGCM.NonceSize():] diff --git a/internal/middlewares/gzlib-handler.go b/internal/middlewares/gzlib-handler.go deleted file mode 100644 index 6751f4f..0000000 --- a/internal/middlewares/gzlib-handler.go +++ /dev/null @@ -1,45 +0,0 @@ -package middlewares - -import ( - "compress/gzip" - "io" - "log" - "net/http" - "strings" -) - -type gzlibWriter struct { - http.ResponseWriter - Writer io.Writer -} - -func (w gzlibWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -func GzlibCompressHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - next.ServeHTTP(rw, r) - - return - } - - zw, err := gzip.NewWriterLevel(rw, gzip.BestSpeed) - - if err != nil { - log.Println("error compress response", err.Error()) - http.Error(rw, err.Error(), http.StatusInternalServerError) - - return - } - - defer zw.Close() - - rw.Header().Add("Content-Encoding", "gzip") - - // TODO: Вынести сюда логику с decompress, разбирать body и складывать stream buffer в body - - next.ServeHTTP(gzlibWriter{rw, zw}, r) - }) -} diff --git a/internal/middlewares/logger.go b/internal/middlewares/logger.go new file mode 100644 index 0000000..025cd8c --- /dev/null +++ b/internal/middlewares/logger.go @@ -0,0 +1,74 @@ +package middlewares + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/v5/middleware" + "go.uber.org/zap" +) + +type StructuredLogger struct { + Logger *zap.Logger +} + +func (s *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { + entry := &StructuredLoggerEntry{Logger: s.Logger.Named("Http Request")} + fields := make([]zap.Field, 0) + + //fields = append(fields, zap.String("ts", time.Now().UTC().Format(time.RFC1123))) + + if reqID := middleware.GetReqID(r.Context()); reqID != "" { + fields = append(fields, zap.String("req_id", reqID)) + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + fields = append(fields, zap.String("http_scheme", scheme)) + fields = append(fields, zap.String("http_method", r.Method)) + fields = append(fields, zap.String("remote_addr", r.RemoteAddr)) + fields = append(fields, zap.String("user_agent", r.UserAgent())) + fields = append(fields, zap.String("uri", fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI))) + + entry.Logger = entry.Logger.With(fields...) + + entry.Logger.Info("request started") + + return entry +} + +type StructuredLoggerEntry struct { + Logger *zap.Logger +} + +func (s *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { + log := s.Logger.With( + zap.Int("resp_status", status), + zap.Int("resp_bytes_length", bytes), + zap.Float64("resp_elapsed_ms", float64(elapsed.Nanoseconds())/1000000.0), //nolint:gomnd + ) + + switch { + case status <= http.StatusBadRequest: + log.Info("request complete") + case status <= http.StatusInternalServerError: + log.Warn("request complete") + default: + log.Error("request complete") + } +} + +func (s *StructuredLoggerEntry) Panic(v interface{}, stack []byte) { + s.Logger = s.Logger.With( + zap.ByteString("stack", stack), + zap.String("panic", fmt.Sprintf("%+v", v)), + ) +} + +func NewStructuredLogger(logger *zap.Logger) func(next http.Handler) http.Handler { + return middleware.RequestLogger(&StructuredLogger{logger}) +} diff --git a/internal/pkg/fans/worker.go b/internal/pkg/fans/worker.go index 1abfa2b..bc79e02 100644 --- a/internal/pkg/fans/worker.go +++ b/internal/pkg/fans/worker.go @@ -1,12 +1,9 @@ package fans -import "log" - func worker(inputCh, outCh chan *FanDeleteJob) { go func() { - for num := range inputCh { - log.Println(num) - outCh <- num + for job := range inputCh { + outCh <- job } close(outCh) diff --git a/internal/pkg/workerpool/workerpool.go b/internal/pkg/workerpool/workerpool.go deleted file mode 100644 index 30dde2f..0000000 --- a/internal/pkg/workerpool/workerpool.go +++ /dev/null @@ -1,112 +0,0 @@ -package workerpool - -import ( - "runtime" - "sync" - - "go.uber.org/zap" -) - -type JobDeleteURLs struct { - UserID string - URLIDs []string -} - -type Queue struct { - arr []*JobDeleteURLs - mu sync.Mutex - cond *sync.Cond -} - -func NewQueue() *Queue { - q := Queue{} - q.cond = sync.NewCond(&q.mu) - - return &q -} - -func (q *Queue) Push(task *JobDeleteURLs) { - q.mu.Lock() - defer q.mu.Unlock() - - q.arr = append(q.arr, task) - q.cond.Signal() -} - -func (q *Queue) PopWait() *JobDeleteURLs { - q.mu.Lock() - defer q.mu.Unlock() - - if len(q.arr) == 0 { - q.cond.Wait() - } - - t := q.arr[0] - - q.arr = q.arr[1:] - - return t -} - -type JobDeleter func(*JobDeleteURLs) error - -type Worker struct { - id int - log *zap.Logger - queue *Queue - jobDeleter JobDeleter -} - -func (w *Worker) Loop(stopCh chan struct{}) { - for { - select { - case <-stopCh: - return - default: - t := w.queue.PopWait() - - if err := w.jobDeleter(t); err != nil { - w.log.Error("error worker", zap.Int("workerID", w.id), zap.Error(err)) - } - } - } -} - -type WorkerPool struct { - workers []*Worker - queue *Queue - - stopCh chan struct{} -} - -func NewWorkerPool(log *zap.Logger, jobDeleter JobDeleter) *WorkerPool { - wp := WorkerPool{ - queue: NewQueue(), - workers: make([]*Worker, 0, runtime.NumCPU()), - - stopCh: make(chan struct{}), - } - - for i := 0; i < runtime.NumCPU(); i++ { - wp.workers = append(wp.workers, &Worker{ - id: i, - log: log, - queue: wp.queue, - jobDeleter: jobDeleter, - }) - } - - for _, worker := range wp.workers { - go worker.Loop(wp.stopCh) - } - - return &wp -} - -func (wp *WorkerPool) Push(job *JobDeleteURLs) { - wp.queue.Push(job) -} - -func (wp *WorkerPool) Stop() { - close(wp.stopCh) -} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index ba722d2..ceab7cf 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -39,7 +39,7 @@ func NewStorage(log *zap.Logger, fileStoragePath string, dataBaseDSN string) (*S if repositoryType == RepositoryTypeFile { log.Info("Init file storage") - shorterFileRepository, err := storagefile.NewShortURLStore(fileStoragePath) + shorterFileRepository, err := storagefile.NewShortURLStore(log, fileStoragePath) if err != nil { return nil, fmt.Errorf("storage error when initialize file: %w", err) diff --git a/internal/storage/storage_file/short_urls.go b/internal/storage/storage_file/short_urls.go index 2da51ed..11af800 100644 --- a/internal/storage/storage_file/short_urls.go +++ b/internal/storage/storage_file/short_urls.go @@ -3,7 +3,7 @@ package storagefile import ( "context" "encoding/json" - "log" + "go.uber.org/zap" "os" "sync" @@ -11,6 +11,7 @@ import ( ) type shortURLRepository struct { + log *zap.Logger pathToFile string file *os.File decoder *json.Decoder @@ -19,7 +20,7 @@ type shortURLRepository struct { mutex *sync.RWMutex } -func NewShortURLStore(fileStoragePath string) (*shortURLRepository, error) { +func NewShortURLStore(log *zap.Logger, fileStoragePath string) (*shortURLRepository, error) { file, err := os.OpenFile(fileStoragePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { @@ -27,6 +28,7 @@ func NewShortURLStore(fileStoragePath string) (*shortURLRepository, error) { } return &shortURLRepository{ + log: log, pathToFile: fileStoragePath, mutex: &sync.RWMutex{}, file: file, @@ -48,7 +50,7 @@ func (s *shortURLRepository) GetByID(id string) (*core.ShortURL, bool) { file, err := os.OpenFile(s.pathToFile, os.O_RDONLY|os.O_CREATE, 0644) if err != nil { - log.Printf("Error open file for read: %s", err) + s.log.Error("error open file for read", zap.Error(err)) return nil, false } defer file.Close() @@ -60,7 +62,7 @@ func (s *shortURLRepository) GetByID(id string) (*core.ShortURL, bool) { err := decoder.Decode(&shortURL) if err != nil { - log.Printf("Error read shorted json:%s\n", err) + s.log.Error("error read shorted json", zap.Error(err)) return nil, false } @@ -79,7 +81,7 @@ func (s *shortURLRepository) AllByUserID(id string) ([]*core.ShortURL, error) { file, err := os.OpenFile(s.pathToFile, os.O_RDONLY|os.O_CREATE, 0644) if err != nil { - log.Printf("Error open file for read: %s", err) + s.log.Error("error open file for read", zap.Error(err)) return nil, err } defer file.Close() @@ -93,7 +95,7 @@ func (s *shortURLRepository) AllByUserID(id string) ([]*core.ShortURL, error) { err := decoder.Decode(&shortURL) if err != nil { - log.Printf("Error read shorted json:%s\n", err) + s.log.Error("Error read shorted json", zap.Error(err)) return nil, err }