chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// defaultSettings returns the default settings map.
|
||||
func defaultSettings() map[string]any {
|
||||
return map[string]any{
|
||||
"defaultUserAgent": "clash.meta/v1.19.24",
|
||||
"defaultFlowUserAgent": "clash.meta/v1.19.24",
|
||||
"defaultTimeout": "30000",
|
||||
"backendRequestConcurrency": "3",
|
||||
"backendRequestConcurrencyWaitTime": "100",
|
||||
"remoteCacheTtl": "300",
|
||||
"remoteCacheStaleOnError": true,
|
||||
"nodeInfoApiUrl": "https://ipwho.is/{ip}",
|
||||
"theme": map[string]any{
|
||||
"auto": true,
|
||||
"name": "light",
|
||||
"dark": "dark",
|
||||
"light": "light",
|
||||
},
|
||||
"appearanceSetting": map[string]any{
|
||||
"isSimpleMode": true,
|
||||
"isLeftRight": false,
|
||||
"isDefaultIcon": false,
|
||||
"isIconColor": false,
|
||||
"isShowIcon": true,
|
||||
"isSimpleShowRemark": false,
|
||||
"isEditorCommon": false,
|
||||
"manualSubscriptionsDisplayMode": "collapsed",
|
||||
"editorGroupingMode": "always",
|
||||
"isSimpleReicon": false,
|
||||
"isSubItemMenuFold": true,
|
||||
"showFloatingRefreshButton": false,
|
||||
"showFloatingAddButton": false,
|
||||
"createItemPosition": "bottom",
|
||||
"displayPreviewInWebPage": true,
|
||||
"subProgressStyle": "hidden",
|
||||
"listPageViewMode": "single-column",
|
||||
"listPageViewModeInWideScreenNarrowMode": "single-column",
|
||||
"useNarrowModeOnWideScreen": false,
|
||||
},
|
||||
"appName": "Sub-Store",
|
||||
}
|
||||
}
|
||||
|
||||
// mergeSettings merges default + stored settings.
|
||||
// Per review-resolution #4: top-level keys from stored override default;
|
||||
// theme and appearanceSetting are shallow-merged (stored child keys override default child keys).
|
||||
func mergeSettings(base, stored map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(base))
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range stored {
|
||||
if v != nil {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
// Shallow merge for theme and appearanceSetting
|
||||
result["theme"] = shallowMergeObject(base["theme"], stored["theme"])
|
||||
result["appearanceSetting"] = shallowMergeObject(base["appearanceSetting"], stored["appearanceSetting"])
|
||||
return result
|
||||
}
|
||||
|
||||
func shallowMergeObject(base, overlay any) map[string]any {
|
||||
result := map[string]any{}
|
||||
if b, ok := base.(map[string]any); ok {
|
||||
for k, v := range b {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
if o, ok := overlay.(map[string]any); ok {
|
||||
for k, v := range o {
|
||||
if v != nil {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// HandleGetSettings returns merged settings.
|
||||
func (d *Deps) HandleGetSettings(c fiber.Ctx) error {
|
||||
stored, err := d.SettingsRepo.Get()
|
||||
if err != nil {
|
||||
return failed(c, "Failed to read settings", 500)
|
||||
}
|
||||
return success(c, mergeSettings(defaultSettings(), stored))
|
||||
}
|
||||
|
||||
// HandleUpdateSettings patches settings.
|
||||
func (d *Deps) HandleUpdateSettings(c fiber.Ctx) error {
|
||||
var input map[string]any
|
||||
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
||||
input = map[string]any{}
|
||||
}
|
||||
merged, err := d.SettingsRepo.Update(input)
|
||||
if err != nil {
|
||||
return failed(c, "Failed to update settings", 500)
|
||||
}
|
||||
return success(c, mergeSettings(defaultSettings(), merged))
|
||||
}
|
||||
|
||||
// HandleExportStorage exports all data.
|
||||
// Per review-resolution #31: exclude built-in templates.
|
||||
func (d *Deps) HandleExportStorage(c fiber.Ctx) error {
|
||||
sources, _ := d.SourceRepo.List()
|
||||
collections, _ := d.CollectionRepo.List()
|
||||
templates, _ := d.TemplateRepo.List()
|
||||
settings, _ := d.SettingsRepo.Get()
|
||||
|
||||
// Filter out built-in templates
|
||||
exportedTemplates := make([]any, 0, len(templates))
|
||||
builtinIDs := getBuiltinTemplateIDs()
|
||||
for _, t := range templates {
|
||||
if !builtinIDs[t.ID] {
|
||||
exportedTemplates = append(exportedTemplates, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Actually filter properly — already done above
|
||||
|
||||
payload := fiber.Map{
|
||||
"version": 1,
|
||||
"exportedAt": "now",
|
||||
"settings": settings,
|
||||
"sources": sources,
|
||||
"collections": collections,
|
||||
"templates": exportedTemplates,
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "application/json; charset=utf-8")
|
||||
c.Set("Content-Disposition", `attachment; filename="sub-store-backup.json"`)
|
||||
return c.JSON(payload)
|
||||
}
|
||||
|
||||
// HandleImportStorage imports data.
|
||||
// Per review-resolution #30: import order = settings → sources → templates → collections.
|
||||
func (d *Deps) HandleImportStorage(c fiber.Ctx) error {
|
||||
input, err := parseJsonOrText(c)
|
||||
if err != nil {
|
||||
return failed(c, "Invalid import payload", 400)
|
||||
}
|
||||
|
||||
// Import settings first
|
||||
if settings, ok := input["settings"].(map[string]any); ok {
|
||||
d.SettingsRepo.Update(settings)
|
||||
}
|
||||
|
||||
// Import sources
|
||||
if sources, ok := input["sources"].([]any); ok {
|
||||
for _, s := range sources {
|
||||
if record, ok := s.(map[string]any); ok {
|
||||
d.SourceRepo.Upsert(mapToSourceRecord(record))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import templates (before collections, since collections reference templates)
|
||||
builtinIDs := getBuiltinTemplateIDs()
|
||||
if templates, ok := input["templates"].([]any); ok {
|
||||
for _, t := range templates {
|
||||
if record, ok := t.(map[string]any); ok {
|
||||
id := getStringValue(record["id"])
|
||||
if id != "" && !builtinIDs[id] {
|
||||
d.TemplateRepo.Upsert(mapToTemplateRecord(record))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import collections last
|
||||
if collections, ok := input["collections"].([]any); ok {
|
||||
for _, col := range collections {
|
||||
if record, ok := col.(map[string]any); ok {
|
||||
d.CollectionRepo.Upsert(mapToCollectionRecord(record))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success(c, fiber.Map{"restored": true})
|
||||
}
|
||||
|
||||
// parseJsonOrText parses body as JSON, or wraps text as {content: text}.
|
||||
// Per review-resolution #9: JSON first, plain text fallback.
|
||||
func parseJsonOrText(c fiber.Ctx) (map[string]any, error) {
|
||||
body := c.Body()
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(body, &result); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
// Try as string
|
||||
text := string(body)
|
||||
if text == "" {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
return map[string]any{"content": text}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user