Files

150 lines
3.6 KiB
Go

package api
import (
_ "embed"
"encoding/json"
"errors"
"io"
"io/fs"
"net/http"
"os"
"rawmark/internal/i18n"
"regexp"
)
var themeIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,63}$`)
type themeManifest struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
}
//go:embed themes/white.css
var whiteThemeCSS string
//go:embed themes/black.css
var blackThemeCSS string
var builtinThemes = []struct {
themeManifest
css string
}{
{themeManifest{"white", "白色", "1"}, whiteThemeCSS},
{themeManifest{"black", "黑色", "1"}, blackThemeCSS},
}
func builtinTheme(id string) (themeManifest, string, bool) {
for _, theme := range builtinThemes {
if theme.ID == id {
return theme.themeManifest, theme.css, true
}
}
return themeManifest{}, "", false
}
func readThemeFile(root *os.Root, name string, limit int64) ([]byte, error) {
f, err := root.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() {
return nil, i18n.New("theme_file_must_be_a_1fe9df")
}
data, err := io.ReadAll(io.LimitReader(f, limit+1))
if err != nil {
return nil, err
}
if int64(len(data)) > limit {
return nil, i18n.New("theme_file_is_too_large_08667a")
}
return data, nil
}
func (s *Server) readTheme(id string) (themeManifest, string, error) {
if manifest, css, ok := builtinTheme(id); ok {
return manifest, css, nil
}
var manifest themeManifest
if !themeIDPattern.MatchString(id) || id == "default" || s.ThemesDir == "" {
return manifest, "", fs.ErrNotExist
}
root, err := os.OpenRoot(s.ThemesDir)
if err != nil {
return manifest, "", err
}
defer root.Close()
dir, err := root.OpenRoot(id)
if err != nil {
return manifest, "", err
}
defer dir.Close()
data, err := readThemeFile(dir, "manifest.json", 16<<10)
if err != nil {
return manifest, "", err
}
if err := json.Unmarshal(data, &manifest); err != nil {
return manifest, "", i18n.New("could_not_parse_manifest_json_4bef07", id, err)
}
if manifest.ID != id || manifest.Name == "" || manifest.Version == "" {
return manifest, "", i18n.New("theme_s_has_an_invalid_8ed29c", id)
}
css, err := readThemeFile(dir, "theme.css", 256<<10)
return manifest, string(css), err
}
func (s *Server) listThemes(w http.ResponseWriter, r *http.Request) {
themes := []themeManifest{{ID: "default", Name: message(w, "theme_default"), Version: "1"}}
for _, theme := range builtinThemes {
manifest := theme.themeManifest
manifest.Name = message(w, "theme_"+theme.ID)
themes = append(themes, manifest)
}
if s.ThemesDir != "" {
entries, err := os.ReadDir(s.ThemesDir)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
internalError(w, r, err)
return
}
for _, entry := range entries {
if _, _, ok := builtinTheme(entry.Name()); ok {
continue
}
if !entry.IsDir() || !themeIDPattern.MatchString(entry.Name()) || entry.Name() == "default" {
continue
}
manifest, _, err := s.readTheme(entry.Name())
if err != nil {
internalError(w, r, err)
return
}
themes = append(themes, manifest)
}
}
writeJSON(w, 200, map[string]any{"themes": themes})
}
func (s *Server) getTheme(w http.ResponseWriter, r *http.Request) {
manifest, css, err := s.readTheme(r.PathValue("id"))
if errors.Is(err, fs.ErrNotExist) {
writeError(w, 404, message(w, "theme_not_found_or_files_3b0d5b"))
return
}
if err != nil {
internalError(w, r, err)
return
}
if _, _, ok := builtinTheme(manifest.ID); ok {
manifest.Name = message(w, "theme_"+manifest.ID)
}
writeJSON(w, 200, struct {
themeManifest
CSS string `json:"css"`
}{manifest, css})
}