admin: Refactor synchronization status report + display last git error

This commit is contained in:
nemunaire 2025-01-13 18:00:27 +01:00
parent cc725c8e8f
commit 51f60e59d4
6 changed files with 81 additions and 49 deletions

View File

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"reflect"
"strconv" "strconv"
"time" "time"
@ -26,7 +25,6 @@ func declareSettingsRoutes(router *gin.RouterGroup) {
router.GET("/challenge.json", getChallengeInfo) router.GET("/challenge.json", getChallengeInfo)
router.PUT("/challenge.json", saveChallengeInfo) router.PUT("/challenge.json", saveChallengeInfo)
router.GET("/settings-ro.json", getROSettings)
router.GET("/settings.json", getSettings) router.GET("/settings.json", getSettings)
router.PUT("/settings.json", saveSettings) router.PUT("/settings.json", saveSettings)
router.DELETE("/settings.json", func(c *gin.Context) { router.DELETE("/settings.json", func(c *gin.Context) {
@ -98,24 +96,6 @@ func fullGeneration(c *gin.Context) {
}) })
} }
func getROSettings(c *gin.Context) {
syncMtd := "Disabled"
if sync.GlobalImporter != nil {
syncMtd = sync.GlobalImporter.Kind()
}
var syncId *string
if sync.GlobalImporter != nil {
syncId = sync.GlobalImporter.Id()
}
c.JSON(http.StatusOK, gin.H{
"sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
"sync-id": syncId,
"sync": syncMtd,
})
}
func GetChallengeInfo() (*settings.ChallengeInfo, error) { func GetChallengeInfo() (*settings.ChallengeInfo, error) {
var challengeinfo string var challengeinfo string
var err error var err error

View File

@ -7,6 +7,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"reflect"
"strings" "strings"
"srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/generation"
@ -17,6 +18,8 @@ import (
"go.uber.org/multierr" "go.uber.org/multierr"
) )
var lastSyncError = ""
func flatifySyncErrors(errs error) (ret []string) { func flatifySyncErrors(errs error) (ret []string) {
for _, err := range multierr.Errors(errs) { for _, err := range multierr.Errors(errs) {
ret = append(ret, err.Error()) ret = append(ret, err.Error())
@ -27,12 +30,37 @@ func flatifySyncErrors(errs error) (ret []string) {
func declareSyncRoutes(router *gin.RouterGroup) { func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncRoutes := router.Group("/sync") apiSyncRoutes := router.Group("/sync")
// Return the global sync status
apiSyncRoutes.GET("/status", func(c *gin.Context) {
syncMtd := "Disabled"
if sync.GlobalImporter != nil {
syncMtd = sync.GlobalImporter.Kind()
}
var syncId *string
if sync.GlobalImporter != nil {
syncId = sync.GlobalImporter.Id()
}
c.JSON(http.StatusOK, gin.H{
"sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
"sync-id": syncId,
"sync": syncMtd,
"pullMutex": !sync.OneGitPullStatus(),
"syncMutex": !sync.OneDeepSyncStatus() && !sync.OneThemeDeepSyncStatus(),
"progress": sync.DeepSyncProgress,
"lastError": lastSyncError,
})
})
// Base sync checks if the local directory is in sync with remote one. // Base sync checks if the local directory is in sync with remote one.
apiSyncRoutes.POST("/base", func(c *gin.Context) { apiSyncRoutes.POST("/base", func(c *gin.Context) {
err := sync.GlobalImporter.Sync() err := sync.GlobalImporter.Sync()
if err != nil { if err != nil {
lastSyncError = err.Error()
c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
} else { } else {
lastSyncError = ""
c.JSON(http.StatusOK, true) c.JSON(http.StatusOK, true)
} }
}) })
@ -45,15 +73,10 @@ func declareSyncRoutes(router *gin.RouterGroup) {
}) })
// Deep sync: a fully recursive synchronization (can be limited by theme). // 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) { apiSyncRoutes.POST("/deep", func(c *gin.Context) {
c.JSON(http.StatusOK, sync.SyncDeep(sync.GlobalImporter)) r := sync.SyncDeep(sync.GlobalImporter)
lastSyncError = ""
c.JSON(http.StatusOK, r)
}) })
apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid") apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid")
@ -66,6 +89,7 @@ func declareSyncRoutes(router *gin.RouterGroup) {
} }
sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false) sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
sync.DeepSyncProgress = 255 sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st) c.JSON(http.StatusOK, st)
}) })
apiSyncDeepRoutes.POST("", func(c *gin.Context) { apiSyncDeepRoutes.POST("", func(c *gin.Context) {
@ -79,6 +103,7 @@ func declareSyncRoutes(router *gin.RouterGroup) {
} }
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false) sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
sync.DeepSyncProgress = 255 sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st) c.JSON(http.StatusOK, st)
}) })
@ -90,6 +115,7 @@ func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncRoutes.POST("/themes", func(c *gin.Context) { apiSyncRoutes.POST("/themes", func(c *gin.Context) {
_, errs := sync.SyncThemes(sync.GlobalImporter) _, errs := sync.SyncThemes(sync.GlobalImporter)
lastSyncError = ""
c.JSON(http.StatusOK, flatifySyncErrors(errs)) c.JSON(http.StatusOK, flatifySyncErrors(errs))
}) })

View File

@ -230,9 +230,6 @@ angular.module("FICApp")
.factory("File", function ($resource) { .factory("File", function ($resource) {
return $resource("api/files/:fileId", { fileId: '@id' }) return $resource("api/files/:fileId", { fileId: '@id' })
}) })
.factory("ROSettings", function ($resource) {
return $resource("api/settings-ro.json")
})
.factory("Settings", function ($resource) { .factory("Settings", function ($resource) {
return $resource("api/settings.json", null, { return $resource("api/settings.json", null, {
'update': { method: 'PUT' }, 'update': { method: 'PUT' },
@ -787,9 +784,8 @@ angular.module("FICApp")
template: `<span class="badge {{ $ctrl.color }}">{{ $ctrl.status.hash }}</span> <small>{{ $ctrl.status.text }}</small>` template: `<span class="badge {{ $ctrl.color }}">{{ $ctrl.status.hash }}</span> <small>{{ $ctrl.status.text }}</small>`
}) })
.controller("SyncController", function ($scope, $rootScope, ROSettings, $location, $http, $interval) { .controller("SyncController", function ($scope, $rootScope, $location, $http, $interval) {
$scope.displayDangerousActions = false; $scope.displayDangerousActions = false;
$scope.configro = ROSettings.get();
var needRefreshSyncReportWhenReady = false; var needRefreshSyncReportWhenReady = false;
var refreshSyncReport = function () { var refreshSyncReport = function () {
@ -800,30 +796,28 @@ angular.module("FICApp")
}; };
refreshSyncReport() refreshSyncReport()
$scope.deepSyncInProgress = false;
var progressInterval = $interval(function () { var progressInterval = $interval(function () {
$http.get("api/sync/deep").then(function (response) { $http.get("api/sync/status").then(function (response) {
if (response.data.progress && response.data.progress != 255) if (response.data.progress && response.data.progress != 255)
needRefreshSyncReportWhenReady = true; needRefreshSyncReportWhenReady = true;
else if (needRefreshSyncReportWhenReady) else if (needRefreshSyncReportWhenReady)
refreshSyncReport(); refreshSyncReport();
if (response.data && response.data.progress) { if (response.data && response.data.progress) {
$scope.syncPercent = Math.floor(response.data.progress * 100 / 255); $scope.syncPercent = Math.floor(response.data.progress * 100 / 255);
$scope.syncProgress = Math.floor(response.data.progress * 100 / 255) + " %"; $scope.deepSyncInProgress = response.data.pullMutex && response.data.syncMutex;
} else { } else {
$scope.syncProgress = response.data;
$scope.syncPercent = 0; $scope.syncPercent = 0;
} }
$scope.syncStatus = response.data;
}, function (response) { }, function (response) {
$scope.syncPercent = 0; $scope.syncPercent = 0;
if (response.data && response.data.errmsg) $scope.syncStatus = response.data;
$scope.syncProgress = response.data.errmsg;
else
$scope.syncProgress = response.data;
}) })
}, 1500); }, 1500);
$scope.$on('$destroy', function () { $interval.cancel(progressInterval); }); $scope.$on('$destroy', function () { $interval.cancel(progressInterval); });
$scope.deepSyncInProgress = false;
$scope.deepSync = function (theme) { $scope.deepSync = function (theme) {
if (theme) { if (theme) {
question = 'Faire une synchronisation intégrale du thème ' + theme.name + ' ?' question = 'Faire une synchronisation intégrale du thème ' + theme.name + ' ?'

View File

@ -32,17 +32,25 @@
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-2">Type</dt> <dt class="col-2">Type</dt>
<dd class="col-10" ng-bind="configro['sync-type']"></dd> <dd class="col-10" ng-bind="syncStatus['sync-type']"></dd>
<dt class="col-2">Synchronisation</dt> <dt class="col-2">Synchronisation</dt>
<dd class="col-10" title="{{ configro['sync'] }}" ng-bind="configro.sync"></dd> <dd class="col-10" title="{{ syncStatus['sync'] }}" ng-bind="syncStatus.sync"></dd>
<dt class="col-2" ng-if="configro['sync-id']">ID</dt> <dt class="col-2" ng-if="syncStatus['sync-id']">ID</dt>
<dd class="col-10" ng-if="configro['sync-id']">{{ configro['sync-id'] }}</dd> <dd class="col-10" ng-if="syncStatus['sync-id']">
<dt class="col-2" ng-if="configro['sync']">Statut</dt> {{ syncStatus['sync-id'] }}
<dd class="col-10" ng-if="configro['sync']">{{ syncProgress }}</dd> <button ng-if="syncStatus['sync-type'] === 'GitImporter'" type="button" class="btn btn-sm btn-dark" ng-click="baseSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Pull</button>
</dd>
<dt class="col-2" ng-if="syncStatus['sync']">Statut</dt>
<dd class="col-10" ng-if="syncStatus['sync']">
<span ng-if="(syncStatus.syncMutex !== undefined && syncStatus.syncMutex) || (syncPercent > 0 && syncPercent < 100)">{{ syncPercent }}&nbsp;%</span>
<span class="badge badge-pill" ng-class="{'badge-success': !syncStatus.pullMutex, 'badge-danger': syncStatus.pullMutex}" ng-if="syncStatus.pullMutex !== undefined">Pull</span>
<span class="badge badge-pill" ng-class="{'badge-success': !syncStatus.syncMutex, 'badge-danger': syncStatus.syncMutex}" ng-if="syncStatus.syncMutex !== undefined">Synchronisation</span>
</dd>
</dl> </dl>
<div class="d-flex justify-content-around" ng-if="configro.sync"> <pre style="background: black; color: white;" class="p-2" ng-if="syncStatus.lastError">{{ syncStatus.lastError }}</pre>
<button ng-if="configro['sync-type'] === 'GitImporter'" type="button" class="btn btn-info" ng-click="baseSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Pull</button>
<div class="d-flex justify-content-around" ng-if="syncStatus.sync">
<div class="btn-group dropright"> <div class="btn-group dropright">
<button type="button" class="btn btn-secondary" ng-click="deepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation intégrale</button> <button type="button" class="btn btn-secondary" ng-click="deepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation intégrale</button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="deepSyncInProgress"> <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="deepSyncInProgress">

View File

@ -24,6 +24,22 @@ var oneThemeDeepSync sync.Mutex
// DeepSyncProgress expose the progression of the depp synchronization (0 = 0%, 255 = 100%). // DeepSyncProgress expose the progression of the depp synchronization (0 = 0%, 255 = 100%).
var DeepSyncProgress uint8 var DeepSyncProgress uint8
func OneDeepSyncStatus() bool {
if oneDeepSync.TryLock() {
oneDeepSync.Unlock()
return true
}
return false
}
func OneThemeDeepSyncStatus() bool {
if oneThemeDeepSync.TryLock() {
oneThemeDeepSync.Unlock()
return true
}
return false
}
type SyncReport struct { type SyncReport struct {
DateStart time.Time `json:"_started"` DateStart time.Time `json:"_started"`
DateEnd time.Time `json:"_ended"` DateEnd time.Time `json:"_ended"`

View File

@ -12,6 +12,14 @@ var gitRemoteRe = regexp.MustCompile(`^(?:(?:git@|https://)([\w.@]+)(?:/|:))((?:
var oneGitPull sync.Mutex var oneGitPull sync.Mutex
func OneGitPullStatus() bool {
if oneGitPull.TryLock() {
oneGitPull.Unlock()
return true
}
return false
}
func countFileInDir(dirname string) (int, error) { func countFileInDir(dirname string) (int, error) {
files, err := os.ReadDir(dirname) files, err := os.ReadDir(dirname)
if err != nil { if err != nil {