// Package settings is shared across multiple services for easy parsing and // retrieval of the challenge settings. package settings import ( "encoding/json" "log" "os" "path" "time" "gopkg.in/fsnotify.v1" ) // SettingsFile is the expected name of the file containing the settings. const SettingsFile = "settings.json" // SettingsDir is the relative location where the SettingsFile lies. var SettingsDir string = "./SETTINGS" // FICSettings represents the settings panel. type FICSettings struct { // Title is the displayed name of the challenge. Title string `json:"title"` // Authors is the group name of people making the challenge. Authors string `json:"authors"` // VideoLink is the link to explaination videos when the challenge is over. VideosLink string `json:"videoslink"` // Start is the departure time (expected or effective). Start time.Time `json:"start"` // End is the expected end time. End time.Time `json:"end"` // Generation is a value used to regenerate static files. Generation time.Time `json:"generation"` // FirstBlood is the coefficient applied to each first team who solve a challenge. FirstBlood float64 `json:"firstBlood"` // SubmissionCostBase is a complex number representing the cost of each attempts. SubmissionCostBase float64 `json:"submissionCostBase"` // AllowRegistration permits unregistered Team to register themselves. AllowRegistration bool `json:"allowRegistration"` // DenyNameChange disallow Team to change their name. DenyNameChange bool `json:"denyNameChange"` // EnableResolutionRoute activates the route displaying resolution movies. EnableResolutionRoute bool `json:"enableResolutionRoute"` // PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try. PartialValidation bool `json:"partialValidation"` // EnableExerciceDepend don't show (or permit to solve) to team challenges they are not unlocked through dependancies. EnableExerciceDepend bool `json:"enableExerciceDepend"` // SubmissionUniqueness don't count multiple times identical tries. SubmissionUniqueness bool `json:"submissionUniqueness"` } // ExistsSettings checks if the settings file can by found at the given path. func ExistsSettings(settingsPath string) bool { _, err := os.Stat(settingsPath) return !os.IsNotExist(err) } // ReadSettings parses the file at the given location. func ReadSettings(path string) (FICSettings, error) { var s FICSettings if fd, err := os.Open(path); err != nil { return s, err } else { defer fd.Close() jdec := json.NewDecoder(fd) if err := jdec.Decode(&s); err != nil { return s, err } return s, nil } } // SaveSettings saves settings at the given location. func SaveSettings(path string, s FICSettings) error { if fd, err := os.Create(path); err != nil { return err } else { defer fd.Close() jenc := json.NewEncoder(fd) if err := jenc.Encode(s); err != nil { return err } return nil } } // ForceRegeneration makes a small change to the settings structure in order to force the regeneration of all static files. func ForceRegeneration() error { location := path.Join(SettingsDir, SettingsFile) if settings, err := ReadSettings(location); err != nil { return err } else { settings.Generation = time.Now() return SaveSettings(location, settings) } } // LoadAndWatchSettings is the function you are looking for! // Giving the location and a callback, this function will first call your reload function // before returning (if the file can be parsed); then it starts watching modifications made to // this file. Your callback is then run each time the file is modified. func LoadAndWatchSettings(settingsPath string, reload func (FICSettings)) { // First load of configuration if it exists if _, err := os.Stat(settingsPath); !os.IsNotExist(err) { if config, err := ReadSettings(settingsPath); err != nil { log.Println("ERROR: Unable to read challenge settings:", err) } else { reload(config) } } // Watch the configuration file if watcher, err := fsnotify.NewWatcher(); err != nil { log.Fatal(err) } else { if err := watcher.Add(path.Dir(settingsPath)); err != nil { log.Fatal("Unable to watch: ", path.Dir(settingsPath), ": ", err) } go func() { defer watcher.Close() for { select { case ev := <-watcher.Events: if path.Base(ev.Name) == SettingsFile && ev.Op & fsnotify.Write == fsnotify.Write { log.Println("Settings file changes, reloading it!") if config, err := ReadSettings(settingsPath); err != nil { log.Println("ERROR: Unable to read challenge settings:", err) } else { reload(config) } } case err := <-watcher.Errors: log.Println("watcher error:", err) } } }() } }