feat(settings): manage shared interface and editor fonts
This commit is contained in:
1 parent
1d5637a5eb
commit
1882312435
19 files changed
+849
-16
No files matched your search
@@ -40,6 +40,7 @@ type Server struct {
|
||||
// ThemesDir 指向数据目录中的主题目录;为空时只有内置主题。
|
||||
ThemesDir string
|
||||
PluginsDir string
|
||||
FontsDir string
|
||||
pluginsMu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -98,6 +99,10 @@ func (s *Server) routes() []route {
|
||||
|
||||
{"GET /api/v1/settings", authed, s.getSettings},
|
||||
{"PATCH /api/v1/settings", sessionOnly, s.patchSettings},
|
||||
{"GET /api/v1/fonts", sessionOnly, s.listFonts},
|
||||
{"POST /api/v1/fonts", sessionOnly, s.importFont},
|
||||
{"GET /api/v1/fonts/{id}/file", sessionOnly, s.fontFile},
|
||||
{"DELETE /api/v1/fonts/{id}", sessionOnly, s.deleteFont},
|
||||
{"GET /api/v1/plugins", sessionOnly, s.listPlugins},
|
||||
{"POST /api/v1/plugins", sessionOnly, s.installPlugin},
|
||||
{"PATCH /api/v1/plugins/{id}", sessionOnly, s.togglePlugin},
|
||||
|
||||
@@ -130,6 +130,7 @@ func newFixture(t *testing.T) *fixture {
|
||||
server := New(v, am, st, hub, idx)
|
||||
server.ThemesDir = filepath.Join(dir, "themes")
|
||||
server.PluginsDir = filepath.Join(dir, "plugins")
|
||||
server.FontsDir = filepath.Join(dir, "fonts")
|
||||
srv := httptest.NewServer(server.Handler(fstest.MapFS{}))
|
||||
t.Cleanup(srv.Close)
|
||||
f := &fixture{t: t, url: srv.URL, root: v.Root(), store: st, hub: hub, admin: newClient()}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 = fmt.Errorf("字体安装信息无效")
|
||||
}
|
||||
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, "无法读取字体文件,单个文件上限 30 MB")
|
||||
return
|
||||
}
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
f, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
writeError(w, 400, "请选择字体文件")
|
||||
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, "字体文件超过 30 MB")
|
||||
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, "请导入有效的 TTF、OTF、WOFF 或 WOFF2 字体文件")
|
||||
return
|
||||
}
|
||||
if s.FontsDir == "" {
|
||||
writeError(w, 503, "字体目录未配置")
|
||||
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, "字体文件名无效或过长")
|
||||
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, "字体不存在")
|
||||
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 = fmt.Errorf("字体必须为普通文件")
|
||||
}
|
||||
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, "Maple 是默认字体,不可删除")
|
||||
return
|
||||
}
|
||||
s.settingsMu.Lock()
|
||||
defer s.settingsMu.Unlock()
|
||||
dir, _, err := s.openFont(id)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
writeError(w, 404, "字体不存在")
|
||||
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, fmt.Errorf("字体已停用,但文件清理失败:%w", err))
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, map[string]bool{"ok": true})
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func uploadTestFont(t *testing.T, f *fixture, client *http.Client, name string, data []byte) *http.Response {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
form := multipart.NewWriter(&body)
|
||||
file, err := form.CreateFormFile("file", name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := file.Write(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := form.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req, err := http.NewRequest("POST", f.url+"/api/v1/fonts", &body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", form.FormDataContentType())
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func testFontData(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("../../web/src/themes/fonts/maple/regular-00.woff2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestFontImportReuseReadAndDelete(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
data := testFontData(t)
|
||||
info := decode[fontInfo](t, uploadTestFont(t, f, f.admin, "示例字体.woff2", data))
|
||||
if !fontIDPattern.MatchString(info.ID) || info.Name != "示例字体" || info.Format != "woff2" {
|
||||
t.Fatalf("字体信息错误:%+v", info)
|
||||
}
|
||||
again := decode[fontInfo](t, uploadTestFont(t, f, f.admin, "renamed.woff2", data))
|
||||
if again != info {
|
||||
t.Fatalf("相同内容未复用原字体:%+v", again)
|
||||
}
|
||||
list := decode[struct{ Fonts []fontInfo }](t, f.do(f.admin, "GET", "/api/v1/fonts", "", nil))
|
||||
if len(list.Fonts) != 1 || list.Fonts[0] != info {
|
||||
t.Fatalf("列表不符:%+v", list)
|
||||
}
|
||||
res := f.do(f.admin, "GET", "/api/v1/fonts/"+info.ID+"/file", "", nil)
|
||||
got, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil || !bytes.Equal(got, data) || res.Header.Get("Content-Type") != "font/woff2" {
|
||||
t.Fatal("字体文件不一致", err)
|
||||
}
|
||||
f.do(f.admin, "PATCH", "/api/v1/settings", `{"uiFont":"`+info.ID+`","textFont":"`+info.ID+`","codeFont":"`+info.ID+`","plugin.data":{"keep":true}}`, nil).Body.Close()
|
||||
f.do(f.admin, "DELETE", "/api/v1/fonts/"+info.ID, "", nil).Body.Close()
|
||||
settings := decode[map[string]any](t, f.do(f.admin, "GET", "/api/v1/settings", "", nil))
|
||||
for _, key := range []string{"uiFont", "textFont", "codeFont"} {
|
||||
if settings[key] != "maple" {
|
||||
t.Fatalf("%s 未恢复默认:%v", key, settings[key])
|
||||
}
|
||||
}
|
||||
if settings["plugin.data"] == nil {
|
||||
t.Fatal("无关设置被删除")
|
||||
}
|
||||
if code := f.status(f.admin, "GET", "/api/v1/fonts/"+info.ID+"/file", "", nil); code != 404 {
|
||||
t.Fatal("已删除字体仍可读取", code)
|
||||
}
|
||||
list = decode[struct{ Fonts []fontInfo }](t, f.do(f.admin, "GET", "/api/v1/fonts", "", nil))
|
||||
if len(list.Fonts) != 0 {
|
||||
t.Fatal("已删除字体仍在列表")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFontBoundaries(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
guest := newClient()
|
||||
for _, endpoint := range []string{"/api/v1/fonts", "/api/v1/fonts/maple/file"} {
|
||||
if code := f.status(guest, "GET", endpoint, "", nil); code != 401 {
|
||||
t.Fatal("匿名字体访问未拒绝", code)
|
||||
}
|
||||
}
|
||||
res := uploadTestFont(t, f, guest, "font.woff2", testFontData(t))
|
||||
res.Body.Close()
|
||||
if res.StatusCode != 401 {
|
||||
t.Fatal("匿名上传未拒绝", res.StatusCode)
|
||||
}
|
||||
if code := f.status(f.admin, "DELETE", "/api/v1/fonts/maple", "", nil); code != 400 {
|
||||
t.Fatal("Maple不应可删除", code)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"font.exe", testFontData(t)}, {"font.ttf", testFontData(t)}, {".woff2", testFontData(t)}, {"font.woff2", []byte("invalid")},
|
||||
} {
|
||||
res := uploadTestFont(t, f, f.admin, tc.name, tc.data)
|
||||
res.Body.Close()
|
||||
if res.StatusCode != 400 {
|
||||
t.Fatalf("非法字体 %s 未拒绝:%d", tc.name, res.StatusCode)
|
||||
}
|
||||
}
|
||||
res = uploadTestFont(t, f, f.admin, "large.woff2", bytes.Repeat([]byte{'x'}, maxFontBytes+1))
|
||||
res.Body.Close()
|
||||
if res.StatusCode != http.StatusRequestEntityTooLarge {
|
||||
t.Fatal("超大字体未拒绝", res.StatusCode)
|
||||
}
|
||||
token := decode[struct{ Token string }](t, f.do(f.admin, "POST", "/api/v1/tokens", `{"name":"fonts","scope":"write"}`, nil))
|
||||
for _, tc := range []struct{ method, path string }{{"GET", "/api/v1/fonts"}, {"DELETE", "/api/v1/fonts/maple"}} {
|
||||
if code := f.status(guest, tc.method, tc.path, "", map[string]string{"Authorization": "Bearer " + token.Token}); code != 403 {
|
||||
t.Fatal("令牌不应管理字体", code)
|
||||
}
|
||||
}
|
||||
info := decode[fontInfo](t, uploadTestFont(t, f, f.admin, "font.woff2", testFontData(t)))
|
||||
dir := filepath.Join(filepath.Dir(f.root), "fonts", info.ID)
|
||||
if err := os.Remove(filepath.Join(dir, "font.woff2")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join(filepath.Dir(f.root), "rawmark.db"), filepath.Join(dir, "font.woff2")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code := f.status(f.admin, "GET", "/api/v1/fonts/"+info.ID+"/file", "", nil); code != 500 {
|
||||
t.Fatal("字体不应读取目录外的数据", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFontDeleteRollsBackWhenSettingsWriteFails(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
info := decode[fontInfo](t, uploadTestFont(t, f, f.admin, "font.woff2", testFontData(t)))
|
||||
f.do(f.admin, "PATCH", "/api/v1/settings", `{"uiFont":"`+info.ID+`"}`, nil).Body.Close()
|
||||
db, err := sql.Open("sqlite", filepath.Join(filepath.Dir(f.root), "rawmark.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err := db.Exec(`CREATE TRIGGER reject_font_settings BEFORE UPDATE ON settings BEGIN SELECT RAISE(ABORT, 'test write failure'); END`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code := f.status(f.admin, "DELETE", "/api/v1/fonts/"+info.ID, "", nil); code != 500 {
|
||||
t.Fatal("设置失败应报告错误", code)
|
||||
}
|
||||
res := f.do(f.admin, "GET", "/api/v1/fonts/"+info.ID+"/file", "", nil)
|
||||
res.Body.Close()
|
||||
settings := decode[map[string]any](t, f.do(f.admin, "GET", "/api/v1/settings", "", nil))
|
||||
if settings["uiFont"] != info.ID {
|
||||
t.Fatal("失败后设置未保留")
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(filepath.Dir(f.root), "fonts"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), ".remove-") {
|
||||
t.Fatal("失败后字体未恢复")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2022,6 +2022,139 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/fonts": {
|
||||
"get": {
|
||||
"summary": "列出已导入字体",
|
||||
"tags": [
|
||||
"设置"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookie": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功"
|
||||
},
|
||||
"401": {
|
||||
"description": "未登录"
|
||||
},
|
||||
"403": {
|
||||
"description": "只接受登录会话"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "导入字体,按内容去重(TTF/OTF/WOFF/WOFF2,上限30 MB)",
|
||||
"tags": [
|
||||
"设置"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookie": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "成功"
|
||||
},
|
||||
"401": {
|
||||
"description": "未登录"
|
||||
},
|
||||
"403": {
|
||||
"description": "只接受登录会话"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"file"
|
||||
],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/fonts/{id}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"delete": {
|
||||
"summary": "删除导入字体,将使用它的界面、正文和代码字体恢复Maple;Maple不可删除",
|
||||
"tags": [
|
||||
"设置"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookie": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功"
|
||||
},
|
||||
"401": {
|
||||
"description": "未登录"
|
||||
},
|
||||
"403": {
|
||||
"description": "只接受登录会话"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/fonts/{id}/file": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"summary": "读取导入字体文件",
|
||||
"tags": [
|
||||
"设置"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"cookie": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功"
|
||||
},
|
||||
"401": {
|
||||
"description": "未登录"
|
||||
},
|
||||
"403": {
|
||||
"description": "只接受登录会话"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
Reference in new issue
Block a user