Switch from PostgreSQL to SQLite (modernc.org/sqlite, pure Go) for simpler deployment on the MCP platform. Fix URL normalization to preserve query parameters so sites like YouTube deduplicate correctly. Add Dockerfile, Makefile, and MCP service definition. Add pg2sqlite migration tool. Support $PORT env var for MCP port assignment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
2.8 KiB
Go
136 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.wntrmute.dev/kyle/goutils/config"
|
|
"git.wntrmute.dev/kyle/kls/links"
|
|
)
|
|
|
|
type server struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
//go:embed templates/*.tpl
|
|
var templateFiles embed.FS
|
|
|
|
var templates = template.Must(template.ParseFS(templateFiles, "templates/*.tpl"))
|
|
|
|
type page struct {
|
|
Short string
|
|
URLs []*links.URL
|
|
}
|
|
|
|
func (srv *server) servePage(w http.ResponseWriter, p page, tpl string) {
|
|
err := templates.ExecuteTemplate(w, tpl, p)
|
|
if err != nil {
|
|
log.Printf("error executing template: %s", err)
|
|
http.Error(w, fmt.Sprintf("template execution failed: %s", err.Error()),
|
|
http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (srv *server) postURL(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
stripQuery := boolean(r.FormValue("strip"))
|
|
|
|
url := r.FormValue("value")
|
|
if len(url) == 0 {
|
|
http.Error(w, "invalid URL", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
url, err = links.CleanString(url, stripQuery)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
short, err := links.StoreURL(ctx, srv.db, url)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
srv.servePage(w, page{Short: short}, "index.tpl")
|
|
}
|
|
|
|
func (srv *server) redirect(w http.ResponseWriter, r *http.Request) {
|
|
short := strings.TrimPrefix(r.URL.Path, "/")
|
|
u, err := links.RetrieveURL(context.Background(), srv.db, short)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, u, http.StatusFound)
|
|
}
|
|
|
|
func (srv *server) listAll(w http.ResponseWriter, r *http.Request) {
|
|
allPosts, err := links.FetchAll(context.Background(), srv.db)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
p := page{
|
|
URLs: allPosts,
|
|
}
|
|
|
|
srv.servePage(w, p, "list.tpl")
|
|
}
|
|
|
|
func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet && links.ValidShortCode.MatchString(r.URL.Path) {
|
|
srv.redirect(w, r)
|
|
return
|
|
}
|
|
|
|
user, pass, ok := r.BasicAuth()
|
|
username := config.Get("HTTP_USER")
|
|
password := config.Get("HTTP_PASS")
|
|
|
|
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="kls"`)
|
|
w.WriteHeader(401)
|
|
w.Write([]byte("Unauthorised.\n"))
|
|
return
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
srv.postURL(w, r)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/list" {
|
|
srv.listAll(w, r)
|
|
return
|
|
}
|
|
|
|
srv.servePage(w, page{}, "index.tpl")
|
|
}
|
|
|
|
func boolean(s string) bool {
|
|
switch s {
|
|
case "on", "true", "True", "yes", "Yes":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|