diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abbe685 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/terse diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..de263dc --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +terse: ./url.go ./server.go ./cmd/terse/terse.go + go build ./cmd/terse + +clean: + rm -f ./terse diff --git a/cmd/terse/terse.go b/cmd/terse/terse.go new file mode 100644 index 0000000..64e7524 --- /dev/null +++ b/cmd/terse/terse.go @@ -0,0 +1,22 @@ +package main + +import ( + "log" + + "github.com/brettlangdon/go-arg" + "github.com/brettlangdon/terse" +) + +var args struct { + MaxEntries int `arg:"-m,--max,help:max number of links to keep [default: 1000]"` + Bind string `arg:"-b,--bind,help:\"[host]:\" to bind the server to [default: 127.0.0.1:5892]"` +} + +func main() { + args.MaxEntries = 1000 + args.Bind = "127.0.0.1:5892" + arg.MustParse(&args) + + server := terse.NewServer(args.Bind, args.MaxEntries) + log.Fatal(server.ListenAndServe()) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..ae99a2f --- /dev/null +++ b/server.go @@ -0,0 +1,61 @@ +package terse + +import ( + "bufio" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/golang/groupcache/lru" +) + +type Handler struct { + cache *lru.Cache +} + +func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + handler.HandleGet(w, r) + case "POST": + handler.HandlePost(w, r) + default: + http.Error(w, fmt.Sprintf("Method \"%s\" Not Allowed", r.Method), http.StatusMethodNotAllowed) + } +} + +func (handler *Handler) HandleGet(w http.ResponseWriter, r *http.Request) { + url := strings.Trim(r.URL.Path, "/") + code := strings.SplitN(url, "/", 2)[0] + value, ok := handler.cache.Get(code) + if ok { + switch url := value.(type) { + case string: + http.Redirect(w, r, url, http.StatusMovedPermanently) + return + } + } + http.NotFound(w, r) +} + +func (handler *Handler) HandlePost(w http.ResponseWriter, r *http.Request) { + reader := bufio.NewScanner(r.Body) + reader.Scan() + url, err := url.ParseRequestURI(reader.Text()) + if err != nil { + panic(err) + } + code := GetShortCode([]byte(url.String())) + handler.cache.Add(code, url.String()) + fmt.Fprintf(w, code) +} + +func NewServer(bind string, maxEntries int) *http.Server { + return &http.Server{ + Addr: bind, + Handler: &Handler{ + cache: lru.New(maxEntries), + }, + } +} diff --git a/url.go b/url.go new file mode 100644 index 0000000..16a7b2b --- /dev/null +++ b/url.go @@ -0,0 +1,20 @@ +package terse + +import ( + "bytes" + "hash/crc32" +) + +const ALPHABET string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~" +const BASE uint32 = uint32(len(ALPHABET)) + +func GetShortCode(url []byte) string { + var code bytes.Buffer + num := crc32.ChecksumIEEE(url) + for num > 0 { + next := (num % BASE) + code.WriteRune(rune(ALPHABET[next])) + num = num / 62 + } + return code.String() +}