diff --git a/internal/cli.go b/internal/cli.go new file mode 100644 index 0000000..6045b6a --- /dev/null +++ b/internal/cli.go @@ -0,0 +1,7 @@ +package internal + +type CLI struct { + File string `arg:"" name:"file" short:"f" help:"Path to CSV redirects file." type:"path"` + Host string `name:"host" help:"HTTP listen host." default:"${host}"` + Port string `name:"port" short:"p" help:"HTTP listen port." default:"${port}"` +} diff --git a/internal/csv.go b/internal/csv.go new file mode 100644 index 0000000..d22ea45 --- /dev/null +++ b/internal/csv.go @@ -0,0 +1,72 @@ +package internal + +import ( + "encoding/csv" + "errors" + "io" + "os" + "strconv" +) + +type Headings []string + +func (h Headings) IndexOf(heading string) int { + for i, value := range h { + if value == heading { + return i + } + } + + return -1 +} + +func ReadCsvRedirects(file string) (Redirects, error) { + reader, err := os.Open(file) + if err != nil { + return nil, err + } + + csvReader := csv.NewReader(reader) + + // Process headings + textHeadings, err := csvReader.Read() + if err != nil { + return nil, err + } + headings := Headings(textHeadings) + fromCol := headings.IndexOf("From") + toCol := headings.IndexOf("To") + statusCol := headings.IndexOf("Status Code") + if fromCol == -1 || toCol == -1 || statusCol == -1 { + return nil, errors.New("CSV must contain From, To, and Status Code headings") + } + + redirects := Redirects{} + for { + row, err := csvReader.Read() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + if row == nil { + break + } + + statusCode, err := strconv.Atoi(row[statusCol]) + if err != nil { + return nil, err + } + + redirect := &Redirect{ + From: row[fromCol], + To: row[toCol], + StatusCode: statusCode, + } + + redirects = append(redirects, redirect) + } + + return redirects, nil +} diff --git a/internal/http.go b/internal/http.go new file mode 100644 index 0000000..323f910 --- /dev/null +++ b/internal/http.go @@ -0,0 +1,26 @@ +package internal + +import ( + "net/http" + + "github.com/rs/zerolog" +) + +type HttpServer struct { + CLI *CLI + Log zerolog.Logger + Redirects Redirects +} + +func (srv *HttpServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + redirect := srv.Redirects.Find(req.URL.Path) + if redirect != nil { + srv.Log.Info().Int("result", redirect.StatusCode).Msg(req.URL.Path) + w.WriteHeader(redirect.StatusCode) + w.Header().Add("location", redirect.To) + } else { + statusCode := 404 + srv.Log.Error().Int("result", statusCode).Msg(req.URL.Path) + w.WriteHeader(statusCode) + } +} diff --git a/internal/redirect.go b/internal/redirect.go new file mode 100644 index 0000000..2cc8c22 --- /dev/null +++ b/internal/redirect.go @@ -0,0 +1,24 @@ +package internal + +type Redirect struct { + From string + To string + StatusCode int +} + +func (r *Redirect) Match(from string) bool { + return from == r.From +} + +type Redirects []*Redirect + +func (rs Redirects) Find(from string) *Redirect { + for i := 0; i < len(rs); i++ { + redirect := rs[i] + if redirect.Match(from) { + return redirect + } + } + + return nil +} diff --git a/main.go b/main.go index 9a3a0ee..09e4b1f 100644 --- a/main.go +++ b/main.go @@ -6,37 +6,27 @@ import ( "github.com/alecthomas/kong" "github.com/rs/zerolog" + "gogs.home.aneur.in/go/ultrashorty/internal" ) -type CLI struct { - File string `arg:"" name:"file" short:"f" help:"Path to CSV redirects file." type:"path"` - Host string `name:"host" help:"HTTP listen host." default:"${host}"` - Port string `name:"port" short:"p" help:"HTTP listen port." default:"${port}"` -} - -type HttpServer struct { - CLI *CLI - Log zerolog.Logger -} - -func (srv *HttpServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - statusCode := 404 - srv.Log.Trace().Int("result", statusCode).Msg(req.URL.Path) - w.WriteHeader(statusCode) -} - func main() { vars := kong.Vars{ "host": "localhost", "port": "4000", } - cli := &CLI{} + cli := &internal.CLI{} kong.Parse(cli, vars) - srv := &HttpServer{ - CLI: cli, - Log: zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Logger(), + redirects, err := internal.ReadCsvRedirects(cli.File) + if err != nil { + panic(err) + } + + srv := &internal.HttpServer{ + CLI: cli, + Log: zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Logger(), + Redirects: redirects, } addr := net.JoinHostPort(cli.Host, cli.Port) @@ -46,6 +36,6 @@ func main() { }() srv.Log.Info().Msgf("Listening at %s", addr) - err := <-errc + err = <-errc panic(err) }