From 3c237819c3df346286ec4c13450daf27036a480c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 26 May 2022 22:54:46 +0200 Subject: [PATCH] settings: Save future changes in a dedicated file --- admin/api/settings.go | 124 +++++++++++++++++++++++++++++-- admin/static/js/app.js | 41 ++++++++-- admin/static/views/settings.html | 26 ++++++- settings/diff.go | 117 +++++++++++++++++++++++++++++ settings/settings.go | 2 +- 5 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 settings/diff.go diff --git a/admin/api/settings.go b/admin/api/settings.go index 8aa49d83..8c0c5636 100644 --- a/admin/api/settings.go +++ b/admin/api/settings.go @@ -9,6 +9,7 @@ import ( "os" "path" "reflect" + "strconv" "time" "srs.epita.fr/fic-server/admin/sync" @@ -38,9 +39,34 @@ func declareSettingsRoutes(router *gin.RouterGroup) { c.JSON(http.StatusOK, true) }) + router.GET("/settings-next", listNextSettings) + + apiNextSettingsRoutes := router.Group("/settings-next/:ts") + apiNextSettingsRoutes.Use(NextSettingsHandler) + apiNextSettingsRoutes.GET("", getNextSettings) + apiNextSettingsRoutes.DELETE("", deleteNextSettings) + router.POST("/reset", reset) } +func NextSettingsHandler(c *gin.Context) { + ts, err := strconv.ParseInt(string(c.Params.ByName("ts")), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid next settings identifier"}) + return + } + + nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", ts)), ts) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Next settings not found"}) + return + } + + c.Set("next-settings", nsf) + + c.Next() +} + func getROSettings(c *gin.Context) { syncMtd := "Disabled" if sync.GlobalImporter != nil { @@ -142,14 +168,102 @@ func saveSettings(c *gin.Context) { return } - if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil { - log.Println("Unable to SaveSettings:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())}) + // Is this a future setting? + if c.Request.URL.Query().Has("t") { + t, err := time.Parse(time.RFC3339, c.Request.URL.Query().Get("t")) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + // Load current settings to perform diff later + init_settings, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) + if err != nil { + log.Println("Unable to ReadSettings:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())}) + return + } + + current_settings := init_settings + // Apply already registered settings + nsu, err := settings.MergeNextSettingsUntil(&t) + if err == nil { + current_settings = settings.MergeSettings(*init_settings, nsu) + } else { + log.Println("Unable to MergeNextSettingsUntil:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to merge next settings: %s", err.Error())}) + return + } + + // Keep only diff + diff := settings.DiffSettings(current_settings, config) + + hasItems := false + for _, _ = range diff { + hasItems = true + break + } + + if !hasItems { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "No difference to apply."}) + return + } + + if !c.Request.URL.Query().Has("erase") { + // Check if there is already diff to apply at the given time + if nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), t.Unix()); err == nil { + for k, v := range nsf.Values { + if _, ok := diff[k]; !ok { + diff[k] = v + } + } + } + } + + // Save the diff + settings.SaveSettings(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), diff) + + // Return current settings + c.JSON(http.StatusOK, current_settings) + } else { + // Just apply settings right now! + if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil { + log.Println("Unable to SaveSettings:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())}) + return + } + + ApplySettings(config) + c.JSON(http.StatusOK, config) + } +} + +func listNextSettings(c *gin.Context) { + nsf, err := settings.ListNextSettingsFiles() + if err != nil { + log.Println("Unable to ListNextSettingsFiles:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list next settings files: %s", err.Error())}) return } - ApplySettings(config) - c.JSON(http.StatusOK, config) + c.JSON(http.StatusOK, nsf) +} + +func getNextSettings(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("next-settings").(*settings.NextSettingsFile)) +} + +func deleteNextSettings(c *gin.Context) { + nsf := c.MustGet("next-settings").(*settings.NextSettingsFile) + + err := os.Remove(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", nsf.Id))) + if err != nil { + log.Println("Unable to remove the file:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the file: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, true) } func ApplySettings(config *settings.Settings) { diff --git a/admin/static/js/app.js b/admin/static/js/app.js index fc06d574..06f94728 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -234,6 +234,11 @@ angular.module("FICApp") 'update': {method: 'PUT'}, }) }) + .factory("NextSettings", function($resource) { + return $resource("api/settings-next/:tsId", { tsId: '@id'}, { + 'update': {method: 'PUT'}, + }) + }) .factory("SettingsChallenge", function($resource) { return $resource("api/challenge.json", null, { 'update': {method: 'PUT'}, @@ -510,7 +515,23 @@ angular.module("FICApp") $scope.monitor = Monitor.get(); }) - .controller("SettingsController", function($scope, $rootScope, Settings, SettingsChallenge, $location, $http, $interval) { + .controller("SettingsController", function($scope, $rootScope, NextSettings, Settings, SettingsChallenge, $location, $http, $interval) { + $scope.nextsettings = NextSettings.query(); + $scope.erase = false; + $scope.editNextSettings = function(ns) { + $scope.activateTime = ns.date; + $scope.erase = true; + Object.keys(ns.values).forEach(function(k) { + $scope.config[k] = ns.values[k]; + }); + $scope.config.enableExerciceDepend = $scope.config.unlockedChallengeDepth >= 0; + } + $scope.deleteNextSettings = function(ns) { + ns.$delete().then(function() { + $scope.nextsettings = NextSettings.query(); + }) + } + $scope.displayDangerousActions = false; $scope.config = Settings.get(); $scope.dist_config = {}; @@ -524,6 +545,7 @@ angular.module("FICApp") }) $scope.challenge = SettingsChallenge.get(); $scope.duration = 360; + $scope.activateTime = "0001-01-01T00:00:00Z"; $scope.challenge.$promise.then(function(c) { if (c.duration) $scope.duration = c.duration; @@ -549,17 +571,24 @@ angular.module("FICApp") var nStart = this.config.start; var nEnd = this.config.end; var nGen = this.config.generation; - var aTime = this.config.activateTime; var state = this.config.enableExerciceDepend; this.config.unlockedChallengeDepth = (this.config.enableExerciceDepend?this.config.unlockedChallengeDepth:-1) - this.config.$update(function(response) { + var updateQuery = {}; + if (this.activateTime && this.activateTime != '0001-01-01T00:00:00Z') { + updateQuery['t'] = this.activateTime; + } + if (this.erase) { + updateQuery['erase'] = true; + this.erase = false; + } + this.config.$update(updateQuery, function(response) { $scope.dist_config = Object.assign({}, response); $scope.addToast('success', msg); + $scope.nextsettings = NextSettings.query(); response.enableExerciceDepend = response.unlockedChallengeDepth >= 0; $rootScope.settings.start = new Date(nStart); $rootScope.settings.end = new Date(nEnd); $rootScope.settings.generation = new Date(nGen); - $rootScope.settings.activateTime = new Date(aTime); }, function(response) { $scope.addToast('danger', 'An error occurs when saving settings:', response.data.errmsg); }); @@ -582,12 +611,12 @@ angular.module("FICApp") }); } $scope.updateActivateTime = function() { - $rootScope.settings.activateTime = this.config.activateTime; + $rootScope.settings.activateTime = this.activateTime; } $scope.updActivateTime = function(modulo) { var ts = Math.floor((new Date(this.config.end) - Date.now() - (60000 * modulo / 2)) / (60000 * modulo)) * (60000 * modulo); var d = new Date(this.config.end) - ts; - this.config.activateTime = new Date(d).toISOString(); + this.activateTime = new Date(d).toISOString(); this.updateActivateTime(); } $scope.reset = function(type) { diff --git a/admin/static/views/settings.html b/admin/static/views/settings.html index 7fda4224..5b961969 100644 --- a/admin/static/views/settings.html +++ b/admin/static/views/settings.html @@ -6,7 +6,7 @@
Propagation dans : {{ activateTimeCountDown | timer }} @@ -279,6 +279,26 @@

Changements anticipés

+
+
+

+ {{ ns.date }} +

+
+ + +
+
+
    +
  • + {{ k }} → {{ v }} +
  • +
+
diff --git a/settings/diff.go b/settings/diff.go new file mode 100644 index 00000000..34a2f855 --- /dev/null +++ b/settings/diff.go @@ -0,0 +1,117 @@ +package settings + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path" + "reflect" + "strconv" + "strings" + "time" +) + +// DiffSettings returns only the fields that differs between the two objects. +func DiffSettings(old, new *Settings) (ret map[string]interface{}) { + ret = map[string]interface{}{} + for _, field := range reflect.VisibleFields(reflect.TypeOf(*old)) { + if !reflect.DeepEqual(reflect.ValueOf(*old).FieldByName(field.Name).Interface(), reflect.ValueOf(*new).FieldByName(field.Name).Interface()) { + name := field.Name + + if tag, ok := field.Tag.Lookup("json"); ok { + name = strings.Split(tag, ",")[0] + } + + ret[name] = reflect.ValueOf(*new).FieldByName(field.Name).Interface() + } + } + + return +} + +type NextSettingsFile struct { + Id int64 `json:"id"` + Date time.Time `json:"date"` + Values map[string]interface{} `json:"values"` +} + +func ReadNextSettingsFile(filename string, ts int64) (*NextSettingsFile, error) { + fd, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("unable to open(%s): %w", filename, err) + } else { + defer fd.Close() + + var s map[string]interface{} + + jdec := json.NewDecoder(fd) + + if err := jdec.Decode(&s); err != nil { + return nil, fmt.Errorf("an error occurs during JSON decoding of %s: %w", filename, err) + } + + return &NextSettingsFile{ + Id: ts, + Date: time.Unix(ts, 0), + Values: s, + }, nil + } +} + +func ListNextSettingsFiles() ([]*NextSettingsFile, error) { + files, err := os.ReadDir(SettingsDir) + if err != nil { + return nil, err + } + + var ret []*NextSettingsFile + for _, file := range files { + ts, err := strconv.ParseInt(file.Name()[:10], 10, 64) + if err == nil { + nsf, err := ReadNextSettingsFile(path.Join(SettingsDir, file.Name()), ts) + if err != nil { + return nil, err + } + + ret = append(ret, nsf) + } + } + + return ret, nil +} + +func MergeNextSettingsUntil(until *time.Time) (map[string]interface{}, error) { + nsfs, err := ListNextSettingsFiles() + if err != nil { + return nil, err + } + + ret := map[string]interface{}{} + for _, nsf := range nsfs { + if until == nil || time.Unix(nsf.Id, 0).Before(*until) { + for k, v := range nsf.Values { + ret[k] = v + } + } + } + + return ret, nil +} + +func MergeSettings(current Settings, new map[string]interface{}) *Settings { + for _, field := range reflect.VisibleFields(reflect.TypeOf(current)) { + name := field.Name + + if tag, ok := field.Tag.Lookup("json"); ok { + name = strings.Split(tag, ",")[0] + } + + if v, ok := new[name]; ok { + log.Println(name, field.Name, v) + reflect.ValueOf(¤t).Elem().FieldByName(field.Name).Set(reflect.ValueOf(v)) + } + } + + return ¤t +} diff --git a/settings/settings.go b/settings/settings.go index a16f5207..37971c4e 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -103,7 +103,7 @@ func ReadSettings(path string) (*Settings, error) { } // SaveSettings saves settings at the given location. -func SaveSettings(path string, s *Settings) error { +func SaveSettings(path string, s interface{}) error { if fd, err := os.Create(path); err != nil { return err } else {