package api import ( "fmt" "log" "net/http" "net/url" "os" "path" "strings" "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" "github.com/gin-gonic/gin" "go.uber.org/multierr" ) func flatifySyncErrors(errs error) (ret []string) { for _, err := range multierr.Errors(errs) { ret = append(ret, err.Error()) } return } func declareSyncRoutes(router *gin.RouterGroup) { apiSyncRoutes := router.Group("/sync") // Base sync checks if the local directory is in sync with remote one. apiSyncRoutes.POST("/base", func(c *gin.Context) { err := sync.GlobalImporter.Sync() if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) } else { c.JSON(http.StatusOK, true) } }) // Speedy sync performs a recursive synchronization without importing files. apiSyncRoutes.POST("/speed", func(c *gin.Context) { st := sync.SpeedySyncDeep(sync.GlobalImporter) sync.EditDeepReport(&st, false) c.JSON(http.StatusOK, st) }) // Deep sync: a fully recursive synchronization (can be limited by theme). apiSyncRoutes.GET("/deep", func(c *gin.Context) { if sync.DeepSyncProgress == 0 { c.AbortWithStatusJSON(http.StatusTooEarly, gin.H{"errmsg": "Pas de synchronisation en cours"}) return } c.JSON(http.StatusOK, gin.H{"progress": sync.DeepSyncProgress}) }) apiSyncRoutes.POST("/deep", func(c *gin.Context) { c.JSON(http.StatusOK, sync.SyncDeep(sync.GlobalImporter)) }) apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid") apiSyncDeepRoutes.Use(ThemeHandler) // Special route to handle standalone exercices apiSyncRoutes.POST("/deep/0", func(c *gin.Context) { var st []string for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.Theme{Path: sync.StandaloneExercicesDirectory}, 0, 250, nil)) { st = append(st, se.Error()) } sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false) sync.DeepSyncProgress = 255 c.JSON(http.StatusOK, st) }) apiSyncDeepRoutes.POST("", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadThemeException(sync.GlobalImporter, theme) var st []string for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions)) { st = append(st, se.Error()) } sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false) sync.DeepSyncProgress = 255 c.JSON(http.StatusOK, st) }) // Auto sync: to use with continuous deployment, in a development env apiSyncRoutes.POST("/auto/*p", autoSync) // Themes apiSyncRoutes.POST("/fixurlids", fixAllURLIds) apiSyncRoutes.POST("/themes", func(c *gin.Context) { _, errs := sync.SyncThemes(sync.GlobalImporter) c.JSON(http.StatusOK, flatifySyncErrors(errs)) }) apiSyncThemesRoutes := apiSyncRoutes.Group("/themes/:thid") apiSyncThemesRoutes.Use(ThemeHandler) apiSyncThemesRoutes.POST("/fixurlid", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) if theme.FixURLId() { v, err := theme.Update() if err != nil { log.Println("Unable to UpdateTheme after fixurlid:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the theme."}) return } c.JSON(http.StatusOK, v) } else { c.AbortWithStatusJSON(http.StatusOK, 0) } }) // Exercices declareSyncExercicesRoutes(apiSyncRoutes) declareSyncExercicesRoutes(apiSyncThemesRoutes) // Videos sync imports resolution.mp4 from path stored in database. apiSyncRoutes.POST("/videos", func(c *gin.Context) { exercices, err := fic.GetExercices() if err != nil { log.Println("Unable to GetExercices:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."}) return } for _, e := range exercices { if len(e.VideoURI) == 0 || !strings.HasPrefix(e.VideoURI, "$RFILES$/") { continue } vpath, err := url.PathUnescape(strings.TrimPrefix(e.VideoURI, "$RFILES$/")) if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": fmt.Sprintf("Unable to perform URL unescape: %s", err.Error())}) return } _, err = sync.ImportFile(sync.GlobalImporter, vpath, func(filePath, URI string) (interface{}, error) { e.VideoURI = path.Join("$FILES$", strings.TrimPrefix(filePath, fic.FilesDir)) return e.Update() }) if err != nil { c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) return } } c.JSON(http.StatusOK, true) }) // Remove soluces from the database. apiSyncRoutes.POST("/drop_soluces", func(c *gin.Context) { exercices, err := fic.GetExercices() if err != nil { log.Println("Unable to GetExercices:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."}) return } var errs error for _, e := range exercices { // Remove any published video if len(e.VideoURI) > 0 && strings.HasPrefix(e.VideoURI, "$FILES$") { vpath := path.Join(fic.FilesDir, strings.TrimPrefix(e.VideoURI, "$FILES$/")) err = os.Remove(vpath) if err != nil { errs = multierr.Append(errs, fmt.Errorf("unable to delete published video (%q): %w", e.VideoURI, err)) } } // Clean the database if len(e.VideoURI) > 0 || len(e.Resolution) > 0 { e.VideoURI = "" e.Resolution = "" _, err = e.Update() if err != nil { errs = multierr.Append(errs, fmt.Errorf("unable to update exercice (%d: %s): %w", e.Id, e.Title, err)) } } } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"errmsg": flatifySyncErrors(err)}) } else { c.JSON(http.StatusOK, true) } }) } func declareSyncExercicesRoutes(router *gin.RouterGroup) { router.POST("/exercices", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadThemeException(sync.GlobalImporter, theme) _, errs := sync.SyncExercices(sync.GlobalImporter, theme, exceptions) c.JSON(http.StatusOK, flatifySyncErrors(errs)) }) apiSyncExercicesRoutes := router.Group("/exercices/:eid") apiSyncExercicesRoutes.Use(ExerciceHandler) apiSyncExercicesRoutes.POST("", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) exercice := c.MustGet("exercice").(*fic.Exercice) exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) _, _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil, exceptions) c.JSON(http.StatusOK, flatifySyncErrors(errs)) }) apiSyncExercicesRoutes.POST("/files", func(c *gin.Context) { exercice := c.MustGet("exercice").(*fic.Exercice) theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) c.JSON(http.StatusOK, flatifySyncErrors(sync.SyncExerciceFiles(sync.GlobalImporter, exercice, exceptions))) }) apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) { exercice := c.MustGet("exercice").(*fic.Exercice) if exercice.FixURLId() { v, err := exercice.Update() if err != nil { log.Println("Unable to UpdateExercice after fixurlid:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the exercice."}) return } c.JSON(http.StatusOK, v) } else { c.AbortWithStatusJSON(http.StatusOK, 0) } }) apiSyncExercicesRoutes.POST("/hints", func(c *gin.Context) { exercice := c.MustGet("exercice").(*fic.Exercice) theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) _, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions) c.JSON(http.StatusOK, flatifySyncErrors(errs)) }) apiSyncExercicesRoutes.POST("/flags", func(c *gin.Context) { exercice := c.MustGet("exercice").(*fic.Exercice) theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions) _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions) c.JSON(http.StatusOK, flatifySyncErrors(multierr.Append(errs, herrs))) }) } // autoSync tries to performs a smart synchronization, when in development environment. // It'll sync most of modified things, and will delete out of sync data. // Avoid using it in a production environment. func autoSync(c *gin.Context) { p := strings.Split(strings.TrimPrefix(c.Params.ByName("p"), "/"), "/") if !IsProductionEnv { if err := sync.GlobalImporter.Sync(); err != nil { log.Println("Unable to sync.GI.Sync:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."}) return } } themes, err := fic.GetThemes() if err != nil { log.Println("Unable to GetThemes:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve theme list."}) return } // No argument, do a deep sync if len(p) == 0 { if !IsProductionEnv { for _, theme := range themes { theme.DeleteDeep() } } st := sync.SyncDeep(sync.GlobalImporter) c.JSON(http.StatusOK, st) return } var theTheme *fic.Theme // Find the given theme for _, theme := range themes { if theme.Path == p[0] { theTheme = theme break } } if theTheme == nil { // The theme doesn't exists locally, perhaps it has not been imported already? rThemes, err := sync.GetThemes(sync.GlobalImporter) if err == nil { for _, theme := range rThemes { if theme == p[0] { sync.SyncThemes(sync.GlobalImporter) themes, err := fic.GetThemes() if err == nil { for _, theme := range themes { if theme.Path == p[0] { theTheme = theme break } } } break } } } if theTheme == nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p[0])}) return } } if !IsProductionEnv { exercices, err := theTheme.GetExercices() if err == nil { for _, exercice := range exercices { if len(p) <= 1 || exercice.Path == path.Join(p[0], p[1]) { exercice.DeleteDeep() } } } } exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme) var st []string for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions)) { st = append(st, se.Error()) } sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false) sync.DeepSyncProgress = 255 resp, err := generation.FullGeneration() if err == nil { defer resp.Body.Close() } c.JSON(http.StatusOK, st) }