2022-05-16 09:38:46 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
2022-06-12 10:15:39 +00:00
|
|
|
"net/url"
|
2024-03-14 17:43:07 +00:00
|
|
|
"os"
|
2022-06-12 10:15:39 +00:00
|
|
|
"path"
|
2022-05-16 09:38:46 +00:00
|
|
|
"strings"
|
|
|
|
|
2023-07-10 10:10:03 +00:00
|
|
|
"srs.epita.fr/fic-server/admin/generation"
|
2022-05-16 09:38:46 +00:00
|
|
|
"srs.epita.fr/fic-server/admin/sync"
|
|
|
|
"srs.epita.fr/fic-server/libfic"
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2023-11-22 11:16:53 +00:00
|
|
|
"go.uber.org/multierr"
|
2022-05-16 09:38:46 +00:00
|
|
|
)
|
|
|
|
|
2023-11-22 11:16:53 +00:00
|
|
|
func flatifySyncErrors(errs error) (ret []string) {
|
|
|
|
for _, err := range multierr.Errors(errs) {
|
2022-11-24 09:19:56 +00:00
|
|
|
ret = append(ret, err.Error())
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-16 09:38:46 +00:00
|
|
|
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)
|
2022-07-01 22:01:05 +00:00
|
|
|
sync.EditDeepReport(&st, false)
|
2022-05-16 09:38:46 +00:00
|
|
|
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)
|
2024-03-15 16:46:50 +00:00
|
|
|
// 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)
|
|
|
|
})
|
2022-05-16 09:38:46 +00:00
|
|
|
apiSyncDeepRoutes.POST("", func(c *gin.Context) {
|
|
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
|
|
|
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
|
2022-10-29 15:03:57 +00:00
|
|
|
|
2022-07-12 10:35:49 +00:00
|
|
|
var st []string
|
2023-11-22 11:16:53 +00:00
|
|
|
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions)) {
|
2022-07-12 10:35:49 +00:00
|
|
|
st = append(st, se.Error())
|
|
|
|
}
|
|
|
|
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
|
2022-05-16 09:38:46 +00:00
|
|
|
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) {
|
2022-10-29 15:03:57 +00:00
|
|
|
_, errs := sync.SyncThemes(sync.GlobalImporter)
|
2022-11-24 09:19:56 +00:00
|
|
|
c.JSON(http.StatusOK, flatifySyncErrors(errs))
|
2022-05-16 09:38:46 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
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)
|
2022-06-12 10:15:39 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
})
|
2024-03-14 17:43:07 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
})
|
2022-05-16 09:38:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func declareSyncExercicesRoutes(router *gin.RouterGroup) {
|
|
|
|
router.POST("/exercices", func(c *gin.Context) {
|
|
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
|
2022-10-29 15:03:57 +00:00
|
|
|
|
2023-07-09 17:05:58 +00:00
|
|
|
_, errs := sync.SyncExercices(sync.GlobalImporter, theme, exceptions)
|
|
|
|
c.JSON(http.StatusOK, flatifySyncErrors(errs))
|
2022-05-16 09:38:46 +00:00
|
|
|
})
|
|
|
|
apiSyncExercicesRoutes := router.Group("/exercices/:eid")
|
|
|
|
apiSyncExercicesRoutes.Use(ExerciceHandler)
|
|
|
|
apiSyncExercicesRoutes.POST("", func(c *gin.Context) {
|
2022-10-29 15:03:57 +00:00
|
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
2022-05-16 09:38:46 +00:00
|
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
|
|
|
|
|
2023-07-09 17:05:58 +00:00
|
|
|
_, _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil, exceptions)
|
2022-11-24 09:19:56 +00:00
|
|
|
c.JSON(http.StatusOK, flatifySyncErrors(errs))
|
2022-05-16 09:38:46 +00:00
|
|
|
})
|
|
|
|
apiSyncExercicesRoutes.POST("/files", func(c *gin.Context) {
|
|
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
2022-10-29 15:03:57 +00:00
|
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
|
|
|
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
|
2022-10-29 15:03:57 +00:00
|
|
|
|
2023-10-13 19:55:32 +00:00
|
|
|
c.JSON(http.StatusOK, flatifySyncErrors(sync.SyncExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
|
2022-05-16 09:38:46 +00:00
|
|
|
})
|
|
|
|
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)
|
2022-10-29 15:03:57 +00:00
|
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
|
|
|
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
|
2022-10-29 15:03:57 +00:00
|
|
|
|
|
|
|
_, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
|
2022-11-24 09:19:56 +00:00
|
|
|
c.JSON(http.StatusOK, flatifySyncErrors(errs))
|
2022-05-16 09:38:46 +00:00
|
|
|
})
|
|
|
|
apiSyncExercicesRoutes.POST("/flags", func(c *gin.Context) {
|
|
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
2022-10-29 15:03:57 +00:00
|
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
|
|
|
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
|
2022-10-29 15:03:57 +00:00
|
|
|
_, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions)
|
|
|
|
_, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
|
2023-11-22 11:16:53 +00:00
|
|
|
c.JSON(http.StatusOK, flatifySyncErrors(multierr.Append(errs, herrs)))
|
2022-05-16 09:38:46 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2023-05-05 10:04:28 +00:00
|
|
|
p := strings.Split(strings.TrimPrefix(c.Params.ByName("p"), "/"), "/")
|
2022-05-16 09:38:46 +00:00
|
|
|
|
2023-05-06 00:27:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-16 09:38:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-05-05 10:04:28 +00:00
|
|
|
// No argument, do a deep sync
|
|
|
|
if len(p) == 0 {
|
2022-05-16 09:38:46 +00:00
|
|
|
if !IsProductionEnv {
|
|
|
|
for _, theme := range themes {
|
|
|
|
theme.DeleteDeep()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
st := sync.SyncDeep(sync.GlobalImporter)
|
|
|
|
c.JSON(http.StatusOK, st)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-05-05 09:44:09 +00:00
|
|
|
var theTheme *fic.Theme
|
|
|
|
|
2023-05-05 10:04:28 +00:00
|
|
|
// Find the given theme
|
2022-05-16 09:38:46 +00:00
|
|
|
for _, theme := range themes {
|
2023-05-05 10:04:28 +00:00
|
|
|
if theme.Path == p[0] {
|
2023-05-05 09:44:09 +00:00
|
|
|
theTheme = theme
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2022-05-16 09:38:46 +00:00
|
|
|
|
2023-05-05 09:44:09 +00:00
|
|
|
if theTheme == nil {
|
2023-05-05 10:03:35 +00:00
|
|
|
// 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 {
|
2023-05-05 10:04:28 +00:00
|
|
|
if theme == p[0] {
|
2023-05-05 10:03:35 +00:00
|
|
|
sync.SyncThemes(sync.GlobalImporter)
|
|
|
|
|
|
|
|
themes, err := fic.GetThemes()
|
|
|
|
if err == nil {
|
|
|
|
for _, theme := range themes {
|
2023-05-05 10:04:28 +00:00
|
|
|
if theme.Path == p[0] {
|
2023-05-05 10:03:35 +00:00
|
|
|
theTheme = theme
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-05-05 10:04:28 +00:00
|
|
|
|
|
|
|
break
|
2023-05-05 10:03:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if theTheme == nil {
|
2023-05-05 10:04:28 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p[0])})
|
2023-05-05 10:03:35 +00:00
|
|
|
return
|
|
|
|
}
|
2023-05-05 09:44:09 +00:00
|
|
|
}
|
2022-10-29 15:03:57 +00:00
|
|
|
|
2023-05-05 09:44:09 +00:00
|
|
|
if !IsProductionEnv {
|
|
|
|
exercices, err := theTheme.GetExercices()
|
|
|
|
if err == nil {
|
|
|
|
for _, exercice := range exercices {
|
2023-05-05 10:04:28 +00:00
|
|
|
if len(p) <= 1 || exercice.Path == path.Join(p[0], p[1]) {
|
|
|
|
exercice.DeleteDeep()
|
|
|
|
}
|
2022-07-12 10:35:49 +00:00
|
|
|
}
|
2023-05-05 09:44:09 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-16 09:38:46 +00:00
|
|
|
|
2023-06-14 15:24:15 +00:00
|
|
|
exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme)
|
2022-05-16 09:38:46 +00:00
|
|
|
|
2023-05-05 09:44:09 +00:00
|
|
|
var st []string
|
2023-11-22 11:16:53 +00:00
|
|
|
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions)) {
|
2023-05-05 09:44:09 +00:00
|
|
|
st = append(st, se.Error())
|
2022-05-16 09:38:46 +00:00
|
|
|
}
|
2023-05-05 09:44:09 +00:00
|
|
|
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false)
|
|
|
|
sync.DeepSyncProgress = 255
|
|
|
|
|
2023-07-10 10:10:03 +00:00
|
|
|
resp, err := generation.FullGeneration()
|
|
|
|
if err == nil {
|
|
|
|
defer resp.Body.Close()
|
|
|
|
}
|
2022-05-16 09:38:46 +00:00
|
|
|
|
2023-05-05 09:44:09 +00:00
|
|
|
c.JSON(http.StatusOK, st)
|
2022-05-16 09:38:46 +00:00
|
|
|
}
|