add postgresql support

This commit is contained in:
4rkal 2024-12-23 16:31:53 +02:00
parent 9f1a5ae971
commit 3968e10d20
11 changed files with 130 additions and 29 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
tmp/ tmp/
.env

View File

@ -1,11 +1,14 @@
package main package main
import ( import (
"database/sql"
"flag" "flag"
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"os"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -15,6 +18,7 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
_ "github.com/lib/pq"
) )
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@ -30,11 +34,40 @@ type StatsFormData struct {
var linkMap = map[string]*models.Link{} var linkMap = map[string]*models.Link{}
var baseurl *string var baseurl *string
var db *sql.DB
func init() { 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() 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() { func main() {
@ -45,11 +78,11 @@ func main() {
e.Use(middleware.Secure()) e.Use(middleware.Secure())
e.GET("/stats", StatsHandler) e.GET("/stats", StatsHandler)
e.POST("/stats", StatsSubmissionHandler)
e.GET("/:id", RedirectHandler) e.GET("/:id", RedirectHandler)
e.GET("/:id/", RedirectHandler) e.GET("/:id/", RedirectHandler)
e.GET("/", IndexHandler) e.GET("/", IndexHandler)
e.POST("/submit", SubmitHandler) e.POST("/submit", SubmitHandler)
e.GET("/health", HealthHandler)
e.Logger.Fatal(e.Start(":8080")) e.Logger.Fatal(e.Start(":8080"))
} }
@ -68,6 +101,13 @@ func RedirectHandler(c echo.Context) error {
link.Clicks = link.Clicks + 1 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) 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} 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()) return views.Submission(id, *baseurl).Render(c.Request().Context(), c.Response())
} }
func StatsHandler(c echo.Context) error { func StatsHandler(c echo.Context) error {
id := c.QueryParam("id")
if id == "" {
return views.StatsForm().Render(c.Request().Context(), c.Response()) return views.StatsForm().Render(c.Request().Context(), c.Response())
} } else {
link, found := linkMap[id]
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 { if !found {
return c.String(http.StatusNotFound, "Id not found") return c.String(http.StatusNotFound, "Id not found")
} }
return views.Stats(link).Render(c.Request().Context(), c.Response()) 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 { func isURL(s string) bool {
@ -149,3 +194,21 @@ func generateRandomString(length int) string {
} }
return string(result) 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
}

View File

@ -8,3 +8,23 @@ services:
- "8080:8080" - "8080:8080"
restart: unless-stopped restart: unless-stopped
command: ["/shortr", "--url", "app.4rkal.com"] 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:

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.22.6
require ( require (
github.com/a-h/templ v0.2.778 github.com/a-h/templ v0.2.778
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.12.0
github.com/lib/pq v1.10.9
) )
require ( require (

2
go.sum
View File

@ -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/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 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

View File

@ -180,7 +180,7 @@ templ Base(){
{ children... } { children... }
</div> </div>
<footer> <footer>
With ❤️ by <a href="https://4rkal.com">4rkal</a> With ❤️ by <a href="https://4rkal.com">4rkal</a> | <a href="https://github.com/4rkal/shortr">Github Repo</a>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -37,7 +37,7 @@ func Base() templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><footer>With ❤️ by <a href=\"https://4rkal.com\">4rkal</a></footer></body></html>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><footer>With ❤️ by <a href=\"https://4rkal.com\">4rkal</a> | <a href=\"https://github.com/4rkal/shortr\">Github Repo</a></footer></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -8,7 +8,7 @@ import (
templ StatsForm(){ templ StatsForm(){
@Base(){ @Base(){
<form action="/stats" method="post"> <form action="/stats" method="get">
<label for="name">Website ID:</label> <label for="name">Website ID:</label>
<input type="text" id="id" name="id" required> <input type="text" id="id" name="id" required>
<br> <br>
@ -19,7 +19,8 @@ templ StatsForm(){
templ Stats(link *models.Link){ templ Stats(link *models.Link){
@Base(){ @Base(){
<h1>Statistics for {link.Url}</h1> <h1>Statistics for {link.Id}</h1>
<h2>{link.Url}</h2>
<h2>Visitors {fmt.Sprintf("%v",link.Clicks)}</h2> <h2>Visitors {fmt.Sprintf("%v",link.Clicks)}</h2>

View File

@ -46,7 +46,7 @@ func StatsForm() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form action=\"/stats\" method=\"post\"><label for=\"name\">Website ID:</label> <input type=\"text\" id=\"id\" name=\"id\" required><br><input type=\"submit\" value=\"Submit\"></form>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form action=\"/stats\" method=\"get\"><label for=\"name\">Website ID:</label> <input type=\"text\" id=\"id\" name=\"id\" required><br><input type=\"submit\" value=\"Submit\"></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -98,27 +98,40 @@ func Stats(link *models.Link) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string 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 { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><h2>Visitors ") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><h2>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string 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 { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2><h2>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("</h2>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err

View File

@ -5,7 +5,7 @@ templ Submission(url, baseurl string){
@Base() { @Base() {
<h1>Your url is: <code>{baseurl + "/" + url}</code></h1> <h1>Your url is: <code>{baseurl + "/" + url}</code></h1>
<form action="/stats" method="post"> <form action="/stats" method="get">
<input type="hidden" id="id" name="id" value={url}> <input type="hidden" id="id" name="id" value={url}>
<button type="submit">Stats</button> <button type="submit">Stats</button>
</form> </form>

View File

@ -54,7 +54,7 @@ func Submission(url, baseurl string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code></h1><form action=\"/stats\" method=\"post\"><input type=\"hidden\" id=\"id\" name=\"id\" value=\"") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code></h1><form action=\"/stats\" method=\"get\"><input type=\"hidden\" id=\"id\" name=\"id\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }