199 lines
4.8 KiB
Go
199 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"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, fmt.Errorf("主题文件必须为普通文件")
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(f, limit+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(data)) > limit {
|
|
return nil, fmt.Errorf("主题文件过大")
|
|
}
|
|
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, "", fmt.Errorf("主题 %s 的 manifest.json 无法解析:%w", id, err)
|
|
}
|
|
if manifest.ID != id || manifest.Name == "" || manifest.Version == "" {
|
|
return manifest, "", fmt.Errorf("主题 %s 的 id、name 或 version 无效", 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: "默认", Version: "1"}}
|
|
for _, theme := range builtinThemes {
|
|
themes = append(themes, theme.themeManifest)
|
|
}
|
|
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, "主题不存在或文件不完整")
|
|
return
|
|
}
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, struct {
|
|
themeManifest
|
|
CSS string `json:"css"`
|
|
}{manifest, css})
|
|
}
|
|
|
|
// 只有有效分享的访客可读取外观;不返回账户、插件和其他应用设置。
|
|
func (s *Server) publicAppearance(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.openShare(w, r); !ok {
|
|
return
|
|
}
|
|
settings, err := s.readSettings()
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
out := map[string]any{}
|
|
for _, key := range []string{"colorMode", "fontSize", "contentWidth", "textFont", "codeFont", "lineHeight", "paragraphSpacing", "glassOpacity"} {
|
|
if value, ok := settings[key]; ok {
|
|
out[key] = value
|
|
}
|
|
}
|
|
var snippets []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
CSS string `json:"css"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if value, ok := settings["cssSnippets"]; ok {
|
|
if err := json.Unmarshal(value, &snippets); err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
active := snippets[:0]
|
|
for _, snippet := range snippets {
|
|
if snippet.Enabled {
|
|
active = append(active, snippet)
|
|
}
|
|
}
|
|
out["cssSnippets"] = active
|
|
var theme string
|
|
if value, ok := settings["theme"]; ok {
|
|
if err := json.Unmarshal(value, &theme); err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
if theme != "" && theme != "default" {
|
|
_, css, err := s.readTheme(theme)
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
out["themeCSS"] = css
|
|
}
|
|
out["theme"] = theme
|
|
writeJSON(w, 200, out)
|
|
}
|