Files
gopeed/pkg/download/protocol_config.go

173 lines
4.5 KiB
Go

package download
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/util"
)
func (d *Downloader) protocolManager(name string) (fetcher.FetcherManager, error) {
for _, fm := range d.cfg.FetchManagers {
if fm.Name() == name {
return fm, nil
}
}
return nil, fmt.Errorf("%w: %s", ErrUnSupportedProtocol, name)
}
func jsonObject(v any) (map[string]json.RawMessage, error) {
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}
var object map[string]json.RawMessage
if err = json.Unmarshal(raw, &object); err != nil {
return nil, err
}
if object == nil {
object = map[string]json.RawMessage{}
}
return object, nil
}
func normalizeProtocol(fm fetcher.FetcherManager, v any) (any, error) {
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}
target := fm.DefaultConfig()
if target == nil {
return nil, errors.New("protocol does not expose configuration")
}
// Managers may return either typed pointers or value/map defaults.
valueDefault := reflect.TypeOf(target).Kind() != reflect.Pointer
if valueDefault {
pointer := reflect.New(reflect.TypeOf(target))
defaults, err := json.Marshal(target)
if err != nil {
return nil, err
}
if err = json.Unmarshal(defaults, pointer.Interface()); err != nil {
return nil, err
}
target = pointer.Interface()
}
if err = json.Unmarshal(raw, target); err != nil {
return nil, err
}
if normalizer, ok := fm.(fetcher.ConfigNormalizer); ok {
raw, err = json.Marshal(target)
if err != nil {
return nil, err
}
return normalizer.NormalizeConfig(raw)
}
if valueDefault {
return reflect.ValueOf(target).Elem().Interface(), nil
}
return target, nil
}
// preserveProtocolState ignores all client-supplied state, including null or
// malformed state values, before decoding/validating the writable config.
func preserveProtocolState(next, previous any) (any, error) {
object, err := jsonObject(next)
if err != nil {
return nil, err
}
before, err := jsonObject(previous)
if err != nil {
return nil, err
}
// Match encoding/json field folding as well as the canonical spelling.
for key := range object {
if strings.EqualFold(key, "state") {
delete(object, key)
}
}
if state, ok := before["state"]; ok {
object["state"] = state
}
return object, nil
}
func (d *Downloader) readProtocolConfig(name string) (any, error) {
if _, err := d.protocolManager(name); err != nil {
return nil, err
}
d.configLock.RLock()
defer d.configLock.RUnlock()
value := d.cfg.ProtocolConfig[name]
return *util.DeepClone(&value), nil
}
// updateProtocolState is the only internal write path for backend-owned state.
// It never publishes callback edits outside the reserved state section.
func (d *Downloader) updateProtocolState(name string, change func(config, state json.RawMessage) (any, error)) error {
fm, err := d.protocolManager(name)
if err != nil {
return err
}
d.configLock.Lock()
defer d.configLock.Unlock()
if d.closed.Load() {
return errors.New("downloader is closed")
}
next := util.DeepClone(d.cfg.DownloaderStoreConfig)
object, err := jsonObject(next.ProtocolConfig[name])
if err != nil {
return err
}
state, ok := object["state"]
if !ok {
return fmt.Errorf("protocol %s does not expose persistent state", name)
}
raw, err := json.Marshal(object)
if err != nil {
return err
}
value, err := change(raw, state)
if err != nil {
return err
}
// Keep the transaction's original config isolated from callback mutations.
object["state"], err = json.Marshal(value)
if err != nil {
return err
}
normalized, err := normalizeProtocol(fm, object)
if err != nil {
return err
}
next.ProtocolConfig[name] = normalized
if err = d.storage.Put(bucketConfig, "config", next); err != nil {
return err
}
d.cfg.DownloaderStoreConfig.ProtocolConfig = next.ProtocolConfig
return nil
}
// initProtocolConfigs loads the current schema and initializes missing defaults.
// This release intentionally does not import settings from older config layouts.
func (d *Downloader) initProtocolConfigs() error {
cfg := d.cfg.DownloaderStoreConfig
cfg.Init()
if err := cfg.GithubMirror.Validate(); err != nil {
return err
}
for _, fm := range d.cfg.FetchManagers {
value, ok := cfg.ProtocolConfig[fm.Name()]
if !ok {
value = map[string]any{}
}
normalized, err := normalizeProtocol(fm, value)
if err != nil {
return fmt.Errorf("%s config: %w", fm.Name(), err)
}
cfg.ProtocolConfig[fm.Name()] = normalized
}
return d.storage.Put(bucketConfig, "config", cfg)
}