package sync import ( "fmt" "image" "image/jpeg" "math/rand" "os" "path" "regexp" "strings" "unicode" "github.com/julienschmidt/httprouter" "github.com/russross/blackfriday/v2" "golang.org/x/image/draw" "srs.epita.fr/fic-server/libfic" ) // GetThemes returns all theme directories in the base directory. func GetThemes(i Importer) ([]string, error) { var themes []string if dirs, err := i.listDir("/"); err != nil { return nil, err } else { for _, dir := range dirs { if !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{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{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, errs []string) { th = &fic.Theme{} th.Path = tdir // 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, fmt.Sprintf("%q: unable to get AUTHORS.txt: %s", th.Name, err)) return nil, errs } else { // Format authors th.Authors = strings.Join(authors, ", ") } if intro, err := getFileContent(i, path.Join(tdir, "overview.txt")); err != nil { errs = append(errs, fmt.Sprintf("%q: unable to get theme's overview: %s", th.Name, err)) } else { // 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, fmt.Sprintf("%q: overview.txt: an error occurs during markdown formating: %s", tdir, err)) } th.Headline = string(blackfriday.Run([]byte(th.Headline))) } 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, fmt.Sprintf("%q: heading.jpg: No such file", tdir)) } 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, fmt.Sprintf("%q: unable to get partner's text: %s", th.Name, err)) } else { th.PartnerText, err = ProcessMarkdown(i, txt, tdir) if err != nil { errs = append(errs, fmt.Sprintf("%q: partner.txt: an error occurs during markdown formating: %s", tdir, err)) } } } return } // SyncThemes imports new or updates existing themes. func SyncThemes(i Importer) []string { var errs []string if themes, err := GetThemes(i); err != nil { errs = append(errs, err.Error()) } else { rand.Shuffle(len(themes), func(i, j int) { themes[i], themes[j] = themes[j], themes[i] }) for _, tdir := range themes { btheme, berrs := BuildTheme(i, tdir) errs = append(errs, berrs...) if btheme == nil { continue } 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, fmt.Sprintf("%q: unable to import heading image: %s", tdir, 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, fmt.Sprintf("%q: unable to import partner image: %s", tdir, err)) } } var theme fic.Theme if theme, err = fic.GetThemeByPath(btheme.Path); err != nil { if _, err := fic.CreateTheme(*btheme); err != nil { errs = append(errs, fmt.Sprintf("%q: an error occurs during add: %s", tdir, err)) continue } } if !fic.CmpTheme(theme, *btheme) { btheme.Id = theme.Id if _, err := btheme.Update(); err != nil { errs = append(errs, fmt.Sprintf("%q: an error occurs during update: %s", tdir, err)) continue } } } } return errs } // ApiListRemoteThemes is an accessor letting foreign packages to access remote themes list. func ApiListRemoteThemes(_ httprouter.Params, _ []byte) (interface{}, error) { return GetThemes(GlobalImporter) } // ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes. func ApiGetRemoteTheme(ps httprouter.Params, _ []byte) (interface{}, error) { r, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) if r == nil { return r, fmt.Errorf("%q", errs) } else { return r, nil } }