Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
1030e9c614 |
|
|
@ -13,7 +13,7 @@
|
||||||
A browser omni search bar enhancer. Side effects may include enhanced productivity, happiness and less google search tracking.
|
A browser omni search bar enhancer. Side effects may include enhanced productivity, happiness and less google search tracking.
|
||||||
|
|
||||||
|
|
||||||
## How to install
|
## How to use
|
||||||
|
|
||||||
- Install it:
|
- Install it:
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: riffraff
|
|
||||||
labels:
|
|
||||||
name: riffraff
|
|
||||||
app: riffraff
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
serviceName: riffraff
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: riffraff
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
name: riffraff
|
|
||||||
app: riffraff
|
|
||||||
spec:
|
|
||||||
restartPolicy: Always
|
|
||||||
containers:
|
|
||||||
- image: vdhsn/riffraff:v0.2.2
|
|
||||||
name: riffraff
|
|
||||||
stdin: true
|
|
||||||
tty: true
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: "64m"
|
|
||||||
memory: "128M"
|
|
||||||
requests:
|
|
||||||
cpu: "64m"
|
|
||||||
memory: "128M"
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
volumeMounts:
|
|
||||||
- name: riffraff-data
|
|
||||||
mountPath: /data/
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
securityContext:
|
|
||||||
runAsUser: 1000
|
|
||||||
runAsGroup: 1000
|
|
||||||
fsGroup: 1000
|
|
||||||
volumes:
|
|
||||||
- name: riffraff-data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: riffraff-data-claim
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: riffraff
|
|
||||||
spec:
|
|
||||||
type: LoadBalancer
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
name: web
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 8080
|
|
||||||
selector:
|
|
||||||
app: riffraff
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
|
||||||
kind: IngressRoute
|
|
||||||
metadata:
|
|
||||||
name: riffraff-server-http-tls
|
|
||||||
spec:
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
routes:
|
|
||||||
- match: Host(`riffraff.vdhsn.com`, `riffraff.veldhousen.com`)
|
|
||||||
kind: Rule
|
|
||||||
services:
|
|
||||||
- name: riffraff
|
|
||||||
port: 80
|
|
||||||
middlewares:
|
|
||||||
- name: ssl-redirect-header
|
|
||||||
- name: gzip
|
|
||||||
tls:
|
|
||||||
certResolver: default
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: riffraff-data-claim
|
|
||||||
spec:
|
|
||||||
volumeMode: Filesystem
|
|
||||||
storageClassName: standard
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 10Mi
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
name: riffraff-data-pv
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolume
|
|
||||||
metadata:
|
|
||||||
name: riffraff-data-pv
|
|
||||||
labels:
|
|
||||||
name: riffraff-data-pv
|
|
||||||
spec:
|
|
||||||
volumeMode: Filesystem
|
|
||||||
storageClassName: standard
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce #type of access
|
|
||||||
capacity:
|
|
||||||
storage: 10Mi #Size of the volume
|
|
||||||
hostPath:
|
|
||||||
path: "/storage/volumes/riffraff"
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -4,6 +4,7 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gobuffalo/packr v1.30.1
|
github.com/gobuffalo/packr v1.30.1
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible
|
||||||
github.com/golangci/golangci-lint v1.21.0 // indirect
|
github.com/golangci/golangci-lint v1.21.0 // indirect
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
3
go.sum
3
go.sum
|
|
@ -75,6 +75,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
|
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
|
||||||
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
|
@ -254,6 +256,7 @@ github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOV
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
type Bookmark struct {
|
||||||
|
ID string
|
||||||
|
URL string
|
||||||
|
Name string
|
||||||
|
Tags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uint64
|
||||||
|
Username string
|
||||||
|
PasswordHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
AddUser(User) (uint64, error)
|
||||||
|
AddBookmark(string, string, []string) error
|
||||||
|
GetBookmarks(string) ([]Bookmark, error)
|
||||||
|
DeleteBookmark(string) error
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
uuid "github.com/satori/go.uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Shortcuts map[string]string
|
type Shortcuts map[string]string
|
||||||
|
|
@ -33,75 +26,5 @@ func NewServer(tp TemplateRenderer, shortcutStore *ShortcutStore, accessLogging
|
||||||
mux.HandleFunc("/search", handlerFunc)
|
mux.HandleFunc("/search", handlerFunc)
|
||||||
mux.HandleFunc("/search_to_home", handlerFunc)
|
mux.HandleFunc("/search_to_home", handlerFunc)
|
||||||
|
|
||||||
return accessLoggerMiddleware(mux)
|
return AccessLoggerMiddleware(mux)
|
||||||
}
|
|
||||||
|
|
||||||
func accessLoggerMiddleware(middleware http.Handler) http.Handler {
|
|
||||||
accessLogger := log.New(os.Stdout, "[access] ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
|
|
||||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
requestId := uuid.NewV4()
|
|
||||||
setupHeaders(res.Header(), requestId.String())
|
|
||||||
accessLogger.Printf("[%s] %s %s '%s'", requestId, getRemoteIP(req), req.Method, req.URL.String())
|
|
||||||
middleware.ServeHTTP(res, req.WithContext(context.WithValue(req.Context(), "id", requestId.String())))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchHandler(scs *CommandHandler, shortcutStore *ShortcutStore, logAccess bool) http.HandlerFunc {
|
|
||||||
searchLogger := log.New(os.Stdout, "[search] ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
|
|
||||||
return func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
requestId := req.Context().Value("id")
|
|
||||||
v := req.URL.Query()
|
|
||||||
commandString := v.Get("q")
|
|
||||||
|
|
||||||
if commandString == "" {
|
|
||||||
http.Redirect(res, req, "https://duckduckgo.com", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
action, err := scs.Handle(commandString)
|
|
||||||
if err != nil {
|
|
||||||
if logAccess {
|
|
||||||
searchLogger.Printf("[%s] '%s' -> got error: '%s'", requestId, commandString, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(res, req, fmt.Sprintf("https://duckduckgo.com?q=%s", commandString), http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
searchLogger.Printf("[%s] '%s' %s -> 302 %s", requestId, commandString, action.Action, action.Location)
|
|
||||||
if action.Action != "lookup" {
|
|
||||||
if err := shortcutStore.SaveShortcuts(scs.Shortcuts, nil); err != nil {
|
|
||||||
if logAccess {
|
|
||||||
searchLogger.Printf("[%s] '%s' %s -> could not save shortcuts database file: %v", requestId, commandString, action.Action, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(res, err.Error(), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(res, req, action.Location, http.StatusFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHeaders(headers http.Header, requestId string) {
|
|
||||||
headers.Set("X-RiffRaff-Request-Id", requestId)
|
|
||||||
headers.Set("Server", "Riff Raff")
|
|
||||||
headers.Set("Cache-Control", fmt.Sprintf("public, max-age=%s", time.Hour/time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRemoteIP(req *http.Request) string {
|
|
||||||
ip := req.RemoteAddr
|
|
||||||
h := req.Header
|
|
||||||
|
|
||||||
if realIpHeader := h.Get("X-Real-Ip"); realIpHeader != "" {
|
|
||||||
ip = realIpHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
if forwardIpHeader := h.Get("X-Forwarded-For"); forwardIpHeader != "" {
|
|
||||||
ip = forwardIpHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AccessLoggerMiddleware(middleware http.Handler) http.Handler {
|
||||||
|
accessLogger := log.New(os.Stdout, "[access] ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
requestId := uuid.Must(uuid.NewV4())
|
||||||
|
setupHeaders(res.Header(), requestId.String())
|
||||||
|
accessLogger.Printf("[%s] %s %s '%s'", requestId, getRemoteIP(req), req.Method, req.URL.String())
|
||||||
|
middleware.ServeHTTP(res, req.WithContext(context.WithValue(req.Context(), "id", requestId.String())))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHeaders(headers http.Header, requestId string) {
|
||||||
|
headers.Set("X-RiffRaff-Request-Id", requestId)
|
||||||
|
headers.Set("Server", "Riff Raff")
|
||||||
|
headers.Set("Cache-Control", fmt.Sprintf("public, max-age=%s", time.Hour/time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRemoteIP(req *http.Request) string {
|
||||||
|
ip := req.RemoteAddr
|
||||||
|
h := req.Header
|
||||||
|
|
||||||
|
if realIpHeader := h.Get("X-Real-Ip"); realIpHeader != "" {
|
||||||
|
ip = realIpHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
if forwardIpHeader := h.Get("X-Forwarded-For"); forwardIpHeader != "" {
|
||||||
|
ip = forwardIpHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultSearchProvider = "https://duckduckgo.com/%s"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUnrecognizedCommand = errors.New("unrecognized command")
|
|
||||||
ErrNotEnoughArguments = errors.New("not enough arguments")
|
|
||||||
)
|
|
||||||
|
|
||||||
type CommandHandler struct {
|
|
||||||
*sync.Mutex
|
|
||||||
Shortcuts map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandHandler) Handle(input string) (Command, error) {
|
|
||||||
rawArgs := strings.Fields(input)
|
|
||||||
fragmentCount := len(rawArgs)
|
|
||||||
farg := "*"
|
|
||||||
|
|
||||||
var shortcut string
|
|
||||||
|
|
||||||
if fragmentCount > 0 {
|
|
||||||
farg = rawArgs[0]
|
|
||||||
|
|
||||||
if fragmentCount > 1 {
|
|
||||||
shortcut = rawArgs[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateShortcutParams string
|
|
||||||
if fragmentCount > 2 {
|
|
||||||
updateShortcutParams = strings.Join(rawArgs[2:], " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd, err := c.updateShortcut(farg, shortcut, updateShortcutParams); err != ErrUnrecognizedCommand {
|
|
||||||
return cmd, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.getShortcut(farg, rawArgs...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandHandler) updateShortcut(action, shortcut, location string) (Command, error) {
|
|
||||||
command := Command{
|
|
||||||
Action: action,
|
|
||||||
Name: shortcut,
|
|
||||||
Location: location,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action {
|
|
||||||
case "add":
|
|
||||||
if location == "" {
|
|
||||||
return Command{}, ErrNotEnoughArguments
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Lock()
|
|
||||||
c.Shortcuts[shortcut] = command.Location
|
|
||||||
c.Unlock()
|
|
||||||
|
|
||||||
case "remove":
|
|
||||||
if shortcut == "" {
|
|
||||||
return Command{}, ErrNotEnoughArguments
|
|
||||||
}
|
|
||||||
|
|
||||||
command.Location = c.Shortcuts[shortcut]
|
|
||||||
|
|
||||||
c.Lock()
|
|
||||||
delete(c.Shortcuts, shortcut)
|
|
||||||
c.Unlock()
|
|
||||||
|
|
||||||
default:
|
|
||||||
return Command{}, ErrUnrecognizedCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
return command, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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") {
|
|
||||||
location = fmt.Sprintf(location, parameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command{
|
|
||||||
Action: "lookup",
|
|
||||||
Name: key,
|
|
||||||
Location: location,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDefaultShortcuts() Shortcuts {
|
|
||||||
return Shortcuts{
|
|
||||||
"*": DefaultSearchProvider,
|
|
||||||
"help": "/",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Command struct {
|
|
||||||
Action string
|
|
||||||
Name string
|
|
||||||
Location string
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Handle(t *testing.T) {
|
|
||||||
testHandleCases := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want Command
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty input should send to default search provider",
|
|
||||||
input: "",
|
|
||||||
want: Command{
|
|
||||||
Action: "lookup",
|
|
||||||
Name: "*",
|
|
||||||
Location: fmt.Sprintf(DefaultSearchProvider, ""),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add shortcut: 'add gh https://github.com'",
|
|
||||||
input: "add gh https://github.com",
|
|
||||||
want: Command{
|
|
||||||
Action: "add",
|
|
||||||
Name: "gh",
|
|
||||||
Location: "https://github.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add shortcut without location: 'add gh'",
|
|
||||||
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",
|
|
||||||
want: Command{
|
|
||||||
Action: "remove",
|
|
||||||
Name: "gh",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remove command with incorrect number of args: 'remove'",
|
|
||||||
input: "remove",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forward search to search provider: 'golang unit testing frameworks'",
|
|
||||||
input: "golang unit testing frameworks",
|
|
||||||
want: Command{
|
|
||||||
Action: "lookup",
|
|
||||||
Name: "*",
|
|
||||||
Location: fmt.Sprintf(DefaultSearchProvider, "golang unit testing frameworks"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "visit a shortcut: 'fb'",
|
|
||||||
input: "fb",
|
|
||||||
want: Command{
|
|
||||||
Action: "lookup",
|
|
||||||
Name: "fb",
|
|
||||||
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 _, 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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := cm.Handle(tt.input)
|
|
||||||
|
|
||||||
if tt.wantErr != (err != nil) {
|
|
||||||
t.Errorf("wantErr: %v got '%v'", tt.wantErr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("parseInput('%s') = %v, want %v", tt.input, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +1,12 @@
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import "net/http"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"html/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TemplateRenderer struct {
|
type TemplateRenderer struct {
|
||||||
FS http.FileSystem
|
http.FileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateVariables struct {
|
func (tp TemplateRenderer) Handle(templateFile string, model interface{}) http.Handler {
|
||||||
Host string
|
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
Entries map[string]string
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func ToTemplateVars(req *http.Request, shortcuts map[string]string) TemplateVariables {
|
|
||||||
scheme := req.URL.Scheme
|
|
||||||
host := req.URL.Host
|
|
||||||
|
|
||||||
if scheme == "" {
|
|
||||||
scheme = "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
if host == "" {
|
|
||||||
if host = req.Host; host == "" {
|
|
||||||
host = req.Header.Get("X-Forwarded-For")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fqdn := fmt.Sprintf("%s://%s", scheme, host)
|
|
||||||
|
|
||||||
return TemplateVariables{
|
|
||||||
Host: fqdn,
|
|
||||||
Entries: shortcuts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr TemplateRenderer) Render(name string, templVars TemplateVariables, w io.Writer) error {
|
|
||||||
templateFileName := fmt.Sprintf("%s.tpl", name)
|
|
||||||
|
|
||||||
f, err := tr.FS.Open(templateFileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
templateBytes, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
templateStr := string(templateBytes)
|
|
||||||
|
|
||||||
templateObj, parseTemplateErr := template.New(templateFileName).Parse(templateStr)
|
|
||||||
if parseTemplateErr != nil {
|
|
||||||
return fmt.Errorf("could not parse template for '%s': %w", templateFileName, parseTemplateErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%+v", templVars)
|
|
||||||
|
|
||||||
return templateObj.Execute(w, templVars)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr TemplateRenderer) RenderHandler(filename string, ss *CommandHandler, headers http.Header) http.HandlerFunc {
|
|
||||||
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 {
|
|
||||||
http.Error(res, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -13,7 +13,7 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
port := flag.Int("port", 80, "port to listen on")
|
port := flag.Int("port", 80, "port to listen on")
|
||||||
bindAddr := flag.String("bind", "127.0.0.1", "interface to bind to")
|
bindAddr := flag.String("bind", "127.0.0.1", "interface to bind to")
|
||||||
dbPath := flag.String("data", "./data.json", "path to save shortcut database")
|
dbPath := flag.String("data", "./data", "path to save shortcut database")
|
||||||
enableAccessLogging := flag.Bool("accesslog", true, "Enable access logging")
|
enableAccessLogging := flag.Bool("accesslog", true, "Enable access logging")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|
|
||||||
3
makefile
3
makefile
|
|
@ -37,6 +37,9 @@ lint: $(LINTBIN)
|
||||||
$(LINTBIN):
|
$(LINTBIN):
|
||||||
@GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint
|
@GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||||
|
|
||||||
|
$(TMPDIR):
|
||||||
|
mkdir $(TMPDIR)
|
||||||
|
|
||||||
package:
|
package:
|
||||||
@docker build --build-arg VERSION=$(VERSION) \
|
@docker build --build-arg VERSION=$(VERSION) \
|
||||||
--build-arg COMMIT=$(GIT_SHA) \
|
--build-arg COMMIT=$(GIT_SHA) \
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue