package sync import ( "bytes" "fmt" "image" "image/jpeg" "math/rand" "net/http" "os" "path" "regexp" "strings" "unicode" "github.com/gin-gonic/gin" "github.com/yuin/goldmark" "golang.org/x/image/draw" "srs.epita.fr/fic-server/libfic" ) // GetThemes returns all theme directories in the base directory. func GetThemes(i Importer) (themes []string, err error) { if dirs, err := i.listDir("/"); err != nil { return nil, err } else { for _, dir := range dirs { if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") { if _, err := i.listDir(dir); err == nil { themes = append(themes, dir) } } } } return themes, nil } // resizePicture makes the given image just fill the given rectangle. func resizePicture(importedPath string, rect image.Rectangle) error { if fl, err := os.Open(importedPath); err != nil { return err } else { if src, _, err := image.Decode(fl); err != nil { fl.Close() return err } else if src.Bounds().Max.X > rect.Max.X && src.Bounds().Max.Y > rect.Max.Y { fl.Close() mWidth := rect.Max.Y * src.Bounds().Max.X / src.Bounds().Max.Y mHeight := rect.Max.X * src.Bounds().Max.Y / src.Bounds().Max.X if mWidth > rect.Max.X { rect.Max.X = mWidth } else { rect.Max.Y = mHeight } dst := image.NewRGBA(rect) draw.CatmullRom.Scale(dst, rect, src, src.Bounds(), draw.Over, nil) dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg") if err != nil { return err } defer dstFile.Close() if err = jpeg.Encode(dstFile, dst, &jpeg.Options{Quality: 100}); err != nil { return err } } else { dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg") if err != nil { return err } defer dstFile.Close() if err = jpeg.Encode(dstFile, src, &jpeg.Options{Quality: 100}); err != nil { return err } } } return nil } // getAuthors parses the AUTHORS file. func getAuthors(i Importer, tname string) ([]string, error) { if authors, err := GetFileContent(i, path.Join(tname, "AUTHORS.txt")); err != nil { return nil, err } else { var ret []string re := regexp.MustCompile("^([^<]+)(?: +<(.*)>)?$") for _, a := range strings.Split(authors, "\n") { a = strings.TrimFunc(a, unicode.IsSpace) grp := re.FindStringSubmatch(a) if len(grp) < 2 || grp[2] == "" { ret = append(ret, a) } else { ret = append(ret, fmt.Sprintf("%s", grp[2], grp[1])) } } return ret, nil } } // BuildTheme creates a Theme from a given importer. func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExceptions, errs []error) { th = &fic.Theme{} th.Path = tdir // Get exceptions exceptions = LoadThemeException(i, th) // Overwrite language if language, err := GetFileContent(i, path.Join(tdir, "language.txt")); err == nil { language = strings.TrimSpace(language) if strings.Contains(language, "\n") { errs = append(errs, NewThemeError(th, fmt.Errorf("language.txt: Language can't contain new lines"))) } else { th.Language = language } } // Extract theme's label if tname, err := GetFileContent(i, path.Join(tdir, "title.txt")); err == nil { th.Name = fixnbsp(tname) } else if f := strings.Index(tdir, "-"); f >= 0 { th.Name = fixnbsp(tdir[f+1:]) } else { th.Name = fixnbsp(tdir) } th.URLId = fic.ToURLid(th.Name) if authors, err := getAuthors(i, tdir); err != nil { errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err))) return nil, nil, errs } else { // Format authors th.Authors = strings.Join(authors, ", ") } var intro string var err error if i.Exists(path.Join(tdir, "overview.txt")) { intro, err = GetFileContent(i, path.Join(tdir, "overview.txt")) } else if i.Exists(path.Join(tdir, "overview.md")) { intro, err = GetFileContent(i, path.Join(tdir, "overview.md")) } else { err = fmt.Errorf("unable to find overview.txt nor overview.md") } if err != nil { errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's overview: %w", err))) } else { // Call checks hooks for _, h := range hooks.mdTextHooks { for _, err := range h(intro, th.Language, exceptions.GetFileExceptions("overview.md", "overview.txt")) { errs = append(errs, NewThemeError(th, fmt.Errorf("overview.md: %w", err))) } } // Split headline from intro ovrvw := strings.Split(fixnbsp(intro), "\n") th.Headline = ovrvw[0] if len(ovrvw) > 1 { intro = strings.Join(ovrvw[1:], "\n") } // Format overview (markdown) th.Intro, err = ProcessMarkdown(i, intro, tdir) if err != nil { errs = append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating: %w", err))) } var buf bytes.Buffer err := goldmark.Convert([]byte(th.Headline), &buf) if err != nil { errs = append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating of the headline: %w", err))) } else { th.Headline = string(buf.Bytes()) } } if i.Exists(path.Join(tdir, "heading.jpg")) { th.Image = path.Join(tdir, "heading.jpg") } else if i.Exists(path.Join(tdir, "heading.png")) { th.Image = path.Join(tdir, "heading.png") } else { errs = append(errs, NewThemeError(th, fmt.Errorf("heading.jpg: No such file"))) } if i.Exists(path.Join(tdir, "partner.jpg")) { th.PartnerImage = path.Join(tdir, "partner.jpg") } else if i.Exists(path.Join(tdir, "partner.png")) { th.PartnerImage = path.Join(tdir, "partner.png") } if i.Exists(path.Join(tdir, "partner.txt")) { if txt, err := GetFileContent(i, path.Join(tdir, "partner.txt")); err != nil { errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get partner's text: %w", err))) } else { th.PartnerText, err = ProcessMarkdown(i, txt, tdir) if err != nil { errs = append(errs, NewThemeError(th, fmt.Errorf("partner.txt: an error occurs during markdown formating: %w", err))) } } } return } // SyncThemes imports new or updates existing themes. func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs []error) { if themes, err := GetThemes(i); err != nil { errs = append(errs, fmt.Errorf("Unable to list themes: %w", err)) } else { rand.Shuffle(len(themes), func(i, j int) { themes[i], themes[j] = themes[j], themes[i] }) exceptions = map[string]*CheckExceptions{} for _, tdir := range themes { btheme, excepts, berrs := BuildTheme(i, tdir) for _, e := range berrs { errs = append(errs, e) } if btheme == nil { continue } exceptions[tdir] = excepts if len(btheme.Image) > 0 { if _, err := i.importFile(btheme.Image, func(filePath string, origin string) (interface{}, error) { if err := resizePicture(filePath, image.Rect(0, 0, 500, 300)); err != nil { return nil, err } btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir) return nil, nil }); err != nil { errs = append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err))) } } if len(btheme.PartnerImage) > 0 { if _, err := i.importFile(btheme.PartnerImage, func(filePath string, origin string) (interface{}, error) { btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir) return nil, nil }); err != nil { errs = append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err))) } } var theme *fic.Theme if theme, err = fic.GetThemeByPath(btheme.Path); err != nil { if _, err := fic.CreateTheme(btheme); err != nil { errs = append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during add: %w", err))) continue } } if !fic.CmpTheme(theme, btheme) { btheme.Id = theme.Id if _, err := btheme.Update(); err != nil { errs = append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during update: %w", err))) continue } } } } return } func LoadThemeExceptions(i Importer, theme *fic.Theme) (*CheckExceptions, error) { return nil, nil } // ApiListRemoteThemes is an accessor letting foreign packages to access remote themes list. func ApiListRemoteThemes(c *gin.Context) { themes, err := GetThemes(GlobalImporter) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } c.JSON(http.StatusOK, themes) } // ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes. func ApiGetRemoteTheme(c *gin.Context) { r, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if r == nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)}) return } c.JSON(http.StatusOK, r) }