Sessions: Add max age and timeout config options #98 #782

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2022-10-03 22:59:29 +02:00
parent 96dfe6c7c9
commit bac6ae0cbd
45 changed files with 779 additions and 191 deletions

View file

@ -173,7 +173,7 @@ func ShareWithAccount(router *gin.RouterGroup) {
entity.FirstOrCreateFileShare(entity.NewFileShare(file.ID, m.ID, alias))
}
workers.StartShare(service.Config())
workers.RunShare(service.Config())
c.JSON(http.StatusOK, files)
})
@ -288,7 +288,7 @@ func UpdateAccount(router *gin.RouterGroup) {
}
if m.AccSync {
workers.StartSync(service.Config())
workers.RunSync(service.Config())
}
c.JSON(http.StatusOK, m)

View file

@ -17,7 +17,7 @@ import (
var ConvertCommand = cli.Command{
Name: "convert",
Usage: "Converts files in other formats to JPEG and AVC as needed",
ArgsUsage: "[SUB-FOLDER]",
ArgsUsage: "[sub-folder]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "force, f",

View file

@ -20,7 +20,7 @@ var CopyCommand = cli.Command{
Name: "cp",
Aliases: []string{"copy"},
Usage: "Copies media files to originals",
ArgsUsage: "[IMPORT PATH]",
ArgsUsage: "[source]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "dest, d",

View file

@ -53,7 +53,7 @@ var FacesCommand = cli.Command{
{
Name: "index",
Usage: "Searches originals for faces",
ArgsUsage: "[SUB-FOLDER]",
ArgsUsage: "[sub-folder]",
Action: facesIndexAction,
},
{

View file

@ -20,7 +20,7 @@ var ImportCommand = cli.Command{
Name: "mv",
Aliases: []string{"import"},
Usage: "Moves media files to originals",
ArgsUsage: "[SOURCE PATH]",
ArgsUsage: "[source]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "dest, d",

View file

@ -20,7 +20,7 @@ import (
var IndexCommand = cli.Command{
Name: "index",
Usage: "Indexes original media files",
ArgsUsage: "[SUB-FOLDER]",
ArgsUsage: "[sub-folder]",
Flags: indexFlags,
Action: indexAction,
}

View file

@ -18,7 +18,7 @@ var MigrationsStatusCommand = cli.Command{
Name: "ls",
Aliases: []string{"status", "show"},
Usage: "Lists the status of schema migrations",
ArgsUsage: "[MIGRATIONS...]",
ArgsUsage: "[migrations...]",
Flags: report.CliFlags,
Action: migrationsStatusAction,
}
@ -27,7 +27,7 @@ var MigrationsRunCommand = cli.Command{
Name: "run",
Aliases: []string{"execute", "migrate"},
Usage: "Executes database schema migrations",
ArgsUsage: "[MIGRATIONS...]",
ArgsUsage: "[migrations...]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "failed, f",

View file

@ -64,24 +64,24 @@ func resetAction(ctx *cli.Context) error {
log.Infoln("reset: enabled trace mode")
}
resetIndex := ctx.Bool("yes")
confirmed := ctx.Bool("yes")
// Show prompt?
if !resetIndex {
if !confirmed {
removeIndexPrompt := promptui.Prompt{
Label: "Delete and recreate index database?",
IsConfirm: true,
}
if _, err := removeIndexPrompt.Run(); err == nil {
resetIndex = true
confirmed = true
} else {
log.Infof("keeping index database")
}
}
// Reset index?
if resetIndex {
if confirmed {
resetIndexDb(conf)
}

View file

@ -18,6 +18,7 @@ import (
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/server"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/session"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@ -119,6 +120,7 @@ func startAction(ctx *cli.Context) error {
}
// Start background workers.
session.Monitor(time.Hour)
workers.Start(conf)
auto.Start(conf)
@ -131,6 +133,7 @@ func startAction(ctx *cli.Context) error {
// Stop all background activity.
auto.Stop()
workers.Stop()
session.Shutdown()
mutex.CancelAll()
log.Info("shutting down...")

View file

@ -20,8 +20,9 @@ const (
// UsersCommand registers the user management subcommands.
var UsersCommand = cli.Command{
Name: "users",
Usage: "User management subcommands",
Name: "users",
Aliases: []string{"user"},
Usage: "User management subcommands",
Subcommands: []cli.Command{
UsersListCommand,
UsersAddCommand,

View file

@ -38,6 +38,28 @@ func (c *Config) AdminPassword() string {
return clean.Password(c.options.AdminPassword)
}
// SessMaxAge returns the time in seconds until browser sessions expire automatically.
func (c *Config) SessMaxAge() int64 {
if c.options.SessMaxAge < 0 {
return 0
} else if c.options.SessMaxAge == 0 {
return DefaultSessMaxAge
}
return c.options.SessMaxAge
}
// SessTimeout returns the time in seconds until browser sessions expire due to inactivity
func (c *Config) SessTimeout() int64 {
if c.options.SessTimeout < 0 {
return 0
} else if c.options.SessTimeout == 0 {
return DefaultSessTimeout
}
return c.options.SessTimeout
}
// Public checks if app runs in public mode and requires no authentication.
func (c *Config) Public() bool {
return c.AuthMode() == AuthModePublic

View file

@ -37,6 +37,24 @@ func TestAuth(t *testing.T) {
assert.False(t, c.Auth())
}
func TestSessMaxAge(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, DefaultSessMaxAge, c.SessMaxAge())
c.options.SessMaxAge = -1
assert.Equal(t, int64(0), c.SessMaxAge())
c.options.SessMaxAge = 0
assert.Equal(t, DefaultSessMaxAge, c.SessMaxAge())
}
func TestSessTimeout(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, DefaultSessTimeout, c.SessTimeout())
c.options.SessTimeout = -1
assert.Equal(t, int64(0), c.SessTimeout())
c.options.SessTimeout = 0
assert.Equal(t, DefaultSessTimeout, c.SessTimeout())
}
func TestUtils_CheckPassword(t *testing.T) {
c := NewConfig(CliTestContext())

View file

@ -48,3 +48,18 @@ const DefaultResolutionLimit = 150 // 150 Megapixels
// serialName is the name of the unique storage serial.
const serialName = "serial"
// UnixHour is one hour in UnixTime.
const UnixHour int64 = 3600
// UnixDay is one day in UnixTime.
const UnixDay = UnixHour * 24
// UnixWeek is one week in UnixTime.
const UnixWeek = UnixDay * 7
// DefaultSessMaxAge is the default session expiration time in seconds.
const DefaultSessMaxAge = UnixWeek * 2
// DefaultSessTimeout is the default session timeout time in seconds.
const DefaultSessTimeout = UnixWeek

View file

@ -25,6 +25,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"admin-user", c.AdminUser()},
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
{"public", fmt.Sprintf("%t", c.Public())},
{"sess-maxage", fmt.Sprintf("%d", c.SessMaxAge())},
{"sess-timeout", fmt.Sprintf("%d", c.SessTimeout())},
// Logging.
{"log-level", c.LogLevel().String()},

View file

@ -28,6 +28,8 @@ type Options struct {
Public bool `yaml:"Public" json:"-" flag:"public"`
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
SessMaxAge int64 `yaml:"SessMaxAge" json:"-" flag:"sess-maxage"`
SessTimeout int64 `yaml:"SessTimeout" json:"-" flag:"sess-timeout"`
LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
Prod bool `yaml:"Prod" json:"Prod" flag:"prod"`
Debug bool `yaml:"Debug" json:"Debug" flag:"debug"`

View file

@ -18,6 +18,13 @@ var Flags = CliFlags{
Value: "password",
EnvVar: "PHOTOPRISM_AUTH_MODE",
}},
CliFlag{
Flag: cli.BoolFlag{
Name: "public, p",
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
EnvVar: "PHOTOPRISM_PUBLIC",
}},
CliFlag{
Flag: cli.StringFlag{
Name: "admin-user, login",
@ -32,11 +39,18 @@ var Flags = CliFlags{
EnvVar: "PHOTOPRISM_ADMIN_PASSWORD",
}},
CliFlag{
Flag: cli.BoolFlag{
Name: "public, p",
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
EnvVar: "PHOTOPRISM_PUBLIC",
Flag: cli.Int64Flag{
Name: "sess-maxage",
Value: DefaultSessMaxAge,
Usage: "time in `SECONDS` until user sessions expire automatically (-1 to disable)",
EnvVar: "PHOTOPRISM_SESS_MAXAGE",
}},
CliFlag{
Flag: cli.Int64Flag{
Name: "sess-timeout",
Value: DefaultSessTimeout,
Usage: "time in `SECONDS` until user sessions expire due to inactivity (-1 to disable)",
EnvVar: "PHOTOPRISM_SESS_TIMEOUT",
}},
CliFlag{
Flag: cli.StringFlag{

View file

@ -28,31 +28,32 @@ type Sessions []Session
// Session represents a User session.
type Session struct {
ID string `gorm:"type:VARBINARY(2048);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
Status int `json:"Status,omitempty" yaml:"-"`
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthMethod,omitempty"`
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthProvider,omitempty"`
AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"-" yaml:"AuthDomain,omitempty"`
AuthScope string `gorm:"size:1024;default:'';" json:"-" yaml:"AuthScope,omitempty"`
AuthID string `gorm:"type:VARBINARY(128);index;default:'';" json:"-" yaml:"AuthID,omitempty"`
ClientIP string `gorm:"size:64;column:client_ip;index" json:"-" yaml:"ClientIP,omitempty"`
UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID,omitempty" yaml:"UserUID,omitempty"`
UserName string `gorm:"size:64;index;" json:"UserName,omitempty" yaml:"UserName,omitempty"`
user *User `gorm:"-"`
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthProvider,omitempty"`
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthMethod,omitempty"`
AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"-" yaml:"AuthDomain,omitempty"`
AuthID string `gorm:"type:VARBINARY(128);index;default:'';" json:"-" yaml:"AuthID,omitempty"`
AuthScope string `gorm:"size:1024;default:'';" json:"-" yaml:"AuthScope,omitempty"`
LastActive int64 `json:"LastActive,omitempty" yaml:"LastActive,omitempty"`
SessExpires int64 `gorm:"index" json:"Expires,omitempty" yaml:"Expires,omitempty"`
SessTimeout int64 `json:"Timeout,omitempty" yaml:"Timeout,omitempty"`
PreviewToken string `gorm:"type:VARBINARY(64);column:preview_token;default:'';" json:"-" yaml:"-"`
DownloadToken string `gorm:"type:VARBINARY(64);column:download_token;default:'';" json:"-" yaml:"-"`
AccessToken string `gorm:"type:VARBINARY(4096);column:access_token;default:'';" json:"-" yaml:"-"`
RefreshToken string `gorm:"type:VARBINARY(512);column:refresh_token;default:'';" json:"-" yaml:"-"`
IdToken string `gorm:"type:VARBINARY(1024);column:id_token;default:'';" json:"IdToken,omitempty" yaml:"IdToken,omitempty"`
UserAgent string `gorm:"size:512;" json:"-" yaml:"UserAgent,omitempty"`
ClientIP string `gorm:"size:64;column:client_ip;" json:"-" yaml:"ClientIP,omitempty"`
LoginIP string `gorm:"size:64;column:login_ip" json:"-" yaml:"-"`
LoginAt time.Time `json:"-" yaml:"-"`
DataJSON json.RawMessage `gorm:"type:VARBINARY(4096);" json:"Data,omitempty" yaml:"Data,omitempty"`
data *SessionData `gorm:"-"`
RefID string `gorm:"type:VARBINARY(16);default:'';" json:"-" yaml:"-"`
LoginIP string `gorm:"size:64;column:login_ip" json:"-" yaml:"-"`
LoginAt time.Time `json:"-" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt"`
MaxAge time.Duration `json:"MaxAge,omitempty" yaml:"MaxAge,omitempty"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt"`
Timeout time.Duration `json:"Timeout,omitempty" yaml:"Timeout,omitempty"`
Status int `gorm:"-" json:"Status,omitempty" yaml:"-"`
}
// TableName returns the entity table name.
@ -60,31 +61,48 @@ func (Session) TableName() string {
return "auth_sessions"
}
// NewSession creates a new session and returns it.
func NewSession(maxAge, timeout time.Duration) (m *Session) {
// NewSession creates a new session using the maxAge and timeout in seconds.
func NewSession(maxAge, timeout int64) (m *Session) {
created := TimeStamp()
// Makes no sense for the timeout to be longer than the max age.
if timeout >= maxAge {
maxAge = timeout
timeout = 0
} else if maxAge == 0 {
// Set maxAge to default if not specified.
maxAge = time.Hour * 24 * 7
}
m = &Session{
ID: rnd.SessionID(),
MaxAge: maxAge,
Timeout: timeout,
RefID: rnd.RefID(SessionPrefix),
CreatedAt: created,
UpdatedAt: created,
}
if maxAge > 0 {
m.SessExpires = created.Unix() + maxAge
}
if timeout > 0 {
m.SessTimeout = timeout
}
return m
}
// DeleteExpiredSessions deletes expired sessions.
func DeleteExpiredSessions() (deleted int) {
expired := Sessions{}
if err := Db().Where("sess_expires > 0 AND sess_expires < ?", UnixTime()).Find(&expired).Error; err != nil {
event.AuditErr([]string{"failed to fetch sessions sessions", "%s"}, err)
return deleted
}
for _, s := range expired {
if err := s.Delete(); err != nil {
event.AuditErr([]string{s.IP(), "session %s", "failed to delete", "%s"}, s.RefID, err)
} else {
deleted++
}
}
return deleted
}
// SessionStatusUnauthorized returns a session with status unauthorized (401).
func SessionStatusUnauthorized() *Session {
return &Session{Status: http.StatusUnauthorized}
@ -363,16 +381,57 @@ func (m *Session) RedeemToken(token string) (n int) {
// ExpiresAt returns the time when the session expires.
func (m *Session) ExpiresAt() time.Time {
return m.CreatedAt.Add(m.MaxAge)
if m.SessExpires <= 0 {
return time.Time{}
}
return time.Unix(m.SessExpires, 0)
}
// TimeoutAt returns the time at which the session will expire due to inactivity.
func (m *Session) TimeoutAt() time.Time {
if m.SessTimeout <= 0 || m.LastActive <= 0 {
return m.ExpiresAt()
} else if t := m.LastActive + m.SessTimeout; t <= m.SessExpires || m.SessExpires <= 0 {
return time.Unix(m.LastActive+m.SessTimeout, 0)
} else {
return m.ExpiresAt()
}
}
// TimedOut checks if the session has expired due to inactivity..
func (m *Session) TimedOut() bool {
if at := m.TimeoutAt(); at.IsZero() {
return false
} else {
return at.Before(UTC())
}
}
// Expired checks if the session has expired.
func (m *Session) Expired() bool {
if m.MaxAge <= 0 {
if m.SessExpires <= 0 {
return m.TimedOut()
} else if at := m.ExpiresAt(); at.IsZero() {
return false
} else {
return at.Before(UTC())
}
}
// UpdateLastActive sets the last activity of the session to now.
func (m *Session) UpdateLastActive() *Session {
if m.Invalid() {
return m
}
return m.ExpiresAt().Before(UTC())
m.LastActive = UnixTime()
if err := Db().Model(m).UpdateColumn("LastActive", m.LastActive).Error; err != nil {
event.AuditWarn([]string{m.IP(), "session %s", "failed to update last active time", "%s"}, m.RefID, err)
}
return m
}
// Invalid checks if the session does not belong to a registered user or a visitor with shares.

View file

@ -6,6 +6,7 @@ import (
gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -38,17 +39,25 @@ func FindSession(id string) (s Session, err error) {
// Find cached session.
if cacheData, ok := sessionCache.Get(id); ok {
return cacheData.(Session), nil
s = cacheData.(Session)
s.LastActive = UnixTime()
return s, nil
}
// Search database and return session if found.
if r := Db().First(&s, "id = ?", id); r.RecordNotFound() {
err = fmt.Errorf("not found")
return s, fmt.Errorf("not found")
} else if r.Error != nil {
err = r.Error
return s, r.Error
} else if !rnd.IsSessionID(s.ID) {
err = fmt.Errorf("has invalid id %s", clean.LogQuote(s.ID))
return s, fmt.Errorf("has invalid id %s", clean.LogQuote(s.ID))
} else if s.Expired() {
if err = s.Delete(); err != nil {
event.AuditErr([]string{s.IP(), "session %s", "failed to delete after expiration", "%s"}, s.RefID, err)
}
return s, fmt.Errorf("expired")
} else {
s.UpdateLastActive()
sessionCache.SetDefault(s.ID, s)
}

View file

@ -1,7 +1,5 @@
package entity
import "time"
type SessionMap map[string]Session
func (m SessionMap) Get(name string) Session {
@ -22,49 +20,49 @@ func (m SessionMap) Pointer(name string) *Session {
var SessionFixtures = SessionMap{
"alice": {
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0",
Timeout: time.Hour * 24 * 3,
MaxAge: time.Hour * 24 * 7,
user: UserFixtures.Pointer("alice"),
UserUID: UserFixtures.Pointer("alice").UserUID,
UserName: UserFixtures.Pointer("alice").UserName,
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0",
SessTimeout: UnixDay * 3,
SessExpires: UnixTime() + UnixWeek,
user: UserFixtures.Pointer("alice"),
UserUID: UserFixtures.Pointer("alice").UserUID,
UserName: UserFixtures.Pointer("alice").UserName,
},
"bob": {
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1",
Timeout: time.Hour * 24 * 3,
MaxAge: time.Hour * 24 * 7,
user: UserFixtures.Pointer("bob"),
UserUID: UserFixtures.Pointer("bob").UserUID,
UserName: UserFixtures.Pointer("bob").UserName,
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1",
SessTimeout: UnixDay * 3,
SessExpires: UnixTime() + UnixWeek,
user: UserFixtures.Pointer("bob"),
UserUID: UserFixtures.Pointer("bob").UserUID,
UserName: UserFixtures.Pointer("bob").UserName,
},
"unauthorized": {
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2",
Timeout: time.Hour * 24 * 3,
MaxAge: time.Hour * 24 * 7,
user: UserFixtures.Pointer("unauthorized"),
UserUID: UserFixtures.Pointer("unauthorized").UserUID,
UserName: UserFixtures.Pointer("unauthorized").UserName,
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac2",
SessTimeout: UnixDay * 3,
SessExpires: UnixTime() + UnixWeek,
user: UserFixtures.Pointer("unauthorized"),
UserUID: UserFixtures.Pointer("unauthorized").UserUID,
UserName: UserFixtures.Pointer("unauthorized").UserName,
},
"visitor": {
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3",
Timeout: time.Hour * 24 * 3,
MaxAge: time.Hour * 24 * 7,
user: &Visitor,
UserUID: Visitor.UserUID,
UserName: Visitor.UserName,
DataJSON: []byte(`{"tokens":["1jxf3jfn2k"],"shares":["at9lxuqxpogaaba8"]}`),
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac3",
SessTimeout: UnixDay * 3,
SessExpires: UnixTime() + UnixWeek,
user: &Visitor,
UserUID: Visitor.UserUID,
UserName: Visitor.UserName,
DataJSON: []byte(`{"tokens":["1jxf3jfn2k"],"shares":["at9lxuqxpogaaba8"]}`),
data: &SessionData{
Tokens: []string{"1jxf3jfn2k"},
Shares: UIDs{"at9lxuqxpogaaba8"},
},
},
"friend": {
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4",
Timeout: time.Hour * 24 * 3,
MaxAge: time.Hour * 24 * 7,
user: UserFixtures.Pointer("friend"),
UserUID: UserFixtures.Pointer("friend").UserUID,
UserName: UserFixtures.Pointer("friend").UserName,
ID: "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac4",
SessTimeout: UnixDay * 3,
SessExpires: UnixTime() + UnixWeek,
user: UserFixtures.Pointer("friend"),
UserUID: UserFixtures.Pointer("friend").UserUID,
UserName: UserFixtures.Pointer("friend").UserName,
},
}

View file

@ -0,0 +1,31 @@
package entity
import (
"fmt"
)
// Report returns the entity values as rows.
func (m *Session) Report(skipEmpty bool) (rows [][]string, cols []string) {
cols = []string{"Name", "Value"}
// Extract model values.
values, _, err := ModelValues(m, "ID")
// Ok?
if err != nil {
return rows, cols
}
rows = make([][]string, 0, len(values))
for k, v := range values {
s := fmt.Sprintf("%v", v)
// Skip empty values?
if !skipEmpty || s != "" {
rows = append(rows, []string{k, s})
}
}
return rows, cols
}

View file

@ -2,7 +2,6 @@ package entity
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -11,7 +10,7 @@ import (
func TestNewSession(t *testing.T) {
t.Run("NoSessionData", func(t *testing.T) {
m := NewSession(time.Hour*24, time.Hour*6)
m := NewSession(UnixDay, UnixHour*6)
assert.True(t, rnd.IsSessionID(m.ID))
assert.False(t, m.CreatedAt.IsZero())
@ -22,7 +21,7 @@ func TestNewSession(t *testing.T) {
assert.Equal(t, 0, len(m.Data().Tokens))
})
t.Run("EmptySessionData", func(t *testing.T) {
m := NewSession(time.Hour*24, time.Hour*6)
m := NewSession(UnixDay, UnixHour*6)
m.SetData(NewSessionData())
assert.True(t, rnd.IsSessionID(m.ID))
@ -36,7 +35,7 @@ func TestNewSession(t *testing.T) {
t.Run("WithSessionData", func(t *testing.T) {
data := NewSessionData()
data.Tokens = []string{"foo", "bar"}
m := NewSession(time.Hour*24, time.Hour*6)
m := NewSession(UnixDay, UnixHour*6)
m.SetData(data)
assert.True(t, rnd.IsSessionID(m.ID))
@ -48,14 +47,12 @@ func TestNewSession(t *testing.T) {
assert.Len(t, m.Data().Tokens, 2)
assert.Equal(t, "foo", m.Data().Tokens[0])
assert.Equal(t, "bar", m.Data().Tokens[1])
// t.Logf("Session: %#v", m)
})
}
func TestSession_SetData(t *testing.T) {
t.Run("Nil", func(t *testing.T) {
m := NewSession(time.Hour*24, time.Hour*6)
m := NewSession(UnixDay, UnixHour*6)
assert.NotNil(t, m)
@ -66,3 +63,98 @@ func TestSession_SetData(t *testing.T) {
assert.Equal(t, sess.ID, m.ID)
})
}
func TestSession_TimedOut(t *testing.T) {
t.Run("NewSession", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour)
assert.False(t, m.TimeoutAt().IsZero())
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
assert.False(t, m.TimedOut())
})
t.Run("NoExpiration", func(t *testing.T) {
m := NewSession(0, UnixHour)
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
assert.True(t, m.TimeoutAt().IsZero())
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
assert.False(t, m.TimedOut())
assert.True(t, m.ExpiresAt().IsZero())
})
t.Run("NoTimeout", func(t *testing.T) {
m := NewSession(UnixDay, 0)
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
assert.False(t, m.TimeoutAt().IsZero())
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
assert.False(t, m.TimedOut())
assert.False(t, m.ExpiresAt().IsZero())
})
t.Run("TimedOut", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour)
utc := UnixTime()
m.LastActive = utc - (UnixHour + 1)
assert.False(t, m.TimeoutAt().IsZero())
assert.True(t, m.TimedOut())
})
t.Run("NotTimedOut", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour)
utc := UnixTime()
m.LastActive = utc - (UnixHour - 10)
assert.False(t, m.TimeoutAt().IsZero())
assert.False(t, m.TimedOut())
})
}
func TestSession_Expired(t *testing.T) {
t.Run("NewSession", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour)
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
assert.False(t, m.ExpiresAt().IsZero())
assert.False(t, m.Expired())
assert.False(t, m.TimeoutAt().IsZero())
assert.False(t, m.TimedOut())
})
t.Run("NoExpiration", func(t *testing.T) {
m := NewSession(0, 0)
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
assert.True(t, m.ExpiresAt().IsZero())
assert.False(t, m.Expired())
assert.True(t, m.TimeoutAt().IsZero())
assert.False(t, m.TimedOut())
})
t.Run("NoExpiration", func(t *testing.T) {
m := NewSession(0, 0)
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
assert.True(t, m.ExpiresAt().IsZero())
assert.False(t, m.Expired())
assert.True(t, m.TimeoutAt().IsZero())
assert.False(t, m.TimedOut())
})
t.Run("Expired", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour)
t.Logf("Timeout: %s, Expiration: %s", m.TimeoutAt().String(), m.ExpiresAt())
utc := UnixTime()
m.SessExpires = utc - 10
assert.False(t, m.ExpiresAt().IsZero())
assert.True(t, m.Expired())
assert.False(t, m.TimeoutAt().IsZero())
assert.True(t, m.TimedOut())
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
})
t.Run("NotExpired", func(t *testing.T) {
m := NewSession(UnixDay, UnixHour)
utc := UnixTime()
m.SessExpires = utc + 10
assert.False(t, m.ExpiresAt().IsZero())
assert.False(t, m.Expired())
assert.False(t, m.TimeoutAt().IsZero())
assert.False(t, m.TimedOut())
assert.Equal(t, m.ExpiresAt(), m.TimeoutAt())
})
}

View file

@ -1,6 +1,17 @@
package entity
import "github.com/jinzhu/gorm"
import (
"time"
"github.com/jinzhu/gorm"
)
// Set UTC as the default for created and updated timestamps.
func init() {
gorm.NowFunc = func() time.Time {
return UTC()
}
}
// Db returns the default *gorm.DB connection.
func Db() *gorm.DB {

View file

@ -1,14 +1,31 @@
package entity
import "time"
import (
"time"
)
// Day specified as time.Duration to improve readability.
const Day = time.Hour * 24
// UnixHour is one hour in UnixTime.
const UnixHour int64 = 3600
// UnixDay is one day in UnixTime.
const UnixDay = UnixHour * 24
// UnixWeek is one week in UnixTime.
const UnixWeek = UnixDay * 7
// UTC returns the current Coordinated Universal Time (UTC).
func UTC() time.Time {
return time.Now().UTC()
}
// UnixTime returns the current time in seconds since January 1, 1970 UTC.
func UnixTime() int64 {
return UTC().Unix()
}
// TimeStamp returns the current timestamp in UTC rounded to seconds.
func TimeStamp() time.Time {
return UTC().Truncate(time.Second)

View file

@ -3,8 +3,39 @@ package entity
import (
"testing"
"time"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
)
func TestUTC(t *testing.T) {
t.Run("Zone", func(t *testing.T) {
utc := UTC()
if zone, offset := utc.Zone(); zone != time.UTC.String() {
t.Error("should be utc")
} else if offset != 0 {
t.Error("offset should be 0")
}
})
t.Run("Gorm", func(t *testing.T) {
utc := UTC()
utcGorm := gorm.NowFunc()
t.Logf("NOW: %s, %s", utc.String(), utcGorm.String())
assert.True(t, utcGorm.After(utc))
if zone, offset := utcGorm.Zone(); zone != time.UTC.String() {
t.Error("gorm time should be utc")
} else if offset != 0 {
t.Error("gorm time offset should be 0")
}
assert.InEpsilon(t, utc.Unix(), utcGorm.Unix(), 2)
})
}
func TestTimeStamp(t *testing.T) {
t.Run("UTC", func(t *testing.T) {
if TimeStamp().Location() != time.UTC {

View file

@ -20,7 +20,7 @@ func CancelAll() {
FacesWorker.Cancel()
}
// WorkersRunning checks if a worker is currently running.
func WorkersRunning() bool {
// IndexWorkersRunning checks if a worker is currently running.
func IndexWorkersRunning() bool {
return MainWorker.Running() || SyncWorker.Running() || ShareWorker.Running() || MetaWorker.Running() || FacesWorker.Running()
}

View file

@ -7,5 +7,5 @@ import (
)
func TestWorkersBusy(t *testing.T) {
assert.False(t, WorkersRunning())
assert.False(t, IndexWorkersRunning())
}

View file

@ -1,25 +1,56 @@
package query
import (
"time"
"fmt"
"strings"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/rnd"
)
// Sessions returns stored sessions.
func Sessions() (result entity.Sessions, err error) {
err = Db().
Table(entity.Session{}.TableName()).
Select("*").
Where("expires_at > ?", time.Now()).
Scan(&result).Error
return result, err
}
// Session finds an existing session by id.
// Session finds an existing session by its id.
func Session(id string) (result entity.Session, err error) {
err = Db().Where("id = ?", id).First(&result).Error
if l := len(id); l < 6 && l > 2048 {
return result, fmt.Errorf("invalid session id")
} else if rnd.IsRefID(id) {
err = Db().Where("ref_id = ?", id).First(&result).Error
} else {
err = Db().Where("id LIKE ?", id).First(&result).Error
}
return result, err
}
// Sessions finds user sessions and returns them.
func Sessions(limit, offset int, sortOrder, search string) (result entity.Sessions, err error) {
result = entity.Sessions{}
stmt := Db()
search = strings.TrimSpace(search)
if search == "expired" {
stmt = stmt.Where("sess_expires > 0 AND sess_expires < ?", entity.UnixTime())
} else if rnd.IsSessionID(search) {
stmt = stmt.Where("id = ?", search)
} else if rnd.IsUID(search, entity.UserUID) {
stmt = stmt.Where("user_uid = ?", search)
} else if search != "" {
stmt = stmt.Where("user_name LIKE ?", search+"%")
}
if sortOrder == "" {
sortOrder = "last_active, user_name"
}
if limit > 0 {
stmt = stmt.Limit(limit)
if offset > 0 {
stmt = stmt.Offset(offset)
}
}
err = stmt.Order(sortOrder).Find(&result).Error
return result, err
}

View file

@ -0,0 +1,97 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSession(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
result, err := Session("")
t.Logf("session: %#v", result)
assert.Error(t, err)
assert.NotNil(t, result)
assert.Equal(t, "", result.ID)
assert.Equal(t, "", result.UserUID)
assert.Equal(t, "", result.UserName)
})
t.Run("Alice", func(t *testing.T) {
if result, err := Session("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0"); err != nil {
t.Fatal(err)
} else {
t.Logf("session: %#v", result)
assert.NotNil(t, result)
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", result.ID)
assert.Equal(t, "uqxetse3cy5eo9z2", result.UserUID)
assert.Equal(t, "alice", result.UserName)
}
})
t.Run("Bob", func(t *testing.T) {
if result, err := Session("69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1"); err != nil {
t.Fatal(err)
} else {
t.Logf("session: %#v", result)
assert.NotNil(t, result)
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac1", result.ID)
assert.Equal(t, "uqxc08w3d0ej2283", result.UserUID)
assert.Equal(t, "bob", result.UserName)
}
})
}
func TestSessions(t *testing.T) {
t.Run("Default", func(t *testing.T) {
if results, err := Sessions(0, 0, "", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 2, len(results))
//t.Logf("sessions: %#v", results)
}
})
t.Run("Limit", func(t *testing.T) {
if results, err := Sessions(1, 0, "", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 1, len(results))
//t.Logf("sessions: %#v", results)
}
})
t.Run("Offset", func(t *testing.T) {
if results, err := Sessions(0, 1, "", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 2, len(results))
//t.Logf("sessions: %#v", results)
}
})
t.Run("SearchAlice", func(t *testing.T) {
if results, err := Sessions(100, 0, "", "alice"); err != nil {
t.Fatal(err)
} else {
t.Logf("sessions: %#v", results)
assert.LessOrEqual(t, 1, len(results))
if len(results) > 0 {
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", results[0].ID)
assert.Equal(t, "uqxetse3cy5eo9z2", results[0].UserUID)
assert.Equal(t, "alice", results[0].UserName)
}
}
})
t.Run("SortByID", func(t *testing.T) {
if results, err := Sessions(100, 0, "id", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 2, len(results))
//t.Logf("sessions: %#v", results)
}
})
t.Run("SearchAliceSortByID", func(t *testing.T) {
if results, err := Sessions(100, 0, "id", "alice"); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 1, len(results))
//t.Logf("sessions: %#v", results)
}
})
}

View file

@ -1,7 +1,11 @@
package query
import (
"strings"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
)
// RegisteredUsers finds all registered users.
@ -12,3 +16,39 @@ func RegisteredUsers() (result entity.Users) {
return result
}
// Users finds users and returns them.
func Users(limit, offset int, sortOrder, search string) (result entity.Users, err error) {
result = entity.Users{}
stmt := Db()
search = strings.TrimSpace(search)
if search == "all" {
stmt = stmt.Where("sess_expires > 0 AND sess_expires < ?", entity.UnixTime())
} else if id := txt.Int(search); id != 0 {
stmt = stmt.Where("id = ?", id)
} else if rnd.IsUID(search, entity.UserUID) {
stmt = stmt.Where("user_uid = ?", search)
} else if search != "" {
stmt = stmt.Where("user_name LIKE ? OR user_email LIKE ? OR display_name LIKE ?", search+"%", search+"%", search+"%")
} else {
stmt = stmt.Where("id > 0")
}
if sortOrder == "" {
sortOrder = "id"
}
if limit > 0 {
stmt = stmt.Limit(limit)
if offset > 0 {
stmt = stmt.Offset(offset)
}
}
err = stmt.Order(sortOrder).Find(&result).Error
return result, err
}

View file

@ -12,10 +12,59 @@ func TestRegisteredUsers(t *testing.T) {
for _, user := range users {
t.Logf("user: %v, %s, %s, %s", user.ID, user.UserUID, user.Name(), user.DisplayName)
assert.NotEmpty(t, user.UserUID)
}
t.Logf("user count: %v", len(users))
assert.GreaterOrEqual(t, len(users), 3)
})
}
func TestUsers(t *testing.T) {
t.Run("Default", func(t *testing.T) {
if results, err := Users(0, 0, "", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 2, len(results))
}
})
t.Run("Limit", func(t *testing.T) {
if results, err := Users(1, 0, "", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 1, len(results))
}
})
t.Run("Offset", func(t *testing.T) {
if results, err := Users(0, 1, "", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 2, len(results))
}
})
t.Run("SearchAlice", func(t *testing.T) {
if results, err := Users(100, 0, "", "alice"); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 1, len(results))
if len(results) > 0 {
assert.Equal(t, 5, results[0].ID)
assert.Equal(t, "uqxetse3cy5eo9z2", results[0].UserUID)
assert.Equal(t, "alice", results[0].UserName)
}
}
})
t.Run("SortByID", func(t *testing.T) {
if results, err := Users(100, 0, "id", ""); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 2, len(results))
}
})
t.Run("SearchAliceSortByID", func(t *testing.T) {
if results, err := Users(100, 0, "id", "alice"); err != nil {
t.Fatal(err)
} else {
assert.LessOrEqual(t, 1, len(results))
}
})
}

View file

@ -0,0 +1,45 @@
package session
import (
"time"
"github.com/dustin/go-humanize/english"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
)
var stop = make(chan bool, 1)
// MonitorAction deletes expired sessions.
var MonitorAction = func() {
if n := entity.DeleteExpiredSessions(); n > 0 {
event.AuditInfo([]string{"deleted %s"}, english.Plural(n, "expired session", "expired sessions"))
} else {
event.AuditDebug([]string{"found no expired sessions"})
}
}
// Monitor starts a background worker that periodically deletes expired sessions.
func Monitor(interval time.Duration) {
ticker := time.NewTicker(interval)
MonitorAction()
go func() {
for {
select {
case <-stop:
ticker.Stop()
return
case <-ticker.C:
MonitorAction()
}
}
}()
}
// Shutdown shuts down the session watcher.
func Shutdown() {
stop <- true
}

View file

@ -0,0 +1,11 @@
package session
import (
"testing"
"time"
)
func TestWatch(t *testing.T) {
Monitor(time.Minute)
Shutdown()
}

View file

@ -1,16 +0,0 @@
package session
import (
"time"
"github.com/photoprism/photoprism/internal/config"
)
// MaxAge is the maximum duration after which a session expires.
var MaxAge = 168 * time.Hour * 24 * 7
var Timeout = 168 * time.Hour * 24 * 3
// New creates a new session store with default values.
func New(conf *config.Config) *Session {
return &Session{MaxAge: MaxAge, Timeout: Timeout, conf: conf}
}

View file

@ -25,8 +25,6 @@ Additional information can be found in our Developer Guide:
package session
import (
"time"
gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/config"
@ -37,8 +35,11 @@ var log = event.Log
// Session represents a session store.
type Session struct {
conf *config.Config
cache *gc.Cache
MaxAge time.Duration
Timeout time.Duration
conf *config.Config
cache *gc.Cache
}
// New creates a new session store with default values.
func New(conf *config.Config) *Session {
return &Session{conf: conf}
}

View file

@ -8,5 +8,5 @@ import (
// New creates a session with a context if it is specified.
func (s *Session) New(c *gin.Context) (m *entity.Session) {
return entity.NewSession(s.MaxAge, s.Timeout).SetContext(c)
return entity.NewSession(s.conf.SessMaxAge(), s.conf.SessTimeout()).SetContext(c)
}

View file

@ -15,7 +15,7 @@ func (s *Session) Public() *entity.Session {
return Public
}
Public = entity.NewSession(s.MaxAge, s.Timeout)
Public = entity.NewSession(0, 0)
Public.ID = PublicID
Public.AuthMethod = "public"
Public.SetUser(&entity.Admin)

View file

@ -15,7 +15,7 @@ func (s *Session) Save(m *entity.Session) (*entity.Session, error) {
}
// Save session.
return m, m.Save()
return m.UpdateLastActive(), m.Save()
}
// Create initializes a new client session and returns it.
@ -24,7 +24,9 @@ func (s *Session) Create(u *entity.User, c *gin.Context, data *entity.SessionDat
m = s.New(c).SetUser(u).SetData(data)
// Create session.
err = m.Create()
if err = m.Create(); err != nil {
m.UpdateLastActive()
}
return m, err
}

View file

@ -6,15 +6,17 @@ import (
"github.com/sirupsen/logrus"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
)
func TestMain(m *testing.M) {
log = logrus.StandardLogger()
log.SetLevel(logrus.TraceLevel)
event.AuditLog = log
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DRIVER"), os.Getenv("PHOTOPRISM_TEST_DSN"))
defer db.Close()
c := config.TestConfig()
defer c.CloseDb()
code := m.Run()

View file

@ -5,8 +5,6 @@ import (
"path/filepath"
"runtime/debug"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
@ -17,6 +15,7 @@ import (
"github.com/photoprism/photoprism/internal/remote/webdav"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
)
// Share represents a share worker.
@ -30,14 +29,14 @@ func NewShare(conf *config.Config) *Share {
}
// logError logs an error message if err is not nil.
func (worker *Share) logError(err error) {
func (w *Share) logError(err error) {
if err != nil {
log.Errorf("share: %s", err.Error())
}
}
// Start starts the share worker.
func (worker *Share) Start() (err error) {
func (w *Share) Start() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("share: %s (panic)\nstack: %s", r, debug.Stack())
@ -71,7 +70,7 @@ func (worker *Share) Start() (err error) {
files, err := query.FileShares(a.ID, entity.FileShareNew)
if err != nil {
worker.logError(err)
w.logError(err)
continue
}
@ -110,16 +109,16 @@ func (worker *Share) Start() (err error) {
srcFileName := photoprism.FileName(file.File.FileRoot, file.File.FileName)
if fs.ImageJPEG.Equal(file.File.FileType) && size.Width > 0 && size.Height > 0 {
srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbCachePath(), size.Width, size.Height, file.File.FileOrientation, size.Options...)
srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, w.conf.ThumbCachePath(), size.Width, size.Height, file.File.FileOrientation, size.Options...)
if err != nil {
worker.logError(err)
w.logError(err)
continue
}
}
if err := client.Upload(srcFileName, file.RemoteName); err != nil {
worker.logError(err)
w.logError(err)
file.Errors++
file.Error = err.Error()
} else {
@ -138,7 +137,7 @@ func (worker *Share) Start() (err error) {
return nil
}
worker.logError(entity.Db().Save(&file).Error)
w.logError(entity.Db().Save(&file).Error)
}
}
@ -155,7 +154,7 @@ func (worker *Share) Start() (err error) {
files, err := query.ExpiredFileShares(a)
if err != nil {
worker.logError(err)
w.logError(err)
continue
}
@ -182,7 +181,7 @@ func (worker *Share) Start() (err error) {
}
if err := entity.Db().Save(&file).Error; err != nil {
worker.logError(err)
w.logError(err)
}
}
}

View file

@ -27,21 +27,21 @@ func NewSync(conf *config.Config) *Sync {
}
// logError logs an error message if err is not nil.
func (worker *Sync) logError(err error) {
func (w *Sync) logError(err error) {
if err != nil {
log.Errorf("sync: %s", err.Error())
}
}
// logWarn logs a warning message if err is not nil.
func (worker *Sync) logWarn(err error) {
func (w *Sync) logWarn(err error) {
if err != nil {
log.Warnf("sync: %s", err.Error())
}
}
// Start starts the sync worker.
func (worker *Sync) Start() (err error) {
func (w *Sync) Start() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("sync: %s (panic)\nstack: %s", r, debug.Stack())
@ -71,7 +71,7 @@ func (worker *Sync) Start() (err error) {
a.AccSync = false
if err := entity.Db().Save(&a).Error; err != nil {
worker.logError(err)
w.logError(err)
} else {
log.Warnf("sync: disabled sync, %s failed more than %d times", a.AccName, a.RetryLimit)
}
@ -88,7 +88,7 @@ func (worker *Sync) Start() (err error) {
switch a.SyncStatus {
case entity.AccountSyncStatusRefresh:
if complete, err := worker.refresh(a); err != nil {
if complete, err := w.refresh(a); err != nil {
accErrors++
accError = err.Error()
} else if complete {
@ -106,7 +106,7 @@ func (worker *Sync) Start() (err error) {
}
}
case entity.AccountSyncStatusDownload:
if complete, err := worker.download(a); err != nil {
if complete, err := w.download(a); err != nil {
accErrors++
accError = err.Error()
syncStatus = entity.AccountSyncStatusRefresh
@ -121,7 +121,7 @@ func (worker *Sync) Start() (err error) {
}
}
case entity.AccountSyncStatusUpload:
if complete, err := worker.upload(a); err != nil {
if complete, err := w.upload(a); err != nil {
accErrors++
accError = err.Error()
syncStatus = entity.AccountSyncStatusRefresh
@ -149,7 +149,7 @@ func (worker *Sync) Start() (err error) {
"AccErrors": accErrors,
"SyncStatus": syncStatus,
"SyncDate": syncDate}); err != nil {
worker.logError(err)
w.logError(err)
} else if synced {
event.Publish("sync.synced", event.Data{"account": a})
}

View file

@ -17,12 +17,12 @@ import (
type Downloads map[string][]entity.FileSync
// downloadPath returns a temporary download path.
func (worker *Sync) downloadPath() string {
return worker.conf.TempPath() + "/sync"
func (w *Sync) downloadPath() string {
return w.conf.TempPath() + "/sync"
}
// relatedDownloads returns files to be downloaded grouped by prefix.
func (worker *Sync) relatedDownloads(a entity.Account) (result Downloads, err error) {
func (w *Sync) relatedDownloads(a entity.Account) (result Downloads, err error) {
result = make(Downloads)
maxResults := 1000
@ -35,7 +35,7 @@ func (worker *Sync) relatedDownloads(a entity.Account) (result Downloads, err er
// Group results by directory and base name
for i, file := range files {
k := fs.AbsPrefix(file.RemoteName, worker.conf.Settings().StackSequences())
k := fs.AbsPrefix(file.RemoteName, w.conf.Settings().StackSequences())
result[k] = append(result[k], file)
@ -49,7 +49,7 @@ func (worker *Sync) relatedDownloads(a entity.Account) (result Downloads, err er
}
// Downloads remote files in batches and imports / indexes them
func (worker *Sync) download(a entity.Account) (complete bool, err error) {
func (w *Sync) download(a entity.Account) (complete bool, err error) {
// Set up index worker
indexJobs := make(chan photoprism.IndexJob)
@ -61,10 +61,10 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
go photoprism.ImportWorker(importJobs)
defer close(importJobs)
relatedFiles, err := worker.relatedDownloads(a)
relatedFiles, err := w.relatedDownloads(a)
if err != nil {
worker.logError(err)
w.logError(err)
return false, err
}
@ -81,9 +81,9 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
var baseDir string
if a.SyncFilenames {
baseDir = worker.conf.OriginalsPath()
baseDir = w.conf.OriginalsPath()
} else {
baseDir = fmt.Sprintf("%s/%d", worker.downloadPath(), a.ID)
baseDir = fmt.Sprintf("%s/%d", w.downloadPath(), a.ID)
}
done := make(map[string]bool)
@ -124,7 +124,7 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
}
if err := entity.Db().Save(&file).Error; err != nil {
worker.logError(err)
w.logError(err)
} else {
files[i] = file
}
@ -141,10 +141,10 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
continue
}
related, err := mf.RelatedFiles(worker.conf.Settings().StackSequences())
related, err := mf.RelatedFiles(w.conf.Settings().StackSequences())
if err != nil {
worker.logWarn(err)
w.logWarn(err)
continue
}
@ -176,7 +176,7 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
FileName: mf.FileName(),
Related: related,
IndexOpt: photoprism.IndexOptionsAll(),
ImportOpt: photoprism.ImportOptionsMove(baseDir, worker.conf.ImportDest()),
ImportOpt: photoprism.ImportOptionsMove(baseDir, w.conf.ImportDest()),
Imp: service.Import(),
}
}
@ -186,10 +186,10 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
// Any files downloaded?
if len(done) > 0 {
// Update precalculated photo and file counts.
worker.logWarn(entity.UpdateCounts())
w.logWarn(entity.UpdateCounts())
// Update album, subject, and label cover thumbs.
worker.logWarn(query.UpdateCovers())
w.logWarn(query.UpdateCovers())
}
return false, nil

View file

@ -9,7 +9,7 @@ import (
)
// Updates the local list of remote files so that they can be downloaded in batches
func (worker *Sync) refresh(a entity.Account) (complete bool, err error) {
func (w *Sync) refresh(a entity.Account) (complete bool, err error) {
if a.AccType != remote.ServiceWebDAV {
return false, nil
}
@ -67,11 +67,11 @@ func (worker *Sync) refresh(a entity.Account) (complete bool, err error) {
}
if f.Status == entity.FileSyncIgnore && a.SyncRaw && (content == media.Raw || content == media.Video) {
worker.logError(f.Update("Status", entity.FileSyncNew))
w.logError(f.Update("Status", entity.FileSyncNew))
}
if f.Status == entity.FileSyncDownloaded && !f.RemoteDate.Equal(file.Date) {
worker.logError(f.Updates(map[string]interface{}{
w.logError(f.Updates(map[string]interface{}{
"Status": entity.FileSyncNew,
"RemoteDate": file.Date,
"RemoteSize": file.Size,

View file

@ -15,7 +15,7 @@ import (
)
// Uploads local files to a remote account
func (worker *Sync) upload(a entity.Account) (complete bool, err error) {
func (w *Sync) upload(a entity.Account) (complete bool, err error) {
maxResults := 250
// Get upload file list from database
@ -51,7 +51,7 @@ func (worker *Sync) upload(a entity.Account) (complete bool, err error) {
}
if err := client.Upload(fileName, remoteName); err != nil {
worker.logError(err)
w.logError(err)
continue // try again next time
}
@ -69,7 +69,7 @@ func (worker *Sync) upload(a entity.Account) (complete bool, err error) {
return false, nil
}
worker.logError(entity.Db().Save(&fileSync).Error)
w.logError(entity.Db().Save(&fileSync).Error)
}
return false, nil

View file

@ -59,9 +59,9 @@ func Start(conf *config.Config) {
mutex.SyncWorker.Cancel()
return
case <-ticker.C:
StartMeta(conf)
StartShare(conf)
StartSync(conf)
RunMeta(conf)
RunShare(conf)
RunSync(conf)
}
}
}()
@ -72,9 +72,9 @@ func Stop() {
stop <- true
}
// StartMeta runs the metadata worker once.
func StartMeta(conf *config.Config) {
if !mutex.WorkersRunning() {
// RunMeta runs the metadata worker once.
func RunMeta(conf *config.Config) {
if !mutex.IndexWorkersRunning() {
go func() {
worker := NewMeta(conf)
@ -88,8 +88,8 @@ func StartMeta(conf *config.Config) {
}
}
// StartShare runs the share worker once.
func StartShare(conf *config.Config) {
// RunShare runs the share worker once.
func RunShare(conf *config.Config) {
if !mutex.ShareWorker.Running() {
go func() {
worker := NewShare(conf)
@ -100,8 +100,8 @@ func StartShare(conf *config.Config) {
}
}
// StartSync runs the sync worker once.
func StartSync(conf *config.Config) {
// RunSync runs the sync worker once.
func RunSync(conf *config.Config) {
if !mutex.SyncWorker.Running() {
go func() {
worker := NewSync(conf)

View file

@ -7,11 +7,13 @@ import (
"github.com/sirupsen/logrus"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
)
func TestMain(m *testing.M) {
log = logrus.StandardLogger()
log.SetLevel(logrus.TraceLevel)
event.AuditLog = log
c := config.TestConfig()
defer c.CloseDb()