309 lines
8.8 KiB
Go
309 lines
8.8 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type configDocument struct {
|
|
Version int `json:"version"`
|
|
Download downloadSettings `json:"download"`
|
|
Network networkSettings `json:"network"`
|
|
Sources *sourceSettings `json:"sources,omitempty"`
|
|
Advanced *advancedSettings `json:"advanced,omitempty"`
|
|
}
|
|
|
|
type downloadSettings struct {
|
|
Directory string `json:"directory"`
|
|
GroupBySource bool `json:"groupBySource,omitempty"`
|
|
Concurrency int `json:"concurrency"`
|
|
FFmpeg string `json:"ffmpeg,omitempty"`
|
|
}
|
|
|
|
type networkSettings struct {
|
|
Proxy string `json:"proxy"`
|
|
RequestConcurrency int `json:"requestConcurrency"`
|
|
RequestIntervalMS int `json:"requestIntervalMs"`
|
|
}
|
|
|
|
type sourceSettings struct {
|
|
Hongguo *siteSettings `json:"hongguo,omitempty"`
|
|
}
|
|
|
|
type siteSettings struct {
|
|
BaseURL string `json:"baseURL,omitempty"`
|
|
}
|
|
|
|
type advancedSettings struct {
|
|
MaxPagesPerSort int `json:"maxPagesPerSort,omitempty"`
|
|
Retries int `json:"retries,omitempty"`
|
|
SkipBytes int64 `json:"skipBytes,omitempty"`
|
|
InsecureTLS bool `json:"insecureTLS,omitempty"`
|
|
}
|
|
|
|
func documentFromConfig(cfg Config) configDocument {
|
|
defaults := defaultConfig()
|
|
document := configDocument{
|
|
Version: 1,
|
|
Download: downloadSettings{Directory: firstNonEmpty(cfg.outputDirSetting, cfg.OutputDir), GroupBySource: cfg.GroupBySource, Concurrency: cfg.Concurrency},
|
|
Network: networkSettings{Proxy: firstNonEmpty(cfg.ProxyURL, "auto"), RequestConcurrency: cfg.RequestConcurrency, RequestIntervalMS: cfg.RequestIntervalMS},
|
|
}
|
|
if cfg.FFmpeg != "" && cfg.FFmpeg != defaults.FFmpeg {
|
|
document.Download.FFmpeg = cfg.FFmpeg
|
|
}
|
|
sources := sourceSettings{}
|
|
if cfg.HongguoURL != "" {
|
|
sources.Hongguo = &siteSettings{BaseURL: cfg.HongguoURL}
|
|
}
|
|
if sources.Hongguo != nil {
|
|
document.Sources = &sources
|
|
}
|
|
advanced := advancedSettings{InsecureTLS: cfg.InsecureTLS}
|
|
if cfg.MaxPagesPerSort != defaults.MaxPagesPerSort {
|
|
advanced.MaxPagesPerSort = cfg.MaxPagesPerSort
|
|
}
|
|
if cfg.Retries != defaults.Retries {
|
|
advanced.Retries = cfg.Retries
|
|
}
|
|
if cfg.SkipBytes != defaults.SkipBytes {
|
|
advanced.SkipBytes = cfg.SkipBytes
|
|
}
|
|
if advanced != (advancedSettings{}) {
|
|
document.Advanced = &advanced
|
|
}
|
|
return document
|
|
}
|
|
|
|
func (document configDocument) config() (Config, error) {
|
|
if document.Version != 1 {
|
|
return Config{}, fmt.Errorf("不支持的配置版本 %d,请使用兼容的程序版本", document.Version)
|
|
}
|
|
cfg := defaultConfig()
|
|
cfg.OutputDir = document.Download.Directory
|
|
cfg.GroupBySource = document.Download.GroupBySource
|
|
cfg.Concurrency = document.Download.Concurrency
|
|
cfg.FFmpeg = firstNonEmpty(document.Download.FFmpeg, cfg.FFmpeg)
|
|
cfg.ProxyURL = document.Network.Proxy
|
|
cfg.RequestConcurrency = document.Network.RequestConcurrency
|
|
cfg.RequestIntervalMS = document.Network.RequestIntervalMS
|
|
if sources := document.Sources; sources != nil {
|
|
if sources.Hongguo != nil {
|
|
cfg.HongguoURL = sources.Hongguo.BaseURL
|
|
}
|
|
}
|
|
if advanced := document.Advanced; advanced != nil {
|
|
if advanced.MaxPagesPerSort > 0 {
|
|
cfg.MaxPagesPerSort = advanced.MaxPagesPerSort
|
|
}
|
|
if advanced.Retries > 0 {
|
|
cfg.Retries = advanced.Retries
|
|
}
|
|
if advanced.SkipBytes > 0 {
|
|
cfg.SkipBytes = advanced.SkipBytes
|
|
}
|
|
cfg.InsecureTLS = advanced.InsecureTLS
|
|
}
|
|
settings := runtimeSettings{Concurrency: cfg.Concurrency, RequestConcurrency: cfg.RequestConcurrency, RequestIntervalMS: cfg.RequestIntervalMS, ProxyURL: &cfg.ProxyURL, OutputDir: &cfg.OutputDir}
|
|
if err := settings.validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := cfg.validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func readConfigDocument(path string) (configDocument, error) {
|
|
document := documentFromConfig(defaultConfig())
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return document, err
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return document, err
|
|
}
|
|
if info.Size() > 1<<20 {
|
|
return document, errors.New("配置文件超过 1 MB")
|
|
}
|
|
decoder := json.NewDecoder(io.LimitReader(file, (1<<20)+1))
|
|
content := &document
|
|
if err := decoder.Decode(&content); err != nil {
|
|
return document, fmt.Errorf("配置文件格式无效: %w", err)
|
|
}
|
|
if content == nil {
|
|
return document, errors.New("配置文件必须是 JSON 对象")
|
|
}
|
|
if err := decoder.Decode(new(any)); !errors.Is(err, io.EOF) {
|
|
return document, errors.New("配置文件含有多余内容")
|
|
}
|
|
if _, err := document.config(); err != nil {
|
|
return document, err
|
|
}
|
|
return document, nil
|
|
}
|
|
|
|
func writeConfigDocument(path string, document configDocument) error {
|
|
if _, err := document.config(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.CreateTemp(filepath.Dir(path), ".config-*.tmp")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(file.Name())
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
err = encoder.Encode(document)
|
|
if err == nil {
|
|
err = file.Sync()
|
|
}
|
|
closeErr := file.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
return os.Rename(file.Name(), path)
|
|
}
|
|
|
|
func loadApplicationConfig(dataDirectory, importPath, outputOverride string) (Config, error) {
|
|
dataDirectory, err := filepath.Abs(dataDirectory)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
path := runtimeSettingsPath(dataDirectory)
|
|
document, err := readConfigDocument(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
cfg, legacyDirectories, importErr := importLegacyConfig(firstNonEmpty(importPath, "config.json"), outputOverride)
|
|
if importErr != nil {
|
|
return Config{}, importErr
|
|
}
|
|
document = documentFromConfig(cfg)
|
|
if _, err := document.config(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := migrateLegacyData(dataDirectory, legacyDirectories); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := writeConfigDocument(path, document); err != nil {
|
|
return Config{}, fmt.Errorf("首次生成配置失败: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return Config{}, fmt.Errorf("无法读取 %s,原文件未修改: %w", path, err)
|
|
}
|
|
cfg, err := document.config()
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.settingsLoaded = true
|
|
cfg.dataDir = dataDirectory
|
|
applyConfigEnvironment(&cfg)
|
|
if outputOverride != "" {
|
|
cfg.OutputDir = outputOverride
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func importLegacyConfig(path, outputOverride string) (Config, []string, error) {
|
|
cfg := defaultConfig()
|
|
body, err := os.ReadFile(path)
|
|
if err == nil {
|
|
if err := json.Unmarshal(body, &cfg); err != nil {
|
|
return cfg, nil, fmt.Errorf("旧配置格式错误,未覆盖原文件: %w", err)
|
|
}
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return cfg, nil, err
|
|
}
|
|
if cfg.OutputDir == "" {
|
|
cfg.OutputDir = defaultOutputDir()
|
|
}
|
|
if cfg.RequestConcurrency <= 0 {
|
|
cfg.RequestConcurrency = 2
|
|
}
|
|
if cfg.RequestIntervalMS <= 0 {
|
|
cfg.RequestIntervalMS = 500
|
|
}
|
|
directories := []string{firstNonEmpty(outputOverride, cfg.OutputDir), cfg.OutputDir}
|
|
candidates := []string{filepath.Join(filepath.Dir(path), "juku-settings.json")}
|
|
for _, directory := range directories {
|
|
candidates = append(candidates, filepath.Join(directory, "settings.json"))
|
|
}
|
|
for _, candidate := range candidates {
|
|
body, err = os.ReadFile(candidate)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return cfg, nil, err
|
|
}
|
|
var settings runtimeSettings
|
|
if err := json.Unmarshal(body, &settings); err != nil {
|
|
return cfg, nil, fmt.Errorf("旧设置格式错误: %w", err)
|
|
}
|
|
if err := settings.validate(); err != nil {
|
|
return cfg, nil, err
|
|
}
|
|
applyRuntimeSettings(&cfg, settings)
|
|
break
|
|
}
|
|
return cfg, append(directories, cfg.OutputDir, filepath.Dir(path)), nil
|
|
}
|
|
|
|
func migrateLegacyData(directory string, candidates []string) error {
|
|
if err := os.MkdirAll(directory, 0o755); err != nil {
|
|
return err
|
|
}
|
|
for _, name := range []string{"library.json", "ui-state.json", "failed.json"} {
|
|
target := filepath.Join(directory, name)
|
|
if _, err := os.Stat(target); err == nil {
|
|
continue
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
for _, candidate := range candidates {
|
|
if sameDirectory(directory, candidate) {
|
|
continue
|
|
}
|
|
original := filepath.Join(candidate, name)
|
|
body, err := os.ReadFile(original)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !json.Valid(body) {
|
|
return fmt.Errorf("旧数据 %s 格式无效,原文件未修改", original)
|
|
}
|
|
file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
|
if errors.Is(err, os.ErrExist) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = file.Write(body)
|
|
if err == nil {
|
|
err = file.Sync()
|
|
}
|
|
closeErr := file.Close()
|
|
if err != nil || closeErr != nil {
|
|
_ = os.Remove(target)
|
|
return errors.Join(err, closeErr)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|