diff --git a/cmd/config/config.go b/cmd/config/config.go new file mode 100644 index 0000000..a6080bc --- /dev/null +++ b/cmd/config/config.go @@ -0,0 +1,51 @@ +package config + +import ( + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "github.com/nabbat/23_kogorta_shotener/internal/flags" + "github.com/nabbat/23_kogorta_shotener/internal/liblog" + "github.com/nabbat/23_kogorta_shotener/internal/storage" + "github.com/nabbat/23_kogorta_shotener/internal/storage/filestorage" + urlstorage "github.com/nabbat/23_kogorta_shotener/internal/storage/internalstorage" +) + +type Config struct { + RunAddr string + ResultURL string + FileName string +} + +func SetEnv(log liblog.Logger) (storage.Storage, *Config, error) { + fl := flags.ParseFlags() + en := envirements.ParseEnv() + c := &Config{} + + 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 + } + + if en.FileName != "" { + c.FileName = en.FileName + st, _ := filestorage.NewFileStorage(c.FileName, log, &filestorage.NewFile{}) + + return st, c, nil + } + + if fl.FileName != "" { + c.FileName = "/tmp/" + fl.FileName + st, _ := filestorage.NewFileStorage(c.FileName, log, &filestorage.NewFile{}) + + return st, c, nil + } + + st := urlstorage.NewURLStorage() + return st, c, nil +} diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 38dd16d..50f89a1 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,3 +1,49 @@ 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" + "github.com/nabbat/23_kogorta_shotener/internal/middlewares" + "net/http" +) + +func main() { + //TODO make test MF!!! 🤬 + + // Инициализируем логер + log := liblog.NewLogger() + + // Получаем переменные если они есть + storage, c, err := config.SetEnv(log) + if err != nil { + log.Info(err) + } + + defer storage.Close() + + // Создаем хэндлеры + redirectHandler := &handlers.RedirectHandler{} + shortenURLHandler := &handlers.ShortenURLHandler{} + + r := mux.NewRouter() + r.Use(middlewares.GzipMiddleware(log)) + // Регистрируем middleware для логирования запросов + r.Use(middlewares.RequestLoggingMiddleware(log)) + // Регистрируем middleware для логирования ответов + r.Use(middlewares.ResponseLoggingMiddleware(log)) + r.Use(middlewares.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..9c13e4a --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +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 + go.uber.org/zap v1.25.0 +) + +require ( + github.com/stretchr/testify v1.8.4 // indirect + go.uber.org/multierr v1.10.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e654c33 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +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/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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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..d6a7d11 --- /dev/null +++ b/internal/envirements/envirements.go @@ -0,0 +1,25 @@ +package envirements + +import ( + "os" + "strings" +) + +type EnvConfig struct { + RunAddr string + ResultURL string + FileName string +} + +// ParseEnv Get system environments +func ParseEnv() *EnvConfig { + env := &EnvConfig{} + env.RunAddr = os.Getenv("RUN_ADDR") + env.ResultURL = os.Getenv("SERVER_ADDRESS") + env.FileName = os.Getenv("FILE_STORAGE_PATH") + // парсим переданные серверу аргументы в зарегистрированные переменные + if !strings.HasPrefix(env.ResultURL, "http://") && env.ResultURL != "" { + env.ResultURL = "http://" + env.ResultURL + } + return env +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go new file mode 100644 index 0000000..9724814 --- /dev/null +++ b/internal/flags/flags.go @@ -0,0 +1,29 @@ +package flags + +import ( + flag "github.com/spf13/pflag" + "strings" +) + +// Flags структура для хранения настроек +type Flags struct { + RunAddr string + ResultURL string + FileName 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.StringVarP(&flg.FileName, "f", "f", "short-url-db.json", "Имя файла для записи на диск. пустое значение отключает функцию записи на диск") + // парсим переданные серверу аргументы в зарегистрированные переменные + flag.Parse() + if !strings.HasPrefix(flg.ResultURL, "http://") { + flg.ResultURL = "http://" + flg.ResultURL + } + return flg +} diff --git a/internal/handlers/urlhandlers.go b/internal/handlers/urlhandlers.go new file mode 100644 index 0000000..95a3611 --- /dev/null +++ b/internal/handlers/urlhandlers.go @@ -0,0 +1,115 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "github.com/nabbat/23_kogorta_shotener/cmd/config" + "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" +) + +type RedirectHandler struct{} + +func (rh *RedirectHandler) HandleRedirect(storage urlstorage.Storage, 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, err := storage.GetOriginalURL(shortURL) + if err != nil { + log.Info(err) + } + + if originalURL == "" { + http.Error(w, "Ссылка не найдена", http.StatusBadRequest) + log.Info("Ссылка не найдена") + return + } + // Устанавливаем заголовок Location и возвращаем ответ с кодом 307 + w.Header().Set("Location", originalURL) + w.WriteHeader(http.StatusTemporaryRedirect) + log.Info("Location set:" + originalURL) + + } +} + +type ShortenURLHandler struct{} + +func (sh *ShortenURLHandler) HandleShortenURL(storage urlstorage.Storage, c *config.Config, 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 (sh *ShortenURLHandler) HandleShortenURLJSON(storage urlstorage.Storage, c *config.Config, 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/middlewares/middlewares.go b/internal/middlewares/middlewares.go new file mode 100644 index 0000000..e127cb9 --- /dev/null +++ b/internal/middlewares/middlewares.go @@ -0,0 +1,179 @@ +package middlewares + +import ( + "compress/gzip" + "github.com/gorilla/mux" + "github.com/nabbat/23_kogorta_shotener/internal/liblog" + "io" + "net/http" + "strings" + "time" +) + +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 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) + }) +} + +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") + }) + } +} + +// 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 < 400 { + 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() +} + +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) + }) + } +} + +// GzipMiddleware Делает сжатие и распаковку если это возможно +func GzipMiddleware(log liblog.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // по умолчанию устанавливаем оригинальный http.ResponseWriter как тот, + // который будем передавать следующей функции + ow := w + + // проверяем, что клиент умеет получать от сервера сжатые данные в формате gzip + acceptEncoding := r.Header.Get("Accept-Encoding") + supportsGzip := strings.Contains(acceptEncoding, "gzip") + if supportsGzip { + // оборачиваем оригинальный http.ResponseWriter новым с поддержкой сжатия + cw := newCompressWriter(w) + // меняем оригинальный http.ResponseWriter на новый + ow = cw + // не забываем отправить клиенту все сжатые данные после завершения middleware + defer cw.Close() + log.Info("COMPRESSION SUPPORTED") + } else { + log.Info("COMPRESSION UNSUPPORTED") + } + // проверяем, что клиент отправил серверу сжатые данные в формате gzip + contentEncoding := r.Header.Get("Content-Encoding") + sendsGzip := strings.Contains(contentEncoding, "gzip") + if sendsGzip { + // оборачиваем тело запроса в io.Reader с поддержкой декомпрессии + cr, err := newCompressReader(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + // меняем тело запроса на новое + r.Body = cr + defer cr.Close() + } + // передаём управление хендлеру + next.ServeHTTP(ow, r) + }) + } +} diff --git a/internal/shotenermaker/makeurl.go b/internal/shotenermaker/makeurl.go new file mode 100644 index 0000000..a0807bf --- /dev/null +++ b/internal/shotenermaker/makeurl.go @@ -0,0 +1,20 @@ +package shotenermaker + +import ( + "crypto/sha1" + "encoding/hex" +) + +// GenerateID Функция для генерации уникального идентификатора +func GenerateID(fullURL []byte) string { + h := sha1.New() + h.Write(fullURL) + hashBytes := h.Sum(nil) // TODO Добавить соль + encodedStr := hex.EncodeToString(hashBytes) + + // Возвращаем первые 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/filestorage/filestorage.go b/internal/storage/filestorage/filestorage.go new file mode 100644 index 0000000..82e8496 --- /dev/null +++ b/internal/storage/filestorage/filestorage.go @@ -0,0 +1,122 @@ +package filestorage + +import ( + "bufio" + "encoding/json" + "errors" + "github.com/nabbat/23_kogorta_shotener/internal/liblog" + "os" +) + +type Closer interface { + CloseFile() +} + +type URLDataJSON struct { + UUID int `json:"uuid"` + ShortURL string `json:"short_url"` + OriginalURL string `json:"original_url"` +} + +type NewFile struct { + os.File + i int +} + +// NewFileStorage Создает или подключает существующий файл +func NewFileStorage(filename string, log liblog.Logger, storage *NewFile) (*NewFile, error) { + // TRYING TO OPEN OR CREATE A FILE + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0777) + if err != nil { + log.Info("Failed to open or create the file: %v", err) + return nil, err + } + + // Прочитать последнее значение UUID + lastUUID, err := readLastUUID(file) + if err != nil { + log.Info("Failed to read the last UUID: %v", err) + storage.i = 0 + storage.File = *file + return storage, err + } + + storage.i = lastUUID + storage.File = *file + return storage, nil +} + +// readLastUUID FUNCTION TO READ THE LATEST UUID FROM A FILE +func readLastUUID(file *os.File) (int, error) { + var lastUUID int + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + var urlData URLDataJSON + err := json.Unmarshal([]byte(line), &urlData) + if err != nil { + return 0, err + } + lastUUID = urlData.UUID + } + + if err := scanner.Err(); err != nil { + return 0, err + } + + return lastUUID, nil +} + +// AddURL adds a pair of shortened URL -> original URL +func (storage *NewFile) AddURL(shortURL, originalURL string) error { + storage.i++ + u := URLDataJSON{ + UUID: storage.i, + ShortURL: shortURL, + OriginalURL: originalURL, + } + findURL, err := storage.GetOriginalURL(shortURL) + if findURL == "" { + d, err := json.Marshal(u) + if err != nil { + return err + } + + d = append(d, '\n') + _, err = storage.File.Write(d) + if err != nil { + return err + } + storage.File.Sync() + return nil + } + return err +} + +// GetOriginalURL returns the original URL from the shortened URL +func (storage *NewFile) GetOriginalURL(shortURL string) (string, error) { + filename := storage.Name() + f, _ := os.OpenFile(filename, os.O_RDONLY, 0777) + defer f.Close() + s := bufio.NewScanner(f) + + for s.Scan() { + buff := s.Bytes() + u := URLDataJSON{} + err := json.Unmarshal(buff, &u) + if err != nil { + continue + } + + if u.ShortURL == shortURL { + return u.OriginalURL, nil + } + } + return "", errors.New("short url not found") +} + +// Close закрывает файл +func (storage *NewFile) Close() { + storage.File.Close() +} diff --git a/internal/storage/internalstorage/internalstorage.go b/internal/storage/internalstorage/internalstorage.go new file mode 100644 index 0000000..f526adb --- /dev/null +++ b/internal/storage/internalstorage/internalstorage.go @@ -0,0 +1,28 @@ +package internalstorage + +type InternalURLStorage struct { + urlMap map[string]string +} + +func NewURLStorage() *InternalURLStorage { + storage := &InternalURLStorage{ + urlMap: make(map[string]string), + } + return storage +} + +// AddURL adds a pair of shortened URL -> original URL +func (storage *InternalURLStorage) AddURL(shortURL, originalURL string) error { + storage.urlMap[shortURL] = originalURL + return nil +} + +// GetOriginalURL returns the original URL from the shortened URL +func (storage *InternalURLStorage) GetOriginalURL(shortURL string) (string, error) { + return storage.urlMap[shortURL], nil +} + +// Close returns the original URL from the shortened URL +func (storage *InternalURLStorage) Close() { + // May be late +} diff --git a/internal/storage/main.go b/internal/storage/main.go new file mode 100644 index 0000000..ace0cda --- /dev/null +++ b/internal/storage/main.go @@ -0,0 +1,7 @@ +package storage + +type Storage interface { + AddURL(shortURL, originalURL string) error + GetOriginalURL(shortURL string) (string, error) + Close() +} diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..215d808 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-go:latest