backend: new option --skipInitialGeneration to skip the full static files regeneratio...
[fic/server.git] / backend / main.go
index 6158cb8b046b87c30ae45996904dc39ea5a7f773..96c8b60a3009fa67efe3cedfaff6dd2dbbd5fed8 100644 (file)
@@ -7,19 +7,22 @@ import (
        "math/rand"
        "os"
        "path"
+       "strconv"
        "strings"
        "time"
 
-       "golang.org/x/exp/inotify"
        "srs.epita.fr/fic-server/libfic"
+       "srs.epita.fr/fic-server/settings"
+
+       "gopkg.in/fsnotify.v1"
 )
 
+var TeamsDir string
 var SubmissionDir string
-var BaseURL string
 
-func watchsubdir(watcher *inotify.Watcher, pathname string) error {
+func watchsubdir(watcher *fsnotify.Watcher, pathname string) error {
        log.Println("Watch new directory:", pathname)
-       if err := watcher.AddWatch(pathname, inotify.IN_CLOSE_WRITE|inotify.IN_CREATE); err != nil {
+       if err := watcher.Add(pathname); err != nil {
                return err
        }
 
@@ -28,23 +31,60 @@ func watchsubdir(watcher *inotify.Watcher, pathname string) error {
        } else {
                for _, d := range ds {
                        p := path.Join(pathname, d.Name())
-                       if d.IsDir() {
+                       if d.IsDir() && d.Name() != ".tmp" && d.Mode() & os.ModeSymlink == 0 {
                                if err := watchsubdir(watcher, p); err != nil {
                                        return err
                                }
+                       } else if d.Mode().IsRegular() {
+                               go treat(p)
                        }
                }
                return nil
        }
 }
 
+var lastRegeneration time.Time
+var skipInitialGeneration = false
+
+func reloadSettings(config settings.FICSettings) {
+       if lastRegeneration != config.Generation || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallenges != !config.EnableExerciceDepend || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase {
+               fic.PartialValidation = config.PartialValidation
+               fic.UnlockedChallenges = !config.EnableExerciceDepend
+
+               fic.FirstBlood = config.FirstBlood
+               fic.SubmissionCostBase = config.SubmissionCostBase
+
+               if !skipInitialGeneration {
+                       log.Println("Generating files...")
+                       go func() {
+                               genAll()
+                               log.Println("Full generation done")
+                       }()
+               } else {
+                       skipInitialGeneration = false
+                       log.Println("Regeneration skipped by option.")
+               }
+               lastRegeneration = config.Generation
+       } else {
+               log.Println("No change found. Skipping regeneration.")
+       }
+}
+
 func main() {
-       var dbfile = flag.String("db", "fic.db", "Path to the DB")
-       flag.StringVar(&BaseURL, "baseurl", "http://fic.srs.epita.fr/", "URL prepended to each URL")
+       var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
+       flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings")
        flag.StringVar(&SubmissionDir, "submission", "./submissions", "Base directory where save submissions")
+       flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
+       flag.StringVar(&fic.FilesDir, "files", "/files", "Request path prefix to reach files")
+       var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events")
+       flag.BoolVar(&skipInitialGeneration, "skipfullgeneration", skipInitialGeneration, "Skip the initial regeneration")
        flag.Parse()
 
+       log.SetPrefix("[backend] ")
+
+       settings.SettingsDir = path.Clean(settings.SettingsDir)
        SubmissionDir = path.Clean(SubmissionDir)
+       TeamsDir = path.Clean(TeamsDir)
 
        rand.Seed(time.Now().UnixNano())
 
@@ -56,41 +96,91 @@ func main() {
        }
 
        log.Println("Opening DB...")
-       if err := fic.DBInit(*dbfile); err != nil {
+       if err := fic.DBInit(*dsn); err != nil {
                log.Fatal("Cannot open the database: ", err)
-               os.Exit(1)
-
        }
        defer fic.DBClose()
 
+       // Load configuration
+       settings.LoadAndWatchSettings(path.Join(settings.SettingsDir, settings.SettingsFile), reloadSettings)
+
        log.Println("Registering directory events...")
-       watcher, err := inotify.NewWatcher()
+       watcher, err := fsnotify.NewWatcher()
        if err != nil {
                log.Fatal(err)
        }
+       defer watcher.Close()
 
        if err := watchsubdir(watcher, SubmissionDir); err != nil {
                log.Fatal(err)
        }
 
+       watchedNotify := fsnotify.Create
+
        for {
                select {
-               case ev := <-watcher.Event:
-                       if ev.Mask&inotify.IN_CREATE == inotify.IN_CREATE {
+               case ev := <-watcher.Events:
+                       if d, err := os.Lstat(ev.Name); err == nil && ev.Op & fsnotify.Create == fsnotify.Create && d.Mode().IsDir() && d.Mode() & os.ModeSymlink == 0 && d.Name() != ".tmp" {
                                // Register new subdirectory
-                               if d, err := os.Stat(ev.Name); err == nil && d.IsDir() {
-                                       if err := watchsubdir(watcher, ev.Name); err != nil {
-                                               log.Println(err)
-                                       }
+                               if err := watchsubdir(watcher, ev.Name); err != nil {
+                                       log.Println(err)
                                }
-                       } else if ev.Mask&inotify.IN_CLOSE_WRITE == inotify.IN_CLOSE_WRITE {
-                               // Extract
-                               spath := strings.Split(strings.TrimPrefix(ev.Name, SubmissionDir), "/")
-
-                               go treatSubmission(ev.Name, spath[1], spath[2])
+                       } else if ev.Op & watchedNotify == watchedNotify && d.Mode().IsRegular() {
+                               if *debugINotify {
+                                       log.Println("Treating event:", ev, "for", ev.Name)
+                               }
+                               go treat(ev.Name)
+                       } else if ev.Op & fsnotify.Write == fsnotify.Write {
+                               log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.")
+                               watchedNotify = fsnotify.Write
+                       } else if *debugINotify {
+                               log.Println("Skipped event:", ev, "for", ev.Name)
                        }
-               case err := <-watcher.Error:
+               case err := <-watcher.Errors:
                        log.Println("error:", err)
                }
        }
 }
+
+func treat(raw_path string) {
+       // Extract
+       spath := strings.Split(strings.TrimPrefix(raw_path, SubmissionDir), "/")
+
+       if len(spath) == 3 {
+               if spath[1] == "_registration" {
+                       treatRegistration(raw_path, spath[2])
+                       return
+               }
+
+               var team fic.Team
+
+               if strings.HasPrefix(spath[1], "_AUTH_ID_") {
+                       if serial, err := strconv.ParseInt(strings.TrimPrefix(spath[1], "_AUTH_ID_"), 16, 64); err != nil {
+                               log.Println("[ERR]", err)
+                               return
+                       } else if team, err = fic.GetTeamBySerial(serial); err != nil {
+                               log.Println("[ERR]", err)
+                               return
+                       }
+               } else if teamid, err := strconv.ParseInt(spath[1], 10, 64); err != nil {
+                       log.Println("[ERR]", err)
+                       return
+               } else if team, err = fic.GetTeam(teamid); err != nil {
+                       log.Println("[ERR]", err)
+                       return
+               }
+
+               switch spath[2] {
+               case "name":
+                       treatRename(raw_path, team)
+               case "hint":
+                       treatOpeningHint(raw_path, team)
+               case "choices":
+                       treatWantChoices(raw_path, team)
+               default:
+                       treatSubmission(raw_path, team, spath[2])
+               }
+       } else {
+               log.Println("Invalid new file:", raw_path)
+       }
+}