diff --git a/cmd/config/config.go b/cmd/config/config.go new file mode 100644 index 0000000..5736a65 --- /dev/null +++ b/cmd/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "fmt" + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "github.com/nabbat/23_kogorta_shotener/internal/flags" +) + +func SetEnv() *envirements.EnvConfig { + fl := flags.ParseFlags() + en := envirements.ParseEnv() + c := &envirements.EnvConfig{} + + if en.RunAddr != "" { + c.RunAddr = en.RunAddr + } else { + c.RunAddr = fl.RunAddr + } + + if en.ResultURL != "" && en.RunAddr != "http://" { + c.ResultURL = en.ResultURL + } else { + c.ResultURL = fl.ResultURL + } + fmt.Println(c) + return c +} diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go new file mode 100644 index 0000000..bdd04a3 --- /dev/null +++ b/cmd/config/config_test.go @@ -0,0 +1,23 @@ +package config + +import ( + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "reflect" + "testing" +) + +func TestSetEnv(t *testing.T) { + tests := []struct { + name string + want *envirements.EnvConfig + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SetEnv(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetEnv() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 38dd16d..fd1a34d 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,3 +1,44 @@ package main -func main() {} +import ( + "github.com/gorilla/mux" + "github.com/nabbat/23_kogorta_shotener/cmd/config" + "github.com/nabbat/23_kogorta_shotener/internal/handlers" + "github.com/nabbat/23_kogorta_shotener/internal/liblog" + urlstorage "github.com/nabbat/23_kogorta_shotener/internal/storage" + "net/http" +) + +func main() { + // Инициализируем логер + log := liblog.NewLogger() + + // Получаем переменные если они есть + c := config.SetEnv() + + // Создаем хранилище + storage := urlstorage.NewURLStorage() + // Создаем хэндлеры + redirectHandler := &handlers.RedirectHandler{} + shortenURLHandler := &handlers.ShortenURLHandler{} + + r := mux.NewRouter() + + // Регистрируем middleware для логирования запросов + r.Use(handlers.RequestLoggingMiddleware(log)) + // Регистрируем middleware для логирования ответов + r.Use(handlers.ResponseLoggingMiddleware(log)) + r.Use(handlers.PanicHandler) // Добавляем PanicHandler middleware + + r.HandleFunc("/api/shorten", shortenURLHandler.HandleShortenURLJSON(storage, c, log)).Methods("POST") + r.HandleFunc("/", shortenURLHandler.HandleShortenURL(storage, c, log)).Methods("POST") + r.HandleFunc("/{idShortenURL}", redirectHandler.HandleRedirect(storage, log)).Methods("GET") + + log.Info("RunAddr: ", c.RunAddr, " | ", "ResultURL: ", c.ResultURL) + log.Info("Running server on ", c.RunAddr) + + err := http.ListenAndServe(c.RunAddr, r) + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5bb4e30 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/nabbat/23_kogorta_shotener + +go 1.20 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/spf13/pflag v1.0.5 + 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 + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d735a8b --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +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/README.md b/internal/app/README.md deleted file mode 100644 index ba14e13..0000000 --- a/internal/app/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# internal/app - -В данной директории будет содержаться имплементация вашего сервиса \ No newline at end of file diff --git a/internal/envirements/envirements.go b/internal/envirements/envirements.go new file mode 100644 index 0000000..5bd2e52 --- /dev/null +++ b/internal/envirements/envirements.go @@ -0,0 +1,23 @@ +package envirements + +import ( + "os" + "strings" +) + +type EnvConfig struct { + RunAddr string + ResultURL string +} + +// ParseEnv Get system environments +func ParseEnv() *EnvConfig { + env := &EnvConfig{} + env.RunAddr = os.Getenv("RUN_ADDR") + env.ResultURL = os.Getenv("SERVER_ADDRESS") + // парсим переданные серверу аргументы в зарегистрированные переменные + if !strings.HasPrefix(env.ResultURL, "http://") && env.ResultURL != "" { + env.ResultURL = "http://" + env.ResultURL + } + return env +} diff --git a/internal/envirements/envirements_test.go b/internal/envirements/envirements_test.go new file mode 100644 index 0000000..eb68724 --- /dev/null +++ b/internal/envirements/envirements_test.go @@ -0,0 +1,22 @@ +package envirements + +import ( + "reflect" + "testing" +) + +func TestParseEnv(t *testing.T) { + tests := []struct { + name string + want *EnvConfig + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseEnv(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseEnv() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go new file mode 100644 index 0000000..db3f284 --- /dev/null +++ b/internal/flags/flags.go @@ -0,0 +1,27 @@ +package flags + +import ( + flag "github.com/spf13/pflag" + "strings" +) + +// Flags структура для хранения настроек +type Flags struct { + RunAddr string + ResultURL string +} + +// ParseFlags обрабатывает аргументы командной строки +// и сохраняет их значения в соответствующих переменных +func ParseFlags() *Flags { + // Create a Config instance + flg := &Flags{} + flag.StringVarP(&flg.RunAddr, "a", "a", "localhost:8080", "Адрес запуска HTTP-сервера.") + flag.StringVarP(&flg.ResultURL, "b", "b", "http://localhost:8080", "Адрес результирующего сокращённого URL.") + // парсим переданные серверу аргументы в зарегистрированные переменные + flag.Parse() + if !strings.HasPrefix(flg.ResultURL, "http://") { + flg.ResultURL = "http://" + flg.ResultURL + } + return flg +} diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go new file mode 100644 index 0000000..40a1f67 --- /dev/null +++ b/internal/flags/flags_test.go @@ -0,0 +1,22 @@ +package flags + +import ( + "reflect" + "testing" +) + +func TestParseFlags(t *testing.T) { + tests := []struct { + name string + want *Flags + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseFlags(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseFlags() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/handlers/urlhandlers.go b/internal/handlers/urlhandlers.go new file mode 100644 index 0000000..f66df80 --- /dev/null +++ b/internal/handlers/urlhandlers.go @@ -0,0 +1,179 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "github.com/nabbat/23_kogorta_shotener/internal/liblog" + "github.com/nabbat/23_kogorta_shotener/internal/shotenermaker" + urlstorage "github.com/nabbat/23_kogorta_shotener/internal/storage" + "io" + "net/http" + "time" +) + +type RedirectHandler struct{} + +func (rh *RedirectHandler) HandleRedirect(storage *urlstorage.URLStorage, log liblog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "invalid request type", http.StatusBadRequest) + log.Info("invalid request type") + return + } + + // Получаем идентификатор из URL-пути + vars := mux.Vars(r) + shortURL := vars["idShortenURL"] + + // Получаем оригинальный URL + originalURL := storage.GetOriginalURL(shortURL) + + if originalURL == "" { + http.Error(w, "Ссылка не найдена", http.StatusBadRequest) + log.Info("Ссылка не найдена") + return + } + // Устанавливаем заголовок Location и возвращаем ответ с кодом 307 + w.Header().Set("Location", originalURL) + w.WriteHeader(http.StatusTemporaryRedirect) + + } +} + +type ShortenURLHandler struct{} + +func (sh *ShortenURLHandler) HandleShortenURL(storage *urlstorage.URLStorage, c *envirements.EnvConfig, log liblog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Читаем тело запроса (URL) + defer r.Body.Close() + urlBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Ошибка чтения запроса", http.StatusBadRequest) + return + } + + // Генерируем уникальный идентификатор сокращённого URL + shortURL := shotenermaker.GenerateID(urlBytes) + + // Добавляем соответствие в словарь + storage.AddURL(shortURL, string(urlBytes)) + + // Отправляем ответ с сокращённым URL + shortenedURL := fmt.Sprintf("%s/%s", c.ResultURL, shortURL) + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusCreated) + if _, err := io.WriteString(w, shortenedURL); err != nil { + log.Info("Ошибка записи ответа", err) + } + } +} + +func PanicHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} + +type ResponseLogger struct { + ResponseWriter http.ResponseWriter + StatusCode int +} + +type responseWriterWrapper struct { + http.ResponseWriter + status int + size int +} + +func (rw *responseWriterWrapper) Status() int { + return rw.status +} + +func (rw *responseWriterWrapper) Size() int { + return rw.size +} + +func (rw *responseWriterWrapper) Write(b []byte) (int, error) { + n, err := rw.ResponseWriter.Write(b) + rw.size += n + return n, err +} + +func (rw *responseWriterWrapper) WriteHeader(status int) { + rw.ResponseWriter.WriteHeader(status) + rw.status = status +} + +func ResponseLoggingMiddleware(log liblog.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rw := &responseWriterWrapper{ResponseWriter: w} + next.ServeHTTP(rw, r) + + status := rw.Status() + size := rw.Size() + + log.Info("Status: ", status, " | Size: ", size, " bytes") + }) + } +} + +func RequestLoggingMiddleware(log liblog.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + duration := time.Since(start) + + log.Info("Request: ", r.Method, " ", r.RequestURI, " | Time: ", duration) + }) + } +} + +func (sh *ShortenURLHandler) HandleShortenURLJSON(storage *urlstorage.URLStorage, c *envirements.EnvConfig, log liblog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Читаем JSON из тела запроса + type URLJSONRequest struct { + URL string `json:"url"` + } + var urlJSONRequest URLJSONRequest + var buf bytes.Buffer + + // читаем тело запроса + _, err := buf.ReadFrom(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // десериализуем JSON + if err = json.Unmarshal(buf.Bytes(), &urlJSONRequest); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Генерируем уникальный идентификатор сокращенного URL + shortURL := shotenermaker.GenerateID([]byte(urlJSONRequest.URL)) + + // Добавляем соответствие в словарь + storage.AddURL(shortURL, urlJSONRequest.URL) + + // Формируем JSON-ответ с сокращенным URL + response := map[string]string{"result": c.ResultURL + "/" + shortURL} + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Error("Ошибка записи JSON-ответа:", err) + http.Error(w, "Ошибка записи JSON-ответа", http.StatusInternalServerError) + } + } +} diff --git a/internal/liblog/main.go b/internal/liblog/main.go new file mode 100644 index 0000000..8d4ba3f --- /dev/null +++ b/internal/liblog/main.go @@ -0,0 +1,31 @@ +package liblog + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Logger interface { + Debug(args ...interface{}) + Info(args ...interface{}) + Warn(args ...interface{}) + Error(fields ...interface{}) +} + +func NewLogger() *zap.SugaredLogger { + config := zap.Config{ + Level: zap.NewAtomicLevelAt(zapcore.DebugLevel), + Development: true, + Encoding: "json", + EncoderConfig: zap.NewProductionEncoderConfig(), + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + } + + logger, _ := config.Build() + defer logger.Sync() + + sugar := logger.Sugar() + + return sugar +} diff --git a/internal/shotenermaker/makeurl.go b/internal/shotenermaker/makeurl.go new file mode 100644 index 0000000..fbde6e4 --- /dev/null +++ b/internal/shotenermaker/makeurl.go @@ -0,0 +1,13 @@ +package shotenermaker + +import "encoding/base64" + +// GenerateID Функция для генерации уникального идентификатора +func GenerateID(fullURL []byte) string { + encodedStr := base64.URLEncoding.EncodeToString(fullURL) + // Возвращаем первые 6 символов закодированной строки + if len(encodedStr) > 6 { + return encodedStr[:6] + } + return encodedStr +} diff --git a/internal/shotenermaker/makeurl_test.go b/internal/shotenermaker/makeurl_test.go new file mode 100644 index 0000000..43ba915 --- /dev/null +++ b/internal/shotenermaker/makeurl_test.go @@ -0,0 +1,23 @@ +package shotenermaker + +import "testing" + +func TestGenerateID(t *testing.T) { + type args struct { + fullURL []byte + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GenerateID(tt.args.fullURL); got != tt.want { + t.Errorf("GenerateID() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/storage/urlstorage.go b/internal/storage/urlstorage.go new file mode 100644 index 0000000..45067ae --- /dev/null +++ b/internal/storage/urlstorage.go @@ -0,0 +1,24 @@ +package storage + +type URLStorage struct { + urlMap map[string]string +} + +func NewURLStorage() *URLStorage { + storage := &URLStorage{ + urlMap: make(map[string]string), + } + // Adding Test Compliance + storage.AddURL("aHR0cH", "https://practicum.yandex.ru/") + return storage +} + +// AddURL adds a pair of shortened URL -> original URL +func (storage *URLStorage) AddURL(shortURL, originalURL string) { + storage.urlMap[shortURL] = originalURL +} + +// GetOriginalURL returns the original URL from the shortened URL +func (storage *URLStorage) GetOriginalURL(shortURL string) string { + return storage.urlMap[shortURL] +} diff --git a/internal/storage/urlstorage_test.go b/internal/storage/urlstorage_test.go new file mode 100644 index 0000000..0f6ab6a --- /dev/null +++ b/internal/storage/urlstorage_test.go @@ -0,0 +1,74 @@ +package storage + +import ( + "reflect" + "testing" +) + +func TestNewURLStorage(t *testing.T) { + tests := []struct { + name string + want *URLStorage + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewURLStorage(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewURLStorage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestURLStorage_AddURL(t *testing.T) { + type fields struct { + urlMap map[string]string + } + type args struct { + shortURL string + originalURL string + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + storage := &URLStorage{ + urlMap: tt.fields.urlMap, + } + storage.AddURL(tt.args.shortURL, tt.args.originalURL) + }) + } +} + +func TestURLStorage_GetOriginalURL(t *testing.T) { + type fields struct { + urlMap map[string]string + } + type args struct { + shortURL string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + storage := &URLStorage{ + urlMap: tt.fields.urlMap, + } + if got := storage.GetOriginalURL(tt.args.shortURL); got != tt.want { + t.Errorf("GetOriginalURL() = %v, want %v", got, tt.want) + } + }) + } +}