2017-12-09 00:21:58 +00:00
package sync
import (
2022-05-19 10:45:32 +00:00
"bytes"
2017-12-09 00:21:58 +00:00
"fmt"
2021-05-13 23:14:30 +00:00
"log"
2022-05-16 09:38:46 +00:00
"net/http"
2022-05-24 15:36:33 +00:00
"net/url"
2017-12-09 00:21:58 +00:00
"path"
"strconv"
2019-07-04 18:12:59 +00:00
"strings"
2017-12-09 00:21:58 +00:00
2019-07-21 20:31:43 +00:00
"github.com/BurntSushi/toml"
2022-05-16 09:38:46 +00:00
"github.com/gin-gonic/gin"
2022-05-19 10:45:32 +00:00
"github.com/yuin/goldmark"
2019-07-21 20:31:43 +00:00
2019-07-04 18:12:59 +00:00
"srs.epita.fr/fic-server/libfic"
2017-12-09 00:21:58 +00:00
)
2021-05-13 23:14:30 +00:00
// LogMissingResolution logs the absence of resolution.mp4 instead of returning an error.
var LogMissingResolution = false
2018-12-09 22:59:20 +00:00
func fixnbsp ( s string ) string {
return strings . Replace ( strings . Replace ( strings . Replace ( s , " ?" , " ?" , - 1 ) , " !" , " !" , - 1 ) , " :" , " :" , - 1 )
}
2019-07-04 18:12:59 +00:00
// GetExercices returns all exercice directories existing in a given theme, considering the given Importer.
2021-11-22 14:35:07 +00:00
func GetExercices ( i Importer , theme * fic . Theme ) ( [ ] string , error ) {
2017-12-09 00:21:58 +00:00
var exercices [ ] string
2018-07-13 05:25:21 +00:00
if len ( theme . Path ) == 0 {
return [ ] string { } , nil
} else if dirs , err := i . listDir ( theme . Path ) ; err != nil {
2017-12-09 00:21:58 +00:00
return [ ] string { } , err
} else {
for _ , dir := range dirs {
2018-07-13 05:25:21 +00:00
if _ , err := i . listDir ( path . Join ( theme . Path , dir ) ) ; err == nil {
2021-05-13 23:25:08 +00:00
if dir [ 0 ] != '.' && strings . Contains ( dir , "-" ) {
2019-07-05 23:39:35 +00:00
exercices = append ( exercices , dir )
}
2017-12-09 00:21:58 +00:00
}
}
}
return exercices , nil
}
2021-11-22 14:35:07 +00:00
func buildDependancyMap ( i Importer , theme * fic . Theme ) ( dmap map [ int64 ] * fic . Exercice , err error ) {
2018-12-05 04:02:27 +00:00
var exercices [ ] string
2019-07-04 18:12:59 +00:00
if exercices , err = GetExercices ( i , theme ) ; err != nil {
2018-12-05 04:02:27 +00:00
return
2017-12-09 00:21:58 +00:00
} else {
2021-11-22 14:35:07 +00:00
dmap = map [ int64 ] * fic . Exercice { }
2018-12-05 04:02:27 +00:00
2017-12-09 00:21:58 +00:00
for _ , edir := range exercices {
2018-12-05 04:02:27 +00:00
var eid int
var ename string
eid , ename , err = parseExerciceDirname ( edir )
if err != nil {
2018-12-09 23:30:02 +00:00
err = nil
continue
2017-12-09 00:21:58 +00:00
}
2021-11-22 14:35:07 +00:00
var e * fic . Exercice
2018-12-05 04:02:27 +00:00
e , err = theme . GetExerciceByTitle ( ename )
2017-12-09 00:21:58 +00:00
if err != nil {
2018-12-05 04:02:27 +00:00
return
2017-12-09 00:21:58 +00:00
}
2018-12-05 04:02:27 +00:00
dmap [ int64 ( eid ) ] = e
}
return
}
}
2017-12-09 00:21:58 +00:00
2018-12-05 04:02:27 +00:00
func parseExerciceDirname ( edir string ) ( eid int , ename string , err error ) {
edir_splt := strings . SplitN ( edir , "-" , 2 )
if len ( edir_splt ) != 2 {
2020-04-15 05:39:38 +00:00
err = fmt . Errorf ( "%q is not a valid exercice directory: missing id prefix" , edir )
2018-12-05 04:02:27 +00:00
return
}
2017-12-17 13:30:48 +00:00
2018-12-05 04:02:27 +00:00
eid , err = strconv . Atoi ( edir_splt [ 0 ] )
if err != nil {
2020-04-15 05:39:38 +00:00
err = fmt . Errorf ( "%q: invalid exercice identifier: %s" , edir , err )
2018-12-05 04:02:27 +00:00
return
}
ename = edir_splt [ 1 ]
return
}
2019-07-04 18:12:59 +00:00
// BuildExercice creates an Exercice from a given importer.
2021-11-22 14:35:07 +00:00
func BuildExercice ( i Importer , theme * fic . Theme , epath string , dmap * map [ int64 ] * fic . Exercice ) ( e * fic . Exercice , p ExerciceParams , eid int , edir string , errs [ ] string ) {
2019-07-04 18:12:59 +00:00
e = & fic . Exercice { }
2018-12-05 04:02:27 +00:00
e . Path = epath
2019-07-04 18:12:59 +00:00
edir = path . Base ( epath )
2018-12-05 04:02:27 +00:00
2019-07-04 18:12:59 +00:00
var err error
2018-12-05 04:02:27 +00:00
eid , e . Title , err = parseExerciceDirname ( edir )
if err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: unable to parse exercice directory: %s" , edir , err ) )
2019-07-04 18:12:59 +00:00
return nil , p , eid , edir , errs
2018-12-05 04:02:27 +00:00
}
2022-01-20 15:16:52 +00:00
// Overwrite title if title.txt exists
2022-05-24 19:52:58 +00:00
if myTitle , err := GetFileContent ( i , path . Join ( epath , "title.txt" ) ) ; err == nil {
2022-01-20 15:16:52 +00:00
myTitle = strings . TrimSpace ( myTitle )
if strings . Contains ( myTitle , "\n" ) {
errs = append ( errs , fmt . Sprintf ( "%q: title.txt: Title can't contain new lines" , edir ) )
} else {
e . Title = myTitle
}
}
2018-12-05 04:02:27 +00:00
e . URLId = fic . ToURLid ( e . Title )
2018-12-09 22:59:20 +00:00
e . Title = fixnbsp ( e . Title )
2018-12-05 04:02:27 +00:00
// Texts to format using Markdown
2022-05-19 14:39:24 +00:00
if i . exists ( path . Join ( epath , "overview.txt" ) ) {
2022-05-24 19:52:58 +00:00
e . Overview , err = GetFileContent ( i , path . Join ( epath , "overview.txt" ) )
2022-05-19 14:39:24 +00:00
} else if i . exists ( path . Join ( epath , "overview.md" ) ) {
2022-05-24 19:52:58 +00:00
e . Overview , err = GetFileContent ( i , path . Join ( epath , "overview.md" ) )
2022-05-19 14:39:24 +00:00
} else {
err = fmt . Errorf ( "Unable to find overview.txt nor overview.md" )
}
2018-12-05 04:02:27 +00:00
if err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: overview.txt: %s" , edir , err ) )
} else {
2018-12-09 22:59:20 +00:00
e . Overview = fixnbsp ( e . Overview )
2022-05-19 10:45:32 +00:00
var buf bytes . Buffer
err := goldmark . Convert ( [ ] byte ( strings . Split ( e . Overview , "\n" ) [ 0 ] ) , & buf )
if err != nil {
2022-05-19 14:39:24 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: overview.md: an error occurs during markdown formating of the headline: %s" , edir , err ) )
2022-05-19 10:45:32 +00:00
} else {
e . Headline = string ( buf . Bytes ( ) )
}
2022-05-19 14:39:24 +00:00
if e . Overview , err = ProcessMarkdown ( i , e . Overview , epath ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: overview.md: an error occurs during markdown formating: %s" , edir , err ) )
}
2018-12-05 04:02:27 +00:00
}
2022-05-19 14:39:24 +00:00
if i . exists ( path . Join ( epath , "statement.txt" ) ) {
2022-05-24 19:52:58 +00:00
e . Statement , err = GetFileContent ( i , path . Join ( epath , "statement.txt" ) )
2022-05-19 14:39:24 +00:00
} else if i . exists ( path . Join ( epath , "statement.md" ) ) {
2022-05-24 19:52:58 +00:00
e . Statement , err = GetFileContent ( i , path . Join ( epath , "statement.md" ) )
2022-05-19 14:39:24 +00:00
} else {
err = fmt . Errorf ( "Unable to find statement.txt nor statement.md" )
}
2018-12-05 04:02:27 +00:00
if err != nil {
2022-05-19 14:39:24 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: statement.md: %s" , edir , err ) )
2018-12-05 04:02:27 +00:00
} else {
2018-12-09 22:59:20 +00:00
if e . Statement , err = ProcessMarkdown ( i , fixnbsp ( e . Statement ) , epath ) ; err != nil {
2022-05-19 14:39:24 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: statement.md: an error occurs during markdown formating: %s" , edir , err ) )
2018-12-05 04:02:27 +00:00
}
}
if i . exists ( path . Join ( epath , "finished.txt" ) ) {
2022-05-24 19:52:58 +00:00
e . Finished , err = GetFileContent ( i , path . Join ( epath , "finished.txt" ) )
2018-12-05 04:02:27 +00:00
if err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: finished.txt: %s" , edir , err ) )
} else {
if e . Finished , err = ProcessMarkdown ( i , e . Finished , epath ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: finished.txt: an error occurs during markdown formating: %s" , edir , err ) )
2017-12-09 00:21:58 +00:00
}
2018-12-05 04:02:27 +00:00
}
}
2017-12-09 00:21:58 +00:00
2018-12-05 04:02:27 +00:00
// Parse challenge.txt
2019-07-21 20:31:43 +00:00
var md toml . MetaData
p , md , err = parseExerciceParams ( i , epath )
2018-12-05 04:02:27 +00:00
if err != nil {
2019-07-21 20:31:43 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: %s" , edir , err ) )
2018-12-05 04:02:27 +00:00
return
}
2019-07-21 20:31:43 +00:00
// Alert about unknown keys in challenge.txt
if len ( md . Undecoded ( ) ) > 0 {
for _ , k := range md . Undecoded ( ) {
2019-09-06 18:41:15 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: challenge.txt: unknown key %q found, check https://srs.nemunai.re/fic/files/challenge/" , edir , k ) )
2019-07-21 20:31:43 +00:00
}
}
2018-12-05 04:02:27 +00:00
if p . Gain == 0 {
errs = append ( errs , fmt . Sprintf ( "%q: challenge.txt: Undefined gain for challenge" , edir ) )
} else {
e . Gain = p . Gain
}
// Handle dependency
if len ( p . Dependencies ) > 0 {
if len ( p . Dependencies [ 0 ] . Theme ) > 0 && p . Dependencies [ 0 ] . Theme != theme . Name {
errs = append ( errs , fmt . Sprintf ( "%q: unable to treat dependency to another theme (%q): not implemented." , edir , p . Dependencies [ 0 ] . Theme ) )
} else {
if dmap == nil {
if dmap2 , err := buildDependancyMap ( i , theme ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: unable to build dependency map: %s" , edir , err ) )
} else {
dmap = & dmap2
2018-12-02 16:53:26 +00:00
}
}
2018-12-05 04:02:27 +00:00
if dmap != nil {
for edk , ed := range * dmap {
if edk == p . Dependencies [ 0 ] . Id {
e . Depend = & ed . Id
break
}
}
if e . Depend == nil {
dmap_keys := [ ] string { }
for k , _ := range * dmap {
dmap_keys = append ( dmap_keys , fmt . Sprintf ( "%d" , k ) )
2018-09-09 20:50:06 +00:00
}
2019-07-04 18:12:59 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: Unable to find required exercice dependancy %d (available at time of processing: %s)" , edir , p . Dependencies [ 0 ] . Id , strings . Join ( dmap_keys , "," ) ) )
2018-09-09 20:50:06 +00:00
}
2018-01-10 02:45:56 +00:00
}
2018-12-05 04:02:27 +00:00
}
}
2017-12-09 00:21:58 +00:00
2021-12-10 17:55:47 +00:00
// Handle resolutions
resolutionFound := false
2018-12-05 04:02:27 +00:00
e . VideoURI = path . Join ( epath , "resolution.mp4" )
if ! i . exists ( e . VideoURI ) {
e . VideoURI = ""
2019-01-19 00:13:17 +00:00
} else if size , err := getFileSize ( i , e . VideoURI ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: resolution.mp4: " , edir , err ) )
e . VideoURI = ""
} else if size == 0 {
errs = append ( errs , fmt . Sprintf ( "%q: resolution.mp4: The file is empty!" , edir ) )
e . VideoURI = ""
2021-12-10 17:55:47 +00:00
} else {
2022-05-24 15:36:33 +00:00
e . VideoURI = strings . Replace ( url . PathEscape ( path . Join ( "$FILES$" , e . VideoURI ) ) , "%2F" , "/" , - 1 )
2021-12-10 17:55:47 +00:00
resolutionFound = true
}
writeup := path . Join ( epath , "resolution.md" )
if ! i . exists ( writeup ) {
writeup = path . Join ( epath , "resolution.txt" )
}
if i . exists ( writeup ) {
if size , err := getFileSize ( i , writeup ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: resolution.md: %s" , edir , err . Error ( ) ) )
} else if size == 0 {
errs = append ( errs , fmt . Sprintf ( "%q: resolution.md: The file is empty!" , edir ) )
2022-05-24 19:52:58 +00:00
} else if e . Resolution , err = GetFileContent ( i , writeup ) ; err != nil {
2021-12-10 17:55:47 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: resolution.md: %s" , edir , err . Error ( ) ) )
2022-05-19 14:39:24 +00:00
} else if e . Resolution , err = ProcessMarkdown ( i , e . Resolution , epath ) ; err != nil {
2021-12-10 17:55:47 +00:00
errs = append ( errs , fmt . Sprintf ( "%q: resolution.md: error during markdown processing: %s" , edir , err . Error ( ) ) )
} else {
resolutionFound = true
}
}
if ! resolutionFound {
if LogMissingResolution {
log . Printf ( "%q: no resolution video or text file found in %s" , edir , epath , epath )
} else {
errs = append ( errs , fmt . Sprintf ( "%q: no resolution video or text file found in %s" , edir , epath ) )
}
2018-12-05 04:02:27 +00:00
}
2017-12-09 00:21:58 +00:00
2019-07-04 18:12:59 +00:00
return
}
2018-08-17 19:18:10 +00:00
2019-07-04 18:12:59 +00:00
// SyncExercice imports new or updates existing given exercice.
2021-11-22 14:35:07 +00:00
func SyncExercice ( i Importer , theme * fic . Theme , epath string , dmap * map [ int64 ] * fic . Exercice ) ( e * fic . Exercice , eid int , errs [ ] string ) {
2019-07-04 18:12:59 +00:00
var err error
var edir string
var p ExerciceParams
e , p , eid , edir , errs = BuildExercice ( i , theme , epath , dmap )
if e != nil {
// Create or update the exercice
err = theme . SaveNamedExercice ( e )
if err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: error on exercice save: %s" , edir , err ) )
2018-12-05 04:02:27 +00:00
return
}
2019-07-04 18:12:59 +00:00
// Import eercice tags
if _ , err := e . WipeTags ( ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: Unable to wipe tags: %s" , edir , err ) )
}
for _ , tag := range p . Tags {
if _ , err := e . AddTag ( tag ) ; err != nil {
errs = append ( errs , fmt . Sprintf ( "%q: Unable to add tag: %s" , edir , err ) )
return
}
}
2018-12-05 04:02:27 +00:00
}
2019-07-04 18:12:59 +00:00
2018-12-05 04:02:27 +00:00
return
}
2018-11-18 21:44:23 +00:00
2018-12-05 04:02:27 +00:00
// SyncExercices imports new or updates existing exercices, in a given theme.
2021-11-22 14:35:07 +00:00
func SyncExercices ( i Importer , theme * fic . Theme ) ( errs [ ] string ) {
2019-07-04 18:12:59 +00:00
if exercices , err := GetExercices ( i , theme ) ; err != nil {
2018-12-05 04:02:27 +00:00
errs = append ( errs , err . Error ( ) )
} else {
emap := map [ string ] int { }
dmap , _ := buildDependancyMap ( i , theme )
for _ , edir := range exercices {
e , eid , cur_errs := SyncExercice ( i , theme , path . Join ( theme . Path , edir ) , & dmap )
2019-07-04 18:12:59 +00:00
if e != nil {
emap [ e . Title ] = eid
2021-11-22 14:35:07 +00:00
dmap [ int64 ( eid ) ] = e
2019-07-04 18:12:59 +00:00
errs = append ( errs , cur_errs ... )
}
2017-12-09 00:21:58 +00:00
}
2017-12-12 15:54:32 +00:00
// Remove old exercices
if exercices , err := theme . GetExercices ( ) ; err == nil {
for _ , ex := range exercices {
if _ , ok := emap [ ex . Title ] ; ! ok {
2018-12-04 22:11:45 +00:00
ex . DeleteCascade ( )
2017-12-17 15:00:32 +00:00
}
}
}
2017-12-09 00:21:58 +00:00
}
2018-12-05 04:02:27 +00:00
return
2017-12-09 00:21:58 +00:00
}
2018-05-11 23:08:37 +00:00
// ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
2022-05-16 09:38:46 +00:00
func ApiListRemoteExercices ( c * gin . Context ) {
theme , errs := BuildTheme ( GlobalImporter , c . Params . ByName ( "thid" ) )
2019-09-06 23:25:42 +00:00
if theme != nil {
2022-05-16 09:38:46 +00:00
exercices , err := GetExercices ( GlobalImporter , theme )
if err != nil {
c . AbortWithStatusJSON ( http . StatusInternalServerError , gin . H { "errmsg" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , exercices )
2019-09-06 23:25:42 +00:00
} else {
2022-05-16 09:38:46 +00:00
c . AbortWithStatusJSON ( http . StatusInternalServerError , fmt . Errorf ( "%q" , errs ) )
return
2019-09-06 23:25:42 +00:00
}
}
// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
2022-05-16 09:38:46 +00:00
func ApiGetRemoteExercice ( c * gin . Context ) {
theme , errs := BuildTheme ( GlobalImporter , c . Params . ByName ( "thid" ) )
2019-09-06 23:25:42 +00:00
if theme != nil {
2022-05-16 09:38:46 +00:00
exercice , _ , _ , _ , errs := BuildExercice ( GlobalImporter , theme , path . Join ( theme . Path , c . Params . ByName ( "exid" ) ) , nil )
2019-09-06 23:25:42 +00:00
if exercice != nil {
2022-05-16 09:38:46 +00:00
c . JSON ( http . StatusOK , exercice )
return
2019-09-06 23:25:42 +00:00
} else {
2022-05-16 09:38:46 +00:00
c . JSON ( http . StatusInternalServerError , gin . H { "errmsg" : fmt . Errorf ( "%q" , errs ) } )
return
2019-09-06 23:25:42 +00:00
}
} else {
2022-05-16 09:38:46 +00:00
c . JSON ( http . StatusInternalServerError , gin . H { "errmsg" : fmt . Errorf ( "%q" , errs ) } )
return
2019-09-06 23:25:42 +00:00
}
2017-12-09 00:21:58 +00:00
}