diff --git a/internal/api/session.go b/internal/api/session.go index 146d82670..f6a9ef70d 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -6,7 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/form" - "github.com/photoprism/photoprism/internal/session" + "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/pkg/txt" ) @@ -27,7 +27,7 @@ func CreateSession(router *gin.RouterGroup, conf *config.Config) { user := gin.H{"ID": 1, "FirstName": "Admin", "LastName": "", "Role": "admin", "Email": "photoprism@localhost"} - token := session.Create(user) + token := service.Session().Create(user) c.Header("X-Session-Token", token) @@ -42,7 +42,7 @@ func DeleteSession(router *gin.RouterGroup, conf *config.Config) { router.DELETE("/session/:token", func(c *gin.Context) { token := c.Param("token") - session.Delete(token) + service.Session().Delete(token) c.JSON(http.StatusOK, gin.H{"status": "ok", "token": token}) }) @@ -59,5 +59,5 @@ func Unauthorized(c *gin.Context, conf *config.Config) bool { token := c.GetHeader("X-Session-Token") // Check if session token is valid - return !session.Exists(token) + return !service.Session().Exists(token) } diff --git a/internal/api/websocket.go b/internal/api/websocket.go index e4dd8f5df..59f5b61dd 100644 --- a/internal/api/websocket.go +++ b/internal/api/websocket.go @@ -10,7 +10,7 @@ import ( "github.com/gorilla/websocket" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/event" - "github.com/photoprism/photoprism/internal/session" + "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -54,7 +54,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c if err := json.Unmarshal(m, &info); err != nil { log.Error(err) } else { - if session.Exists(info.SessionToken) { + if service.Session().Exists(info.SessionToken) { log.Debug("websocket: authenticated") wsAuth.mutex.Lock() diff --git a/internal/service/service.go b/internal/service/service.go index 1a0f244ca..00cc88150 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -5,6 +5,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/session" ) var conf *config.Config @@ -16,6 +17,7 @@ var services struct { Convert *photoprism.Convert Resample *photoprism.Resample Classify *classify.TensorFlow + Session *session.Session } func SetConfig(c *config.Config) { diff --git a/internal/service/session.go b/internal/service/session.go new file mode 100644 index 000000000..9b7c7f967 --- /dev/null +++ b/internal/service/session.go @@ -0,0 +1,21 @@ +package service + +import ( + "sync" + "time" + + "github.com/photoprism/photoprism/internal/session" +) + +var onceSession sync.Once + +func initSession() { + // keep sessions for 7 days by default + services.Session = session.New(168*time.Hour, Config().CachePath()) +} + +func Session() *session.Session { + onceSession.Do(initSession) + + return services.Session +} diff --git a/internal/session/session.go b/internal/session/session.go index 4e5cb2732..a5ae7d2cf 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -8,7 +8,46 @@ https://github.com/photoprism/photoprism/wiki package session import ( + "encoding/json" + "io/ioutil" + "path" + "time" + + gc "github.com/patrickmn/go-cache" "github.com/photoprism/photoprism/internal/event" ) var log = event.Log + +// Session represents a session store. +type Session struct { + cacheFile string + cache *gc.Cache +} + +// New returns a new session store with an optional cachePath. +func New(expiration time.Duration, cachePath string) *Session { + s := &Session{} + + cleanupInterval := 15 * time.Minute + + if cachePath != "" { + var items map[string]gc.Item + + s.cacheFile = path.Join(cachePath, "sessions.json") + + if cached, err := ioutil.ReadFile(s.cacheFile); err != nil { + log.Infof("session: %s", err) + } else if err := json.Unmarshal(cached, &items); err != nil { + log.Errorf("session: %s", err) + } else { + s.cache = gc.NewFrom(expiration, cleanupInterval, items) + } + } + + if s.cache == nil { + s.cache = gc.New(expiration, cleanupInterval) + } + + return s +} diff --git a/internal/session/store.go b/internal/session/store.go index 3e0844059..fced4b3ea 100644 --- a/internal/session/store.go +++ b/internal/session/store.go @@ -1,31 +1,51 @@ package session import ( - "time" + "encoding/json" + "io/ioutil" gc "github.com/patrickmn/go-cache" ) -var cache = gc.New(72*time.Hour, 30*time.Minute) - -func Create(data interface{}) string { +func (s *Session) Create(data interface{}) string { token := Token() - cache.Set(token, data, gc.DefaultExpiration) + s.cache.Set(token, data, gc.DefaultExpiration) log.Debugf("session: created") + + if err := s.Save(); err != nil { + log.Errorf("session: %s", err) + } + return token } -func Delete(token string) { - cache.Delete(token) +func (s *Session) Delete(token string) { + s.cache.Delete(token) log.Debugf("session: deleted") + + if err := s.Save(); err != nil { + log.Errorf("session: %s", err) + } } -func Get(token string) (data interface{}, exists bool) { - return cache.Get(token) +func (s *Session) Get(token string) (data interface{}, exists bool) { + return s.cache.Get(token) } -func Exists(token string) bool { - _, found := cache.Get(token) +func (s *Session) Exists(token string) bool { + _, found := s.cache.Get(token) return found } + +func (s *Session) Save() error { + if s.cacheFile == "" { + return nil + } else if serialized, err := json.MarshalIndent(s.cache.Items(), "", " "); err != nil { + return err + } else if err = ioutil.WriteFile(s.cacheFile, serialized, 0600); err != nil { + return err + } + + return nil +} diff --git a/internal/session/store_test.go b/internal/session/store_test.go index b0be4d856..c0b85b734 100644 --- a/internal/session/store_test.go +++ b/internal/session/store_test.go @@ -2,44 +2,49 @@ package session import ( "testing" + "time" "github.com/stretchr/testify/assert" ) -func TestCreate(t *testing.T) { - token := Create(23) +func TestSession_Create(t *testing.T) { + s := New(time.Hour, "testdata") + token := s.Create(23) t.Logf("token: %s", token) assert.Equal(t, 48, len(token)) } -func TestDelete(t *testing.T) { - Delete("abc") +func TestSession_Delete(t *testing.T) { + s := New(time.Hour, "testdata") + s.Delete("abc") } -func TestGet(t *testing.T) { - token := Create(42) +func TestSession_Get(t *testing.T) { + s := New(time.Hour, "testdata") + token := s.Create(42) t.Logf("token: %s", token) assert.Equal(t, 48, len(token)) - data, exists := Get(token) + data, exists := s.Get(token) assert.Equal(t, 42, data) assert.True(t, exists) - Delete(token) + s.Delete(token) - data, exists = Get(token) + data, exists = s.Get(token) assert.Nil(t, data) - assert.False(t, Exists(token)) + assert.False(t, s.Exists(token)) } -func TestExists(t *testing.T) { - assert.False(t, Exists("xyz")) - token := Create(23) +func TestSession_Exists(t *testing.T) { + s := New(time.Hour, "testdata") + assert.False(t, s.Exists("xyz")) + token := s.Create(23) t.Logf("token: %s", token) assert.Equal(t, 48, len(token)) - assert.True(t, Exists(token)) - Delete(token) - assert.False(t, Exists(token)) + assert.True(t, s.Exists(token)) + s.Delete(token) + assert.False(t, s.Exists(token)) } diff --git a/internal/session/testdata/.gitignore b/internal/session/testdata/.gitignore new file mode 100644 index 000000000..c96a04f00 --- /dev/null +++ b/internal/session/testdata/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file