feat: added shortener & redirect url into project

develop
quockhanh 2024-09-24 18:59:27 +07:00
parent 489bd4b63c
commit c36e227133
22 changed files with 192 additions and 309 deletions

View File

@ -15,7 +15,7 @@
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
Dockerfile
**/node_modules
**/npm-debug.log
**/obj
@ -28,7 +28,7 @@ out/
**/.vagrant/
.idea/
local/data/
.env
.env
.terraform
*tfstate*

6
.env
View File

@ -1,6 +0,0 @@
APP_ADDRESS = "localhost"
APP_PORT = "3000"
REDIS_ADDRESS="localhost"
REDIS_PORT="6379"
REDIS_PASSWORD = "110502"

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM golang:1.19.2-alpine3.16 as builder
COPY . /app
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build
FROM scratch
CMD ["/app"]

View File

@ -1,4 +0,0 @@
FROM ubuntu:latest
LABEL authors="nqkhanh2003"
ENTRYPOINT ["top", "-b"]

18
go.mod
View File

@ -3,20 +3,6 @@ module url-shortener
go 1.23.1
require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0
github.com/joho/godotenv v1.5.1
github.com/sirupsen/logrus v1.9.3
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
google.golang.org/protobuf v1.34.2
)
require (
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/grpc v1.64.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/lib/pq v1.10.9
github.com/sony/sonyflake v1.2.0
)

50
go.sum
View File

@ -1,46 +1,4 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ=
github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y=

71
main.go
View File

@ -1,45 +1,80 @@
package main
import (
"encoding/json"
"fmt"
"github.com/joho/godotenv"
"github.com/sony/sonyflake"
"log"
"net/http"
"os"
)
type ShorterRequest struct {
Url string
}
var sf *sonyflake.Sonyflake
const (
base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
func main() {
envError := godotenv.Load()
if envError != nil {
panic("Error loading .env file")
db := connectDB()
router := http.NewServeMux()
port := os.Getenv("APP_PORT")
var st sonyflake.Settings
sf = sonyflake.NewSonyflake(st)
if sf == nil {
panic("Sonyflake not created")
}
router := http.NewServeMux()
router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
path := r.URL.Path
code := path[1:]
if code == "" {
http.Error(w, "Page not found", http.StatusNotFound)
return
}
url, err := getUrlByCode(db, code)
if url == "" || err != nil {
http.Error(w, "Page not found", http.StatusNotFound)
return
}
http.Redirect(w, r, url, http.StatusFound)
})
router.HandleFunc("POST /shorten", func(w http.ResponseWriter, r *http.Request) {
requestData := ShorterRequest{}
err := json.NewDecoder(r.Body).Decode(&requestData)
originalURL := r.FormValue("url")
if originalURL == "" {
http.Error(w, "URL parameter is missing", http.StatusBadRequest)
return
}
codeExisted, err := checkURLAlreadyExists(db, originalURL)
if err != nil {
fmt.Println(err)
panic(err)
log.Fatalf("Error checking URL exists: %v\n", err)
}
w.Write([]byte(requestData.Url))
if codeExisted == "" {
code, err := insertURL(db, originalURL)
if err != nil {
log.Fatalf("Error when insert URL: %v\n", err)
}
_, writeErr := w.Write([]byte(fmt.Sprintf("%s/%s", r.Host, code)))
if writeErr != nil {
return
}
}
_, writeErr := w.Write([]byte(fmt.Sprintf("%s/%s", r.Host, codeExisted)))
if writeErr != nil {
return
}
})
server := http.Server{Addr: ":3000", Handler: router}
server := http.Server{Addr: fmt.Sprintf(":%s", port), Handler: router}
fmt.Println("URL Shortener is running on :3000")
fmt.Println(fmt.Sprintf("App is running in port: %s", port))
err := server.ListenAndServe()
if err != nil {
fmt.Println(err)
return
}
}

View File

@ -1,17 +0,0 @@
package configs
type (
App struct {
Name string `env-required:"true" yaml:"name" env:"APP_NAME"`
Version string `env-required:"true" yaml:"version" env:"APP_VERSION"`
}
HTTP struct {
Host string `env-required:"true" yaml:"host" env:"HTTP_HOST"`
Port int `env-required:"true" yaml:"port" env:"HTTP_PORT"`
}
Log struct {
Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"`
}
)

View File

@ -1 +0,0 @@
package consumer

View File

@ -1 +0,0 @@
package consumer

View File

@ -1 +0,0 @@
package consumer

View File

@ -1 +0,0 @@
package kafka

View File

@ -1 +0,0 @@
package producer

View File

@ -1 +0,0 @@
package producer

View File

@ -1 +0,0 @@
package producer

View File

@ -1,79 +0,0 @@
package logger
// refs:
// https://josephwoodward.co.uk/2022/11/slog-structured-logging-proposal
// https://thedevelopercafe.com/articles/logging-in-go-with-slog-a7bb489755c2
import (
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slog"
)
type LogrusHandler struct {
logger *logrus.Logger
}
func NewLogrusHandler(logger *logrus.Logger) *LogrusHandler {
return &LogrusHandler{
logger: logger,
}
}
func ConvertLogLevel(level string) logrus.Level {
var l logrus.Level
switch strings.ToLower(level) {
case "error":
l = logrus.ErrorLevel
case "warm":
l = logrus.WarnLevel
case "info":
l = logrus.InfoLevel
case "debug":
l = logrus.DebugLevel
default:
l = logrus.InfoLevel
}
return l
}
func (h *LogrusHandler) Enabled(_ slog.Level) bool {
// support all logging levels
return true
}
func (h *LogrusHandler) Handle(rec slog.Record) error {
fields := make(map[string]interface{}, rec.NumAttrs())
rec.Attrs(func(a slog.Attr) {
fields[a.Key] = a.Value.Any()
})
entry := h.logger.WithFields(fields)
switch rec.Level {
case slog.DebugLevel:
entry.Debug(rec.Message)
case slog.InfoLevel.Level():
entry.Info(rec.Message)
case slog.WarnLevel:
entry.Warn(rec.Message)
case slog.ErrorLevel:
entry.Error(rec.Message)
}
return nil
}
func (h *LogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
// not implemented for brevity
return h
}
func (h *LogrusHandler) WithGroup(name string) slog.Handler {
// not implemented for brevity
return h
}

View File

@ -1,9 +0,0 @@
package postgres
import "database/sql"
type DBEngine interface {
GetDB() *sql.DB
Configure(...Option) DBEngine
Close()
}

View File

@ -1,17 +0,0 @@
package postgres
import "time"
type Option func(*postgres)
func ConnAttempts(attempts int) Option {
return func(p *postgres) {
p.connAttempts = attempts
}
}
func ConnTimeout(timeout time.Duration) Option {
return func(p *postgres) {
p.connTimeout = timeout
}
}

View File

@ -1,70 +0,0 @@
package postgres
import (
"database/sql"
"log"
"time"
"golang.org/x/exp/slog"
)
const (
_defaultConnAttempts = 3
_defaultConnTimeout = time.Second
)
type DBConnString string
type postgres struct {
connAttempts int
connTimeout time.Duration
db *sql.DB
}
var _ DBEngine = (*postgres)(nil)
func NewPostgresDB(url DBConnString) (DBEngine, error) {
slog.Info("CONN", "connect string", url)
pg := &postgres{
connAttempts: _defaultConnAttempts,
connTimeout: _defaultConnTimeout,
}
var err error
for pg.connAttempts > 0 {
pg.db, err = sql.Open("postgres", string(url))
if err != nil {
break
}
log.Printf("Postgres is trying to connect, attempts left: %d", pg.connAttempts)
time.Sleep(pg.connTimeout)
pg.connAttempts--
}
slog.Info("📰 connected to postgresdb 🎉")
return pg, nil
}
func (p *postgres) Configure(opts ...Option) DBEngine {
for _, opt := range opts {
opt(p)
}
return p
}
func (p *postgres) GetDB() *sql.DB {
return p.db
}
func (p *postgres) Close() {
if p.db != nil {
p.db.Close()
}
}

View File

@ -1,11 +0,0 @@
package utils
import "os"
func IsRunningInContainer() bool {
if _, err := os.Stat("/.dockerenv"); err != nil {
return false
}
return true
}

122
postgres.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/lib/pq"
"log"
"os"
)
type DbConfig struct {
dbName string
dbUser string
dbPassword string
dbHost string
dbPort string
}
func connectDB() *sql.DB {
dbConfig := &DbConfig{
dbName: os.Getenv("DB_NAME"),
dbUser: os.Getenv("DB_USER"),
dbHost: os.Getenv("DB_HOST"),
dbPort: os.Getenv("DB_PORT"),
dbPassword: os.Getenv("DB_PASSWORD"),
}
connectStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%s sslmode=disable", dbConfig.dbUser, dbConfig.dbPassword, dbConfig.dbName, dbConfig.dbHost, dbConfig.dbPort)
db, err := sql.Open("postgres", connectStr)
if err != nil {
fmt.Println("Error connecting to database", err)
panic(err)
}
err = db.Ping()
if err != nil {
log.Fatal("Failed to connect to the database:", err)
}
fmt.Println("Connected to the database")
_, tbErr := db.Exec("CREATE TABLE IF NOT EXISTS urls (id bigint NOT NULL PRIMARY KEY , url TEXT NOT NULL, code varchar(18) NULL, createdAt TIMESTAMP DEFAULT NOW(), updatedAt TIMESTAMP DEFAULT NOW())")
if tbErr != nil {
log.Fatal("Error creating table:", tbErr)
}
fmt.Printf("Table '%s' created successfully.\n", "urls")
return db
}
func insertURL(db *sql.DB, url string) (string, error) {
id := generateNewID()
code := intToBase62(id)
_, err := db.Exec("INSERT INTO urls(id, url, code) VALUES ($1, $2, $3)", id, url, code)
if err != nil {
log.Fatal("Error inserting url:", err)
return "", err
}
return code, nil
}
func checkURLAlreadyExists(db *sql.DB, url string) (string, error) {
var code string
rows, err := db.Query(`SELECT code from urls where url=$1`, url)
if err != nil {
log.Fatalln("Error querying rows:", err)
return "", err
} else {
for rows.Next() {
err := rows.Scan(&code)
if err != nil {
return "", err
}
return code, nil
}
}
return "", err
}
func getUrlByCode(db *sql.DB, code string) (string, error) {
var url string
rows, err := db.Query("SELECT url FROM urls WHERE code=$1", code)
if err != nil {
log.Fatalln(err)
return "", err
}
for rows.Next() {
err = rows.Scan(&url)
return url, err
}
return "", errors.New("code not found")
}
func generateNewID() uint64 {
id, err := sf.NextID()
if err != nil {
fmt.Println(err)
panic(err)
}
return id
}
func intToBase62(n uint64) string {
if n == 0 {
return string(base62[0])
}
var result []byte
for n > 0 {
result = append(result, base62[n%62])
n /= 62
}
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return string(result)
}

View File

@ -1,7 +0,0 @@
package tools
import (
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
)