diff --git a/.gitignore b/.gitignore index 27c099f..6d88c2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .bin -.vscode \ No newline at end of file +.vscode +riffraff +data.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 818b160..a27e67d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,11 @@ RUN make clean build FROM alpine ARG VERSION=dev +ARG COMMIT=00000000000000000000 LABEL maintainer="Adam Veldhousen " LABEL version=${VERSION} +LABEL commit=${COMMIT} WORKDIR /usr/local/bin COPY --from=build /go/src/riffraff/.bin/riffraff /usr/local/bin/ @@ -25,4 +27,4 @@ USER riffraff EXPOSE 8080 -ENTRYPOINT [ "/usr/local/bin/riffraff", "-port", "8080", "-accesslog"] \ No newline at end of file +ENTRYPOINT [ "/usr/local/bin/riffraff", "-bind", "127.0.0.1", "-port", "8080", "-accesslog"] \ No newline at end of file diff --git a/internal/http.go b/internal/http.go index 363601c..e6e4001 100644 --- a/internal/http.go +++ b/internal/http.go @@ -8,15 +8,15 @@ import ( "sync" ) -func NewServer(tp TemplateRenderer, accessLogging bool) http.Handler { +type Shortcuts map[string]string + +func NewServer(tp TemplateRenderer, shortcutStore *ShortcutStore, accessLogging bool) http.Handler { mux := http.NewServeMux() + shorts, _ := shortcutStore.LoadShortcuts(nil) ss := &CommandHandler{ - Mutex: &sync.Mutex{}, - Shortcuts: map[string]string{ - "*": DefaultSearchProvider, - "help": "/", - }, + Mutex: &sync.Mutex{}, + Shortcuts: shorts, } mux.HandleFunc("/", tp.RenderHandler("index.html", ss, nil)) @@ -25,27 +25,43 @@ func NewServer(tp TemplateRenderer, accessLogging bool) http.Handler { "Content-Type": []string{"application/opensearchdescription+xml"}, })) - handlerFunc := searchHandler(ss, accessLogging) + handlerFunc := searchHandler(ss, shortcutStore, accessLogging) mux.HandleFunc("/search", handlerFunc) mux.HandleFunc("/search_to_home", handlerFunc) return mux } -func searchHandler(scs *CommandHandler, logAccess bool) http.HandlerFunc { +func searchHandler(scs *CommandHandler, shortcutStore *ShortcutStore, logAccess bool) http.HandlerFunc { accessLogger := log.New(os.Stdout, "[access] ", log.Ldate|log.Lmicroseconds|log.Lshortfile) return func(res http.ResponseWriter, req *http.Request) { v := req.URL.Query() commandString := v.Get("q") + action, err := scs.Handle(commandString) if err != nil { - accessLogger.Printf("'%s' -> got error: '%s'", commandString, err.Error()) + if logAccess { + accessLogger.Printf("'%s' -> got error: '%s'", commandString, err.Error()) + } http.Redirect(res, req, fmt.Sprintf("https://duckduckgo.com?q=%s", commandString), http.StatusFound) + return } accessLogger.Printf("'%s' %s -> 302 %s", commandString, action.Action, action.Location) + + if action.Action != "lookup" { + if err := shortcutStore.SaveShortcuts(scs.Shortcuts, nil); err != nil { + if logAccess { + accessLogger.Printf("'%s' %s -> could not save shortcuts database file: %v", commandString, action.Action, err) + } + http.Error(res, err.Error(), http.StatusInternalServerError) + + return + } + } + http.Redirect(res, req, action.Location, http.StatusFound) } } diff --git a/internal/shortcuts.go b/internal/shortcuts.go index cc05de7..5b067cb 100644 --- a/internal/shortcuts.go +++ b/internal/shortcuts.go @@ -25,7 +25,6 @@ func (c *CommandHandler) Handle(input string) (Command, error) { rawArgs := strings.Fields(input) fragmentCount := len(rawArgs) farg := "*" - parameters := input var shortcut string @@ -46,7 +45,7 @@ func (c *CommandHandler) Handle(input string) (Command, error) { } } - return c.getShortcut(farg, parameters), nil + return c.getShortcut(farg, rawArgs...), nil } func (c *CommandHandler) updateShortcut(action, shortcut, location string) (Command, error) { @@ -84,11 +83,16 @@ func (c *CommandHandler) updateShortcut(action, shortcut, location string) (Comm return command, nil } -func (c *CommandHandler) getShortcut(key string, parameter string) Command { +func (c *CommandHandler) getShortcut(key string, input ...string) Command { + var parameter string + location, ok := c.Shortcuts[key] if !ok { location = DefaultSearchProvider key = "*" + parameter = strings.Join(input, " ") + } else { + parameter = strings.Join(input[1:], " ") } if strings.Contains(location, "%s") { @@ -102,6 +106,13 @@ func (c *CommandHandler) getShortcut(key string, parameter string) Command { } } +func NewDefaultShortcuts() Shortcuts { + return Shortcuts{ + "*": DefaultSearchProvider, + "help": "/", + } +} + type Command struct { Action string Name string diff --git a/internal/shortcuts_test.go b/internal/shortcuts_test.go index c42b18a..6ca0864 100644 --- a/internal/shortcuts_test.go +++ b/internal/shortcuts_test.go @@ -3,11 +3,12 @@ package internal import ( "fmt" "reflect" + "sync" "testing" ) func Test_Handle(t *testing.T) { - tests := []struct { + testHandleCases := []struct { name string input string want Command @@ -36,6 +37,15 @@ func Test_Handle(t *testing.T) { input: "add gh", wantErr: true, }, + { + name: "add search shortcut: 'add so https://stackvoerflow.com?q=%s'", + input: "add so https://stackoverflow.com?q=%s", + want: Command{ + Action: "add", + Name: "so", + Location: "https://stackoverflow.com?q=%s", + }, + }, { name: "remove shortcut: 'remove gh'", input: "remove gh", @@ -67,13 +77,25 @@ func Test_Handle(t *testing.T) { Location: "https://facebook.com", }, }, + { + name: "visit a search shortcut: 'go net/http'", + input: "go net/http", + want: Command{ + Action: "lookup", + Name: "go", + Location: "https://godoc.org/net/http", + }, + }, } - for _, tt := range tests { + for _, testCase := range testHandleCases { + tt := testCase t.Run(tt.name, func(t *testing.T) { cm := &CommandHandler{ + Mutex: &sync.Mutex{}, Shortcuts: map[string]string{ "fb": "https://facebook.com", + "go": "https://godoc.org/%s", }, } diff --git a/internal/storage.go b/internal/storage.go new file mode 100644 index 0000000..354667a --- /dev/null +++ b/internal/storage.go @@ -0,0 +1,120 @@ +package internal + +import ( + "encoding/json" + "io" + "log" + "os" +) + +type ShortcutStore struct { + Path string +} + +type ShortcutData struct { + Shortcuts Shortcuts `json:"shortcuts"` +} + +func (ss *ShortcutStore) Init() error { + defaultSS := NewDefaultShortcuts() + + if _, err := os.Stat(ss.Path); os.IsNotExist(err) { + log.Printf("file doesn't exist %s: %v", ss.Path, err) + + if err := ss.SaveShortcuts(defaultSS, nil); err != nil { + return err + } + + return nil + } + + if _, err := ss.LoadShortcuts(nil); err != nil { + return err + } + + return nil +} + +func (ss *ShortcutStore) SaveShortcuts(shorts Shortcuts, copyTo io.Writer) error { + file, err := os.Create(ss.Path) + if err != nil { + return err + } + + defer file.Close() + + temp := Shortcuts{} + + for k, v := range shorts { + if k != "help" { + temp[k] = v + } + } + + sc := ShortcutData{Shortcuts: temp} + + var target io.Writer + if copyTo != nil { + target = io.MultiWriter(file, copyTo) + } else { + target = file + } + + encoder := json.NewEncoder(target) + encoder.SetIndent("", "\t") + + if err := encoder.Encode(&sc); err != nil { + return err + } + + return nil +} + +func (ss *ShortcutStore) LoadShortcuts(copyTo io.Writer) (Shortcuts, error) { + var sc ShortcutData + + file, err := os.Open(ss.Path) + if err != nil { + if !os.IsExist(err) { + return NewDefaultShortcuts(), nil + } + + return sc.Shortcuts, err + } + + defer file.Close() + + finfo, err := file.Stat() + if err != nil { + return sc.Shortcuts, err + } + + if finfo.Size() == 0 { + return NewDefaultShortcuts(), nil + } + + var target io.Reader + + if copyTo != nil { + target = io.TeeReader(file, copyTo) + } else { + target = file + } + + decoder := json.NewDecoder(target) + if err := decoder.Decode(&sc); err != nil { + return sc.Shortcuts, err + } + + if sc.Shortcuts == nil { + return NewDefaultShortcuts(), nil + } + + sc.Shortcuts["help"] = "/" + + if v, ok := sc.Shortcuts["*"]; !ok || v == "" { + sc.Shortcuts["*"] = DefaultSearchProvider + } + + return sc.Shortcuts, nil +} diff --git a/internal/templates.go b/internal/templates.go index 182d51a..f5002c9 100644 --- a/internal/templates.go +++ b/internal/templates.go @@ -72,9 +72,11 @@ func (tr TemplateRenderer) RenderHandler(filename string, ss *CommandHandler, he return func(res http.ResponseWriter, req *http.Request) { templVar := ToTemplateVars(req, ss.Shortcuts) h := res.Header() + for k, v := range headers { h[k] = v } + h.Set("Cache-Control", "max-age 0; no-cache; private") if err := tr.Render(filename, templVar, res); err != nil { diff --git a/internal/templates/index.html.tpl b/internal/templates/index.html.tpl index a78010a..a5c5dd1 100644 --- a/internal/templates/index.html.tpl +++ b/internal/templates/index.html.tpl @@ -15,7 +15,7 @@
-

Hello internet - {{ .Host }} greets you.

+

I was only away for a minute... master.

diff --git a/main.go b/main.go index cf6fdd3..98ad029 100644 --- a/main.go +++ b/main.go @@ -12,17 +12,25 @@ import ( func main() { port := flag.Int("port", 80, "port to listen on") + bindAddr := flag.String("bind", "0.0.0.0", "interface to bind to") + dbPath := flag.String("data", "./data.json", "path to save shortcut database") enableAccessLogging := flag.Bool("accesslog", true, "Enable access logging") flag.Parse() box := packr.NewBox("./internal/templates") tp := internal.TemplateRenderer{FS: box} - server := internal.NewServer(tp, *enableAccessLogging) + ss := &internal.ShortcutStore{Path: *dbPath} + + if err := ss.Init(); err != nil { + log.Fatalf("could not access database file: %v", err) + } + + server := internal.NewServer(tp, ss, *enableAccessLogging) log.SetPrefix("[INFO] ") - addr := fmt.Sprintf("0.0.0.0:%d", *port) + addr := fmt.Sprintf("%s:%d", *bindAddr, *port) log.Printf("Listening @ %s", addr) log.Fatal(http.ListenAndServe(addr, server)) } diff --git a/makefile b/makefile index b4b8455..edaa973 100644 --- a/makefile +++ b/makefile @@ -1,32 +1,74 @@ APP := riffraff +PKGS := $(shell go list ./... | grep -v vendor) +GOBIN := $(GOPATH)/bin +LINTBIN := $(GOBIN)/golangci-lint +OUTDIR := .bin +BINARY := $(OUTDIR)/$(app) -dev: clean .bin/$(APP)-dev - ./.bin/$(APP)-dev -port 8080 -accesslog=true +GIT_SHA := $$(git rev-parse HEAD) +GIT_BRANCH := $$(git rev-parse --abbrev-ref HEAD) +VERSION := $(git desribe) + + +define SHORTCUT_DATA +{ + "shortcuts": { + "*": "https://duckduckgo.com/%s", + "fb": "https://facebook.com", + "gh": "https://github.com", + "gitemoji": "https://www.webfx.com/tools/emoji-cheat-sheet/" + } +} +endef +export SHORTCUT_DATA + +dev: clean $(BINARY)dev + ./$(BINARY)-dev -port 8080 -accesslog=true -data=data.json + +test: lint + go test -v -cover ./... + +lint: $(LINTBIN) + $(LINTBIN) run -p format -p unused -p bugs -p performance + +$(LINTBIN): + @GO111MODULE=off go get github.com/golangci/golangci-lint/cmd/golangci-lint package: - docker build --build-arg VERSION=$${VERSION:-dev} -t vdhsn/$(APP):$${VERSION:-dev} . + docker build --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(GIT_SHA) \ + -t vdhsn/$(APP):$(VERSION) . + docker tag vdhsn/$(APP):$(VERSION) vdhsn/$(APP):$(GIT_SHA) + docker tag vdhsn/$(APP):$(VERSION) vdhsn/$(APP):$(GIT_BRANCH) package-run: - docker run -it --rm --name riffraff -u 1000:1000 -p 8080:8080 vdhsn/$(APP):$${VERSION:-dev} + docker run -it --rm --name riffraff -u 1000:1000 -p 8080:8080 vdhsn/$(APP):$(GIT_SHA) publish: package - docker push vdhsn/$(APP):$${VERSION:-dev} + docker push vdhsn/$(APP):$(VERSION) + docker push vdhsn/$(APP):$(GIT_BRANCH) -build: .bin/$(APP) +build: $(BINARY) -.bin: - mkdir .bin +$(OUTDIR): + mkdir $@ -.bin/$(APP): packr .bin - packr build -o .bin/$(APP) -v . +$(BINARY): packr .bin + packr build -o $@ -v . -.bin/$(APP)-dev: .bin - go build -o .bin/$(APP)-dev -v . +$(BINARY)-dev: .bin + go build -o $(BINARY)-dev -v . clean: rm -rf .bin +clobber: clean + rm -rf data.json + +data.json: + @echo "$${SHORTCUT_DATA}" > data.json + packr: go get -u github.com/gobuffalo/packr/packr -.PHONY: build clean dev package package-run publish packr \ No newline at end of file +.PHONY: build clean clobber dev lint package package-run publish packr test