Merge pull request #5 from Nucrea/feature/url-shortener

Feature/url shortener
This commit is contained in:
Sergey Chubaryan 2024-08-16 00:37:58 +03:00 committed by GitHub
commit 73e7a25b11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 268 additions and 60 deletions

9
.gitignore vendored
View File

@ -21,5 +21,14 @@
go.work
go.work.sum
# Binary dir
.build/
# env file
.env
.run
# temporary
coworker/
webapp/

View File

@ -8,6 +8,7 @@ RUN go mod download && go mod verify
EXPOSE 8080
COPY . .
RUN go build -v -o /usr/local/bin/app ./...
RUN go build -v -o ./app .
RUN chmod +x ./app
CMD ["app"]
CMD ["./app", "-c", "./misc/config.yaml"]

34
main.go
View File

@ -1,15 +1,16 @@
package main
import (
"backend/args_parser"
"backend/config"
"backend/logger"
"backend/src/handlers"
"backend/src/middleware"
"backend/src/models"
"backend/src/repo"
"backend/src/services"
"backend/src/utils"
"backend/src/args_parser"
"backend/src/client_notifier"
"backend/src/config"
"backend/src/core/models"
"backend/src/core/repos"
"backend/src/core/services"
"backend/src/core/utils"
"backend/src/logger"
"backend/src/server/handlers"
"backend/src/server/middleware"
"crypto/rsa"
"crypto/x509"
"database/sql"
@ -75,10 +76,12 @@ func main() {
jwtUtil := utils.NewJwtUtil(key)
passwordUtil := utils.NewPasswordUtil()
userRepo := repo.NewUserRepo(sqlDb)
userCache := repo.NewCacheInmem[string, models.UserDTO](60 * 60)
emailRepo := repo.NewEmailRepo()
actionTokenRepo := repo.NewActionTokenRepo(sqlDb)
userRepo := repos.NewUserRepo(sqlDb)
userCache := repos.NewCacheInmem[string, models.UserDTO](60 * 60)
emailRepo := repos.NewEmailRepo()
actionTokenRepo := repos.NewActionTokenRepo(sqlDb)
clientNotifier := client_notifier.NewBasicNotifier()
userService := services.NewUserService(
services.UserServiceDeps{
@ -92,7 +95,7 @@ func main() {
)
linkService := services.NewShortlinkSevice(
services.NewShortlinkServiceParams{
Cache: repo.NewCacheInmem[string, string](7 * 24 * 60 * 60),
Cache: repos.NewCacheInmem[string, string](7 * 24 * 60 * 60),
},
)
@ -116,6 +119,9 @@ func main() {
dummyGroup.Use(middleware.NewAuthMiddleware(userService))
dummyGroup.GET("/", handlers.NewDummyHandler())
lpGroup := r.Group("/pooling")
lpGroup.GET("/", handlers.NewLongPoolingHandler(clientNotifier))
listenAddr := fmt.Sprintf(":%d", conf.GetPort())
logger.Log().Msgf("server listening on %s", listenAddr)

4
makefile Normal file
View File

@ -0,0 +1,4 @@
all: release
release:
go build -o ./.build/release/backend main.go

View File

@ -0,0 +1,12 @@
package client_notifier
type Event struct {
Type EventType
Data []byte
}
type EventType string
const (
EventTypeEmailConfirmed EventType = "event_email_confirmed"
)

View File

@ -0,0 +1,57 @@
package client_notifier
import "sync"
type ClientNotifier interface {
RegisterClient(id string) <-chan Event
UnregisterClient(id string)
NotifyClient(id string, e Event)
}
type client struct {
id string
eventChan chan Event
}
func NewBasicNotifier() ClientNotifier {
return &basicNotifier{
m: &sync.RWMutex{},
clients: map[string]client{},
}
}
type basicNotifier struct {
m *sync.RWMutex
clients map[string]client
}
func (p *basicNotifier) RegisterClient(id string) <-chan Event {
p.m.Lock()
defer p.m.Unlock()
eventChan := make(chan Event)
p.clients[id] = client{
id: id,
eventChan: eventChan,
}
return eventChan
}
func (p *basicNotifier) UnregisterClient(id string) {
p.m.Lock()
defer p.m.Unlock()
delete(p.clients, id)
}
func (p *basicNotifier) NotifyClient(id string, e Event) {
p.m.RLock()
defer p.m.RUnlock()
client, ok := p.clients[id]
if !ok {
return
}
client.eventChan <- e
}

View File

@ -1,7 +1,7 @@
package repo
package repos
import (
"backend/src/models"
"backend/src/core/models"
"context"
"database/sql"
)

View File

@ -1,4 +1,4 @@
package repo
package repos
import (
"sync"

View File

@ -1,4 +1,4 @@
package repo
package repos
import (
"strings"

View File

@ -1,7 +1,7 @@
package repo
package repos
import (
"backend/src/models"
"backend/src/core/models"
"context"
"database/sql"
"errors"

View File

@ -1,11 +1,9 @@
package services
import (
"backend/src/repo"
"backend/src/core/repos"
"backend/src/core/utils"
"fmt"
"math/rand"
"strings"
"time"
)
type ShortlinkService interface {
@ -15,38 +13,23 @@ type ShortlinkService interface {
type NewShortlinkServiceParams struct {
Endpoint string
Cache repo.Cache[string, string]
Cache repos.Cache[string, string]
}
func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService {
return &shortlinkService{
randomUtil: *utils.NewRand(),
cache: params.Cache,
}
}
type shortlinkService struct {
cache repo.Cache[string, string]
}
func (s *shortlinkService) randomStr() string {
src := rand.NewSource(time.Now().UnixMicro())
randGen := rand.New(src)
builder := strings.Builder{}
for i := 0; i < 9; i++ {
offset := 0x41
if randGen.Int()%2 == 1 {
offset = 0x61
}
byte := offset + (randGen.Int() % 26)
builder.WriteRune(rune(byte))
}
return builder.String()
randomUtil utils.RandomUtil
cache repos.Cache[string, string]
}
func (s *shortlinkService) CreateLink(in string) (string, error) {
str := s.randomStr()
str := s.randomUtil.RandomID(10, utils.CharsetAll)
s.cache.Set(str, in, 7*24*60*60)
return str, nil
}

View File

@ -1,9 +1,9 @@
package services
import (
"backend/src/models"
"backend/src/repo"
"backend/src/utils"
"backend/src/core/models"
"backend/src/core/repos"
"backend/src/core/utils"
"context"
"fmt"
@ -32,10 +32,10 @@ func NewUserService(deps UserServiceDeps) UserService {
type UserServiceDeps struct {
Jwt utils.JwtUtil
Password utils.PasswordUtil
UserRepo repo.UserRepo
UserCache repo.Cache[string, models.UserDTO]
EmailRepo repo.EmailRepo
ActionTokenRepo repo.ActionTokenRepo
UserRepo repos.UserRepo
UserCache repos.Cache[string, models.UserDTO]
EmailRepo repos.EmailRepo
ActionTokenRepo repos.ActionTokenRepo
}
type userService struct {

70
src/core/utils/random.go Normal file
View File

@ -0,0 +1,70 @@
package utils
import (
"math/rand"
"strings"
"time"
)
type Charset int
const (
CharsetAll Charset = iota
CharsetLettersLower
CharsetLettersUpper
CharsetLetters
CharsetNumeric
)
type charsetPart struct {
Offset int
Size int
}
var charsets = map[Charset][]charsetPart{}
func NewRand() *RandomUtil {
charsetLettersLower := charsetPart{ //CharsetLettersLower
Offset: 0x41,
Size: 26,
}
charsetLettersUpper := charsetPart{ //CharsetLettersUpper
Offset: 0x61,
Size: 26,
}
charsetNumeric := charsetPart{ //CharsetLettersNumeric
Offset: 0x30,
Size: 10,
}
charsets = map[Charset][]charsetPart{
CharsetNumeric: {charsetNumeric},
CharsetLettersLower: {charsetLettersLower},
CharsetLettersUpper: {charsetLettersUpper},
CharsetLetters: {charsetLettersLower, charsetLettersUpper},
CharsetAll: {charsetLettersLower, charsetLettersUpper, charsetNumeric},
}
return &RandomUtil{}
}
type RandomUtil struct{}
func (r *RandomUtil) RandomID(outputLenght int, charset Charset) string {
src := rand.NewSource(time.Now().UnixMicro())
randGen := rand.New(src)
charsetData := charsets[charset]
builder := strings.Builder{}
for i := 0; i < outputLenght; i++ {
charsetIdx := randGen.Int() % len(charsetData)
charsetPart := charsetData[charsetIdx]
byte := charsetPart.Offset + (randGen.Int() % charsetPart.Size)
builder.WriteRune(rune(byte))
}
return builder.String()
}

View File

@ -0,0 +1,25 @@
package leader_elector
import (
"context"
"database/sql"
)
func Lock(ctx context.Context, db *sql.DB, lockName, id string) error {
query := `
update locks (id)
set id = $1
where name == lockName and timestamp < $1 returning id
on conflict
insert into locks(id, name) values($1, $2);`
row := db.QueryRowContext(ctx, query, id)
result := ""
err := row.Scan(&result)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,27 @@
package handlers
import (
"backend/src/client_notifier"
"backend/src/server/utils"
"github.com/gin-gonic/gin"
)
func NewLongPoolingHandler(notifier client_notifier.ClientNotifier) gin.HandlerFunc {
return func(c *gin.Context) {
user := utils.GetUserFromRequest(c)
if user == nil {
c.Data(403, "plain/text", []byte("Unauthorized"))
return
}
eventChan := notifier.RegisterClient(user.Id)
select {
case <-c.Done():
notifier.UnregisterClient(user.Id)
case event := <-eventChan:
c.Data(200, "application/json", event.Data)
}
}
}

View File

@ -1,7 +1,7 @@
package handlers
import (
"backend/src/services"
"backend/src/core/services"
"encoding/json"
"fmt"
"net/url"

View File

@ -1,7 +1,7 @@
package handlers
import (
"backend/src/services"
"backend/src/core/services"
"encoding/json"
"github.com/gin-gonic/gin"

View File

@ -1,7 +1,7 @@
package handlers
import (
"backend/src/services"
"backend/src/core/services"
"encoding/json"
"github.com/gin-gonic/gin"

View File

@ -1,7 +1,7 @@
package middleware
import (
"backend/src/services"
"backend/src/core/services"
"fmt"
"github.com/gin-gonic/gin"

View File

@ -1,7 +1,7 @@
package middleware
import (
"backend/logger"
"backend/src/logger"
"time"
"github.com/gin-gonic/gin"

14
src/server/utils/user.go Normal file
View File

@ -0,0 +1,14 @@
package utils
import (
"backend/src/core/models"
"github.com/gin-gonic/gin"
)
func GetUserFromRequest(c *gin.Context) *models.UserDTO {
if user, ok := c.Get("user"); ok {
return user.(*models.UserDTO)
}
return nil
}