2024-11-24 21:36:03 -06:00
package main
import (
2024-11-24 22:05:22 -06:00
"database/sql"
2024-12-03 13:54:46 -08:00
"encoding/json"
2024-11-24 21:36:03 -06:00
"fmt"
2024-11-26 15:04:05 -06:00
"html/template"
2024-11-24 21:36:03 -06:00
"log"
"net/http"
"os"
2024-11-27 22:29:03 -06:00
"strings"
2024-11-26 22:20:10 -06:00
"time"
2024-11-24 22:05:22 -06:00
_ "github.com/lib/pq"
2024-11-26 22:20:10 -06:00
"golang.org/x/exp/rand"
2024-11-24 21:36:03 -06:00
)
2024-11-26 15:04:05 -06:00
type ShortLinks struct {
tmpl * template . Template
2024-12-03 15:01:42 -08:00
help * template . Template
2024-12-05 00:07:04 -08:00
list * template . Template
2024-11-26 15:04:05 -06:00
mux * http . ServeMux
2024-11-26 21:46:19 -06:00
db * sql . DB
2024-11-26 22:20:10 -06:00
r * rand . Rand
2024-11-27 22:29:03 -06:00
oai * oaiClient
2024-12-02 21:25:40 -08:00
domainAliases map [ string ] string
writableDomains map [ string ] bool
2024-12-06 11:59:31 -08:00
responseFormat * oaiResponseFormat
2024-11-26 15:04:05 -06:00
}
2024-11-27 22:29:03 -06:00
type setResponse struct {
2024-12-03 13:54:46 -08:00
Short string ` json:"short" `
Domain string ` json:"domain" `
2024-12-03 20:33:57 -08:00
URL string ` json:"url" `
2024-11-26 21:38:49 -06:00
}
2024-12-06 11:59:31 -08:00
type suggestRequest struct {
Shorts [ ] string ` json:"shorts,omitempty" `
Title string ` json:"title,omitempty" `
}
2024-11-27 22:29:03 -06:00
type suggestResponse struct {
Shorts [ ] string ` json:"shorts" `
2024-12-03 13:54:46 -08:00
Domain string ` json:"domain" `
2024-11-27 22:29:03 -06:00
}
2024-12-05 17:26:00 -08:00
type linkBase struct {
2024-12-05 00:07:04 -08:00
Short string ` json:"short" `
Long string ` json:"long" `
Domain string ` json:"domain" `
Generated bool ` json:"generated" `
2024-12-05 17:26:00 -08:00
URL string ` json:"url" `
}
type link struct {
linkBase
History [ ] linkHistory ` json:"history" `
}
type linkHistory struct {
linkBase
Until time . Time ` json:"until" `
2024-12-05 00:07:04 -08:00
}
2024-11-26 15:04:05 -06:00
2024-12-05 00:07:04 -08:00
func NewShortLinks ( db * sql . DB , domainAliases map [ string ] string , writableDomains map [ string ] bool ) ( * ShortLinks , error ) {
2024-12-05 17:26:00 -08:00
funcMap := template . FuncMap {
"lower" : strings . ToLower ,
"join" : strings . Join ,
}
tmpl , err := template . New ( "index.html" ) . Funcs ( funcMap ) . ParseFiles ( "static/index.html" )
2024-11-26 15:04:05 -06:00
if err != nil {
return nil , fmt . Errorf ( "static/index.html: %w" , err )
}
2024-12-05 17:26:00 -08:00
help , err := template . New ( "help.html" ) . Funcs ( funcMap ) . ParseFiles ( "static/help.html" )
2024-12-03 15:01:42 -08:00
if err != nil {
return nil , fmt . Errorf ( "static/help.html: %w" , err )
}
2024-12-05 17:26:00 -08:00
list , err := template . New ( "list.html" ) . Funcs ( funcMap ) . ParseFiles ( "static/list.html" )
2024-12-05 00:07:04 -08:00
if err != nil {
return nil , fmt . Errorf ( "static/list.html: %w" , err )
}
2024-11-27 22:29:03 -06:00
oai , err := newOAIClientFromEnv ( )
if err != nil {
return nil , fmt . Errorf ( "newOAIClientFromEnv: %w" , err )
}
2024-11-26 15:04:05 -06:00
sl := & ShortLinks {
tmpl : tmpl ,
2024-12-03 15:01:42 -08:00
help : help ,
2024-12-05 00:07:04 -08:00
list : list ,
2024-11-26 15:04:05 -06:00
mux : http . NewServeMux ( ) ,
2024-11-26 21:46:19 -06:00
db : db ,
2024-11-26 22:20:10 -06:00
r : rand . New ( rand . NewSource ( uint64 ( time . Now ( ) . UnixNano ( ) ) ) ) ,
2024-11-27 22:29:03 -06:00
oai : oai ,
2024-12-02 21:25:40 -08:00
domainAliases : domainAliases ,
writableDomains : writableDomains ,
2024-12-06 11:59:31 -08:00
responseFormat : & oaiResponseFormat {
Type : "json_schema" ,
JSONSchema : map [ string ] any {
"name" : "suggest_response" ,
"strict" : true ,
"schema" : map [ string ] any {
"type" : "object" ,
"properties" : map [ string ] any {
"shorts" : map [ string ] any {
"type" : "array" ,
"items" : map [ string ] any {
"type" : "string" ,
} ,
} ,
} ,
"required" : [ ] string { "shorts" } ,
"additionalProperties" : false ,
} ,
} ,
} ,
2024-11-26 15:04:05 -06:00
}
2024-11-26 21:38:49 -06:00
sl . mux . HandleFunc ( "GET /{$}" , sl . serveRoot )
2024-12-04 21:37:23 -08:00
sl . mux . HandleFunc ( "GET /_favicon.png" , sl . serveFavicon )
2024-12-05 00:07:04 -08:00
sl . mux . HandleFunc ( "GET /_help" , sl . serveHelp )
sl . mux . HandleFunc ( "GET /_list" , sl . serveList )
2025-10-10 16:19:47 -07:00
sl . mux . HandleFunc ( "GET /{short...}" , sl . serveShort )
2024-11-26 21:38:49 -06:00
sl . mux . HandleFunc ( "POST /{$}" , sl . serveSet )
2024-11-27 22:29:03 -06:00
sl . mux . HandleFunc ( "QUERY /{$}" , sl . serveSuggest )
2024-12-03 21:17:16 -08:00
sl . mux . HandleFunc ( "OPTIONS /{$}" , sl . serveOptions )
2024-11-26 15:04:05 -06:00
return sl , nil
}
func ( sl * ShortLinks ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
sl . mux . ServeHTTP ( w , r )
}
func ( sl * ShortLinks ) serveRoot ( w http . ResponseWriter , r * http . Request ) {
2024-12-03 21:17:16 -08:00
err := sl . initRequest ( w , r )
2024-12-03 13:54:46 -08:00
if err != nil {
2024-12-03 21:17:16 -08:00
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
2024-12-03 13:54:46 -08:00
return
}
2024-12-02 21:25:40 -08:00
2025-08-02 12:32:47 -07:00
short := strings . ToLower ( r . Form . Get ( "short" ) )
2024-12-03 13:15:36 -08:00
if sl . isWritable ( r . Host ) {
2025-08-02 12:32:47 -07:00
sl . serveRootWithShort ( w , r , short )
2024-12-03 13:15:36 -08:00
return
}
2025-04-21 10:35:48 -07:00
if sl . tryServeDomain ( w , r ) {
2024-12-02 22:54:22 -08:00
return
}
2025-04-21 10:35:48 -07:00
if sl . tryServeFakeRoot ( w , r ) {
2024-12-02 22:54:22 -08:00
return
}
2025-08-02 12:32:47 -07:00
sl . serveRootWithShort ( w , r , short )
2025-04-21 10:35:48 -07:00
}
func ( sl * ShortLinks ) tryServeFakeRoot ( w http . ResponseWriter , r * http . Request ) bool {
return sl . serveRedirect ( w , r , "_root" ) == nil
}
func ( sl * ShortLinks ) tryServeDomain ( w http . ResponseWriter , r * http . Request ) bool {
parts := strings . SplitN ( r . Host , "." , 2 )
if len ( parts ) != 2 {
return false
}
2025-08-02 12:32:47 -07:00
short := strings . ToLower ( parts [ 0 ] )
return sl . serveRedirect ( w , r , short ) == nil
2024-11-26 22:07:05 -06:00
}
2024-11-26 15:04:05 -06:00
2024-12-05 00:07:04 -08:00
func ( sl * ShortLinks ) serveRootWithShort ( w http . ResponseWriter , r * http . Request , short string ) {
2024-12-03 21:17:16 -08:00
err := sl . initRequest ( w , r )
2024-12-02 22:29:01 -08:00
if err != nil {
2024-12-03 21:17:16 -08:00
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
2024-12-02 22:29:01 -08:00
return
}
2024-12-02 22:54:22 -08:00
2024-12-02 21:25:40 -08:00
if ! sl . isWritable ( r . Host ) {
2025-04-21 10:35:48 -07:00
err = sl . serveRedirect ( w , r , "_404" )
if err != nil {
sendError ( w , http . StatusNotFound , "not found" )
return
}
2024-12-02 21:25:40 -08:00
return
}
2024-12-02 22:29:01 -08:00
err = sl . tmpl . Execute ( w , map [ string ] any {
2024-12-05 00:07:04 -08:00
"short" : short ,
"host" : sl . getDomain ( r . Host ) ,
"long" : r . Form . Get ( "long" ) ,
2024-12-06 11:59:31 -08:00
"title" : r . Form . Get ( "title" ) ,
2024-11-26 22:07:05 -06:00
} )
2024-11-26 15:04:05 -06:00
if err != nil {
sendError ( w , http . StatusInternalServerError , "error executing template: %s" , err )
return
}
}
2024-11-26 21:38:49 -06:00
func ( sl * ShortLinks ) serveShort ( w http . ResponseWriter , r * http . Request ) {
2024-12-04 21:37:23 -08:00
err := sl . initRequest ( w , r )
if err != nil {
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
return
}
2024-11-26 22:07:05 -06:00
2025-08-02 12:32:47 -07:00
short := strings . ToLower ( r . PathValue ( "short" ) )
2024-11-26 22:07:05 -06:00
2025-04-21 10:35:48 -07:00
err = sl . serveRedirect ( w , r , short )
2024-11-26 22:07:05 -06:00
if err != nil {
2024-12-05 00:07:04 -08:00
sl . serveRootWithShort ( w , r , short )
2024-11-26 22:07:05 -06:00
return
}
2025-04-21 10:35:48 -07:00
}
func ( sl * ShortLinks ) serveRedirect ( w http . ResponseWriter , r * http . Request , short string ) error {
long , err := sl . getLong ( short , sl . getDomain ( r . Host ) )
if err != nil {
return err
}
2024-11-26 22:07:05 -06:00
http . Redirect ( w , r , long , http . StatusTemporaryRedirect )
2025-04-21 10:35:48 -07:00
return nil
2024-11-26 21:38:49 -06:00
}
func ( sl * ShortLinks ) serveSet ( w http . ResponseWriter , r * http . Request ) {
2024-12-03 21:17:16 -08:00
err := sl . initRequest ( w , r )
2024-11-26 21:38:49 -06:00
if err != nil {
2024-12-03 21:17:16 -08:00
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
2024-11-26 21:38:49 -06:00
return
}
2024-12-02 21:25:40 -08:00
if ! sl . isWritable ( r . Host ) {
2024-12-02 23:03:48 -08:00
sendError ( w , http . StatusNotFound , "not found" )
2024-12-02 21:25:40 -08:00
return
}
2024-11-26 21:38:49 -06:00
2025-08-02 12:32:47 -07:00
short := strings . ToLower ( r . Form . Get ( "short" ) )
2024-12-02 21:13:34 -08:00
generated := false
2024-11-26 21:38:49 -06:00
2024-11-26 22:20:10 -06:00
if short == "" {
2024-12-02 21:25:40 -08:00
short , err = sl . genShort ( sl . getDomain ( r . Host ) )
2024-11-26 22:20:10 -06:00
if err != nil {
sendError ( w , http . StatusInternalServerError , "genShort: %s" , err )
return
}
2024-12-02 21:13:34 -08:00
generated = true
2024-11-26 22:20:10 -06:00
}
2024-11-26 21:38:49 -06:00
long := r . Form . Get ( "long" )
if long == "" {
sendError ( w , http . StatusBadRequest , "long= param required" )
return
}
2024-12-02 21:25:40 -08:00
_ , err = sl . db . Exec ( ` SELECT update_link($1, $2, $3, $4); ` , short , long , sl . getDomain ( r . Host ) , generated )
2024-11-26 21:46:19 -06:00
if err != nil {
2024-12-01 13:05:16 -08:00
sendError ( w , http . StatusInternalServerError , "update_link: %s" , err )
2024-11-26 21:46:19 -06:00
return
}
2024-11-26 21:38:49 -06:00
2024-11-27 22:29:03 -06:00
sendJSON ( w , setResponse {
2024-12-03 13:54:46 -08:00
Short : short ,
Domain : sl . getDomain ( r . Host ) ,
2024-12-03 20:33:57 -08:00
URL : fmt . Sprintf ( "https://%s/%s" , sl . getDomain ( r . Host ) , short ) ,
2024-11-26 21:38:49 -06:00
} )
}
2024-11-27 22:29:03 -06:00
func ( sl * ShortLinks ) serveSuggest ( w http . ResponseWriter , r * http . Request ) {
2024-12-03 21:17:16 -08:00
err := sl . initRequest ( w , r )
2024-11-27 22:29:03 -06:00
if err != nil {
2024-12-03 21:17:16 -08:00
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
2024-11-27 22:29:03 -06:00
return
}
2024-12-02 21:25:40 -08:00
if ! sl . isWritable ( r . Host ) {
2024-12-02 23:03:48 -08:00
sendError ( w , http . StatusNotFound , "not found" )
2024-12-02 21:25:40 -08:00
return
}
2024-11-27 22:29:03 -06:00
2024-12-06 11:59:31 -08:00
if ! r . Form . Has ( "shorts" ) && ! r . Form . Has ( "title" ) {
sendError ( w , http . StatusBadRequest , "shorts= or title= param required" )
2024-11-27 22:29:03 -06:00
return
}
2024-12-06 11:59:31 -08:00
in := suggestRequest {
Shorts : r . Form [ "shorts" ] ,
Title : r . Form . Get ( "title" ) ,
}
out := & suggestResponse { }
2024-11-27 22:29:03 -06:00
2024-12-06 11:59:31 -08:00
err = sl . oai . completeChat (
"You are an assistant helping a user choose useful short names for a URL shortener. The request contains JSON object where the optional `shorts` key contains a list of recent names chosen by the user, with the most recent names first, and the optional `title` key contains a title for the URL. Respond with only a JSON object where the `shorts` key contains a list of possible suggestions for additional short names. In descending order of preference, suggestions should include: plural/singular variations, 2 and 3 letter abbreivations, conceptual variations, other variations that are likely to be useful. Your bar for suggestions should be relatively high; responding with a shorter list of high quality suggestions is preferred." ,
in ,
sl . responseFormat ,
out ,
2024-11-27 22:29:03 -06:00
)
if err != nil {
sendError ( w , http . StatusInternalServerError , "oai.completeChat: %s" , err )
return
}
2024-12-06 12:07:18 -08:00
send := & suggestResponse {
Domain : sl . getDomain ( r . Host ) ,
}
for _ , short := range out . Shorts {
if short != "" {
send . Shorts = append ( send . Shorts , strings . ToLower ( strings . TrimSpace ( short ) ) )
}
}
2024-11-27 22:29:03 -06:00
2024-12-06 12:07:18 -08:00
sendJSON ( w , send )
2024-11-27 22:29:03 -06:00
}
2024-12-03 15:01:42 -08:00
func ( sl * ShortLinks ) serveHelp ( w http . ResponseWriter , r * http . Request ) {
2024-12-03 21:38:13 -08:00
err := sl . initRequest ( w , r )
if err != nil {
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
return
}
2024-12-03 15:01:42 -08:00
if ! sl . isWritable ( r . Host ) {
sendError ( w , http . StatusNotFound , "not found" )
return
}
2024-12-03 21:38:13 -08:00
err = sl . help . Execute ( w , map [ string ] any {
2024-12-03 21:17:16 -08:00
"writeHost" : r . Host ,
"readHost" : sl . getDomain ( r . Host ) ,
2024-12-03 15:01:42 -08:00
} )
if err != nil {
sendError ( w , http . StatusInternalServerError , "error executing template: %s" , err )
return
}
}
2024-12-03 21:17:16 -08:00
func ( sl * ShortLinks ) serveOptions ( w http . ResponseWriter , r * http . Request ) {
err := sl . initRequest ( w , r )
if err != nil {
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
return
}
w . WriteHeader ( http . StatusNoContent )
}
2024-12-02 21:13:34 -08:00
func ( sl * ShortLinks ) genShort ( domain string ) ( string , error ) {
2024-11-26 22:20:10 -06:00
for chars := 3 ; chars <= 10 ; chars ++ {
b := make ( [ ] byte , chars )
for i := range b {
2025-08-02 12:32:47 -07:00
b [ i ] = "0123456789abcdefghijklmnopqrstuvwxyz" [ sl . r . Intn ( 62 ) ]
2024-11-26 22:20:10 -06:00
}
short := string ( b )
exists := false
2024-12-02 21:13:34 -08:00
err := sl . db . QueryRow ( "SELECT EXISTS(SELECT 1 FROM links WHERE short = $1 AND domain = $2)" , short , domain ) . Scan ( & exists )
2024-11-26 22:20:10 -06:00
if err != nil {
return "" , fmt . Errorf ( "check exists: %w" , err )
}
if ! exists {
return short , nil
}
}
return "" , fmt . Errorf ( "no available short link found" )
}
2024-12-04 21:37:23 -08:00
func ( sl * ShortLinks ) serveFavicon ( w http . ResponseWriter , r * http . Request ) {
err := sl . initRequest ( w , r )
if err != nil {
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
return
}
w . Header ( ) . Set ( "Content-Type" , "image/png" )
http . ServeFile ( w , r , "static/favicon.png" )
}
2024-12-05 00:07:04 -08:00
func ( sl * ShortLinks ) serveList ( w http . ResponseWriter , r * http . Request ) {
err := sl . initRequest ( w , r )
if err != nil {
sendError ( w , http . StatusBadRequest , "init request: %s" , err )
return
}
2024-12-05 00:24:40 -08:00
if ! sl . isWritable ( r . Host ) {
sendError ( w , http . StatusNotFound , "not found" )
return
}
2024-12-05 17:26:00 -08:00
rows , err := sl . db . Query ( `
SELECT
short ,
long ,
domain ,
generated ,
CURRENT_TIMESTAMP as until ,
0 as is_history
FROM links
WHERE domain = $ 1
UNION ALL
SELECT
short ,
long ,
domain ,
generated ,
until ,
1 as is_history
FROM links_history
WHERE domain = $ 1
ORDER BY
short ASC ,
is_history ,
until DESC
` , sl . getDomain ( r . Host ) )
2024-12-05 00:07:04 -08:00
if err != nil {
sendError ( w , http . StatusInternalServerError , "select links: %s" , err )
return
}
defer rows . Close ( )
links := [ ] link { }
2024-12-05 17:26:00 -08:00
2024-12-05 00:07:04 -08:00
for rows . Next ( ) {
link := link { }
2024-12-05 17:26:00 -08:00
hist := linkHistory { }
isHistory := false
err := rows . Scan ( & link . Short , & link . Long , & link . Domain , & link . Generated , & hist . Until , & isHistory )
2024-12-05 00:07:04 -08:00
if err != nil {
sendError ( w , http . StatusInternalServerError , "scan link: %s" , err )
return
}
2024-12-05 17:26:00 -08:00
if ! isHistory {
link . URL = fmt . Sprintf ( "https://%s/%s" , link . Domain , link . Short )
links = append ( links , link )
} else {
hist . linkBase = link . linkBase
links [ len ( links ) - 1 ] . History = append ( links [ len ( links ) - 1 ] . History , hist )
}
2024-12-05 00:07:04 -08:00
}
err = sl . list . Execute ( w , map [ string ] any {
"links" : links ,
} )
if err != nil {
sendError ( w , http . StatusInternalServerError , "error executing template: %s" , err )
return
}
}
2024-12-02 21:25:40 -08:00
func ( sl * ShortLinks ) getDomain ( host string ) string {
if alias , ok := sl . domainAliases [ host ] ; ok {
return alias
}
return host
}
func ( sl * ShortLinks ) isWritable ( host string ) bool {
return sl . writableDomains [ host ]
}
2024-12-02 22:54:22 -08:00
func ( sl * ShortLinks ) getLong ( short , domain string ) ( string , error ) {
var long string
err := sl . db . QueryRow ( "SELECT long FROM links WHERE short = $1 AND domain = $2" , short , domain ) . Scan ( & long )
if err != nil {
return "" , err
}
return long , nil
}
2024-12-03 21:17:16 -08:00
func ( sl * ShortLinks ) initRequest ( w http . ResponseWriter , r * http . Request ) error {
log . Printf ( "%s %s %s %s %s %#v" , r . RemoteAddr , r . Method , r . Host , sl . getDomain ( r . Host ) , r . URL , r . Form )
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , "*" )
w . Header ( ) . Set ( "Access-Control-Allow-Methods" , "GET, POST, QUERY, OPTIONS" )
w . Header ( ) . Set ( "Access-Control-Allow-Headers" , "*" )
2024-12-03 13:54:46 -08:00
defer r . Body . Close ( )
err := r . ParseForm ( )
if err != nil {
return err
}
if r . Header . Get ( "Content-Type" ) == "application/json" {
dec := json . NewDecoder ( r . Body )
js := map [ string ] any { }
err := dec . Decode ( & js )
if err != nil {
return err
}
for k , v := range js {
switch v := v . ( type ) {
case [ ] any :
for _ , s := range v {
r . Form . Add ( k , fmt . Sprintf ( "%v" , s ) )
}
default :
r . Form . Set ( k , fmt . Sprintf ( "%v" , v ) )
}
}
}
return nil
}
2024-11-24 21:36:03 -06:00
func main ( ) {
port := os . Getenv ( "PORT" )
if port == "" {
2024-11-24 22:05:22 -06:00
log . Fatalf ( "please set PORT" )
}
pgConn := os . Getenv ( "PGCONN" )
if pgConn == "" {
log . Fatalf ( "please set PGCONN" )
}
2024-11-25 10:21:25 -06:00
db , err := sql . Open ( "postgres" , pgConn )
2024-11-24 22:05:22 -06:00
if err != nil {
log . Fatal ( err )
2024-11-24 21:36:03 -06:00
}
2024-12-01 13:05:16 -08:00
stmts := [ ] string {
`
CREATE TABLE IF NOT EXISTS links (
2024-12-02 21:13:34 -08:00
short VARCHAR ( 100 ) NOT NULL ,
long TEXT NOT NULL ,
domain VARCHAR ( 255 ) NOT NULL ,
generated BOOLEAN NOT NULL ,
PRIMARY KEY ( short , domain )
2024-12-01 13:05:16 -08:00
) ;
` ,
`
CREATE TABLE IF NOT EXISTS links_history (
short VARCHAR ( 100 ) ,
long TEXT NOT NULL ,
2024-12-02 21:13:34 -08:00
domain VARCHAR ( 255 ) NOT NULL ,
generated BOOLEAN NOT NULL ,
2024-12-01 13:05:16 -08:00
until TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
) ;
` ,
`
CREATE OR REPLACE FUNCTION update_link (
_short VARCHAR ( 100 ) ,
2024-12-02 21:13:34 -08:00
_long TEXT ,
_domain VARCHAR ( 255 ) ,
_generated BOOLEAN
2024-12-01 13:05:16 -08:00
) RETURNS void AS $ $
DECLARE
old RECORD ;
BEGIN
2024-12-02 21:13:34 -08:00
SELECT * INTO old FROM links WHERE short = _short AND domain = _domain ;
2024-12-01 13:05:16 -08:00
IF old IS NOT NULL THEN
2024-12-02 21:13:34 -08:00
INSERT INTO links_history ( short , long , domain , generated )
VALUES ( old . short , old . long , old . domain , old . generated ) ;
2024-12-01 13:05:16 -08:00
UPDATE links
2024-12-02 21:13:34 -08:00
SET long = _long , generated = _generated
WHERE short = _short AND domain = _domain ;
2024-12-01 13:05:16 -08:00
ELSE
2024-12-02 21:13:34 -08:00
INSERT INTO links ( short , long , domain , generated )
VALUES ( _short , _long , _domain , _generated ) ;
2024-12-01 13:05:16 -08:00
END IF ;
END ;
$ $ LANGUAGE plpgsql ;
` ,
}
for _ , stmt := range stmts {
_ , err := db . Exec ( stmt )
if err != nil {
log . Fatalf ( "Failed to create tables & functions: %v" , err )
}
2024-11-25 10:21:25 -06:00
}
2024-12-02 21:25:40 -08:00
domainAliases , err := loadDomainAliases ( )
if err != nil {
log . Fatalf ( "Failed to load domain aliases: %v" , err )
}
writableDomains , err := loadWritableDomains ( )
if err != nil {
log . Fatalf ( "Failed to load writable domains: %v" , err )
}
sl , err := NewShortLinks ( db , domainAliases , writableDomains )
2024-11-26 15:04:05 -06:00
if err != nil {
log . Fatalf ( "Failed to create shortlinks: %v" , err )
}
http . Handle ( "/" , sl )
2024-11-24 21:36:03 -06:00
bind := fmt . Sprintf ( ":%s" , port )
log . Printf ( "listening on %s" , bind )
if err := http . ListenAndServe ( bind , nil ) ; err != nil {
log . Fatalf ( "listen: %s" , err )
}
}
2024-12-02 21:25:40 -08:00
func loadDomainAliases ( ) ( map [ string ] string , error ) {
ret := map [ string ] string { }
s := os . Getenv ( "DOMAIN_ALIASES" )
if s == "" {
return ret , nil
}
for _ , pair := range strings . Split ( s , "," ) {
parts := strings . SplitN ( pair , "=" , 2 )
if len ( parts ) != 2 {
return nil , fmt . Errorf ( "invalid domain alias: %s" , pair )
}
ret [ parts [ 0 ] ] = parts [ 1 ]
}
return ret , nil
}
func loadWritableDomains ( ) ( map [ string ] bool , error ) {
ret := map [ string ] bool { }
s := os . Getenv ( "WRITABLE_DOMAINS" )
if s == "" {
return ret , nil
}
for _ , domain := range strings . Split ( s , "," ) {
ret [ domain ] = true
}
return ret , nil
}