Files
kls/handler.go
Kyle Isom 0fa85cb300 migrate to SQLite and prepare for MCP deployment
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>
2026-03-27 16:18:37 -07:00

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