This commit is contained in:
Arkaprabha Chakraborty
2025-12-14 05:33:05 +05:30
parent 25ead1d8bd
commit 43c516a005
37 changed files with 1037 additions and 523 deletions

11
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Go
bin/
pkg/
*.exe
*.dll
*.so
*.test
*.prof
.env
# Local database (if applicable)
reduce.db

View File

@@ -1,23 +1,23 @@
# Use the official Golang image
FROM golang:1.20-alpine
FROM golang:1.21-alpine AS builder
RUN apk add --no-cache gcc musl-dev
# Set the Current Working Directory inside the container
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download
# Copy the source code into the container
COPY . .
# Build the Go app
RUN go build -o main .
RUN CGO_ENABLED=1 \
CGO_CFLAGS="-D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE" \
CGO_LDFLAGS="-lm" \
go build -ldflags="-s -w" .
FROM alpine:latest
COPY --from=builder /app/reduce /reduce
# Expose port 8080 to the outside world
EXPOSE 8080
# Command to run the executable
CMD ["./main"]
CMD ["/reduce"]

View File

@@ -1,25 +1,24 @@
module url-shortener-backend
module reduce
go 1.20
require (
github.com/jinzhu/gorm v1.9.16
github.com/labstack/echo v3.3.10+incompatible
)
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/jinzhu/gorm v1.9.16 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/labstack/echo v3.3.10+incompatible // indirect
github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/mattn/go-sqlite3 v1.14.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
)

View File

@@ -1,24 +1,24 @@
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -29,9 +29,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -57,5 +58,4 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -4,61 +4,18 @@ import (
"log"
"math/rand"
"net/http"
"os"
"time"
"fmt"
"os"
"errors"
"strconv"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
var db *gorm.DB
type URL struct {
ID string `gorm:"primary_key" json:"id"`
LongURL string `json:"long_url"`
ShortURL string `json:"short_url"`
CreatedAt time.Time `json:"created_at"`
}
func init() {
var err error
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
dbUser := os.Getenv("DB_USER")
dbName := os.Getenv("DB_NAME")
dbPassword := os.Getenv("DB_PASSWORD")
dsn := "host=" + dbHost + " port=" + dbPort + " user=" + dbUser + " dbname=" + dbName + " password=" + dbPassword + " sslmode=disable"
maxRetries := 5
retryDelay := 2 * time.Second
for i := 0; i < maxRetries; i++ {
db, err = gorm.Open("postgres", dsn)
if err == nil {
// Connection successful, break out of the loop
break
}
// Log the error and wait before retrying
fmt.Printf("Failed to connect to database (attempt %d/%d): %v\n", i+1, maxRetries, err)
time.Sleep(retryDelay)
}
if err != nil {
// Panic if the final attempt also fails
panic(fmt.Sprintf("Failed to connect to database after %d attempts: %v", maxRetries, err))
}
// Auto-migrate database
db.AutoMigrate(&URL{})
}
func generateRandomString(length int) string {
func codegen(length int) string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
bytes := make([]byte, length)
for i := range bytes {
@@ -70,7 +27,7 @@ func generateRandomString(length int) string {
func shortenURL(c echo.Context) error {
// Define a struct for binding the request body
type RequestBody struct {
LongURL string `json:"long_url"`
LURL string `json:"lurl"`
BaseURL string `json:"base_url"` // Expect base URL in the request
}
@@ -90,23 +47,31 @@ func shortenURL(c echo.Context) error {
}
// Check if the long URL already exists in the database
var existingURL URL
if err := db.Where("long_url = ?", reqBody.LongURL).First(&existingURL).Error; err == nil {
var existingURL CodeURLMap
if err := db.Where("lurl = ?", reqBody.LURL).First(&existingURL).Error; err == nil {
// If the long URL exists, return the existing short URL
return c.JSON(http.StatusOK, existingURL)
return c.JSON(http.StatusOK, map[string]string{
"surl": reqBody.BaseURL + "/" + existingURL.Code,
})
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
// If there's an error other than record not found, return an error
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to check existing URL")
}
// Generate a unique ID
id := generateRandomString(6)
// Generate a unique code
codelen := 6
if os.Getenv("CODE_LENGTH") != "" && os.Getenv("CODE_LENGTH") != "0" {
t, err := strconv.Atoi(os.Getenv("CODE_LENGTH"))
if err == nil {
codelen = t
}
}
code := codegen(codelen)
// Create URL record
url := &URL{
ID: id,
LongURL: reqBody.LongURL,
ShortURL: reqBody.BaseURL + "/" + id,
url := &CodeURLMap{
Code: code,
LURL: reqBody.LURL,
CreatedAt: time.Now(),
}
@@ -115,17 +80,19 @@ func shortenURL(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create URL record")
}
return c.JSON(http.StatusCreated, url)
return c.JSON(http.StatusCreated, map[string]string{
"surl": reqBody.BaseURL + "/" + code,
})
}
func fetchLongURL(c echo.Context) error {
id := c.Param("id")
var url URL
if err := db.Where("id = ?", id).First(&url).Error; err != nil {
func fetchLURL(c echo.Context) error {
code := c.Param("code")
var url CodeURLMap
if err := db.Where("code = ?", code).First(&url).Error; err != nil {
log.Println("Error retrieving URL:", err)
return echo.NewHTTPError(http.StatusNotFound, "URL not found")
}
return c.JSON(http.StatusOK, map[string]string{"long_url": url.LongURL})
return c.JSON(http.StatusOK, map[string]string{"lurl": url.LURL})
}
@@ -148,7 +115,7 @@ func main() {
})
e.POST("/reduce/shorten", shortenURL)
e.GET("/reduce/:id", fetchLongURL)
e.GET("/reduce/:code", fetchLURL)
e.Logger.Fatal(e.Start(":8080"))
}

34
backend/store.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"fmt"
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"os"
)
type CodeURLMap struct {
Code string `gorm:"primary_key" json:"code"`
LURL string `json:"lurl" gorm:"column:lurl"`
CreatedAt time.Time `json:"created_at"`
}
var db *gorm.DB
func init() {
var err error
dbPath := os.Getenv("DB_PATH")
if dbPath == "" {
dbPath = "reduce.db"
}
db, err = gorm.Open("sqlite3", dbPath)
if err != nil {
panic(fmt.Sprintf("Failed to connect to database: %v", err))
}
// Auto-migrate database
db.AutoMigrate(&CodeURLMap{})
}