package api import ( "fmt" "log" "net/http" "net/url" "path" "strings" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/settings" "github.com/gin-gonic/gin" ) func flatifySyncErrors(errs []error) (ret []string) { for _, err := range 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) apiSyncDeepRoutes.POST("", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadException(sync.GlobalImporter, theme) var st []string for _, se := range 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) }) } func declareSyncExercicesRoutes(router *gin.RouterGroup) { router.POST("/exercices", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadException(sync.GlobalImporter, theme) c.JSON(http.StatusOK, flatifySyncErrors(sync.SyncExercices(sync.GlobalImporter, theme, exceptions))) }) apiSyncExercicesRoutes := router.Group("/exercices/:eid") apiSyncExercicesRoutes.Use(ExerciceHandler) apiSyncExercicesRoutes.POST("", func(c *gin.Context) { theme := c.MustGet("theme").(*fic.Theme) exceptions := sync.LoadException(sync.GlobalImporter, theme) exercice := c.MustGet("exercice").(*fic.Exercice) _, _, 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.LoadException(sync.GlobalImporter, theme) c.JSON(http.StatusOK, 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.LoadException(sync.GlobalImporter, theme) _, 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.LoadException(sync.GlobalImporter, theme) _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions) _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions) c.JSON(http.StatusOK, flatifySyncErrors(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.TrimPrefix(c.Params.ByName("p"), "/") 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 } if p == "" { if !IsProductionEnv { for _, theme := range themes { theme.DeleteDeep() } } st := sync.SyncDeep(sync.GlobalImporter) c.JSON(http.StatusOK, st) return } for _, theme := range themes { if theme.Path == p { if !IsProductionEnv { exercices, err := theme.GetExercices() if err == nil { for _, exercice := range exercices { exercice.DeleteDeep() } } } exceptions := sync.LoadException(sync.GlobalImporter, theme) var st []string for _, se := range 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 settings.ForceRegeneration() c.JSON(http.StatusOK, st) return } } c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p)}) }