v0.1 I guess
This commit is contained in:
parent
f0f27aa3cf
commit
77ee9da9ea
16 changed files with 401 additions and 2 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
court
|
14
Dockerfile
Normal file
14
Dockerfile
Normal 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
101
database/db.go
Normal 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
18
docker-compose.yaml
Normal 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
2
go.mod
|
@ -1,3 +1,5 @@
|
||||||
module court
|
module court
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
|
require github.com/lib/pq v1.10.9
|
||||||
|
|
2
go.sum
Normal file
2
go.sum
Normal 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
87
handlers/court.go
Normal 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
20
handlers/webui.go
Normal 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
5
initdb/01_table.sql
Normal 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);
|
2
initdb/02_default_data.sql
Normal file
2
initdb/02_default_data.sql
Normal 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
27
main.go
|
@ -1,7 +1,30 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"court/handlers"
|
||||||
|
"court/utils"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
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
16
models/court.go
Normal 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
|
||||||
|
}
|
3
templates/fragments/link.html
Normal file
3
templates/fragments/link.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/{{.ID}}">{{.BaseUrl}}{{.ID}}</a>
|
||||||
|
</div>
|
75
templates/index.html
Normal file
75
templates/index.html
Normal 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
12
utils/utils.go
Normal 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
18
utils/uuid.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue