v0.1 I guess

This commit is contained in:
Léo 2024-03-28 11:29:32 +01:00
parent f0f27aa3cf
commit 77ee9da9ea
16 changed files with 401 additions and 2 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
court

14
Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM golang:1.22-alpine3.19
WORKDIR /src
COPY . /src
RUN go mod tidy && go build
FROM scratch
WORKDIR /app
COPY templates templates
COPY --from=0 /src/court court
EXPOSE 8080
CMD ["/app/court"]

101
database/db.go Normal file
View file

@ -0,0 +1,101 @@
package database
import (
"court/models"
"court/utils"
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
type DB struct {
db *sql.DB
}
func (self DB) CreateRandUrl(url string) (error, string) {
uuid := utils.GenerateUUID()
_, err := self.db.Exec("INSERT INTO court (id, url) VALUES ($1, $2)", uuid, url)
if err != nil {
return err, ""
}
return nil, uuid
}
func (self DB) CreateChosenUrl(id, url string) (error, string) {
_, err := self.db.Exec("INSERT INTO court (id, url) VALUES ($1, $2)", id, url)
if err != nil {
return err, ""
}
return nil, id
}
func (self DB) Close() {
self.db.Close()
}
func (self DB) GetAllUrls() (error, *[]models.Court) {
var res models.Court
var courts []models.Court
rows, err := self.db.Query("SELECT * FROM court")
defer rows.Close()
if err != nil {
return err, nil
}
for rows.Next() {
err = rows.Scan(&res.ID, &res.URL)
if err != nil {
return err, nil
}
courts = append(courts, res)
}
return nil, &courts
}
func (self DB) GetUrlByID(id string) (error, string) {
var reponse string
err := self.db.QueryRow("SELECT url FROM court WHERE id=$1", id).Scan(&reponse)
if err != nil {
if err == sql.ErrNoRows {
return nil, ""
}
return err, ""
}
return nil, reponse
}
func (self DB) DeleteByID(id string) (error, int) {
// Check if id exist in table
err, url := self.GetUrlByID(id)
if err != nil {
return err, 500
}
if url == "" {
return nil, 404
}
_, err = self.db.Exec("DELETE FROM court WHERE id=$1", id)
if err != nil {
return err, 500
}
return nil, 200
}
func ConnectDB() (error, *DB) {
dbuser := utils.Getenv("DB_USER", "postgres")
dbname := utils.Getenv("DB_NAME", "court")
dbhost := utils.Getenv("DB_HOST", "localhost")
dbpassword := utils.Getenv("DB_PASSWORD", "bonjour")
dbport := utils.Getenv("DB_PORT", "5432")
connStr := fmt.Sprintf("host=%s user=%s password=%s port=%s dbname=%s sslmode=disable", dbhost, dbuser, dbpassword, dbport, dbname)
db, err := sql.Open("postgres", connStr)
if err != nil {
return err, nil
}
err = db.Ping()
if err != nil {
return err, nil
}
return nil, &DB{db}
}

18
docker-compose.yaml Normal file
View file

@ -0,0 +1,18 @@
services:
db:
image: postgres:16-alpine
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: bonjour
POSTGRES_DB: court
volumes:
- ./initdb/:/docker-entrypoint-initdb.d
court:
build: .
ports:
- 8000:8080
environment:
DB_HOST: db
depends_on:
- db

2
go.mod
View file

@ -1,3 +1,5 @@
module court
go 1.22.0
require github.com/lib/pq v1.10.9

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=

87
handlers/court.go Normal file
View file

@ -0,0 +1,87 @@
package handlers
import (
"court/database"
"court/models"
"fmt"
"html/template"
"net/http"
)
var (
DB *database.DB
BaseURL string
)
func SetupDB() error {
var err error
err, DB = database.ConnectDB()
if err != nil {
return err
}
return nil
}
func Redirect(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
err, url := DB.GetUrlByID(id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
if url == "" {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found"))
return
}
http.Redirect(w, r, url, http.StatusSeeOther)
}
func Delete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_, status := DB.DeleteByID(id)
if status == 500 {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
} else if status == 404 {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found"))
} else {
fmt.Fprint(w, "")
}
}
func Post(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
id := r.FormValue("id")
url := r.FormValue("url")
tmpl := template.Must(template.ParseFiles("templates/fragments/link.html"))
if url == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Bad Request"))
return
}
if id == "" {
err, randid := DB.CreateRandUrl(url)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
tmpl.Execute(w, &models.Link{BaseUrl: BaseURL, ID: randid})
} else {
err, getid := DB.CreateChosenUrl(id, url)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
tmpl.Execute(w, &models.Link{BaseUrl: BaseURL, ID: getid})
}
}

20
handlers/webui.go Normal file
View file

@ -0,0 +1,20 @@
package handlers
import (
//"fmt"
"court/models"
"html/template"
"net/http"
)
func Webui(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/index.html"))
err, courts := DB.GetAllUrls()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
data := models.PageData{PageTitle: "Court - a URL Shortener", Courts: *courts}
tmpl.Execute(w, data)
}

5
initdb/01_table.sql Normal file
View file

@ -0,0 +1,5 @@
DROP TABLE IF EXISTS "court";
CREATE TABLE "public"."court" (
"id" text NOT NULL,
"url" text NOT NULL
) WITH (oids = false);

View file

@ -0,0 +1,2 @@
INSERT INTO court (id, url) VALUES ('aaaaaa', 'https://www.google.com/');
INSERT INTO court (id, url) VALUES ('bbbbbb', 'https://go.dev/')

27
main.go
View file

@ -1,7 +1,30 @@
package main
import "fmt"
import (
"court/handlers"
"court/utils"
"fmt"
"log"
"net/http"
)
func main() {
fmt.Printf("Hello, World!")
port := utils.Getenv("COURT_PORT", "8080")
address := utils.Getenv("COURT_ADDR", "0.0.0.0")
handlers.BaseURL = utils.Getenv("COURT_URL", "https://court.local/")
err := handlers.SetupDB()
if err != nil {
log.Panic(err)
}
defer handlers.DB.Close()
http.HandleFunc("GET /webui", handlers.Webui)
http.HandleFunc("GET /{id}", handlers.Redirect)
http.HandleFunc("DELETE /{id}", handlers.Delete)
http.HandleFunc("POST /", handlers.Post)
listen_addr := fmt.Sprintf("%s:%s", address, port)
fmt.Printf("\nListening on address: %s\n", listen_addr)
http.ListenAndServe(listen_addr, nil)
}

16
models/court.go Normal file
View file

@ -0,0 +1,16 @@
package models
type Court struct {
ID string
URL string
}
type Link struct {
BaseUrl string
ID string
}
type PageData struct {
PageTitle string
Courts []Court
}

View file

@ -0,0 +1,3 @@
<div class="mt-3">
<a href="/{{.ID}}">{{.BaseUrl}}{{.ID}}</a>
</div>

75
templates/index.html Normal file
View file

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<title>{{.PageTitle}}</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.9.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
</head>
<body>
<header class="flex flex-col w-full p-4 flex gap-4">
<h1 class="grid place-items-center text-5xl font-bold">{{.PageTitle}}</h1>
<input type="checkbox" value="light" class="toggle theme-controller"/>
</header>
<div class="divider"></div>
<div class="flex flex-col w-full p-4 flex gap-4">
<div class="grid place-items-center">
<h2 class="text-xl mt-2">Get a random link</h2>
<form id="randLink">
<div class="grid gap-3">
<label for="url" class="input input-bordered input-md flex items-center gap-2 mt-2">
Enter a URL
<input type="text" name="url" id="url" class="grow" placeholder="https://git.gnous.eu/" required>
</label>
</div>
<div class="grid gap-3 mt-2">
<button class="btn btn-accent" hx-post="/" hx-include="#randLink" hx-target="#randLink" hx-swap="outerHTML">Get Short Link</button>
</div>
</form>
<div class="divider"></div>
<h2 class="text-xl mt-2">Set the path of the link</h2>
<form id="setLink">
<label for="id" class="input input-bordered input-md flex items-center gap-2 mt-2">
Enter an ID
<input type="text" name="id" id="id" class="grow" placeholder="123456" required>
</label>
<label for="url" class="input input-bordered input-md flex items-center gap-2 mt-2">
Enter a URL
<input type="text" name="url" id="url" class="grow" placeholder="https://git.gnous.eu/" required>
</label>
<div class="grid gap-3 mt-2">
<button class="btn btn-accent" hx-post="/" hx-include="#setLink" hx-target="#setLink" hx-swap="outerHTML">Get Short Link</button>
</div>
</form>
</div>
<div class="divider"></div>
<div class="grid place-items-center py-24 px-10">
<table class="table">
<thead>
<tr>
<th>Path</th>
<th>URL</th>
<th>Delete</th>
</tr>
</thead>
{{range .Courts}}
<tbody>
<tr class="hover">
<td>/{{.ID}}</td>
<td>{{.URL}}</td>
<td><button class="btn btn-primary" hx-delete="/{{.ID}}" hx-target="closest tr" hx-swap="outerHTML">X</button></td>
</tr>
</tbody>
{{end}}
</table>
</div>
<div>
</body>
</html>

12
utils/utils.go Normal file
View file

@ -0,0 +1,12 @@
package utils
import "os"
func Getenv(environnement_variable, default_value string) string {
value := os.Getenv(environnement_variable)
if value == "" {
return default_value
} else {
return value
}
}

18
utils/uuid.go Normal file
View file

@ -0,0 +1,18 @@
package utils
import (
"math/rand/v2"
)
const (
rune = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"
rune_length = len(rune)
)
func GenerateUUID() string {
uuid := make([]byte, 6)
for idx := range 6 {
uuid[idx] = rune[rand.IntN(rune_length)]
}
return string(uuid)
}