diff --git a/README.md b/README.md index 18f5ed6..86805b7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,6 @@ This project is a very simple and data protecting alternative to sharing a location through Glympse or similar services. -You can setup your own instance in minutes, it does not require any database (even retained location data is dropped on restart of the service!) and you can share your location from a mobile browser. To view the location nothing more than a browser is required. +You can setup your own instance in minutes, it does not require any database (though you can have the retained locations stored on disk) and you can share your location from a mobile browser. To view the location nothing more than a browser is required. -When sharing a location you have the choice to select whether the server should retain the location data (until restart) or just pipe it through. Retaining the data has the advantage new viewers (or viewers whose websocket has reconnected) instantly see your location. When not retaining data the data is received, sent to all connected sockets and afterwards instantly forgotten. +When sharing a location you have the choice to select whether the server should retain the location data or just pipe it through. Retaining the data has the advantage new viewers (or viewers whose websocket has reconnected) instantly see your location. When not retaining data the data is received, sent to all connected sockets and afterwards instantly forgotten. diff --git a/go.mod b/go.mod index 19b2de0..2f41181 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.1 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/pkg/errors v0.8.1 github.com/sirupsen/logrus v1.4.2 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e // indirect ) diff --git a/go.sum b/go.sum index 12f76bd..62db64f 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/handler.go b/handler.go index 193f5a2..e08b9e4 100644 --- a/handler.go +++ b/handler.go @@ -58,7 +58,7 @@ func handleMapSocket(w http.ResponseWriter, r *http.Request) { // In case a retained position is available queue it reqRetainerLock.RLock() - if p, ok := reqRetainer[mapID]; ok { + if p, ok := reqRetainer[mapID]; ok && time.Since(p.Time) < cfg.StateTimeout { updates <- p } reqRetainerLock.RUnlock() @@ -111,6 +111,12 @@ func handleMapSubmit(w http.ResponseWriter, r *http.Request) { } reqRetainerLock.Unlock() + go func() { + if err := retainState(); err != nil { + log.WithError(err).Error("Unable to retain state to disk") + } + }() + reqDistributorsLock.RLock() defer reqDistributorsLock.RUnlock() diff --git a/main.go b/main.go index dbc1d85..8ee45a6 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "time" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" @@ -13,15 +14,18 @@ import ( var ( cfg = struct { - Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` - LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` - VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + StateFile string `flag:"state-file" default:"" description:"Where to store retained locations (empty for no state)"` + StateTimeout time.Duration `flag:"state-timeout" default:"24h" description:"When to drop retained states"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` }{} version = "dev" ) func init() { + rconfig.AutoEnv(true) if err := rconfig.ParseAndValidate(&cfg); err != nil { log.Fatalf("Unable to parse commandline options: %s", err) } @@ -39,6 +43,10 @@ func init() { } func main() { + if err := loadState(); err != nil { + log.WithError(err).Fatal("Unable to load state") + } + r := mux.NewRouter() r.HandleFunc("/", handleRedirectRandom).Methods(http.MethodGet) diff --git a/state.go b/state.go new file mode 100644 index 0000000..2a8232f --- /dev/null +++ b/state.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/json" + "os" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func loadState() error { + if cfg.StateFile == "" { + // No state file, no retaining + return nil + } + + if _, err := os.Stat(cfg.StateFile); err != nil { + log.WithError(err).Warn("Unable to load state, using empty state") + if os.IsNotExist(err) { + return nil + } + return errors.Wrap(err, "Unable to access state file") + } + + reqRetainerLock.Lock() + defer reqRetainerLock.Unlock() + + f, err := os.Open(cfg.StateFile) + if err != nil { + return errors.Wrap(err, "Unable to open state file") + } + defer f.Close() + + return errors.Wrap(json.NewDecoder(f).Decode(&reqRetainer), "Unable to decode state file") + +} + +func retainState() error { + if cfg.StateFile == "" { + // No state file, no retaining + return nil + } + + f, err := os.Create(cfg.StateFile) + if err != nil { + return errors.Wrap(err, "Unable to create state file") + } + defer f.Close() + + reqRetainerLock.RLock() + defer reqRetainerLock.RUnlock() + + var tmpState = make(map[string]position) + for m, p := range reqRetainer { + if time.Since(p.Time) > cfg.StateTimeout { + continue + } + tmpState[m] = p + } + + return errors.Wrap(json.NewEncoder(f).Encode(tmpState), "Unable to encode state file") +}