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
9 changes: 7 additions & 2 deletions internal/domain/link/linkstorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ var (
ErrNotExist = errors.New("link does not exist")
)

type UserLink struct {
ShortenLink string
RealLink string
}

type Interface interface {
CreateShortLink(link string, userId string) (string, error)
GetLinkByKey(key string) (string, error)
MakeRedirect(key string) (string, error)
DeleteLink(key string, userId string) (string, error)
GetUserLinks(userId string) ([]string, error)
GetUserLinks(userId string) ([]UserLink, error)
GetLinkStat(link string) (uint64, error)
CreateUserLinksStorage(userId string) (string, error)
}
}
161 changes: 144 additions & 17 deletions internal/interface/httpapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@ package httpapi
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
link2 "koro.che/internal/domain/link"
"koro.che/internal/interface/prom"
"koro.che/internal/usecases/account"
"koro.che/internal/usecases/link"
"net/http"
"net/url"
"os"
"sync"
"time"
)

type Api struct {
AccountUseCases account.AccountUseCasesInterface
LinkUseCases link.LinkUseCasesInterface
Logger zerolog.Logger
Logger zerolog.Logger
Ctx context.Context
}

func NewApi(a account.AccountUseCasesInterface, l link.LinkUseCasesInterface) *Api {
func NewApi(ctx context.Context, a account.AccountUseCasesInterface, l link.LinkUseCasesInterface) *Api {
return &Api{
AccountUseCases: a,
LinkUseCases: l,
Logger: log.With().Str("module", "http-server").Logger(),
Logger: log.With().Str("module", "http-server").Logger(),
Ctx: ctx,
}
}

Expand Down Expand Up @@ -136,8 +143,8 @@ func (a *Api) logout(writer http.ResponseWriter, request *http.Request) {

func (a *Api) redirectToRealLink(writer http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
if link, err := a.LinkUseCases.MakeRedirect(vars["key"]); err == nil {
http.Redirect(writer, request, "https://"+link, http.StatusMovedPermanently)
if lnk, err := a.LinkUseCases.MakeRedirect(vars["key"]); err == nil {
http.Redirect(writer, request, "https://"+lnk, http.StatusMovedPermanently)
} else {
writer.WriteHeader(http.StatusNotFound)
}
Expand All @@ -150,8 +157,8 @@ type linkModel struct {
func (a *Api) getRealLink(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
vars := mux.Vars(request)
if link, err := a.LinkUseCases.GetRealLink(vars["key"]); err == nil {
o := linkModel{Link: link}
if lnk, err := a.LinkUseCases.GetRealLink(vars["key"]); err == nil {
o := linkModel{Link: lnk}
if err := json.NewEncoder(writer).Encode(o); err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
Expand Down Expand Up @@ -216,15 +223,135 @@ func (a *Api) deleteLink(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
}

func urlChecker(ctx context.Context, in <-chan string, out chan<- bool, stop <-chan struct{}) error {
for {
select {
case <-stop:
return nil
case <-ctx.Done():
return ctx.Err()
case u, ok := <-in:
if !ok {
return nil
}
flag, err := checkUrl(ctx, u)
if err != nil {
return err
}
out <- flag
}
}
}

func checkUrl(ctx context.Context, urlStr string) (bool, error) {
c := http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return false, err
}
resp, err := c.Do(req)
if err != nil {
if e, ok := err.(*url.Error); ok {
if e.Timeout() || e.Temporary() {
return false, nil
} else {
return false, err
}
} else {
return false, err
}
}
if resp != nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return false, nil
} else {
return true, nil
}
}
return false, nil
}

const (
urlsConcurrency = 4
)

func getLinksAvailability(ctx context.Context, urls []string) ([]bool, error) {
stopChan := make(chan struct{})
errChan := make(chan string, urlsConcurrency)
urlsChan := make(chan string)
availabilitiesChan := make(chan bool, urlsConcurrency)
go func() {
for _, u := range urls {
urlsChan <- u
}
close(urlsChan)
}()
var wg sync.WaitGroup
wg.Add(urlsConcurrency)
for i := 0; i < urlsConcurrency; i++ {
go func(ctx context.Context, in <-chan string, out chan<- bool, stop <-chan struct{}) {
err := urlChecker(ctx, in, out, stop)
if err != nil {
fmt.Fprintln(os.Stderr, "error url checking")
errChan <- err.Error()
}
wg.Done()
}(ctx, urlsChan, availabilitiesChan, stopChan)
}

availabilities := make([]bool, 0)

for {
if len(availabilities) == len(urls) {
close(stopChan)
break
}
select {
case e := <-errChan:
close(stopChan)
return []bool{}, errors.New(e)
case <-ctx.Done():
break
case b := <-availabilitiesChan:
availabilities = append(availabilities, b)
}
}
wg.Wait()
return availabilities, nil
}

type UserLinkResponse struct {
Link string `json:"link"`
Available bool `json:"available"`
}

func (a *Api) getUserLinks(w http.ResponseWriter, r *http.Request) {
var links []string
var links []link2.UserLink
userId := r.Context().Value("account_id").(string)
links, err := a.LinkUseCases.GetUserLinks(userId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(links); err != nil {
realLinks := make([]string, len(links))
for i, lnk := range links {
realLinks[i] = lnk.RealLink
}
availabilities, err := getLinksAvailability(a.Ctx, realLinks)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

resp := make([]UserLinkResponse, len(links))
for i := 0; i < len(links); i++ {
resp[i] = UserLinkResponse{Link: links[i].ShortenLink, Available: availabilities[i]}
}

if err := json.NewEncoder(w).Encode(resp); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
Expand All @@ -249,7 +376,7 @@ func (a *Api) getUserLinkStats(w http.ResponseWriter, r *http.Request) {

type responseWriterObserver struct {
http.ResponseWriter
status int
status int
wroteHeader bool
}

Expand All @@ -275,12 +402,12 @@ func (a *Api) logger(next http.Handler) http.Handler {
o := &responseWriterObserver{ResponseWriter: w}
next.ServeHTTP(o, r)
a.Logger.Info().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("protocol", r.Proto).
Int("status-code", o.StatusCode()).
Str("remote-addr", r.RemoteAddr).
Dur("duration", time.Since(start)).
Msg("")
Str("method", r.Method).
Str("url", r.URL.String()).
Str("protocol", r.Proto).
Int("status-code", o.StatusCode()).
Str("remote-addr", r.RemoteAddr).
Dur("duration", time.Since(start)).
Msg("")
})
}
31 changes: 19 additions & 12 deletions internal/interface/memory/linkrepo/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import (
)

type Memory struct {
linkByKey map[string]string
StatsByKey map[string]uint64
linkByKey map[string]string
StatsByKey map[string]uint64
userToLinksKeys map[string]map[string]bool
mu *sync.Mutex
mu *sync.Mutex
}

func NewMemory() *Memory {
return &Memory{
linkByKey: make(map[string]string),
StatsByKey: make(map[string]uint64),
linkByKey: make(map[string]string),
StatsByKey: make(map[string]uint64),
userToLinksKeys: make(map[string]map[string]bool),
mu: &sync.Mutex{},
mu: &sync.Mutex{},
}
}

Expand Down Expand Up @@ -71,7 +71,7 @@ func (m *Memory) MakeRedirect(key string) (string, error) {
if link == "" {
err = link2.ErrNotExist
}
if err!= nil {
if err != nil {
return "", err
}
m.StatsByKey[key] += 1
Expand All @@ -97,16 +97,23 @@ func (m *Memory) DeleteLink(key string, userId string) (string, error) {
return link, err
}

func (m *Memory) GetUserLinks(userId string) ([]string, error) {
func (m *Memory) GetUserLinks(userId string) ([]link2.UserLink, error) {
m.mu.Lock()
defer m.mu.Unlock()
var links, ok = m.userToLinksKeys[userId]
if !ok {
return []string{}, link2.ErrNotExist
return []link2.UserLink{}, link2.ErrNotExist
}
keys := make([]string, 0, len(links))
keys := make([]link2.UserLink, 0, len(links))
for k, _ := range links {
keys = append(keys, k)
realLink, ok := m.linkByKey[k]
if !ok {
return []link2.UserLink{}, link2.ErrNotExist
}
keys = append(keys, link2.UserLink{
ShortenLink: k,
RealLink: realLink,
})
}
return keys, nil
}
Expand All @@ -126,4 +133,4 @@ func (m *Memory) CreateUserLinksStorage(userId string) (string, error) {
defer m.mu.Unlock()
m.userToLinksKeys[userId] = map[string]bool{}
return "", nil
}
}
15 changes: 8 additions & 7 deletions internal/interface/postgres/linkrepo/linkrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const queryDeleteLink = `
`

const queryUserLinks = `
select key from links
select key, real_link from links
where creator_id = $1
`

Expand All @@ -72,7 +72,7 @@ func (p *Postgres) CreateShortLink(link string, userId string) (string, error) {
err := row.Scan()
if err == sql.ErrNoRows {
p.conn.QueryRow(queryCreateLink, userId, link, key)
// todo need wrapping???
// todo need wrapping???
break
}
}
Expand Down Expand Up @@ -118,18 +118,19 @@ func (p *Postgres) DeleteLink(key string, userId string) (string, error) {
return realLink, err
}

func (p *Postgres) GetUserLinks(userId string) ([]string, error) {
var userLinks = make([]string, 0)
func (p *Postgres) GetUserLinks(userId string) ([]link2.UserLink, error) {
var userLinks = make([]link2.UserLink, 0)
rows, err := p.conn.Query(queryUserLinks, userId)
if err != nil {
return nil, err
}
for rows.Next() {
var link string
if err := rows.Scan(&link); err != nil {
var shortLink string
var realLink string
if err := rows.Scan(&shortLink, &realLink); err != nil {
return nil, err
}
userLinks = append(userLinks, link)
userLinks = append(userLinks, link2.UserLink{ShortenLink: shortLink, RealLink: realLink})
}
return userLinks, nil
}
Expand Down
Loading