shortr/cmd/main.go

216 lines
4.5 KiB
Go
Raw Normal View History

2024-09-11 17:58:42 +03:00
package main
import (
2024-12-23 16:31:53 +02:00
"database/sql"
2024-09-12 21:59:49 +03:00
"flag"
2024-09-11 17:58:42 +03:00
"fmt"
2024-12-23 16:31:53 +02:00
"log"
2024-09-11 17:58:42 +03:00
"math/rand"
"net/http"
"net/url"
2024-12-23 16:31:53 +02:00
"os"
2024-09-12 21:10:09 +03:00
"regexp"
"strings"
2024-12-23 16:39:33 +02:00
"sync"
2024-09-11 17:58:42 +03:00
"time"
"github.com/4rkal/shortr/models"
"github.com/4rkal/shortr/views"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
2024-12-23 16:31:53 +02:00
_ "github.com/lib/pq"
2024-09-11 17:58:42 +03:00
)
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
type FormData struct {
Url string `form:"url"`
}
type StatsFormData struct {
Id string `form:"id"`
}
2024-09-11 21:57:40 +03:00
var linkMap = map[string]*models.Link{}
2024-12-23 16:39:33 +02:00
var linkMapMutex sync.RWMutex
2024-09-11 17:58:42 +03:00
2024-09-12 21:59:49 +03:00
var baseurl *string
2024-12-23 16:31:53 +02:00
var db *sql.DB
2024-09-12 21:59:49 +03:00
func init() {
2024-12-23 16:31:53 +02:00
baseurl = flag.String("url", "127.0.0.1:8080", "The URL (domain) that the server is running on")
2024-09-12 21:59:49 +03:00
flag.Parse()
2024-12-23 16:31:53 +02:00
var err error
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
panic(fmt.Sprintf("Failed to connect to database: %v", err))
}
if err := loadLinksIntoMemory(); err != nil {
panic(fmt.Sprintf("Failed to load links into memory: %v", err))
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS links (
id VARCHAR(255) PRIMARY KEY,
url TEXT NOT NULL,
clicks INTEGER NOT NULL DEFAULT 0
)
`)
if err != nil {
log.Fatal("Error creating table: ", err)
}
2024-09-12 21:59:49 +03:00
}
2024-09-11 17:58:42 +03:00
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.Secure())
e.GET("/stats", StatsHandler)
e.GET("/:id", RedirectHandler)
2024-09-15 13:03:19 +03:00
e.GET("/:id/", RedirectHandler)
2024-09-11 17:58:42 +03:00
e.GET("/", IndexHandler)
e.POST("/submit", SubmitHandler)
2024-12-23 16:31:53 +02:00
e.GET("/health", HealthHandler)
2024-09-11 17:58:42 +03:00
e.Logger.Fatal(e.Start(":8080"))
}
func RedirectHandler(c echo.Context) error {
id := c.Param("id")
2024-12-23 16:39:33 +02:00
linkMapMutex.RLock()
2024-09-11 17:58:42 +03:00
link, found := linkMap[id]
2024-12-23 16:39:33 +02:00
linkMapMutex.RUnlock()
2024-09-11 17:58:42 +03:00
if !found {
return c.String(http.StatusNotFound, "Link not found")
}
2024-09-12 21:10:09 +03:00
if !strings.Contains(link.Url, "://") {
link.Url = "http://" + link.Url
}
2024-09-11 17:58:42 +03:00
link.Clicks = link.Clicks + 1
2024-12-23 16:31:53 +02:00
go func(id string, clicks int) {
_, err := db.Exec("UPDATE links SET clicks = $1 WHERE id = $2", clicks, id)
if err != nil {
fmt.Printf("Failed to update clicks for id %s: %v\n", id, err)
}
}(id, link.Clicks)
2024-09-11 17:58:42 +03:00
return c.Redirect(http.StatusMovedPermanently, link.Url)
}
func IndexHandler(c echo.Context) error {
return views.Index().Render(c.Request().Context(), c.Response())
}
func SubmitHandler(c echo.Context) error {
var data FormData
if err := c.Bind(&data); err != nil {
return err
}
fmt.Println(data)
if !isURL(data.Url) {
return c.JSON(http.StatusBadRequest, "not a valid url")
}
var id string
for {
id = generateRandomString(6)
if _, exists := linkMap[id]; !exists {
break
}
}
2024-12-23 16:39:33 +02:00
linkMapMutex.Lock()
2024-09-11 17:58:42 +03:00
linkMap[id] = &models.Link{Id: id, Url: data.Url}
2024-12-23 16:39:33 +02:00
linkMapMutex.Unlock()
2024-09-11 17:58:42 +03:00
2024-12-23 16:31:53 +02:00
_, err := db.Exec("INSERT INTO links (id, url, clicks) VALUES ($1, $2, $3)", id, data.Url, 0)
if err != nil {
return fmt.Errorf("failed to save link to database: %v", err)
}
2024-09-12 21:59:49 +03:00
return views.Submission(id, *baseurl).Render(c.Request().Context(), c.Response())
2024-09-11 17:58:42 +03:00
}
func StatsHandler(c echo.Context) error {
2024-12-23 16:31:53 +02:00
id := c.QueryParam("id")
if id == "" {
return views.StatsForm().Render(c.Request().Context(), c.Response())
} else {
link, found := linkMap[id]
if !found {
return c.String(http.StatusNotFound, "Id not found")
}
2024-09-11 17:58:42 +03:00
2024-12-23 16:31:53 +02:00
return views.Stats(link).Render(c.Request().Context(), c.Response())
2024-09-11 17:58:42 +03:00
}
2024-12-23 16:31:53 +02:00
}
2024-09-11 17:58:42 +03:00
2024-12-23 16:31:53 +02:00
func HealthHandler(c echo.Context) error {
return c.JSON(http.StatusOK, "ok")
2024-09-11 17:58:42 +03:00
}
func isURL(s string) bool {
2024-09-12 21:10:09 +03:00
if !strings.Contains(s, "://") {
s = "http://" + s
}
parsedUrl, err := url.ParseRequestURI(s)
if err != nil {
return false
}
if parsedUrl.Host == "" {
return false
}
domainRegex := `^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`
matched, err := regexp.MatchString(domainRegex, parsedUrl.Host)
if err != nil || !matched {
return false
}
return true
2024-09-11 17:58:42 +03:00
}
func generateRandomString(length int) string {
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
var result []byte
for i := 0; i < length; i++ {
index := seededRand.Intn(len(charset))
result = append(result, charset[index])
}
return string(result)
}
2024-12-23 16:31:53 +02:00
func loadLinksIntoMemory() error {
rows, err := db.Query("SELECT id, url, clicks FROM links")
if err != nil {
return fmt.Errorf("query error: %v", err)
}
defer rows.Close()
2024-12-23 16:39:33 +02:00
linkMapMutex.Lock()
defer linkMapMutex.Unlock()
2024-12-23 16:31:53 +02:00
for rows.Next() {
var id, url string
var clicks int
if err := rows.Scan(&id, &url, &clicks); err != nil {
return fmt.Errorf("scan error: %v", err)
}
linkMap[id] = &models.Link{Id: id, Url: url, Clicks: clicks}
}
return nil
}