Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"time"
)

const popularLinksTTL = time.Hour

type LinksCache struct {
rdb *redis.Client
}
Expand All @@ -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())
}
Expand Down
10 changes: 10 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
@@ -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")
)
164 changes: 41 additions & 123 deletions handler/handler.go
Original file line number Diff line number Diff line change
@@ -1,178 +1,96 @@
package handler

import (
"crypto/rand"
"encoding/base64"
"errors"
"github.com/berduk-dev/networks/cache"
"github.com/berduk-dev/networks/repo"
errors2 "github.com/berduk-dev/networks/errors"
"github.com/berduk-dev/networks/manager"
"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
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 (h *Handler) CreateLink(c *gin.Context) {
// анмаршаллинг
var req CreateLinkRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(http.StatusBadRequest, "У вас невалидный запрос")
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.LinksCache.GetLink(shortLink)
err := h.linksService.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, "Произошла ошибка, попробуйте позже!")
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.LinksRepository.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)})
}

func IsLinkValid(customLink string) bool {
if customLink != "" && len(customLink) == ShortLinkLength {
return true
}
return false
c.JSON(http.StatusOK, gin.H{
"redirects": redirects,
"total_count": len(redirects),
})
}
17 changes: 12 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -16,7 +18,10 @@ import (
"github.com/jackc/pgx/v5"
)

const cacheLinksInterval = time.Hour
const (
cacheLinksInterval = time.Hour
popularLinksCount = 10
)

func main() {
rdb := redis.NewClient(&redis.Options{
Expand All @@ -41,7 +46,9 @@ func main() {

linksRepository := repo.New(conn)
linksCache := cache.New(rdb)
linksHandler := handler.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)
Expand All @@ -58,7 +65,7 @@ func main() {
}
}()

// --- CORS middleware ---
// --- CORS middleware ----
r.Use(func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if origin == "" || origin == "null" {
Expand All @@ -82,14 +89,14 @@ 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()
}

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)
}
Expand Down
71 changes: 71 additions & 0 deletions manager/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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/berduk-dev/networks/service"
"github.com/jackc/pgx/v5"
)

type LinksManager struct {
cache *cache.LinksCache
repo *repo.Repository
}

func New(cache *cache.LinksCache, repo *repo.Repository) LinksManager {
return LinksManager{
repo: repo,
cache: cache,
}
}

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)
}

// если нашли, возвращаем
if longLink != "" {
return longLink, nil
}

// идем в бд
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)
}

return longLink, nil
}

func (m *LinksManager) CreateLink(ctx context.Context, longLink string, shortLink string) error {
return m.repo.CreateLink(ctx, longLink, shortLink)
}

func (m *LinksManager) StoreRedirect(ctx context.Context, params repo.StoreRedirectParams) error {
return m.repo.StoreRedirect(ctx, params)
}

func (m *LinksManager) GetRedirectsByShortLink(ctx context.Context, shortLink string) ([]repo.Redirect, error) {
return m.repo.GetRedirectsByShortLink(ctx, shortLink)
}

func (m *LinksManager) GetShortByLong(ctx context.Context, longLink string) (string, error) {
return m.repo.GetShortByLong(ctx, longLink)
}

func (m *LinksManager) GetPopularLinks(ctx context.Context, n int) ([]repo.LinkPair, error) {
return m.repo.GetPopularLinks(ctx, n)
}
Loading