diff --git a/internal/common/common.go b/internal/common/common.go index 39bb2ce5..a8bff346 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -170,6 +170,7 @@ func Initialize(c Configuration, isShared int) error { isShuttingDown.Store(false) util.SetUmask(c.Umask) version.SetConfig(c.ServerVersion) + dataprovider.SetTZ(c.TZ) Config = c Config.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true) Config.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true) @@ -588,6 +589,10 @@ type Configuration struct { Umask string `json:"umask" mapstructure:"umask"` // Defines the server version ServerVersion string `json:"server_version" mapstructure:"server_version"` + // TZ defines the time zone to use for the EventManager scheduler and to + // control time-based access restrictions. Set to "local" to use the + // server's local time, otherwise UTC will be used. + TZ string `json:"tz" mapstructure:"tz"` // Metadata configuration Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"` idleTimeoutAsDuration time.Duration diff --git a/internal/common/eventscheduler.go b/internal/common/eventscheduler.go index 71315e81..762880f7 100644 --- a/internal/common/eventscheduler.go +++ b/internal/common/eventscheduler.go @@ -19,6 +19,8 @@ import ( "github.com/robfig/cron/v3" + "github.com/drakkan/sftpgo/v2/internal/dataprovider" + "github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/util" ) @@ -36,7 +38,15 @@ func stopEventScheduler() { func startEventScheduler() { stopEventScheduler() - eventScheduler = cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger)) + options := []cron.Option{ + cron.WithLogger(cron.DiscardLogger), + } + if !dataprovider.UseLocalTime() { + eventManagerLog(logger.LevelDebug, "use UTC time for the scheduler") + options = append(options, cron.WithLocation(time.UTC)) + } + + eventScheduler = cron.New(options...) eventManager.loadRules() _, err := eventScheduler.AddFunc("@every 10m", eventManager.loadRules) util.PanicOnError(err) diff --git a/internal/config/config.go b/internal/config/config.go index 431ffd08..a85c8777 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -234,6 +234,7 @@ func Init() { RateLimitersConfig: []common.RateLimiterConfig{defaultRateLimiter}, Umask: "", ServerVersion: "", + TZ: "", Metadata: common.MetadataConfig{ Read: 0, }, @@ -2007,6 +2008,7 @@ func setViperDefaults() { viper.SetDefault("common.defender.login_delay.password_failed", globalConf.Common.DefenderConfig.LoginDelay.PasswordFailed) viper.SetDefault("common.umask", globalConf.Common.Umask) viper.SetDefault("common.server_version", globalConf.Common.ServerVersion) + viper.SetDefault("common.tz", globalConf.Common.TZ) viper.SetDefault("common.metadata.read", globalConf.Common.Metadata.Read) viper.SetDefault("acme.email", globalConf.ACME.Email) viper.SetDefault("acme.key_type", globalConf.ACME.KeyType) diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 6b420c82..677ee0bb 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -187,6 +187,7 @@ var ( ErrDuplicatedKey = errors.New("duplicated key not allowed") // ErrForeignKeyViolated occurs when there is a foreign key constraint violation ErrForeignKeyViolated = errors.New("violates foreign key constraint") + tz = "" isAdminCreated atomic.Bool validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)} config Config @@ -590,6 +591,16 @@ func (c *Config) doBackup() (string, error) { return outputFile, nil } +// SetTZ sets the configured timezone. +func SetTZ(val string) { + tz = val +} + +// UseLocalTime returns true if local time should be used instead of UTC. +func UseLocalTime() bool { + return tz == "local" +} + // ExecuteBackup executes a backup func ExecuteBackup() (string, error) { return config.doBackup() diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go index 80970c26..a6a2f1ed 100644 --- a/internal/dataprovider/user.go +++ b/internal/dataprovider/user.go @@ -342,7 +342,11 @@ func (u *User) isTimeBasedAccessAllowed(when time.Time) bool { if when.IsZero() { when = time.Now() } - when = when.UTC() + if UseLocalTime() { + when = when.Local() + } else { + when = when.UTC() + } weekDay := when.Weekday() hhMM := when.Format("15:04") for _, p := range u.Filters.AccessTime { diff --git a/sftpgo.json b/sftpgo.json index e634fedf..d66d8951 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -24,6 +24,7 @@ "allow_self_connections": 0, "umask": "", "server_version": "", + "tz": "", "metadata": { "read": 0 }, diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index 0cf02af2..25254bc8 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -730,7 +730,7 @@ "external_auth_cache_time": "External auth cache time", "external_auth_cache_time_help": "Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache", "access_time": "Access time restrictions", - "access_time_help": "No restrictions means access is always allowed, the time must be set in the format HH:MM. Use UTC time" + "access_time_help": "No restrictions means access is always allowed, the time must be set in the format HH:MM" }, "admin": { "role_permissions": "A role admin cannot have the following permissions: {{val}}", @@ -1071,7 +1071,7 @@ "sync_unsupported_fs_event": "Synchronous execution is only supported for upload and pre-* filesystem events", "only_failure_actions": "At least a non-failure action is required", "sync_action_required": "Event \"{{val}}\" requires at least a synchronous action", - "scheduler_help": "The scheduler uses UTC time. Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on", + "scheduler_help": "Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on", "concurrent_run": "Allow concurrent execution from multiple instances", "protocol_filters": "Protocol filters", "object_filters": "Object filters", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index ff108b41..de6bd488 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -730,7 +730,7 @@ "external_auth_cache_time": "Cache per autenticazione esterna", "external_auth_cache_time_help": "Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache", "access_time": "Limitazioni temporali all'accesso", - "access_time_help": "Nessuna restrizione significa che l'accesso è sempre consentito, l'ora deve essere impostata nel formato HH:MM. Utilizzare l'ora UTC" + "access_time_help": "Nessuna restrizione significa che l'accesso è sempre consentito, l'ora deve essere impostata nel formato HH:MM" }, "admin": { "role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}", @@ -1071,7 +1071,7 @@ "sync_unsupported_fs_event": "L'esecuzione sincrona è supporta solo per gli eventi \"upload\" e \"pre-*\"", "only_failure_actions": "E' richiesta almeno un'azione che non venga eseguita su errore", "sync_action_required": "L'evento \"{{val}}\" richiede almeno un'azione da eseguire sincronamente", - "scheduler_help": "Lo scheduler utilizza l'ora UTC. Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via", + "scheduler_help": "Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via", "concurrent_run": "Consentire l'esecuzione simultanea da più istanze", "protocol_filters": "Filtro su protocolli", "object_filters": "Filtro su oggetti",