255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"rawmark/internal/i18n"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"rawmark/internal/events"
|
|
)
|
|
|
|
const maxFontBytes = 30 << 20
|
|
|
|
var fontIDPattern = regexp.MustCompile(`^custom-[a-f0-9]{64}$`)
|
|
|
|
type fontInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Format string `json:"format"`
|
|
}
|
|
|
|
func (s *Server) openFont(id string) (*os.Root, fontInfo, error) {
|
|
var info fontInfo
|
|
if !fontIDPattern.MatchString(id) || s.FontsDir == "" {
|
|
return nil, info, fs.ErrNotExist
|
|
}
|
|
root, err := os.OpenRoot(s.FontsDir)
|
|
if err != nil {
|
|
return nil, info, err
|
|
}
|
|
defer root.Close()
|
|
dir, err := root.OpenRoot(id)
|
|
if err != nil {
|
|
return nil, info, err
|
|
}
|
|
data, err := dir.ReadFile("manifest.json")
|
|
if err == nil {
|
|
err = json.Unmarshal(data, &info)
|
|
}
|
|
if err == nil && (info.ID != id || info.Name == "" || !validFontFormat(info.Format)) {
|
|
err = i18n.New("invalid_font_installation_information_77cff8")
|
|
}
|
|
if err != nil {
|
|
dir.Close()
|
|
return nil, info, err
|
|
}
|
|
return dir, info, nil
|
|
}
|
|
|
|
func validFontFormat(format string) bool {
|
|
return format == "ttf" || format == "otf" || format == "woff" || format == "woff2"
|
|
}
|
|
|
|
func (s *Server) listFonts(w http.ResponseWriter, r *http.Request) {
|
|
s.settingsMu.Lock()
|
|
defer s.settingsMu.Unlock()
|
|
fonts := []fontInfo{}
|
|
if s.FontsDir != "" {
|
|
entries, err := os.ReadDir(s.FontsDir)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() || !fontIDPattern.MatchString(entry.Name()) {
|
|
continue
|
|
}
|
|
dir, info, err := s.openFont(entry.Name())
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
dir.Close()
|
|
fonts = append(fonts, info)
|
|
}
|
|
}
|
|
writeJSON(w, 200, map[string]any{"fonts": fonts})
|
|
}
|
|
|
|
func (s *Server) importFont(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxFontBytes+(1<<20))
|
|
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
writeError(w, 400, message(w, "could_not_read_font_file_b7b543"))
|
|
return
|
|
}
|
|
defer r.MultipartForm.RemoveAll()
|
|
f, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeError(w, 400, message(w, "choose_a_font_file_4c95b9"))
|
|
return
|
|
}
|
|
defer f.Close()
|
|
format := strings.TrimPrefix(strings.ToLower(filepath.Ext(header.Filename)), ".")
|
|
data, err := io.ReadAll(io.LimitReader(f, maxFontBytes+1))
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
if len(data) > maxFontBytes {
|
|
writeError(w, 413, message(w, "font_file_exceeds_mb_cc001c"))
|
|
return
|
|
}
|
|
magic := map[string]string{"ttf": "\x00\x01\x00\x00", "otf": "OTTO", "woff": "wOFF", "woff2": "wOF2"}
|
|
if !validFontFormat(format) || len(data) < 12 || string(data[:4]) != magic[format] {
|
|
writeError(w, 400, message(w, "import_a_valid_ttf_otf_b67fd3"))
|
|
return
|
|
}
|
|
if s.FontsDir == "" {
|
|
writeError(w, 503, message(w, "font_directory_is_not_configured_73d32a"))
|
|
return
|
|
}
|
|
hash := sha256.Sum256(data)
|
|
id := "custom-" + hex.EncodeToString(hash[:])
|
|
info := fontInfo{id, strings.TrimSuffix(filepath.Base(header.Filename), filepath.Ext(header.Filename)), format}
|
|
if strings.TrimSpace(info.Name) == "" || len(info.Name) > 200 || strings.ContainsAny(info.Name, "\x00\r\n") {
|
|
writeError(w, 400, message(w, "font_filename_is_invalid_or_a3fd5e"))
|
|
return
|
|
}
|
|
s.settingsMu.Lock()
|
|
defer s.settingsMu.Unlock()
|
|
if dir, existing, err := s.openFont(id); err == nil {
|
|
dir.Close()
|
|
writeJSON(w, 200, existing)
|
|
return
|
|
} else if !errors.Is(err, fs.ErrNotExist) {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
if err := os.MkdirAll(s.FontsDir, 0o755); err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
stage, err := os.MkdirTemp(s.FontsDir, ".import-")
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
defer os.RemoveAll(stage)
|
|
manifest, err := json.Marshal(info)
|
|
if err == nil {
|
|
err = os.WriteFile(filepath.Join(stage, "manifest.json"), manifest, 0o600)
|
|
}
|
|
if err == nil {
|
|
err = os.WriteFile(filepath.Join(stage, "font."+format), data, 0o600)
|
|
}
|
|
if err == nil {
|
|
err = os.Rename(stage, filepath.Join(s.FontsDir, id))
|
|
}
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
s.hub.Broadcast(events.Event{Type: "settings"})
|
|
writeJSON(w, 201, info)
|
|
}
|
|
|
|
func (s *Server) fontFile(w http.ResponseWriter, r *http.Request) {
|
|
s.settingsMu.Lock()
|
|
dir, info, err := s.openFont(r.PathValue("id"))
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
s.settingsMu.Unlock()
|
|
writeError(w, 404, message(w, "font_not_found_4383e9"))
|
|
return
|
|
}
|
|
if err != nil {
|
|
s.settingsMu.Unlock()
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
defer dir.Close()
|
|
f, err := dir.Open("font." + info.Format)
|
|
s.settingsMu.Unlock()
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
stat, err := f.Stat()
|
|
if err != nil || !stat.Mode().IsRegular() {
|
|
if err == nil {
|
|
err = i18n.New("a_font_must_be_a_5b8b51")
|
|
}
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "font/"+info.Format)
|
|
http.ServeContent(w, r, "font."+info.Format, stat.ModTime(), f)
|
|
}
|
|
|
|
func (s *Server) deleteFont(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
if id == "maple" {
|
|
writeError(w, 400, message(w, "maple_is_the_default_font_a8a571"))
|
|
return
|
|
}
|
|
s.settingsMu.Lock()
|
|
defer s.settingsMu.Unlock()
|
|
dir, _, err := s.openFont(id)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
writeError(w, 404, message(w, "font_not_found_4383e9"))
|
|
return
|
|
}
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
dir.Close()
|
|
settings, err := s.readSettings()
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
for _, key := range []string{"uiFont", "textFont", "codeFont"} {
|
|
var selected string
|
|
if json.Unmarshal(settings[key], &selected) == nil && selected == id {
|
|
settings[key] = json.RawMessage(`"maple"`)
|
|
}
|
|
}
|
|
data, err := json.Marshal(settings)
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
root, err := os.OpenRoot(s.FontsDir)
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
defer root.Close()
|
|
// 先撤下资源;设置写失败时恢复,避免留下指向已删除字体的设置。
|
|
removed := ".remove-" + id
|
|
if err := root.Rename(id, removed); err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
if err := s.store.SetSetting(settingsKey, string(data)); err != nil {
|
|
internalError(w, r, errors.Join(err, root.Rename(removed, id)))
|
|
return
|
|
}
|
|
s.hub.Broadcast(events.Event{Type: "settings"})
|
|
if err := root.RemoveAll(removed); err != nil {
|
|
internalError(w, r, i18n.New("font_disabled_but_file_cleanup_d8b892", err))
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
}
|