Add server components
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:28:09 +08:00
parent 7ecc6a8923
commit 079ee4eaeb
168 changed files with 37475 additions and 0 deletions
@@ -0,0 +1,215 @@
package legacy
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"time"
"ymhut-box/server/unified-management/internal/config"
"ymhut-box/server/unified-management/internal/db"
)
type Service struct {
cfg *config.Config
store *db.Store
}
type Document struct {
Name string `json:"name"`
Raw string `json:"raw"`
Parsed map[string]any `json:"parsed"`
Path string `json:"path"`
UpdatedAt string `json:"updatedAt"`
Revisions []db.LegacyJsonRevision `json:"revisions"`
}
type SaveRequest struct {
Raw string `json:"raw"`
Parsed map[string]any `json:"parsed"`
Note string `json:"note"`
}
func NewService(cfg *config.Config, store *db.Store) *Service {
return &Service{cfg: cfg, store: store}
}
func (s *Service) Get(ctx context.Context, name string) (Document, error) {
fileName, err := fileNameFor(name)
if err != nil {
return Document{}, err
}
path := filepath.Join(s.cfg.UpdatePublicDir, fileName)
data, err := os.ReadFile(path)
if err != nil {
return Document{}, err
}
parsed, formatted, err := parseAndFormat(name, data)
if err != nil {
return Document{}, err
}
revisions, _ := s.store.ListLegacyRevisions(name, 20)
updatedAt := ""
if info, err := os.Stat(path); err == nil {
updatedAt = info.ModTime().UTC().Format(time.RFC3339)
}
return Document{Name: name, Raw: formatted, Parsed: parsed, Path: path, UpdatedAt: updatedAt, Revisions: revisions}, nil
}
func (s *Service) Validate(ctx context.Context, name string, req SaveRequest) (Document, error) {
raw, err := requestRaw(req)
if err != nil {
return Document{}, err
}
parsed, formatted, err := parseAndFormat(name, []byte(raw))
if err != nil {
return Document{}, err
}
return Document{Name: name, Raw: formatted, Parsed: parsed}, nil
}
func (s *Service) Save(ctx context.Context, name string, req SaveRequest, actor string) (Document, error) {
fileName, err := fileNameFor(name)
if err != nil {
return Document{}, err
}
raw, err := requestRaw(req)
if err != nil {
return Document{}, err
}
parsed, formatted, err := parseAndFormat(name, []byte(raw))
if err != nil {
return Document{}, err
}
path := filepath.Join(s.cfg.UpdatePublicDir, fileName)
current, _ := os.ReadFile(path)
if len(current) > 0 {
_, _ = s.store.SaveLegacyRevision(name, string(current), "auto backup before save", actor)
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return Document{}, err
}
if err := atomicWrite(path, []byte(formatted)); err != nil {
return Document{}, err
}
_, _ = s.store.SaveLegacyRevision(name, formatted, req.Note, actor)
_ = s.store.InsertAudit(db.AuditLog{Actor: actor, Type: "legacy_json.saved", Target: name, Message: "Legacy JSON saved"})
revisions, _ := s.store.ListLegacyRevisions(name, 20)
return Document{Name: name, Raw: formatted, Parsed: parsed, Path: path, UpdatedAt: db.Now(), Revisions: revisions}, nil
}
func (s *Service) Restore(ctx context.Context, name string, revisionID int64, actor string) (Document, error) {
fileName, err := fileNameFor(name)
if err != nil {
return Document{}, err
}
revision, err := s.store.GetLegacyRevision(name, revisionID)
if err != nil {
return Document{}, err
}
parsed, formatted, err := parseAndFormat(name, []byte(revision.Raw))
if err != nil {
return Document{}, err
}
path := filepath.Join(s.cfg.UpdatePublicDir, fileName)
current, _ := os.ReadFile(path)
if len(current) > 0 {
_, _ = s.store.SaveLegacyRevision(name, string(current), "auto backup before restore", actor)
}
if err := atomicWrite(path, []byte(formatted)); err != nil {
return Document{}, err
}
_, _ = s.store.SaveLegacyRevision(name, formatted, "restored revision", actor)
_ = s.store.InsertAudit(db.AuditLog{Actor: actor, Type: "legacy_json.restored", Target: name, Message: "Legacy JSON restored"})
revisions, _ := s.store.ListLegacyRevisions(name, 20)
return Document{Name: name, Raw: formatted, Parsed: parsed, Path: path, UpdatedAt: db.Now(), Revisions: revisions}, nil
}
func fileNameFor(name string) (string, error) {
switch strings.TrimSpace(name) {
case "update-info":
return "update-info.json", nil
case "media-types":
return "media-types.json", nil
default:
return "", errors.New("unsupported legacy document")
}
}
func requestRaw(req SaveRequest) (string, error) {
if strings.TrimSpace(req.Raw) != "" {
return req.Raw, nil
}
if req.Parsed == nil {
return "", errors.New("raw or parsed JSON is required")
}
data, err := json.Marshal(req.Parsed)
if err != nil {
return "", err
}
return string(data), nil
}
func parseAndFormat(name string, data []byte) (map[string]any, string, error) {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
var parsed map[string]any
if err := decoder.Decode(&parsed); err != nil {
return nil, "", err
}
if err := validate(name, parsed); err != nil {
return nil, "", err
}
out, err := json.MarshalIndent(parsed, "", " ")
if err != nil {
return nil, "", err
}
return parsed, string(out) + "\n", nil
}
func validate(name string, parsed map[string]any) error {
switch name {
case "update-info":
if _, ok := parsed["app_version"]; !ok {
if _, ok := parsed["title"]; !ok {
return errors.New("update-info requires app_version or title")
}
}
case "media-types":
if _, ok := parsed["categories"].([]any); !ok {
return errors.New("media-types requires categories array")
}
if _, ok := parsed["layout_version"]; !ok {
parsed["layout_version"] = "1.0.0"
}
if _, ok := parsed["last_updated"]; !ok {
parsed["last_updated"] = time.Now().UTC().Format(time.RFC3339)
}
}
return nil
}
func atomicWrite(path string, data []byte) error {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
if err != nil {
return err
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Chmod(tmpName, 0o640); err != nil {
return err
}
return os.Rename(tmpName, path)
}
@@ -0,0 +1,66 @@
package legacy
import (
"context"
"os"
"path/filepath"
"testing"
"ymhut-box/server/unified-management/internal/config"
"ymhut-box/server/unified-management/internal/db"
)
func TestSaveValidateAndRestoreLegacyJSON(t *testing.T) {
root := t.TempDir()
public := filepath.Join(root, "public")
if err := os.MkdirAll(public, 0o755); err != nil {
t.Fatal(err)
}
path := filepath.Join(public, "media-types.json")
if err := os.WriteFile(path, []byte(`{"layout_version":"1","categories":[]}`), 0o644); err != nil {
t.Fatal(err)
}
cfg := &config.Config{
StorageDir: filepath.Join(root, "storage"),
UpdatePublicDir: public,
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(root, "storage", "unified.sqlite"),
FailoverEnabled: true,
HealthIntervalSec: 3600,
MaxOpenConns: 1,
MaxIdleConns: 1,
ConnMaxLifetimeSeconds: 60,
},
}
store, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
defer store.Close()
service := NewService(cfg, store)
if _, err := service.Validate(context.Background(), "media-types", SaveRequest{Raw: `{"not_categories":[]}`}); err == nil {
t.Fatal("expected validation failure")
}
saved, err := service.Save(context.Background(), "media-types", SaveRequest{Raw: `{"categories":[{"id":"image","name":"Image","subcategories":[]}]}`, Note: "test"}, "admin")
if err != nil {
t.Fatal(err)
}
if saved.Parsed["layout_version"] == nil {
t.Fatal("layout_version was not filled")
}
revisions, err := store.ListLegacyRevisions("media-types", 10)
if err != nil {
t.Fatal(err)
}
if len(revisions) < 2 {
t.Fatalf("expected auto backup and saved revision, got %d", len(revisions))
}
restored, err := service.Restore(context.Background(), "media-types", revisions[len(revisions)-1].ID, "admin")
if err != nil {
t.Fatal(err)
}
if restored.Parsed["categories"] == nil {
t.Fatal("restored document missing categories")
}
}