diff --git a/.gitignore b/.gitignore index c036379..3c07df2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -tmp/ \ No newline at end of file +tmp/ +.env \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 4ce6623..f43f25c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,11 +1,14 @@ package main import ( + "database/sql" "flag" "fmt" + "log" "math/rand" "net/http" "net/url" + "os" "regexp" "strings" "time" @@ -15,6 +18,7 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + _ "github.com/lib/pq" ) const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" @@ -30,11 +34,40 @@ type StatsFormData struct { var linkMap = map[string]*models.Link{} var baseurl *string +var db *sql.DB func init() { - baseurl = flag.String("url", "127.0.0.1:8080", "The url (domain) that the server is running on") - + baseurl = flag.String("url", "127.0.0.1:8080", "The URL (domain) that the server is running on") flag.Parse() + + 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 := db.Ping(); err != nil { + panic(fmt.Sprintf("Failed to ping 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) + } + + if err := loadLinksIntoMemory(); err != nil { + log.Fatalf("Failed to load links into memory: %v", err) + } } func main() { @@ -45,11 +78,11 @@ func main() { e.Use(middleware.Secure()) e.GET("/stats", StatsHandler) - e.POST("/stats", StatsSubmissionHandler) e.GET("/:id", RedirectHandler) e.GET("/:id/", RedirectHandler) e.GET("/", IndexHandler) e.POST("/submit", SubmitHandler) + e.GET("/health", HealthHandler) e.Logger.Fatal(e.Start(":8080")) } @@ -68,6 +101,13 @@ func RedirectHandler(c echo.Context) error { link.Clicks = link.Clicks + 1 + 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) + return c.Redirect(http.StatusMovedPermanently, link.Url) } @@ -96,25 +136,30 @@ func SubmitHandler(c echo.Context) error { linkMap[id] = &models.Link{Id: id, Url: data.Url} + _, 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) + } + return views.Submission(id, *baseurl).Render(c.Request().Context(), c.Response()) } func StatsHandler(c echo.Context) error { - return views.StatsForm().Render(c.Request().Context(), c.Response()) + 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") + } + + return views.Stats(link).Render(c.Request().Context(), c.Response()) + } } -func StatsSubmissionHandler(c echo.Context) error { - var data StatsFormData - if err := c.Bind(&data); err != nil { - return err - } - - link, found := linkMap[data.Id] - if !found { - return c.String(http.StatusNotFound, "Id not found") - } - - return views.Stats(link).Render(c.Request().Context(), c.Response()) +func HealthHandler(c echo.Context) error { + return c.JSON(http.StatusOK, "ok") } func isURL(s string) bool { @@ -149,3 +194,21 @@ func generateRandomString(length int) string { } return string(result) } + +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() + + 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 +} diff --git a/docker-compose.yml b/docker-compose.yml index 66b000a..a802854 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,23 @@ services: - "8080:8080" restart: unless-stopped command: ["/shortr", "--url", "app.4rkal.com"] + environment: + DATABASE_URL: postgres://user:CKoA5cTWHVqBxaMz@postgres:5432/shortr?sslmode=disable + depends_on: + - postgres + + postgres: + image: postgres:15 + container_name: postgres_container + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: CKoA5cTWHVqBxaMz + POSTGRES_DB: shortr + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + +volumes: + pgdata: diff --git a/go.mod b/go.mod index fb3e755..db28720 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.6 require ( github.com/a-h/templ v0.2.778 github.com/labstack/echo/v4 v4.12.0 + github.com/lib/pq v1.10.9 ) require ( diff --git a/go.sum b/go.sum index cf31771..017dc9e 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+k 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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/views/base.templ b/views/base.templ index 39050ed..22a5e6e 100644 --- a/views/base.templ +++ b/views/base.templ @@ -180,7 +180,7 @@ templ Base(){ { children... } diff --git a/views/base_templ.go b/views/base_templ.go index d73f5ae..083ff55 100644 --- a/views/base_templ.go +++ b/views/base_templ.go @@ -37,7 +37,7 @@ func Base() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/stats.templ b/views/stats.templ index c80b49b..52b790f 100644 --- a/views/stats.templ +++ b/views/stats.templ @@ -8,7 +8,7 @@ import ( templ StatsForm(){ @Base(){ -
+
@@ -19,7 +19,8 @@ templ StatsForm(){ templ Stats(link *models.Link){ @Base(){ -

Statistics for {link.Url}

+

Statistics for {link.Id}

+

{link.Url}

Visitors {fmt.Sprintf("%v",link.Clicks)}

diff --git a/views/stats_templ.go b/views/stats_templ.go index 739d6d9..d57692e 100644 --- a/views/stats_templ.go +++ b/views/stats_templ.go @@ -46,7 +46,7 @@ func StatsForm() templ.Component { }() } ctx = templ.InitializeContext(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -98,27 +98,40 @@ func Stats(link *models.Link) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(link.Url) + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(link.Id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/stats.templ`, Line: 22, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/stats.templ`, Line: 22, Col: 31} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Visitors ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", link.Clicks)) + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(link.Url) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/stats.templ`, Line: 24, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/stats.templ`, Line: 23, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Visitors ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", link.Clicks)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/stats.templ`, Line: 25, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/views/submit.templ b/views/submit.templ index 7016910..2e9b92e 100644 --- a/views/submit.templ +++ b/views/submit.templ @@ -5,7 +5,7 @@ templ Submission(url, baseurl string){ @Base() {

Your url is: {baseurl + "/" + url}

-
+
diff --git a/views/submit_templ.go b/views/submit_templ.go index 3081533..1c34aaf 100644 --- a/views/submit_templ.go +++ b/views/submit_templ.go @@ -54,7 +54,7 @@ func Submission(url, baseurl string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("