commit 3380bc5e65935bbed7be3f43191393942570338e Author: James Mills Date: Sun Jul 2 22:51:46 2017 -0700 Initial Commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..53bba36 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:alpine + +EXPOSE 8000/tcp + +ENTRYPOINT ["shorturl"] + +RUN \ + apk add --update git && \ + rm -rf /var/cache/apk/* + +RUN mkdir -p /go/src/shorturl +WORKDIR /go/src/shorturl + +COPY . /go/src/shorturl + +RUN go get -v -d +RUN go install -v diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..32275b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (C) 2017 James Mills + +httpfs is covered by the MIT license:: + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0f06925 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: dev build clean + +all: dev + +dev: build + ./shorturl -bind 127.0.0.1:8000 + +build: clean + go get ./... + go build -o ./shorturl . + +clean: + rm -rf bin shorturl diff --git a/README.md b/README.md new file mode 100644 index 0000000..66c7f14 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# shorturl +[![Build Status](https://travis-ci.org/prologic/shorturl.svg)](https://travis-ci.org/prologic/shorturl) +[![GoDoc](https://godoc.org/github.com/prologic/shorturl?status.svg)](https://godoc.org/github.com/prologic/shorturl) +[![Wiki](https://img.shields.io/badge/docs-wiki-blue.svg)](https://github.com/prologic/shorturl/wiki) +[![Go Report Card](https://goreportcard.com/badge/github.com/prologic/shorturl)](https://goreportcard.com/report/github.com/prologic/shorturl) +[![Coverage](https://coveralls.io/repos/prologic/shorturl/badge.svg)](https://coveralls.io/r/prologic/shorturl) + +shorturl is a web app that allows you to create smart bookmarks, commands and aliases by pointing your web browser's default search engine at a running instance. Similar to bunny1 or yubnub. + +## Installation + +### Source + +```#!bash +$ go install github.com/prologic/shorturl/... +``` + +### OS X Homebrew + +There is a formula provided that you can tap and install from +[prologic/homebrew-shorturl](https://github.com/prologic/homebrew-shorturl): + +```#!bash +$ brew tap prologic/shorturl +$ brew install shorturl +``` + +**NB:** This installs the latest released binary; so if you want a more +recent unreleased version from master you'll have to clone the repository +and build yourself. + +shorturl is still early days so contributions, ideas and expertise are +much appreciated and highly welcome! + +## Usage + +Run shorturl: + +```#!bash +$ shorturl -bind 127.0.0.1:8000 +``` + +Set your browser's default shorturl engine to http://localhost:8000/?q=%s + +Then type `help` to view the main help page, `g foo bar` to perform a [Google](https://google.com) search for "foo bar" or `list` to list all available commands. + +## License + +MIT diff --git a/config.go b/config.go new file mode 100644 index 0000000..fd28cd5 --- /dev/null +++ b/config.go @@ -0,0 +1,6 @@ +package main + +// Config ... +type Config struct { + FQDN string +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..2057603 --- /dev/null +++ b/config_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestZeroConfig(t *testing.T) { + assert := assert.New(t) + + cfg := Config{} + assert.Equal(cfg.FQDN, "") +} + +func TestConfig(t *testing.T) { + assert := assert.New(t) + + cfg := Config{FQDN: "bar.com"} + assert.Equal(cfg.FQDN, "bar.com") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..690e2e8 --- /dev/null +++ b/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "log" + + "github.com/boltdb/bolt" + "github.com/namsral/flag" +) + +var ( + db *bolt.DB + cfg Config +) + +func main() { + var ( + config string + dbpath string + bind string + fqdn string + ) + + flag.StringVar(&config, "config", "", "config file") + flag.StringVar(&dbpath, "dbpth", "urls.db", "Database path") + flag.StringVar(&bind, "bind", "0.0.0.0:8000", "[int]: to bind to") + flag.StringVar(&fqdn, "fqdn", "localhost", "FQDN for public access") + flag.Parse() + + // TODO: Abstract the Config and Handlers better + cfg.FQDN = fqdn + + var err error + db, err = bolt.Open(dbpath, 0600, nil) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + NewServer(bind, cfg).ListenAndServe() +} diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..051a0b7 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +echo -n "Version to tag: " +read TAG + +echo -n "Name of release: " +read NAME + +echo -n "Desc of release: " +read DESC + +git tag ${TAG} +git push --tags + +if [ ! -d ./bin ]; then + mkdir bin +else + rm -rf ./bin/* +fi + +echo -n "Building binaries ... " + +rice embed-go + +GOOS=linux GOARCH=amd64 go build -o ./bin/shorturl-Linux-x86_64 . +GOOS=linux GOARCH=arm64 go build -o ./bin/shorturl-Linux-x86_64 . +GOOS=darwin GOARCH=amd64 go build -o ./bin/shorturl-Darwin-x86_64 . +GOOS=windows GOARCH=amd64 go build -o ./bin/shorturl-Windows-x86_64.exe . + +echo "DONE" + +echo -n "Uploading binaries ... " + +github-release release \ + -u prologic -p -r shorturl \ + -t ${TAG} -n "${NAME}" -d "${DESC}" + +for file in bin/*; do + name="$(echo $file | sed -e 's|bin/||g')" + github-release upload -u prologic -r shorturl -t ${TAG} -n $name -f $file +done + +echo "DONE" diff --git a/server.go b/server.go new file mode 100644 index 0000000..1839ddd --- /dev/null +++ b/server.go @@ -0,0 +1,101 @@ +package main + +import ( + "log" + "net/http" + + "github.com/GeertJohan/go.rice" + "github.com/julienschmidt/httprouter" + "go.iondynamics.net/templice" +) + +// Server ... +type Server struct { + bind string + config Config + templates *templice.Template + router *httprouter.Router +} + +func (s *Server) render(w http.ResponseWriter, tmpl string, data interface{}) { + err := s.templates.ExecuteTemplate(w, tmpl+".html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// IndexHandler ... +func (s *Server) IndexHandler() httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + s.render(w, "index", nil) + } +} + +// ShortenHandler ... +func (s *Server) ShortenHandler() httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + err := r.ParseForm() + if err != nil { + http.Error(w, "Internal Error", http.StatusInternalServerError) + return + } + + url := r.Form.Get("url") + u, err := NewURL(url) + if err != nil { + http.Error(w, "Internal Error", http.StatusInternalServerError) + return + } + + s.render(w, "url", struct{ ID string }{ID: u.ID()}) + } +} + +// RedirectHandler ... +func (s *Server) RedirectHandler() httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + id := p.ByName("id") + if id == "" { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + u, ok := LookupURL(id) + if !ok { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + http.Redirect(w, r, u.url, http.StatusFound) + } +} + +// ListenAndServe ... +func (s *Server) ListenAndServe() { + log.Fatal(http.ListenAndServe(s.bind, s.router)) +} + +func (s *Server) initRoutes() { + s.router.GET("/", s.IndexHandler()) + s.router.POST("/", s.ShortenHandler()) + s.router.GET("/:id", s.RedirectHandler()) +} + +// NewServer ... +func NewServer(bind string, config Config) *Server { + server := &Server{ + bind: bind, + config: config, + router: httprouter.New(), + templates: templice.New(rice.MustFindBox("templates")), + } + + err := server.templates.Load() + if err != nil { + log.Panicf("error loading templates: %s", err) + } + + server.initRoutes() + + return server +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..722a9ab --- /dev/null +++ b/templates/index.html @@ -0,0 +1,14 @@ + + + shorturl + + + +

Enter your long URL here:

+
+ + +
+ + + diff --git a/templates/url.html b/templates/url.html new file mode 100644 index 0000000..c8948f8 --- /dev/null +++ b/templates/url.html @@ -0,0 +1,10 @@ + + + shorturl + + + +

Your short url is: {{.ID}}

+ + + diff --git a/urls.go b/urls.go new file mode 100644 index 0000000..110eaaf --- /dev/null +++ b/urls.go @@ -0,0 +1,96 @@ +package main + +import ( + "log" + + "github.com/boltdb/bolt" +) + +// URL ... +type URL struct { + id string + url string +} + +// ID ... +func (u URL) ID() string { + return u.id +} + +// URL ... +func (u URL) URL() string { + return u.url +} + +// Save ... +func (u URL) Save() error { + log.Printf("u: %v", u) + if u.id == "" || u.url == "" { + log.Printf("u is nil :/") + return nil + } + + err := db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte("urls")) + if err != nil { + log.Printf("create bucket failed: %s", err) + return err + } + + err = b.Put([]byte(u.id), []byte(u.url)) + if err != nil { + log.Printf("put key failed: %s", err) + return err + } + + return nil + }) + + return err +} + +// LookupURL ... +func LookupURL(id string) (u URL, ok bool) { + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("urls")) + if b == nil { + return nil + } + + v := b.Get([]byte(id)) + if v != nil { + u = URL{id: id, url: string(v)} + ok = true + } + + return nil + }) + + if err != nil { + log.Printf("error looking up url for %s: %s", id, err) + } + + return +} + +// NewURL ... +func NewURL(url string) (u URL, err error) { + log.Printf("NewURL: %v", url) + var id string + + for { + // TODO: Make length (5) configurable + id = RandomString(5) + _, ok := LookupURL(id) + if ok { + continue + } else { + break + } + } + + u = URL{id: id, url: url} + err = u.Save() + + return +} diff --git a/urls_test.go b/urls_test.go new file mode 100644 index 0000000..9918949 --- /dev/null +++ b/urls_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "testing" + + "github.com/boltdb/bolt" + + "github.com/stretchr/testify/assert" +) + +func TestZeroURL(t *testing.T) { + assert := assert.New(t) + + u := URL{} + assert.Equal(u.ID(), "") + assert.Equal(u.URL(), "") +} + +func TestURLSaveLookup(t *testing.T) { + assert := assert.New(t) + + db, _ = bolt.Open("test.db", 0600, nil) + defer db.Close() + + URL{id: "asdf", url: "https://localhost"}.Save() + + u, ok := LookupURL("asdf") + assert.True(ok) + assert.Equal(u.id, "asdf") + assert.Equal(u.url, "https://localhost") +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..d5fb2b8 --- /dev/null +++ b/utils.go @@ -0,0 +1,19 @@ +package main + +import ( + "crypto/rand" +) + +const ( + alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +) + +// RandomString ... +func RandomString(length int) string { + bytes := make([]byte, length) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphabet[b%byte(len(alphabet))] + } + return string(bytes) +}