diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9add109 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +court diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e93e8f0 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..8bc1bd1 --- /dev/null +++ b/database/db.go @@ -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} +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..a8d9e2c --- /dev/null +++ b/docker-compose.yaml @@ -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 diff --git a/go.mod b/go.mod index 13a69a8..078d3ac 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module court go 1.22.0 + +require github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aeddeae --- /dev/null +++ b/go.sum @@ -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= diff --git a/handlers/court.go b/handlers/court.go new file mode 100644 index 0000000..5f5a7ef --- /dev/null +++ b/handlers/court.go @@ -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}) + + } +} diff --git a/handlers/webui.go b/handlers/webui.go new file mode 100644 index 0000000..4ed21df --- /dev/null +++ b/handlers/webui.go @@ -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) +} diff --git a/initdb/01_table.sql b/initdb/01_table.sql new file mode 100644 index 0000000..483e8fd --- /dev/null +++ b/initdb/01_table.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS "court"; +CREATE TABLE "public"."court" ( + "id" text NOT NULL, + "url" text NOT NULL +) WITH (oids = false); diff --git a/initdb/02_default_data.sql b/initdb/02_default_data.sql new file mode 100644 index 0000000..9d89520 --- /dev/null +++ b/initdb/02_default_data.sql @@ -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/') diff --git a/main.go b/main.go index b3a9096..c34a325 100644 --- a/main.go +++ b/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) } diff --git a/models/court.go b/models/court.go new file mode 100644 index 0000000..b02e9ba --- /dev/null +++ b/models/court.go @@ -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 +} diff --git a/templates/fragments/link.html b/templates/fragments/link.html new file mode 100644 index 0000000..0e84564 --- /dev/null +++ b/templates/fragments/link.html @@ -0,0 +1,3 @@ +
+ {{.BaseUrl}}{{.ID}} +
diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a8e24d8 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,75 @@ + + + + + {{.PageTitle}} + + + + + + +
+

{{.PageTitle}}

+ +
+ +
+ +
+
+

Get a random link

+ + +
+ +

Set the path of the link

+ +
+
+
+ + + + + + + + + {{range .Courts}} + + + + + + + + + {{end}} +
PathURLDelete
/{{.ID}}{{.URL}}
+
+
+ + diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..4ef0268 --- /dev/null +++ b/utils/utils.go @@ -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 + } +} diff --git a/utils/uuid.go b/utils/uuid.go new file mode 100644 index 0000000..34f19db --- /dev/null +++ b/utils/uuid.go @@ -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) +}