mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 00:25:43 +00:00
173 lines
4.5 KiB
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)
|
|
}
|