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
|
||||
|
||||
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
|
||||
|
||||
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
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