mirror of
https://github.com/4rkal/shortr.git
synced 2025-01-19 15:03:38 +02:00
add postgresql support
This commit is contained in:
parent
9f1a5ae971
commit
3968e10d20
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
tmp/
|
||||
tmp/
|
||||
.env
|
95
cmd/main.go
95
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
|
||||
}
|
||||
|
@ -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:
|
||||
|
1
go.mod
1
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 (
|
||||
|
2
go.sum
2
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=
|
||||
|
@ -180,7 +180,7 @@ templ Base(){
|
||||
{ children... }
|
||||
</div>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -37,7 +37,7 @@ func Base() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
templ StatsForm(){
|
||||
@Base(){
|
||||
<form action="/stats" method="post">
|
||||
<form action="/stats" method="get">
|
||||
<label for="name">Website ID:</label>
|
||||
<input type="text" id="id" name="id" required>
|
||||
<br>
|
||||
@ -19,7 +19,8 @@ templ StatsForm(){
|
||||
|
||||
templ Stats(link *models.Link){
|
||||
@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>
|
||||
|
||||
|
@ -46,7 +46,7 @@ func StatsForm() templ.Component {
|
||||
}()
|
||||
}
|
||||
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 {
|
||||
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("</h1><h2>Visitors ")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><h2>")
|
||||
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("</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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
|
@ -5,7 +5,7 @@ templ Submission(url, baseurl string){
|
||||
@Base() {
|
||||
<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}>
|
||||
<button type="submit">Stats</button>
|
||||
</form>
|
||||
|
@ -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("</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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user