From b73e89374f3adeb5bf1418defff1827dcdc258d2 Mon Sep 17 00:00:00 2001 From: Win-10 Date: Wed, 27 Aug 2025 17:44:36 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cache/cache.go | 4 ++- handler/handler.go | 25 ++++++--------- main.go | 9 ++++-- manager/manager.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 manager/manager.go diff --git a/cache/cache.go b/cache/cache.go index fdab64c..e0dd811 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -7,6 +7,8 @@ import ( "time" ) +const popularLinksTTL = time.Hour + type LinksCache struct { rdb *redis.Client } @@ -18,7 +20,7 @@ func New(rdb *redis.Client) *LinksCache { } func (c *LinksCache) StoreLink(shortLink string, longLink string) error { - cmd := c.rdb.Set(shortLink, longLink, time.Hour) + cmd := c.rdb.Set(shortLink, longLink, popularLinksTTL) if cmd.Err() != nil { return fmt.Errorf("error StoreLink: %w", cmd.Err()) } diff --git a/handler/handler.go b/handler/handler.go index 3cf22ca..e26916f 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "github.com/berduk-dev/networks/cache" + "github.com/berduk-dev/networks/manager" "github.com/berduk-dev/networks/repo" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" @@ -129,28 +130,20 @@ func (h *Handler) CreateLink(c *gin.Context) { } func (h *Handler) Redirect(c *gin.Context) { + linksManager := manager.New(h.LinksRepository, h.LinksCache) + shortLink := c.Param("path") - // сначала посмотреть в кэше - longLink, err := h.LinksCache.GetLink(shortLink) + longLink, err := linksManager.Redirect(c, shortLink) if err != nil { - log.Println("error LinksCache.GetLink: ", err) - } - - if longLink == "" { - longLink, err = h.LinksRepository.GetLongByShort(c, shortLink) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - c.JSON(http.StatusNotFound, "Ссылка не найдена!") - return - } - log.Println("GetLongByShort error: ", err) - c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") - return + if errors.Is(err, pgx.ErrNoRows) { + c.JSON(http.StatusNotFound, "Ссылка не найдена!") } + log.Println("GetLongByShort error: ", err) + c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") } - err = h.LinksRepository.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) + err = linksManager.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) if err != nil { c.JSON(http.StatusInternalServerError, "Произошла ошибка. Попробуйте позже!") log.Println("error CreateAnalytics: ", err) diff --git a/main.go b/main.go index 5cc59a5..68f099c 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,10 @@ import ( "github.com/jackc/pgx/v5" ) -const cacheLinksInterval = time.Hour +const ( + cacheLinksInterval = time.Hour + popularLinksCount = 10 +) func main() { rdb := redis.NewClient(&redis.Options{ @@ -58,7 +61,7 @@ func main() { } }() - // --- CORS middleware --- + // --- CORS middleware ---- r.Use(func(c *gin.Context) { origin := c.Request.Header.Get("Origin") if origin == "" || origin == "null" { @@ -89,7 +92,7 @@ func main() { } func cachePopularLinks(linksRepository *repo.Repository, linksCache *cache.LinksCache) error { - links, err := linksRepository.GetPopularLinks(context.Background(), 10) + links, err := linksRepository.GetPopularLinks(context.Background(), popularLinksCount) if err != nil { return fmt.Errorf("error updateCache GetPopularLinks: %w", err) } diff --git a/manager/manager.go b/manager/manager.go new file mode 100644 index 0000000..4bf3398 --- /dev/null +++ b/manager/manager.go @@ -0,0 +1,79 @@ +package manager + +import ( + "context" + "errors" + "github.com/berduk-dev/networks/cache" + "github.com/berduk-dev/networks/repo" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5" + "log" + "net/http" +) + +type LinksManager struct { + LinksRepo *repo.Repository + LinksCache *cache.LinksCache +} + +func New(linksRepo *repo.Repository, linksCache *cache.LinksCache) LinksManager { + return LinksManager{ + LinksRepo: linksRepo, + LinksCache: linksCache, + } +} + +func (m *LinksManager) CreateLink(c *gin.Context, longLink string, shortLink string) error { + return m.LinksRepo.CreateLink(c, longLink, shortLink) +} + +func (m *LinksManager) CreateRedirect(c *gin.Context, longLink, shortLink, userAgent string) error { + return m.LinksRepo.CreateRedirect(c, longLink, shortLink, userAgent) +} + +func (m *LinksManager) GetLongByShort(c *gin.Context, shortLink string) (string, error) { + return m.LinksRepo.GetLongByShort(c, shortLink) +} + +func (m *LinksManager) GetShortByLong(c *gin.Context, longLink string) (string, error) { + return m.LinksRepo.GetShortByLong(c, longLink) +} + +func (m *LinksManager) GetCacheLongLink(shortLink string) (string, error) { + return m.LinksCache.GetLink(shortLink) +} + +func (m *LinksManager) GetPopularLinks(ctx context.Context, n int) ([]repo.LinkPair, error) { + return m.LinksRepo.GetPopularLinks(ctx, n) +} + +func (m *LinksManager) Redirect(c *gin.Context, shortLink string) (string, error) { + + // сначала посмотреть в кэше + longLink, err := m.GetCacheLongLink(shortLink) + if err != nil { + log.Println("error LinksCache.GetLink: ", err) + } + + if longLink == "" { + longLink, err = m.GetLongByShort(c, shortLink) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + c.JSON(http.StatusNotFound, "Ссылка не найдена!") + return "", pgx.ErrNoRows + } + log.Println("GetLongByShort error: ", err) + c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") + return "", err + } + } + + err = m.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) + if err != nil { + c.JSON(http.StatusInternalServerError, "Произошла ошибка. Попробуйте позже!") + log.Println("error CreateAnalytics: ", err) + } + + c.Redirect(http.StatusTemporaryRedirect, longLink) + return longLink, nil +} From 331dc6bd7dd5f010430368bd096db247aae8bbee Mon Sep 17 00:00:00 2001 From: Win-10 Date: Wed, 27 Aug 2025 18:35:48 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handler/handler.go | 21 ++++++++++----------- main.go | 1 + 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/handler/handler.go b/handler/handler.go index e26916f..566823d 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -21,6 +21,7 @@ const ( type Handler struct { LinksRepository *repo.Repository LinksCache *cache.LinksCache + LinksManager *manager.LinksManager } type CreateLinkRequest struct { @@ -35,6 +36,13 @@ func New(linksRepo *repo.Repository, linksCache *cache.LinksCache) Handler { } } +func IsLinkValid(customLink string) bool { + if customLink != "" && len(customLink) == ShortLinkLength { + return true + } + return false +} + func (h *Handler) CreateLink(c *gin.Context) { var req CreateLinkRequest err := c.ShouldBindJSON(&req) @@ -130,11 +138,9 @@ func (h *Handler) CreateLink(c *gin.Context) { } func (h *Handler) Redirect(c *gin.Context) { - linksManager := manager.New(h.LinksRepository, h.LinksCache) - shortLink := c.Param("path") - longLink, err := linksManager.Redirect(c, shortLink) + longLink, err := h.LinksManager.Redirect(c, shortLink) if err != nil { if errors.Is(err, pgx.ErrNoRows) { c.JSON(http.StatusNotFound, "Ссылка не найдена!") @@ -143,7 +149,7 @@ func (h *Handler) Redirect(c *gin.Context) { c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") } - err = linksManager.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) + err = h.LinksManager.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) if err != nil { c.JSON(http.StatusInternalServerError, "Произошла ошибка. Попробуйте позже!") log.Println("error CreateAnalytics: ", err) @@ -162,10 +168,3 @@ func (h *Handler) Analytics(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"redirects": redirects, "total_count": len(redirects)}) } - -func IsLinkValid(customLink string) bool { - if customLink != "" && len(customLink) == ShortLinkLength { - return true - } - return false -} diff --git a/main.go b/main.go index 68f099c..8db13ff 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { linksRepository := repo.New(conn) linksCache := cache.New(rdb) linksHandler := handler.New(&linksRepository, linksCache) + //linksManager := manager.New(&linksRepository, linksCache) go func() { // TODO: Вынести из main.go в другое место err := cachePopularLinks(&linksRepository, linksCache) From ab2c879e410c99e37f78c3450a4773e96ac1ee83 Mon Sep 17 00:00:00 2001 From: Win-10 Date: Fri, 29 Aug 2025 15:12:20 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20Redirect=20?= =?UTF-8?q?=D0=B8=20GetAnalytics=20=D0=B2=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- errors/errors.go | 10 +++ handler/handler.go | 154 ++++++++++++--------------------------------- main.go | 9 ++- manager/manager.go | 90 ++++++++++++-------------- repo/repo.go | 86 +++++++++++++------------ service/service.go | 153 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 292 insertions(+), 210 deletions(-) create mode 100644 errors/errors.go create mode 100644 service/service.go diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..f64ae6b --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,10 @@ +package errors + +import "errors" + +var ( + ErrorLinkAlreadyExists = errors.New("error link is already exists") + ErrorLinkTooShort = errors.New("error link is too short") + ErrorInvalidSymbolInLink = errors.New("error invalid symbol in link") + ErrorLinkNotFound = errors.New("error link not found") +) diff --git a/handler/handler.go b/handler/handler.go index 566823d..cfdd4d5 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1,49 +1,38 @@ package handler import ( - "crypto/rand" - "encoding/base64" "errors" - "github.com/berduk-dev/networks/cache" + errors2 "github.com/berduk-dev/networks/errors" "github.com/berduk-dev/networks/manager" - "github.com/berduk-dev/networks/repo" + "github.com/berduk-dev/networks/service" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5" "log" "net/http" ) const ( - HostURL = "127.0.0.1:8080/" - ShortLinkLength = 6 + HostURL = "127.0.0.1:8080/" ) type Handler struct { - LinksRepository *repo.Repository - LinksCache *cache.LinksCache - LinksManager *manager.LinksManager + linksManager manager.LinksManager + linksService service.LinksService } type CreateLinkRequest struct { - Link string `json:"link"` - Custom string `json:"custom"` + Link string `json:"link"` + CustomShortLink *string `json:"customshortlink"` } -func New(linksRepo *repo.Repository, linksCache *cache.LinksCache) Handler { +func New(linksManager manager.LinksManager, linksService service.LinksService) Handler { return Handler{ - LinksRepository: linksRepo, - LinksCache: linksCache, + linksManager: linksManager, + linksService: linksService, } } -func IsLinkValid(customLink string) bool { - if customLink != "" && len(customLink) == ShortLinkLength { - return true - } - return false -} - func (h *Handler) CreateLink(c *gin.Context) { + // анмаршаллинг var req CreateLinkRequest err := c.ShouldBindJSON(&req) if err != nil { @@ -51,120 +40,57 @@ func (h *Handler) CreateLink(c *gin.Context) { return } - // Проверка на наличие длинной ссылки в БД - - shortLink, err := h.LinksRepository.GetShortByLong(c, req.Link) - if err != nil && !errors.Is(err, pgx.ErrNoRows) { - log.Println("Ошибка при проверке long_link в БД: ", err) - c.JSON(http.StatusInternalServerError, "Что-то пошло не так. Попробуйте позже!") - return - } - if err == nil { - c.JSON(http.StatusOK, gin.H{ - "short": HostURL + shortLink, - "long": req.Link, - }) - return - } - - // Кастомная ссылка - - if IsLinkValid(req.Custom) { - for _, r := range []rune(req.Custom) { - if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') { - c.JSON(http.StatusBadRequest, "Кастомная ссылка содержит недопустимые символы.") - return - } - } - - isExists, err := h.LinksRepository.IsShortExists(c, req.Custom) - if !isExists { - if err != nil { - log.Println("Произошла ошибка: ", err) - c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") - return - } - - err = h.LinksRepository.CreateLink(c, req.Link, req.Custom) - if err != nil { - log.Println("Ошибка при занесении ссылок в БД: ", err) - c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") - return - } - - c.JSON(http.StatusOK, gin.H{ - "short": HostURL + req.Custom, - "long": req.Link, - }) - return - } - c.JSON(http.StatusConflict, "Такая короткая ссылка уже существует! Попробуйте другую.") - return - } - - // Генерация короткой ссылки и проверка на её наличие в БД - for { - b := make([]byte, ShortLinkLength) - _, err = rand.Read(b) - if err != nil { - log.Println("Ошибка при генерации короткой ссылки: ", err) - c.JSON(http.StatusInternalServerError, "Ошибка во время генерации ссылки") - return - } - shortLink = base64.URLEncoding.EncodeToString(b)[:ShortLinkLength] - - isExist, err := h.LinksRepository.IsShortExists(c, shortLink) - if err != nil { - c.JSON(http.StatusInternalServerError, "Произошла ошибка БД, попробуйте позже!") + shortLink, err := h.linksService.CreateShortLink(c, req.Link, req.CustomShortLink) + if err != nil { + if errors.Is(err, errors2.ErrorLinkAlreadyExists) || + errors.Is(err, errors2.ErrorLinkTooShort) || + errors.Is(err, errors2.ErrorInvalidSymbolInLink) { + c.JSON(http.StatusBadRequest, err) return } - if !isExist { - break - } - } - // Добавляем в БД - err = h.LinksRepository.CreateLink(c, req.Link, shortLink) - if err != nil { - c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже") - return + log.Printf("error linksService.CreateShortLink: %v", err) + c.JSON(http.StatusInternalServerError, "Ошибка! Попробуйте позже!") } + // ответ юзеру c.JSON(http.StatusOK, gin.H{ "short": HostURL + shortLink, "long": req.Link, }) - return } func (h *Handler) Redirect(c *gin.Context) { shortLink := c.Param("path") - longLink, err := h.LinksManager.Redirect(c, shortLink) + err := h.linksService.Redirect(c, shortLink) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - c.JSON(http.StatusNotFound, "Ссылка не найдена!") + if errors.Is(err, errors2.ErrorLinkNotFound) { + c.JSON(http.StatusNotFound, "link not found") + return } log.Println("GetLongByShort error: ", err) c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") + return } - - err = h.LinksManager.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) - if err != nil { - c.JSON(http.StatusInternalServerError, "Произошла ошибка. Попробуйте позже!") - log.Println("error CreateAnalytics: ", err) - } - - c.Redirect(http.StatusTemporaryRedirect, longLink) - return } -func (h *Handler) Analytics(c *gin.Context) { - redirects, err := h.LinksRepository.GetRedirects(c) +func (h *Handler) GetAnalytics(c *gin.Context) { + shortLink := c.Param("short_url") + + redirects, err := h.linksService.GetAnalytics(c, shortLink) if err != nil { - log.Println("Ошибка получения аналитики: ", err) - c.JSON(http.StatusInternalServerError, "Ошибка при получении аналитики") + if errors.Is(err, errors2.ErrorLinkNotFound) { + c.JSON(http.StatusNotFound, "link not found") + return + } + log.Println("GetLongByShort error: ", err) + c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") // 500 return } - c.JSON(http.StatusOK, gin.H{"redirects": redirects, "total_count": len(redirects)}) + + c.JSON(http.StatusOK, gin.H{ + "redirects": redirects, + "total_count": len(redirects), + }) } diff --git a/main.go b/main.go index 8db13ff..877379b 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/berduk-dev/networks/cache" "github.com/berduk-dev/networks/handler" + "github.com/berduk-dev/networks/manager" "github.com/berduk-dev/networks/repo" + "github.com/berduk-dev/networks/service" "time" "log" @@ -44,8 +46,9 @@ func main() { linksRepository := repo.New(conn) linksCache := cache.New(rdb) - linksHandler := handler.New(&linksRepository, linksCache) - //linksManager := manager.New(&linksRepository, linksCache) + linksManager := manager.New(linksCache, &linksRepository) + linksService := service.New(linksManager) + linksHandler := handler.New(linksManager, *linksService) go func() { // TODO: Вынести из main.go в другое место err := cachePopularLinks(&linksRepository, linksCache) @@ -86,7 +89,7 @@ func main() { // API-роуты: r.POST("/shorten", linksHandler.CreateLink) r.POST("/shorten/:custom", linksHandler.CreateLink) // можно убрать, если перешёл на JSON-поле "custom" - r.GET("/analytics/:short_url", linksHandler.Analytics) + r.GET("/analytics/:short_url", linksHandler.GetAnalytics) r.GET("/:path", linksHandler.Redirect) r.Run() diff --git a/manager/manager.go b/manager/manager.go index 4bf3398..da8c802 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -3,77 +3,69 @@ package manager import ( "context" "errors" + "fmt" "github.com/berduk-dev/networks/cache" + errors2 "github.com/berduk-dev/networks/errors" "github.com/berduk-dev/networks/repo" - "github.com/gin-gonic/gin" + //"github.com/berduk-dev/networks/service" "github.com/jackc/pgx/v5" - "log" - "net/http" ) type LinksManager struct { - LinksRepo *repo.Repository - LinksCache *cache.LinksCache + cache *cache.LinksCache + repo *repo.Repository } -func New(linksRepo *repo.Repository, linksCache *cache.LinksCache) LinksManager { +func New(cache *cache.LinksCache, repo *repo.Repository) LinksManager { return LinksManager{ - LinksRepo: linksRepo, - LinksCache: linksCache, + repo: repo, + cache: cache, } } -func (m *LinksManager) CreateLink(c *gin.Context, longLink string, shortLink string) error { - return m.LinksRepo.CreateLink(c, longLink, shortLink) +func (m *LinksManager) IsShortExists(ctx context.Context, shortLink string) (bool, error) { + return m.repo.IsShortExists(ctx, shortLink) } +func (m *LinksManager) GetLongByShort(ctx context.Context, shortLink string) (string, error) { + // сначала ищем в кэше + longLink, err := m.cache.GetLink(shortLink) + if err != nil { + return "", fmt.Errorf("error LinksCache.GetLink: %w", err) + } -func (m *LinksManager) CreateRedirect(c *gin.Context, longLink, shortLink, userAgent string) error { - return m.LinksRepo.CreateRedirect(c, longLink, shortLink, userAgent) -} + // если нашли, возвращаем + if longLink != "" { + return longLink, nil + } -func (m *LinksManager) GetLongByShort(c *gin.Context, shortLink string) (string, error) { - return m.LinksRepo.GetLongByShort(c, shortLink) -} + // идем в бд + longLink, err = m.repo.GetLongByShort(ctx, shortLink) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return "", errors2.ErrorLinkNotFound + } + return "", fmt.Errorf("error repo.GetLongByShort: %w", err) + } -func (m *LinksManager) GetShortByLong(c *gin.Context, longLink string) (string, error) { - return m.LinksRepo.GetShortByLong(c, longLink) + return longLink, nil } -func (m *LinksManager) GetCacheLongLink(shortLink string) (string, error) { - return m.LinksCache.GetLink(shortLink) +func (m *LinksManager) CreateLink(ctx context.Context, longLink string, shortLink string) error { + return m.repo.CreateLink(ctx, longLink, shortLink) } -func (m *LinksManager) GetPopularLinks(ctx context.Context, n int) ([]repo.LinkPair, error) { - return m.LinksRepo.GetPopularLinks(ctx, n) +func (m *LinksManager) StoreRedirect(ctx context.Context, params repo.StoreRedirectParams) error { + return m.repo.StoreRedirect(ctx, params) } -func (m *LinksManager) Redirect(c *gin.Context, shortLink string) (string, error) { - - // сначала посмотреть в кэше - longLink, err := m.GetCacheLongLink(shortLink) - if err != nil { - log.Println("error LinksCache.GetLink: ", err) - } - - if longLink == "" { - longLink, err = m.GetLongByShort(c, shortLink) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - c.JSON(http.StatusNotFound, "Ссылка не найдена!") - return "", pgx.ErrNoRows - } - log.Println("GetLongByShort error: ", err) - c.JSON(http.StatusInternalServerError, "Произошла ошибка, попробуйте позже!") - return "", err - } - } +func (m *LinksManager) GetRedirectsByShortLink(ctx context.Context, shortLink string) ([]repo.Redirect, error) { + return m.repo.GetRedirectsByShortLink(ctx, shortLink) +} - err = m.CreateRedirect(c, longLink, shortLink, c.Request.UserAgent()) - if err != nil { - c.JSON(http.StatusInternalServerError, "Произошла ошибка. Попробуйте позже!") - log.Println("error CreateAnalytics: ", err) - } +func (m *LinksManager) GetShortByLong(ctx context.Context, longLink string) (string, error) { + return m.repo.GetShortByLong(ctx, longLink) +} - c.Redirect(http.StatusTemporaryRedirect, longLink) - return longLink, nil +func (m *LinksManager) GetPopularLinks(ctx context.Context, n int) ([]repo.LinkPair, error) { + return m.repo.GetPopularLinks(ctx, n) } diff --git a/repo/repo.go b/repo/repo.go index 2906c2d..4baf492 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -4,10 +4,7 @@ import ( "context" "errors" "fmt" - "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" - "log" - "net/http" "time" ) @@ -21,6 +18,12 @@ func New(db *pgx.Conn) Repository { } } +type StoreRedirectParams struct { + UserAgent string + LongLink string + ShortLink string +} + type Redirect struct { ID int `json:"id"` LongLink string `json:"long_link"` @@ -34,79 +37,74 @@ type LinkPair struct { Long string } -func (r *Repository) CreateLink(c *gin.Context, longLink string, shortLink string) error { - _, err := r.db.Exec(c, "INSERT INTO links (long_link, short_link) VALUES ($1, $2)", longLink, shortLink) +func (r *Repository) IsShortExists(ctx context.Context, shortLink string) (bool, error) { + var existingShortLink string + err := r.db.QueryRow(ctx, "SELECT short_link FROM links WHERE short_link = $1", shortLink).Scan(&existingShortLink) if err != nil { - return err + if errors.Is(err, pgx.ErrNoRows) { + return false, nil + } + return false, err } - - return nil + return true, nil } -func (r *Repository) CreateRedirect(c *gin.Context, longLink, shortLink, userAgent string) error { - _, err := r.db.Exec(c, - "INSERT INTO redirects (long_link, short_link, user_agent) VALUES ($1, $2, $3)", - longLink, shortLink, userAgent) +func (r *Repository) CreateLink(ctx context.Context, longLink string, shortLink string) error { + _, err := r.db.Exec(ctx, "INSERT INTO links (long_link, short_link) VALUES ($1, $2)", longLink, shortLink) if err != nil { return err } - return nil -} -func (r *Repository) GetLongByShort(c *gin.Context, shortLink string) (string, error) { - var longLink string - err := r.db.QueryRow(c, "SELECT long_link FROM links WHERE short_link = $1", shortLink).Scan(&longLink) - if err != nil { - return "", err - } - return longLink, nil + return nil } -func (r *Repository) GetShortByLong(c *gin.Context, longLink string) (string, error) { - var shortLink string - err := r.db.QueryRow(c, "SELECT short_link FROM links WHERE long_link = $1", longLink).Scan(&shortLink) - if err != nil { - return "", err - } - return shortLink, nil +func (r *Repository) StoreRedirect(ctx context.Context, params StoreRedirectParams) error { + _, err := r.db.Exec(ctx, + "INSERT INTO redirects (long_link, short_link, user_agent) VALUES ($1, $2, $3)", + params.LongLink, + params.ShortLink, + params.UserAgent, + ) + return err } -func (r *Repository) GetRedirects(c *gin.Context) ([]Redirect, error) { - shortLink := c.Param("short_url") - rows, err := r.db.Query(c, "SELECT id, long_link, short_link, user_agent, created_at FROM redirects WHERE short_link = $1", shortLink) +func (r *Repository) GetRedirectsByShortLink(ctx context.Context, shortLink string) ([]Redirect, error) { + rows, err := r.db.Query(ctx, "SELECT id, long_link, short_link, user_agent, created_at FROM redirects WHERE short_link = $1", shortLink) var redirects []Redirect for rows.Next() { - var redirect Redirect err = rows.Scan(&redirect.ID, &redirect.LongLink, &redirect.ShortLink, &redirect.UserAgent, &redirect.CreatedAt) if err != nil { - c.JSON(http.StatusInternalServerError, "Ошибка при выводе аналитики!") - log.Println("Ошибка при выводе аналитики: ", err) return nil, err } redirects = append(redirects, redirect) } if err = rows.Err(); err != nil { - log.Println("Ошибка БД Query: ", err) - c.JSON(http.StatusInternalServerError, "Произошла ошибка! Попробуйте позже!") return nil, err } + return redirects, nil } -func (r *Repository) IsShortExists(c *gin.Context, shortLink string) (bool, error) { - var existingShortLink string - err := r.db.QueryRow(c, "SELECT short_link FROM links WHERE short_link = $1", shortLink).Scan(&existingShortLink) +func (r *Repository) GetLongByShort(ctx context.Context, shortLink string) (string, error) { + var longLink string + err := r.db.QueryRow(ctx, "SELECT long_link FROM links WHERE short_link = $1", shortLink).Scan(&longLink) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return false, nil - } - return false, err + return "", err } - return true, nil + return longLink, nil +} + +func (r *Repository) GetShortByLong(ctx context.Context, longLink string) (string, error) { + var shortLink string + err := r.db.QueryRow(ctx, "SELECT short_link FROM links WHERE long_link = $1", longLink).Scan(&shortLink) + if err != nil { + return "", err + } + return shortLink, nil } func (r *Repository) GetPopularLinks(ctx context.Context, n int) ([]LinkPair, error) { diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..0bb372d --- /dev/null +++ b/service/service.go @@ -0,0 +1,153 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + errors2 "github.com/berduk-dev/networks/errors" + "github.com/berduk-dev/networks/manager" + "github.com/berduk-dev/networks/repo" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5" + "log" + "net/http" + "unicode" +) + +const ShortLinkLength = 6 + +type LinksService struct { + linksManager manager.LinksManager +} + +func New(linksManager manager.LinksManager) *LinksService { + return &LinksService{ + linksManager: linksManager, + } +} + +func (s *LinksService) CreateShortLink(ctx context.Context, longLink string, customShortLink *string) (string, error) { + // проверка на наличие длинной ссылки в БД + existingShortLink, err := s.linksManager.GetShortByLong(ctx, longLink) + if err == nil { + return existingShortLink, nil + } + + if !errors.Is(err, pgx.ErrNoRows) { + return "", fmt.Errorf("linksManager.GetShortByLong: %w", err) // 500 + } + + // создание по кастомной ссылке + if customShortLink != nil { + err := validateShortLink(*customShortLink) + if err != nil { + return "", err // 400 + } + + isExists, err := s.linksManager.IsShortExists(ctx, *customShortLink) + if err != nil { + return "", fmt.Errorf("linksManager.IsShortExists: %w", err) // 500 + } + + if isExists { + return "", errors2.ErrorLinkAlreadyExists // 400 + } + + err = s.linksManager.CreateLink(ctx, longLink, *customShortLink) + if err != nil { + return "", fmt.Errorf("linksManager.CreateLink: %w", err) // 500 + } + + return *customShortLink, nil + } + + // генерация случайной короткой ссылки и проверка на её наличие в БД + shortLink := "" + for { + b := make([]byte, ShortLinkLength) + _, err = rand.Read(b) + if err != nil { + log.Println("Ошибка при генерации короткой ссылки: ", err) + } + shortLink = base64.URLEncoding.EncodeToString(b)[:ShortLinkLength] + + isExist, err := s.linksManager.IsShortExists(ctx, shortLink) + if err != nil { + return "", fmt.Errorf("linksManager.IsShortExists: %w", err) // 500 + } + + if !isExist { + break + } + } + + // добавляем в БД которкую ссылку + err = s.linksManager.CreateLink(ctx, longLink, shortLink) + if err != nil { + return "", fmt.Errorf("linksManager.CreateLink: %w", err) // 500 + } + + return shortLink, nil +} + +func (s *LinksService) Redirect(c *gin.Context, shortLink string) error { + + longLink, err := s.linksManager.GetLongByShort(c, shortLink) + if err != nil { + if errors.Is(err, errors.New("error link not found")) { + return errors2.ErrorLinkNotFound + } + return err + } + + err = s.linksManager.StoreRedirect(c, repo.StoreRedirectParams{ + UserAgent: c.GetHeader("User-Agent"), + LongLink: longLink, + ShortLink: shortLink, + }) + if err != nil { + return err + } + + c.Redirect(http.StatusTemporaryRedirect, longLink) + return nil +} + +func (s *LinksService) GetAnalytics(c *gin.Context, shortLink string) ([]repo.Redirect, error) { + + _, err := s.linksManager.GetLongByShort(c, shortLink) + if err != nil { + if errors.Is(err, errors.New("error link not found")) { + return nil, errors2.ErrorLinkNotFound + } + return nil, err + } + + redirects, err := s.linksManager.GetRedirectsByShortLink(c, shortLink) + if err != nil { + log.Println("Ошибка получения аналитики: ", err) + c.JSON(http.StatusInternalServerError, "Ошибка при получении аналитики") + return nil, err + } + + c.JSON(http.StatusOK, gin.H{ + "redirects": redirects, + "total_count": len(redirects), + }) + return redirects, nil +} + +func validateShortLink(link string) error { + if len(link) < ShortLinkLength { + return errors2.ErrorLinkTooShort + } + + for _, r := range link { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + return errors2.ErrorInvalidSymbolInLink + } + } + return nil +}